对 JS virtual DOM 的一知半解

 ES6     JavaScript      React.js   Node.js    React-Native

  我记得中学课本对函数的定义好像是,对于一个 X,总有一个相应的Y值与之对应,这就叫做函数,可以表示为 y=f(x) /  f(x)=x,f( )即是这种映射关系,当然那时候根本不懂什么叫映射,为什么要用 f ,等学了编程才知道这不就是 func()或者 function()吗? f(x)不就是 function(x){ //函数的实现  }, y就相当于返回值啊,原来这特么还真的是函数,扯了这么多其实就是想说,永远对生活充满好奇和兴趣,这才是生活真正的乐趣所在,如果整天都在重复一种单调乏味的机械性工作,,就像被小学老师罚抄几十遍的课文一样那种感觉现在想起来就手疼.

  接触前端也有一段时间了,我认为以后也必定是前端的未来,一个手机完全没必要装上百个 APP, 整天下载更新,我们的手机只要有个浏览器有网络就够了,这些 APP 完全可以换成网页版的,何况现在连个绝味鸭脖都有 APP 了,你也根本装不过来,我们完全可以像点击百度图标一样就打开百度网页,打开一个 APP, 不需要占用手机内存空间,不需要下载更新,如果你喜欢桌面操作,完全可以添加个桌面的快捷方式.

  现在就说说这几种语言之间的关系吧,JavaScript 一种脚本语言,风靡世界的前端神器,配合 HTML5,CSS , JQuery操作 DOM 节点,这么说吧,你见到的每一个网页基本都有JS 的身影, ES6这是一种标准,其实你完全可以把它当成 JS, 当然了还有 ES5,ES4,将来还有 ES7,JavaScript 只是ES 的一种实现,react.js是一种 JavaScript 框架,起源于 Facebook 被用于 instagram,用于构建复杂且可维护的用户界面,后面会有详细的介绍, Node.js,js 是脚本语言,脚本语言都需要有一个解析器才能运行,对于HTML 里的 js,浏览器充当了解析器的角色,对于需要独立运行的 JS,nodeJS 就相当于一个解析器,每一种解析器都是一种运行环境,不但允许 js 定义各种数据结构进行各种计算,还允许 js 使用运行环境提供的内置对象和方法做一些事情,在浏览器的环境下, js 的作用是操作 DOM, 浏览器提供了 document 之类的操作对象,而运行在 node.js 里的 js的用途是操作磁盘文件或搭建 HTTP服务器, node.js 就提供了 fs,http 这样的内置对象,另外, js 是一种单线程语言,一般都是通过回调函数来完成各种异步操作.

  至于 RN 你可以理解为就是一种利用 JS 来写手机 native 端的界面的技术,他不同于以往的 Hybird的 webview 那种混合开发,底层依然是 Facebook 实现的与 native 组件的桥接.js写的当然是 iOS 安卓两端通用的,而且他是通过加载 js bundle包来更新的,自然就支持热更新,只需要更新 js bundle 包就可以了,不需要新版本审核等个十天半月的,至于怎么实现的,那就要用到 virtual DOM 了,后面会有讲到.

 

React的核心思想:

一个Component拯救世界,忘掉烦恼,从此不再操心界面。

只关心 state状态机变量, 只有 set state才会触发渲染,改变数据,更新界面

 

1. Virtual Dom快,有两个前提

1.1 Javascript很快

Chrome刚出来的时候,在Chrome里跑Javascript非常快,给了其它浏览器很大压力。而现在经过几轮你追我赶,各主流浏览器的Javascript执行速度都很快了。

Julia有一个Benchmark 可以看到Javascript跟C语言很接近了,也就几倍的差距,跟Java基本也是一个量级。

所以说,单纯的Javascript其实速度是很快的。

多说一句,这种benchmark并不是绝对的依据,因为用这个语言写这个跑得快,并不代表一定是用这个语言写那个也跑得快。

 

1.2 DOM很慢

关于什么CSS,什么layout那些我不懂,就不瞎说了,咱就说说DOM的结构。

当你用document.createElement()创建一个空的Element的时候(比如创建一个空的div),有以下这几页的东西需要实现(当然,这不是标准,只是个大概的意思):

HTMLElement - Web API Interfaces

Element - Web API Interfaces

GlobalEventHandlers

非常非常多,并且还有不少嵌套引用。

你可以在Chrome console里手动调用document.createElement 然后插入DOM里看看效果。

这还是一个空的Elemnt,啥内容也没有,就这么复杂。所以说DOM的操作非常慢是可以理解的。不是浏览器不想好好实现DOM,而是DOM设计得太复杂,没办法。

 

而更糟糕的是,我们(以及很多框架)在调用DOM的API的时候做得不好,导致整个过程更加的慢。React的Virtual Dom解决的是这一部分问题,它并不能解决DOM本身慢的问题。

比如说,现在你的list是这样,

<ul>

<li>0</li>

<li>1</li>

<li>2</li>

<li>3</li>

</ul>

你想把它变成这样

<ul>

<li>6</li>

<li>7</li>

<li>8</li>

<li>9</li>

<li>10</li>

</ul>

通常的操作是什么?

先把0, 1,2,3这些Element删掉,然后加几个新的Element 6,7,8,9,10进去,这里面就有4次Element删除,5次Element添加。

而React会把这两个做一下Diff,然后发现其实不用删除0,1,2,3,而是可以直接改innerHTML,然后只需要添加一个Element(10)就行了,这样就是4次innerHTML操作加1个Element添加,比9次Element操作快多了吧?

 

当然还有其它一些例子能够优化我们对DOM的操作,就不举例子了。(实际上是因为我举不出例子。。。)

 

2. 关于React

2.1 接口和设计

在React的设计里,是完全不需要你操作DOM的。在React里其实根本就没有DOM这个概念的存在,只有Component。当你写好一个Component以后,Component会完全负责UI,你不需要也不应该去也不能够指挥Component怎么显示,你只能告诉它你想要显示一个香蕉还是两个梨。

隔离DOM并不是因为DOM慢(当然DOM确实慢),而是把界面和业务完全隔离,操作数据的只关心数据,操作界面的只关心界面。可以想象成把MVC里面的Controller分成两个部分,一部分合并到M里面去,一部分合并到V里面去,就剩下MV,没有C了。。。其实M也并不是Model了。

重复一遍,React的意思是,我提供一个Component,然后你只管给我数据,界面的事情完全不用你操心,我保证会把界面变成你想要的样子。

你可以把一个React的Component想象成一个Pure Function,只要你给的数据是[1, 2, 3],我保证显示的是[1, 2, 3]。没有什么删除一个Element,添加一个Element这样的事情。NO。你要我显示什么就给我一个完整的列表。

 

说到这里,插一句别的,我一开始看到这里还以为这样的处理方式比较适合一般的WEB应用,写游戏啊什么的可能这个模式不太好用,然后我就看到Pete Hunt那个Talk,说DOOM 3就是这么干的。

。。。

眼泪都下来了,大神们的思路果然我是摸不着边的,洗洗睡吧。

 

再说几句瞎扯的话,Flux虽然说的是单向的Data Flow,但是实际上就是单向的Observer。

Store->View->Action->Store(箭头是数据流向,实现上可以理解为View监听Store,View直接trigger action,然后Store监听Action)

等等,不是说Component是pure function不跟谁绑定吗,为啥View要监听Store?你这个骗子。怪不得都没有人给你点赞。

。。。

。。

我们还是继续说React把,Flux是什么鬼,我反正没听过。

 

 

2.2 实现

OK,那么,如何实现React呢?

其实对于React来说,最容易实现的办法是每次完全摧毁整个DOM,然后重新建立一个全新的DOM。因为一个Component是一个Pure function,根本就没有State这个概念,我又不知道DOM现在是什么样子,那最简单的办法当然是只要你给新数据,我就把整个DOM删了,然后根据你给的数据重新生成一个DOM咯。

 

等等,Virtual DOM哪儿去了?

 

事实是这样的,最简单实现React的方式虽然说非常简单,但是效率实在是太低了,你居然要全部都删了重建DOM,DOM本身已经很慢了,你还这么去用,谁能忍啊?

 

然后Virtual DOM就来救场了。

 

Virtual DOM和DOM是啥关系呢?

首先,Virtual DOM并没有完全实现DOM,Virtual DOM最主要的还是保留了Element之间的层次关系和一些基本属性。因为DOM实在是太复杂,一个空的Element都复杂得能让你崩溃,并且几乎所有内容我根本不关心好吗。所以Virtual DOM里每一个Element实际上只有几个属性,并且没有那么多乱七八糟的引用。所以哪怕是直接把Virtual DOM删了,根据新传进来的数据重新创建一个新的Virtual DOM出来都非常非常非常快。(每一个component的render函数就是在做这个事情,给新的virtual dom提供input)

 

所以,引入了Virtual DOM之后,React是这么干的:

你给我一个数据,我根据这个数据生成一个全新的Virtual DOM,然后跟我上一次生成的Virtual DOM去 diff,得到一个Patch,然后把这个Patch打到浏览器的DOM上去。完事。

 

有点像版本控制打patch的思路。

假设在任意时候有,VirtualDom1 == DOM1 (组织结构相同)

当有新数据来的时候,我生成VirtualDom2,然后去和VirtualDom1做diff,得到一个Patch。

然后将这个Patch去应用到DOM1上,得到DOM2。

如果一切正常,那么有VirtualDom2 == DOM2。

 

这里你可以做一些小实验,去破坏VirtualDom1 == DOM1这个假设(手动在DOM里删除一些Element,这时候VirtualDom里的Element没有被删除,所以两边不一样了)。

然后给新的数据,你会发现生成的界面就不是你想要的那个界面了。

 

 

最后,回到为什么Virtual Dom快这个问题上。

其实是由于每次生成virtual dom很快,diff生成patch也比较快,而在对DOM进行patch的时候,我能够根据Patch的内容,优化一部分DOM操作,比如之前1.2里的那个例子。

重点就在最后,哪怕是我生成了virtual dom,哪怕是我跑了diff,但是我根据patch简化了那些DOM操作省下来的时间依然很可观。所以总体上来说,还是比较快。

 

简单发散一下思路,如果哪一天,DOM本身的已经操作非常非常非常快了,并且我们手动对于DOM的操作都是精心设计优化过后的,那么加上了VirtualDom还会快吗?

当然不行了,毕竟你多做了这么多额外的工作。

 

但是那一天会来到吗?

大不了到时候不用Virtual DOM。

2 对前端应用状态管理的思考

 

假如现在你需要写一个像下面一样的表格的应用程序,这个表格可以根据不同的字段进行升序或者降序的展示。

<img src="https://pic3.zhimg.com/0c1ee6d9a303d5a43b0fa4b3fc82f3a2_b.png" data-rawwidth="899" data-rawheight="375" class="origin_image zh-lightbox-thumb" width="899" data-original="https://pic3.zhimg.com/0c1ee6d9a303d5a43b0fa4b3fc82f3a2_r.png">

 

 

这个应用程序看起来很简单,你可以想出好几种不同的方式来写。最容易想到的可能是,在你的 JavaScript 代码里面存储这样的数据:

var sortKey = "new" // 排序的字段,新增(new)、取消(cancel)、净关注(gain)、累积(cumulate)人数

var sortType = 1 // 升序还是逆序

var data = [{...}, {...}, {..}, ..] // 表格数据

 

用三个字段分别存储当前排序的字段、排序方向、还有表格数据;然后给表格头部加点击事件:当用户点击特定的字段的时候,根据上面几个字段存储的内容来对内容进行排序,然后用 JS 或者 jQuery 操作 DOM,更新页面的排序状态(表头的那几个箭头表示当前排序状态,也需要更新)和表格内容。

 

这样做会导致的后果就是,随着应用程序越来越复杂,需要在JS里面维护的字段也越来越多,需要监听事件和在事件回调用更新页面的DOM操作也越来越多,应用程序会变得非常难维护。后来人们使用了 MVC、MVP 的架构模式,希望能从代码组织方式来降低维护这种复杂应用程序的难度。但是 MVC 架构没办法减少你所维护的状态,也没有降低状态更新你需要对页面的更新操作(前端来说就是DOM操作),你需要操作的DOM还是需要操作,只是换了个地方。

 

既然状态改变了要操作相应的DOM元素,为什么不做一个东西可以让视图和状态进行绑定,状态变更了视图自动变更,就不用手动更新页面了。这就是后来人们想出了 MVVM 模式,只要在模版中声明视图组件是和什么状态进行绑定的,双向绑定引擎就会在状态更新的时候自动更新视图(关于MV*模式的内容,可以看这篇介绍)。

 

MVVM 可以很好的降低我们维护状态 -> 视图的复杂程度(大大减少代码中的视图更新逻辑)。但是这不是唯一的办法,还有一个非常直观的方法,可以大大降低视图更新的操作:一旦状态发生了变化,就用模版引擎重新渲染整个视图,然后用新的视图更换掉旧的视图。就像上面的表格,当用户点击的时候,还是在JS里面更新状态,但是页面更新就不用手动操作 DOM 了,直接把整个表格用模版引擎重新渲染一遍,然后设置一下innerHTML就完事了。

 

听到这样的做法,经验丰富的你一定第一时间意识这样的做法会导致很多的问题。最大的问题就是这样做会很慢,因为即使一个小小的状态变更都要重新构造整棵 DOM,性价比太低;而且这样做的话,input和textarea的会失去原有的焦点。最后的结论会是:对于局部的小视图的更新,没有问题(Backbone就是这么干的);但是对于大型视图,如全局应用状态变更的时候,需要更新页面较多局部视图的时候,这样的做法不可取。

 

但是这里要明白和记住这种做法,因为后面你会发现,其实 Virtual DOM 就是这么做的,只是加了一些特别的步骤来避免了整棵 DOM 树变更

 

另外一点需要注意的就是,上面提供的几种方法,其实都在解决同一个问题:维护状态,更新视图。在一般的应用当中,如果能够很好方案来应对这个问题,那么就几乎降低了大部分复杂性。

 

3 Virtual DOM算法

 

DOM是很慢的。如果我们把一个简单的div元素的属性都打印出来,你会看到:

 

<img src="https://pic4.zhimg.com/d5cda33e28d83ba12368202645f9e35b_b.png" data-rawwidth="1239" data-rawheight="336" class="origin_image zh-lightbox-thumb" width="1239" data-original="https://pic4.zhimg.com/d5cda33e28d83ba12368202645f9e35b_r.png">

 

 

而这仅仅是第一层。真正的 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树。

 

之前的章节所说的,状态变更->重新渲染整个视图的方式可以稍微修改一下:用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对象结构。当然这样做其实没什么卵用,因为真正的页面其实没有改变。

 

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

这就是所谓的 Virtual DOM 算法。包括几个步骤:

  1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
  2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  3. 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

 

4 算法实现

 

4.1 步骤一:用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>

 

 

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

 

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

<img src="https://pic2.zhimg.com/6d64b0b7889e7f020bb020aea5947a09_b.png" data-rawwidth="912" data-rawheight="471" class="origin_image zh-lightbox-thumb" width="912" data-original="https://pic2.zhimg.com/6d64b0b7889e7f020bb020aea5947a09_r.png">

 

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

 

4.2.1 深度优先遍历,记录差异

 

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

<img src="https://pic2.zhimg.com/c4ba535164d29fd46383d19512c37349_b.png" data-rawwidth="1018" data-rawheight="513" class="origin_image zh-lightbox-thumb" width="1018" data-original="https://pic2.zhimg.com/c4ba535164d29fd46383d19512c37349_r.png">

 

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

// diff 函数,对比两棵树

function diff (oldTree, newTree) {

  var index = 0 // 当前节点的标志

  var patches = {} // 用来记录每个节点差异的对象

  dfsWalk(oldTree, newTree, index, patches)

  return patches

}

 

// 对两棵树进行深度优先遍历

function dfsWalk (oldNode, newNode, index, patches) {

  // 对比oldNodenewNode的不同,记录下来

  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],类推。

 

4.2.2 差异类型

 

上面说的节点的差异指的是什么呢?对 DOM 操作可能会:

  1. 替换掉原来的节点,例如把上面的div换成了section
  2. 移动、删除、新增子节点,例如上面div的子节点,把p和ul顺序互换
  3. 修改了节点的属性
  4. 对于文本节点,文本内容可能会改变。例如修改上面的文本节点2内容为Virtual DOM 2。

 

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

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"

}]

 

那如果把我div的子节点重新排序呢?例如p, ul, div的顺序换成了div, p, ul。这个该怎么对比?如果按照同层级进行顺序对比的话,它们都会被替换掉。如p和div的tagName不同,p会被div所替代。最终,三个节点都会被替换,这样DOM开销就非常大。而实际上是不需要替换节点,而只需要经过节点移动就可以达到,我们只需知道怎么进行移动。

 

这牵涉到两个列表的对比算法,需要另外起一个小节来讨论。

 

4.2.3 列表对比算法

 

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

旧的节点顺序:

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 算法代码可见 

 

4.3 步骤三:把差异应用到真正的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)

    }

  })

}

 

 

5 结语

 

Virtual DOM 算法主要是实现上面步骤的三个函数:elementdiffpatch。然后就可以实际的进行使用:

// 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了。

 

虽然Virtual DOM确实是性能不错,但是其实可以说它是无心插柳的一个结果。

 

转载于:https://www.cnblogs.com/ChrisZhou666/p/6027729.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值