【看的见的思考】compiler-core
看的见的思考
先从compile 看起, compile 里面的 baseCompile 是整个 core 的入口函数
先看看单元测试吧
test('function mode', () => {
const { code, map } = compile(source, {
sourceMap: true,
filename: `foo.vue`
})
expect(code).toMatchSnapshot()
expect(map!.sources).toEqual([`foo.vue`])
expect(map!.sourcesContent).toEqual([source])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, `id`))
).toMatchObject(getPositionInCode(source, `id`))这个测试相当的大,先一点点的看
先看input source 是
const source = `
<div id="foo" :class="bar.baz">
{{ world.burn() }}
<div v-if="ok">yes</div>
<template v-else>no</template>
<div v-for="(value, index) in list"><span>{{ value + index }}</span></div>
</div>
`.trim()这个就是 template 了。 然后第二个参数是对应的 CompilerOptions 先不用管
在去看看输出 output
下面是 code
const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, renderList: _renderList, createElementVNode: _createElementVNode, normalizeClass: _normalizeClass } = _Vue
return (_openBlock(), _createElementBlock("div", {
id: "foo",
class: _normalizeClass(bar.baz)
}, [
_createTextVNode(_toDisplayString(world.burn()) + " ", 1 /* TEXT */),
ok
? (_openBlock(), _createElementBlock("div", { key: 0 }, "yes"))
: (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [
_createTextVNode("no")
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)),
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (value, index) => {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, _toDisplayString(value + index), 1 /* TEXT */)
]))
}), 256 /* UNKEYED_FRAGMENT */))
], 2 /* CLASS */))
}
}就是生成的 render 函数
那map 是什么?
{
version: 3,
sources: [ 'foo.vue' ],
names: [],
mappings: ';;;;;;0BAAA,oBAKM;MALD,EAAE,EAAC,KAAK;MAAE,KAAK,EAApB,gBAAsB,OAAO;;MAA7B,kCACK,YAAY,IAAG,GAClB;MAAW,EAAE;yBAAb,oBAAwB,SAF1B,KAAA,KAEiB,KAAG;yBAClB,oBAA8B,aAHhC,KAAA;YAAA,iBAGmB,IAAE;;yBACnB,oBAA0E,iBAJ5E,YAIgC,IAAI,EAJpC,CAIe,KAAK,EAAE,KAAK;8BAAzB,oBAA0E;UAAtC,oBAAgC,+BAAvB,aAAa',
sourcesContent: [
'<div id="foo" :class="bar.baz">\n' +
' {{ world.burn() }}\n' +
' <div v-if="ok">yes</div>\n' +
' <template v-else>no</template>\n' +
' <div v-for="(value, index) in list"><span>{{ value + index }}</span></div>\n' +
'</div>'
]
}应该是 sourcemap
接下来这个 单元测试所以的点都是测试 sourcemap 的点,所以我们先不看了
那接着我们看看 baseCompile 函数是通过几个步骤生成的 code
baseCompile
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
const onError = options.onError || defaultOnError
const isModuleMode = options.mode === 'module'
/* istanbul ignore if */
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers)
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}好,最主要的就是三个部分
生成 ast - 通过 baseParse
调用 transform - 来处理 ast
使用 generate 生成 code
baseParse
那我们先看是如何生成ast的把
也就是baseParse,还是先看单元测试
下面的逻辑是只测试的 TextNode
test('simple text', () => {
const ast = baseParse('some text')
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: 'some text',
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 9, line: 1, column: 10 },
source: 'some text'
}
})
})可以看到 node 对象的关键的几个属性了
接着看看是如何解析出来的把
export function baseParse(
content: string,
options: ParserOptions = {}
): RootNode {
const context = createParserContext(content, options)
const start = getCursor(context)
return createRoot(
parseChildren(context, TextModes.DATA, []),
getSelection(context, start)
)
}
这里的重点是 context 是什么
继续去看 createParserContext
function createParserContext(
content: string,
rawOptions: ParserOptions
): ParserContext {
const options = extend({}, defaultParserOptions)
let key: keyof ParserOptions
for (key in rawOptions) {
// @ts-ignore
options[key] =
rawOptions[key] === undefined
? defaultParserOptions[key]
: rawOptions[key]
}
return {
options,
column: 1,
line: 1,
offset: 0,
originalSource: content,
source: content,
inPre: false,
inVPre: false,
onWarn: options.onWarn
}
}只是生成了一个配置对象
{
options: {
delimiters: [ '{{', '}}' ],
getNamespace: [Function: getNamespace],
getTextMode: [Function: getTextMode],
isVoidTag: [Function: NO],
isPreTag: [Function: NO],
isCustomElement: [Function: NO],
decodeEntities: [Function: decodeEntities],
onError: [Function: defaultOnError],
onWarn: [Function: defaultOnWarn],
comments: true
},
column: 1,
line: 1,
offset: 0,
originalSource: 'some text',
source: 'some text',
inPre: false,
inVPre: false,
onWarn: [Function: defaultOnWarn]
}接着是调用了 getCursor
function getCursor(context: ParserContext): Position {
const { column, line, offset } = context
return { column, line, offset }
}数据是来自 context 里面的
继续最后一个逻辑
return createRoot(
parseChildren(context, TextModes.DATA, []),
getSelection(context, start)
)先看 parseChildren
function parseChildren(
context: ParserContext,
mode: TextModes,
ancestors: ElementNode[]
): TemplateChildNode[] {
const parent = last(ancestors)
const ns = parent ? parent.ns : Namespaces.HTML
const nodes: TemplateChildNode[] = []
while (!isEnd(context, mode, ancestors)) {
__TEST__ && assert(context.source.length > 0)
const s = context.source
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
// '{{'
node = parseInterpolation(context, mode)
} else if (mode === TextModes.DATA && s[0] === '<') {太多了,就不copy过来了。不过激动的是这里和我们之前去刷编译原理时候处理语法的时候逻辑是一致的,解析成功的话,那么就创建一个 node 节点对象
而且是用 nodes 把所有node对象都收集起来
至于解析的规则的话,就是按照 html 的规则来的
回头去刷编译原理就好了,这里去解析 html 的套路都是一样的
现在我们只需要知道返回一个处理完的 nodes就ok了
继续去看下一个逻辑点
getSelection
function getSelection(
context: ParserContext,
start: Position,
end?: Position
): SourceLocation {
end = end || getCursor(context)
return {
start,
end,
source: context.originalSource.slice(start.offset, end.offset)
}
}这里就是返回对应这段代码的信息的
createRoot
export function createRoot(
children: TemplateChildNode[],
loc = locStub
): RootNode {
return {
type: NodeTypes.ROOT,
children,
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc
}
}这里的 createRoot 就是直接创建一个 root 节点给外面就ok了
而关键的 children 就是通过parseChildren生成的 nodes。
transform
看看 transform 阶段是做了什么
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)是基于 ast 来做处理,第二个参数就是 transformOptions 了
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseNode(root, context)
if (options.hoistStatic) {
hoistStatic(root, context)
}
if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = [...context.helpers.keys()]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = context.imports
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
if (__COMPAT__) {
root.filters = [...context.filters!]
}
}还是先处理配置 context
看看长什么样子
{
selfName: null,
prefixIdentifiers: false,
hoistStatic: false,
cacheHandlers: false,
nodeTransforms: [ [Function: plugin] ],
directiveTransforms: {},
transformHoist: null,
isBuiltInComponent: [Function: NOOP],
isCustomElement: [Function: NOOP],
expressionPlugins: [],
scopeId: null,
slotted: true,
ssr: false,
inSSR: false,
ssrCssVars: '',
bindingMetadata: {},
inline: false,
isTS: false,
onError: [Function: defaultOnError],
onWarn: [Function: defaultOnWarn],
compatConfig: undefined,
root: {
type: 0,
children: [ [Object] ],
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc: {
start: [Object],
end: [Object],
source: '<div>hello {{ world }}</div>'
}
},
helpers: Map(0) {},
components: Set(0) {},
directives: Set(0) {},
hoists: [],
imports: [],
constantCache: Map(0) {},
temps: 0,
cached: 0,
identifiers: [Object: null prototype] {},
scopes: { vFor: 0, vSlot: 0, vPre: 0, vOnce: 0 },
parent: null,
currentNode: {
type: 0,
children: [ [Object] ],
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc: {
start: [Object],
end: [Object],
source: '<div>hello {{ world }}</div>'
}
},
childIndex: 0,
inVOnce: false,
helper: [Function: helper],
removeHelper: [Function: removeHelper],
helperString: [Function: helperString],
replaceNode: [Function: replaceNode],
removeNode: [Function: removeNode],
onNodeRemoved: [Function: onNodeRemoved],
addIdentifiers: [Function: addIdentifiers],
removeIdentifiers: [Function: removeIdentifiers],
hoist: [Function: hoist],
cache: [Function: cache],
filters: Set(0) {}
}这里的好多属性看起来都是 vue 特有的
第二步的时候就是 调用 traverseNode
后面的逻辑是处理一些特殊key 的
root.helpers = [...context.helpers.keys()]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = context.imports
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached这里是把一些额外的方法给到了 root 上面,而root 是 AST 的root node
traverseNode
在看这个函数之前,先找个测试看看
test('context state', () => {
const ast = baseParse(`<div>hello {{ world }}</div>`)
// manually store call arguments because context is mutable and shared
// across calls
const calls: any[] = []
const plugin: NodeTransform = (node, context) => {
calls.push([node, { ...context }])
}
transform(ast, {
nodeTransforms: [plugin]
})
const div = ast.children[0] as ElementNode
expect(calls.length).toBe(4)
expect(calls[0]).toMatchObject([
ast,
{
parent: null,
currentNode: ast
}
])
expect(calls[1]).toMatchObject([
div,
{
parent: ast,
currentNode: div
}
])
expect(calls[2]).toMatchObject([
div.children[0],
{
parent: div,
currentNode: div.children[0]
}
])
expect(calls[3]).toMatchObject([
div.children[1],
{
parent: div,
currentNode: div.children[1]
}
])
})通过这个测试可以知道,transform 是在本身的 AST 的基础上直接修改数据的
而这里的执行模式应该和 babel 的 plugin 的形式也差不多,通过 visit 的处理方式来调用
上面的 nodeTransforms:[plugin] 就是处理方式,看起来是当所有的node调用的时候,就会执行这个 nodeTransforms 里面给的函数
在看看第二个测试
test('context.replaceNode', () => {
const ast = baseParse(`<div/><span/>`)
const plugin: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
// change the node to <p>
context.replaceNode(
Object.assign({}, node, {
tag: 'p',
children: [
{
type: NodeTypes.TEXT,
content: 'hello',
isEmpty: false
}
]
})
)
}
}
const spy = jest.fn(plugin)
transform(ast, {
nodeTransforms: [spy]
})
expect(ast.children.length).toBe(2)
const newElement = ast.children[0] as ElementNode
expect(newElement.tag).toBe('p')
expect(spy).toHaveBeenCalledTimes(4)
// should traverse the children of replaced node
expect(spy.mock.calls[2][0]).toBe(newElement.children[0])
// should traverse the node after the replaced node
expect(spy.mock.calls[3][0]).toBe(ast.children[1])
})在 plugin 的实现里面可以看到就是通过替换node来达到修改代码的效果
而context 是什么?哦,看起来 context 是用来处理 node 的
traverseNode
使用的基本逻辑明白了 那接着看看 traverseNode 内部是如何实现的把
export function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
) {
context.currentNode = node
// apply transform plugins
const { nodeTransforms } = context
const exitFns = []
for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context)
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit)
} else {
exitFns.push(onExit)
}
}
if (!context.currentNode) {
// node was removed
return
} else {
// node may have been replaced
node = context.currentNode
}
}
switch (node.type) {
case NodeTypes.COMMENT:
if (!context.ssr) {
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
}
break
case NodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helper
if (!context.ssr) {
context.helper(TO_DISPLAY_STRING)
}
break
// for container types, further traverse downwards
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
traverseNode(node.branches[i], context)
}
break
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context)
break
}
// exit transforms
context.currentNode = node
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}这里的 context 就是通过 createTransformContext 生成的,里面有好多方法可以处理 node
第一步是先调用用户通过 config 注入的 nodeTransforms 里面的函数,也就是单测里面给的 plugin 函数
参数就是把 node 和 context 给到,所以用户可以在 plugin 里面通过 context 提供的方法来处理 node
这里的细节是, plugin 是可以返回一个函数的,这个函数就做 onExit
接着会基于 node 的类型做不同的处理
switch (node.type) {
case NodeTypes.COMMENT:
if (!context.ssr) {
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
}
break
case NodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helper
if (!context.ssr) {
context.helper(TO_DISPLAY_STRING)
}
break
// for container types, further traverse downwards
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
traverseNode(node.branches[i], context)
}
break
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context)
break
}NodeTypes.COMMENT → context.helper(CREATE_COMMENT)
NodeTypes.INTERPOLATION → context.helper(TO_DISPLAY_STRING)
NodeTypes.IF → traverseNode(node.branches[i], context)
NodeTypes.IF_BRANCH || NodeTypes.FOR || NodeTypes.ELEMENT || NodeTypes.ROOT:
traverseChildren(node, context)
这个处理完成后在统一的调用 exitFn
// exit transforms
context.currentNode = node
let i = exitFns.length
while (i--) {
exitFns[i]()
}这里应该是方便让用户做一些清理逻辑
createTransformContext - context
继续来分析一下 context
他里面有几个关键的方法
helper(name) {
},
removeHelper(name) {
},
helperString(name) {
},
replaceNode(node) {
},
removeNode(node) {
onNodeRemoved: () => {},
addIdentifiers(exp) {
},
removeIdentifiers(exp) {
},
hoist(exp) {
},
cache(exp, isVNode = false) {
}先来看看 helper
helper(name) {
const count = context.helpers.get(name) || 0
context.helpers.set(name, count + 1)
return name
},逻辑是加一个 count ,那么是用在哪里的呢?
没找到 继续看看 removeHelper
removeHelper(name) {
const count = context.helpers.get(name)
if (count) {
const currentCount = count - 1
if (!currentCount) {
context.helpers.delete(name)
} else {
context.helpers.set(name, currentCount)
}
}
},和 helper 是对应的,这里是删除一个 count
在看 helperString
helperString(name) {
return `_${helperNameMap[context.helper(name)]}`
},这里的重点是 helperNameMap ,而 context.helper(name ) 是基于 name 计数了一下,然后把 name 返回。
那看看 helperNameMap
export const helperNameMap: any = {
[FRAGMENT]: `Fragment`,
[TELEPORT]: `Teleport`,
[SUSPENSE]: `Suspense`,
[KEEP_ALIVE]: `KeepAlive`,
[BASE_TRANSITION]: `BaseTransition`,
[OPEN_BLOCK]: `openBlock`,
[CREATE_BLOCK]: `createBlock`,
[CREATE_ELEMENT_BLOCK]: `createElementBlock`,
[CREATE_VNODE]: `createVNode`,
[CREATE_ELEMENT_VNODE]: `createElementVNode`,
[CREATE_COMMENT]: `createCommentVNode`,太多了,截取了一部分,可以看到,这里存储的都是对应的处理函数,也就是所谓的 helper
那可以说是 helperString 就是返回对应 helper 的名称
继续看replaceNod
Source: cuixiaorui/mini-vue