Vue3源码

起源

本次我看 Vue3 源码的初衷是因为,在目前 Vue3 的设计中,给子组件绑定原生事件的时候,比如 onClick,在父组件 re-render 的时候,子组件上的 onClick 也会重新生成,这就会导致子组件的 re-render。特别是在使用 v-for 的时候,那这个子组件更新的频率就很高了,如果要优化的话有点像 react 的 useCallback 了。虽然 Vue3 也提供了 v-memo 让我们手动去设置依赖(使用 memo 的时候甚至会略过 VNode 的生成),但是如果这原本就可以从底层去实现的话,岂不是更好。所以我就开始基于这个问题去构思一下是从可以给 Vue3 提个 pr。

不过既然准备开始看 Vue3 源码,那也还是记录一下,常看常新。

入口

runtime-dom/index.ts 暴露出 createApp Api,其中需要通过 createRenderer 生成 baseRenderer,其中 baseRenderer 中包含了 patch 等各种底层 api。而 baseRenderer 导出项为:

return {
  render,
  hydrate,
  createApp: createAppAPI(render, hydrate),
};

导出的 createApp 函数中,会返回 app 对象,其中有熟知的 mount,unmount,component,mixin,directive,use 等 api。

在执行 mount 的时候,会基于 rootComponent,通过 createVNode 来生成 rootVNode,这里的 node 是包含第一层的,并不会直接遍历子节点。

if (isHydrate && hydrate) {
  // 主要用于ssr,用来复用原本已经存在的DOM 节点,减少重新生成节点以及删除原本DOM 节点的开销,来加速初次渲染的功能,应该是这样
  hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
  // 我们客户端使用的时候就会调用这个
  // 在这里就会变遍历子节点同时render
  render(vnode, rootContainer, isSVG)
}

而这里的 render 函数,就是 baseRenderer 中导出的 render。

const render: RootRenderFunction = (vnode, container, isSVG) => {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true);
    }
  } else {
    // 因为首次render的时候是没有_vnode的,所以会直接去生成节点
    patch(container._vnode || null, vnode, container, null, null, null, isSVG);
  }
  flushPreFlushCbs();
  flushPostFlushCbs();
  container._vnode = vnode;
};

既然我们主要去看组件的 patch,那就直接看 processComponent 的实现。

  // renderer.ts
  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

  // renderer.ts
  const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
    const instance = (n2.component = n1.component)!
    if (shouldUpdateComponent(n1, n2, optimized)) {
      if (
        __FEATURE_SUSPENSE__ &&
        instance.asyncDep &&
        !instance.asyncResolved
      ) {
        // async & still pending - just update props and slots
        // since the component's reactive effect for render isn't set-up yet
        if (__DEV__) {
          pushWarningContext(n2)
        }
        updateComponentPreRender(instance, n2, optimized)
        if (__DEV__) {
          popWarningContext()
        }
        return
      } else {
        // normal update
        instance.next = n2
        // in case the child component is also queued, remove it to avoid
        // double updating the same child component in the same flush.
        invalidateJob(instance.update)
        // instance.update is the reactive effect.
        instance.update()
      }
    } else {
      // no update needed. just copy over properties
      n2.el = n1.el
      instance.vnode = n2
    }
  }

  // componentRenderUtils.ts
  export function shouldUpdateComponent(
    prevVNode: VNode,
    nextVNode: VNode,
    optimized?: boolean
  ): boolean {
    const { props: prevProps, children: prevChildren, component } = prevVNode
    const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
    const emits = component!.emitsOptions

    // Parent component's render function was hot-updated. Since this may have
    // caused the child component's slots content to have changed, we need to
    // force the child to update as well.
    if (__DEV__ && (prevChildren || nextChildren) && isHmrUpdating) {
      return true
    }

    // force child update for runtime directive or transition on component vnode.
    if (nextVNode.dirs || nextVNode.transition) {
      return true
    }

    if (optimized && patchFlag >= 0) {
      if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
        // slot content that references values that might have changed,
        // e.g. in a v-for
        return true
      }
      if (patchFlag & PatchFlags.FULL_PROPS) {
        if (!prevProps) {
          return !!nextProps
        }
        // presence of this flag indicates props are always non-null
        return hasPropsChanged(prevProps, nextProps!, emits)
      } else if (patchFlag & PatchFlags.PROPS) {
        const dynamicProps = nextVNode.dynamicProps!
        for (let i = 0; i < dynamicProps.length; i++) {
          const key = dynamicProps[i]
          if (
            nextProps![key] !== prevProps![key] &&
            !isEmitListener(emits, key)
          ) {
            return true
          }
        }
      }
    } else {
      ...
    }

    return false
  }

这里可以看到,首先我们更新的时候 patchFlag 并不是 DYNAMIC_SLOTS 也不是 FULL_PROPS,其实就是 dynamicProps 上我们绑定的原生事件前后 2 个 VNode 对比不一样了,所以就 update 了。那是不是我们只要把我们自己绑定的原生事件从 dynamicProps 上去掉就可以了呢?如果不是原生事件就依旧保留在 dynamicProps。

那我们就要去查看生成 VNode 函数的地方了,结果发现 dynamicProps 是一个参数,那我们还是根据第一次组件挂载的顺序去看代码吧。

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode {
  ...
}

第一次挂载的时候,将根组件 processComponent,然后会执行 mountComponent,接着在 mountComponent 中执行 setupComponent(instance)来进一步完善组件实例。

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR;

  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;
}

这里会去初始化 props 和 slots,然后会根据 setup 是否返回了值来决定是否要执行 setupStatefulComponent,在 setupStatefulComponent,中还会执行 finishComponentSetup,会获取这个 component 的 render 函数。

执行完 setupComponent 后就会去执行 setupRenderEffect。在 setupRenderEffect 中,因为已经拿到了组件的 render 函数,然后就会根据 render 函数去执行 renderComponentRoot 获得 subTree,其中会执行 instance.render,而这里的 render 函数其实就是 Component.render(在 finishComponentSetup 中获取到的)。

其中 Component.render 是基于 Vue 的 compileToFunction 来获得的。

// vue/src/index.ts
function compileToFunction(
  template: string | HTMLElement,
  options?: CompilerOptions
): RenderFunction {
  if (!isString(template)) {
    if (template.nodeType) {
      template = template.innerHTML
    } else {
      __DEV__ && warn(`invalid template option: `, template)
      return NOOP
    }
  }

  const key = template
  const cached = compileCache[key]
  if (cached) {
    return cached
  }

  if (template[0] === '#') {
    const el = document.querySelector(template)
    if (__DEV__ && !el) {
      warn(`Template element not found or is empty: ${template}`)
    }
    // __UNSAFE__
    // Reason: potential execution of JS expressions in in-DOM template.
    // The user must make sure the in-DOM template is trusted. If it's rendered
    // by the server, the template should not contain any user data.
    template = el ? el.innerHTML : ``
  }

  const opts = extend(
    {
      hoistStatic: true,
      onError: __DEV__ ? onError : undefined,
      onWarn: __DEV__ ? e => onError(e, true) : NOOP
    } as CompilerOptions,
    options
  )

  if (!opts.isCustomElement && typeof customElements !== 'undefined') {
    opts.isCustomElement = tag => !!customElements.get(tag)
  }
  const { code } = compile(template, opts)

  function onError(err: CompilerError, asWarning = false) {
    const message = asWarning
      ? err.message
      : `Template compilation error: ${err.message}`
    const codeFrame =
      err.loc &&
      generateCodeFrame(
        template as string,
        err.loc.start.offset,
        err.loc.end.offset
      )
    warn(codeFrame ? `${message}\n${codeFrame}` : message)
  }

  // The wildcard import results in a huge object with every export
  // with keys that cannot be mangled, and can be quite heavy size-wise.
  // In the global build we know `Vue` is available globally so we can avoid
  // the wildcard object.
  const render = (
    __GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
  ) as RenderFunction

  // mark the function as runtime compiled
  ;(render as InternalRenderFunction)._rc = true

  return (compileCache[key] = render)
}

这里的 render 函数会先通过 compile 来获取编译完的代码块,其中包含了静态提升等一些编译优化。


//template
<div id="demo">
  <item v-for="(i, index) in arr" :key="index" :is-spread="i.age" @click="ageUp( index)" ></item>
  <h2 @click="aUp"></h2>
</div>

const arr = ref([
  {age:1},
  {age:10},
  {age:30},
  {age:50},
])

const a = ref(1)
const ageUp = (index) => {arr.value[index].age += 1}
const aUp = () => a.value++

// 编译之后的code
const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue

const _hoisted_1 = ["onClick"]

return function render(_ctx, _cache) {
  with (_ctx) {
    const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, resolveComponent: _resolveComponent, createBlock: _createBlock, toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = _Vue

    const _component_item = _resolveComponent("item")

    return (_openBlock(), _createElementBlock(_Fragment, null, [
      (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(arr, (i, index) => {
        return (_openBlock(), _createBlock(_component_item, {
          key: index,
          "is-spread": i.age,
          onClick: $event => (ageUp( index))
        }, null, 8 /* PROPS */, ["is-spread", "onClick"]))
      }), 128 /* KEYED_FRAGMENT */)),
      _createElementVNode("h2", { onClick: aUp }, _toDisplayString(a), 9 /* TEXT, PROPS */, _hoisted_1)
    ], 64 /* STABLE_FRAGMENT */))
  }
}

在我拿到的 code 中可以看到,首先 h2 标签的 onClick 是被静态提升了,但是组件 item 的 onClick 并没有,那我就要去分析这是为什么了。

compile -> baseCompile -> tramsform -> hoistStatic -> walk 发现只有单纯的元素节点或者文本才有机会去执行静态提升,所以这里的 h2 的 onClick 会被静态提升,但是组件 item 的就不会。

if (
  child.type === NodeTypes.ELEMENT &&
  child.tagType === ElementTypes.ELEMENT
){
  ...
  if (codegenNode.dynamicProps) {
    codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps)
  }
}

还是回到之前的 dynamicProps。 compile -> transform -> traverseNode -> nodeTransforms(transformElement) -> buildProps。 在 buildProps 之后,我们就可以拿到组件上的 dynamicProps,接着再通过静态标记,来把所需要静态提升的变量给提取出来。

最终回到原来的问题,如果我们想自动忽略由于父组件更新导致子组件 onClick 事件更新并重新渲染的问题,最好还是通过静态提升来实现。因为 dynamicProps 属性的获取属于一个普遍的方法,在这里去处理适用性很差,而且也会对别的元素节点产生影响,耦合比较严重。在静态提升那部分代码中,感觉可以判断单独判断一下组件的 for 循环,将部分原生事件的直接静态提升,不如这个改动的破坏性应该比较大,还需要好好思考怎么优化。

blog comments powered by Disqus
目 录