关于javascript:Vue3源码分析编译模块和编译器

7次阅读

共计 11205 个字符,预计需要花费 29 分钟才能阅读完成。

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. 元素开始标签 <div>
  2. 元素完结标签 </div>
  3. 正文节点 <!– 123 –>
  4. 文件申明 <!DOCTYPE html>

用伪代码示意,近似过程如下:

while (s.length) {if (startsWith(s, '{{')) { // 如果开始为 '{{'
        node = parseInterpolation(context, mode)
    } else if (s[0] === '<') { // 元素开始标签
        if (s[1] === '!') {if (startsWith(s, '<!--')) { // 正文节点
                node = parseComment(context)
            } else if (startsWith(s, '<!DOCTYPE')) { // 文档语句
                node = parseBogusComment(context)
            }
        } else if (s[1] === '/') { // 完结标签
            parseTag(context, TagType.End, parent)
        } else if (/[a-z]/i.test(s[1])) { // 开始标签名
            node = parseElement(context, ancestors)
        }
    } else { // 一般文本节点     
        node = parseText(context, mode)
    }
}

原始代码点这里 vue-next parse.ts

绝对应的几个函数如下:

  1. parseChildren(), 入口函数
  2. parseInterpolation(),剖析双花插值表达式
  3. parseComment(),解析正文
  4. parseBogusComment(),剖析文件申明
  5. parseTag(),剖析标签
  6. parseElement(),剖析元素节点,它将在外部执行 parseTag()
  7. parseText(),剖析一般文本
  8. parseAttribute(),分析属性

当标签、文本、正文等每个节点生成相应的 AST 节点时,Vue 将截断解析的字符串。

字符串被截断是应用 AdvanceBy(context,numberOfCharacters)函数,context 是字符串的上下文对象,numberOfCharacters是要截断的字符数。

应用一个简略的示例来模仿截断操作:

<div name="test">
  <p></p>
</div>

首先剖析 <div,而后执行 advanceBy(context,4) 截断操作(外部执行 s=s.slice(4)),变成:

name="test">
  <p></p>
</div>

而后分析属性,并将其截断为:

<p></p>
</div>

相似地,以下内容的截断为:

></p>
</div>
</div>
<!-- 所有字符串都已解析 -->

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

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

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

<div name="test">
     <!-- This is a comment-->
  <p>{{test}}</p>
     A text node
  <div>good job!</div>
</div>

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

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

  1. 遇到 ! 时,调用字符串原始办法 startsWith(),剖析是 <!–的结尾,还是 <!DOCTYPE 的结尾,它们对应的处理函数不同,例子中代码最终将解析到正文节点。
  2. 如果是 /,按完结标签。
  3. 如果不是 /,按开始标签解决。

在咱们的示例中,这是一个 <div> 开始标签。

这里要提到的一点,Vue 将应用栈来保留已解析的元素标签。当遇到开始标记时,标签被推入栈中。当遇到完结标记时,将弹出栈。它的作用是保留已解析但尚未解析完的元素标签。在这个栈中还有另一个角色,通过 stack[stack.length-1],能够失去它的父元素。

从咱们的例子来看,在解析过程中,栈中存储如下:

1. [div] // div 入栈
2. [div, P] // p 入栈
3. [div] // P 弹出
4. [div, div] // div 入栈
5. [div] // div 弹出
6. [] // 最初一个 div 弹出后,模板字符串已解析,栈为空。

依照下面的例子,接下来将截断 <div 字符串,并解析其属性。

属性有两种状况:

  1. HTML 的一般属性
  2. Vue 的指令

生成的类型节点值,HTML 一般属性节点类型为 6,Vue 指令节点类型为 7。

所有节点类型值详情如下:

Root, // 根节点为 0
Element, // 元素节点为 1
Text, // 文本节点为 2
Comment, // 正文节点为 3
Simple_expression, // 简略表达式为 4
Interpolation, // 双花插值 {{}} 为 5
Attribute, // 属性为 6
Directive, // 指令为 7

属性剖析后,div 开始标签被剖析结束,<div name=”test”> 此行字符串被截断。其余字符串当初如下所示:

<!-- This is a comment -->
  <p>{{test}}</p>
     A text node
  <div>good job!</div>
</div>

正文文本和一般文本节点解析规定比较简单简略,间接截断,生成节点。正文节点调用 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 如下所示:

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 模板
}

次要用于辨别以上四种类型的节点。

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”)

在上面两张图片中,前一张为 hoistStatic=false,后一张为 hoistStatic=true,都能够本人尝试一下 地址。

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 过程中依据不同的修补标记应用不同的修补办法。

下图为变换后的 AST

能够看到 CodegenNodeHelpersHoists 已填充了相应的值。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
},

让咱们来看看具体的转换过程是如何应用的。用 <p>{{test}}</p> 举例说明。

此节点对应 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 晋升

是否将节点晋升,次要看它是否是动态节点。

<div name = "test"> // 动态属性节点
     <! - This is a comment->
  <p>{{test}}</p>
     A text node // 动态节点
     <div> good job! </div> // 动态节点
</div>

能够看到,下面有三个动态节点,因而 hoists 数组有 3 个值。正文为什么不算动态节点,临时还没有找到起因。。。

TYPE changes 类型扭转

从上图中能够看出,最外层 div 的类型为 1,由 Transform 生成的 CodeGen node 中的类型为 13。

这 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 nodecreateVnodeCall 函数生成。

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

从下面的代码能够看出,type 设置为 nodetypes.VNODE_CALL,即 13。
每个不同的节点由不同的变换函数解决。能够本人再深刻的理解。

Codegen 阶段

代码生成阶段最初生成了一个字符串,去掉了字符串的双引号,具体内容是什么:

const _Vue = Vue
const {createVNode: _createVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode} = _Vue

const _hoisted_1 = {name: "test"}
 const _hoisted_2 = / * # __ pure __ * / _ createtextVNode ("a text node")
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)

return function render(_ctx, _cache) {with (_ctx) {const { createCommentVNode: _createCommentVNode, toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createBlock: _createBlock} = _Vue

    return (_openBlock(), _createBlock("div", _hoisted_1, [_CreateCommentVNode ("This is a comment"),
      _createVNode("p", null, _toDisplayString(test), 1 /* TEXT */),
      _hoisted_2,
      _hoisted_3
    ]))
  }
}

代码生成模式

能够看到下面的代码最终返回了 render() 函数,生成相应的 VNODE

实际上,代码生成有两种模式:模块和函数。选取哪种模式由前缀标识符决定。

函数模式性能:应用 const {helpers…}=Vue 获取帮忙函数的办法,即 createVode()createCommentVNode() 这些函数,最初返回 render() 函数。

模块模式为:应用 ES6 模块导入导出性能,即 importexport

Static node 动态节点

此外,还有三个变量。以 hoisted 命名,前面跟数字,示意这是动态变量。

看看解析阶段的 HTML 模板字符串:

<div name="test">
     <! - This is a comment->
  <p>{{test}}</p>
     A text node
  <div>good job!</div>
</div>

这个示例只有一个动静节点,即 {{test},其余的都是动态节点。从生成的代码中还能够看出,生成的节点和模板中的代码对应于一个或多个节点。动态节点的作用是只生成一次,当前间接重用。

仔细的你可能会发现 Highed_2Highed_3 变量有一个 /#\_PURE_/ 的正文。

此正文的 作用是表明此性能是纯性能,无副作用,次要用于 Tree-shaking 。压缩工具将间接从打包时未应用的代码中删除。

来看下一代动静节点,{{test}} 生成代码对应为 _createVNode(“p”, null, _toDisplayString(test), 1 / TEXT /)

其中,_toDisplayString(test) 的外部实现是:

return val == null
    ? ''
    : isObject(val)
      ? JSON.stringify(val, replacer, 2)
      : String(val)

该代码非常简单,它是一个字符串转换输入。

_createVNode(“p”, null, _toDisplayString(test), 1 / TEXT /) 的最初一个参数减少转换时的 Patchflag 值。

Help function 辅助函数

TransformCodegen 阶段,都看到了 helpers 辅助函数的影子,它是什么呢?

Name mapping for runtime helpers that need to be imported from 'vue' in
generated code. Make sure these are correctly exported in the runtime!
Using `any` here because TS doesn't allow symbols as index type.
// 须要从生成代码中的“vue”导入的运行时帮忙程序的名称映射。// 确保这些文件在运行时正确导出!// 此处应用 'any',因为 TS 不容许将符号作为索引类型。export const helperNameMap: any = {[FRAGMENT]: `Fragment`,
  [TELEPORT]: `Teleport`,
  [SUSPENSE]: `Suspense`,
  [KEEP_ALIVE]: `KeepAlive`,
  [BASE_TRANSITION]: `BaseTransition`,
  [OPEN_BLOCK]: `openBlock`,
  [CREATE_BLOCK]: `createBlock`,
  [CREATE_VNODE]: `createVNode`,
  [CREATE_COMMENT]: `createCommentVNode`,
  [CREATE_TEXT]: `createTextVNode`,
  [CREATE_STATIC]: `createStaticVNode`,
  [RESOLVE_COMPONENT]: `resolveComponent`,
  [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
  [RESOLVE_DIRECTIVE]: `resolveDirective`,
  [WITH_DIRECTIVES]: `withDirectives`,
  [RENDER_LIST]: `renderList`,
  [RENDER_SLOT]: `renderSlot`,
  [CREATE_SLOTS]: `createSlots`,
  [TO_DISPLAY_STRING]: `toDisplayString`,
  [MERGE_PROPS]: `mergeProps`,
  [TO_HANDLERS]: `toHandlers`,
  [CAMELIZE]: `camelize`,
  [CAPITALIZE]: `capitalize`,
  [SET_BLOCK_TRACKING]: `setBlockTracking`,
  [PUSH_SCOPE_ID]: `pushScopeId`,
  [POP_SCOPE_ID]: `popScopeId`,
  [WITH_SCOPE_ID]: `withScopeId`,
  [WITH_CTX]: `withCtx`
}

export function registerRuntimeHelpers(helpers: any) {Object.getOwnPropertySymbols(helpers).forEach(s => {helperNameMap[s] = helpers[s]
  })
}

事实上,帮忙函数是 Vue 在代码生成时引入的一些函数,因而程序能够失常执行,从下面生成的代码能够看出。helperNameMap 是默认的映射表名,它是要从 Vue 引入的函数名。

此外,咱们还能够看到一个注册函数。registerRuntimeHelpers(helpers: any() 是做什么用的呢?

咱们晓得编译模块的编译器外围是一个独立于平台的,而 编译 Dom是一个与浏览器相干的编译模块。要在浏览器中运行 Vue 程序,请导入与浏览器相干的 Vue 数据和性能。

registerRuntimeHelpers(helpers: any() 用于执行此操作,能够从 Compiler-domruntimehelpers.ts 文件中看到:

registerRuntimeHelpers({[V_MODEL_RADIO]: `vModelRadio`,
  [V_MODEL_CHECKBOX]: `vModelCheckbox`,
  [V_MODEL_TEXT]: `vModelText`,
  [V_MODEL_SELECT]: `vModelSelect`,
  [V_MODEL_DYNAMIC]: `vModelDynamic`,
  [V_ON_WITH_MODIFIERS]: `withModifiers`,
  [V_ON_WITH_KEYS]: `withKeys`,
  [V_SHOW]: `vShow`,
  [TRANSITION]: `Transition`,
  [TRANSITION_GROUP]: `TransitionGroup`
})

运行 registerRuntimeHelpers(helpers: any() 映射表被注入与浏览器相干的函数。

如何应用这些辅助函数?

在解析阶段,解析不同节点时会生成相应的类型。

在转换阶段,生成一个辅助对象,它是一个汇合数据结构。每当转换 AST 时,都会依据 AST 节点的类型增加不同的帮忙器函数。

例如,假如当初正在转换正文节点,它将执行 context.helper(CREATE_COMMENT),外部通过 helpers.add(‘createCommentVNode’) 增加。

而后在 Codegen 阶段,遍历 helpers,从 Vue 导入所需的函数,代码实现如下:

// 这是模块模式
`import { ${ast.helpers
  .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
  .join(',')} } from ${JSON.stringify(runtimeModuleName)}\n`

如何生成代码?

Codegen.ts 文件中,能够看到许多代码生成函数:

Generate () // 入口文件
 GenfunctionExpression () // 生成函数表达式
 Gennode () // 生成 vNode 节点
...

生成代码是基于不同的 AST 节点调用不同的代码生成函数,最初将代码字符串拼合在一起,输入残缺的代码字符串。

老规矩,还是看一个例子:

const _hoisted_1 = {name: "test"}
 const _hoisted_2 = / * # __ pure __ * / _ createtextVNode ("a text node")
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)

看看这段代码是如何生成的,外部会执行 genHoists(ast.hoists, context),晋升的动态节点作为第一个参数,genHoists() 外部简化实现:

hoists.forEach((exp, i) => {if (exp) {push(`const _hoisted_${i + 1} = `);
        genNode(exp, context);
        newline();}
})

从下面的代码能够看出,遍历 hoists 数组,调用 genNode(exp, context) 函数。genNode() 依据不同的类型执行不同的性能。

const _hoisted_1 = {name: "test"}

这一行的 const _hoisted_1 = 通过 genHoists() 函数生成,{name: “test”} 是通过 genObjectExpression() 函数生成。

正文完
 0