渲染这部分可以说是理解 vue3 的过程中最重要的部分,这里面会涉及到响应式原理,这部分在这里会简单讲解,但是细节都会放到单独的模块中进行书写。好的,废话不多,开始。

const setupResult = isStateful
  ? setupStatefulComponent(instance, isSSR)
  : undefined;
isInSSRComponentSetup = false;
return setupResult;

在执行完initPropsinitSlots之后,我们就进入了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;
}

这里我们又看到了enableTrackingresetTracking,也就是在调用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>Compchildren或者在 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会在父节点之前执行。