Vue3 源码解析系列 - 编译流程

344 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

前言

上篇我们讲了 mountComponent 里面的逻辑,知道了里面是如何处理属性、插槽、还有 setup 方法,并在最后进行调用 finishComponentSetup 方法,这一篇我们着重学习一下 finishComponentSetup的逻辑。

finishComponentSetup

export function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean,
  skipOptions?: boolean
) {
  const Component = instance.type as ComponentOptions

  // template / render function normalization
  // could be already set when returned from setup()
  if (!instance.render) {
    // only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
    // is done by server-renderer
    if (!isSSR && compile && !Component.render) {
      const template =
        (__COMPAT__ &&
          instance.vnode.props &&
          instance.vnode.props['inline-template']) ||
        Component.template
      if (template) {
        const { isCustomElement, compilerOptions } = instance.appContext.config
        const { delimiters, compilerOptions: componentCompilerOptions } =
          Component
        const finalCompilerOptions: CompilerOptions = extend(
          extend(
            {
              isCustomElement,
              delimiters
            },
            compilerOptions
          ),
          componentCompilerOptions
        )
        Component.render = compile(template, finalCompilerOptions)
      }
    }

    instance.render = (Component.render || NOOP) as InternalRenderFunction

    // for runtime-compiled render functions using `with` blocks, the render
    // proxy used needs a different `has` handler which is more performant and
    // also only allows a whitelist of globals to fallthrough.
    if (installWithProxy) {
      installWithProxy(instance)
    }
  }

  // support for 2.x options
  if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
    setCurrentInstance(instance)
    pauseTracking()
    applyOptions(instance)
    resetTracking()
    unsetCurrentInstance()
  }
}

我们在写组件的时候,会有两种写法,一个时常用的使用 template模版 来写html 代码,或者是使用直接在在组件的 render 方法内调用 h 方法来创建 vnode。 所以这里首先判断了组件的render 函数是否存在,如果不存在说明使用的是 template模版。

编译流程

如果没有render函数,而是使用了template,我们会调用 compile 方法进行编译,不知道大家还记不记得,在一开始的时候,Vue 就会调用一个 registerRuntimeCompiler 方法 会把提前声明的 compileToFunction 方法赋值给这个 compile 变量。所以这里调用的 compile 方法其实就是 compileToFunction。
我们回到最初声明的 compileToFunction 方法。

function compileToFunction(
  template: string | HTMLElement,
  options?: CompilerOptions
): RenderFunction {
  if (!isString(template)) {
    if (template.nodeType) {
      template = template.innerHTML
    }
  }
  const key = template
  const cached = compileCache[key]
  if (cached) {
    return cached
  }
  if (template[0] === '#') {
    const el = document.querySelector(template)
    template = el ? el.innerHTML : ``
  }
  const { code } = compile(
    template,
    extend(
      {
        hoistStatic: true,
        onError: __DEV__ ? onError : undefined,
        onWarn: __DEV__ ? e => onError(e, true) : NOOP
      } as CompilerOptions,
      options
    )
  )
  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)
}
  1. 如果template不是字符串,而且拥有 nodeType 属性,说明是dom节点,所以直接取 template.innerHTML
  2. 有缓存的话直接取缓存
  3. 调用 compile 获取 code,这一步是这个方法最关键的一步,我们待会详细说说
  4. 使用 new Function() 把 code 变成渲染函数。

我们下面看看生成code 的 compile 方法的实现,我们带着问题去看,为什么 code 能用 new Function() 变成函数。

// packages/compiler-dom/src/index.ts
export function compile(
  template: string,
  options: CompilerOptions = {}
): CodegenResult {
  return baseCompile(
    template,
    extend({}, parserOptions, options)
  )
}

里面调用了 baseCompile 方法,并返回结果。

// packages/compiler-core/src/compile.ts 
export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  const isModuleMode = options.mode === 'module'
  const ast = isString(template) ? baseParse(template, options) : template
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )

  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}
  1. 在 baseCompile 中,使用 baseParse 把 template 编译成 抽象语法树AST
  2. 调用 transform 方法,来给 ast 加点属性
  3. 调用 generate 生成code。

那 code 是如何生成的呢?我们看下 generate

// packages/compiler-core/src/codegen.ts
function  (ast: RootNode, options): CodegenResult {
  const context = createCodegenContext(ast, options)
  if (options.onContextCreated) options.onContextCreated(context)
  const {
    mode,
    push,
    prefixIdentifiers,
    indent,
    deindent,
    newline,
    scopeId,
    ssr
  } = context

  const hasHelpers = ast.helpers.length > 0
  const useWithBlock = !prefixIdentifiers && mode !== 'module'
  const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
  const isSetupInlined = !__BROWSER__ && !!options.inline

  const preambleContext = isSetupInlined
    ? createCodegenContext(ast, options)
    : context
  if (!__BROWSER__ && mode === 'module') {
    genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
  } else {
    genFunctionPreamble(ast, preambleContext)
  }
  // enter render function
  const functionName = ssr ? `ssrRender` : `render`
  const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
  if (!__BROWSER__ && options.bindingMetadata && !options.inline) {
    // binding optimization args
    args.push('$props', '$setup', '$data', '$options')
  }
  const signature =
    !__BROWSER__ && options.isTS
      ? args.map(arg => `${arg}: any`).join(',')
      : args.join(', ')

  if (isSetupInlined) {
    push(`(${signature}) => {`)
  } else {
    push(`function ${functionName}(${signature}) {`)
  }
  indent()

  if (useWithBlock) {
    push(`with (_ctx) {`)
    indent()
    // function mode const declarations should be inside with block
    // also they should be renamed to avoid collision with user properties
    if (hasHelpers) {
      push(
        `const { ${ast.helpers
          .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
          .join(', ')} } = _Vue`
      )
      push(`\n`)
      newline()
    }
  }

  // generate asset resolution statements
  if (ast.components.length) {
    genAssets(ast.components, 'component', context)
    if (ast.directives.length || ast.temps > 0) {
      newline()
    }
  }
  if (ast.directives.length) {
    genAssets(ast.directives, 'directive', context)
    if (ast.temps > 0) {
      newline()
    }
  }
  if (__COMPAT__ && ast.filters && ast.filters.length) {
    newline()
    genAssets(ast.filters, 'filter', context)
    newline()
  }

  if (ast.temps > 0) {
    push(`let `)
    for (let i = 0; i < ast.temps; i++) {
      push(`${i > 0 ? `, ` : ``}_temp${i}`)
    }
  }
  if (ast.components.length || ast.directives.length || ast.temps) {
    push(`\n`)
    newline()
  }

  // generate the VNode tree expression
  if (!ssr) {
    push(`return `)
  }
  if (ast.codegenNode) {
    genNode(ast.codegenNode, context)
  } else {
    push(`null`)
  }

  if (useWithBlock) {
    deindent()
    push(`}`)
  }

  deindent()
  push(`}`)

  return {
    ast,
    code: context.code,
    preamble: isSetupInlined ? preambleContext.code : ``,
    // SourceMapGenerator does have toJSON() method but it's not in the types
    map: context.map ? (context.map as any).toJSON() : undefined
  }
}

通过 createCodegenContext 创建完上下文后,会根据 options 的参数, 拼接一个 函数字符串,大概的样子如下

`const _Vue = Vue
const {  } = _Vue

const _hoisted_1 = ["onClick"]

return function render(_ctx, _cache) {
  with (_ctx) {
    const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

    return (_openBlock(), _createElementBlock("h1", { onClick: changeName }, "age:" + _toDisplayString(a), 9 /* TEXT, PROPS */, _hoisted_1))
  }
`

可以看到,这是一个字符串,里面返回的是一个 render 方法。
所以回到 compileToFunction 中,

const { code } = compile(template)
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)

我们生成了 code之后,使用 new Function() 让他变成一个函数,并执行这个函数,以此返回一个 render 方法,方法的作用就是创建 VNode
然后保存到缓存 compileCache 中,方便下一次使用,并返回 render 方法。

小结

这篇我们解析了vue3 的编译流程,获取到了最终的 render 方法。