-
createApp
createApp 可选两个参数// 在 Vue.js 3.0 中,初始化一个应用的方式如下 import { createApp } from 'vue' import App from './app' const app = createApp(App) app.mount('#app')

createApp 源码内部总体概览
const createApp = (...args) => {
// 1.创建 app 对象
const app = ensureRenderer().createApp(...args);
const { mount } = app;
// 2.重写 mount 方法
app.mount = (containerOrSelector) => {
// ...
};
// 3.返回app实例
return app;
};
1.创建 app 对象 ,createApp 函数内部流程
// 渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法
function ensureRenderer() {
return renderer || renderer = createRenderer(rendererOptions); // 初始时化走后面,创建渲染器
}
function createRenderer(options) {
return baseCreateRenderer(options);
}
function baseCreateRenderer(options) {
function render(vnode, container) {
// 2000行 组件渲染的核心逻辑
}
return {
render,
createApp: createAppAPI(render), // render就是上两行的这个,此render并不是组件中用到的render。
};
}
function createAppAPI(render) {
// createApp createApp 方法接受的两个参数:根组件的对象和 prop
return function createApp(rootComponent, rootProps = null) { // 该createApp 就是暴露给main中调用的方法
const app = {
_component: rootComponent,
_props: rootProps,
mixin(),
use(){},
mount(rootContainer) { // 这个mount在外面被重写了,最终app.mount()调用的是下方重写的mount
// 创建根组件的 vnode
const vnode = createVNode(rootComponent, rootProps);
// 利用渲染器渲染 vnode
render(vnode, rootContainer);
app._container = rootContainer;
return vnode.component.proxy;
},
};
return app;
};
}
- 重写
mount函数,也可以理解为 对上面的app实例里本来有的mount函数的前置处理
const { mount } = app
// 2. 重写 mount 方法 也是最外面用户调用mount方法的时候,就是调用这个函数
app.mount = (containerOrSelector) => {
// container是真实的DOM元素 这一步是对容器标准化
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component // 组件的options
//如果组件对象中没有方法且没有渲染函数且没有模板,那么直接把innerHTML方法作为组件的模板内容
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// 清空 容器中的内容
container.innerHTML = ''
//运行重写后,与平台无关的mount,实现挂载,利用了函数包函数,也就是函数珂理化
const proxy = mount(container)
if (container instanceof Element) {
// 删除元素上的 v-cloak 指令
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
这里先进行mount的前置处理(重写)是为了适应平台的一些内容的处理,可以兼容vue2的写法,因为vue不单单是为了web平台服务的,是需要进行跨平台渲染的,因此内部不能够包含任何指定平台的内容,createApp函数内部的app.mount方法是一个标准的可跨平台的组件渲染流程:先创建VNode,再渲染VNode。
首先是通过 normalizeContainer 标准化容器(这里可以传字符串选择器或者 DOM 对象,但如果是字符串选择器,就需要把它转成 DOM 对象,作为最终挂载的容器),然后做一个 if 判断,如果组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容;接着在挂载前清空容器内容,最终再调用 app.mount 的方法走标准的组件渲染流程。
在这里,重写的逻辑都是和 Web 平台相关的,所以要放在外部实现。此外,这么做的目的是既能让用户在使用 API 时可以更加灵活,也兼容了 Vue.js 2.x 的写法,比如 app.mount 的第一个参数就同时支持选择器字符串和 DOM 对象两种类型。
- 返回app实例
return app2
})
上面是 const app = createApp(App) 干的事,总结一下createApp
ensureRenderer()判断是否为初始化加载,如果是初始化加载就创建渲染器,否则就复用createRenderer()没干别的 ,单纯返回baseCreateRenderer()baseCreateRenderer()核心渲染逻辑就在这里面,接着返回时,把创建的渲染器给createAppAPI调用createAppAPI(render),createAppAPI中定义了mixin(),use() 等方法 ,mount方法在后面被重写,返回app实例
从 app.mount(#app)开始,才算真正进入组件渲染流程,那么接下来,我们就重点看一下核心渲染流程做的两件事情:创建 vnode 和渲染 vnode。
mount内部
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// 创建根组件的 vnode
const vnode = createVNode(rootComponent, rootProps)
// 利用渲染器渲染 vnode
render(vnode, rootContainer)
app._container = rootContainer
return vnode.component.proxy
}
},
createVNode 内部
function createVNode(type, props = null, children = null) {
const vnode = {
type,
props,
...
};
// 标准化子节点,把不同数据类型的 children 转成数组或者文本类型
normalizeChildren(vnode, children);
return vnode;
}
再看render内部
const render = (vnode, container) => {
if (vnode == null) {
// 销毁组件
if (container._vnode) {
unmount(container._vnode, null, null, true);
}
} else {
// 创建或者更新组件
patch(container._vnode || null, vnode, container);
}
};
这个渲染函数 render 的实现很简单,如果它的第一个参数 vnode 为空,则执行销毁组件的逻辑,否则执行创建或者更新组件的逻辑。
着看一下渲染 vnode的 patch 函数
patch内部
const patch = ( n1,n2,container,....) => {
// 如果存在新旧节点, 且新旧节点类型不同,则销毁旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1);
unmount(n1, parentComponent, parentSuspense, true);
n1 = null;
}
if (n2.shapeFlag & 1 /* ELEMENT */) {
// 渲染普通 DOM 元素
processElement(
n1,
n2,
container,
);
} else if (shapeFlag & 6 /* COMPONENT */) {
// 渲染组件
processComponent(
n1,
n2,
container,
);
}
}
};
1.渲染组件的 processComponent 函数的实现:
const processComponent = (
n1,
n2,
container,
) => {
if (n1 == null) {
// 挂载组件
mountComponent(
n2,
container,
);
} else {
// 更新组件
updateComponent(n1, n2, parentComponent, optimized);
}
};
该函数的逻辑很简单,如果 n1 为 null,则执行挂载组件的逻辑,否则执行更新组件的逻辑。
看看其中mountComponent实现
const mountComponent = (
initialVNode,
container,
) => {
// 创建组件实例
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
));
// 设置组件实例
setupComponent(instance);
// 设置并运行带副作用的渲染函数
setupRenderEffect( );
};
setupRenderEffect带副作用的渲染函数内部
const setupRenderEffect = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 渲染组件生成子树 vnode
const subTree = (instance.subTree = renderComponentRoot(instance));
// 把子树 vnode 挂载到 container 中
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
// 保留渲染生成的子树根 DOM 节点
initialVNode.el = subTree.el;
instance.isMounted = true;
} else {
// 更新组件
}
}, prodEffectOptions);
};
这里当组件的数据发生变化时,effect 函数包裹的内部渲染函数 componentEffect 会重新执行一遍,从而达到重新渲染组件的目的。
setupRenderEffect 主要做两件事情渲染组件生成 subTree、把 subTree 挂载到 container 中。
知识点:
- 节点渲染生成的 vnode ,对应的就是 Hello 组件的 initialVNode eg : p标签
- 组件内部整个 DOM 节点对应的 vnode 就是执行 renderComponentRoot 渲染生成对应的 subTree,我们可以把它称作“子树 vnode”
每个组件都会有对应的 render 函数, template也会编译成 render 函数,而 renderComponentRoot 函数就是去执行 render 函数创建整个组件树内部的 vnode,把这个 vnode 再经过内部一层标准化,就得到了该函数的返回结果:子树 vnode
渲染生成子树 vnode 后,接下来就是把 vnode 挂载到 container 中了。
2.渲染普通 DOM元素的 processElement 函数
const processElement = (
n1,
n2,
container,
) => {
if (n1 == null) {
//挂载元素节点
mountElement();
} else {
//更新元素节点
patchElement();
}
};
该函数的逻辑很简单,如果 n1 为 null,走挂载元素节点的逻辑,否则走更新元素节点逻辑。
mountElement()内部实现
const mountElement = (
vnode,
container,
) => {
// 创建 DOM 元素节点
let el = vnode.el = hostCreateElement();
if (props) {
// 处理 props,比如 class、style、event 等属性
}
if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// 处理子节点是数组的情况
mountChildren(
// 循环递归 patch 挂载 child
patch(
null,
child,
);
);
}
// 把创建的 DOM 元素节点挂载到 container 上
hostInsert(el, container, anchor);
};
hostCreateElement() 它调用了底层的 DOM API document.createElement 创建元素,所以本质上 Vue.js 强调不去操作 DOM ,只是希望用户不直接碰触 DOM,vue来帮你操作 DOM。
最后app.mount()流程结束 ,总结一下mount()
createVNode(rootComponent, rootProps)拿到元素和属性,创建节点render(vnode, container)拿到上面创建好的节点,render渲染器开始渲染patch()创建或者更新组件,内部根据元素的类型不同来调用不同的方法渲染processComponent ()渲染组件mountComponent ()创建组件实例、设置组件实例、设置并运行带副作用的渲染函数setupRenderEffectsetupRenderEffect()当组件的数据发生变化时,渲染组件生成 subTree(子树 vnode)、把 subTree 挂载到 container 中。
processElement ()渲染html元素mountElement ()调用hostCreateElement()创建节点, 处理 props、处理 children、挂载 DOM 元素到 container 上。hostCreateElement()调用了底层的 DOM APIdocument.createElement创建元素
- patch 构造完成DOM 树,完成组件的渲染。
- 最后通过
hostInsert ()方法把构造 DOM树挂载到 container 上
本文详细解析了Vue.js3.0中createApp的使用方法,包括创建app对象、重写mount方法以及核心渲染流程,展示了如何通过createAppAPI创建和挂载组件,强调了跨平台渲染和组件生命周期管理的关键点。
762

被折叠的 条评论
为什么被折叠?



