渲染这部分可以说是理解 vue3 的过程中最重要的部分,这里面会涉及到响应式原理,这部分在这里会简单讲解,但是细节都会放到单独的模块中进行书写。好的,废话不多,开始。
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
isInSSRComponentSetup = false;
return setupResult;
在执行完initProps
和initSlots
之后,我们就进入了setupStatefulComponent
,注意这里只有有状态组件才会执行,也就是说函数组件并没有 setup。
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions;
// 0. create render proxy property access cache
instance.accessCache = {};
// 1. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
// 2. call setup()
const { setup } = Component;
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null);
currentInstance = instance;
pauseTracking();
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
);
resetTracking();
currentInstance = null;
if (isPromise(setupResult)) {
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR);
});
} else if (__FEATURE_SUSPENSE__) {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult;
}
} else {
handleSetupResult(instance, setupResult, isSSR);
}
} else {
finishComponentSetup(instance, isSSR);
}
}
首先我们创建了instance.proxy
,这个其实就是我们在使用 option api 的时候的this
,所以这里要创建一个 proxy,在你调用this.xxx
的时候他才能响应式得作出反应。
然后创建了 context 对象:
function createSetupContext(instance: ComponentInternalInstance): SetupContext {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,
};
}
也就是我们在创建组件 setup 的时候用到的:
const MyComp = {
setup(props, context) {},
};
这里的 context 对象
然后就是调用 setup 方法了,注意这里用callWithErrorHandling
来进行调用,一个主要目的就是为了支持suspense
,这个支持很简单,就是在渲染中throw Promise
就行了,我们可以看到后面判断是否是promise
的代码。
这里有趣的一点是,在开始执行setup
之前和之后分别调用了连个函数:
pauseTracking();
const setupResult = callWithErrorHandling();
// args
resetTracking();
这两方法是干嘛的呢?看字面意思pause
就是暂停,reset
就是重置,Tracking
就是跟踪。那么字面理解一下就是暂停跟踪和重置跟踪的意思。那么这个跟踪啥意思呢?这就涉及到响应式原理了。简单说一下吧:
const Comp = {
setup() {
const state = reactive({ a: 1 });
const x = state.a;
},
};
假设这是我们的组件,在执行setup
的时候,我们的 state 其实是一个 reactive 的对象,后续我们调用state.a
的时候,其实就是把当前的方法上下文(setup 方法)作为state
对象的依赖进行保存,也就是说以后state.a
修改的时候setup
会重新调用!这自然是我们不希望看到的,因为其实setup
只是创建这些响应式对象,其本身自然不应该依赖于他们的变化,那么pauseTracking
就是告诉响应式系统,接下去我们执行的方法就不要记录依赖了。
OK,看到这里大家可能也还会有点模糊,什么叫建立依赖啊,为什么执行中调用就会建立调用啊,这个咱们都放在reactive模块里面去讲解,术业有专攻嘛,所有东西放在这里讲就太杂了。
咱们继续看下去。下面根据setup
方法的返回来做不同的操作,promise 的情况说明是Suspense
,这个我们放在专门的章节讲解。其他情况就调用handleSetupResult
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// setup returned an inline render function
instance.render = setupResult as InternalRenderFunction;
} else if (isObject(setupResult)) {
instance.setupState = reactive(setupResult);
}
finishComponentSetup(instance, isSSR);
}
这里就是两种情况,如果setup
返回方法,说明这是render
方法,二返回对象,则是对应但文件组件的用法,会把返回的对象里面的内容挂到this
能调用到地方。
最终调用finishComponentSetup
function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions;
// template / render function normalization
if (__NODE_JS__ && isSSR) {
if (Component.render) {
instance.render = Component.render as InternalRenderFunction;
}
} else if (!instance.render) {
if (compile && Component.template && !Component.render) {
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement || NO,
});
// mark the function as runtime compiled
(Component.render as InternalRenderFunction)._rc = true;
}
instance.render = (Component.render || NOOP) as InternalRenderFunction;
if (instance.render._rc) {
instance.withProxy = new Proxy(
instance.ctx,
RuntimeCompiledPublicInstanceProxyHandlers
);
}
}
// support for 2.x options
if (__FEATURE_OPTIONS__) {
currentInstance = instance;
applyOptions(instance, Component);
currentInstance = null;
}
}
这里主要就是处理render
方法了,如果没有render
根据情况,可能会把template
编译成render
,这里就不再多做讲解了(相信大家都来看源码了这点代码应该没啥问题)。
setup 处理完了,我们就要回到mountComponent
了,如果你已经忘了这是啥,可以再去回顾一下,在mountComponent
里面执行完setup
之后,就只剩下
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
);
setupRenderEffect
这个方法又很长,就补贴了,在runtime-core -> renderer.ts -> setupRenderEffect
大家自己看就可以了,这里一开始就调用了:
instance.update = effect(function componentEffect() {
// ...content
});
那么就这里简单先展开一下关于 vue3 中的 effect 的内容,我们先看effect
方法的代码:
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw;
}
const effect = createReactiveEffect(fn, options);
if (!options.lazy) {
effect();
}
return effect;
}
通过createReactiveEffect
包装了我们传入的函数,然后直接调用,我们来看createReactiveEffect
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn();
}
if (!effectStack.includes(effect)) {
cleanup(effect);
try {
enableTracking();
effectStack.push(effect);
activeEffect = effect;
return fn();
} finally {
effectStack.pop();
resetTracking();
activeEffect = effectStack[effectStack.length - 1];
}
}
} as ReactiveEffect;
effect.id = uid++;
effect._isEffect = true;
effect.active = true;
effect.raw = fn;
effect.deps = [];
effect.options = options;
return effect;
}
这里我们又看到了enableTracking
和resetTracking
,也就是在调用fn
的时候我们是开启跟踪的。enableTracking
做的事情很简单,最主要的其实是给一个全局标示shouldTrack
设置其为true
,至于trackStack
这里就说不清楚了,后面在 reactive 里面再讲。
export function enableTracking() {
trackStack.push(shouldTrack);
shouldTrack = true;
}
到这里其实我们还是没有讲到 reactive 的原理,但是准备工作我们已经做了,后面真正讲这部分的时候,回过头来看这里机会豁然开朗,相信我,保持耐心。
继续讲 render,这里大概做了这些事:
// beforeMount生命周期
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parent, initialVNode);
}
// ...
// 渲染子树
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
// 把mounted生命周期方法加入到队列
queuePostRenderEffect(m, parentSuspense);
// 标记实例已经挂载
instance.isMounted = true;
这里我们要注意两个概念,直接用代码解释:
const Comp = (p, { slots }) => {
return <div>{slots.default()}</div>;
};
<Comp>
<span>123</span>
</Comp>;
这里面<span>123</span>
是Comp
的children
或者在 vue 里更准确的说是slots
,而div
则是Comp
的子树。
- div 和 Comp 的 owner 关系
- span 和 Comp 是 parent 关系
queuePostRenderEffect
里面做了啥呢?
export function queuePostFlushCb(cb: Function | Function[]) {
if (!isArray(cb)) {
if (
!pendingPostFlushCbs ||
!pendingPostFlushCbs.includes(cb, pendingPostFlushIndex)
) {
postFlushCbs.push(cb);
}
} else {
postFlushCbs.push(...cb);
}
queueFlush();
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true;
nextTick(flushJobs);
}
}
其实就是把内容推入队列,然后在nextTick
之后把所有回调调用。而且这里有个细节,那就是执行顺序:
- 先执行子树
patch
- 然后执行
mounted
入队列
既然他执行了子树的patch
,那么子树的节点自然也会把他的mounted
入队列,所以子节点的mounted
会在父节点之前执行。