【Vue3源码解析】应用实例创建及页面渲染

下载源码

git clone https://github.com/vuejs/core.git

写该文章时的Vue版本为:

"version": "3.5.13",

这里要注意 pnpm 的版本不能太低,我此时的版本为 9.15.4。更新 pnpm 版本:

npm install -g pnpm

然后安装依赖:

pnpm i 

然后打包开发环境:

pnpm run dev

之后就可以编写自己的测试程序,debugger 调试代码。

Vue3 整体架构

image-20250213104157644

image-20250213104310536

在打包后的文件夹内创建一个自己用来测试的 demo 文件夹,我这里叫做 heo 文件夹

file_1739416744668_221

编写测试代码:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<div id="app"><h1>计数器案例</h1><h2>当前计数: {{counter}}</h2><button @click="increment">+1</button><button @click="decrement">-1</button>
</div>
<script src="../dist/vue.global.js"></script>
<script>const {ref, createApp} = Vue// 根组件const App = {setup() {const counter = ref(0)function increment() {counter.value++}function decrement() {counter.value--}return {counter,increment,decrement}}}// 创建全局 app对象,调用 mount 方法挂载到页面上const app = createApp(App)app.mount('#app')
</script>
</body>
</html>

然后再使用内置插件自带的服务器预览当前页面:

image-20250213111951443

createApp 的创建过程

找到创建 app 实例的 createApp 函数:

image-20250213154607136

从创建 app 对象到创建 渲染器的过程:

image-20250213160930449

/*** 创建一个 Vue 应用实例(入口)。** @param {...any} args - 传递给内部创建应用方法的参数。* @returns {CreateAppFunction<Element>} 返回一个带有自定义 `mount` 方法的应用实例对象 app。*/
export const createApp = ((...args) => {// 调用确保渲染器存在并创建应用实例 app// ensureRenderer() 实例化一个渲染器 渲染器就是把 Vue 代码(组件)渲染到DOM 上// ensureRenderer() 最终的返回值 是一个对象// {//   render, // 核心函数 负责将 VNode 渲染成真实 DOM//   hydrate, // SSR 的函数//   createApp: createAppAPI(render, hydrate),// }const app = ensureRenderer().createApp(...args)// 在开发模式下注入原生标签检查和编译选项检查if (__DEV__) {injectNativeTagCheck(app)injectCompilerOptionsCheck(app)}// 获取原始的 mount 方法const { mount } = app// 自定义(重写) mount 方法,接受DOM或选择器作为参数 当我们调用mount时 本质上在调用重写的 mount 方法// 装饰器模式 不改变原有实现 对 mount 进行增强app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {// 将传入的选择器或元素转换为实际的 DOM 元素 也就是我们传入的要挂载的 DOM// normalizeContainer 转换DOM 比如将 "#app" 转为 querySelector 函数拿到DOMconst container = normalizeContainer(containerOrSelector)if (!container) return // 如果DOM不存在,直接返回const component = app._component // 获取应用的根组件// 如果根组件没有 render 函数或 template 属性,则从容器(#app)内的HTML作为组件的模版if (!isFunction(component) && !component.render && !component.template) {// __UNSAFE__// 原因:潜在的执行在 DOM 内的模板中的 JS 表达式。// 用户必须确保该模板是可信的。如果由服务器渲染,则模板不应包含任何用户数据。component.template = container.innerHTML// 2.x 兼容性检查if (__COMPAT__ && __DEV__ && container.nodeType === 1) {for (let i = 0; i < (container as Element).attributes.length; i++) {const attr = (container as Element).attributes[i]// 检查是否有 v- 或 : 或 @ 开头的属性,提示用户使用新的挂载方式if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {compatUtils.warnDeprecation(DeprecationTypes.GLOBAL_MOUNT_CONTAINER,null,)break}}}}// 在挂载前清除容器的内容if (container.nodeType === 1) {container.textContent = ''}// 调用原始的 mount 方法进行挂载,并返回代理实例const proxy = mount(container, false, resolveRootNamespace(container))// 如果容器是一个元素,移除 v-cloak 属性并添加 data-v-app 属性if (container instanceof Element) {container.removeAttribute('v-cloak')container.setAttribute('data-v-app', '')}return proxy}return app
}) as CreateAppFunction<Element>

我们自己开发的时候根组件都是 template 属性,而如果没有template 或者 render ,则从容器(#app)内的HTML作为组件的模版,例如:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<!--将该 div 作为 作为组件的模版-->
<div id="app"><h1>计数器案例</h1><h2>当前计数: {{counter}}</h2><button @click="increment">+1</button><button @click="decrement">-1</button>
</div>
<script src="../dist/vue.global.js"></script>
<script>const {ref, createApp} = Vue// 根组件const App = {setup() {const counter = ref(0)function increment() {counter.value++}function decrement() {counter.value--}return {counter,increment,decrement}}}// 创建全局 app对象,调用 mount 方法挂载到页面上const app = createApp(App)app.mount('#app')
</script>
</body>
</html>

创建渲染器和 mount 的执行

从创建虚拟节点到渲染页面的过程:

image-20250213160951570

// nodeOps 节点操作(比如insert/remove/createComment创建注释元素 等)和 patchProp 属性操作(比如 class/style/@click 等)
const rendererOptions = /*@__PURE__*/ extend({ patchProp }, nodeOps)// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer/*** 确保渲染器已被创建** 此函数用于检查全局变量 `renderer` 是否已存在,如果不存在,则使用给定的 `rendererOptions`* 创建一个新的渲染器并赋值给 `renderer`,以确保渲染器的单例模式* 这种方法保证了在整个应用中只有一个渲染器实例,从而优化性能 如果有多个渲染器实例可能导致状态管理和渲染更新的冲突和重复** rendererOptions - 定制化 针对不同平台(移动端/SSR)的渲染器配置** @returns {Renderer} 已存在的渲染器或新创建的渲染器实例*/
function ensureRenderer() {return (renderer ||(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions)))
}
/*** The createRenderer function accepts two generic arguments:* HostNode and HostElement, corresponding to Node and Element types in the* host environment. For example, for runtime-dom, HostNode would be the DOM* `Node` interface and HostElement would be the DOM `Element` interface.** Custom renderers can pass in the platform specific types like this:** ```js* const { render, createApp } = createRenderer<Node, Element>({*   patchProp,*   ...nodeOps* })* ```*/
export function createRenderer<HostNode = RendererNode,HostElement = RendererElement,
>(options: RendererOptions<HostNode, HostElement>): Renderer<HostElement> {return baseCreateRenderer<HostNode, HostElement>(options)
}

因此 createRenderer 才是创建渲染器的代码实现(有2000行代码,这里不再粘代码)。它的返回值很重要是:

{render, // 核心函数 负责将 VNode 渲染成真实 DOMhydrate, // SSR 的函数createApp: createAppAPI(render, hydrate),
}
export function createAppAPI<HostElement>(render: RootRenderFunction<HostElement>,hydrate?: RootHydrateFunction,
): CreateAppFunction<HostElement> {// 返回的函数 也就是我们调用createApp()函数创建的真实app(这里使用函数柯里化传递了render和hydrate)return function createApp(rootComponent, rootProps = null) {// 如果不是函数组件 转化为对象的形式if (!isFunction(rootComponent)) {rootComponent = extend({}, rootComponent)}// rootProps 如果不是对象则警告if (rootProps != null && !isObject(rootProps)) {__DEV__ && warn(`root props passed to app.mount() must be an object.`)rootProps = null}// 创建app上下文实例 存储 app配置信息const context = createAppContext()// 安装 plugins 使用set防止重复安装const installedPlugins = new WeakSet()const pluginCleanupFns: Array<() => any> = []// 目前刚刚创建app实例 没有挂载到dom上let isMounted = false// app 实例对象 函数的最后返回 该app实例对象const app: App = (context.app = {_uid: uid++,_component: rootComponent as ConcreteComponent,_props: rootProps,_container: null,_context: context,_instance: null,version,//...use(plugin: Plugin, ...options: any[]) {if (installedPlugins.has(plugin)) {__DEV__ && warn(`Plugin has already been applied to target app.`)} else if (plugin && isFunction(plugin.install)) {installedPlugins.add(plugin)plugin.install(app, ...options)} else if (isFunction(plugin)) {installedPlugins.add(plugin)plugin(app, ...options)} else if (__DEV__) {warn(`A plugin must either be a function or an object with an "install" ` +`function.`,)}return app},//...mount(rootContainer: HostElement,isHydrate?: boolean,namespace?: boolean | ElementNamespace,): any {if (!isMounted) {// #5571if (__DEV__ && (rootContainer as any).__vue_app__) {warn(`There is already an app instance mounted on the host container.\n` +` If you want to mount another app on the same host container,` +` you need to unmount the previous app by calling \`app.unmount()\` first.`,)}// 根据传入的根组件和属性 创建对应的 VNodeconst vnode = app._ceVNode || createVNode(rootComponent, rootProps)// store app context on the root VNode.// this will be set on the root instance on initial mount.// 在 vnode 存储上下文vnode.appContext = context//...if (isHydrate && hydrate) {hydrate(vnode as VNode<Node, Element>, rootContainer as any)} else {// 非ssr 则将上面的虚拟节点渲染到dom上render(vnode, rootContainer, namespace)}// 设置已经挂载的标记isMounted = trueapp._container = rootContainer// for devtools and telemetry;(rootContainer as any).__vue_app__ = appif (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {app._instance = vnode.componentdevtoolsInitApp(app, version)}return getComponentPublicInstance(vnode.component!)} else if (__DEV__) {warn(`App has already been mounted.\n` +`If you want to remount the same app, move your app creation logic ` +`into a factory function and create fresh app instances for each ` +`mount - e.g. \`const createMyApp = () => createApp(App)\``,)}},//...return app}
}

patch 代码执行过程:

image-20250213161012947

patch 组件的 children 过程:

image-20250213161306436

组件内部 effect 处理过程:

image-20250213161222976

createVNode 的过程(h 函数的本质)

image-20250213161448205

packages/runtime-core/src/h.ts

/*** 创建一个虚拟节点(VNode),基于提供的类型、属性和子节点。* 对外暴露为 h 函数,内部使用 createVNode 函数来创建虚拟节点。** @param type VNode的组件类型,通常是一个表示DOM元素或组件函数的字符串。* @param propsOrChildren VNode的属性或子节点,取决于上下文。* @param children VNode的子节点,可以是单个VNode或VNode数组。* @returns 返回创建的VNode。*/
export function h(type: any, propsOrChildren?: any, children?: any): VNode {// 获取参数的数量,以确定如何处理它们。const l = arguments.length// 如果只提供了两个参数,则进一步根据第二个参数的类型进行区分。if (l === 2) {// 判断第二个参数是否为对象且不是数组,以决定它是属性还是子节点。if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {// 单个VNode且没有属性if (isVNode(propsOrChildren)) {return createVNode(type, null, [propsOrChildren])}// 有属性但没有子节点return createVNode(type, propsOrChildren)} else {// 省略属性return createVNode(type, null, propsOrChildren)}} else {// 处理超过三个参数的情况,将第二个参数之后的所有参数转换为子节点数组。if (l > 3) {children = Array.prototype.slice.call(arguments, 2)} else if (l === 3 && isVNode(children)) {// 如果正好提供了三个参数且第三个参数是单个VNode,则将其封装到数组中。children = [children]}// 最后,使用属性和子节点创建VNode。return createVNode(type, propsOrChildren, children)}
}
(type, propsOrChildren)} else {// 省略属性return createVNode(type, null, propsOrChildren)}} else {// 处理超过三个参数的情况,将第二个参数之后的所有参数转换为子节点数组。if (l > 3) {children = Array.prototype.slice.call(arguments, 2)} else if (l === 3 && isVNode(children)) {// 如果正好提供了三个参数且第三个参数是单个VNode,则将其封装到数组中。children = [children]}// 最后,使用属性和子节点创建VNode。return createVNode(type, propsOrChildren, children)}
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu 系统 cuda12.2 安装 MMDetection3D

DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&#xff0c;持续增加中。 需要更多数据资源和技术解决方案&#xff0c;知识星球&#xff1a; “DataBall - X 数据球(free)” 贵在坚持&#xff01; ---------------------------------------…

云贝餐饮连锁V3独立版全开源+vue源码

一.介绍 云贝餐饮连锁V3独立版&#xff0c;作为一款全开源、全插件的源码部署系统&#xff0c;其在餐饮行业软件系统中独树一帜。该系统不仅功能全面&#xff0c;涵盖了餐饮连锁企业的日常运营、财务管理、库存管理、会员管理等多个方面&#xff0c;而且框架结构清晰&#xff…

learngit git常用指令

learngit & git常用指令 目录 learngit & git常用指令1.add && commit2.status && diff3.log && reset && reflog4.Repositoty(版本库)5.checkout6.rm7.github(push && rm)8.clone9.branch && switch && merg…

算法1-7 搜索

目录 1 深度优先搜索 1.1 P1219 八皇后 1.2 P1135 深搜剪枝 1.3 P1605 多路深搜回溯 2 广度优先搜索 2.1 P1443 马的遍历 3 多方向搜索 3.1 P1101 单词方阵 1 深度优先搜索 需要考虑深度的情况&#xff1a; 固定长度组合&#xff1a;当问题要求生成确定长度的组合&…

响应式布局学习笔记

什么是响应式布局&#xff1f; 响应式布局&#xff08;Responsive Web Design&#xff09;是一种网页设计方法&#xff0c;使网站能够根据设备屏幕尺寸&#xff08;如手机、平板、电脑&#xff09;自动调整内容和布局&#xff0c;提供最佳浏览体验。 如何调试响应式布局&…

Cursor 与团队协作:提升团队开发效率

引言 在团队开发中&#xff0c;代码质量参差不齐、重复错误频发、代码审查耗时过长是制约效率的三大痛点。据 GitHub 调查&#xff0c;开发者平均每周花费 4.3 小时修复他人代码问题&#xff0c;而 60% 的合并请求&#xff08;PR&#xff09;因风格或低级错误被驳回。Cursor 作…

rocketmq-netty通信设计-request和response

1、NettyRemotingServer启动分析 org.apache.rocketmq.remoting.netty.NettyRemotingServer#start public void start() {this.defaultEventExecutorGroup new DefaultEventExecutorGroup(nettyServerConfig.getServerWorkerThreads(),new ThreadFactory() {private AtomicI…

蓝桥杯之图

图&#xff1a; 对于图来说&#xff0c;重点在于之后的最短路径算法&#xff0c;这边简单做一下了解即可 代码&#xff1a; #include<iostream> #include<string> #include<vector> #include<list> #include<queue> using namespace std; clas…

mysql 学习15 SQL优化,插入数据优化,主键优化,order by优化,group by 优化,limit 优化,count 优化,update 优化

插入数据优化&#xff0c; insert 优化&#xff0c; 批量插入&#xff08;一次不超过1000条&#xff09; 手动提交事务 主键顺序插入 load 从本地一次插入大批量数据&#xff0c; 登陆时 mysql --local-infile -u root -p load data local infile /root/sql1.log into table tb…

143,【3】 buuctf web [GYCTF2020]EasyThinking

进入靶场 一开始那个题目名字就想到了框架 扫描目录 访问后自动下载了 找源码 <?php namespace app\home\controller;use think\exception\ValidateException; use think\facade\Db; use think\facade\View; use app\common\model\User; use think\facade\Request; use …

数据守护者:备份文件的重要性及自动化备份实践

在信息化社会&#xff0c;数据已成为企业运营和个人生活的重要组成部分。无论是企业的核心业务数据&#xff0c;还是个人的珍贵照片、重要文档&#xff0c;数据的丢失或损坏都可能带来无法估量的损失。因此&#xff0c;备份文件的重要性愈发凸显&#xff0c;它不仅是数据安全的…

PHP支付宝--转账到支付宝账户

官方参考文档&#xff1a; ​https://opendocs.alipay.com/open/62987723_alipay.fund.trans.uni.transfer?sceneca56bca529e64125a2786703c6192d41&pathHash66064890​ 可以使用默认应用&#xff0c;也可以自建新应用&#xff0c;此处以默认应用来讲解【默认应用默认支持…

vscode插件开发

准备 安装开发依赖 npm install -g yo generator-code 安装后&#xff0c;运行命令 yo code 运行 打开项目&#xff0c; 点击 vscode 调式 按 F5 或点击调试运行按钮 会打开一个新窗口&#xff0c;在新窗口按快捷键 CtrlShiftP &#xff0c;搜索 Hello World 选择执行 右下角出…

win11安装wsl报错:无法解析服务器的名称或地址(启用wsl2)

1. 启用wsl报错如下 # 查看可安装的 wsl --install wsl --list --online此原因是因为没有开启DNS的原因&#xff0c;所以需要我们手动开启DNS。 2. 按照如下配置即可 Google的DNS&#xff08;8.8.8.8和8.8.4.4) 全国通用DNS地址 (114.114.114.114) 3. 运行以下命令来重启 WSL…

mysql 存储空间增大解决方案

一&#xff1a;查询数据库中表占比比较多的表 SELECT table_name AS "Tables", round(((data_length index_length) / 1024 / 1024), 2) AS "Size (MB)" FROM information_schema.tables WHERE table_schema "自己的数据库名"; …

【MySQL】数据库基础库/表的操作数据类型详解

主页&#xff1a;醋溜马桶圈-CSDN博客 专栏&#xff1a;实战项目_醋溜马桶圈的博客-CSDN博客 gitee&#xff1a;mnxcc (mnxcc) - Gitee.com 目录 1.什么是数据库 2.主流数据库 3.基本使用 3.1 MySQL安装 3.2 连接服务器 3.3 服务器管理 3.4 服务器、数据库、表关系 3.5 …

【kafka系列】消费者

目录 获取消息 1. 消费者获取消息的流程逻辑分析 阶段一&#xff1a;消费者初始化 阶段二&#xff1a;分区分配与重平衡&#xff08;Rebalance&#xff09; 阶段三&#xff1a;消息拉取与处理 阶段四&#xff1a;偏移量提交 核心设计思想 2. 流程 关键点总结 常见参数…

仿叮咚买菜鸿蒙原生APP

# DingdongShopping 这是一个原生鸿蒙版的仿叮咚买菜APP项目 鸿蒙Next发布至今已经有一年多的时间了&#xff0c;但有时候我们想要实现一些复杂的功能或者效果&#xff0c;在开发文档上查阅一些资料还是比较费时的&#xff0c;有可能还找不到我们想要的内容。而社会层面上分享…

Linux 进程控制(进程创建,进程等待)

目录 进程创建 fork函数初识 fork函数返回值 写时拷贝 fork常规用法 fork调用失败的原因 进程终止 进程退出场景 进程退出码 进程常见退出方法 exit函数 _exit函数 return退出 return、exit和_exit之间的区别与联系 进程异常退出 进程等待 进程等待的必要性 获…

ROS2下Rviz显示orbbec相机depth深度图

ROS2下Rviz显示orbbec相机depth深度图 视频讲解 ROS2下Rviz显示orbbec相机depth深度图 在《ROS2下编写orbbec相机C package并Rviz显示》的基础上&#xff0c;继续添加depth图像的获取及显示 rgb_publisher_ this->create_publisher<sensor_msgs::msg::Image>("…