前言:
以下就是vue响应式的核心
1、reactive:数据劫持
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
| import { isObject } from '@vue/shared' import { track, trigger } from './effect'
// 缓存代理结果:保证了同一个对象返回相同的代理结果,用weakMap的原因是它的属性可以是object,且不存在垃圾回收的问题 const reactiveMap = new WeakMap() // 常量:实现将代理后的对象再次传入reactive中,不进行再一次的代理 const enum ReactiveFlags { IS_REACTIVE = '__v_isReactive' }
// 被Proxy包裹后,每次读取属性时都会先通过这个方法 const mutableHandlers = { get(target, key, receiver) { // 1. 在经过get劫持后,如果访问到的key就是ReactiveFlags.IS_REACTIVE,就说明被代理的对象,又被传进来了,所以直接返回true if (key === ReactiveFlags.IS_REACTIVE) return true // 2. 使用reflect的原因是通过改变this指向来保证对象中的每一个属性都能够被依赖收集(详细看下面) const res = Reflect.get(target, key, receiver) // 3. 进行依赖收集逻辑 track(target, key) // 4. 判断如果res是一个对象,则进行递归代理。保证对象内部的对象也被Proxy if(isObject(res)){ return reactive(res); } return res }, set(target, key, value, receiver) { let oldValue = target[key] Reflect.set(target, key, value, receiver) // 1.新旧值不一样的时候,触发更新逻辑 if (oldValue !== value) { trigger(target, key, value, oldValue) } return true } }
export function reactive(target) { // 1. 先判断target是不是个对象,reactive只能处理对象类型的数据 if (!isObject(target)) return // 2. 如果能够从从缓存中读取,则直接返回 const existingProxy = reactiveMap.get(target) if(existingProxy) return existingProxy // 3. 如果被代理后的对象,又被传入进来了,那么应该将这个被代理的对象直接返回,而不是再代理一次 // 第一次没被代理过if里面是undefined, // 第二次target被代理过了,并且target[ReactiveFlags.IS_REACTIVE]是个取值操作所以会走上面get逻辑返回true if (target[ReactiveFlags.IS_REACTIVE]) return target // 4. 没有缓存过,就使用proxy进行代理 const proxy = new Proxy(target, mutableHandlers) // 5. 缓存proxy结果 reactiveMap.set(target, proxy) return proxy }
|
2、effect:实现依赖收集和触发更新
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| // 1. 当前正在执行的effect // 通过一个变量让effect和reactive之间成功建立了联系。实现了依赖收集,就是reactive中值发生改变了,自动执行相应effect函数的功能 export let activeEffect = undefined
// 声明清理effect的一个方法,在每次依赖收集前进行调用 function cleanupEffect(effect) { const { deps } = effect; // 清理effect for (let i = 0; i < deps.length; i++) { deps[i].delete(effect); } effect.deps.length = 0; }
// 编写ReactiveEffect类 class ReactiveEffect { // 2. 设置一个父节点的标识:来解决effect嵌套调用的问题 parent = undefined // 定义一个依赖数组,保存着一个effect对应了哪些依赖 deps = [] // 表示当前处于激活态,要进行依赖收集 active = true // 将scheduler挂载effect实例上,保证当依赖发生变化的时候,我们可以执行自己的逻辑。 constructor(public fn, public scheduler) { } run() { // 失活态默认调用run的时候,只是重新执行传入的函数,并不会发生依赖收集 if (!this.active) { return this.fn() } try { // 1-1.设置正在运行的是当前effect activeEffect = this // 清理上一次依赖收集 cleanupEffect(this) // 执行传入的函数 return this.fn() } finally { activeEffect = this.parent // 2-1. 执行完当前effect之后,还原activeEffect为当前effect的父节点 this.parent = undefined // 2-2. 重置父节点标记 } } // 声明stop方法 stop() { if (this.active) { // 失活就停止依赖收集 this.active = false cleanupEffect(this) } } }
// 我们创建一个响应式effect导出,并且让effect首先默认执行 // effect是底层方法, 很多方法都是基于它进行封装的。 export function effect(fn, options: any = { }) { const _effect = new ReactiveEffect(fn, options.scheduler) _effect.run() // 给effect添加一个返回值,通过这个返回值可以调用_effect实例中的stop和run等方法 // 手动调用_effect中的stop可以停止依赖收集、手动调用_effect的run方法就类似于Vue中的forceUpdate,可以强制刷新组件 const runner = _effect.run.bind(_effect) runner.effect = _effect return runner }
// track函数实现了依赖收集的逻辑 // targetMap的key是整个对象,value是一个map结构。 // map结构的key是属性,value是set结构,存储和属性对应的一个effect const targetMap = new WeakMap() export function track(target, key) { // 只有在effect方法中改变了reactive对象,才会被进行依赖收集,因为此时activeEffect不是undefined if (activeEffect) { // 首先在targetMap中获取target let depsMap = targetMap.get(target) // 如果没有,就新建一个映射表,这里使用Map是因为之后的key可能是字符串 if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } // 如果有映射表,就查找有没有当前的属性 let dep = depsMap.get(key) // 如果没有这个属性,就使用Set添加一个集合 if (!dep) { depsMap.set(key, (dep = new Set())) } // 判断如果没有的话,再去添加 let shouldTrack = !dep.has(activeEffect) if (shouldTrack) { // 在同一个effect中,如果多次使用同一属性,那么就不需要多次进行依赖收集 dep.add(activeEffect) activeEffect.deps.push(dep) // 在effect中记录所有依赖,后续便于清理(多对多联系建立) } } }
// trigger实现触发更新的逻辑 export function trigger(target, key, newValue, oldValue) { // 通过对象找到对应属性,让这个属性对应的effect重新执行 const depsMap = targetMap.get(target) // 获取对应的映射表 if (!depsMap) return const dep = depsMap.get(key) // 属性对应的所有effect集合,是个set // 进行一次拷贝,防止自己删除元素的同时,自己添加,造成死循环 const effects = [...dep] effects && effects.forEach(effect => { // 执行每个effect中的run方法;正在执行的effect,不要多次执行,防止死循环 if (effect !== activeEffect) { // 如果用户传入了scheduler,那么就执行用户自定义逻辑,否则还是执行run逻辑 if(effect.scheduler) { effect.scheduler() } else { effect.run() } } }) }
|
一、Vue2和Vue3的对比
这里我们不得不先提及一下Vue2的响应式原理,说句现实的话,面试的时候,肯定会一起问的,那么如果能够将两者结合在一起,进行有条理的对比分析回答,那么绝对是一个亮眼的加分项。
1、响应式原理对比
Vue2不足:
- 在使用Vue2的时候,进行数据劫持使用的是Object.defineproperty,需要对我们data中定义的所有属性进行重写,从而添加getter和setter,正是因为了这一步,所以导致,如果data中定义的属性过多,性能就会变差。
- 在写项目的时候,有的时候会碰到需要新增或删除属性的操作,那么直接新增/删除,就无法监控变化,所以需要通过一些api比如$set,$delete进行实现,其实原理上还是使用了Object.defineproperty进行了数据劫持。
- 针对数组的处理,没有使用Object.defineproperty进行数据劫持,因为如果给一个很长的数组的每一项,都添加getter和setter,那多来几个数组,就崩掉了,而且日常开发中我们通过数组索引进行修改数组的操作比较少。所以Vue2的方式就是采用重写了一些常用的数组方法比如unshift,shift,push,pop,splice,sort,reverse这七个方法,来解决数组数据响应式的问题。
Vue3改进:
- Vue3使用了Proxy来实现了响应式数据变化,从而从根本上解决了上述问题,逻辑也简化了好多。
2、写法区别对比
- 在Vue2中使用的是OptionsAPI,我们在写代码的时候,如果页面比较复杂,那么可能就会在data中定义很多属性,methods中定义很多方法,那么相关的逻辑就不在同一块地方,我们在找代码的时候,就可能比较累,鼠标滚轮或者触摸板来回上下翻找。Vue3使用了CompositionAPI,可以把某一块逻辑,单独写在一起,解决了这种反复横跳的问题。
- Vue2中所有的属性都是通过this来进行访问的,this的指向一直是JS中很恶心的问题,一不小心就搞不清this的指向,代码就会出问题。Vue3直接干掉了this。
- Vue2中,很多没有使用的方法或者属性,都会被打包,并且全局的API都可以在Vue对象上访问到。比如我们在Computed中,定义了3个值,但是页面中只用到了1个,那么依旧会把这3个Computed值全部都打包。Vue3使用的CompositionAPI,对tree-shaking非常友好,代码压缩后的体积也就更小。
- Vue2中的mixins可以实现相同逻辑复用,抽离到一个mixin文件中,但是会有数据来源不明确的问题,命名上也会产生冲突。而Vue3使用CompositionAPI,提取公共逻辑可以抽成单独的hooks,非常方便,避免了之前的问题。
当然,在简单的页面中,我们依旧可以使用OptionsAPI,就是Vue2的写法。CompositionAPI在开发比较复杂的页面中,书写起来显得非常方便。
二、reactivity模块的基本使用
老规矩,我们先简单的看下,这个模块的使用方法,然后再来一步一步,简单实现里边的方法。打开上篇文章创建好的项目,在项目根目录,我们执行pnpm install vue -w,先用一下Vue3官方提供的方法,看看是啥效果。安装好后,我们通过node_modules文件夹找到@vue/reactivity/dist/reactivity.esm-browser.js这个文件,通过文件名字我们就能看出来,这个是esModule可以放在浏览器中运行的。把这个文件复制一份,直接放在我们自己reactivity/dist目录下,然后修改reactivity/dist/index.html的代码如下:
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
| <!DOCTYPE html> <html lang="en">
<head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head>
<body> <div id="app"> </div> <script type="module"> import { effect, reactive } from './reactivity.esm-browser.js' const state = reactive({ name: '张三', age: 18 }) // effect 会自动执行进行页面渲染。effect中使用reactive对象的时候,会进行依赖收集,reactive对象属性变化的时候,effect会重新执行 effect(() => { app.innerHTML = state.name + ': ' + state.age }) setTimeout(() => { state.name = '李四' }, 2000) </script> </body>
</html>
|
我们这里介绍上述代码中的两个API,第一个就是我们熟知的reactive,没错,在项目中如果想定义一个响应式对象的话,就把对象传进reactive中就好了。
那么effect又是啥呢?如果我们只是写业务,其实很难用到这个方法,但effect确是一个非常重要的方法(又叫副作用函数),执行effect就会渲染页面,所以渲染页面的核心离不开effect方法。
一句话,reactive方法会将对象变成proxy对象,effect中使用reactive对象的时候,会进行依赖收集,等之后reactive对象中的属性发生变化的时候,会重新执行effect函数。
我们在浏览器中执行上边的代码,会发现过了2秒后,我们只是将state.name赋值成了李四,但是页面也重新被渲染了,名字从张三变成了李四。等看完本篇文章的代码后,可以回过头来再来理解上边的那句话。
有人可能有些疑问了,reactive我在项目中确实有用到过,但是这个effect方法,在项目中根本没用到过啊,甚至听都没听说过,没错,effect方法是底层方法,项目中用不到非常正常,但是watch,watchEffect总该用过吧?嘿嘿,没错,都是基于effect进行了封装从而实现的,别急,我们在下边的文章中会娓娓道来。
三、开始实现reactivity模块中的方法
首先我们在shared中添加一个新方法:
1 2 3 4
| // 用来判断是不是一个对象 export const isObject = value => { return value != null && typeof value === 'object' }
|
之后,我们在reactivity/src目录下,新建reactive.ts文件,(用来1. 写reactive的主逻辑):
1、实现reactive的基本主逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import { isObject } from '@vue/shared' const mutableHandlers = { get(target, key, receiver) { return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { Reflect.set(target, key ,value, receiver) // 严格模式下如果不返回true就会报错 return true } } export function reactive(target) { // 先判断target是不是个对象,reactive只能处理对象类型的数据 if (!isObject(target)) return const proxy = new Proxy(target, mutableHandlers) return proxy }
|
2、使用Reflect的原因:
我们用最简单的代码,写了reactive的核心逻辑,从代码中也看到,reactive中只能处理对象类型的数据。还有一点,细心的朋友可能会发现,在get和set中,使用了Reflect的get,set方法,那为什么不直接用target[key]呢,效果不是一样的么?看起来是这样,但是在一些情况下,就能看到明显的问题。我们先举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| let obj = { name: 'zhangsan', get nickName{ return 'nickName:' + this.name } } let proxyObj = new Proxy(obj, { get(target, key, receiver) { console.log('收集依赖:', key) return target[key] } }) // 进行取值操作 console.log(proxyObj.nickName)
|
上述代码中,是一个很简单的代理,如果我们在页面中,使用了proxyObj.nickName这个取值代码,那么根据相应逻辑,执行代码打印的结果就是:
1 2
| 收集依赖: nickName nickName:zhangsan
|
那么很明显的问题就是,obj中的name属性,没有被依赖收集,那么如果在后续操作中,我们对proxyObj.name = ‘xxxxxx’进行赋值了,因为没有被依赖收集到,所以虽然数据变化了,但是页面视图却并没有同步发生变化。说到底还是因为this指向的原因,当前this指向了obj,而我们希望这个this指向被代理后的proxyObj,这样才能够将name属性也收集到,那么所以,我们此时应该使用Reflect,来使this正确的指向被代理后的proxyObj属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| let obj = { name: 'zhangsan', get nickName() { return 'nickName:' + this.name } } let proxyObj = new Proxy(obj, { get(target, key, receiver) { console.log('收集依赖:', key) return Reflect.get(target, key, receiver) } }) // 进行取值操作 console.log(proxyObj.nickName)
|
经过此番修改,我们再执行代码,会发现,诶name属性也被成功的进行依赖收集了,达到了我们的预期.这就是为什么这里要使用Reflect的原因啦。
1 2 3
| 收集依赖: nickName 收集依赖: name nickName:zhangsan
|
3、实现传入同一个对象,返回相同的代理结果:
经过这个小插曲,我们回到reactive代码中。虽然核心逻辑写好了,但是我们要考虑一些小问题,比如在下方代码中,如果用Vue3官方源码来执行,那么如果对于同一个对象进行多次代理,都应该返回同一个代理,结果为true,但是在我们目前的代码中,没有过这个判断,只要在reactive中传入一个对象,就进行new Proxy()生成一个新的代理,所以结果为false,这样肯定是不合理的。
1 2 3 4 5
| import { reactive } from 'vue' const obj = { name: 'zhangsan' } let proxy1 = reactive(obj) let proxy2 = reactive(obj) console.log(proxy1 === proxy2)
|
那么应该如何做到如果传入同一个对象,就返回相同的代理结果呢?其实想一想大致的思路就有了,没错,需要有个缓存表,来记录每次传入的对象是不是重复了,如果重复,就返回已经存在的代理对象。那应该用什么缓存呢?没错,就是用WeekMap,好处就是它的key能存放object类型的数据,而且不存在垃圾回收的问题,我们来补充完整逻辑吧!
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
| import { isObject } from '@vue/shared' // 1.我们利用WeakMap,来定义一个缓存表 const reactiveMap = new WeakMap() const mutableHandlers = { get(target, key, receiver) { return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { Reflect.set(target, key ,value, receiver) // 严格模式下如果不返回true就会报错 return true } } export function reactive(target) { // 先判断target是不是个对象,reactive只能处理对象类型的数据 if (!isObject(target)) return // 2.先从缓存表中读取代理结果,如果能找到,就直接返回 const existingProxy = reactiveMap.get(target) if(existingProxy) return existingProxy // 没有缓存过就正常new Proxy() const proxy = new Proxy(target, mutableHandlers) // 代理后,在缓存表中缓存结果 reactiveMap.set(target, proxy) return proxy }
|
4、实现将代理后的对象再次传入reactive中,不进行再一次的代理:
这时候,我们再引入自己的reactive,执行刚才那段测试代码,发现console.log(proxy1 === proxy2)
返回的就是true。这个问题解决了,但是新的问题又来了,还是回到刚才那个测试代码,这次将代理后的对象,再次传入到reactive中。在源码中返回的结果依旧是true,但是在我们的代码中,因为传入被代理后的对象,又是一个新的对象,所以会再次被代理。那么,我们怎么才能够判断这种情况呢?
1 2 3 4 5
| import { reactive } from 'vue' const obj = { name: 'zhangsan' } let proxy1 = reactive(obj) let proxy2 = reactive(proxy1) console.log(proxy1 === proxy2)
|
很多人第一反应就是我判断传入的值是不是proxy不就完事了,首先,并没有什么好的办法,判断传入的值是一个proxy代理后的对象,其次,如果用户自己new Proxy()生成了一个代理的对象,那么凭啥不让人家传入reactive中呢?之所以要做上文和现在这两点优化,是因为同一个对象,或同一个对象经过代理后的结果,多次传入reactive中后不会被再次进行代理,提高了效率。
这里,新版本的Vue3采用了一个比较巧妙的方法来解决这个问题,第一次看可能会有些绕,所以最好多看几遍代码,或在浏览器中进行断点调试。
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
| import { isObject } from '@vue/shared'
const reactiveMap = new WeakMap() const enum ReactiveFlags { IS_REACTIVE = '__v_isReactive' } const mutableHandlers = { get(target, key, receiver) { // 2.在经过get劫持后,如果访问到的key就是ReactiveFlags.IS_REACTIVE,就说明被代理的对象,又被传进来了,所以直接返回true if (key === ReactiveFlags.IS_REACTIVE) return true // 保证target[ReactiveFlags.IS_REACTIVE]为true return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { Reflect.set(target, key ,value, receiver) // 严格模式下如果不返回true就会报错 return true } }
export function reactive(target) { // 先判断target是不是个对象,reactive只能处理对象类型的数据 if (!isObject(target)) return // 如果能够从从缓存中读取,则直接返回 const existingProxy = reactiveMap.get(target) if(existingProxy) return existingProxy // 1.如果被代理后的对象,又被传入进来了,那么应该将这个被代理的对象直接返回,而不是再代理一次 if (target[ReactiveFlags.IS_REACTIVE]) return target // 第一次没被代理过不走上面的get逻辑,第二次被代理过了走上面get逻辑返回true // 没有缓存过,就使用proxy进行代理 const proxy = new Proxy(target, mutableHandlers) // 缓存proxy结果 reactiveMap.set(target, proxy) return proxy }
|
其实就是增加了一个常量枚举值,那么在Vue3内部,这些常量都是以__v开头的,IS_REACTIVE这个常量就代表着是否是一个已经被代理的reactive对象。新增的代码非常简洁,我们简单过一遍整体的流程。
首先,当一个普通对象第一次被传入进reactive中的时候,target[ReactiveFlags.IS_REACTIVE]肯定是undefined,这个毫无疑问,返回的值我们称为proxy1。注意重点来了,当我们再次将proxy1传入到reactive中的时候,因为proxy1已经是一个被代理的对象了,所以在经过if(target[ReactiveFlags.IS_REACTIVE]) return target这行代码的时候,因为target[ReactiveFlags.IS_REACTIVE]是一个取值操作,所以就会命中get中的逻辑,也就是命中这行代码if (key === ReactiveFlags.IS_REACTIVE) return true,返回了true,因为返回了true,所以根据后边的逻辑,就直接return target,将proxy1自己直接返回了。
好好品味一下这段逻辑,非常的巧妙。到这里,reactive的核心内容我们已经完成了,那么还有一些其他的方法,和细节,我们这里就不再多说,之后分析源码的时候,如果遇到再去讲解分析。
四、编写effect方法
1、effect的主逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| // reactivity/src/effect.ts 文件
// 2.编写ReactiveEffect类 class ReactiveEffect { constructor(public fn) { } run() { // 执行传入的函数 return this.fn() } }
// 1. 首先我们创建一个响应式effect导出,并且让effect首先默认执行 export function effect(fn) { const _effect = new ReactiveEffect(fn) _effect.run() }
|
2、实现reactive中值发生改变了,自动执行相应effect函数的功能(也就是实现怎么进行依赖收集):
那么,effect的最基本架子,就搭起来了。接下来是一个很关键的步骤,effect是怎么和reactive建立起联系,产生关联的呢?换句话讲,当我们定义的reactive变量中的值发生变化了,是怎么执行相应effect的函数呢?有些朋友自然而然就想到了依赖收集、触发更新这两个词,别急,我们一步一步来分析,其实建立联系用到了一个很巧妙的方法,那就是导出一个变量,那么这个变量就代表着effect的实例,从reactive模块中再导入这个变量,那么就相当于建立起了联系,我们看具体代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| // reactivity/src/effect.ts 文件
// 3、当前正在执行的effect export let activeEffect = undefined // 2.编写ReactiveEffect类 class ReactiveEffect { constructor(public fn) { } run() { // 4.设置正在运行的是当前effect activeEffect = this // 执行传入的函数 return this.fn() } } // 1. 首先我们创建一个响应式effect导出,并且让effect首先默认执行 export function effect(fn) { const _effect = new ReactiveEffect(fn) _effect.run() }
|
没错,就是这两行简单的代码,其实就解释了依赖收集,是怎么收集的。我们可以先在reactive模块中导入这个变量,简单的调试看下结果:
1 2 3 4 5 6 7 8 9
| // reactivity/src/reactive.ts import { activeEffect } from './effect' ... get() { ... console.log(activeEffect) ... } ...
|
多余的代码不写了,为了清晰,我们只写调试代码。刷新页面,我们可以看到,在执行effect方法中传入的函数时,因为我们在函数中使用到了reactive定义的变量,所以可以清楚地看到activeEffect被成功的打印了出来,至此,effect和reactive之间成功建立了联系。后续所有的代码都是建立在这条之上的。
3、每次执行effect方法的时候,activeEffect都为当前的effect
有聪明的小伙伴可能有疑问了,那如果我们在index.html中,调用了2次或多次effect函数,按现在的代码不就有问题了么,因为run了多次之后,或者在effect外部又改变了reactive定义变量的值,那activeEffect不就乱套了么?没错,所以我们要保证,每次执行effect方法的时候,activeEffect都为当前的effect,解决方法也很简单,我们再添加几行代码:
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
| // reactivity/src/effect.ts 文件
// 3、当前正在执行的effect export let activeEffect = undefined // 2.编写ReactiveEffect类 class ReactiveEffect { constructor(public fn) { } run() { try { // 4.设置正在运行的是当前effect activeEffect = this // 执行传入的函数 return this.fn() } finally { // 5.在执行完传入的函数后,将activeEffect置空,这样做还有个好处就是,如果在effect方法外部使用 // 了reactive定义的变量,那么就不会被监听到,因为此时activeEffect已经被置为null了 activeEffect = null } } } // 1. 首先我们创建一个响应式effect导出,并且让effect首先默认执行 export function effect(fn) { const _effect = new ReactiveEffect(fn) _effect.run() }
|
4、解决effect嵌套调用的问题
我们继续,那么问题又来了,如果按照现在我们的effect中的代码,如果在使用effect方法的时候,进行了嵌套调用,那activeEffect就会出bug了,什么意思呢?我们改变一下index.html中的代码,然后稍加分析。
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
| <!DOCTYPE html> <html lang="en">
<head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head>
<body> <div id="app"> </div> <script type="module"> import { effect, reactive } from './reactivity.esm-browser.js' const state = reactive({ name: '张三', age: 18 }) effect(() => { app.innerHTML = state.name + ': ' + state.age effect(() => { app.innerHTML = state.name }) app.innerHTML = state.age }) </script> </body> </html>
|
我们仔细分析下嵌套部分的代码:当调用外部的effect方法时,activeEffect为外部的effect,我们这里简称outer effect,紧接着,又调用了内部的effect方法,那么按照我们现有的effect逻辑,此时activeEffect又会变为内部的effect,我们简称inner effect,注意,此时我们内部的effect执行完毕后,按照现有逻辑,activeEffect会清空变为null,但是此时外部的effect并没有执行完毕,还剩一句app.innerHTML = state.age代码没有执行,没错,这就有问题了,当前的activeEffect因为被清空重置为null了,所以当对state.age进行取值的时候,effect和reactive之间的联系就断了(没有被依赖收集),而想正确建立联系,那么此时的activeEffect就应该是outer effect,怎么去做呢?这种嵌套的关系,是不是很像树形结构?树型结构的特点就是有父节点和子节点,所以,我们只需要标记父子关系即可:
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
| // reactivity/src/effect.ts 文件
// 3、当前正在执行的effect export let activeEffect = undefined // 2.编写ReactiveEffect类 class ReactiveEffect { // 6.设置一个父节点的标识 parent = undefined constructor(public fn) { } run() { try { // 4.设置正在运行的是当前effect activeEffect = this // 执行传入的函数 return this.fn() } finally { // 5. 6.合并成下方代码 activeEffect = this.parent // 执行完当前effect之后,还原activeEffect为当前effect的父节点 this.parent = undefined // 重置父节点标记 } } } // 1. 首先我们创建一个响应式effect导出,并且让effect首先默认执行 export function effect(fn) { const _effect = new ReactiveEffect(fn) _effect.run() }
|
这下,按照上边的逻辑,我们再分析下嵌套逻辑,就能跑的通了,所以属性发生变化的时候,都可以在reactive中的get中被监听到。那么接下来,我们便可以写之前常提到的依赖收集和触发更新了。我们发现,reactive和effect方法,其实是多对多的关系,即一个reactive中的属性,可以在多个effect方法中使用,而一个effect方法中,又可以使用多个reactive中的属性。
所以,我们之前常说的依赖收集,其实可以理解为,使用我们自己定义的一个名叫track的方法,在get中收集每个响应式属性对应的effect方法,让这个属性和effect产生关联;而触发更新,则是使用我们自己定义的trigger方法,在set中触发更新的逻辑,执行每个响应式属性所对应的effect方法。
那么我们首先在reactive文件中,导入并且调用这两个方法,之后,我们再去effect文件中实现这两个方法:
5、实现依赖收集和触发更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| // reactivity/src/reactive.ts 文件 import { track, trigger } from './effect' ... const mutableHandlers = { get(target, key, receiver) { if (key === ReactiveFlags.IS_REACTIVE) return true const res = Reflect.get(target, key, receiver) // 1. 进行依赖收集逻辑 track(target, key) return res }, set(target, key, value, receiver) { let oldValue = target[key] Reflect.set(target, key, value, receiver) // 2.新旧值不一样的时候,触发更新逻辑 if (oldValue !== value) { trigger(target, key, value, oldValue) } return true } } ...
|
接下来,我们在effect中再实现这两个方法:
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| // reactivity/src/effect.ts 文件
// 3、当前正在执行的effect export let activeEffect = undefined // 2.编写ReactiveEffect类 class ReactiveEffect { // 6.设置一个父节点的标识 parent = undefined // 定义一个依赖数组,保存着一个effect对应了哪些依赖 deps = [] constructor(public fn) { } run() { try { // 4.设置正在运行的是当前effect activeEffect = this // 执行传入的函数 return this.fn() } finally { // 5. 6.合并成下方代码 activeEffect = this.parent // 执行完当前effect之后,还原activeEffect为当前effect的父节点 this.parent = undefined // 重置父节点标记 } } } // 1. 首先我们创建一个响应式effect导出,并且让effect首先默认执行 export function effect(fn) { const _effect = new ReactiveEffect(fn) _effect.run() }
// 7. 实现依赖收集的逻辑 // 记录依赖关系 const targetMap = new WeakMap() export function track(target, key) { // 只有在effect方法中改变了reactive对象,才会被进行依赖收集,因为此时activeEffect不是undefined if (activeEffect) { // 首先在targetMap中获取target let depsMap = targetMap.get(target) // 如果没有,就新建一个映射表,这里使用Map是因为之后的key可能是字符串 if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } // 如果有映射表,就查找有没有当前的属性 let dep = depsMap.get(key) // 如果没有这个属性,就使用Set添加一个集合 if (!dep) { depsMap.set(key, (dep = new Set())) } // 判断如果没有的话,再去添加 let shouldTrack = !dep.has(activeEffect) if (shouldTrack) { // 在同一个effect中,如果多次使用同一属性,那么就不需要多次进行依赖收集 dep.add(activeEffect) activeEffect.deps.push(dep) // 在effect中记录所有依赖,后续便于清理(多对多联系建立) } } }
// 8. 实现触发更新 export function trigger(target, key, newValue, oldValue) { // 通过对象找到对应属性,让这个属性对应的effect重新执行 const depsMap = targetMap.get(target) // 获取对应的映射表 if (!depsMap) return const dep = depsMap.get(key) // 属性对应的所有effect集合,是个set dep && dep.forEach(effect => { // 执行每个effect中的run方法;正在执行的effect,不要多次执行,防止死循环 if (effect !== activeEffect) effect.run() }) }
|
那么注意,此时的数据结构,很可能会让人很晕乎,我们稍作解释:此时的targetMap大致上应该是长这个样子的(注意,key是对象):{ {name: ‘xxx’, age: xxx}: {‘name’: [dep]} },也就是weakMap : map : set这种结构,targetMap的key是整个对象,value是一个map结构,map结构的key是属性,value是set结构,存储和属性对应的一个个effect,如果还是不清楚,那么可以将targetMap打印在控制台中。
关于第8步骤trigger中,在循环调用effect.run方法前,会有一个防止死循环的判断,这是啥意思呢?我们简单解释一下,如果在index.html中,这样调用effect方法的话:
1 2 3 4 5
| effect(() => { // 每次修改state.name都是新的随机数 state.name = Math.random() app.innerHTML = state.name + ':' + state.age })
|
很明显,上述代码就变成了死循环,因为当state.name的值发生变化后,就会触发更新,又执行了effect方法,而在执行effect方法的时候,又因为重新改变了state.name的值,所以就又会触发effect方法,就成了无线递归的死循环代码。所以,我们这边要加一个判断,表明如果当前正在执行的effect如果和activeEffect不相同的时候,才去执行,这样,就不会造成自己调用自己,死循环的结果。
到这里,我们的代码依旧有些小问题可以优化,我们来看一个比较有意思的场景,改变index.html中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script> ... const state = reactive({ name: '张三', age: 18, flag: true }) effect(() => { console.log('页面刷新') app.innerHTML = state.flag ? state.name : state.age }) setTimeout(() => { state.flag = false setTimeout(() => { console.log('name被修改了') state.name = '李四' }) }, 1000) </script>
|
我们在浏览器中执行这个代码,会发现页面过了1秒,变为了18,控制台的结果却打印了4行,顺序是:
1 2 3 4 5 6
| 页面刷新 // 1秒后 页面刷新 // 又过了1秒后 name被修改了 页面刷新
|
那么问题来了,name被修改后,不应该又触发一次页面刷新的逻辑,因为此时flag已经变为了false,按理来说依赖收集应该只收集flag和age,所以当改变name的时候,不会触发更新。我们再梳理下当前代码,依赖收集和触发更新的流程:一开始effect会直接执行,所以会直接输出页面刷新,此时依赖收集的属性有flag和name,过了1秒钟,flag改为了false,所以又会触发页面更新,此时依赖收集的是flag和age(注意,name的依赖收集依旧存在,没有被清理掉,问题就出在这),又过了1秒钟,打印了name被修改了,但是因为此时name的依赖收集依旧存在,在改了name的值后,依旧触发了effect函数,所以紧接着就打印了页面刷新。
看到这,是不是就知道问题所在和怎么去解决呢?没错,就是在进行下次依赖收集之前,要把之前的依赖收集先进行清空,这样,就不会存在上边这种,明明没有收集name的依赖,但是当改变name的值后,页面依旧触发更新的情况了。
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| // reactivity/src/effect.ts 文件
// 3. 当前正在执行的effect export let activeEffect = undefined // 9-1. 声明清理effect的一个方法,在每次依赖收集前进行调用 function cleanupEffect(effect) { const { deps } = effect; // 清理effect for (let i = 0; i < deps.length; i++) { deps[i].delete(effect); } effect.deps.length = 0; } // 2.编写ReactiveEffect类 class ReactiveEffect { // 6.设置一个父节点的标识 parent = undefined // 定义一个依赖数组,保存着一个effect对应了哪些依赖 deps = [] constructor(public fn) { } run() { try { // 4.设置正在运行的是当前effect activeEffect = this // 9-2. 清理上一次依赖收集 cleanupEffect(this) // 执行传入的函数 return this.fn() } finally { // 5. 6.合并成下方代码 activeEffect = this.parent // 执行完当前effect之后,还原activeEffect为当前effect的父节点 this.parent = undefined // 重置父节点标记 } } } // 1. 首先我们创建一个响应式effect导出,并且让effect首先默认执行 export function effect(fn) { const _effect = new ReactiveEffect(fn) _effect.run() }
// 7. 实现依赖收集的逻辑 // 记录依赖关系 const targetMap = new WeakMap() export function track(target, key) { // 只有在effect方法中改变了reactive对象,才会被进行依赖收集,因为此时activeEffect不是undefined if (activeEffect) { // 首先在targetMap中获取target let depsMap = targetMap.get(target) // 如果没有,就新建一个映射表,这里使用Map是因为之后的key可能是字符串 if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } // 如果有映射表,就查找有没有当前的属性 let dep = depsMap.get(key) // 如果没有这个属性,就使用Set添加一个集合 if (!dep) { depsMap.set(key, (dep = new Set())) } // 判断如果没有的话,再去添加 let shouldTrack = !dep.has(activeEffect) if (shouldTrack) { // 在同一个effect中,如果多次使用同一属性,那么就不需要多次进行依赖收集 dep.add(activeEffect) activeEffect.deps.push(dep) // 在effect中记录所有依赖,后续便于清理(多对多联系建立) } } }
// 8. 实现触发更新 export function trigger(target, key, newValue, oldValue) { // 通过对象找到对应属性,让这个属性对应的effect重新执行 const depsMap = targetMap.get(target) // 获取对应的映射表 if (!depsMap) return const dep = depsMap.get(key) // 属性对应的所有effect集合,是个set // 9-3 进行一次拷贝,防止自己删除元素的同时,自己添加,造成死循环 const effects = [...dep] effects && effects.forEach(effect => { // 执行每个effect中的run方法;正在执行的effect,不要多次执行,防止死循环 if (effect !== activeEffect) effect.run() }) }
|
我们看9-1步骤,那么这步就是用到了我们之前定义的deps = []这个存放当前activeEffect对应了哪些依赖(set结构)。找到后清理掉所有的effect,再进行下一次的依赖收集,这样就不会造成类似于”缓存”的问题。那么在9-3步骤,为什么要进行一次拷贝呢?其实很简单,在一个循环中,同时对effect进行了添加和删除操作,刚删完元素,就又添加了新元素,那岂不是循环就成了死循环,一直跳不出来了么,所以,解决的方法就是进行一次拷贝,删除和运行分开进行,就不会有死循环的问题了。
经过我们一步步的完善,那么effect的代码就逐渐接近尾声了。我们加把劲,继续来!
那么有一种很常见的场景,当我们代理的对象,内部又有很多对象,那这些对象就不会被代理,比如:
1 2 3 4 5 6 7
| const obj = reactive({ name: '张三', info: { age: 18, sex: '男' } })
|
那么这时候,我们就需要进行递归代理,方法也很简单,在reactive.ts文件中get最后添加几行代码即可:
1 2 3 4 5 6 7 8 9 10 11
| get(target, key, receiver) { ...... if (key === ReactiveFlags.IS_REACTIVE) return true const res = Reflect.get(target, key, receiver) track(target, key) // 判断如果res是一个对象,则进行递归代理 if(isObject(res)){ return reactive(res); } return res },
|
接下来我们增加实例的2个方法。对于effect方法,其实是有一个返回值的,那么我们拿到这返回值,通过调用里边的方法,可以手动进行执行effect中的run方法,和停止依赖收集的stop方法,我们首先来实现拿到返回值进行手动调用(类似于Vue中的forceUpdate,可以强制刷新组件),其实原理非常简单,就把new ReactiveEffect(fn)这个结果,当成返回值不就好了么,没错,不过有些细节,我们通过完善effect.ts文件来继续看:
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| // reactivity/src/effect.ts 文件
// 3. 当前正在执行的effect export let activeEffect = undefined // 9-1. 声明清理effect的一个方法,在每次依赖收集前进行调用 function cleanupEffect(effect) { const { deps } = effect; // 清理effect for (let i = 0; i < deps.length; i++) { deps[i].delete(effect); } effect.deps.length = 0; } // 2.编写ReactiveEffect类 class ReactiveEffect { // 6.设置一个父节点的标识 parent = undefined // 定义一个依赖数组,保存着一个effect对应了哪些依赖 deps = [] // 11-1. 表示当前处于激活态,要进行依赖收集 active = true constructor(public fn) { } run() { // 11-3. 失活态默认调用run的时候,只是重新执行传入的函数,并不会发生依赖收集 if (!this.active) { return this.fn() } try { // 4.设置正在运行的是当前effect activeEffect = this // 9-2. 清理上一次依赖收集 cleanupEffect(this) // 执行传入的函数 return this.fn() } finally { // 5. 6.合并成下方代码 activeEffect = this.parent // 执行完当前effect之后,还原activeEffect为当前effect的父节点 this.parent = undefined // 重置父节点标记 } } // 11-2. 声明stop方法 stop() { if (this.active) { // 失活就停止依赖收集 this.active = false cleanupEffect(this) } } } // 1. 首先我们创建一个响应式effect导出,并且让effect首先默认执行 export function effect(fn) { const _effect = new ReactiveEffect(fn) _effect.run() // 10. 给effect添加一个返回值,通过这个返回值可以调用_effect实例中的stop和run等方法 const runner = _effect.run.bind(_effect) runner.effect = _effect return runner }
// 7. 实现依赖收集的逻辑 // 记录依赖关系 const targetMap = new WeakMap() export function track(target, key) { // 只有在effect方法中改变了reactive对象,才会被进行依赖收集,因为此时activeEffect不是undefined if (activeEffect) { // 首先在targetMap中获取target let depsMap = targetMap.get(target) // 如果没有,就新建一个映射表,这里使用Map是因为之后的key可能是字符串 if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } // 如果有映射表,就查找有没有当前的属性 let dep = depsMap.get(key) // 如果没有这个属性,就使用Set添加一个集合 if (!dep) { depsMap.set(key, (dep = new Set())) } // 判断如果没有的话,再去添加 let shouldTrack = !dep.has(activeEffect) if (shouldTrack) { // 在同一个effect中,如果多次使用同一属性,那么就不需要多次进行依赖收集 dep.add(activeEffect) activeEffect.deps.push(dep) // 在effect中记录所有依赖,后续便于清理(多对多联系建立) } } }
// 8. 实现触发更新 export function trigger(target, key, newValue, oldValue) { // 通过对象找到对应属性,让这个属性对应的effect重新执行 const depsMap = targetMap.get(target) // 获取对应的映射表 if (!depsMap) return const dep = depsMap.get(key) // 属性对应的所有effect集合,是个set // 9-3 进行一次拷贝,防止自己删除元素的同时,自己添加,造成死循环 const effects = [...dep] effects && effects.forEach(effect => { // 执行每个effect中的run方法;正在执行的effect,不要多次执行,防止死循环 if (effect !== activeEffect) effect.run() }) }
|
我们直接看步骤10,这样写的好处就是const runner = effect(() => { console.log(‘页面刷新’) app.innerHTML = state.name }),在通过上述方式拿到了返回值runner后,我们可以手动执行runner()方法,或runner.effect.run()方法,进行手动刷新页面,我们通过修改index.html文件,来尝试用下这个功能,不然只说概念,没有场景,很难理解。
1 2 3 4 5 6 7 8 9 10 11 12
| <script> import { effect, reactive } from './reactivity.js' const state = reactive({ name: '张三', age: 18, flag: true }) let a = '李四' const runner = effect(() => { app.innerHTML = state.name + a }) setTimeout(() => { a = '王五' runner() }, 1000) </script>
|
通过上边的代码,我们执行后发现,页面在1秒钟后,还是发生了改变,虽然我们只是在定时器里边改了变量a的值,但是因为我们进行了手动触发effect.run()方法,所以页面还是会更新的。那么我们继续看什么叫做停止依赖收集。看步骤11-1~11-3,非常明确,如果调用了stop方法,那么就会停止所有的依赖收集,并且就算之后进行了手动调用runner.run()方法,因为步骤11-3,所以也只是会再次调用effect中传入的函数,并不会进行依赖收集和触发更新。
到这里,effect就接近尾声了,那么为了和下篇文章进行接轨,我们再讲最后的一个优化点。上文提到了,我们可以手动执行runner()或runner.effect.run()方法进行页面的强制更新,但是这个runner方法,我们现在是写在effect方法之外的地方,能不能想个办法,将这个逻辑放在effect方法中呢?我们对index.html稍加改造,然后根据我们想要的数据结构,来反向推断代码应该如何写,我们想要的结果是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <script> import { effect, reactive } from './reactivity.js' const state = reactive({ name: '张三', age: 18 }) const runner = effect(() => { app.innerHTML = state.name console.log('我执行啦') }, { scheduler: () => { setTimeout(() => { console.log('页面重新刷新了') runner() }, 1000) } }) setTimeout(() => { state.name = '王五' console.log('名字改变了') }, 1000) </script>
|
我们给effect方法,提供第二个参数,参数中有一个scheduler属性,这个属性就对应着我们刚才定时器中的逻辑。我们期望的结果是,过了1秒钟,state.name = ‘王五’发生改变后,触发的是我们effect方法中第二个参数中的scheduler对应的逻辑,而不是effect方法中的第一个回调逻辑,这样就达到了当依赖发生变化的时候,我们可以执行自己的逻辑。想要的效果很明确了,那我们来完善下逻辑吧!
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| // reactivity/src/effect.ts 文件
// 3. 当前正在执行的effect export let activeEffect = undefined // 9-1. 声明清理effect的一个方法,在每次依赖收集前进行调用 function cleanupEffect(effect) { const { deps } = effect; // 清理effect for (let i = 0; i < deps.length; i++) { deps[i].delete(effect); } effect.deps.length = 0; } // 2.编写ReactiveEffect类 class ReactiveEffect { // 6.设置一个父节点的标识 parent = undefined // 定义一个依赖数组,保存着一个effect对应了哪些依赖 deps = [] // 11-1. 表示当前处于激活态,要进行依赖收集 active = true // 12-2. 将scheduler挂载effect实例上 constructor(public fn, public scheduler) { } run() { // 11-3. 失活态默认调用run的时候,只是重新执行传入的函数,并不会发生依赖收集 if (!this.active) { return this.fn() } try { // 4.设置正在运行的是当前effect activeEffect = this // 9-2. 清理上一次依赖收集 cleanupEffect(this) // 执行传入的函数 return this.fn() } finally { // 5. 6.合并成下方代码 activeEffect = this.parent // 执行完当前effect之后,还原activeEffect为当前effect的父节点 this.parent = undefined // 重置父节点标记 } } // 11-2. 声明stop方法 stop() { if (this.active) { // 失活就停止依赖收集 this.active = false cleanupEffect(this) } } } // 1. 首先我们创建一个响应式effect导出,并且让effect首先默认执行 export function effect(fn, options: any = { }) { // 12-1. 添加options.scheduler的传参 const _effect = new ReactiveEffect(fn, options.scheduler) _effect.run() // 10. 给effect添加一个返回值,通过这个返回值可以调用_effect实例中的stop和run等方法 const runner = _effect.run.bind(_effect) runner.effect = _effect return runner }
// 7. 实现依赖收集的逻辑 // 记录依赖关系 const targetMap = new WeakMap() export function track(target, key) { // 只有在effect方法中改变了reactive对象,才会被进行依赖收集,因为此时activeEffect不是undefined if (activeEffect) { // 首先在targetMap中获取target let depsMap = targetMap.get(target) // 如果没有,就新建一个映射表,这里使用Map是因为之后的key可能是字符串 if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } // 如果有映射表,就查找有没有当前的属性 let dep = depsMap.get(key) // 如果没有这个属性,就使用Set添加一个集合 if (!dep) { depsMap.set(key, (dep = new Set())) } // 判断如果没有的话,再去添加 let shouldTrack = !dep.has(activeEffect) if (shouldTrack) { // 在同一个effect中,如果多次使用同一属性,那么就不需要多次进行依赖收集 dep.add(activeEffect) activeEffect.deps.push(dep) // 在effect中记录所有依赖,后续便于清理(多对多联系建立) } } }
// 8. 实现触发更新 export function trigger(target, key, newValue, oldValue) { // 通过对象找到对应属性,让这个属性对应的effect重新执行 const depsMap = targetMap.get(target) // 获取对应的映射表 if (!depsMap) return const dep = depsMap.get(key) // 属性对应的所有effect集合,是个set // 9-3 进行一次拷贝,防止自己删除元素的同时,自己添加,造成死循环 const effects = [...dep] effects && effects.forEach(effect => { // 执行每个effect中的run方法;正在执行的effect,不要多次执行,防止死循环 if (effect !== activeEffect) { // 12-3. 如果用户传入了scheduler,那么就执行用户自定义逻辑,否则还是执行run逻辑 if(effect.scheduler) { effect.scheduler() }else { effect.run() } } }) }
|
通过12-1~12-3的这三个步骤,我们不难理解,只需要在trigger方法中,也就是触发的时候通过判断是否传入了options.scheduler属性,来执行我们自己定义的scheduler函数逻辑或者是执行默认的effect.run方法。到此,我们的effect.ts文件可以说是暂时写完了。
结语
呼,长舒一口气。聪明的你,有没有发现,最后effect增加的内容,有点眼熟的感觉呢?没错,这种写法像极了watch和watchEffect,类似于第一个参数是观察的属性,第二个参数是执行的回调。那么剩下的内容,就是我们下篇文章要说的了,面试中也经常会问到watch,computed是如何实现的呢?且听下回分解~
作者:柠檬soda水
链接:https://juejin.cn/post/7200699300337418297
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。