一步一步带你实现virtual dom(一)
一步一步带你实现virtual dom(二)--Props和事件html
要写你本身的虚拟DOM,有两件事你必须知道。你甚至都不用翻看React的源代码,或者其余的基于虚拟DOM的代码。他们代码量都太大,太复杂。然而要实现一个虚拟DOM的主要部分只须要大约50行的代码。50行代码!!node
下面就是那两个你要知道的事情:react
下面咱们就来看看这两条是如何实现的。web
首先咱们须要在内存里存储咱们的DOM树。只要使用js就能够达到这个目的。假设咱们有这样的一个树:算法
<ul class="list"> <li>item 1</li> <li>item 2</li> </ur>
看起来很是简单对吧。咱们怎么用js的对象来对应到这个树呢?babel
{ type: 'ul', props: {'class': 'list}, children: [ {type: 'li', props: {}, children: ['item 1']}, {type: 'li', props: {}, children: ['item 2']} ]}
这里咱们会注意到两件事:app
{type: '...', props: {...}, children: [...]}
。function h(type, props, ...children) { return {type, props, children}; }
如今咱们能够这样生成一个虚拟DOM树:dom
h('ul', {'class': 'list'}, h('li', {}, 'item 1'), h('li', {}, 'item 2'), )
这样看起来就清晰了不少。可是咱们还能够作的更好。你应该据说过JSX对吧。是的,咱们也要用那种方式。可是,这个应该如何下手呢?spa
若是你读过Babel的JSX文档的话,你就会知道这些都是Babel的功劳。Babel会把下面的代码转码:.net
<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') );
你注意到多类似了吗?若是把React.createElement(...)
体换成咱们本身的h
方法的话,那咱们也已使用相似于JSX的语法。咱们只须要在咱们的文件最顶端加这么一句话:
/** @jsx h */ <ul className="list"> <li>item 1</li> <li>item 2</li> </ul>
这一行/** @jsx h */
就是在告诉Babel“大兄弟,按照jsx的方式转码,可是不要用React.createElement
, 使用h
。你可使用任意的东西来代替h。
那么把上面咱们说的总结一下,咱们会这样写咱们的虚拟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。咱们来写一个方法:createElement()
,这个方法能够接收一个虚拟节点以后返回一个真实的DOM节点。先不考虑props
和children
,这个以后会有介绍。
function createElement(node) { if(typeof node === 'string') { return document.createTextNode(node); } return document.createElement(node.type); }
由于咱们不只须要处理文本节点(js的字符串),还要处理各类元素(element)。这些元素都是想js的对象同样的:
{ type: '-', props: {...}, children: [...]}
咱们能够用这个结构来处理文本节点和各类element了。
那么子节点如何处理呢,他们也基本是文本节点或者各类元素。这些子节点也能够用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了。如今该考虑比较咱们的虚拟树了。基本上咱们须要写一点算法了。虚拟树的比较须要用到这个算法,比较以后只作必要的修改。
如何比较树的不一样?
appendChild
方法来添加。//new <ul> <li>item 1</li> <li>item 2</li> </ul> //old <ul> <li>item 1</li> </ul>
removeChild
方法来删除掉多余的子节点。//new <ul> <li>item 1</li> </ul> //old <ul> <li>item 1</li> <li>item 2</li> // 这个要被删掉 </ul>
replaceChild
方法。//new <div> <p>hi there!</p> <p>hello</p> </div> //old <div> <p>hi there!</p> <button>click it</button> //发生了修改,变成了new里的<p />节点 </div>
//new <ul> <li>item 1</li> <li> //* <span>hello</span> <span>hi!</span> </li> </ul> //old <ul> <li>item 1</li> <li> //* <span>hello</span> <div>hi!</div> </li> </ul>
加醒的两个节点能够看到都是<li>
,是相等的。可是它的子节点里面却有不一样的节点。
咱们来写一个方法updateElement
,它接收三个参数:$parent
、newNode
和oldNode
。$parent
是真的DOM元素。它是咱们虚拟节点的父节点。如今咱们来看看如何处理上面提到的所有问题。
这个问题很简单:
function updateElement($parent, newNode, oldNode) { if(!oldNode) { $parent.appendChild( createElement(newNode) ); } }
若是当前没有新的虚拟节点,咱们就应该把它从真的DOM里删除掉。可是,如何作到呢?咱们知道父节点(做为参数传入了方法),那么咱们就能够调用$parent.removeChild
方法,并传入真DOM的引用。可是咱们没法获得它,若是咱们知道的节点在父节点的位置,就能够用$parent.childNodes[index]
来获取它的引用。index
就是节点的位置。
假设index
也做为参数传入了咱们的方法,咱们的方法就能够这么写:
function updateElement($parent, newNode, oldNode, index = 0) { if(!oldNode) { $parent.appendChild( createElement(newNode); ); } else if(!newNode) { $parent.removeChild( $parent.childNodes[index]; ); } }
首先写一个方法来比较两个节点(新的和旧的)来区分节点是否发生了改变。要记住,节点能够是文本节点,也能够是元素(element):
function changed(node1, node2) { return typeof node1 !== typeof node2 || typeof node1 === 'string' && node1 !== node2 || node1.type !== node2.type; }
如今有了当前节点的index
了,index就是当前节点在父节点的位置。这样能够很容易用新建立的节点来代替当前节点了。
function updateElement($parent, newNode, oldNode, index = 0) { if(!oldNode) { $parent.appendChild( createElement(newNode); ); } else if(!newNode) { $parent.removeChild( $parent.childNOdes[index]; ); } else if(chianged(newNode, oldNode)) { $parent.replaceChild( createElement(newNode), $parent.childNodes[index] ); } }
最后,须要遍历新旧节点的子节点,并比较他们。能够在每一个节点上都使用updateElement
方法。是的,递归。
可是在开始代码以前须要考虑一些问题:
undefined
的状况。没有关系,咱们的方法能够处理。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] ); } 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 ); } } }
在JSFiddle里看看代码把!
祝贺你!咱们搞定了。咱们写出了虚拟节点的实现。从上面的例子中你已经能够理解虚拟节点的概念了,也大致能够知道React是如何运做的了。
当时还有不少须要讲述的内容,其中包括:
原文地址:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060