前言
通过手写Vue2源码,更深入了解Vue; 在项目开发过程,一步一步实现Vue核心功能,我会将不同功能放到不同分支,方便查阅; 另外我会编写一些开发文档,阐述编码细节及实现思路; 源码地址:手写Vue2源码
为何需要进行异步更新
上一篇我们介绍了利用Vue观察者模式实现数据驱动视图,视图的更新通过调用的watcher.update()
方法实现;但是更新过程存在优化空间。试想一下,如果短时间内某一数据修改了很多次,是否有必要每次修改都进行视图更新,这必然会造成性能浪费。
改写watcher
watcher要怎么优化:
- update()更新需要异步执行
- 单一事件循环中,同一watcher只更新一次,即保证watcher的唯一性
// src/observer/watcher
import { queueWatcher } from "./scheduler";
export default class Watcher {
// 更新当前watcher相关的视图
// Vue中的更新是异步的
update() {
// 每次watcher进行更新的时候,可以让他们先缓存起来,之后再一起调用
// 异步队列机制
queueWatcher(this);
}
// 真正更新视图的方法
run() {
this.getter.call(this.vm)
}
}
queueWatcher实现队列机制
在watcher.update()
中调用queueWatcher(watcher)
方法,queueWatcher主要做三件事:
- 创建一个任务数组,存放不同的watcher
- 将watcher放入任务数组中,鉴别watcher的唯一性(相同watcher不重复push)
- 调用
nextTick(flushSchedulerQueue)
,将任务数组清空(调用每个watcher的run()
)
// src/observer/scheduler.js
import { nextTick } from "../util/next-tick";
let queue = [];
let has = {}; // 维护存放了哪些watcher
/**
* queueWatcher逻辑:
* 1. 对watcher去重(有相同watcher的情况下,不重复push)
* 2. 防抖:一段时间内只执行一次的更新(遍历所有watcher,执行watcher.run())
*/
export function queueWatcher(watcher) {
const id = watcher.id;
// watcher去重,即相同watcher只push一次
if (!has[id]) {
// 同步代码执行 把全部的watcher都放到队列里面去
queue.push(watcher);
has[id] = true;
// 开启一次异步更新操作,批处理(防抖)
// 进行异步调用
nextTick(flushSchedulerQueue);
}
}
function flushSchedulerQueue() {
for (let index = 0; index < queue.length; index++) {
// 调用watcher的run方法,执行真正的更新操作
queue[index].run();
}
// 执行完之后清空队列
queue = [];
has = {};
}
nextTick实现异步队列
nextTick()
主要做两件事:
- 创建一个任务队列callbacks
- 将所有调用
nextTick(cb)
的回调函数cb放入任务队列callbacks中,在微任务中去清空这个队列(微任务会在同步任务执行完之后执行)
// src/util/next-tick.js
const callbacks = [];
function flushCallbacks() {
callbacks.forEach((cb) => cb());
waiting = false;
}
let waiting = false;
/**
* 流程:
* 1. watcher更新流程:
* ——> watcher.update()
* ——> queueWatcher(watcher)
* ——> 对watcher去重,并将watcher放到一个数组中;最后执行 nextTick(flushSchedulerQueue)(flushSchedulerQueue的作用是遍历watcher数组,调用watcher.run())
* ——> 将 flushSchedulerQueue 放入一个 回调函数数组callbacks 中;定义一个微任务:flushCallbacks(callbacks);
* 2. vm.$nextTick(cb):
* ——> 直接会执行Vue原型上的$nextTick()方法,即nextTick(cb)方法
* ——> 将cb 放入 上述的回调函数数组 callbacks 中,紧接着上述的flushSchedulerQueue,在微任务中一并执行
* ——> 由于在flushSchedulerQueue中会执行 watcher.run() 创建真实DOM,所以可以在$nextTick()回调中获取到最新DOM节点
*
* 总结:
* 1. callbacks 中包含 flushSchedulerQueue,以及$nextTick()的回调
* 2. dep.subs中每个watcher执行update时,最后都会执行nextick,
* 3. 执行nextick是否会创建微任务,取决于上一个微任务是否完成
* 4. 一轮事件循环中,flushCallbacks只会执行一次
*/
export function nextTick(cb) {
callbacks.push(cb);
if (!waiting) {
// 异步执行callBacks
Promise.resolve().then(flushCallbacks);
waiting = true;
}
}
$nextTick 实现
$nextTick
是定义在vue原型上的,它的具体实现就是上面的nextTick方法;
在原型上挂载$nextTick
:
// src/render.js
import { nextTick } from "./util/next-tick";
export function renderMixin(Vue) {
// 挂载在原型的nextTick方法 可供用户手动调用
Vue.prototype.$nextTick = nextTick;
}
总结
为何能在$nextTick中获取到选然后的DOM?
watcher的更新视图操作和$nextTick(cb)
的回调函数都会放到一个callbacks异步任务队列中,待同步任务执行完成后一并执行;watcher更新视图会创建新的DOM,所以在$nextTick(cb)中可以获取到新的DOM
为何在UI还没完全渲染完成,就能拿到DOM?
$nextTick()回调中获取的是内存中的DOM,不关心UI有没有渲染完成
系列文章
- 手写Vue2源码(一)—— 环境搭建
- 手写Vue2源码(二)—— 数据劫持
- 手写Vue2源码(三)—— 模板编译
- 手写Vue2源码(四)—— 初次渲染
- 手写Vue2源码(五)—— 观察者模式
- 手写Vue2源码(六)—— 异步更新及nextTick
- 手写Vue2源码(七)—— 侦听属性
- 手写Vue2源码(八)—— 计算属性
- 手写Vue2源码(九)—— 混入原理与生命周期
- 手写Vue2源码(十)—— 组件原理
- 手写Vue2源码(十一)—— diff算法
- 手写Vue2源码(十二)—— keep-alive
- 手写Vue2源码(十三)—— 全局API
- vue-router原理解析
- vuex原理解析
- vue3原理解析