【手写 Vue2.x 源码】第三十篇 - diff算法-比对优化(上)

news2024/11/15 8:30:31

一,前言

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

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

本篇,diff算法-比对优化


二,比对儿子节点

1,前文回顾

上篇,通过构建两个虚拟节点来模拟 v-if 的效果,通过 patch 方法比对实现了外层节点的复用

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);

执行结果:

初始化时为蓝色文本

image.png

更新后变为红色文本

image.png

发现问题:

仅更新了外层 div 的 style,但 name 并没有更新为 BraveWang,
即只做了第一层节点的比对和属性更新,没有进行深层的 diff 比对

2,如何比对儿子节点

把“新的儿子节点”和“老的儿子节点”都拿出来,依次进行比对

//src/vdom/patch.js

/**
 * 将虚拟节点转为真实节点后插入到元素中
 * @param {*} el    当前真实元素 id#app
 * @param {*} vnode 虚拟节点
 * @returns         新的真实元素
 */
export function patch(oldVnode, vnode) {
  const isRealElement = oldVnode.nodeType;
  if(isRealElement){
    // 1,根据虚拟节点创建真实节点
    const elm = createElm(vnode);
    // 2,使用真实节点替换掉老节点
    const parentNode = oldVnode.parentNode;
    parentNode.insertBefore(elm, oldVnode.nextSibling); 
    parentNode.removeChild(oldVnode);
    return elm;
  }else{// diff:新老虚拟节点比对
    if(!isSameVnode(oldVnode, vnode)){
      return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
    }
    let el = vnode.el = oldVnode.el;
    if(!oldVnode.tag){
      if(oldVnode.text !== vnode.text){
        return el.textContent = vnode.text;
      }else{
        return; 
      }
    }
    updateProperties(vnode, oldVnode.data);

    // TODO:比较儿子节点...
    let oldChildren = oldVnode.children || {};
    let newChildren = vnode.children || {};
  }
}

3,新老儿子节点的几种情况

  • 情况 1:老的有儿子,新的没有儿子
  • 情况 2:老的没有儿子,新的有儿子
  • 情况 3:新老都有儿子

情况 1:老的有儿子,新的没有儿子

处理方法:直接将多余的老 dom 元素删除即可
// src/vdom/patch.js#patch

...
// 比较儿子节点
let oldChildren = oldVnode.children || {};
let newChildren = vnode.children || {};
    
// 情况 1:老的有儿子,新的没有儿子;直接将多余的老 dom 元素删除即可;
if(oldChildren.length > 0 && newChildren.length == 0){
  // 更好的处理:由于子节点中可能包含组件,需要封装removeChildNodes方法,将子节点全部删掉
  el.innerHTML = '';// 暴力写法直接清空;
}

备注:这里直接清空innerHTML是暴力写法;由于子节点中可能包含组件,所以更好的处理方式是封装一个 removeChildNodes 方法,用于删掉全部子节点

测试方法:

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"></div>');
let newVnode = render2.call(vm2);

setTimeout(() => {
    patch(oldVnode, newVnode); 
}, 1000);

情况 2:老的没有儿子,新的有儿子

处理方法:直接将新的儿子节点放入对应的老节点中即可;
//src/vdom/patch.js#patch

...
// 比较儿子节点
let oldChildren = oldVnode.children || {};
let newChildren = vnode.children || {};

// 情况 1:老的有儿子,新的没有儿子;直接将多余的老 dom 元素删除即可;
if(oldChildren.length > 0 && newChildren.length == 0){
  el.innerHTML = '';
// 情况 2:老的没有儿子,新的有儿子;直接将新的儿子节点放入对应的老节点中即可
}else if(oldChildren.length == 0 && newChildren.length > 0){
  newChildren.forEach((child)=>{// 注意:这里的child是虚拟节点,需要变为真实节点
    let childElm = createElm(child); // 根据新的虚拟节点,创建一个真实节点
    el.appendChild(childElm);// 将生成的真实节点,放入 dom
  })
}

备注:newChildren中的child为虚拟节点,需要先通过createElm(child)创建为真实节点

测试:

let vm1 = new Vue({
    data() {
        return { name: 'Brave' }
    }
})
let render1 = compileToFunction('<div style="color:blue"></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);

情况 3:新老都有儿子

处理方法:进行 diff 比对
// src/vdom/patch.js#patch

...
// 比较儿子节点
let oldChildren = oldVnode.children || {};
let newChildren = vnode.children || {};

// 情况 1:老的有儿子,新的没有儿子;直接将对于的老 dom 元素干掉即可;
if(oldChildren.length > 0 && newChildren.length == 0){
  el.innerHTML = '';
// 情况 2:老的没有儿子,新的有儿子;直接将新的儿子节点放入对应的老节点中即可
}else if(oldChildren.length == 0 && newChildren.length > 0){
  newChildren.forEach((child)=>{
    let childElm = createElm(child);
    el.appendChild(childElm);
  })
// 情况 3:新老都有儿子
}else{
  // diff 比对的核心逻辑
  updateChildren(el, oldChildren, newChildren); 
}

这里对“老的有儿子,新的没有儿子”和“老的没有儿子,新的有儿子”两种特殊情况做了特殊的处理
接下来,当新老节点都有儿子时,就必须进行 diff 比对了;
所以,updateChildren 才是 diff 算法的核心


三,新老儿子 diff 比对的核心逻辑 updateChildren 方法

1,新老儿子 diff 比对方案介绍

继续,当新老节点都有儿子时,就需要对新老儿子节点进行比对了

新老节点的比对方案是:采用头尾双指针的方式,进行新老虚拟节点的依次比对

每次节点比对完成,如果是头节点就向后移动指针,尾节点就向前移动指针;

image.png

直至一方遍历完成,比对才结束;

即:“老的头指针和尾指针重合"或"新的头指针和尾指针重合”;

image.png

这里,为了能够提升diff算法的性能,并不会直接全部采用最耗性能的“乱序比对”

而是结合了日常使用场景,优先对4种特殊情况进行了特殊的除了:头头、尾尾、头尾、尾头

  • 头和头比较,将头指针向后移动;
  • 尾和尾比较,将尾指针向前移动;
  • 头和尾比较,将头指针向后移动,尾指针向前移动;
  • 尾和尾比较,将尾指针向后移动,头指针向前移动;

每次比对时,优先进行头头、尾尾、头尾、尾头的比对尝试,如果都没有命中才会进行乱序比较

2,diff 比对的几种特殊情况(头头、尾尾、头尾、尾头)

备注:由于 4 种情况需要画图说明,单独一篇:第三十一篇 - diff算法-比对优化(下)

除了这 4 钟特殊情况外,就只能进行乱序比对了

虽然是做乱序比对,但目标依然是最大程度实现节点复用,提升渲染性能;

备注:乱序比对如何进行节点复用,单独一篇:第三十二篇 - diff算法-乱序比对

四,结尾

本篇,diff算法-比对优化(上),主要涉及以下几个点:

  • 介绍了如何进行儿子节点比对;
  • 新老儿子节点可能存在的3种情况及代码实现;
  • 新老节点都有儿子时的 diff 方案介绍与处理逻辑分析;

下篇,diff算法-比对优化(下)

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

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

相关文章

在线教育-谷粒学院学习笔记(五)

文章目录1 内容介绍2 课程分类前端实现3 课程列表功能实现4 课程管理概括5 添加课程信息后端实现6 添加课程信息前端实现7 前端完善1 内容介绍 添加课程分类前端实现课程分类列表显示功能&#xff08;树形结构&#xff09;课程管理模块需求添加课程基本信息功能 2 课程分类前…

如何设置OpenFeign请求超时

Feign和OpenFeign介绍 Feign集成了Ribbon、RestTemplate实现了负载均衡的执行Http调用&#xff0c;只不过对原有的方式&#xff08;RibbonRestTemplate&#xff09;进行了封装&#xff0c;开发者不必手动使用RestTemplate调服务&#xff0c;而是定义一个接口&#xff0c;在这个…

byzer笔记本使用

byzer笔记本使用 数据源的定义 对于etl&#xff0c;起始的第一步往往是定义一个数据来源。 设置数据源 通过设置 -> 外部数据源 -> 新增可以新增jdbc类型的数据源。 命令指定数据源 notebook中可以使用connect命令进行jdbc数据源的指定: > SET user"root&q…

Elasticsearch(四)--一文弄懂ES的映射操作

一、前言 上一章学习了ES的索引相关操作&#xff0c;那么这一章就轮到映射&#xff0c;了解映射操作最重要的点就是去学习ES的数据类型。那么本章我们会了解到映射的创建、查看和修改操作&#xff0c;然后详细介绍ES中的基本数据类型和复杂的数据类型&#xff0c;并且会对常用…

解决ElementUI导航栏重复点菜单报错问题

在使用ElementUI中的导航时&#xff0c;默认情况下如果重复点击某选项&#xff0c;会报错。 element-ui.common.js?b705:3354 Error: Avoided redundant navigation to current location: “/home/home1”. at createRouterError (vue-router.esm.js?8c4f:2060) at createNa…

Java IO流 - 打印流详细使用介绍

文章目录打印流打印流基本使用输出语句重定向打印流 打印流基本使用 打印流: 作用&#xff1a;打印流可以实现更方便、更高效的打印数据到文件中去。打印流一般是指&#xff1a;PrintStream&#xff0c;PrintWriter两个类。 可以实现打印什么数据就是什么数据&#xff0c;例如…

【GD32F427开发板试用】06-硬件I2C软件I2C驱动0.91OLED

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;Stark_GS I2C 简介及特点 并行总线至 I2C 总线协议的转换及接口&#xff1b;同一接口既可实现主机功能又可实现从机功能&#xff1b;主从机之…

在线教育-谷粒学院学习笔记(四)

文章目录1 内容介绍2 阿里云存储OSS3 Java代码操作阿里云oss4 搭建阿里云oss项目环境5 上传头像后端实现6 Nginx使用7 上传头像前端实现8 课程分类管理介绍9 EasyExcel工具10 课程分类添加功能1 内容介绍 添加讲师实现头像上传功能 阿里云oss存储服务 添加课程分类功能 使用Ea…

欧拉的“她力量”,如何为品牌注入新能量?

文|智能相对论作者| Kinki近日&#xff0c;百度营销联合CBNData推出的《2022新能源汽车趋势洞察》正式发布&#xff0c;报告显示&#xff0c;随着新能源汽车的普及&#xff0c;新中产女性已成为了“消费新势力”。女性更偏爱新能源汽车已不是新鲜观点&#xff0c;调研显示&…

物理主机telenet登录ensp虚拟网络设备并显示配置python脚本

一、物理主机telnet登录ensp虚拟网络设备 1、物理主机配置 1.1、物理主机环回口虚拟网卡配置 见本人博客:Ensp用windows回环口连接cloud配置_ensp环回口配置_林锋Space的博客-CSDN博客https://liulinfeng.blog.csdn.net/article/details/128098737 1.2、物理主机telnet开启 …

大数据的整体见解

如何建设高效的数据模型体系&#xff0c;使数据易用&#xff0c;避免重复建设和数据不一致性&#xff0c;保证数据的规范性&#xff1b;如何提供高效易用的数据开发工具&#xff1b;如何做好数据质量保障&#xff1b;如何有效管理和控制日益增长的存储和计算消耗&#xff1b;如…

文件下载 response响应ContentType与a标签download属性

参考资料 MediaType介绍了解HTML/HTML5中的download属性header中Content-Disposition的作用 目录一. ContentType二. a标签download属性2.1 下载同源静态资源文件2.2 后台可省略Content-Disposition一. ContentType 我们在进行文件下载的时候&#xff0c;后端往往需要通过如下…

CSRF(跨站请求伪造)

一、csrf是什么CSRF&#xff08;Cross Site Request Forgery&#xff0c;跨站请求伪造&#xff09;。是一种对网站的恶意利用&#xff0c;通过伪装来自受信任用户的请求来利用受信任的网站。原理是攻击者构造网站后台某个功能接口的请求地址&#xff0c;诱导用户去点击或者用特…

uni-app 中实现文件和图片的上传-H5

之前写过一篇上传的文章&#x1f4d5;&#xff0c;但是那篇文章仅仅只能实现上传图片的功能&#xff0c;而且代码写的比较乱&#xff0c;看起来很繁杂&#xff0c;最近有幸又遇到了上传图片和文件的需求&#xff0c;在完成这个功能后&#xff0c;整理一下&#xff0c;希望能给需…

第十四届蓝桥杯单片机组学习笔记(2):按键

第十四届蓝桥杯单片机组学习笔记&#xff08;2&#xff09;&#xff1a;按键前言区分高低电平驱动按键消抖软件消抖触发处理的方式矩阵键盘最简单常用的人机交互手段——按键 前言 实现按键检测需要解决的问题&#xff1a; 按键是低电平按下还是高电平按下&#xff1b;按键消…

(1分钟速览)KBM-SLAM 论文阅读笔记

编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;这篇博客记录了上面这篇论文的学习笔记。编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;这个是本文的摘要&#xff0c;一眼开过去就是单目slam当今所遇到…

机器学习(一):人工智能概述

文章目录 人工智能概述 一、人工智能应用场景 二、人工智能小案例 三、人工智能发展必备三要素 四、人工智能、机器学习和深度学习 人工智能概述 一、人工智能应用场景 二、人工智能小案例 案例一 学习链接&#xff1a;https://quickdraw.withgoogle.com 案例二 学习链…

老杨说运维 | 2023,浅谈智能运维趋势(三)

文末附有视频回顾 前言&#xff1a; 在回顾&#xff08;一&#xff09;中&#xff0c;老杨提到的智能运维发展趋势中&#xff0c;面对国际化形势不确定的情况&#xff0c;信创部分的比例要求正在递增。作为国家经济发展的新动能&#xff0c;信创发展已步入深水区&#xff0c;智…

cv2.imread()、cv2.putText、cv2.imwrite()、cv2.waitKey()

cv2cv2.imread()cv2.putText&#xff08;&#xff09;cv2.imwrite&#xff08;&#xff09;cv2.waitKey()cv2.imread() 用于读取图像数据案例演示&#xff1a; import cv2# ouput img properties img_pathC:/Users/WHY/Pictures/Saved Pictures/OIP-C (1).jfif def funOutput…

安卓手机使用Linux Deploy安装CentOS

目录 前言 准备工作 实践 busybox安装 安装并配置Linux Deploy 局域网使用ssh连接服务器 公网使用ssh连接服务器 环境安装 写在最后 前言 最近沉迷于服务器搭建&#xff0c;书接上文&#xff0c;在安卓高版本中成功安装了服务器&#xff0c;但是安卓低版本的却一言难尽…