Virtual Dom && Diff原理,极简版

前言

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

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算法中会用到的函数,并串联它们之间的关系并不会给出具体实现的代码bash

介绍

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

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

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 的基本原理和流程,若是想更深刻的了解,能够在网上查阅相关资料。ui

相关文章
相关标签/搜索