在调用 createApp 时,Vue 为我们做了那些工作?
createApp

createApp 方法接收一个参数,然后调用 ensureRenderer 方法。
ensureRenderer 的作用是确保渲染器存在,如果不存在就创建一个渲染器,然后调用渲染器的 createApp 方法。createApp 的作用是创建一个应用实例,然后将这个应用实例返回,相当于一个单例模式。

这里的传入 createRenderer 函数的参数 rendererOptions 是渲染器的一些配置。
const extend = Object.assign;
const patchProp = (el, key, prevValue, nextValue, isSVG = false, prevChildren, parentComponent, parentSuspense, unmountChildren) => {
if (key === "class") {
patchClass(el, nextValue, isSVG);
} else if (key === "style") {
patchStyle(el, prevValue, nextValue);
} else if (isOn(key)) {
if (!isModelListener(key)) {
patchEvent(el, key, prevValue, nextValue, parentComponent);
}
} else if (key[0] === "." ? (key = key.slice(1), true) : key[0] === "^" ? (key = key.slice(1), false) : shouldSetAsProp(el, key, nextValue, isSVG)) {
patchDOMProp(
el,
key,
nextValue,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
);
} else {
if (key === "true-value") {
el._trueValue = nextValue;
} else if (key === "false-value") {
el._falseValue = nextValue;
}
patchAttr(el, key, nextValue, isSVG);
}
};
const nodeOps = {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null);
},
remove: (child) => {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
},
createElement: (tag, isSVG, is, props) => {
const el = isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag, is ? { is } : void 0);
if (tag === "select" && props && props.multiple != null) {
el.setAttribute("multiple", props.multiple);
}
return el;
},
createText: (text) => doc.createTextNode(text),
createComment: (text) => doc.createComment(text),
setText: (node, text) => {
node.nodeValue = text;
},
setElementText: (el, text) => {
el.textContent = text;
},
parentNode: (node) => node.parentNode,
nextSibling: (node) => node.nextSibling,
querySelector: (selector) => doc.querySelector(selector),
setScopeId(el, id) {
el.setAttribute(id, "");
},
// __UNSAFE__
// Reason: innerHTML.
// Static content here can only come from compiled templates.
// As long as the user only uses trusted templates, this is safe.
insertStaticContent(content, parent, anchor, isSVG, start, end) {
const before = anchor ? anchor.previousSibling : parent.lastChild;
if (start && (start === end || start.nextSibling)) {
while (true) {
parent.insertBefore(start.cloneNode(true), anchor);
if (start === end || !(start = start.nextSibling))
break;
}
} else {
templateContainer.innerHTML = isSVG ? `<svg>${content}</svg>` : content;
const template = templateContainer.content;
if (isSVG) {
const wrapper = template.firstChild;
while (wrapper.firstChild) {
template.appendChild(wrapper.firstChild);
}
template.removeChild(wrapper);
}
parent.insertBefore(template, anchor);
}
return [
// first
before ? before.nextSibling : parent.firstChild,
// last
anchor ? anchor.previousSibling : parent.lastChild
];
}
};
const rendererOptions = /* @__PURE__ */ extend({ patchProp }, nodeOps); 现在我们先简单的动手实现一下 createApp 方法,新建一个 runtime-dom.js 文件,然后内容如下:
import { createRenderer } from "./runtime-core";
const createApp = (...args) => {
const rendererOptions = {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null);
},
createText: (text) => document.createTextNode(text)
};
const app = createRenderer(rendererOptions).createApp(...args);
const { mount } = app;
app.mount = (containerOrSelector) => {
//...后面分析再补上
};
return app;
};
export { createApp };现在可以看到我们在实现 createApp 方法的时候,直接调用了 createRenderer 方法,这个方法是创建渲染器的方法,这个方法的实现在 runtime-core 包中; 所以我们需要补上 runtime-core 包中的 createRenderer 方法的实现;
createRenderer
createRenderer 源码实现如下:
function createRenderer(options) {
return baseCreateRenderer(options);
}
function baseCreateRenderer(options, createHydrationFns) {
{
initFeatureFlags();
}
const target = getGlobalThis();
target.__VUE__ = true;
if (!!(process.env.NODE_ENV !== "production") || __VUE_PROD_DEVTOOLS__) {
setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__, target);
}
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
insertStaticContent: hostInsertStaticContent
} = options;
const patch = () => {/**... */}
const processText = () => {/**... */}
const processCommentNode = () => {/**... */}
const mountStaticNode = () => {/**... */}
const patchStaticNode = () => {/**... */}
const moveStaticNode = () => {/**... */}
const removeStaticNode = () => {/**... */}
const processElement = () => {/**... */}
const mountElement = () => {/**... */}
const setScopeId = () => {/**... */}
const mountChildren = () => {/**... */}
const patchElement = () => {/**... */}
const patchBlockChildren = () => {/**... */}
const patchProps = () => {/**... */}
const processFragment = () => {/**... */}
const processComponent = () => {/**... */}
const mountComponent = () => {/**... */}
const updateComponent = () => {/**... */}
const setupRenderEffect = () => {/**... */}
const updateComponentPreRender = () => {/**... */}
const patchChildren = () => {/**... */}
const patchUnkeyedChildren = () => {/**... */}
const patchKeyedChildren = () => {/**... */}
const move = () => {/**... */}
const unmount = () => {/**... */}
const remove = () => {/**... */}
const removeFragment = () => {/**... */}
const unmountComponent = () => {/**... */}
const unmountChildren = () => {/**... */}
const getNextHostNode = () => {/**... */}
const render = () => {/**... */}
const internals = {
p: patch,
um: unmount,
m: move,
r: remove,
mt: mountComponent,
mc: mountChildren,
pc: patchChildren,
pbc: patchBlockChildren,
n: getNextHostNode,
o: options
};
let hydrate;
let hydrateNode;
if (createHydrationFns) {
[hydrate, hydrateNode] = createHydrationFns(
internals
);
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
};
}createRenderer 内部返回 baseCreateRenderer 方法的执行结果,这个方法的作用会返回 render、hydrate、createApp 三个方法;
而我们最后需要调用的 createApp 方法就是在这三个方法中的其中一个,而 createApp 方法的是通过 createAppAPI 方法创建的,同时剩下的两个方法 render 和 hydrate 也是在 createAppAPI 方法中被调用的,所以我们还需要看一下 createAppAPI 方法的实现;
createAppAPI
这个方法也在 runtime-core 包中。
createAppAPI 方法的实现如下:
function createAppAPI(render, hydrate) {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent);
}
if (rootProps != null && !isObject(rootProps)) {
!!(process.env.NODE_ENV !== "production") && warn(`root props passed to app.mount() must be an object.`);
rootProps = null;
}
const context = createAppContext();
if (!!(process.env.NODE_ENV !== "production")) {
Object.defineProperty(context.config, "unwrapInjectedRef", {
get() {
return true;
},
set() {
warn(
`app.config.unwrapInjectedRef has been deprecated. 3.3 now always unwraps injected refs in Options API.`
);
}
});
}
const installedPlugins = /* @__PURE__ */ new Set();
let isMounted = false;
const app = context.app = {
_uid: uid$1++,
_component: rootComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {
return context.config;
},
set config(v) {
if (!!(process.env.NODE_ENV !== "production")) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
);
}
},
use(plugin, ...options) {
if (installedPlugins.has(plugin)) {
!!(process.env.NODE_ENV !== "production") && 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 (!!(process.env.NODE_ENV !== "production")) {
warn(
`A plugin must either be a function or an object with an "install" function.`
);
}
return app;
},
mixin(mixin) {
if (__VUE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin);
} else if (!!(process.env.NODE_ENV !== "production")) {
warn(
"Mixin has already been applied to target app" + (mixin.name ? `: ${mixin.name}` : "")
);
}
} else if (!!(process.env.NODE_ENV !== "production")) {
warn("Mixins are only available in builds supporting Options API");
}
return app;
},
component(name, component) {
if (!!(process.env.NODE_ENV !== "production")) {
validateComponentName(name, context.config);
}
if (!component) {
return context.components[name];
}
if (!!(process.env.NODE_ENV !== "production") && context.components[name]) {
warn(`Component "${name}" has already been registered in target app.`);
}
context.components[name] = component;
return app;
},
directive(name, directive) {
if (!!(process.env.NODE_ENV !== "production")) {
validateDirectiveName(name);
}
if (!directive) {
return context.directives[name];
}
if (!!(process.env.NODE_ENV !== "production") && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`);
}
context.directives[name] = directive;
return app;
},
mount(rootContainer, isHydrate, isSVG) {
if (!isMounted) {
if (!!(process.env.NODE_ENV !== "production") && rootContainer.__vue_app__) {
warn(
`There is already an app instance mounted on the host container.
If you want to mount another app on the same host container, you need to unmount the previous app by calling \`app.unmount()\` first.`
);
}
const vnode = createVNode(rootComponent, rootProps);
vnode.appContext = context;
if (!!(process.env.NODE_ENV !== "production")) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG);
};
}
if (isHydrate && hydrate) {
hydrate(vnode, rootContainer);
} else {
render(vnode, rootContainer, isSVG);
}
isMounted = true;
app._container = rootContainer;
rootContainer.__vue_app__ = app;
if (!!(process.env.NODE_ENV !== "production") || __VUE_PROD_DEVTOOLS__) {
app._instance = vnode.component;
devtoolsInitApp(app, version);
}
return getExposeProxy(vnode.component) || vnode.component.proxy;
} else if (!!(process.env.NODE_ENV !== "production")) {
warn(
`App has already been mounted.
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)\``
);
}
},
unmount() {
if (isMounted) {
render(null, app._container);
if (!!(process.env.NODE_ENV !== "production") || __VUE_PROD_DEVTOOLS__) {
app._instance = null;
devtoolsUnmountApp(app);
}
delete app._container.__vue_app__;
} else if (!!(process.env.NODE_ENV !== "production")) {
warn(`Cannot unmount an app that is not mounted.`);
}
},
provide(key, value) {
if (!!(process.env.NODE_ENV !== "production") && key in context.provides) {
warn(
`App already provides property with key "${String(key)}". It will be overwritten with the new value.`
);
}
context.provides[key] = value;
return app;
},
runWithContext(fn) {
currentApp = app;
try {
return fn();
} finally {
currentApp = null;
}
}
};
return app;
};
}看到这里,我们就可以知道,createApp 方法的实现其实就是在 createAppAPI 方法中返回一个函数,这个函数就是 createApp 方法;
这个方法并没有多么特殊,就是返回了一堆对象,这些对象就是我们在使用 createApp 方法时,可以调用的方法;
这里可以看到我们常用的 use、mixin、component、directive、mount、unmount、provide 等方法都是在 app 对象上的,也是通过这个函数制造并返回的;
现在我们继续完善我们的学习 demo 代码,现在新建一个 runtime-core.js 文件夹。
但是我们不能全都都直接照搬,上面的对象这么多的属性我们只需要保留 mount,因为还需要挂载才能看到效果,demo 代码如下:
function createRenderer(options) {
// 先省略 render 和 hydrate 方法的实现,后面会讲到
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
};
}
function createAppAPI(render, hydrate) {
return function createApp(rootComponent, rootProps = null) {
// 省略参数校验
rootComponent = Object.assign({}, rootComponent);
// 省略上下文的创建
const context = {
app: null
}
// 忽略其他函数的实现,只保留 mount 函数和私有变量
let isMounted = false;
const app = (context.app = {
_uid: uid$1++,
_component: rootComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
mount(rootContainer, isHydrate, isSVG) {
// ...
},
});
return app;
};
}这样我们就完成了 createApp 函数的简化版实现,接下来我们就可以开始挂载了;
mount 挂载
上面我们已经学习到了 createApp 函数的实现,现在还需要通过 mount 方法来挂载我们的根组件,才能验证我们的 demo 代码是否正确;
我们在调用 createApp 方法时,会返回一个 app 对象,这个对象上有一个 mount 方法,我们需要通过这个方法来挂载我们的根组件;
在这之前,我们看到了 createApp 的实现中重写了 mount 方法,如下:
const createApp = (...args) => {
// ...省略其他代码
// 备份 mount 方法
const { mount } = app;
// 重写 mount 方法
app.mount = (containerOrSelector) => {
// 获取挂载的容器
const container = normalizeContainer(containerOrSelector);
if (!container)
return;
// _component 指向的是 createApp 传入的根组件
const component = app._component;
// 验证根组件是否是一个对象,并且有 render 和 template 两个属性之一
if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's
// rendered by the server, the template should not contain any user data.
// 确保模板是可信的,因为模板可能会有 JS 表达式,具体可以翻译上面的注释
component.template = container.innerHTML;
}
// clear content before mounting
// 挂载前清空容器
container.innerHTML = '';
// 正式挂载
const proxy = mount(container, false, container instanceof SVGElement);
// 挂载完成
if (container instanceof Element) {
// 清除容器的 v-cloak 属性,这也就是我们经常看到的 v-cloak 的作用
container.removeAttribute('v-cloak');
// 设置容器的 data-v-app 属性
container.setAttribute('data-v-app', '');
}
// 返回根组件的实例
return proxy;
};
return app;
}上面重写的 mount 方法中,其实最主要的做的是三件事:
- 获取挂载的容器
- 调用原本的 mount 方法挂载根组件
- 为容器设置 vue 的专属属性
现在到我们动手实现一个简易版的 mount 方法了;
// 备份 mount 方法
const { mount } = app;
// 重写 mount 方法
app.mount = (containerOrSelector) => {
// 获取挂载的容器
const container = document.querySelector(containerOrSelector);
if (!container)
return;
const component = app._component;
container.innerHTML = '';
// 正式挂载
return mount(container, false, container instanceof SVGElement);
}这里的挂载其实还是使用的是 createApp 函数中的 mount 方法,我们可以看到 mount 方法的实现如下:
function mount(rootContainer, isHydrate, isSVG) {
// 判断是否已经挂载
if (!isMounted) {
// 这里的 #5571 是一个 issue 的 id,可以在 github 上搜索,这是一个在相同容器上重复挂载的问题,这里只做提示,不做处理
// #5571
if ((process.env.NODE_ENV !== 'production') && rootContainer.__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.`);
}
// 通过在 createApp 中传递的参数来创建虚拟节点
const vnode = createVNode(rootComponent, rootProps);
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
// 上面有注释,在根节点上挂载 app 上下文,这个上下文会在挂载时设置到根实例上
vnode.appContext = context;
// HMR root reload
// 热更新
if ((process.env.NODE_ENV !== 'production')) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG);
};
}
// 通过其他的方式挂载,这里不一定指代的是服务端渲染,也可能是其他的方式
// 这一块可以通过创建渲染器的源码可以看出,我们日常在客户端渲染,不会使用到这一块,这里只是做提示,不做具体的分析
if (isHydrate && hydrate) {
hydrate(vnode, rootContainer);
}
// 其他情况下,直接通过 render 函数挂载
// render 函数在 createRenderer 中定义,传递到 createAppAPI 中,通过闭包缓存下来的
else {
render(vnode, rootContainer, isSVG);
}
// 挂载完成后,设置 isMounted 为 true
isMounted = true;
// 设置 app 实例的 _container 属性,指向挂载的容器
app._container = rootContainer;
// 挂载的容器上挂载 app 实例,也就是说我们可以通过容器找到 app 实例
rootContainer.__vue_app__ = app;
// 非生产环境默认开启 devtools,也可以通过全局配置来开启或关闭
// __VUE_PROD_DEVTOOLS__ 可以通过自己使用的构建工具来配置,这里只做提示
if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
app._instance = vnode.component;
devtoolsInitApp(app, version);
}
// 返回 app 实例,这里不做具体的分析
return getExposeProxy(vnode.component) || vnode.component.proxy;
}
// 如果已经挂载过则输出提示消息,在非生产环境下
else if ((process.env.NODE_ENV !== 'production')) {
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)``);
}
}通过上面的一通分析,其实挂载主要就是用的两个函数将内容渲染到容器中;
- createVNode 创建虚拟节点
- render 渲染虚拟节点
我们这里就实现一个简易版的 mount 函数,来模拟挂载过程,代码如下:
function mount(rootContainer, isHydrate) {
// createApp 中传递的参数在我们这里肯定是一个对象,所以这里不做创建虚拟节点的操作,而是模拟一个虚拟节点
const vnode = {
type: rootComponent,
children: [],
component: null,
}
// 通过 render 函数渲染虚拟节点
render(vnode, rootContainer);
// 返回 app 实例
return vnode.component
}虚拟节点
虚拟节点在 Vue 中已经是非常常见的概念了,其实就是一个 js 对象,包含了 dom 的一些属性,比如 tag、props、children 等等;
在 Vue3 中维护了一套自己的虚拟节点,大概信息如下:
export interface VNode {
__v_isVNode: true;
__v_skip: true;
type: VNodeTypes;
props: VNodeProps | null;
key: Key | null;
ref: Ref<null> | null;
scopeId: string | null;
children: VNodeNormalizedChildren;
component: ComponentInternalInstance | null;
suspense: SuspenseBoundary | null;
dirs: DirectiveBinding[] | null;
transition: TransitionHooks<null> | null;
el: RendererElement | null;
anchor: RendererNode | null;
target: RendererNode | null;
targetAnchor: RendererNode | null;
staticCount: number;
shapeFlag: ShapeFlags;
patchFlag: number;
dynamicProps: string[] | null;
dynamicChildren: VNode[] | null;
appContext: AppContext | null;
}完整的 type 信息太多,这里就只贴 VNode 的相关定义。
render
render 函数是在讲 createRenderer 的时候出现的,是在 baseCreateRenderer 中定义的,具体源码如下:
function baseCreateRenderer(options, createHydrationFns) {
// ...
// 创建 render 函数
const render = (vnode, container, isSVG) => {
// 如果 vnode 不存在,并且容器是发生过渲染,那么将执行卸载操作
if (vnode == null) {
// container._vnode 指向的是上一次渲染的 vnode,在这个函数的最后一行
if (container._vnode) {
unmount(container._vnode, null, null, true);
}
}
// 执行 patch 操作,这里不做具体的分析,牵扯太大,后面会单独讲
else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG);
}
// 刷新任务队列,通常指代的是各种回调函数,比如生命周期函数、watcher、nextTick 等等
// 这里不做具体的分析,后面会单独讲
flushPreFlushCbs();
flushPostFlushCbs();
// 记录 vnode,现在的 vnode 已经是上一次渲染的 vnode 了
container._vnode = vnode;
};
// ...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
};
}render 函数的主要作用就是将虚拟节点渲染到容器中,unmount 函数用来卸载容器中的内容,patch 函数用来更新容器中的内容;
现在来实现一个简易版的 render 函数:
const render = (vnode, container) => {
patch(container._vnode || null, vnode, container);
// 记录 vnode,现在的 vnode 已经是上一次渲染的 vnode 了
container._vnode = vnode;
}unmount 函数不是我们这次主要学习的内容,所以这里不做具体的分析;
patch 函数是 Vue 中最核心的函数,这次也不做具体的分析,后面会单独讲,但是要验证我们这次的学习成果,所以我们需要一个只有挂载功能的 patch 函数,这里我们就自己实现一个简单的 patch 函数;
patch
patch 函数的主要作用就是将虚拟节点渲染到容器中,patch 函数也是在 baseCreateRenderer 中定义的;
patch 函数这次就不看了,因为内部的实现会牵扯到非常多的内容,这次只是它的出现只是走个过场,后面会单独讲;
我们这次的目的只是验证我们这次源码学习的成成果,所以我们只需要一个只有挂载功能的 patch 函数,这里我们就自己实现一个简单的 patch 函数;
// options 是在创建渲染器的时候传入的,还记得在 createApp 的实现中,我们传入了一个有 insert 和 createText 方法的对象吗?不记得可以往上翻翻
const { insert: hostInsert, createText: hostCreateText} = options;
// Note: functions inside this closure should use `const xxx = () => {}`
// style in order to prevent being inlined by minifiers.
/**
* 简易版的实现,只是删除了一些不必要的逻辑
* @param n1 上一次渲染的 vnode
* @param n2 当前需要渲染的 vnode
* @param container 容器
* @param anchor 锚点,用来标记插入的位置
*/
const patch = (n1, n2, container, anchor = null) => {
// 上一次渲染的 vnode 和当前需要渲染的 vnode 是同一个 vnode,那么就不需要做任何操作
if (n1 === n2) {
return;
}
// 获取当前需要渲染的 vnode 的类型
const { type } = n2;
switch (type) {
// 如果是文本节点,那么就直接创建文本节点,然后插入到容器中
case Text:
processText(n1, n2, container, anchor);
break;
// 还会有其他的类型,这里不做具体的分析,后面会单独讲
// 其他的情况也会有很多种情况,这里统一当做是组件处理
default:
processComponent(n1, n2, container, anchor);
}
};patch 函数的主要作用就是将虚拟节点正确的渲染到容器中,这里我们只实现了文本节点和组件的渲染,其他的类型的节点,后面会单独讲;
而我们在使用 createApp 的时候,通常会传入一个根组件,这个根组件就会走到 processComponent 函数中;
所以我们这里还需要实现了一个简单的 processComponent 函数;
const processComponent = (n1, n2, container, anchor) => {
if (n1 == null) {
mountComponent(n2, container, anchor);
}
// else {
// updateComponent(n1, n2, optimized);
// }
};processComponent 函数也是定义在 baseCreateRenderer 中的,这里还是和 patch 函数一样,只是实现了一个简单的功能,后面会单独讲;
processComponent 函数做了两件事,一个是挂载组件,一个是更新组件,这里我们只实现了挂载组件的功能;
挂载组件是通过 mountComponent 函数实现的,这个函数也是定义在 baseCreateRenderer 中的,但是我们这次就不再继续深入内部调用了,直接实现一个简易的:
const mountComponent = (initialVNode, container, anchor) => {
// 通过调用组件的 render 方法,获取组件的 vnode
const subTree = initialVNode.type.render.call(null);
// 将组件的 vnode 渲染到容器中,直接调用 patch 函数
patch(null, subTree, container, anchor);
};这样我们就实现了一个简易版的挂载组件的功能,这里我们只是简单的调用了组件的 render 方法,render 方法会返回一个 vnode,然后调用 patch 函数将 vnode 渲染到容器中;
现在回头看看 patch 函数,还差一个 processText 函数没有实现,这个函数也是定义在 baseCreateRenderer 中的,这个比较简单,下面的代码就是实现的 processText 函数:
const processText = (n1, n2, container, anchor) => {
if (n1 == null) {
hostInsert((n2.el = hostCreateText(n2.children)), container, anchor);
}
// else {
// const el = (n2.el = n1.el);
// if (n2.children !== n1.children) {
// hostSetText(el, n2.children);
// }
// }
};我这里屏蔽掉了更新的操作,这里只管挂载,这里的 hostInsert 和 hostCreateText 函数就是在我们实现简易 patch 函数的时候,在 patch 函数实现的上面,通过解构赋值获取的,没印象可以回去看看;
验证
现在我们已经实现了一个简易版的 createApp 函数,并且我们可以通过 createApp 函数创建一个应用,然后通过 mount 方法将应用挂载到容器中;
我们可以通过下面的代码来验证一下:
import { createApp } from "./runtime-dom";
const app = createApp({
render() {
return {
type: "Text",
children: "hello world"
};
}
});
app.mount("#app");总结
我们通过阅读 Vue3 的源码,了解了 Vue3 的 createApp 函数的实现,createApp 函数是 Vue3 的入口函数,通过 createApp 函数我们可以创建一个应用;
createApp 的实现是借助了 createRenderer 函数,createRenderer 的实现就是包装了 baseCreateRenderer;
baseCreateRenderer 函数是一个工厂函数,通过 baseCreateRenderer 函数我们可以创建一个渲染器; baseCreateRenderer 函数接收一个 options 对象,这个 options 对象中包含了一些渲染器的配置,比如 insert、createText 等;
这些配置是在 runtime-dom 中实现的,runtime-dom 中的 createApp 函数会将这些配置透传递给 baseCreateRenderer 函数,然后 baseCreateRenderer 函数会返回一个渲染器,这个渲染器中有一个函数就是 createApp;
createApp 函数接收一个组件,然后返回一个应用,这个应用中有一个 mount 方法,这个 mount 方法就是用来将应用挂载到容器中的;
在 createApp 中重写了 mount 方法,内部的实现是通过调用渲染器的 mount 方法;
这个 mount 方法是在 baseCreateRenderer 函数中实现的,baseCreateRenderer 函数中的 mount 方法会调用 patch 函数;
patch 函数内部会做很多的事情,虽然我们这里只实现了挂载的逻辑,但是也是粗窥了 patch 函数的内部一些逻辑;
最后我们实现了一个精简版的 createApp 函数,通过这个函数我们可以创建一个应用,然后通过 mount 方法将应用挂载到容器中,这个过程中我们也了解了 Vue3 的一些实现细节;