写完这个就差不多了,准备干新项目了。

  确实挺不擅长写东西,感觉都是罗列代码写点注释的感觉,这篇就简单阐述一下数据变动时DOM是如何更新的,主要讲解下其中的diff算法。

  先来个正常的html模板:

    <body>
<div id='app'>
<div v-for="item in items">{{item}}</div>
<div @click='click'>click me!</div>
</div>
</body>
<script src='./vue.js'></script>
<script>
new Vue({
el: '#app',
data: {
items: [1]
},
methods: {
click: function() {
this.items.push(2);
}
}
})

  页面上有一个通过v-for渲染的div,还有一个按钮,点击按钮时会让div数量+1。

  

  首先需要提到的是,每一次渲染DOM,都会保存一份当前虚拟DOM的副本挂载到_vnode属性上,如图:

  点击前,整个VNode结构为:根节点及3个子节点,子节点均包含2个div标签和一个空白文本节点,div包含对应的文本节点。

  点击后,由于vue劫持了部分数组方法,所以会进入自定义的push方法中,将弹入的新元素进行广播,过程就不看了。

  完成数组添加后,会生成一个新的render函数与新的VNode,diff算法就是比较新旧VNode的差异,通过最小的变化操作渲染新的DOM。

  讲VNode的diff算法之前,有一个小点先讲一下:如何判断当前VNode可复用?

  销毁一个DOM节点并创建一个新的再插入是消耗非常大的,无论是DOM对象本身的复杂性还是操作引起的重绘重排,所以虚拟DOM的目标是尽可能复用现有DOM进行更新。

  其中涉及的概念就是新的VNode能否在旧的基础上修改并复用呢?有一个函数就是做这个判断的:

    function sameVnode(a, b) {
return (
// key来源于v-for或者自定的:key属性
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
}

  该判断有5重标准:

  (1)key:key属性如果没有设置默认是undefined,当且仅当v-for的列表渲染中会给节点加一个唯一的key,形式如图:,key不一样的节点不进行复用,官方文档也有说明设置key属性可以强制重新生成一个新DOM。

  (2)tag:复用的节点必须保证标签名一致,毕竟没有更改tag名的API

  (3)isComment:注释与普通的DOM不是一个次元,所以需要判断

  (4)isDef(*.data):这个涉及属性的更新,如果一个节点没有任何属性,即data为undefined,与一个有data属性的节点进行更新不如直接渲染一个新的

  (5)sameInputType:这个主要是input标签type属性异同判断,不同的type相当于不同的tag

  如果均满足,可以判定该节点可复用。

  前面说了,每一个更改数据源,会生成一个新的VNode,来与旧的VNode进行比较,节点间的比较无非是判断是否可复用,再进行属性置换。

  而diff算法主要是针对子节点的更新,即两个数组之间的异同比较与更新。

  一个数组的变化无非3个状态:增、删、改,但是其中增删会涉及数组索引与对应元素的变动,总体来讲还是比较复杂的。

  源码中有一个函数专门处理子节点比较,整体如下:

    function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// var...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 旧VNode不存在
if (isUndef(oldStartVnode)) {
// ...
} else if (isUndef(oldEndVnode)) {
// ...
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// ...
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// ...
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// ...
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// ...
} else {
// ...
}
}
if (oldStartIdx > oldEndIdx) {
// ...
} else if (newStartIdx > newEndIdx) {
// ...
}
}

  第一次看还是比较懵逼的,主路线while循环中有7重判断,分别对应7种情况。

  分解本例中的情况,不贴代码,尝试画个图:

  

  对比新旧VNode,可以看出新的VNode在索引0的后面插入了一个新的tag

  接下来通过updateChildren函数进行比较,有很多的变量,这里还需要一个图:

  在函数中有8个变量,其中4个旧VNode,4个新VNode,分别是一一对应的,解释一半就行了:

    var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];

  (1)oldStartIdx => 从前往后的旧VNode数组索引,初始化时为0 => 简称为前索引

  (2)oldStartVnode => 对应索引的旧VNode元素 => 简称为前元素

  (3)oldEndIdx => 从后往前的旧VNode数组索引,初始化为children的数组长度 => 简称为后索引

  (4)oldEndVnode => 对应索引的旧Vnode元素 => 简称为后元素

  后面的阐述全部用简称,不然太难讲了,并且新VNode的数组简称newCh,旧VNode的数组简称oldCh

  另外4个变量只是将old更换为new,并对应新VNode的索引与元素。

  

  接下来是一个大while循环,终止条件是前索引大于后索引(newCh或oldCh):

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// ...
} else if (isUndef(oldEndVnode)) {
// ...
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// ...
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// ...
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// ...
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// ...
} else {
// ...
}
}

  由于有几种情况我模拟不出来,只能大概过一下。

1、isUndef(oldStartVnode)、isUndef(oldEndVnode)

  前两种是oldCh前元素oldCh后元素不存在,我能模拟的情况是当oldCh中没有元素时,会出现这种情况。

  这时只是单纯加前索引加1或者后索引减1,而oldCh长度此时为0,会立即跳出while循环,进入下一步。

2、sameVnode(a,b)

  下面的的4种情况都是判断节点是否可复用,然后进行更新。其中对比的情况有4对:

  oldCh前元素 => newCh前元素

  oldCh后元素 => newCh后元素

  oldCh前元素 => newCh后元素

  oldCh后元素 => newCh前元素 

  取第一种情况来说,如果比较通过,说明oldCh前元素可以被复用,随即调用patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)来对DOM进行更新,由于tag是不变的,可以直接对DOM进行各种API调用,比如说事件更改,只要remove旧事件,add新事件就行,这里只是DOM对象的属性更改,不会影响到DOM的增删。

  当patch完毕后,会将oldCh前索引newCh的前索引加1,并更新对应的元素,然后进入下一轮循环。

  画一轮图解释:

  

  此时第一个子节点已经更新完毕,然后重新开始对比,如果oldCh与newCh的索引1处也可复用,会再次更新并加1,直到前索引大于后索引时,说明所有可能的比较都进行完毕。

  这里的4种比较没有必要重复过一遍,如果是前索引就加1,后索引就减1。

3、else{...}

  最后一种情况是需要强制更新元素时才会有的情况,比如:

    <body>
<div id='app'>
<div v-if="!vIfIter" key='o'>old Ele1</div>
<div v-if="vIfIter" key='n'>new Ele</div>
<div @click='click'>click me!</div>
</div>
</body>
<script src='./vue.js'></script>
<script>
new Vue({
el: '#app',
data: {
vIfIter: false
},
methods: {
click: function() {
this.vIfIter = true;
}
}
})
</script>

  此时,由于设置了单独的key值,所以div被标记为不可复用,跳过了所有判断进入了else阶段:

    // 这里将旧VNode中剩余的元素key值作为对象输出
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 判断新VNode中是否存在可复用的元素
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null;
// 不存在就创建一个新的插入DOM中
if (isUndef(idxInOld)) {
// New element
}
// 存在
else {
elmToMove = oldCh[idxInOld];
if (sameVnode(elmToMove, newStartVnode)) {
// 更新VNode
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
// 把旧的VNode置空 此处会触发到while循环的前两个判断
oldCh[idxInOld] = undefined;
// 移动更新后的VNode
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);
newStartVnode = newCh[++newStartIdx];
}
// 同样的key值不同的tag 创建新DOM插入
else {
// same key but different element. treat as new element
}
}

  简单来讲还是可复用就复用,不可复用创建新DOM插入。

  最后来看看while循环跳出来的语句,其实很简单:

    // VNode数量增加了
if (oldStartIdx > oldEndIdx) {
// 如果VNode是中间插入就会存在refElm
// 否则refElm为null 调用insertBefore会将DOM插入父元素尾部
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
}
// 减少了
else if (newStartIdx > newEndIdx) {
// 移除多出来的DOM节点
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}

  至此,所有的分析完了,上面的案例有兴趣可以自己跑跑。

  

  不容易啊,写完了。。。已经入行5个月,由于没有什么好项目练手,只能看源码提升基本功,接下来可能很长时间不写博客了。(反正也没人看,啊哈哈哈哈~)

  (定个小目标,Codewars刷到3kyu,加油!)

Vue源码终笔-VNode更新与diff算法初探的更多相关文章

  1. vue源码逐行注释分析+40多m的vue源码程序流程图思维导图 (diff部分待后续更新)

    vue源码业余时间差不多看了一年,以前在网上找帖子,发现很多帖子很零散,都是一部分一部分说,断章的很多,所以自己下定决定一行行看,经过自己坚持与努力,现在基本看完了,差ddf那部分,因为考虑到自己要换 ...

  2. Vue 源码解读(4)—— 异步更新

    前言 上一篇的 Vue 源码解读(3)-- 响应式原理 说到通过 Object.defineProperty 为对象的每个 key 设置 getter.setter,从而拦截对数据的访问和设置. 当对 ...

  3. 大白话Vue源码系列(05):运行时鸟瞰图

    阅读目录 Vue 实例的生命周期 实例创建 响应的数据绑定 挂载到 DOM 节点 结论 研究 runtime 一边 Vue 一边源码 初看 Vue 是 Vue 源码是源码 再看 Vue 不是 Vue ...

  4. 入口文件开始,分析Vue源码实现

    Why? 网上现有的Vue源码解析文章一搜一大批,但是为什么我还要去做这样的事情呢?因为觉得纸上得来终觉浅,绝知此事要躬行. 然后平时的项目也主要是Vue,在使用Vue的过程中,也对其一些约定产生了一 ...

  5. 入口开始,解读Vue源码(一)-- 造物创世

    Why? 网上现有的Vue源码解析文章一搜一大批,但是为什么我还要去做这样的事情呢?因为觉得纸上得来终觉浅,绝知此事要躬行. 然后平时的项目也主要是Vue,在使用Vue的过程中,也对其一些约定产生了一 ...

  6. 【vuejs深入三】vue源码解析之二 htmlParse解析器的实现

    写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. 昨天博主分析了一下在vue中,最为基础核心的api,parse函数,它的作用是将vue的模板字符串转换成ast,从而 ...

  7. 【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码

    前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一 ...

  8. 前端Vue 源码分析-逻辑层

    Vue 源码分析-逻辑层 预期的效果: 监听input的输入,input在输入的时候,会触发 watch与computed函数,并且会更新原始的input的数值.所以直接跟input相关的处理就有3处 ...

  9. 学习 vue 源码 -- 响应式原理

    概述 由于刚开始学习 vue 源码,而且水平有限,有理解或表述的不对的地方,还请不吝指教. vue 主要通过 Watcher.Dep 和 Observer 三个类来实现响应式视图.另外还有一个 sch ...

随机推荐

  1. Elasticsearch 的分页报错 result window is too large

    检查自己分页查询的代码 Pageable pageable = new PageRequest(0, 10000); searchQuery.setPageable(pageable); // 分页效 ...

  2. MyBatis的俩种事务管理器的类型

    JDBC – 这个配置直接简单使用了 JDBC 的提交和回滚设置. 它依赖于从数据源得 到的连接来管理事务范围. MANAGED从来不回滚或提交一个连接而它会让 容器来管理事务的整个生命周期(比如 S ...

  3. [cocos2dx] lua注册回调到c++

    思路 像所有语言一样,绑定回调主要是执行的任务执行到特定情形的时候,调用对用回调方法. 这里也一样.核心思路是,当c代码执行到特定特定情形的时候,调用lua的方法 我这里使用的是用lua_stack直 ...

  4. tomcat部署项目的一点心得

    打包方式 eclipse  右键项目Export  选择WAR file 在选择将打包好的war 包存放的位置 放到tomcat中运行   : 首先将war包fang放到解压的tomcat中的weba ...

  5. 搭建dubbo+zookeeper+dubboadmin分布式服务框架(windows平台下)

    1.zookeeper注册中心的配置安装 1.1 下载zookeeper包(zookeeper-3.4.6.tar.gz),ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Goo ...

  6. H5音频处理的一些小知识

      前  言 LiuDaP 十一过后,小编要做一个关于音乐播放器的项目,要用到大量H5音频处理的内容,于是在十月一日国庆黄金周闲暇之际,自己学习了一下H5音频的相关内容.虽然自学的没有那么深入,但是对 ...

  7. ViewData 不可以有特殊字符,比如. ,等只允许数字字符和空格

    ViewData 不可以有特殊字符,比如. ,等只允许数字字符和空格

  8. 简单说明如何设置系统中的NLS_LANG环境变量

    概述:本地化是系统或软件运行的语言和文化环境.设置NLS_LANG环境参数是规定Oracle数据库软件本地化行为最简单的方式.NLS_LANG参数不但指定了客户端应用程序和Oracle数据库所使用的语 ...

  9. 22.Linux-块设备驱动之框架详细分析(详解)

    本节目的: 通过分析块设备驱动的框架,知道如何来写驱动 1.之前我们学的都是字符设备驱动,先来回忆一下 字符设备驱动: 当我们的应用层读写(read()/write())字符设备驱动时,是按字节/字符 ...

  10. Hexo + GitHub Pages搭建博客

    搭建 Node.js 环境 为什么要搭建 Node.js 环境? – 因为 Hexo 博客系统是基于 Node.js 编写的 Node.js 是一个基于 Chrome V8 引擎的 JavaScrip ...