在实现了 ELEMENT
、COMMENT
、TEXT
节点的挂载后,我们最后再来实现一下组件的挂载与更新
开始实现组件之前,我们需要明确 vue
中一些关于组件的基本概念:
组件本身是一个对象(仅考虑对象的情况,忽略函数式组件)。它必须包含一个 render
函数,该函数决定了它的渲染内容。
如果我们想要定义数据,那么需要通过 data
选项进行注册。data
选项应该是一个 函数,并且 renturn
一个对象,对象中包含了所有的响应性数据。
除此之外,我们还可以定义例如 生命周期、计算属性、watch
等对应内容。
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
源码中分析它是如何处理的:
vue
的渲染逻辑,都会从 render
函数进入 patch
函数,所以我们可以直接在 patch
函数中进入 debugger
:patch
方法中,最终会进入到 processComponent
方法去:processComponent
方法中,代码最终会进入到 mountComponent
方法去挂载组件,我们进入到 mountComponent
中:mountComponent
中代码会先执行 createComponentInstance
方法创建 instance
,这个 instance
就是组件实例tsconst 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
函数。我们现在进入 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
函数进行挂载
明确好了源码的无状态组件挂载之后,那么接下来我们来进行一下对应实现。
packages/runtime-core/src/renderer.ts
的 patch
方法中,创建 processComponent
的触发:else if (shapeFlag & ShapeFlags.COMPONENT) { // 组件 processComponent(oldVNode, newVNode, container, anchor) }
processComponent
函数:ts/**
* 组件的打补丁操作
*/
const processComponent = (oldVNode, newVNode, container, anchor) => {
if (oldVNode == null) {
// 挂载
mountComponent(newVNode, container, anchor)
}
}
mountComponent
方法:tsconst 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
函数逻辑:\tslet 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
函数逻辑:ts/**
* 规范化组件实例数据
*/
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
函数:ts/**
* 设置组件渲染
*/
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
函数:ts 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
测试实例:
html<script>
const { h, render } = Vue
const component = {
render() {
return h('div', 'hello component')
}
}
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
此时,组件渲染完成。
此时我们的无状态组件挂载已经完成,接下来我们来看一下 无状态组件更新 的处理逻辑。
创建测试实例:
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
方法,执行组件挂载,这里不在复述。render
方法,此时是第二个 component 的挂载,即: 更新patch
,此时的参数为:VNode
,所以 if (n1 && !isSameVNodeType(n1, n2))
判断为 true
,此时将执行 卸载旧的 VNode
逻辑执行 ·unmount(n1, parentComponent, parentSuspense, true)· ,触发 卸载逻辑
代码继续执行,经过 switch
,再次执行 processComponent
,因为 旧的 VNode
已经被卸载,所以此时 n1 = null
代码继续执行,发现 再次触发 mountComponent
,执行 挂载操作
后续省略
至此,组件更新完成。
由以上代码可知:
因为目前我们的代码 支持 组件的更新操作
所以可以直接可创建测试实例 packages/vue/examples/runtime/render-component-update.html
:
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
函数返回值的渲染ReactiveEffect
实例 effect
instance
的实例,该实例表示 组件本身,同时 vnode.component
指向它sMounted
但是以上的内容,全部都是针对于 无状态 组件来看的。在我们的实际开发中,组件通常是 有状态(即:存在 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
操作。
剔除掉之前的重复逻辑,我们之前的关注点在 渲染,现在我们把关注点放在 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
明确好了有状态组件的挂载逻辑之后,我们接下里就进行对应的实现。
packages/runtime-core/src/component.ts
中,新增 applyOptions
方法,为 instance
赋值 data
:tsfunction 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
:tsexport function finishComponentSetup(instance) {
const Component = instance.type
instance.render = Component.render
// 改变 options 中的 this 指向
applyOptions(instance)
}
packages/runtime-core/src/componentRenderUtils.ts
中,为 render
的调用,通过 call
方法修改 this
指向:ts /**
* 解析 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
:
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>
在前面几节,我们其实已经在源码中查看到了对应的一些生命周期处理逻辑。
我们知道 vue
把生命周期叫做生命周期回调钩子,说白了就是一个:在指定时间触发的回调方法。
我们查看 packages/runtime-core/src/component.ts
中 第 213
行可以看到 ComponentInternalInstance
接口,该接口描述了组件的所有选项,其中包含:
ts /**
* @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
对象:
tsexport 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
方法被回调。
那么明确好了这个之后,我们来看一个测试实例:
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>
根据之前的经验,我们知道,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
里面,然后在合适的时机去触发明确好了源码的生命周期处理之后,那么接下来我们来实现一下对应的逻辑。
我们本小节要处理的生命周期有四个,首先我们先处理前两个 beforeCreate
和 created
,我们知道这两个回调方法是在 applyOptions
方法中回调的:
packages/runtime-core/src/component.ts
的 applyOptions
方法中:tsfunction 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
:ts/**
* 触发 hooks
*/
function callHook(hook: Function) {
hook()
}
至此, beforeCreate
和 created
完成。
接下来我们来去处理 beforeMount
和 mounted
,对于这两个生命周期而言,他需要先注册,在触发。
那么首先我们先来处理注册的逻辑:
首先我们需要先创建 LifecycleHooks
packages/runtime-core/src/component.ts
中:ts/**
* 生命周期钩子
*/
export const enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
MOUNTED = 'm'
}
ts/**
* 创建组件实例
*/
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
注册方法:tsimport { 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
方法中,完成对应的注册:
tsfunction applyOptions(instance: any) {
...
function registerLifecycleHook(register: Function, hook?: Function) {
register(hook, instance)
}
// 注册 hooks
registerLifecycleHook(onBeforeMount, beforeMount)
registerLifecycleHook(onMounted, mounted)
}
将 bm
和 m
注册到组件实例之后,下面就可以在 componentUpdateFn
中触发对应 hooks
了:
ts// 组件挂载和更新的方法
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
:
ts<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>
测试成功
对于我们当前的代码,还不能生命周期钩子中访问响应式数据,那么要如何解决这个问题呢?
我们从源码中分析一下:
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>
created
通过之前的代码我们已经知道,created
的回调是在 applyOptions
中触发的,所以我们可以直接在这里进行 debugger
:
applyOptions
if (created) {...}
通过上他我们很容易分析 created
能获取到响应数据的原因。
mounted
对于 mounted
而言,我们知道它的 生命周期注册 是在 applyOptions
方法内的 registerLifecycleHook
方法中,我们可以直接来看一下源码中的 registerLifecycleHoo
方法:
tsfunction 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
指向,以达到在回调钩子中访问响应式数据的目的。
根据上一小节的描述,我们只需要 改变生命周期钩子的 this
指向即可
packages/runtime-core/src/component.ts
中为 callHook
方法增加参数,以此来改变 this
指向:ts/**
* 触发 hooks
*/
function callHook(hook: Function, proxy) {
hook.bind(proxy)()
}
applyOptions
方法中为 callHoo
的调用,传递第二个参数:ts// hooks
if (beforeCreate) {
callHook(beforeCreate, instance.data)
}
...
// hooks
if (created) {
callHook(created, instance.data)
}
registerLifecycleHook
中,为 hook
修改 this
指向tsfunction registerLifecycleHook(register: Function, hook?: Function) {
register(hook?.bind(instance.data), instance)
}
至此,代码完成。
创建对应测试实例 packages/vue/examples/runtime/redner-component-hook-data.html
:
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>
数据访问成功
虽然目前我们已经完成了在生命周期中访问响应性数据,但是还有个问题就是:响应性数据改变,没有触发组件发生变化。
再来看这一块内容之前,首先我们需要先来明确一些基本的概念:
组件的渲染,本质上是 render
函数返回值的渲染。
所谓响应性数据,指的是:
getter
时收集依赖setter
时触发依赖那么根据以上概念,我们所需要做的就是:
getter
时,我们应该收集依赖。那么组件什么时候触发的 getter
呢?在 packages/runtime-core/src/renderer.ts
的 setupRenderEffect
方法中,我们创建了一个 effect
,并且把 effect
的 fn
指向了 componentUpdateFn
函数。在该函数中,我们触发了 getter
,然后得到了 subTree
,然后进行渲染。所以依赖收集的函数为 componentUpdateFn
。setter
时,我们应该触发依赖。我们刚才说了,收集的依赖本质上是 componentUpdateFn
函数,所以我们在触发依赖时,所触发的也应该是 componentUpdateFn
函数。明确好了以上内容之后,我们就去分析一下源码是怎么做的:
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
,因为这次组件已经挂载过了,所以会执行 else
,在 else
中将下一次要渲染的 vnode
赋值给 next
,我们继续往下执行:renderComponentRoot
, 而对于 renderComponentRoot
方法,我们也很熟悉了,它内部会调用tsresult = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
同样通过 call
方法,改变 this
指向,触发 render
。然后通过 normalizeVNode
得到 vnode
,这次得到的 vnode
就是 下一次要渲染的 subTree
。接着跳出renderComponentRoot
方法继续执行代码:
可以看到,最终触发 patch(...)
方法,完成 更新操作
至此,整个 组件视图的更新完成。
总结:
所谓的组件响应性更新,本质上指的是: componentUpdateFn
的再次触发,根据新的 数据 生成新的 subTree
,再通过 path
进行 更新 操作
packages/runtime-core/src/renderer.ts
的 componentUpdateFn
方法中,加入如下逻辑:ts// 组件挂载和更新的方法
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
:
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>
得到响应性的组件更新。
到现在我们已经处理好了组件非常多的概念,但是我们还知道对于 vue3
而言,提供了 composition API
,即 setup
函数的概念。
那么如果我们想要通过 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
对于组件的挂载,本质上是触发 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
之后,后面就是正常的组件挂载了明确好了 setup
函数的渲染逻辑之后,那么下面我们就可以进行对应的实现了。
在 packages/runtime-core/src/component.ts
模块的 setupStatefulComponent
方法中,增加 setup
判定:
tsfunction setupStatefulComponent(instance) {
const Component = instance.type
const { setup } = Component
// 存在 setup ,则直接获取 setup 函数的返回值即可
if (setup) {
const setupResult = setup()
handleSetupResult(instance, setupResult)
} else {
// 获取组件实例
finishComponentSetup(instance)
}
}
handleSetupResult
方法:tsexport function handleSetupResult(instance, setupResult) {
// 存在 setupResult,并且它是一个函数,则 setupResult 就是需要渲染的 render
if (isFunction(setupResult)) {
instance.render = setupResult
}
finishComponentSetup(instance)
}
finishComponentSetup
中,如果已经存在 render
,则不需要重新赋值:tsexport 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
:
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>
挂载 和 更新 都可成功
在本章中,我们处理了 vue
中组件对应的 挂载、更新 逻辑。
我们知道组件本质上就是一个对象(或函数),组件的渲染本质上是 render
函数返回值的渲染。
组件渲染的内部,构建了 ReactiveEffect
的实例,其目的是为了实现组件的响应性渲染。
而当我们期望在组件中访问响应性数据时,分为两种情况:
this
访问:对于这种情况我们需要改变 this
指向,改变的方式是通过 call
方法或者 bind
方法setup
访问:这种方式因为不涉及到 this
指向问题,反而更加简单
当组件内部的响应性数据发生变化时,会触发 componentUpdateFn
函数,在该函数中根据 isMounted
的值的不同,进行了不同的处理。组件的生命周期钩子,本质上只是一些方法的回调,当然,如果我们希望在生命周期钩子中通过 this
访问响应式数据,那么一样需要改变 this
指向。
本文作者:叶继伟
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!