Vue中的编译器

391 阅读3分钟

1. 简介

就是将用户在SFC中写的template编译成选项,这个选项就是一个js对象

2. 执行时机

  • webpack下不带编译器,会通过vue-loader进行预编译
  • 带运行时,vm.$mount(vue2) vm.mount(vue3)的时刻会执行

注意:运行时的编译会存在性能问题,不仅仅vue变大了,运行时的速度也变慢了

编译器原理

未命名文件.png

vue3 和 vue2 编译器实现基本相同只不过做了几点有话,后面会说到

vue3中编译器源码实现

// vue包/src/index.ts
// 模版获取
// 编译template微render函数
function compileToFunction(
  template: string | HTMLElement,
  options?: CompilerOptions
): RenderFunction {
  if (!isString(template)) {
    if (template.nodeType) {
      // 从素宿主元素的innerHTML获取模版
      // 总之一定要得到template
      template = template.innerHTML
    } else {
      __DEV__ && warn(`invalid template option: `, template)
      return NOOP
    }
  }

  // 获取上次编译的缓存结果
  const key = template
  const cached = compileCache[key]
  if (cached) {
    return cached
  }
  // 用户传递进来的是0选择器(#app)
  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.
    // 找到el,在拿到innerHTML
    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
    )
  )

  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.
  // code: `function() { return () => {} }`
  // render包装成函数
  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)
}
// we name it `baseCompile` so that higher order compilers like
// @vue/compiler-dom can export `compile` while re-exporting everything else.
// compileToFunction调用的函数就是它
export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  const onError = options.onError || defaultOnError
  const isModuleMode = options.mode === 'module'
  /* istanbul ignore if */
  if (__BROWSER__) {
    if (options.prefixIdentifiers === true) {
      onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
    } else if (isModuleMode) {
      onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
    }
  }

  const prefixIdentifiers =
    !__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
  if (!prefixIdentifiers && options.cacheHandlers) {
    onError(createCompilerError(ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED))
  }
  if (options.scopeId && !isModuleMode) {
    onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
  }
  // 1. 解析(parse):template => ast
  const ast = isString(template) ? baseParse(template, options) : template
  // 2. 转换(深加工 transform) ast => ast
  const [nodeTransforms, directiveTransforms] =
    getBaseTransformPreset(prefixIdentifiers)

  if (!__BROWSER__ && options.isTS) {
    const { expressionPlugins } = options
    if (!expressionPlugins || !expressionPlugins.includes('typescript')) {
      options.expressionPlugins = [...(expressionPlugins || []), 'typescript']
    }
  }
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )
  // 生成:ast =》js function
  // 里面是一段递归的遍历,找到一个节点生成一段代码
  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}

4. vue3当中编译器的优化

准备

源码中运行npm run dev-compiler; 打开template-explorer/local.html

1. 静态节点提升

image.png

如图,render函数外面定义了静态节点,静态节点会被缓存起来,下次再来的时候就没有必要进行创建和生成(用内存换取事件)

2. 补丁标记和动态属性记录

image.png

createElementVNode 第二个参数会把动态属性标记出来(动态属性记录) 第四个参数 8 8 2^3 1000 第四位为1,说明属性是动态的 9 8+1 1001 第1位为1说明内部文本是动态的

这种操作就是patchFlag(布丁标记)

3. 缓存事件处理程序

vue中 @click="onClick" react onClick={(...args) => onClick(...args)}

为啥vue可以直接写函数名呢?因为vue编译器会进行处理,vue3会做一件事,叫回调函数的缓存,内部会处理成成一个像react一样的剪头函数

如果在@click时候写一个箭头函数,那么每次进来都会是一个新的函数,就会导致子树的整棵树的更新,React中有优化useCallback,就是为了解决这个问题;在vue中你只需写一个函数名称,内部会自动对事件程序做一个缓存

4. 块block

一个代码片段会成为一个block,块中有一个数组dynamicChildren,存储需要更新的节点,最后只需要遍历这个数组即可