第3章〓Vue3整体实现
本章将通过对源码的调试,介绍Vue3的整体实现。本章内容是第2章内容的延伸,核心逻辑与第2章基本相同。读者可以将两章内容对照进行学习。读者可以跟随本章的内容学习源码调试方法,简单理解Vue3的运行原理。

观看视频速览本章主要内容。


视频讲解


3.1源码调试◆

(1) 基础环境: npm、node、vscode; 

(2) 下载源码; 


git clone git@github.com:vuejs/vue-next.git




注: git@github.com为ssh下载,若未设置免密,则可使用HTTPS下载。

(3) 安装项目依赖; 

(4) 安装yarn插件; 


npm install yarn -g




(5) 安装依赖; 


yarn install




安装依赖时,如果提示需安装pnpm,则会在执行时提示“This repository requires using pnpm as the package manager for scripts to work properly.”,如图3.1所示。pnpm命令可以通过npm安装。



图3.1报错信息


(6) 安装pnpm; 


npm install -g pnpm




使用pnpm时需注意,pnpm命令对node版本有依赖,例如,在本地执行pnpm时,提示node版本过低,此时需要升级node版本,以支持依赖,提示信息如图3.2所示。



图3.2提示信息


(7) 升级node版本。

本书的操作环境安装有nvm,可以直接切换多个版本node,关于node升级此处不过多介绍,完成node版本升级后,通过pnpm安装依赖。



图3.3打包结果


3.1.1代码调试

依赖包安装完成后即可运行vue代码,执行npm run build打包最新的代码。完成打包后可以在packages/vue/dist内找到对应文件,打包结果如图3.3所示。

完成打包后,新建html文件并引入编译好的vue.global.js文件,在html文件内实现简单的内容: 渲染一个按钮和提示语,单击按钮后可以反转提示语的字母位置。该demo实现十分简单,但是通过该操作可以帮助读者探究Vue3内部运行原理,覆盖Vue3的核心逻辑。整体代码如下: 

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<meta http-equiv="X-UA-Compatible" content="ie=edge">

<title>Hello Vue3.0</title>

<style>

body,

#app {

text-align: center;

padding: 30px;

}

</style>

<script src="./packages/vue/dist/vue.global.js"></script>

</head>

<body>

<h3>reactive</h3>

<div id="app"></div>

</body>

<script>

const { createApp, reactive } = Vue;

const App = {

template: `<button @click="click">reverse</button><div style="margin-top: 20px">{{ state.message }}</div>`,

setup() {

console.log("setup ");

const state = reactive({

message: "Hello Vue3!!"

});

click = () => {

state.message = state.message.split("").reverse().join("");

};

return {

state,

click

};

}

};

createApp(App).mount("#app");

</script>

</html>

上述代码使用了Vue3的createApp()函数和reactive()函数,createApp()函数完成组件的初始化、挂载、更新和内部setup()函数执行等过程。reactive()函数完成内部响应式数据的实现。该demo实现的功能很简单,但覆盖了整个Vue3的核心逻辑。

上述页面完成后,即可在浏览器打开页面进行调试。查看Source标签页可以发现Vue3源码全在一个文件内不利于阅读和调试,需手动开启sourceMap。

3.1.2开启sourceMap

开启sourceMap需要修改rollup.config.js文件内容,在createConfig方法内配置output.sourcemap=true,设置sourceMap如图3.4所示。



图3.4设置sourceMap


在tsconfig.json中配置sourceMap输出,将sourceMap从false改为true,打开sourceMap,如图3.5所示。



图3.5打开sourceMap


完成后重新打包即可开启sourceMap,页面涉及的Vue源码如图3.6所示。



图3.6Vue源码


3.1.3总结

本节主要介绍Vue3源码调试的步骤。本节的设置和调试作用将贯穿全文,后续源码解读和分析均会使用该demo,因此学习并开始Vue源码调试十分重要,建议读者通过对源码的调试(debug),学习Vue3的内部实现,提升自己的动手和代码阅读能力。

后面将以表格的形式列出每个函数对应的文件,由于文件路径较长,为了便于阅读,以简写路径代替真实路径。

3.2createApp()函数◆


3.2.1涉及文件

createApp()函数涉及文件路径如表3.1所示。


表3.1createApp()函数涉及文件路径


名称简 写 路 径文 件 路 径


createApp$runtimedom_index$root/packages/runtimedom/src/index.ts
ensureRenderer$runtimedom_index$root/packages/runtimedom/src/index.ts
baseCreateRenderer$runtimecore_render$root/packages/runtimecore/src/render.ts
render$runtimecore_render$root/packages/runtimecore/src/render.ts
createAppAPI$runtimecore_apiCreateApp$root/packages/runtimecore/src/apiCreateApp.ts


3.2.2调用createApp()函数

调用createApp()函数返回一个应用实例,它的实现在$runtimedom_index.ts文件内,Vue3源码内的createApp()函数内部主要创建实例并对实例的mount()方法进行重写,代码如下所示: 

export const createApp = ((...args) => {

// 创建渲染器并调用渲染器的createApp()方法创建app实例

const app = ensureRenderer().createApp(...args);

// 重写app的mount()方法

const { mount } = app;

app.mount = (containerOrSelector: Element | string): any => {

// ...

};

// 返回app

return app;

}) as CreateAppFunction<Element>;

上述代码,主要完成两项工作: 

(1) 初始化app实例; 

(2) 重写app实例的mount()方法。

在初始化app时,首先创建渲染器,并且调用渲染器的createApp()方法,查看ensureRenderer()函数实现逻辑,具体代码如下: 

// $runtime-dom_index

function ensureRenderer() {

// 若renderer存在则直接返回,若不存在则创建

return renderer ||

(renderer = createRenderer<Node, Element>(rendererOptions))

}

该代码采用单例模式实现,若renderer存在则直接返回,若不存在则调用createRenderer()函数创建。使用单例模式优化可以避免重复创建renderer,减少性能消耗。调用createRenderer()函数时传入rendererOptions对象,该对象内含有与DOM相关的方法,在DOM渲染等时将会大量使用。在查看createRenderer()函数前,先简单介绍该对象内的实现。该对象由3个对象合并而来,分别是patchProp、forcePatchProp和nodeOps,patch(class、style等)的处理方法在前两个对象内,DOM的处理方法在nodeOps对象内。

rendererOptions生成代码实现如下: 

const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)

rendererOptions流程图如图3.7所示。



图3.7rendererOptions 流程图


3.2.3调用createRenderer()函数

继续介绍创建createApp()函数的主线逻辑,调用$runtimecore_render文件内createRenderer()函数。该函数采用高阶函数的方式,在函数内部返回另外一个函数,具体代码如下: 

// $runtime-core_render

export function createRenderer<

HostNode = RendererNode,

HostElement = RendererElement

>(options: RendererOptions<HostNode, HostElement>) {

return baseCreateRenderer<HostNode, HostElement>(options)

}

在返回的函数baseCreateRenderer()中,既可以传入浏览器平台的options,也可以传入小程序等其他环境的options,实现对上层平台API的解耦。

在createRenderer()函数所在的文件内可以找到3个baseCreateRenderer()函数定义,这是TypeScript内函数重载的写法。可以根据传入参数类型选择不同的函数体执行。涉及逻辑代码如下: 

// $runtime-core_render

// overload 1: no hydration

function baseCreateRenderer<

HostNode = RendererNode,

HostElement = RendererElement

>(options: RendererOptions<HostNode, HostElement>): Renderer<HostElement>

// overload 2: with hydration

function baseCreateRenderer(

options: RendererOptions<Node, Element>,

createHydrationFns: typeof createHydrationFunctions

): HydrationRenderer

// implementation

function baseCreateRenderer(

options: RendererOptions,

createHydrationFns?: typeof createHydrationFunctions

): any {

…

}

此处着重介绍第三个baseCreateRenderer()函数的实现逻辑: 

(1) 从options中取出平台相关的API; 

(2) 创建patch函数; 

(3) 创建render函数; 

(4) 返回一个渲染器对象。

上述4点简单概括了baseCreateRenderer()函数所做工作,对函数精简后的主要逻辑代码如下: 

// $runtime-core_render

function baseCreateRenderer(

options: RendererOptions,

createHydrationFns?: typeof createHydrationFunctions

): any {

// 从options中取出传递进来的API

const { ... } = options

// 创建patch函数

const patch: PatchFn = ( … ) => {}

const mountElement = () => {}

const patchProps = () => {}

const mountComponent: MountComponentFn = () => {}

const updateComponent = () => {}

const unmount: UnmountFn = () => {}

// 创建render函数

const render: RootRenderFunction = (vnode, container) => {}

// 返回渲染器对象

return {

render,

hydrate,

createApp: createAppAPI(render, hydrate)

}

}

该函数内部需要实现很多内置方法,包括第2章介绍的mount挂载、props处理、unmount卸载、update更新、patch和render等。由于代码较为繁杂,不利于理解主体逻辑,在后续内容将逐步细化分析,此处暂不给出具体实现。

注: hydrate参数主要在SSR时使用。将后端渲染中的纯字符串转换为可展示的HTML页面,这个过程就是hydrate。

在查看createAppAPI()函数实现前,先查看render函数,它用于内部的render渲染操作,根据组件状态判断执行挂载还是更新,该函数内部的实现代码如下: 

// $runtime-core_render

const render: RootRenderFunction = (vnode, container) => {

if (vnode == null) {

if (container._vnode) {

unmount(container._vnode, null, null, true)

}

} else {

patch(container._vnode || null, vnode, container)

}

flushPostFlushCbs()

container._vnode = vnode

}

上述代码与第2章介绍watchEffect()函数内传入的匿名函数十分相似。此处调用render函数将会执行unmount()、patch()和flushPostFlushCbs()函数。

继续查看createAppAPI()函数,它位于$runtimecore_apiCreateApp文件内。通过查看该函数的实现,可以发现内部又返回了一个createApp()函数。此处多次返回函数的设计主要是采用函数柯里化的方式持有render函数以及hydrate参数,避免了用户在应用内使用时需要手动传入render函数给createApp。

根据createAppAPI()函数传递的参数可知,render函数被传递到该函数内部,涉及的关键代码如下: 

// $runtime-core_apiCreateApp

export function createAppAPI<HostElement>(

render: RootRenderFunction,

hydrate?: RootHydrateFunction

): CreateAppFunction<HostElement> {

return function createApp(rootComponent, rootProps = null) {

const context = createAppContext()

const installedPlugins = new Set()

let isMounted = false

const app: App = (context.app = {

get config() {},

set config(v) {},

use(plugin: Plugin, ...options: any[]) {},

mixin(mixin: ComponentOptions) {},

component(name: string, component?: Component): any {},

directive(name: string, directive?: Directive) {},

mount(rootContainer: HostElement, isHydrate?: boolean): any {},

unmount() {},

provide(key, value) {}

})

return app

}

该函数内部实现代码较多,内部主要做如下处理:

(1) 判断rootProps类型,调整其值; 

(2) 通过createAppContext创建context上下文; 

(3) 初始化已安装插件installedPlugins和isMounted状态对象; 

(4) 通过对象字面量的方式创建一个完全实现app实例接口的对象并返回。

其中创建context上下文内容如下: 

export function createAppContext(): AppContext {

return {

app: null as any,

config: {

isNativeTag: NO,

performance: false,

globalProperties: {},

optionMergeStrategies: {},

errorHandler: undefined,

warnHandler: undefined,

compilerOptions: {}

},

mixins: [],// 混入

components: {}, // 组件

directives: {}, // 指令

provides: Object.create(null), // 注入对象

optionsCache: new WeakMap(), // options缓存

propsCache: new WeakMap(), // props缓存

emitsCache: new WeakMap() // emits缓存

}

}

该函数初始化app全局方法,包括全局混入、全局组件、全局指令和全局注入等。完成context上下文创建后,再查看创建app实例的部分,代码如下: 

function createApp(rootComponent, rootProps = null) {

…

// 创建context上下文

const context = createAppContext()

// 创建插件安装set

const installedPlugins = new Set()

// 是否挂载

let isMounted = false

const app: App = (context.app = {

…

use(plugin: Plugin, ...options: any[]) { ... }

mixin(mixin: ComponentOptions) { ... }

component(name: string, component?: Component): any { ... }

directive(name: string, directive?: Directive) { ... }

mount(rootContainer: HostElement, isHydrate?: boolean): any { ... }

unmount() { ... }

provide(key, value) { ... }

return app

}

}

组件内的方法和属性大部分在此处声明和初始化,该函数主要涉及: 

(1) 基本属性,包括_uid(唯一标识)、props、配置设置等; 

(2) 插件安装,通过调用use()方法处理插件相关内容; 

(3) 混入函数,通过mixin()函数处理混入相关内容; 

(4) 组件内容,通过component()函数处理; 

(5) 指令内容,通过directive()函数注册全局指令; 

(6) 挂载组件,通过mount()函数进行挂载; 

(7) 卸载组件,通过unmount()函数进行卸载; 

(8) 注入组件,通过provide()函数进行注入。

通过该函数,可以大致了解Vue的内部情况,完成初始化操作后,返回app对象。

此处执行初始化操作,且为非SSR模式,所以设置isMounted=false,isHydrate=false。通过createVNode方法创建根组件VNode,并由render函数从根组件VNode进行渲染,传入VNode、rootContainer参数,完成渲染后,将isMounted标识为true(已挂载),然后绑定根实例和根容器,最后返回根组件的代理,完成挂载。主要逻辑代码如下: 

mount(

rootContainer: HostElement,

isHydrate ? : boolean,

isSVG ? : boolean

): any {

if (!isMounted) {

...

const vnode = createVNode(

rootComponent as ConcreteComponent,

rootProps

)

vnode.appContext = context

…

if (isHydrate && hydrate) {

hydrate(vnode as VNode<Node, Element>, rootContainer as any)

} else {

render(vnode, rootContainer, isSVG)

}

isMounted = true

app._container = rootContainer;

(rootContainer as any).__vue_app__ = app

return getExposeProxy(vnode.component!) || vnode.component!.proxy

}

...

}

注: 此处的render方法是上文createAppAPI方法特有的,该方法是由上文传入,所以此处可以直接调用。

app初始化完成并且调用mount()方法后,回到本节开始的地方,会发现通过app实例解构出mount()方法后对其进行了重写,整个过程通过处理容器元素、判断根组件是否存在模板或渲染函数、清空容器内容3个步骤,确保原mount()方法的正常执行。在完成这部分工作后createApp(app).mount()全流程就完成了。重写mount()方法的具体代码如下: 

app.mount = (containerOrSelector: Element | string): any => {

// 标准化容器元素 element | string --> element

const container = normalizeContainer(containerOrSelector)

// 若找不到元素,则直接return

if (!container) return

// 拿到app组件

const component = app._component

// 如果既不是函数组件也没有render和模板,则取容器元素的innerHTML当作模板

if (!isFunction(component) && !component.render && !component.template) {

component.template = container.innerHTML

…

}

// 在挂载前清空容器的innerHTML

container.innerHTML = "

// 执行挂载,得到返回的代理对象

const proxy = mount(container, false, container instanceof SVGElement)

if (container instanceof Element) {

container.removeAttribute('v-cloak')

container.setAttribute('data-v-app', ")

}

return proxy

}

在对mount重写时,主要对DOM相关逻辑进行处理。例如,通过innerHTML将初始化完成的内容挂载到页面上等操作,是为了将浏览器平台的内容与核心内容进行分离,也方便用户自定义mount挂载逻辑,使之应用到不同的平台,为实现跨平台提供帮助。

Vue初始化app实例如图3.8所示。



图3.8Vue初始化app实例


3.2.4总结

本节主要讲解整个Vue初始化app实例的流程,结合第2章对createApp核心内容和调用流程的讲解,相信读者对整个Vue的初始化有了深刻的认识。熟悉该调用流程后,我们将在后面展开介绍内部的实现原理。本节内也提到了很多设计模式等的使用,在阅读源码的过程中,可以多进行学习,并应用到自己的项目中,本节主要使用的技巧包括但不限于,函数柯里化、单例模式和函数重载的应用。

3.3mounted挂载◆


3.3.1涉及文件

mounted挂载涉及文件路径如表3.2所示。


表3.2mounted挂载涉及文件路径


名称简 写 路 径文 件 路 径


mount$runtimecore_apiCreateApp$root/packages/runtimecore/src/apiCreateApp.ts
processComponent$runtimecore_renderer$root/packages/runtimecore/src/renderer.ts
setupComponent$runtimecore_components$root/packages/runtimecore/src/component.ts
setupStatefulComponent$runtimecore_components$root/packages/runtimecore/src/component.ts
finishComponentSetup$runtimecore_components$root/packages/runtimecore/src/component.ts


本节将详细介绍Vue3中mounted挂载时的整体流程,通过该流程了解组件挂载的详细情况。下面对涉及的虚拟DOM和VNode的相关知识进行简单介绍。

mount(挂载)内部将VNode转换为真实的DOM结构再挂载到DOM根节点内,mount的调用在createApp函数内已有简单介绍,对render函数的作用也有一定了解,本节将继续围绕该内容展开介绍。

虚拟DOM本质是通过JavaScript对象描述真实DOM的结构和属性。因为在DOM操作的过程中会涉及页面的重绘和回流,所以引入虚拟DOM减少对DOM对象的操作,降低性能损耗。如果只是对页面内某个极小部分修改就造成整个DOM树的重绘,那么会有极大的性能损耗。虚拟DOM概念从Vue2开始引入,在真实操作DOM前对需要修改的内容进行判断,保证只有DOM结构的最小化修改,减少重绘和回流的次数,以达到节约性能损耗的目的。在Vue3中依然保留虚拟DOM,并且对其进行大幅度优化。

VNode是虚拟DOM的外在表现,对VNode的增、删、改和查可映射为对DOM的增、删、改和查。整个渲染核心均是围绕虚拟DOM展开的,通过VNode的形式呈现。该对象声明位置在$runtimecore_vnode文件内,具体声明及含义可在4.1.2节查看。

对虚拟DOM和VNode有简单了解后,再回到$runtimecore_apiCreateApp文件内查看与mount相关的内容。mount内部首先会判断是否已经挂载,如果已挂载,则结束mount函数执行。如果未挂载,则创建一个VNode,再执行render渲染。mount的挂载本质是调用apiCreateApp()函数,该函数简化后代码如下: 

// $runtime-core_apiCreateApp

mount(rootContainer: HostElement, isHydrate?: boolean): any {

if (!isMounted) {

// 创建根组件VNode

const vnode = createVNode(

rootComponent as ConcreteComponent,

rootProps

)

// 在根节点VNode上存储应用上下文

vnode.appContext = context

// HMR根节点重载

if (__DEV__) {

context.reload = () => {

render(cloneVNode(vnode), rootContainer)

}

}

if (isHydrate && hydrate) {

hydrate(vnode as VNode<Node, Element>, rootContainer as any)

} else {

// 从根组件VNode开始渲染

render(vnode, rootContainer)

}

// 标识已挂载

isMounted = true

// 绑定根实例和根容器

app._container = rootContainer

// 调试工具和探测

;(rootContainer as any).__vue_app__ = app

if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {

devtoolsInitApp(app, version)

}

// 返回根组件的代理

return vnode.component!.proxy

} else if (__DEV__) {

// ...

}

}

上述代码通过createVNode()函数创建VNode上下文,将创建好的上下文通过render函数进行渲染。此处mounted挂载将分为3个步骤: 

(1) 根据根组件信息创建根组件VNode,在根节点VNode上存储应用上下文; 

(2) 从根组件VNode开始递归渲染生成DOM树并挂载到根容器上; 

(3) 处理根组件挂载后的相关工作并返回根组件的代理。

注: 此render函数是内部渲染函数,内部利用递归的方式将VNode转换为DOM结构使用,与后面将模板转换为VNode的render渲染函数无任何关联。

3.3.2创建根组件VNode

通过调用createVNode()函数实现对根组件VNode的创建,该函数位于$runtimecore_vnode文件内,实现代码如下: 

// $runtime-core_vnode

export const createVNode = (__DEV__

? createVNodeWithArgsTransform

: _createVNode) as typeof _createVNode

该函数内部首先判断是否为开发环境,然后根据环境输出代码的执行情况,最后调
用_createVNode()函数创建一个VNode并返回。_createVNode()函数的内部实现逻辑主要包括: 

(1) 对class 和 style 进行标准化; 

(2) 对VNode的形态类型进行确定(patch时依赖); 

(3) 通过对象字面量的方式来创建VNode; 

(4) 标准化子节点; 

(5) 完成创建后,返回VNode。

注: 此处的标准化操作十分有必要,并且是提升遍历速度的措施之一。此处标准化后,在后续对VNode进行递归遍历时,可以减少判断分支,提高递归效率和减少其他未知情况的影响,进而提升遍历稳定性、遍历速度和代码可读性。

因_createVNode()函数内容较多,此处不便展开,将放到4.1.2节详细介绍,并对较核心的内容进行注解。

3.3.3递归渲染

完成根组件创建后,开始调用render函数渲染。render函数是以参数的形式传入createAppAPI()函数内,因此在调用createAppAPI()的地方,可以查到对应render函数的实现。代码位于$runtimecore_render文件内,定义在baseCreateRenderer()函数内,精简后的代码如下: 

$runtime-core_render

const render: RootRenderFunction = (vnode, container) => {

if (vnode == null) {

// 若无新的VNode,则卸载组件

if (container._vnode) {

unmount(container._vnode, null, null, true)

}

} else {

// 挂载分支

patch(container._vnode || null, vnode, container)

}

// 执行postFlush任务队列

flushPostFlushCbs()

// 保存当前渲染完毕的根VNode在容器上

container._vnode = vnode

}

上述代码调用render函数执行卸载和挂载逻辑,因介绍mounted挂载逻辑,此处默认有VNode传入,执行patch函数。

patch函数的主要逻辑代码如下: 

const patch: PatchFn = (

// 旧VNode

n1,

// 新VNode

n2,

// 挂载容器

container,

// 锚点

anchor = null,

parentComponent = null,

parentSuspense = null,

isSVG = false,

slotScopeIds = null,

optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren

) => {

if (n1 === n2) {

return

}

// 判断不是同一个VNode,直接卸载旧VNode

if (n1 && !isSameVNodeType(n1, n2)) {

// 获取插入的标识位

anchor = getNextHostNode(n1)

unmount(n1, parentComponent, parentSuspense, true)

n1 = null

}

if (n2.patchFlag === PatchFlags.BAIL) {

optimized = false


n2.dynamicChildren = null

}

// 取出关键信息

const { type, ref, shapeFlag } = n2

switch (type) {

case Text:

// 处理文本节点...

break

case Comment:

// 处理注释节点...

break

case Static:

// 处理静态节点...

break

case Fragment:

// 处理fragment ...

break

default:

// 判断VNode类型

if (shapeFlag & ShapeFlags.ELEMENT) {

// 处理元素节点

processElement(...)

} else if (shapeFlag & ShapeFlags.COMPONENT) {

// 处理组件

processComponent(...)

} else if (shapeFlag & ShapeFlags.TELEPORT) {

// 处理 teleport组件

;(type as typeof TeleportImpl).process(...)

} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {

// 处理suspense组件

;(type as typeof SuspenseImpl).process(...)

} else if (__DEV__) {

warn('Invalid VNode type:', type, `(${typeof type})`)

}

}

}

整个patch函数主要是判断不同的VNode类型(此处的VNode类型即创建时初始化的类型),并根据不同类型执行不同的分支,实现递归整个VNode的逻辑。Vue3在处理VNode的shapeFlag时采用了位运算的方式,关于位运算的知识,将在7.6节介绍。根据调试情况来看,程序执行逻辑最终会进入processComponent()函数中,该函数具体实现如下: 

const processComponent = (...) => {

if (n1 == null) {

…

// 挂载组件

mountComponent(

n2,

container,

anchor,

parentComponent,

parentSuspense,

isSVG,

optimized

)

} else {

// 更新组件

updateComponent(n1, n2, optimized)

}

}

本节主要关注挂载逻辑,处理mounted挂载的mountComponent()函数,具体实现代码如下:

const mountComponent: MountComponentFn = (

initialVNode,

container,

anchor,

parentComponent,

parentSuspense,

isSVG,

optimized

) => {

const compatMountInstance =

__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component

// 创建组件上下文实例

const instance: ComponentInternalInstance = 

compatMountInstance ||

(initialVNode.component = createComponentInstance(

initialVNode,

parentComponent,

parentSuspense

));

…

// 启动组件

setupComponent(instance);

…

// setup函数为异步的相关处理,忽略相关逻辑

if (__FEATURE_SUSPENSE__ && instance.asyncDep) {

parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect);

if (!initialVNode.el) {

const placeholder = (instance.subTree = createVNode(Comment));

processCommentNode(null, placeholder, container!, anchor);

}

return;

}

// 启动带副作用的render函数

setupRenderEffect(

instance,

initialVNode,

container,

anchor,

parentSuspense,

isSVG,

optimized

);

};

在上述代码中,mountComponent()函数内部主要完成如下3项工作: 

(1) 创建组件上下文实例; 

(2) 执行组件setup函数; 

(3) 创建带副作用的render函数。

接下来详细介绍上述3点内容。

3.3.4创建组件上下文实例

export function createComponentInstance(

vnode: VNode,

parent: ComponentInternalInstance | null,

suspense: SuspenseBoundary | null

) {

// 根据条件获取上下文,可能的值包括父组件、组件自身或初始化完成的默认值

const appContext =

(parent ? parent.appContext : vnode.appContext) || emptyAppContext

// 通过对象字面量创建instance

const instance: ComponentInternalInstance = {...}

instance.ctx = { _: instance }

instance.root = parent ? parent.root : instance

instance.emit = emit.bind(null, instance)

// 自定义元素特殊处理

if (vnode.ce) {

vnode.ce(instance)

}

return instance

}

该方法返回通过对象字面量创建的instance。为帮助读者阅读源码及了解相关特性,我们可以通过instance的interface来了解组件实例包含的属性。ComponentInternalInstance的interface定义如下: 

export interface ComponentInternalInstance {

// 组件唯一id

uid: number

// 组件类型

type: ConcreteComponent

// 父组件实例

parent: ComponentInternalInstance | null

// 根组件实例

root: ComponentInternalInstance

// 根组件上下文

appContext: AppContext

// 在父组件的Vdom树中表示该组件的VNode

vnode: VNode

// 父组件更新该VNode后的新VNode

next: VNode | null

// 当前组件Vdom树的根VNode

subTree: VNode

// 副作用渲染函数

effect: ReactiveEffect

// 副作用函数的运行方法传递给调度程序

update: SchedulerJob

// 返回Vdom树的渲染函数

render: InternalRenderFunction | null

// ssr渲染方法

ssrRender?: Function | null

// 保存此组件为其后代提供的值

provides: Data

// 保存当前VNode的effect副作用函数,用于在组件卸载时清理相关effect副作用函数

scope: EffectScope

// 缓存代理访问类型,以避免has Oun Property调用

accessCache: Data | null

// 缓存依赖_ctx但不需要更新的渲染函数

renderCache: (Function | VNode)[]

// 已注册的组件,仅适用于带有mixin或extends的组件

components: Record<string, ConcreteComponent> | null

// 注册指令表

directives: Record<string, Directive> | null

// 保存过滤方法

filters?: Record<string, Function>

// 保存属性设置

propsOptions: NormalizedPropsOptions

// 保存emit信息

emitsOptions: ObjectEmitsOptions | null

// 是否继承属性

inheritAttrs?: Boolean

// 是否自定义属性

isCE?: Boolean

// 特定自定义元素的HMR方法

ceReload?: (newStyles?: string[]) => void

// 组件代理相关

proxy: ComponentPublicInstance | null

// 通过exposed公开属性

exposed: Record<string, any> | null

exposeProxy: Record<string, any> | null

withProxy: ComponentPublicInstance | null

ctx: Data

// 内部状态

data: Data

props: Data

attrs: Data

slots: InternalSlots

refs: Data

emit: EmitFn

// 收集带 .once的emit事件

emitted: Record<string, boolean> | null

// props默认值

propsDefaults: Data

// setup相关函数状态

setupState: Data

devtoolsRawSetupState?: any

setupContext: SetupContext | null

// suspense相关组件

suspense: SuspenseBoundary | null

suspenseId: number

asyncDep: Promise<any> | null

asyncResolved: boolean

// 生命周期相关标识

isMounted: boolean

isUnmounted: boolean

isDeactivated: boolean

// 生命周期钩子函数

...

}

因组件属性较多,此处进行简单注解以便于读者理解。许多属性在日常开发过程中均可能涉及,但此处不需要完全记住,对相关属性有了解后,后续若有需要,可以查阅相关文档。

介绍完interface属性定义后,继续回到mountComponent()函数。得到初始化数据后,开始执行setupComponent()函数,该函数主要初始化props和slots,再执行setup和兼容(Vue2)options API的方法,最终得到instance上下文实例。此处简单查看setupComponent()函数的实现,具体解析将放在3.4节中介绍。

setupComponent()代码逻辑如下: 

export function setupComponent(

instance: ComponentInternalInstance,

isSSR = false

) {

isInSSRComponentSetup = isSSR

const { props, children, shapeFlag } = instance.vnode;

// 是否是包含状态的组件

const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT;

// 初始化Props

initProps(instance, props, isStateful, isSSR);

// 初始化Slots

initSlots(instance, children);

// 判断是包含有状态的方法,执行对应逻辑

const setupResult = isStateful

? setupStatefulComponent(instance, isSSR)

: undefined;

isInSSRComponentSetup = false

return setupResult;

}

上述代码执行完成后,对于数据的处理包括instance、props、slots和state。继续调用带副作用的render函数,在该函数内创建带副作用的渲染函数并保存在update()方法中,然后调用update()方法。在update()方法内根据挂载情况,判断执行挂载或更新逻辑。主要代码逻辑如下: 

// $runtime-core_renderer

const setupRenderEffect: SetupRenderEffectFn = (

instance,

initialVNode,

container,

anchor,

parentSuspense,

isSVG,

optimized

) => {

const componentUpdateFn = () => {

if (!instance.isMounted) {

// 创建

...

} else {

// 更新

...

}

}

// 渲染时创建effect副作用函数

const effect = (instance.effect = new ReactiveEffect(

componentUpdateFn,

() => queueJob(instance.update),

instance.scope // 在组件的影响范围内跟踪它

))

const update = (instance.update = effect.run.bind(effect) as SchedulerJob)

update.id = instance.uid

// allowRecurse

// 组件渲染effect应该允许递归更新

toggleRecurse(instance, true)

// 执行update()

update()

}

setupRenderEffect()函数内保存effect副作用函数,整个副作用函数通过componentUpdateFn()函数实现,完成后将effect.run方法绑定到当前上下文的update()方法上,完成后执行update()方法,触发effect副作用函数的执行(挂载和更新,均会触发)。componentUpdateFn()函数挂载逻辑涉及的代码如下: 

$runtime-core_renderer

const componentUpdateFn = () => {

if (!instance.isMounted) {

let vnodeHook: VNodeHook | null | undefined

const { el, props } = initialVNode

const { bm, m, parent } = instance

const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

toggleRecurse(instance, false)

// beforeMount钩子函数

if (bm) {

invokeArrayFns(bm)

}

// onVnodeBeforeMount

if (

!isAsyncWrapperVNode &&

(vnodeHook = props && props.onVnodeBeforeMount)

) {

invokeVNodeHook(vnodeHook, parent, initialVNode)

}

if (

__COMPAT__ &&

isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)

) {

instance.emit('hook:beforeMount')

}

toggleRecurse(instance, true)

// 服务器端渲染处理

if (el && hydrateNode) {

const hydrateSubTree = () => {

instance.subTree = renderComponentRoot(instance)

hydrateNode!(

el as Node,

instance.subTree,

instance,

parentSuspense,

null

)

}

// 服务器端渲染不用将渲染调用移到异步回调中,因为在服务器端只调用一次,后续不会触发更改

if (isAsyncWrapperVNode) {

;(initialVNode.type as ComponentOptions).__asyncLoader!().then(

() => !instance.isUnmounted && hydrateSubTree()

)

} else {

hydrateSubTree()

}

} else {

// 渲染子树

const subTree = (instance.subTree = renderComponentRoot(instance))

// patch子树

patch(

null,

subTree,

container,

anchor,

instance,

parentSuspense,

isSVG

)

initialVNode.el = subTree.el

}

// mounted钩子函数

if (m) {

queuePostRenderEffect(m, parentSuspense)

}

// onVnodeMounted

if (

!isAsyncWrapperVNode &&

(vnodeHook = props && props.onVnodeMounted)

) {

const scopedInitialVNode = initialVNode

queuePostRenderEffect(

() => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),

parentSuspense

)

}

if (

__COMPAT__ &&

isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)

) {

queuePostRenderEffect(

() => instance.emit('hook:mounted'),

parentSuspense

)

}

// 处理钩子函数

if (initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {

instance.a && queuePostRenderEffect(instance.a, parentSuspense)

if (

__COMPAT__ &&

isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)

) {

queuePostRenderEffect(

() => instance.emit('hook:activated'),

parentSuspense

)

}

}

// 标识为已挂载

instance.isMounted = true

// 仅挂载对象参数以防止内存泄漏

initialVNode = container = anchor = null as any

}

}

ComponentUpdateFn()函数内部主要涉及挂载和更新流程,对挂载逻辑进一步分析可以看到,虽然上述代码较多,但是大部分在处理回调钩子函数。挂载流程实际上主要分为两个步骤: 

(1) 渲染组件子树; 

(2) patch子树。

渲染组件子树时,主要涉及renderComponentRoot()函数,根据代码执行逻辑找到该函数的实现。该函数位于$runtimecore_componentRenderUtils文件内。renderComponentRoot()函数接收一个instance作为参数,返回渲染结果(根组件的VNode)。了解该函数的作用后,再深入查看对应的实现逻辑,主要代码如下: 

export function renderComponentRoot(

instance: ComponentInternalInstance

): VNode {

const {

...

} = instance

let result

let fallthroughAttrs

const prev = setCurrentRenderingInstance(instance)

try {

// 如果vnode组件是有状态组件

if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {

// withProxy只适用于在'with'作用域下运行时编译的渲染函数

const proxyToUse = withProxy || proxy

result = normalizeVNode(

render!.call(

proxyToUse,

proxyToUse!,

renderCache,

props,

setupState,

data,

ctx

)

)

fallthroughAttrs = attrs

} else {

const render = Component as FunctionalComponent

// 直接合并props、slot、attrs和emit即可

result = normalizeVNode(

render.length > 1

? render(

props,

{ attrs, slots, emit }

)

: render(props, null as any /* we know it doesn't need it */)

)

fallthroughAttrs = Component.props

? attrs

: getFunctionalFallthrough(attrs)

}

} catch (err) {

blockStack.length = 0

handleError(err, instance, ErrorCodes.RENDER_FUNCTION)

result = createVNode(Comment)

}

// 在开发模式下属性合并注释被保留,可以在根元素旁边添加注释,使其成为一个片段

let root = result

let setRoot: ((root: VNode) => void) | undefined = undefined

if (fallthroughAttrs && inheritAttrs !== false) {

const keys = Object.keys(fallthroughAttrs)

const { shapeFlag } = root

if (keys.length) {

//如果是元素或组件类型

if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)) {

if (propsOptions && keys.some(isModelListener)) {

// 如果v-model监听属性和props属性相同,则优先使用v-model处理

fallthroughAttrs = filterModelListeners(

fallthroughAttrs,

propsOptions

)

}

root = cloneVNode(root, fallthroughAttrs)

}

}

}

// 合并处理当前节点和props传递的class和style内容

if (

__COMPAT__ &&

isCompatEnabled(DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE, instance) &&

vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT &&

root.shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)

) {

const { class: cls, style } = vnode.props || {}

if (cls || style) {

root = cloneVNode(root, {

class: cls,

style: style

})

}

}

// 继承指令

if (vnode.dirs) {

root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs

}

// 继承过渡数据

if (vnode.transition) {

root.transition = vnode.transition

}

result = root

setCurrentRenderingInstance(prev)

return result

}

注: 此处的render渲染函数是组件的渲染函数,用于将template转换为VNode使用,并非3.3.1节中的render分发函数。

上述代码的核心是调用instance的render方法进行处理,通过调用render渲染函数完成转换过程得到VNode后,将执行第(2)步patch子树。

3.3.5patch子树

patch子树的作用是将VNode通过递归的方式挂载为VNode Tree并转换为渲染函数的过程。在转换过程中,会根据子树的类型进行patch分发,调用不同的逻辑渲染。关于patch函数的具体实现,将会在4.2节着重介绍。

在完成patch子树步骤后,会再次调用patch函数,此时将会得到根组件的真实DOM节点,即id="root"的div元素。根据传入的类型可知,本次patch函数将会分发到processElement()函数内。该函数主要根据判断结果,执行对元素的挂载或更新。具体代码逻辑如下: 

// $runtime-core_renderer

const processElement = (...) => {

isSVG = isSVG || (n2.type as string) === 'svg'

if (n1 == null) {

// 挂载元素

mountElement(...)

} else {

// 更新元素

patchElement(...)

}

}

此处为首次渲染,会通过mountElement()函数对节点元素进行挂载,涉及代码逻辑如下: 

const mountElement = (

vnode: VNode,

container: RendererElement,

anchor: RendererNode | null,

parentComponent: ComponentInternalInstance | null,

parentSuspense: SuspenseBoundary | null,

isSVG: boolean,

slotScopeIds: string[] | null,

optimized: boolean

) => {

let el: RendererElement;

let vnodeHook: VNodeHook | undefined | null;

const {

type,

props,

shapeFlag,

transition,

patchFlag,

dirs,

} = vnode;

// 判断VNode是否属于静态标识

if (

!__DEV__ &&

vnode.el &&

hostCloneNode !== undefined &&

patchFlag === PatchFlags.HOISTED

) {

// 如果一个vnode有非空的el,则意味着正在被重用。只有静态的vnode可以被重用,所以挂载的

// DOM节点应该是完全相同的,并且只在生产环境使用

el = vnode.el = hostCloneNode(vnode.el)

} else {

el = vnode.el = hostCreateElement(

vnode.type as string,

isSVG,

props && props.is,

props

);

// 文本节点的children

if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {

// 如果是文本类型的子节点,则直接设置子节点的内容

hostSetElementText(el, vnode.children as string);

} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {

// 数组型的children

mountChildren(

vnode.children as VNodeArrayChildren,

el,

null,

parentComponent,

parentSuspense,

isSVG && type !== "foreignObject",

slotScopeIds,

optimized

);

}

if (dirs) {

invokeDirectiveHook(vnode, null, parentComponent, 'created')

}

// 设置props

if (props) {

for (const key in props) {

if (key !== 'value' && !isReservedProp(key)) {

hostPatchProp(

el,

key,

null,

props[key],

isSVG,

vnode.children as VNode[],

parentComponent,

parentSuspense,

unmountChildren

);

}

}

if ('value' in props) {

hostPatchProp(el, 'value', null, props.value)

}

if ((vnodeHook = props.onVnodeBeforeMount)) {

invokeVNodeHook(vnodeHook, parentComponent, vnode)

}

}

// 触发钩子函数

if (dirs) {

invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')

}

// 设置scopeId

setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)

}

// 判断组件需要触发transition的钩子函数

const needCallTransitionHooks =

(!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&

transition &&

!transition.persisted

// 如果涉及动画触发,则触发beforeEnter

if (needCallTransitionHooks) {

transition!.beforeEnter(el)

}

// 插入容器元素中

hostInsert(el, container, anchor)

if (

(vnodeHook = props && props.onVnodeMounted) ||

needCallTransitionHooks ||

dirs

) {

// 元素插入完成,需触发transition的enter效果

queuePostRenderEffect(() => {

vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)

needCallTransitionHooks && transition!.enter(el)

dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')

}, parentSuspense)

}

}

上述代码的主要逻辑如下: 

(1) 创建el元素; 

(2) 根据children类型,处理子节点; 

(3) 处理props; 

(4) 将元素插入DOM节点内。

mountElement()函数内的其他流程逻辑都较为清晰和简单,下面简单介绍mountChildren()函数的实现。该函数内部通过循环判断children数组,直接调用patch函数递归挂载组件,具体代码实现逻辑如下: 

const mountChildren: MountChildrenFn = (

children,

container,

anchor,

parentComponent,

parentSuspense,

isSVG,

slotScopeIds,

optimized,

start = 0

) => {

for (let i = start; i < children.length; i++) {

// 标准化VNode

const child = (children[i] = optimized

? cloneIfMounted(children[i] as VNode)

: normalizeVNode(children[i]))

// 直接调用patch()

patch(

null,

child,

container,

anchor,

parentComponent,

parentSuspense,

isSVG,

slotScopeIds,

optimized

);

}

};

完成渲染后,会返回mount并执行重写mount的方法,将元素挂载到页面上。整个mount方法挂载流程如图3.9所示。



图3.9mount方法挂载流程


3.3.6总结

整个mounted挂载过程介绍完成。通过本节的学习,可对Vue3中整个mounted实现过程有深入的认识。本节内涉及的函数较多,函数内实现代码逻辑较多,可以结合流程图帮助理解流程。本节通过重写mount方法,将核心的处理流程和平台的API进行分离,通过重写的方法巧妙地将平台方法和核心方法分离,该写法使得核心方法不局限于Web端,可进行多端扩展。该方法和技巧值得学习,可根据实际情况应用到自身项目内。

3.4setup函数◆


setup函数涉及文件路径如表3.3所示。


表3.3setup函数涉及文件路径



名称简 写 路 径文 件 路 径


mountComponent$runtimecore_render$root/package/runtimecore/src/render.ts
setupComponent$runtimecore_components$root/package/runtimecore/src/component.ts
PublicInstanceProxyHandlers$runtimecore_componentPublicInstance$root/package/runtimecore/src/componentPublicInstance.ts
createSetupContext$runtimecore_components$root/package/runtimecore/src/component.ts

reactivity/src/ref.ts$reactivity_ref$root/package/reactivity/ref.ts
finishComponentSetup$runtimecore_Component$root/packages/runtimecore/src/
component.ts


3.4.1涉及文件

setup函数是Vue3新推出的函数,该函数将用来承载composition API,同时替代beforeCreated和created生命周期函数。Vue3内大多数方法和函数均可以在该函数内使用。该函数仅会在组件启动的时候执行一次,内部会执行初始化,并确认组件与视图的数据、行为和作用。setup函数作为桥梁存在,连接模板渲染、数据处理和响应式数据监听。

3.4.2mountComponent()函数

在前面介绍mounted挂载执行逻辑时,提到内部将会触发setupComponent()函数,在该函数内创建好instance后,将启动setup处理内部暴露和声明的内容。setup处理代码位于$runtimecore_render文件的mountComponent()函数内。对应代码片段如下: 

// $runtime-core_render

// 创建组件实例

const instance: ComponentInternalInstance = 

compatMountInstance ||

(initialVNode.component = createComponentInstance(

initialVNode,

parentComponent,

parentSuspense

));

// 启动组件

setupComponent(instance);

// 启动带副作用的render函数

setupRenderEffect(

instance,

initialVNode,

container,

anchor,

parentSuspense,

isSVG,

optimized

);

3.4.3setupComponent()函数

根据上述代码逻辑,创建组件实例完成后将调用setupComponent()函数,该函数位于$runtimecore_components文件内,函数实现逻辑如下: 

// $runtime-core_components

export function setupComponent(

instance: ComponentInternalInstance,

isSSR = false

) {

isInSSRComponentSetup = isSSR

const { props, children } = instance.vnode

const isStateful = isStatefulComponent(instance)

// 初始化props

initProps(instance, props, isStateful, isSSR)

// 初始化插槽

initSlots(instance, children)

// 执行 setup

const setupResult = isStateful

? setupStatefulComponent(instance, isSSR)

: undefined

isInSSRComponentSetup = false

// 返回setup执行结果

return setupResult

}

该函数的主要逻辑如下: 

(1) 初始化props; 

(2) 初始化插槽slot; 

(3) 根据条件判断是否为带状态的组件选择是否执行setup; 

(4) 返回setup执行结果。

根据判断条件可知,核心方法是setupStatefulComponent(),此处暂不介绍props和slot的初始化方法,直接在当前文件内查看setupStatefulComponent()函数的实现。主要代码逻辑如下: 

// $runtime-core_components

function setupStatefulComponent(

instance: ComponentInternalInstance,

isSSR: boolean

) {

const Component = instance.type as ComponentOptions

// 初始化缓存

instance.accessCache = Object.create(null)

// 创建组件的渲染上下文代理

instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))

// 调用 setup

const { setup } = Component

if (setup) {

// 按需创建setup第二个参数

const setupContext = (instance.setupContext =

setup.length > 1 ? createSetupContext(instance) : null)

// 设置当前组件实例

setCurrentInstance(instance)

pauseTracking()

// 调用setup

const setupResult = callWithErrorHandling(

setup,

instance,

ErrorCodes.SETUP_FUNCTION,

[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]

)

resetTracking()

unsetCurrentInstance()

// 处理setup的执行结果

if (isPromise(setupResult)) {

setupResult.then(unsetCurrentInstance, unsetCurrentInstance)

if (isSSR) {

// 在ssr(服务器端渲染)环境下,等待promise返回再处理setup的执行结果

return setupResult.then((resolvedResult: unknown) => {

handleSetupResult(instance, resolvedResult, isSSR)

})

.catch(e => {

handleError(e, instance, ErrorCodes.SETUP_FUNCTION)

})

} else if (__FEATURE_SUSPENSE__) {

// 保存异步依赖

instance.asyncDep = setupResult

} 

} else {

// 更详细地处理setup执行结果

handleSetupResult(instance, setupResult, isSSR)

}

} else {

// 完成组件启动的后续工作

finishComponentSetup(instance, isSSR)

}

}

该函数的主要逻辑如下: 

(1) 初始化代理缓存; 

(2) 创建渲染上下文代理; 

(3) 调用setup函数; 

(4) 处理setup函数执行结果; 

(5) 完成组件启动。

3.4.4初始化代理上下文

通过Proxy创建代理上下文,Proxy对象是ES6新增加的属性,主要用于创建一个对象的代理。该对象不支持IE11以下的浏览器,因此Vue3将不兼容老浏览器。使用该对象可以无损害地劫持对象的部分属性,并重写该属性,达到数据动态监听的目的,同时也是响应式数据的核心。

根据Proxy语法要求,传入两个参数: target和handler。其中target是需要被代理的对象,handler主要是重写或自定义拦截函数,达到拦截target对象并且实现自定义逻辑的目的。具体写法如下: 

const p = new Proxy(target, handler)

简单介绍完Proxy语法后,查看其具体实现,涉及代码如下: 

instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))

被代理对象是instance.ctx,传入的拦截函数是PublicInstanceProxyHandlers()。该函数位于$runtimecore_componentPublicInstance文件内,内部实现代码较多,核心逻辑主要实现了get、set、has方法的监听,核心代码如下: 

// $runtime-core_componentPublicInstance

export const PublicInstanceProxyHandlers: ProxyHandler<any> = {

get({ _: instance }: ComponentRenderContext, key: string) { … }

set(

{ _: instance }: ComponentRenderContext,

key: string,

value: any

): boolean { … }

has(

{

_: { data, setupState, accessCache, ctx, appContext, propsOptions }

}: ComponentRenderContext,

key: string

) { … }

}

3.4.5get方法

该方法内需要传入instance上下文和需要获取的key值。若key值不是以$开头,则根据key值判断是否缓存在accessCache中,若存在,则根据setup、data、ctx和props的顺序查询对应值并返回,若不存在则进一步判断。


let normalizedProps

if (key[0] !== '$') {

const n = accessCache![key]

if (n !== undefined) {

switch (n) {

case AccessTypes.SETUP:

return setupState[key]

case AccessTypes.DATA:

return data[key]

case AccessTypes.CONTEXT:

return ctx[key]

case AccessTypes.PROPS:

return props![key]

// default: just fallthrough

}

}

...

}

根据同样的顺序判断setup、data、props和ctx对象上的属性是否有值,若存在则直接返回,若不存在则开始全局扫描。

else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {

accessCache![key] = AccessTypes.SETUP

return setupState[key]

} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {

accessCache![key] = AccessTypes.DATA

return data[key]

} else if (

// props

(normalizedProps = instance.propsOptions[0]) &&

hasOwn(normalizedProps, key)

) {

accessCache![key] = AccessTypes.PROPS

return props![key]

} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {

accessCache![key] = AccessTypes.CONTEXT

return ctx[key]

} else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {

accessCache![key] = AccessTypes.OTHER

}

若在缓存内未找到对应key,则开始遍历使用$表示的公开属性并执行对应的逻辑。在介绍内部实现前,先查看以$开头的公开属性: 

export const publicPropertiesMap: PublicPropertiesMap = extend(

Object.create(null),

{

$: i => i,

$el: i => i.vnode.el,

$data: i => i.data,

$props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),

$attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),

$slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),

$refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),

$parent: i => getPublicInstance(i.parent),

$root: i => getPublicInstance(i.root),

$emit: i => i.emit,

$options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),

$forceUpdate: i => () => queueJob(i.update),

$nextTick: i => nextTick.bind(i.proxy!),

$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)

} as PublicPropertiesMap

)

publicPropertiesMap对象定义公开的内部属性,涉及常见的$el、$data等。

若为公开属性,并且是$attrs,则调用track收集依赖; 若是css module,则暂时不处理,直接返回; 若是用户自定义的以$开头的属性,则查询上下文ctx并返回; 若是全局属性,则查询全局对象并返回。

此处最核心的地方是使用track进行依赖的收集,具体代码实现如下: 

const publicGetter = publicPropertiesMap[key]

let cssModule, globalProperties

// public $xxx properties

if (publicGetter) {

if (key === '$attrs') {

track(instance, TrackOpTypes.GET, key)

__DEV__ && markAttrsAccessed()

}

return publicGetter(instance)

} else if (

// css module (injected by vue-loader)

(cssModule = type.__cssModules) &&

(cssModule = cssModule[key])

) {

return cssModule

} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {

// user may set custom properties to `this` that start with `$`

accessCache![key] = AccessTypes.CONTEXT

return ctx[key]

} else if (

// global properties

((globalProperties = appContext.config.globalProperties),

hasOwn(globalProperties, key))

) {

if (__COMPAT__) {

const desc = Object.getOwnPropertyDescriptor(globalProperties, key)!

if (desc.get) {

return desc.get.call(instance.proxy)

} else {

const val = globalProperties[key]

return isFunction(val) ? val.bind(instance.proxy) : val

}

} else {

return globalProperties[key]

}

}

3.4.6set方法

完成get方法处理后,继续查看set方法的实现。相较于get方法,set方法的内部逻辑判断更加简单,只要判断传入值类型并保存到对应类型属性中即可。具体实现代码如下: 

set(

{ _: instance }: ComponentRenderContext,

key: string,

value: any

): boolean {

const { data, setupState, ctx } = instance

if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {

setupState[key] = value

} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {

data[key] = value

} else if (hasOwn(instance.props, key)) {

__DEV__ &&

warn(

`Attempting to mutate prop "${key}". Props are readonly.`,

instance

)

return false

}

if (key[0] === '$' && key.slice(1) in instance) {

__DEV__ &&

warn(

`Attempting to mutate public property "${key}". ` +

`Properties starting with $ are reserved and readonly.`,

instance

)

return false

} else {

if (__DEV__ && key in instance.appContext.config.globalProperties) {

Object.defineProperty(ctx, key, {

enumerable: true,

configurable: true,

value

})

} else {

ctx[key] = value

}

}

return true

}

set方法解析出data、setupState和ctx属性,并将key的名称与解析出的属性名称进行对比。若相同,则直接保存; 若不相同,则判断key的名称与instance的props属性是否相等,若相等则结束set方法的执行,并在开发模式下抛出不能修改的警告。

完成上述判断后,若key以$开头且名称在instance上下文内,则直接返回false,结束set方法执行。若不是以$开头,则进一步判断是否为开发模式且为全局属性,若是则通过instance.appContext.config.globalProperties对象保存,并通过Object.defineProperty方法实现对应值的响应式监听。

3.4.7has方法

完成set代码解析后,继续查看has方法的实现。只需要判断对应key是否有缓存,具体代码如下: 

has(

{

_: { data, setupState, accessCache, ctx, appContext, propsOptions }

}: ComponentRenderContext,

key: string

) {

let normalizedProps

return (

accessCache![key] !== undefined ||

(data !== EMPTY_OBJ && hasOwn(data, key)) ||

(setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||

((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||

hasOwn(ctx, key) ||

hasOwn(publicPropertiesMap, key) ||

hasOwn(appContext.config.globalProperties, key)

)

}

3.4.8调用setup函数

完成代理创建后,开始调用setup函数。获取到setup函数后,根据setup的值判断是否有第二个值传入,若有则调用createSetupContext()函数初始化,若没有则直接返回null。createSetupContext()函数简化后的代码如下: 

// $runtime-core_components

export function createSetupContext(

instance: ComponentInternalInstance

): SetupContext {

const expose: SetupContext['expose'] = exposed => {

instance.exposed = exposed || {}

}

return {

get attrs() {

return attrs || (attrs = createAttrsProxy(instance))

},

slots: instance.slots,

emit: instance.emit,

expose

}

}

该方法返回4个属性,分别为attrs、slots、emit和expose。所以setup的ctx上只能获取到这4个属性。

完成ctx的处理后开始执行setup函数,通过callWithErrorHandling函数包裹执行,方便捕获异步执行时抛出的错误。完成执行后开始处理执行结果setupResult,判断是否为promise对象,若是则执行then方法,然后执行handleSetupResult()函数,将setupResult返回值作为参数传入handleSetupResult()函数; 如果不是则直接调用handleSetupResult()函数将setupResult的返回值作为参数传入。

handleSetupResult()函数内部根据传入值判断是函数还是对象。若是函数,则将该函数挂载到instance.render上; 若是对象,则通过proxyRefs()函数将对象保存在instance.setupState属性中。完成该步骤后,调用finishComponentSetup()函数处理setup完成后的其他步骤。

在查看finishComponentSetup()函数前,先简单介绍proxyRefs()函数的内部实现。由函数名称可知,该函数的主要作用是将一个对象变成响应式对象,具体代码如下: 

// reactivity_ref

export function proxyRefs<T extends object>(

objectWithRefs: T

): ShallowUnwrapRef<T> {

return isReactive(objectWithRefs)

? objectWithRefs

: new Proxy(objectWithRefs, shallowUnwrapHandlers)

}

如果需要将一个对象变为响应式,那么最核心的方法是使用new Proxy()实例,与前面介绍的响应式方法实现类似,同样传入shallowUnwrapHandlers对象,该对象内部也是通过劫持set和get方法,并利用Reflect内置对象来保存数据。

完成setup函数执行后通过finishComponentSetup()函数处理后续逻辑。在该函数内部处理render渲染函数,依次执行如下步骤。

(1) 获取render属性值; 

(2) 判断运行环境是否为SSR,若不是则进一步判断; 

(3) 是否有complie()函数,是否有template字段,是否无render渲染函数,该逻辑主要判断当前VNode是否需要将template模板字符串转换为render渲染函数,对于完整版的Vue3,会在此处注入complie()转换函数,执行template模板字符串的渲染。在7.1节将着重介绍complie的注入逻辑,介绍template模板字符串到render渲染函数的转换。

上述判断完成后,将template模板转换为VNode树,再转换为html DOM树,主要逻辑为: 

(1) 获取 template; 

(2) 根据当前上下文的设置,合并options; 

(3) 调用compile函数,将模板编译为render渲染函数。

render函数内主要处理页面渲染模板的转换,将template模板转换为对应的VNode,再将VNode转换为对应的DOM树。完成后判断是否有_rc属性,重新调用proxy实现ctx的代理函数,并且判断是否为2.x版本,通过applyOptins处理instance,兼容Vue2的语法。

3.4.9finishComponentSetup()函数

finishComponentSetup()函数内执行render渲染逻辑,将VNode转换为真实DOM结构,并将转换完成的DOM节点挂载到根节点上。

finishComponentSetup()函数逻辑如下: 

// $runtime-core_component

export function finishComponentSetup(

instance: ComponentInternalInstance,

isSSR: boolean,

skipOptions?: boolean

) {

const Component = instance.type as ComponentOptions

if (__COMPAT__) {

convertLegacyRenderFn(instance)

}

if (!instance.render) {

// SSR的实时编译由服务器端渲染器完成

// 若compile存在,且没有render渲染函数,在不是SSR的情况下开始模板编译

if (!isSSR && compile && !Component.render) {

const template =

(__COMPAT__ &&

instance.vnode.props &&

instance.vnode.props['inline-template']) ||

Component.template

if (template) {

// 判断是否为自定义元素,和渲染属性设置进行合并

const { isCustomElement, compilerOptions } = instance.appContext.config

const { delimiters, compilerOptions: componentCompilerOptions } =

Component

const finalCompilerOptions: CompilerOptions = extend(

extend(

{

isCustomElement,

delimiters

},

compilerOptions

),

componentCompilerOptions

)

if (__COMPAT__) {

// 将兼容属性传入compiler

finalCompilerOptions.compatConfig = Object.create(globalCompatConfig)

if (Component.compatConfig) {

extend(finalCompilerOptions.compatConfig, Component.compatConfig)

}

}

// 生成渲染函数

Component.render = compile(template, finalCompilerOptions)

}

}

instance.render = (Component.render || NOOP) as InternalRenderFunction

// 兼容使用'with'作用域的在运行时进行编译的渲染函数

if (installWithProxy) {

installWithProxy(instance)

}

}

// 兼容2.x版本属性

if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {

setCurrentInstance(instance)

pauseTracking()

applyOptions(instance)

resetTracking()

unsetCurrentInstance()

}

}

得到render渲染函数后,使用该render函数进行渲染。继续回到mountComponent函数逻辑内,完成setupComponent函数后,通过setupRenderEffect函数启动带副作用的render函数。这里之所以叫带副作用的render函数,是因为effect副作用函数在此处传入,具体代码如下: 

const setupRenderEffect: SetupRenderEffectFn = (

instance,

initialVNode,

container,

anchor,

parentSuspense,

isSVG,

optimized

) => {

const componentUpdateFn = () => {

if (!instance.isMounted) {

// 创建

...

} else {

// 更新

...

}

}

// 渲染时创建effect副作用函数

const effect = (instance.effect = new ReactiveEffect(

componentUpdateFn,

() => queueJob(instance.update),

instance.scope// 在组件的影响范围内跟踪它

))

const update = (instance.update = effect.run.bind(effect) as SchedulerJob)

update.id = instance.uid

// allowRecurse

// 组件渲染effects应该允许递归更新

toggleRecurse(instance, true)

// 执行update

update()

}

此步骤与effect副作用函数关联,整个代码的执行逻辑也基本清晰。在mounted挂载的时候,effect副作用函数会调用update方法,触发内部的effect副作用函数执行,该函数在组件挂载和更新的时候均会执行。

如果引入的Vue3不是runtime版本,则会在此处对compile函数进行注入。以函数注入的方式进行挂载,可以更加优雅地完成模板渲染和runtimecore核心逻辑的解构。

3.4.10总结

完成该函数的解析后,整个setup执行步骤即处理完成。由分析结果可知,该函数主要处理不同类型的情况,执行setup函数后,再调用渲染函数,将模板转换为render对象内容,以备后续使用。setup函数内使用了许多优化措施,包括通过accessCache缓存已挂载数据,加快已处理数据取值; 根据setup传入参数个数动态创建setupContext。通过proxy对象,解决Vue2遗留的响应式数据问题的操作。

本节对响应式数据的依赖收集track和派发更新trigger做了简单介绍。通过对本节的学习,读者应该对Vue3整个执行和渲染逻辑有了全面的了解。此处介绍的也是整个Vue3最核心的地方,后续大部分章节将围绕该流程进行更加深入的展开。在后面的学习中可以参考本节的实现逻辑。比如最核心的diff其实就是对比新旧VNode的过程,响应式数据就是通过new proxy实例,劫持get、set和has等方法,实现依赖收集(track)和派发更新(trigger)的过程,调用过程如图3.10所示。



图3.10调用过程


3.5update方法◆

介绍完app初始化和mounted挂载流程后,本节介绍Vue3内执行update的整体流程。在Vue3中引入VNode来处理虚拟DOM结构到真实DOM树之间的关系。update方法是通过对比新、旧VNode树的变化,找出不一致的地方调用渲染函数,对真实的DOM进行局部修改,避免大面积修改页面DOM,以及页面重绘或回流,提高渲染性能,从而以最小的代价完成页面的操作,更快地完成页面的更新。

3.5.1涉及文件

update方法涉及文件路径如表3.4所示。


表3.4update方法涉及文件路径



名称简 写 路 径文 件 路 径


setupRenderEffect$runtimecore_render$root/packages/runtimecore/src/renderer.ts
updateComponentPreRender$runtimecore_render$root/packages/runtimecore/src/renderer.ts
updateComponent$runtimecore_render$root/packages/runtimecore/src/renderer.ts
shouldUpdateComponent$runtimecore_componentRenderUtils$root/packages/runtimecore/src/componentRenderUtils.ts
processElement$runtimecore_renderer$root/packages/runtimecore/src/renderer.ts
patchChildren$runtimecore_renderer$root/packages/runtimecore/src/renderer.ts
patchProps$runtimedom_patchProp/package/runtimedom/src/patchProp.ts
patchChildren$runtimecore_renderer$root/packages/runtimecore/src/renderer.ts


3.5.2setupRenderEffect()函数

当组件内部状态变化、父组件更新子组件或触发forceUpdate时,将触发组件的update方法。该方法在组件初始化的时候已经挂载,关于组件对应方法的实现,已经在介绍render函数时有过说明。

下面查看组件更新的实现步骤,具体代码逻辑位于$runtimecore_render.ts文件内。update实现代码较多,此处先了解该方法的实现,简化后的代码如下: 

// $runtime-core_render

const setupRenderEffect: SetupRenderEffectFn = (

instance,

initialVNode,

container,

anchor,

parentSuspense,

isSVG,

optimized

) => {

// 创建执行带副作用的渲染函数并保存在update属性中

instance.update = effect(function componentEffect() {

if (!instance.isMounted) {

// 挂载组件

} else {

// 组件更新

// 组件自身发起的更新,next属性值为null

// 父组件发起的更新,next属性值为当前组件更新后VNode

let { next, vnode } = instance;

let originNext = next;

if (next) {

// 如果存在next,则需要更新组件实例相关信息

// 修正instance 和 nextVNode的指向关系

// 更新Props和Slots

updateComponentPreRender(instance, next, optimized);

} else {

next = vnode;

}

// 渲染新的子树

const nextTree = renderComponentRoot(instance);

const prevTree = instance.subTree;

instance.subTree = nextTree;

next.el = vnode.el;

// diff子树

patch(

prevTree,

nextTree,

// 排除teleport的情况,及时获取父节点

hostParentNode(prevTree.el!)!,

// 排除fragement情况,及时获取下一个节点

getNextHostNode(prevTree),

instance,

parentSuspense,

isSVG

);

next.el = nextTree.el;

}

}, EffectOptions);

上述代码的主要实现逻辑如下: 

(1) 判断是否为自身执行的更新,若有next则代表是父组件发起更新,若无则代表自身触发更新; 

(2) 修正instance和nextVNode的指向关系; 

(3) 渲染子树; 

(4) diff子树。

具体查看该函数内部实现,在修正指向关系时,会调用updateComponentPreRender()函数,并传入instance、next和optimized参数。该函数将传入的next(nextVnode)赋值到instance对象的VNode上,将instance上的next设置为null,以达到修正instance属性VNode指向的目的。完成后调用updateProps和updateSlots更新组件的props和slots对象。

3.5.3updateComponentPreRender()函数

执行当前VNode的update方法,具体代码实现如下: 

// $runtime-core_render

const updateComponentPreRender = (

instance: ComponentInternalInstance,

nextVNode: VNode,

optimized: boolean

) => {

nextVNode.component = instance

const prevProps = instance.vnode.props

instance.vnode = nextVNode

instance.next = null

updateProps(instance, nextVNode.props, prevProps, optimized)

updateSlots(instance, nextVNode.children, optimized)

pauseTracking()

// props update may have triggered pre-flush watchers.

// flush them before the render update.

flushPreFlushCbs(undefined, instance.update)

resetTracking()

}

关于props和slot的update方法,将在8.4节和8.5节详细介绍,此处暂不展开。完成指向关系修正后,继续执行后续逻辑。通过renderComponentRoot()函数渲染新的子树,使用当前子树(prevTree)和新子树(nextTree)作为参数调用patch函数,该函数主要对新旧子树进行对比和判断。patch完成后,将next.el重新赋值给当前实例next。

next.el = nextTree.el

在3.3节中曾经介绍过patch内部主要对不同的类型进行分发,组件VNode将执行processComponent()函数,该函数的内部判断传入的VNode是否为null,判断执行挂载还是更新逻辑,具体实现代码如下: 

const processComponent = (...) => {if (n1 == null) {

// 组件挂载

mountComponent(...)

} else {

// 组件更新

updateComponent(n1, n2, optimized)

}

}

3.5.4updateComponent()函数

updateComponent()函数内部判断是否需要更新,若需要更新则通过异步方式进行更新; 若不需要更新则重置实例指向,更新instance和VNode的关系后结束。具体实现代码如下: 

// $runtime-core_render

const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {

const instance = (n2.component = n1.component)!;

// 组件是否需要更新

if (shouldUpdateComponent(n1, n2, optimized)) {

instance.next = n2;

// 去除异步队列中的子组件,避免重复更新

invalidateJob(instance.update);

// 同步执行组件更新

instance.update();

} else {

// 不需要更新,只需要更新 instance和VNode的关系

n2.component = n1.component;

n2.el = n1.el;

instance.vnode = n2;

}

};

注: 因当前组件也有可能状态更改,触发更新但是还未执行,因此清理该组件的更新任务。

3.5.5shouldUpdateComponent()函数

shouldUpdateComponent()函数通过组件slots及props判断是否执行更新操作,若需要执行更新,则设置组件的next VNode并且将异步更新队列中的该组件更新任务清除,以防止重复更新。

下面简单介绍shouldUpdateComponent()函数的内部实现逻辑,代码如下: 

// $runtime-core_componentRenderUtils

export function shouldUpdateComponent(

prevVNode: VNode,

nextVNode: VNode,

optimized?: boolean

): boolean {

const { props: prevProps, children: prevChildren, component } = prevVNode

const { props: nextProps, children: nextChildren, patchFlag } = nextVNode

const emits = component!.emitsOptions

if (__DEV__ && (prevChildren || nextChildren) && isHmrUpdating) {

return true

}

// 若涉及指令或transition,则需更新

if (nextVNode.dirs || nextVNode.transition) {

return true

}

// 涉及优化的情况下,根据状态判断是否需要更新

if (optimized && patchFlag >= 0) {

…

}else{

…

}

上述代码在有patchflag优化的情况下,进一步判断优化类型,涉及动态插槽、props判断和模板编译阶段优化。具体代码实现如下: 

if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {

// 动态插槽情况

return true;

}

if (patchFlag & PatchFlags.FULL_PROPS) {

// 全量props的情况

if (!prevProps) {

// 若没有旧Props,则由新Props决定

return !!nextProps;

}

// 若都存在,则查询有无变化

return hasPropsChanged(prevProps, nextProps!);

} else if (patchFlag & PatchFlags.PROPS) {

// 在模板编译阶段优化动态props

const dynamicProps = nextVNode.dynamicProps!;

for (let i = 0; i < dynamicProps.length; i++) {

const key = dynamicProps[i];

if (nextProps![key] !== prevProps![key]) {

return true;

}

}

}

在不涉及patchflag优化的情况下(例如,手动创建render函数等),进一步对比是否有更新的情况,若有则直接强制更新,具体代码实现如下:

if (prevChildren || nextChildren) {

if (!nextChildren || !(nextChildren as any).$stable) {

return true;

}

}

// props未改变

if (prevProps === nextProps) {

return false;

}

if (!prevProps) {

// 没有旧props ---> 由新props决定

return !!nextProps;

}

if (!nextProps) {

// 存在旧props ---> 不存在新props

return true;

}

// 若新旧props都存在,则判断是否有变化

return hasPropsChanged(prevProps, nextProps);

...

return false;

以上代码主要介绍shouldUpdateComponent()函数内部实现,判断props、slot等是否需要更新的情况。根据判断结果继续查看updateComponent()函数的内部逻辑。
若不需要更新则直接调整VNode指向,结束代码执行。具体代码实现如下: 

n2.component = n1.component

n2.el = n1.el

instance.vnode = n2

若需要更新则判断是否为Suspense类型组件,若是Suspense类型组件则调用updateComponentPreRender()函数,更新slots和props后异步执行update方法; 若不是Suspense类型组件则直接执行当前实例的update方法。

3.5.6processElement()函数

组件只是某一段具体 DOM的抽象,经过不同类型的patch递归处理后,最后一次进行diff的必然是普通元素,因此直接关注普通元素的更新,即可查看整个更新逻辑的完整步骤。在patch函数中找到processElement()逻辑分支,该分支方法实现了普通元素的更新流程,具体代码如下: 

// $runtime-core_renderer

const patchElement = (

n1: VNode,

n2: VNode,

parentComponent: ComponentInternalInstance | null,

parentSuspense: SuspenseBoundary | null,

isSVG: boolean,

optimized: boolean

) => {

// 初始化变量

const el = (n2.el = n1.el!)

const oldProps = n1.props || EMPTY_OBJ

const newProps = n2.props || EMPTY_OBJ

// 更新props

patchProps(...)

// 更新children

const areChildrenSVG = isSVG && n2.type !== 'foreignObject'

patchChildren(...)

}

为更好地查看核心逻辑,上述代码暂时将钩子函数相关的代码以及针对patchFlags的优化操作省略,该函数内主要实现如下逻辑: 

(1) 更新 props; 

(2) 更新 children。

由代码逻辑可知,patchProps调用的是初始化时传递的patchProp函数,位于$runtimedom_patchProp文件内,主要是针对class和style以及指令事件等内容进行对比更新,当props更新完成后还需要对children进行更新。

3.5.7patchChildren()函数

对props和children更新完成后就开始对整个DOM结构进行更新,此处直接查看patchChildren()函数的实现代码如下: 

// $runtime-core_renderer

const patchChildren: PatchChildrenFn = (

n1,

n2,

container,

anchor,

parentComponent,

parentSuspense,

isSVG,

optimized = false

) => {

// 初始化变量

const c1 = n1 && n1.children;

const prevShapeFlag = n1 ? n1.shapeFlag : 0;

const c2 = n2.children;

const { patchFlag, shapeFlag } = n2;

// children 存在3种情况: 文本节点、数组、无children

if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {

// 新children文本类型的子节点

if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {

// 旧children是数组,直接卸载

unmountChildren(c1 as VNode[], parentComponent, parentSuspense);

}

if (c2 !== c1) {

// 新旧都是文本类型,但是文本内容不相同直接替换

hostSetElementText(container, c2 as string);

}

} else {

if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {

// 旧children是数组

if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {

// 新children是数组

patchKeyedChildren(

c1 as VNode[],

c2 as VNodeArrayChildren,

container,

anchor,

parentComponent,

parentSuspense,

isSVG,

optimized

);

} else {

// 不存在新children,直接卸载旧children

unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true);

}

} else {

// 旧children可能是文本或者为空

// 新children可能是数组或者为空

if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {

// 如果旧children是文本类型,那么无论新children是哪种类型都需要先清除文本内容

hostSetElementText(container, "");

}

// 此时原DOM内容应该为空

if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {

// 如果新children为数组,则直接挂载

mountChildren(

c2 as VNodeArrayChildren,

container,

anchor,

parentComponent,

parentSuspense,

isSVG,

optimized

);

}

}

}

};

创建VNode时会对children进行标准化处理,在diff children的时候可以只考虑children的类型为数组、文本和空这3种情况。结合代码来看,其中if条件的设置也很巧妙,既包含所有情况,又能清晰地拆分出挂载、删除、对比3个操作。

满足diff条件将会执行patchKeyedChildren函数,关于该函数将在4.3节中对diff算法进行详细介绍。新旧VNode的diff操作完成后,对映射的DOM元素进行调整更新,完成组件的更新步骤。

3.5.8总结

update方法执行时,整个执行逻辑与mount类似,但在这个过程中将会经历新、旧VNode的diff过程,找出更新的内容点以最小的代价对DOM树进行更新操作。通过对本节的学习,可以掌握Vue3内部更新逻辑的处理,帮助读者理解path和diff的概念。

3.6unmount方法◆


前面介绍了mount(挂载)方法和update(更新)方法,本节将介绍unmount方法的实现。

3.6.1涉及文件

unmount方法涉及文件路径如表3.5所示。


表3.5unmount方法涉及文件路径


名称简 写 路 径文 件 路 径


unmount$runtimecore_render$root/package/runtimecore/src/renderer.ts
unmountChildren$runtimecore_render$root/package/runtimecore/src/renderer.ts


3.6.2baseCreateRenderer()函数

unmount方法同样位于$runtimecore_render文件内,在baseCreateRenderer()函数内部定义,该函数传入5个参数,分别为vnode、parentComponent、parentSuspense、doRemove和optimized,传入参数类型如下: 

type UnmountFn = (

vnode: VNode,

parentComponent: ComponentInternalInstance | null,

parentSuspense: SuspenseBoundary | null,

doRemove?: boolean,

optimized?: boolean

) => void

根据传入参数可知,前面3个为必传参数,主要涉及vnode结构、parentComponent组件和parentSuspense组件。该函数内涉及内容较多,函数内主要涉及逻辑如下: 

若涉及ref数据,则将其设置为空; 

若为keepalive缓存组件,则卸载; 

若为component类型组件,则调用unmountComponent()函数卸载; 

若为suspense组件,则卸载; 

若为fragments、array类型组件,则调用unmountChildren()函数卸载; 

若为teleport,则在卸载组件的同时卸载子节点; 

若为动态子节点,则调用unmountChildren()函数卸载; 

若为fragment类型,则调用unmountChildren()函数卸载; 

根据doRemove参数判断是否卸载整个VNode。

上述逻辑根据类型判断待卸载VNode类型,并执行不同的卸载逻辑。

3.6.3ref数据

判断VNode是否有ref,若有则调用setRef,传入null值,清理该ref数据。具体代码实现如下: 

if (ref != null) {

setRef(ref, null, parentSuspense, null)

}

关于setRef()的处理,可以在$runtimecore_rendererTemplateRef文件中看到具体实现,简单查看该函数的处理流程。

若传入的ref为数组类型,则对ref进行递归遍历,具体实现代码如下: 

if (isArray(rawRef)) {

rawRef.forEach((r, i) =>

setRef(

r,

oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef),

parentSuspense,

vnode,

isUnmount

)

)

return

}

若VNode属于异步加载组件,且状态不是卸载,则直接跳过。因为异步组件的引用在内部组件,所以此处直接返回即可。

if (isAsyncWrapper(vnode) && !isUnmount) {

return

}

若未传入VNode,则直接设置value为null,否则判断是否为组件类型,若是组件类型则提取组件的参数,若两者均不是则直接提取el参数并设置为value。具体代码实现如下: 

const refValue =

vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT

? getExposeProxy(vnode.component!) || vnode.component!.proxy

: vnode.el

const value = isUnmount ? null : refValue

完成value值设置后,提取oldRef和refs以及setup状态,若存在oldRef则需要进行卸载。具体代码实现如下: 

// unset old ref  

if (oldRef != null && oldRef !== ref) {

if (isString(oldRef)) {

refs[oldRef] = null

if (hasOwn(setupState, oldRef)) {

setupState[oldRef] = null

}

} else if (isRef(oldRef)) {

oldRef.value = null

}

}

获取当前rawRef的ref属性,判断该属性的类型,如果是非空值,则直接设置为-1,标识卸载; 如果是空值,则根据不同类型处理ref属性。具体代码实现如下: 

// 如果是函数,则直接执行

if (isFunction(ref)) {

callWithErrorHandling(ref, owner, ErrorCodes.FUNCTION_REF, [value, refs])

} else {

const _isString = isString(ref)

const _isRef = isRef(ref)

// 如果是string或Ref类型

if (_isString || _isRef) {

const doSet = () => {

...

}

if (value) {

// 如果是非空值,则设置id为-1(标识卸载)

;(doSet as SchedulerJob).id = -1

queuePostRenderEffect(doSet, parentSuspense)

} else {

doSet()

}

} else if (__DEV__) {

warn('Invalid template ref type:', ref, `(${typeof ref})`)

}

}

如果是字符串类型,则判断是否有value值,若有则通过queuePostRenderEffect()函数排队执行doSet()函数,若无value值则直接执行doSet()函数。该函数主要设置refs内对应的ref属性值。具体代码实现如下: 

const doSet = () => {

// refInFor标识

if (rawRef.f) {

// 判断ref的类型获取值

const existing = _isString ? refs[ref] : ref.value

// 如果标识卸载

if (isUnmount) {

// 如果是数组,则通过remove删除

isArray(existing) && remove(existing, refValue)

} else {

// 如果不是数组

if (!isArray(existing)) {

// 如果是字符串

if (_isString) {

refs[ref] = [refValue]

} else {

// 如果是其他情况

ref.value = [refValue]

// 判断ref是否有key

if (rawRef.k) refs[rawRef.k] = ref.value

}

// 如果是数组但是不包含refValue值

} else if (!existing.includes(refValue)) {

existing.push(refValue)

}

}

} else if (_isString) { // 如果是字符串类型

refs[ref] = value

if (hasOwn(setupState, ref)) {

setupState[ref] = value

}

} else if (isRef(ref)) { // 如果是ref类型

ref.value = value

if (rawRef.k) refs[rawRef.k] = value

} else if (__DEV__) { // 无效的ref类型

warn('Invalid template ref type:', ref, `(${typeof ref})`)

}

}

该函数根据传入参数来判断是设置ref还是清理ref。若传入的是null,则对应value=null,将ref值设置为null,以达到清理ref数据的目的。

3.6.4keepalive组件

完成setRef逻辑后,根据代码执行顺序继续查看后续处理。首先判断是否为keepalive组件,若是则在parentComponent实例上删除该组件缓存。主要代码如下: 

if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {

;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)

return

}

deactivate方法内主要调用move函数对元素节点进行移除,完成后调用对应钩子函数即可。涉及代码如下: 

sharedContext.deactivate = (vnode: VNode) => {

const instance = vnode.component!

// 传入null,进行清理

move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)

// 通过异步队列的方式进行钩子函数的回调

queuePostRenderEffect(() => {

if (instance.da) {

invokeArrayFns(instance.da)

}

const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted

if (vnodeHook) {

invokeVNodeHook(vnodeHook, instance.parent, vnode)

}

// 标识为卸载状态

instance.isDeactivated = true

}, parentSuspense)

if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {

// Update components tree

devtoolsComponentAdded(instance)

}

}

keepalive组件调用deactivate卸载VNode后,结束unmount操作。若不是keepalive组件,则进一步判断组件类型。

3.6.5component组件

若为component类型,则调用unmountComponent()函数处理,涉及代码如下: 

if (shapeFlag & ShapeFlags.COMPONENT) {

unmountComponent(vnode.component!, parentSuspense, doRemove)

} else { ... }

unmountComponent()函数内包括如下逻辑: 

(1) 停止该组件上的effects副作用函数执行; 

(2) 停止update队列并且调用组件卸载函数unmount; 

(3) 将当前组件标记为已卸载; 

(4) 处理suspense类型组件。

该函数执行完成后,对应组件卸载即完成,主要代码实现如下: 

const unmountComponent = (

instance: ComponentInternalInstance,

parentSuspense: SuspenseBoundary | null,

doRemove?: boolean

) => {

const { bum, effects, update, subTree, um } = instance

// beforeUnmount钩子函数回调触发

if (bum) {

invokeArrayFns(bum)

}

// 停止所有副作用函数

if (effects) {

for (let i = 0; i < effects.length; i++) {

stop(effects[i])

}

}

// 如果组件在异步设置之前被卸载,则update可能为空

if (update) {

stop(update)

unmount(subTree, instance, parentSuspense, doRemove)

}

// unmounted钩子函数回调

if (um) {

queuePostRenderEffect(um, parentSuspense)

}

queuePostRenderEffect(() => {

instance.isUnmounted = true

}, parentSuspense)

// 判断suspense组件情况,从父suspense组件内删除,并且收集的依赖减少1,如果是最后一个,则

// 直接执行resolve表示结束; 如果还有其他suspense,则忽略

if (

__FEATURE_SUSPENSE__ &&

parentSuspense &&

parentSuspense.pendingBranch &&

!parentSuspense.isUnmounted &&

instance.asyncDep &&

!instance.asyncResolved &&

instance.suspenseId === parentSuspense.pendingId

) {

parentSuspense.deps--

if (parentSuspense.deps === 0) {

parentSuspense.resolve()

}

}

}

3.6.6suspense组件

如果是suspense类型组件,则调用suspense对象上的unmount卸载。

if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {

vnode.suspense!.unmount(parentSuspense, doRemove)

return

}

3.6.7telport组件

完成该处理后继续判断组件是否为telport类型,若为telport类型组件,则同上述处理,调用telport类型的remove方法进行卸载。然后根据传入的doRemove参数判断是否调用remove方法,删除对应DOM节点。完成后执行unmount钩子函数,涉及代码逻辑如下: 

if (shapeFlag & ShapeFlags.TELEPORT) {

(vnode.type as typeof TeleportImpl).remove(

vnode,

parentComponent,

parentSuspense,

optimized,

internals,

doRemove

)

}

3.6.8动态子组件等

如果是动态子组件,且是特定fragment或数组类型,则直接调用unmountChildren()函数,传入不同参数进行处理,核心代码如下: 

if (

dynamicChildren &&

// #1153: fast path should not be taken for non-stable (v-for) fragments

(type !== Fragment ||

(patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))

) {

// fast path for block nodes: only need to unmount dynamic children.

unmountChildren(

dynamicChildren,

parentComponent,

parentSuspense,

false,

true

)

} else if (

(type === Fragment &&

(patchFlag & PatchFlags.KEYED_FRAGMENT ||

patchFlag & PatchFlags.UNKEYED_FRAGMENT)) ||

(!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN)

) {

unmountChildren(children as VNode[], parentComponent, parentSuspense)

}

在unmountChildren()函数内,无论是fragment或array类型,均通过for循环逐个调用unmount函数卸载。

3.6.9总结

unmount函数内部主要针对VNode的类型执行不同的卸载逻辑。熟悉本节内容有助于理解组件的卸载对不同类型的处理方法,进一步理解组件的挂载、更新和卸载的整体流程。