注: 为了直观的看到 Vue3 的实现逻辑, 本文移除了边缘状况处理、兼容处理、DEV环境的特殊逻辑等, 只保留了核心逻辑javascript
vue-next/reactivity 实现了 Vue3 的响应性, reactivity 提供了如下接口:html
export { ref, // 代理基本类型 shallowRef, // ref 的浅代理模式 isRef, // 判断一个值是不是 ref toRef, // 把响应式对象的某个 key 转为 ref toRefs, // 把响应式对象的全部 key 转为 ref unref, // 返回 ref.value 属性 proxyRefs, customRef, // 自行实现 ref triggerRef, // 触发 customRef Ref, // 类型声明 ToRefs, // 类型声明 UnwrapRef, // 类型声明 ShallowUnwrapRef, // 类型声明 RefUnwrapBailTypes // 类型声明 } from './ref' export { reactive, // 生成响应式对象 readonly, // 生成只读对象 isReactive, // 判断值是不是响应式对象 isReadonly, // 判断值是不是只读对象 isProxy, // 判断值是不是 proxy shallowReactive, // 生成浅响应式对象 shallowReadonly, // 生成浅只读对象 markRaw, // 让数据不可被代理 toRaw, // 获取代理对象的原始对象 ReactiveFlags, // 类型声明 DeepReadonly // 类型声明 } from './reactive' export { computed, // 计算属性 ComputedRef, // 类型声明 WritableComputedRef, // 类型声明 WritableComputedOptions, // 类型声明 ComputedGetter, // 类型声明 ComputedSetter // 类型声明 } from './computed' export { effect, // 定义反作用函数, 返回 effect 自己, 称为 runner stop, // 中止 runner track, // 收集 effect 到 Vue3 内部的 targetMap 变量 trigger, // 执行 targetMap 变量存储的 effects enableTracking, // 开始依赖收集 pauseTracking, // 中止依赖收集 resetTracking, // 重置依赖收集状态 ITERATE_KEY, // 固定参数 ReactiveEffect, // 类型声明 ReactiveEffectOptions, // 类型声明 DebuggerEvent // 类型声明 } from './effect' export { TrackOpTypes, // track 方法的 type 参数的枚举值 TriggerOpTypes // trigger 方法的 type 参数的枚举值 } from './operations'
target: 普通的 JS 对象vue
reactive: @vue/reactivity
提供的函数, 接收一个对象, 并返回一个 代理对象, 即响应式对象java
shallowReactive: @vue/reactivity
提供的函数, 用来定义浅响应对象python
readonly:@vue/reactivity
提供的函数, 用来定义只读对象react
shallowReadonly: @vue/reactivity
提供的函数, 用来定义浅只读对象git
handlers: Proxy 对象暴露的钩子函数, 有 get()
、set()
、deleteProperty()
、ownKeys()
等, 能够参考MDNgithub
targetMap: @vue/reactivity
内部变量, 存储了全部依赖数组
effect: @vue/reactivit
提供的函数, 用于定义反作用, effect(fn, options)
的参数就是反作用函数缓存
watchEffect: @vue/runtime-core
提供的函数, 基于 effect 实现
track: @vue/reactivity
内部函数, 用于收集依赖
trigger: @vue/reactivity
内部函数, 用于消费依赖
scheduler: effect 的调度器, 容许用户自行实现
先看下边的流程简图, 图中 Vue 代码的功能是: 每隔一秒在 id
为 Box
的 div
中输出当前时间
在开始梳理 Vue3 实现响应式的步骤以前, 要先简单理解 effect
, effect
是响应式系统的核心, 而响应式系统又是 Vue3 的核心
上图中从 track
到 targetMap
的黄色箭头, 和从 targetMap
到 trigger
的白色箭头, 就是 effect
函数要处理的环节
effect
函数的语法为:
effect(fn, options)
effect
接收两个参数, 第一个必填参数 fn
是反作用函数
第二个选填 options
的参数定义以下:
export interface ReactiveEffectOptions { lazy?: boolean // 是否延迟触发 effect scheduler?: (job: ReactiveEffect) => void // 调度函数 onTrack?: (event: DebuggerEvent) => void // 追踪时触发 onTrigger?: (event: DebuggerEvent) => void // 触发回调时触发 onStop?: () => void // 中止监听时触发 allowRecurse?: boolean // 是否容许递归 }
下边从流程图中左上角的 Vue 代码开始
经过 reactive
方法将 target
对象转为响应式对象, reactive
方法的实现方法以下:
import { mutableHandlers } from './baseHandlers' import { mutableCollectionHandlers } from './collectionHandlers' const reactiveMap = new WeakMap<Target, any>() const readonlyMap = new WeakMap<Target, any>() export function reactive(target: object) { return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers ) } function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { const proxyMap = isReadonly ? readonlyMap : reactiveMap const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } const targetType = getTargetType(target) // 先忽略, 上边例子中, targetType 的值为: 1 const proxy = new Proxy( target, targetType === 2 ? collectionHandlers : baseHandlers ) proxyMap.set(target, proxy) return proxy }
reactive
方法携带 target
对象和 mutableHandlers
、mutableCollectionHandlers
调用 createReactiveObject
方法, 这两个 handers 先忽略
createReactiveObject
方法经过 reactiveMap
变量缓存了一份响应式对象, reactiveMap
和 readonlyMap
变量是文件内部的变量, 至关于文件级别的闭包变量
其中 targetType 有三种枚举值: 0 表明不合法, 1 表明普通对象, 2 表明集合, 图中例子中, targetType
的值为 1, 对于 { text: '' }
这个普通对象传进 reactive()
方法时, 使用 baseHandlers
提供的 mutableHandlers
最后调用 Proxy 方法将 target 转为响应式对象, 其中 "响应" 体如今 handers 里, 能够这样理解: reactive = Proxy (target, handlers)
mutableHandlers
负责挂载 get
、set
、deleteProperty
、has
、ownKeys
这五个方法到响应式对象上
其中 get
、has
、ownKeys
负责收集依赖, set
和 deleteProperty
负责消费依赖
响应式对象的 get
、has
和 ownKeys
方法被触发时, 会调用 createGetter
方法, createGetter
的实现以下:
function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { const res = Reflect.get(target, key, receiver) if (!isReadonly) { track(target, TrackOpTypes.GET, key) } if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) } return res } }
当 { text: '' }
这个普通JS对象传到 createGetter
时, key 的值为: text
, res 的值为: String
类型, 若是 res 的值为 Object
类型则会递归调用, 将 res 转为响应式对象
createGetter
方法的目的是触发 track
方法, 对应本文的第 3 步
响应式对象的 set
和 deleteProperty
方法被触发时, 会调用 createSetter
方法, createSetter
的实现以下:
function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { const oldValue = (target as any)[key] const result = Reflect.set(target, key, value, receiver) trigger(target, TriggerOpTypes.SET, key, value, oldValue) return result } }
createSetter
方法的目的是触发 trigger
方法, 对应本文的第 4 步
这一步是整个响应式系统最关键的一步, 即咱们常说的依赖收集, 依赖收集的概念很简单, 就是把 响应式数据 和 反作用函数 创建联系
文章一开始流程图的例子中, 就是把 target
对象和 document.getElementById("Box").innerText = date.text;
这个反作用函数创建关联, 这个 "关联" 指的就是上边提到的 targetMap
变量, 后边会详细描述一下 targetMap
对象的结构
第 2 步介绍了 createGetter
方法的核心是调用 track
方法, track
方法由 @/vue/reativity/src/effect.ts
提供, 下面看一下 track
的实现:
const targetMap = new WeakMap<any, KeyToDepMap>() // target: { text: '' } // type: get // key: text export function track(target: object, type: TrackOpTypes, key: unknown) { 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) } }
从 track
方法咱们能看到 targetMap
这个闭包变量上储存了全部的 effect
, 换句话说是把能影响到 target
的反作用函数收集到 targetMap
变量中
targetMap 是个 WeakMap, WeakMap 和 Map 的区别在于 WeakMap 的键只能是对象, 用 WeakMap 而不用 Map 是由于 Proxy 对象不能代理普通数据类型
targetMap 的结构:
const targetMap = { [target]: { [key1]: [effect1, effect2, effect3, ...], [key2]: [effect1, effect2, effect3, ...] } }
{ text: '' }
这个target 传进来时, targetMap 的结构是:
// 上边例子中用来在 id 为 Box 的 div 中输出当前时间的反作用函数 const effect = () => { document.getElementById("Box").innerText = date.text; }; const target = { "{ text: '' }": { "text": [effect] } }
举三个例子, 来分析一下 targetMap 的结构, 第一个例子是多个 target 状况:
<script> import { effect, reactive } from "@vue/reactivity"; const target1 = { language: "JavaScript"}; const target2 = { language: "Go"}; const target3 = { language: "Python"}; const r1 = reactive(target1); const r2 = reactive(target2); const r3 = reactive(target3); // effect1 effect(() => { console.log(r1.language); }); // effect2 effect(() => { console.log(r2.language); }); // effect3 effect(() => { console.log(r3.language); }); // effect4 effect(() => { console.log(r1.language); console.log(r2.language); console.log(r3.language); }); </script>
这种状况下 targetMap 的构成是:
const effect1 = () => { console.log(r1.language); }; const effect2 = () => { console.log(r2.language); }; const effect3 = () => { console.log(r3.language); }; const effect4 = () => { console.log(r1.language); console.log(r2.language); console.log(r3.language); }; const targetMap = { '{"language":"JavaScript"}': { "language": [effect1, effect4] }, '{"language":"Go"}': { "language": [effect2, effect4] }, '{"language":"Python"}': { "language": [effect3, effect4] } }
第二个例子是单个 target 多个属性时:
import { effect, reactive } from "@vue/reactivity"; const target = { name: "rmlzy", age: "27", email: "rmlzy@outlook.com"}; const user = reactive(target); effect(() => { console.log(user.name); console.log(user.age); console.log(user.email); });
这种状况下 targetMap 的构成是:
const effect = () => { console.log(user.name); console.log(user.age); console.log(user.email); }; const targetMap = { '{"name":"rmlzy","age":"27","email":"rmlzy@outlook.com"}': { "name": [effect], "age": [effect], "email": [effect] } }
第三个例子是多维对象时:
import { effect, reactive } from "@vue/reactivity"; const target = { name: "rmlzy", skills: { frontend: ["JS", "TS"], backend: ["Node", "Python", "Go"] } }; const user = reactive(target); // effect1 effect(() => { console.log(user.name); }); // effect2 effect(() => { console.log(user.skills); }); // effect3 effect(() => { console.log(user.skills.frontend); }); // effect4 effect(() => { console.log(user.skills.frontend[0]); });
这种状况下 targetMap 的构成是:
const effect1 = () => { console.log(user.name); }; const effect2 = () => { console.log(user.skills); }; const effect3 = () => { console.log(user.skills.frontend); }; const effect4 = () => { console.log(user.skills.frontend[0]); }; const targetMap = { '{"name":"rmlzy","skills":{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}}': { "name": [effect1], "skills": [effect2, effect3, effect4] }, '{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}': { "frontend": [effect3, effect4] } }
第 3 步的目的是收集依赖, 这一步的目的是消费依赖
这里要注意, 只有当 target 代理对象的 get
方法被触发时, 才会真正执行 track
, 换句话说, 没有地方须要 get
target 对象时, target 没有依赖, 也就没有收集依赖一说
下边的例子中只是把 target 转换为了响应式对象, 并无触发依赖收集, targetMap 是空的
const target = {"text": ""}; const date = reactive(target); effect(() => { date.text = new Date().toString(); });
第 2 步介绍了 createSetter
方法的核心是调用 trigger
方法, trigger
方法由 @/vue/reativity/src/effect.ts
提供, 下面看一下 trigger
的实现:
export 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) { // never been tracked return } const effects = new Set<ReactiveEffect>() if (isMap(target)) { effects.add(depsMap.get(ITERATE_KEY)) } const run = (effect: ReactiveEffect) => { if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() } } effects.forEach(run) }
trigger 的实现很简单, 先把 target 相关的 effect 汇总到 effects 数组中, 而后调用 effects.forEach(run)
执行全部的反作用函数
再回顾一下 effect 方法的定义: effect(fn, options)
, 其中 options 有个可选属性叫 scheduler
, 从上边 run
函数也能够看到 scheduler
的做用是让用户自定义如何执行反作用函数
又回到了本文最开始讲的 effect, effect 函数的实现以下:
export 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 }
effect 的核心是调用 createReactiveEffect
方法
能够看到 options.lazy
默认为 false
会直接执行 effect, 当设置为 true
时, 会返回 effect 由用户手动触发
createReactiveEffect
函数的实现以下:
const effectStack: ReactiveEffect[] = [] let activeEffect: ReactiveEffect | undefined 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 }
首先定义了 effect 是个普通的 function
, 先看后边 effect 函数挂载的属性:
effect.id = uid++ // 自增ID, 每一个 effect 惟一的ID effect.allowRecurse = !!options.allowRecurse // 是否容许递归 effect._isEffect = true // 特殊标记 effect.active = true // 激活状态 effect.deps = [] // 依赖数组 effect.raw = fn // 缓存一份用户传入的反作用函数 effect.options = options // 缓存一份用户传入的配置
isEffect
函数用来判断值是不是 effect, 就是根据上边 _isEffect
变量判断的, isEffect
函数实现以下:
function isEffect(fn) { return fn && fn._isEffect === true; }
再来看 effect 的核心逻辑:
cleanup(effect) try { enableTracking() effectStack.push(effect) activeEffect = effect return fn() } finally { effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1] }
effectStack
用数组实现栈,activeEffect
是当前生效的 effect
先执行 cleanup(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 } }
cleanup
的目的是清空 effect.deps
, deps
是持有该 effect 的依赖数组, deps
的结构以下
清除完依赖后, 开始从新收集依赖, 把当前 effect 追加到 effectStack, 将 activeEffect 设置为当前的 effect, 而后调用 fn 而且返回 fn() 的结果
第 4 步提过到: "只有当 target 代理对象的 get
方法被触发时, 才会真正执行 track
", 至此才是真正的触发了 target
代理对象的 get
方法, 执行了track
方法而后收集到了依赖
等到 fn
执行结束, finally 阶段, 把当前的 effect 弹出, 恢复 effectStack 和 activeEffect, Vue3 整个响应式的流程到此结束
个人理解是为了暴露给 onTrack
方法, 来总体看一下 activeEffect 出现的地方:
let activeEffect; function effect(fn, options = EMPTY_OBJ) { const effect = createReactiveEffect(fn, options); return effect; } function createReactiveEffect(fn, options) { const effect = function reactiveEffect() { // 省略部分代码 ... try { activeEffect = effect; return fn(); } finally { activeEffect = effectStack[effectStack.length - 1]; } }; // 省略部分代码 ... return effect; } function track(target, type, key) { if (activeEffect === undefined) { return; } let dep = targetMap.get(target).get(key); // dep 是存储 effect 的 Set 数组 if (!dep.has(activeEffect)) { dep.add(activeEffect); activeEffect.deps.push(dep); if (activeEffect.options.onTrack) { activeEffect.options.onTrack({ effect: activeEffect, target, type, key }); } } }
在 fn
执行前, activeEffect
被赋值为当前 effect
在 fn
执行时的依赖收集阶段, 获取 targetMap 中的 dep (存储 effect 的 Set 数组), 并暴露给 options.onTrack
接口
@vue/reactivity
提供了 stop 函数, effect
能够被 stop 函数终止
const obj = reactive({ foo: 0 }); const runner = effect(() => { console.log(obj.foo); }); // effect 被执行一次, 输出 0 // obj.foo 被赋值一次, effect 被执行一次, 输出 1 obj.foo ++; // 中止 effect stop(runner); // effect 不会被触发, 无输出 obj.foo ++;
watchEffect
来自 @vue/runtime-core
, effect
来自 @vue/reactivity
watchEffect
基于 effect
实现watchEffect
会维护与组件实例的关系, 若是组件被卸载, watchEffect
会被 stop
, 而 effect
不会被 stop
watchEffect
接收的反作用函数, 会携带一个 onInvalidate
的回调函数做为参数, 这个回调函数会在反作用无效时执行
watchEffect(async (onInvalidate) => { let valid = true; onInvalidate(() => { valid = false; }); const data = await fetch(obj.foo); if (valid) { // 获取到 data } else { // 丢弃 } });
JS数据类型:
由于 Proxy 只能代理对象, reactive
函数的核心又是 Proxy, 因此 reactive 不能代理基本类型
对于基本类型须要用 ref 函数将基本类型转为对象:
class RefImpl<T> { private _value: T public readonly __v_isRef = true constructor(private _rawValue: T, public readonly _shallow = false) { this._value = _shallow ? _rawValue : convert(_rawValue) } get value() { track(toRaw(this), TrackOpTypes.GET, 'value') return this._value } set value(newVal) { if (hasChanged(toRaw(newVal), this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : convert(newVal) trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal) } } }
其中 __v_isRef
参数用来标志当前值是 ref 类型, isRef
的实现以下:
export function isRef(r: any): r is Ref { return Boolean(r && r.__v_isRef === true) }
这样作有个缺点, 须要多取一层 .value
:
const myRef = ref(0); effect(() => { console.log(myRef.value); }); myRef.value = 1;
这也是 Vue ref 语法糖提案的缘由, 能够参考 如何评价 Vue 的 ref 语法糖提案?
shallowReactive
用来定义浅响应数据, 深层次的对象值是非响应式的:
const target = { foo: { bar: 1 } }; const obj = shallowReactive(target); effect(() => { console.log(obj.foo.bar); }); obj.foo.bar = 2; // 无效, reactive 则有效 obj.foo = { bar: 2 }; // 有效
相似 shallowReactive
, 深层次的对象值是能够被修改的
markRaw 的做用是让数据不可被代理, 全部携带 __v_skip
属性, 而且值为 true
的数据都会被跳过:
export function markRaw<T extends object>(value: T): T { def(value, ReactiveFlags.SKIP, true) return value }
toRaw 的做用是获取代理对象的原始对象:
const obj = {}; const reactiveProxy = reactive(obj); console.log(toRaw(reactiveProxy) === obj); // true
const myRef = ref(0); const myRefComputed = computed(() => { return myRef.value * 2; }); effect(() => { console.log(myRef.value * 2); });
当 myRef
值变化时, computed 会执行一次, effect 会执行一次
当 myRef
值未变化时, computed 不会执行, effect 依旧会执行
若是你有问题欢迎留言和我交流, 阅读原文