Vue3源码--响应式原理1(effect)

 最近学习了下Vue3的源码,抽空写一些自己对3.x源码的解读,同时算是学习的一个总结吧,也能加深自己的印象。  就先从3.x的响应式系统说起吧。 回忆  首先大概回忆一下2.x的响应式系统,主要由这几个模块组成,Observer,Watcher,Dep。 Observer负责通过defineProperty劫持Data,每个Data都各自在闭包中维护一个Dep的实例,用于收集依赖着它的Watcher。Dep维护一个公共的Target属性,用于保存当前的需要被收集依赖的Watcher。每次Data被劫持的getter执行的时候,如果Dep.Target!==undefine, dep和Watcher实例就互相收集对方~  2.x的响应式系统其实是围绕着Watcher,也可以说围绕着watch API的,包括render是一个renderWatcher,computed是通过lazyWatcher实现。这并不是一个好的设计模式,不符合六个设计原则的(单一职责原则,开闭原则)。而响应式系统也无法独立出来。 对比  那么3.x是怎样实现这一块的内容的呢。  首先3.x响应式系统相关的代码在packages/reactivity/src里。3.x的响应式系统的核心由两个模块构成: effect, reactive。  reactive模块的功能比较简单,就是给数据设置代理,类似于2.x的Observer,不同的点在于是用的Proxy去做代理。  effect模块,传入一个函数,然后让这个函数需要被响应式数据影响,目前具体在3.x中包括,watch API,computed API,还有组件的更新都是依赖effect实现的,但是这个模块没有暴露在Vue对象上面。所以说effect模块是一个偏向于底层只有基础功能的模块,相比2.x,这明显是一个较好的设计模式。 Effect  关于effect模块,最主要的是里面的effect,track,trigger三个方法。  effect方法是一个高阶函数,或者也可以说是工厂方法,接收一个函数作为参数,返回一个effect实例方法,它使这个函数中的响应式数据可追踪到这个effect实例,如果有响应式数据发生了改变,就会再次执行这个effect,可以参照源码中调用这个方法的三个地方computed.ts,apiWatch.ts,renderer.ts。  首先来看看track:以下是track方法的主要逻辑以及注释,track方法按字面的解释就是追踪,会在数据Proxy的get代理中调用,track这个数据本身。其实简单说就做了一件事情,把当前的active effect收集到响应式数据的depsMap里面。 其实并不复杂,这里和2.x不同的是,2.x是每个数据各自都在闭包中维护deps对象,这里是用一个全局的Store去保存响应式数据影响的effects,实现了模块的解耦。 // target为传入的响应式数据对象,type为操作类型,key为target上被追踪的key export function track(target: object, type: TrackOpTypes, key: unknown) { // 如果shouldTrack为false 或者 当前没有活动中的effect,不需要执行追踪的逻辑 // shouldTrack为依赖追踪提供一个全局的开关,可以很方便暂停/开启,比如用于setup以及生命周期执行的时候 if (!shouldTrack || activeEffect === undefined) { return } // 所有响应式数据都是被封装的对象,所以用一个Map来保存更方便,Map的key为响应式数据的对象 let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } // 同样为每个响应式数据按key建立一个Set,用来保存target[key]所影响的effects let dep = depsMap.get(key) if (dep === void 0) { // 用一个Set去保存effects,省去了去重的判断 depsMap.set(key, (dep = new Set())) } // 如果target[key]下面没有当前活动中的effect,就把这个effect加入到这个deps中 if (!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) } }  看完track方法的逻辑之后,effect方法的主要逻辑其实就呼之欲出了,那就是启动响应式追踪---设置shouldTrack为true,设置activeEffect为当前的effect,然后再调用传入的方法并追踪依赖,最后返回一个封装后的实例effect方法。 export function effect( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect { if (isEffect(fn)) { fn = fn.raw } // createReactiveEffect是一个工厂方法,返回一个函数实例 const effect = createReactiveEffect(fn, options) // 如果不是lazy effect(lazy effect主要用于computed),立即执行这个effect if (!options.lazy) { effect() } return effect } // createReactiveEffect是一个工厂方法,返回一个函数实例 function createReactiveEffect( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect { const effect = function reactiveEffect(...args: unknown[]): unknown { return run(effect, fn, args) } as ReactiveEffect effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect } function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown { // 如果effect.active为false,跳过追踪直接调用传入的函数 if (!effect.active) { return fn(...args) } if (!effectStack.includes(effect)) { // 清除effect中之前记录的deps cleanup(effect) try { // 设置shouldTrack为true enableTracking() // 设置activeEffect为当前的effect,另外把当前的effect入栈(比如渲染子组件的时候,这个栈就起作用了) effectStack.push(effect) activeEffect = effect // 执行传入effect的函数 return fn(...args) } finally { effectStack.pop() // 设置shouldTrack为上一次的shouldTrack(注:和effect一样,shouldTrack也有一个栈) resetTracking() // 设置activeEffect为上一个activeEffect activeEffect = effectStack[effectStack.length - 1] } } }  最后来看一下trigger方法,trigger方法的调用在Proxy的set代理中,作用就是在修改一个响应式数据的时候,执行这个响应式对象的depsMap中所有的effect。 // target为修改的响应式数据对象,type为操作类型,key为target上具体修改的参数 // newValue,oldValue, oldTarget都很好理解 export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map | Set ) { const depsMap = targetMap.get(target) if (depsMap === void 0) { // never been tracked return } const effects = new Set() const computedRunners = new Set() // 如果操作类型是CLEAR,说明数据类型是Map,或者Set(注意,3.x的响应式系统是支持Map和Set的) // CLEAR操作需要触发集合上的所有属性的effects if (type === TriggerOpTypes.CLEAR) { // collection being cleared // trigger all effects for target depsMap.forEach(dep => { // addRunners功能其实很简单,就是区分这个effect是普通的effect还是一个computed effect addRunners(effects, computedRunners, dep) }) // 如果是更改length长度,说明是个数组,只需要触发key在这个新的length之后的数据 } else if (key === 'length' && isArray(target)) { depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { addRunners(effects, computedRunners, dep) } }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { // 大部分的情况,触发这个key下面的effets addRunners(effects, computedRunners, depsMap.get(key)) } // also run for iteration key on ADD | DELETE | Map.SET if ( type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE || (type === TriggerOpTypes.SET && target instanceof Map) ) { // 如果是添加/删除数组里的项,或者Set,Map的add,delete,set几个方法,同时也会改变length或者size, // 在Map和Set里面,受size影响的一些方法(比如size,forEach,entries,keys,values),都会把effect收集到ITERATE_KEY里面。 // 具体可参考packages/reactivity/src/collectionHandler.ts里面的实现 const iterationKey = isArray(target) ? 'length' : ITERATE_KEY addRunners(effects, computedRunners, depsMap.get(iterationKey)) } } const run = (effect: ReactiveEffect) => { scheduleRun( effect, target, type, key, __DEV__ ? { newValue, oldValue, oldTarget } : undefined ) } // Important: computed effects must be run first so that computed getters // can be invalidated before any normal effects that depend on them are run. // run每个effect computedRunners.forEach(run) effects.forEach(run) } // addRunners功能其实很简单,就是区分这个effect是普通的effect还是一个computed effect // 普通的effect存在effects里面,computed effect存在computedRunners里面 function addRunners( effects: Set, computedRunners: Set, effectsToAdd: Set | undefined ) { // 省略 } // 调度将要执行的effect,是否传入effect.options.scheduler决定了执行的方式 // 若没有传入,就立即同步执行,若有,则执行调度方法,传入effect // 3.x中关于异步调度方法的实现可以查看packages/runtime-core/src/scheduler.ts中的queueJob方法 function scheduleRun( effect: ReactiveEffect, target: object, type: TriggerOpTypes, key: unknown, extraInfo?: DebuggerEventExtraInfo ) { if (effect.options.scheduler !== void 0) { effect.options.scheduler(effect) } else { effect() } }  以上源码都是基于 vue-next-alpha8 版本。  effect模块相关的内容就这些,下一篇是关于reactive模块的。

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):