原文博客地址:https://finget.github.io/2018/05/22/virtualDom/

什么是虚拟DOM

  • 用JS模拟DOM结构
  • DOM变化的对比,放在JS层来做(图灵完备语言)
  • 提高重绘性能

重绘和回流

页面渲染过程:

  • 当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。
  • 当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。

模拟虚拟DOM

<ul id="list"><li class="item">Item 1</li><li class="item">Item 2</li>
</ul>
// js模拟虚拟DOM
{tag: 'ul',attrs:{id: 'list'},children:[{tag: 'li',attrs: {className: 'item'},children: ['Item 1']},{tag: 'li',attrs: {className: 'item'},children: ['Item 2']}]
}
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Document</title><script src="https://cdn.bootcss.com/jquery/2.2.0/jquery.min.js"></script>
</head>
<body><div id="container"></div><button id="btn-change">change</button><script>var data = [{name: '张三',age: '20',address: '北京'},{name: '王五',age: '22',address: '成都'},{name: '李四',age: '21',address: '上海'}]// 渲染函数function render(data) {var $container = $('#container');// 清空容器,重要!!!$container.html('');// 拼接 tablevar $table = $('<table>');$table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>'));data.forEach(function (item) {$table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address   + '</td>/tr>'))});// 渲染到页面$container.append($table);}$('#btn-change').click(function () {data[1].age = 30;data[2].address = '深圳';// re-render  再次渲染render(data);})// 页面加载完立刻执行(初次渲染)render(data);</script>
</body>
</html>

虽然只改变了两个数据,但是整个table都闪烁了(回流&重绘)

  • DOM操作是‘昂贵’的,js运行效率高
  • 尽量减少DOM操作,尽量减少回流重绘

虚拟DOM如何应用,核心API是什么

介绍 snabbdom

snabbdom GitHub地址

官网例子:

var snabbdom = require('snabbdom');
var patch = snabbdom.init([ // Init patch function with chosen modulesrequire('snabbdom/modules/class').default, // makes it easy to toggle classesrequire('snabbdom/modules/props').default, // for setting properties on DOM elementsrequire('snabbdom/modules/style').default, // handles styling on elements with support for animationsrequire('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodesvar container = document.getElementById('container');// h函数生成一个虚拟节点
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),' and this is just normal text',h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode); // 把vnode加入到container中// 数据改变,重新生成一个newVnode
var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),' and this is still just normal text',h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
]);
// Second `patch` invocation
// 将newVnode更新到之前的vnode中,从而更新视图
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state

snabbdom h 函数

var vnode = h('ul#list',{},[h('li.item',{},'Item 1'),h('li.item',{},'Item 2')
]){tag: 'ul',attrs:{id: 'list'},children:[{tag: 'li',attrs: {className: 'item'},children: ['Item 1']},{tag: 'li',attrs: {className: 'item'},children: ['Item 2']}]
}

snabbdom patch 函数

var vnode = h('ul#list',{},[h('li.item',{},'Item 1'),h('li.item',{},'Item 2')
])
var container = document.getElementById('container');
patch(container, vnode);// 模拟改变
var btnChange = document.getElementById('btn-change');
btnChange.addEventListener('click',function(){var newVnode = h('ul#list',{},[h('li.item',{},'Item 111'),h('li.item',{},'Item 222'),h('li.item',{},'Item 333')])patch(vnode, newVnode);
})

snabbdom例子

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Document</title><script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
</head>
<body><div id="container"></div><button id="btn-change">change</button><script>var snabbdom = window.snabbdom;// 定义 patchvar patch = snabbdom.init([snabbdom_class,snabbdom_props,snabbdom_style,snabbdom_eventlisteners])// 定义 hvar h = snabbdom.h;var container = document.getElementById('container');// 生成 vnodevar vnode = h('ul#list',{},[h('li.item',{},'Item 1'),h('li.item',{},'Item 2')])patch(container, vnode);// 模拟数据改变var btnChange = document.getElementById('btn-change');btnChange.addEventListener('click',function(){var newVnode = h('ul#list',{},[h('li.item',{},'Item 1'),h('li.item',{},'Item 222'),h('li.item',{},'Item 333')])patch(vnode, newVnode);})</script>
</body>
</html>

看图,只有修改了的数据才进行了刷新,减少了DOM操作,这其实就是vnode与newVnode对比,找出改变了的地方,然后只重新渲染改变的

重做之前的demo

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>Document</title>
</head>
<body><div id="container"></div><button id="btn-change">change</button><script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-class.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-props.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-style.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-eventlisteners.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.0/h.js"></script><script type="text/javascript">var snabbdom = window.snabbdom;// 定义关键函数 patchvar patch = snabbdom.init([snabbdom_class,snabbdom_props,snabbdom_style,snabbdom_eventlisteners]);// 定义关键函数 hvar h = snabbdom.h;// 原始数据var data = [{name: '张三',age: '20',address: '北京'},{name: '王五',age: '22',address: '成都'},{name: '李四',age: '21',address: '上海'}]// 把表头也放在 data 中data.unshift({name: '姓名',age: '年龄',address: '地址'});var container = document.getElementById('container')// 渲染函数var vnode;function render(data) {var newVnode = h('table', {}, data.map(function (item) {var tds = [];var i;for (i in item) {if (item.hasOwnProperty(i)) {tds.push(h('td', {}, item[i] + ''));}}return h('tr', {}, tds)}));if (vnode) {// re-renderpatch(vnode, newVnode);} else {// 初次渲染patch(container, newVnode);}// 存储当前的 vnode 结果vnode = newVnode;}// 初次渲染render(data)var btnChange = document.getElementById('btn-change')btnChange.addEventListener('click', function () {data[1].age = 30data[2].address = '深圳'// re-renderrender(data)})</script>
</body>
</html>

核心API

  • h('<标签名>',{...属性...},[...子元素...])
  • h('<标签名>',{...属性...},'...')
  • patch(container,vnode)
  • patch(vnode,newVnode)

简单介绍 diff 算法

什么是 diff 算法

这里有两个文本文件:

借用git bashdiff 命令可以比较两个文件的区别:

在线diff工具

虚拟DOM ---> DOM

// 一个实现流程,实际情况还很复杂
function createElement(vnode) {var tag = vnode.tag  // 'ul'var attrs = vnode.attrs || {}var children = vnode.children || []if (!tag) {return null}// 创建真实的 DOM 元素var elem = document.createElement(tag)// 属性var attrNamefor (attrName in attrs) {if (attrs.hasOwnProperty(attrName)) {// 给 elem 添加属性elem.setAttribute(attrName, attrs[attrName])}}// 子元素children.forEach(function (childVnode) {// 给 elem 添加子元素elem.appendChild(createElement(childVnode))  // 递归})// 返回真实的 DOM 元素return elem
}

vnode ---> newVnode

function updateChildren(vnode, newVnode) {var children = vnode.children || [];var newChildren = newVnode.children || [];children.forEach(function (childVnode, index) {var newChildVnode = newChildren[index];if (childVnode.tag === newChildVnode.tag) {// 深层次对比,递归updateChildren(childVnode, newChildVnode);} else {// 替换replaceNode(childVnode, newChildVnode);}})
}function replaceNode(vnode, newVnode) {var elem = vnode.elem;  // 真实的 DOM 节点var newElem = createElement(newVnode);// 替换
}

最后

创建了一个前端学习交流群,感兴趣的朋友,一起来嗨呀!

JavaScript从初级往高级走系列————Virtual Dom相关推荐

  1. JavaScript从初级往高级走系列————prototype

    原文博客地址:https://finget.github.io/2018/05/10/javascript-prototype/ 原型 下面内容为转载的,原地址,写的真的很好! 构造函数创建对象 我们 ...

  2. 深入框架本源系列 —— Virtual Dom

    该系列会逐步更新,完整的讲解目前主流框架中底层相通的技术,接下来的代码内容都会更新在 这里 为什么需要 Virtual Dom 众所周知,操作 DOM 是很耗费性能的一件事情,既然如此,我们可以考虑通 ...

  3. 6本适合初级到高级HTML5程序员阅读的书籍推荐,读完事半功倍

    随着移动互联网的日益兴起,IT行业对于前端的需求也在不断的提高,那么从前端小白修炼成为HTML5前端大神的这个过程之中,一些必备的枕边书也是必不可少的. 俗话说书籍是人类进步的阶梯,对HTML5程序员 ...

  4. 原生 遍历_细品原生JS从初级到高级知识点汇总(三)

    作者:火狼1 转发链接:https://juejin.im/post/5daeefc8e51d4524f007fb15 目录 细品原生JS从初级到高级知识点汇总(一) 细品原生JS从初级到高级知识点汇 ...

  5. 年底了,如何准备 Java 初级和高级的技术面试?

    作者:hsm_computer 来自:cnblogs.com/JavaArchitect/p/9032323.html 本人最近几年一直在做java后端方面的技术面试官,而在最近两周,又密集了面试了一 ...

  6. 初级中级高级_初级职位,(半)高级职位

    初级中级高级 As a recent hire at my new job, as expected, a lot of things seemed scary and overwhelming. T ...

  7. 如何准备Java初级和高级技术的面试呢?

    IT行业的崛起带动了一大批的新兴职业,Java数据开发就是其中之一,作为IT行业的刚需职位,企业对合格的Java开发人员求贤若渴, 在各大主流招聘平台上, Java相关职位数量一直名列前茅,那么我们如 ...

  8. java面试总结(一)-----如何准备Java初级和高级的技术面试

    java面试总结(一)--如何准备Java初级和高级的技术面试 本文内容来自:https://mp.weixin.qq.com/s?__biz=MzAxNDMwMTMwMw==&mid=224 ...

  9. IT:后端进阶技术路线图(初级→中级→高级)、后端开发工程师(技术方向分类之后台业务开发/中间件/内核/分布式架构)基础知识简介、技术路线/技术趋势指南(如何选择自己的技术方向)之详细攻略

    IT:后端进阶技术路线图(初级→中级→高级).后端开发工程师(技术方向分类之后台业务开发/中间件/内核/分布式架构)基础知识简介.技术路线/技术趋势指南(如何选择自己的技术方向)之详细攻略 目录 后端 ...

最新文章

  1. php面向对象实践,【技术产品】PHP中的面向对象实践-基本实践案例
  2. 财务大数据比赛有python吗-如何在一个月之内掌握python数据分析,参加大数据比赛?...
  3. 【无标题】RestHighLevelClient工具类
  4. Python脚本实现图片加水印
  5. Android笔记 Android客户端从服务器获取源码乱码demo
  6. 将计算机知识应用于生活中,电脑知识在生活中的灵活运用(6页)-原创力文档...
  7. JSP-session编写购物车
  8. 游戏服务器开发技术栈
  9. 如何制作Excel表头
  10. Python语言程序设计 测验6: 组合数据类型 (第3周)
  11. 7-1 愿天下有情人都是失散多年的兄妹 (25 分)
  12. etr2模式,时力高HXD1C转换开关KRGV+ETR2
  13. 白话 P-value 这个再通俗不过了~
  14. opencv ipcam
  15. nodejs (usb)连接打印机 获取打印状态(escpos-printer和node-escpos)检测USB端口的热插拔
  16. linux怎样收集系统信息,Linux下收集系统和硬件信息的10个实用命令
  17. 【蓝牙系列】蓝牙5.4到底更新了什么(2)
  18. 【电力系统】经济调度、最优潮流、机组组合
  19. SQL查记录总数-总数统计的方法
  20. 商城App接入快递100

热门文章

  1. [网络流24题]圆桌问题
  2. php.ini 中文详解
  3. pat Simulation Test for PAT(B) 9月4日
  4. MySQL视图的应用
  5. 网站检测之防注入绕过的十一种技巧
  6. 第一章 软件工程概论
  7. Every Woman is beautiful
  8. Composite UI Application Block学习笔记之Event Broker
  9. 都9012了,这几个公众号你还没关注?
  10. linux系统如何用root用户登陆,Linux用root账号创建一个新的登录账号的方法