Vue3 源码学习(2)-- 响应式系统(1)
前言
响应式系统式Vue.js的重要组成部分,本章来实现一个简易的响应式系统。
在实现响应式系统之前,我们需要连明白两个概念响应式数据和副作用函数
1、 响应式数据与副作用函数
1.1、副作用函数
副作用函数指的是会产生副作用的函数,如下面代码所示:
function effect(){
document.body.innerText = "hello vue3"
}
当effect函数执行的时候他会设置body的文本内容,但除了effect之外的其他函数仍然可以读取或者设置body的文本内容。也就是说:effect函数的执行会直接或者间接应影响其他函数的执行,这时我们说effect函数产生了副作用,effect函数就是副作用函数。
下面代码就是一个典型的副作用函数:
//全局变量
let val = 1
function effect(){
val =2 //修改全局变量,产生副作用
}
effect()
1.2、什么是响应式数据
理解了什么副作用函数,再来说说什么是响应式数据。假设在一个副作用函数中读取某个对象的属性
const obj = {
text:"hello world "
}
function effect(){
//effect函数的执行会读取obj.text
document.body.innerText = obj.text
}
上述代码所示,副作用函数effect会设置body
的innerText属性
,其值为obj.text
,当obj.text发生变化的时候,我们希望副作用函数effect会重新执行,如果能够实现这个目标,那么对象Obj对象就为响应式数据。
1.3、响应式数据的实现思路
如何让一个普通的obj对象变成响应式数据呢?通过观察发现两点线索
- 副作用函数effect触发的时候,会读取
obj.text
,即触发getter(读取)操作 - 当修改
obj.text
的值的时候,触发setter(设置)操作
如果我们能够拦截对象的getter和setter操作,并进行一下处理:
- 在getter的时候将副作用函数effect收集到一个“桶”里面,
- 在setter的时候重新将副作用函数effect从“桶”里取出并执行
实现上面需求,用到es6新增proxy代理对象,对数据的getter和setter进行代理
2、响应式系统的实现
由上文所述,响应式系统的工作流程如下:
- 副作用函数执行,触发getter操作并且存储effect副作用函数
- 当setter操作的时候取出依赖的effect副作用函数并执行
下面我们将利用TDD的形式来进行响应式系统的实现和完善
2.1、初始化项目和测试环境
- 在项目文件夹下执行
yarn init -y
命令生成package.json
文件 - 执行
yarn add typescript jest @types/jest -D
安装typescript
和jest
以及@types/jest
- 执行
tsc --init
命令生成tsconfig.json
文件,并修改以下几项
"lib": ["DOM", "ES6"], // 引入 DOM 和 ES6
"noImplicitAny": false, // 允许在表达式和声明中不指定类型,使用隐含的 any
"types": ["jest"], // 将 jest 的类型声明文件添加到编译中
"downlevelIteration": true // 开启 for-of 对可迭代对象的支持
- 在
package.json
中配置:
"scripts":{
test:"jest"
}
- 根据 Jest 官网进行配置,以在 Jest 中使用 Babel 和 TypeScript:执行
yarn add -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript
安装 babel-jest、@babel/core、@babel/preset-env 和 @babel/preset-typescript;创建babel.config.js
文件,并写入以下内容:
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript'
]
}
2.2、设计一个相对完善的响应式系统
响应式系统的实现需要两部分组成:
- 相应式数据(reactive、ref等)
- 副作用函数
2.2.1、实现最基础的reactive
查看Vue3文档中响应式Api部分,找到有关
reactive
的介绍Returns a reactive proxy of the object
返回一个对象的响应代理 , 类型声明 `function reactive(target:T):UnwrapNestedRefs
在实现reactive
之前,首先创建测试用例reactive.spec.ts
//reactive.spec.ts
describe("reactive",()=>{
it("Object",()=>{
//原始对象 original
const original = { foo:1}
// reactive返回对象的响应式副本
const observed = reactive(original)
// observed !== original
expect(observed).not.toBe(original)
//observed的property属性值与original值相等
expect(observed.foo).toBe(1)
})
})
实现reactive
// reactive.ts
export function reactive(obj){
// 返回proxy的实例
return new Proxy(obj,{
//对obj 进行getter和setter的代理
get(target,key){
//TODO 收集依赖(副作用函数收集)
return Reflect.get(target,key)
},
set(target,key,newVal){
const res = Reflect.set(target,key,newVal)
// TODO依赖触发(副作用函数触发)
return res
}
})
}
思考: get中为什么不直接返回
return obj[key]
set中为什么不直接赋值`obj[key] = newVal TODO 解决思考
执行yarn test reactive
命令,可以看到测试通过,这样就完成了reactive
最基础的实现。
2.2.2、实现最基础的effect
之前上文我们编写了副作用函数的名字为effect,但在实际生参数,不可能所有的副作用函数都为effect,我们希望的是副作用函数哪怕是一个匿名函数,也能够正确收集起来。为了实现这一点,我们抽离了一个ReactiveEffect
类表示真实副作用函数,effect函数则用来注册副作用函数
在effect函数中立即执行一次副作用函数,用来触发响应式对象的getter
操作,收集ReactiveEffect依赖
在实现effect
之前,首先创建测试用例effect.spec.ts
。
describe("effect",()=>{
it("should run the true effect function at once",()=>{
//真实的effect函数
const fnSpy = jest.fn(()=>{})
effect(fnSpy)
//当程序执行时,传入的副作用函数会被立即执行
expect(fnSpy).toHaveBeenCalledTimes(1)
})
it("should observe basic properties",()=>{
let dummy
//创建响应式对象爱过你
cosnt counter = reactive({num:0})
effect(()=>{dummy = counter.num})
expect(dummy).toBe(0)
//响应式数据发送变化,会再次执行该函数
counter.num = 2
expect(dummy).toBe(2)
couter.num ++
expect(dummy).toBe(3)
})
})
//effect.ts
/**
* @param fn:真实的副作用函数
*/
class ReactiveEffect(fn){
private _fn : any
constructor(fn){
// 将传入的副作用函数fn赋值给实例的私有属性 _fn
this._fn = fn
}
//执行传入的函数
run(){
this._fn()
}
/**
* funtion effect 用来注册副作用函数
* fn : 副作用函数
*/
exprot function effect(fn) {
//创建ReactiveEffect类的实例
const _effect:ReactiveEffect = new ReactiveEffect(fn)
//调用ReactiveEffect实例的run方法,执行纯如的函数
_effect.run()
}
此时我们就实现了一个不完全的effect
之后就如上文所讲,在reactive
函数返回的Proxy实例的get
中收集副作用函数,在set
中触发依赖
//reactive.ts
export function reactive(obj){
// 返回proxy的实例
return new Proxy(obj,{
//对obj 进行getter和setter的代理
get(obj,key){
const res = Reflect.get(target,key)
// 收集依赖(副作用函数收集)
track(target,key)
return res
},
set(obj,key,newVal){
const res = Reflect.set(target,key,newVal)
// TODO依赖触发(副作用函数触发)
trigger(target,key)
return res
})
}
接下来就是实现track
和trriger
函数,在实现之前,我们先思考一下怎么设计用来存储ReactiveEffect
依赖的这个桶
如何设计这个“桶”
在设计桶之前,我们先观察下面的代码:
effect(function effectFn(){
document.body.innerText = obj.text
})
在这段代码中存在三个角色:
- 被操作(读取)的代理对象obj
- 被操作(读取)的key:text
- 使用effect注册的副作用函数effectFn
如果使用target来表示一个代理对象所代理的原始原始对象,用key来表示被操作的字段名,用effetFn来表示被注册的副作用函数,那么额可以用着三个角色建立一下关系
如果两个副作用函数共同读取同一个对象的属性值:
如果在同一个副作用函数中读取了同一个对象的两哥不同属性:
如果不同的副作用读取不同的两个不同对象的不同属性:
明白了这个收集依赖的桶
是一个树形结构,我们来实现这个桶
。在实现过程中使用了一个全局的WeakMap
类型的变量targetsMap
,用于保存程序运行中的所有依赖,以及一个全局的变量activeEffect
,用于保存正在执行的ReactiveEffect
类的实例。
//effect.ts
const targetsMap = new WeakMap() //存储所有副作用的桶
let activeEffect :ReactiveEffect //用于保存当前执行的ReactiveEffect实例
class RectiveRffect {
/*其他代码*/
run(){
//调用run方法时,用全局变量activeEffect保存当前实例
activeEffect = this
this_fn()
}
}
/**
*track 用于收集依赖
*@param target
*@param key
*/
exprot function track(target,key){
if(!activeEffect) return
//获取当前响应式对象依赖的“桶” ,若为undefined,则进行初始化并保存带targetsMap中
/**
* target为当前响应式对象
* depsMap 为Map实例,用于保存与当前响应式对象相关的depsMap
*/
let depsMap:Map<any, Set<ReactiveEffect>> | undefined = targetsMap.get(target)
if(!depsMap){
depsMap = new Map<any, Set<ReactiveEffect>>()
targetsMap.set(target,depsMap)
}
//获取当前property依赖的“桶”,,若为undefined,则进行初始化并保存带depsMap中
/**
* key 为响应式对象的property
* deps 为Set实例,用于存储与当前property相关的ReactiveEffect实例
*/
let deps:Set<ReactiveEffect> | undefined = depsMap.get(key)
if(!deps){
deps = new Set<ReactiveEffect>()
depsMap.set(key,deps)
}
//最后将当前激活的副作用函数实例activeEffect添加到“桶”里
if(deps.has(activeEffect)){
return
}
deps.add(activeEffect)
}
/**
*trigger 用于触发依赖
*@param target
*@param key
*/
export function trigger(target,key){
//获取当前响应式对象对应的Map实例
let depsMap:Map<any, Set<ReactiveEffect>> | undefined = targetsMap.get(target)
//获取当前key对相应的Set实例
let deps:Set<ReactiveEffect> | undefined = depsMap.get(key)
for(const reactiveEffect of deps){
reactiveEffect.run()
}
}
执行yarn test effect
命令,可以看到测试通过,这样就完成了响应式最基础的实现。
思考: 为什么使用WeakMap
2.2.3、完善effect
1、返回变量runner
effect
执行会返回一个函数,用runner
来接受这个函数,调用runner
的时候会再次执行传入的副作用函数,同时返回该函数的返回值
在effect的测试文件effect.spec.ts
添加测试代码:
//effect.spec.ts
describe("effect",()=>{
/*其他测试代码*/
it("should return runner when call effect",()=>{
let foo = 0
//用一个变量runner来接收effect执行后返回的函数
const runner = effect(()=>{
foo++
return "foo"
})
expect(foo).toBe(1)
//调用runner时,会再吃执行传入的副作用函数并返回值
const res = runner()
expect(foo).toBe(2)
//runner执行返回该函数的返回值
expect(res).toBe('foo')
})
})
修改effect.ts
class ReactiveEffect {
/*其他代码*/
run(){
activeEffect = this
//返回传入的函数的执行结果
return this._fn()
}
}
export function effect(fn){
const _effect:ReactiveEffect = new ReactiveEffect(fn)
_effect.run()
//返回 _effect.run 并将this的指向指定为_effect
const runner = _effect.run.bind(_effect)
return runner
执行yarn test effect
命令运行effect
的测试,可以看到测试通过,这样就进一步完善了effect
的实现。
2、调度执行,接收参数scheduler
调度执行是响应式系统非常重要的特性。首先我们先明白什么是可调度性。所谓可调度性,就是当trigger动作触发的副作用函数重新执行时,有能力决定副作用函数的执行时机、次数以及方式。
我们可以设计effect
函数接收一个option
对象作为第二个参数,允许用户指定调度器scheduler
方法。用一个变量runner
接收effect
执行返回的函数,程序运行时会首先执行传入的副作用函数,而不会调用scheduler
方法,之后当传入的函数依赖的响应式对象的property
发生更行时,会调用scheduler
方法而不会直接执行该函数,只有调用runner
时才会重新执行该函数。
添加测试代码
describe("effect",()=>{
/*其他代码*/
it("scheduler",()=>{
let dummy
let run
const scheduler = jest.fn(()=>{
run++
})
const obj = reactive({foo:1})
const runner = effect(
()=>{
dummy = obj.foo
},
{
scheduler
}
)
//程序运行时会首先执行传入的函数,而不会调用scheduler方法
expect(scheduler).not.toHaveBeenCalled()
expect(dummy).toBe(1)
// 当传入的函数依赖的响应式对象的 property 的值更新时,会调用 scheduler 方法而不会执行传入的函数
obj.foo++
expect(scheduler).toHaveBeenCalledTimes(1)
expect(dummy).toBe(1)
// 只有当调用 runner 时才会执行传入的函数
runner()
expect(scheduler).toHaveBeenCalledTimes(1)
expect(dummy).toBe(2)
})
})
为了通过以上测试,需要对effect
和trigger
函数进行完善
//effect.ts
class ReactiveEffect {
scheduler?:()=>void
/*其他代码*/
constructor(fn,option){
this._fn = fn
this.scheduler = option?.scheduler
}
export funtion effect(fn,option={}){
const _effect = new ReactiveEffect(fn,option)
/*其他代码*/
}
export function trigger(traget,key){
/*其他代码*/
for(const reactiveEffect of deps){
if(reactiveEffect.scheduler){
reactiveEffect.scheduler()
}else{
reactiveEffect.run()
}
}
执行yarn test effect
命令运行effect
的测试,可以看到测试通过,这样就进一步完善了effect
的实现。
后面实现computed计算属性就会用到调度器这一属性,用来实现计算属性的缓存效果
3、stop函数
stop
函数接收effect
执行返回的函数(runner)作为参数。调用stop
后,当传入的函数依赖的响应式对象的property
发生值更新时不再执行该函数,只有当调用runner
时才会恢复执行该函数
添加测试代码
//effect.spec.ts
describe('effect', () => {
/* 其他测试代码 */
it('stop', () => {
let dummy
const obj = reactive({ prop: 1 })
const runner = effect(() => {
dummy = obj.prop
})
obj.prop = 2
expect(dummy).toBe(2)
// 调用 stop 后,当传入的函数依赖的响应式对象的 property 的值更新时不会再执行该函数
stop(runner)
obj.prop = 3
expect(dummy).toBe(2)
obj.prop++
expect(dummy).toBe(2)
// 只有当调用`runner`时才会恢复执行该函数
runner()
expect(dummy).toBe(4)
})
})
为了通过以上测试,需要对effect
的实现进行完善,实现并导出stop
调用stop
函数,响应式数据发生变化不在触发副作用函数,其根本就是stop函数触发会清空依赖的副作用函数,所以当响应式数据改变,触发trigger
函数时,副作用函数不会执行,
//effect.ts
//用于记录是否应该收集依赖,防止调用stop后触发响应式对象的property的getter时重新收集
let shouldTrack:boolean = false
class ReactiveEffect {
/*其他代码*/
//用于存储与当前实例相关的响应式对象的property对应的Set实例
deps:Array<Set<ReactiveEffect>>=[]
//用于记录当前实例的状态,为ture时为调用stop方法,否则一调用,避免重复防止重复调用 stop 方法
active:boolean = true
run(){
//若已调用stop方法则直接返回传入的函数的执行结果
if(!this.active){
return this._fn()
}
//应该收集依赖
shouldTrack = true
activeEffect = this
const res = this._fn()
//重置
shouldTrack = false
return res
}
// 用于停止传入的函数的执行
stop(){
if(this.active){
//清空依赖
cleanupEffect(this)
this.active = false
}
}
}
function cleanupEffect(reactiveEffect:ReactiveEffect){
reactiveEffect.deps.forEach(dep=>{
//reactiveEffect.deps.delete(dep) 勘误
dep.delete(reactiveEffect)
})
}
export function effect(fn,option:any={}){
/*其他代码*/
// 用一个变量 runner 保存将 _effect.run 的 this 指向指定为 _effect 的结果
const runner: any = _effect.run.bind(_effect)
// 将 _effect 赋值给 runner 的 effect property
runner.effect = _effect
return runner
}
export function track(target, key){
//若不应该收集依赖直接返回
if(!shouldTrack || activeEffect === undefined){
return
}
/*其他代码*/
dep.add(activeEffect)
// 将 dep 添加到当前正在执行的 ReactiveEffect 类的实例的 deps property 中
activeEffect?.deps.push(dep)
}
export function stop(runner){
// 调用 runner 的 effect property 的 stop 方法
runner.effect.stop()
}
执行yarn test effect
命令运行effect
的测试,可以看到测试通过,这样就实现了stop
,进一步完善了effect
的实现
4、接收onStop参数
effect
接收的第二个参数对象中,还包括一个onStop
方法。用一个变量runner
接受effect
执行返回的函数,调用stop
并传入runner
时,会执行onStop
方法。
测试代码:
//effect.spec.ts
describe('effect', () => {
/* 其他测试代码 */
it('events: onStop', () => {
const onStop = jest.fn()
const runner = effect(() => {}, {
onStop
})
// 调用 stop 时,会执行 onStop 方法
stop(runner)
expect(onStop).toHaveBeenCalled()
})
})
//effect.ts
class ReactiveEffect {
/*其他代码*/
onStop?:()=>void
constructor(fn,option){
/*其他代码*/
this.onStop = option?.onStop
}
stop(){
if(this.active){
cleanupEffect(this)
if(this.onStop){
this.onStop()
}
this.active =false
}
}
执行yarn test effect
命令运行effect
的测试,可以看到测试通过,这样就进一步完善了effect
的实现。
总结
本篇主要实现了一个最基本的reactive
和effect
,基本实现了一个简易的响应式系统。
其中还有许多edge case
的实现,《Vuejs的设计和实现》中的4.4分支切换和cleanup
、4.5嵌套effect与effect栈
等还需努力