本文转载自:众成翻译
译者:我是搬运工
链接: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的库,甚至对你写出类似的库也是有帮助的
在这篇博客中,我将会举一些简单示例,并且复习一下不同的小知识,给你一个关于它们到底如何工作的概念。特别地,我会复习:
- Babel 和 JSX
- 创建一个VNode - 一个简单的虚拟DOM元素
- 处理组件及子组件
- 初始化渲染并且创建一个DOM元素
- 重新渲染
- 移除DOM元素
- 替换DOM元素
关于这个demo:
这是一个简单过滤搜索应用, 仅包含有两个组件“FilteredList” 和 “List”。这个List组件渲染列表项(默认是“California” 和 “New York”)。这个应用有一个搜索的区域,可以根据字母来过滤列表项。非常的直观。
相关图片
大图
高级一点儿,我们用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: |
Option 2: |
“h” —通过Babel的配置 (点击放大)
挂载到真实DOM
组件的的render方法中的代码不仅被转换为“h”函数,而且开始挂载。
这是执行和一切的开始
//Mount to real DOM |
//Converted to "h": |
“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.
看起来像下面这样:
{ |
举个例子,我们的应用的Input表单的VNode像这样:
{ |
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