写点什么

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

用户头像
Brave
关注
发布于: 23 小时前
【Vue2.x 源码学习】第二十四篇 - 异步更新流程

一,前言


上篇,介绍了 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 = [];           // 用于缓存渲染 watcherlet 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);
复制代码


控制台输出结果:



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

7,获取更新后的 dom

Vue 中使用 vm.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);})
复制代码




三,异步更新实现的优化


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

  • 第一次,更新数据时创造了一个 promise

  • 第二次,在 nextTick 中又创造了一个 promise


第一个 promise 先执行;第二个 promise 再执行;

所以第二个拿到的其实是第一个成功后的结果


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

  • 更新数据时,将更新逻辑存起来;

  • 当用户 nextTick 取值时,继续将取值逻辑存起来;


将两个逻辑存到一个数组中,在一个微任务中全部执行并清空即可

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


// src/utils.js
let callbacks = []; // 缓存异步更新的 nextTicklet 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,而非多次


测试结果:




四,结尾


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


  • 为什么要做异步更新

  • 异步更新的实现思路

  • 数据变更缓存的位置

  • 缓存 watcher 更新逻辑

  • vm.$nextTick 获取更新后的 dom

  • 测试异步更新


下一篇,数组的依赖收集


用户头像

Brave

关注

还未添加个人签名 2018.12.13 加入

还未添加个人简介

评论

发布
暂无评论
【Vue2.x 源码学习】第二十四篇 - 异步更新流程