如何理解java虚拟机虚拟DOM

REACT(18)
作者:戴嘉华
链接:/question//answer/
来源:知乎
著作权归作者所有,转载请联系作者获得授权。
写一个就知道了。刚好写了一篇博客刚好可以回答这个问题,copy过来:
1 前言2 对前端应用状态管理思考3 Virtual DOM 算法4 算法实现
4.1 步骤一:用JS对象模拟DOM树4.2 步骤二:比较两棵虚拟DOM树的差异4.3 步骤三:把差异应用到真正的DOM树上
5 结语6 References
本文会在教你怎么用 300~400 行代码实现一个基本的 Virtual DOM 算法,并且尝试尽量把 Virtual DOM 的算法思路阐述清楚。希望在阅读本文后,能让你深入理解 Virtual DOM 算法,给你现有前端的编程提供一些新的思考。
本文所实现的完整代码存放在 。
2 对前端应用状态管理的思考
假如现在你需要写一个像下面一样的表格的应用程序,这个表格可以根据不同的字段进行升序或者降序的展示。
&img src=&/0c1ee6d9a303d5a43b0fa4b3fc82f3a2_b.png& data-rawwidth=&899& data-rawheight=&375& class=&origin_image zh-lightbox-thumb& width=&899& data-original=&/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=&/d5cda33e28d83baf9e35b_b.png& data-rawwidth=&1239& data-rawheight=&336& class=&origin_image zh-lightbox-thumb& width=&1239& data-original=&/d5cda33e28d83baf9e35b_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&
既然原来 DOM 树的信息都可以用 JavaScript 对象来表示,反过来,你就可以根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树。
之前的章节所说的,状态变更-&重新渲染整个视图的方式可以稍微修改一下:用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对象结构。当然这样做其实没什么卵用,因为真正的页面其实没有改变。
但是可以用新渲染的对象树去和旧的树进行对比,记录这两棵树差异。记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。这样就可以做到:视图的结构确实是整个全新渲染了,但是最后操作DOM的时候确实只变更有不同的地方。
这就是所谓的 Virtual DOM 算法。包括几个步骤:
用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异把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)
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&
完整代码可见 。
4.2 步骤二:比较两棵虚拟DOM树的差异
正如你所预料的,比较两棵DOM树的差异是 Virtual DOM 算法最核心的部分,这也是所谓的 Virtual DOM 的 diff 算法。两个树的完全的 diff 算法是一个时间复杂度为 O(n^3) 的问题。但是在前端当中,你很少会跨越层级地移动DOM元素。所以 Virtual DOM 只会对同一个层级的元素进行对比:
上面的div只会和同一层级的div对比,第二层级的只会跟第二层级对比。这样算法复杂度就可以达到 O(n)。
4.2.1 深度优先遍历,记录差异
在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:
在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。
// diff 函数,对比两棵树
function diff (oldTree, newTree) {
var index = 0 // 当前节点的标志
var patches = {} // 用来记录每个节点差异的对象
dfsWalk(oldTree, newTree, index, patches)
return patches
// 对两棵树进行深度优先遍历
function dfsWalk (oldNode, newNode, index, patches) {
// 对比oldNode和newNode的不同,记录下来
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 操作可能会:
替换掉原来的节点,例如把上面的div换成了section移动、删除、新增子节点,例如上面div的子节点,把p和ul顺序互换修改了节点的属性对于文本节点,文本内容可能会改变。例如修改上面的文本节点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,
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
现在知道了新旧的顺序,求最小的插入、删除操作(移动可以看成是删除和插入操作的结合)。这个问题抽象出来其实是字符串的最小编辑距离问题(),最常见的解决算法是,通过动态规划求解,时间复杂度为 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
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)
case REORDER:
reorderChildren(node, currentPatch.moves)
case PROPS:
setProps(node, currentPatch.props)
case TEXT:
node.textContent = currentPatch.content
throw new Error('Unknown patch type ' + currentPatch.type)
完整代码可见 。
Virtual DOM 算法主要是实现上面步骤的三个函数:,,。然后就可以实际的进行使用:
// 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了。
本文所实现的完整代码存放在 ,仅供学习。
6 References
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:6902734次
积分:60725
积分:60725
排名:第37名
原创:248篇
转载:2607篇
评论:653条
(22)(92)(17)(25)(37)(63)(7)(74)(67)(95)(177)(114)(86)(40)(43)(71)(14)(10)(17)(12)(6)(20)(27)(54)(71)(97)(74)(32)(2)(24)(21)(62)(60)(36)(23)(27)(46)(34)(76)(63)(121)(141)(74)(54)(120)(77)(42)(4)(12)(19)(1)(9)(15)(19)(18)(16)(31)(79)(68)虚拟 DOM 内部是如何工作的?
编者按:本文由我是搬运工在众成翻译平台上翻译。
(图片来自网络)
流程图展现VDOM在Preact中如何工作
虚拟DOM (VDOM,也叫 VNode)非常神奇 ? 但也非常复杂和难以理解??。 React,Preact和一些类似的JS库都在核心代码中使用了虚拟DOM。不幸的是我发现没有一篇好的文章或者文档简洁明了地来介绍它。 因此我决定自己写一篇.
注意:这篇文章很长。我已经添加尽可能多的图片来使其理解更简单一些,但是这样一来,文章就显得更长了。
我用的是 Preact的代码 和 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(JS中的html)写了组件,通过 babel的命令行工具将其转换为原生的 JS。然后 Preact 的 “h” (hyper)函数将它转换为虚拟 DOM 树(也称为 VNode)。最后 Preact 的虚拟 DOM 算法根据虚拟 DOM 创建一个真实的 DOM,用以构成我们的应用。
在我们深入理解VDOM的生命周期之前,让我们理解下 JSX,它为库提供了基础。
1. Babel 和 JSX
在React,Preact 这样的库中,没有 HTML,一切皆 Java。因此我们需要用 Java 来写 HTML。但是用原生 JS 写 DOM 是一种噩梦。 ??
对于我们的应用,我们必须像下面这样书写 HTML:
注意: 等会儿我会介绍 “h”
这时候就轮到 JSX 上场了。JSX 本质上允许我们在 Java 中书写HTML!并且允许我们在 HTML 的 {} 号中使用 JS 的语法。
JSX帮助我们像下面这样轻松地书写组件:
将 JSX 树转换为 Java
JSX 很酷,但它不是合法的 JS,最终我们还是需要真实的 DOM。JSX 只能帮助我们书写对真实 DOM 的描述。除此之外,它毫无用处。
因此我们需要一种方法将 JSX 转换为正确的 JSON 对象(即VDOM, 也是一个“树”形结构),我们需要将 JSX 作为创建真实DOM的基础。我们用一个函数来做这样的事情。
在 Preact 中这个函数就是 “h” 函数。它的作用和 React 中的React.的作用是一样的。
“h”是指 hyper- 一种通过 JS 来创建 HTML 的库。
但是怎样将 JSX 转换为 “h” 函数调用呢?这时就需要 Babel了。Babel 只需要遍历 JSX 的节点,然后将它们转换为 “h” 函数式的调用。
Babel JSX (React Vs Preact)
由于Babel默认针对React,所以Babel 会将 JSX 转换为 React. 函数调用。
左边: JSX。右边: React 的JS版本我们可以像下面这样增加Babel的Pragma配置,就能轻松修改要转换的函数名(比如Preact的“h”函数):
//.babelrc
{"plugins":[
["transform-react-jsx",{"pragma":"h"}]
//Add the below comment as the 1st line in every JSX file
/** @jsx h */
“h” —通过 Babel 的 Pragma 配置
挂载到真实DOM
不光组件里的 render 方法中的代码会转换为 “h” 函数,开始挂载里的render方法中的代码也会被转换成“h”函数。
这是应用执行的开始,也是一切的开始
//挂在到真实DOM
render(&FilteredList/&,document.getElementById(‘app’));
//转换成 "h":
render(**h(FilteredList)**,document.getElementById(‘app’));
“h” 函数的输出
“h” 函数会根据 JSX 的输出,创建一个 “VNode”(React 的 “” 函数会创建 ReactElement)。一个 Preact 的 “VNode”(或者 React 的 “Element”)就是一个JS对象,用以表示单个DOM 节点,并且包含了该节点的属性和子元素。
这个对象看起来像下面这样:
"nodeName":"",
"attributes":{},
"children":[]
举个例子,我们的应用的Input表单的VNode像这样:
"nodeName":"input",
"attributes":{
"type":"text",
"placeholder":"Search",
"onChange":""
"children":[]
注意“h”函数不会创建完整的DOM树!它仅仅对给定的 node 创建了一个JS对象。但是由于 “render” 方法已经有了树形的 DOM JSX 语法。因此最后的结果将会是一个看起来像树的,带有子孙元素的 VNode。
/developit/preact/blob/master/src/h.js
/developit/preact/blob/master/src/vnode.js
“render”:/developit/preact/blob/master/src/render.js
“buildComponentFromVNode”:/developit/preact/blob/master/src/vdom/diff.js#L102
好了,让我们看下虚拟 DOM 如何工作?
Preact 虚拟 DOM 的算法流程图
下面的流程图展现了组件和子组件是如何被Preact创建,更新,删除的。也展现了什么时候会调用生命周期事件,比如“componentWillMount”。
注意: 我们会一步一步讲解每一部分,如果你觉得复杂,不用担心。
没错,很难一下子理解所有的知识。让我们通过一步一步地探索不同的情景,来理清流程图的不同部分。
注意: 当讨论到关键的生命周期的部分我将会用黄色高亮。
情景 1: APP创建初始化 1.1
为指定组件创建一个 VNode(虚拟DOM)
黄色高亮区域展示了为一个给定的组件创建虚拟 DOM 树的初始化循环过程。注意没有为子组件创建虚拟 DOM (这是个不同的循环)
黄色区域展示了虚拟DOM的创建
下面这张图片展示了当应用第一次加载的时候发生了什么。这个库最终为主要组件 “FilteredList” 创建了一个带有子元素和属性的VNode。
注意: 在此过程中它还调用了生命周期方法 “componentWillMount” 和 “render”。(看上面图片绿色的部分)
这个时候,我们有了个 “div” 的父元素,它包含了子节点 “input” 和 “list”。
大多数的生命周期事件,像 componentWillMount,render 等等:/developit/preact/blob/master/src/vdom/component.js
1.2 如果不是一个组件,创建一个真实的 DOM
这一步,它仅会对父元素div创建一个真实的DOM,并且对子节点(“input” 和 “List”)重复这一步骤。
黄色的循环部分展现了子组件的创建。
在这一步,如下面的图片所示,仅有 “div” 被创建出来了。
document.: /developit/preact/blob/master/src/dom/recycler.js
1.3 对所有的子元素重复这一步
在这一步,将会对所有的子元素重复这一循环。在我们的应用中,会对“input” 和 “List” 重复。
循环处理每一个子元素
1.4 处理子元素,并且把它加到父元素上.
在这一步我们将会处理叶子节点。既然“input”有一个父元素“div”,我们将会把input作为一个子元素加到div中。然后停止,返回创建“List”(“div”的第二个子元素)。
结束处理叶子节点
这一步,我们的应用看起来像下面这样:
注意: “input”被创建后,由于它没有子元素,不会立即循环和创建“List”!而是会先将“input”加入到父元素“div”中,然后再返回处理“List”。
: /developit/preact/blob/master/src/vdom/diff.js
1.5 处理子组件(们)
控制流程回到1.1,对“List”组件重复之前的步骤。但是“List”是一个组件,它调用“List”组件的render方法,得到一组新的VNode,像下面这样
对一个子组件重复所有的操作
对List组件重复操作之后,返回的VNode像下面这样:
“buildComponentFromVNode:/developit/preact/blob/master/src/vdom/diff.js#L102
1.6 对所有子节点重复1.1到1.4步骤
它会再一次对每一个节点重复上面的步骤。一旦它到达叶子节点,就会把它加入到节点的父节点,并且重复这个过程。
重复这一步骤,直到所有的父子节点被创建和添加。
下面的图片展示了每个节点的添加(提示: 深度优先)
真实的DOM树如何被虚拟DOM算法创建的。
1.7 结束处理
在这一步,结束处理。它仅对所有的组件调用“componentDidMount”(按照从子组件到父组件的顺序)然后停止。
重要提示:一旦所有步骤执行完毕,会给每个组件实例添加一个真实DOM的引用。这个引用将用于在持续更新(创建,更新,删除)中进行比较,以避免重复创建同样的DOM节点。
情景 2: 删除叶子节点
当我们输入“cal” 关键字,点击确认。将会移除掉第二个list节点,即叶子节点(New York),同时保留所有别的父节点。
让我们看下这个场景的执行过程。
2.1 像之前那样创建VNodes.
在初始化渲染之后,未来的每一个变化都是一个更新。当需要创建VNodes时,更新周期跟创建周期非常相似,并且会再一次创建所有的VNodes。
不过,因为是一个组件更新(不是创建),所以它会调用每个组件和子组件的“componentWillReceiveProps”, “shouldComponentUpdate”, 和 “componentWillUpdate”
另外, 如果元素已经存在,更新循环不会重复创建这些真实DOM。
更新组件的生命周期
引用代码:
removeNode:/developit/preact/blob/master/src/dom/index.js#L9
insertBefore:/developit/preact/blob/master/src/vdom/diff.js#L253
2.2 使用真实DOM节点的引用,避免创建重复的nodes
之前提到过,每个组件都有一个引用,对应地指向初始化加载时所创建的真实DOM树。下面这张图片展现了现在我们应用中的引用。
显示每一个组件和之前的DOM之间的引用
当虚拟DOM被创建,每个虚拟DOM的属性都会跟真实DOM的属性进行比较。如果真实DOM存在,循环处理将会继续处理下一个节点。
真实DOM已经存在(在更新期间)
引用代码:
innerDiffNode: /developit/preact/blob/master/src/vdom/diff.js#L185
2.3 如果在真实的DOM中存在额外的节点,移除他们
下面的图片展现了真实DOM和虚拟DOM的差异
(click to zoom)
由于这里存在差异,所以在真实节点中的“New York”节点会被算法移除,如下面的流程图所示。当所有工作进行完毕算法也会调用“componentDidUpdate”。
移除DOM节点生命周期
情景 3 — 卸载整个组件
让我们看看在filter组件中输入blabla,既然没有匹配到“California” 和 “New York”, 我们不会渲染子组件“List”,这意味着我们需要卸载整个组件。
如果没有结果的话List组件没有被移除
组件FilteredList的render方法
删除一个组件跟删除单个节点差不多。只有一点不同,当我们删除一个相对于组件有引用的节点,框架会调用“componentWillUnmount”,然后递归删除所有的DOM元素。当所有的元素从真实DOM移除,将会调用引用的组件的“componentDidUnmount”方法。
下面的图片显示在真实的DOM“ul”中,对“List”组件的引用。
下面流程图的高亮部分展现了移除和卸载组件的过程
移除和卸载组件
unmountComponent: /developit/preact/blob/master/src/vdom/component.js#L250
我希望这篇博文足以让你理解虚拟DOM是如何工作的(至少在Preact中)。
虽然这些覆盖了主要的场景,但是我还没讲到代码的优化。
如果你发现问题,通知我,我非常乐意更新!如果你想知道更多,也请告诉我!
就这样! ???? ??
——————————————————
责任编辑:
声明:本文由入驻搜狐号的作者撰写,除搜狐官方账号外,观点仅代表作者本人,不代表搜狐立场。
今日搜狐热点

我要回帖

更多关于 深入理解java虚拟机 的文章

 

随机推荐