Vue3组件初始化流程分析

news2024/11/15 11:11:42

本文主要来分析 vue3 组件的初始化(基于runtime-core(核心运行时)包),将从createAppmount 等常用 API 入手来分析组件的挂载、普通元素的挂载流程。

createApp

1、创建一个应用实例。使用方式如下:

import { createApp } from 'vue'
import App from './App.vue'

const rootContainer = document.querySelector("#app");
createApp(App).mount(rootContainer) 

2、内部实现:

function createRenderer(options) {...function render(vnode, container) {patch(null, vnode, container)}function patch(n1, n2, container) { ...}...return {createApp: createAppAPI(render),}
}

function createAppAPI(render) {return function createApp(rootComponent) {const vnode = createVNode(rootComponent);return {mount(rootContainer) {render(vnode, rootContainer)}};};
}

function createVNode(type, props, children) {const vnode = {type,props,children,component: null,key: props && props.key,shapeFlag: typeof type === "string" ? ShapeFlags.ELEMENT : ShapeFlags.STATEFUL_COMPONENT,el: null,};if (typeof children === "string") {vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;} else if (Array.isArray(children)) {vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;}return vnode;
}

export const enum ShapeFlags { // 基于位运算确保代码运行更高效ELEMENT = 1, // 00000001 1FUNCTIONAL_COMPONENT = 1 << 1, // 00000010 2STATEFUL_COMPONENT = 1 << 2, // 00000100 4TEXT_CHILDREN = 1 << 3, ARRAY_CHILDREN = 1 << 4,SLOTS_CHILDREN = 1 << 5,TELEPORT = 1 << 6,SUSPENSE = 1 << 7,COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,COMPONENT_KEPT_ALIVE = 1 << 9,COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

const renderer = createRenderer({ // 传入dom操作相关方法,mountElement 会有说明createElement,patchProp,insert,remove,setElementText,
})



export function createApp(...args) {return renderer.createApp(...args);
} 

上述代码主要做了如下几件事:1、定义 createRenderer 函数(用来创建一个自定义渲染器,通过提供平台特定的节点创建以及更改 API,用户可以在非 DOM 环境中也享受到 Vue 核心运行时的特性)。内部定义 render 函数,createRenderer 执行完后返回一个对象。 对象键名为 createApp ,键值是通过执行 createAppAPI 函数的返回值。

2、createAppAPI 是一个高阶函数,接收 render 函数作为参数,执行会返回 createApp 函数。

3、createApp 接收根组件 rootComponent 作为参数,首先会把 rootComponent 通过 createVnode 转化成该组件对应的 Vnode,然后返回 mount 函数用来挂载应用。

4、createVnode 会创建一个 Vnode, 该 Vnode 上会有 typepropschildrenkeyel(虚拟 DOM 到真实 DOM 的映射,diff 时会用到)、shapeFlag(标识节点类型)等。注: shapeFlag 基于二进制位运算,代码运行更高效。

4、mount 接收 rootContainer 作为参数 (它可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串),执行 mount 时会执行上述传入的 render 函数, 并把组件 VnoderootContainer 作为参数传入。

5、执行 createRenderer 返回 renderer 对象,对外暴露 createApp。外界调用 createApp 时该函数内部会调用 renderer.createApp,调用 mount 时最终会调用 render 函数执行挂载流程。

render

function render(vnode, container) {...
} 

先举一个例子,假设有以下一段代码:

App.vue

import { h, ref } from "vue";
// h 是 createVnode 函数的简写,内部会调用 createVnode 函数,后面会有说明

export const App = {name: "App",setup() {const count = ref(0);const onClick = () => {count.value++;}return {count,onClick}},render() {return h( // h 为 createVnode 的简写,内部会执行 createVnode"div", { id: "root" }, [h("div", {}, "count:" + this.count),h("button", { onClick: this.onClick }, "click")])}
} 

main.js

import { createApp } from 'vue'
import App from './App.vue'

const rootContainer = document.querySelector("#app");
createApp(App).mount(rootContainer) 

调用 createVnode 后生成的 Vnode 如下

接着执行 render 函数把组件 VnoderootContainer 传入。

patch

render 函数接收 VnoderootContainer 作为参数,内部会执行 patch 函数。由于 patch 函数在组件初始化和更新的时候都要执行,它接收n1(老的Vnode),n2(新的Vnode)作为参数。由于是首次渲染,n1null

function render(vnode, container) {patch(null, vnode, container, null, null)
}

function patch(n1, n2, container, parentComponent, anchor) { const { type, shapeFlag } = n2switch (type) {case Fragment:processFragment(n1, n2, container, parentComponent, anchor)break;case Text:processText(n1, n2, container)breakdefault:if (shapeFlag & ShapeFlags.ELEMENT) {processElement(n1, n2, container, parentComponent, anchor)} else if (shapeFlag & ShapeFlags.COMPONENT) {processComponent(n1, n2, container, parentComponent, anchor)} else if(shapeFlag & ShapeFlags.TELEPORT){ // 执行 teleport 逻辑... }break}
} 

patch 函数内部会根据 Vnodetype 来执行不同的处理逻辑。如果是 Fragment,会执行 processFragment;如果是 Text,会执行 processText。如果是 ELEMENT,会执行 processElement,如果是 COMPONENT, 会执行 processComponent,如果是 TELEPORT,会执行 teleport 处理逻辑。

由于我们传入的是组件类型,type 为组件配置对象,此时走到 processComponent 的处理逻辑

processComponent

function processComponent(n1,n2: any,container: any,parentComponent,anchor
 ) {if (!n1) {mountComponent(n2, container, parentComponent, anchor)} else {updateComponent(n1, n2)}
} 

由于 n1 不存在,此时执行 mountComponent

mountComponent

function mountComponent(initialVNode, // n2container,parentComponent,anchor
) {const instance = (initialVNode.component = createComponentInstance(initialVNode,parentComponent))setupComponent(instance)setupRenderEffect(instance, initialVNode, container, anchor)
} 

mountComponent 会做如下操作:

一、创建组件实例

通过 createComponentInstance 生成组件实例 instance,并把 instance 赋值给 n2.component 属性,供组件更新使用。

function createComponentInstance(vnode, parent) {const component = {vnode,type: vnode.type,next: null,setupState: {},props: {},slots: {},provides: parent ? parent.provides : {}, // 依赖注入parent, // 父元素,构建组件父子关系isMounted: false, // 组件是否已挂载subTree: {}, // 组件的虚拟 DOMemit: () => {}}component.emit = emit.bind(null, component) as any;return component;
} 

instance 结构如下:

二、丰富组件实例

1、 执行 setupComponent 函数并把 instance 传入。setupComponent 函数主要做如下操作:初始化实例 的 propsslots 属性。

function setupComponent(instance) {initProps(instance, instance.vnode.props) // 初始化propsinitSlots(instance, instance.vnode.children) // 初始化slotssetupStatefulComponent(instance);
} 

2、执行 setupStatefulComponent

function setupStatefulComponent(instance) {const Component = instance.type;// 创建proxy,调用 redner 函数的时候传入, 并把 render 函数的 this 指向 proxy。在 render 函数内部访问 this 时访问的是 instanceinstance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers);const { setup } = Component;if (setup) {setCurrentInstance(instance);const setupResult = setup(shallowReadonly(instance.props), {emit: instance.emit,});setCurrentInstance(null);handleSetupResult(instance, setupResult);}
}

let currentInstance = null;

function getCurrentInstance() { // 必须在 setup 函数内使用return currentInstance;
}

export function setCurrentInstance(instance) {currentInstance = instance;
} 

3、setupStatefulComponent 函数主要做以下几件事情:

(1)创建 proxy, 并挂载到 instance 上。

(2)判断组件有没有定义 setup,有的话会调用 setCurrentInstance 把全局的 currentInstance 赋值为当前的 instance。接着执行 setup 函数, 并把 instance.props 作为第一个参数传入。 第二个参数是一个对象,这里暂时只传入 emit,并用 setupResult 接收 setup 函数的返回值。setup 执行完后,把全局的 currentInstance 置空 。

(3)执行 handleSetupResultsetupResult 传入 。如果 setupResult 是对象,此时会将 instance.setupState 指向脱 ref 后的 setupResult(好处: 避免用户多写 .value 属性),然后执行 finishComponentSetup

function handleSetupResult(instance, setupResult: any) {if (typeof setupResult === "object") {instance.setupState = proxyRefs(setupResult);}finishComponentSetup(instance);
} 

(4).finishComponentSetup 主要做如下操作。

如果用户没有配置 render 函数, 此时会把template 通过 compiler 编译为可执行的 render 函数,并将 render 函数赋值给instance.render

function finishComponentSetup(instance: any) {const Component = instance.type;if (compiler && !Component.render) {if (Component.template) {Component.render = compiler(Component.template);}}instance.render = Component.render;
} 
三、创建副作用渲染函数

接下来会执行副作用渲染函数 setupRenderEffect,在这个函数内会使用 响应式API effect创建副作用函数 componentEffect。这里只需要简单的理解为,当组件内的数据改动时这个由effect 包裹的 componentEffect 就会重新调用,通过 instance.isMounted 属性来判断当前组件是首次渲染还是更新。

1、如果是首次渲染,会把上一步生成的 proxy 取出。接着执行 render 函数( 首先矫正 render 函数 this 指向,把 this 指向 proxy, 然后传入 proxy),并用 instance.subTree 来接收 render 函数的返回值。

2、 再次调用 patch 函数,第一个参数为null, 然后传入 subTree

3、patch 函数执行完后,用 subTree.el 赋值 n2.el, 并且把 instance.isMounted 置为 true

function setupRenderEffect(instance, initialVNode, container, anchor) {instance.update = effect(() => {if (!instance.isMounted) { // 初次渲染const { proxy } = instance;const subTree = (instance.subTree = instance.render.call(proxy, proxy));patch(null, subTree, container, instance, anchor);initialVNode.el = subTree.el;instance.isMounted = true;} else { // updated...}}...
 )
} 

以上面的 App.vue 为例,由于我们配置了 render 函数, 会生成如下 Vnode

-------------------------------------------------------------------------------------------------------------------------------------------------

再次执行 patch 函数时,由于传入的 Vnodetype 是普通元素(此处是 div),此时会走到 processElement 的逻辑

processElement

function processElement(n1, n2, container, parentComponent, anchor) {if (!n1) { // 挂载mountElement(n2, container, parentComponent, anchor);} else { // 更新patchElement(n1, n2, container, parentComponent, anchor);}
} 

由于 n1 不存在, 此时会执行 mountElement 的逻辑,并把 n2 作为参数传入。

mountElement

function mountElement(vnode, container, parentComponent, anchor) {const el = (vnode.el = hostCreateElement(vnode.type)) // divconst { children, shapeFlag } = vnode;// childrenif (shapeFlag & ShapeFlags.TEXT_CHILDREN) {el.textContent = children;} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {mountChildren(vnode.children, el, parentComponent, anchor);}// propsconst { props } = vnode;for (const key in props) {const val = props[key];hostPatchProp(el, key, null, val);}hostInsert(el, container, anchor);
} 

mountElement 会执行以下操作:

1、基于 vnode.type 使用 hostCreateElement 创建对应的 dom 节点,并将其赋值给vnode.elel

hostCreateElement 是从哪里来的呢? 当我们执行 createRenderer 时会传入与平台相关的操作方法,然后在 createRenderer 内部解构了这些方法。

function createElement(type) {return document.createElement(type)
}

function insert(child, parent, anchor = null) {parent.insertBefore(child, anchor) // anchor 为 null 相当于 appendChild 
}

function remove(child) {const parent = child.parentNodeif (parent) {parent.removeChild(child)}
}

function setElementText(el, text) {el.textContent = text
}

function patchProp(el, key, prevVal, nextVal) {const isOn = (key: string) => /^on[A-Z]/.test(key); // 注册事件 onClickif (isOn(key)) {const event = key.slice(2).toLowerCase();el.addEventListener(event, nextVal);} else {if (nextVal === undefined || nextVal === null) {el.removeAttribute(key);} else {el.setAttribute(key, nextVal);}}
}

const renderer = createRenderer({createElement,patchProp,insert,remove,setElementText
})

function createRenderer(options) {const {createElement: hostCreateElement,patchProp: hostPatchProp,insert: hostInsert,remove: hostRemove,setElementText: hostSetElementText} = options
} 

2、取出 childrenshapeFlag。如果 children 是文本节点,直接将 children 赋值给 el.textContent;如果是数组,会执行 mountChildren。由于此处的 chidren 为数组,会调用 mountChildrenchildren 和 新创建的 el 传入(避免混淆,此处称为 父div)。

3、mountChildren 会遍历传入的 children,拿到每一个 子Vnode,再次调用 patch 方法(深度优先递归),把每一个 vnode父div 作为参数传入。

function mountChildren(children, container, parentComponent, anchor) {children.forEach((v) => {patch(null, v, container, parentComponent, anchor);})
} 

由于 children 长度为2,我们先来分析第1个:

再次调用 patch 函数,由于传入的 type 还是 div(截图如上), 此时还是会走 processElement -> mountElement 的逻辑,然后创建真实 DOM 元素(此处简称 子div)。由于此时的 children 为文本( count: 0),会走到 el.textContent = children 逻辑。由于我们没有传递 props,此时会调用 hostInsert 方法把 子div 添加到 父div 中。

第2个:由于传入的 typebutton(截图如上), 此时依然会走 processElement -> mountElement 的逻辑,然后创建真实 DOM 元素(此处简称 子button)。由于此时的 children 为文本(click),依然会走到 el.textContent = children 逻辑。然后调用hostPatchPropprops 做处理(最终会给 button 添加点击事件)。最后依然调用 hostInsert 方法把 子button 添加到 父div 中。

5、处理完 children 后,此时会回溯到父元素的处理流程中,由于我们传递了 { id: "root" }props 属性,此时会调用 hostPatchProp父div 添加该属性,最后调用 hostInsert 方法把 父div 添加到 div#app 中。至此就完成了元素的 mount 流程。

总结

组件的挂载流程图如下:

以上是对组件挂载流程的分析,下一篇我们来分析组件更新的流程。

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/171693.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

进制转换(二进制、八进制、十进制、十六进制)超详细版

今天来总结一下各种进制转换问题&#xff0c;详细齐全易于理解&#xff0c;希望对你有帮助哦&#xff01; 各种进制之间的相互转换 先从我们最熟悉的十进制入手吧&#xff0c;其他进制与十进制的转换方法都是一样的。 整型有4种进制形式&#xff1a; 1.十进制&#xff1a; …

[ChatGPT]

最近hatGPT火爆全宇宙&#xff0c;几乎所有圈内人都在谈论这个美国人工智能公司OpenAI发布免费机器人对话模型ChatGPT&#xff08;GPT-3.5系列&#xff09;&#xff0c;模型中首次采用RLHF&#xff08;从人类反馈中强化学习&#xff09;方式。模型目前处于测试阶段&#xff0c;…

Windows 服务器刷题(2)(带答案)

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.刷题 前言 本章将会讲解Windows服务器刷题&#xff08;2&#xff09; 一…

新年礼物已收到!2022 Apache IoTDB Commits 数量排名 3/351!

社区喜报&#xff01;据 The Apache Software Foundation 官方 Projects Statistics&#xff08;项目信息统计网站&#xff09;的实时数据显示&#xff0c;Apache IoTDB 在过去 12 个月&#xff08;即 2022 年度&#xff09;共发表 6829 Commits&#xff0c;排名 2022 年度 Apa…

C++ Primer笔记——默认移动操作、移动迭代器、左右值引用成员函数、标准库仿函数、function包装器

目录 一.P476 合成的移动操作 二.P480 移动迭代器 三.P483 右值和左值引用成员函数 四.P510 标准库定义的仿函数 五.P512 标准库function类型&#xff08;包装器&#xff09; 一.P476 合成的移动操作 什么时候会有默认的移动构造和移动赋值函数&#xff0c;需满足以下几点…

Java设计模式-中介者模式Mediator

介绍 中介者模式&#xff08;Mediator Pattern&#xff09;&#xff0c;用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用&#xff0c;从而使其耦合松散&#xff0c;而且可以独立地改变它们之间的交互。中介者模式属于行为型模式&#xff0c;使代…

linux搭建webapp实战

首先介绍下linux&#xff0c;linux因其开源&#xff0c;定制化高&#xff0c;安全等原因&#xff0c;成为了目前web应用部署首选的操作系统&#xff0c;linux操作系统有很多版本&#xff0c;常见的有centos&#xff0c;debian&#xff0c;RHLE&#xff0c;redhat&#xff0c;乌…

【Linux】gcc/g++编译器、make/Makefile自动化构建工具

作者&#xff1a;小卢 专栏&#xff1a;《Linux》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 目录 1.gcc/c的概念&#xff1a; 2.程序编译过程详解&#xff1a; 2.1程序编译过程&#xff1a; 2.…

微电网(风、光、储能、需求响应)【Simulink 仿真实现】

目录 1 展现 2 典型几个介绍 2.1 采用PR的三相逆变器电压控制 2.2 太阳能直流微电网系统 2.3 主电网故障时的交流微电网性能 2.4 混合光伏、双馈发电和电池能源的微电网集成 3 写在后面 4 完整资源 1 展现 随便打开一个&#xff0c;就以第一个&#xff08;采用PID的三…

【GD32F427开发板试用】macOS/Linux系统开发环境搭建(开发、编译、烧录、调试)

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;HonestQiao 我日常的工作环境使用的是macOS系统或者Linux系统&#xff0c;万不得已才使用Windows。 GD32官方提供了Keil MDK-ARM和IAR两款工具…

如何使用Dx命令将jar打包成Dex,需要使用D8命令

使用dx打包的完整命令&#xff0c;将D:\ssh1.0.jar打包成MyClass.dex&#xff0c;MyClass.dex将会输出在当前CMD命令窗口的目录下。dx --dex --no-strict --min-sdk-version26 --outputMyClass.dex D:\ssh1.0.jar运行结果&#xff1a;不要使用dx命令&#xff0c;打出来的dex可以…

NestJS 项目实战 技术选型 (文末附视频)

前言 通过上一章的学习&#xff0c;我们了解了网关系统&#xff0c;并且针对要做的功能做了项目架构设计与需求拆解。 那在一个项目正式开发之前&#xff0c;我们还需要做一个技术调研&#xff0c;从开发框架、使用的工具、数据库等等进行一系列的预研&#xff0c;避免在业务…

嘿~ 基于分布式架构的技术交流社区(WhiteHoleV0.7)即将竣工!

文章目录前言项目介绍WhiteHole期望立项作者功能/模块简介用户模块问答模块社区模块博文模块Next前言 拖更&#xff0c;拖延了这么久&#xff0c;耗时超过3个月的项目&#xff0c;WhiteHoleV0.7 版本即将迎来最后的收尾工作。当然考虑到服务成本&#xff0c;和开发进度&#x…

【以太网硬件十九】SGMII到底能不能直接出光模块?

SGMII接口到底能不能直接出光模块&#xff1f;先说结论&#xff1a;我认为可以&#xff0c;但是有限制&#xff0c;此时的光口只支持强制模式&#xff0c;不支持自协商。如果对端是1000base-X接口&#xff0c;对端也需要把自协商关闭才能与SGMII直出的光口建立连接。为什么SGMI…

你是真的“C”——C语言详解求素数n种境界~

详解C语言函数模块知识(下篇&#xff09;&#x1f60e;前言&#x1f64c;必备小知识~&#x1f618;C语言详解《试除法》求解素数&#x1f64c;试除法第一层境界~ &#x1f60a;试除法境界2~&#x1f60a;试除法境界3~&#x1f60a;试除法境界4~&#x1f60a;C语言详解《筛选法》…

为何限制英伟达出售GPU芯片?中国的AI技术领先,让美国怕了

2022年下半年美国突然要求GPU芯片领军者NVIDIA停止对中国出售高端GPU芯片&#xff0c;此举让人疑惑不解&#xff0c;近期日本媒体《日本经济新闻》与荷兰的一家学术信息巨头的分析给出了答案&#xff0c;中国在AI技术上已领先于美国&#xff0c;而NVIDIA的高端GPU芯片有助于中国…

Web端即时通信技术-WebSocket

最近在做项目的时候&#xff0c;遇到了一个前端页面需要实时刷新的功能&#xff0c;一种方法是我们通过短轮询的方式&#xff0c;但这种方式虽然简单&#xff0c;但是无用的请求过多&#xff0c;占用资源&#xff0c;并且如果是对数据要求高较高的场景&#xff0c;就不适用了。…

汽车ECU软件开发之应用层软件与底层软件

随着汽车产品日渐趋向电子化&#xff0c;人们对各类先进的安全系统、高级驾驶辅助系统的接受程度日益提升。而原本昂贵的盲点检测、自动紧急制动等功能的成本下滑&#xff0c;使其快速获得了市场青睐。富士奇美拉研究所&#xff08;Fuji Chimera Research Institute&#xff09…

MySQL算术/比较/逻辑/位/运算符与正则全解

文章目录1. 算术运算符加法与减法运算符乘法与除法运算符求模&#xff08;求余&#xff09;运算符2. 比较运算符等号运算符安全等于运算符不等于运算符非符号类型的运算符空运算符非空运算符最小值运算符最大值运算符BETWEEN AND运算符IN运算符NOT IN运算符LIKE运算符ESCAPEREG…

〖金融帝国实验室〗(Capitalism Lab)v8.2.00官方重大版本更新(2023.01.17正式发布)

〖金融帝国实验室〗&#xff08;CapLab&#xff09;v8.2.00正式发布&#xff01; ————————————— ※v8.2.00更新内容摘要&#xff1a; 1.新增生活服务业&#xff1a;快餐店、比萨店、蛋糕店和咖啡店。 2.可以通过在农场周围购买土地并耕耘来实现产能扩容。 3.…