写文章不容易,点个赞呗兄弟

专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue版本 【2.5.17】

如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧

【Vue原理】Diff - 源码版 之 Diff 流程

今天终于要开始探索 Vue 更新DOM 的重点了,就是 Diff

Diff 的内容不算多,但是如果要讲得很详细的话,就要说很多了,而且要配很多图

这是 Diff 的最后一篇文章,最重要也是最详细的一篇了

所以本篇内容很多,先提个内容概览

1、分析 Diff 源码比较步骤2、个人思考为什么如此比较3、写个例子,一步步走个Diff 流程

文章很长,也非常详细,如果你对这内容有兴趣的话,也推荐边阅读源码边看,如果你对本内容暂时没有了解,可以先看不涉及源码的白话版 Diff - 白话版

下面开始我们的正文

在之前一篇文章 Diff - 源码版 之 从新建实例到开始diff ,我们已经探索了 Vue 是如何从新建实例到开始diff 的

你应该还有印象,其中Diff涉及的一个重要函数就是 createPatchFunciton

var patch = createPatchFunction();Vue.prototype.__patch__ =  patch

那么我们就来看下这个函数


createPatchFunction

function createPatchFunction() {  return function patch(oldVnode, vnode, parentElm, refElm    ) {      // 没有旧节点,直接生成新节点if (!oldVnode) {createElm(vnode, parentElm, refElm);} else {     // 且是一样 Vnodeif (sameVnode(oldVnode, vnode)) {                // 比较存在的根节点patchVnode(oldVnode, vnode);} else {    // 替换存在的元素var oldElm = oldVnode.elm;                var _parentElm = oldElm.parentNode    // 创建新节点createElm(vnode, _parentElm, oldElm.nextSibling);   // 销毁旧节点if (_parentElm) {removeVnodes([oldVnode], 0, 0);}}}        return vnode.elm}
}

这个函数的作用就是

比较 新节点 和 旧节点 有什么不同,然后完成更新

所以你看到接收一个 oldVnode 和 vnode

处理的流程分为

1、没有旧节点2、旧节点 和 新节点 自身一样(不包括其子节点)3、旧节点 和 新节点自身不一样

速度来看下这三个流程了

1 没有旧节点

没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了

直接全部都是新建,所以只调用 createElm

2 旧节点 和 新节点 自身一样

通过 sameVnode 判断节点是否一样,这个函数在上篇文章中说过了

旧节点 和 新节点自身一样时,直接调用 patchVnode 去处理这两个节点

patchVnode 下面会讲到这个函数

在讲 patchVnode 之前,我们先思考这个函数的作用是什么?

当两个Vnode自身一样的时候,我们需要做什么?

首先,自身一样,我们可以先简单理解,是 Vnode 的两个属性 tag 和 key 一样

那么,我们是不知道其子节点是否一样的,所以肯定需要比较子节点

所以,patchVnode 其中的一个作用,就是比较子节点

3 旧节点 和 新节点 自身不一样

当两个节点不一样的时候,不难理解,直接创建新节点,删除旧节点


patchVnode

在上一个函数 createPatchFunction 中,有出现一个函数 patchVnode

我们思考了这个函数的其中的一个作用是 比较两个Vnode 的子节点

是不是我们想的呢,可以先来过一下源码

function patchVnode(oldVnode, vnode) { if (oldVnode === vnode) returnvar elm = vnode.elm = oldVnode.elm;    var oldCh = oldVnode.children;    var ch = vnode.children;   // 更新childrenif (!vnode.text) {   // 存在 oldCh 和 ch 时if (oldCh && ch) {            if (oldCh !== ch) updateChildren(elm, oldCh, ch);}    // 存在 newCh 时,oldCh 只能是不存在,如果存在,就跳到上面的条件了else if (ch) {   if (oldVnode.text) elm.textContent = '';      for (var i = 0; i <= ch.length - 1; ++i) {createElm(ch[i],elm, null);}} else if (oldCh) {     for (var i = 0; i<= oldCh.length - 1; ++i) {oldCh[i].parentNode.removeChild(el);}} else if (oldVnode.text) {elm.textContent = '';}} else if (oldVnode.text !== vnode.text) {elm.textContent = vnode.text;}
}

我们现在就来分析这个函数

没错,正如我们所想,这个函数的确会去比较处理子节点

总的来说,这个函数的作用是

1、Vnode 是文本节点,则更新文本(文本节点不存在子节点)

2、Vnode 有子节点,则处理比较更新子节点

更进一步的总结就是,这个函数主要做了两种判断的处理

1、Vnode 是否是文本节点

2、Vnode 是否有子节点

下面我们来看看这些步骤的详细分析

1 Vnode是文本节点

当 VNode 存在 text 这个属性的时候,就证明了 Vnode 是文本节点

我们可以先来看看 文本类型的 Vnode 是什么样子

所以当 Vnode 是文本节点的时候,需要做的就是,更新文本

同样有两种处理

1、当 新Vnode.text 存在,而且和 旧 VNode.text 不一样时

直接更新这个 DOM 的 文本内容

elm.textContent = vnode.text;

注:textContent 是 真实DOM 的一个属性, 保存的是 dom 的文本,所以直接更新这个属性

2、新Vnode 的 text 为空,直接把 文本DOM 赋值给空

elm.textContent = '';

2 Vnode存在子节点

当 Vnode 存在子节点的时候,因为不知道 新旧节点的子节点是否一样,所以需要比较,才能完成更新

这里有三种处理

1、新旧节点 都有子节点,而且不一样

2、只有新节点

3、只有旧节点

后面两个节点,相信大家都能想通,但是我们还是说一下

1 只有新节点

只有新节点,不存在旧节点,那么没得比较了,所有节点都是全新的

所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节点的

2 只有旧节点

只有旧节点而没有新节点,说明更新后的页面,旧节点全部都不见了

那么要做的,就是把所有的旧节点删除

也就是直接把DOM 删除

3 新旧节点 都有子节点,而且不一样

咦惹,又出现了一个新函数,那就是 updateChildren

预告一下,这个函数非常的重要,是 Diff 的核心模块,蕴含着 Diff 的思想

可能会有点绕,但是不用怕,相信在我的探索之下,可以稍微明白些

同样的,我们先来思考下 updateChildren 的作用

记得条件,当新节点 和 旧节点 都存在,要怎么去比较才能知道有什么不一样呢?

哦没错,使用遍历,新子节点和旧子节点一个个比较

如果一样,就不更新,如果不一样,就更新

下面就来验证下我们的想法,来探索一下 updateChildren 的源码


updateChildren

这个函数非常的长,但是其实不难,就是分了几种处理流程而已,但是一开始看可能有点懵

或者可以先跳过源码,看下分析,或者便看分析边看源码

function updateChildren(parentElm, oldCh, newCh) {var oldStartIdx = 0;    var oldEndIdx = oldCh.length - 1;    var oldStartVnode = oldCh[0];    var oldEndVnode = oldCh[oldEndIdx];    var newStartIdx = 0;    var newEndIdx = newCh.length - 1;    var newStartVnode = newCh[0];    var newEndVnode = newCh[newEndIdx];    var oldKeyToIdx, idxInOld, vnodeToMove, refElm;// 不断地更新 OldIndex 和 OldVnode ,newIndex 和 newVnodewhile (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {        if (!oldStartVnode) {oldStartVnode = oldCh[++oldStartIdx];}     else if (!oldEndVnode) {oldEndVnode = oldCh[--oldEndIdx];}   //  旧头 和新头 比较else if (sameVnode(oldStartVnode, newStartVnode)) {patchVnode(oldStartVnode, newStartVnode);oldStartVnode = oldCh[++oldStartIdx];newStartVnode = newCh[++newStartIdx];}    //  旧尾 和新尾 比较else if (sameVnode(oldEndVnode, newEndVnode)) {patchVnode(oldEndVnode, newEndVnode);oldEndVnode = oldCh[--oldEndIdx];newEndVnode = newCh[--newEndIdx];}                // 旧头 和 新尾 比较else if (sameVnode(oldStartVnode, newEndVnode)) {patchVnode(oldStartVnode, newEndVnode);            // oldStartVnode 放到 oldEndVnode 后面,还要找到 oldEndValue 后面的节点parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);oldStartVnode = oldCh[++oldStartIdx];newEndVnode = newCh[--newEndIdx];}   //  旧尾 和新头 比较else if (sameVnode(oldEndVnode, newStartVnode)) {patchVnode(oldEndVnode, newStartVnode);            // oldEndVnode 放到 oldStartVnode 前面parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);oldEndVnode = oldCh[--oldEndIdx];newStartVnode = newCh[++newStartIdx];}        // 单个新子节点 在 旧子节点数组中 查找位置else {    // oldKeyToIdx 是一个 把 Vnode 的 key 和 index 转换的 mapif (!oldKeyToIdx) {oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);}     // 使用 newStartVnode 去 OldMap 中寻找 相同节点,默认key存在idxInOld = oldKeyToIdx[newStartVnode.key]        //  新孩子中,存在一个新节点,老节点中没有,需要新建 if (!idxInOld) {  //  把  newStartVnode 插入 oldStartVnode 的前面createElm(newStartVnode, parentElm, oldStartVnode.elm);}            else {                //  找到 oldCh 中 和 newStartVnode 一样的节点vnodeToMove = oldCh[idxInOld];     if (sameVnode(vnodeToMove, newStartVnode)) {patchVnode(vnodeToMove, newStartVnode);  // 删除这个 indexoldCh[idxInOld] = undefined;                    // 把 vnodeToMove 移动到  oldStartVnode 前面parentElm.insertBefore(vnodeToMove.elm, oldStartVnode.elm);}                // 只能创建一个新节点插入到 parentElm 的子节点中else {                    // same key but different element. treat as new elementcreateElm(newStartVnode, parentElm, oldStartVnode.elm);}}            // 这个新子节点更新完毕,更新 newStartIdx,开始比较下一个newStartVnode = newCh[++newStartIdx];}}    // 处理剩下的节点if (oldStartIdx > oldEndIdx) {  var newEnd = newCh[newEndIdx + 1]refElm = newEnd ? newEnd.elm :null;        for (; newStartIdx <= newEndIdx; ++newStartIdx) {createElm(newCh[newStartIdx], parentElm, refElm);}}    // 说明新节点比对完了,老节点可能还有,需要删除剩余的老节点else if (newStartIdx > newEndIdx) {       for (; oldStartIdx<=oldEndIdx; ++oldStartIdx) {oldCh[oldStartIdx].parentNode.removeChild(el);}}
}

首先要明确这个函数处理的是什么

处理的是 新子节点 和 旧子节点,循环遍历逐个比较

如何 循环遍历?

1、使用 while

2、新旧节点数组都配置首尾两个索引

新节点的两个索引:newStartIdx , newEndIdx

旧节点的两个索引:oldStartIdx,oldEndIdx

以两边向中间包围的形式 来进行遍历

头部的子节点比较完毕,startIdx 就加1

尾部的子节点比较完毕,endIdex 就减1

只要其中一个数组遍历完(startIdx<endIdx),则结束遍历

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oaL7pEiQ-1570843827763)( https://pic1.zhimg.com/80/v2-436ec22ba4a0b626e7dd4f414684eafa_hd.png )]

源码处理的流程分为两个

1、比较新旧子节点

2、比较完毕,处理剩下的节点

我们来逐个说明这两个流程

1 比较新旧子节点

注:这里有两个数组,一个是 新子Vnode数组,一个旧子Vnode数组

在比较过程中,不会对两个数组进行改变(比如不会插入,不会删除其子项)

而所有比较过程中都是直接 插入删除 真实页面DOM

我们明确一点,比较的目的是什么?

找到 新旧子节点中 的 相同的子节点,尽量以 移动 替代 新建 去更新DOM

只有在实在不同的情况下,才会新建

比较更新计划步骤

首先考虑,不移动DOM

其次考虑,移动DOM

最后考虑,新建 / 删除 DOM

能不移动,尽量不移动。不行就移动,实在不行就新建

下面开始说源码中的比较逻辑

五种比较逻辑如下

1、旧头 == 新头2、旧尾 == 新尾3、旧头 == 新尾4、旧尾 == 新头5、单个查找

来分析下这五种比较逻辑

1 旧头 == 新头

sameVnode(oldStartVnode, newStartVnode)

当两个新旧的两个头一样的时候,并不用做什么处理

符合我们的步骤第一条,不移动DOM完成更新

但是看到一句,patchVnode

就是为了继续处理这两个相同节点的子节点,或者更新文本

因为我们不考虑多层DOM 结构,所以 新旧两个头一样的话,这里就算结束了

可以直接进行下一轮循环

newStartIdx ++ , oldStartIdx ++

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WAa55wp2-1570843827763)( https://pic2.zhimg.com/80/v2-ad42a7d1c10051e260c05b8c8e6840ac_hd.png )]

2 旧尾 == 新尾

sameVnode(oldEndVnode, newEndVnode)

和 头头 相同的处理是一样的

尾尾相同,直接跳入下个循环

newEndIdx ++ , oldEndIdx ++

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qPAYB8wE-1570843827763)( https://pic3.zhimg.com/80/v2-3548dfcffc10efeaa00fae64be509e14_hd.png)]

3 旧头 == 新尾

sameVnode(oldStartVnode, newEndVnode)

这步不符合 不移动DOM,所以只能 移动DOM 了

怎么移动?

源码是这样的

parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling
);

以 新子节点的位置 来移动的,旧头 在新子节点的 末尾

所以把 oldStartVnode 的 dom 放到 oldEndVnode 的后面

但是因为没有把dom 放到谁后面的方法,所以只能使用 insertBefore

即放在 oldEndVnode 后一个节点的前面

图示是这样的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vaXRusTM-1570843827764)( https://pic2.zhimg.com/80/v2-c84929b58ec7bf1bc29c56042e18349f_hd.png )]

然后更新两个索引

oldStartIdx++,newEndIdx--

4 旧尾 == 新头

sameVnode(oldEndVnode, newStartVnode)

同样不符合 不移动DOM,也只能 移动DOM 了

怎么移动?
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm
);

把 oldEndVnode DOM 直接放到 当前 oldStartVnode.elm 的前面

图示是这样的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v4qC9bgH-1570843827764)( https://pic2.zhimg.com/80/v2-a8a8ddbb2fc2b3792158134b60d2512d_hd.png )]

然后更新两个索引

oldEndIdx--,newStartIdx++

5 单个遍历查找

当前面四种比较逻辑都不行的时候,这是最后一种处理方法

拿 新子节点的子项,直接去 旧子节点数组中遍历,找一样的节点出来

流程大概是

1、生成旧子节点数组以 vnode.key 为key 的 map 表

2、拿到新子节点数组中 一个子项,判断它的key是否在上面的map 中

3、不存在,则新建DOM

4、存在,继续判断是否 sameVnode

下面就详细说一下

1 生成map 表

这个map 表的作用,就主要是判断存在什么旧子节点

比如你的旧子节点数组是

[{    tag:"div",  key:1
},{  tag:"strong", key:2
},{  tag:"span",  key:4
}]

经过 createKeyToOldIdx 生成一个 map 表 oldKeyToIdx

{ vnodeKey: 数组Index }

属性名是 vnode.key,属性值是 该 vnode 在children 的位置

是这样(具体源码看上篇文章 Diff - 源码版 之 相关辅助函数)

oldKeyToIdx = {1:0,2:1,4:2
}
2 判断 新子节点是否存在旧子节点数组中

拿到新子节点中的 子项Vnode,然后拿到它的 key

去匹配map 表,判断是否有相同节点

oldKeyToIdx[newStartVnode.key]
3 不存在旧子节点数组中

直接创建DOM,并插入oldStartVnode 前面

createElm(newStartVnode, parentElm, oldStartVnode.elm);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oHCzbXLA-1570843827764)( https://pic4.zhimg.com/80/v2-b5304d0a6ba1dc90bbc02ef4ba519c9b_hd.png )]

4 存在旧子节点数组中

找到这个旧子节点,然后判断和新子节点是否 sameVnode

如果相同,直接移动到 oldStartVnode 前面

如果不同,直接创建插入 oldStartVnode 前面

我们上面说了比较子节点的处理的流程分为两个

1、比较新旧子节点

2、比较完毕,处理剩下的节点

比较新旧子节点上面已经说完了,下面就到了另一个流程,比较剩余的节点,详情看下面

处理可能剩下的节点

在updateChildren 中,比较完新旧两个数组之后,可能某个数组会剩下部分节点没有被处理过,所以这里需要统一处理

1 新子节点遍历完了
newStartIdx > newEndIdx

新子节点遍历完毕,旧子节点可能还有剩

所以我们要对可能剩下的旧节点进行 批量删除!

就是遍历剩下的节点,逐个删除DOM

for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {oldCh[oldStartIdx].parentNode.removeChild(el);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rw4AWwjU-1570843827765)( https://pic4.zhimg.com/80/v2-1d91dace25b34e5e6fef34d0a3a38833_hd.png )]

2旧子节点遍历完了
oldStartIdx > oldEndIdx

旧子节点遍历完毕,新子节点可能有剩

所以要对剩余的新子节点处理

很明显,剩余的新子节点不存在 旧子节点中,所以全部新建

for (; newStartIdx <= newEndIdx; ++newStartIdx) {createElm(newCh[newStartIdx], parentElm, refElm);
}

但是新建有一个问题,就是插在哪里?

所以其中的 refElm 就成了疑点,看下源码

var newEnd = newCh[newEndIdx + 1]refElm = newEnd ? newEnd.elm :null;

refElm 获取的是 newEndIdx 后一位的节点

当前没有处理的节点是 newEndIdx

也就是说 newEndIdx+1 的节点如果存在的话,肯定被处理过了

如果 newEndIdx 没有移动过,一直是最后一位,那么就不存在 newCh[newEndIdx + 1]

那么 refElm 就是空,那么剩余的新节点 就全部添加进 父节点孩子的末尾,相当于

for (; newStartIdx <= newEndIdx; ++newStartIdx) {     parentElm.appendChild(newCh[newStartIdx]);}

如果 newEndIdx 移动过,那么就逐个添加在 refElm 的前面,相当于

for (; newStartIdx <= newEndIdx; ++newStartIdx) {parentElm.insertBefore(newCh[newStartIdx] ,refElm );}

如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tHXZ3TQW-1570843827765)( https://pic4.zhimg.com/80/v2-2458ff6c8577012eb9505dd9e5729f92_hd.png )]


思考为什么这么比较

我们已经讲完了所有 Diff 的内容,大家也应该能领悟到 Diff 的思想

但是我强迫自己去思考一个问题,就是

为什么会这样去比较?

以下纯属个人意淫想法,没有权威认证,仅供参考

我们所有的比较,都是为了找到 新子节点 和 旧子节点 一样的子节点

而且我们的比较处理的宗旨是

1、能不移动,尽量不移动

2、没得办法,只好移动

3、实在不行,新建或删除

首先,一开始比较,肯定是按照我们的第一宗旨 不移动 ,找到可以不移动的节点

而 头头,尾尾比较 符合我们的第一宗旨,所以出现在最开始,嗯,这个可以想通

然后就到我们的第二宗旨 移动,按照 updateChildren 的做法有

旧头新尾比较,旧尾新头比较,单个查找比较

我开始疑惑了,咦?头尾比较为了移动我知道,但是为什么要出现这种比较?

明明我可以用 单个查找 的方式,完成所有的移动操作啊?

我思考了很久,头和尾的关系,觉得可能是为了避免极端情况的消耗??

怎么说?

比如当我们去掉头尾比较,全部使用单个查找的方式

如果出现头 和 尾 节点一样的时候,一个节点需要遍历 从头找到尾 才能找到相同节点

这样实在是太消耗了,所以这里加入了 头尾比较 就是为了排除 极端情况造成的消耗操作

当然,这只是我个人的想法,仅供参考,虽然这么说,我也的确做了个例子测试

子节点中加入了出现两个头尾比较情况的子项 b div

oldCh = ['header','span','div','b']
newCh = ['sub','b','div','strong']

使用 Vue 去更新,比较更新速度,然后更新十次,计算平均值

1、全用 单个查找,用时 0.91ms

2、加入头尾比较,用时 0.853ms

的确是快一些喔


走流程

我相信经过这么长的一篇文章,大家的脑海中还没有把所有的知识点集合起来,可能对整个流程还有点模糊

没事,我们现在就来举一个例子,一步步走流程,完成更新

以下的节点,绿色表示未处理,灰色表示已经处理,淡绿色表示正在处理,红色表示新插入,如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m6rs3dA5-1570843827766)( https://pic3.zhimg.com/80/v2-9041886ec653b49b1d164ada9fca6002_hd.png )]

现在Vue 需要更新,存在下面两组新旧子节点,需要进行比较,来判断需要更新哪些节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZkRh3PJM-1570843827766)( https://pic4.zhimg.com/80/v2-b307e247e02fef84a0b05e1315fd2e75_hd.png )]

1头头比较,节点一样,不需移动,只用更新索引

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m9tWpiWU-1570843827766)( https://pic1.zhimg.com/80/v2-b43e330eeb5b8f44cf84f4416b9cdd95_hd.png )]

更新索引,newStartIdx++ , oldStartIdx++

开始下轮处理

一系列判断之后,【旧头 2】 和 【 新尾 2】相同,直接移动到 oldEndVnode 后面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uPv5t7H8-1570843827766)( https://pic1.zhimg.com/80/v2-93e5bb51edb631517797305ba02df449_hd.png )]

更新索引,newEndIdx-- ,oldStartIdx ++

开始下轮处理

3一系列判断之后,【旧头 2】 和 【 新尾 2】相同,直接移动到 oldStartVnode 前面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LEVEVtjb-1570843827767)( https://pic1.zhimg.com/80/v2-2d145dd1efaee4238ecffe099948bd7d_hd.png )]

更新索引,oldEndIdx-- ,newStartIdx++

开始下轮比较

4只剩一个节点,走到最后一个判断,单个查找

找不到一样的,直接创建插入到 oldStartVnode 前面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Sw8SLaT-1570843827767)( https://pic4.zhimg.com/80/v2-0e540cc8605702eb00600235c66e6225_hd.png )]

更新索引,newStartIdx++

此时 newStartIdx> newEndIdx ,结束循环

5 批量删除可能剩下的老节点

此时看 旧 Vnode 数组中, oldStartIdx 和 oldEndIdx 都指向同一个节点,所以只用删除 oldVnode-4 这个节点

ok,完成所有比较流程

耶,Diff 内容讲完了,谢谢大家的观看

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nMLeXZMu-1570843827767)( https://pic1.zhimg.com/80/v2-3b85bc8c144fb58033d1ef5bf001f312_hd.gif )]


最后

鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢

【Vue原理】Diff - 源码版 之 Diff 流程相关推荐

  1. spring系列-注解驱动原理及源码-spring容器创建流程

    目录 一.spring容器的refresh()[创建刷新] 二.BeanFactory 的创建及预准备工作 1.prepareRefresh()刷新前预处理工作. 2.obtainFreshBeanF ...

  2. c++ gdb 绑定源码_【Vue原理】VNode 源码版

    ↑点击上方 "神仙朱" 一起研究Vue源码吧 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版 ...

  3. element 往node里面增加属性值_【Vue原理】Compile - 源码版 之 Parse 属性解析

    写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本 [2.5.17] 如果你觉得排版难 ...

  4. 初始化触发点击事件_【Vue原理】Event - 源码版 之 自定义事件

    专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本[2.5.17] Vue 的自定义事件很简单,就是使用 观察者模 ...

  5. 【Vue原理】Methods - 源码版

    写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本 [2.5.17] 如果你觉得排版难 ...

  6. vue使用computed有参数_【Vue原理】Computed - 源码版

    专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本2.5.17 今天要记录 computed 的源码,有时候想,理 ...

  7. vue如何创建vnode_【Vue原理】Component - 源码版 之 创建组件VNode

    专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本[2.5.17] 今天就要开启我们 Component 探索之旅 ...

  8. vue 数值 拼接字符串_【Vue原理】Compile - 白话版

    写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本 [2.5.17] 如果你觉得排版难 ...

  9. v-model双向绑定原理_【Vue原理】VModel 白话版

    ↑点击上方 "神仙朱" 一起研究Vue源码吧 专注 Vue 源码分享,为了方便大家理解,分为了白话版和 源码版,白话版让大家可以轻松理解工作原理,源码版让大家更清楚内部操作和 Vu ...

最新文章

  1. oracle 隐藏视图定义,【学习笔记】show hidden parameter 创建查看隐藏参数视图
  2. android连接mysql查询表内记录_Android Room数据库多表查询的使用实例
  3. WPF 中Frame + Page 的使用
  4. CENTOS7配置静态IP后无法ping通外部网络的问题
  5. Zend Framework(一)概述介绍
  6. Python中__init__和self的意义和作用
  7. 【重识 HTML + CSS】CSS 特性
  8. 命令行,使用java的java 命令,直接调用执行class文件
  9. 2018年python工作好找吗-2018年涨工资了吗?Python 工程师薪资最新出炉
  10. 进程 线程 协程_进程 线程 协程 管程 纤程 概念对比理解
  11. linux apache支持ipv6,如何在Nginx和Apache中启用IPv6?
  12. groupadd命令详解(实例)
  13. hihocoder-13892016北京网赛07 Sewage Treatment(二分+网络流)
  14. servlet原理+流程图+简单实现案例(javaweb)
  15. 计算机cpu的介绍,CPU的详细介绍
  16. 【Tableau Desktop 企业日常问题28】Tableau 如何发布到public ?
  17. wp8.1 java_WP8.1系统升级到Win10 Mobile正式版的方法
  18. 一只公鸡5块钱,一只母鸡3块钱,3只小鸡一块钱,一个农夫用100块钱买100只鸡(不许解方程),怎么实现?编写java程序。...
  19. 手机通讯录 客户端/服务器数据同步方案设计
  20. 01、pyqt入门使用--01布局、基本组件、第一个示例、qtdesigner大概使用

热门文章

  1. Ajax Upload上传图片失败的解决方法:
  2. 冥想(自我修养- 慎独 自我修行)
  3. 计算机授课教案模板,讲课教案模板.doc
  4. Java APNS开源库apns4j-1.0.1发布
  5. codevs3147 矩阵乘法 2 (推导小公式)
  6. linux中sl是什么命令,Linux系统使用sl命令制作火车动态桌面的技巧
  7. BASE64加密解密及乱码问题
  8. python之base64加解密
  9. Apple Http Live Stream
  10. 简明介绍 n-gram