超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

虚拟 DOM 内部是如何工作的?

本文转载自:众成翻译
译者:我是搬运工
链接:http://www.zcfy.cc/article/3248
原文:https://medium.com/@rajaraodv/the-inner-workings-of-virtual-dom-666ee7ad47cf

流程图展现VDOM在Preact中如何工作

虚拟DOM (VDOM 也叫 VNode)非常有魔力 ✨ 但是也非常复杂和难以理解😱. React, 在Preact和一些类似的JS库的核心代码中使用. 不幸的是我发现没有一篇好的文章或者文档简洁明了的来介绍它。 因此我决定自己写一篇.

注意: 这篇文章很长. 我已经添加尽可能多的图片来使其理解更简单一些,但是我发现这样的话,文章更长了.

我用的是 Preact’s 代码 和 VDOM,因为它很小,你可以在将来很舒适的阅读它。 但是我相信几乎所有的概念同样适用于React.

我希望你读完这篇文章后,能够很容易的理解像React或者Preact的库,甚至对你写出类似的库也是有帮助的

在这篇博客中,我将会举一些简单示例,并且复习一下不同的小知识,给你一个关于它们到底如何工作的概念。特别地,我会复习:

  1. Babel 和 JSX
  2. 创建一个VNode - 一个简单的虚拟DOM元素
  3. 处理组件及子组件
  4. 初始化渲染并且创建一个DOM元素
  5. 重新渲染
  6. 移除DOM元素
  7. 替换DOM元素

关于这个demo:

这是一个简单过滤搜索应用, 仅包含有两个组件“FilteredList” 和 “List”。这个List组件渲染列表项(默认是“California” 和 “New York”)。这个应用有一个搜索的区域,可以根据字母来过滤列表项。非常的直观。

相关图片

应用代码: http://codepen.io/rajaraodv/pen/BQxmjj

大图

高级一点儿,我们用JSX写了组件,可以通过babel的命令行工具将其转换为原生的JS.然后Preact的“h”函数将它转换为虚拟DOM树(也称为 VNode)。最后Preact的虚拟DOM算法,根据虚拟DOM创建一个真实的DOM,来构成我们的应用。

大图

在我们深入理解VDOM的生命周期之前,让我们理解下JSX,它为库提供了基础

Babel 和 JSX

在React中,像Preact这样的库,没有HTML语法,取而代之的是一切皆Javascript。因此我们需要用Javascript来写HTML。但是用原生JS写DOM是一种噩梦。 😱

对于我们的应用,我们将会像下面这样书写HTML:

注意: 等会儿我会介绍“h”

这就是JSX的由来,JSX本质上允许我们在Javascript中书写HTML!并且允许我们在HTML中的{}号中使用JS的语法。

JSX帮助我们像下面这样很容易的书写组件:

将JSX树转换为Javascript

JSX很酷,但是不是合法的JS,但是根本上我们还是需要真实的DOM。JSX仅仅是帮助我们书写真实DOM的一种方法。除此之外,它毫无用处。

因此我们需要一种方法将JSX转换为正确的JSON对象(VDOM 也是一个“树”形的结构),我们需要将JSX作为创建真实DOM的基础。我们函数来做这样的事情.

在Preact中这个函数就是“h”函数.它作用和React中的React.createElement作用是一样的。

“h”是指 hyperscript - 一种可以通过JS来创建HTML的库。

但是怎样将JSX转换为“h”函数式的调用?这就是Babel的由来。Babel可以很轻松的遍历JSX的节点,然后将它们转换为“h”函数式的调用。

Babel JSX (React Vs Preact)

在React中babel会将JSX转换为React.createElement函数调用

左边: JSX 右边: React 的JS版本 (点击放大)

我们可以像下面这样增加[Babel Pragma]配置,可以很轻松为Preact的函数的名字起任何一个你想起的名字。

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 */`

“h” —通过Babel的配置 (点击放大)

挂载到真实DOM

组件的的render方法中的代码不仅被转换为“h”函数,而且开始挂载。

这是执行和一切的开始

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

//Converted to "h":
render(**h(FilteredList)**, document.getElementById(‘app’));

“h”函数的输出

The “h” function takes the output of JSX and creates something called a “VNode” (React’s “createElement” creates ReactElement). A Preact’s “VNode” (or a React’s “Element”) is simply a JS object representation of a single DOM node with it’s properties and children.

看起来像下面这样:

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

举个例子,我们的应用的Input表单的VNode像这样:

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

Note: “h” function doesn’t create the entire tree! It simply creates JS object for a given node. But since the “render” method already has the DOM JSX in a tree fashion, the end result will be a VNode with children and grand children that looks like a tree.
注意“h”函数不会创建完整的树 它仅仅对于给定的node创建了一个JS对象。但是。 最后的结果将会是一个带有子元素和看起来像树的重要子元素的VNode.

参考代码:

“h” :https://github.com/developit/preact/blob/master/src/h.js

VNode: https://github.com/developit/preact/blob/master/src/vnode.js

“render”: https://github.com/developit/preact/blob/master/src/render.js

“buildComponentFromVNode: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L102

好了,让我们看下虚拟DOM如何工作的?

Preact虚拟DOM的算法流程图

下面的流程图展现了组件和子组件如何被Preact创建,更新,删除的。也展现了生命周期的不同阶段,对应的回调函数被调用,像“componentWillMount”。

注意: 我们会一步一步的复习每一部分,如果你会觉复杂,不用担心。

是的,立马理解所有的知识很难。让我们一步一步得通过浏览不同的情景,来复习流程图的不同部分。

注意: 当讨论到关键的生命周期的部分我将会用黄色高亮。

APP创建初始化

对一个给定的组件创建一个VNode

黄色高亮区域对于一个给定的组件创建虚拟DOM数,初始化处理循环。注意没有为子组件创建虚拟DOM(这是个不同的循环)

黄色区域展示了虚拟DOM的创建

下面这张图片展示了当我们应用第一次加载的时候发生了什么。这个库最终为主要组件“FilteredList”创建了一个带有子元素和属性的VNode。

注意: 它连着调用了生命周期方法“componentWillMount” 和 “render”.(看上面图片绿色的部分)

(click to zoom)

这个时候,我们有了个“div”的父元素,它包含了子节点“input”和“list”。

引用:

大多数的生命周期事件,像componentWillMount,render等等: https://github.com/developit/preact/blob/master/src/vdom/component.js

如果不是一个组件,创建一个真实的DOM

这一步,它仅会对父元素div创建一个真实的DOM,并且对于子节点(“input” 和 “List”)重复这一步骤。

黄色的循环部分展现了子组件的创建。

这一步,下面的图片中仅仅“div”被显示出来了。

引用:

document.createElement: https://github.com/developit/preact/blob/master/src/dom/recycler.js

对所有的子元素重复这一步

这一步,对所有的子元素将会重复这一步。在我们的应用中,会对“input” 和 “List” 重复。

对每一个子元素重复

处理子元素,并且把它加到父元素上.

这一步我们将会处理子树。既然“input”有一个父元素“div”,我们将会把input作为一个子元素加到div中。然后停止,返回创建“List”(第二个div子元素)。

结束处理子树

这一步,我们的应用看起来像下面这样:

注意: “input”被创建后,由于没有任何一个子元素,不会理解循环和创建“List”。它会首先将“input”加入到父元素“div”中,然后返回处理“List”。

引用:

appendChild: https://github.com/developit/preact/blob/master/src/vdom/diff.js

处理子组件(们)

控制流程回到1.1,对“List”组件开始所有的。但是“List”是一个组件,它调用“List”组件的方法render,得到一组新的虚DOM,像下面这样

对一个子组件重复所有的操作

对List组件重复操作之后,返回VNode像下面这样:

引用:

“buildComponentFromVNode: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L102

对所有子节点重复上面四小节的步骤

它会再一次对每一个节点重复上面的步骤。一旦它到达子节点,就会把它加入到节点的父节点,并且重复处理。

重复这一步骤,直到所有的父子节点被创建和添加。

下面的图片展示了每个节点的添加(提示: 深度优先)

真实的DOM树如何被虚拟DOM算法创建的。

结束处理

这一步,结束处理。它仅对所有的组件调用了“componentDidMount”(从子组件到父组件)并且停止。

重要提示: 一旦所有所有做完之后,一个真实DOM的引用被添加到每个组件的实例上去。这个引用被用来更新(创建,更新,删除)比较,避免重复创建同样的DOM节点。

删除叶子节点

当我们输入“cal” 关键字,确认。将会移除掉第二个list节点,保留所有的父节点。

让我们看下,怎么样看这个情景?

像之前那样创建VNodes.

当初始化渲染之后,未来的每一个变化都是一个更新。当需要创建VNodes时,更新的周期工作跟创建的周期非常的相似,并且再一次创建所有的VNodes

既然是一个组件的更新(不是创建),每个组件和子组件都会调用“componentWillReceiveProps”, “shouldComponentUpdate”, 和 “componentWillUpdate”

另外, update cycle, 如果那些元素已经存在不会重复创建真实的DOM。

更新组件的生命周期

引用

removeNode: https://github.com/developit/preact/blob/master/src/dom/index.js#L9

insertBefore: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L253

用引用的真实DOM,避免创建重复的nodes

像之前提到的,在初始化加载期间,每个组件相对应我们创建的真实DOM树有一个引用。下面这张图片展现了这一刻我们的应用的引用。

显示每一个组件 和 之前的DOM的差异

当虚拟DOM被创建,每个虚拟DOM的属性都会跟真实DOM的属性进行比较如果真实DOM存在,循环处理将会进行下一步

真实DOM已经存在(在更新期间)

引用

innerDiffNode: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L185

如果他们在真实的DOM中是额外的节点,移除他们

下面的图片展现了真实DOM和虚拟DOM的差异

(click to zoom)

这里有一点儿不同。在真实节点中的“New York”节点被算法移除了像下面流程图那样。当所有工作进行完毕算法也会调用“componentDidUpdate”。

移除DOM节点生命周期

卸载整个组件

让我们看看在filter组件中输入blabla,既然没有匹配到“California” 和 “New York”, 我们不会渲染子组件“List”,这意味着我们需要卸载整个组件。

如果没有结果的话List组件没有被移除

组件FilteredList的render方法

删除一个组件跟删除一个单一节点差不多,当我们删除一个相对于组件有引用的节点,框架会调用“componentWillUnmount”,然后安全的删除所有的DOM元素。当所有的元素从真实DOM移除,将会调用引用的组件的“componentDidUnmount”方法。

下面的图片显示在真实的DOM“ul”中,“List”组件的引用。

下面流程图的高亮部分展现了移除和卸载组件的过程

移除和卸载组件

引用

unmountComponent: https://github.com/developit/preact/blob/master/src/vdom/component.js#L250

最后一点:

我希望这篇博文能够给你关于虚拟DOM如何工作足够的启示(至少在Preact中)。

虽然这些覆盖了主要的场景,但是我还没讲到代码的优化。

如果你发现问题,通知我,我非常乐意更新!如果你想知道更多,也请告诉我!

就这样! 🙏🏼 👍

🎉🎉🎉 如果你喜欢这篇文章, please 1. 💚在Medium点喜欢 and 2. 在twitter上分享._🎉🎉🎉

你可以在这里找到我: https://twitter.com/rajaraodv