0%

watch源码剖析

watch的本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数,利用了effect和options.scheduler选项,利用副作用函数重新执行时的可调度性,一个watch本身会创建一个effect,当这个effect依赖的响应式数据变化时,会执行该effect的调度函数,即scheduler,这里的scheduler可以认为是“回调”,所以我们只需要在scheduler中执行用户通过watch注册的回调函数即可

比如以下例子:

1
2
3
4
5
6
const data={foo:1}
const obj = new Proxy(data,{})
watch(obj,()=>{
console.log("数据变化了")
})
obj.foo++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//首先判断source类型,如果是函数类型,则说明用户直接传递了getter函数,这时直接使用用户的getter函数
//如果不是函数类型,调用traverse函数递归读取
//traverse函数递归读取,当任意属性发送变化时都能触发回调函数的执行
function watch(source, cb) {
let getter;
//如果source是函数,则说明传递的是getter,则直接赋值给getter,触发读取操作,建立联系
if (typeof source === "function") {
getter = source;
} else {
//按照原来的实现调用traverse
getter = () => traverse(source);
}
//定义旧值和新值
let oldValue, newValue;
//开启lazy选项并把返回值存储到effectfn中一遍后续手动调用
const effectfn = effect(
//执行getter
() => getter,
{
lazy: true,
scheduler() {
//在scheduler重新执行一遍副作用函数得到的是新值
newValue = effectfn();
cb(newValue, oldValue);
//更新旧值
oldValue = newValue;
},
}
);
//第一次执行得到的值时旧值
oldValue = effectfn();
}
//能读取一个对象上的任意属性,当任意属性发生变化时都能够触发回调函数执行
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 (let k in value) {
traverse(value[k], seen);
}
}

如何拿到新值和旧值:lazy选项创建了一个懒执行的effect,最下面部分我们手动调用effectFn函数得到的返回值就是旧值,即第一次执行得到的值,当变化发生触发scheduler调度函数执行时,会重新调用effectFn函数并得到新值,这样我们总可以拿到旧值和新值,接着把它们传递给回调函数cb即可,再用新值更新旧值

立即执行的watch和回调执行时机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function watch(source, cb,options={}) {
let getter;
//如果source是函数,则说明传递的是getter,则直接赋值给getter
if (typeof source === "function") {
getter = source;
} else {
//按照原来的实现调用traverse
getter = () => traverse(source);
}
//定义旧值和新值
let oldValue, newValue;
//提取scheduler调度函数为一个独立的job函数
const job = () => {
newValue=effectfn()
cb(newValue, oldValue);
//更新旧值
oldValue = newValue;
}
//开启lazy选项并把返回值存储到effectfn中一遍后续手动调用

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();
}
}

由于回调函数时立即执行,所以第一次回调执行时没有旧值,因此此时回调函数的oldValue值为undefined

过期的副作用:

watch回调函数接收第三个参数onInvalidate,它是一个函数,类似于事件监听器,我们可以使用onInvalidate函数注册一个回调,这个回调函数会在当前副作用函数过期时执行

使用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//过期副作用
watch(obj,async(newValue,oldValue,onInvalidate) => {
//定义一个标志,代表当前副作用函数是否过期,默认为false,代表没有过期
let expired = false
//调用onInvalidate函数注册一个过期回调
onInvalidate(()=>{
//过期时设置expired为true
expired=true
})
//发送请求
const res=await fetch('path/to/request')
//只有副作用函数没过期时才执行后序操作
if(!expired){
finalData=res
}
})
//第一次修改
obj.foo++;
setTimeout(()=>{
//第二次修改
obj.foo++
},200)

watch处理过期回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function watch(source, cb,options={}) {
let getter;
//如果source是函数,则说明传递的是getter,则直接赋值给getter
if (typeof source === "function") {
getter = source;
} else {
//按照原来的实现调用traverse
getter = () => traverse(source);
}
//定义旧值和新值
let oldValue, newValue;
//cleanup存储用户注册的过期回调
let cleanup
function onInvalidate(fn){
cleanup=fn
}
//提取scheduler调度函数为一个独立的job函数
const job = () => {
newValue=effectfn()
//调用回调函数前,先调用过期回调
if(cleanup){
cleanup()
}
//将onInvalidate作为回调函数第三个参数,以便用户使用
cb(newValue, oldValue,onInvalidate);
//更新旧值
oldValue = newValue;
}
//开启lazy选项并把返回值存储到effectfn中一遍后续手动调用

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();
}
}

第一次修改obj.foo,立即执行,watch回调函数调用onInvalidata,注册过期回调,接着A请求,加入1000ms返回结果,我们在200ms后第二次修改obj,foo,又会导致watch回调函数执行,会执行过期回调,将expired设为true,则请求A的结果返回将被抛弃,避免过期副作用回调函数带来的影响