【手写 Vue2.x 源码】第二十四篇 - 异步更新流程

news2024/12/22 20:34:18

一,前言

上篇,介绍了 Vue依赖收集的视图更新部分,主要涉及以下几点:

视图初始化时:

  • render方法中会进行取值操作,进入 Object.defineProperty 的 get 方法
  • get 方法中为数据添加 dep,并记录当前的渲染 watcher
  • 记录方式:watcher查重并记住 dep,dep 再记住 watcher

数据更新时:

  • 当数据发生改,会进入 Object.defineProperty 的 set 方法
  • 在 set 方法中,使 dep 中收集的全部 watcher 执行视图渲染操作 watcher.get()
  • 在视图渲染前(this.getter方法执行前),通过 dep.target 记录当前的渲染 watcher
  • 重复视图初始化流程

本篇,介绍 Vue 的异步更新流程


二,异步更新的实现

1,为什么要做异步更新

上文末尾提到了一个问题:

当前版本,在视图渲染阶段进行依赖收集,数据改变通知所有被收集的 watcher 更新视图

let vm = new Vue({
  el: '#app',
  data() {
    return { name: "Brave" , age: 123}
  }
}); 
vm.name = "Brave Wang";
vm.name = "Brave";
vm.name = "Brave Wang";
vm.name = "Brave";
vm.name = "Brave Wang";
vm.name = "Brave";

在这种情况下,频繁更新同一数据,就会多次触发视图渲染dep.notify->watcher.update

虽然name的值变化了6次,但只在最后一次进行视图更新即可

由于当前逻辑是同步调用watcher.update进行更新的,即数据变化一次就会触发一次视图更新

要想做到只在最后执行一次视图更新,就需要将视图更新改造为异步更新的机制

2,异步更新的实现思路

当数据发生变化时,将数据变更的逻辑先缓存起来不直接处理,如果有相同数据更新就进行合并,在最后做更新一次

在 Vue 中,vue.nextTick 方法能够实现异步更新

3,数据变更缓存的位置

数据变更就会进入 setter,但不能在 setter 进行缓存,因为数组的变化是不会进入 setter 的

但不管是何种数据变化,最终视图渲染都会汇集到 watcher.update 方法,所以在这里缓存是最佳的

4,缓存 watcher 更新逻辑

可以先将 watcher 集中缓存到一个队列中,缓存过程中可以进行合并,会后一次执行即可

因为此时为异步代码,当逻辑都执行完成后,才会执行会把队列中的 watcher 都 run

在 vue 中有一个任务调度方法:src/observe/schedule.js

创建watcher 缓存队列 queueWatcher,作用:做 watcher 的去重和缓存

let queue = [];           // 用于缓存渲染 watcher
let has = {};             // 存放 watcher 唯一 id,用于 watcher 的查重
let pending = false;      // 控制 setTimeout 只走一次

/**
 * 将 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) {       // 等效于防抖
      setTimeout(() => {
        queue.forEach(watcher => watcher.run()) // 依次触发视图更新
        queue = [];       // reset
        has = {};         // reset
        pending = false;  // reset
      }, 0);
      pending = true;     // 首次进入被置为 true,使微任务执行完成后宏任务才执行
    }
  }
}

Watcher类 update 方法使用 queueWatcher 方法,添加 run 方法做视图更新

从而实现异步更新:

// src/observe/watcher.js

import Dep from "./dep";
import { queueWatcher } from "./scheduler";

let id = 0;
class Watcher {
  constructor(vm, fn, cb, options){
    this.vm = vm;
    this.fn = fn;
    this.cb = cb;
    this.options = options;
    this.id = id++;
    this.depsId = new Set();
    this.deps = [];
    this.getter = fn;
    this.get();
  }
  addDep(dep){
    let did = dep.id;
    if(!this.depsId.has(did)){
      this.depsId.add(did);
      this.deps.push(dep);
      dep.addSub(this); 
    }
  }
  get(){
    Dep.target = this; 
    this.getter();
    Dep.target = null; 
  }
  update(){
    console.log("watcher-update", "查重并缓存需要更新的 watcher")
    queueWatcher(this);
  }
  run(){
    console.log("watcher-run", "真正执行视图更新")
    this.get();
  }
}

export default Watcher;

TODO 问题:Vue 的更新策略是:等待同步代码都执行完,再更新异步

5,代码重构

  • nextTick 异步方案改用 promise 方案实现
// src/utils.js

/**
 * 将方法异步化
 * @param {*} fn 需要异步化的方法
 * @returns 
 */
export function nextTick(fn) {
  return Promise.resolve().then(fn);
}
  • 将刷新队列逻辑抽取为独立的方法 flushschedulerQueue

    setTimeiout 中的逻辑用于刷新队列:执行所有 watcher.run 并将队列清空;

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

  • 改造后的代码:
**
 * 将 watcher 进行查重并缓存,最后统一执行更新
 * @param {*} watcher 需更新的 watcher
 */
export function queueWatcher(watcher) {
  let id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    queue.push(watcher);
    if (!pending) {
      nextTick(flushschedulerQueue); // 改造后使用 nextTick
      pending = true;
    }
  }
}

6,测试异步更新

let vm = new Vue({
  el: '#app',
  data() {
    return { name:  "Brave"}
  }
}); 
vm.name = "Brave Wang";
console.log("数据更新后立即获取 dom", vm.$el.innerHTML);

控制台输出结果:

image.png

此时,控制台输出取到的 dom 元素为是旧值,因为 vm.name 已变更为异步更新;
那么如何获取到更新后的 dom?

7,获取更新后的 dom

Vue中使用 vm.$nextTick 方法,所以在Vue初始化的 initMixin 中为其添加原型方法 $nextTick:

// src/init.js

import { nextTick } from "./utils";

export function initMixin(Vue) {·
  Vue.prototype._init = function (options) {...}
  Vue.prototype.$mount = function (el) {...}
  // 为 Vue 扩展原型方法 $nextTick
  Vue.prototype.$nextTick = nextTick;
}

测试:

let vm = new Vue({
  el: '#app',
  data() {
    return { name:  "Brave"}
  }
}); 

vm.name = "Brave Wang";
console.log("数据更新后立即获取 dom", vm.$el.innerHTML);

vm.$nextTick(()=>{
  console.log("$nextTick获取更新后的 dom", vm.$el.innerHTML);
})

image.png


三,异步更新实现的优化

在上边的实现中,共创造了两个 promise

  • 第一次,更新数据时创造了一个 promise
  • 第二次,在 nextTick 中又创造了一个 promise
    第一个promise先执行;第二个promise再执行;
    所以第二个拿到的其实是第一个成功后的结果

这里可以优化成为创建一个 promise,与 watcher 异步执行跟新的原理相似:

  • 更新数据时,将更新逻辑存起来;
  • 当用户nextTick取值时,继续将取值逻辑存起来;
    将两个逻辑存到一个数组中,在一个微任务中全部执行并清空即可

这样,整个过程就只创建了一个 promise

// src/utils.js

let callbacks = []; // 缓存异步更新的 nextTick
let waiting = false;
function flushsCallbacks() {
  callbacks.forEach(fn => fn()) // 依次执行 nextTick
  callbacks = [];   // reset
  waiting = false;  // reset
}

/**
 * 将方法异步化
 * @param {*} fn 需要异步化的方法
 * @returns 
 */
export function nextTick(fn) {
  // return Promise.resolve().then(fn);
  callbacks.push(fn); // 先缓存异步更新的nextTick,后续统一处理
  if(!waiting){
    Promise.resolve().then(flushsCallbacks);
    waiting = true; // 首次进入被置为 true,控制逻辑只走一次
  }
}

callbacks中,第一个 fn 一定来自是内部的;第二个 fn 才是用户写的;

将两个 fn 先进行缓存,实现将用户的 nextTick 和内部更新的 nextTick 合并在一起;

let vm = new Vue({
  el: '#app',
  data() {
    return { name:  "Brave"}
  }
}); 

vm.name = "Brave Wang";
console.log("数据更新后立即获取 dom", vm.$el.innerHTML);

vm.$nextTick(()=>{
  console.log("$nextTick获取更新后的 dom", vm.$el.innerHTML);
})
vm.$nextTick(()=>{
  console.log("$nextTick获取更新后的 dom", vm.$el.innerHTML);
})
vm.$nextTick(()=>{
  console.log("$nextTick获取更新后的 dom", vm.$el.innerHTML);
})
所以,在这种情况下:
    更新数据的nextTick + 3次用户手写的nextTick,
    共四次,只创建了一个 promise, 
    最后只用了一个微任务就都清空了,这是一个批处理的思想
    多个nextTick执行一次 then,而非多次

测试结果:

image.png


四,结尾

本篇,主要介绍了 Vue 的异步更新流程,主要涉及以下几点:

  • 为什么要做异步更新
  • 异步更新的实现思路
  • 数据变更缓存的位置
  • 缓存 watcher 更新逻辑
  • vm.$nextTick 获取更新后的 dom
  • 测试异步更新

下一篇,数组的依赖收集

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

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

相关文章

20230115英语学习

Gold From Old Sim Cards Could Help Make Future Drugs SIM卡中回收的黄金,可用于制造药品 Chemists are paving a road to recycle discarded SIM cards, not for electronics, but for medicine. SIM cards, which allow your phone to connect to your netwo…

ATTCK 05

环境搭建 自行下载安装包 解压VMware中win7 win8 同样方法所要用到的攻击机为kali 调节kali的网络适配器为vmnet8 调节win7的网络适配器 增加vmnet5用来连接内网win8 vmnet5名称ip角色kali192.168.115.129攻击机win7192.168.115.150192.168.138.136win8192.168.138.138DC拓…

【 java 反射上篇】java反射机制不难吧?来看看这篇

📋 个人简介 💖 作者简介:大家好,我是阿牛,全栈领域优质创作者。😜📝 个人主页:馆主阿牛🔥🎉 支持我:点赞👍收藏⭐️留言&#x1f4d…

BMS系统—产生原因如何工作

1 为什么需要BMS系统 1.1 介绍 1)BMS,battery management system,电池管理系统 2)BMS是一套嵌入式系统,由硬件和软件共同组成 3)BMS功能:管理多节锂电池组成的电池包,实现充放电管理、安全保护…

jsp动物园网上购票系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 动物园网上购票系统 是一套完善的web设计系统,对理解JSP java编程开发语言有帮助,系统采用web模式开发,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#x…

Python:每日一题之FBI树(深度优先遍历)

题目描述 我们可以把由 “0” 和 “1” 组成的字符串分为三类:全 “0” 串称为 B 串,全 “1” 串称为 I 串,既含 “0” 又含 “1” 的串则称为 F 串。 FBI树是一种二叉树,它的结点类型也包括 F 结点,B 结点和 I 结点…

Anolis 8.6 部署 Kafka 3.3.1 安装和测试

龙蜥 8.6 安装 Kafka 3.3.1 并通过 SpringBoot 3.0.1 版本调试一.Kafka 安装1.下载编译后的文件2.拷贝到 Anolis 并解压3.启动服务3.常用命令1.Topic 增查删2.生产消费测试二.SpringBoot 连接 Kafka1.项目结构、依赖、配置文件和启动类2.生产者和生产监听3.消费者和消费监听4.自…

【Java寒假打卡】Java基础-并发工具类

【Java寒假打卡】Java基础-并发工具类HashMap在多线程下的问题ConcurrentHashMapCountDownLatchHashMap在多线程下的问题 package com.hfut.edu.test14;import com.sun.jdi.request.StepRequest;import java.util.HashMap;public class test2 {public static void main(String…

信号的时域和频域特性的区别到底是什么?

不严谨的说,时域和频域分析就是在不同的空间看待问题的,不同空间所对应的原子(基函数)是不同的。你想一下时域空间的基函数是什么?频域空间的基函数是什么?一般的时-频联合域空间的基函数是什么?小波域空间的基函数是什…

线索二叉树(c++)

1.引言: 二叉树的三种遍历方法能将二叉树中的结点按某种方式生成一个线性序列,能将一个非线性结构进行线性化操作。但随之也产生两个问题: 遍历效率低 在采用左右链表示方法作为二叉树的存储结构时,当二叉树更新后,并…

【博客586】ipvs的hook点位置以及hook点钩子函数剖析

ipvs的hook点位置以及hook点钩子函数剖析 ipvs实现负载均衡的基础 ipvs其实是基于netfilter框架来挂载hook点,从而对流量进行dnat等操作 ipvs的hook点剖析 IPVS的实现利用了Netfilter的三个Hook点,分别是:NF_INET_LOCAL_IN、NF_INET_LOCAL_O…

【nvidia CUDA 高级编程】NVSHMEM 直方图——复制式方法

博主未授权任何人或组织机构转载博主任何原创文章,感谢各位对原创的支持! 博主链接 本人就职于国际知名终端厂商,负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作,目前牵头6G算力网络技术标准研究。 博客…

pandas时间序列,案列

一:pandas时间序列 1.1为什么要学习pandas中的时间序列 不管在什么行业,时间序列都是一种非常重要的数据形式,很多统计数据以及数据的规律也都和时间序列有着非常重要的联系,而且在pandas中处理时间序列是非常简单的 1.2生成一段时…

【Python技巧】:cmd查看Python版本号居然与自己电脑装的版本不一致,特此提出解决方案

项目场景: 大家好!欢迎大家看我的博客,最近学习Python的GUI(PyQt5)的时候发现了自己电脑的一个python问题,我本想装一下PyQt5,顺手查了一下自己电脑的Python版本,没想到居然是Python…

哈希表(二)—— 开散列 / 拉链法 / 哈希桶的模拟实现

哈希表的基本思路是通过某种方式将某个值映射到对应的位置,这里的采取的方式是除留余数法,即将原本的值取模以后再存入到数组的对应下标,即便存入的值是一个字符串,也可以根据字符串哈希算法将字符串转换成对应的ASCII码值&#x…

Week 6 hw3-1 全连接网络反向传播推导

Week 6 hw3-1 全连接网络反向传播推导 折腾了半天&#xff0c;记录一下。 作业中网络由若干全连接层ReLU组成&#xff0c;输出层的函数为softmax&#xff0c;损失函数为交叉熵。 一、记号 设网络有nnn层。如图&#xff0c;当i<ni<ni<n时&#xff0c;我们有如下几条…

机器学习/人工智能 实验一:典型监督学习方法分类实践与比较分析

一、实验目的与要求 (1)利用所学习的监督学习方法完成目标识别实验方案的设计。 (2)编程并利用相关软件完成实验测试&#xff0c;得到实验结果。 (3)通过对实验数据的分析﹑整理&#xff0c;方法的对比&#xff0c;得出实验结论&#xff0c;培养学生创新思维和编写实验报告的能…

【PyTorch深度学习实践】09_卷积神经网络基础

文章目录1.卷积操作1.1 卷积操作1.2 padding-填充1.3 stride-步长1.4 pooling-池化1.5 基础版CNN代码示例1.6 完整CNN代码示例1.卷积操作 卷积神经网络概览 1.1 卷积操作 输入通道数卷积核通道数&#xff0c;卷积核个数输出通道数 1.2 padding-填充 padding是为了让源图像最…

FPGA图像处理HLS实现三种图像缩放算法,线性插值、双线性插值、双三次插值,提供HLS工程和vivado工程源码

目录一、三种图像缩放算法介绍线性插值双线性插值双三次插值二、HLS实现线性插值图像缩放三、HLS实现双线性插值图像缩放四、HLS实现双三次插值图像缩放五、HLS在线仿真并导出IP六、其他FPGA型号HLS在线仿真并导出IP七、zynq7100开发板vivado工程八、上板调试验证九、福利&…

纪念QT可直接安装的离线版最后版本5.14.2

为什么说纪念呢&#xff1f;因为&#xff0c;这个版本之后再也没有可下载下来安装的版本了&#xff0c;因为我们以后再也没有这么方便了。为是很么说纪念呢&#xff1f;因为我们从QT还很柔弱的时候开始就是使用的离线版。 以前用c#来做组态&#xff0c;自定义控件开发起来也还…