您现在的位置是:首页 >技术教程 >4.1 Vue中watch函数的实现原理网站首页技术教程
4.1 Vue中watch函数的实现原理
watch函数的基本使用
Vue中的watch
作为侦听属性,他的作用为侦听一个数据的变化,若数据变化则触发相应的回调函数。
watch(obj, () => {
console.log('数据变化了')
})
如上述代码所示,如果obj
发生变化,则触发回调。当然watch
函数并不仅仅是这些功能,这些功能其实副作用函数也能实现,watch
函数可以获取变化前后的值,接收getter
函数,获取变化前后的值,通过调度器调度回调函数的执行时机等。这些功能都会在下文进行详细的叙述。
watch的基本实现
watch
函数的实现实质上就是使用effect
和scheduler
实现的,只是对effect
函数的封装。当触发副作用函数时如果scheduler
存在就会直接执行scheduler
,而我们将回调函数放入scheduler
中,执行时就会将scheduler
中的回调函数执行,这其实就是watch
函数的执行流程,最简单的代码实现如下:
// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb) {
effect(
// 触发读取操作,从而建立联系
() => source.foo,
{
scheduler: () => {
// 当数据变化时,调用cb函数
cb()
}
}
)
}
可以这样使用
const data = {foo: 1}
const obj = new Proxy(data, {
get: (target, key, receiver) => {
track(target, key, receiver)
return Reflect.get(target, key, receiver)
},
set: (target, key, value, receiver) => {
Reflect.set(target, key, value, receiver)
trigger(target, key)
return true
}
});
watch(obj, () => {console.log('change')})
obj.foo++
这样一个最简单的watch
函数就实现了,同时也可以运行结果,但是实际上我们上面代码的watch
函数只是针对了obj.foo
做侦听,实际上我们可能对整个对象做侦听,所以在访问对象元素做关联的部分需要完善
// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb) {
effect(
// 触发读取操作,从而建立联系
() => traverse(source),
{
scheduler: () => {
// 当数据变化时,调用cb函数
cb()
}
}
)
// seen防止交叉引用形成死循环
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
的第一个元素,即侦听的元素除了使用对象的方式传入还可以使用``getter`函数传入,如:
watch(() => obj.foo, () => {console.log('change')})
所以我们在处理的时候需要将传入的两种情况兼容一下,其实也很简单,我们可以统一使用getter
的方式来进行处理。
// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb) {
let getter
if(typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
effect(
// 触发读取操作,从而建立联系
() => getter(),
{
scheduler: () => {
// 当数据变化时,调用cb函数
cb()
}
}
)
// 省略代码 ......
}
获取watch函数的新值和旧值
获取watch
函数的新值和旧值主要是依靠的lazy
机制,在effect
当中副作用函数传入的是getter
函数,所以只要我们每次执行就可以获取到值,使用了lazy
标记之后,每次会返回一个待执行的getter
,这样在获取新值时就可以直接执行getter
函数获取值。
const effectStack = []
function effect(fn, options = {}) {
const effectFn = () => {
activeEffect = effectFn
//先清除再执行,自然就形成了分支切换
cleanup(effectFn)
// 用栈记录下当前的辅作用函数
effectStack.push(effectFn)
const res = fn()
effectStack.pop()
// 递归出来时改变activeEffect指向
activeEffect = effectStack[effectStack.length-1]
return res
}
effectFn.options = options
effectFn.deps = []
if(options.lazy)
return effectFn
effectFn()
}
// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb) {
let getter
if(typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let newValue, oldValue
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(),
{
// 打上lazy标记,在需要获取的地方再执行
lazy: true,
scheduler: () => {
// 调用effectFn获取到改变后的值
newValue = effectFn()
// 当数据变化时,调用cb函数
cb(oldValue, newValue)
// 回调结束后更新旧值
oldValue = newValue
}
}
)
// 第一次手动调用effectFn,相当于直接执行getter,获取到初始值
oldValue = effectFn()
// 省略代码 ......
}
watch(() => obj.foo, (oldValue, newValue) => {console.log(oldValue, newValue)})
obj.foo++
watch函数的执行时机
watch
函数实质上是对effect
函数的封装,上面章节中对effect
函数做了调度,但是对于watch
函数还没做调度,一般来说watch
函数的调度有两种
- 立即执行的调度,在创建时就执行(不需要第一次发生修改)。
- 设置
watch
函数的执行时机。
首先要实现的是创建时就立即执行的标记,还是正常使用前面章节中提到的调度器,加入一个immediate
字段作为标记,代码如下:
// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb, options = {}) {
let getter
if(typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let newValue, oldValue
// 将调度函数封装成一个独立的job函数
const job = () => {
// 调用effectFn获取到改变后的值
newValue = effectFn()
// 当数据变化时,调用cb函数
cb(oldValue, newValue)
// 回调结束后更新旧值
oldValue = newValue
}
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(),
{
// 打上lazy标记,在需要获取的地方再执行
lazy: true,
scheduler: job
}
)
// 当immediate为true的时候立即执行job,从而触发回调
if(options.immediate) {
job()
} else {
oldValue = effectFn()
}
// 第一次手动调用effectFn,相当于直接执行getter,获取到初始值
oldValue = effectFn()
}
第二种调度是在Vue3中新增的,设置回调函数的执行时机,一般通过flush
字段控制,可选择'post'/'pre'/'sync'
例如对post的实现,这表示回调函数会在dom更新之后执行,实现方法是将回调函数放入微任务队列中,这里使用的是promise
来实现的
// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb, options = {}) {
// 省略代码 ......
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(),
{
// 打上lazy标记,在需要获取的地方再执行
lazy: true,
scheduler: () => {
if(options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
// 当immediate为true的时候立即执行job,从而触发回调
if(options.immediate) {
job()
} else {
oldValue = effectFn()
}
// 省略代码.....