Vue源码探秘之 虚拟DOM和diff算法

Vue源码探秘之 虚拟DOM和diff算法

 

扪心自问 你到底懂不懂虚拟 DOM diff 算法??
 
Vue源码探秘之 虚拟DOM和diff算法_第1张图片

 

先简单介绍一下虚拟 DOM diff 算法
 

一、虚拟dom是什么

1.它是一个Object对象模型,用来模拟真实dom节点的结构
(虚拟dom其实是里面内存型对象(js内存对象) 属于内存数据 真实dom的一层映射)
2.提供一种方便的工具,使得开发效率得到保证
3.保证最小化的DOM操作,使得执行效率得到保证

二、虚拟dom的使用基本流程(前四步骤)

​ 1.获取数据
​ 2.创建vdom
​ 3.将vdom渲染成真实dom
​ 4.数据更改了
5.使用diff算法比对两次vdom,将之前的虚拟dom树结合新的数据生成一颗新的虚拟dom树
​ 6.根据key将patch对象渲染到页面中改变的结构上,而其他没有改变的地方是不做任何修改的( 虚拟dom的惰性原则 )

三、diff算法是什么

​ 用来做比对两次vdom结构

注意:vue是一个mvvm框架,Vue高性能的原因之一就是vdom

Vue源码探秘之 虚拟DOM和diff算法_第2张图片

Vue源码探秘之 虚拟DOM和diff算法_第3张图片

Vue源码探秘之 虚拟DOM和diff算法_第4张图片

Vue源码探秘之 虚拟DOM和diff算法_第5张图片

Vue源码探秘之 虚拟DOM和diff算法_第6张图片

新虚拟 DOM 和老虚拟 DOM 进行 diff (精细化比较),算出应该如何最小量更新,最后反映到真正的 DOM 上。

Vue源码探秘之 虚拟DOM和diff算法_第7张图片

snabbdom 是瑞典语单词,单词原意“速度”;
snabbdom 是著名的虚拟 DOM 库,是 diff 算法的鼻祖, Vue 源码借鉴了 snabbdom
官方 git https://github.com/snabbdom/snabbdom
 
Vue源码探秘之 虚拟DOM和diff算法_第8张图片
 
 
虚拟 DOM :用 JavaScript 对象 描述DOM 的层次结构。 DOM 中的一切属性都在虚拟DOM 中 有对应的属性。
新虚拟 DOM 和老虚拟 DOM 进行 diff (精细化比较),算出应该如何最小量更新,最后反映到真正的 DOM 上。
 
h 函数用来产生虚拟节点
比如这样调用h函数
h('a', { props: { href: 'http://www.baidu.com' }}, '百度');
将得到这样的虚拟节点:
{ 
  "sel": "a", 
  "data": { props: { href: 'http://www.baidu.com' } },
  "text": "百度" 
}
它表示的真正的 DOM 节点:
百度
一个虚拟节点有哪些属性?
{
    children: undefined
    data: {}
    elm: undefined
    key: undefined
    sel: "div"
    text: "我是一个盒子" 
}
h 函数可以嵌套使用,从而得到虚拟 DOM 树(重要)
比如这样嵌套使用 h 函数:
h('ul', {}, [ 
   h('li', {}, '牛奶'),
   h('li', {}, '咖啡'),
   h('li', {}, '可乐')
]);
将得到这样的虚拟 DOM 树:
{
"sel": "ul",
"data": {},
"children":
 [ 
  { "sel": "li", "text": "牛奶" },
  { "sel": "li", "text": "咖啡" },
  { "sel": "li", "text": "可乐" } 
 ] 
}

 感受diff算法的心得

最小量更新太厉害啦!真的是最小量更新! 当然, key 很重要。 key 是这个节点的 唯一标识,告诉 diff 算法,在更改前后它们是同一个 DOM 节点。
 
只有是同一个虚拟节点,才进行精细化比较 ,否则就是暴力删除旧的、插入新的。 延伸问题:如何定义是同一个虚拟节点?答:选择器相同且key 相同。
 
只进行同层比较,不会进行跨层比较。 即使是同一片虚拟节点,但是跨层了,对 不起,精细化比较不diff 你,而是暴力删除旧的、然后插入新的。
Vue源码探秘之 虚拟DOM和diff算法_第9张图片
 
diff 并不是那么的“无微不至”啊!真的影响效率么??
 
答:上面 2 3 操作在实际 Vue 开发中,基本不会遇见,所以这是合理的优化 机制。
 
diff处理新旧节点不是同一个节点时
 
Vue源码探秘之 虚拟DOM和diff算法_第10张图片
 

如何定义“同一个节点”这个事儿

 
旧节点的 key 要和新节点的 key 相同且 旧节点的选择器要和新节点的选择器相同
 

 

创建节点时,所有子节点需要递归创建的
 
Vue源码探秘之 虚拟DOM和diff算法_第11张图片
 
 
diff处理新旧节点是同一个节点时
 
 
Vue源码探秘之 虚拟DOM和diff算法_第12张图片
 
diff算法的子节点更新策略
 
四种命中查找:
① 新前与旧前
② 新后与旧后
③ 新后与旧前 (此种发生了,涉及移动节点,那么新前指向的节点,移动的旧后之后)
④ 新前与旧后 (此种发生了,涉及移动节点,那么新前指向的节点,移动的旧前之前)
 
命中一种就不再进行命中判断了
如果都没有命中,就需要用循环来寻找了。移动到 oldStartIdx 之前。

 

新增的情况
Vue源码探秘之 虚拟DOM和diff算法_第13张图片
Vue源码探秘之 虚拟DOM和diff算法_第14张图片
 
 
删除的情况
Vue源码探秘之 虚拟DOM和diff算法_第15张图片

Vue源码探秘之 虚拟DOM和diff算法_第16张图片

复杂的情况 

Vue源码探秘之 虚拟DOM和diff算法_第17张图片
Vue源码探秘之 虚拟DOM和diff算法_第18张图片
更新函数韩信代码展示:
import patchVnode from './patchVnode.js';
import createElement from './createElement.js';

// 判断是否是同一个虚拟节点
function checkSameVnode(a, b) {
    return a.sel == b.sel && a.key == b.key;
};

export default function updateChildren(parentElm, oldCh, newCh) {
    console.log('我是updateChildren');
    console.log(oldCh, newCh);

    // 旧前
    let oldStartIdx = 0;
    // 新前
    let newStartIdx = 0;
    // 旧后
    let oldEndIdx = oldCh.length - 1;
    // 新后
    let newEndIdx = newCh.length - 1;
    // 旧前节点
    let oldStartVnode = oldCh[0];
    // 旧后节点
    let oldEndVnode = oldCh[oldEndIdx];
    // 新前节点
    let newStartVnode = newCh[0];
    // 新后节点
    let newEndVnode = newCh[newEndIdx];

    let keyMap = null;

    // 开始大while了
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        console.log('★');
        // 首先不是判断①②③④命中,而是要略过已经加undefined标记的东西
        if (oldStartVnode == null || oldCh[oldStartIdx] == undefined) {
            oldStartVnode = oldCh[++oldStartIdx];
        } else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined) {
            oldEndVnode = oldCh[--oldEndIdx];
        } else if (newStartVnode == null || newCh[newStartIdx] == undefined) {
            newStartVnode = newCh[++newStartIdx];
        } else if (newEndVnode == null || newCh[newEndIdx] == undefined) {
            newEndVnode = newCh[--newEndIdx];
        } else if (checkSameVnode(oldStartVnode, newStartVnode)) {
            // 新前和旧前
            console.log('①新前和旧前命中');
            patchVnode(oldStartVnode, newStartVnode);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        } else if (checkSameVnode(oldEndVnode, newEndVnode)) {
            // 新后和旧后
            console.log('②新后和旧后命中');
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (checkSameVnode(oldStartVnode, newEndVnode)) {
            // 新后和旧前
            console.log('③新后和旧前命中');
            patchVnode(oldStartVnode, newEndVnode);
            // 当③新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
            // 如何移动节点??只要你插入一个已经在DOM树上的节点,它就会被移动
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (checkSameVnode(oldEndVnode, newStartVnode)) {
            // 新前和旧后
            console.log('④新前和旧后命中');
            patchVnode(oldEndVnode, newStartVnode);
            // 当④新前和旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            // 如何移动节点??只要你插入一个已经在DOM树上的节点,它就会被移动
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        } else {
            // 四种命中都没有命中
            // 制作keyMap一个映射对象,这样就不用每次都遍历老对象了。
            if (!keyMap) {
                keyMap = {};
                // 从oldStartIdx开始,到oldEndIdx结束,创建keyMap映射对象
                for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                    const key = oldCh[i].key;
                    if (key != undefined) {
                        keyMap[key] = i;
                    }
                }
            }
            console.log(keyMap);
            // 寻找当前这项(newStartIdx)这项在keyMap中的映射的位置序号
            const idxInOld = keyMap[newStartVnode.key];
            console.log(idxInOld);
            if (idxInOld == undefined) {
                // 判断,如果idxInOld是undefined表示它是全新的项
                // 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
                parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
            } else {
                // 如果不是undefined,不是全新的项,而是要移动
                const elmToMove = oldCh[idxInOld];
                patchVnode(elmToMove, newStartVnode);
                // 把这项设置为undefined,表示我已经处理完这项了
                oldCh[idxInOld] = undefined;
                // 移动,调用insertBefore也可以实现移动。
                parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
            }
            // 指针下移,只移动新的头
            newStartVnode = newCh[++newStartIdx];
        }
    }

    // 继续看看有没有剩余的。循环结束了start还是比old小
    if (newStartIdx <= newEndIdx) {
        console.log('new还有剩余节点没有处理,要加项。要把所有剩余的节点,都要插入到oldStartIdx之前');
        // 遍历新的newCh,添加到老的没有处理的之前
        for (let i = newStartIdx; i <= newEndIdx; i++) {
            // insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了。
            // newCh[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM
            parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
        }
    } else if (oldStartIdx <= oldEndIdx) {
        console.log('old还有剩余节点没有处理,要删除项');
        // 批量删除oldStart和oldEnd指针之间的项
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            if (oldCh[i]) {
                parentElm.removeChild(oldCh[i].elm);
            }
        }
    }
};

 github源码地址:https://github.com/russ-gao/mysnabbdom

你可能感兴趣的:(Vue源码探秘,vue.js,javascript,typescript)