Vue3源码之mount挂载
前言
当调用Vue.createApp后就会生成一个应用上下文实例,该实例暴露了相关功能API,其中就包含mount函数,该函数实现组件挂载。简易实例如下:
const Counter = {
data() {
return {
counter: 0
}
}
}
Vue.createApp(Counter).mount('#counter')
本文就是梳理mount函数的主要逻辑,旨在理清基本的处理流程(Vue 3.1.1版本)。
mount处理逻辑
在之前的文章Vue3源码之createApp中,mount函数被重写,其逻辑具体如下:
const { mount } = app;
app.mount = (containerOrSelector) => {
const container = normalizeContainer(containerOrSelector);
if (!container) return;
const component = app._component;
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML;
}
container.innerHTML = '';
const proxy = mount(container, false, container instanceof SVGElement);
if (container instanceof Element) {
container.removeAttribute('v-cloak');
container.setAttribute('data-v-app', '');
}
return proxy;
};
实际上逻辑也非常清晰,具体如下:
- 检查挂载点是否是合法
- 设置根组件的template内容,组件要求非函数并且不存在render函数和template
- 清空挂载点中所有内容
- 调用重写前的mount函数实际上就是应用上下文对象输出的原始mount,这是核心逻辑
应用上下文对象中的mount函数
context.app = {
mount(rootContainer, isHydrate, isSVG) {
if (!isMounted) {
const vnode = createVNode(rootComponent, rootProps);
vnode.appContext = context;
// SSR时对应处理
if (isHydrate && hydrate) {
hydrate(vnode, rootContainer);
}
else {
render(vnode, rootContainer, isSVG);
}
isMounted = true;
app._container = rootContainer;
rootContainer.__vue_app__ = app;
{
app._instance = vnode.component;
}
return vnode.component.proxy;
}
}
}
实际上mount函数的主要逻辑是两点:
- createVNode:根据根组件创建对应的虚拟DOM
- render函数执行:需要注意的是该render函数不是根据template解析生成的,而是渲染器内部定义的
createVNode函数生成虚拟DOM
该函数中主要功能就是输出vnode对象,其中主要点有不少,这里暂不做深入讨论,后面会出专门文章。目前主要关注一个属性shapeFlag,该属性是用于标记当前组件的类型,具体逻辑如下:
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0;
需要注意这里的type参数,其值的类型实际上代表了不同的vnode类型,从上面代码逻辑可以知道type参数的类型有如下几种:
- 字符串类型:表示是原生标签HTML标签或SVG标签
- 对象类型:表示是组件
- 函数类型:表示是函数式组件
- 其他类型:可能是原生的文本节点Text、注释节点Comment,也可能是组件例如Fragment、Static等,这涉及到Vue中一些优化相关的处理
实际上对于type参数的理解,在patch函数可以进一步加深对其的理解:
const patch = function() {
switch (type) {
case Text:
case Comment$1:
case Static:
case Fragment:
default:
if (shapeFlag & 1 /* ELEMENT */) {}
else if (shapeFlag & 6 /* COMPONENT */) {}
else if (shapeFlag & 64 /* TELEPORT */) {}
else if (shapeFlag & 128 /* SUSPENSE */) {}
else {
warn('Invalid VNode type:', type, `(${typeof type})`);
}
}
}
渲染器创建时定义的内部函数render
渲染器的创建是惰性单例式的,在其创建过程中实际上定义了许多相关内部函数,在之前的文章Vue3源码之createApp中这部分没有细提,都是在baseCreateRenderer函数中。
渲染器创建时定义的内部函数render其具体逻辑如下:
const render = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true);
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG);
}
flushPostFlushCbs();
container._vnode = vnode;
};
从上面逻辑可知,对于初次挂载就是调用patch函数:
patch函数就是对比新旧虚拟节点,按照对应类型调用对应的方法来处理,例如组件就是调用processComponent、标签就调用processElement等等
这里以组件为实例来梳理,实际上就是调用processComponent:
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
n2.slotScopeIds = slotScopeIds;
if (n1 == null) {
if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized);
} else {
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
} else {
updateComponent(n1, n2, optimized);
}
};
processComponenth函数处理的逻辑非常清晰:mountComponent 还是 updateComponent。对于创建阶段就是调用mountComponent,这里也只关心其逻辑处理。
mountComponent
挂载组件函数的逻辑实际上主要就是如下几点:
const mountComponent = function(initialVNode) {
const instance = createComponentInstance(..);
initialVNode.component = instance;
// 其他逻辑
setupComponent(instance);
if (instance.asyncDep) { // 相关处理 }
// 其他逻辑
setupRenderEffect(..);
}
实际上就是三个函数的调用:
-
createComponentInstance:创建组件实例instance
组件实例中存在一个ctx属性即上下文,该上下文就是一个对象,实际上就是通过Object.defineProperty中定义了_属性(返回instance本身)和以$开头的实例属性,例如$el、$data、$props等
-
setupComponent:执行组件的setup函数(组合式API)
-
setupRenderEffect:创建effect,实际上就是在组件实例对象上定义update函数
setupComponent
该函数的主要逻辑如下:
function setupComponent(instance, isSSR = false) {
const { props, children } = instance.vnode;
const isStateful = isStatefulComponent(instance);
initProps(instance, props, isStateful, isSSR);
initSlots(instance, children);
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
isInSSRComponentSetup = false;
return setupResult;
}
实际上主要逻辑有三点:
- initProps:初始化props
- initSlots:初始化插槽相关的
- setupStatefulComponent:该函数主要就是执行setup函数,针对该函数返回值是否是Promise做不同处理。无论setup函数是否存在,最后都会调用finishComponentSetup函数。
setupStatefulComponent
该函数主要就是执行setup函数,但是还有一个逻辑是创建一个proxy对象,即:
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
为什么说这个逻辑点呢?实际上所谓的组件实例就是该proxy属性值,这里需要结合如下几处来看:
// 逻辑点1
const mountComponent = () => {
...
const instance = (initialVNode.component = createComponentInstance(...));
...
}
// 逻辑点2
context.app = {
mount() {
...
const vnode = createVNode(rootComponent, rootProps);
...
return vnode.component.proxy;
}
}
// 逻辑点3
const { mount } = app;
app.mount = (containerOrSelector) => {
...
const proxy = mount(container, false, container instanceof SVGElement);
...
return proxy;
};
从上面的逻辑点就可以知道组件实例就是一个proxy对象,在组件的方法中this就是组件实例:
组件中this === 组件实例,本质上就是一个被Proxy的对象
finishComponentSetup
finishComponentSetup函数核心的功能如下:
function finishComponentSetup(instance, isSSR, skipOptions) {
...
Component.render = compile(template, finalCompilerOptions);
instance.render = (Component.render || NOOP);
...
// 初始化state
applyOptions(instance);
}
上面是该函数的核心逻辑点,主要就是2点:
- 根据template创建render函数
- 调用applyOptions,该函数主要有3个核心功能
- 执行beforeCreate、create生命周期
- 处理data、computed、watch、methods、inject、provide等
- 注册setup中使用的生命周期
其中对于data的处理,会调用reactive函数进行数据拦截实现响应式,这里暂不谈论Vue3的响应式。
setupRenderEffect
该函数的主要逻辑就是创建update函数,该函数用于之后的视图更新操作,具体逻辑如下:
function createDevEffectOptions(instance) {
return {
// 调度器
scheduler: queueJob,
allowRecurse: true,
onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc, e) : void 0,
onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg, e) : void 0
};
}
const setupRenderEffect = function(instance) {
const effectOptions = createDevEffectOptions(instance);
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
...
} else {
}
}, effectOptions);
};
在setupRenderEffect函数中,会额外调用2个函数:
- createDevEffectOptions:创建effect选项
- effect:创建reactive effect
effect函数的具体逻辑如下:
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
if (!effect.active) {
return fn();
}
if (!effectStack.includes(effect)) {
cleanup(effect);
...
effectStack.push(effect);
return fn();
...
};
};
effect.id = uid++;
effect.allowRecurse = !!options.allowRecurse;
effect._isEffect = true;
effect.active = true;
effect.raw = fn;
effect.deps = [];
effect.options = options;
return effect;
}
function effect(fn, options = EMPTY_OBJ) {
// 创建effect函数
const effect = createReactiveEffect(fn, options);
// 执行effect函数
if (!options.lazy) {
effect();
}
return effect;
}
通过上面的逻辑总结实际上就是如下几点:
创建effect函数,将effect函数推入effectStack中,并且执行componentEffect回调函数
实际上通过effectOptions中的scheduler,可知effect必然与视图渲染的更新机制相关,这里暂不细说相关机制。
这里聊聊componentEffect的执行逻辑,其中主要点如下:
function componentEffect() {
if (!instance.isMounted) {
// beforeMount hook
if (bm) {
invokeArrayFns(bm);
}
// onVnodeBeforeMount
...
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
...
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense);
}
// onVnodeMounted
} else {
...
}
}
除了生命周期beforeMount、mounted的执行逻辑外,最核心的逻辑就是:renderComponentRoot 和 patch的执行,而renderComponentRoot核心的功能就是调用组件的render函数。
可以看出这边的主要的执行逻辑:
beforeMount -> render函数调用 -> patch -> mounted
总结
Vue3的挂载处理与Vue2相比更加的复杂,这其中涉及到非常多的细节处理,这里总结下主要的流程:
更多推荐
所有评论(0)