手写Vue3计算属性和Watch
# 前言
上一篇文章讲了Vue3的响应式,这篇文章就来解析一下和响应式息息相关的
计算属性和监视,computed和watch也是日常开发中常用的api,他们的实现都是基于响应式原理去做的,下面就来看看具体是如何实现的吧
# computed
首先来讲讲computed,聊到这个api首先需要知道他的特性,有以下两点
- 本质上是一个
依赖于其他响应式数据计算出的最终值 - 具有
缓存的特性
根据上一篇文章的effect函数(也就是响应式系统中生成依赖的工具函数)我们可以大致写出如下代码
class createComputed{
constructor(getter) {
this._effect = new CreateEffect(getter,()=>this.isCache = true) // 创建一个依赖
this.isCache = true //缓存标识,只有当他为true时才去重新计算值
this._value = null
}
get value(){
if(this.isCache){
this.isCache = false
this._value = this._effect.run()
}
return this._value
}
set value(val){
throw new Error('计算属性不允许赋值!')
}
}
function computed(getter){
return new createComputed(getter)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
首先我们创建一个初始化计算属性的类,在这个类的构造函数里我们去创建了一个effect,并且传入了计算函数和另外一个函数。下面贴一下和依赖相关的代码,更详细的了解请看Vue3响应式原理
// 存放所有依赖
let globalMap = new WeakMap()
let activityEffect = null //用于保存当前所需要被收集的依赖,对标Vue2中new Watcher时将自身保存在Dep的target属性上一样
class CreateEffect {
/**
* @param fn 更新试图的函数
* @param scheduler 触发计算属性的更新函数
*/
constructor(fn,scheduler) {
this.fn = fn
this.scheduler = scheduler;
}
run(){
activityEffect = this //将自身设置为当前需要被收集的依赖
const res = this.fn()
activityEffect = null
return res
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
直接看代码可能有点懵,下面我用文字描述一下计算属性是如何工作的。
const proxy1 = ref(1)
const proxy2 = ref(2)
const computedVal = computed(()=>{
return proxy1 + proxy2
})
2
3
4
5
6
上面是一个很简单的计算属性的使用例子,他的运行步骤如下
- 首先我们调用
computed函数,并传入计算的回调函数。 - 此时
computed函数调用new createComputed进行初始化,此时的函数可以这样表示
new createComputed(()=>{
return proxy1 + proxy2
})
2
3
接下来会在
createComputed这个类的构造函数里去创建一个依赖,也就是走了new CreateEffect(getter,()=>this.isCache = true),并且把计算函数和另外一个触发计算属性的函数传入进去了。下文中我们把这个effect暂且称之为computedEffect当我们在访问这个计算属性的时候,就会触发到
get value的拦截而
get value这个函数里就会去执行之前创建出来的那个依赖的run方法,effect的run方法会调用我们之前传入的getter函数,并且返回getter的值。至此,计算属性就完成了对依赖的计算,但还没完。下面抛出一个问题,因为计算属性是依赖于
被计算的属性的,那如果被计算的属性没有发生变化,我们还有必要在get value的时候去重复的执行计算函数吗?答案当然是
没必要了,所以在get value的时候有这么一个判断
get value(){
if(this.isCache){
this.isCache = false
this._value = this._effect.run()
}
return this._value
}
2
3
4
5
6
7
只有当
isCache为true的时候才回去重新计算值,并且会把isCache改为false,防止下次访问计算属性的时候进行没必要的计算。那么问题来了,我们怎么知道被依赖的属性发生了变化呢?也就是说什么时候把
isCache变为true呢?还记得
new CreateEffect(getter,()=>this.isCache = true)这行代码里我们传入的第二个函数吗?他就是用来修改isCache的值的。至于他什么时候触发,我们就要回到第一次获取计算属性的值的时候当我们第一次触发计算属性的
get的时候由于isCache默认值是true,所以此时我们会触发computedEffect的run方法,由于我们触发了依赖的
run方法,所以此时的被激活的依赖(也就是activityEffect)会变成computedEffect,而当我们在执行计算函数的时候,会触发被依赖的响应式数据的get,也就是上文中proxy1和proxy2的get。这个时候
proxy1和proxy2的依赖数组里都会把computedEffect添加进去。当我们对proxy1和proxy2进行set操作的时候会触发所有相关依赖,而computedEffect同样也会被触发。再回顾一段
trigger(触发所有依赖)中的代码
effectSet.forEach((effect) => effect.scheduler ? effect.scheduler() : effect.run())
2
3
- 由于
computedEffect.scheduler的存在,所以再触发依赖时回去调用scheduler函数,也就是上文中我们传入的()=>this.isCache = true函数 - 这样依赖
isCache就变为true了,而当我们下一次获取计算属性的值的时候就会重新去执行计算函数了。
以上就是计算属性最最最完整的执行逻辑了,如果还看不懂我就没办法了。
# computed 总结
computed
computed其实就是创建了一个新的依赖并将getter函数作为依赖的更新函数,然后拦截这个计算属性的get,在get中执行getter函数返回最终值,并且在执行函数之前会判断当前是否需要重新计算,这个判断的值则是通过被依赖的属性发生变更才去改变的
在初次访问计算属性的时候会将计算依赖添加进被依赖的属性的依赖数组内,当被计算的属性发生变化,会触发这个计算依赖,从而导致计算属性是否重新计算的流程控制变量发生变化,这样当我们下一次获取计算属性时就会重新计算,否则会使用缓存数据
# watch
说完了计算属性,下面再来学习一下watch,watch其实和computed一样,都是借助依赖函数effect来完成
首先简写的watch如下
function Watch (fn,cb,opt){
let _effect
let oldVal
function job (){
const value = _effect.run()
cb(value,oldVal)
oldVal = value
}
_effect = new CreateEffect(fn,()=>job())
oldVal = _effect.run()
}
2
3
4
5
6
7
8
9
10
11
代码剖析:首先函数接受三个参数,对应我们在使用watch时传入的那三个函数——被监听的值,回调函数,配置项。
fn里需要接收一个函数,并且返回被监视的值。这一块为什么要这么写?
(还记不记得在创建依赖时我们需要传入一个更新函数,依赖在被执行的时候会去调用这个更新函数。而这里watch会把第一个参数作为依赖的更新函数传入。这样当我们创建依赖的时候就会触发被监视属性的get,他就会收集起这个监视的依赖了。)
回到函数里,首先在函数体内创建了一个job函数,这个函数的作用就是获取当前依赖的最新值,然后调用cb将新值旧值传递出去。而调用job函数的时机是什么时候呢?
和computed类似,computed再创建依赖的时候传递了第二个参数给依赖,他通过这个参数来实现缓存的功能。而watch也是通过这种方式来实现调用cb的。
当被监视的属性的set被触发后,调用监视依赖。此时我们传入的()=>job(),就对应了依赖的scheduler。依赖就会去执行这个函数,而不是执行run方法。
在job中我们手动触发依赖的run方法得到最新值然后调用cb将新旧两个值传过去。这样一个监视就完成了。
# 总结
computed和watch的实现其实都离不开依赖,而依赖在设计的过程中开放了第二个回调的参数给这两个Api,所以两者都是借助依赖然后在不同的时机去执行回调函数来实现的。两者其实在实现上非常的像,都是依赖于一些响应式数据的变化而去做某些事,不同点是计算属性是一个具体的值,由被依赖的属性计算而来。而watch的动作主要在回调函数上,回调函数的内容可以自行定义。并且计算属性带有缓存,而watch没有