目前vue3还没完全稳定下来,许多rfcs都有变化的可能。本文基于目前最新(2019-11-07)fork的 vue源码进行原理分析。官方提供了在Vue2.x尝试最新Vue3功能的插件库:Vue Composition API (以前该库叫vue-function-api,现在叫composition-api)。
如果较少关注vue3征求意见稿vue rfcs,可能大部分人对vue3还停留在Vue Function API。作者尤雨溪专门为这重大改变的API做过详细的叙述,并特意翻译了中文Vue Function-based API RFC。目前Vue 官方发布了最新的3.0 API 修改草案,并在充分采纳社区的意见后,将Vue Function API 更正为 Vue Composition API.
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
}
}
}
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
}
// 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)
}
}
Vue3 响应式原理 - Ref/Reactive/Effect源码分析
众所周知,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. 重大变化点
2. 了解Vue Composition API
3. 建议阅读资料
源码解析
1. ref
先从入口ref看起,ref常用于基本类型,reactive用于引用类型。如果ref传入对象,其实内部会自动变为reactive。
ref本质上是把js 基本类型(string/number/bool)包装为引用对象,使得具有响应式特性。
同时ref支持把reactive转为refs对象 - toRefs:
2. reactive
再来看下vue3的响应式reactive源码:
先认识下以下4个全局存储,使用weakmap存储起普通对象和生成的响应式对象,因为很多地方都需要用到判断以及取值。其中rawToReactive和reactiveToRaw是一组,只不过key和value互相对调。
下面是reactive入口,如果传入参数是只读响应式,或者是用户设置的只读类型,返回处理。大部分都会走createReactiveObject方法:
如下面注释解释,大部分代码都是为了做边界和重复处理。最重要的还是创建proxy对象: observed = new Proxy(target, mutableHandlers)。
所以还是看代理对象mutableHandlers中的处理:
get、has、deleteProperty、ownKeys代理方法中,都调用了track函数,用来收集依赖,这个下文讲;而set调用了trigger函数,当响应式数据变化时,收集的依赖被执行回调。从原理看,这跟vue2.x是一致的。
看下最常用的get、set。get中除常规边界处理外,最重要是根据代理值的类型,对object类型进行递归调用reactive。
set函数里除了代理set方法外,最重要的莫过于当值改变时,触发trigger方法,下文详细讲述该函数。
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时,依赖集更新。
3.2 对应源码解释
理解了上面这个案例,源码阅读就能顺畅的多。
先挑effect实现过程,再来看依赖收集track函数和执行依赖函数trigger。effect api主要用effect包装了回调函数fn,并默认执行fn回调函数,最终执行run(effect, fn, args)。
再看run函数内容。其实就是执行回调函数时,先对effect入栈,使得当前effectStack有值。这个就非常巧妙,当执行fn回调时,回调函数的代码中又会去访问响应式数据(reactive),这样又会执行响应数据的get方法,get方法又会去执行后文讲的trick方法,trick进行依赖收集。
依赖收集哪些东西呢?就是收集当前的effect回调函数。这个回调函数(被effect包装)不就是刚被存储在effectStack么,所以在后续trick函数中可以看到使用effectStack栈。当执行完回调函数,再进行出栈。
通过使用栈数据结构,以及对代码执行的时机,非常巧妙的就把当前effect传递过去,最终被响应式数据收集到依赖集中。
再来看看依赖收集trick/trigger具体实现细节。
先来看下几个存储变量,主要是依赖收集时用到的:
track函数进行数据依赖采集, 以便于后面数据更改能够触发对应的函数。
trigger,将track收集到的effect函数集合,添加到runners中(二选一放进effects或computedRunners中),并通过scheduleRun执行effect:
总结
响应式数据,就是当数据对象改变时(set),有用到数据对象的地方,都会自动执行响应的逻辑。比如effect/computed/watch等js api用到数据对象,则执行对应的回调函数。而视图view用到数据对象时,则重新vnode diff,最后自动进行dom更新(即视图更新)。
而Vue3响应式源码跟Vue2.x源码流程基本一致,依然是利用在使用响应式数据时,执行数据的get方法,收集相关的依赖(依赖可以是回调函数,如effect/computed,也可以是视图自动更新);在数据进行变化的时候,执行数据的set方法,把收集的依赖都依次执行。