Vue3 响应式原理
Opened this issue · 0 comments
Vue3 响应式原理 - Ref/Reactive/Effect源码分析
目前vue3还没完全稳定下来,许多rfcs都有变化的可能。本文基于目前最新(2019-11-07)fork的 vue源码进行原理分析。官方提供了在Vue2.x尝试最新Vue3功能的插件库:Vue Composition API (以前该库叫vue-function-api,现在叫composition-api)。
众所周知,Vue3使用ES6 Proxy替代ES5 Object.defineProperty实现数据响应式,这也是Vue最为核心的功能之一。Vue3相比Vue2.x,API变化很大,提出了Vue Composition API。但在响应式原理实现方面,源码依然还是依赖收集 + 执行回调,只不过api变化后,形式也有点变化。想了解vue 2.x实现方式,可以看下笔者以前写的 Vue2.x源码分析 - 响应式原理。
你必须知道的Vue3 RFCS ChangeLog
如果较少关注vue3征求意见稿vue rfcs,可能大部分人对vue3还停留在Vue Function API。作者尤雨溪专门为这重大改变的API做过详细的叙述,并特意翻译了中文Vue Function-based API RFC。目前Vue 官方发布了最新的3.0 API 修改草案,并在充分采纳社区的意见后,将Vue Function API 更正为 Vue Composition API.
1. 重大变化点
- state更名为reactive
- reactive等价于 Vue 2.x 的Vue.observable()
- value更名为ref,并提供isRef和toRefs
- 使用ref来创建包装对象进行传递
- computed可传入get和set,用于定义可更改的计算属性
- effect 更名为 watchEffect
Vue官方团队建议在组合函数中都通过返回ref对象。
2. 了解Vue Composition API
import { reactive, computed, toRefs, watchEffect } from "vue";
export default {
setup() {
const event = reactive({
capacity: 4,
attending: ["Tim", "Bob", "Joe"],
spacesLeft: computed(() => { return event.capacity - event.attending.length; })
});
watchEffect(() => console.log(event.capacity))
function increaseCapacity() {
event.capacity++;
}
return { ...toRefs(event), increaseCapacity };
}
};
3. 建议阅读资料
源码解析
1. ref
先从入口ref看起,ref常用于基本类型,reactive用于引用类型。如果ref传入对象,其实内部会自动变为reactive。
ref本质上是把js 基本类型(string/number/bool)包装为引用对象,使得具有响应式特性。
export function ref(raw: unknown) {
if (isRef(raw)) {
return raw
}
// ref常用于基本类型,reactive用于引用类型。如果ref传入对象,其实内部会自动变为reactive
raw = convert(raw)
// 基本类型,转为含有getter/setter的对象
const r = {
_isRef: true, // 判断isRef
// 基本类型无法被追踪,所以使用ref包装为object,使得可以被追踪
get value() {
track(r, OperationTypes.GET, '')
return raw
},
set value(newVal) {
raw = convert(newVal)
trigger(r, OperationTypes.SET, '')
}
}
return r as Ref
}
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
同时ref支持把reactive转为refs对象 - toRefs:
export function toRefs<T extends object>(
object: T
): { [K in keyof T]: Ref<T[K]> } {
const ret: any = {}
for (const key in object) { // for in 展开一层
ret[key] = toProxyRef(object, key)
}
return ret
}
function toProxyRef<T extends object, K extends keyof T>(
object: T,
key: K
): Ref<T[K]> {
return {
_isRef: true,
get value(): any {
return object[key]
},
set value(newVal) {
object[key] = newVal
}
}
}
2. reactive
再来看下vue3的响应式reactive源码:
先认识下以下4个全局存储,使用weakmap存储起普通对象和生成的响应式对象,因为很多地方都需要用到判断以及取值。其中rawToReactive和reactiveToRaw是一组,只不过key和value互相对调。
// 防止重复设置响应式对象,建立store存起来
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // 原始object对象:封装的响应式对象
const reactiveToRaw = new WeakMap<any, any>() // 封装的响应式对象:原始object对象
// 只读响应式
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
下面是reactive入口,如果传入参数是只读响应式,或者是用户设置的只读类型,返回处理。大部分都会走createReactiveObject方法:
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (readonlyToRaw.has(target)) {
return target
}
// target is explicitly marked as readonly by user
if (readonlyValues.has(target)) {
return readonly(target)
}
// 给普通对象创建响应式对象
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
如下面注释解释,大部分代码都是为了做边界和重复处理。最重要的还是创建proxy对象:
observed = new Proxy(target, mutableHandlers)。
function createReactiveObject(
target: unknown, // 原始对象
toProxy: WeakMap<any, any>, // 全局rawToReactive
toRaw: WeakMap<any, any>, // 全局reactiveToRaw
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 必须是对象
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 重复的对象引用,最终都返回初始的监听对象,这就是创建全局store的原因之一
// target already has corresponding Proxy
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// target is already a Proxy
if (toRaw.has(target)) {
return target
}
// vue对象、vnode对象等不能被创建为响应式
// only a whitelist of value types can be observed.
if (!canObserve(target)) {
return target
}
// 真正创建代理Proxy对象并返回
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers // [Set, Map, WeakMap, WeakSet]对象走这个handles
: baseHandlers // 大部分走baseHandle
observed = new Proxy(target, handlers)
// 创建完马上全局缓存
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
所以还是看代理对象mutableHandlers中的处理:
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
get、has、deleteProperty、ownKeys代理方法中,都调用了track函数,用来收集依赖,这个下文讲;而set调用了trigger函数,当响应式数据变化时,收集的依赖被执行回调。从原理看,这跟vue2.x是一致的。
看下最常用的get、set。get中除常规边界处理外,最重要是根据代理值的类型,对object类型进行递归调用reactive。
function createGetter(isReadonly: boolean) {
return function get(target: object, key: string | symbol, receiver: object) {
// 获取到代理的值
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
// 如果是ref包裹的对象,直接返回解包后的值
if (isRef(res)) {
return res.value
}
// track是逻辑和视图变化重要的一块
track(target, OperationTypes.GET, key)
// 值类型,直接返回值;对象类型,递归响应式reactive(res)
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
set函数里除了代理set方法外,最重要的莫过于当值改变时,触发trigger方法,下文详细讲述该函数。
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
value = toRaw(value)
const oldValue = (target as any)[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
// prxoy
const hadKey = hasOwn(target, key) // 是否target本来旧有key属性,等价于:key in target
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key) // 触发新增
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key) // 触发修改
}
}
return result
}
3. track/trigger
这里是vue3响应式源码的难点。但原理跟vue2.x基本一致,只不过实现方式上有些不同。
track用于收集依赖deps(依赖一般收集effect/computed/watch的回调函数),trigger 用于通知deps,通知依赖这一状态的对象更新。
3.1 举个例子解释
如下代码,使用effect或computed api时,里面使用了count.num,意味着这个effect依赖于count.num。当count.num set改变值时,需要通知该effect去执行。那什么时候count.num收集到effect这个依赖呢?
答案是创建effect时的回调函数。如果回调函数中用到响应式数据(意味着会去执行get函数),则同步这个effect到响应式数据(这里是count.num)的依赖集中。
其流程是(全文重点):1. effect/computed函数执行 -> 2. 代码有书写响应式数据,调用到get,依赖收集 -> 3. 当有set时,依赖集更新。
const count = reactive({ num: 0 })
// effect默认没带lazy参数,先会执行effect
effect(() => {
// effect用到对应响应式数据时,count.num get就已经收集好了该effect依赖
// 同理,使用computed api时,
console.log(count.num)
})
// computed依赖于count.num,也意味着该computed是count.num的依赖项
const computedNum = computed(() => 2 * count.num))
count.num = 7
3.2 对应源码解释
理解了上面这个案例,源码阅读就能顺畅的多。
先挑effect实现过程,再来看依赖收集track函数和执行依赖函数trigger。effect api主要用effect包装了回调函数fn,并默认执行fn回调函数,最终执行run(effect, fn, args)。
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
// 回调fn函数,包装成effect
const effect = createReactiveEffect(fn, options)
// 默认不是懒加载,lazy=fasle,执行effect函数。
if (!options.lazy) {
effect()
}
return effect
}
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
return run(effect, fn, args) // 创建effect时,执行run
} as ReactiveEffect
effect._isEffect = true // 判断是effect
effect.active = true // effect支持手动stop,此时active会被设置为false
effect.raw = fn
effect.scheduler = options.scheduler
effect.onTrack = options.onTrack
effect.onTrigger = options.onTrigger
effect.onStop = options.onStop
effect.computed = options.computed
effect.deps = []
return effect
}
再看run函数内容。其实就是执行回调函数时,先对effect入栈,使得当前effectStack有值。这个就非常巧妙,当执行fn回调时,回调函数的代码中又会去访问响应式数据(reactive),这样又会执行响应数据的get方法,get方法又会去执行后文讲的trick方法,trick进行依赖收集。
依赖收集哪些东西呢?就是收集当前的effect回调函数。这个回调函数(被effect包装)不就是刚被存储在effectStack么,所以在后续trick函数中可以看到使用effectStack栈。当执行完回调函数,再进行出栈。
通过使用栈数据结构,以及对代码执行的时机,非常巧妙的就把当前effect传递过去,最终被响应式数据收集到依赖集中。
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
if (!effect.active) {
return fn(...args)
}
// 通常都是走这里,执行回调,同时不同时机effect入栈/出栈
if (!effectStack.includes(effect)) {
cleanup(effect)
// 这里的try finally很巧妙
// 入栈 -> 回调函数执行(使用栈,相当于把effect传递过去了) -> 出栈
try {
effectStack.push(effect)
return fn(...args)
} finally {
effectStack.pop()
}
}
}
再来看看依赖收集trick/trigger具体实现细节。
先来看下几个存储变量,主要是依赖收集时用到的:
// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<any, Dep>
// 原始对象: new Map({key1: new Set([effect1, effect2,...])}, {key2: Set2}, ...)
// key是原始对象里的属性, 值为该key改变后会触发的一系列的函数, 比如渲染、computed
export const targetMap = new WeakMap<any, KeyToDepMap>()
track函数进行数据依赖采集, 以便于后面数据更改能够触发对应的函数。
// 收集target key的依赖
// get: track(target, OperationTypes.GET, key)
export function track(target: object, type: OperationTypes, key?: unknown) {
// 定义的computed、effect api都会推入effectStack栈中
if (!shouldTrack || effectStack.length === 0) {
return
}
// 调用effect/computed api时,能拿到effect对象(即依赖的回调函数)
const effect = effectStack[effectStack.length - 1]
if (type === OperationTypes.ITERATE) {
key = ITERATE_KEY
}
// targetMap = {target1: deps = {key1: [], key2: [], ...}},两层嵌套
// 初始化target
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
// 初始化target.key。键是target.key,值是依赖的effect数组,是个集合。
let dep = depsMap.get(key!)
if (dep === void 0) {
depsMap.set(key!, (dep = new Set()))
}
// 依赖收集
if (!dep.has(effect)) {
dep.add(effect)
effect.deps.push(dep)
}
}
trigger,将track收集到的effect函数集合,添加到runners中(二选一放进effects或computedRunners中),并通过scheduleRun执行effect:
// set: trigger(target, OperationTypes.SET, key)
export function trigger(
target: object,
type: OperationTypes,
key?: unknown,
extraInfo?: DebuggerEventExtraInfo
) {
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
// never been tracked
return
}
// 把拿到的depsMap.get(key),二选一放进effects或computedRunners中。
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
// 根据不同的OperationTypes,把effect=depsMap.get(key)放进runners中
if (type === OperationTypes.CLEAR) {
// collection being cleared, trigger all effects for target
depsMap.forEach(dep => {
addRunners(effects, computedRunners, dep)
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
addRunners(effects, computedRunners, depsMap.get(key))
}
// also run for iteration key on ADD | DELETE
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
}
// 执行runners,即执行effects
const run = (effect: ReactiveEffect) => {
scheduleRun(effect, target, type, key, extraInfo)
}
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
effects.forEach(run)
}
// 添加runner时,二选一
function addRunners(
effects: Set<ReactiveEffect>,
computedRunners: Set<ReactiveEffect>,
effectsToAdd: Set<ReactiveEffect> | undefined
) {
if (effectsToAdd !== void 0) {
effectsToAdd.forEach(effect => {
if (effect.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
})
}
}
总结
响应式数据,就是当数据对象改变时(set),有用到数据对象的地方,都会自动执行响应的逻辑。比如effect/computed/watch等js api用到数据对象,则执行对应的回调函数。而视图view用到数据对象时,则重新vnode diff,最后自动进行dom更新(即视图更新)。
而Vue3响应式源码跟Vue2.x源码流程基本一致,依然是利用在使用响应式数据时,执行数据的get方法,收集相关的依赖(依赖可以是回调函数,如effect/computed,也可以是视图自动更新);在数据进行变化的时候,执行数据的set方法,把收集的依赖都依次执行。