当前位置: 首页 > 编程日记 > 正文

Vue源码终笔-VNode更新与diff算法初探

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

确实挺不擅长写东西,感觉都是罗列代码写点注释的感觉,这篇就简单阐述一下数据变动时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;// 移动更新后的VNodecanMove && 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,加油!)

转载于:https://www.cnblogs.com/QH-Jimmy/p/7449789.html

相关文章:

JS获取当月每天的日期,JS获取本周每天的日期

获取当前月每天的日期&#xff0c;获取当前周每天的日期实现代码&#xff1a; 调用代码&#xff1a; console.log(-----------------, getNowM(), getWeekDay()) 结果&#xff1a;我今天是2020-2-28日 封装方法&#xff1a; function getDay(num, str) {var today new Dat…

@Scheduled注解的scheduler属性什么作用

注解是 Spring Framework 提供的一种机制,用于定义计划任务,即周期性执行的任务。 注解可以应用于方法上,以指示 Spring 容器在特定的时间间隔或按照某种调度规则来调用该方法。 属性是 注解的一个可选属性,它的作用是允许开发者指定一个自定义的 对象来控制任务的调度方式。默认情况下, 注解使用 Spring 内部的 来执行任务,但如果需要更高级的定制化需求,可以通过 属性指定一个自定义的 实现。自定义调度器:共享调度器资源:高级调度需求:假设你想使用 作为调度器,并且希望所有带有

自行车车把会吧车刮坏吗_花10分钟即可开始使用车把

自行车车把会吧车刮坏吗by Wing Puah永帕(Wing Puah) 花10分钟即可开始使用车把 (Take 10 minutes to get started with Handlebars) Nowadays front-end development is no longer about building static HTML markup and compiling SASS files. The rise of Single Page App…

每天一个linux命令(33):df 命令

linux中df命令的功能是用来检查linux服务器的文件系统的磁盘空间占用情况。可以利用该命令来获取硬盘被占用了多少空间&#xff0c;目前还剩下多少空间等信息。 1&#xff0e;命令格式&#xff1a; df [选项] [文件] 2&#xff0e;命令功能&#xff1a; 显示指定磁盘文件的可用…

一:HDFS 用户指导

1.hdfs的牛逼特性 Hadoop, including HDFS, is well suited for distributed storage and distributed processing using commodity hardware. It is fault tolerant, scalable, and extremely simple to expand. MapReduce, well known for its simplicity and applicability …

uni-app 封装企业微信config

第一步&#xff0c;在项目根目录加一个html文件&#xff0c; index.html 代码如下&#xff1a; <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"utf-8"><meta http-equiv"X-UA-Compatible" content"I…

sqoop架构_SQOOP架构的深入介绍

sqoop架构by Jayvardhan Reddy通过杰伊瓦尔丹雷迪(Jayvardhan Reddy) SQOOP架构的深入介绍 (An in-depth introduction to SQOOP architecture) Apache Sqoop is a data ingestion tool designed for efficiently transferring bulk data between Apache Hadoop and structure…

JS实现录音,播放完整代码带示例图

效果图&#xff1a; 实现代码&#xff1a; <!DOCTYPE html> <html><head><script src"recorder.js" type"text/javascript" charset"utf-8"></script><meta name"viewport" content"widthdevi…

r.json()

requests模块中&#xff0c;r.json()为Requests中内置的JSON解码器 其中只有response返回为json格式时&#xff0c;用r.json()打印出响应的内容&#xff0c; 如果response返回不为json格式&#xff0c;使用r.json()会报错 报错内容&#xff1a;ValueError: Expecting property …

冒泡排序语法树

转载于:https://www.cnblogs.com/alfredzhu/p/4939268.html

valve 的设计_向Valve Portal开发人员学习游戏设计原则

valve 的设计In this talk, Valve programers who created the game Portal discuss problems they faced in development and how they solved them. Leaning about how they solved Portal problems can give you insight into how to design better games.在本次演讲中&…

Android之控件使用

Android系统为我们提供了大量的控件&#xff0c;例如&#xff1a;开关控件、单选按钮、多选按钮、单选菜单等等&#xff0c;那么这些控件如何使用呢&#xff1f;本篇我将带领大家一道学习一下如何使用这些控件。所谓无图无真相&#xff0c;先让大家看一下效果图&#xff1a; 下…

《对软件工程课程的期望》

要学习到的能力的预期&#xff1a;要学会个人&#xff0c;结对&#xff0c;团队的代码编辑流程&#xff0c;学会和别人进行交流。 对项目课程的期望&#xff1a;希望不是枯燥的代码详解。 对项目的愿景规划&#xff1a;希望团队里的每个人都能学到有用的知识。转载于:https://w…

HTML发送语音,上传音频PHP接收

实现需求&#xff1a;网页录制音频上传给后端接收&#xff0c;接收后PHP把文件的名字存到数据库的表里面&#xff0c;这里我的后端用的是PHP&#xff0c;并且把代码贴出来了。 前端实现代码&#xff1a; <!DOCTYPE HTML> <html><head><meta http-equiv&q…

html:漂亮的原生表格_HTML表格:关于它们的所有知识

html:漂亮的原生表格by Alexander Gilmanov亚历山大吉尔马诺夫(Alexander Gilmanov) HTML表格&#xff1a;关于它们的所有知识 (HTML Tables: All there is to know about them) Judging by the fact that we created wpDataTables, it’s no secret that we like tables. So …

[BZOJ] 1606: [Usaco2008 Dec]Hay For Sale 购买干草

1606: [Usaco2008 Dec]Hay For Sale 购买干草 Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 1335 Solved: 989[Submit][Status][Discuss]Description 约翰遭受了重大的损失&#xff1a;蟑螂吃掉了他所有的干草&#xff0c;留下一群饥饿的牛&#xff0e;他乘着容量为C(1≤C≤…

PHP TP5框架 安装运行 Warning: require(E:\phpstudy_pro\WWW\TP5\tp5\public/../thinkphp/base.php): failed to

创建一个新的项目&#xff1a;进入项目的根目录执行 git 命令&#xff1a; 先执行 git clone -b 5.1 https://git.coding.net/liu21st/thinkphp5.git tp5 进入 tp5目录 cd tp5再执行 git clone -b 5.1 https://git.coding.net/liu21st/framework.git thinkphp 执行更新框…

python之模块base64

# -*- coding: cp936 -*- #python 27 #xiaodeng>>> help(base64) #用来作base64编码解码 FUNCTIONS #函数(功能) •b16decode(s, casefoldFalse)Decode a Base16 encoded string. #解码 decode_stringbase64…

github pages_使用GitHub Pages和Lighthouse增强您的开发人员产品组合

github pagesFor someone who is trying to break into software development, it doesn’t matter where you look — LinkedIn, career advice boards, youtube tutorials — the advice is always the same: you need a portfolio. freeCodeCamp knows this advise, and the…

Angular 4+ HttpClient

个人博客迁移至 http://www.sulishibaobei.com 处&#xff1b; 这篇&#xff0c;算是上一篇Angular 4 Http的后续&#xff1b; Angular 4.3.0-rc.0 版本已经发布?。在这个版本中&#xff0c;我们等到了一个令人兴奋的新功能 - HTTPClient API 的改进版本&#xff1b; HttpCli…

PHP TP5入门 二:写接口,添加控制器并访问

默认访问地址&#xff1a;http://localhost/TP5/tp5/public/index.php/index/hello_world 实现代码&#xff1a; <?php namespace app\index\controller;class HelloWorld {public function index(){return 22hello&#xff0c;world&#xff01;;} } 添加一个控制器如…

Possion 分布

泊松分布的概率函数为&#xff1a; \[P(Xk)\frac{\lambda^k}{k!}e^{-\lambda},k0,1,2,\cdots\] 如果 $X_i \sim P(\lambda_i)$,并且 互相独立&#xff0c;那么: \[Y\left( \sum\limits_{i1}^n{X_i} \right) \sim P \left( \sum\limits_{i1}^n{\lambda_i} \right)\] 从上面公式…

如何使您的Kotlin Android动画可访问

When researching examples for a first ever Android contribution, few examples existed for animations written in Kotlin. There were also few code examples of accessibility considerations within native animations.在研究有史以来第一个Android贡献的示例时&#…

指针空间的申请与释放

一、malloc()和free()的基本概念以及基本用法&#xff1a; 1、函数原型及说明&#xff1a; void *malloc(long NumBytes)&#xff1a;该函数分配了NumBytes个字节&#xff0c;并返回了指向这块内存的指针。如果分配失败&#xff0c;则返回一个空指针&#xff08;NULL&#xff0…

UIGraphicsBeginImageContext - 位图上下文

UIGraphicsBeginImageContext 首先&#xff0c;先来认识一个UIGraphicsBeginImageContext&#xff0c;它会创建一个基于位图的上下文(context)&#xff08;默认创建一个透明的位图上下文&#xff09;,并将其设置为当前上下文。 位图图形上下文UIKit是不会负责创建的&#xff0c…

小程序双击事件

代码&#xff1a; <button data-time"{{lastTapTime}}" data-title"标题" bindtap"doubleClick">双击</button> js data: {lastTapTime:0,}, doubleClick: function (e) {var curTime e.timeStampvar lastTime e.currentTarget…

快速了解Kubernetes微服务中的通信

by Adam Henson亚当汉森(Adam Henson) 快速了解Kubernetes微服务中的通信 (A quick look at communication in Kubernetes microservices) “服务”概念和一个Node.js示例 (The “service” concept and a Node.js example) Based on complexity, a layer of microservices ca…

连接 linux服务器

操作步骤&#xff1a; xshell 下载 https://xshell.en.softonic.com/ 点击下载后&#xff0c;会有邮箱验证&#xff0c;点击验证通过就会自动下载&#xff0c;然后安装就行。 打开工具&#xff0c;点击新建会话 然后 浏览文件后直接点击确认&#xff0c;出来这样就登录成功了…

【bzoj3924】[Zjoi2015]幻想乡战略游戏 动态点分治

题目描述 傲娇少女幽香正在玩一个非常有趣的战略类游戏&#xff0c;本来这个游戏的地图其实还不算太大&#xff0c;幽香还能管得过来&#xff0c;但是不知道为什么现在的网游厂商把游戏的地图越做越大&#xff0c;以至于幽香一眼根本看不过来&#xff0c;更别说和别人打仗了。 …

面试题05-UI控件

怎么解决缓存池满的问题(cell)ios中不存在缓存池满的情况&#xff0c;因为通常我们ios中开发&#xff0c;对象都是在需要的时候才会创建&#xff0c;有种常用的说话叫做懒加载&#xff0c;还有在UITableView中一般只会创建刚开始出现在屏幕中的cell&#xff0c;之后都是从缓存池…