Virtual DOM算法

把一个div元素的属性打印出来,如下:

可以看到仅仅是第一层,真正DOM的元素是非常庞大的,这也是DOM加载慢的原因。

相对于DOM对象,原生的JavaScript对象处理起来更快,而且更简单。DOM树上的结构、属性信息都可以用JavaScript对象表示出来:

var element = {
tagName: 'ul', // 节点标签名
props: { // DOM的属性,用一个对象存储键值对
id: 'list'
},
children: [ // 该节点的子节点
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
]
}

上面对应的HTML写法是:

<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>

DOM树的信息可以用JavaScript对象表示出来,则说明可以用JavaScript对象去表示树结构来构建一棵真正的DOM树。

状态变更->重新渲染整个视图的方式可以用新渲染的对象树去和旧的树进行对比,记录这两棵树的差异。两者的不同之处就是我们需要对页面真正的DOM操作,然后把它们应用在真正的DOM树上,页面就变更了。这样可以做到:视图的结构确实是整个全新渲染了,但是最后操作DOM的只有变更不同的地方。

Virtual DOM算法,可以归纳为以下几个步骤:
1.用JavaScript对象结构表示DOM树的结构,然后用这个树构建一个真正的DOM树,插到文档当中

2.当状态变更的时候,重新构建一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树的差异

3.把2所记录的差异应用到步骤1所构建的的真正的DOM树上,视图就更新了

Virtual DOM本质就是在JS和DOM之间做了一个缓存,JS操作Virtual DOM,最后再应用到真正的DOM上。

难点-算法实现

步骤一:用JS对象模拟虚拟DOM树

用JavaScript来表示一个DOM节点,则需要记录它的节点类型、属性、子节点:
element.js

function Element (tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
} module.exports = function (tagName, props, children) {
return new Element(tagName, props, children)
}

上面的DOM结构可以表示为:

var el = require('./element')

var ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
])

现在ul只是一个JavaScript对象表示的DOM结构,页面上并没有这个结构。可以根据这个ul构建真正的<ul>:

Element.prototype.render = function () {
var el = document.createElement(this.tagName) // 根据tagName构建
var props = this.props for (var propName in props) { // 设置节点的DOM属性
var propValue = props[propName]
el.setAttribute(propName, propValue)
} var children = this.children || [] children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
: document.createTextNode(child) // 如果字符串,只构建文本节点
el.appendChild(childEl)
}) return el
}

render方法会根据tagName构建一个真正的DOM节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。所以需要:

var ulRoot = ul.render()
document.body.appendChild(ulRoot)

上面的ulRoot是真正的DOM节点,把它塞进文档中,这样body里面就有了真正的<ul>的DOM结构:

<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>

步骤二:比较两棵虚拟DOM树的差异

比较两棵DOM树的差异是Virtual DOM算法最核心的部分,就是diff算法。两棵树的完全diff算法是一个时间复杂度为O(n^3)的问题。但在前端中,很少会跨越层级地移动DOM元素。所以Virtual DOM只会对同一层级的元素进行对比:

上面的div只会和同一层级的div对比,第二层级的只会跟第二层级对比。这样算法复杂度就可以达到O(n)。

a.深度优先遍历,记录差异

在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:

在深度优先遍历的时候,每遍历到一个节点就把该节点和新的树进行对比。如果有差异的话就记录到一个对象里面。

// diff 函数,对比两棵树
function diff (oldTree, newTree) {
var index = 0 // 当前节点的标志
var patches = {} // 用来记录每个节点差异的对象
dfsWalk(oldTree, newTree, index, patches)
return patches
} // 对两棵树进行深度优先遍历
function dfsWalk (oldNode, newNode, index, patches) {
// 对比oldNode和newNode的不同,记录下来
patches[index] = [...] diffChildren(oldNode.children, newNode.children, index, patches)
} // 遍历子节点
function diffChildren (oldChildren, newChildren, index, patches) {
var leftNode = null
var currentNodeIndex = index
oldChildren.forEach(function (child, i) {
var newChild = newChildren[i]
currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识
? currentNodeIndex + leftNode.count + 1
: currentNodeIndex + 1
dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点
leftNode = child
})
}

例如,上面的div和新的div有差异,当前的标记是0,那么:

patches[0] = [{difference}, {difference}, ...] // 用数组存储新旧节点的不同

同理p是patches[1],ul是patches[3],以此类推

b.差异类型

对DOM操作会有的差异:
 1替换掉原来的节点,例如把上面的div换成了section

2.移动、删除、新增子节点,例如上面的div的子节点,把p和ul顺序互换

3.修改了节点的属性

4.对于文本节点,文本内容可能会改变。例如修改上面的文本节点2内容为Virtual DOM2

所以定义了几种差异类型:

var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3

对于节点的替换,判断新旧节点的tagName和是不是一样,如果不一样就替换掉。如div换成section,记录如下:

patches[0] = [{
type: REPALCE,
node: newNode // el('section', props, children)
}]

如果给div新增了属性id为container,记录如下:

patches[0] = [{
type: REPALCE,
node: newNode // el('section', props, children)
}, {
type: PROPS,
props: {
id: "container"
}
}]

如果修改文本节点,如上面的文本节点2,记录如下:

patches[2] = [{
type: TEXT,
content: "Virtual DOM2"
}]

c.列表对比算法

上面如果把div中的子节点重新排序,看如p,ul,div的顺序换成了div,p,ul。按照同层进行顺序对比的话,它们都会被替换掉,这样DOM开销非常大。而实际上只需要通过节点移动就可以的了。

假设现在可以英文字母唯一得标志每一个子节点:

旧的节点顺序:
a  b  c  d  e  f  g  h  i

现在对节点进行删除、插入、移动的操作。新增j节点,删除e节点,移动h节点:

新的节点顺序:

a  b  c  h  d  f  g  i  j

现在知道了新旧的顺序,求最小的插入、删除操作(移动可以看成是删除和插入操作的结合)。这个问题抽象出来其实是字符串的最小编辑距离问题(Edition Distance),最常见的算法是Levenshtein Distance,

通过动态规划求解,时间复杂度为O(M*N)。而我们只需要优化一些常见的移动操作,牺牲一定的DOM操作,让算法时间复杂度达到线性的O((max(M,N)))。

获取某个父节点的子节点的操作,就可以记录如下:

patches[0] = [{
type: REORDER,
moves: [{remove or insert}, {remove or insert}, ...]
}]

由于tagName是可以重复的,所以不能用这个来进行对比。需要给子节点加上一盒唯一标识key,列表对比的时候,使用key进行对比,这样就能复用旧的DOM树上的节点。

通过深度优先遍历两棵树,每层节点进行对比,记录下每个节点的差异。完整的diff算法访问:https://github.com/livoras/simple-virtual-dom/blob/master/lib/diff.js

步骤三:把差异应用到真正的DOM树上

因为步骤一所构建的JavaScript对象树和render出来的真正的DOM树的信息、结构是一样的。所以可以对那棵DOM树也进行深度优先遍历,遍历的时候从步骤二生成的patches对象中找出当前遍历的节点差异,然后进行DOM操作。

function patch (node, patches) {
var walker = {index: 0}
dfsWalk(node, walker, patches)
} function dfsWalk (node, walker, patches) {
var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异 var len = node.childNodes
? node.childNodes.length
: 0
for (var i = 0; i < len; i++) { // 深度遍历子节点
var child = node.childNodes[i]
walker.index++
dfsWalk(child, walker, patches)
} if (currentPatches) {
applyPatches(node, currentPatches) // 对当前节点进行DOM操作
}
}

applyPatches,根据不同类型的差异对当前节点进行 DOM 操作:

function applyPatches (node, currentPatches) {
currentPatches.forEach(function (currentPatch) {
switch (currentPatch.type) {
case REPLACE:
node.parentNode.replaceChild(currentPatch.node.render(), node)
break
case REORDER:
reorderChildren(node, currentPatch.moves)
break
case PROPS:
setProps(node, currentPatch.props)
break
case TEXT:
node.textContent = currentPatch.content
break
default:
throw new Error('Unknown patch type ' + currentPatch.type)
}
})
}

完整patch代码访问:https://github.com/livoras/simple-virtual-dom/blob/master/lib/patch.js

结语:

Virtual DOM算法主要是实现上面步骤的三个函数:element,diff,patch。

// 1. 构建虚拟DOM
var tree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: blue'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li')])
]) // 2. 通过虚拟DOM构建真正的DOM
var root = tree.render()
document.body.appendChild(root) // 3. 生成新的虚拟DOM
var newTree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li'), el('li')])
]) // 4. 比较两棵虚拟DOM树的不同
var patches = diff(tree, newTree) // 5. 在真正的DOM元素上应用变更
patch(root, patches)

实际中还需要处理事件监听等,生成虚拟DOM的时候也可以加入JSX语法。这些都做了就可以构建一个简单的ReactJS。

参考:戴嘉华https://github.com/livoras/blog/issues/13

实现虚拟(Virtual)DOM的更多相关文章

  1. Virtual DOM 系列一:认识虚拟DOM

    1. 什么是Virtual DOM? Virtual DOM(虚拟DOM)是指用JS模拟DOM结构.本质上来讲VD是一个JS对象,并且至少包含三个属性:tag(html标签),props(标签的属性, ...

  2. Virtual DOM 虚拟DOM的理解(转)

    作者:戴嘉华 转载请注明出处并保留原文链接( #13 )和作者信息. 目录: 1 前言 2 对前端应用状态管理思考 3 Virtual DOM 算法 4 算法实现 4.1 步骤一:用JS对象模拟DOM ...

  3. React:关于虚拟DOM(Virtual DOM)

    Virtual DOM 是一个模拟 DOM 树的 JavaScript 对象. React 使用 Virtual DOM 来渲染 UI,当组件状态 state 有更改的时候,React 会自动调用组件 ...

  4. React virtual DOM explained in simple English/简单语言解释React的虚拟DOM

    初学React,其中一个很重要的概念是虚拟DOM,看了一篇文章,顺带翻译一下. If you are using React or learning React, you must have hear ...

  5. 窥探Vue.js 2.0 - Virtual DOM到底是个什么鬼?

    引言 你可能听说在Vue.js 2.0已经发布,并且在其中新添加如了一些新功能.其中一个功能就是"Virtual DOM". Virtual DOM是什么 在之前,React和Em ...

  6. 抛开react,如何理解virtual dom和immutability

    去年以来,React的出现为前端框架设计和编程模式吹来了一阵春风.很多概念,无论是原本已有的.还是由React首先提出的,都因为React的流行而倍受关注,成为大家研究和学习的热点.本篇分享主要就聚焦 ...

  7. 深度剖析:如何实现一个 Virtual DOM 算法

    本文转载自:https://github.com/livoras/blog/issues/13 目录: 1 前言 2 对前端应用状态管理思考 3 Virtual DOM 算法 4 算法实现 4.1 步 ...

  8. Virtual DOM 算法

    前端 virtual-dom react.js javascript 目录: 1 前言 2 对前端应用状态管理思考 3 Virtual DOM 算法 4 算法实现 4.1 步骤一:用JS对象模拟DOM ...

  9. 深度理解 Virtual DOM

    目录: 1 前言 2 技术发展史 3 Virtual DOM 算法 4 Virtual DOM 实现 5 Virtual DOM 树的差异(Diff算法) 6 结语 7 参考链接 1 前言 我会尽量把 ...

随机推荐

  1. 在 Visual Studio 中调试 XAML 设计时异常

    在 Visual Studio 中进行 WPF, UWP, Silverlight 开发时,经常会遇到 XAML 设计器由于遭遇异常而无法正常显示设计器视图的情况.很多时候由于最终生成的项目在运行时并 ...

  2. SpringSecurity 3.2入门(7)自定义权限控制介绍

    总结Spring Security的使用方法有如下几种: 一种是全部利用配置文件,将用户.权限.资源(url)硬编码在xml文件中. 二种是用户和权限用数据库存储,而资源(url)和权限的对应关系硬编 ...

  3. 如何更新maven需要的jar包

    第一次使用maven,检出项目生成时出现缺少xxx.jar,目录在C盘下: 拿mybatis-spring-1.2.2.jar来说,发现在C:\Users\Administrator\.m2\repo ...

  4. mvc 中Request[""]与Request.QueryString[""]

    1.Request[""]与Request.QueryString[""]获取不到值时返回null: 2.Request[""]与Reque ...

  5. mui使用技巧

    1.document.addEventListener('plusready', function(){ //console.log("所有plus api都应该在此事件发生后调用,否则会出 ...

  6. 菜鸟学习Spring——SpringIoC容器基于三种配置的对比

    一.概述 对于实现Bean信息定义的目标,它提供了基于XML.基于注解及基于java类这三种选项.下面总结一下三种配置方式的差异. 二.Bean不同配置方式比较. 三.Bean不同配置方式的适用场合. ...

  7. CSS样式编写案例

    1.制作如图三角形效果: 步骤一:将右侧盒子设置为相对定位 步骤二:在右侧盒子里面新建个子盒子,设置宽高相等,为正方形,绝对定位 步骤三:将绝对定位的盒子用CSS3旋转属性旋转 2.制定如图的序列号 ...

  8. 关于scheduleAtFixedRate方法与scheduleWithFixedDelay的使用

    一.scheduleAtFixedRate方法 该方法是ScheduledExecutorService中的方法,用来实现周期性执行给定的任务,public ScheduledFuture<?& ...

  9. VS中bin,app_code,app_data,app_browser,app_GlobalResources等文件夹的作用 .

    1.  Bin文件夹 Bin文件夹包含应用程序所需的,用于控件.组件或者需要引用的任何其他代码的可部署程序集.该目录中存在的任何.dll文件将自动地链接到应用程序.如果在该文件夹中留有不用的或过期的文 ...

  10. geth访问公有链

    同步以太坊,配置rpc地址 mkdir /opt/blockchain nohup geth --syncmode "fast" --cache=1024 --maxpeers 3 ...