vue3 源码解析(4)— createApp 源码的实现

前言

本文是 vue3 源码分析系列的第四篇文章,在使用 vue3 时,我们需要使用 createApp 来创建一个应用实例,然后使用 mount 方法将应用挂载到某个DOM节点上。那么在调用 createApp 时,vue 再背后做了些什么事情呢?在这篇文章中,我们将深入探讨 createApp 的实现原理,并通过源码分析来理解其工作机制。

createApp 的基本用法

我们先来看一下 createApp 的基本使用方式:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>createApp</title>
</head>
<body>
<div id="app"></div>
<script src="../packages/runtime-dom/dist/runtime-dom.global.js"></script>
<script>const { createApp, h, reactive } = VueRuntimeDom;const App = {setup (props, context) {const state = reactive({ name: "11" });return {state};},render(proxy) {return h("div", {}, proxy.state.name);},};createApp(App, {}).mount("#app");
</script>
</body>
</html>

在上面的例子中, 我们从 vue 包中导出 createApp 方法,其中一个参数是组件配置对象,返回一个应用实例,然后调用 mount 方法将应用挂载到某个DOM节点上。我们先从入口函数 createApp 出发。

createApp

// 合并传入的参数
const renderOptionDom = extend({ patchProp }, nodeOptions)const createApp = (rootComponent, rootProps) => {// 使用createRenderer函数创建一个渲染器实例,并调用其createApp方法来创建一个应用实例  // 这个方法通常用于创建应用实例,可以兼容不同的平台  const app = createRenderer(renderOptionDom).createApp(rootComponent, rootProps)const { mount } = app // render(vnode, container)// 定义一个名为mount的新方法app.mount = (selector) => {// 使用querySelector方法根据传入的selector选择器查找对应的DOM容器元素 const container = nodeOptions.querySelector(selector)container.innerHTML = ''// 调用mount方法,将容器作为参数传入,用于挂载应用实例到该容器上 mount(container)}return app
}

在源码中,我们直接调用了 createRenderer 方法,这个方法是创建渲染器的方法。
其中 renderOptionDom 是一些渲染器的配置,主要的作用是用来操作DOM的。先简单的来认识一下 renderOptionDom,这个里面的方法后面会用到。

// 定义一个对象包含了一些与DOM节点相关的功能方法  
const nodeOptions = {createElement: tag => document.createElement(tag),// 将一个子节点插入到父节点中的指定位置(锚点)的方法insert: (child, parent, anchor) => {parent.insertBefore(child, anchor || null);},querySelector: selector => document.querySelector(selector),createText: text => document.createTextNode(text),
}

接下来看下 createRenderer 方式的实现。

createRenderer

const createRenderer = (options) => {// 这个函数是用于渲染虚拟DOM,其中vnode表示虚拟节点,container表示渲染的目标容器// render函数的目的是解耦平台,使其能够兼容不同的平台或框架let render = (vnode, container) => {// 首次渲染时// null 上一次渲染的 vnode// vnode 当前需要渲染的 vnode// container 挂载的容器patch(null, vnode, container)}return {createApp: createAppAPI(render)}
}

createRenderer 内部返回 createApp 这个方法。而 createApp 方法的是通过 createAppAPI 方法创建的,所以我们还需要看一下 createAppAPI 方法的实现。

createAppAPI

const createAppAPI = (render) => {// 返回一个函数,主要是通过闭包来缓存上面传入的参数return function createApp (rootComponent, rootProps) {// 创建 app 对象const app = {// rootComponent 是我们传入的根组件_component: rootComponent,// rootProps 是我们传入的根组件的 props,这个参数必须是一个对象_props: rootProps,// 挂载的 DOM 节点_container: null,// 添加相关的事件mount (container) {// ...},use (plugin, ...options) {// ...},mixin(mixin) {// ...},component(name, component) {// ...},directive(name, directive) {// ...}}// 返回 app 对象return app}
}

看到这里,我们可以知道,createApp 方法的实现其实就是 createAppAPI 方法中返回的一个函数。在返回的函数中返回了一堆对象,这里可以看到我们常用的 use、mixin、component、directive、mount 等方法都是在 app 对象上的。这些对象就是我们在使用 createApp 方法时,可以调用的方法。

在入口函数 createApp 中,我们已经了解到我们在调用 createApp 方法时,会返回一个 app 对象,这个对象上有一个 mount 方法,我们需要通过这个方法来挂载我们的根组件。详细看下 mount 方法时如何实现的。

mount

// container 挂载的容器
mount (container) {// 创建虚拟domconst vnode = createVNode(rootComponent, rootProps)// render 函数是在 createRenderer 中定义,传递到 createAppAPI 中,通过闭包缓存下来的// 通过传入的自定义渲染函数进行渲染render(vnode, container)// 设置 app 实例的 _container 属性,指向挂载的容器app._container = container
}

这段代码定义了一个 mount 函数,用于将一个应用实例挂载到指定的容器上。首先创建一个虚拟节点,然后使用自定义的渲染函数将应用渲染到容器中,并设置应用实例的_container属性为挂载的容器。

createVNode

虚拟节点其实就是一个 js 对象,包含了 dom 的一些属性,比如 tag、props、children 等等。虚拟节点,大概信息如下:

const createVNode = (type, props, children = null) => {const vnode = {// 添加一个特殊的属性__v_isVNode,用于标记这个对象是一个虚拟节点__v_isVNode: true,// 设置虚拟节点的类型type,// 设置虚拟节点的属性。这些属性通常用于表示元素的属性和样式。  props,// 设置虚拟节点的子节点。这些子节点可以是另一个虚拟节点,也可以是实际的数据或组件children,// 为虚拟节点设置一个 key 属性。key 用于优化虚拟DOM的diff算法,帮助识别哪些节点发生了变化key: props && props.key, // diff 算法// 初始化虚拟节点的 el 属性为 null,表示这个虚拟节点还没有与实际的DOM节点对应起来el: null,// 初始化虚拟节点的属性为一个空对象,用于存储与该虚拟节点关联的组件实例。component: {}, // 组件实例// 用于标记虚拟节点的形状或类型shapeFlag}// 返回创建的虚拟节点对象return vnode
}

这里就只贴了部分 VNode 的相关定义,这里只是做一个简单的概念介绍。

render

render 函数是在 createRenderer 中定义的。这里可以通过传入的自定义的 render 渲染函数进行不同平台的渲染。具体源码如下:

let render = (vnode, container) => {// 将虚拟节点渲染到容器中patch(null, vnode, container)
}

patch

patch 函数的主要作用就是将虚拟节点渲染到容器中。由于 patch 函数内部的实现会牵扯到非常多的内容,这里只是大致的了解下原理即可。

/*** * @param n1 上一次渲染的 vnode* @param n2 当前需要渲染的 vnode* @param container 容器* @param anchor 锚点, 用来标记插入的位置* @returns */
const patch = (n1, n2, container, anchor = null) => {// n1 和 n2 是否相同if (n1 === n2) {return}// n1 是否存在且与 n2 的类型是否一致if (n1 && !isSameVNodeType(n1, n2)) {unmount(n1) // 删除元素n1 = null // 删除之后重新加载}const { shapeFlag, type } = n2if (type === Text) { // 文本console.log('文本')// 处理文本节点processText(n1, n2, container)} else if (shapeFlag & ShapeFlags.ELEMENT) { // 元素console.log('元素')// 处理元素节点processElement(n1, n2, container, anchor)} else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { // 组件console.log('组件')// 处理组件节点  processComponent(n1, n2, container)}
}

这段代码通过判断虚拟节点的类型(文本、元素或组件),来决定如何更新虚拟DOM。通常我们在使用 createApp 的时候,通常会传入一个根组件,这个根组件就会走到 processComponent 函数中。

processComponent

const processComponent = (n1, n2, container) => {// n1 为 null 说明这是首次挂载组件首次挂载if (n1 === null) {// 挂载组件到容器上mountComponent(n2, container)} else {// 更新组件节点 updateComponent(n1, n2, container)}
}

processComponent 函数做了两件事,一个是挂载组件,一个是更新组件。

const mountComponent = (initialVNode, container, anchor) => {// 通过调用组件的 render 方法,获取组件的 vnodeconst subTree = initialVNode.type.render.call(null)// 直接调用 patch 函数,将 subTree 渲染到指定的容器和锚点上patch(null, subTree, container, anchor);)
}

总结

我们通过阅读源码了解到,createApp 函数是 vue3 的入口函数,通过 createApp 函数我们可以创建一个应用。

createApp 的实现是借助了 createRenderer 函数,createRenderer 的实现内部包装了createAppAPI。

createApp 函数接收一个组件,然后返回一个应用,这个应用中有一个 mount 方法,这个 mount 方法就是用来将应用挂载到容器中的。

在 createApp 中重写了 mount 方法,内部的实现是通过调用渲染器的 mount 方法。

这个 mount 方法是在 createAppAPI 的内部函数 createApp 中实现的,createApp 函数中的 mount 方法会调用 patch 函数。

patch 函数内部会做很多的事情,虽然我们这里只是调用 mountComponent 实现了挂载的逻辑。

以上就是调用 createApp 时 vue 工作过程原理的详细内容。

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

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

相关文章

类和对象---C++

类和对象目录 类和对象1.封装1.1 封装的意义1.2 struct和class区别1.3 成员属性设置为私有1.3.1 联系---判断圆和点的位置关系 2.对象的初始化和清理2.1 构造函数和析构函数2.2 构造函数的分类及调用2.2.1无参构造函数调用2.2.2有参构造函数调用2.2.2.1括号法2.2.2.2显式法2.2.…

微信小程序快速入门02(含案例)

&#x1f3e1;浩泽学编程&#xff1a;个人主页 &#x1f525; 推荐专栏&#xff1a;《深入浅出SpringBoot》《java项目分享》 《RabbitMQ》《Spring》《SpringMVC》 &#x1f6f8;学无止境&#xff0c;不骄不躁&#xff0c;知行合一 文章目录 前言一、页面导航1.…

互联网资讯精选:科技爱好者周刊 | 开源日报 No.145

ruanyf/weekly Stars: 37.4k License: NOASSERTION 记录每周值得分享的科技内容&#xff0c;提供大量就业信息。欢迎投稿、推荐或自荐文章/软件/资源&#xff0c;并可通过多种方式进行搜索。 提供丰富的科技内容每周更新可以提交工作/实习岗位支持投稿和推荐功能 GyulyVGC/…

bootloader学习笔记及SD卡启动盘制作

Bootloader介绍 在操作系统运行之前运行的一小段代码&#xff0c;用于将软硬件环境初始化到一个合适的状态&#xff0c;为操作系统的加载和运行做准备&#xff08;其本身不是操作系统&#xff09; Bootloader基本功能 1、初始化软硬件环境 2、引导加载linux内核 3、给linux…

一个无敌的 Python 文件系统监控库

在软件开发和系统管理领域&#xff0c;经常需要监控文件和目录的变化&#xff0c;以便在文件被创建、修改或删除时触发相应的操作。Python Watchdog是一个强大的Python库&#xff0c;它提供了简单而灵活的方式来监控文件系统的变化。本文将详细介绍Python Watchdog的用法和功能…

旅游数据可视化大屏:一屏掌控,畅游数据之海

随着旅游业的蓬勃发展&#xff0c;如何有效地管理和分析旅游数据成为行业关注的焦点。旅游数据可视化大屏作为一种新兴的技术手段&#xff0c;为旅游业带来了前所未有的机遇和挑战。 旅游数据可视化大屏集成了丰富的数据资源&#xff0c;通过直观的图表、图像和交互界面&#x…

慢 SQL 的优化思路

分析慢 SQL 如何定位慢 SQL 呢&#xff1f; 可以通过 slow log 来查看慢SQL&#xff0c;默认的情况下&#xff0c;MySQL 数据库是不开启慢查询日志&#xff08;slow query log&#xff09;。所以我们需要手动把它打开。 查看下慢查询日志配置&#xff0c;我们可以使用 show …

C++力扣题目654--最大二叉树

给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最大值。递归地在最大值 左边 的 子数组前缀上 构建左子树。递归地在最大值 右边 的 子数组后缀上 构建右子树。 返回 nums 构建的 最大二叉树…

【Spring Boot 3】【数据源】自定义JPA数据源

【Spring Boot 3】【数据源】自定义JPA数据源 背景介绍开发环境开发步骤及源码工程目录结构总结背景 软件开发是一门实践性科学,对大多数人来说,学习一种新技术不是一开始就去深究其原理,而是先从做出一个可工作的DEMO入手。但在我个人学习和工作经历中,每次学习新技术总是…

使用DevEco Studio导入Har模块,提示“Module Check Failed”—鸿蒙开发已解决

文章目录 项目场景:问题描述原因分析:解决方案:此Bug解决方案总结总结心得:寄语项目场景: 最近也是遇到了这个问题,看到网上也有人在询问这个问题,本文总结了自己和其他人的解决经验,解决了导入Har模块,提示“Module Check Failed" 的问题。 使用DevEco Studio…

7、防写一个shell 命令解释器

1、代码部分 #include <stdio.h> #include <stdlib.h> #include <string.h>#define MAX_INPUT_LENGTH 100void parse_command(char *command) {// 用于存储解析后的命令和参数char cmd[MAX_INPUT_LENGTH];char args[MAX_INPUT_LENGTH];// 将输入的命令拷贝到…

微信商家转账到零钱,既能单笔又能批量,支持多商户管理

大家好&#xff0c;我是小悟 微信商家转账到零钱的功能大家应该都熟悉吧&#xff0c;为了满足商家向用户微信零钱转账的需求&#xff0c;微信支付推出【商家转账到零钱】服务&#xff0c;方便商户可以一次向单个或多个用户的微信零钱转账。 商家转账到零钱为商户提供了简便、…

计算机毕业设计----SSH高校科研管理系统平台

项目介绍 本项目包含超级管理员、管理员、教师三种角色&#xff1b; 超级管理员角色包含以下功能&#xff1a; 登录,教师管理,管理员管理等功能。 管理员角色包含以下功能&#xff1a; 登录,专业参赛奖项管理,科技论文发表管理,出版专业著作管理,科研项目立项管理,科研项目结…

MySQL从0到1全教程【2】SQL语言的通用语法及分类

1 SQL语言的通用语法格式 无论是那种数据库的产品&#xff0c;SQL语法都是通用的。 SQL语句可以单行编写也可以多行编写&#xff0c;以分号结尾。SQL语句可以使用空格或者缩进的方式来增强语句的可读性&#xff0c;空格和缩进的数量没有限制。MySQL数据库的SQL语句是不区分大…

常常放生,与佛心更相契

弘一法师曾说&#xff1a;“世上最好的放生&#xff0c;就是放过自己。”天地与我并生&#xff0c;万物与我为一&#xff0c;每一个众生最宝贵的是自己的生命。而人自称万物之灵&#xff0c;就应当有爱护万物、保护万物的责任心&#xff0c;心中要有慈悲和恻隐之心&#xff0c;…

金蝶云星空和吉客云单据接口对接

金蝶云星空和吉客云单据接口对接 对接系统&#xff1a;吉客云 吉客云是基于“网店管家”十五年电商ERP行业和技术积累基础上顺应产业发展需求&#xff0c;重新定位、全新设计推出的换代产品&#xff0c;从业务数字化和组织数字化两个方向出发&#xff0c;以构建流程的闭环为依归…

什么是分治法算法思想?

一、问题 分治与递归就像⼀对孪⽣兄弟&#xff0c;在设计算法时经常是同时应⽤的&#xff0c;递归算法⽐较好理解&#xff0c;那么什么是分治法算法思想呢&#xff1f; 二、解答 分治法算法的设计思想就是将⼀个难以直接解决的⼤问题&#xff0c;分割成⼀些规模较⼩的相同问题…

高级JavaScript。如何用JavaScript手撸一个富文本编辑器?

要素过多建议收藏 - 富文本编辑 基本的技术就是在空白 HTML 文件中嵌入一个 iframe 。通过 designMode 属性&#xff0c;可以将这个空白文档变成可以编辑的&#xff0c;实际编辑的则是 <body> 元素 的 HTML 。 designMode 属性有两个可能的值&#xff1a; "…

高级RAG(六): 句子-窗口检索

之前我们介绍了LlamaIndex的从小到大的检索 的检索方法&#xff0c;今天我们再来介绍llamaindex的另外一种高级检索方法: 句子-窗口检索(Sentence Window Retrieval)&#xff0c;在开始介绍之前让我们先回顾一下基本的RAG检索的流程&#xff0c;如下图所示&#xff1a; 在执行基…

Fedora 36 正式发布稳定的Linux桌面版本

Fedora 36今天发布&#xff0c;这是最近一段时间以来又一个强大、前沿而又稳定可靠的Linux发行版本&#xff0c;除了这些特点外&#xff0c;Fedora 36还在原先的基础上增加了新的功能和细节打磨。 Fedora 36使用GNOME 42作为其默认的Fedora工作站桌面环境。 OpenSSL 3.0&#x…