【手写 Vue2.x 源码】第二十八篇 - diff算法-问题分析与patch优化

news2024/11/26 16:57:03

一,前言

首先对 6 月更文内容做一下简单的回顾:

  • Vue2.x 源码环境的搭建
  • Vue2.x 初始化流程介绍
  • 对象的单层、深层劫持
  • 数组的单层、深层劫持
  • 数据代理的实现
  • 对象、数组数据变化的观测
  • Vue 数据渲染流程介绍
  • 模板生成 AST 语法树
  • AST 语法树生成 render 函数
  • render 函数生成 Vnode
  • 根据 Vnode 创建真实节点
  • 真实节点替换原始节点
  • Vue2.x 依赖收集的流程分析
  • 依赖收集和视图更新流程(dep 和 watcher 关联)
  • 异步更新流程说明
  • 数组的依赖收集
  • Vue 生命周期和 Mixin 的实现

本篇开始,继续Vue2.x源码的diff算法部分;


二,当前版本存在的问题

1,初始化与更新流程分析

Vue 初始化,会在挂载时调用 mountComponent 方法

// src/init.js

Vue.prototype.$mount = function (el) {
    const vm = this;
    const opts = vm.$options;
    el = document.querySelector(el); // 获取真实的元素
    vm.$el = el; // vm.$el 表示当前页面上的真实元素

    // 如果没有 render, 看 template
    if (!opts.render) {
      // 如果没有 template, 采用元素内容
      let template = opts.template;
      if (!template) {
        // 拿到整个元素标签,将模板编译为 render 函数
        template = el.outerHTML;
      }
      let render = compileToFunction(template);
      opts.render = render;
    }

    mountComponent(vm);
  }

在 mountComponent 方法中,会创建一个 watcher

// src/lifeCycle.js

export function mountComponent(vm) {

  let updateComponent = ()=>{
    vm._update(vm._render());  
  }
  // 当视图渲染前,调用钩子: beforeCreate
  callHook(vm, 'beforeCreate');

  // 渲染 watcher :每个组件都有一个 watcher
  new Watcher(vm, updateComponent, ()=>{
    // 视图更新后,调用钩子: created
    callHook(vm, 'created');
  },true)

   // 当视图挂载完成,调用钩子: mounted
   callHook(vm, 'mounted');
}

数据更新时,会进入 set 方法

// src/observe/index.js

function defineReactive(obj, key, value) {
  // childOb 是数据组进行观测后返回的结果,内部 new Observe 只处理数组或对象类型
  let childOb = observe(value);// 递归实现深层观测
  let dep = new Dep();  // 为每个属性添加一个 dep
  Object.defineProperty(obj, key, {
    // get方法构成闭包:取obj属性时需返回原值value,
    // value会查找上层作用域的value,所以defineReactive函数不能被释放销毁
    get() {
      if(Dep.target){
        // 对象属性的依赖收集
        dep.depend();
        // 数组或对象本身的依赖收集
        if(childOb){  // 如果 childOb 有值,说明数据是数组或对象类型
          // observe 方法中,会通过 new Observe 为数组或对象本身添加 dep 属性
          childOb.dep.depend();    // 让数组和对象本身的 dep 记住当前 watcher
          if(Array.isArray(value)){// 如果当前数据是数组类型
            // 可能数组中继续嵌套数组,需递归处理
            dependArray(value)
          }  
        }
      }
      return value;
    },
    set(newValue) { // 确保新对象为响应式数据:如果新设置的值为对象,需要再次进行劫持
      console.log("修改了被观测属性 key = " + key + ", newValue = " + JSON.stringify(newValue))
      if (newValue === value) return
      observe(newValue);  // observe方法:如果是对象,会 new Observer 深层观测
      value = newValue;
      dep.notify(); // 通知当前 dep 中收集的所有 watcher 依次执行视图更新
    }
  })
}

此时,会调用 dep.notify() 通知对应的 watcher 调用 update 方法做更新

class Dep {
  constructor(){
    this.id = id++;
    this.subs = [];
  }
  // 让 watcher 记住 dep(查重),再让 dep 记住 watcher
  depend(){
    Dep.target.addDep(this);  
  }
  // 让 dep 记住 watcher - 在 watcher 中被调用
  addSub(watcher){
    this.subs.push(watcher);
  }
  // dep 中收集的全部 watcher 依次执行更新方法 update
  notify(){
    this.subs.forEach(watcher => watcher.update())
  }
}

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

// src/observe/watcher.js

class Watcher {
  constructor(vm, fn, cb, options){
    this.vm = vm;
    this.fn = fn;
    this.cb = cb;
    this.options = options;

    this.id = id++;   // watcher 唯一标记
    this.depsId = new Set();  // 用于当前 watcher 保存 dep 实例的唯一id
    this.deps = []; // 用于当前 watcher 保存 dep 实例
    this.getter = fn; // fn 为页面渲染逻辑
    this.get();
  }
  addDep(dep){
    let did = dep.id;
    // dep 查重 
    if(!this.depsId.has(did)){
      // 让 watcher 记住 dep
      this.depsId.add(did);
      this.deps.push(dep);
      // 让 dep 也记住 watcher
      dep.addSub(this); 
    }
  }
  get(){
    Dep.target = this;  // 在触发视图渲染前,将 watcher 记录到 Dep.target 上
    this.getter();      // 调用页面渲染逻辑
    Dep.target = null;  // 渲染完成后,清除 Watcher 记录
  }
  update(){
    console.log("watcher-update", "查重并缓存需要更新的 watcher")
    queueWatcher(this);
  }
  run(){
    console.log("watcher-run", "真正执行视图更新")
    this.get();
  }
}

queueWatcher 方法:

// src/observe/scheduler.js

/**
 * 将 watcher 进行查重并缓存,最后统一执行更新
 * @param {*} watcher 需更新的 watcher
 */
export function queueWatcher(watcher) {
  let id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    queue.push(watcher);  // 缓存住watcher,后续统一处理
    if (!pending) {       // 等效于防抖
      nextTick(flushschedulerQueue);
      pending = true;     // 首次进入被置为 true,使微任务执行完成后宏任务才执行
    }
  }
}

/**
 * 刷新队列:执行所有 watcher.run 并将队列清空;
 */
function flushschedulerQueue() {
  // 更新前,执行生命周期:beforeUpdate
  queue.forEach(watcher => watcher.run()) // 依次触发视图更新
  queue = [];       // reset
  has = {};         // reset
  pending = false;  // reset
  // 更新完成,执行生命周期:updated
}

flushschedulerQueue 方法执行时,会调用 watcher 的 run 方法

run 内部调用watcher 的 get 方法,get方法中记录当前 watcher 并调用 getter

this.getter 即 watcher 初始化时传入的视图更新方法 fn,

即 updateComponent 视图渲染逻辑

// src/lifeCycle.js

export function mountComponent(vm) {

  let updateComponent = ()=>{
    vm._update(vm._render());  
  }
  // 当视图渲染前,调用钩子: beforeCreate
  callHook(vm, 'beforeCreate');

  // 渲染 watcher :每个组件都有一个 watcher
  new Watcher(vm, updateComponent, ()=>{
    // 视图更新后,调用钩子: created
    callHook(vm, 'created');
  },true)

   // 当视图挂载完成,调用钩子: mounted
   callHook(vm, 'mounted');
}

这样,就会再次执行 updateComponent->vm._render,

会根据当前的最新数据,重新生成虚拟节点,并且再次调用 update

// src/lifeCycle.js

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

附一张 Vue 流程图:

vue 流程图.png

2,问题分析与优化思路

update 方法会使用新的虚拟节点重新生成真实 dom,并替换掉原来的dom

在 Vue 的实现中,会做一次 diff 算法优化:尽可能复用原有节点,以提升渲染性能

所以,patch方法即为重点优化对象:

当前的 patch 方法,仅考虑了初始化的情况,还需要处理更新操作
patch 方法需要对新老虚拟节点进行一次比对,尽可能复用原有节点,以提升渲染性能
  • 首次渲染,根据虚拟节点生成真实节点,替换掉原来的节点
  • 更新渲染,生成新的虚拟节点,并和老的虚拟节点进行对比,再渲染

三,模拟新老虚拟节点比对

模拟两个虚拟节点的比对:

  • 生成虚拟节点1
  • 生成虚拟节点2
  • 调用 patch 方法进行新老虚拟节点比对

1,生成第一个虚拟节点

首次,生成虚拟节点后,直接进行挂载

// src/index.js

// 1,生成第一个虚拟节点
// new Vue会对数据进行劫持
let vm1 = new Vue({
    data(){
        return {name:'Brave'}
    }
})
// 将模板 render1 生成为 render 函数
let render1 = compileToFunction('<div>{{name}}</div>');// 调用 compileToFunction,将模板生成 render 函数,会解析模板,最终包成一个 function
// 调用 render 函数,产生虚拟节点
let oldVnode = render1.call(vm1)    // oldVnode:第一次的虚拟节点
// 将虚拟节点生成真实节点
let el1 = createElm(oldVnode);
// 将真实节点渲染到页面上
document.body.appendChild(el1);

2,生成第二个虚拟节点

// src/index.js

// 2,生成第二个虚拟节点
let vm2 = new Vue({
    data(){
        return {name:'BraveWang'}
    }
})
let render2 = compileToFunction('<p>{{name}}</p>');
let newVnode = render2.call(vm2);

// 延迟看效果:初始化完成显示 el1,1 秒后移除 el1 显示 el2
setTimeout(()=>{
    let el2 = createElm(newVnode);
    document.body.removeChild(el1);
    document.body.appendChild(el2);
}, 1000);

export default Vue;

3,patch 方法比对新老虚拟节点

patch 方法:将新老虚拟节点进行一次比对,尽可能复用原有节点,以提升渲染性能

节点复用逻辑:标签名和key相同即判定可复用

// 如果标签名一样就复用
// 3,调用 patch 方法进行比对
setTimeout(()=>{
    // 比对新老虚拟节点的差异,尽可能复用原有节点,以提升渲染性能
    patch(oldVnode,newVnode); 
}, 1000);

4,查看新老节点

let vm = new Vue({
    data(){
        return {name:'Brave'}
    }
})
let render = compileToFunction('<div>{{name}}</div>');/
let oldVnode = render.call(vm)
let el = createElm(oldVnode);
document.body.appendChild(el);

// 数据更新后,再次调用 render 函数
vm.name = 'BraveWang';
let newVnode = render.call(vm);

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

查看生成的两个真实节点
image.png

接下来开始改造patch方法,以实现节点对比和复用


四,patch 方法优化

1,当前的 patch 方法

当前的 patch 方法仅考虑到初始化的情况,所以每次都会直接替换掉老节点

export function patch(el, vnode) {
  // 1,根据虚拟节点创建真实节点
  const elm = createElm(vnode);
  // 2,使用真实节点替换掉老节点
  // 找到元素的父亲节点
  const parentNode = el.parentNode;
  // 找到老节点的下一个兄弟节点(nextSibling 若不存在将返回 null)
  const nextSibling = el.nextSibling;
  // 将新节点elm插入到老节点el的下一个兄弟节点nextSibling的前面
  // 备注:若nextSibling为 null,insertBefore 等价与 appendChild
  parentNode.insertBefore(elm, nextSibling); 
  // 删除老节点 el
  parentNode.removeChild(el);

  return elm;
}

2,改造 patch 方法

当前patch方法的两个入参分别是:元素和虚拟节点
将虚拟节点创建为真实节点,直接进行元素替换,完成数据更新

现在需要将新老虚拟节点进行比对,尽可能复用原有节点,提高渲染性能
所以patch方法需改造为入参是新老虚拟节点:oldVnode、vnode

当前的 patch 方法仅考虑到初始化的情况;
现在还需要支持数据更新的情况;
export function patch(oldVnode, vnode) {
  const elm = createElm(vnode);
  const parentNode = oldVnode.parentNode;
  parentNode.insertBefore(elm, oldVnode.nextSibling); 
  parentNode.removeChild(oldVnode);
  
  return elm;
}

问题:初渲染 OR 更新渲染?

通过判断 oldVnode.nodeType 节点类型是否为真实节点;
是真实节点,需要进行新老虚拟节点比对
非真实节点,即为真实dom时,进行初渲染逻辑

改造完成后的 patch 方法:

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

后边开始针对更新渲染的情况,进行新老虚拟节点的比对,即 diff 算法逻辑


五,结尾

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

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

下篇,diff 算法-节点比对


维护日志

20210802:添加“四,patch 方法优化”;添加 Vue 执行流程图;更新文章标题和摘要;
20210806:调整布局与格式,修改部分错别字和歧义语句;

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

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

相关文章

【Java寒假打卡】Java基础-XML文件

【Java寒假打卡】Java基础-XML文件概述标签的规则xml的语法规则解析XMLXML解析的准备工作XML解析文件的代码实现概述 标签的规则 xml的语法规则 <?xml version"1.0" encoding"UTF-8" ?> <!--本xml文件用于描述多个学生信息--> <students&…

教程: nodejs 做微信公众号开发,回复 xml 消息

教程&#xff1a; nodejs 做微信公众号开发&#xff0c;回复 xml 消息 首先需要你的后台跟服务器已经可以建立连接&#xff0c;这个不再冗述看官方教程就好 接入指南 。此篇介绍的是如何获取用户发来的信息&#xff0c;并回复它。 一、接收 xml 信息内容 我用的是 nodejs 的…

Android应用模块化开发指南

Android应用模块化开发指南 包含多个Gradle模块的项目称为多模块项目。本文包含多模块应用项目的最佳实践和推荐模式。 代码规模变大带来的问题 可扩缩性、可读性和整体代码质量会随着时间的推移而降低&#xff0c;代码维护者未采取积极的措施来保持易于维护的结构。模块化是…

【营销】uplift建模方案-专利总结

之前准备写专利的时候浏览了一下其他公司的专利&#xff0c;对于one model&#xff0c;还是two model&#xff0c;基模型是什么做了简单总结。 浦发银行&#xff08;CN 112446541 A&#xff09;——one model&#xff08;标签转换&#xff09; 基模型&#xff1a;NN分类融合m…

经济学学习(宏观)

--------------------------------------- 第8篇&#xff1a;宏观经济学的数据 --------------------------------------- 23. 一国收入的衡量(GDP&#xff0c;通胀) gdp衡量总收入和总支出&#xff0c;总收入总支出 某一既定时期&#xff0c;一个国家内生产的所有最终商品…

【科研试剂】16-Heptadecynoic acid,93813-16-2,16-庚二酸

【中文名称】16-庚二酸【英文名称】 16-Heptadecynoic acid&#xff0c;16-Heptadecynoic COOH【结 构 式】【CAS】93813-16-2【分子式】C17H30O2【分子量】266.43【纯度标准】95%【包装规格】1g&#xff0c;5g&#xff0c;10g【是否接受定制】可进行定制&#xff0c;定制时间周…

Java日志系统介绍和slf4j的使用

目录1. 日志系统介绍2. slf4j的使用2.1 slf4j的入门2.2 slf4j绑定日志框架1. 日志系统介绍 日志门面位于应用程序和日志框架之间&#xff0c;日志门面提供一个抽象的能力&#xff0c;日志框架进行具体的日志实现。可以很方便的更换日志框架。类似JDBC驱动 日志门面有&#xf…

业务逻辑漏洞

1、容易忽略的低危漏洞以及延伸利用 一、容易忽略的低危漏洞以及延伸利用 在挖洞的过程当中&#xff0c;比如我们碰到信息泄露漏洞&#xff0c;但是我们不知道这个是信息泄露&#xff1b;或者说我们碰到一个xss&#xff0c;我们不会利用&#xff0c;只能弹个窗&#xff0c;比如…

AcWing 4510. 寻宝!大冒险!(暴力枚举)

题目如下&#xff1a; 输入样例1&#xff1a; 5 100 2 0 0 1 1 2 2 3 3 4 4 0 0 1 0 1 0 1 0 0输出样例1&#xff1a; 3样例 111 解释 绿化图上 (0,0)(0,0)(0,0)、(1,1)(1,1)(1,1) 和 (2,2)(2,2)(2,2) 三处均可能埋有宝藏。 输入样例2&#xff1a; 5 4 2 0 0 1 1 2 2 3 3 …

C++入门:命名空间

目录 一.前言 C关键字(C98)总览&#xff1a; 一.作用域 二.命名冲突 三.命名空间 命名空间定义&#xff1a; 命名空间的嵌套定义&#xff1a; 四.命名空间的使用 五.命名空间的本质 一.前言 C是从C语言延伸出来的编程语言&#xff0c;C兼容了C语言百分之九十九的语法…

Lr 12 ACR 15:蒙版

Adobe Camera Raw &#xff08;简称为 ACR&#xff09;与 Lightroom Classic&#xff08;简称为 Lr 或 LrC&#xff09;使用同一引擎&#xff0c;其中的蒙版 Mask功能变得日益强大。基于人工智能技术&#xff08;AI 驱动&#xff09;&#xff0c;可快速而精准地选择主体、天空、…

Redis - Redis 6.0 新特性之多线程模型

1. Redis6.0之前的版本真的是单线程么&#xff1f; 否&#xff01;Redis 在处理客户端的请求时&#xff0c;包括获取 (socket 读)、解析、执⾏、内容返回 (socket 写) 等都由⼀个顺序串⾏的主线程处理&#xff0c;这就是所谓的「单线程」。 在执行命令阶段&#xff1a;Redis是…

【jQuery超快速入门教程】上篇

&#x1f340;作者主页&#xff1a;在下周周ovo&#x1f340;系列专栏&#xff1a;从零开始百天学习前端基础&#x1f340;其他平台&#xff1a;博客园1️⃣前言&#xff1a;jQuery必备网站jQuery下载地址jQuery中文文档jQuery插件库1️⃣一、为什么要学习jQuery&#xff1f;jQ…

蓝库云|2023年企业4个数字化转型关键,成功之路近在咫尺

数字化转型&#xff1a;由上而下的过程 企业数字化转型最主要的原因在于企业管理者的决定。数字化转型是由「上」而「下」的过程&#xff0c;如果管理层没有转型的确切目标与规划&#xff0c;与竞争者相比之下&#xff0c;经营模式将会原地踏步、无法超越。蓝库云根据最新客户…

C++:C++全局变量:看完还不懂全局变量来捶我

我们知道&#xff0c;全局变量时C语言语法和语义中一个很重要的知识点&#xff0c;首先它的存在意义需要从三个不同角度去理解。 对于程序员来说&#xff0c;它是一个记录内容的变量&#xff08;variable&#xff09;对于编译/链接器来说&#xff0c;它是一个需要解析的符号 &a…

java使用JSch连接服务器实现命令交互

java使用JSch连接服务器实现命令交互1、通过maven引入jsch2、代码编写&#xff08;1&#xff09;创建MyUserInfo&#xff08;2&#xff09;创建Shell类连接服务器&#xff08;3&#xff09;启动3、测试结果JSch官网 1、通过maven引入jsch <dependency><groupId>co…

万字长文--详解Git(快速入门)

Git基础与扩展Git1、Git概念1.1 关于版本控制1.2 Git基础概念2、Git基础操作2.1 安装并配置Git2.2 Git的基本操作3、Github操作3.1 关于开源3.2 注册账号3.3 远程仓库的使用4、Git分支操作4.1 本地分支操作4.2 远程分支操作Git 1、Git概念 1.1 关于版本控制 文件的版本管理的…

7 种常用的数据挖掘技术分享

有人说&#xff1a;一个人从1岁活到80岁很平凡&#xff0c;但如果从80岁倒着活&#xff0c;那么一半以上的人都可能不凡。 生活没有捷径&#xff0c;我们踩过的坑都成为了生活的经验&#xff0c;这些经验越早知道&#xff0c;你要走的弯路就会越少。 摘要: 随着信息领域的进步…

详解DFS(深度优先搜索)算法+模板+指数+排列+组合型枚举+带分数四道例题

目录 前言&#xff1a; 1.背景 2.图解分析 3.算法思想 4.dfs四大例题 4.1.递归实现指数型枚举 题解&#xff1a; 4.2.递归实现排列型枚举 题解&#xff1a; 字典序: 4.3.递归实现组合型枚举 题解: 4.4.带分数 题解&#xff1a; 5.最后&#xff1a; 前言&#xff1a;…

来了解一下ASN.1?

想要了解证书&#xff0c;必须先了解ASN.1和编码规则。这篇文章简单介绍ASN.1&#xff0c;不过分探讨细节&#xff0c;大家如果有兴趣可以继续深入研究。 一、ASN.1 ASN.1是Abstract Syntax Notation One&#xff08;抽象文法描述语言&#xff09;的缩写。计算机系统之间交换…