本篇文章将讲解Vue3
编译器,在阅读本篇文章前建议先看一下阅读一下我之前的一篇文章《AST抽象语法树》 尤其是 全网最小编译器,强烈建议跟着写一遍,读一篇可能当时理解了,然后吃过饭就忘,手写一遍代码胜过千言万语, 你的印象会深刻很多, 你会对AST
有什深刻的认识,对于学习babel插件
及本篇文章都很有帮助。本篇文章假设你已经对AST有一些了解了。
回归正题,先介绍本文的内容也就是Vue3编译器的思路
Vue3
编译器的实现思路 源代码(模版) --》 错误分析 --》 parse(生成AST) --》transform(生成JavaScriptAST) --》 generate生成目标代码AST
AST
转换为JavaScriptAST
(增加了codegenNode
属性)JavaScriptAST
生成render函数
createApp
实现 打通compile
与runtime
html<body>
<div id="app"></div>
<script>
const { compile, render, h } = Vue
const template = `<div>hello world</div>`
const component = {
render: compile(template)
}
const vnode = h(component)
render(vnode, document.getElementById('app'))
</script>
</body>
h函数
和render函数
前一篇文章已经讲过,至于依赖收集
,这个先不考虑,我们先实现一个逻辑大幅简化的编译器骨架
compile函数
正式本篇文章所要讲的内容,它的作用是将template模版
转换为render函数
compile的思路如下
tsfunction compile(template: string, options) {
const ast = baseParse(template)
transform(
ast,
Object.assign(options, {
nodeTransforms: [transformElement, transformText, transformIf]
})
)
const {code} = generate(ast)
return new Function(code)()
}
baseParse
函数将 template
转换为ast
transform
函数将 ast
转换为JavaScript Ast
就是生成codegenNode
属性generate
函数将JavaScript Ast
转换为render函数我做了一张图
parse函数
生成context
实例parseChildren
, 处理所有子节点
token
生成AST
结构JS AST
,构建测试注:关于有限状态机, 参考阮老师的文章《JS与有限状态机》
这里模版的解析过程就类比有限状态机的转换
packages/compile-dom/src/index.ts
tsimport { baseCompile } from '@vue/compile-core'
export function compile(template: string, options) {
return baseCompile(template, options)
}
packages/compile-core/src/compile.ts
tsimport { baseParse } from './parse'
export function baseCompile(template: string, options) {
const ast = baseParse(template)
console.log(ast);
/* transform(
ast,
extend(options, {
nodeTransforms: [transformElement, transformText, transformIf]
})
)
return generate(ast) */
}
packages/compile-core/src/parse.ts
tsimport { ElementTypes, NodeTypes } from './ast'
// ElementTypes、NodeTypes 是两个常数枚举,copy自源码
export function baseParse(content: string) {
const context: ParserContext = {
source: content
}
const children = parseChildren(context, [])
return createRoot(children)
}
export function createRoot(children) {
return {
type: NodeTypes.ROOT,
children,
loc: {}
}
}
export interface ParserContext {
source: string
}
const enum TagType {
Start,
End
}
function parseChildren(context: ParserContext, ancestors) {
const nodes: any[] = []
while (!isEnd(context, ancestors)) {
const s = context.source
let node
if (s.startsWith('{{')) {
// TODO
} else if (s[0] === '<') {
if (/[a-z]/i.test(s[1])) {
node = parseElement(context, ancestors)
}
}
if (!node) {
node = parseText(context)
}
nodes.push(node)
}
return nodes
}
function parseElement(context: ParserContext, ancestors) {
const element = parseTag(context, TagType.Start)
ancestors.push(element)
const children = parseChildren(context, ancestors)
ancestors.pop()
element.children = children
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End)
}
return element
}
function parseTag(context: ParserContext, type: TagType) {
const match: any = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)
const tag = match[1]
advanceBy(context, match[0].length)
let isSelfCloseing = context.source.startsWith('/>')
advanceBy(context, isSelfCloseing ? 2 : 1)
return {
type: NodeTypes.ELEMENT,
tag,
tagType: ElementTypes.ELEMENT,
children: [] as any[],
props: []
}
}
function parseText(context: ParserContext) {
const endToken = '<'
let endIndex = context.source.length
const index = context.source.indexOf(endToken, 1)
if (index !== -1 && endIndex > index) {
endIndex = index
}
const content = parseTextData(context, endIndex)
return {
type: NodeTypes.TEXT,
content
}
}
function parseTextData(context: ParserContext, length: number) {
const rawText = context.source.slice(0, length)
advanceBy(context, length)
return rawText
}
function isEnd(context: ParserContext, ancestors) {
const s = context.source
if (s.startsWith('</')) {
for (let i = ancestors.length - 1; i >= 0; --i) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true
}
}
}
return !s
}
/**
* 判断当前是否为《标签结束的开始》。比如 </div> 就是 div 标签结束的开始
* @param source 模板。例如:</div>
* @param tag 标签。例如:div
* @returns
*/
function startsWithEndTagOpen(source: string, tag: string): boolean {
// return source.startsWith('</')
return (
source.startsWith('</') &&
source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase() &&
/[\t\r\n\f />]/.test(source[2 + tag.length] || '>')
)
}
function advanceBy(context: ParserContext, numberOfCharacters: number) {
const { source } = context
context.source = source.slice(numberOfCharacters)
}
验证AST有些麻烦
需要打印ASTconsole.log(JSON.stringify(ast))
然后copy到官方源码里(或者扔到22.compile-generate里),使用本文开头的测试用例,看看能否正常渲染
遵循的策略
先看个后序深度优先遍历的例子, 略有点绕,照着官方代码思路写的,为了方面后序代码的理解, 有兴趣可以使用 npx ts-node
执行看看
tsclass MyNode {
constructor(public value, public children: MyNode[] | null = null) {}
}
const nodes = new MyNode(1, [
new MyNode(2, [new MyNode(3), new MyNode(4)]),
new MyNode(5)
])
const context = {
nodeTransforms: [
arg => {
// console.log(arg?.value) // 这里是先序遍历
return _arg => console.log(_arg?.value)
},
/* arg => {
// console.log(arg?.value) // 这里是先序遍历
return _arg => console.log(':: ' + _arg?.value)
} */
]
}
// 按照先序访问 1,2,3,4,5
// 按照后续访问 3,4,2,5,1
traverseNode(nodes, context)
interface TransformContext {
nodeTransforms: any[]
}
function traverseNode(node: MyNode, context: TransformContext) {
const { nodeTransforms } = context
const existFns: any = []
for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context)
onExit && existFns.push(onExit)
}
if (node.children?.length) {
traverseChildren(node, context)
}
let i = existFns.length
while (i--) {
existFns[i]?.(node)
}
}
function traverseChildren(parent: MyNode, context: TransformContext) {
parent.children!.forEach((node, index) => {
traverseNode(node, context)
})
}
context
就是上下文对象, 在遍历的过程中访问或修改上下文对象上的属性nodeTransforms
就是用来存储转换函数的,遍历ast的时候会调用这里的方法packages/compile-core/src/compile.ts
packages/compile-core/src/transfrom.ts
tsimport { NodeTypes } from './ast'
import { isSingleElementRoot } from './hoistStatic'
export function createTransformContext(root, { nodeTransforms = [] }) {
// 记录AST --> JS AST 转换过程的状态
// 会不断访问和修改该对象下的属性
const context: TransformContext = {
nodeTransforms,
root,
helpers: new Map(), // 第三阶段generate 从这里根据标记取函数
currentNode: root,
parent: null,
childIndex: 0,
helper(name) { // 第二阶段生成 JS AST 往里面放 函数标记
const count = context.helpers.get(name) || 0
context.helpers.set(name, count + 1)
return name
}
}
return context
}
export function transform(root, options) {
const context = createTransformContext(root, options)
traverseNode(root, context)
createRootCodegen(root)
root.helpers = [...context.helpers.keys()]
// 这些属性本阶段用不到还是注释吧,方面上手
/* root.components = []
root.directives = []
root.imports = []
root.hoists = []
root.temps = []
root.cached = [] */
}
export function traverseNode(node, context: TransformContext) {
context.currentNode = node
const { nodeTransforms } = context
const existFns: any = []
for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context)
if (onExit) {
existFns.push(onExit)
}
}
switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context)
break
default:
return
}
context.currentNode = node
let i = existFns.length
while (i--) {
existFns[i]()
}
}
export function traverseChildren(parent, context: TransformContext) {
parent.children.forEach((node, index) => {
context.parent = parent
context.childIndex = index
traverseNode(node, context)
})
}
function createRootCodegen(root) {
const { children } = root
// Vue2 仅支持单个根节点
if (children.length === 1) {
const child = children[0]
if (isSingleElementRoot(root, child) && child.codegenNode) {
root.codegenNode = child.codegenNode
}
}
}
export interface TransformContext {
root
parent: ParentNode | null
childIndex: number
currentNode
helpers: Map<symbol, number>
helper<T extends symbol>(name: T): T
nodeTransforms: any[]
}
transformElements方法
有大致印象即可, 需要结合下一阶段generate()
才好理解
compile-core/src/transforms/transformElements.ts
tsimport { createVNodeCall, NodeTypes } from '../ast'
export const transformElement = (node, context) => {
return function postTransformElement() {
node = context.currentNode
if (node.type !== NodeTypes.ELEMENT) {
return
}
const { tag } = node
let vnodeTag = `"${tag}"`
let vnodeProps = []
let vnodeChildren = node.children
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren
)
}
}
compile-core/src/transforms/ast.ts
新增 createVNodeCall
方法tsexport function createVNodeCall(context, tag, props?, children?) {
if (context) {
// 往Map对象context.helper中函数标志,在generate阶段使用
context.helper(CREATE_ELEMENT_VNODE)
}
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children
}
}
剩下的都是一些工具方法
ts/* packages/compile-core/src/hoistStatic.ts */
import { NodeTypes } from './ast'
export function isSingleElementRoot(root, child) {
const { children } = root
return children.length === 1 && child.type === NodeTypes.ELEMENT
}
/* packages/compile-core/src/runtimeHelpers.ts */
export const CREATE_ELEMENT_VNODE = Symbol('createElementVNode')
export const CREATE_VNODE = Symbol('createVNode')
export const helperNameMap = {
[CREATE_ELEMENT_VNODE]: 'createElementVNode',
[CREATE_VNODE]: 'createVNode'
}
/* packages/compile-core/src/utils.ts */
import { NodeTypes } from './ast'
export function isText(node) {
return [NodeTypes.TEXT].includes(node.type)
}
如何验证JavaScript AST
是否正确?
与模版AST验证的方法类似, console.log(JSON.stringify(ast))
cope代码到Vue源码(或者放在22.compile-generate这里), 然后使用文本开头的测试用例看看是否正常渲染。
但是这里有个注意事项 Symbol序列化后的值变成了null 要修复symbol值
ts- helpers: [null],
+ helpers: [Symbol('createElementVNode')],
JavaScript AST
是无法生成函数的,它生成后的是字符串,通过 new Function(code)()
转换成函数。
对照JavaScript AST 和 生成后代码
generate方法
写起来只剩下体力活
代码改动
packages/compile-core/src/compile.ts
packages/compile-core/src/codegen.ts
tsimport { NodeTypes } from './ast'
import {
CREATE_ELEMENT_VNODE,
CREATE_VNODE,
helperNameMap
} from './runtimeHelpers'
/**
* 先按照这个拼接
const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode } = _Vue
return _createElementVNode(
"div",
[],
["hello world"]
)
}
}
*/
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
export function generate(ast) {
const context = createCodegenContext(ast)
const { push, newline, indent, deindent, runtimeGlobalName } = context
genFunctionPreamble(context)
push(`function render(_ctx, _cache){`)
indent()
push(`with(_ctx){`)
indent()
const hasHelpers = ast.helpers.length > 0
if (hasHelpers) {
const varStrs = ast.helpers.map(aliasHelper).join(',')
push(`const { ${varStrs} } = _${runtimeGlobalName}`)
}
newline()
push(`return `)
// 还剩 _createElementVNode("div", [], ["hello world"])
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
push('null')
}
deindent()
push(`}`)
deindent()
push(`}`)
console.log(context.code)
return {
ast,
code: context.code
}
}
function createCodegenContext(ast) {
const context = {
code: ``,
runtimeGlobalName: 'Vue',
source: ast.loc.source,
indentLevel: 0, // 锁进级别
helper(key) {
return `_${helperNameMap[key]}`
},
push(code) {
context.code += code
},
newline() {
newline(context.indentLevel)
},
// 增加锁进+换行
indent() {
newline(++context.indentLevel)
},
// 减少锁进和换行
deindent() {
newline(--context.indentLevel)
}
}
function newline(n: number) {
context.code += `\n` + ` `.repeat(n)
}
return context
}
function genFunctionPreamble(context) {
const { push, newline, runtimeGlobalName } = context
push(`const _${runtimeGlobalName} = ${runtimeGlobalName}`)
newline()
push('return ')
}
function genNode(node, context) {
switch (node.type) {
case NodeTypes.VNODE_CALL: // 13
genVNodeCall(node, context)
break
case NodeTypes.ELEMENT: // 1
genNode(node.codegenNode, context)
break
case NodeTypes.TEXT: // 2
genText(node, context)
}
}
function genVNodeCall(node, context) {
const { push, helper } = context
const { tag, props, children, isComponent } = node
const callHelper = isComponent ? CREATE_VNODE : CREATE_ELEMENT_VNODE
push(`${helper(callHelper)}(`)
const args = [tag, props, children].map(arg => arg || null)
genNodeList(args, context)
push(`)`)
}
function genText(node, context) {
context.push(JSON.stringify(node.content))
}
function genNodeList(nodes, context) {
const { push } = context
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (typeof node === 'string') {
push(node)
} else if (Array.isArray(node)) {
context.push(`[`)
genNodeList(node, context)
context.push(`]`)
} else {
genNode(node, context)
}
if (i < nodes.length - 1) {
push(', ')
}
}
}
运行本文开头的测试用例
有个小问题,因为使用了with
语句要避免with(undefind)
| with(null)
报错, 如果你熟悉我的前一篇文章vue3运行时的话,很快就找到generate()
生成的匿名函数是runtime-core/src/renderer.ts
调用的
这避免这两处data
为 null
或undefined
instance.data
是在 createComponentInstance
方法中初始化的
也就是runtime-core/src/component.ts
有一处代码要调整
这里的render调用generate函数
生成的匿名函数。
到这一步为止,才跑通本文开头的测试用例
先来看一下测试用例
html<body>
<div id="app"></div>
<script>
const { compile, h, render } = Vue
const component = {
data() {
return {
msg: 'world'
}
},
render: compile(`<div> hello {{ msg }} </div>`),
created() {
setTimeout(() => {
this.msg = '世界'
}, 2000)
}
}
const vnode = h(component);
render(vnode, document.getElementById('app'))
</script>
</body>
通过对源码的调试,发现三个阶段 AST
、JavaScriptAst
、生成的代码
都有改动。
AST
改动
注:默认右边为改动后的数据
JavaScript AST
改动
生成后的代码改动
根据上述数据结构变更我们看一下代码改动
parse
改动runtime-core/src/parse.ts
parseInterpolation()
代码如下ts// 解析插值表达式 {{ xxx }}
function parseInterpolation(context: ParserContext) {
const [open, close] = ['{{', '}}']
advanceBy(context, open.length)
const closeIndex = context.source.indexOf(close, open.length)
const preTrimContent = parseTextData(context, closeIndex)
const content = preTrimContent.trim()
advanceBy(context, close.length)
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
content
}
}
}
此外parseText()
方法也要调整
transform
改动runtime-core/src/compile.ts
runtime-core/src/transforms/transformText.ts
tsimport { createCompoundExpression, NodeTypes } from '../ast'
import { isText } from '../utils'
/**
* 将相邻的文本节点和表达式合并为一个表达式。
*
* 例如:
* <div>hello {{ msg }}</div>
* 上述模板包含两个节点:
* 1. hello:TEXT 文本节点
* 2. {{ msg }}:INTERPOLATION 表达式节点
* 这两个节点在生成 render 函数时,需要被合并: 'hello' + _toDisplayString(_ctx.msg)
* 那么在合并时就要多出来这个 + 加号。
* 例如:
* children:[
* { TEXT 文本节点 },
* " + ",
* { INTERPOLATION 表达式节点 }
* ]
*/
export const transformText = (node, context) => {
if (
[
NodeTypes.ROOT,
NodeTypes.ELEMENT,
].includes(node.type)
) {
return () => {
const children = node.children
let currentContainer
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (!isText(child)) {
continue
}
for (let j = i + 1; j < children.length; j++) {
let next = children[j]
if (!isText(next)) {
currentContainer = undefined
break
}
if (!currentContainer) {
currentContainer = children[i] = createCompoundExpression(
[child],
child.loc
)
}
// 在 当前节点 child 和 下一个节点 next 中间,插入 "+" 号
currentContainer.children.push(` + `, next)
// 把下一个删除
children.splice(j, 1)
j--
}
}
}
}
}
runtime-core/src/transform.ts
其他一些常量或工具方法改动
generate()
改动runtime-core/src/codegen.ts
新增方法内容如下ts// 表达式处理 4
function genExpression(node, context) {
const { content, isStatic } = node
context.push(isStatic ? JSON.stringify(content) : content, node)
}
// {{}} 处理 5
function genInterpolation(node, context) {
const { push, helper } = context
push(`${helper(TO_DISPLAY_STRING)}(`)
genNode(node.content, context)
push(`)`)
}
// 复合表达式处理 8
function genCompoundExpression(node, context) {
for (let i = 0; i < node.children!.length; i++) {
const child = node.children![i]
if (typeof child === 'string') {
context.push(child)
} else {
genNode(child, context)
}
}
}
运行效果
先来看一下测试用例
html<body>
<div id="app"></div>
<script>
const { createApp, h, compile, reactive } = Vue
createApp({
template: '<div>{{obj.name}}<div>',
setup() {
const obj = reactive({
name: '张三'
})
setTimeout(() => {
obj.name = '李四'
}, 2000);
return {
obj
}
}
}).mount('#app')
</script>
</body>
createApp(component).mount(el)
等价于 render(h(component), el)
总的来说createApp
就是组装轮子,实现起来很简单,但是以前我们没有对template
和setup
返回的对象类型进行处理,这次我们一块补上
代码实现
runtime-dom/src/index.ts
runtime-core/src/renderer.ts
runtime-core/src/apiCreateApp.ts
tsimport { createVNode } from './vnode'
export function createAppAPI<HostElement>(render) {
return function createApp(rootComponent, rootProps) {
const app = {
_component: rootComponent,
_container: null,
mount(rootContainer: HostElement) {
const vnode = createVNode(rootComponent, rootProps)
render(vnode, rootContainer)
}
}
return app
}
}
runtime-core/src/components.ts
运行结果
再次提醒: 本文所有案例 Vue3 study
本文作者:郭敬文
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!