Effect和Reactive
effect作为Vue响应式原理中的核心,在Computed、Watch、Reactive中都有出现
主要和Reactive(Proxy)、track、trigger等函数配合实现收集依赖,触发依赖更新
Effect
effect可以被理解为一个副作用函数,被当做依赖收集,在响应式数据更新后被触发。
Vue的响应式API例如Computed、Watch都有用到effect来实现
先来看看入口函数
入口函数主要是一些逻辑处理,核心逻辑位于createReactiveEffect
function effect <T = any >( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect <T > { if (isEffect(fn)) { fn = fn.raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect }
const effectStack: ReactiveEffect[] = []function createReactiveEffect <T = any >( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect <T > { const effect = function reactiveEffect ( ): unknown { if (!effect.active) { return options.scheduler ? undefined : fn() } if (!effectStack.includes(effect)) { cleanup(effect) try { enableTracking() effectStack.push(effect) activeEffect = effect return fn() } finally { effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1 ] } } } as ReactiveEffect effect.id = uid++ effect.allowRecurse = !!options.allowRecurse effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect } function cleanup (effect: ReactiveEffect ) { const { deps } = effect if (deps.length) { for (let i = 0 ; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } }
Track
Track这个函数常出现在reactive的getter函数中,用于依赖收集
源码详解见注释
function track (target: object , type : TrackOpTypes, key: unknown ) { if (!shouldTrack || activeEffect === undefined ) { return } 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 ())) } if (!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) if (__DEV__ && activeEffect.options.onTrack) { activeEffect.options.onTrack({ effect: activeEffect, target, type , key }) } } }
Trigger
Trigger常出现在reactive中的setter函数中,用于触发依赖更新
源码详解见注释
function trigger ( target: object , type : TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map <unknown, unknown> | Set <unknown> ) { const depsMap = targetMap.get(target) if (!depsMap) { return } const effects = new Set <ReactiveEffect>() const add = (effectsToAdd: Set <ReactiveEffect> | undefined ) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { if (effect !== activeEffect || effect.allowRecurse) { effects.add(effect) } }) } } if (type === TriggerOpTypes.CLEAR) { depsMap.forEach(add) } else if (key === 'length' && isArray(target)) { depsMap.forEach((dep, key ) => { if (key === 'length' || key >= (newValue as number )) { add(dep) } }) } else { if (key !== void 0 ) { add(depsMap.get(key)) } switch (type ) { case TriggerOpTypes.ADD: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) if (isMap(target)) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { add(depsMap.get('length' )) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) if (isMap(target)) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } break case TriggerOpTypes.SET: if (isMap(target)) { add(depsMap.get(ITERATE_KEY)) } break } } const run = (effect: ReactiveEffect ) => { if (__DEV__ && effect.options.onTrigger) { effect.options.onTrigger({ effect, target, key, type , newValue, oldValue, oldTarget }) } if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() } } effects.forEach(run) }
Reactive
了解了Track用于依赖收集,Trigger用于依赖触发,那么他们的调用时机是什么时候呢?来看看Reactive的源码就清楚了,源码详解见注释。
注:源码结构较为复杂(封装),为便于理解原理,以下为简化源码。
总结来说
在getter时进行依赖收集
在setter时触发依赖更新
function reactive (target:object ) { return new Proxy (target,{ get (target: Target, key: string | symbol, receiver: object ) { const res = Reflect .get(target, key, receiver) track(target, TrackOpTypes.GET, key) return res } set (target: object , key: string | symbol, value: unknown, receiver: object ) { let oldValue = (target as any )[key] const result = Reflect .set(target, key, value, receiver) trigger(target, TriggerOpTypes.SET, key, value, oldValue) return result } }) }
Computed
Computed是Vue中常用且好用的一个属性,这个属性的值在依赖改变后同步进行改变,在依赖未改变时使用缓存的值。
Vue2
在Vue2中Computed的实现通过嵌套watcher,实现响应式数据的依赖收集,间接链式触发依赖更新。
Vue3中出现了effect,重新实现了Computed属性
effect可以被理解为副作用函数,被当做依赖收集,在响应式数据更新后被触发。
Show me the Code
读完这段computed函数会发现,这里只是做了简要的getter和setter的赋值处理
function computed <T >( getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T> ) { let getter: ComputedGetter<T> let setter: ComputedSetter<T> if (isFunction(getterOrOptions)) { getter = getterOrOptions setter = __DEV__ ? () => { console .warn('Write operation failed: computed value is readonly' ) } : NOOP } else { getter = getterOrOptions.get setter = getterOrOptions.set } return new ComputedRefImpl( getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set ) as any }
核心逻辑都在ComputedRefImpl中,我们接着往下看
通过dirty变量标记数据是否为旧数据
在响应式数据更新后将dirty赋值为true
在下一次get时,dirty为true时进行重新计算,并将dirty赋值为false
class ComputedRefImpl <T > { private _value!: T private _dirty = true public readonly effect: ReactiveEffect<T> public readonly __v_isRef = true ; public readonly [ReactiveFlags.IS_READONLY]: boolean constructor ( getter: ComputedGetter<T>, private readonly _setter: ComputedSetter<T>, isReadonly: boolean ) { this .effect = effect(getter, { lazy: true , scheduler: () => { if (!this ._dirty) { this ._dirty = true trigger(toRaw(this ), TriggerOpTypes.SET, 'value' ) } } }) this [ReactiveFlags.IS_READONLY] = isReadonly } get value () { const self = toRaw(this ) if (self._dirty) { self._value = this .effect() self._dirty = false } track(self, TrackOpTypes.GET, 'value' ) return self._value } set value (newValue: T ) { this ._setter(newValue) } }
Watch
Watch主要用于对某个变量的监听,并做相应的处理
Vue3中不仅重构了watch,还多了一个WatchEffect API
用于对某个变量的监听,同时可以通过callBack拿到新值和旧值
watch(state, (state, prevState )=> {})
每次更新都会执行,自动收集使用到的依赖
无法获取到新值和旧值,可手动停止监听
onInvalidate(fn)
传入的回调会在 watchEffect
重新运行或者 watchEffect
停止的时候执行
const stop = watchEffect((onInvalidate )=> { onInvalidate(()=> { }) }) stop()
watch和watchEffect的不同点
watch惰性执行,watchEffect每次代码加载都会执行
watch可指定监听变量,watchEffect自动依赖收集
watch可获取新旧值,watchEffect不行
watchEffect有onInvalidate功能,watch没有
watch只可监听ref、reactive等对象,watchEffect只可监听具体属性
Source Code
Show me the Code
这里可以看到watch和watchEffet的核心逻辑都封装到了doWatch中
export function watch <T = any , Immediate extends Readonly <boolean > = false >( source: T | WatchSource<T>, cb: any , options?: WatchOptions<Immediate> ): WatchStopHandle { if (__DEV__ && !isFunction(cb)) { warn( `\`watch(fn, options?)\` signature has been moved to a separate API. ` + `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + `supports \`watch(source, cb, options?) signature.` ) } return doWatch(source as any , cb, options) } export function watchEffect ( effect: WatchEffect, options?: WatchOptionsBase ): WatchStopHandle { return doWatch(effect, null , options) }
以下为删减版源码,理解核心原理即可
详情见注释
function doWatch ( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null , { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ, instance = currentInstance ): WatchStopHandle { let getter: () => any let forceTrigger = false let isMultiSource = false if (isRef(source)) { getter = () => (source as Ref).value forceTrigger = !!(source as Ref)._shallow } else if (isReactive(source)) { getter = () => source deep = true } 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, [ instance && (instance.proxy as any) ]) } else { __DEV__ && warnInvalidSource(s) } }) } else if (isFunction(source)) { if (cb) { getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER, [ instance && (instance.proxy as any) ]) } else { getter = () => { if (instance && instance.isUnmounted) { return } if (cleanup) { cleanup() } return callWithAsyncErrorHandling( source, instance, ErrorCodes.WATCH_CALLBACK, [onInvalidate] ) } } } else { getter = NOOP __DEV__ && warnInvalidSource(source) } if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) } let cleanup: () => void let onInvalidate: InvalidateCbRegistrator = (fn: () => void ) => { cleanup = runner.options.onStop = () => { callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } } let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE const job: SchedulerJob = () => { if (!runner.active) { return } if (cb) { const newValue = runner() 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)) ) { if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onInvalidate ]) oldValue = newValue } } else { runner() } } job.allowRecurse = !!cb let scheduler: ReactiveEffectOptions['scheduler' ] if (flush === 'sync' ) { scheduler = job } else if (flush === 'post' ) { scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { scheduler = () => { if (!instance || instance.isMounted) { queuePreFlushCb(job) } else { job() } } } const runner = effect(getter, { lazy: true , onTrack, onTrigger, scheduler }) recordInstanceBoundEffect(runner, instance) if (cb) { if (immediate) { job() } else { oldValue = runner() } } else if (flush === 'post' ) { queuePostRenderEffect(runner, instance && instance.suspense) } else { runner() } return () => { stop(runner) if (instance) { remove(instance.effects!, runner) } } }
Mixin
Mixin意为混合,是公共逻辑封装利器。
原理比较简单,那就是合并。
合并分为对象的合并和生命周期的合并
对象,mergeOption
类型Object.assign的合并,会出现覆盖现象
生命周期,mergeHook
mergeOptions
function mergeOptions ( to: any , from : any , instance?: ComponentInternalInstance | null , strats = instance && instance.appContext.config.optionMergeStrategies ) { if (__COMPAT__ && isFunction(from )) { from = from .options } const { mixins, extends : extendsOptions } = from extendsOptions && mergeOptions(to, extendsOptions, instance, strats) mixins && mixins.forEach((m: ComponentOptionsMixin ) => mergeOptions(to, m, instance, strats) ) for (const key in from ) { if (strats && hasOwn(strats, key)) { to[key] = strats[key](to[key], from [key], instance && instance.proxy, key) } else { to[key] = from [key] } } return to }
简单粗暴放进Set,调用时依次调用
function mergeHook ( to: Function [] | Function | undefined , from : Function | Function [] ) { return Array .from(new Set ([...toArray(to), ...toArray(from )])) }
Diff算法优化
了解Vue3的Diff算法优化前,可以先了解一下Vue2的Diff算法
本部分注重把算法讲清楚,将不进行逐行源码分析
Vue3中的主要优化点为
在updateChildren时双端比较 -> 最长递增子序列
全量Diff -> 静态标记 + 非全量Diff
静态提升
updateChildren
Vue2
头 - 头比较
尾 - 尾比较
头 - 尾比较
尾 - 头比较
Vue3
头 - 头比较
尾 - 尾比较
基于最长递增子序列进行移动 / 删除 / 新增
举个🌰
oldChild [a,b,c,d,e,f,g]
newChild [a,b,f,c,d,e,h,g]
首先进行头 - 头比较,比较到不一样的节点时跳出循环
然后进行尾 - 尾比较,比较到不一样的节点时跳出循环
剩余[f,c,d,e,h]
通过newIndexToOldIndexMap生成数组[5, 2, 3, 4, -1]
得出最长递增子序列[2, 3, 4]对应节点为[c, d, e]
剩余的节点基于[c, d, e]进行移动 / 新增 / 删除
最长递增子序列 减少Dom元素的移动,达到最少的 dom 操作以减小开销。
关于最长递增子序列算法可以看看最长递增子序列
静态标记
Vue2中对vdom进行全量Diff,Vue3中增加了静态标记进行非全量Diff
对vnode打了像以下枚举内的静态标记
export const enum PatchFlags{ TEXT = 1 , CLASS = 1 << 1 , STYLE = 1 << 2 , PROPS = 1 << 3 , FULL_PROPS = 1 << 4 , HYDRATE_EVENTS = 1 << 5 , STABLE_FRAGMENT = 1 << 6 , KEYED_FRAGMENT = 1 << 7 , UNKEYEN_FRAGMENT = 1 << 8 , NEED_PATCH = 1 << 9 , DYNAMIC_SLOTS = 1 << 10 , HOISTED = -1 , BAIL = -2 }
举个🌰
<div > <p > Hello World</p > <p > {{msg}}</p > </div >
对msg变量进行了标记
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue" export function render (_ctx, _cache, $props, $setup, $data, $options ) { return (_openBlock(), _createBlock("div" , null , [ _createVNode("p" , null , "Hello World" ), _createVNode("p" , null , _toDisplayString(_ctx.msg), 1 ) ])) }
总结
对vnode进行标记,将需要动态更新和不需要动态更新的节点进行分类
静态节点仅需创建一次,渲染直接复用,不参与diff算法流程。
静态提升
Vue2中无论是元素是否参与更新,每次都会重新创建
Vue3中对于不参与更新的元素,只会被创建一次,之后会在每次渲染时候被不停地复用
以后每次进行render的时候,就不会重复创建这些静态的内容,而是直接从一开始就创建好的常量中取就行了。
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue" export function render (_ctx, _cache, $props, $setup, $data, $options ) { return (_openBlock(), _createBlock("div" , null , [ _createVNode("p" , null , "Xmo" ), _createVNode("p" , null , "Xmo" ), _createVNode("p" , null , "Xmo" ), _createVNode("p" , null , _toDisplayString(_ctx.msg), 1 ) ])) } const _hoisted_1 = _createVNode("p" , null , "Xmo" , -1 )const _hoisted_2 = _createVNode("p" , null , "Xmo" , -1 )const _hoisted_3 = _createVNode("p" , null , "Xmo" , -1 )export function render (_ctx, _cache, $props, $setup, $data, $options ) { return (_openBlock(), _createBlock("div" , null , [ _hoisted_1, _hoisted_2, _hoisted_3, _createVNode("p" , null , _toDisplayString(_ctx.msg), 1 ) ])) }
cacheHandlers 事件侦听器缓存
<div> <button @click ="onClick" >btn</button> </div> import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue" export function render (_ctx, _cache, $props, $setup, $data, $options ) { return (_openBlock(), _createBlock("div" , null , [ _createVNode("button" , { onClick : _ctx.onClick }, "btn" , 8 , ["onClick" ]) ])) } import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue" export function render (_ctx, _cache, $props, $setup, $data, $options ) { return (_openBlock(), _createBlock("div" , null , [ _createVNode("button" , { onClick: _cache[1 ] || (_cache[1 ] = (...args ) => (_ctx.onClick(...args))) }, "btn" ) ])) }
它的意思很明显,onClick 方法被存入 cache。
在使用的时候,如果能在缓存中找到这个方法,那么它将直接被使用。
如果找不到,那么将这个方法注入缓存。
总之,就是把方法给缓存了。
掘金:前端LeBron
知乎:前端LeBron
持续分享技术博文,关注微信公众号👇🏻