Vue3 源码头脑风暴之 4 ☞compiler-dom
文章目录
stb-vue-next 完全拷贝于 vue-next ,主要目的用于学习。
声明 :vue-next compiler-dom 模块 调试 :所有测试用例可通过
<F12>
控制台查看更新日志&Todos :
[2020-12-16 19:40:21] 创建
[2020-12-19 13:01:02] 完成
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
功能预览:
ignoreSideEffectTags.ts
忽略模板中的style, script
标签stringifyStatic.ts
对静态提升做的一些处理transformStyle.ts
将静态 style 属性转成指令类型属性,且将内容转成对象如:
<div style="color:red" />
会转成:1 2 3
_createBlock('div', { style: { color: "red" } }, null, 1 /* TEXT */)
vHtml.ts
将 v-html 表达式转成innerHTML
内容vModel.ts
处理<input>
类型,删除modelValue: value
属性,不允许有参数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
)
vShow.ts
针对 v-show 指令,这个可能需要在 runtime 期间处理vText.ts
v-text 指令的处理,将表达式内容解析成textContent
属性内容warnTransitionChildren.ts
文件是针对<transition>
内置标签的检测,它的孩 子节点只能有一个decodeHtml[Browser].ts
是用来处理 html 符号语义化的,分别是 NODE 环境和浏览器环 境的处理逻辑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 的 baseCompile
和 baseParse
函数。
即: compiler-dom 是针对 compiler-core 的进一步封装处理,传递一些 transformXxx
函数,至于这些函数做了什么处理,需要下面一步步来揭开。
compile :
|
|
parse:
|
|
其实到这里也是应该可以执行的,来测试下:
|
|
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
型的动态属性,将内联转成对象。
|
|
内联转对象解析函数: parseInlineCSS
|
|
parseStringStyle
处理其实就是以 ;
为分隔符,将 name:value
分割出来,解析出
name
和 value
组成对象。
测试:
|
|
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 指令转换。
代码很简单:
|
|
其实就是针对 v-html
将其转成 innerHTML
动态属性,检测两个不合法使用情况
没有表达式
包含孩子节点
测试:
|
|
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
这里 v-html 属性会被解析成
node.props
里面动态属性,属性名为innerHTML
。如果有
v-html
指令是该组件下面就不能有任何孩子节点
4f3a4ee add v-text transform
feat(add): v-text transform · gcclll/stb-vue-next@4f3a4ee
v-text 指令转换函数,转成属性为 textContent
。
代码:
|
|
测试:
|
|
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 处理的最后结 果是什么❓
|
|
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
|
|
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
变化:
不支持参数了
删除了
value: result
属性(默认是modelValue
)。用
_withDirectives
将<input>
包起来了这个函数定义是在
runtime-core
里面定义了,作用就是将 第二个参数[ [_vModelText, result] ]
里面的指令塞到vnode.dirs
指令集中去。
代码:
|
|
源码分析:
只处理
input, textarea, select
文本框标签,或自定义的标签<input>
标签类型分为radio
和checkbox
单复选项框处理,不能使用type='file'
类型<select>
下拉选项框的处理过滤掉 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 阶段:
|
|
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 处理逻辑:
resolveModifiers(key, modifiers)
解析出三类修饰符keyModifiers
修饰符键盘事件:
onkeyup, onkeydown, onkeypress
eventOptionModifiers
事件选项修饰符,只有三种passive, once, capture
nonKeyModifiers
非按键类修饰符事件冒泡管理:
stop,prevent,self
系统修饰符+exact:
ctrl,shift,alt,meta,exact
, exact 表示精确匹配按键。鼠标按键修饰符:
middle
经过 1 之后得出三种类型的修饰符,处理其中的
nonKeyModifiers
将这种类型的修饰符中的
right, middle
转换成对应的onContextmenu
和onMouseup
事件即:
如果有
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
处理
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
处理
eventOptionModifiers
结合key
生成对应的事件名表达式事件选项修饰符只有三个:
capture,passive,once
passive: passive的作用和原理_个人文章 - SegmentFault 思否
解析结果,事件选项修饰符被合并到事件名中:
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
如果事件名为动态或是键盘事件,得用
_withKeys()
包一层
测试:
|
|
>>> 多个修饰符 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>
打开控制台查看更多测试用例结果。
小结 :
事件修饰符分为三大类
事件选项类型修饰符(passive,capture,once)
会和事件名合并:
click.capture.once
->onClickCaptureOnce
键盘事件(包括键盘按键 a-b-c-…)
其他类型事件修饰符(如:stop,prevent,self, ctrl,shift,alt,meta,exact)
关于
right, middle
修饰符处理情况
right 处理成
onContextmenu
事件middle 处理成
onMouseup
事件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
|
|
测试:
|
|
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>
标签。
|
|
>>> 忽略 <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
的一种扩展:
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag)
vue-next/packages/shared/src/domTagConfig.ts
中制定了一些原生的标签。
isPreTag
:pre
标签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 € b e ▪ d ⋙ f' const res = decodeHtml(rawText) // while 循环里首先是匹配正则:/&(?:#x?)?/ -> &#x...十六进制数 或 `&[name];` // 形式的字符,name 取自 namedChars.json 文件的 key // 如果都没有匹配到直接退出循环, // 即 decodeHtml 目的是将16进制和 named 表示的符号转换语义化的符号 // 如: € -> "euro;": "€" -> € 符号 // 或者 符号 // 或者使用 namedChars.json 中的名字来作为特殊字符,如: ▪ -> ▪, ⋙ -> ⋙ console.log(res)
a € b e ▪ d ⋙ f undefined
isBuiltInComponent
, 两个内置组件:Transition, TransitionGroup
getNamespace
, 更详细的命名空间检测getTextMode
, 文本模式检测textarea, title
标签视为TextModes.RCDATA
类型style,iframe,script,noscript
标签视为TextModes.RAWTEXT
类型其他视为
TextModes.DATA
类型
用例测试(<f12>
查看控制台):
总结
这一模块的完成,零零散散时间大概花了不到一周的时间,也是由于有 compiler-core 包 的完成及分析的基础上,进展才会这么顺利。
对于 compiler-dom 这一模块主要内容,快速预览可以查看第一章节的功能预览。
这里重点关注的地方,我认为有以下几个:
v-model
对<input>
类型检测和处理v-on
事件修饰符的处理,我感觉是这个模块的 重中之重另外对于 node 环境的 html 符号语义化的处理也值得分析