Virtual Dom && Diff原理,极简版

Virtual Dom && Diff原理,极简版

前言

先介绍一个概念Virtual Dom,我猜大家或多或少都应该知道什么是Virtual Dom吧,简单来说就是用js来模拟DOM中的节点。

Virtual Dom

下面就是一个Virtual Dom的结构,包含了标签名,拥有的属性,孩子节点,render函数

class Element {
  constructor(tagName, attrs, children) {
    this.tagName  = tagName;
    this.attrs    = attrs || {};
    this.children = children || [];
  }
  render () {
    //这个函数是用来生成真实DOM的,最后会把return的结果添加到页面中去 
  }
}

创建一棵个Virtual Dom Tree && 渲染

/**
<ul id="list">
  <li class="a">txt_a</li>
  <li class="a">txt_b</li>
</ul>
**/
//根据上面结构可以用一下方式创建一棵 Virtual Dom Tree
let ul = Element('ul', { id: 'list' }, [
  Element('li', { class: 'a' }, ['txt_a']),
  Element('li', { class: 'b' }, ['txt_b'])
]);//ul 就是一棵个Virtual Dom Tree
let ulDom = ul.render();//生成真实Dom
document.body.appendChild(ulDom);//添加到页面中

以上就是Virtual Dom Tree如何被转换成真实Dom并添加到网页中的过程,再这个过程中我把render函数给省略,只是为了让你们先了解原理,具体实现可以以后再深究。我学一个东西的时候,习惯是先把整体原理弄清楚,再去深入学习相关的知识。

Diff 算法

在介绍Diff算法之前,再次声明我只会列举Diff算法中会用到的函数,并串联它们之间的关系并不会给出具体实现的代码

介绍

diff算法是进行虚拟节点Element的对比,并返回一个patchs对象,用来存储两个节点不同的地方,最后用patchs记录的消息去局部更新Dom。

两个树如果完全比较的话需要时间复杂度为O(n^3),如果对O(n^3)不太清楚的话建议去网上搜索资料。而在Diff算法中因为考虑效率的问题,只会对同层级元素比较,时间复杂度则为O(n),说白了就是深度遍历,并比较同层级的节点。

Diff只需下面两句代码

  • 判断两棵Virtual Dom Tree 差异
  • 把差异更新到真实Dom中去
let patchs = diff(oldTree, newTree);//获取两棵Virtual Dom Tree 差异
patch(ulDom, patchs);//找到对应的真实dom,进行部分渲染

Diff中所用到的函数

//深度遍历树,将需要做变更操作的取出来
//局部更新 DOM
function patch(node,patchs){
    //代码略
}

// diff 入口,比较新旧两棵树的差异
function diff (oldTree, newTree) {
  let index   = 0
  let patches = {} // 记录每个节点差异的补丁
  dfs(oldTree, newTree, index, patches)
  return patches;
}
/**
 * dfs 深度遍历查找节点差异
 * @param  oldNode - 旧虚拟Dom树
 * @param  newNode - 新虚拟Dom树
 * @param  index - 当前所在树的第几层
 * @param  patches - 记录节点差异
 */
function dfs (oldNode, newNode, index, patches){
    let currentPatch = [];//当前层的差异对比
    if (!newNode) {
        //如果节点不存不用处理,listdiff函数会处理被删除的节点
    }else if (isTxt(oldNode) && isTxt(newNode)) {//isTxt用来判断是否是文本,为了简便这边并没有声明
        if (newNode !== oldNode) 
            currentPatch.push({ type: "text", content: newNode })
        //如果发现文本不同,currentPatch会记录一个差异 
    }else if(oldNode.tagName === newNode.tagName && oldNode.key === newNode.key){
        //如果发现两个节点一样 则去判断节点是属性是否一样,并记录下来
        let attrsPatches = diffAttrs(oldNode, newNode)
        if (attrsPatches) {//有属性差异则把差异记录下来
          currentPatch.push({ type: "attrs", "attrs": attrsPatches })
        }
        // 递归遍历子节点,并对子节点进行diff比较
        diffChildren(oldNode.children, newNode.children, index, patches)
    }else{
        //最后一种情况是,两个节点完全不一样,这样只需要把旧节点之间替换就行
        //把当前差异记录下来
        currentPatch.push({ type: "replace", node: newNode})
    }

    //如果有差异则记录到当前层去
    if (currentPatch.length) {
        if (patches[index]) {
          patches[index] = patches[index].concat(currentPatch)
        } else {
          patches[index] = currentPatch
        }
     }
}
//判断两个节点的属性差异
function diffAttrs(oldNode, newNode){
    let attrsPatches = {};//记录差异
    let count = 0;//记录差异的条数

    /**
    代码略
    判断两个节点的属性差异的代码就略了,
    让你们知道这里的代码就是判断两个节点的属性有哪些差异,
    如果有差异就记录在attrsPatches这个对象中,并把它返回
    **/
    if(0 == count){
        return null;
    }else {
       return attrsPatches; 
    }
}
//判断孩子节点
function diffChildren(oldChild, newChild, index, patches){
    let { changes, list } = listDiff(oldChild, newChild, index, patches);
    if (changes.length) {//如果有差异则记录到当前层去
        if (patches[index]) {
          patches[index] = patches[index].concat(changes);
        } else {
          patches[index] = changes;
        }
    }
    // 代码略
    //遍历当前数组
    oldChild && oldChild.forEach((item, i) => {
        // 代码略
        let node;// 经过判断后node节点是同时存在于oldChild 和 newChild中
        //则对节点进行递归遍历 相当于 进入下一层 节点,
        let curIndex;
        dfs(item, node, curIndex, patches);
        // 代码略
    })

}

//判断oldNodeList, newNodeList 节点的位置差,主要是为了判断哪些节点被移动、删除、新增。
function listDiff(oldNodeList, newNodeList, index){
    let changes = [];//记录 oldNodeList, newNodeList节点的位置差异,是被移动、删除、新增
    let list = [];//记录 oldNodeList,newNodeList 同时存在的节点
    /**
    具体判断逻辑的代码就略了
    **/
    return {changes,list};
}

如果大家对函数之间的调用还不明白的话可以看下面的图



最后

Virtual Dom 算法的实现也就是以下三步

  • 通过 JS 来模拟生成 Virtual Dom Tree
  • 判断两个 Tree 的差异
  • 渲染差异

上面省略了很多代码,主要是为了让大家快速了解Dom diff 的基本原理和流程,如果想更深入的了解,可以在网上查阅相关资料。

编辑于 2019-04-20 01:53