Virtual dom至关于框架API与运行环境之间的中间层,将框架渲染与DOM API进行解耦,增长了跨平台能力。(能够将virtual dom映射为DOM的步骤,改成映射到其余的执行环境,好比安卓、IOS)javascript
设计js数据结构来表示DOM节点:html
function VNode(tagName, props, children, key) { this.tagName = tagName this.props = props this.children = children this.key = key }
实现一个方法,可以从VNode生成真正的DOM tree(VNode对应根节点):vue
function render({tag, props, children, key}) { // 经过 tag 建立节点 let el = document.createElement(tag) // 设置节点属性 for (const key in props) { if (props.hasOwnProperty(key)) { const value = props[key] el.setAttribute(key, value) } } if (key) { el.setAttribute('key', key) } // 递归建立子节点 if (children) { children.forEach(element => { let child if (element instanceof VNode) { child = this._createElement( element.tag, element.props, element.children, element.key ) } else { child = document.createTextNode(element) } el.appendChild(child) }) } return el }
这个映射是由用户来定义的(通常经过template),因此这个方法通常是经过编译template来获得。java
第4步和第5步的2个函数能够合并。找出新vdom和DOM tree之间的差别,同时更新DOM tree来消除差别。git
首次渲染:github
状态更新:根据model渲染新的vdom tree,与旧的vdom tree对比,计算出须要的更新。算法
若是选择合并diff和patch算法,渲染出新vdom之后,将新的vdom tree与真实的DOM tree对比,同时更新DOM tree,使view(DOM)与新 vdom(data model)保持一致。segmentfault
diff的算法有不少种实现(见下面的参考资料),目的都是计算出须要的更新步骤,以便应用到真实的DOM上。
各类vdom tree diff算法之间的主要差别在于diff子节点列表的算法(也就是下面的listDiff)。把握各类listDiff算法的关键在于,数组
开始对2棵树同时进行深度优先遍历。这是特殊的深度优先遍历,每次同时访问2个节点用于对比:旧vdom的节点(如下称为oldNode)和新vdom的对应节点(如下称为newNode)。缓存
若是newNode.tag === oldNode.tag && newNode.key === oldNode.key(key能够都为undefined),将它们视为同一个元素在不一样时刻的状态。要分别diff它们的属性和子节点。
diffChildren(oldNode.children, newNode.children),检测子节点(数组)是否发生了变化,这些修改应该记录为patch。此外,diffChildren将会递归调用diffTree,来检测子树的变化。
要检测子节点数组的变化,即须要一个算法来找出:oldNode.children数组如何经过 增长、删除、移动 节点,变成newNode.children。咱们把这种算法称为listDiff:
检测删除的节点:遍历oldNode.children,对于每一个child,查找newNode.children中是否有相同key的节点(用map数据结构,查找的时间为log(n))。若是不存在,说明这个是被删除的节点,要输出删除操做,并记录它在中间状态数组中的下标。
检测增长和移动的节点:遍历newNode.children,对于每一个child,查找oldNode.children中是否存在相同key的节点。
若是存在,可是在newNode.children中的下标不等于在中间状态数组中的下标,说明这个是被移动的节点,要输出这个节点移动前和移动后的中间状态数组中的位置。
找到2个对应的子节点(一个在oldNode.children中,一个在newNode.children中,两个节点是同一个元素在不一样时刻的状态)来调用diffTree:
遍历oldNode.children,对于每个child,找出在newNode.children中有相同key的节点,这两个节点就是相互对应的节点。以这两个节点为参数调用diffTree。
diffTree的基本代码以下(先不考虑patch):
function diffTree(oldTreeRootNode, newTreeRootNode) { diffProps(oldTreeRootNode, newTreeRootNode); // 旧树和新树中对应的节点结对返回 const pairs = listDiff(oldTreeRootNode.children, newTreeRootNode.children); for (const [oldChild, newChild] of pairs) { diffTree(oldChild, newChild); } }
上面的讨论没有考虑没有key的节点。能够将【旧list的无key节点】与【新list的无key节点】按出现顺序一一对应,视为同一个节点。这个算法的实现能够参考参考资料1。这个实现仅用于理解中间状态diff算法的思想。
这个算法并非vue所使用的(见参考资料2)。这个算法仅用于理解diff的思想。
patch的意思是“如何修改旧的vdom,将它变为新的vdom”。它是diff vdom最重要的输出,毕竟咱们diff vdom的目的就是要知道如何修改DOM。
patch的操做包括:
增长、删除、移动某个节点的子节点。
可见,任何patch的操做都和某个节点相关,而且这个节点一定在旧vdom和新vdom中都存在的。
反证法:假设某个patch的操做(设为patchA)做用的节点不存在于旧vdom中,说明这个节点或它的某个祖先节点是新增的节点,也就是说,一定有一个“增长某个节点的子节点”的patch操做(设为patchB)做用于一个祖先节点。既然patchB意味着增长整个子树,那么patchA根本就没有存在的必要,由于它所在的整个子树在patchB的时候就被已经正确建立了。
由于每个patch操做都关联于一个已存在节点,因此咱们存储patch的方式是:为每一个旧vdom中的节点分配一个数组,这个数组包括了全部和这个节点有关的patch操做。所以最终的patches是一个二维数组。在第一个维度上,节点按照深度优先遍历的顺序排列,也就是第1行是根节点的patch操做,第2行是左子节点的patch操做,第3行是【左子节点的左子节点】的patch操做,以此类推。
获得了patches之后,经过将patches中的操做应用于对应的DOM节点,咱们就能够更新DOM树,使得DOM树等价于新的vdom树。
更新的方法就是,对dom进行深度优先遍历,当前元素深度优先遍历的序号是n,那么patches[n]就是这个元素关联的patch操做,而后将这些操做依次应用于这个元素。
若是有一个子节点是被增长的,那么这个子节点下面的子树就能够被跳过了,由于这是按照新vdom建立的子树,不须要更新。
对patch的处理上, 参考资料5实现得比较清晰。