跳到主要内容

模拟实现

2023年06月11日
柏拉文
越努力,越幸运

一、/src/watcher.js Watcher()


// 用来标记 watcher
let uid = 0

**
* @param {*} cb 回调函数,负责更新 DOM 的回调函数
* @param {*} options watcher 的配置项
*/
export default function Watcher(cb, options = {}, vm = null) {
// 标识 watcher
this.uid = uid++
// ...
}

二、/src/watcher.js watcher.update()


/**
* 响应式数据更新时,dep 通知 watcher 执行 update 方法,
* 让 update 方法执行 this._cb 函数更新 DOM
*/
Watcher.prototype.update = function () {
if (this.options.lazy) { // 懒执行,比如 computed 计算属性
// 将 dirty 置为 true,当页面重新渲染获取计算属性时就可以执行 evalute 方法获取最新的值了
this.dirty = true
} else {
// 将 watcher 放入异步 watcher 队列
queueWatcher(this)
}
}

三、/src/watcher.js watcher.run()


/**
* 由刷新 watcher 队列的函数调用,负责执行 watcher.get 方法
*/
Watcher.prototype.run = function () {
this.get()
}

四、/src/asyncUpdateQueue.js


/**
* 异步更新队列
*/

// 存储本次更新的所有 watcher
const queue = []

// 标识现在是否正在刷新 watcher 队列
let flushing = false
// 标识,保证 callbacks 数组中只会有一个刷新 watcher 队列的函数
let waiting = false
// 存放刷新 watcher 队列的函数,或者用户调用 Vue.nextTick 方法传递的回调函数
const callbacks = []
// 标识浏览器当前任务队列中是否存在刷新 callbacks 数组的函数
let pending = false

五、/src/asyncUpdateQueue.js queueWatcher()


/**
* 将 watcher 放入队列
* @param {*} watcher 待会儿需要被执行的 watcher,包括渲染 watcher、用户 watcher、computed
*/
export function queueWatcher(watcher) {
if (!queue.includes(watcher)) { // 防止重复入队
if (!flushing) { // 现在没有在刷新 watcher 队列
queue.push(watcher)
} else { // 正在刷新 watcher 队列,比如用户 watcher 的回调函数中更改了某个响应式数据
// 标记当前 watcher 在 for 中是否已经完成入队操作
let flag = false
// 这时的 watcher 队列时有序的(uid 由小到大),需要保证当前 watcher 插入进去后仍然有序
for (let i = queue.length - 1; i >= 0; i--) {
if (queue[i].uid < watcher.uid) { // 找到了刚好比当前 watcher.uid 小的那个 watcher 的位置
// 将当前 watcher 插入到该位置的后面
queue.splice(i + 1, 0, watcher)
flag = true
break;
}
}
if (!flag) { // 说明上面的 for 循环在队列中没找到比当前 watcher.uid 小的 watcher
// 将当前 watcher 插入到队首
queue.unshift(watcher)
}
}
if (!waiting) { // 表示当前 callbacks 数组中还没有刷新 watcher 队列的函数
// 保证 callbacks 数组中只会有一个刷新 watcher 队列的函数
// 因为如果有多个,没有任何意义,第二个执行的时候 watcher 队列已经为空了
waiting = true
nextTick(flushSchedulerQueue)
}
}
}

六、/src/asyncUpdateQueue.js flushSchedulerQueue()


/**
* 负责刷新 watcher 队列的函数,由 flushCallbacks 函数调用
*/
function flushSchedulerQueue() {
// 表示正在刷新 watcher 队列
flushing = true
// 给 watcher 队列排序,根据 uid 由小到大排序
queue.sort((a, b) => a.uid - b.uid)
// 遍历队列,依次执行其中每个 watcher 的 run 方法
while (queue.length) {
// 取出队首的 watcher
const watcher = queue.shift()
// 执行 run 方法
watcher.run()
}
// 到这里 watcher 队列刷新完毕
flushing = waiting = false
}

七、/src/asyncUpdateQueue.js nextTick()


/**
* 将刷新 watcher 队列的函数或者用户调用 Vue.nextTick 方法传递的回调函数放入 callbacks 数组
* 如果当前的浏览器任务队列中没有刷新 callbacks 的函数,则将 flushCallbacks 函数放入任务队列
*/
function nextTick(cb) {
callbacks.push(cb)
if (!pending) { // 表明浏览器当前任务队列中没有刷新 callbacks 数组的函数
// 将 flushCallbacks 函数放入浏览器的微任务队列
Promise.resolve().then(flushCallbacks)
// 标识浏览器的微任务队列中已经存在 刷新 callbacks 数组的函数了
pending = true
}
}

八、/src/asyncUpdateQueue.js flushCallbacks


/**
* 负责刷新 callbacks 数组的函数,执行 callbacks 数组中的所有函数
*/
function flushCallbacks() {
// 表示浏览器任务队列中的 flushCallbacks 函数已经被拿到执行栈执行了
// 新的 flushCallbacks 函数可以进入浏览器的任务队列了
pending = false
while(callbacks.length) {
// 拿出最头上的回调函数
const cb = callbacks.shift()
// 执行回调函数
cb()
}
}