前言
本文是 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 工作过程原理的详细内容。