虚拟DOM内部是如何工做的

英文原文连接javascript

Virtual DOM很神奇,同时也比较复杂,难以理解。react,preact和类似的js库都使用了virtual dom。然而,我找不到任何好的文章或者文档,能够详细地又容易理解的方式来解释它。所以我决定本身写一篇。java

注意:文章篇幅较长,文中有大量的图片来帮助理解。文中使用的是preact的代码,由于它体积小,容易阅读。可是它与React里大部分的几率是保持一致的。但愿阅读完这篇文章后,你能够更好地理解React和Preact这样的类库,甚至为它们做出贡献。node

在这篇文章中,我将列举一个简单的例子来解释如下这些是如何工做的:react

  1. Babel和JSX算法

  2. 建立VNode-一个简单的virtual DOM元素babel

  3. 处理组件和子组件app

  4. 初始化渲染和建立一个DOM元素dom

  5. 从新渲染函数

  6. 移除DOM元素spa

  7. 替换DOM元素

The app

这是一个简单地可筛选的搜索应用,它包含了两个组件FilteredListListList组件用来渲染一组items(默认:"California"和"New York")。这个应用有一个搜索框,能够根据字母来过滤列表项。很是地直观:

img

概览图

咱们用jsx来写组件,它会被babel转换成纯js,而后Preact的h函数会将这段js转换成DOM树,最后Preact的Virtual DOM算法会将virtual DOM转换成真实的DOM树,来构建咱们的应用。
img

在深刻Virtual DOM的生命周期以前,咱们先理解一下jsx,由于它为库提供了入口。

Babel And JSX

在React,Preact这样的类库中,没有HTML标签,取而代之的是,一切都是javascript。因此咱们要在js中写HTML标签,可是在js中写HTML简直就是噩梦?

对于咱们的应用来讲,咱们将会像下面这样来写HTML

img
img

这就是jsx的由来。jsx本质上就是容许咱们在javascript中书写HTML!而且容许咱们在HTML中经过使用花括号来使用js。
jsx帮助咱们像下面这样写组件
img
img

jsx转换成js

jsx很酷,但它不是合法的js,而且最终咱们须要的是真实的DOM。JSX只是帮助编写一个真实DOM的替代品,除此以外,它别无用处。因此咱们须要一种方法将它转换成对应的JSON对象(也就是Virtual DOM),做为转化成真实DOM的输入。咱们须要一个函数来实现这个功能。

在Preact中h函数就是干这件事情的,等同于React中的React.createElement

可是如何将jsx转换成h函数的调用呢?Babel就是干这件事情的。Babel遍历每一个jsx节点,并将它们转换成h函数调用。
img

Babel JSX(React vs Preact)

默认状况下,Babel将jsx转换成React.createElement调用
img
可是咱们能够很容易地将函数名修改为任何名称,只须要在babelrc中配置一下便可

Option 1:
//.babelrc
{   "plugins": [
      ["transform-react-jsx", { "pragma": "h" }]
     ]
}
Option 2:
//Add the below comment as the 1st line in every JSX file
/** @jsx h */

img

挂载到真实DOM

不只仅是render中的代码会被转换成h函数,最初的挂载也会!

这就是代码执行开始的地方

//Mount to real DOM
render(<FilteredList/>, document.getElementById(‘app’));
//Converted to "h":
render(h(FilteredList), document.getElementById(‘app’));

h函数的输出

h函数将jsx转化后的内容转换成Virtual DOM节点。一个Preact的Virtual DOM节点就是一个简单的表明了单个包含属性和子节点的DOM节点的js对象,以下所示:

{
   "nodeName": "",
   "attributes": {},
   "children": []
}

好比,应用的input标签对应的Virtual DOM以下:

{
   "nodeName": "input",
   "attributes": {
    "type": "text",
    "placeholder": "Search",
    "onChange": ""
   },
   "children": []
}

注意:h函数并非建立整棵树!它只是简单地建立某个节点的js对象。可是由于render方法。。。

好了,让咱们看看Virtual DOM是如何工做的。

Preact中的Virtual DOM算法

在下面的流程图中,展现了在Preact中,组件是如何被建立、更新和删除的过程。同时也展现了像componentWillMount这样的生命周期事件是何时被调用的。
img

如今理解起来有些困难,因此咱们一步一步来拆解流程图中的每种状况。

情景1:初始化app

1.1 建立Virtual DOM

高亮的部分展现了根据给定的组件生成的Virtual DOM树。注意一点这里并无为子组件建立Virtual DOM
img
下面这幅图展现了应用首次加载时发生的状况。这个库最后为FilteredList组件建立了带有子节点和属性
的Virtual DOM
img
注意:在这个过程当中还调用了componentWillMountrender生命周期方法(在上图中的绿色区块)

此时,咱们有了一个Virtual DOM,div元素是父亲节点,带有一个input和一个list的子节点

1.2 若是不是一个组件,则建立真实的DOM

在这一步中,它只是为父亲节点建立一个真实DOM,对于子节点,重复这个过程
img
此时,咱们在下图中只有一个div展现出来
img

1.3 对于子元素重复这个过程

在这一步中,循环全部的子节点。在咱们的应用中,将会循环input和list
img

1.4 处理孩子节点和添加到父亲节点

在这一步中,咱们将会处理叶子节点,因为input有个父节点div,那么咱们将会将input添加到div中做为
子节点。而后流程转向建立List(第二个子节点是div)
img
此时,咱们的app长下面这样
img

注意:在input被建立以后,因为它没有任何子节点,并不会立马就去循环和建立List组件。相反地,它会首先
input标签添加到父节点div中去,完事以后再返回处理List标签

1.5 处理子节点

如今控制流回到了步骤1.1,而且开始处理List组件。可是因为List是一个组件,因此它会遍历执行自身的render方法,从而得到一组VNodes,就像下面这样:
img
List组件的循环完成时,它会返回List的VNode,就像下面这样:
img

1.6 对于全部的子节点,重复步骤1.1到1.4

对于每一个节点,它将会重复以上的每一步。一旦到达叶子节点,它将会被加入到父节点中去,而且重复这个过程。
img
下面的图片展现了每一个节点是如何添加上去的(深度优先遍历)
img

1.7 处理完成

此时已经完成了处理过程。而后对于全部的组件,会调用componentDidMount方法(从子组件开始,直到父组件)
img

注意:当一切准备就绪,一个真实DOM的引用会被添加到每一个组件的实例中。这个引用会在接下来的一些更新操做(建立、更新、删除)被用来比较,避免重复建立相同的DOM节点

情景2:删除叶子节点

当输入"cal"并按回车,这将会删除第二个列表子元素,也就是一个叶子节点(New York),同时其余父元素都会保留。
img

让咱们看下这种情景下,流程是怎么样的

2.1 建立VNodes

在初始化渲染以后,后面的每次改变都是一次"更新"。当建立VNodes时,更新周期与建立周期很是类似,而且再一次建立全部的VNodes。不过既然是更新(不是建立)组件,将会调用每一个组件和子组件相应的componentWillReceiveProps,shouldComponentUpdatecomponentWillUpdate方法。

另外,更新周期并不会从新建立已经存在的DOM元素。
img

2.2 使用真实DOM引用,避免建立重复的节点

以前提到过,在初始化加载期间,每一个组件都有一个指向真实DOM树的引用。下面的图展现了引用是如何寻找咱们的应用的。

img
当VNodes被建立后,每一个VNode的属性都会与真实DOM的属性相比较。若是真实DOM存在,循环将会转移到下个节点
img

2.3 若是在真实DOM中有其它的节点,则删除

下面的图展现了真实DOM和VNode之间的不一样

img
因为存在不一样,真实DOM中的"New York"节点会被算法删除掉,正以下面图展现的那样。这个算法也称为"componentDidUpdate"生命周期。
img

情景3-卸载整个组件

举例:当输入blabla时,因为不匹配"California"和"New York",咱们将不会渲染子组件List。这意味着,咱们须要卸载整个组件
img
img
删除一个组件相似于删除一个单独的节点。除此以外,当咱们删除一个包含组件引用的节点,将会调用"componentWillUnmount",而后递归删除全部的DOM元素。在删除了全部的真实DOM元素以后,"componentDidUnmount"将会被调用。
下面的图片展现了真实DOM元素"ul"包含了指向"List"组件的引用。
img
下面的图片在流程图中高亮了deleting/unmounting一个组件是如何工做的
img

最后

但愿这篇文章能帮助你理解Virtual DOM是如何工做的(至少在Preact中)

相关文章
相关标签/搜索