【手写 Vue2.x 源码】第二十九篇 - diff算法-节点比对

news2024/9/25 8:21:13

一,前言

上篇,diff 算法问题分析与 patch 方法改造,主要涉及以下几点:

  • 初始化与更新流程分析;
  • 问题分析与优化思路;
  • 新老虚拟节点比对模拟;
  • patch 方法改造;

下篇,diff 算法-节点比对


二,diff 算法

上一篇完成了 patch 方法的改造,

接下来,开始编写视图更新时新老虚拟节点比对的 diff 算法;

在这之前,先介绍一下 diff 算法:

1,diff 算法的简单介绍

diff 算法也叫做同层比较算法;

首先,dom 是一个树型结构:

image.png

在日常开发中,很少会将 B 和 A 或是 D 和 A 的位置进行调换,即:很少将父亲和儿子节点进行交换

而且跨层的节点比对会非常麻烦,所以,diff 算法考虑到应用场景与性能,只会进行同层节点的比较;

2,diff 算法的比较方式

diff 算法将新老虚拟节点,"两棵树"进行比对

从树的根节点,即 LV1 层开始比较:

image.png

A 比较完成后,查看 A 节点是否有儿子节点,即 B 和 C,优先比较 B:

image.png

B 比较完成后,查看 B 节点是否有儿子节点,即 D 和 E,优先比较 D

D 比较完成后,没有儿子;继续比较 E,当前层处理完成,返回上层处理;

继续比较 C,C 有儿子 F,继续比较 F,最后全部比较完成,结束;

所以,diff 比对是深度优先遍历的递归比对;

备注:递归比对是 vue2 的性能瓶颈,当组件树庞大时会有性能问题;

3,diff 算法的节点复用

如何确定两个节点为复用,一般来说,相同标签的元素即可进行复用;
但也有标签相同,实际场景并不希望复用的情况,这时可使用 key 属性进行标记;
如果 key 不相同,即便标签名相同的两个元素,也不会进行复用;

所以,在编写代码时,相同节点的复用标准如下:

  1. 标签名和 key 均相同,是相同节点;
  2. 如果标签名和 key 不完全相同,不是相同节点;

isSameVnode 方法:用于判断是否为相同节点:

// src/vdom/index.js

/**
 * 判断两个虚拟节点是否是同一个虚拟节点
 *  逻辑:标签名 和 key 都相同
 * @param {*} newVnode 新虚拟节点
 * @param {*} oldVnode 老虚拟节点
 * @returns 
 */
export function isSameVnode(newVnode, oldVnode){
  return (newVnode.tag === oldVnode.tag)&&(newVnode.key === oldVnode.key); 
}

当新老虚拟节点的标签和 key 均相同时,即 isSameVnode 返回 true,复用节点仅做属性更新即可;


三,虚拟节点比对

1,不是相同节点的情况

创建两个虚拟节点,模拟视图的更新:

// 模拟初渲染
let vm1 = new Vue({
    data() {
        return { name: 'Brave' }
    }
})
let render1 = compileToFunction('<div>{{name}}</div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);

// 模拟新的虚拟节点 newVnode
let vm2 = new Vue({
    data() {
        return { name: 'BraveWang' }
    }
})
let render2 = compileToFunction('<p>{{name}}</p>');
let newVnode = render2.call(vm2);

// diff:新老虚拟节点对比
setTimeout(() => {
    patch(oldVnode, newVnode); 
}, 1000);

由于新老虚拟节点的标签名 tag 不同(模拟 v-if 和 v-else 的情况),

所以不是相同节点,不考虑复用(放弃跨层复用),直接使用新的替换掉旧的真实节点

在 patch 方法中,打印新老虚拟节点:

image.png

image.png

如何替换节点

由于父节点的标签名不同,导致节点不复用,
需根据新的虚拟节点生成真实节点,并替换掉老节点
  1. 使用新的虚拟节点创建真实节点:

    createElm(vnode);

  2. 替换老节点,如果获取到老的真实节点?

    根据vnode生成真实节点时,通过 vnode.el 将真实节点与虚拟节点进行了映射
    所以,此时可以通过 oldVnode.el 获取到老的真实节点;

    备注:$el是指整棵树,这里不可用;

结论:

  • 新的真实节点:createElm(vnode);
  • 老的真实节点:oldVnode.el
export function patch(oldVnode, vnode) {
  const isRealElement = oldVnode.nodeType;
  if(isRealElement){// 真实节点
    const elm = createElm(vnode);
    const parentNode = oldVnode.parentNode;
    parentNode.insertBefore(elm, oldVnode.nextSibling); 
    parentNode.removeChild(oldVnode);
    return elm;
  }else{ // diff:新老虚拟节点比对
    console.log(oldVnode, vnode)
    if(!isSameVnode(oldVnode, vnode)){// 不是相同节点,不考虑复用直接替换
      return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
    }
  }
}
当包含子组件时,每个组件都有一个 watcher,
将会通过 diff 进行局部更新,并不会做整个树的更新
所以,只要组件拆分合理,一般不会有性能问题

2,是相同节点的情况

如果元素的标签名和 key 都相同,即判定为相同节点,即isSameVnode返回 true;

此时,只需要更新属性即可(文本、样式等)

2-1 文本的处理

  • 文本节点没有标签名
  • 文本节点没有有儿子
// 文本的处理:文本可以直接更新,因为文本没有儿子
// 组件中 Vue.component(‘xxx’);xxx 就是组件的 tag
let el = vnode.el = oldVnode.el;  // 节点复用:将老节点 el 赋值给新节点 el
if(!oldVnode.tag){// 文本:没有标签名
  if(oldVnode.text !== vnode.text){// 内容变更:更新文本内容
    return el.textContent = vnode.text;// 新内容替换老内容
  } else{
    return; 
  }
}

2-2 元素的处理

相同节点且新老节点不都是文本时,会对元素进行处理

需要对 updateProperties 方法进行重构调整:

重构前:直接传入真实元素vnode.el 和 data 属性,进行替换,仅具有渲染功能

// src/vdom/patch.js

export function createElm(vnode) {
  let{tag, data, children, text, vm} = vnode;
  if(typeof tag === 'string'){
    vnode.el = document.createElement(tag)
    updateProperties(vnode.el, data)
    children.forEach(child => {
      vnode.el.appendChild(createElm(child))
    });
  } else {
    vnode.el = document.createTextNode(text)
  }
  return vnode.el;
}

function updateProperties(el, props = {} ) { 
  for(let key in props){
    el.setAttribute(key, props[key])
  }
}
updateProperties 方法的重构方式:
第一个参数是:新的虚拟节点
第二个参数是:老的数据,因为需要对新老数据做 diff 比对

重构后:updateProperties方法,既有渲染功能,又有更新功能

// src/vdom/patch.js

export function createElm(vnode) {
  let{tag, data, children, text, vm} = vnode;
  if(typeof tag === 'string'){
    vnode.el = document.createElement(tag)
    updateProperties(vnode, data) // 修改。。。
    children.forEach(child => {
      vnode.el.appendChild(createElm(child))
    });
  } else {
    vnode.el = document.createTextNode(text)
  }
  return vnode.el;
}

// 1,初次渲染,用oldProps给vnode的 el 赋值即可
// 2,更新逻辑,拿到老的props和vnode中的 data 进行比对
function updateProperties(vnode, oldProps = {} ) { 
  let el = vnode.el; // dom上的真实节点(上边复用老节点时已经赋值了)
  let newProps = vnode.data || {};  // 拿到新的数据
  // 新旧比对:两个对象比对差异
  for(let key in newProps){ // 直接用新的盖掉老的,但还要注意:老的里面有,可能新的里面没有了
    el.setAttribute(key, newProps[key])
  }
  // 处理老的里面有,可能新的里面没有的情况,需要再删掉
  for(let key in oldProps){
    if(!newProps[key]){
      el.removeAttribute(key)
    }
  }
}

测试:节点元素名相同,属性不同

// 调用 updateProperties 属性更新
updateProperties(vnode, oldVnode.data);
let vm1 = new Vue({
    data() {
        return { name: 'Brave' }
    }
})
let render1 = compileToFunction('<div id="a">{{name}}</div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);

let vm2 = new Vue({
    data() {
        return { name: 'BraveWang' }
    }
})
let render2 = compileToFunction('<div class="b">{{name}}</div>');
let newVnode = render2.call(vm2);
setTimeout(() => {
    patch(oldVnode, newVnode); 
}, 1000);

测试结果:

image.png

image.png

2-3 style的处理

除了属性需要更新外,还有其他特殊属性也需要更新,如:style 样式

let vm1 = new Vue({
    data() {
        return { name: 'Brave' }
    }
})
let render1 = compileToFunction('<div style="color:blue">{{name}}</div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);

let vm2 = new Vue({
    data() {
        return { name: 'BraveWang' }
    }
})
let render2 = compileToFunction('<div style="color:red">{{name}}</div>');
let newVnode = render2.call(vm2);
setTimeout(() => {
    patch(oldVnode, newVnode); 
}, 1000);

新老元素都有 style,不能用当前逻辑el.setAttribute(key, newProps[key])来处理

style 中是字符串类型,不能直接做替换,需要对样式属性进行收集,再进行比较和更新

function updateProperties(vnode, oldProps = {} ) {
  let el = vnode.el;
  let newProps = vnode.data || {};

  let newStyly = newProps.style || {};  // 新样式对象
  let oldStyly = oldProps.style || {};  // 老样式对象
  
  // 老样式对象中有,新样式对象中没有,删掉多余样式
  for(let key in oldStyly){
    if(!newStyly[key]){
      el.style[key] = ''
    }
  }
  
  // 新样式对象中有,覆盖到老样式对象中
  for(let key in newProps){
    if(key == 'style'){ // 处理style样式
      for(let key in newStyly){
          el.style[key] = newStyly[key]
      }
    }else{
      el.setAttribute(key, newProps[key])
    }
  }

  for(let key in oldProps){
    if(!newProps[key]){
      el.removeAttribute(key)
    }
  }
}

更新前:

image1.png

更新后:

image2.png

至此,外层的 div 已经实现了 diff 更新,但内层 name 属性还并没有更新

接下来继续对比儿子节点


四,结尾

本篇,介绍了diff算法-节点比对,主要涉及以下几点:

  • 介绍了 diff 算法、对比方式、节点复用
  • 实现了外层节点的 diff 算法
  • 不同节点如何做替换更新
  • 相同节点如何做复用更新:文本、元素、样式处理

下篇,diff算法-比对优化


维护日志:

20210806:调整文章的排版布局;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/170779.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mysql的锁问题:

Mysql的锁问题&#xff1a; 1.1锁的概述&#xff1a; ​ Mysql锁的机制比较简单&#xff0c;不同的存储引擎支持不同的锁机制&#xff1a;MyISAM和MEMORY存储引擎支持表级锁&#xff1b;DBD支持页面锁&#xff0c;但是它也支持表级锁&#xff1b;InnoDB既支持行级锁也支持表级…

23-1-18 文件上传

步骤 file01 / file02 分别是两台java服务 功能: 主要负责接收用户上传的文件存储在指定目录 并记录(上传时间、上传人、文件信息(大小&#xff0c;源文件名&#xff0c;存储后的文件名 ....) 文件权限(共享、私有))。负责接收处理用户的下载请求&#xff0c;用户可以根据文件…

【开发Log】C++QT连连看

1.10开发的初衷是闲来无事开了把宠物连连看&#xff0c;然后发现打了几遍第一关都过不去&#xff0c;于是想自己写个&#xff0c;这样就可以任意使用提示次数了&#xff08;bushi。其实今天正好是老妈生日&#xff0c;问了下老妈她竟然还玩连连看&#xff0c;还ak了orz。于是乎…

分布式理论

目录 1.定义 2.关键技术 3.关键问题 4.基本定理 4.1.CAP定理 4.2.BASE定理 1.定义 分布式的本质是一系列计算机集群通过网络共同完成一串连贯的任务。 2.关键技术 分布式主要关注的几个关键点技术是&#xff1a; 性能容错通信 性能&#xff1a; 可扩展性&#xff0c…

深度学习入门基础CNN系列——池化(Pooling)和Sigmoid、ReLU激活函数

想要入门深度学习的小伙伴们&#xff0c;可以了解下本博主的其它基础内容&#xff1a; &#x1f3e0;我的个人主页 &#x1f680;深度学习入门基础CNN系列——卷积计算 &#x1f31f;深度学习入门基础CNN系列——填充&#xff08;padding&#xff09;与步幅&#xff08;stride&…

Acwing - 算法基础课 - 笔记(数学知识 · 三)(补)

数学知识&#xff08;三&#xff09; 这一小节讲的是高斯消元&#xff0c;组合数。 高斯消元 高斯消元是用来解方程的&#xff0c;通常来说可以在 O(n3)O(n^3)O(n3) 的时间复杂度内&#xff0c;求出包含 n 个未知数的&#xff0c;n个方程的多元线性方程组的解。如下的方程组…

人机界面石油行业应用:一个设备构建石油罐区状态监测系统

一、应用背景 石油罐区是石油石化企业重要的生产设施&#xff0c;负责存储和输送各类油品&#xff0c;而石油罐区状态参数的监控是生产管理的重要部分&#xff0c;不仅可以及时准确地获取现场设备数据&#xff0c;保证罐区的正常运行&#xff0c;还可以防止安全事故的发生。 …

Python如何解决“快手滑块验证码”(4)

前言 本文是该专栏的第32篇,后面会持续分享python的干货知识,记得关注。 很多时候,我们打开一个页面还没开始进行浏览,就跳出一个滑块验证的图片,需要拖到滑块至缺口处,才可以正常浏览。这对于我们正常人浏览页面来说,几乎没什么难度,但是当我们需要用到脚本去实现的时…

本地服务器如何让外网远程桌面连接?

远程访问是远程办公和服务器管理常用的网络应用场景。那么&#xff0c;当我们需要面对远程目标主机是内网服务器电脑时&#xff0c;在不是同个局域网的跨网环境下&#xff0c;内网可以远程控制电脑吗&#xff1f;答案是可以&#xff0c;使用快解析内网映射方案就能实现将本地服…

MyBatis一级缓存 二级缓存

MyBatis一级缓存 二级缓存什么是缓存?一级缓存一级缓存失效的四种情况二级缓存怎样开启二级缓存使二级缓存失效的情况二级缓存相关配置缓存查询的数据顺序整合第三方缓存EHCache&#xff08;代替二级缓存&#xff09;什么是缓存? 这是一个地图软件上的根据城市查询模块,对于那…

Freemarker页面静态化开发

4.5 页面静态化 4.5.1 什么是页面静态化 根据课程发布的操作流程&#xff0c;执行课程发布后要将课程详情信息页面静态化&#xff0c;生成html页面上传至文件系统。 什么是页面静态化&#xff1f; 课程预览功能通过模板引擎技术在页面模板中填充数据&#xff0c;生成html页…

Mybatis Plus轻松上手

Mybatis Plus 今日目标&#xff1a; 了解mybatisplus的特点能够掌握mybatisplus快速入门能够掌握mybatisplus常用注解能够掌握mybatisplus常用的增删改查能够掌握mybatisplus自动代码生成 Mybatis: ✔需要程序员编写sql语句程序员可以干预sql对sql进行调优(优化) MybatisPlu…

强化学习笔记:基于价值的学习方法之价值估计(python实现)

目录 1. 前言 2. 数学原理 3. 代码实现 3.1 游戏设定 3.2 class State 3.3 class Action 3.4 Class Agent 3.5 Class Environment 4. 仿真结果及其分析 4.1 play() 4.2 value_evaluation_all_states(grid, max_steps) 4.3 value_evaluation_one_state(grid, s) 4.4…

ZYNQ FPGA嵌入式开发 - 小梅哥(二)

创建工程打开Xilinx SDK创建工程Next 创建Empty Application添加文件编写代码参考文档 UG585 Zynq 7000 Technical Reference Manual寄存器说明 Appx.B: Registe Detial查看帮助文档Import Examples跨平台使用&#xff1a;头文件&#xff1a;unistd.h 每个平台都会提供sleep() …

论文阅读笔记:Attention is All You Need

论文标题&#xff1a;Attention is All You Need 目录 论文标题&#xff1a;Attention is All You Need 1.摘要 2.前言 3.模型结构 自注意力机制 多头自注意力机制 注意力机制在Transformer中的应用 1.摘要 过去最优的模型是带有attention连接的encoder-decoder模型&…

string的应用和模拟实现(上)

目录 string的应用 insert插入元素 erase删除元素 assign赋值&#xff1a; replace代替函数的一部分 find&#xff1a;从string对象中找元素 c_str:得到c类型的字符串的指针 substr&#xff1a;取部分元素构建成新的string对象 rfind find_first_of:从string查找元素 stri…

JVM【类的加载过程(类的生命周期)详解】

概述 在 Java 中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义&#xff0c;引用数据类型则需要进行类的加载。 按照 Java 虚拟机规范&#xff0c;从 class 文件到加载到内存中的类&#xff0c;到类卸载出内存为止&#xff0c;它的整个生命周期包括如…

软件测试之python学习

1、pycharm的常用配置 1.1修改主题配置 1、点击菜单file,选择settings选项2、选择editor&#xff0c;点击color scheme配色方案3、在右侧选择对应的主题配置1.2修改背景颜色 1、点击菜单file,选择settings选项2、选择appearance&#xff0c;点击Theme 1.3调整字体大小 1、点…

基于K8S+eureka的java应用快速上下线的WEB平台

刚进公司时&#xff0c;由于历史原因&#xff0c;应用发布通过&#xff1a;发布新版&#xff08;新老并存&#xff09;->下线老版->删除老版的方式&#xff0c;每次通过手工处理&#xff0c;蛋疼&#xff08;不方便且高风险&#xff09;。于是马上写了比较直观的脚本方案…

关于java移位运算的一点讨论

框架乱飞的年代&#xff0c;时常还得往框架源码里看&#xff0c;对内在原理没点理解&#xff0c;人家就会认为你不太行。平时开发你可能没咋用过位移运算&#xff0c;但往源码里一看&#xff0c;就时常能看到它。我也是看着看着&#xff0c;突然仔细一琢磨&#xff0c;又不由得…