"version": "3.2.20"
本文导读 vue3 的响应式原理,包括数据的处理、收集依赖和触发依赖。(也就是修改数据-页面的重新渲染,中间发生了什么)
<template>
<button type="button" @click="add">count is: {{ state.count }}</button>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
const add = () => {
state.count = state.count + 1
}
</script>
数据处理
Vue3 提供的数据响应式方法有很多,原理都类似,下面来一起看看 reactive 方法。
reactive 方法目录: /packages/reactivity/src/reactive.ts
reactive 调用 createReactiveObject ,使用 Proxy 处理数据,生成的新数据放进全局 reactiveMap ,并返回。简写如下:
export const reactiveMap = new WeakMap<Target, any>()
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
function reactive(target: object) {
// 目标已有相应的代理
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(target, baseHandlers)
proxyMap.set(target, proxy)
return proxy
}
数据的读取和修改会触发对应的 getter 和 setter ,与 Vue2 类似,会在 getter 中收集依赖, setter 中触发依赖,不过这里的依赖不是 Watcher ,而是 ReactiveEffect 的实例 effect 。
看 getter 和 setter 之前,先看看 mount 的过程会比较好理解。
APP 首次 mount 过程
通常项目的 main.js 会有这样一段代码: createApp(App).mount('#app') ,这个就是项目的根节点挂载。
下面来看源码的 createApp 方法,目录: /packages/runtime-dom/src/index.ts 。
createApp
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
const { mount } = app
app.mount = (containerOrSelector) => {
const container = normalizeContainer(containerOrSelector)
const proxy = mount(container, false, container instanceof SVGElement)
return proxy
}
return app
})
app 生成的调用过程 ensureRenderer -> createRenderer -> baseCreateRenderer -> createAppAPI ,app 是在 createAppAPI 里生成的。和 Vue2 不同, Vue2 是 new Vue() 生成 vue 实例,而 Vue3 是直接生成的 app 对象。
项目 main.js createApp 后调用了 mount 方法,在源码中对应的是 createAppAPI 方法里的 app.mount 。
mount
mount 方法关键一段 render(vnode, rootContainer, isSVG) ,这里的 render 是 createAppAPI 的第一个参数,也就是 baseCreateRenderer 方法里的 render 。
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}
patch 方法触发对比更新,由于是首次渲染,很容易定位到 patch 方法里的 processComponent 函数。进而定位到 mountComponent 方法。
mountComponent
组件的首次挂载都会调用 mountComponent 方法。
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
...
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
在 Vue2 中,mountComponent 方法会生成一个更新组件的 watcher ,Vue3 对应的方法是 setupRenderEffect 。
setupRenderEffect
顾名思义,建立渲染的副作用。这个方法会创建一个更新组件的 effect ,并调用 effect.run ,run 方法会调用 effect 实例化时候的第一个参数,也就是 componentUpdateFn ,触发组件的第一次渲染。
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {...}
// create reactive effect for rendering
const effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update),
instance.scope // track it in component's effect scope
)
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
update.id = instance.uid
update()
}
接下来一起来看 ReactiveEffect 。
依赖收集
Vue3 中没有了 watcher ,取而代之的是 effect 。这里创建的是渲染的 effect ,代码中,也可以生成监听数据的 effect 。
ReactiveEffect
目录: /packages/reactivity/src/effect.ts
ReactiveEffect 是 Vue3 的一个核心类,用于创建 effect 实例。
核心内容如下:
class ReactiveEffect<T = any> {
deps: Dep[] = [] // 记录收集过自己的 dep ,当自己删除时候,通知 dep 删除自己。
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope | null
) {
recordEffectScope(this, scope)
}
run() {...}
}
上面讲到,首次渲染的 mountComponent 方法调用 setupRenderEffect ,而 setupRenderEffect 调用 ReactiveEffect 生成了一个更新组件的 effect 。并调用了 effect.run。
run() {
...
try {
effectStack.push((activeEffect = this))
...
return this.fn()
} finally {
...
effectStack.pop()
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
...
}
看到这儿,感觉就来了,和 Vue2 Watcher 的神似。
在 Vue2 中使用 Dep.target 和 targetStack 标记需要被收集的依赖:
Dep.target = null;
var targetStack = [];
Vue3 类似,使用 activeEffect 和 effectStack 标记需要被收集的依赖:
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined
run 方法 先标记依赖,后删除依赖,中间的 this.fn() 就是收集依赖的过程。
此时的 fn 是传进来的 componentUpdateFn 方法,只看名字就知道是渲染组件,那么想要渲染必然会读取数据,例如文章开始例子中的 state.count 。由于我们的 state 是 Proxy 处理过的对象的响应式副本,所以会调用 get 方法。
getter
目录: /packages/reactivity/src/baseHandlers.ts
上面 数据处理 讲过,getter 是 mutableHandlers.get ,深入会找到 createGetter 方法。
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
...
const res = Reflect.get(target, key, receiver)
...
track(target, TrackOpTypes.GET, key)
...
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
getter 的值会通过 Reflect.get 获取并返回,还会调用 track 方法收集依赖。
const targetMap = new WeakMap<any, KeyToDepMap>()
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 = createDep()))
}
...
trackEffects(dep, eventInfo)
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = !dep.has(activeEffect!)
...
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
...
}
}
targetMap 的结构是 {target -> key -> dep} ,存放所有的响应式数据。 dep 中收集了数据相关的依赖(effect),那么数据被修改的时候,就会通知这些 effect 。
setter
目录: /packages/reactivity/src/baseHandlers.ts
与 getter 相同,也能找到 createSetter 这个方法
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
...
const result = Reflect.set(target, key, value, receiver)
...
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
return result
}
}
通过 Reflect.set 修改数据,并调用 trigger 通知依赖。
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
...
let deps: (Dep | undefined)[] = []
...
deps.push(depsMap.get(key))
...
if (deps.length === 1) {
if (deps[0]) {
triggerEffects(deps[0])
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
triggerEffects(createDep(effects))
}
}
trigger 循环调用 triggerEffects 通知依赖
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
effect.run()
}
}
triggerEffects 调用 effect.run ,run 会调用 fn 方法,并且会再次收集依赖,就形成了闭环。
与 Vue2 对比
- Vue2 初始化会把 data 数据递归处理,全部响应式处理,然后渲染时候通过 getter 收集依赖
- Vue3 初始化只会把 外层对象 调用 reactive 方法处理。当用到了对象内的数据,并且返回值是对象,继续调用 reactive 方法处理。
- 总的来说 Vue2 是 data 数据全部处理,Vue3 是用到的数据才处理。
Vue2 为什么不按需处理数据?
Todo..
总结
Vue3 使用 Proxy 代理对象, getter 会收集放在全局的依赖(effect), setter 通知收集到的依赖(effect)。
组件的首次渲染会生成一个 用于渲染组件的 effect ,并放在全局,然后渲染页面,此时页面上用到的数据在获取时会调用 getter ,把 effect 收集。
当代码修改数据时调用 setter 通知这个 effect 重新渲染页面。
方法整理:
// {target -> key -> dep} 所有的响应式副本都在这儿, target 是当前对象,当 target 不使用,会被垃圾回收
const targetMap = new WeakMap<any, KeyToDepMap>()
// 类似 Vue2 targetStack ,依赖栈,例如父子嵌套渲染时候,依赖出现递进关系
const effectStack: ReactiveEffect[] = []
// 类似 Vue2 Dep.target ,需要被收集的依赖
let activeEffect: ReactiveEffect | undefined
// 生成 effect
class ReactiveEffect {
fn() {} // 传入的回调
run() {} // 将自己放在全局 -> 执行 fn ( fn 中获取数据的会把自己收集) -> 把自己在全局删除
}
// Proxy
// getter
track() {} // 获取 dep ,调用 trackEffects 收集依赖
trackEffects() {} // 收集依赖
// setter
trigger() {} // 循环 deps 调用 triggerEffects(effect)
triggerEffects() {} // 通知依赖 调用 effect.run()