Effect 和 Reactive

effect 作为 Vue 响应式原理中的核心,在 Computed、Watch、Reactive 中都有出现

主要和 Reactive(Proxy)、track、trigger 等函数配合实现收集依赖,触发依赖更新

  • Effect
    • 副作用依赖函数
  • Track
    • 依赖收集
  • Trigger
    • 依赖触发

Effect

effect 可以被理解为一个副作用函数,被当做依赖收集,在响应式数据更新后被触发。

Vue 的响应式 API 例如 Computed、Watch 都有用到 effect 来实现

  • 先来看看入口函数
    • 入口函数主要是一些逻辑处理,核心逻辑位于 createReactiveEffect
function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
// 如果已经是effect,则重置
if (isEffect(fn)) {
fn = fn.raw;
}
// 创建effect
const effect = createReactiveEffect(fn, options);
// 如果不是惰性执行,先执行一次
if (!options.lazy) {
effect();
}
return effect;
}
  • createReactiveEffect
const effectStack: ReactiveEffect[] = [];

function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
// 没有激活,说明调用了effect stop函数
if (!effect.active) {
// 无调度者则直接返回,否则执行fn
return options.scheduler ? undefined : fn();
}
// 判断EffectStack中有没有effect,有则不处理
if (!effectStack.includes(effect)) {
// 清除effect
cleanup(effect);
try {
/*
* 开始重新收集依赖
* 压入stack
* 将effect设置为activeEffect
* */
enableTracking();
effectStack.push(effect);
activeEffect = effect;
return fn();
} finally {
/*
* 完成后将effect弹出
* 重置依赖
* 重置activeEffect
* */
effectStack.pop();
resetTracking();
activeEffect = effectStack[effectStack.length - 1];
}
}
} as ReactiveEffect;
effect.id = uid++; // 自增id,effect唯一标识
effect.allowRecurse = !!options.allowRecurse;
effect._isEffect = true; // 是否是effect
effect.active = true; // 是否激活
effect.raw = fn; // 挂载原始对象
effect.deps = []; // 当前effect的dep数组
effect.options = options; // 传入的options
return effect;
}

// 每次effect运行都会重新收集依赖,deps是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) {
// activeEffect为空表示没有依赖
if (!shouldTrack || activeEffect === undefined) {
return;
}

// targetMap依赖管理Map,用于收集依赖
// 检查targetMap中有没有target,没有则新建
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}

// dep用来收集依赖函数,当监听的key值发生变化,触发dep中的依赖函数更新
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
// 开发环境会触发onTrack,仅用于调试
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>
) {
// 获取依赖Map,如果没有则不需要触发
const depsMap = targetMap.get(target);
if (!depsMap) {
// never been tracked
return;
}

// 使用Set保存需要触发的effect,避免重复
const effects = new Set<ReactiveEffect>();
// 定义依赖添加函数
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach((effect) => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect);
}
});
}
};

// 将depsMap中的依赖添加到effects中
// 只为了理解和原理的话 各个分支不用细看
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add);
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep);
}
});
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key));
}

// also run for iteration key on ADD | DELETE | Map.SET
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)) {
// new index added to array -> length changes
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;
}
}

// 封装effects执行函数
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget,
});
}
// 如果存在scheduler则调用
if (effect.options.scheduler) {
effect.options.scheduler(effect);
} else {
effect();
}
};

// 触发effects中的所有依赖函数
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.ADD, key, value)
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 的赋值处理
    • 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,
// 响应式数据更新后将dirty赋值为true
// 下次执行getter判断dirty为true即重新计算computed值
scheduler: () => {
if (!this._dirty) {
this._dirty = true;
// 派发所有引用当前计算属性的副作用函数effect
trigger(toRaw(this), TriggerOpTypes.SET, 'value');
}
},
});

this[ReactiveFlags.IS_READONLY] = isReadonly;
}

get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this);
// 当响应式数据更新后dirty为true
// 重新计算数据后,将dirty赋值为false
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

  • Watch

用于对某个变量的监听,同时可以通过 callBack 拿到新值和旧值

watch(state, (state, prevState) => {});
  • WatchEffect

每次更新都会执行,自动收集使用到的依赖

无法获取到新值和旧值,可手动停止监听

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 中
// watch
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);
}
  • doWatch

以下为删减版源码,理解核心原理即可

详情见注释

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

// 对不同的情况做getter赋值
if (isRef(source)) {
// ref通过.value获取
getter = () => (source as Ref).value
forceTrigger = !!(source as Ref)._shallow
} else if (isReactive(source)) {
// reactive直接获取
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)) {
// 如果是函数的情况
// 有cb则为watch,没有则为watchEffect
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER, [
instance && (instance.proxy as any)
])
} else {
// no cb -> simple effect
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)
}
}

// 记录oldValue,并通过runner获取newValue
// callback的封装处理为job
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => {
if (!runner.active) {
return
}
if (cb) {
// watch(source, 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))
) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onInvalidate
])
oldValue = newValue
}
} else {
// watchEffect
runner()
}
}

// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb


// 通过读取配置,处理job的触发时机
// 并再次将job的执行封装到scheduler中
let scheduler: ReactiveEffectOptions['scheduler']
if (flush === 'sync') { // 同步执行
scheduler = job
} else if (flush === 'post') { // 更新后执行
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
// 更新前执行
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job)
} else {
// with 'pre' option, the first call must happen before
// the component is mounted so it is called synchronously.
job()
}
}
}

// 使用effect副作用处理依赖收集,在依赖更新后调用scheduler(其中封装了callback的执行)
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
})

// 收集依赖
recordInstanceBoundEffect(runner, instance)

// 读取配置,进行watch初始化
// 是否有cb
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)
);

// 对mixin中的对象进行遍历
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;
}
  • mergeHook

简单粗暴放进 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]
  1. 首先进行头 - 头比较,比较到不一样的节点时跳出循环
    • 得到[a,b]
  2. 然后进行尾 - 尾比较,比较到不一样的节点时跳出循环
    • 得到[g]
  3. 剩余[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 打了像以下枚举内的静态标记

  • patchFlag
export const enum PatchFlags {
TEXT = 1, //动态文本节点
CLASS = 1 << 1, //2 动态class
STYLE = 1 << 2, //4 动态style
PROPS = 1 << 3, //8 动态属性,但不包含类名和样式
FULL_PROPS = 1 << 4, //16 具有动态key属性,当key改变时,需进行完整的diff比较
HYDRATE_EVENTS = 1 << 5, //32 带有监听事件的节点
STABLE_FRAGMENT = 1 << 6, //64 一个不会改变子节点顺序的fragment
KEYED_FRAGMENT = 1 << 7, //128 带有key属性的fragment或部分子节点有key
UNKEYEN_FRAGMENT = 1 << 8, //256 子节点没有key的fragment
NEED_PATCH = 1 << 9, //512 一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10, //1024 动态slot
HOISTED = -1, //静态节点
//指示在diff过程中要退出优化模式
BAIL = -2,
}

举个 🌰

  • 模板长这样
<div>
<p>Hello World</p>
<p>{{msg}}</p>
</div>
  • 生成 vdom 源码

对 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 /* TEXT */),
])
);
}

// Check the console for the AST

总结

  • 对 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 /* TEXT */),
])
);
}

/*
* 静态提升后
*/
const _hoisted_1 = /*#__PURE__*/ _createVNode(
'p',
null,
'Xmo',
-1 /* HOISTED */
);
const _hoisted_2 = /*#__PURE__*/ _createVNode(
'p',
null,
'Xmo',
-1 /* HOISTED */
);
const _hoisted_3 = /*#__PURE__*/ _createVNode(
'p',
null,
'Xmo',
-1 /* HOISTED */
);

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 /* TEXT */),
])
);
}

// Check the console for the AST

cacheHandlers 事件侦听器缓存

  • 默认情况下 onClick 会被视为动态绑定,所以每次都会去追踪它的变化

  • 但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可。

// 模板
<div>
<button @click="onClick">btn</button>
</div>


// 使用缓存前
// 这里我们还没有开启事件监听缓存,熟悉的静态标记 8 /* PROPS */ 出现了,
// 它将标签的 Props (属性) 标记动态属性。
// 如果我们存在属性不会改变,不希望这个属性被标记为动态,那么就需要 cacheHandler 的出场了。
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 /* PROPS */, ["onClick"])
]))
}

// Check the console for the AST


// 使用缓存后
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")
]))
}

// Check the console for the AST

它的意思很明显,onClick 方法被存入 cache。

在使用的时候,如果能在缓存中找到这个方法,那么它将直接被使用。

如果找不到,那么将这个方法注入缓存。

总之,就是把方法给缓存了。

掘金:前端 LeBron

知乎:前端 LeBron

持续分享技术博文,关注微信公众号 👇🏻

img