第十章:runtime 运行时 - 组件的设计原理与渲染方案
01:前言
在本章中,我们将要关注于 component
组件的渲染逻辑。对于组件而言,本身就比较复杂所以我们单独拿出来一章来进行讲解。
在学习本章之前,我们需要需要先来回忆一下在学习 h
函数时,我们所学习到的内容。
组件本身是一个对象(仅考虑对象的情况,忽略函数式组件)。它必须包含一个
render
函数,该函数决定了它的渲染内容。如果我们想要定义数据,那么需要通过
data
选项进行注册。data
选项应该是一个 函数,并且renturn
一个对象,对象中包含了所有的响应性数据。除此之外,我们还可以定义例如 生命周期、计算属性、
watch
等对应内容。
以上是关于组件的一些基本概念,这些是需要大家首先能够明确的。
组件的处理非常复杂,所以我们依然会采用和之前一样的标准:没有使用就当做不存在 、最少代码的实现逻辑 。来实现我们的组件功能。
02:源码阅读:无状态基础组件挂载逻辑
Vue
中通常把 状态 比作 数据 的意思。我们所谓的无状态,指的就是 无数据 的意思。
我们先来定一个目标:本小节我们 仅关注无状态基础组件挂载逻辑,而忽略掉其他所有。
基于以上目标我们创建对应测试实例 packages/vue/examples/imooc/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>
基于以上代码我们知道,vue
的渲染逻辑,都会从 render
函数,进入 patch
函数,所以我们可以直接在 patch
函数中进入 debugger
:
进入
patch
函数触发
switch
,执行if (shapeFlag & ShapeFlags.COMPONENT)
,触发processComponent
方法。该方法即为 组件渲染 方法:进入
processComponent
,此时各参数为:该函数内部逻辑分为三块:
Keep alive
- 组件挂载
- 组件更新
我们当前处于 组件挂载 状态,所以代码会进入
mountComponent
方法进入
mountComponent
方法,此时各参数为:代码执行:
jsconst instance: ComponentInternalInstance = compatMountInstance || (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense ))
该代码通过
createComponentInstance
方法生成了instance
实例,我们来看一下createComponentInstance
方法:进入
createComponentInstance
方法该方法中,最重要的内容就是生成了
instance
实例:jsconst instance: ComponentInternalInstance = { ... }
instance 实例就是 component 组件实例
通过以上代码,我们 生成了
component
组件实例,并且把 组件实例绑定到了vnode.component
中 *,即:*initialVNode.component = instance = 组件实例
执行
setupComponent
,该方法主要为了初始化组件的各个数据,比如props、slot、render
进入
setupComponent
方法,仅关注render
执行
setupStatefulComponent(instance, isSSR)
进入
finishComponentSetup
,因为我们当前没有setup
函数所以会执行
finishComponentSetup
进入
finishComponentSetup
:查看当前
instance
,因为不存在render
,所以我们需要为instance
的render
赋值执行:
jsinstance.render = (Component.render || NOOP) as InternalRenderFunction
至此
instance
组件实例,具备render
属性
执行
setupRenderEffect
方法,这个方法 非常重要,我们进入来看一下:进入
setupRenderEffect
方法,此时参数为:首先:创建
componentUpdateFn
函数。- 这个函数因为现在没有执行,所以我们先不需要去管它。但是我们需要知道:我们创建了一个函数
componentUpdateFn
- 这个函数因为现在没有执行,所以我们先不需要去管它。但是我们需要知道:我们创建了一个函数
第二段:创建了
ReactiveEffect
实例。ReactiveEffect
我们应该是了解的,它可以帮助我们生成一个 响应性的effect
实例。- 当执行
run
时,会触发fn
。即:第一个参数 - 提供了
scheduler
调度器的功能。
- 当执行
明确好了以上内容,我们来看这段代码:
jsconst effect = (instance.effect = new ReactiveEffect( componentUpdateFn, () => queueJob(update), instance.scope // track it in component's effect scope ))
- 在这段代码中,我们把
componentUpdateFn
作为第一个参数传入,它将承担fn
的作用。 - 第二个参数:是一个匿名函数,它将承担
scheduler
调度器的功能。- 其中的
queueJob
方法,我们是遇见过的,它是packages/runtime-core/src/scheduler.ts
中的函数,是一个基于Promise.resolve()
的 微任务队列处理 的函数,因为通过Set
构建的队列,所以具备去重的能力。 - 那么
update
是什么呢?我们来看下一行代码
- 其中的
- 在这段代码中,我们把
第三段:创建
update
对象:const update: SchedulerJob = (instance.update = () => effect.run())
- 通过以上代码可以看出:我们把
update
和instance.update
绑定到了同一块内存空间。 - 它们都指向一块函数,即
() => effect.run()
,而run
函数的触发,其实是fn
的触发。而fn
又是componentUpdateFn
。 - 所以,通过以上的代码我们也知道:当
update
函数被触发时,其实触发的是componentUpdateFn
函数。
- 通过以上代码可以看出:我们把
第四段:触发
update()
函数。根据刚才所说,
udpate
的触发,标志着componentUpdateFn
的触发。所以此时代码会 进入
componentUpdateFn
函数。进入
componentUpdateFn
函数。观察函数代码可以发现,整个
componentUpdateFn
的代码被分成了两部分:if(!instance.isMounted)
else {}
我们知道在
vue
中存在一个生命周期钩子 mounted ,根据这个可以理解为:instance.isMounted === false
时:表示 组件挂载前instance.isMounted === true
时:表示 组件挂载后
我们此时处于
instance.isMounted === false
,所以为 挂载前 状态。接下来要进行的就是 挂载逻辑忽略 与挂载无关 的逻辑之后,代码最终执行:
jspatch(...)
对于
patch
函数,我们应该是熟悉的,它是一个 打补丁 函数- 我们知道
render
函数其实就是触发了patch
来完成的渲染。 - 对于
patch
函数我们知道,它的第二个参数表示为 新节点。即:要渲染的内容。 - 那么大家想一下对于当前测试实例的组件而言,它要渲染的内容是什么呢?
- 是不是就是 组件
render
函数的返回值啊
- 是不是就是 组件
- 那么在这里 第二个参数为
subTree
。那么我们来看看这个subTree
是什么,能不能验证我们的猜想。
- 我们知道
subTree
变量的创建在patch
函数上面:jsconst subTree = (instance.subTree = renderComponentRoot(instance))
subTree
通过renderComponentRoot
方法创建。我们再次
debugger
,进入这个方法:进入
renderComponentRoot
方法- 进入之后可以发现,这个函数相当复杂。但是不用担心,记住我们的目标:我们只关注组件挂载 相关的。
- 根据我们刚才的猜想(6-4),我们知道:组件挂载本质上是
render
函数返回值的挂载,所以我们只关心render
函数。
代码首先创建了两个变量:
render
:这是从instance
组件实例中结构出来的。result
:这是该函数的返回值,即:subTree
代码执行:
jsresult = normalizeVNode( render!.call( proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx ) )
在这个代码中,我们触发了两个函数:
render!.call
:对于 call 函数大家应该是比较熟悉的,它的作用是改变
this
指向。- 它存在一个返回值,就是:
this
值和参数调用该函数的返回值。即:render
函数的返回值 - 那么代入到我们的测试实例代码,这里的返回值为:
h('div', 'hello component')
- 它存在一个返回值,就是:
normalizeVNode
:明确好了参数之后,我们进入normalizeVNode
函数进入
normalizeVNode
函数,此时它的参数为:jsh('div', 'hello component')
代码执行
cloneIfMounted(child)
:进入
cloneIfMounted
函数:jsreturn child.el === null || child.memo ? child : cloneVNode(child)
可以看到触发了
cloneVNode
:进入
cloneVNode
从内部代码可以看到,
cloneVNode
内就是创建了一个新的vnode
针对于我们当前的 挂载 场景,
cloned
和child
并 没有区别
那么根据以上描述,
normalizeVNode
最终的返回值其实就是h('div', 'hello component')
。即:result
=h('div', 'hello component')
那么到这里,整个
renderComponentRoot
方法中和渲染相关的逻辑就已经全部完成了,最终返回值即为:h('div', 'hello component')
那么到这里我们就知道了,
subTree = renderComponentRoot()
,即:subTree = h('div', 'hello component')
明确好了
subTree
之后,我们再看一下patch(...)
函数,并比较清晰了。等同于我们触发了:jspatch( null, h('div', 'hello component'), container, null )
即:组件
render
返回值的渲染
至此,我们明确好了整个组件挂载逻辑
由以上代码可知:
- 组件的挂载主要通过
mountComponent
完成 - 内部通过
createComponentInstance
生成了组件实例 - 通过
ReactiveEffect
生成了effect
实例,并支持微任务队列的调度器 - 在
effect.run
执行时,触发了组件的挂载 - 组件挂载本质上是
- 拿到
render
函数返回值 - 通过
patch
方法进行挂载操作
- 拿到
03:框架实现:完成无状态基础组件的挂载逻辑
明确好了源码的无状态组件挂载之后,那么接下来我们来进行一下对应实现。
在
packages/runtime-core/src/renderer.ts
的patch
方法中,创建processComponent
的触发:jselse if (shapeFlag & ShapeFlags.COMPONENT) { // 组件 processComponent(oldVNode, newVNode, container, anchor) }
创建
processComponent
函数:js/** * 组件的打补丁操作 */ const processComponent = (oldVNode, newVNode, container, anchor) => { if (oldVNode == null) { // 挂载 mountComponent(newVNode, container, anchor) } }
创建
mountComponent
方法:jsconst 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
函数逻辑:jslet 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
函数逻辑:js/** * 规范化组件实例数据 */ 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
函数:js/** * 设置组件渲染 */ 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
函数:jsimport { 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>
此时,组件渲染完成。
04:源码阅读:无状态基础组件更新逻辑
此时我们的无状态组件挂载已经完成,接下来我们来看一下 无状态组件更新 的处理逻辑。
创建如下测视案例 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>
在 render
中进入 debugger
:
第一次 进入
render
,执行 组件挂载 逻辑- 第一次 触发
patch
函数,执行组件挂载- 进入
patch
函数 - 因为当前是组件挂载,所以会触发
processComponent
方法- 进入
processComponent
- 触发
mountComponent
- 进入
mountComponent
- 生成组件实例
instance
和initialVNode.component
- 执行
setupComponent(instance)
,为instance.render
赋值 - 执行
setupRenderEffect
方法- 进入
setupRenderEffect
方法 - 生成
effect
实例,绑定fn
为componentUpdateFn
- 创建
update
,绑定到到effect.run
- 执行
update
方法,从而触发componentUpdateFn
- 进入
componentUpdateFn
- 通过
renderComponentRoot
方法,触发render
拿到subTree
- 通过
patch
方法进行挂载
- 进入
- 进入
- 进入
- 进入
- 进入
- 第二次 触发
patch
,此时为component
的render
渲染- 因为
render
为ELEMENT
的渲染操作 - 所以会触发
processElement
- ...
- 因为
- 第一次 触发
此时第一次
component
的挂载操作完成延迟两秒之后,再次进入
render
,此时是第二个component
的挂载,即: 更新同样进入
patch
,此时的参数为:此时存在两个不同的
VNode
,所以if (n1 && !isSameVNodeType(n1, n2))
判断为true
,此时将执行 卸载旧的VNode
逻辑执行
unmount(n1, parentComponent, parentSuspense, true)
,触发 卸载逻辑代码继续执行,经过
switch
,再次执行processComponent
,因为 旧的VNode
已经被卸载,所以此时n1 = null
进入
processComponent
方法,此时的参数为:代码继续执行,发现 再次触发
mountComponent
,执行 挂载操作后续省略…
至此,组件更新完成。
由以上代码可知:
- 所谓的组件更新,其实本质上就是一个 卸载、挂载 的逻辑
- 对于这样的卸载逻辑,我们之前已经完成过。
- 所以,目前我们的代码 支持 组件的更新操作。
可以在 vue-next-mini
中 直接通过测试实例进行测试 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', '你好,世界')
}
}
const vnode2 = h(component2)
// 挂载
render(vnode2, document.querySelector('#app'))
}, 2000);
</script>
05:局部总结:无状态组件的挂载、更新、卸载总结
那么到现在我们已经完成了 无状态组件的挂载、更新、卸载 操作。
从以上的内容中我们可以发现:
- 所谓组件渲染,本质上指的是
render
函数返回值的渲染 - 组件渲染的过程中,会生成
ReactiveEffect
实例effect
- 额外还存在一个
instance
的实例,该实例表示 组件本身,同时vnode.component
指向它 - 组件本身额外提供了很多的状态,比如:
isMounted
但是以上的内容,全部都是针对于 无状态 组件来看的。
在我们的实际开发中,组件通常是 有状态(即:存在 data
响应性数据 ) 的,那么有状态的组件和无状态组件他们之间的渲染存在什么差异呢?让我们继续来往下看。
06:源码阅读:有状态的响应性组件挂载逻辑
和之前一样,我们先创建一个 有状态的组件 packages/vue/examples/imooc/runtime/
render-component-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>
该组件存在一个 data
选项,data
选项对外 return
了一个包含 msg
数据的对象。然后我们可以在 render
中通过 this.msg
来访问到 msg
数据。
这样的一种包含 data
选项的组件,我们就把它叫做有状态的组件。
那么下面,我们对当前实例进行 debugger
操作。
剔除掉之前的重复逻辑,我们从 mountComponent
方法开始进入 debugger
:
进入
mountComponent
方法,此时的参数为:通过
createComponentInstance
方法,生成instance
实例代码执行
setupComponent
方法,我们知道这个方法是用来初始化组件实例instance
的,我们进入到这个方法来看一下进入
setupComponent
方法执行
const isStateful = isStatefulComponent(instance)
,判断当前是否是一个有状态的组件。那么它是怎么进行判定的呢?进入
isStatefulComponent
方法该方法判断的逻辑比较简单:
jsreturn instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
即:直接通过
shapeFlag
进行 位与运算 即可
因为我们知道当前是有状态的,此时得到 isStateful = 4
跳过
props
和slots
因为当前
isStateful = 4
,所以会执行setupStatefulComponent
方法进入
setupStatefulComponent
方法执行
const Component = instance.type
,得到Component
实例为:js{ data() { return { msg: 'hello component' } }, render() { return h('div', this.msg) } }
执行
const { setup } = Component
,从上面的Component
可以看出,我们并不存在setup
函数进入
if
判断逻辑,将执行else
操作执行
finishComponentSetup(instance, isSSR)
进入
finishComponentSetup
函数同样执行
const Component = instance.type
,得到Component
实例。执行
instance.render = (Component.render || NOOP)
,为组件实例的render
赋值代码继续执行,触发
applyOptions
方法对
options
进行解构,解构之后,得到两个关键属性:dataOptions
:render:
因为
dataOptions
存在,所以if (dataOptions)
,被判定为true
,处理data
相关逻辑- 执行
const data = dataOptions.call(publicThis, publicThis)
方法- 我们知道 call 方法会改变
this
指向,即把dataOptions
中的this
,修改为publicThis
- 而
dataOptions
的值,我们已经知道了(见4-4-1-1
)
- 我们知道 call 方法会改变
- 所以此时的
data
值为{msg: 'hello component'}
。即:data
函数返回值 - 因为
data
当前是一个对象,所以会执行instance.data = reactive(data)
- 即:通过
reactive
方法,构建data
为proxy
实例 - 此时
instance.data
的值为proxy
实例,它的被代理对象为{msg: 'hello component'}
- 即:通过
- 执行
至此
setupComponent
完成。完成之后instance
将具备data
属性,值为proxy
,被代理对象为{msg: 'hello component'}
代码继续执行,触发
setupRenderEffect
方法,我们知道该方法为组件的渲染方法进入
setupRenderEffect
方法创建
ReactiveEffect
实例effect
最后触发
update
,我们知道update
的触发,本质上是componentUpdateFn
的触发。所以,此时代码会进入
componentUpdateFn
进入
componentUpdateFn
执行
const subTree = (instance.subTree = renderComponentRoot(instance))
,获取subTree
我们知道此时
subTree
即为 真实渲染的节点那么
render
函数的值为:所以真实渲染节点时,我们必须要把
this.msg
替换为hello component
那么这一步怎么做的呢?
进入
renderComponentRoot
方法,我们来一探究竟:进入
renderComponentRoot
方法,此时instance
中:data
的值为:render
的值为:
代码执行:
jsresult = normalizeVNode( render!.call( proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx ) )
这个代码我们之前是见过的,大家应该眼熟。
在这里使用了一个
call
方法,对于call
现在大家应该已经熟悉了:它会改变this
指向那么我们期望
this
指向改变为什么呢?- 我们期望
this.msg
变为hello component
- 那么
this
的指向是不是就应该为data
?
- 我们期望
所以,该代码执行完成之后,
result
的值为:至此,我们已经成功解析了
render
,把this.msg
成功替换为了hello component
后面的逻辑,就与 无状态组件 挂载完全相同了。
至此,代码解析完成。
由以上代码可知:
- 有状态的组件渲染,核心的点是:让
render
函数中的this.xx
得到真实数据 - 那么想要达到这个目的,我们就必须要 改变
this
的指向 - 改变的方式就是在:生成
subTree
时,通过call
方法,指定this
07:框架实现:有状态的响应性组件挂载逻辑
那么明确好了有状态组件的挂载逻辑之后,我们接下里就进行对应的实现。
在
packages/runtime-core/src/component.ts
中,新增applyOptions
方法,为instance
赋值data
jsfunction 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
:jsexport function finishComponentSetup(instance) { const Component = instance.type instance.render = Component.render // 改变 options 中的 this 指向 applyOptions(instance) }
在
packages/runtime-core/src/componentRenderUtils.ts
中,为render
的调用,通过call
方法修改this
指向:js/** * 解析 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>
可以发现,渲染成功。
08:源码阅读:组件生命周期回调处理逻辑
在前面我们查看《有状态的响应性组件挂载逻辑》时,其实已经在源码中查看到了对应的一些生命周期处理逻辑。
我们知道 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
方法被回调。
那么明确好了这个之后,我们来看一个测试实例 packages/vue/examples/imooc/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>
我们知道对于组件的挂载其实会触发 mountComponent
方法,所以本次,我们直接从该方法进行 debugger
,注意: 本次我们仅关心生命周期回调的逻辑:
进入
mountComponent
方法触发
setupComponent(instance)
方法进入
setupComponent(instance)
方法触发
setupStatefulComponent
方法进入
setupStatefulComponent
方法触发
finishComponentSetup
方法进入
finishComponentSetup
方法触发
applyOptions(instance)
方法进入
applyOptions
方法执行
if (options.beforeCreate)
我们知道
beforeCreate
是生命周期回调钩子,我们当前是存在这个回调钩子的所以接下来会执行
callHook(options.beforeCreate, instance, LifecycleHooks.BEFORE_CREATE)
,我们进入到该方法来看一下进入
callHook
,此时的参数为:- 在参数中,我们可以很清楚的看到
hook
的值就是我们写入到beforeCreate
函数
- 在参数中,我们可以很清楚的看到
接下来触发
callWithAsyncErrorHandling
- 对于该方法我们是熟悉的,它本质上就是:通过
try...catch
捕获函数执行 的一个方法 - 所以我们可以直接理解为 在组件初始化完成之后,触发了
beforeCreate
方法
- 对于该方法我们是熟悉的,它本质上就是:通过
代码继续向下进行,此时触发了
beforeCreate
,进行alert
打印接下来代码触发
if (created) {...}
- 和刚才的
beforeCreate
触发一样 - 此时 在组件实例处理完所有与状态相关的选项之后,触发了
create
生命周期回调
- 和刚才的
至此,我们在
applyOptions
方法中,触发了beforeCreate
和created
代码继续执行~~~
触发
registerLifecycleHook(onBeforeMount, beforeMount)
方法进入
registerLifecycleHook
方法,此时传入的hook
的值为:执行
register((hook as Function).bind(publicThis))
这段代码可以分成两块来看:
(hook as Function).bind(publicThis)
:我们知道对于 bind 方法而言,它本身是可以改变this
指向,并且返回一个新的函数register(新的函数)
:该方法从名字来看是注册的意思。那么我们进入这个方法,看看它内部做了什么事情:进入
register
进入之后可以发现,它本质上是触发了
createHook
方法进入
createHook
方法内部触发了
injectHook
方法- 进入
injectHook
方法,此时各参数的值为:
. 执行
const hooks = target[type] || (target[type] = [])
方法,即完成bm
初始化代码执行
hooks.push(wrappedHook)
此时
wrappedHook
的值为它内部比较复杂,做了什么我们也暂时不需要去管
我们只需要知道,它被放入到了对应的
hooks
中即可
- 进入
当
registerLifecycleHook(onBeforeMount, beforeMount)
方法执行完成之后,我们查看instance
的值,可以发现此时instance
中已经存在bm
属性(即:beforeCreate
)后面的代码相同,不再意义关注。总之:
registerLifecycleHook
方法执行完成之后,组件中将具备对应回调钩子。
至此
setupComponent
全部执行完成接下来,需要触发的是
beforeMount
和mounted
这两个与 挂载 相关的回调方法,在
componentUpdateFn
方法被回调之后触发- 进入
componentUpdateFn
方法 - 代码执行
const { bm, m, parent } = instance
- 根据
LifecycleHooks
的值我们知道bm
代表beforeMount
m
代表mounted
- 根据
- 执行
if (bm) {...}
- 存在
bm
选项,进入if
判断 - 执行
invokeArrayFns
方法- 进入
invokeArrayFns
方法 - 这里的代码非常简单,就是一个
for
循环触发beforeMount
方法。 - 至此:
beforeMount
被触发,打印对应alert
- 进入
- 存在
- 代码继续执行,在
patch
方法被触发之后,执行if (m){ ... }
- 执行
queuePostRenderEffect
方法,- 进入
queuePostRenderEffect
方法 - 我们当前不存在
queuePostFlushCb
,所以会直接触发queuePostFlushCb
- 这个方法我们是熟悉的,它就是通过 微任务队列 完成的一个循环触发
- 至此:
mounted
方法被触发,打印对应alert
- 进入
- 执行
- 进入
至此:
beforeMount
和mounted
被触发。
由以上代码可知:
- 对于
beforeCreate
和created
而言,他们的触发主要是在applyOptions
中触发,它们的触发非常简单,直接通过try...catch
触发即可。 - 而对于
beforeMount
和mounted
的触发相对比较复杂,分为两步:- 注册:
registerLifecycleHook
- 触发:
bm
||m
- 注册:
源码的处理逻辑颇为复杂,主要是因为源码需要应对足够多的边缘情况。如果我们期望自己进行实现,就不需要这么复杂了,我们只需要保证在合适的时机,触发对应的回调即可。
09:框架实现:组件生命周期回调处理逻辑
明确好了源码的生命周期处理之后,那么接下来我们来实现一下对应的逻辑。
我们本小节要处理的生命周期有四个,首先我们先处理前两个 beforeCreate
和 created
,我们知道这两个回调方法是在 applyOptions
方法中回调的:
在
packages/runtime-core/src/component.ts
的applyOptions
方法中:jsfunction 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
:js/** * 触发 hooks */ function callHook(hook: Function) { hook() }
至此, beforeCreate
和 created
完成。
接下来我们来去处理 beforeMount
和 mounted
,对于这两个生命周期而言,他需要先注册,在触发。
那么首先我们先来处理注册的逻辑:
首先我们需要先创建
LifecycleHooks
在
packages/runtime-core/src/component.ts
中:js/** * 生命周期钩子 */ export const enum LifecycleHooks { BEFORE_CREATE = 'bc', CREATED = 'c', BEFORE_MOUNT = 'bm', MOUNTED = 'm' }
在生成组件实例时,提供对应的生命周期相关选项:
diff/** * 创建组件实例 */ 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
注册方法:jsimport { 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>
测试成功
10:源码阅读:生命回调钩子中访问响应性数据
在实际开发中,我们通常都会在生命周期钩子中访问响应式数据,比如我们来看如下测试实例 packages/vue/examples/imooc/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>
这样的一个代码,在我们的 vue-next-mini
中是无法打印出对应数据的。
那么本小节我们期望的就是:如何可以在生命钩子中访问响应性数据。
对于这样的一个需求,大家应该是感觉有一点似曾相识的。不知道大家还记不记得,我们之前在 render
函数中访问过 this.msg
。当时的做法是通过 call
方法改变了 this
指向。
那么对于当前的场景而言,也是一样的。
我们这里分别去看 created
和 mounted
created
通过之前的代码我们已经知道,created
的回调是在 applyOptions
中触发的,所以我们可以直接在这里进行 debugger
:
进入
applyOptions
剔除之前相同的逻辑,代码执行
if (created) {...}
进入
if
,触发callHook
方法我们可以发现,该方法中存在这样一个三元表达式:
jsisArray(hook) ? hook.map(h => h.bind(instance.proxy!)) : hook.bind(instance.proxy!),
因为我们当前的
hook
不存在数组的情况,所以,我们直接看hook.bind(instance.proxy!)
即可- 对于 bind 方法,大家应该也是熟悉的,它会改变
this
指向,并且返回一个方法的引用 - 那么换句话而言,也就是在 此时,改变了
this
指向,和render
一样,我们通过一个新的this
指向了数据源
- 对于 bind 方法,大家应该也是熟悉的,它会改变
由以上代码可知:
- 对于
created
而言,想要在它这里访问响应式数据,我们只需要通过bind
改变this
指向即可。
mounted
对于 mounted
而言,我们知道它的 生命周期注册 是在 applyOptions
方法内的 registerLifecycleHook
方法中,我们可以直接来看一下源码中的 registerLifecycleHook
方法:
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
指向,以达到在回调钩子中访问响应式数据的目的。
11:框架实现:生命回调钩子中访问响应性数据
根据上一小节的描述,我们只需要 改变生命周期钩子的 this
指向即可
在
packages/runtime-core/src/component.ts
中为callHook
方法增加参数,以此来改变this
指向:/** * 触发 hooks */ function callHook(hook: Function, proxy) { hook.bind(proxy)() }
在
applyOptions
方法中为callHook
的调用,传递第二个参数:js// hooks if (beforeCreate) { callHook(beforeCreate, instance.data) } ... // hooks if (created) { callHook(created, instance.data) }
在
registerLifecycleHook
中,为hook
修改this
指向jsfunction 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>
数据访问成功
12:源码阅读:响应性数据改变,触发组件的响应性变化
此时我们已经可以在生命周期回调钩子中访问到对应的响应性数据了,根据响应性数据的概念,当数据发生变化时,视图应该跟随发生变化,所以我们接下来就要来看一下 组件中响应性数据引起的视图改变。
再来看这一块内容之前,首先我们需要先来明确一些基本的概念:
- 组件的渲染,本质上是
render
函数返回值的渲染。 - 所谓响应性数据,指的是:
getter
时收集依赖setter
时触发依赖
那么根据以上概念,我们所需要做的就是:
- 在组件的数据被触发
getter
时,我们应该收集依赖。那么组件什么时候触发的getter
呢?在packages/runtime-core/src/renderer.ts
的setupRenderEffect
方法中,我们创建了一个effect
,并且把effect
的fn
指向了componentUpdateFn
函数。在该函数中,我们触发了getter
,然后得到了subTree
,然后进行渲染。所以依赖收集的函数为componentUpdateFn
。 - 在组件的数据被触发
setter
时,我们应该触发依赖。我们刚才说了,收集的依赖本质上是componentUpdateFn
函数,所以我们在触发依赖时,所触发的也应该是componentUpdateFn
函数。
明确好了以上内容之后,下面我们就可以创建对应的测试案例 packages/vue/examples/imooc/runtime/redner-component-hook-data-change.html
:
<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>
在 componentUpdateFn
中进行 debugger
,等待 第二次 进入 componentUpdateFn
函数(注意: 此时我们仅关注依赖触发,生命周期的触发不再关注对象中,会直接跳过):
第二次进入
componentUpdateFn
函数,此时因为 组件已经被挂载,所以 不再 执行if (!instance.isMounted)
,而是会直接进入else
执行
let { next, bu, u, parent, vnode } = instance
,从instance
中获取next
和vnode
- 此时拿到的
next
,表示下一次的subTree
,现在为null
- 此时拿到的
vnode
,为当前组件的vnode
- 此时拿到的
执行
if (next)
,因为next = null
,所以会进入else
- 进入
else
- 执行
next = vnode
。即:下一次的渲染为vnode
- 进入
代码执行
const nextTree = renderComponentRoot(instance)
,这个代码我们是熟悉的,并且非常关键,表示我们下一次要渲染的VNode
,我们进入renderComponentRoot
方法进入
renderComponentRoot
方法执行
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT)
,因为当前是 有状态的数据,所以会进入if
进入
if
之后的代码我们就比较熟悉了:jsresult = normalizeVNode( render!.call( proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx ) )
同样通过
call
方法,改变this
指向,触发render
。然后通过normalizeVNode
得到vnode
这次得到的
vnode
就是 下一次要渲染的subTree
跳出
renderComponentRoot
方法,此时得到的nextTree
的值为:代码执行:
jsconst prevTree = instance.subTree instance.subTree = nextTree
保存上一次的 subTree
,同时赋值新的 subTree
。之所以要保存上一次的 subTree
是因为我们后面要进行 更新 操作
- 触发
patch(...)
方法,完成 更新操作
至此,整个 组件视图的更新完成。
由以上代码可知:
- 所谓的组件响应性更新,本质上指的是:
componentUpdateFn
的再次触发,根据新的 数据 生成新的subTree
,再通过path
进行 更新 操作
13:框架实现:响应性数据改变,触发组件的响应性变化
明确好了组件响应性更新,那么下面我们来实现下 vue-next-mini
的对应代码逻辑。
在
packages/runtime-core/src/renderer.ts
的componentUpdateFn
方法中,加入如下逻辑:js// 组件挂载和更新的方法 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
:
<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>
得到响应性的组件更新。
14:源码阅读:composition API ,setup 函数挂载逻辑
那么到现在我们已经处理好了组件非常多的概念,但是我们还知道对于 vue 3
而言,提供了 composition API,即 setup
函数的概念。
那么如果我们想要通过 setup
函数来进行一个响应性数据的挂载,那么又应该怎么做呢?
我们来看下面的这个测试案例 packages/vue/examples/imooc/runtime/redner-component-setup.html
:
<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
中渲染 张三
。
那么对于以上的代码,vue
内部又是如何进行处理的呢?
我们知道,vue
对于组件的挂载,本质上是触发 mountComponent
,在 mountComponent
中调用了 setupComponent
函数,通过此函数来对组件的选项进行标准化。
那么 setup
函数本质上就是一个 vue
组件的选项,所以对于 setup
函数处理的核心逻辑,就在 setupComponent
中。我们在这个函数内部进行 debugger
。
进入
setupComponent
关注
setupStatefulComponent
函数的触发进入
setupStatefulComponent
函数代码执行
const Component = instance.type as ComponentOptions
,此时得到的Component
的值为:代码执行
const { setup } = Component
,由上面component
的值可知setup
是存在的,所以会进入if
代码执行:
jsconst setupResult = callWithErrorHandling( setup, instance, ErrorCodes.SETUP_FUNCTION, [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext] )
我们知道
callWithErrorHandling
本质上是一个try...catch
,所以以上代码 “约等于”setup()
的执行。由此得到
setupResult
的值为() => h('div', obj.name)
。即:setup 函数的返回值代码继续执行,触发
handleSetupResult(instance, setupResult, isSSR)
进入
handleSetupResult
,此时各参数为:执行
if (isFunction(setupResult)) {...}
,由以上参数可知setupResult
是一个函数,所以会进入if
判断- 执行
instance.render = setupResult
。此时:instance.render
有值- 回顾一下我们之前所学习的 有状态的响应性组件挂载逻辑,当
instance.render
存在值时,我们后面的渲染就会变得非常简单了。
- 回顾一下我们之前所学习的 有状态的响应性组件挂载逻辑,当
- 执行
代码继续执行,触发
finishComponentSetup
,这个方法我们之前是熟悉的- 进入
finishComponentSetup
方法 - 执行
if (!instance.render)
,因为当前instance
已经存在render
,所以 不会再次为render
赋值
- 进入
后面的逻辑就是 有状态的响应性组件挂载逻辑 的逻辑了。这里就不再详细说了。
由以上代码可知:
- 对于
setup
函数的composition API
语法的组件挂载,本质上只是多了一个setup
函数的处理 - 因为
setup
函数内部,可以完成对应的 自洽 ,所以我们 无需 通过call
方法来改变this
指向,即可得到真实的render
- 得到真实的
render
之后,后面就是正常的组件挂载了
15:框架实现:composition API ,setup 函数挂载逻辑
明确好了 setup
函数的渲染逻辑之后,那么下面我们就可以进行对应的实现了。
在
packages/runtime-core/src/component.ts
模块的setupStatefulComponent
方法中,增加setup
判定:jsfunction setupStatefulComponent(instance) { const Component = instance.type const { setup } = Component // 存在 setup ,则直接获取 setup 函数的返回值即可 if (setup) { const setupResult = setup() handleSetupResult(instance, setupResult) } else { // 获取组件实例 finishComponentSetup(instance) } }
创建
handleSetupResult
方法:jsexport function handleSetupResult(instance, setupResult) { // 存在 setupResult,并且它是一个函数,则 setupResult 就是需要渲染的 render if (isFunction(setupResult)) { instance.render = setupResult } finishComponentSetup(instance) }
在
finishComponentSetup
中,如果已经存在render
,则不需要重新赋值:jsexport 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>
挂载 和 更新 都可成功
16:总结
在本章中,我们处理了 vue
中组件对应的 挂载、更新 逻辑。
我们知道组件本质上就是一个对象(或函数),组件的渲染本质上是 render
函数返回值的渲染。
组件渲染的内部,构建了 ReactiveEffect
的实例,其目的是为了实现组件的响应性渲染。
而当我们期望在组件中访问响应性数据时,分为两种情况:
- 通过
this
访问:对于这种情况我们需要改变this
指向,改变的方式是通过call
方法或者bind
方法 - 通过
setup
访问:这种方式因为不涉及到this
指向问题,反而更加简单
当组件内部的响应性数据发生变化时,会触发 componentUpdateFn
函数,在该函数中根据 isMounted
的值的不同,进行了不同的处理。
组件的生命周期钩子,本质上只是一些方法的回调,当然,如果我们希望在生命周期钩子中通过 this
访问响应式数据,那么一样需要改变 this
指向。
哪怕我们说了那么多,我们所说的依然只是 vue
中组件挂载的冰山一角,我们知道对于组件而言,除了我们所讲到的内容之外,还包括很多东西,比如:
computed
watch
composition API
的生命周期方法- …
但是因为课程篇幅的问题,我们也没有办法完全讲完 vue
中所有的边缘情况(这样课程可能会超过 200 小时了)。所以我们只能够挑选重要的内容来进行讲解。对于其他的一些没有讲到的知识点,大家可以根据在课程中所学到的知识进行对应的 debugger
,也可以在问答区留言,我们一起来进行讨论。
那么下一章,我们将要来看渲染器的最后一个环节 diff
算法,大家准备好了吗?