Vue3源码Ⅳ-watch实现

watch & watchEffect 的基本用法及实现

watch

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
<script type="module">
// vue 依赖于 runtime-dom 依赖于 runtime-core 依赖于 reactivity
import { reactive, effect, watch} from '../../../node_modules/@vue/runtime-dom/dist/runtime-dom.esm-browser.js'
const state = reactive({name: 'gzy' , address: { n : 401 }})
// 对象是无法监控到前后值的更改
// watch 可以监控对象 监控函数
// 性能差,会访问对象中的所有属性,进行取值操作
watch(state, (newVal, oldVal)=>{
console.log('数据变化了',newVal, oldVal)
}, { flush: 'sync' })
state.name = 'ggggg'
console.log('数据变化outer')


// state.name 监控的是一个固定值,不能这样写
// ()=> state 不能这样写,没有访问对象中的属性,没有取值操作,没有收集依赖
watch(()=> state.name, (newVal, oldVal)=>{
console.log('数据变化了',newVal, oldVal)
}, { flush: 'sync' })
state.name = 'ggggg'
console.log('数据变化outer')

// 发现 先打印的 数据变化outer 然后才打印 数据变化了 -> 默认的watch是异步的

// {flush: 'sync'} 表示同步的调用watch

</script>

例子中可以看出

  • watch 常见用法就是监控一个函数或者响应式对象 根据返回值的变化触发对应的函数
    • 监控一个响应式对象,性能会比较差,因为会访问响应式对象中的所有属性,进行取值操作
    • 监控一个函数,函数执行时去访问响应对象中的某个属性
  • 默认watch函数是异步执行的,传入参数 { flush:’sync’ } 表示同步执行watch

watch 的实现

watch 可以看做 effect() + scheduler

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

export function watch(source, cb){
// 1)source 是一个响应式对象
// 2)source 是一个函数

// effect() + scheduler
// effect 应该传递一个函数
let getter;
if(isReactive(source)){
// 如果是响应式对象就需要包装成一个函数
getter = () => traverse(source)
}else if(isFunction(source)){
// 对于传递的本来就是函数而言不需要变化
getter = source
}
let oldVal;
// 里面的属性就会收集当前的effect
// 如果数据变化后会执行对应的scheduler方法
const effect = new ReactiveEffect(getter,()=>{
const newVal = effect.run()
cb(newVal,oldVal)
oldVal = newVal
})
oldVal = effect.run() // 会让属性和effect关联
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// = 深拷贝 seen防止死循环
function traverse(value,seen = new Set()) {
if(!isObject(value)){
return value
}

// 如果已经循环了这个对象了,那么再循环会导致死循环
if(seen.has(value)){
return value
}
seen.add(value)
for(const key in value){
traverse(value[key],seen) // 触发属性的getter
}
return value
}
1
2
3
4

export const isFunction = value => {
return typeof value === 'function'
}

watchEffect

watchEffect 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
// vue -> runtime-dom -> runtime-core -> reactivity
import { reactive, effect, watch, watchEffect} from '../../../node_modules/@vue/runtime-dom/dist/runtime-dom.esm-browser.js'
// import { reactive, effect, watch, watchEffect} from './reactivity.js'

const state = reactive({name: 'gzy' , address: { n : 401 }})

watchEffect(()=>{
// 自动收集依赖
app.innerHTML = state.name
})
setTimeout(() => {
state.name = 'gggg'
},1000)

watchEffect 的实现

可以发现watchEffect的使用方法和effect一样,而且和watch的区别只是是否传递了 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
export function watch(source,cb,options){
return dowatch(source,cb,options)
}

export function watchEffect(source,options){
return dowatch(source,null,options)
}

export function dowatch(source, cb){
let getter;
if(isReactive(source)){
// 如果是响应式对象就需要包装成一个函数
getter = () => traverse(source)
}else if(isFunction(source)){
// 对于传递的本来就是函数而言不需要变化
getter = source
}
let oldVal;
// 里面的属性就会收集当前的effect
// 如果数据变化后会执行对应的scheduler方法
const job = () => {
if(cb){
const newVal = effect.run()
cb(newVal,oldVal)
oldVal = newVal
}else{
effect.run() // watchEffect只需要运行自身就可以了
}
}
const effect = new ReactiveEffect(getter,job)
oldVal = effect.run() // 会让属性和effect关联
}
  • 把原来的watch修改为dowatch
  • 新增对应的watch和watchEffect方法,内部调用dowatch并对参数进行区分
  • dowatch内部看是否传递了cb, 传递了获取新的值和老的值传递给cb,没有传递说明是watchEffect,只需要运行自身就可以了

cleanup

下一次watch函数执行前清理掉上一次的watch

1
2
3
4
5
6
7
8
9
10
11
12
13
const state = reactive({name: 'gzy' , address: { n : 401 },age:1})

watch(()=> state.name, (newVal, oldVal, onCleanup)=>{
let flag = true
onCleanup(function(){
flag = false
})
flag && (app.innerHTML = r)
}, { flush: 'sync' })
state.age = '11'
state.age = '111'
state.age = '1111'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function dowatch(source, cb){
// ...
let clear
let onCleanup = (fn) => {
clear = fn
}
// 里面的属性就会收集当前的effect
// 如果数据变化后会执行对应的scheduler方法
const job = () => {
if(cb){
const newVal = effect.run()
cb(newVal,oldVal,onCleanup)
oldVal = newVal
}else{
effect.run() // watchEffect只需要运行自身就可以了
}
}
// ...
}