第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$runtimedom_index$root/packages/runtimedom/src/index.ts ensureRenderer$runtimedom_index$root/packages/runtimedom/src/index.ts baseCreateRenderer$runtimecore_render$root/packages/runtimecore/src/render.ts render$runtimecore_render$root/packages/runtimecore/src/render.ts createAppAPI$runtimecore_apiCreateApp$root/packages/runtimecore/src/apiCreateApp.ts 3.2.2调用createApp()函数 调用createApp()函数返回一个应用实例,它的实现在$runtimedom_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()函数的主线逻辑,调用$runtimecore_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()函数,它位于$runtimecore_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$runtimecore_apiCreateApp$root/packages/runtimecore/src/apiCreateApp.ts processComponent$runtimecore_renderer$root/packages/runtimecore/src/renderer.ts setupComponent$runtimecore_components$root/packages/runtimecore/src/component.ts setupStatefulComponent$runtimecore_components$root/packages/runtimecore/src/component.ts finishComponentSetup$runtimecore_components$root/packages/runtimecore/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的形式呈现。该对象声明位置在$runtimecore_vnode文件内,具体声明及含义可在4.1.2节查看。 对虚拟DOM和VNode有简单了解后,再回到$runtimecore_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的创建,该函数位于$runtimecore_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函数的实现。代码位于$runtimecore_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()函数,根据代码执行逻辑找到该函数的实现。该函数位于$runtimecore_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$runtimecore_render$root/package/runtimecore/src/render.ts setupComponent$runtimecore_components$root/package/runtimecore/src/component.ts PublicInstanceProxyHandlers$runtimecore_componentPublicInstance$root/package/runtimecore/src/componentPublicInstance.ts createSetupContext$runtimecore_components$root/package/runtimecore/src/component.ts reactivity/src/ref.ts$reactivity_ref$root/package/reactivity/ref.ts finishComponentSetup$runtimecore_Component$root/packages/runtimecore/src/ component.ts 3.4.1涉及文件 setup函数是Vue3新推出的函数,该函数将用来承载composition API,同时替代beforeCreated和created生命周期函数。Vue3内大多数方法和函数均可以在该函数内使用。该函数仅会在组件启动的时候执行一次,内部会执行初始化,并确认组件与视图的数据、行为和作用。setup函数作为桥梁存在,连接模板渲染、数据处理和响应式数据监听。 3.4.2mountComponent()函数 在前面介绍mounted挂载执行逻辑时,提到内部将会触发setupComponent()函数,在该函数内创建好instance后,将启动setup处理内部暴露和声明的内容。setup处理代码位于$runtimecore_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()函数,该函数位于$runtimecore_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()。该函数位于$runtimecore_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函数进行注入。以函数注入的方式进行挂载,可以更加优雅地完成模板渲染和runtimecore核心逻辑的解构。 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$runtimecore_render$root/packages/runtimecore/src/renderer.ts updateComponentPreRender$runtimecore_render$root/packages/runtimecore/src/renderer.ts updateComponent$runtimecore_render$root/packages/runtimecore/src/renderer.ts shouldUpdateComponent$runtimecore_componentRenderUtils$root/packages/runtimecore/src/componentRenderUtils.ts processElement$runtimecore_renderer$root/packages/runtimecore/src/renderer.ts patchChildren$runtimecore_renderer$root/packages/runtimecore/src/renderer.ts patchProps$runtimedom_patchProp/package/runtimedom/src/patchProp.ts patchChildren$runtimecore_renderer$root/packages/runtimecore/src/renderer.ts 3.5.2setupRenderEffect()函数 当组件内部状态变化、父组件更新子组件或触发forceUpdate时,将触发组件的update方法。该方法在组件初始化的时候已经挂载,关于组件对应方法的实现,已经在介绍render函数时有过说明。 下面查看组件更新的实现步骤,具体代码逻辑位于$runtimecore_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函数,位于$runtimedom_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$runtimecore_render$root/package/runtimecore/src/renderer.ts unmountChildren$runtimecore_render$root/package/runtimecore/src/renderer.ts 3.6.2baseCreateRenderer()函数 unmount方法同样位于$runtimecore_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()的处理,可以在$runtimecore_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的类型执行不同的卸载逻辑。熟悉本节内容有助于理解组件的卸载对不同类型的处理方法,进一步理解组件的挂载、更新和卸载的整体流程。