一起理解 Virtual DOM

一起理解 Virtual DOM

2016/11/14 · JavaScript
· DOM

本文作者: 伯乐在线 –
luobotang
。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

前言

React 好像已经火了很久很久,以致于我们对于 Virtual DOM
这个词都已经很熟悉了,网上也有非常多的介绍 React、Virtual DOM
的文章。但是直到前不久我专门花时间去学习 Virtual DOM,才让我对 Virtual
DOM
有了一定的理解,以致于要怀疑起很久之前看过的那些文章来。倒不是这些文章讲得不对,而是现在在我看来角度不太好,说得越多,越说不清。

让我能够有所开窍(自认为)的,是这篇文章:


Change And Its Detection In JavaScript Frameworks
Monday Mar 2, 2015 by Tero Parviainen
http://teropa.info/blog/2015/03/02/change-and-its-detection-in-javascript-frameworks.html


作者看问题的角度很棒,从数据变更与UI同步的角度来介绍各个典型框架,特别是对于
React 的 Virtual DOM,从这个角度理解起来更容易些。

感兴趣的同学,如果没有读过这篇文章,推荐去看一看,不感兴趣就算了。不过接下来我要讲的东西,部分整理自这篇文章,特别是从这篇文章中引用的图片,非常棒。当然还有我自己的一些思考,以及一些对于目前
Virtual DOM 实现的开源库的分析。

如果读了上面推荐的这篇文章,我倒是不介意你不再继续把本文读下去,因为有些东西你已经领会到了。当然,也不反对。

React通过JS对象模拟原生DOM,加上DOM Diff 极大提升了DOM操作的性能。
Virtual DOM:ReactElement 树,轻量级的JavaScript对象,并通过
ReactDOM 的 render 方法渲染到真实 DOM 上。

奥门威尼斯网址 1

React的贡献在于它提出的组件化、虚拟DOM、帮助整个前端进入工程化、将函数式编程的思想带给前端。
这里记录一下学习过程时关于虚拟DOM算法的实现。

前言

React 好像已经火了很久很久,以致于我们对于 Virtual DOM
这个词都已经很熟悉了,网上也有非常多的介绍 React、Virtual DOM
的文章。但是直到前不久我专门花时间去学习 Virtual DOM,才让我对 Virtual
DOM
有了一定的理解,以致于要怀疑起很久之前看过的那些文章来。倒不是这些文章讲得不对,而是现在在我看来角度不太好,说得越多,越说不清。

让我能够有所开窍(自认为)的,是这篇文章:


Change And Its Detection In JavaScript Frameworks
Monday Mar 2, 2015 by Tero Parviainen


作者看问题的角度很棒,从数据变更与UI同步的角度来介绍各个典型框架,特别是对于
React 的 Virtual DOM,从这个角度理解起来更容易些。

感兴趣的同学,如果没有读过这篇文章,推荐去看一看,不感兴趣就算了。不过接下来我要讲的东西,部分整理自这篇文章,特别是从这篇文章中引用的图片,非常棒。当然还有我自己的一些思考,以及一些对于目前
Virtual DOM 实现的开源库的分析。

如果读了上面推荐的这篇文章,我倒是不介意你不再继续把本文读下去,因为有些东西你已经领会到了。当然,也不反对。

变化这件事

谈论页面的变化之前,咱们先看下数据和页面(视觉层面的页面)的关系。数据是隐藏在页面底下,通过渲染展示给用户。同样的数据,按照不同的页面设计和实现,会以不同形式、样式的页面呈现出来。有时候在一个页面内的不同位置,也会有相同数据的不同表现。

奥门威尼斯网址,Paste_Image.png

Web
的早期,这些页面通常是静态的,页面内容不会变化。而如果数据发生了变化,通常需要重新请求页面,得到基于新的数据渲染出的新的页面。

Paste_Image.png

至少,这个模式理解起来挺简单不是吗。

直到 Web
应用复杂起来,开发者们开始关注用户体验,开始将大量的处理向前端迁移,页面变得动态、灵活起来。一个显著的特征是,数据发生变化之后,不再需要刷新页面就能看到页面上的内容随之更新了。

前端需要做的事情变得多了起来,前端工程师们也就修炼了起来,各种前端技术也就出现了。

首先,聪明的工程师们发现既然是在前端渲染页面,如果只是部分数据发生了变化,就要把页面整体或一大块区域重新渲染就有点笨了。为什么不把事情做得更极致些,只更新变化的数据对应的页面的内容呢?

怎么做呢?操作 DOM 呗。DOM
就是浏览器提供给开发者用于操作页面的模型嘛,直接通过脚本来调用 DOM
的各种接口就 OK 了。而且我们还有了像 jQuery 这样的棒棒的工具,操作 DOM
变得 so easy。

然而,页面越来越复杂,聪明的工程师们发现数据变化之后,老是需要手动编码去操作对应的
DOM
节点执行更新,有点烦,不够懒啊。于是各种框架如雨后春笋般出现了,纷纷表示可以简化这个过程。

稍微早期的框架有这样的:

Paste_Image.png

开发者借助框架,监听数据的变更,在数据变更后更新对应的 DOM
节点。虽然还是要写一些代码,但是写出来的代码好像很有条理的样子,至少更容易理解和维护了,也不错嘛。

更进一步,MVVM 框架出现了,以 AngularJS 为代表:

Paste_Image.png

仍然是数据变化后更新对应 DOM
节点的方式,但是建立这种绑定关系的过程被框架所处理,开发者要写的代码变少了,而且代码更易读和维护了。

再然后呢,大家就在这个棒棒的模式上继续深耕,纷纷表示还可以在性能上做得更好,前端领域一片繁荣。

再后来 React 出现了,它不仅不是 MVVM 框架,甚至连 MV*
框架都不是。这年头,不是个 MV* 框架还好意思出门?可 React
还真的带来了新的思路!

什么思路呢?

就是回到过去,回到那个简单而美好的时候。具体而言,就是每次数据发生变化,就重新执行一次整体渲染。的确这样更简单,不用去琢磨到底是数据的哪一部分变化了,需要更新页面的哪一部分。但是坏处太明显,体验不好啊。而
React 给出了解决方案,就是 Virtual DOM。

Virtual DOM 概况来讲,就是在数据和真实 DOM
之间建立了一层缓冲。对于开发者而言,数据变化了就调用 React
的渲染方法,而 React 并不是直接得到新的 DOM 进行替换,而是先生成 Virtual
DOM,与上一次渲染得到的 Virtual DOM 进行比对,在渲染得到的 Virtual DOM
上发现变化,然后将变化的地方更新到真实 DOM 上。

简单来说,React 在提供给开发者简单的开发模式的情况下,借助 Virtual DOM
实现了性能上的优化,以致于敢说自己“不慢”。

var Element = {
  type: 'div',
  props: {
    className: css,
    childlren: []
  }
};

豆瓣


变化这件事

谈论页面的变化之前,咱们先看下数据和页面(视觉层面的页面)的关系。数据是隐藏在页面底下,通过渲染展示给用户。同样的数据,按照不同的页面设计和实现,会以不同形式、样式的页面呈现出来。有时候在一个页面内的不同位置,也会有相同数据的不同表现。

奥门威尼斯网址 2

Paste_Image.png

Web
的早期,这些页面通常是静态的,页面内容不会变化。而如果数据发生了变化,通常需要重新请求页面,得到基于新的数据渲染出的新的页面。

奥门威尼斯网址 3

Paste_Image.png

至少,这个模式理解起来挺简单不是吗。

直到 Web
应用复杂起来,开发者们开始关注用户体验,开始将大量的处理向前端迁移,页面变得动态、灵活起来。一个显著的特征是,数据发生变化之后,不再需要刷新页面就能看到页面上的内容随之更新了。

前端需要做的事情变得多了起来,前端工程师们也就修炼了起来,各种前端技术也就出现了。

首先,聪明的工程师们发现既然是在前端渲染页面,如果只是部分数据发生了变化,就要把页面整体或一大块区域重新渲染就有点笨了。为什么不把事情做得更极致些,只更新变化的数据对应的页面的内容呢?

怎么做呢?操作 DOM 呗。DOM
就是浏览器提供给开发者用于操作页面的模型嘛,直接通过脚本来调用 DOM
的各种接口就 OK 了。而且我们还有了像 jQuery 这样的棒棒的工具,操作 DOM
变得 so easy。

然而,页面越来越复杂,聪明的工程师们发现数据变化之后,老是需要手动编码去操作对应的
DOM
节点执行更新,有点烦,不够懒啊。于是各种框架如雨后春笋般出现了,纷纷表示可以简化这个过程。

稍微早期的框架有这样的:

奥门威尼斯网址 4

Paste_Image.png

开发者借助框架,监听数据的变更,在数据变更后更新对应的 DOM
节点。虽然还是要写一些代码,但是写出来的代码好像很有条理的样子,至少更容易理解和维护了,也不错嘛。

更进一步,MVVM 框架出现了,以 AngularJS 为代表:

奥门威尼斯网址 5

Paste_Image.png

仍然是数据变化后更新对应 DOM
节点的方式,但是建立这种绑定关系的过程被框架所处理,开发者要写的代码变少了,而且代码更易读和维护了。

再然后呢,大家就在这个棒棒的模式上继续深耕,纷纷表示还可以在性能上做得更好,前端领域一片繁荣。

再后来 React 出现了,它不仅不是 MVVM 框架,甚至连 MV
框架都不是。这年头,不是个 MV 框架还好意思出门?可 React
还真的带来了新的思路!

什么思路呢?

就是回到过去,回到那个简单而美好的时候。具体而言,就是每次数据发生变化,就重新执行一次整体渲染。的确这样更简单,不用去琢磨到底是数据的哪一部分变化了,需要更新页面的哪一部分。但是坏处太明显,体验不好啊。而
React 给出了解决方案,就是 Virtual DOM。

Virtual DOM 概况来讲,就是在数据和真实 DOM
之间建立了一层缓冲。对于开发者而言,数据变化了就调用 React
的渲染方法,而 React 并不是直接得到新的 DOM 进行替换,而是先生成 Virtual
DOM,与上一次渲染得到的 Virtual DOM 进行比对,在渲染得到的 Virtual DOM
上发现变化,然后将变化的地方更新到真实 DOM 上。

简单来说,React 在提供给开发者简单的开发模式的情况下,借助 Virtual DOM
实现了性能上的优化,以致于敢说自己“不慢”。

Virtual DOM

React 基于 Virtual DOM 的数据更新与UI同步机制:

React – 初始渲染

初始渲染时,首先将数据渲染为 Virtual DOM,然后由 Virtual DOM 生成 DOM。

React – 数据更新

数据更新时,渲染得到新的 Virtual DOM,与上一次得到的 Virtual DOM 进行
diff,得到所有需要在 DOM 上进行的变更,然后在 patch 过程中应用到 DOM
上实现UI的同步更新。

Virtual DOM 作为数据结构,需要能准确地转换为真实
DOM,并且方便进行对比。除了 Virtual DOM 外,React
还实现了其他的特性,为了专注于 Virtual DOM,我另外找了两个比较 Virtual
DOM 来学习:

  • virtual-dom
  • Snabbdom

这里也推荐给感兴趣且还没有读过两个库源码的同学。

由于只关注 Virtual DOM,通过阅读两个库的源码,对于 Virtual DOM
的定位有了更深一步的理解。

首先看数据结构。

** Virtual DOM 数据结构 **

DOM 通常被视为一棵树,元素则是这棵树上的节点(node),而 Virtual DOM
的基础,就是 Virtual Node 了。

在 virtual-dom 中,给 Virtual Node 声明了对应的类
VirtualNode,基本是用于存储数据,包括:

  • tagName
  • properties
  • children
  • key
  • namespace
  • count
  • hasWidgets
  • hasThunks
  • hooks
  • descendantHooks

Snabbdom 的 Virtual Node 则是纯数据对象,通过
vnode
模块来创建,对象属性包括:

  • sel
  • data
  • children
  • text
  • elm
  • key

虽然有所差别,除去实现上的差别和库本身的额外特性,可以看到 Virtual Node
用于创建真实节点的数据包括:

  • 元素类型
  • 元素属性
  • 元素的子节点

有了这些其实就可以创建对应的真实节点了。

创建 Virtual DOM

嵌套 Virtual Node 就可以得到一棵树了。virtual-dom 和 Snabbdom
都提供了函数调用的方式来创建 Virtual Tree,这个过程就是渲染了:

var vTree = h('div', [
  h('span', 'hello'),
  h('span', 'world')
])

React 提供 JSX 这颗糖,使得我们可以用类似 HTML
的语法来编写,不过编译后实质还是通过函数调用来得到一棵嵌套的 Virtual
Tree。而且这对于理解 Virtual DOM 机制来说不是特别重要,先不管这个。

使用 Virtual DOM

首先来看初始化,virtual-dom 提供了
createElement
函数:

var rootNode = createElement(tree)
document.body.appendChild(rootNode)

根据 Virtual Node 创建真实 DOM 元素,然后再追加到页面上。

再来看更新。virtual-dom 有明确的两步操作,首先 diff,然后 patch:

var newTree = render(count)
var patches = diff(tree, newTree)
rootNode = patch(rootNode, patches)

而 Snabbdom 则简单些,只有一个 patch
函数,内部在进行比对的同时将更新应用到了真实 DOM 上,而且初始化也是用的
patch 函数:

var vnode = render(data)
var container = document.getElementById('container')
patch(container, vnode)

// after data changed
var newVnode = render(data)
patch(vnode, newVnode)

性能优化

关于性能优化,除了 Virtual DOM 机制本身提供的特性以外,再就是不同的
Virtual DOM 库自身的优化方案了,这个可以看上面两个库的文档,不再赘述。

其实提到 Virtual DOM
的差异比对,有人会对其内部如何处理数组感兴趣。的确,如果数组元素的位置发生了改变,这个要识别起来是有点麻烦。为此,上面两个库和
React 其实都在 Virtual Node
上额外记录了一个属性“key”,就是用来辅助进行 Virtual Node 的比对的。

简单来说,如果两个 Virtual Node 的位置不同,但是 key
属性相同,那么会将这两个节点视为由相同数据渲染得到的,然后进一步进行差异分析。所以,并不是仅仅按照位置进行比对,具体的实现可以查看各个库的源码。

React diff:帮助我们计算出 Virtual DOM 中真正变化的部分,由虚拟
DOM来确保只对界面上真正变化的部分进行实际的DOM操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染。

一、浏览器工作流

1、创建DOM树

一旦浏览器接收到一个HTML文件,渲染引擎(render
engine)就开始解析它,并根据HTML元素(elements)一一对应地生成DOM
节点(nodes),组成一棵DOM树。

2、创建渲染树

同时,浏览器也会解析来自外部CSS文件和元素上的inline样式。通常浏览器会为这些样式信息,连同包含样式信息的DOM树上的节点,再创建另外一个树,一般被称作渲染树(render
tree)

3、创建渲染树背后的故事

WebKit内核的浏览器上,处理一个节点的样式的过程称为attachment。DOM树上的每个节点都有一个attach方法,它接收计算好的样式信息,返回一个render对象(又名renderer)
Attachment的过程是同步的,新节点插入DOM树时,会调用新节点的attach方法。
构建渲染树时,由于包含了这些render对象,每个render对象都需要计算视觉属性(visual
properties);这个过程通过计算每个元素的样式属性来完成。

4、布局 Layout

又被简称为Reflow[2]
构造了渲染树以后,浏览器引擎开始着手布局(layout)。布局时,渲染树上的每个节点根据其在屏幕上应该出现的精确位置,分配一组屏幕坐标值。

5、绘制 Painting

接着,浏览器将会通过遍历渲染树,调用每个节点的paint方法来绘制这些render对象。paint方法根据浏览器平台,使用不同的UI后端API(agnostic
UI backend API)。 通过绘制,最终将在屏幕上展示内容。

一、Virtual DOM

在React中,render 执行的结果得到的并不是真正的 DOM
节点,结果仅仅是轻量级的 JavaScript 对象,我们称之为virtual DOM。
真正的 DOM
元素属性非常多,每次操作很有可能引起回流(Reflow)和重绘(Repaint)。
相对于真正的 DOM 对象,原生的 JavaScript
对象处理起来更快,而且更简单。DOM
树上的结构、属性信息我们都可以很容易地用 JavaScript 对象表示出来:

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

对应的HTML:

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

所以可以用 JavaScript 对象表示的树结构来构建一棵真正的DOM树。用
JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个
JavaScript
的对象结构。然后新渲染的对象树去和旧的树进行对比,记录这两棵树差异。记录下来的不同就是我们需要对页面真正的
DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。
Virtual DOM 算法包括几个步骤:

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

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。因为DOM
很慢,JS只操作Virtual DOM,最后的时候再把变更渲染到DOM。

Virtual DOM

React 基于 Virtual DOM 的数据更新与UI同步机制:

奥门威尼斯网址 6

React – 初始渲染

初始渲染时,首先将数据渲染为 Virtual DOM,然后由 Virtual DOM 生成 DOM。

奥门威尼斯网址 7

React – 数据更新

数据更新时,渲染得到新的 Virtual DOM,与上一次得到的 Virtual DOM 进行
diff,得到所有需要在 DOM 上进行的变更,然后在 patch 过程中应用到 DOM
上实现UI的同步更新。

Virtual DOM 作为数据结构,需要能准确地转换为真实
DOM,并且方便进行对比。除了 Virtual DOM 外,React
还实现了其他的特性,为了专注于 Virtual DOM,我另外找了两个比较 Virtual
DOM 来学习:

  • virtual-dom
  • Snabbdom

这里也推荐给感兴趣且还没有读过两个库源码的同学。

由于只关注 Virtual DOM,通过阅读两个库的源码,对于 Virtual DOM
的定位有了更深一步的理解。

首先看数据结构。

Virtual DOM 数据结构

DOM 通常被视为一棵树,元素则是这棵树上的节点(node),而 Virtual DOM
的基础,就是 Virtual Node 了。

在 virtual-dom 中,给 Virtual Node 声明了对应的类
VirtualNode,基本是用于存储数据,包括:

  • tagName
  • properties
  • children
  • key
  • namespace
  • count
  • hasWidgets
  • hasThunks
  • hooks
  • descendantHooks

Snabbdom 的 Virtual Node 则是纯数据对象,通过
vnode
模块来创建,对象属性包括:

  • sel
  • data
  • children
  • text
  • elm
  • key

虽然有所差别,除去实现上的差别和库本身的额外特性,可以看到 Virtual Node
用于创建真实节点的数据包括:

  • 元素类型
  • 元素属性
  • 元素的子节点

有了这些其实就可以创建对应的真实节点了。

创建 Virtual DOM

嵌套 Virtual Node 就可以得到一棵树了。virtual-dom 和 Snabbdom
都提供了函数调用的方式来创建 Virtual Tree,这个过程就是渲染了:

JavaScript

var vTree = h(‘div’, [ h(‘span’, ‘hello’), h(‘span’, ‘world’) ])

1
2
3
4
var vTree = h(‘div’, [
  h(‘span’, ‘hello’),
  h(‘span’, ‘world’)
])

React 提供 JSX 这颗糖,使得我们可以用类似 HTML
的语法来编写,不过编译后实质还是通过函数调用来得到一棵嵌套的 Virtual
Tree。而且这对于理解 Virtual DOM 机制来说不是特别重要,先不管这个。

使用 Virtual DOM

首先来看初始化,virtual-dom 提供了
createElement
函数:

JavaScript

var rootNode = createElement(tree) document.body.appendChild(rootNode)

1
2
var rootNode = createElement(tree)
document.body.appendChild(rootNode)

根据 Virtual Node 创建真实 DOM 元素,然后再追加到页面上。

再来看更新。virtual-dom 有明确的两步操作,首先 diff,然后 patch:

JavaScript

var newTree = render(count) var patches = diff(tree, newTree) rootNode =
patch(rootNode, patches)

1
2
3
var newTree = render(count)
var patches = diff(tree, newTree)
rootNode = patch(rootNode, patches)

而 Snabbdom 则简单些,只有一个 patch
函数,内部在进行比对的同时将更新应用到了真实 DOM 上,而且初始化也是用的
patch 函数:

JavaScript

var vnode = render(data) var container =
document.getElementById(‘container’) patch(container, vnode) // after
data changed var newVnode = render(data) patch(vnode, newVnode)

1
2
3
4
5
6
7
var vnode = render(data)
var container = document.getElementById(‘container’)
patch(container, vnode)
 
// after data changed
var newVnode = render(data)
patch(vnode, newVnode)

性能优化

关于性能优化,除了 Virtual DOM 机制本身提供的特性以外,再就是不同的
Virtual DOM 库自身的优化方案了,这个可以看上面两个库的文档,不再赘述。

其实提到 Virtual DOM
的差异比对,有人会对其内部如何处理数组感兴趣。的确,如果数组元素的位置发生了改变,这个要识别起来是有点麻烦。为此,上面两个库和
React 其实都在 Virtual Node
上额外记录了一个属性“key”,就是用来辅助进行 Virtual Node 的比对的。

简单来说,如果两个 Virtual Node 的位置不同,但是 key
属性相同,那么会将这两个节点视为由相同数据渲染得到的,然后进一步进行差异分析。所以,并不是仅仅按照位置进行比对,具体的实现可以查看各个库的源码。

小结

OK,以上就是我要讲的全部所有内容了。

相信很多同学之前对 Virtual DOM
已经很熟悉了,比我理解得更深入的同学相信也不会少。不过从“数据变化与UI同步更新”这个角度来理解
Virtual DOM,在我看来是比较好的,所以整理在这里了。

有个问题挺常见,AngularJS 和 React 哪个更好?

如果说各有千秋的话,估计大家就“呵呵”了。但是这两个框架/库从“数据变化与UI同步更新”的角度来看,的确都解决了问题,而且解决问题的方式大家都挺认可(至少在喜欢它们的同学眼里是这样的)。

而且,如果大家关注 Vue 的话,可以看到,这个 MVVM 框架已经发布了
2.0,其中就采用了 Virtual DOM 实现其UI同步更新!所以,这的确不矛盾啊。

第二个而且,技术本身不是目的,能够更好地解决问题才是王道嘛。

浅谈ReactDiff算法的几个规则:

二、总结

你可以看到,从创建渲染树,到布局,一直到绘制,只要你在这过程中进行一次DOM更新,整个渲染流程都会重做一遍。尤其是创建渲染树,它需要重新计算所有元素上的所有样式。
在一个复杂的单页面应用中,经常会涉及到大量的DOM操作,这将引起多次计算,使得整个流程变得低效,这应该尽量避免。
Virtual
DOM这个抽象层真正的闪光点正在于此:每当你想对视图进行一次更新,那些本该直接作用于真实DOM的改动,都会先作用于Virtual
DOM,然后再将要改动的部分通知到真实DOM。这样可以大幅减少DOM操作带来的重计算步骤。

既然如此,我们再来看看Virtual DOM到底解决了什么问题。
首先,它把管理DOM碎片这件事情自动化、抽象化了,使得你无需再去手动处理。另外,当你要手动去做这件事情的时候,你还得记得哪些部分变化了,哪些部分没变,毕竟之后重绘时,DOM树上的大量细节你都不需要重新刷新。这时候Virtual
DOM的自动化对你来说就非常有用了,如果它的实现是正确的,那么它就会知道到底哪些地方应该需要刷新,哪些地方不要。
最后,Virtual
DOM通过各种组件和你写的一些代码来请求对它进行操作,而不是直接对它本身进行操作,使你不必非要跟Virtual
DOM交互,也不必非要去了解Virtual
DOM修改DOM树的原理,也就不用再想着去修改DOM了。

二、算法实现

2.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'])
])

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 ul = ul.render()
document.body.appendChild(ulRoot)

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

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

2.2. 比较两棵DOM树的差异
传统 diff 算法的复杂度为 O(n3),React 通过制定大胆的策略:

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
  2. 两个相同组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
  • 对于同一层次的一组子节点,它们可以通过唯一的id进行区分。

将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题。
通过diff策略,React 分别对 tree diff、component diff 以及 element diff
进行算法优化。

  1. tree diff
    基于策略一,React
    对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个
    DOM 树的比较。
  2. component diff
  • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
  • 如果不是,则将该组件判断为 dirty
    component,从而替换整个组件下的所有子节点。
  • 对于同一类型的组件,有可能其 Virtual DOM
    没有任何变化,如果能够确切的知道这点那可以节省大量的 diff
    运算时间,因此 React 允许用户通过 shouldComponentUpdate()
    来判断该组件是否需要进行 diff。
  1. element diff
    当节点处于同一层级时,React diff
    提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和
    REMOVE_NODE(删除)。
  • INSERT_MARKUP:新的 component 类型不在老集合里,
    即是全新的节点,需要对新节点执行插入操作。
  • MOVE_EXISTING:在老集合有新 component 类型,且 element
    是可更新的类型,generateComponentChildren 已调用
    receiveComponent,这种情况下
    prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
  • REMOVE_NODE:老 component 类型,在新集合里也有,但对应的 element
    不同则不能直接复用和更新,需要执行删除操作,或者老 component
    不在新集合里的,也需要执行删除操作。
    React对其进行了优化,当节点相同时,只是由于位置发生变化。允许开发者对同一层级的同组子节点,添加唯一
    key 进行区分。
    新老集合进行 diff 差异化对比,通过 key
    发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置。

2.3把差异应用到真正的DOM树上

具体可参考:
如何实现一个Virtual
DOM算法
不可思议的react
diff

小结

OK,以上就是我要讲的全部所有内容了。

相信很多同学之前对 Virtual DOM
已经很熟悉了,比我理解得更深入的同学相信也不会少。不过从“数据变化与UI同步更新”这个角度来理解
Virtual DOM,在我看来是比较好的,所以整理在这里了。

有个问题挺常见,AngularJS 和 React 哪个更好?

如果说各有千秋的话,估计大家就“呵呵”了。但是这两个框架/库从“数据变化与UI同步更新”的角度来看,的确都解决了问题,而且解决问题的方式大家都挺认可(至少在喜欢它们的同学眼里是这样的)。

而且,如果大家关注 Vue 的话,可以看到,这个 MVVM 框架已经发布了
2.0,其中就采用了 Virtual DOM 实现其UI同步更新!所以,这的确不矛盾啊。

第二个而且,技术本身不是目的,能够更好地解决问题才是王道嘛。

打赏支持我写出更多好文章,谢谢!

打赏作者

  • 不同节点比较:节点类型不同则直接删除旧节点,插入新节点
  • 列表节点的比较:设置唯一的key,可以帮助react快速定位要修改的元素
    JSX:每一个JSX元素都只是 React.createElement(component, props,
    …children)
    的语法糖。因此,任何时候你用JSX语法写的代码也可以用普通的 JavaScript
    语法写出来。
另外分享几个技术周刊:

FrontEnd Focus
JavaScript
Weekly
FEX
技术周刊
众成翻译每周精选
湾区日报

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

奥门威尼斯网址 8
奥门威尼斯网址 9

1 赞 3 收藏
评论

关于作者:luobotang

奥门威尼斯网址 10

前端工程师@网易
个人主页 ·
我的文章 ·
4 ·
 

奥门威尼斯网址 11

class Hello extends React.Component {
  render() {
    return <div>Hello {this.props.toWhat}</div>;
  }
}

ReactDOM.render(
  <Hello toWhat="World" />,
  document.getElementById('root')
);
//可以被编译为不使用JSX的代码
class Hello extends React.Component {
  render() {
    return React.createElement('div', null, `Hello ${this.props.toWhat}`);
  }
}

ReactDOM.render(
  React.createElement(Hello, {toWhat: 'World'}, null),
  document.getElementById('root')
);

发表评论

电子邮件地址不会被公开。 必填项已用*标注