Vue3源码分析——编译模块和编译器

Vue3 的编译模块包含4个目录:

compiler-core // 编译核心
Compiler-DOM // 浏览器相关
Compiler-sfc // 单文件组件
Compiler-SSR // 服务端渲染

其中,compiler-core 模块是Vue编译的核心模块,与平台无关。其余三个基于 compiler-core,适用于不同的平台。

Vue 的编译分为三个阶段,即 解析(Parse)、转换(Transform)和代码生成(Codegen)

Parse 阶段将模板字符串转换为语法抽象树 ASTTransform 阶段对 AST 做一些转换处理。Codegen 阶段根据 AST 生成相应的渲染函数字符串。

Parse 阶段

分析模板字符串时,Vue 可分为两种情况:以< 开头的字符串,和不是以 < 开头的字符串。

不是以 < 开头的字符串有两种情况:文本节点或者插入表达式 {{exp}}

使用 < 将字符串的开头分为以下几种情况:

  1. 元素开始标签 
  2. 元素结束标签 
  • 注释节点 
  • 文件声明 
  • 用伪代码表示,近似过程如下:

    while (s.length) {
        if (startsWith(s, '{{')) { // 如果开始为 '{{'
            node = parseInterpolation(context, mode)
        } else if (s[0] === '<') { // 元素开始标签
            if (s[1] === '!') {
                if (startsWith(s, '

    所有 AST 节点定义都在 Compiler-core/astts 文件中,下面是元素节点的定义:

    export interface BaseElementNode extends Node {
         TYPE: NODETYPES.EEMENT / / Type 类型
         NS: namespace // 名称空间默认为html, ie 0
         Tag: String // 标签名称
         tagType: ElementTypes // 元素类型
         IsselfClosing: boolean // 是否为自闭标记, 例如 
    Props: Array // 属性, 包含 Html 属性和指令 Children: TemplateChildNode [] // 子级模板指向 }

    用一个比较复杂的例子来解释解析过程。

    {{ test }}

    A text node
    good job!

    上面的模板字符串假定为 S,第一个字符 S[0] 在开始时为 <,这意味着它只能是刚才提到的四种情况之一。

    再看看 S[1] 第二个字符的规则:

    1. 遇到 ! 时,调用字符串原始方法 startsWith(),分析是 

      {{ test }}

      A text node
      good job!

    注释文本和普通文本节点解析规则比较简单简单,直接截断,生成节点。注释节点调用 parseComment() 函数处理,Text 节点调用 parseText() 处理。

    双花插值 {{test}} 的字符串处理逻辑稍微复杂一些:

    1. 首先提取出双括号内的内容,即 test,调用 trim 函数去掉两边空格。
    2. 然后生成两个节点,一个节点为 INTERPOLATION 类型值为5,表示它是一个双花插值。
    3. 第二个节点是其内容 test,将生成节点为 Simple_expression,类型值为4。
    return {
      TYPE: NODETYPES.ITERPOLATION, // 双花括号类型
      content: {
        type: NodeTypes.SIMPLE_EXPRESSION, // 简单表达式类型
        Isstatic: false, // 不是静态节点
        isConstant: false,
        content,
        loc: getSelection(context, innerStart, innerEnd)
      },
      loc: getSelection(context, start)
    }

    字符串解析逻辑的其余部分与上述内容类似,因此未对其进行解释。

    从 AST 中,还可以看到一些节点上的其他属性:

    1. NS,命名空间,通常为 HTML,值为0
    2. LOC,它是一条位置消息,指示此节点位于源 HTML字符串的位置,包含行、列、偏移量等信息。
    3. {{ test }} 解析后的节点将具有 isStatic属性,该值为 false,表示这是一个动态节点。如果是静态节点,则只生成一次,并且会复用相同的节点,不需要进行差异比较。

    还有一个标签类型值,它有4个值:

    export const enum ElementTypes {
      ELEMENT, // 0 元素节点 
      Component, // 1 注释节点
      Slot, // 2 插槽节点
      Template // 3 模板
    }

    主要用于区分以上四种类型的节点。

    如果你想开发小程序或者APP软件,可以通过专业开发公司,来帮助你实现开发需求:厦门在乎科技-专注厦门小程序开发、APP开发、网站开发、H5小游戏开发

    Transform 阶段

    在转换阶段,Vue 将对 AST 执行一些转换操作,主要是根据 CodeGen阶段 使用的不同 AST节点添加不同的选项参数。以下是一些重要的选项:

    cacheHandlers 缓存处理程序

    如果 CacheHandlers 的值为 true,则启用函数缓存。例如 @click="foo" 默认情况下编译为 {onClick:foo},如果打开此选项,则编译为:

    { onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) }  // 具备缓存功能

    hoistStatic 静态提升

    hoistStatic 是一个标识符,表示是否应启用静态节点提升。如果值为 true ,静态节点将被提升在 render() 函数外部,生成名为 _hoisted_x 的变量。

    例如,文本 A text node 生成的代码为 const hoisted_2 = / pure createtextVNode ("a text node")

    prefixIdentifiers 前缀标识

    此参数的角色用于代码生成。例如,{{ foo }} 模块(module)模式下生成的代码是 _ctx.foo,函数(function)模式下生成的代码是 width(this){…}。因为在模块(module)模式下,默认为严格模式,不能使用 with 语句。

    PatchFlags 补丁标识

    转换为 AST 节点时,使用 PatchFlag 参数,该参数主要用于差异比较 diff 过程。当 DOM 节点具有此标志且大于0时,它将被更新,并且不会跳过。

    来看看 PatchFlag 的值:

    export const enum PatchFlags {
      // 动态文本节点
      TEXT = 1,
        // 动态类
      CLASS = 1 << 1, // 2
        // 动态Style
      STYLE = 1 << 2, // 4
        // 动态属性,但不包括 calss 和 style
      // 如果是组件,则可以包含 calss 和 style。
      PROPS = 1 << 3, // 8
      // 具有动态键属性,当键更改时,需要进行完整的 DIFF 差异比较
      FULL_PROPS = 1 << 4, // 16
        // 具有侦听事件的节点
      HYDRATE_EVENTS = 1 << 5, // 32
        // 不改变子序列的片段
      STABLE_FRAGMENT = 1 << 6, // 64
        // 具有key属性的片段或部分子字节具有key
      KEYED_FRAGMENT = 1 << 7, // 128
      // 子节点没有密钥的 key
      UNKEYED_FRAGMENT = 1 << 8, // 256
      // 节点将仅执行 non-PROPS 比较
      NEED_PATCH = 1 << 9, // 512
      // 动态插槽
      DYNAMIC_SLOTS = 1 << 10, // 1024
      // 静态节点
      HOISTED = -1,
      // 退出 DIFF 差异比较优化模式
      BAIL = -2
    }

    从上面的代码可以看出,PatchFlag 使用 bit-map 来表示不同的值,每个值都有不同的含义。 Vue 会在 diff 过程中根据不同的修补标志使用不同的修补方法。

    可以看到 CodegenNodeHelpers 和 Hoists 已填充了相应的值。CodegenNode 是生成要使用的代码的数据。Hoists 存储静态节点。Helpers 存储创建 vNode 的函数名(实际上是 Symbol)。

    在正式开始转换之前,需要创建一个 transformContext,即转换上下文。与这三个属性相关的数据和方法如下:

    helpers: new Set(),
    hoists: [],
    
    // methods
    helper(name) {
      context.helpers.add(name)
      return name
    },
    helperString(name) {
      return `_${helperNameMap[context.helper(name)]}`
    },
    hoist(exp) {
      context.hoists.push(exp)
      const identifier = createSimpleExpression(
        `_hoisted_${context.hoists.length}`,
        false,
        exp.loc,
        true
      )
      identifier.hoisted = exp
      return identifier
    },

    让我们来看看具体的转换过程是如何使用的。用 

    {{ test }}

     举例说明。

    此节点对应 TransformElement() 转换函数,因为 p 没有绑定动态属性,没有绑定指令,所以焦点不在它上面。而 {{test}} 是一个双花插值表达式,所以将其 patchflag 设置为1(动态文本节点),相应的执行代码 patchFlag |=1。然后执行 createVNodeCall() 函数,其返回值为该节点的 codegennode 值。

    node.codegenNode = createVNodeCall(
        context,
        vnodeTag,
        vnodeProps,
        vnodeChildren,
        vnodePatchFlag,
        vnodeDynamicProps,
        vnodeDirectives,
        !!shouldUseBlock,
        false /* disableTracking */,
        node.loc
    )

    createVNodeCall() 会相应的在 createVNode() 中添加一个符号,它放置在 helpers 中。事实上,helpers 功能将在代码生成阶段引入。

    // createVNodeCall () 内部执行过程,多余代码已删除
    context.helper(CREATE_VNODE)
    return {
      type: NodeTypes.VNODE_CALL,
      tag,
      props,
      children,
      patchFlag,
      dynamicProps,
      directives,
      isBlock,
      disableTracking,
      loc
    }

    hoists 提升

    是否将节点提升,主要看它是否是静态节点。

    // 静态属性节点

    {{ test }}

    A text node // 静态节点
    good job!
    // 静态节点

    可以看到,上面有三个静态节点,因此 hoists 数组有3个值。

    这13是 VNODE_CALL 对应的类型值,其他还有:

    // codegen
    VNODE_CALL, // 13
    JS_CALL_EXPRESSION, // 14
    JS_OBJECT_EXPRESSION, // 15
    JS_PROPERTY, // 16
    JS_ARRAY_EXPRESSION, // 17
    JS_FUNCTION_EXPRESSION, // 18
    JS_CONDITIONAL_EXPRESSION, // 19
    JS_CACHE_EXPRESSION, // 20

    刚才提到的例子 {{ test }}, 其 codegen node是 createVnodeCall 函数生成。

    return {
      type: NodeTypes.VNODE_CALL,
      tag,
      props,
      children,
      patchFlag,
      dynamicProps,
      directives,
      isBlock,
      disableTracking,
      loc
    }

    从上面的代码可以看出,type 设置为 nodetypes.VNODE_CALL,即13。
    每个不同的节点由不同的变换函数处理。可以自己再深入的了解。

    你可能感兴趣的:(文章,vue.js,前端,javascript)