[Vue] 解析 Vue 框架底层源码(下)
温馨提示:在写这篇文章的时候其实是图文并茂的,但由于图片都保存在第三方平台的图床中(notion, juejin),搬运到博客也较为麻烦一些,所以博文中就没有图片,如果对图片感兴趣的小伙伴,可以看我的掘金文章,那里有图文并茂的源码解释
目录
1. 生命周期
2. KeepAlive
3. provide | inject
4. Vue Diff
5. toRef | toRefs | proxyRefs
异步组件
代码示例
let asyncComponent = defineAsyncComponent({
loader: async () => {
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 3000)
})
return import('./test.js').then(data => data.default)
},
timeout: 2000,
delay: 1000,
loadingComponent: {
render: () => {
return h('div', 'loading…………')
}
},
errorComponent: {
render: () => {
return h('div', 'error…………')
}
}
})
createApp(asyncComponent).mount('#app')
// ./test.js
const App = {
setup() {
return () => h('div', 'App')
}
}
export default App
第一:首先调用 defineAsyncComponent 函数,并传入 option. 在 函数中初始化状态后,返回一个 defineComponent 定义的组件对象。
const {
loader,
loadingComponent,
errorComponent,
delay = 200,
timeout, // undefined = never times out
suspensible = true,
onError: userOnError
} = source
let retries = 0
const retry = () => {
retries++
pendingRequest = null
return load()
}
const load = (): Promise<ConcreteComponent> => {}
return defineComponent({
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance)
} else if (error.value && errorComponent) {
return createVNode(errorComponent as ConcreteComponent, {
error: error.value
})
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent as ConcreteComponent)
}
}
}
}) as T
第二:执行,createApp(asyncComponent).mount('#app'),执行 asyncComponent 组件对象的 setup 函数:
- 初始化 loaded, error, delay 响应式状态。
- 调用 load 函数,调用我们传入的 loader 异步函数。异步函数中异步 resolve 挂起微任务队列。继续执行同步任务。
- 同步任务继续执行,判断当前 loaded 状态为 false, 然后 delay 为 true, 白屏渲染。
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance)
} else if (error.value && errorComponent) {
return createVNode(errorComponent as ConcreteComponent, {
error: error.value
})
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent as ConcreteComponent)
}
}
- delay 一秒之后,将 delay 置为 false, delay 响应式对象触发组件更新,重新执行 render 函数,然后 delay 为 false 和 loadingComponent, loadingComponent渲染。
if (delay) {
setTimeout(() => {
delayed.value = false
}, delay)
}
timout 到第三秒的时候,这时 loaded 仍然为 false,但是触发超时,error 响应式对象触发组件更新,触发 onError 函数,errorComponent渲染
等到第三秒的时候,resolve 的微任务队列执行。拿到要渲染的组件对象赋值给 resolvedComp,然后将 loaded 置为 true, loaded 响应式对象触发组件更新, 显示 App 组件。
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance)
} else if (error.value && errorComponent) {
return createVNode(errorComponent as ConcreteComponent, {
error: error.value
})
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent as ConcreteComponent)
}
}
至此异步组件渲染完毕。
生命周期
代码示例
<div id="app"></div>
<script>
let {
createApp,
reactive,
Fragment,
toRefs,
h,
getCurrentInstance,
provide,
inject,
onMounted
} = Vue
const MyCpn = {
setup(props, { emit, slots }) {
const state = inject('state')
return () => h('div', null, [state.count])
}
}
const App = {
setup() {
const state = reactive({ count: 0 })
provide('state', state)
onMounted(() => {
setTimeout(() => {
state.count++
}, 2000)
})
},
render() {
return h(MyCpn, null)
}
}
createApp(App).mount('#app')
</script>
第一:调用 onMounted 生命周期函数,实际上是调用了 createHook 函数,并通过枚举类型,传入生命周期的 type。
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
export const onUpdated = createHook(LifecycleHooks.UPDATED)
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
export const onServerPrefetch = createHook(LifecycleHooks.SERVER_PREFETCH)
生命周期 枚举类型的 type, 就是首字母的缩写
export const enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u',
BEFORE_UNMOUNT = 'bum',
UNMOUNTED = 'um',
DEACTIVATED = 'da',
ACTIVATED = 'a',
RENDER_TRIGGERED = 'rtg',
RENDER_TRACKED = 'rtc',
ERROR_CAPTURED = 'ec',
SERVER_PREFETCH = 'sp'
}
第二:在 createHook 函数中,返回生命周期钩子函数,我们组件当中调用的生命周期函数就是 createHook 返回的这个函数。
export const createHook =
<T extends Function = () => any>(lifecycle: LifecycleHooks) =>
(hook: T, target: ComponentInternalInstance | null = currentInstance) =>
// post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
(!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
injectHook(lifecycle, (...args: unknown[]) => hook(...args), target)
第三:传入钩子 hook 和 当前组件实例,调用 injectHook 函数。
第四:在 injectHook 函数中,将生命周期函数,加入到组件实例对应的生命周期队列当中去。
{
m: [() => {}, () => {}],
u: [() => {}, () => {}]
}
export function injectHook(
type: LifecycleHooks,
hook: Function & { __weh?: Function },
target: ComponentInternalInstance | null = currentInstance,
prepend: boolean = false
): Function | undefined {
if (target) {
const hooks = target[type] || (target[type] = [])
// cache the error handling wrapper for injected hooks so the same hook
if (prepend) {
hooks.unshift(wrappedHook)
} else {
hooks.push(wrappedHook)
}
return wrappedHook
}
}
最后就是将这些生命周期的钩子函数,在组件挂载和更新的阶段进行调用了。
KeepAlive 原理
代码示例
const c1 = {
name: "c1",
setup() {
return () => {
console.log('c1 渲染')
return h('div', null, 'c1')
}
}
}
const c2 = {
name: "c2",
setup() {
return () => {
console.log('c2 渲染')
return h('div', null, 'c2')
}
}
}
render(h(KeepAlive, null, { default: () => h(c1) }), app)
setTimeout(() => {
render(h(KeepAlive, null, { default: () => h(c2) }), app)
}, 2000)
setTimeout(() => {
render(h(KeepAlive, null, { default: () => h(c1) }), app)
}, 3000)
第一:第一次 KeepAlive 组件挂载的时候,还是走正常逻辑,组件挂载可以看之前的这篇文章, [# [Vue 源码] Vue 3.2 - 组件挂载原理](https://juejin.cn/post/7210584352635125818) 这里我们直接到执行 setup 函数,及其返回的 render 函数流程。
执行 setup 函数。
const keys: Keys = new Set()
存储缓存组件的 key, 如果没有就存储 组件的 name。const cache: Cache = new Map()
存储缓存 KeepAlive 组件 name 和 subTree 的映射 (也就是组件 rener函数/vue 模板的虚拟dom)onMounted(cacheSubtree)
挂载的时候去收集 subTree 和 Name 映射。本案例是 c1 -> c1 的subTree.onUpdated(cacheSubtree)
更新的时候去收集 subTree 和 Name 映射。- 初始化 activate 和 deactivate 方法,前者是 激活 KeepAlive 组件方法,后者是 KeepAlive 卸载的失活方法。
- 从组件示例的 ctx 拿到 renderer 渲染器,上面定义了一些供 keepAlive 操作 Dom 的一些方法。这些方法是在第一次挂载 keepAlive 组件时挂载的。
// mountComponent 这些方法是在第一次挂载 keepAlive 组件时挂载的。
// inject renderer internals for keepAlive
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// 定义了一些供 keepAlive 操作 Dom 的一些方法
const {
renderer: {
p: patch,
m: move,
um: _unmount,
o: { createElement }
}
} = sharedContext
- 执行 setup 函数返回的 render 函数。
- keepAlive 组件的 children 会被编译成组件的 default 插槽。
render(h(KeepAlive, null, { default: () => h(c1) }), app)
- 渲染的时候通过 const children = slots.default(),取出默认插槽进行渲染。
- keepAlive 组件的 children 会被编译成组件的 default 插槽。
至此第一次挂载完毕,页面上显示出了 c1.并在控制台打印出 c1 渲染。
- 两秒过后,再次渲染 c2 的 KeepAlive 组件,先执行 c1 的 unmout 方法,keepAlive 组件 unmount 的时 候,会调用 deactivate 方法.
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
}
- 在 deactivate 中将 c1 的 dom 并没有被卸载/remoe, 而是缓存在了 storageContainer 这个 dom 中。
- storageContainer 就是在 KeepAlive 中创建的 div,
const storageContainer = createElement('div')
. - c1 从页面上消失,因为已经把 subTree 移到了 storageContainer 中。
- 开始渲染 c2 KeepAlive 组件, setup 函数 -> render 函数, 再将 c2 和 对应的subTree 放入缓存中去
- 执行 c2 的 render 函数,打印 c2 渲染,返回虚拟dom,patch 更新插槽之后完成渲染。
自此 c2 被渲染到了页面上。
- 过了一秒后,再次渲染 c1 的 KeepAlive 组件,先执行 c2 的 unmout 方法,keepAlive 组件 unmount 的时 候,会调用 deactivate 方法.
- 在 deactivate 中将 c2 的 dom 并没有被卸载/remoe, 而是还存在了 storageContainer 这个 dom 中。
- storageContainer 就是在 KeepAlive 中创建的 div,
const storageContainer = createElement('div')
. - c2 从页面上消失,因为已经把 subTree 移到了 storageContainer 中。
- 开始渲染 c1 KeepAlive 组件,发现 c1 已经被缓存过了, 给 KeepAlive innerChild 孩子打上COMPONENT_KEPT_ALIVE 标记 。
if (cachedVNode) { // copy over mounted state vnode.el = cachedVNode.el vnode.component = cachedVNode.component vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE }
- 由于 KeepALive 组件的第一个孩子 vnode 的 shapeFlag 打上了缓存标记,所以走 active 逻辑。
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
}
- 在 active 中将 subTree 指向的 dom 引用,从 storageContainer 缓存容器中移动了回来。并没有触发 render 函数,这时候界面上已经 有 c1 了。
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component!
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
自此 KeepAlive 缓存组件渲染完毕!
核心原理
KeepAlive 中的核心原理,就是 dom 并没有被卸载/remoe, 而是缓存在了 storageContainer 这个 dom 中。希望以下的这段代码可以给读者更多的启示和思考。
当然 KeepAlive 中的 LRU 最近最少使用算法,也值得读者去阅读和探索。
<div id="app"></div>
<div id="storage"></div>
<script>
let divItem = document.createElement('div')
divItem.innerHTML = 'app'
let obj = { item: divItem }
storage.appendChild(obj.item)
setTimeout(() => {
app.appendChild(obj.item)
}, 2000)
</script>
provide | inject 原理
代码示例
<div id="app"></div>
<script>
let {
createApp,
reactive,
Fragment,
toRefs,
h,
getCurrentInstance,
provide,
inject
} = Vue
const MyCpn = {
setup(props, { emit, slots }) {
const state = inject('state')
return () => h('div', null, [state.count])
}
}
const App = {
setup() {
const state = reactive({ count: 0 })
provide('state', state)
setTimeout(() => {
state.count++
}, 2000)
},
render() {
return h(MyCpn, null)
}
}
createApp(App).mount('#app')
</script>
第一:mountComponent 挂载组件阶段, 创建每个组件实例的时候,每个子组件实例的 provides 属性,默认继承自父组件实例的 provides 属性。
const instance: ComponentInternalInstance = {
provides: parent ? parent.provides : Object.create(appContext.provides),
}
第二:调用 Provide 函数:
- 先获取到当前组件实例的 provides, 再获取到当前父亲组件实例的 provides。
- 第一次两个 provides 相同,所以会通过 Object.create() 再 parentProvides 的基础上创建子 provides (子对象的原型链指向父对象)
- 如果在子组件中多次 provide, 就会走到 else 逻辑,在子 provide 上进行赋值操作。
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`)
}
} else {
let provides = currentInstance.provides
const parentProvides =
currentInstance.parent && currentInstance.parent.provides
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides)
}
// TS doesn't allow symbol as index type
provides[key as string] = value
}
}
每个子组件都是将所有父组件的 provides 通过 Object.create 关联起来,再生成自己的 provides。
第二:调用 inject 函数
instance.parent.provides
获取到 父组件的 Providesreturn provides[key as string]
从 provides 中找到 key 属性对应的属性值,并返回。
export function inject(
key: InjectionKey<any> | string,
defaultValue?: unknown,
treatDefaultAsFactory = false
) {
// fallback to `currentRenderingInstance` so that this can be called in
// a functional component
const instance = currentInstance || currentRenderingInstance
if (instance) {
// #2400
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
const provides =
instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
if (provides && (key as string | symbol) in provides) {
// TS doesn't allow symbol as index type
return provides[key as string]
} else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance.proxy)
: defaultValue
} else if (__DEV__) {
warn(`injection "${String(key)}" not found.`)
}
} else if (__DEV__) {
warn(`inject() can only be used inside setup() or functional components.`)
}
}
子组件消费的是父组件的 provides ,不包括本组件的 provides。
Vue Diff
概述
Vue Diff ,React Diff 算法都是同级节点的递归比较, 核心就是复用。
为什么是同级比较,笔者认为用户在操作 Dom 节点的时候,很少有将儿子节点变为父亲的情况,多数都是 向后追加,向前插入,正序,反序,这是非常常见的。如果不同级比较,去比较两棵树,是非常耗费性能的。
旧节点:a b c d e q f g
新节点:a b e c d h f g
假设 key 都是自身的字母,同一字母的 type 不变。
React Diff 流程
对于以上两个新旧节点虚拟列表(React 是新的虚拟 dom 和 旧的 fiber 列表进行比较生产新的 fiber 列表),React 的做法是:
- 遍历新节点,与旧节点进行对比
- a,b 发现 type 和 key 相同,复用老的 fiber 去创建新的 fiber 节点
- 发现 e 和 c 不相同,break 跳出循环
- 将 c 之后包括 c 的旧节点加入 map中,键:key 值:旧的 fiber 节点
- 继续遍历新节点,遍历到 e 从 map 当中找,发现有,设置 lastIndex 为 4,4为 e 在旧节点当中的索引
- c 从 map 中找有,index < lastIndex, 2 < 4, 新的 fiber 节点标记移动
- d 从 map 中找有,index < lastIndex, 3 < 3,新的 fiber 节点标记移动
- h 从 map 中找,没有,lastIndex = 5,标记新增
- f 从 map 中找,有 index > lastIndex 6 > 5, lastIndex = 6,复用呆在原地
- g 从 map 中找,有 index > lastIndex 7 > 6, lastIndex = 7,复用呆在原地
- 删除真实dom节点中要移动和删除的节点,展示变为 a b e f g
- 开始移动
- c 节点在 新结点中索引为 3, mountIndex 为 3,发现 3 在 a b e f g 有元素f。
- insertBefore 将 c 插入到 f 前,变为 a b e c f g
- d 节点在 新结点中索引为 4, mountIndex 为 3,发现 3 在 a b e c f g 有元素f。
- insertBefore 将 c 插入到 f 前,变为 a b e c d h f g。
- 完成。
Vue Diff 流程
对于以上两个新旧节点虚拟列表(Vue 是新的虚拟 dom 和 旧的虚拟 dom 列表进行比较生产新的 fiber 列表),Vue 的做法是:
- 遍历新节点,与旧节点进行对比
- async start,从前遍历,a, b 与旧节点相同,不动
- 发现一个不相同的节点,break
- async end, 再向后遍历,f, g 与旧节点相同,不动
- 发现一个不相同的节点,break
- unkoown sequence, 两边尽可能复用,然后从中间开始遍历
- 用新的元素做成一个映射表,遍历老链表去找新的
- 老 c 从 新的映射表去找,发现有,复用 patch 操作
- 老 d 从 新的映射表去找,发现有,复用 patch 操作
- 老 e 从 新的映射表去找,发现有,复用 patch 操作
- 老 q 从 新的映射表去找,发现没有,卸载 unMount 操作
- 寻找最长递增序列,后缀添加 -> 贪心,替换得到最长递增子序列的个数 -> 倒叙追溯拿到不会移动的节点列表,计算出不移动的节点的索引。在这里是 3,4 也就是 c d节点
- 继续从中间遍历的最后一个节点 h 开始遍历,索引不是 3,4 以下一个 f 为achor插入。
- 继续遍历到 d,索引是4,则跳过不动。
- 继续遍历到 c,索引是3,则跳过不动。
- 继续遍历到 e ,索引不是 3,4 以下一个 d 为achor插入。
- 完成。
toRef | toRefs | proxyRefs
代码示例
<script src="./dist/reactivity.global.js"></script>
<body>
<div id="app"></div>
<script>
let { ref, effect, reactive, toRef } = VueReactivity
let obj = reactive({ flag: false })
let state = toRef(obj, "flag")
effect(() => {
app.innerHTML = `ref is ${state.value} `
})
setTimeout(() => {
state.value = true
}, 2000)
</script>
挂载阶段
第一:调用 toRef 函数,调用 new ObjectRefImpl 创建 objectRef 对象。
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue?: T[K]
): ToRef<T[K]> {
const val = object[key]
return isRef(val)
? val
: (new ObjectRefImpl(object, key, defaultValue) as any)
}
第二:调用 ObjectRefImpl 的 constructor 方法。初始化 object, key 属性。返回 objectRef 对象。
constructor(
private readonly _object: T,
private readonly _key: K,
private readonly _defaultValue?: T[K]
) {}
第三:再 effect 函数 执行 effct.fn 函数,也就是 () => {
state.value = true
}
, 访问 state.value, 触发 objectRef 对象 value 属性的 get 操作。收集依赖到 obj 响应式对象的 flag 属性的依赖列表中去,返回属性值。
const targetMap = {
{flag :false}: {flag: [ReactiveEffect]}
}
get value() {
const val = this._object[this._key]
return val === undefined ? (this._defaultValue as T[K]) : val
}
第四:初次挂载完成,渲染完毕
更新阶段
执行第一句代码:
setTimeout(() => {
state.value = true
}, 2000)
触发 state objectRef 对象 value 属性的 setter 方法,实际上触发的是 reactive 响应式对象 obj 的 setter 操作。
set value(newVal) {
this._object[this._key] = newVal
}
再 setter 操作中,通过 Reflect.set 方法修改响应式对象的值,再通过 trigger 操作。触发 flag 属性的依赖列表。再次执行 ReactiveEffect 对象的 fn 属性,拿到最新值,进行渲染。
更新完毕
toRefs 原理
<script>
let { ref, effect, toRefs, reactive } = VueReactivity
let state = reactive({ num: 0, name: 'cyan' })
const { num, name } = toRefs(state)
effect(() => {
app.innerHTML = `ref is ${num.value} ${name.value}`
})
setInterval(() => {
num.value++
name.value = 'mike'
}, 2000)
</script>
就是将对象的每一个属性,都调用 toRef 函数,转成 objectRef 对象。这样就可以进行解构,拿到每个 objectRef 对象,且仍然具有响应式。
export function toRefs<T extends object>(object: T): ToRefs<T> {
if (__DEV__ && !isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`)
}
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
proxyRefs 原理
let { ref, effect, toRefs, reactive, proxyRefs } = VueReactivity
let state = reactive({ num: 0, name: 'cyan' })
const p = proxyRefs(toRefs(state))
effect(() => {
app.innerHTML = `ref is ${p.num} ${p.name}`
})
setInterval(() => {
p.num++
p.name = 'mike'
}, 2000)
proxyRefs 原理就是将 ref 对象做了一层 proxy 代理,访问 p.num 实际上是访问 p.num.value, 触发的仍然是 objectRef 对象 的 getter。 赋值p.num = "",实际上是 p.num.value = "", 触发的仍然是 objectRef 对象的 settter。
export function proxyRefs<T extends object>(
objectWithRefs: T
): ShallowUnwrapRef<T> {
return isReactive(objectWithRefs)
? objectWithRefs
: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
set: (target, key, value, receiver) => {
const oldValue = target[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
}
}