前言
在实现了 ELEMENT
、COMMENT
、TEXT
节点的挂载后,我们最后再来实现一下组件的挂载与更新
开始实现组件之前,我们需要明确 vue
中一些关于组件的基本概念:
- 组件本身是一个对象(仅考虑对象的情况,忽略函数式组件)。它必须包含一个
render
函数,该函数决定了它的渲染内容。 - 如果我们想要定义数据,那么需要通过
data
选项进行注册。data
选项应该是一个 函数,并且renturn
一个对象,对象中包含了所有的响应性数据。 - 除此之外,我们还可以定义例如 生命周期、计算属性、
watch
等对应内容。
1. 无状态组件的挂载
Vue
中通常把
状态 比作
数据 的意思。我们所谓的无状态,指的就是
无数据 的意思。
我们先定一个小目标,本小节 仅关注无状态组件的处理逻辑 。
创建以下测试实例:
<script>
const { h, render } = Vue
const component = {
render() {
return h('div', 'hello component')
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
上面的代码很简单:
- 使用
h
函数 生成组件的vnode
- 使用
render
函数将组件 挂载到dom
上
下面我们先从 vue
源码中分析它是如何处理的:
1.1 源码阅读
- 根据之前的经验,我们知道
vue
的渲染逻辑,都会从render
函数进入patch
函数,所以我们可以直接在patch
函数中进入debugger
:
- 可以看到在
patch
方法中,最终会进入到processComponent
方法去:
- 接着在
processComponent
方法中,代码最终会进入到mountComponent
方法去挂载组件,我们进入到mountComponent
中:
- 在
mountComponent
中代码会先执行createComponentInstance
方法创建instance
,这个instance
就是组件实例
const instance: ComponentInternalInstance = { ... }
- 通过以上代码最终 生成了
component
组件实例,并且把 组件实例绑定到了vnode.component
中 ,即:initialVNode.component = instance = 组件实例
,接下来我们从createComponentInstance
方法返回到mountComponent
方法中执行代码:
- 根据上图可知在
setupComponent
方法中执行了setupStatefulComponent(instance, isSSR)
,我们进入setupStatefulComponent
方法:
- 在
setupStatefulComponent
方法中最终会执行finishComponentSetup
方法,我们进入finishComponentSetup
方法:
- 可以看到,在
finishComponentSetup
方法中,最终使instance
具备了render
属性。 - 我们目前只关注渲染的逻辑,接着
setupStatefulComponent
会返回到setupComponent
,setupComponent
再返回到mountComponent
方法继续执行:
我们总结一下上图的逻辑,在
mountComponent
方法中程序接着会进入到一个 很重要 的方法:setupRenderEffect
。这个方法内部主要做了以下几件事:- 定义了一个更新函数
componentUpdateFn
- 创建了一个
ReactiveEffect
实例 - 将
update
和instance.update
都绑定到一个匿名函数,而这个函数就是用来执行上面的componentUpdateFn
函数。 - 最后执行 update 函数。'
- 定义了一个更新函数
我们现在进入 componentUpdateFn
看看里面到底执行了什么:
- 根据上图可知当前
instance.isMounted === false
表示组件没挂载。会执行patch
方法进行挂载操作,而这个patch
方法我们也很熟悉了。它是一个 打补丁 函数,我们知道对于patch
函数来说,第一个参数是n1
,第二个参数是n2
,此时的n1
为null
,当第一个函数为null
时就会去挂载n2
,而此时的n2(subTree)
又是什么呢?往上翻一下代码,我们找到了subTree
的创建:
- 根据上图我们知道了
subTree
实际上就是render
调用返回的vnode
,最终执行patch
函数将vnode
挂载到dom
上去,patch
的逻辑就不在过了,之前过很多遍了。 - 至此,我们的组件 挂载成功。
总结:
重新捋一遍整个组件的挂载过程
- 首先整个组件的挂载开始于
mountComponent
方法 - 在
mountComponent
方法的内部会通过createComponentInstance
得到一个组件的实例。组件的实例会和vnode
进行一个双向绑定的关系。
vnode.component = instance
instance.vnode = initialVNode
- 接着,代码执行
setupComponent
,在这里会初始化prop
slot
等等属性。对于我们当前测试实例而言,最重要的就是执行了setupStatefulComponent
方法为instance.render
赋值 - 接着执行
setupRenderEffect
方法,在setupRenderEffect
中创建了一个ReactiveEffect
对象,利用update
方法 触发了componentUpdateFn
方法 - 在
componentUpdateFn
方法中,根据当前的状态isMounted
,生成了subTree
。subTree
本质上就是render
函数生成的vnode
,最后通过patch
函数进行挂载
1.2 代码实现
明确好了源码的无状态组件挂载之后,那么接下来我们来进行一下对应实现。
- 在
packages/runtime-core/src/renderer.ts
的patch
方法中,创建processComponent
的触发:
else if (shapeFlag & ShapeFlags.COMPONENT) {
// 组件
processComponent(oldVNode, newVNode, container, anchor)
}
- 创建
processComponent
函数:
/**
* 组件的打补丁操作
*/
const processComponent = (oldVNode, newVNode, container, anchor) => {
if (oldVNode == null) {
// 挂载
mountComponent(newVNode, container, anchor)
}
}
- 创建
mountComponent
方法:
const mountComponent = (initialVNode, container, anchor) => {
// 生成组件实例
initialVNode.component = createComponentInstance(initialVNode)
// 浅拷贝,绑定同一块内存空间
const instance = initialVNode.component
// 标准化组件实例数据
setupComponent(instance)
// 设置组件渲染
setupRenderEffect(instance, initialVNode, container, anchor)
}
- 创建
packages/runtime-core/src/component.ts
模块,构建createComponentInstance
函数逻辑:\
let uid = 0
/**
* 创建组件实例
*/
export function createComponentInstance(vnode) {
const type = vnode.type
const instance = {
uid: uid++, // 唯一标记
vnode, // 虚拟节点
type, // 组件类型
subTree: null!, // render 函数的返回值
effect: null!, // ReactiveEffect 实例
update: null!, // update 函数,触发 effect.run
render: null // 组件内的 render 函数
}
return instance
}
- 在
packages/runtime-core/src/component.ts
模块,创建setupComponent
函数逻辑:
/**
* 规范化组件实例数据
*/
export function setupComponent(instance) {
// 为 render 赋值
const setupResult = setupStatefulComponent(instance)
return setupResult
}
function setupStatefulComponent(instance) {
finishComponentSetup(instance)
}
export function finishComponentSetup(instance) {
const Component = instance.type
instance.render = Component.render
}
- 在
packages/runtime-core/src/renderer.ts
中,创建setupRenderEffect
函数:
/**
* 设置组件渲染
*/
const setupRenderEffect = (instance, initialVNode, container, anchor) => {
// 组件挂载和更新的方法
const componentUpdateFn = () => {
// 当前处于 mounted 之前,即执行 挂载 逻辑
if (!instance.isMounted) {
// 从 render 中获取需要渲染的内容
const subTree = (instance.subTree = renderComponentRoot(instance))
// 通过 patch 对 subTree,进行打补丁。即:渲染组件
patch(null, subTree, container, anchor)
// 把组件根节点的 el,作为组件的 el
initialVNode.el = subTree.el
} else {
}
}
// 创建包含 scheduler 的 effect 实例
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queuePreFlushCb(update)
))
// 生成 update 函数
const update = (instance.update = () => effect.run())
// 触发 update 函数,本质上触发的是 componentUpdateFn
update()
}
- 创建
packages/runtime-core/src/componentRenderUtils.ts
模块,构建renderComponentRoot
函数:
import { ShapeFlags } from 'packages/shared/src/shapeFlags'
/**
* 解析 render 函数的返回值
*/
export function renderComponentRoot(instance) {
const { vnode, render } = instance
let result
try {
// 解析到状态组件
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// 获取到 result 返回值
result = normalizeVNode(render!())
}
} catch (err) {
console.error(err)
}
return result
}
/**
* 标准化 VNode
*/
export function normalizeVNode(child) {
if (typeof child === 'object') {
return cloneIfMounted(child)
}
}
/**
* clone VNode
*/
export function cloneIfMounted(child) {
return child
}
至此代码完成。
创建 packages/vue/examples/runtime/render-component.html
测试实例:
<script>
const { h, render } = Vue
const component = {
render() {
return h('div', 'hello component')
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
此时,组件渲染完成。
2 无状态组件的更新与卸载
此时我们的无状态组件挂载已经完成,接下来我们来看一下 无状态组件更新 的处理逻辑。
创建测试实例:
<script>
const { h, render } = Vue
const component = {
render() {
return h('div', 'hello component')
}
}
const vnode = h(component)
render(vnode, document.querySelector('#app'))
setTimeout(() => {
const component2 = {
render() {
return h('div', 'update component')
}
}
const vnode2 = h(component2)
render(vnode2, document.querySelector('#app'))
}, 2000);
</script>
2.1 源码阅读
在 render
中进入 debugger
:
- 第一次 进入
render
方法,执行组件挂载,这里不在复述。 - 第二次 进入
render
方法,此时是第二个 component 的挂载,即: 更新 - 同样进入
patch
,此时的参数为:
- 此时存在两个不同的
VNode
,所以if (n1 && !isSameVNodeType(n1, n2))
判断为true
,此时将执行 卸载旧的VNode
逻辑
- 执行 ·unmount(n1, parentComponent, parentSuspense, true)· ,触发 卸载逻辑
- 代码继续执行,经过
switch
,再次执行processComponent
,因为 旧的VNode
已经被卸载,所以此时n1 = null
- 代码继续执行,发现 再次触发
mountComponent
,执行 挂载操作 - 后续省略
至此,组件更新完成。
由以上代码可知:
所谓的组件更新,其实本质上就是一个 卸载、挂载 的逻辑
- 对于这样的卸载逻辑,我们之前已经完成过。
- 所以,目前我们的代码 支持 组件的更新操作。
2.2 代码实现
因为目前我们的代码 支持 组件的更新操作
所以可以直接可创建测试实例 packages/vue/examples/imooc/runtime/render-component-update.html
:
<script>
const { h, render } = Vue
const component = {
render() {
return h('div', 'hello component')
}
}
const vnode = h(component)
render(vnode, document.querySelector('#app'))
setTimeout(() => {
const component2 = {
render() {
return h('div', 'update component')
}
}
const vnode2 = h(component2)
render(vnode2, document.querySelector('#app'))
}, 2000)
</script>
测试通过
‘
3. 局部总结
那么到现在我们已经完成了 无状态组件的挂载、更新、卸载 操作。
从以上的内容中我们可以发现:
- 所谓组件渲染,本质上指的是
render
函数返回值的渲染 - 组件渲染的过程中,会生成
ReactiveEffect
实例effect
- 额外还存在一个
instance
的实例,该实例表示 组件本身,同时vnode.component
指向它 - 组件本身额外提供了很多的状态,比如:
sMounted
但是以上的内容,全部都是针对于 无状态 组件来看的。
在我们的实际开发中,组件通常是 有状态(即:存在 data
响应性数据 ) 的,那么有状态的组件和无状态组件他们之间的渲染存在什么差异呢?让我们继续来往下看。
4. 有状态的响应性组件
和之前一样,我们先创建一个 有状态的组件 测试实例,从源码上分析:
<script>
const { h, render } = Vue
const component = {
data() {
return {
msg: 'hello component'
}
},
render() {
return h('div', this.msg)
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
该组件存在一个 data
选项,data
选项对外 return
了一个包含 msg
数据的对象。然后我们可以在 render
中通过 this.msg
来访问到 msg
数据。
这样的一种包含 data
选项的组件,我们就把它叫做有状态的组件。
4.1 源码阅读
那么下面,我们对当前实例进行 debugger
操作。
剔除掉之前的重复逻辑,我们之前的关注点在 渲染,现在我们把关注点放在 data
上。直接从 mountComponent
方法开始进入 debugger
:
- 进入
mountComponent
方法,首先会通过createComponentInstance
生成instance
实例,代码继续执行:
- 接着会执行
setupComponent
,这个方法是用来初始化组件实例instance
的,我们跳过props
和slots
,程序会进入到setupStatefulComponent
方法:
- 可以看到
setupStatefulComponent
方法执行了finishComponentSetup
,进入finishComponentSetup
方法:
- 我们在第
842
行看了一个applyOptions
,很明显的名称告诉我们就是我们想要找的方法,直接进入:
- 根据上图可以看到,在
applyOptions
方法中,首先将data
和render
取了出来,还有很多我们熟悉的属性比如生命周期等等,应该可以意识到vue
会在这个方法中对他们进行一一处理。代码接着执行:
- 接着调用了通过
const data = dataOptions.call(publicThis, publicThis)
调用data
函数返回了对象,而且还将this
传给了data
,最后将data
通过reactive
转换为响应式的proxy
代理对象 - 至此
setupComponent
完成。完成之后instance
将具备data
属性,值为proxy
,被代理对象为{msg: 'hello component'}
- 代码继续执行,触发
setupRenderEffect
方法,我们知道该方法为组件的渲染方法,最终会通过renderComponentRoot
生成的subTree
(一个vnode
)patch
到dom
上。setupRenderEffect
这里的逻辑就不在多复述。
- 到这里 我们已经成功解析了
render
,把this.msg
成功替换为了hello component
- 后面的逻辑,就与 无状态组件 挂载完全相同了。
至此,代码解析完成。
总结:
由以上代码可知:
- 有状态的组件渲染,核心的点是:让
render
函数中的this.xx
得到真实数据 - 那么想要达到这个目的,我们就必须要 改变
this
的指向 - 改变的方式就是在:生成
subTree
时,通过call
方法,指定this
4.2 代码实现
明确好了有状态组件的挂载逻辑之后,我们接下里就进行对应的实现。
- 在
packages/runtime-core/src/component.ts
中,新增applyOptions
方法,为instance
赋值data
:
function applyOptions(instance: any) {
const { data: dataOptions } = instance.type
// 存在 data 选项时
if (dataOptions) {
// 触发 dataOptions 函数,拿到 data 对象
const data = dataOptions()
// 如果拿到的 data 是一个对象
if (isObject(data)) {
// 则把 data 包装成 reactiv 的响应性数据,赋值给 instance
instance.data = reactive(data)
}
}
}
- 在
finishComponentSetup
方法中,触发applyOptions
:
export function finishComponentSetup(instance) {
const Component = instance.type
instance.render = Component.render
// 改变 options 中的 this 指向
applyOptions(instance)
}
- 在
packages/runtime-core/src/componentRenderUtils.ts
中,为render
的调用,通过call
方法修改this
指向:
/**
* 解析 render 函数的返回值
*/
export function renderComponentRoot(instance) {
+ const { vnode, render, data } = instance
let result
try {
// 解析到状态组件
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// 获取到 result 返回值,如果 render 中使用了 this,则需要修改 this 指向
+ result = normalizeVNode(render!.call(data))
}
} catch (err) {
console.error(err)
}
return result
}
至此,代码完成。
我们可以创建对应测试实例 packages/vue/examples/runtime/render-comment-data.html
:
<script>
const { h, render } = Vue
const component = {
data() {
return {
msg: 'hello component'
}
},
render() {
return h('div', this.msg)
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
5. 组件生命周期的回调处理
在前面几节,我们其实已经在源码中查看到了对应的一些生命周期处理逻辑。
我们知道 vue
把生命周期叫做生命周期回调钩子,说白了就是一个:在指定时间触发的回调方法。
我们查看 packages/runtime-core/src/component.ts
中 第 213
行可以看到 ComponentInternalInstance
接口,该接口描述了组件的所有选项,其中包含:
/**
* @internal
*/
[LifecycleHooks.BEFORE_CREATE]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.CREATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.MOUNTED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.BEFORE_UPDATE]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.UPDATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.UNMOUNTED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.RENDER_TRACKED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.ACTIVATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.DEACTIVATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
以上全部都是 vue
生命周期回调钩子的选项描述,大家可以在 官方文档 中查看到详细的生命周期钩子描述。
这些生命周期全部都指向 LifecycleHooks
这个 enum
对象:
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'
}
在 LifecycleHooks
中,对生命周期的钩子进行了简化的描述,比如:created
被简写为 c
。即:c
方法触发,就意味着 created
方法被回调。
那么明确好了这个之后,我们来看一个测试实例:
<script>
const { h, render } = Vue
const component = {
data() {
return {
msg: 'hello component'
}
},
render() {
return h('div', this.msg)
},
// 组件初始化完成之后
beforeCreate() {
alert('beforeCreate')
},
// 组件实例处理完所有与状态相关的选项之后
created() {
alert('created')
},
// 组件被挂载之前
beforeMount() {
alert('beforeMount')
},
// 组件被挂载之后
mounted() {
alert('mounted')
},
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
5.1 源码阅读
根据之前的经验,我们知道,vue 对 options 选项的处理都在全部都是在位于 packages/runtime-core/src/componentOptions.ts
这个文件第 549
行的 applyOptions
方法中处理的,在执行的执行顺序为 mountComponent() -> setupComponent() -> setupStatefulComponent() -> finishComponentSetup() -> applyOptions(instance)
,我们直接跳到 applyOptions
进行调试代码:
- 可以看到代码首先会进入
callHook
方法中:
- 关于
callHook
里面的执行,上图应该讲得很清楚了,在cakkWithErrorHandling
中执行了fn
函数,也就代表此时我们的beforeCreate
钩子函数执行,执行了alert('beforeCreate')
,页面弹出弹框。 - 此时
if (options.beforeCreate)
中的callHook
代码执行完成,我们继续回到applyOptions
中:
- 我们忽略其他属性的设置,直接来到 第
745
行,可以看到此时代码触发if (created) {...}
,和刚才的beforeCreate
触发一样,此时 在组件实例处理完所有与状态相关的选项之后,触发了create
生命周期回调。 - 至此,我们在
applyOptions
方法中,触发了beforeCreate
和created
,代码继续执行~~~
- 代码接着会执行
11
个registerLifeHook
,我们先从第一个进去看,由上图可知最终会执行injectHook
方法,我们再进入这个方法看下:
- 到这已经清楚了
injectHook
方法的作用了,它的最终目的就是在当前组件的实例上的生命周期钩子上注入一个wrappedHook
函数,至于这个函数里面的逻辑我们可以先不分析,但是我们应该也能清楚它是会执行我们在beforeMounted
的代码的,后面的 10 个registerLifeHook
原理相同我们就直接跳过了。 - 至此我们当前实例整个
setupComponent
方法执行完成,接下来会执行setupRenderEffect
渲染dom
,我们再次进入分析一下,现在重点关注钩子函数的执行时期吗,我们直接来到componentUpdateFn
函数:
- 在
setupRenderEffect
方法最后调用update
触发的compoentUpdateFn
方法中,程序先是从instance
中拿出了bm
和m
也就是beforeMounted
和Mounted
两个钩子,然后执行了invokeArrayFns
方法,而invokeArrayFns
方法很简单就是循环调用了bm
数组中的函数,此时调用的函数也就是我们在第 7 步 创建的wrappedhook
,在wrappedhook
中 主要就是通过执行callWithAsyncErrorHandling
,这个方法我们在beforeCreated
时就碰到过。至此beforeMounted
生命周期函数执行完成,执行了alert('beforeMount')
,页面显示弹窗。我们接着执行代码:
- 接着程序会在
patch
方法后面之后判断m
也就是created
钩子是否存在,我们当前肯定是存在的,所以会执行queuePostRenderEffect
,而在queuePostRenderEffect
中最终执行了queuePostFlushCb
,而这个函数我们之前也是接触过的,它是一个Promise
的任务队列,最终函数会循环执行钩子函数的。最终执行了mounted
中的代码,执行alert('mounted')
,页面显示弹窗。 - 至此,代码执行完成。
总结:
由以上源码阅读可知:
整个源码可以分为两大块:
- 第一块是
beforeCreated
和created
,它俩的执行主要是在 applyOptions 中执行的,我们直接通过options.beforeCretad
或options.created
来判断是否有这两个钩子,在通过callHook
执行。 - 第二块是对于其余的
11
个生命周期,我们都是通过registerLifecycleHook
方法将这些生命周期注入到instance
里面,然后在合适的时机去触发
5.2 代码实现
明确好了源码的生命周期处理之后,那么接下来我们来实现一下对应的逻辑。
我们本小节要处理的生命周期有四个,首先我们先处理前两个 beforeCreate
和 created
,我们知道这两个回调方法是在 applyOptions
方法中回调的:
- 在
packages/runtime-core/src/component.ts
的applyOptions
方法中:
function applyOptions(instance: any) {
const {
data: dataOptions,
beforeCreate,
created,
beforeMount,
mounted
} = instance.type
// hooks
if (beforeCreate) {
callHook(beforeCreate)
}
// 存在 data 选项时
if (dataOptions) {
...
}
// hooks
if (created) {
callHook(created)
}
}
- 创建对应的
callHook
:
/**
* 触发 hooks
*/
function callHook(hook: Function) {
hook()
}
至此, beforeCreate
和 created
完成。
接下来我们来去处理 beforeMount
和 mounted
,对于这两个生命周期而言,他需要先注册,在触发。
那么首先我们先来处理注册的逻辑:
首先我们需要先创建 LifecycleHooks
- 在
packages/runtime-core/src/component.ts
中:
/**
* 生命周期钩子
*/
export const enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
MOUNTED = 'm'
}
- 在生成组件实例时,提供对应的生命周期相关选项:
/**
* 创建组件实例
*/
export function createComponentInstance(vnode) {
const type = vnode.type
const instance = {
...
+ // 生命周期相关
+ isMounted: false, // 是否挂载
+ bc: null, // beforeCreate
+ c: null, // created
+ bm: null, // beforeMount
+ m: null // mounted
}
return instance
}
- 创建
packages/runtime-core/src/apiLifecycle.ts
模块,处理对应的hooks
注册方法:
import { LifecycleHooks } from './component'
/**
* 注册 hook
*/
export function injectHook(
type: LifecycleHooks,
hook: Function,
target
): Function | undefined {
// 将 hook 注册到 组件实例中
if (target) {
target[type] = hook
return hook
}
}
/**
* 创建一个指定的 hook
* @param lifecycle 指定的 hook enum
* @returns 注册 hook 的方法
*/
export const createHook = (lifecycle: LifecycleHooks) => {
return (hook, target) => injectHook(lifecycle, hook, target)
}
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
这样我们注册 hooks
的一些基础逻辑完成。
那么下面我们就可以 applyOptions
方法中,完成对应的注册:
function applyOptions(instance: any) {
...
function registerLifecycleHook(register: Function, hook?: Function) {
register(hook, instance)
}
// 注册 hooks
registerLifecycleHook(onBeforeMount, beforeMount)
registerLifecycleHook(onMounted, mounted)
}
将 bm
和 m
注册到组件实例之后,下面就可以在 componentUpdateFn
中触发对应 hooks
了:
// 组件挂载和更新的方法
const componentUpdateFn = () => {
// 当前处于 mounted 之前,即执行 挂载 逻辑
if (!instance.isMounted) {
// 获取 hook
const { bm, m } = instance
// beforeMount hook
if (bm) {
bm()
}
// 从 render 中获取需要渲染的内容
const subTree = (instance.subTree = renderComponentRoot(instance))
// 通过 patch 对 subTree,进行打补丁。即:渲染组件
patch(null, subTree, container, anchor)
// mounted hook
if (m) {
m()
}
// 把组件根节点的 el,作为组件的 el
initialVNode.el = subTree.el
} else {
}
}
至此,生命周期逻辑处理完成。
可以创建对应测试实例 packages/vue/examples/runtime/redner-component-hook.html
:
<script>
const { h, render } = Vue
const component = {
data() {
return {
msg: 'hello component'
}
},
render() {
return h('div', this.msg)
},
// 组件初始化完成之后
beforeCreate() {
alert('beforeCreate')
},
// 组件实例处理完所有与状态相关的选项之后
created() {
alert('created')
},
// 组件被挂载之前
beforeMount() {
alert('beforeMount')
},
// 组件被挂载之后
mounted() {
alert('mounted')
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
测试成功
6. 生命回调钩子中访问响应性数据
对于我们当前的代码,还不能生命周期钩子中访问响应式数据,那么要如何解决这个问题呢?
我们从源码中分析一下:
<script>
const { h, render } = Vue
const component = {
data() {
return {
msg: 'hello component'
}
},
render() {
return h('div', this.msg)
},
// 组件实例处理完所有与状态相关的选项之后
created() {
console.log('created', this.msg)
},
// 组件被挂载之后
mounted() {
console.log('mounted', this.msg)
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
6.1 源码阅读
created
通过之前的代码我们已经知道,created
的回调是在 applyOptions
中触发的,所以我们可以直接在这里进行 debugger
:
- 进入
applyOptions
- 剔除之前相同的逻辑,代码执行
if (created) {...}
通过上他我们很容易分析 created
能获取到响应数据的原因。
mounted
对于 mounted
而言,我们知道它的 生命周期注册 是在 applyOptions
方法内的 registerLifecycleHook
方法中,我们可以直接来看一下源码中的 registerLifecycleHoo
方法:
function registerLifecycleHook(
register: Function,
hook?: Function | Function[]
) {
if (isArray(hook)) {
hook.forEach(_hook => register(_hook.bind(publicThis)))
} else if (hook) {
register((hook as Function).bind(publicThis))
}
}
该方法中的逻辑非常简单,可以看到它和 created
的处理几乎一样,都是通过 bind
方法来改变 this
指向
总结:
无论是 created
也好,还是 mounted
也好,本质上都是通过 bind
方法来修改 this
指向,以达到在回调钩子中访问响应式数据的目的。
6.2 代码实现
根据上一小节的描述,我们只需要 改变生命周期钩子的 this
指向即可
- 在
packages/runtime-core/src/component.ts
中为callHook
方法增加参数,以此来改变this
指向:
/**
* 触发 hooks
*/
function callHook(hook: Function, proxy) {
hook.bind(proxy)()
}
- 在
applyOptions
方法中为callHoo
的调用,传递第二个参数:
// hooks
if (beforeCreate) {
callHook(beforeCreate, instance.data)
}
...
// hooks
if (created) {
callHook(created, instance.data)
}
- 在
registerLifecycleHook
中,为hook
修改this
指向
function registerLifecycleHook(register: Function, hook?: Function) {
register(hook?.bind(instance.data), instance)
}
至此,代码完成。
创建对应测试实例 packages/vue/examples/runtime/redner-component-hook-data.html
:
<script>
const { h, render } = Vue
const component = {
data() {
return {
msg: 'hello component'
}
},
render() {
return h('div', this.msg)
},
// 组件实例处理完所有与状态相关的选项之后
created() {
console.log('created', this.msg)
},
// 组件被挂载之后
mounted() {
console.log('mounted', this.msg)
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
数据访问成功
7. 响应性数据改变,触发组件的响应性变化
虽然目前我们已经完成了在生命周期中访问响应性数据,但是还有个问题就是:响应性数据改变,没有触发组件发生变化。
再来看这一块内容之前,首先我们需要先来明确一些基本的概念:
组件的渲染,本质上是 render
函数返回值的渲染。
所谓响应性数据,指的是:
getter
时收集依赖setter
时触发依赖
那么根据以上概念,我们所需要做的就是:
- 在组件的数据被触发
getter
时,我们应该收集依赖。那么组件什么时候触发的getter
呢?在packages/runtime-core/src/renderer.ts
的setupRenderEffect
方法中,我们创建了一个effect
,并且把effect
的fn
指向了componentUpdateFn
函数。在该函数中,我们触发了getter
,然后得到了subTree
,然后进行渲染。所以依赖收集的函数为componentUpdateFn
。 - 在组件的数据被触发
setter
时,我们应该触发依赖。我们刚才说了,收集的依赖本质上是componentUpdateFn
函数,所以我们在触发依赖时,所触发的也应该是componentUpdateFn
函数。
明确好了以上内容之后,我们就去分析一下源码是怎么做的:
<script>
const { h, render } = Vue
const component = {
data() {
return {
msg: 'hello component'
}
},
render() {
return h('div', this.msg)
},
// 组件实例处理完所有与状态相关的选项之后
created() {
setTimeout(() => {
this.msg = '你好,世界'
}, 2000)
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
7.1 源码阅读
在 componentUpdateFn
中进行 debugger
,等待 第二次 进入 componentUpdateFn
函数(注意: 此时我们仅关注依赖触发,生命周期的触发不再关注对象,会直接跳过):
- 第二次进入
componentUpdateFn
,因为这次组件已经挂载过了,所以会执行else
,在else
中将下一次要渲染的vnode
赋值给next
,我们继续往下执行:
- 在 else 中,代码最终会执行
renderComponentRoot
, 而对于renderComponentRoot
方法,我们也很熟悉了,它内部会调用
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
同样通过 call
方法,改变 this
指向,触发 render
。然后通过 normalizeVNode
得到 vnode
,这次得到的 vnode
就是 下一次要渲染的 subTree
。接着跳出renderComponentRoot
方法继续执行代码:
- 可以看到,最终触发
patch(...)
方法,完成 更新操作 - 至此,整个 组件视图的更新完成。
总结:
所谓的组件响应性更新,本质上指的是: componentUpdateFn
的再次触发,根据新的 数据 生成新的 subTree
,再通过 path
进行 更新 操作
## 7.2 代码实现
- 在
packages/runtime-core/src/renderer.ts
的componentUpdateFn
方法中,加入如下逻辑:
// 组件挂载和更新的方法
const componentUpdateFn = () => {
// 当前处于 mounted 之前,即执行 挂载 逻辑
if (!instance.isMounted) {
...
// 修改 mounted 状态
instance.isMounted = true
} else {
let { next, vnode } = instance
if (!next) {
next = vnode
}
// 获取下一次的 subTree
const nextTree = renderComponentRoot(instance)
// 保存对应的 subTree,以便进行更新操作
const prevTree = instance.subTree
instance.subTree = nextTree
// 通过 patch 进行更新操作
patch(prevTree, nextTree, container, anchor)
// 更新 next
next.el = nextTree.el
}
}
至此,代码完成。
创建对应测试实例 packages/vue/examples/runtime/redner-component-hook-data-change.html
:
<body>
<div id="app"></div>
</body>
<script>
const { h, render } = Vue
const component = {
data() {
return {
msg: 'hello component'
}
},
render() {
return h('div', this.msg)
},
// 组件实例处理完所有与状态相关的选项之后
created() {
setTimeout(() => {
this.msg = '你好,世界'
}, 2000)
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
得到响应性的组件更新。
8. composition API ,setup 函数挂载逻辑
到现在我们已经处理好了组件非常多的概念,但是我们还知道对于 vue3
而言,提供了 composition API
,即 setup
函数的概念。
那么如果我们想要通过 setup
函数来进行一个响应性数据的挂载,那么又应该怎么做呢?
我们继续从源码中找答案:
<script>
const { reactive, h, render } = Vue
const component = {
setup() {
const obj = reactive({
name: '张三'
})
return () => h('div', obj.name)
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
在上面的代码中,我们构建了一个 setup
函数,并且在 setup
函数中 return
了一个函数,函数中返回了一个 vnode
。
上面的代码运行之后,浏览器会在一个 div
中渲染 张三
。
8.1 源码阅读
我们知道,vue
对于组件的挂载,本质上是触发 mountComponent
,在 mountComponent
中调用了 setupComponent
函数,通过此函数来对组件的选项进行标准化。
那么 setup
函数本质上就是一个 vue
组件的选项,所以对于 setup
函数处理的核心逻辑,就在 setupComponent
中。我们在这个函数内部进行 debugger
。
- 由上图我们看到了
setup
函数最终被执行了,由此得到setupResult
的值为() => h('div', obj.name)
。即:setup
函数的返回值。我们代码继续执行:
- 可以看到,先是触发了
handleSetupResult
方法, 在handleSetupResult
方法中会将setupResult
赋值给instance.render
,最后进行了finishComponentSetup
。 - 后面的逻辑就是 有状态的响应性组件挂载逻辑 的逻辑了。这里就不再详细说了。
总结:
- 对于
setup
函数的composition API
语法的组件挂载,本质上只是多了一个setup
函数的处理 - 因为
setup
函数内部,可以完成对应的 自洽 ,所以我们 无需 通过call
方法来改变this
指向,即可得到真实的render
- 得到真实的
render
之后,后面就是正常的组件挂载了
8.2 代码实现
明确好了 setup
函数的渲染逻辑之后,那么下面我们就可以进行对应的实现了。
在 packages/runtime-core/src/component.ts
模块的 setupStatefulComponent
方法中,增加 setup
判定:
function setupStatefulComponent(instance) {
const Component = instance.type
const { setup } = Component
// 存在 setup ,则直接获取 setup 函数的返回值即可
if (setup) {
const setupResult = setup()
handleSetupResult(instance, setupResult)
} else {
// 获取组件实例
finishComponentSetup(instance)
}
}
- 创建
handleSetupResult
方法:
export function handleSetupResult(instance, setupResult) {
// 存在 setupResult,并且它是一个函数,则 setupResult 就是需要渲染的 render
if (isFunction(setupResult)) {
instance.render = setupResult
}
finishComponentSetup(instance)
}
- 在
finishComponentSetup
中,如果已经存在render
,则不需要重新赋值:
export function finishComponentSetup(instance) {
const Component = instance.type
// 组件不存在 render 时,才需要重新赋值
if (!instance.render) {
instance.render = Component.render
}
// 改变 options 中的 this 指向
applyOptions(instance)
}
至此,代码完成。
创建对应测试实例 packages/vue/examples/runtime/redner-component-setup.html
:
<script>
const { reactive, h, render } = Vue
const component = {
setup() {
const obj = reactive({
name: '张三'
})
setTimeout(() => {
obj.name = '李四'
}, 2000)
return () => h('div', obj.name)
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
挂载 和 更新 都可成功
9. 总结
在本章中,我们处理了 vue
中组件对应的 挂载、更新 逻辑。
我们知道组件本质上就是一个对象(或函数),组件的渲染本质上是 render
函数返回值的渲染。
组件渲染的内部,构建了 ReactiveEffect
的实例,其目的是为了实现组件的响应性渲染。
而当我们期望在组件中访问响应性数据时,分为两种情况:
- 通过
this
访问:对于这种情况我们需要改变this
指向,改变的方式是通过call
方法或者bind
方法 - 通过
setup
访问:这种方式因为不涉及到this
指向问题,反而更加简单
当组件内部的响应性数据发生变化时,会触发 componentUpdateFn
函数,在该函数中根据 isMounted
的值的不同,进行了不同的处理。
组件的生命周期钩子,本质上只是一些方法的回调,当然,如果我们希望在生命周期钩子中通过 this
访问响应式数据,那么一样需要改变 this
指向。