You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
transform(ast,extend({},options,{
prefixIdentifiers,nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms||[])// user transforms],directiveTransforms: extend({},directiveTransforms,options.directiveTransforms||{}// user transforms)}))
是基于 ast 来做处理,第二个参数就是 transformOptions 了
exportfunctiontransform(root: RootNode,options: TransformOptions){constcontext=createTransformContext(root,options)traverseNode(root,context)if(options.hoistStatic){hoistStatic(root,context)}if(!options.ssr){createRootCodegen(root,context)}// finalize meta informationroot.helpers=[...context.helpers.keys()]root.components=[...context.components]root.directives=[...context.directives]root.imports=context.importsroot.hoists=context.hoistsroot.temps=context.tempsroot.cached=context.cachedif(__COMPAT__){root.filters=[...context.filters!]}}
test('context state',()=>{constast=baseParse(`<div>hello {{ world }}</div>`)// manually store call arguments because context is mutable and shared// across callsconstcalls: any[]=[]constplugin: NodeTransform=(node,context)=>{calls.push([node,{ ...context}])}transform(ast,{nodeTransforms: [plugin]})constdiv=ast.children[0]asElementNodeexpect(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]}])})
test('context.replaceNode',()=>{constast=baseParse(`<div/><span/>`)constplugin: 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}]}))}}constspy=jest.fn(plugin)transform(ast,{nodeTransforms: [spy]})expect(ast.children.length).toBe(2)constnewElement=ast.children[0]asElementNodeexpect(newElement.tag).toBe('p')expect(spy).toHaveBeenCalledTimes(4)// should traverse the children of replaced nodeexpect(spy.mock.calls[2][0]).toBe(newElement.children[0])// should traverse the node after the replaced nodeexpect(spy.mock.calls[3][0]).toBe(ast.children[1])})
在 plugin 的实现里面可以看到就是通过替换node来达到修改代码的效果
而context 是什么?哦,看起来 context 是用来处理 node 的
traverseNode
使用的基本逻辑明白了 那接着看看 traverseNode 内部是如何实现的把
export functiontraverseNode(node: RootNode|TemplateChildNode,context: TransformContext){context.currentNode=node// apply transform pluginsconst{ nodeTransforms }=contextconstexitFns=[]for(leti=0;i<nodeTransforms.length;i++){constonExit=nodeTransforms[i](node,context)if(onExit){if(isArray(onExit)){exitFns.push(...onExit)}else{exitFns.push(onExit)}}if(!context.currentNode){// node was removedreturn}else{// node may have been replacednode=context.currentNode}}switch(node.type){caseNodeTypes.COMMENT:
if(!context.ssr){// inject import for the Comment symbol, which is needed for creating// comment nodes with `createVNode`context.helper(CREATE_COMMENT)}breakcaseNodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helperif(!context.ssr){context.helper(TO_DISPLAY_STRING)}break// for container types, further traverse downwardscaseNodeTypes.IF:
for(leti=0;i<node.branches.length;i++){traverseNode(node.branches[i],context)}breakcaseNodeTypes.IF_BRANCH:
caseNodeTypes.FOR:
caseNodeTypes.ELEMENT:
caseNodeTypes.ROOT:
traverseChildren(node,context)break}// exit transformscontext.currentNode=nodeleti=exitFns.lengthwhile(i--){exitFns[i]()}}
switch(node.type){caseNodeTypes.COMMENT:
if(!context.ssr){// inject import for the Comment symbol, which is needed for creating// comment nodes with `createVNode`context.helper(CREATE_COMMENT)}breakcaseNodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helperif(!context.ssr){context.helper(TO_DISPLAY_STRING)}break// for container types, further traverse downwardscaseNodeTypes.IF:
for(leti=0;i<node.branches.length;i++){traverseNode(node.branches[i],context)}breakcaseNodeTypes.IF_BRANCH:
caseNodeTypes.FOR:
caseNodeTypes.ELEMENT:
caseNodeTypes.ROOT:
traverseChildren(node,context)break}
addIdentifiers(exp){// identifier tracking only happens in non-browser builds.if(!__BROWSER__){if(isString(exp)){addId(exp)}elseif(exp.identifiers){exp.identifiers.forEach(addId)}elseif(exp.type===NodeTypes.SIMPLE_EXPRESSION){addId(exp.content)}}},
traverseChildren 的逻辑很简单,就是标准的 for children 然后再调用 traverseNode
至此,所有的 node 都会被执行到 nodeTransforms 这个里面的函数内
并且还收集完了 helper , 以及做好了 count 计数
generate
看看如何做代码生成
exportfunctiongenerate(ast: RootNode,options: CodegenOptions&{onContextCreated?: (context: CodegenContext)=>void}={}): CodegenResult{const context =createCodegenContext(ast,options)if(options.onContextCreated)options.onContextCreated(context)const{mode,
push,
prefixIdentifiers,
indent,
deindent,
newline,
scopeId,
ssr
}=contextconsthasHelpers=ast.helpers.length>0constuseWithBlock=!prefixIdentifiers&&mode!=='module'constgenScopeId=!__BROWSER__&&scopeId!=null&&mode==='module'constisSetupInlined=!__BROWSER__&&!!options.inline// preambles// in setup() inline mode, the preamble is generated in a sub context// and returned separately.constpreambleContext=isSetupInlined
? createCodegenContext(ast,options)
: contextif(!__BROWSER__&&mode==='module'){genModulePreamble(ast,preambleContext,genScopeId,isSetupInlined)}else{genFunctionPreamble(ast,preambleContext)}// enter render functionconstfunctionName=ssr ? `ssrRender` : `render`
constargs=ssr ? ['_ctx','_push','_parent','_attrs'] : ['_ctx','_cache']if(!__BROWSER__&&options.bindingMetadata&&!options.inline){// binding optimization argsargs.push('$props','$setup','$data','$options')}constsignature=!__BROWSER__&&options.isTS
? args.map(arg=>`${arg}: any`).join(',')
: args.join(', ')if(isSetupInlined){push(`(${signature}) => {`)}else{push(`function ${functionName}(${signature}) {`)}indent()if(useWithBlock){push(`with (_ctx) {`)indent()// function mode const declarations should be inside with block// also they should be renamed to avoid collision with user propertiesif(hasHelpers){push(`const { ${ast.helpers.map(s=>`${helperNameMap[s]}: _${helperNameMap[s]}`).join(', ')} } = _Vue`)push(`\n`)newline()}}// generate asset resolution statementsif(ast.components.length){genAssets(ast.components,'component',context)if(ast.directives.length||ast.temps>0){newline()}}if(ast.directives.length){genAssets(ast.directives,'directive',context)if(ast.temps>0){newline()}}if(__COMPAT__&&ast.filters&&ast.filters.length){newline()genAssets(ast.filters,'filter',context)newline()}if(ast.temps>0){push(`let `)for(leti=0;i<ast.temps;i++){push(`${i>0 ? `, ` : ``}_temp${i}`)}}if(ast.components.length||ast.directives.length||ast.temps){push(`\n`)newline()}// generate the VNode tree expressionif(!ssr){push(`return `)}if(ast.codegenNode){genNode(ast.codegenNode,context)}else{push(`null`)}if(useWithBlock){deindent()push(`}`)}deindent()push(`}`)return{ast,code: context.code,preamble: isSetupInlined ? preambleContext.code : ``,// SourceMapGenerator does have toJSON() method but it's not in the typesmap: context.map ? (context.mapasany).toJSON() : undefined}}
去找个单元测试
test('module mode preamble',()=>{constroot=createRoot({helpers: [CREATE_VNODE,RESOLVE_DIRECTIVE]})const{ code }=generate(root,{mode: 'module'})expect(code).toMatch(`import { ${helperNameMap[CREATE_VNODE]} as _${helperNameMap[CREATE_VNODE]}, ${helperNameMap[RESOLVE_DIRECTIVE]} as _${helperNameMap[RESOLVE_DIRECTIVE]} } from "vue"`)expect(code).toMatchSnapshot()})
// preambles// in setup() inline mode, the preamble is generated in a sub context// and returned separately.constpreambleContext=isSetupInlined
? createCodegenContext(ast,options)
: contextif(!__BROWSER__&&mode==='module'){genModulePreamble(ast,preambleContext,genScopeId,isSetupInlined)}else{genFunctionPreamble(ast,preambleContext)}
这里是基于不同的 mode 来生成不同的代码
而代码这里其实就是 string 的拼接
我们先看 genModulePreamble
functiongenModulePreamble(ast: RootNode,context: CodegenContext,genScopeId: boolean,inline?: boolean){const{ push, newline, optimizeImports, runtimeModuleName }=contextif(genScopeId){ast.helpers.push(WITH_SCOPE_ID)if(ast.hoists.length){ast.helpers.push(PUSH_SCOPE_ID,POP_SCOPE_ID)}}// generate import statements for helpersif(ast.helpers.length){if(optimizeImports){// when bundled with webpack with code-split, calling an import binding// as a function leads to it being wrapped with `Object(a.b)` or `(0,a.b)`,// incurring both payload size increase and potential perf overhead.// therefore we assign the imports to variables (which is a constant ~50b// cost per-component instead of scaling with template size)push(`import { ${ast.helpers.map(s=>helperNameMap[s]).join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`)push(`\n// Binding optimization for webpack code-split\nconst ${ast.helpers.map(s=>`_${helperNameMap[s]} = ${helperNameMap[s]}`).join(', ')}\n`)}else{push(`import { ${ast.helpers.map(s=>`${helperNameMap[s]} as _${helperNameMap[s]}`).join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`)}}if(ast.ssrHelpers&&ast.ssrHelpers.length){push(`import { ${ast.ssrHelpers.map(s=>`${helperNameMap[s]} as _${helperNameMap[s]}`).join(', ')} } from "@vue/server-renderer"\n`)}if(ast.imports.length){genImports(ast.imports,context)newline()}genHoists(ast.hoists,context)newline()if(!inline){push(`export `)}}
最简单的分支是
push(`import { ${ast.helpers.map(s=>`${helperNameMap[s]} as _${helperNameMap[s]}`).join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`)
return{
ast,code: context.code,preamble: isSetupInlined ? preambleContext.code : ``,// SourceMapGenerator does have toJSON() method but it's not in the typesmap: context.map ? (context.mapasany).toJSON() : undefined}
ast 对象不用说了
code 就是基于 ast 生成的代码
preamble 是前导码头部的代码
map 是处理 sourcemap
The text was updated successfully, but these errors were encountered:
看的见的思考
先从compile 看起, compile 里面的 baseCompile 是整个 core 的入口函数
先看看单元测试吧
这个测试相当的大,先一点点的看
先看input source 是
这个就是 template 了。 然后第二个参数是对应的 CompilerOptions 先不用管
在去看看输出 output
下面是 code
就是生成的 render 函数
那map 是什么?
应该是 sourcemap
接下来这个 单元测试所以的点都是测试 sourcemap 的点,所以我们先不看了
那接着我们看看 baseCompile 函数是通过几个步骤生成的 code
baseCompile
好,最主要的就是三个部分
生成 ast - 通过 baseParse
调用 transform - 来处理 ast
使用 generate 生成 code
baseParse
那我们先看是如何生成ast的把
也就是baseParse,还是先看单元测试
下面的逻辑是只测试的 TextNode
可以看到 node 对象的关键的几个属性了
接着看看是如何解析出来的把
这里的重点是 context 是什么
继续去看 createParserContext
只是生成了一个配置对象
接着是调用了 getCursor
数据是来自 context 里面的
继续最后一个逻辑
先看 parseChildren
太多了,就不copy过来了。不过激动的是这里和我们之前去刷编译原理时候处理语法的时候逻辑是一致的,解析成功的话,那么就创建一个 node 节点对象
而且是用 nodes 把所有node对象都收集起来
至于解析的规则的话,就是按照 html 的规则来的
回头去刷编译原理就好了,这里去解析 html 的套路都是一样的
现在我们只需要知道返回一个处理完的 nodes就ok了
继续去看下一个逻辑点
getSelection
这里就是返回对应这段代码的信息的
createRoot
这里的 createRoot 就是直接创建一个 root 节点给外面就ok了
而关键的 children 就是通过parseChildren生成的 nodes。
transform
看看 transform 阶段是做了什么
是基于 ast 来做处理,第二个参数就是 transformOptions 了
还是先处理配置 context
看看长什么样子
这里的好多属性看起来都是 vue 特有的
第二步的时候就是 调用 traverseNode
后面的逻辑是处理一些特殊key 的
这里是把一些额外的方法给到了 root 上面,而root 是 AST 的root node
traverseNode
在看这个函数之前,先找个测试看看
通过这个测试可以知道,transform 是在本身的 AST 的基础上直接修改数据的
而这里的执行模式应该和 babel 的 plugin 的形式也差不多,通过 visit 的处理方式来调用
上面的 nodeTransforms:[plugin] 就是处理方式,看起来是当所有的node调用的时候,就会执行这个 nodeTransforms 里面给的函数
在看看第二个测试
在 plugin 的实现里面可以看到就是通过替换node来达到修改代码的效果
而context 是什么?哦,看起来 context 是用来处理 node 的
traverseNode
使用的基本逻辑明白了 那接着看看 traverseNode 内部是如何实现的把
这里的 context 就是通过 createTransformContext 生成的,里面有好多方法可以处理 node
第一步是先调用用户通过 config 注入的 nodeTransforms 里面的函数,也就是单测里面给的 plugin 函数
参数就是把 node 和 context 给到,所以用户可以在 plugin 里面通过 context 提供的方法来处理 node
这里的细节是, plugin 是可以返回一个函数的,这个函数就做 onExit
接着会基于 node 的类型做不同的处理
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
这里应该是方便让用户做一些清理逻辑
createTransformContext - context
继续来分析一下 context
他里面有几个关键的方法
先来看看 helper
逻辑是加一个 count ,那么是用在哪里的呢?
没找到 继续看看 removeHelper
和 helper 是对应的,这里是删除一个 count
在看 helperString
这里的重点是 helperNameMap ,而 context.helper(name ) 是基于 name 计数了一下,然后把 name 返回。
那看看 helperNameMap
太多了,截取了一部分,可以看到,这里存储的都是对应的处理函数,也就是所谓的 helper
那可以说是 helperString 就是返回对应 helper 的名称
继续看replaceNode
替换节点, 替换的是当前 context 的父级的孩子节点(基于 childIndex 获取的孩子)
下面的是removeNode
处理的也是 context 父级的孩子节点, 直接给删除当前的节点
这里有个回调,删除完成后会执行 context 里面的 onNodeRemoved
addIdentifiers
这里的关键是 addId 函数,等会在看 TODO
后面是 removeIdentifiers
这里是对Id 的删除
hoist 处理静态提升
把静态的标签都缓存起来
这个主要是做优化的,可以作为TODO
cache 方法也是一样的,后续在看
总结来看的话,context 有这么几个职责
处理 helper
处理 node
处理 Identifiers
identifiers 是做什么的,还不知道
添加和删除
以及2个用于优化的方法
hoist
cache
接着我们回到traverseNode 函数内看下面的逻辑
如果是 if 分支的话,那么需要2个分支都处理继续调用
在看 下面
traverseChildren 的逻辑很简单,就是标准的 for children 然后再调用 traverseNode
至此,所有的 node 都会被执行到 nodeTransforms 这个里面的函数内
并且还收集完了 helper , 以及做好了 count 计数
generate
看看如何做代码生成
去找个单元测试
code 长这个样子
那问题
import 上面的代码是怎么生成的?
在这里
这里是基于不同的 mode 来生成不同的代码
而代码这里其实就是 string 的拼接
我们先看 genModulePreamble
最简单的分支是
ast.helpers 是调用的时候给的,然后通知 helperNameMap 映射对应的函数名,最后拼成一个字符串即可
push 的话是 context 内部的方法,其实就是吧所有的 string 都收集进去
接着看 render 函数的生成
基于参数来生成不同的 render string
具体后面所有生成代码的逻辑,都是基于 ast 上面的options 来做处理,有什么就生成什么样子的代码
现在基本上明白了。在回头看看 codegen return 的数据
ast 对象不用说了
code 就是基于 ast 生成的代码
preamble 是前导码头部的代码
map 是处理 sourcemap
The text was updated successfully, but these errors were encountered: