6

The compilation module of Vue3

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

Among them, the compiler-core module is the core module compiled by Vue and has nothing to do with the platform. The remaining three are based on compiler-core and are suitable for different platforms.

Vue is divided into three stages, namely parsing (Parse), conversion (Transform) and code generation (Codegen) .

Parse stage converts the template string into a syntactic abstract tree AST . Transform some conversion processing on the AST 1617ced80ec0cc. Codegen stage generates the corresponding rendering function string according to the AST

Parse stage

When analyzing template strings, Vue can be divided into two situations: < , and strings not starting with < .

There are two cases for strings that do not start with < {{exp}} .

Use < divide the beginning of the string into the following situations:

  1. Element start tag <div>
  2. Element end tag </div>
  3. Comment node <!-- 123 -->
  4. Document declaration <!DOCTYPE html>

Expressed in pseudo code, the approximation process is as follows:

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

Original code point here vue-next parse.ts

The corresponding functions are as follows:

  1. parseChildren() , entry function
  2. parseInterpolation() , analyze the double-flower interpolation expression
  3. parseComment() , parsing comments
  4. parseBogusComment() , analysis file statement
  5. parseTag() , analysis label
  6. parseElement() , analysis element node, it will execute parseTag()
  7. parseText() , analyze ordinary text
  8. parseAttribute() , analysis attributes

When each node such as label, text, comment, etc. generates the corresponding AST node, Vue will truncate the parsed string.

The string is truncated using AdvanceBy(context,numberOfCharacters) function, context is the context object of the string, numberOfCharacters is the number of characters to be truncated.

Use a simple example to simulate the truncation operation:

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

First analyze <div , then execute advanceBy(context,4) truncation operation (internally execute s=s.slice(4) ), it becomes:

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

Then analyze the attribute and truncate it to:

<p></p>
</div>

Similarly, the following is truncated to:

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

All AST node definitions are in the Compiler-core/astts file, the following is the definition of the element node:

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 [] // 子级模板指向
}

Use a more complicated example to explain the parsing process.

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

The above template string is assumed to be S , the first character S[0] at the beginning is < , which means it can only be one of the four cases just mentioned.

Take a look at the rules for the second character of S[1]

  1. I met ! , the original method invocation string startsWith () , analysis is <-! beginning, or ! <DOCTYPE beginning with their corresponding different handler, code examples will eventually Resolve to the comment node.
  2. If it is / , press the end label.
  3. If it is not / , press to start label processing.

In our example, this is a <div> start tag.

One thing to mention here, Vue will use the stack to store the parsed element tags. When the start tag is encountered, the label is pushed onto the stack. When the end tag is encountered, the stack will be popped. Its function is to save the element tags that have been parsed but not yet parsed. There is another role in this stack, through stack[stack.length-1] , you can get its parent element.

From our example, during the parsing process, the stack is stored as follows:

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

According to the above example, the <div will be truncated next, and its attributes will be parsed.

There are two situations for attributes:

  1. Common attributes of HTML
  2. Vue instructions

The generated type node value, HTML common attribute node type is 6, Vue command node type is 7.

The details of all node type values are as follows:

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

After the attribute analysis, the div start tag is analyzed, and the <div name="test"> string is truncated. The remaining strings now look like this:

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

The annotation text and common text node parsing rules are relatively simple and simple, directly truncating and generating nodes. The comment node calls parseComment() function processing, Text node calls parseText() processing.

The string processing logic of double flower interpolation {{test}}

  1. First extract the content in the double brackets, that is, test , and call the trim function to remove the spaces on both sides.
  2. Then generate two nodes, one node is INTERPOLATION type value is 5, which means that it is a double flower interpolation.
  3. The second node is its content test , the node will be generated as Simple_expression , and the type value is 4.
return {
  TYPE: NODETYPES.ITERPOLATION, // 双花括号类型
  content: {
    type: NodeTypes.SIMPLE_EXPRESSION, // 简单表达式类型
    Isstatic: false, // 不是静态节点
    isConstant: false,
    content,
    loc: getSelection(context, innerStart, innerEnd)
  },
  loc: getSelection(context, start)
}

The rest of the string parsing logic is similar to the above, so it is not explained. An example parsing AST is as follows:

From AST , you can also see other attributes on some nodes:

  1. NS , the namespace, usually HTML , the value is 0
  2. LOC , it is a location message indicating that this node is located at the location of the , including row, column, offset and other information.
  3. {{}} Test node parsed having the isStatic attribute, the value to false , it indicates that this is a dynamic node. If it is a static node, it will only be generated once, and the same node will be reused, and there is no need for difference comparison.

There is also a tag type value, which has 4 values:

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

Mainly used to distinguish the above four types of nodes.

Transform stage

In the conversion phase, Vue will perform some conversion operations on AST CodeGen phase using different AST node to add different options parameters. Here are some important options:

cacheHandlers cache handlers

If the value of CacheHandlers true , the function cache is enabled. For example, @click="foo" is compiled as {onClick:foo} , if this option is turned on, it will be compiled as:

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

hoistStatic static promotion

hoistStatic is an identifier that indicates whether static node promotion should be enabled. If the value is true , the static node will be promoted outside the render() function, and a variable _hoisted_x

For example, text A text Node generated code const hoisted_2 = / # Pure / createtextVNode ( "A text Node") .

In the following two pictures, the first one is hoistStatic=false , the latter is hoistStatic=true , you can try address .

prefixIdentifiers prefix identifiers

The role of this parameter is used for code generation. For example, the code generated in {{ foo }} _ctx.foo , and the code generated in function mode is width(this){...} . Because in module mode, the default is strict mode, the with statement cannot be used.

PatchFlags patch flags

When converting to the AST node, use the PatchFlag parameter, which is mainly used for the diff process. When the DOM node has this flag and is greater than 0, it will be updated and will not be skipped.

Take a look at the value of 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
}

As can be seen from the above code, PatchFlag uses bit-map to represent different values, and each value has a different meaning. Vue will use different repair methods in the process of diff

The following figure shows the transformed AST :

You can see that CodegenNode , Helpers and Hoists have been filled with corresponding values. CodegenNode is the data to generate the code to be used. Hoists stores static nodes. Helpers stores the function name for vNode Symbol ).

Before officially starting the conversion, you need to create a transformContext , that is, the conversion context. The data and methods related to these three attributes are as follows:

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

Let's take a look at how the specific conversion process is used. <p>{{ test }}</p> an example.

This node corresponds to the TransformElement() conversion function, because p has no binding dynamic attributes and no binding instructions, so the focus is not on it. {{test}} is a double flower interpolation expression, so set its patchflag to 1 (dynamic text node), and the corresponding execution code patchFlag |=1 . Then execute the createVNodeCall() function, and its return value is the codegennode value of the node.

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

createVNodeCall() will correspondingly createVNode() , which is placed in helpers . In fact, the helpers function will be introduced in the code generation phase.

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

hoists promote

Whether to promote a node depends mainly on whether it is a static node.

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

As you can see, there are three static nodes above, so the hoists array has 3 values. Note why it is not a static node, the reason has not been found yet. . .

TYPE changes

As can be seen from the above figure, div is 1, and the type of CodeGen node Transform is 13.

This 13 is the type value corresponding to 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

In the example just mentioned, {{ test }} , its codegen node is generated by the createVnodeCall function.

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

As can be seen from the above code, type set to nodetypes.VNODE_CALL , which is 13.
Each different node is processed by a different transformation function. You can learn more about it yourself.

Codegen stage

At the end of the code generation stage, a string is generated, and the double quotation marks of the string are removed. What is the specific content:

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
    ]))
  }
}

Code generation mode

You can see that the above code finally returns the render() function, generating the corresponding VNODE .

In fact, there are two modes of code generation: module and function. The selected mode is determined by the prefix identifier.

Function Function mode: Use const {...} = helpers Vue acquired helper function method, i.e. createVode () , createCommentVNode () these functions, and finally returns the render () function.

The module mode is: use ES6 module import and export functions, namely import and export .

Static node

In addition, there are three variables. hoisted , followed by a number to indicate that this is a static variable.

Take a look at the HTML template string in the parsing stage:

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

This example has only one dynamic node, namely {{test} , and the rest are static nodes. It can also be seen from the generated code that the generated node and the code in the template correspond to one or more nodes. The function of a static node is to generate it only once and reuse it directly in the future.

Careful you may find that Highed_2 and Highed_3 variable has a / #\_ PURE_ comments.

this note is to indicate that this function is pure function, no side effects, and is mainly used for Tree-shaking . The compression tool will be deleted directly from the unused code at the time of packaging.

Looking at the next generation of dynamic nodes, {{ test }} generates code corresponding to _createVNode("p", null, _toDisplayString(test), 1 / TEXT /) .

Among them, the internal implementation of _toDisplayString(test)

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

The code is very simple, it is a string conversion output.

_createVNode ( "P", null, _toDisplayString (Test),. 1 / the TEXT /) when the last parameter conversions Patchflag value.

Help function

In the Transform and Codegen stages, we have seen the helpers helper function. What is it?

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]
  })
}

In fact, the helper functions are Vue , so the program can be executed normally, as can be seen from the generated code above. helperNameMap is the default mapping table name, which is the name of the function to be introduced Vue

In addition, we can also see a registration function. registerRuntimeHelpers(helpers: any() is 1617ced80ecda8 for?

We know that the compiler core of the compiled module is platform-independent, and compiled Dom is a browser-related compilation module. To run the Vue program in the browser, please import the Vue data and functions related to the browser.

registerRuntimeHelpers(helpers: any() used to perform this operation, which can be in the runtimehelpers.ts file of Compiler-dom 1617ced80ecddd:

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`
})

Run registerRuntimeHelpers(helpers: any() mapping table is injected into browser-related functions.

use these auxiliary functions?

In the parsing phase, the corresponding types will be generated when parsing different nodes.

In the conversion phase, an auxiliary object is generated, which is a collection data structure. Whenever the AST is converted, different helper functions will be added according to the type of the AST

For example, assuming that the comment node is being converted, it will execute context.helper(CREATE_COMMENT) , and internally helpers.add('createCommentVNode') .

Then in the Codegen stage, traverse helpers , Vue , the code implementation is as follows:

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

How to generate code?

From the Codegen.ts file, you can see many code generation functions:

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

The generated code is based on different AST nodes calling different code generation functions, and finally the code strings are stitched together to output the complete code string.

The old rules, let’s look at an example:

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

Take a look at how this code is generated. genHoists(ast.hoists, context) will be executed internally, with the promoted static node as the first parameter, genHoists() internal simplified implementation:

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

As can be seen from the above code, the hoists array is traversed, and the genNode(exp, context) function is called. genNode() performs different functions according to different types.

const _hoisted_1 = { name: "test" }

const _hoisted_1 = this line is generated by genHoists() function, {name: "test"} is genHoists() () 1617ced80ecf47 function.


戎马
2.4k 声望346 粉丝

前端码农一枚,上班一族,爱文学一本。ส็็็็็็็็็็็็็็ ส้้้้้้้้้้้้้้้้้้้。