前言
在使用 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 () {
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().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 方法创建一个渲染器。接下来看下 createRenderer 方式的实现。
createRenderer
const createRenderer = (options) => {
// 这里可以自定义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方法中会此函数被调用
mount (container) {
// 挂载组件
},
use (plugin, ...options) {
// ...
},
mixin(mixin) {
// ...
},
component(name, component) {
// ...
},
directive(name, directive) {
// ...
}
}
// 返回 app 对象
return app
}
}
看到这里,我们可以知道,createApp 方法的实现其实就是 createAppAPI 方法中返回的一个函数。在函数中返回了一个 app 对象,在 app 对象上可以看到我们常用的 use、mixin、component、directive、mount 等方法。这些对象就是我们在使用 createApp 方法时,可以调用的方法。
在入口函数 createApp 中最终会返回一个 app 对象,在对象上有一个 mount 方法,我们需要通过这个方法来挂载我们的根组件。详细看下 mount 方法时如何实现的。
mount
// container 挂载的容器
mount (container) {
// 创建虚拟dom
const 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 } = n2
if (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 方法,获取组件的 vnode
const subTree = initialVNode.type.render.call(null)
// 直接调用 patch 函数,将 subTree 渲染到指定的容器和锚点上
patch(null, subTree, container, anchor);)
}
总结
我们通过阅读源码了解到,createApp 函数是 vue3 的入口函数,通过 createApp 函数我们可以创建一个应用。
createApp 函数接收一个组件,然后返回一个应用,这个应用中有一个 mount 方法,这个 mount 方法就是用来将应用挂载到容器中的。
createApp 的实现是借助了 createRenderer 函数,createRenderer 的实现内部包装了createAppAPI。
在 createApp 中重写了 mount 方法,内部的实现是通过调用渲染器的 mount 方法。
这个 mount 方法是在 createAppAPI 的内部函数 createApp 中实现的,createApp 函数中的 mount 方法会调用 patch 函数。
patch 函数内部会做很多的事情,虽然我们这里只是调用 mountComponent 实现了挂载的逻辑。
以上就是调用 createApp 时 vue 工作过程原理的详细内容,最后附上一张图可以让大家更好的理解文章中代码的执行