手写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没有