手写vue-diff算法(一)

news2024/11/18 1:48:02

Vue初始化流程

1.Vue流程图

在这里插入图片描述
Vue流程图:

  • Vue的初始化流程,默认会创建一个Vue实例,执行初始化、挂载、模板编译操作,模板被编译成为render函数;
  • render函数初始化时会执行取值操作,从而进入getter方法对当前组件进行依赖收集,收集渲染Watcher
  • 当用户修改数据时,进入setter方法就会通知对应的渲染Watcher执行更新操作;
  • 当前,视图更新操作的实现,是通过patch方法直接进行替换完成的,野蛮且暴力;
// src/lifeCycle.js

export function lifeCycleMixin(Vue){
  
  Vue.prototype._update = function (vnode) {
    const vm = this;
    // 生成新的真实节点,直接将老节点全部替换掉,可以做性能优化
    vm.$el = patch(vm.$el, vnode);
  }
}

2.初始化与更新流程分析

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

当数据更新时,会进入defineProperty的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方法更新视图:

// src/obseve/dep.js

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

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

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

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

  • 当前的 patch 方法,仅考虑了初始化的情况,还需要处理更新操作,

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

  • 首次渲染,根据虚拟节点生成真实节点,替换掉原来的节点;

  • 更新渲染,生成新的虚拟节点,并与老的虚拟节点进行对比,再渲染;

实现diff算法

1.模拟虚拟节点对比

// diff算法是一个平级比较的过程,父亲和父亲比,儿子和儿子比
// 测试用 
let render1 = compileToFunction(`<ul  a="1" style="color:blue">
    <li key="a">a</li>
    <li>b</li>
    <li>c</li>
    <li key="d">d</li>
</ul>`);
let vm1 = new Vue({ data: { name: 'zf' } })
let prevVnode = render1.call(vm1)
let el = createElm(prevVnode)
document.body.appendChild(el)
let render2 = compileToFunction(`<ul  a="1"  style="color:red;">
    <li key="e">e</li>
    <li>m</li>
    <li>p</li>
    <li key="q">q</li>
    
</ul>`);
let vm2 = new Vue({ data: { name: 'zf' } })
let nextVnode = render2.call(vm2)
let el2 = createElm(nextVnode)

2.调用patch优化

setTimeout(() => {
  patch(prevVnode, nextVnode)
}, 1000)

3.目前patch方法

3.1当前版本:

patch方法写的是初渲染流程,仅考虑初始化情况,,直接将新节点替换掉老节点
通过oldVnode.nodeType节点类型判断,如果为真实节点,执行初渲染流程,如果是非真实节点,执行更新逻辑

export function patch(oldVNode, vnode) {
    // 写的是初渲染流程
    const isRealElement = oldVNode.nodeType;
    if (isRealElement) {
        const elm = oldVNode; // 获取真实元素
        const parentElm = elm.parentNode; // 拿到父元素
        let newElm = createElm(vnode);
        parentElm.insertBefore(newElm, elm.nextSibling);
        parentElm.removeChild(elm); // 删除老节点

        return newElm;
    } else {
        // diff算法
        
    } 
}

3.2实现目标

使用diff算法,尽可能复用老节点

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

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

相关文章

【科研入门】会议、期刊、出版社、文献数据库、引文数据库、SCI分区、影响因子等基础科研必备知识

大家好&#xff0c;我是洲洲&#xff0c;欢迎关注&#xff0c;一个爱听周杰伦的程序员。关注公众号【程序员洲洲】即可获得10G学习资料、面试笔记、大厂独家学习体系路线等…还可以加入技术交流群欢迎大家在CSDN后台私信我&#xff01; 本文目录 一、会议与期刊二、如何辨别是否…

【UE5 Cesium】07-Cesium for Unreal 从一个地点飞行到另一个地点(下)

UE版本&#xff1a;5.1 介绍 本文以在基督城&#xff08;新西兰&#xff09;和悉尼&#xff08;澳大利亚&#xff09;这两个城市间为例&#xff0c;在上一篇文章&#xff08;【UE5 Cesium】06-Cesium for Unreal 从一个地点飞行到另一个地点&#xff08;上&#xff09;&#…

vant-weapp源码解析---Tab标签页

这个标签栏&#xff0c;属于一个很常见的组件&#xff0c;一般我不用这个组件&#xff0c;自己手写一个scroll-view以及样式&#xff0c;更加轻便。但是我写的最简单的标签页没有滚动效果&#xff0c;以及选中标签动画效果。因此根据标签栏滚动学习下&#xff0c;并且自己手写一…

动态规划的入门

https://www.bilibili.com/video/BV13Q4y197Wg/ 动态规划解题步骤 —— carl 动态规划&#xff0c;英文&#xff1a;Dynamic Programming&#xff0c;简称DP&#xff0c;如果某一问题有很多重叠子问题&#xff0c;使用动态规划是最有效的。 所以动态规划中每一个状态一定是由…

Postman 最被低估的功能,自动化接口测试效率简直无敌!

目录 该篇文章针对已经掌握 Postman 基本用法的读者&#xff0c;即对接口相关概念有一定了解、已经会使用 Postman 进行模拟请求的操作。 接口结果判断 功能区 脚本相关 代码模板 集合(批量)测试 批量执行 变化的参数数据 请求依赖问题 接口执行顺序 数据传递 解决依…

系统出错。发生系统错误 1067。进程意外终止。

问题描述 使用管理员cmd&#xff0c;任务管理器均无法启动mysql。 报错提示信息&#xff1a;系统出错。发生系统错误 1067。进程意外终止。 错误排查 1.检查3306端口是否被占用 在cmd输入netstat -aon|findstr 3306&#xff0c;结果如下&#xff1a; 如果你发现是端口被占用…

OpenAI 又赢麻了谷歌 DeepMind 创始人刚称 Gemini 能碾压 GPT-4

整理 | 褚杏娟&#xff0c;核子可乐 谷歌在上个月的开发者大会上公布了一系列 AI 新项目&#xff0c;当时首次亮相的 Gemini 曾受到不少嘲讽。但近日&#xff0c;DeepMind 联合创始人兼 CEO Demis Hassabis 自曝&#xff0c;工程师们正在使用 AlphaGo 的技术来开发 Gemini&…

Java框架之spring 的 AOP 和 IOC

写在前面 本文一起看下spring aop 和 IOC相关的内容。 1&#xff1a;spring bean核心原理 1.1&#xff1a;spring bean的生命周期 spring bean生命周期&#xff0c;参考下图&#xff1a; 我们来一步步的看下。 1 其中1构造函数就是执行类的构造函数完成对象的创建&#x…

【Java】Java核心 79:Git 教程(2)Git 安装

文章目录 目标内容总结 上一篇我们讲到&#xff1a;Git是一个分布式版本控制系统&#xff0c;常用于协同开发和版本管理的工具。它可以跟踪文件的修改、记录历史版本&#xff0c;并支持多人协同工作。通过Git&#xff0c;你可以轻松地创建和切换分支、合并代码、回滚修改等操作…

Kicad编译

Windows 1.安装visual studio Pro 2019 以上版本&#xff0c;建议2022&#xff0c;自行破解&#xff0c;安装时一定要勾选上cmake工具&#xff1b;安装Git bash 2.去Gitlab上fork kicad的master分支到自己的gitlab仓库上&#xff0c;在本地创建kicad-source路径&#xff0c;初…

【debug】:安装mmcv-full==1.2.4包过程报错

【debug】:安装mmcv-full1.2.4包过程报错This error originates from a subprocess, and is likely not a problem with pip WARNING: Ignoring invalid distribution -illow (d:\anaconda3\envs\pytorch\lib\site-packages) 这是由于安装包过程中曾经出现问题&#xff0c;会影…

生成特定相关系数的变量

本文转载自根据相关性生成变量 已知一组变量a&#xff0c;想要生成另一组变量b&#xff0c;要求a与b之间相关性为c。 实现思路如下&#xff1a; 设固定变量为x1&#xff0c;随机变量x2&#xff0c;相关系数为rho。x1与x2之间的相关性可以转化为向量之间的夹角问题&#xff0c;…

Linux与Windows:操作系统之争及个人体验比较

在当今数码化的世界中&#xff0c;操作系统扮演着关键的角色。Linux和Windows作为最受欢迎和广泛使用的操作系统之一&#xff0c;具有不同的特点和优势。作为一个AI模型&#xff0c;我虽然没有真正的使用经验&#xff0c;但我可以就这两个操作系统进行比较&#xff0c;并提供一…

pointclouds 点云 demo 中的资源文件如pcd找不到的问题

一、问题描述 点云官网提供了许多学习点云类库的例子和demo &#xff0c;但是在github中找到tutorials后执行 mkdir build cd build cmake .. make 之后却发现没有 资源文件&#xff0c;怎么办 如&#xff1a;pcl-pcl-1.7.2\doc\tutorials\content\sources\normal_estimation…

QVHZO-A-06-3/U0/WG直动式比例流量阀控制器

QVHZO-A-06-3/U0/WG、QVHZO-A-06-12/I/PE、QVHZO-A-06-18/I、QVHZO-A-06-36/U0、QVHZO-A-06-45/I、QVKZOR-A-10-65/I/PE、QVKZOR-A-10-90/I直动式比例流量阀特点&#xff1a; 具有恒压差流量补偿功能 通过比例线圈起动 用于底板安装: 油口安装面符合ISO4401(规格尺寸6和10)…

基于单片机智能温室大棚控制系统

功能介绍 以51单片机作为主控系统&#xff1b; DS18B20温度采集模块检测温度&#xff1b; 光敏电阻和ADC0832组成的光照检测模块&#xff1b; 土壤湿度检测模块检测土壤湿度&#xff1b; CO2检测模块检测CO2浓度&#xff1b; LCD1602显示模块显示测量值、 若温度小于温度最…

Middleware ❀ Zookeeper功能与使用详解

文章目录 1、功能简述1.1 服务目标1.2 文件系统 - 树状结构1.3 数据节点Znode类型1.4 变更通知 - Watcher1.4.1 工作机制1.4.1.1 Client注册1.4.1.2 Server处理1.4.1.3 Client回调 1.4.2 监听特性 1.5 权限控制 - ACL1.5.1 模式 - scheme1.5.2 权限 - Permission 1.6 选举机制1…

Springboot如何手动连接库并获取指定表结构

一、前言&#x1f525; 通过&#xff0c;在使用springboot框架之后&#xff0c;就很少涉及到手动连接数据库的方式了&#xff0c;但bug菌有遇到这么一个需求场景&#xff0c;给到你的是无上限的数据库连接信息&#xff0c;要求你能按连接信息指定获取表数据&#xff0c;突然我就…

玩机搞机---另类操作 修改原生卡刷包转换为线刷包方式刷机

偶然给安卓机型刷写原生安卓的系统。可能其第三方twrp原因或者底包原因导致卡刷一直报错。虽然最终写入开机&#xff0c;但浪费时间&#xff0c;究其原因还在于分区切换和挂载分区导致的。写这篇博文的意义不是在于让玩家按步骤转换线刷&#xff0c;只是明白其分区写入的原理 索…

【JavaEE初阶】TCP/IP协议(二)

文章目录 网络层重点协议IP协议地址管理路由选择 数据链路层重点协议以太网协议MTU 应用层重要协议DNS&#xff08;域名解析系统&#xff09; 网络层重点协议 IP协议 协议头格式如下&#xff1a; 4位版本号&#xff08;version&#xff09;&#xff1a;指定IP协议的版本&am…