做者:deathmood
译者:前端小智
来源:medium
为了保证的可读性,本文采用意译而非直译。node
要构建本身的虚拟DOM,须要知道两件事。你甚至不须要深刻 React 的源代码或者深刻任何其余虚拟DOM实现的源代码,由于它们是如此庞大和复杂——但实际上,虚拟DOM的主要部分只需不到50行代码。react
有两个概念:git
首先,咱们须要以某种方式将 DOM 树存储在内存中。可使用普通的 JS 对象来作。假设咱们有这样一棵树:github
<ul class=”list”> <li>item 1</li> <li>item 2</li> </ul>
看起来很简单,对吧? 如何用JS对象来表示呢?面试
{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [ { type: ‘li’, props: {}, children: [‘item 1’] }, { type: ‘li’, props: {}, children: [‘item 2’] } ] }
这里有两件事须要注意:算法
{ type: ‘…’, props: { … }, children: [ … ] }
可是用这种方式表示内容不少的 Dom 树是至关困难的。这里来写一个辅助函数,这样更容易理解:segmentfault
function h(type, props, …children) { return { type, props, children }; }
用这个方法从新整理一开始代码:数组
h(‘ul’, { ‘class’: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’), );
这样看起来简洁多了,还能够更进一步。这里使用 JSX,以下:服务器
<ul className=”list”> <li>item 1</li> <li>item 2</li> </ul>
编译成:
React.createElement(‘ul’, { className: ‘list’ }, React.createElement(‘li’, {}, ‘item 1’), React.createElement(‘li’, {}, ‘item 2’), );
是否是看起来有点熟悉?若是可以用咱们刚定义的 h(...)
函数代替 React.createElement(…)
,那么咱们也能使用JSX 语法。其实,只须要在源文件头部加上这么一句注释:
/** @jsx h */ <ul className=”list”> <li>item 1</li> <li>item 2</li> </ul>
它实际上告诉 Babel ' 嘿,小老弟帮我编译 JSX 语法,用 h(...)
函数代替 React.createElement(…)
,而后 Babel 就开始编译。'
综上所述,咱们将DOM写成这样:
/** @jsx h */ const a = ( <ul className=”list”> <li>item 1</li> <li>item 2</li> </ul> );
Babel 会帮咱们编译成这样的代码:
const a = ( h(‘ul’, { className: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’), ); );
当函数 “h”
执行时,它将返回普通JS对象-即咱们的虚拟DOM:
const a = ( { type: ‘ul’, props: { className: ‘list’ }, children: [ { type: ‘li’, props: {}, children: [‘item 1’] }, { type: ‘li’, props: {}, children: [‘item 2’] } ] } );
好了,如今咱们有了 DOM 树,用普通的 JS 对象表示,还有咱们本身的结构。这很酷,但咱们须要从它建立一个真正的DOM。
首先让咱们作一些假设并声明一些术语:
$
'开头的变量表示真正的DOM节点(元素,文本节点),所以 $parent 将会是一个真实的DOM元素node
的变量表示* 就像在 React 中同样,只能有一个根节点——全部其余节点都在其中
那么,来编写一个函数 createElement(…)
,它将获取一个虚拟 DOM 节点并返回一个真实的 DOM 节点。这里先不考虑 props
和 children
属性:
function createElement(node) { if (typeof node === ‘string’) { return document.createTextNode(node); } return document.createElement(node.type); }
上述方法我也能够建立有两种节点分别是文本节点和 Dom 元素节点,它们是类型为的 JS 对象:
{ type: ‘…’, props: { … }, children: [ … ] }
所以,能够在函数 createElement
传入虚拟文本节点和虚拟元素节点——这是可行的。
如今让咱们考虑子节点——它们中的每个都是文本节点或元素。因此它们也能够用 createElement(…) 函数建立。是的,这就像递归同样,因此咱们能够为每一个元素的子元素调用 createElement(…),而后使用 appendChild()
添加到咱们的元素中:
function createElement(node) { if (typeof node === ‘string’) { return document.createTextNode(node); } const $el = document.createElement(node.type); node.children .map(createElement) .forEach($el.appendChild.bind($el)); return $el; }
哇,看起来不错。先把节点 props
属性放到一边。待会再谈。咱们不须要它们来理解虚拟DOM的基本概念,由于它们会增长复杂性。
完整代码以下:
/** @jsx h */ function h(type, props, ...children) { return { type, props, children }; } function createElement(node) { if (typeof node === 'string') { return document.createTextNode(node); } const $el = document.createElement(node.type); node.children .map(createElement) .forEach($el.appendChild.bind($el)); return $el; } const a = ( <ul class="list"> <li>item 1</li> <li>item 2</li> </ul> ); const $root = document.getElementById('root'); $root.appendChild(createElement(a));
如今咱们能够将虚拟 DOM 转换为真实的 DOM,这就须要考虑比较两棵 DOM 树的差别。基本的,咱们须要一个算法来比较新的树和旧的树,它可以让咱们知道什么地方改变了,而后相应的去改变真实的 DOM。
怎么比较 DOM 树?须要处理下面的状况:
若是节点相同的——就须要须要深度比较子节点
编写一个名为 updateElement(…) 的函数,它接受三个参数—— $parent
、newNode 和 oldNode,其中 $parent 是虚拟节点的一个实际 DOM 元素的父元素。如今来看看如何处理上面描述的全部状况。
function updateElement($parent, newNode, oldNode) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } }
这里遇到了一个问题——若是在新虚拟树的当前位置没有节点——咱们应该从实际的 DOM 中删除它—— 这要如何作呢?
若是咱们已知父元素(经过参数传递),咱们就能调用 $parent.removeChild(…)
方法把变化映射到真实的 DOM 上。但前提是咱们得知道咱们的节点在父元素上的索引,咱们才能经过 $parent.childNodes[index] 获得该节点的引用。
好的,让咱们假设这个索引将被传递给 updateElement 函数(它确实会被传递——稍后将看到)。代码以下:
function updateElement($parent, newNode, oldNode, index = 0) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } else if (!newNode) { $parent.removeChild( $parent.childNodes[index] ); } }
首先,须要编写一个函数来比较两个节点(旧节点和新节点),并告诉节点是否真的发生了变化。还有须要考虑这个节点能够是元素或是文本节点:
function changed(node1, node2) { return typeof node1 !== typeof node2 || typeof node1 === ‘string’ && node1 !== node2 || node1.type !== node2.type }
如今,当前的节点有了 index 属性,就能够很简单的用新节点替换它:
function updateElement($parent, newNode, oldNode, index = 0) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } else if (!newNode) { $parent.removeChild( $parent.childNodes[index] ); } else if (changed(newNode, oldNode)) { $parent.replaceChild( createElement(newNode), $parent.childNodes[index] ); } }
最后,但并不是最不重要的是——咱们应该遍历这两个节点的每个子节点并比较它们——实际上为每一个节点调用updateElement(…)方法,一样须要用到递归。
undefined
也没有关系,咱们的函数也会正确处理它。function updateElement($parent, newNode, oldNode, index = 0) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } else if (!newNode) { $parent.removeChild( $parent.childNodes[index] ); } else if (changed(newNode, oldNode)) { $parent.replaceChild( createElement(newNode), $parent.childNodes[index] ); } else if (newNode.type) { const newLength = newNode.children.length; const oldLength = oldNode.children.length; for (let i = 0; i < newLength || i < oldLength; i++) { updateElement( $parent.childNodes[index], newNode.children[i], oldNode.children[i], i ); } } }
Babel+JSX
/* @jsx h /
function h(type, props, ...children) { return { type, props, children }; } function createElement(node) { if (typeof node === 'string') { return document.createTextNode(node); } const $el = document.createElement(node.type); node.children .map(createElement) .forEach($el.appendChild.bind($el)); return $el; } function changed(node1, node2) { return typeof node1 !== typeof node2 || typeof node1 === 'string' && node1 !== node2 || node1.type !== node2.type } function updateElement($parent, newNode, oldNode, index = 0) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } else if (!newNode) { $parent.removeChild( $parent.childNodes[index] ); } else if (changed(newNode, oldNode)) { $parent.replaceChild( createElement(newNode), $parent.childNodes[index] ); } else if (newNode.type) { const newLength = newNode.children.length; const oldLength = oldNode.children.length; for (let i = 0; i < newLength || i < oldLength; i++) { updateElement( $parent.childNodes[index], newNode.children[i], oldNode.children[i], i ); } } } // --------------------------------------------------------------------- const a = ( <ul> <li>item 1</li> <li>item 2</li> </ul> ); const b = ( <ul> <li>item 1</li> <li>hello!</li> </ul> ); const $root = document.getElementById('root'); const $reload = document.getElementById('reload'); updateElement($root, a); $reload.addEventListener('click', () => { updateElement($root, b, a); });
HTML
<button id="reload">RELOAD</button> <div id="root"></div>
CSS
#root { border: 1px solid black; padding: 10px; margin: 30px 0 0 0; }
打开开发者工具,并观察当按下“Reload”按钮时应用的更改。
如今咱们已经编写了虚拟 DOM 实现及了解它的工做原理。做者但愿,在阅读了本文以后,对理解虚拟 DOM 如何工做的基本概念以及在幕后如何进行响应有必定的了解。
然而,这里有一些东西没有突出显示(将在之后的文章中介绍它们):
原文:
https://medium.com/@deathmood...
代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug。
干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。
https://github.com/qq44924588...
我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,便可看到福利,你懂的。