从上一节可以看到,watch的本质其实是对effect的二次封装。这里继续讨论watch的两个特性:一个是立即执行的回调函数,另一个是回调函数的执行时机。
首先来看立即执行的回调函数。默认情况下,一个watch的回调只会在响应式数据发生变化时才执行。
在Vue.js中可以通过选项参数immediate来指定回调是否需要立即执行:
watch(obj,()=>{
console.log('变化了')
},{
// 回调函数会在watch创建时立即执行一次
immediate:true
})
其实回调函数的立即执行与后续执行本质上没有任何差别,要做的就是把scheduler调度函数封装为一个通用函数,分别在初始化和变更时执行,如下面代码:
function watch(source, cb, options = {}){
let getter
if(typeof source === 'function'){
getter = source
}else{
getter = () => traverse(source)
}
let oldValue, newValue
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
newValue = effectFn()
cb(newValue, oldValue)
oldValue = newValue
}
const effectFn = effect(
// 执行getter
() => getter(),
{
lazy: true
// 使用job函数作为调度器函数
scheduler: job
}
)
if(options.immediate){
// 当immediate为true时立即执行job,从而触发回调执行
job()
}else{
oldValue = effectFn()
}
}
由于回调函数时立即执行的,所以第一次回调执行时没有所谓的旧值,因此回调函数此时的oldValue值为undefined
还可以通过其他选项参数来指定回调函数的执行时机,例如在Vue.js 3中使用flush选项来指定:
watch(obj, ()=>{
console.log('变化了')
},{
// 回调函数会在watch创建是立即执行一次
flush: 'pre' // 还可以指定为 'post' | 'sync'
})
flush本质上市在指定调度函数的执行时机。flush的功能与scheduler相同,当flush的值为’post’时,代表调度函数需要将副作用函数放到一个微任务队列中,并等待DOM更新结束后再执行,可以用如下代码进行模拟:
function watch(source, cb, options = {}){
let getter
if(typeof source === 'function'){
getter = source
}else{
getter = () => traverse(source)
}
let oldValue, newValue
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
newValue = effectFn()
cb(newValue, oldValue)
oldValue = newValue
}
const effectFn = effect(
// 执行getter
() => getter(),
{
lazy: true,
scheduler: () => {
// 在调度函数中判断flush是否为‘post’,如果是,将其放到微任务队列中执行
if(options.flush === 'post'){
const p = Promise.resolve()
p.then(job)
}else{
job()
}
}
}
)
if(options.immediate){
job()
}else{
oldValue = effectFn()
}
}
如上当options.flush的值为post时,将job函数放到微任务队列中,从而实现异步延迟执行;否则直接执行job函数,本质上相当于‘sync’的实现机制,即同步执行