第5章〓响应式API 本章将详细介绍响应式API,响应式核心实现包括Proxy对象代理和effect副作用函数。读者可以结合第2章的响应式参数逻辑和第3章的整体实现,更深入地了解响应式API的处理逻辑。 视频讲解 观看视频速览本章主要内容。 5.1reactive响应式API◆ 从本节开始,将介绍Vue3中的响应式API,了解响应式API的核心实现逻辑和运行流程。其中,reactive和ref是Vue3中最核心的两个响应式API。下面将详细介绍这两个API的实现,以帮助了解响应式API的原理。 5.1.1使用方式 在Vue2中,响应式数据是通过Object.defineProperty实现的; 在Vue3中,对该部分进行了重构,通过Proxy对象代替Object.defineProperty方法,解决Vue2中无法侦测Object对象的添加和删除、数组元素的增加和删除等问题。 Vue3中响应式的原理和Vue2中差别不大,依然是依赖收集和派发更新两个部分。但在Vue3中引入了effect副作用函数,该函数与第2章的watchEffect内部函数实现基本一致。首先通过reactive处理响应式数据,当组件内部执行完setup后,通过setupRenderEffect()函数调用effect副作用函数。该完整步骤可以在$runtimecore_render文件的mountComponent函数内查看。下面展开介绍响应式数据的详细内容。 5.1.2兼容写法 除上述源码声明响应式数据外,还可以使用Vue2的写法声明响应式数据。Vue3兼容Vue2写法,因此可以将参数声明放到data方法内,返回响应式数据,统一对属性进行响应式处理。使用方式如下: data() { return { count: 1 } } 该处理逻辑在$runtimecore_component.ts文件内实现,在进行setup处理时同步处理data方法适配Vue2写法,涉及代码如下: // support for 2.x options if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) { setCurrentInstance(instance) pauseTracking() // 处理data数据的情况 applyOptions(instance, Component) resetTracking() unsetCurrentInstance() } 继续查看响应式参数的内部实现。根据调用逻辑,将通过applyOptions函数对data返回的对象进行处理,涉及代码如下: if (dataOptions) { if (__DEV__ && !isFunction(dataOptions)) { ... } const data = dataOptions.call(publicThis, publicThis) if (__DEV__ && isPromise(data)) { ... } if (!isObject(data)) { __DEV__ && warn(`data() should return an object.`) } else { instance.data = reactive(data) if (__DEV__) { ... } } } 5.1.3reactive()函数 此处调用reactive()函数对data数据进行处理,通过reactive()结合effect副作用函数实现响应式功能,代码位于$reactivity_reactive文件内,涉及代码如下: // $reactivity_reactive export function reactive(target: object) { if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap ) } 5.1.4createReactiveObject()函数 上述代码通过一个高阶函数来返回另外一个创建响应式对象的函数createReactiveObject()。通过高阶函数的方式保存参数,再通过参数形态导出更高阶的reactive、shallowReadonly等函数。 注: 在无类型 lambda演算中,所有函数都是高阶的; 在有类型 lambda演算(大多数函数式编程语言都从中演化而来)中,高阶函数就是参数为函数或返回值为函数的函数。在函数式编程中,返回另一个函数的高阶函数被称为柯里化函数。 createReactiveObject()函数涉及代码如下: function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler, collectionHandlers: ProxyHandler, proxyMap: WeakMap ) { if (!isObject(target)) { // 传入的不是对象,直接返回,不做处理 // 在开发环境中提出警告 if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`); } return target; } // 如果target已经是一个reactive对象,则直接返回 // 异常:在响应式对象上调用readonly() if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target; } // 存在代理,直接返回 const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } // 是否为可代理数据类型 const targetType = getTargetType(target) if (targetType === TargetType.INVALID) { return target } // 生成代理对象 const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ) // 在原对象上缓存代理后的对象 proxyMap.set(target, proxy) return proxy } target可代理对象包括: 没有skip标识的源对象,没有冻结的对象和Object、Array、Map、Set、WeakMap、WeakSet类型的对象。createReactiveObject()的主要逻辑可简化成如下几个步骤: (1) 过滤不可代理的情况; (2) 查询可使用缓存的情况; (3) 生成缓存代理对象。 下面结合代码对以上步骤进行分析。 (1) 过滤不可代理的情况: 通过reactive代理readonly对象。 const obj = { key: 1 } const n1 = readonly(obj) const n2 = reactive(n1) console.log('直接返回obj对象') (2) 查询可使用缓存的情况: 通过reactive多次代理同一个对象。 const obj = { key: 1 } const n1 = reactive(obj) const n2 = reactive(obj) console.log('返回第一次代理obj对象') (3) 生成缓存代理对象: 通过reactive代理reactive响应式对象。 const obj = { key: 1 } const n1 = reactive(obj) const n2 = reactive(n1) console.log('直接返回obj对象') 5.1.5mutableHandlers()函数 createReactiveObject()函数内部没有复杂逻辑,需关注的还是ProxyHandler部分,涉及代码如下: export const mutableHandlers: ProxyHandler = { get, set, deleteProperty, has, ownKeys } 5.1.6createGetter()函数 由上述代码可知,Vue3对5种方法做了代理,分别是get、set、has、deleteProperty和ownKeys。对用户来说接触最多的是get和set方法。 get通过createGetter()函数实现,涉及代码如下: function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { // 1. reactive标识位处理 if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly; } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly; } else if ( key === ReactiveFlags.RAW && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { return target; } // 2. 处理数组方法key,若有缓存,直接返回 const targetIsArray = isArray(target); if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } // 3. 缓存取值 const res = Reflect.get(target, key, receiver); // 4. 过滤无须track的key if ( // 访问builtInSymbols内的symbol key或者访问原型和ref内部属性 isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key) ) { return res; } // 5. 依赖收集 if (!isReadonly) { // 非readonly 收集一次类型为get的依赖 track(target, TrackOpTypes.GET, key); } // 6. 处理取出的值 if (shallow) { // 浅代理则直接返回通过key取到的值 return res; } if (isRef(res)) { // 如果通过key去除的是ref,则自动解开,仅针对对象 const shouldUnwrap = !targetIsArray || !isIntegerKey(key) return shouldUnwrap ? res.value : res } if (isObject(res)) { // 如果通过key取出是对象,且shallow为false,则进行递归代理 return isReadonly ? readonly(res) : reactive(res); } return res; }; } 上述方法传入两个参数,并通过高阶函数的方式保存两个参数的值。函数体内首先针对3个reactive标识key进行判断,根据key类型不同返回不同值。判断流程如图5.1所示。 图5.1判断流程 由图5.1可知,整个函数执行共分以下几种情况: (1) 判断key是否为3种特定类型的值。若是,则根据key直接返回对应值,结束当前函数的执行。 (2) 若target是数组,则调用arrayInstrumentations函数针对数组进行处理。 (3) 使用Reflect.get对target进行取值和依赖收集。 (4) 取值前过滤不需要依赖收集的key,直接返回对应值。 (5) 针对非readonly的情况进行一次get的依赖收集。 注: Reflect是一个内置的对象,用于获取目标对象的行为,提供拦截JavaScript操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。 对于当前key取出的值仍需要按基本类型、ref类型和对象类型来处理。 (1) 基本类型,直接返回。 (2) ref类型,原数据为对象时解构ref类型后返回,原数据为数组时直接返回。 (3) 对象类型,若是shallow浅代理则直接返回,若不是则返回递归代理的代理对象。 在上述步骤(2)中,针对数组处理的arrayInstrumentations()函数内部实现了对数组的includes、indexOf和lastIndexOf方法重写,加入依赖收集。代码实现如下: const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations() function createArrayInstrumentations() { const instrumentations: Record = {} ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => { instrumentations[key] = function (this: unknown[], ...args: unknown[]) { const arr = toRaw(this) as any for (let i = 0, l = this.length; i < l; i++) { // 针对数组元素进行依赖收集 track(arr, TrackOpTypes.GET, i + '') } const res = arr[key](...args) if (res === -1 || res === false) { return arr[key](...args.map(toRaw)) } else { return res } } }) ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { instrumentations[key] = function (this: unknown[], ...args: unknown[]) { // 上述方法操作时,会改变数组长度,此处暂停依赖收集,避免无限递归 pauseTracking() const res = (toRaw(this) as any)[key].apply(this, args) resetTracking() return res } }) return instrumentations } 在完成对应解析后会进行依赖收集,依赖收集通过track实现,下面介绍依赖收集函数。该函数位于$reactivity_effect.ts内,涉及代码如下: export function track(target: object, type: TrackOpTypes, key: unknown) { // 依赖收集进行的前置条件: // 1. 全局收集标识开启 // 2. 存在激活的副作用函数 if (!isTracking()) { return } // 创建依赖收集map target → deps → effect let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } trackEffects(dep, eventInfo) } export function trackEffects( dep: Dep, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { let shouldTrack = false // 设置收集深度 if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { dep.n |= trackOpBit // set newly tracked shouldTrack = !wasTracked(dep) } } else { // 全量清理 shouldTrack = !dep.has(activeEffect!) } if (shouldTrack) { // 依赖收集副作用 dep.add(activeEffect!) // 副作用保存依赖 activeEffect!.deps.push(dep) // 依赖收集的钩子函数 if (__DEV__ && activeEffect!.onTrack) { activeEffect!.onTrack( Object.assign( { effect: activeEffect! }, debuggerEventExtraInfo ) ) } } } track的目标很简单,建立当前key与当前激活effect的依赖关系,源码中使用了一个较为复杂的方式来保存这种依赖关系,依赖关系如图5.2所示。 完成后整个数据结构如图5.3所示。 图5.2依赖关系 图5.3数据结构 通过target→key→dep的数据结构,完整地存储了对象、键值和副作用的关系,并且通过set实例来对effect副作用函数去重。 理清依赖关系的数据结构后,track函数基本介绍完成。 5.1.7createSetter()函数 依赖收集主要通过get触发,派发更新主要通过set触发。set方法在$reactivity_reactive文件内,通过createSetter()函数实现派发更新,具体代码如下: function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { // 取旧值 const oldValue = (target as any)[key]; if (!shallow && !isReadonly(value)) { // 在深度代理情况下,需要手动处理属性值为ref的情况,将trigger交给ref来触发 value = toRaw(value); oldValue = toRaw(oldValue) if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value; return true; } } else { // 在浅代理模式下,行为与普通对象一致 } // 判断是新增或者修改 const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) // 设置新值 const result = Reflect.set(target, key, value, receiver); // 如果修改了通过原型查找得到的属性,无须trigger if (target === toRaw(receiver)) { if (!hadKey) { // 新增 trigger(target, TriggerOpTypes.ADD, key, value); } else if (hasChanged(value, oldValue)) { // 修改时,需要去除未改变的情况; // 数组增加元素时,会使length改变,但在达到length修改的set时; // 数组已经添加元素成功,取到的oldValue会与value相等,直接过滤掉此次不必要的 // trigger。 trigger(target, TriggerOpTypes.SET, key, value, oldValue); } } return result; }; } set函数的主要目标是修改值并正确地派发更新。首先在非shallow的情况下,需要手动处理属性值为ref的情况,将trigger()交给ref来触发。 赋值完成后的target并没有变化,说明当前设置的key来自原型链,无须触发trigger()。 如果确定是对target上key的修改,仍需要进行add和set的区分。因为存在一种场景如下: let arr = reactive([1, 2, 3]); arr.push(4); 当向响应式数组添加元素时会触发数组length的修改,在length属性修改前,数组元素已经添加成功,通过(target as any)[key]取到的值会与value相等。在这种情况下无须执行trigger()函数,使用hasChanged()函数来进行过滤。 通过trigger()函数触发更新,具体代码实现如下: export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '') export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '') const targetMap = new WeakMap() export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map | Set ) { const depsMap = targetMap.get(target) if (!depsMap) { // 未被依赖收集 return } let deps: (Dep | undefined)[] = [] if (type === TriggerOpTypes.CLEAR) { // 保存target的所有effect到deps deps = [...depsMap.values()] } else if (key === 'length' && isArray(target)) { // 数组长度变化 depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { deps.push(dep) } }) } else { // schedule runs for SET | ADD | DELETE // 除去以上两种特殊情况,若key还存在,则直接添加所有有依赖的副作用函数 if (key !== void 0) { deps.push(depsMap.get(key)) } // 根据类型操作待缓存数据 switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { // 添加新索引到数组,长度改变 deps.push(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } break case TriggerOpTypes.SET: if (isMap(target)) { deps.push(depsMap.get(ITERATE_KEY)) } break } } const eventInfo = __DEV__ ? { target, type, key, newValue, oldValue, oldTarget } : undefined // 如果deps的长度为1,则代表为空 if (deps.length === 1) { if (deps[0]) { if (__DEV__) { triggerEffects(deps[0], eventInfo) } else { triggerEffects(deps[0]) } } } else { // 新增数组变量effects,保存所有的effect副作用函数 const effects: ReactiveEffect[] = [] for (const dep of deps) { if (dep) { effects.push(...dep) } } // 调用triggerEffects()执行effect副作用函数 if (__DEV__) { triggerEffects(createDep(effects), eventInfo) } else { triggerEffects(createDep(effects)) } } } export function triggerEffects( dep: Dep | ReactiveEffect[], debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { // 若是数组则直接遍历,若不是则解构并保存为数组 for (const effect of isArray(dep) ? dep : [...dep]) { // 如果effect不是激活状态或者允许递归,则触发effect副作用函数执行 if (effect !== activeEffect || effect.allowRecurse) { // 开发环境下运行 trigger钩子函数 if (__DEV__ && effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) } // 如果存在调度,则使用调度来执行effect副作用函数 if (effect.scheduler) { effect.scheduler() } else { effect.run() } } } } 上述代码对trigger()函数的核心步骤标注,主要有如下步骤: (1) 依据不同类型保存副作用函数列表; (2) 过滤当前激活副作用函数,添加其他副作用函数; (3) 遍历所有被添加副作用函数,派发更新。 targetMap对象存储依赖和副作用函数之间的关系。trigger()函数会对targetMap对象进行遍历,调用需要被执行的副作用函数。 5.1.8ref解析 前面已经讲过ref的出现是为了包装基本类型以实现代理,API形态也有所展现,通过.value来进行访问和修改。ref函数的实现代码如下: export function ref(value?: unknown) { return createRef(value, false); } function createRef(rawValue: unknown, shallow = false) { // 如果已经是ref,则直接返回 if (isRef(rawValue)) { return rawValue; } return new RefImpl(rawValue, shallow) } class RefImpl { private _value: T private _rawValue: T public dep?: Dep = undefined // ref 标识 public readonly __v_isRef = true constructor(value: T, public readonly _shallow: boolean) { // 如果是浅代理,则使用toRaw获取原始值 this._rawValue = _shallow ? value : toRaw(value) this._value = _shallow ? value : toReactive(value) } get value() { // 依赖收集 trackRefValue(this) return this._value } set value(newVal) { // 产生了变化,则修改 newVal = this._shallow ? newVal : toRaw(newVal) if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : toReactive(newVal) // 派发更新 triggerRefValue(this, newVal) } } } // 如果是对象,则使用reactive代理对象 export const toReactive = (value: T): T => isObject(value) ? reactive(value) : value ref也是以一个高阶函数的形式来创建的,因为ref也存在shallowRef; 通过上述源码也可以看到ref返回的是一个对象,因此在读取和写入时,需要显式地指定.value,用于触发get和set拦截。在完成对reactive方法解读的情况下,再查看ref代码,会发现ref内部使用了一个对象来包装传入的值,若传入的是对象,则直接使用reactive代理,ref只负责处理来自.value的访问和修改。 5.1.9总结 响应式对象调用reactive函数对传入的对象进行判断,决定是否调用createReactiveObject()函数,该函数通过Proxy对象实现对对象的拦截。使用代理函数实现对传入对象的代理监听,保证在对象值变化时能及时得到通知。createReactiveObject()函数内部通过get方法收集依赖和set方法派发更新。在使用get进行依赖收集时,巧妙地使用Reflect对象进行get拦截,再触发整个依赖的收集。在使用set进行派发更新时,对读取收集到的effect副作用函数进行遍历,巧妙地通过run方法,来控制effect副作用函数的执行时机。 整个拦截处理完成后,再根据情况执行属性值读取或修改操作。根据传入的参数可知,此处主要针对对象进行处理,关于数组的处理方法将在5.1.6节详细介绍。整个响应式参数涉及流程如图5.4所示。 图5.4响应式参数 上面介绍了track依赖收集和trigger派发更新,串联track、trigger和effect副作用函数的执行步骤,完成响应式对象的处理。5.2节将介绍effect副作用函数。 5.2effect副作用函数◆ 当响应式数据变化时,effect副作用函数重新触发执行。接下来对effect副作用函数进行分析,其核心流程与第2章基本类似。 5.2.1实现 effect副作用函数内部的主要作用是针对组件的首次渲染和更新,派发处理逻辑,默认会在首次渲染和触发响应式数据变化阶段执行。在组件首次挂载时,触发effect副作用函数; 在组件更新时,重新执行effect副作用函数。该函数位于$runtimecore_render文件内,当组件更新时,触发componentUpdateFn()函数的执行。具体代码实现如下: // $runtime-core_render const effect = (instance.effect = new ReactiveEffect( componentUpdateFn, () => queueJob(instance.update), instance.scope // 限制在组件范围内进行跟踪 )) const update = (instance.update = effect.run.bind(effect) as SchedulerJob) update.id = instance.uid update() componentUpdateFn()函数内有mount(挂载)和update(更新)两个流程。该函数在update属性上,首次挂载的时候,调用update方法执行一次effect副作用函数。 5.2.2mount(挂载) mount(挂载)流程,主要执行patch挂载,并且标识当前组件为已挂载状态,实现代码如下: // $runtime-core_render if (!instance.isMounted) { let vnodeHook: VNodeHook | null | undefined const { el, props } = initialVNode const { bm, m, parent } = instance const isAsyncWrapperVNode = isAsyncWrapper(initialVNode) // 服务器端渲染 if (el && hydrateNode) { const hydrateSubTree = () => { instance.subTree = renderComponentRoot(instance) hydrateNode!( el as Node, instance.subTree, instance, parentSuspense, null ) } if (isAsyncWrapperVNode) { ;(initialVNode.type as ComponentOptions).__asyncLoader!().then( () => !instance.isUnmounted && hydrateSubTree() ) } else { hydrateSubTree() } } else { const subTree = (instance.subTree = renderComponentRoot(instance)) patch( null, subTree, container, anchor, instance, parentSuspense, isSVG ) initialVNode.el = subTree.el } // onVnodeMounted if ( !isAsyncWrapperVNode && (vnodeHook = props && props.onVnodeMounted) ) { const scopedInitialVNode = initialVNode queuePostRenderEffect( () => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode), parentSuspense ) } if (initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { instance.a && queuePostRenderEffect(instance.a, parentSuspense) } instance.isMounted = true initialVNode = container = anchor = null as any } 5.2.3update(更新) update(更新)流程同样执行patch渲染,因为是更新操作,涉及diff新旧VNode的变化,需要提前处理props、slot等内容,所以对更新逻辑进行分开处理。具体代码实现如下: else { let { next, bu, u, parent, vnode } = instance let originNext = next let vnodeHook: VNodeHook | null | undefined toggleRecurse(instance, false) if (next) { next.el = vnode.el updateComponentPreRender(instance, next, optimized) } else { next = vnode } if ( __COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) ) { instance.emit('hook:beforeUpdate') } toggleRecurse(instance, true) const nextTree = renderComponentRoot(instance) const prevTree = instance.subTree instance.subTree = nextTree patch( prevTree, nextTree, //如果是teleport,那么父组件可能会改变 hostParentNode(prevTree.el!)!, // 如果是fragment,那么anchor可能会改变 getNextHostNode(prevTree), instance, parentSuspense, isSVG ) next.el = nextTree.el if (originNext === null) { updateHOCHostEl(instance, nextTree.el) } if ( __COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) ) { queuePostRenderEffect( () => instance.emit('hook:updated'), parentSuspense ) } } 5.2.4创建effect副作用函数 effect副作用函数本身对应的是第2章的watchEffect()函数在Vue3中源码的实现。接下来简单查看该函数的内部逻辑,以便于理解其作用。具体代码实现如下: export function effect( fn: () => T, options?: ReactiveEffectOptions ): ReactiveEffectRunner { // 获取副作用函数 if ((fn as ReactiveEffectRunner).effect) { fn = (fn as ReactiveEffectRunner).effect.fn } const _effect = new ReactiveEffect(fn) if (options) { extend(_effect, options) if (options.scope) recordEffectScope(_effect, options.scope) } // lazy属性值控制是否立即执行 if (!options || !options.lazy) { _effect.run() } const runner = _effect.run.bind(_effect) as ReactiveEffectRunner runner.effect = _effect return runner } 上述代码对fn本身进行处理后,创建了一个effect副作用函数,再判断是否需要懒加载,判断effect副作用函数是否立即执行。 5.2.5ReactiveEffect()函数 effect副作用函数调用了ReactiveEffect()构造函数,该函数接收3个参数: 执行函数、异步队列任务和可选参数scope。ReactiveEffect()构造函数的主要实现逻辑为: 创建一个effect副作用函数; 设置内部属性使需要执行的effect副作用函数在缓存队列快速被找到。捕获传入的fn函数执行的错误,避免fn函数错误导致effect执行崩溃。具体代码实现如下: export class ReactiveEffect { active = true deps: Dep[] = [] // 创建后可以再次计算 computed?: boolean allowRecurse?: boolean onStop?: () => void // dev only onTrack?: (event: DebuggerEvent) => void // dev only onTrigger?: (event: DebuggerEvent) => void constructor( public fn: () => T, public scheduler: EffectScheduler | null = null, scope?: EffectScope | null ) { recordEffectScope(this, scope) } run() { if (!this.active) { // 非激活状态处理 return this.fn() } // 确保副作用栈中没有当前副作用函数 if (!effectStack.includes(this)) { try { // 设置当前副作用函数为激活副作用 effectStack.push((activeEffect = this)) // 开启收集 enableTracking() // 递归次数加1 trackOpBit = 1 << ++effectTrackDepth // 判断是否达到最大递归调用次数 if (effectTrackDepth <= maxMarkerBits) { initDepMarkers(this) } else { // 清理当前副作用函数依赖 cleanupEffect(this) } return this.fn() } finally { // 如果小于最大调用次数,则完成 if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this) } // 完成本次effect副作用函数执行后,标识位减少1 trackOpBit = 1 << --effectTrackDepth // 重置收集栈,删除已执行的副作用函数,重置当前激活的副作用函数 resetTracking() effectStack.pop() const n = effectStack.length activeEffect = n > 0 ? effectStack[n - 1] : undefined } } } stop() { if (this.active) { cleanupEffect(this) if (this.onStop) { this.onStop() } this.active = false } } } effect副作用函数负责预处理fn函数,确保fn函数是一个非effect副作用函数。函数内部lazy选项和computed强相关,配置成true会使得effect副作用函数默认不立即执行。 由ReactiveEffect函数的内部实现逻辑可知,effect副作用函数仅是一个包裹函数,在它的内部执行fn函数和处理effectStack相关内容。 5.2.6处理激活状态 effect.active表示副作用函数的激活状态,默认为true。在停止一个副作用函数后会将其置成false。非激活状态的effect副作用函数被调用时,如果不存在异步调度,则直接执行fn函数; 如果存在异步调度,则直接返回null。 5.2.7清除操作 effect.deps是一个数组,存储的是该effect副作用函数依赖的每个属性的depsSet副作用函数表,track阶段建立的依赖存储在表中。每个响应式对象触发依赖收集的key都会对应depsSet表中的一个副作用函数。 effect的deps存储是的当前effect依赖属性的副作用depsSet表,这是一个双向指针的处理方式,不仅在依赖收集的时候,会将副作用函数与target→key→depsSet关联起来,同时也会保持depsSet的引用存储在effect.deps上。当超出最大递归次数,或调用effect副作用函数的stop方法时,将调用cleanupEffect()函数清理effect副作用函数中保存的依赖。 cleanupEffect()函数的代码实现如下: function cleanupEffect(effect: ReactiveEffect) { const { deps } = effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } } 5.2.8执行fn fn的执行采用try...finally来捕获错误,即使fn执行出错,还是能保证effectStack、activeEffect的正确维护。 在正式执行fn之前,会有一个压栈的操作,由于effect的执行会存在嵌套的情况,比如,组件渲染函数的执行遇到了子组件就会跳到子组件的渲染函数中,函数的嵌套调用使用到栈结构,而栈的先进后出性质能很好地保证activeEffect的正确回退。fn执行完成后,就进行出栈操作,跳回到上一个effect中,至此整个逻辑完整地进行了串联。 5.2.9总结 整个响应式逻辑已介绍完成,包括effect()函数与track和trigger之间的关系,串联响应式数据的处理流程。可以结合第2章的逻辑一起理解整个流程。虽然Vue3内的核心逻辑代码有所增加,对不同值类型、边界情况等进行了处理,但核心逻辑实现与第2章完全一致。下面通过关系图(见图5.5)再对整个effect副作用函数的执行进行梳理。以便读者理解effect副作用函数的作用以及响应式数据的get和set内部作用。 图5.5createReactiveEffect()函数流程 5.3节将会介绍使用effect、track和trigger实现的方法。 5.3watch监听◆ 前面介绍了响应式数据原理后,本节将介绍响应式核心API的使用。watch函数就是对响应式数据的封装实现。watch函数在Vue2.x已存在,在Vue3中进行了增强,并且扩展出watchEffect()函数。 5.3.1watch函数 watch函数可以在$reactivity_apiWatch.ts文件内查看。在该文件内可以看到多个watch函数导出,该方法为TypeScript语法内的函数重载。具体代码实现如下: export function watch = false>( source: T | WatchSource, cb: any, options?: WatchOptions ): WatchStopHandle { return doWatch(source as any, cb, options) } 上述watch函数接收3个参数: WatchSource、cb和options。WatchSource是需要通过watch监听的函数,cb是需要执行的回调函数,options是对应的配置属性。通过高阶函数方法调用doWatch()函数。下面根据代码逻辑查看doWatch()函数的实现。该函数实现内容较多,主要包括以下逻辑: (1) 初始化getter和相关变量; (2) 判断传入数据类型,根据数据类型执行不同的数据处理,得到getter()函数; (3) 处理SSR情况; (4) 创建调度工作; (5) 初始化调度函数; (6) 创建effect副作用函数; (7) 返回stop函数。 上面对doWatch()函数所做的工作进行了简单的介绍,下面详细介绍doWatch函数内部的执行情况。 5.3.2初始化 根据传入的参数对getter()函数执行不同的初始化。watch支持的传入参数类型为ref、reactive object、getter/effect function和包含上述类型的数组。具体逻辑如下: 如果数据源为ref,则直接获取value值并返回,并且判断是否为浅代理,标识是否强制触发。 if (isRef(source)) { getter = () => source.value forceTrigger = !!source._shallow } 如果数据源为reactive类型,则直接返回该对象,并强制设置为深度监听。 else if (isReactive(source)) { getter = () => source deep = true } 如果数据源为数组类型,则标识为多元数据,并根据是否有reactive类型数据标识强制触发。完成标识后遍历数组的每一项,若为ref类型则读取value值; 若为reactive类型则递归数据; 若为函数,则执行对应函数,并通过callWithErrorHandling()函数捕获对应错误。 else if (isArray(source)) { isMultiSource = true forceTrigger = source.some(isReactive) getter = () => source.map(s => { if (isRef(s)) { return s.value } else if (isReactive(s)) { return traverse(s) } else if (isFunction(s)) { return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER) } else { __DEV__ && warnInvalidSource(s) } }) } 如果数据源(source)为函数类型,且有回调函数,则执行对应的source函数,并通过callWithErrorHandling()函数捕获对应的错误; 若没有回调函数,则判断是否有上下文,若有上下文则判断是否为激活状态,清理上一次执行的回调函数,并执行该source函数。具体代码实现如下: else if (isFunction(source)) { if (cb) { // getter with cb getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER) } else { // no cb -> simple effect getter = () => { if (instance && instance.isUnmounted) { return } if (cleanup) { cleanup() } return callWithAsyncErrorHandling( source, instance, ErrorCodes.WATCH_CALLBACK, [onInvalidate] ) } } } 完成getter()函数后,处理Vue2.x的watch监听类型,判断是否为兼容模式。在有回调函数且不是深度遍历的情况下,执行getter()函数,并对函数返回值进行处理,如果是数组则深度遍历,如果不是则直接返回。具体代码实现如下: if (__COMPAT__ && cb && !deep) { const baseGetter = getter getter = () => { const val = baseGetter() if ( isArray(val) && checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance) ) { traverse(val) } return val } } 在有回调函数和深度遍历情况下,直接通过traverse()函数执行深度遍历。具体代码实现如下: if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) } 上述判断完成后,初始化wacth的停止监听函数onInvalidate()。onInvalidate()函数用于watch监听停止后,执行用户传递的回调函数,并且将该方法保存在cleanup对象内,以便后续执行。watch函数监听也会保存在cleanup对象内,在元素进行修改时,会先调用cleanup进行清理。 let cleanup: () => void let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => { cleanup = runner.options.onStop = () => { callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } } 判断是否为SSR环境,若是则立刻执行getter()函数,注销监听,结束该函数的执行。 if (__SSR__ && isInSSRComponentSetup) { onInvalidate = NOOP if (!cb) { getter() } else if (immediate) { callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ getter(), isMultiSource ? [] : undefined, onInvalidate ]) } return NOOP } 接下来初始化异步队列需要执行的job回调函数,该函数将判断是否有用户传入的cb回调函数。若没有则直接执行effect.run()方法,并结束该函数的执行; 若有cb回调函数,则同样执行effect.run()方法,将执行结果保存为新值(newValue)后开始处理旧值,判断是否深度监听、强制触发、多元数据、新值有变化,如果以上情况中有一种为是,则直接清理上一次执行队列,并执行cb回调函数,返回新值、旧值和watch监听停止回调函数onInvalidate。具体代码实现如下: let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE const job: SchedulerJob = () => { if (!effect.active) { return } if (cb) { // watch(source, cb) const newValue = effect.run() if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) ) { // 再次运行cb前进行清理 if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, // 旧值在第一次变化时若未定义,则忽略 oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onInvalidate ]) oldValue = newValue } } else { // watchEffect effect.run() } } 5.3.3scheduler异步队列 异步执行队列声明后,开始创建scheduler异步队列,并根据flush的值决定是同步执行、异步执行,还是在组件挂载前执行一次。 job.allowRecurse = !!cb scheduler: EffectScheduler if (flush === 'sync') { scheduler = job as any } else if (flush === 'post') { scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { // default: 'pre' scheduler = () => { if (!instance || instance.isMounted) { queuePreFlushCb(job) } else { job() } } } 完成该异步队列声明后,使用ReactiveEffect()构造函数实例化出effect副作用函数,以便后续执行。传入getter函数和scheduler()函数(副作用执行函数),代码实现如下: const effect = new ReactiveEffect(getter, scheduler) 若有cb回调函数,且immediate参数值为true,则调用job()函数,若immediate参数值为false,则执行runner()函数并将函数返回值赋值给oldValue;若无cb回调函数,且flush为post,则使用queuePostRenderEffect()函数异步执行effect.run()方法。其他情况直接执行job()方法。 if (cb) { if (immediate) { job() } else { oldValue = effect.run() } } else if (flush === 'post') { queuePostRenderEffect(effect.run.bind(effect), instance && instance.suspense) } else { job() } 调用queuePostRenderEffect()函数判断上下文是否为suspense异步组件,若为suspense异步组件,则收集对应的effect; 若不是suspense异步组件,则直接调用异步队列处理。具体代码实现如下: //$runtime-core_render.ts export const queuePostRenderEffect = __FEATURE_SUSPENSE__ ? queueEffectWithSuspense : queuePostFlushCb 上述方法根据是否为suspense异步组件调用queueEffectWithSuspense()或调用queuePostFlushCb()函数。 queueEffectWithSuspense()内部判断是否异步组件,若是,则在异步组件内收集effect副作用函数; 若不是,则执行queuePostFlushCb()函数。具体代码实现如下: export function queueEffectWithSuspense( fn: Function | Function[], suspense: SuspenseBoundary | null ): void { if (suspense && suspense.pendingBranch) { if (isArray(fn)) { suspense.effects.push(...fn) } else { suspense.effects.push(fn) } } else { queuePostFlushCb(fn) } } 完成执行后,返回停止监听函数。以上便是watch函数的具体内部实现。根据对代码的拆解可以看出,watch函数内部其实也是调用的effect副作用函数收集对应依赖,处理数据监听,并在数据改变后,执行对应的effect副作用函数。 5.3.4watchEffect()函数 完成watch函数的实现后,再来分析watchEffect()函数,可以发现,和watch函数相比,二者本质上只是传入dowatch()函数的参数不同,在介绍dowatch()函数内部实现时,已经对watchEffect()函数进行了适配,根据实现传入参数不同,通过递归等方法注册getter函数,实现依赖收集。具体代码实现如下: function watchEffect( effect: WatchEffect, options?: WatchOptionsBase ): WatchStopHandle { return doWatch(effect, null, options) } 5.3.5总结 watch函数依赖响应式逻辑实现,内部根据传入参数调用getter函数进行依赖收集,并实时监听数据。在数据变化后及时派发更新,通知effect执行。通过对本节的学习,可更好地了解watch函数本身的特性以及watch和watchEffect()函数的差异。 5.4computed函数◆ Vue3中的computed函数与Vue2中的用法不同,Vue2中在data内直接写computed方法即可,在Vue3中该方法已经被函数化。在使用时需要提供一个get和set方法,分别用于接收和传递响应式数据。 computed函数的代码实现如下: // reload 1 export function computed( getter: ComputedGetter, debugOptions?: DebuggerOptions ): ComputedRef // reload 2 export function computed( options: WritableComputedOptions, debugOptions?: DebuggerOptions ): WritableComputedRef // computed export function computed( getterOrOptions: ComputedGetter | WritableComputedOptions, debugOptions?: DebuggerOptions ) { let getter: ComputedGetter let setter: ComputedSetter const onlyGetter = isFunction(getterOrOptions) if (onlyGetter) { getter = getterOrOptions setter = __DEV__ ? () => { console.warn('Write operation failed: computed value is readonly') } : NOOP } else { getter = getterOrOptions.get setter = getterOrOptions.set } const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter) if (__DEV__ && debugOptions) { cRef.effect.onTrack = debugOptions.onTrack cRef.effect.onTrigger = debugOptions.onTrigger } return cRef as any } 此处通过函数重载对多种传参进行处理,直接查看computed()函数的实现。首先声明getter和setter,判断getterOrOptions()是否为函数,若为函数则将该函数赋值给getter变量保存,setter变量设置为空,表示只读,不可修改; 若不是则将传递的参数getterOrOptions中的get和set属性通过getter和setter变量保存,最后返回ComputedRefImpl实例。具体代码实现如下: class ComputedRefImpl { public dep?: Dep = undefined // 缓存当前值 private _value!: T // 是否需要重新求值 private _dirty = true public readonly effect: ReactiveEffect public readonly __v_isRef = true; public readonly [ReactiveFlags.IS_READONLY]: boolean constructor( getter: ComputedGetter, private readonly _setter: ComputedSetter, isReadonly: boolean ) { // 创建getter 副作用函数 this.effect = new ReactiveEffect(getter, () => { if (!this._dirty) { this._dirty = true triggerRefValue(this) } }) this[ReactiveFlags.IS_READONLY] = isReadonly } // 执行get get value() { const self = toRaw(this) trackRefValue(self) if (self._dirty) { self._dirty = false self._value = self.effect.run()! } return self._value } // 执行set set value(newValue: T) { this._setter(newValue) } } 由上述代码可以看出整个ComputedRefImpl()的执行情况。 5.4.1创建getter副作用函数 在computed内部创建ReactiveEffect实例并传入getter副作用函数,其中第二个参数是一个通过匿名函数传入ReactiveEffect()内的定时调度函数。该定时调度函数是否执行,将依赖_dirty属性值的状态。若为true,则不执行内部逻辑; 若为false,则调用triggerRefValue()触发ref值。 ReactiveEffect()内部主要涉及收集、触发、运行和停止的方法。在内部通过数组维护一个临时栈,若当前执行函数未在临时栈内,则执行入栈操作,在执行完成后通过pop执行出栈操作。 此处将会根据栈内数据的情况判断,如果超过30个栈,则直接清理; 若少于30个栈则处理,并且在完成后调用finally()函数,该方法内执行栈的删除和数据的重置工作,以便下一个对象使用。具体代码实现如下: //$reactivity_effect.ts run() { if (!this.active) { return this.fn() } if (!effectStack.includes(this)) { try { effectStack.push((activeEffect = this)) enableTracking() trackOpBit = 1 << ++effectTrackDepth if (effectTrackDepth <= maxMarkerBits) { initDepMarkers(this) } else { cleanupEffect(this) } return this.fn() } finally { if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this) } trackOpBit = 1 << --effectTrackDepth resetTracking() effectStack.pop() const n = effectStack.length activeEffect = n > 0 ? effectStack[n - 1] : undefined } } } 在后面调用run方法时,将执行上述代码,调用副作用函数,触发数据的修改。run方法的调用位置将会在5.4.2节中介绍。该步骤完成后,对ReactiveEffect()构造函数内部有初步的认识。在ComputedRefImpl实例内创建了一个getter副作用函数,并且赋予了对应的方法,可在合适的时机执行该方法。完成getter副作用函数的创建后,应继续创建ref。 5.4.2创建cRef cRef的创建主要关注get的实现即可。get返回的是computed函数作用域下的value。在get方法内部可以看到,get内部主要将当前上下文的数据进行响应式处理,再调用trackRefValue()函数,执行依赖收集,并且通过_dirty属性值判断是否需要重新求值,当_dirty为true时会执行effect副作用函数的run方法求新值,如果不需要则直接返回value。 此处需要注意,_dirty参数值为true时,将把_dirty参数改为false,并调用当前上下文的run方法。run方法在初始化时创建,调度执行函数会根据!this._dirty的值决定是否执行triggerRefValue()函数进行重新求值。 关于触发新值更新的问题,需回到5.4节介绍的run方法的声明位置,在副作用函数的调度函数选项中传入如下匿名函数: () => { if (!this._dirty) { this._dirty = true triggerRefValue(this) } … } 当第一次访问computedRef.value时会执行run方法。经过5.4节的解析可知,effect返回的是一个包裹getter的副作用函数,执行run方法就会触发getter内部访问的响应式变量的依赖收集。当getter依赖的响应式数据发生变化时会触发trigger,重新执行run方法。若传递了调度函数选项,则run方法以scheduler的形式调用。因此在getter依赖的响应式数据产生变化时会触发run方法执行更新。 继续查看scheduler的内部实现,发现其中并没有直接通过getter求值,而是在_dirty为false时触发computedRef的trigger,这意味着此时依赖于computedRef的副作用函数会重新执行,而在这个副作用函数中一定会对computedRef产生get访问,此时回到get函数内部会发现_drity的值被设置为true,执行run方法进行真实的求值。需要求新值的时刻发生在传入computed函数内的响应式数据发生改变的时候。 5.4.3总结 computed函数同样依赖响应式系统,该函数通过getter和setter方法实现,将当前上下文的数据进行响应式处理,再执行依赖收集和派发更新。当依赖的数据发生变化时会重新计算需返回的新值。 5.5拓展方法◆ 5.5.1customRef()函数 前面介绍reactive和ref时,对数据响应式原理进行了分析和理解。在了解基本原理的基础上,本节继续介绍与ref类似的customRef()函数。该函数可以更加灵活地实现响应式数据操作,并且可根据情况自行决定依赖收集和派发更新。具体代码实现如下: customRef((track, trigger) => { return { get() { track(); return value; }, set(newValue) { value = newValue; trigger(); }, }; }); 在上述代码中,customRef()将track和trigger函数以参数形式传入get和set方法,并可以在get和set方法内手动执行依赖收集和派发更新。下面详细介绍该函数的实现方式。 该函数的实现位于$reactivity_ref文件内,函数实现如下: export function customRef(factory: CustomRefFactory): Ref { return new CustomRefImpl(factory) as any } 上述代码返回一个接收factory参数的CustomRefImpl实例。下面给出该实例的实现逻辑: class CustomRefImpl { private readonly _get: ReturnType>['get'] private readonly _set: ReturnType>['set'] public readonly __v_isRef = true constructor(factory: CustomRefFactory) { const { get, set } = factory( () => trackRefValue(this), () => triggerRefValue(this), ) this._get = get this._set = set } get value() { return this._get() } set value(newVal) { this._set(newVal) } } CustomRefImpl()构造函数内通过factory参数将track和trigger进行暴露,并且将用户自定义的get和set保存在私有变量_get和_set中。调用get和set方法时,执行_get和_set变量对应的方法,以达到重写get和set的目的,最终返回包装的ref对象。 经过简单的改造,可以更加灵活地通过customRef()函数控制track和trigger的时机。 5.5.2readonly()函数 围绕响应式数据的封装,本节将会继续介绍readonly()函数的实现,该函数返回只读的响应式数据。使用readonly()函数创建的内容将不支持修改。下面将具体介绍该函数的实现原理。 readonly()函数定义在$reactivity_reactive文件内。具体代码实现如下: export function readonly( target: T ): DeepReadonly> { return createReactiveObject( target, true, readonlyHandlers, readonlyCollectionHandlers, readonlyMap ) } 由上述代码可知,readonly()函数内部返回createReactiveObject()函数,该函数在5.1.4节已进行过介绍。该函数需传入5个参数,分别为target、isReadonly、baseHandlers、collectionHandlers和readonlyMap。在该函数内,如果isReadonly状态为true,则使用readonlyMap对象保存数据,并且通过Proxy对象对传入的代理函数进行代理,此处逻辑与reactive基本相同。拦截函数readonlyHandlers()位于$reactivity_baseHandlrs文件内,具体代码实现如下: export const readonlyHandlers: ProxyHandler = { get: readonlyGet, set(target, key) { if (__DEV__) { console.warn( `Set operation on key "${String(key)}" failed: target is readonly.`, target ) } return true }, deleteProperty(target, key) { if (__DEV__) { console.warn( `Delete operation on key "${String(key)}" failed: target is readonly.`, target ) } return true } } 由上述代码可知,对于只读对象,调用get方法将执行readonlyGet()函数,调用set和deleteProperty()函数将会在开发环境抛出警告,提示不能修改或删除。readonlyGet()函数内部调用createGetter()函数时将传入true参数。该函数内部主要在 只读情况下对数据类型进行判断,然后返回对应值,此处不展开讲解。readonly()函数和reactive()函数最大的区别在于,readonly()函数不进行依赖收集,如果不是浅代理则递归返回数据; 若为浅代理,则直接返回。 5.5.3shallow()函数 shallow()函数表示浅代理,若遇到该状态将不会对数据递归处理,涉及的浅代理函数有 shallowRef()、shallowReactive()和shallowReadonly()。通过浅代理声明的对象将仅代理第一层基础数据。 shallowRef()函数定义在$reactitity_ref文件内,通过高阶函数形式返回createRef()函数。具体代码实现如下: export function shallowRef( value: T ): T extends Ref ? T : Ref export function shallowRef(value: T): Ref export function shallowRef(): Ref export function shallowRef(value?: unknown) { return createRef(value, true) } shallowRef()函数实现也涉及函数重载,根据传入参数不同调用不同的方法。该函数内部对数据进行简单过滤后,通过new返回新实例,该实例的构造函数为RefImpl()。具体代码实现如下: class RefImpl { private _value: T private _rawValue: T public dep?: Dep = undefined public readonly __v_isRef = true constructor(value: T, public readonly _shallow: boolean) { this._rawValue = _shallow ? value : toRaw(value) this._value = _shallow ? value : toReactive(value) } get value() { trackRefValue(this) return this._value } set value(newVal) { newVal = this._shallow ? newVal : toRaw(newVal) if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : toReactive(newVal) triggerRefValue(this, newVal) } } } RefImpl()构造函数内部实现get和set方法,在constructor初始化阶段,若处于浅代理,在get时直接返回value值并做依赖收集。在set时直接返回传入的value值,派发更新,并且不对数据做深层次处理。 5.5.4shallowReactive()函数 shallowReactive()函数的实现与readonly()函数在同一位置,位于$reactivity_reactive文件内。该函数同样返回createReactiveObject()函数,并直接查看代理对象shallowReactiveHandlers。具体代码实现如下: // $reactivity_bbaseHandlers.ts export const shallowReactiveHandlers = /*#__PURE__*/ extend( {}, mutableHandlers, { get: shallowGet, set: shallowSet } ) 通过extend方法合并mutableHandlers、get和set对象,mutableHandlers对象的代码实现如下: export const mutableHandlers: ProxyHandler = { get, set, deleteProperty, has, ownKeys } mutableHandlers对象代理set、deleteProperty、has和ownKeys对象,并且使用shallowGet()和shallowSet()函数重写set和get,上述两个函数的定义内容如下: const shallowGet = /*#__PURE__*/ createGetter(false, true) const shallowSet = /*#__PURE__*/ createSetter(true) 5.5.5shallowReadonly()函数 shallowReadonly()函数同样定义在$reactivity.ts文件内,该函数实现逻辑基本与shallowReactive()函数类似。具体代码实现如下: export function shallowReadonly( target: T ): Readonly<{ [K in keyof T]: UnwrapNestedRefs }> { return createReactiveObject( target, true, shallowReadonlyHandlers, readonlyCollectionHandlers, shallowReadonlyMap ) } 同样,返回createReactiveObject()函数,并且传入代理对象shallowReadonlyHandlers,该对象的代码实现如下: export const shallowReadonlyHandlers = /*#__PURE__*/ extend( {}, readonlyHandlers, { get: shallowReadonlyGet } ) 因为是浅代理并且是readonly状态,所以只需要代理get对象。 5.5.6总结 通过对shallow()函数、readonly()函数、customRef()函数的介绍可知,与reactive相关的函数实现均定义在$reactitity_reactive文件内,与ref相关的函数实现均定义在$reactitity_ref文件内。由上面的分析可知,与ref相关的内容均是调用createRef()函数,与reactive相关的内容均是调用createReactiveObject()函数,这两个函数内部实现了对readonly状态、shallow状态的处理,在创建对应的方法时,可通过策略模式和高阶函数的方式减少重复代码。