上一篇文章中我们实现了一个computed ,这一篇文章中我们来实现一个在Vue同样重要且常用的api---------watch(这两个一起出现就很容易想到:说说computed和watch的区别吧🙏),文章中需要的但是没有列出来的方法在前几篇文章中,如果能串下来看会没那么懵逼(绝对不是为了加阅读量的引流说法🔊)
watch的基本实现
常见的一种watch的调用方式如下:
javascript
const obj = { foo: 1 }
const proxyObj = new Proxy({
// ...省略
})
watch(proxyObj, () => {
console.log('proxyObj 里的数据变动了');
})
经过前面的学习,可以很容易想到,这不就是响应式数据proxyObj
的数据改动后,在调度器scheduler调用给定的回调函数就可以了吗。唉,你他*的还真是个天才(李云龙音),撸起袖子就是干,一个基本的watch函数如下:
javascript
function watch(proxyObj, cb) {
effect(() => proxyObj.foo, {
scheduler() {
cb()
}
})
}
watch
函数中调用了effect
函数并传递了调度器使得obj.foo
更改时能够走到调度器中,然后就能执行我们传递过去的想要执行的cb
了。试试更改proxyObj.foo
是否可以触发回调。
javascript
proxyObj.foo++
watch监听所有属性
上一小节的实现中,我们硬编码了对proxyObj.foo
的watch,现在我们拓展一下,使得proxyObj上的任一属性更改了都能触发回调,添加traverse
函数遍历响应式对象。
javascript
function watch(proxyObj, cb) {
effect(() => traverse(proxyObj), {
scheduler() {
cb()
}
})
}
// traverse函数来源于《Vue.js设计与实现》,感恩💖
function traverse(value, seen = new Set()) {
// 如果要读取的数据是原始值, 或者已经被读取过了, 那么什么都不做
if (typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到seen中, 代表遍历地读取过了,避免循环引用引起的死循环
seen.add(value)
// 暂时不考虑数组等其他数据结构
// 假设value就是一个对象, 使用for...in读取对象地每一个值,并递归地调用traverse进行处理
for (const k in value) {
traverse(value[k], seen)
}
return value
}
watch接受getter函数
watch函数除了直接传递一个响应式对象的用法,还有传递一个getter函数的用法,如下:
javascript
watch(() => proxyObj.foo, () => {
console.log('proxyObj 里的数据变动了');
})
我们在watch函数的内部做一下传递进来的是否是函数的区分:
javascript
function watch(source, cb) {
let getter
// 区分传递进来的是函数还是对象
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
effect(() => getter(), {
scheduler() {
cb()
}
})
}
watch存储旧值
watch还有一个很重要的特性,在回调函数中我们应该能够拿到新值 和旧值,例如下面这样:
javascript
watch(() => proxyObj.foo, (newValue, oldValue) => {
console.log(newValue, oldValue, 'proxyObj 的foo属性数据变动了');
})
我们需要存储旧值和新值,为了实现这一特性,就需要用到我们上一篇关于computed实现中的懒执行,我们先来回顾一下:
javascript
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn) // 新增
activeEffectFn = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 接受fn函数的返回值
const res = fn()
// 结束之后,弹出副作用函数
effectStack.pop()
// 把activeEffectFn改为栈顶的函数
activeEffectFn = effectStack[effectStack.length - 1]
return res
}
effectFn.deps = []
// 将 options 挂载到 effectFn 上
effectFn.options = options // 新增
if (!options.lazy) {
effectFn()
}
// 把副作用函数返回
return effectFn
}
- 添加了lazy:true 选项后,会将副作用函数返回,我们可以手动执行副作用函数以拿到返回值
res
,这个就是我们需要存储的旧值; - 新值则在执行到调度器scheduler时重新调用副作用函数拿到。
我们按这个思路修改watch函数的内部实现:
diff
function watch(source, cb) {
let getter
// 定义新值和旧值
+ let oldValue, newValue
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
- // effect(() => getter(), {
// 获取返回的副作用函数
+ const effectFn = effect(() => getter(), {
// 添加lazy:true选项
+ lazy: true,
scheduler() {
- // cb()
// 重新执行副作用函数,拿到新值
+ newValue = effectFn()
// 传递给回调函数旧值和新值
+ cb(newValue, oldValue)
// 存储为旧值
+ oldValue = newValue
}
})
// 手动调用副作用函数,拿到值
+ oldValue = effectFn()
}
立即执行的watch
默认情况下,watch的回调需要监听的值有改动才会执行,但在Vue.js中,我们可以传递immediate: true
来让回调函数立即执行,修改watch内部实现如下:
javascript
function watch(source, cb, options = {}) {
let getter
//定义新值和旧值
let oldValue, newValue
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 将调度器提取出来为一个函数job
const job = () => {
// 重新执行副作用函数,拿到新值
newValue = effectFn()
// 传递给回调函数旧值和新值
cb(newValue, oldValue)
// 存储为旧值
oldValue = newValue
}
// 获取返回的副作用函数
const effectFn = effect(() => getter(), {
// 添加lazy:true选项
lazy: true,
scheduler: job
})
// 传递了立即执行,则调用job函数
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
修改实现中把调度器 独立成了一个job
函数,以便我们可以手动执行,如果传递给watch的选项选项中有immediate: true
则立即执行调度器,这也符合Vue中如果立即执行,oldValue是undefined。
小结
这一节中我们实现了一个基本的watch函数,我们可以对整个对象的所有属性监听,也兼容了传递一个getter的形式,可以在回调中拿到旧值和新值,并且可以选择是否要立即执行回调。有什么写的不对的地方或者什么疑问可以留言,希望文章有帮助到你~
参考书籍
《Vue.js设计与实现》------------霍春阳