目录
- 备注
- 响应式数据创建
- ref 和 reactive 核心 作用
- 第一轮的 依赖收集 发生时机
- setup 阶段 去更改了 响应式数据 会发生依赖收集吗
- 派发更新
- 派发更新是什么时候 触发的?
- 扩展: setup阶段 响应式数据被修改 会触发组件更新吗
- vue 是如何根据派发更新来触发组件的更新渲染的?
- 组件副作用函数执行时 有多个响应式数据更新 是如何保证组件只会触发一次更新渲染的?
- 多余的组件依赖 是如何被清理掉的?
备注
本文中 只会涉及到 setup 主流程的 更新 , watch computed 等后面再分析
带着问题,去源码寻找答案。
响应式数据是什么时候创建的?
什么时候进行的依赖收集?
响应式数据更新后 怎么做的派发更新?
本文中 用到的测试用例
一定得 debug 跟着调试看 不然很容易绕晕
it('should support runtime template compilation', async () => {
const container = document.createElement('div')
container.classList.add('app')
const child = defineComponent({
template: `
<div><p>{{age}}---{{status?add:'hihi'}}</p></div>
`,
props:{
age:{
type: Number,
default:20
}
},
data(){
return {
add: '12',
status: true
}
},
mounted() {
this.status = false
this.add = '24'
},
})
const App = {
components:{child},
beforeMount() {
console.log('beforeMount');
},
data() {
return {
}
},
setup() {
const count = ref(1)
const age = ref('20')
const obj = reactive({name:'ws',address:'usa'})
onMounted(()=>{
obj.name = 'kd'
count.value = 5
age.value = '2'
})
return ()=>{
return h('div',[obj.name,h(child,{age:age.value})])
}
}
}
createApp(App).mount(container)
await nextTick()
expect(container.innerHTML).toBe(`0`)
})
响应式数据创建
还记得 之前文章中 初始化 setup 是在哪个阶段执行的吗?
// packages/runtime-dom/src/renderer.ts
patch 阶段 组件首次挂载时
// mountComponent 方法
1. 先创建 组件 instance 实例
2. 初始化 setup props 等属性
3. 设置并运行带副作用的渲染函数
初始化 setup 时 ,就会创建响应式数据
测试用例中 会先 执行 App 组件中 的setup 函数
setup() {
// 会创建一个 ref 响应式数据
const count = ref(1)
// 会创建一个 ref 响应式数据
const age = ref('20')
// 会创建一个 reactive 响应式数据
const obj = reactive({name:'ws',address:'usa'})
onMounted(()=>{
obj.name = 'kd'
count.value = 5
age.value = '2'
})
return ()=>{
return h('div',[obj.name,h(child,{age:age.value})])
}
}
}
ref 和 reactive 核心 作用
先说结论 :
就是 数据 驱动 视图 更新的 桥梁 。依赖收集(getter) 和 派发更新(setter) 都在里面
ref 和 reactive 差别不大(对于基本数据类型 proxy 无法做代理 ,所以vue3 自己利用 class 类中 get set 做的 代理工作 后续 依赖收集 和 派发更新 原理 和 reactive 基本一致 ) 下面 只对 reactive 做分析
- 先判断 代理对象 做类型分类
function targetTypeMap(rawType) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
- 根据 不同分类 选择 不同的 getter setter 方法
只分析下 最常见的 Object Array 代理
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
。。。。
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 省略。。。
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
会走到 baseHandlers 也就是 mutableHandlers
- mutableHandlers 中 如何 做的 数据代理工作
先看 get 核心 就是 依赖收集 track方法
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 对 ReactiveFlags 的处理部分
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
const targetIsArray = isArray(target)
if (!isReadonly) {
// 数组的特殊方法处理
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 对象 hasOwnProperty 方法处理
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
// 取值
const res = Reflect.get(target, key, receiver)
// Symbol Key 不做依赖收集
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// 进行依赖收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露
if (shallow) {
return res
}
if (isRef(res)) {
//跳过数组、整数 key 的展开
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
// 如果res 是 对象 且不是 readonly 就继续处理成 reactive
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
track 依赖收集
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
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()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
// 将 activeEffect 存入到 dep 同时将 dep[] 存入到 activeEffect 中 deps 属性 上
trackEffects(dep, eventInfo)
}
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
// 如果本轮副作用函数执行过程中已经访问并收集过,则不用再收集该依赖
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked 标识本轮已经被收集过
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode. 判断现在有没有activeEffect 有activeEffect才发生依赖收集
// activeEffect 每个组件初始化的时候会有一个activeEffect
// 这一步的作用 是为了避免多余的依赖收集 例如在setup 创建了 响应式数据 同步 访问 或者 修改 这个数据 这时候 都不会发生 依赖收集。只会在 执行render函数的时候 才发生依赖收集
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
}
reactiveEffect 核心代码
// 用于记录位于响应上下文中的effect嵌套层次数
let effectTrackDepth = 0
// 二进制位,每一位用于标识当前effect嵌套层级的依赖收集的启用状态
export left trackOpBit = 1
// 表示最大标记的位数
const maxMarkerBits = 30
// 当前活跃的 effect
let activeEffect;
export class ReactiveEffect {
// 用于标识副作用函数是否位于响应式上下文中被执行
active = true
// 副作用函数持有它所在的所有依赖集合的引用,用于从这些依赖集合删除自身
deps = []
// 指针为,用于嵌套 effect 执行后动态切换 activeEffect
parent = undefined
// ...
run() {
// 若当前 ReactiveEffect 对象脱离响应式上下文
// 那么其对应的副作用函数被执行时不会再收集依赖
if (!this.active) {
return this.fn()
}
// 缓存是否需要收集依赖
let lastShouldTrack = shouldTrack
try {
// 保存上一个 activeEffect 到当前的 parent 上
this.parent = activeEffect
// activeEffect 指向当前的 effect
activeEffect = this
// shouldTrack 置成 true
shouldTrack = true
// 左移操作符 << 将第一个操作数向左移动指定位数
// 左边超出的位数将会被清除,右边将会补零。
// trackOpBit 是基于 1 左移 effectTrackDepth 位
trackOpBit = 1 << ++effectTrackDepth
// 如果未超过最大嵌套层数,则执行 initDepMarkers
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
// 这里执行了 fn
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
// 用于对曾经跟踪过,但本次副作用函数执行时没有跟踪的依赖采取删除操作。
// 新跟踪的 和 本轮跟踪过的都会被保留
finalizeDepMarkers(this)
}
// << --effectTrackDepth 右移动 effectTrackDepth 位
trackOpBit = 1 << --effectTrackDepth
// 返回上个 activeEffect
activeEffect = this.parent
// 返回上个 shouldTrack
shouldTrack = lastShouldTrack
// 情况本次的 parent 指向
this.parent = undefined
}
}
}
说明 :
depsMap 中 effect[] 用于 每次 派发更新时候 去执行 effect 数组中的 reactiveEffect (实际调用 reactiveEffect 实例的 run 方法)
reactiveEffect 中 会在执行 run 方法的时候 给 initDepMarkers 方法来 给 deps 数组中每个对象 添加 w 属性 表示 已经收集处理 在 依赖收集中 track----> trackEffects 会给 depsMap 中 dep(这个 dep 和 effect 实例的 deps 中 每一个对象 相对应) 赋值 n (表示 是 新收集的)
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
if (wasTracked(dep) && !newTracked(dep)) {
// dep 类型是 set<ReativeEffect>
dep.delete(effect)
} else {
deps[ptr++] = dep
}
// clear bits
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
deps.length = ptr
}
}
当 effect.run() 中 注册的fn 函数执行完后 会调用 finalizeDepMarkers 去 删除掉 这一轮 dep 没有被收集到的 effect 避免 多余的 触发更新逻辑
那测试用例来说明
第一轮的 依赖收集 发生时机
在App 组件 初始化 副作用函数, 会先创建 reactiveEffect 并挂载到 app.instance
app 会主动触发 instance.update() 发生第一次 组件挂载 。
之前章节 说明过 组件首次挂载流程
实际调用的是 reactiveEffect.run ----> 执行componentUpdateFn —>render(生成subtree)----> patch ----> processElement
在render 过程中 使用到的响应式数据 就发生依赖收集 。
这时候 app 组件的 这一轮的依赖收集完成 使用到了 obj 和 age
这时候进行app 组件 processElement 由于存在子组件child 执行 mountChild ----> patch —> child 组件的processComponent child组件 也会和 app 组件 一样 去 初始化 instance 创建 ReactiveEffect 触发update 执行属于 child 的 componentUpdateFn 再 执行 child组件的 render 函数 child组件发生依赖收集
age status add
本轮的依赖收集全部完成。
总结:
组件的首次依赖收集 发生在 render阶段 顺序是 父组件 setup---->父组件 render ---->子组件 setup
----> 子组件render
setup 阶段 去更改了 响应式数据 会发生依赖收集吗
例如:
setup(){
const age = ref(20)
// 这里发生了访问操作
const temp = age.value
return ()=>{
return h('div',[age.value])
}
}
这时候会触发响应式数据的 get 操作
但是由于 没有 activeEffect(这时候 组件还没开始设置副作用函数(SetupRenderEffectFn)所以没有activeEffect) 所以不会发生依赖收集
扩展:
setup(){
const age = ref(20)
setTimeout(()=>{
// 这里发生了访问操作
console.log(age.value);
})
return ()=>{
return h('div',[age.value])
}
}
这时候 也会触发响应式数据的 get 操作 ,也是没有activeEffect(组件已经完成 effect.run 方法了,这时候 activeEffect 已经被置为空) 所以也不会发生依赖收集
后续:
在setup函数之后的生命周期(如mounted、updated等钩子函数)中访问响应式数据会触发依赖收集 (后面再分析)
派发更新
派发更新是什么时候 触发的?
上面 组件 挂载完成后,我在mouted 生命周期钩子里面 写的修改响应式数据操作,会触发 setter 下面看看 reactive 的 setter 源码
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 。。。 省略部分逻辑
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 如果target是原型链上的东西,不要触发
if (target === toRaw(receiver)) {
if (!hadKey) {
// 新增操作
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 更新操作
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 根据 target 查到对应的 depsMap
const depsMap = targetMap.get(target)
// 不存在depsMap 不触发更新
if (!depsMap) {
// never been tracked
return
}
// 用于 暂存 effect
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newLength) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
// 最终处理 在这里
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
// 下面操作 是为了 去重 保证相同的effect 只会有一个
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
// 最终会执行 scheduler 是在 初始化的时候 创建的
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
// 在SetupRenderEffectFn 阶段中 create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),// 这个就是 scheduler
instance.scope // track it in component's effect scope
))
总结:当响应式数据被更新 且 对应的 depsMap 不为空 就会触发 组件更新(如何更新 则下个问题给出答案)
扩展: setup阶段 响应式数据被修改 会触发组件更新吗
setup(){
const age = ref(20)
// 修改操作
age.value = 10
return ()=>{
return h('div',[age.value])
}
}
会触发 setter 操作 由于 depsMap 为空 所以不会发生派发更新
vue 是如何根据派发更新来触发组件的更新渲染的?
派发更新的核心就是 触发 effect.scheduler(常规的组件写法 就是 会给activeEffect 创建 scheduler)
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update), // effect.schedule
instance.scope // track it in component's effect scope
))
分析下 queueJob
export function queueJob(job: SchedulerJob) {
// the dedupe search uses the startIndex argument of Array.includes() 确保不会重复设置 schedule
// by default the search index includes the current job that is being run 默认包括正在运行的 schedule
// so it cannot recursively trigger itself again. 避免递归触发自身再次运行
// if the job is a watch() callback, the search will start with a +1 index to 运行在watch 中 重复运行
// allow it recursively trigger itself - it is the user's responsibility to
// 确保它不会陷入无限循环
// 去重判断
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
//添加到队列尾部
if (job.id == null) {
queue.push(job)
} else {
// 按照 job id 自增的顺序添加 (一般父组件的id 要小于子组件 保证 父组件永远先于子组件触发更新)
// 这个id 是由 instance.uid 决定 就是在初始化组件实例 确定( 具体代码 runtime-core/src/component) 先初始化的 uid(每次创建组件实例 全局 uid会加1) 会小,
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}
// 通过promise.then 创建 微任务(去执行flushjob)
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
function flushJobs(seen?: CountMap) {
// 是否正在等待执行
isFlushPending = false
// 正在执行
isFlushing = true
// 在更新前,重新排序好更新队列 queue 的顺序
// 这确保了:
// 1. 组件都是从父组件向子组件进行更新的。(因为父组件都在子组件之前创建的
// 所以子组件的渲染的 effect 的优先级比较低)
// 2. 如果父组件在更新前卸载了组件,这次更新将会被跳过。
queue.sort(comparator)
try {
// 遍历主任务队列,批量执行更新任务
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue
}
// 这个 job 就是 effect.run
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
// 队列任务执行完,重置队列索引
flushIndex = 0
// 清空队列
queue.length = 0
// 执行后置队列任务
flushPostFlushCbs(seen)
// 重置队列执行状态
isFlushing = false
// 重置当前微任务为 Null
currentFlushPromise = null
// 如果主任务队列、后置任务队列还有没被清空,就继续递归执行
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
总结:在组件mounted 触发的派发更新 会被收集到 一个微任务执行任务队列中 ,在主流程宏任务 执行完后 就会 去执行 微任务任务队列 开始 触发 执行 job (effect.run — > updateComponentFn)
组件副作用函数执行时 有多个响应式数据更新 是如何保证组件只会触发一次更新渲染的?
有了 上面源码的分析 我们已经可以得出答案 为啥 在一个组件的 副作用函数执行时 多个响应式数据更新 只会触发一次 组件更新
演示代码:
setup(){
const num1 = ref(20)
const num2 = ref(10)
onMounted(()=>{
num1.value = 40
num1.value = 50
num2.value = 100
})
return ()=>{
return h('div',[num1.value+num2.value])
}
}
在 onMounted 中 更新了三次 触发三次 triggerEffect 会有三次 往微任务 放入 update 操作,由于 传入的 job.id 都是同一个 所以 在更新队列中 只会被创建一个更新任务 组件也只会被更新一次
多余的组件依赖 是如何被清理掉的?
组件 再每次 渲染后 会 去 清理 后续没被收集的 effect (对应的是 每个响应式数据 对应的dep(set) 中的 reactiveEffect )
例子:
const child = defineComponent({
template: `
<div><p>{{age}}---{{status?add:'hihi'}}</p></div>
`,
props:{
age:{
type: Number,
default:20
}
},
data(){
return {
add: '12',
status: true
}
},
mounted() {
this.status = false
this.add = '24'
},
})
// mounted 阶段 改变了 status 触发了 组件更新 重新 render 的 时候 会发生新的一轮依赖收集
// 之前 组件 是有两个 dep 一个 属于 status 一个属于 add 但是,由于新的依赖收集 add 不会被用到 所以 在 effect.run 执行完 后 add 的 dep 会被清除掉 是根据 dep 赋值的 w 和 n 属性 去比较