1.watch的基础概念
所谓 watch,其本质就是观测一个响应式数据,当数据发生变化 时通知并执行相应的回调函数。举个例子
js
watch(obj, () => {
console.log('数据变了')
})
// 修改响应数据的值,会导致回调函数执行
obj.foo++
假设 obj 是一个响应数据,使用 watch 函数观测它,并传递一个 回调函数,当修改响应式数据的值时,会触发该回调函数执行。
2.watch的简单实现
实际上,watch 的实现本质上就是利用了 effect 以及 options.scheduler 选项,如以下代码所示:
js
effect(
() => {
console.log(obj.foo)
},
{
scheduler() {
// 当 obj.foo 的值变化时,会执行 scheduler 调度函数
}
})
那我们知道上面的代码,那我们就可以实现一个最简单的watch,代码如下:
js
// watch 函数接收两个参数,source 是响应式数据,cb 是回调函数
function watch(source, cb) {
effect(
// 触发读取操作,从而建立联系
() => source.foo,
{
scheduler() {
// 当数据变化时,调用回调函数 cb
cb()
}
})
}
通过上述,我们就实现了一个最简单的watch,但是我们使通过硬编码的方式,直接读取了foo这个属性,那我们能不能让watch更加具有通用性,那我们就需要封装一个通用的读取操作
js
function watch(source, cb, options = {}) {
effect(
() => traverse(source),
{
scheduler() {
cb()
}
}
)
}
function traverse(value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) return
seen.add(value)
// 遍历对象的每个属性,暂不考虑数组格式
for (const k in value) {
traverse(value[k], seen)
}
return value
}
如上面的代码所示,在 watch 内部的 effect 中调用 traverse 函数进行递归的读取操作,代替硬编码的方式,这样就能读取一个对 象上的任意属性,从而当任意属性发生变化时都能够触发回调函数执行。
3.watch中接受getter函数的处理
watch 函数除了可以观测响应式数据,还可以接收一个 getter 函数:
js
watch(
// getter 函数
() => obj.foo,
// 回调函数
() => {
console.log('obj.foo 的值变了')
}
)
如以上代码所示,传递给 watch 函数的第一个参数不再是一个响应式数据,而是一个 getter 函数。在 getter 函数内部,用户可以指定该 watch 依赖哪些响应式数据,只有当这些数据变化时,才会触发回调函数执行。
那我们的代码就需要做如下改造
js
function watch(source, cb, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
effect(
() => getter(),
{
scheduler() {
cb()
}
}
)
}
那我们就处理了传递对象和对getter函数的处理了,那我们接下来就处理,再cb函数中应该拿到newValue和oldValue的问题了
js
function watch(source, cb, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
// 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
const effectFn = effect(
() => getter(),
{
lazy: true,
scheduler() {
newValue = effectFn()
cb(newVlaue, oldValue)
oldValue = newValue
}
}
)
//手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn()
}
我们手动调用 effectFn 函数得到的返回值就是旧值,即第一次执行得到的值。当变化发生并触发 scheduler 调度函数执行时,会重新调用 effectFn 函数并得到新值,这样我们就拿到了旧值与新值。
4.watch的立即执行时机
当 immediate 选项存在并且为 true 时,回调函数会在该 watch 创建时立刻执行一次。那我们简单思考一下,回调稍后执行和立即执行没什么区别,那我们只需要把函数封装一下,再初始化和变更时执行就行了
js
function watch(source, cb, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
const job = () => {
newValue = effectFn()
cb(oldValue, newValue)
oldValue = newValue
}
const effectFn = effect(
// 执行 getter
() => getter(),
{
lazy: true,
scheduler: () => {
job()
}
}
)
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
这样,那我们就实现了watch的立即执行了。
5.回调时间的触发
默认情况下,侦听器回调会在父组件更新 (如有) 之后 、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后 的所属组件的 DOM,你需要指明 flush: 'post'
选项:
flush 本质上是在指定调度函数的执行时机。前文讲解过如何在 微任务队列中执行调度函数 scheduler,这与 flush 的功能相同。 当 flush 的值为 'post' 时,代表调度函数需要将副作用函数放到一 个微任务队列中,并等待 DOM 更新结束后再执行,我们可以用如下代 码进行模拟:
js
function watch(source, cb, options = {}) {
let getter
const effectFn = effect(
// 执行 getter
() => getter(),
{
lazy: true,
scheduler: () => {
if (options.flush === 'post') { //新增代码
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
}