诗号:六道同坠,魔劫万千,引渡如来。

/img/bdx/yiyeshu-001.jpg

stb-vue-next 完全拷贝于 vue-next ,主要目的用于学习。

声明 :vue-next compiler-dom 模块 调试 :所有测试用例可通过 <F12> 控制台查看

更新日志&Todos

  1. [2020-12-16 19:40:21] 创建

  2. [2020-12-19 13:01:02] 完成

  3. TODO 完善测试用例

0062974 compiler-dom模块初始化

feat(init): compiler-dom · gcclll/stb-vue-next@0062974

日常操作先 copy:先将 vue-next/packages/compiler-dom/ 下面的内容拷贝过来,删除 src 目录下的所有代 码。

了解目录结构:

╰─$ tree -C -I 'dist|__tests__'
.
├── LICENSE
├── README.md
├── api-extractor.json
├── index.js
├── package.json
└── src
    ├── decodeHtml.ts
    ├── decodeHtmlBrowser.ts
    ├── errors.ts
    ├── index.ts
    ├── namedChars.json
    ├── parserOptions.ts
    ├── runtimeHelpers.ts
    └── transforms
        ├── ignoreSideEffectTags.ts
        ├── stringifyStatic.ts
        ├── transformStyle.ts
        ├── vHtml.ts
        ├── vModel.ts
        ├── vOn.ts
        ├── vShow.ts
        ├── vText.ts
        └── warnTransitionChildren.ts

2 directories, 21 files

功能预览:

  1. ignoreSideEffectTags.ts 忽略模板中的 style, script 标签

  2. stringifyStatic.ts 对静态提升做的一些处理

  3. transformStyle.ts 将静态 style 属性转成指令类型属性,且将内容转成对象

    如: <div style="color:red" /> 会转成:

    1
    2
    3
    
    _createBlock('div', {
      style: { color: "red" }
    }, null, 1 /* TEXT */)
    
  4. vHtml.ts 将 v-html 表达式转成 innerHTML 内容

  5. vModel.ts 处理 <input> 类型,删除 modelValue: value 属性,不允许有参数

  6. vOn.ts 事件修饰符处理, 详情请查相关章节 >>>

    分为三类:

    • 事件选项修饰符 passive, capture, once 会被解析到事件名后面

      如: <div @click.capture.once="i++" /> 结果:

      1
      2
      3
      
      _createBlock('div', {
        'onClickCaptureOnce': $event => (i++)
      }, null, 40 /* PROPS, HYDRATE_EVENTS */, ["onClickCaptureOnce"])
      
    • 键盘类事件(onkeydown,onkeyup,onkeypress)修饰符 ~~

    • 系统按键类型修饰符(ctrl,alt,shift,meta,exact),捕获冒泡(stop,prevent,self),鼠 标(middle,left,right)

  7. vShow.ts 针对 v-show 指令,这个可能需要在 runtime 期间处理

  8. vText.ts v-text 指令的处理,将表达式内容解析成 textContent 属性内容

  9. warnTransitionChildren.ts 文件是针对 <transition> 内置标签的检测,它的孩 子节点只能有一个

  10. decodeHtml[Browser].ts 是用来处理 html 符号语义化的,分别是 NODE 环境和浏览器环 境的处理逻辑

  11. parserOptions.ts 针对 compiler-core 的选项做了进一步扩展(namespace, native tag, decodeEntities 等)

4fa13bf init parse + compile function

feat(init): compiler-dom index.ts> compile + parse function · gcclll/stb-vue-next@4fa13bf

初始化编译和解析函数,对应着 compile-core 的 baseCompilebaseParse 函数。

即: compiler-dom 是针对 compiler-core 的进一步封装处理,传递一些 transformXxx 函数,至于这些函数做了什么处理,需要下面一步步来揭开。

compile :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export function compile(
  template: string,
  options: CompilerOptions
): CodegenResult {
  return baseCompile(
    template,
    extend({}, parserOptions, {
      nodeTransforms: [],
      directiveTransforms: extend({}),
      // 静态提升 transform
      transformHoist: __BROWSER__ ? null : stringifyStatic
    })
  )
}

parse:

1
2
3
export function parse(template: string, options: ParserOptions = {}): RootNode {
  return baseParse(template, extend({}, parserOptions, options))
}

其实到这里也是应该可以执行的,来测试下:

1
2
3
4
5
6
7
const {
  parse,
  compile
} = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')

const { code, ast } = compile(`<div>{{ test }}</div>`)
console.log(code, '\n>>> AST: \n', ast)
const _Vue = Vue

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

    return (_openBlock(), _createBlock("div", null, _toDisplayString(test), 1 /* TEXT */))
  }
}
>>> AST:
 {
  type: 0,
  children: [
    {
      type: 1,
      ns: 0,
      tag: 'div',
      tagType: 0,
      props: [],
      isSelfClosing: false,
      children: [Array],
      loc: [Object],
      codegenNode: [Object]
    }
  ],
  codegenNode: {
    type: 13,
    tag: '"div"',
    props: undefined,
    children: { type: 5, content: [Object], loc: [Object] },
    patchFlag: '1 /* TEXT */',
    dynamicProps: undefined,
    directives: undefined,
    isBlock: true,
    disableTracking: false,
    loc: { start: [Object], end: [Object], source: '<div>{{ test }}</div>' }
  },
}

接下来才是进入正题 ⛳…🚄🚄🚄

8c86624 add transformStyle node transform

feat: add transformStyle transform · gcclll/stb-vue-next@8c86624

作用是将 node.props 里面的 style 内联属性转成对象类型。

根据条件,这里只检测静态属性,然后将其转成 v-bind 型的动态属性,将内联转成对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
export const transformStyle: NodeTransform = node => {
  if (node.type === NodeTypes.ELEMENT) {
    node.props.forEach((p, i) => {
      if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
        // replace p with an expression node
        node.props[i] = {
          type: NodeTypes.DIRECTIVE,
          name: `bind`,
          arg: createSimpleExpression(`style`, true, p.loc),
          exp: parseInlineCSS(p.value.content, p.loc),
          modifiers: [],
          loc: p.loc
        }
      }
    })
  }
}

内联转对象解析函数: parseInlineCSS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const parseInlineCSS = (
  cssText: string,
  loc: SourceLocation
): SimpleExpressionNode => {
  const normalized = parseStringStyle(cssText)
  return createSimpleExpression(
    JSON.stringify(normalized),
    false,
    loc,
    ConstantTypes.CAN_STRINGIFY
  )
}

parseStringStyle 处理其实就是以 ; 为分隔符,将 name:value 分割出来,解析出 namevalue 组成对象。

测试:

1
2
3
4
5
6
7
8

const {
  parse,
  compile
} = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')

const { code } = compile(`<div style="color:red;font-size:30px;">{{ text }}</div>`)
console.log(code)
const _Vue = Vue

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

    return (_openBlock(), _createBlock("div", { style: {"color":"red","font-size":"30px"} }, _toDisplayString(text), 1 /* TEXT */))
  }
}
undefined

7ea8dfe add v-html transform

feat: add transform v-html · gcclll/stb-vue-next@7ea8dfe

v-html 指令转换。

代码很简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

export const transformVHtml: DirectiveTransform = (dir, node, context) => {
  const { exp, loc } = dir
  if (!exp) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_HTML_NO_EXPRESSION, loc)
    )
  }

  if (node.children.length) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_HTML_WITH_CHILDREN, loc)
    )
    node.children.length = 0
  }

  return {
    props: [
      createObjectProperty(
        createSimpleExpression(`innerHTML`, true, loc),
        exp || createSimpleExpression('', true)
      )
    ]
  }
}

其实就是针对 v-html 将其转成 innerHTML 动态属性,检测两个不合法使用情况

  1. 没有表达式

  2. 包含孩子节点

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

const {
  parse,
  compile
} = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')

const _c = tpl => compile(tpl, {
  onError: e => console.log(`错误描述:` + e.message)
}).code
console.log(_c(`<div v-html="test"/>`))
console.log(`>>> v-html 下不能有任何孩子节点`)
console.log(_c(`<div v-html="test">hello</div>`))
console.log(`>>> v-html 不能没有表达式`)
console.log(_c(`<div v-html></div>`))
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    return (_openBlock(), _createBlock("div", { innerHTML: test }, null, 8 /* PROPS */, ["innerHTML"]))
  }
}
>>> v-html 下不能有任何孩子节点
错误描述:v-html will override element children.
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    return (_openBlock(), _createBlock("div", { innerHTML: test }, null, 8 /* PROPS */, ["innerHTML"]))
  }
}
>>> v-html 不能没有表达式
错误描述:v-html is missing expression.
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    return (_openBlock(), _createBlock("div", { innerHTML: "" }))
  }
}
undefined
  1. 这里 v-html 属性会被解析成 node.props 里面动态属性,属性名为 innerHTML

  2. 如果有 v-html 指令是该组件下面就不能有任何孩子节点

4f3a4ee add v-text transform

feat(add): v-text transform · gcclll/stb-vue-next@4f3a4ee

v-text 指令转换函数,转成属性为 textContent

代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

export const transformVText: DirectiveTransform = (dir, node, context) => {
  const { exp, loc } = dir

  if (!exp) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_TEXT_NO_EXPRESSION, loc)
    )
  }

  if (node.children.length) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_TEXT_WITH_CHILDREN, loc)
    )

    node.children.length = 0
  }

  return {
    props: [
      createObjectProperty(
        createSimpleExpression(`textContent`, true),
        exp
          ? createCallExpression(
              context.helperString(TO_DISPLAY_STRING),
              [exp],
              loc
            )
          : createSimpleExpression('', true)
      )
    ]
  }
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

const {
  parse,
  compile
} = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')

const c = tpl => compile(tpl, {
  onError: e => console.log(`错误描述: ${e.message}`)
}).code

console.log(c(`<div v-text="test"/>`))
console.log(`>>> 包含孩子节点`)
console.log(c(`<div v-text="test">hello</div>`))
console.log(`>>> 无表达式`)
console.log(c(`<div v-text></div>`))
const _Vue = Vue

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

    return (_openBlock(), _createBlock("div", {
      textContent: _toDisplayString(test)
    }, null, 8 /* PROPS */, ["textContent"]))
  }
}
>>> 包含孩子节点
错误描述: v-text will override element children.
const _Vue = Vue

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

    return (_openBlock(), _createBlock("div", {
      textContent: _toDisplayString(test)
    }, null, 8 /* PROPS */, ["textContent"]))
  }
}
>>> 无表达式
错误描述: v-text is missing expression.
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    return (_openBlock(), _createBlock("div", { textContent: "" }))
  }
}
undefined

588d5f1 add v-model transform

v-model 指令转换。

在完成 v-model 指令转换之前,我们看下 compiler-core 里面的 v-model 处理的最后结 果是什么❓

1
2
3
4
5
6
7
8

const {
  parse,
  compile
} = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')

const { code } = compile(`<input v-model:value="result" />`)
console.log(code)
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    return (_openBlock(), _createBlock("input", {
      value: result,
      "onUpdate:value": $event => (result = $event)
    }, null, 40 /* PROPS, HYDRATE_EVENTS */, ["value","onUpdate:value"]))
  }
}
undefined

结果显示:v-model 最终转成了两个属性

{ value: result, "onUpdate:value": $event => (result = $event)}

这个原理应该是这样: 输入框内容绑定 result ,当输入框内容发生变化,触发 onUpdate:value 事件,执行该函数重新复制 result 变更数据。

加上 compiler-dom 阶段的 v-model transform 之后: feat(add): v-model transform · gcclll/stb-vue-next@588d5f1

1
2
3
4
5
6
7
8

const {
  parse,
  compile
} = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')

const { code } = compile(`<input v-model="result" />`)
console.log(code)
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { vModelText : _vModelText, createVNode : _createVNode, withDirectives : _withDirectives, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    return _withDirectives((_openBlock(), _createBlock("input", {
      "onUpdate:modelValue": $event => (result = $event)
    }, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [
      [_vModelText, result]
    ])
  }
}
undefined

变化:

  1. 不支持参数了

  2. 删除了 value: result 属性(默认是 modelValue)。

  3. _withDirectives<input> 包起来了

    这个函数定义是在 runtime-core 里面定义了,作用就是将 第二个参数 [ [_vModelText, result] ] 里面的指令塞到 vnode.dirs 指令集中去。

代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

export const transformModel: DirectiveTransform = (dir, node, context) => {
  const baseResult = baseTransform(dir, node, context)
  // base transform has errors OR component v-model (only need props)
  // 没有 v-model指令,或应用在用户组件上了
  if (!baseResult.props.length || node.tagType === ElementTypes.COMPONENT) {
    return baseResult
  }

  // 不能有参数?
  if (dir.arg) {
    // ... 报错,不能有参数,即必须是 ~v-model="xxx"~ 来使用
  }

  // 不能有 value 属性,因为 input 绑定的就是 value 属性
  function checkDuplicateValue() {
    // ... 这里既是检测是不是有 <input value="xx"> value 属性
  }

  const { tag } = node
  const isCustomElement = context.isCustomElement(tag)
  if (
    tag === 'input' ||
    tag === 'textarea' ||
    tag === 'select' ||
    isCustomElement
  ) {
    let directiveToUse = V_MODEL_TEXT
    let isInvalidType = false
    if (tag === 'input' || isCustomElement) {
      const type = findProp(node, `type`)
      if (type) {
        if (type.type === NodeTypes.DIRECTIVE) {
          // :type='foo'
          directiveToUse = V_MODEL_DYNAMIC
        } else if (type.value) {
          switch (type.value.content) {
            case 'radio':
              directiveToUse = V_MODEL_RADIO
              break
            case 'checkbox':
              directiveToUse = V_MODEL_CHECKBOX
              break
            case 'file':
              isInvalidType = true
              // ERROR 不能用在 <file> 标签上
              break
            default:
              __DEV__ && checkDuplicateValue()
              break
          }
        }
      } else if (hasDynamicKeyVBind(node)) {
        // element has bindings with dynamic keys, which can possibly contain
        // "type".
        directiveToUse = V_MODEL_DYNAMIC
      } else {
        // text type
        __DEV__ && checkDuplicateValue()
      }
    } else if (tag === 'select') {
      directiveToUse = V_MODEL_SELECT
    } else {
      // textarea
      __DEV__ && checkDuplicateValue()
    }

    // inject runtime directive
    // by returning the helper symbol via needRuntime
    // the import will replaced a resolveDirective call.
    if (!isInvalidType) {
      baseResult.needRuntime = context.helper(directiveToUse)
    }
  } else {
    // v-model 应用到不合法的元素上
  }

  // native vmodel doesn't need the `modelValue` props since they are also
  // passed to the runtime as `binding.value`. removing it reduces code size.
  // 最后过滤掉 modelValue: xxx 属性
  baseResult.props = baseResult.props.filter(
    p =>
      !(
        p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
        p.key.content === 'modelValue'
      )
  )
  return baseResult
}

源码分析:

  1. 只处理 input, textarea, select 文本框标签,或自定义的标签

  2. <input> 标签类型分为 radiocheckbox 单复选项框处理,不能使用 type='file' 类型

  3. <select> 下拉选项框的处理

  4. 过滤掉 transform 之后的 {modelValue: value, 'onUpdate:value': $event => value = $event} 里面的 modelValue:value 属性,因为在 runtime-core 时期的 withDirectives() 处理里面会被绑定到 value 属性上

a94aacd add v-on transform

feat(add): v-on transform · gcclll/stb-vue-next@a94aacd

compiler-core 阶段:

1
2
3
4
5
6
7
8
9

const {
  parse,
  compile
} = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')

const { code, ast } = compile(`<div v-on:keyup.enter.prevent="pressKeyup" />`)
console.log(code)
console.log(ast.codegenNode.props.properties)
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    return (_openBlock(), _createBlock("div", { onKeyup: pressKeyup }, null, 40 /* PROPS, HYDRATE_EVENTS */, ["onKeyup"]))
  }
}
[
  {
    type: 16,
    loc: { source: '', start: [Object], end: [Object] },
    key: {
      type: 4,
      loc: [Object],
      content: 'onKeyup',
      isStatic: true,
      constType: 3
    },
    value: {
      type: 4,
      content: 'pressKeyup',
      isStatic: false,
      constType: 0,
      loc: [Object]
    }
  }
]
undefined

可以看到 compile-core 阶段是没有处理修饰符的。

v-on 指令最后解析成 { key, value, type: 16 } 结构。

compiler-dom v-on 处理逻辑:

  1. resolveModifiers(key, modifiers) 解析出三类修饰符

    • keyModifiers 修饰符

      键盘事件: onkeyup, onkeydown, onkeypress

    • eventOptionModifiers 事件选项修饰符,只有三种 passive, once, capture

    • nonKeyModifiers 非按键类修饰符

      事件冒泡管理: stop,prevent,self

      系统修饰符+exact: ctrl,shift,alt,meta,exact , exact 表示精确匹配按键。

      鼠标按键修饰符: middle

  2. 经过 1 之后得出三种类型的修饰符,处理其中的 nonKeyModifiers

    将这种类型的修饰符中的 right, middle 转换成对应的 onContextmenuonMouseup 事件

    即:

    如果有 right 点击事件会触发 onContextmenu 事件,弹出右键菜单?

    如果有 middle 鼠标中间滚轮事件,会触发 onMouseup 鼠标弹起事件

    最后将 nonKeyModifiers 结合 value 创建成函数表达式。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    
    const {
    parse,
    compile
    } = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')
    
    const {code} = compile(
    `<div @click.right="testRight"
         @click.middle="testMiddle"
         @click.left="testLeft" />`)
    console.log(`>>> right 修饰符被当做 onContextmenu 事件处理, middle -> onMouseup`)
    console.log(code)
    
    >>> right 修饰符被当做 onContextmenu 事件处理, middle -> onMouseup
    const _Vue = Vue
    
    return function render(_ctx, _cache) {
      with (_ctx) {
        const { withModifiers : _withModifiers, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue
    
        return (_openBlock(), _createBlock("div", {
          onContextmenu: _withModifiers(testRight, ["right"]),
          onMouseup: _withModifiers(testMiddle, ["middle"]),
          onClick: _withModifiers(testLeft, ["left"])
        }, null, 40 /* PROPS, HYDRATE_EVENTS */, ["onContextmenu","onMouseup","onClick"]))
      }
    }
    undefined
    
  3. 处理 keyModifiers ,如:键盘事件修饰符,系统修饰符等等

    比如:键盘 ctrl-a 组合键

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    
    const {
    parse,
    compile
    } = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')
    
    const { code } = compile(`
    <div @keydown.stop.capture.ctrl.a="test" />`)
    console.log(code)
    
    const _Vue = Vue
    
    return function render(_ctx, _cache) {
      with (_ctx) {
        const { withModifiers : _withModifiers, withKeys : _withKeys, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue
    
        return (_openBlock(), _createBlock("div", {
          onKeydownCapture: _withKeys(_withModifiers(test, ["stop","ctrl"]), ["a"])
        }, null, 40 /* PROPS, HYDRATE_EVENTS */, ["onKeydownCapture"]))
      }
    }
    undefined
    
  4. 处理 eventOptionModifiers 结合 key 生成对应的事件名表达式

    事件选项修饰符只有三个: capture,passive,once

    passive: passive的作用和原理_个人文章 - SegmentFault 思否

    capture: DOM 的事件傳遞機制:捕獲與冒泡

    解析结果,事件选项修饰符被合并到事件名中:

    1
    2
    3
    4
    5
    6
    7
    
    const {
        parse,
        compile
    } = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')
    
    const { code } = compile(`<div @click.stop.capture.once="test" />`)
    console.log(code)
    
    const _Vue = Vue
    
    return function render(_ctx, _cache) {
      with (_ctx) {
        const { withModifiers : _withModifiers, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue
    
        return (_openBlock(), _createBlock("div", {
          onClickCaptureOnce: _withModifiers(test, ["stop"])
        }, null, 40 /* PROPS, HYDRATE_EVENTS */, ["onClickCaptureOnce"]))
      }
    }
    undefined
    

    如:事件名 onClickCaptureOnce

  5. 如果事件名为动态或是键盘事件,得用 _withKeys() 包一层


测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

const {
  parse,
  compile
} = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')
const log = console.log
const c = (tpl, title) => {
  const { code, ast } = compile(tpl, {
    onError: e => log(`错误描述:${e.message}`)
  })

  log(`>>> ${title}`)
  log(code)
  log(ast.codegenNode.props.properties)
}

c(`<div @click.stop.prevent="test" />`, '多个修饰符')
>>> 多个修饰符
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { withModifiers : _withModifiers, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    return (_openBlock(), _createBlock("div", {
      onClick: _withModifiers(test, ["stop","prevent"])
    }, null, 8 /* PROPS */, ["onClick"]))
  }
}
[
  {
    type: 16,
    loc: { source: '', start: [Object], end: [Object] },
    key: {
      type: 4,
      loc: [Object],
      content: 'onClick',
      isStatic: true,
      constType: 3
    },
    value: {
      type: 14,
      loc: [Object],
      callee: Symbol(vOnModifiersGuard),
      arguments: [Array]
    }
  }
]
undefined

<f12> 打开控制台查看更多测试用例结果。

小结 :

事件修饰符分为三大类

  1. 事件选项类型修饰符(passive,capture,once)

    会和事件名合并: click.capture.once -> onClickCaptureOnce

  2. 键盘事件(包括键盘按键 a-b-c-…)

  3. 其他类型事件修饰符(如:stop,prevent,self, ctrl,shift,alt,meta,exact)

关于 right, middle 修饰符处理情况

  1. right 处理成 onContextmenu 事件

  2. middle 处理成 onMouseup 事件

  3. right/middle 是在动态事件名上面,会检测是不是 onClick 如果是进行 1/2 转换,不 是按照原事件名处理。

    如: @[eventName].middle="test" -> eventName === 'onClick' ? 'onMouseup' : eventName

e64a1b3 add v-show transform

feat(add): v-show transform · gcclll/stb-vue-next@e64a1b3

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export const transformShow: DirectiveTransform = (dir, node, context) => {
  const { exp, loc } = dir
  if (!exp) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, loc)
    )
  }

  return {
    props: [],
    needRuntime: context.helper(V_SHOW)
  }
}

测试:

1
2
3
4
5
6
7
8
9

const {
  parse,
  compile
} = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')

const { code, ast } = compile(`<div v-show="test"/>`)
console.log(code)
console.log(`props: `, ast.codegenNode.props)
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { vShow : _vShow, createVNode : _createVNode, withDirectives : _withDirectives, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    return _withDirectives((_openBlock(), _createBlock("div", null, null, 512 /* NEED_PATCH */)), [
      [_vShow, test]
    ])
  }
}
props:  undefined

这里貌似什么都没干,除了返回一个 needRuntime: context.helper(V_SHOW) ,难道 v-show 必须在 runtime 时期处理???

436db72 add transition component warn transform

feat(add): transition component transform · gcclll/stb-vue-next@436db72

这里只是加了个错误用法处理,对于 <transition> 组件下面只能有一个孩子节点。

TODO af56754 add stringifyStatic node 环境静态提升

f0cbb25 add ignoreSideEffectTags transform

feat(add): ignore side effect tags > script/style · gcclll/stb-vue-next@f0cbb25

这个 transform 作用是检测模板中是不是存在 <script>, <style> 标签。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

const {
  parse,
  compile
} = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')

const c = (tpl, title) => {
  console.log(`>>> ${title}`)
  const { code } = compile(tpl, {
    onError: e => console.log(`> 错误描述:${e.message}`)
  })
  console.log(code)
}

c(`<script>console.log(1)</script>`, '忽略 <script> 标签')
c(`<style>h1 { color: red }</style>`, '忽略 <style> 标签')
>>> 忽略 <script> 标签
> 错误描述:Tags with side effect (<script> and <style>) are ignored in client component templates.

return function render(_ctx, _cache) {
  with (_ctx) {
    return null
  }
}
>>> 忽略 <style> 标签
> 错误描述:Tags with side effect (<script> and <style>) are ignored in client component templates.

return function render(_ctx, _cache) {
  with (_ctx) {
    return null
  }
}
undefined

fd0f5ae add dom parserOptions and decode html

对 compiler-core 的 ParserOptions 的一种扩展:

  1. isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag)

    vue-next/packages/shared/src/domTagConfig.ts

    中制定了一些原生的标签。

  2. isPreTag: pre 标签

  3. decodeEntities html 实体转换

    分为浏览器环境和NDOE环境处理

    浏览器环境处理较为简单(转标签内容取出字符串,利用浏览器自身能力来转换):

    1
    2
    3
    4
    5
    
    
    export function decodeHtmlBrowser(raw: string): string {
    ;(decoder || (decoder = document.createElement('div'))).innerHTML = raw
    return decoder.textContent as string
    }
    

    NODE 环境稍微复杂,下面做些简单测试吧:

    https://html.spec.whatwg.org/multipage/named-characters.html

    vue-next/packages/compiler-dom/src/namedChars.json

    上面链接和路径中包含了所有字符的 16进制 - 符号 - 名字对应表(json),下面随便找 几个来测试下-> 分析如注释

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    
    const {
    decodeHtml
    } = require(process.env.PWD + '/../../static/js/vue/compiler-dom.global.js')
    
    let rawText = 'a &#x20ac b &nbsp; e &FilledVerySmallSquare; d &Gg; f'
    const res = decodeHtml(rawText)
    // while 循环里首先是匹配正则:/&(?:#x?)?/ -> &#x...十六进制数 或 `&[name];`
    // 形式的字符,name 取自 namedChars.json 文件的 key
    // 如果都没有匹配到直接退出循环,
    // 即 decodeHtml 目的是将16进制和 named 表示的符号转换语义化的符号
    // 如: &#x20ac -> "euro;": "€" -> € 符号
    // 或者 &nbsp; 符号
    // 或者使用 namedChars.json 中的名字来作为特殊字符,如: &FilledVerySmallSquare; -> ▪, &Gg; ->  ⋙
    console.log(res)
    
    a € b   e ▪ d ⋙ f
    undefined
    
  4. isBuiltInComponent, 两个内置组件: Transition, TransitionGroup

  5. getNamespace, 更详细的命名空间检测

  6. getTextMode, 文本模式检测

    textarea, title 标签视为 TextModes.RCDATA 类型

    style,iframe,script,noscript 标签视为 TextModes.RAWTEXT 类型

    其他视为 TextModes.DATA 类型

用例测试(<f12> 查看控制台):

总结

这一模块的完成,零零散散时间大概花了不到一周的时间,也是由于有 compiler-core 包 的完成及分析的基础上,进展才会这么顺利。

对于 compiler-dom 这一模块主要内容,快速预览可以查看第一章节的功能预览

这里重点关注的地方,我认为有以下几个:

  1. v-model<input> 类型检测和处理

  2. v-on 事件修饰符的处理,我感觉是这个模块的 重中之重

  3. 另外对于 node 环境的 html 符号语义化的处理也值得分析