【手写 Vue2.x 源码】第三十三篇 - diff算法-收尾+阶段性总结

news2024/11/15 7:01:21

一,前言

上篇,diff算法-乱序比对,主要涉及以下几个点:

  • 介绍了乱序比对的方案
  • 介绍了乱序比对的过程分析
  • 实现了乱序比对的代码逻辑

本篇,diff 算法的阶段性梳理


二,初渲染与视图更新流程

  • Vue 初渲染时,会调用 mountComponent 方法进行挂载,在 mountComponent 方法中,会创建一个 watcher;

  • 当数据更新时,进入 Object.defineProperty 的 set 方法,在set 方法中,会调用 dep.notify() 通知收集的 watcher 调用 update 方法做更新渲染;

  • 在 Watcher 类的 update 方法中,调用了 queueWatcher 方法将 watcher 进行缓存、去重操作

  • queueWatcher 方法中调用 flushschedulerQueue 方法,执行所有 watcher.run 并清空队列

  • Watcher类中的 run 方法,内部调用了 Watcher类中的 get 方法:记录当前 watcher 并调用 getter

  • this.getter 是 Watcher类实例化时传入的视图更新方法 fn,即 updateComponent 视图渲染逻辑

  • 执行 updateComponent 中的 vm._render,使用最新数据重新生成虚拟节点并调用 update 更新视图


三,diff 算法的外层更新

在 Vue 中,每次数据变化时,并不会对节点做全量的替换,而是会对新老虚拟节点进行 diff 比对:

  • 首次渲染,根据虚拟节点生成真实节点,替换掉原来的节点
  • 更新渲染,生成新的虚拟节点,并与老的虚拟节点比对,复用老节点进行渲染

diff 算法:

  • 又叫同层比对算法;
  • 深度优先遍历递归;
  • 采用了“头尾指针”的处理;
    通过对新老虚拟节点进行比对,尽可能复用原有节点,以提升渲染性能;

节点可复用的依据:

  • 标签名和 key 均相同,即判定为可复用节点;

patch 方法做节点的递归更新:通过 oldVnode.nodeType 节点类型,判断是否为真实节点;

  • 非真实节点,即为真实dom时,进行初渲染逻辑
  • 是真实节点,需要进行新老虚拟节点比对

新老虚拟节点比对:

  • 节点不相同时,使用新的真实节点:createElm(vnode),替换老的真实节点:oldVnode.el;
    oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
  • 节点相同时,复用老节点,更新文本、样式等属性即可;

文本的处理:

  • 文本节点没有标签名
  • 文本节点没有有儿子

元素的处理:

  • 新老元素都有的属性,用新值覆盖老值;
  • 新的没有,老的有的属性,直接删除掉;

style 的处理:

  • 老样式对象中有,新样式对象中没有,删掉多余样式;
  • 新样式对象中有,覆盖到老样式对象中;

四,diff 算法的比对优化

1,新老儿子节点的情况

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

    处理方法:直接将多余的老 dom 元素删除即可;

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

    处理方法:直接将新的儿子节点放入对应的老节点中即可;

  • 情况 3:新老都有儿子

    处理方法:进行 diff 比对;

2,新老儿子节点的 diff 比对

  • 新老儿子节点的比对,采用了头尾双指针的方法;

  • 新老节点都有儿子时,进行头头、尾尾、头尾、尾头对比;

  • 头头、尾尾、头尾、尾头均没有命中时,进行乱序比对;


五,diff 算法的乱序比对

  • 根据老儿子集合创建一个节点 key 和索引 index 的映射关系 mapping;用新儿子节点依次到 mapping 中查找是否存在可复用的节点;
  • 存在复用节点,更新可复用节点属性并移动到对应位置;(移动走的老位置要做空标记)
  • 不存在复用节点,创建节点并添加到对应位置;
  • 最后,再将不可复用的老节点删除;

六,diff 算法收尾

1,问题分析

至此,已经完成了 diff 算法的全部逻辑编写,但一直使用模拟新老节点更新;

原因在于,每次更新时都执行patch(vm.$el, vnode)

// src/lifecycle.js

export function lifeCycleMixin(Vue){
  Vue.prototype._update = function (vnode) {
    const vm = this;
    // 传入当前真实元素vm.$el,虚拟节点vnode,返回新的真实元素
    vm.$el = patch(vm.$el, vnode);
  }
}

在使用两个虚拟节点模拟 diff 更新时,我们已经修改了 patch 方法:使之既能够支持初渲染,还能支持更新渲染:

// src/vdom/patch.js

/**
 * 将虚拟节点转为真实节点后插入到元素中
 * @param {*} oldVnode  老的虚拟节点
 * @param {*} vnode     新的虚拟节点
 * @returns             新的真实元素
 */
export function patch(oldVnode, vnode) {
  const isRealElement = oldVnode.nodeType;  // 真实节点:1,虚拟节点:无此属性
  if (isRealElement) {// 真实节点
    // 1,根据虚拟节点创建真实节点
    const elm = createElm(vnode);
    // 2,使用真实节点替换掉老节点
    // 找到元素的父亲节点
    const parentNode = oldVnode.parentNode;
    // 找到老节点的下一个兄弟节点(nextSibling 若不存在将返回 null)
    const nextSibling = oldVnode.nextSibling;
    // 将新节点 elm 插入到老节点el的下一个兄弟节点 nextSibling 的前面
    // 备注:若 nextSibling 为 null,insertBefore 等价于 appendChild
    parentNode.insertBefore(elm, nextSibling);
    // 删除老节点 el
    parentNode.removeChild(oldVnode);
    
    return elm;
  } else {
    // diff:新老虚拟节点比对
    if (!isSameVnode(oldVnode, vnode)) {// 同级比较,不是相同节点时,不考虑复用(放弃跨层复用),直接用新的替换旧的
      return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
    }

    // 相同节点,就复用节点(复用老的),再更新不一样的地方(属性),注意文本要做特殊处理,文本是没有标签名的

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

    // 元素的处理:相同节点,且新老节点不都是文本时
    updateProperties(vnode, oldVnode.data);

    // 比较儿子节点
    let oldChildren = oldVnode.children || {};
    let newChildren = vnode.children || {};
    // 情况 1:老的有儿子,新的没有儿子;直接把老的 dom 元素干掉即
    if (oldChildren.length > 0 && newChildren.length == 0) {
      el.innerHTML = '';//暴力写法直接清空;更好的处理是封装removeChildNodes方法:将子节点全部删掉,因为子节点可能包含组件
      // 情况 2:老的没有儿子,新的有儿子;直接将新的插入即可
    } else if (oldChildren.length == 0 && newChildren.length > 0) {
      newChildren.forEach((child) => {// 注意:这里的child是虚拟节点,需要变为真实节点
        let childElm = createElm(child); // 根据新的虚拟节点,创建一个真实节点
        el.appendChild(childElm);// 将生成的真实节点,放入 dom
      })
      // 情况 3:新老都有儿子
    } else {  // 递归: updateChildren 内部调用 patch, patch, 内部还会调用 updateChildren (patch 方法是入口)
      updateChildren(el, oldChildren, newChildren)
    }
    
    return el;// 返回新节点
  }
}

2,正常使用方式

将模拟节点更新的代码全部注释掉,并修改 index.html

<!-- diff算法 -->
<body>
  <!-- 场景:div标签复用,仅更新span标签中的文本 name -->
  <div id="app">
    <span>{{name}}</span>
  </div>
  <script src="./vue.js"></script>
  <script>
    let vm = new Vue({
      el: "#app",
      data() {
        return { name: 'Brave' }
      }
    });
    setTimeout(() => {
      vm.name = "BraveWang";
    }, 1000);
  </script>
</body>

2,测试修改前效果

测试 patch 方法修改前的效果:

image.png

测试结果:将 div 标签全部干掉,重新创建了一次;

原因分析:每次都执行vm.$el = patch(vm.$el, vnode);,没有区分初渲染和更新渲染;

3,如何区分初渲染和更新渲染

如何区分初渲染和更新渲染?

  • 第一次渲染时,在 vm.preVnode 上保存当前 Vnode
  • 第二次渲染时,先取 vm.preVnode,有值就是更新渲染
  • 初渲染,执行patch(vm.$el, vnode)
  • 更新渲染,执行patch(preVnode, vnode)

4,代码实现

export function lifeCycleMixin(Vue){
  Vue.prototype._update = function (vnode) {
    const vm = this;
    // 取上一次的 preVnode
    let preVnode = vm.preVnode;
    // 渲染前,先保存当前 vnode
    vm.preVnode = vnode;
    // preVnode 有值,说明已经有节点了,本次是更新渲染;没值就是初渲染
    if(!preVnode){// 初渲染
      // 传入当前真实元素vm.$el,虚拟节点vnode,返回新的真实元素
      vm.$el = patch(vm.$el, vnode);
    }else{// 更新渲染:新老虚拟节点做 diff 比对
      vm.$el = patch(preVnode, vnode);
    }
  }
}

5,测试修改后的效果:

测试 patch 方法修改后的效果:

image.png

测试结果:div 标签被复用,只更新了 span 中的name;


七,结尾

本篇,diff算法阶段性梳理,主要涉及以下几个点:

  • 初渲染与视图更新流程;
  • diff 算法的外层更新;
  • diff 算法的比对优化;
  • diff 算法的乱序比对;
  • 初渲染和更新渲染判断;

下篇,组件的初始化流程介绍;


更新日志

20210807:添加“diff 算法收尾”部分;更新“结尾”部分;更新文章标题和摘要;

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

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

相关文章

注册商标需要哪些材料和条件?

申请注册商标条件是什么1、申请人必须是申请认定商标的所有人&#xff0c;是在当省区域内的自然人、法人和其他组织;2、该商标自核准注册之起连续使用满三年并继续有效&#xff0c;且无权属争议;3、该商标为相关公众所熟知&#xff0c;在相关市场内具有较高的知名度;4、该商标核…

亚信科技AntDB数据库荣获2022年度技术卓越奖

近日&#xff0c;业界知名IT垂直媒体IT168发布了“2022技术卓越奖”主题奖项&#xff0c;亚信科技AntDB数据库荣获技术卓越奖。 2022 “技术卓越奖”由行业CIO/CTO大咖、技术专家及IT媒体三方联合评选&#xff0c;评判标准代表了用户和媒体声音。经过多方评审&#xff0c;亚信科…

jvm参数简介

Xmx3550m&#xff1a;设置JVM最大堆内存为3550M。 -Xms3550m&#xff1a;设置JVM初始堆内存为3550M。此值可以设置与-Xmx相同&#xff0c;以避免每次垃圾回收完成后JVM重新分配内存。 -Xss128k&#xff1a;设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M&#xff0c;之…

【SCL】1200应用案例:交通灯模拟自动装料控制

使用博图SCL语言来编写 交通灯模拟控制 和 自动装料应用案例 文章目录 目录 前言 一、应用&#xff1a;交通灯模拟控制 1.控制要求 2.I\o分配和接线 3.程序编写和效果 4.小结 二、自动装料模拟控制 1.控制要求 2.I/O分配 3.程序编写 4.小结 总结 前言 本篇文章我们继续学习西…

宏任务和微任务

宏任务和微任务1. 什么是宏任务和微任务2. 宏任务和微任务的执行顺序3. 去银行办业务的场景4. 分析以下代码输出的顺序5. 经典面试题1. 什么是宏任务和微任务 JavaScript 把异步任务又做了进一步的划分&#xff0c;异步任务又分为两类&#xff0c;分别是&#xff1a; ① 宏任…

寄存器、RAM、ROM、Flash

单片机寄存器简述 寄存器详细请点这里 1、单片机寄存器就是单片机片内存储器&#xff08;片内RAM)一部分&#xff0c;每一个都有地址。只不过这几个寄存器有特殊的作用&#xff0c;比如指令&#xff1a;MUL AB,这条指令用到两个寄存器A,B进行乘法&#xff0c;结果存到BA里面&a…

kaggle竞赛 | Quora Insincere Question | 文本情感分析

目录赛题背景赛题评价指标数据集分析pytorch建模之前发布了一遍实战类的情感分析的文章&#xff0c;包括微博爬虫&#xff0c;数据分析&#xff0c;相关模型。 可以参考&#xff1a; https://blog.csdn.net/lijiamingccc/article/details/126963413 比赛链接&#xff1a; http…

Spring Boot学习篇(十二)

Spring Boot学习篇(十二) shiro安全框架使用篇(四) 2 在主页显示用户登录状态、用户信息和完成默认注销(不改shiro原来的配置)操作 2.1 变更SysUserController类 2.1.1 在SysUserController类中注入sysUserMapper Autowired SysUserMapper sysUserMapper;2.1.2 在SysUserC…

1598_AURIX_TC275_GPIO功能以及部分寄存器梳理1

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 接下来&#xff0c;看一下GPIO的寄存器以及部分相关的功能。这部分将会是接下来这个章节剩余的全部&#xff0c;可能内容偏雷同&#xff0c;因此都是跳跃式看。但是中间需要临时关注一下的…

【2022年MathorCup大数据竞赛】B题:北京移动用户体验影响因素研究(二)(问题一的分析和结果)

目录&#xff1a;题目解析一、问题的解答框架二、问题一的分析2.1 附件1的处理流程2.2 附件2的处理流程2.2.1 拉格朗日插补法2.3 数据编码2.4 相关分析2.5 基于互信息GBDT的特征提取2.6 量化分析一、问题的解答框架 二、问题一的分析 针对问题一&#xff0c;首先需要对附件1和…

《MySQL高级篇》十二、MySQL事务日志

文章目录1. redo日志1.1 为什么需要REDO日志1.2 REDO日志的好处、特点1. 好处2. 特点1.3 redo的组成1.4 redo的整体流程1.5 redo log的刷盘策略1.6 不同刷盘策略演示1. 刷盘策略分析2. 举例1.7 写入redo log buffer 过程1. 补充概念:Mini-Transaction2. redo 日志写入log buffe…

「链表」数据结构简析

前言 前言&#xff1a;研究一个数据结构的时候&#xff0c;首先讲的是增删改查。 文章目录前言一、链表简介1. 含义2. 节点组成3. 存储方式1&#xff09;数据在内存中的存储方式2&#xff09;单链表在内存中的存储方式2&#xff09;双链表在内存中的存储方式2&#xff09;循环链…

程序地址空间

目录 1. 验证程序地址空间布局图 2. 虚拟地址空间 什么是虚拟地址空间 3. 进程地址空间 4. 为什么要有虚拟地址空间 1. 有效保护物理内存 2. 使内存管理模块和进程管理模块实现解耦合 3. 将内存分布有序化 1. 验证程序地址空间布局图 下面我们写段代码验证一下上图中…

qt调用matlab生成的dll库

最近由于在项目中要用到matlab的算法&#xff0c;而用C转换matlab算法非常麻烦&#xff0c;所以采用qtmatlab混合编程的方法&#xff0c;在使用中遇到了些许问题&#xff0c;特记录如下。 一、生成matlab库 1、首先需要下载matlab完整版&#xff0c;之前在网上下载的简版&…

基于C#制作一个休息提醒闹钟

> 此文主要通过WinForm来制作一个休息提醒闹钟&#xff0c;通过设置时间间隔进行提醒&#xff0c;避免沉浸式的投入到工作或者学习当中&#xff0c;战斗的同时也要照顾好自己。 实现流程1.1、创建项目1.2、时间间隔配置页1.3、闹钟提醒页1.4、开机自启动配置1.5、日志记录1.…

一个数据库文档生成神器

Gitee项目地址&#xff0c;可以直接去开源项目查看&#xff08;推荐&#xff09; 简介 在企业级开发中、我们经常会有编写数据库表结构文档的时间付出&#xff0c;从业以来&#xff0c;待过几家企业&#xff0c;关于数据库表结构文档状态&#xff1a;要么没有、要么有、但都是…

MySql 5.7.40备份到腾讯云cos+从cos恢复

1 备份 1.1 安装coscli # wget https://github.com/tencentyun/coscli/releases/download/v0.12.0-beta/coscli-linux # mv coscli-linux /usr/bin/coscli # chmod 755 /usr/bin/coscli # coscli --version如果github慢可以使用国内镜像&#xff1a; wget https://cosbrowse…

数电相关知识

文章目录 逻辑电路与或非异或 门电路与的物理电路电压比较器D型锁存器优先编码器边沿触发器RS触发器施密特触发器基本原理555定时器数电电平TTL器件CMOS器件逻辑电路 与或非异或 门电路 与乘大于1或加大于1异或异性为1,异吗? 与的物理电路

Leetcode:17. 电话号码的字母组合(C++)

目录 问题描述&#xff1a; 实现代码与解析&#xff1a; 回溯&#xff1a; 原理思路&#xff1a; 问题描述&#xff1a; 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&…

【数据库】必须知道的MySQL优化

文章目录SQL语言有哪几部分组成为什么要进行MySQL优化&#xff1f;优化方法有哪些&#xff1f;SQL层面优化MySQL配置方面架构设计方面硬件和操作系统方面.SQL语言有哪几部分组成 数据定义语言&#xff0c;简称DDL&#xff1a;DROP,CREATE,ALTER等语句。数据操作语言&#xff0…