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相比更加的复杂,这其中涉及到非常多的细节处理,这里总结下主要的流程:

mount主要流程

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值