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

/img/bdx/yiyeshu-001.jpg

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

声明 :该篇为 ts 源码(commit)版本,之前做过一遍完整的 js 版本,更详细,也可参考

Vue3.0 源码系列(二)编译器核心 - Compiler core 3: compile.ts - 若叶知秋

由于 transform 阶段直接测试不太好直观的看出结果,因此这里会结合 codegen 来一起实 现,即该文包含 compiler-core 三大阶段的最后两个阶段(transform + generate)

调试 :所有测试用例可通过 <F12> 控制台查看

更新日志&Todos

  1. DONE [2020-12-12 18:33:33] 阶段性完成,浏览器支持的所有基本功能(浏览器环境所 有用例测试均已通过,可通过控制台查看测试用例及其运行过程结果)

  2. TODO ssr 服务端渲染

  3. TODO <setup> 标签处理

  4. TODO 非浏览器环境支持(prefixIdentifiers, cacheHandlers 选项需要非浏览器环境)

  5. TODO ref 类型处理

关键知识点

  1. 🔗 root.children.length = 1 且类型是 ELEMENT的时候将 CREATE_VNODE 改成 CREATE_BLOCK

  2. 🔗 动态属性为表达式时,中间不能有空格

    如: <div :[first + second]="third" ... 是非法的。

    因为 parseAttribute() 中的正则是不支持中间有空格的:

    取参数名的正则: /^[^\t\r\n\f />][^\t\r\n\f />=]*/

  3. 🔗 v-bind 指令的几种用法 ->

  4. 🔗 用户组件 -> 插槽处理 -> v-slot 使用方法

脑图

e03a03c init transform module

feat(init): transform section · gcclll/stb-vue-next@e03a03c

初始化函数。

 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
export function createTransformContext(
  root: RootNode,
  {
    prefixIdentifiers = false,
    hoistStatic = false,
    cacheHandlers = false,
    nodeTransforms = [],
    directiveTransforms = {},
    transformHoist = null,
    isBuiltInComponent = NOOP,
    isCustomElement = NOOP,
    expressionPlugins = [],
    scopeId = null,
    ssr = false,
    ssrCssVars = ``,
    bindingMetadata = {},
    onError = defaultOnError
  }: TransformOptions
): TransformContext {
  const context: TransformContext = {

    // ...

    // methods
    helper(name) {
      context.helpers.add(name)
      return name
    },
    helperString(name) {
      return ``
    },
    replaceNode(node) {},
    removeNode(node) {},
    onNodeRemoved: () => {},
    addIdentifiers(exp) {
      // TODO
    },
    removeIdentifiers(exp) {
      // TODO
    },
    hoist(exp) {
      // TODO
      return {} as any
    },
    cache(exp, isVNode = false) {
      // TODO
      return {} as any
    }
  }

  return context
}

export function transform(root: RootNode, options: TransformOptions) {
  // TODO
}

// TODO
// createRootCodegen

export function traverseChildren(
  parent: ParentNode,
  context: TransformContext
) {
  // TODO
}

export function traverseNode(
  node: RootNode | TemplateChildNode,
  context: TransformContext
) {}

export function createStructuralDirectiveTransform(
  name: string | RegExp,
  fn: StructuralDirectiveTransform
): NodeTransform {
  return {} as any
}

fc6f1f1 add transform function

feat: transform function · gcclll/stb-vue-next@fc6f1f1

  1. create transform context

  2. traverse nodes, 递归遍历所有节点,构造器 codegenNode

  3. hoist static, 静态节点提升,复用

  4. ssr render, 不需要创建根节点 codegenNode

  5. 复制 context 属性到 -> root

http://qiniu.ii6g.com/img/20201130231832.png

transform 作用就是通过 traverseNode() 递归遍历所有节点,解析,构造对应的节点 codegenNode 。

b0d72da add compile.ts>compile()

feat(add): compile function · gcclll/stb-vue-next@b0d72da

对外的 compile 函数,执行分为三个阶段:

  • ast(baseParse()) -> 解析出 ast 结构

  • transform(transform()) -> 解析 ast 得到 codegenNode

  • codegen(generate()) -> 将 codegenNode 解析成 Render 函数

这是后面测试的基础,所以得提前实现了。

 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

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  // const onError = options.onError || defaultOnError
  const isModuleMode = options.mode === 'module'

  const prefixIdentifiers =
    !__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)

  // TODO errors
  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 || {}
      )
    })
  )

  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}

35248ce add exports maybe needs

feat(add): compiler-core exports · gcclll/stb-vue-next@35248ce

增加 compiler-core 模块的导出(export)内容

05a223b add transform pure text

feat(add): transform pure text · gcclll/stb-vue-next@05a223b

 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
export function traverseNode(
  node: RootNode | TemplateChildNode,
  context: TransformContext
) {
  // 保存当前被处理的 节点
  context.currentNode = node
  // 应用 transform 插件
  const { nodeTransforms } = context
  // 针对每个节点会收集到一个或多个 transformXxx 函数,用来解析它的 ast
  // 得到 codegenNode ,这些函数会在当前的节点树被递归遍历完之后调用
  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) {
      // 节点可能被删除了,比如: v-else-if, v-else 会合并到 v-if 的 branches[] 中
      return
    } else {
      // 节点可能会替换了,需要更新
      node = context.currentNode
    }
  }

  switch (
    node.type
    // TODO
  ) {
  }

  context.currentNode = node
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }
}

transform 阶段代码毕竟的三个阶段

  1. 收集 transformXxx 函数到 exitFns

  2. 根据 ast节点类型递归遍历子孙节点

  3. 按照收集时相反的顺序执行 exitFns,解析出 codegenNode

为了方便测试,在 generate() 中直接返回 ast : test: generate return ast for test · gcclll/stb-vue-next@999d8d6

1
2
3
4
5
6
const {
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')

const res = baseCompile(`pure text`)
console.log(res.children[0])

+RESULTS:

{
  type: 2,
  content: 'pure text',
  loc: {
    start: { column: 1, line: 1, offset: 0 },
    end: { column: 10, line: 1, offset: 9 },
    source: 'pure text'
  }
}

结果显示并没有 codegenNode 因为在transformText 中满足条件

children.length === 1 && node.type === NodeTypes.ROOT 而直接退出了。

至于 root.codegenNode = undefined 需要实现 createRootCodegen()

61ce406 add createRootCodegen() to create root.codegenNode

feat: createRootCodegen() for pure text · gcclll/stb-vue-next@61ce406

只增加了针对非 ELEMENT 类型或者孩子节点没有 codegenNode 的情况实现(当前 commit 最简化)。

当 root.children 只有一个孩子节点且该节点没有自己的 codegenNode 时候:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function createRootCodegen(root: RootNode, context: TransformContext) {
  // const { helper } = context
  const { children } = root
  if (children.length === 1) {
    // 只有一个孩子节点,直接取该孩子节点 的 codegenNode
    const child = children[0]
    if (isSingleElementRoot(root, child) && child.codegenNode) {
      // 当 root 节点下只有一个 element 元素的孩子节点时,不进行提升
    } else {
      // - single <slot/>, IfNode, ForNode: already blocks.
      // - single text node: always patched.
      // root codegen falls through via genNode()

      root.codegenNode = child
    }
  } else if (children.length > 1) {
    // TODO
  } else {
    // no children = noop, codegen will return null.
  }
}

测试

1
2
3
4
5
6
const {
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')

const res = baseCompile(`pure text`)
console.log(res)
{
  type: 0,
  children: [ { type: 2, content: 'pure text', loc: [Object] } ],
  helpers: [],
  components: [],
  directives: [],
  hoists: [],
  imports: [],
  cached: 0,
  temps: 0,
  codegenNode: {
    type: 2,
    content: 'pure text',
    loc: { start: [Object], end: [Object], source: 'pure text' }
  },
  loc: {
    start: { column: 1, line: 1, offset: 0 },
    end: { column: 10, line: 1, offset: 9 },
    source: 'pure text'
  }
}

注意 codegenNode 其实就是 root.children[0] 节点本身。

b9f3cb7 add transform text

feat: transformText function · gcclll/stb-vue-next@b9f3cb7

http://qiniu.ii6g.com/img/20201130150054.png

  1. 必须是文本节点或者类型是组合表达式类型(COMPOUND_EXPRESSION)

  2. patch flag 处理

  3. 构造 TEXT_CALL 类型节点

  4. codegenNode -> createCallExpression

f6d5271 add generate text codegen

codegen 阶段目的是将 codegenNode 解析成 Render 函数的一部分。

  1. f6d5271 add createCodegenContext()

    feat(add): codegen context creator · gcclll/stb-vue-next@f6d5271

    上下文对象创建函数,重点方法有两个(push(code, node)helper(key))。

    FIX1: lint errors, fix: f6d5271 lint errors · gcclll/stb-vue-next@0ac8c2f

  2. 2ef2699 增加 text codegen generator 实现

    feat: generate text codegen · gcclll/stb-vue-next@2ef2699

    该部分涉及到一个较为完整的 codegen generator 流程,所以增加内容较多,因此这里 不直接贴代码了,请点击上面 commit 链接查看实际增加的源码。

    处理流程:

    • preamble 处理,如果是 Node 环境需要通过 import { ...} from 'vue' 语法,如 果是浏览器环境使用 const { ... } = Vue 解构语法。

    • 是否使用 with() {} 作用域语法,默认是使用的

    • return ... 返回实际 render 函数返回结果,这里将返回最后被渲染的 DOM 结构。

    • genNode() 递归处理 ast 生成 render 函数的对应部分代码

  3. 6b901f9 增加 node 环境或 module 环境处理(genModulePreamble)

    feat: module preamble · gcclll/stb-vue-next@6b901f9 modue preamble : export { ... } from 'vue' function preamble: const { ... } = Vue

重点增加的 genXxx 函数 genText(node, context) 专门用来处理文本节点的。

1
2
3
4
5
6
function genText(
  node: TextNode | SimpleExpressionNode,
  context: CodegenContext
) {
  context.push(JSON.stringify(node.content), node)
}

测试

测试将分为两个部分,

function preamble 形式(作为全局 Vue 对象引入)
1
2
3
4
5
6
const {
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')

const res = baseCompile(`pure text`)
console.log(res.code)

return function render(_ctx, _cache) {
  with (_ctx) {
    return "pure text"
  }
}
undefined

fix: less the last } paren · gcclll/stb-vue-next@6b3bd2e

module preamble 形式(es6 模块化导出导入)
1
2
3
4
5
6
7
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')

const res = baseCompile(`pure text`, { mode: 'module' })
console.log(res.code)

return function render(_ctx, _cache) {
  return "pure text"
}
undefined

这里好像看不出啥区别,后面再说吧。

2f749b2 add interpolation generator

feat(add): transform -> generate interpolation · gcclll/stb-vue-next@2f749b2

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

const res = baseCompile(`{{ a > b }}`)
console.log(res.code)
console.log(res.ast.children[0])

这里实现分几个部分:

transform: traverseNode() 增加对插值的处理,后面增加了 traverseChildren 处理,因为所有的 ast 都是挂在 root.children 中的,所以最开始解析的是 ROOT 节点,因此这里必须 要增加 ROOT 类型的解析,调用 traverseChildren(node, ctx) 去递归解析 root.children

transform() -> traverseNode(): ROOT 解析 -> traverseChildren() -> traverseNode(): INTERPOLATION

新增核心函数:遍历所有 children[] 调用 traverseNode()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
export function traverseChildren(
  parent: ParentNode,
  context: TransformContext
) {
  // TODO	  let i = 0
  const nodeRemoved = () => {
    i--
  }

  for (; i < parent.children.length; i++) {
    const child = parent.children[i]
    if (isString(child)) continue
    context.parent = parent
    context.childIndex = i // 方便在 transformXxx 函数中能快速定位到当前节点
    context.onNodeRemoved = nodeRemoved
    traverseNode(child, context)
  }
}

http://qiniu.ii6g.com/img/20201201155917.png

codegen: genNode() 中新增 INTERPOLATIONSIMPLE_EXPRESSION 类型的处理, 因为 INTERPOLATION 的 ast.content(如上面代码执行结果) 类型是 SIMPLE_EXPRESSION。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
 const { content, isStatic } = node
 context.push(isStatic ? JSON.stringify(content) : content, node)
}

function genInterpolation(node: InterpolationNode, context: CodegenContext) {
 const { push, helper, pure } = context
 if (pure) push(PURE_ANNOTATION)
 push(`${helper(TO_DISPLAY_STRING)}(`)
 genNode(node.content, context)
 push(')')
}

feat(add): comment generator · gcclll/stb-vue-next@2d0e2a6

拓展:add comment generator

1
2
3
4
5
6
7
8

const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')

const res = baseCompile(`<!-- i'm a comment -->`)
console.log(res.code)
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createCommentVNode : _createCommentVNode } = _Vue

    return _createCommentVNode(" i'm a comment ")
  }
}
undefined

add element transfrom and generator

准备工作 compiler-core/src/utils.ts

feat: utils for compiler-core · gcclll/stb-vue-next@9436d8f

相关正则: const memberExpRE = /^[A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*|\[[^\]]+\])*$/

http://qiniu.ii6g.com/img/image.png

feat(add): resolveComponentType · gcclll/stb-vue-next@2265e46

解析出组件的类型,大体分为四类:

  1. 动态组件: <component is="xx"><component v-is="xx">

  2. 内置组件: Teleport, Transition, KeepAlive, Suspense

  3. 用户组件: $setup[] 上的组件

  4. 用户组件: context.components[] 上的组件

87339d2 add element transform

feat(add): transformElement function · gcclll/stb-vue-next@87339d2

普通标签的 transform codegenNode阶段。

  1. add createVNodeCall() 函数,创建基本的 ELEMENT 类型节点 codegenNode

    根据 isBlock 参数决定使用 BLOCK 函数还是 VNODE 函数。

     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
    
     export function createVNodeCall(
         context: TransformContext | null,
         tag: VNodeCall['tag'],
         props?: VNodeCall['props'],
         children?: VNodeCall['children'],
         patchFlag?: VNodeCall['patchFlag'],
         dynamicProps?: VNodeCall['dynamicProps'],
         directives?: VNodeCall['directives'],
         isBlock: VNodeCall['isBlock'] = false,
         disableTracking: VNodeCall['disableTracking'] = false,
         loc = locStub
     ): VNodeCall {
     if (context) {
         if (isBlock) {
             context.helper(OPEN_BLOCK)
             context.helper(CREATE_BLOCK)
         } else {
             context.helper(CREATE_VNODE)
         }
     }
    
     return {
         type: NodeTypes.VNODE_CALL,
         tag,
         props,
         children,
         patchFlag,
         dynamicProps,
         directives,
         isBlock,
         disableTracking,
         loc
     }
    }
    
  2. add createObjectExpression() 函数

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
     export function createObjectExpression(
         properties: ObjectExpression['properties'],
         loc: SourceLocation = locStub
     ): ObjectExpression {
     return {
         type: NodeTypes.JS_OBJECT_EXPRESSION,
         loc,
         properties
     }
     }
    
  3. add getStaticType() 判断节点是否需要做静态提升处理

  4. add transformElement: postTransformElement() 函数

  5. add stringifyDynamicPropNames() 将属性转成数组结构

测试:

1
2
3
4
5
6
7
8
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const res = baseCompile(`<div></div>`)
console.log('root codegenNode: ', res.ast.codegenNode)
console.log(res.code)
root codegenNode:  undefined
const _Vue = Vue

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

    return null
  }
}
undefined

正确结果:

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

    return (_openBlock(), _createBlock("div"))
  }
}

问题:

  1. 根节点 codegenNode 为空

  2. render 函数内没有 openBlock, createBlock 导出

  3. return 后面没内容(这是 generator 范畴,此节不展开)

问题1,2都是在同一个地方处理的,因为当 ROOT 节点只有一个孩子节点的时候,不会用 CREATE_VNODE 创建,而是改用 CREATE_BLOCK,所以这两个问题一起处理

FIX 1,2: fix: no export open/create block function from Vue · gcclll/stb-vue-next@97cf290

修改: createRootCodegen(root: RootNode, context: TransformContext)

2f58786 add element generator

feat: element generator · gcclll/stb-vue-next@2f58786

路径:

  1. VNODE_CALL ->

  2. genVNodeCall() ->

  3. genNodeList([], ctx) ->

    • string: push(node)

    • array: genNodeListAsArray(node, ctx)

    • other: genNode(node, ctx)

测试:

1
2
3
4
5
6
7
8
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const res = baseCompile(`<div></div>`)
console.log('root codegenNode: ', res.ast.codegenNode)
console.log(res.code)
root codegenNode:  {
  type: 13,
  tag: '"div"',
  props: undefined,
  children: undefined,
  patchFlag: undefined,
  dynamicProps: undefined,
  directives: undefined,
  isBlock: true,
  disableTracking: false,
  loc: {
    start: { column: 1, line: 1, offset: 0 },
    end: { column: 12, line: 1, offset: 11 },
    source: '<div></div>'
  }
}
const _Vue = Vue

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

    return (_openBlock(), _createBlock("div"))
  }
}

05ca2f8 root.children 有多个孩子

feat: root.children has multi child · gcclll/stb-vue-next@05ca2f8 · GitHub

当有多个孩子节点的时候,会创建一个 fragment 将他们包起来。

http://qiniu.ii6g.com/img/20201201232048.png

FIX: 死循环, genNode(node.codegenNode, ctx)

http://qiniu.ii6g.com/img/20201201232120.png

测试:

1
2
3
4
5
6
7
8

const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const res = baseCompile(`<div></div><div></div>`)
console.log(res.code)
const _Vue = Vue

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

    return (_openBlock(), _createBlock(_Fragment,null,[
      _createVNode("div"),
      _createVNode("div")
    ],64 /* STABLE_FRAGMENT */))
  }
}
undefined

FIX: 参数之间少了空格(feat: root.children has multi child · gcclll/stb-vue-next@05ca2f8)

正解:

const _Vue = Vue
const { createVNode: _createVNode } = _Vue

const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */)

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

    return (_openBlock(), _createBlock(_Fragment, null, [
      _hoisted_1,
      _hoisted_2
    ], 64 /* STABLE_FRAGMENT */))
  }
}

正确答案中做了静态提升处理,代码在 transform() 函数中 hoistStatic(root, context) 的调用,会从 ROOT 节点开始遍历,将需要提升的节点进行提升处理。

7cb3dbf add hoist static 静态提升

满足提升的三种情况:

  1. tag 和 tagType 都是 ELEMENT 且整棵树都是静态

  2. 包含动态孩子节点,但是有静态属性的,将属性提升

  3. 纯文本节点

feat(add): hoist static · gcclll/stb-vue-next@7d7dbd4

transform() 中增加静态提升处理:

1
2
3
if (options.hoistStatic) {
  hoistStatic(root, context)
}

feat: hoist static · gcclll/stb-vue-next@7cb3dbf

  1. 修改 genFunctionPreamble(ast: RootNode, context: CodegenContext) 解构出需要用到的函数(_createVNode)

    /img/commit/diff-hoist-decon-functions.png

  2. 增加 genHoists() 函数,生成 ast.hoists 中需要提升的节点

     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
    
    function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) {
    if (!hoists.length) {
        return
    }
    
    context.pure = true
    const { push, newline, helper, scopeId, mode } = context
    const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
    newline()
    
    // push scope Id before initializing hoisted vnodes so that these vnodes
    // get the proper scopeId as well.
    if (genScopeId) {
        push(`${helper(PUSH_SCOPE_ID)}("${scopeId}")`)
        newline()
    }
    
    hoists.forEach((exp, i) => {
        if (exp) {
        push(`const _hoisted_${i + 1} = `)
        genNode(exp, context)
        newline()
        }
    })
    
    if (genScopeId) {
        push(`${helper(POP_SCOPE_ID)}()`)
        newline()
    }
    
    context.pure = false
    }
    

测试:

1
2
3
4
5
6
7
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const res = baseCompile(`<div></div><div></div>`, { hoistStatic: true })
console.log(res.code)
const _Vue = Vue
const { createVNode: _createVNode } = _Vue

const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */)

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

    return (_openBlock(), _createBlock(_Fragment, null, [
      _hoisted_1,
      _hoisted_2
    ], 64 /* STABLE_FRAGMENT */))
  }
}
undefined

PS: 静态属性提升 feat: props hoist static · gcclll/stb-vue-next@1e58eeb

prop transform and generator

在这之前我们完成了以下几个基本部分:

接下来需要完成属性的解析才能进行下一步,因为 v-if, v-for, v-slot, ... 都需要属 性解析。

属性转换这里异常复杂,需要慢慢展开来讲,并且涉及到各种指令,因此对于完整的测试需 要等所有指令 transform 完成之后再进行。

1792f93 props transform

buildProps

feat(add): transform props · gcclll/stb-vue-next@1792f93

  • codegenNode.props 构建成 如下结构:

     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
    
    {
      "type":15,
      "properties":[
          {
              "type":16,
              "key":{
                  "type":4,
                  "isConstant":false,
                  "content":"class",
                  "isStatic":true
              },
              "value":{
                  "type":4,
                  "isConstant":false,
                  "content":"second",
                  "isStatic":true
              }
          },
          {
              "type":16,
              "key":{
                  "type":4,
                  "isConstant":false,
                  "content":"onClick",
                  "isStatic":true
              },
              "value":{
                  "type":4,
                  "content":"clickHandle",
                  "isStatic":false,
                  "isConstant":false,
              }
          }
      ]
    }
  • v-bind,v-on 指令,没有参数,需要将 props 合并

e4acc0d props generator

feat: props generator · gcclll/stb-vue-next@e4acc0d

修改点:

  1. add genExpressionAsPropertyKey() 生成属性 key 函数

    三种可能的属性名

    • 静态属性名: <div class="value"> -> { class: "value" }

    • 动态属性名: <div :[propName]="value" -> { [propName]: "value"}

    • 组合表达式属性名:TODO

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
      // 生成对象的属性 key (可能是静态,动态)
      function genExpressionAsPropertyKey(
       node: ExpressionNode,
       context: CodegenContext
      ) {
       const { push } = context
       if (node.type === NodeTypes.COMPOUND_EXPRESSION) {
         // TODO 动态属性名或表达式
       } else if (node.isStatic) {
         // only quote key if necessary
         const text = isSimpleIdentifier(node.content)
           ? node.content
           : JSON.stringify(node.content)
      
         push(text, node)
       } else {
         push(`[${node.content}]`, node)
       }
      }
      
  2. add genObjectExpression() 将属性列表生成对象

    遍历节点的 node.properties 先生成 key(genExpressionAsPropertyKey(key)) 再生成 value(genNode(value)) 。

测试:

1
2
3
4
5
6
7
8

const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const res = baseCompile(`<div class="first" name="div"></div>`, { hoistStatic: true })
console.log(res.code)
const _Vue = Vue
const { createVNode: _createVNode } = _Vue

const _hoisted_1 = {
  class: "first",
  name: "div"
}

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

    return (_openBlock(), _createBlock("div", _hoisted_1))
  }
}
undefined

实例中最后是用的 createBlock() 是因为 root.children 只有一个 child 。

static props

修改函数: transforms/transformElement

1
2
3
4
5
6
7
8

const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const res = baseCompile(`<div class="first"></div><div class="second"></div>`, { hoistStatic: true })
console.log(res.ast.codegenNode.children[0].props[0])
{
  type: 6,
  name: 'class',
  value: {
    type: 2,
    content: 'first',
    loc: { start: [Object], end: [Object], source: '"first"' }
  },
  loc: {
    start: { column: 6, line: 1, offset: 5 },
    end: { column: 19, line: 1, offset: 18 },
    source: 'class="first"'
  }
}
undefined

6951dd1 merge props

feat: merge props · gcclll/stb-vue-next@6951dd1

合并属性的条件:存在没有参数的指令,如: <div v-bind="{...}" v-on="{...}"

FIX: fix: merge toHandlers props · gcclll/stb-vue-next@12a66f0

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

const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
const log = (code, title) => {
  console.log(`>>> ${title}`)
  const res = baseCompile(code)
  console.log(res.code)
}
 
log(`
<div class="first" v-on="{ click: clickHandle  }" v-bind="{ style: 'color:red' }"></div>`, '无参数的指令,合并所有属性')

log(`<div class="second" v-on:click="clickHandle" v-bind:style="color:red"></div>`, '有参数的指令,不合并')
>>> 无参数的指令,合并所有属性
const _Vue = Vue

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

    return (_openBlock(), _createBlock("div", _mergeProps({ class: "first" }, _toHandlers({ click: clickHandle  }), { style: 'color:red' }), null, 16 /* FULL_PROPS */))
  }
}
>>> 有参数的指令,不合并
const _Vue = Vue

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

    return _withDirectives((_openBlock(), _createBlock("div", { class: "second" }, null, 512 /* NEED_PATCH */)), )
  }
}
undefined

有参数指令时,需要结合 v-on 指令解析,因此需要先实现了 transform 指令才能得到下面的正确结果。

不合并(mergeProps()) 的正解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(function anonymous(
) {
const _Vue = Vue

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

    return (_openBlock(), _createBlock("div", {
      class: "second",
      onClick: clickHandle,
      style: { color: 'red' }
    }, null, 12 /* STYLE, PROPS */, ["onClick"]))
  }
}
})

下面将继续完成指令相关的 transform

6c43451 add v-on transform

init: feat(init): v-on directive · gcclll/stb-vue-next@98dcc96

实现:feat: v-on directive transform · gcclll/stb-vue-next@6c43451

1
2
3
4
5
6
7
8
9

const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const res = baseCompile(`<div class="second" v-on:click="clickHandle" v-bind:style="color:red"></div>`)

console.log(res.code)
const _Vue = Vue

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

    return _withDirectives((_openBlock(), _createBlock("div", {
      class: "second",
      onClick: clickHandle
    }, null, 8 /* PROPS */, ["onClick"])), )
  }
}
undefined

问题: v-bind 没有被解析出来。

如果 v-on的 exp 是个简单的表达式,需要将其转成函数 $event => (i++)

feat(add): v-on with simple expression as handler · gcclll/stb-vue-next@1542b41

判断是简单表达式的依据:

const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))

即不是 member expression 也不是 function expression 。

member expression: /^[A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*|\[[^\]]+\])*$/ http://qiniu.ii6g.com/img/20201212163019.png

function expresstion: /^\s*([\w$_]+|\([^)]*?\))\s*=>|^\s*function(?:\s+[\w$]+)?\s*\(/ http://qiniu.ii6g.com/img/20201212163123.png

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const { code, ast } = baseCompile(`<div v-on:click="i++"></div>`)
console.log(code)
console.log(`>>> event name`)
console.log(ast.codegenNode.props.properties[0].key)
console.log(`>>> event handler`)
console.log(ast.codegenNode.props.properties[0].value)
const _Vue = Vue

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

    return (_openBlock(), _createBlock("div", {
      onClick: $event => (i++)
    }, null, 8 /* PROPS */, ["onClick"]))
  }
}
>>> event name
{
  type: 4,
  loc: {
    start: { column: 11, line: 1, offset: 10 },
    end: { column: 16, line: 1, offset: 15 },
    source: 'click'
  },
  content: 'onClick',
  isStatic: true,
  constType: 3
}
>>> event handler
{
  type: 8,
  loc: {
    source: '',
    start: { line: 1, column: 1, offset: 0 },
    end: { line: 1, column: 1, offset: 0 }
  },
  children: [
    '$event => (',
    {
      type: 4,
      content: 'i++',
      isStatic: false,
      constType: 0,
      loc: [Object]
    },
    ')'
  ]
}
undefined

更多测试用例(<f12>)打开控制台查看 ->> 。

options.cacheHandlers 属性要配合 options.prefixIdentifiers 使用。

作用是缓存事件处理函数,原理是:

1
2
3
4
5
6
7
8
9
return function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    return (_openBlock(), _createBlock("div", {
      onClick: _cache[1] || (_cache[1] = () => {})
    }))
  }
}

缓存的附加条件: let shouldCache: boolean = context.cacheHandlers && !exp

没有表达式值的情况下才缓存,因为此时会创建一个空的函数作为事件 handler,为了避免 创建过多的无意义的空函数,使用缓存是个不错的选择(但,一般绑定了事件应该不至于不 给处理函数吧!!!)。

f805858 add v-bind transform

feat(add): v-bind transform · gcclll/stb-vue-next@f805858

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const res = baseCompile(`
<div v-bind:name="test"
  :age="100"
  :[propName]="myName"
  :[first+second]="thrid"
  :no-need-camel-prop="noNeedCamelProp"
  :need-camel-prop.camel="needCamelProp"
  :no-exp-prop.camel
></div>`, {
  onError(e) {
    console.log(e.message)
  }
})
console.log(`>>> render function\n`)
console.log(res.code)
v-bind is missing expression.
>>> render function

const _Vue = Vue

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

    return (_openBlock(), _createBlock("div", {
      name: test,
      age: 100,
      [propName || ""]: myName,
      [first+second || ""]: thrid,
      "no-need-camel-prop": noNeedCamelProp,
      needCamelProp: needCamelProp,
      noExpProp: ""
    }, null, 16 /* FULL_PROPS */, ["name","age","no-need-camel-prop","needCamelProp","noExpProp"]))
  }
}
undefined

v-bind 属性支持以下几种方式:

  • v-bind:name="test" 无缩写属性,最普通的一种用法

  • :age="100" 缩写形式

  • :[propName]="myName" 普通动态属性名

  • :[first+second]="third" 表达式动态属性名

  • :no-need-camel-prop="noNeedCamelProp" 不需要转驼峰的属性名

  • :need-camel-prop.camel="needCamelProp" 需要转成驼峰的属性名,需要制定 .camel 修饰符

  • no-exp-prop.camel 无属性值的属性,会给默认 "" 值,同时给出警告,不建议使用。

更多测试用例(<f12>)打开控制台查看 ->> 。

0cc76f0 add v-model transform

feat(add): v-model transform · gcclll/stb-vue-next@0cc76f0

<input v-model="model" />

经过 transformModel 之后的 node.props:

 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
[
    {
        "type":16, // JS_PROPERTY
        "key":{
            "type":4, // SIMPLE_EXPRESSION
            "content":"modelValue",
            "isStatic":true,
            "constType":3
        },
        "value":{
            "type":4,
            "content":"model",
            "isStatic":false,
            "constType":0,
        }
    },
    {
        "type":16,
        "key":{
            "type":4,
            "content":"onUpdate:modelValue",
            "isStatic":true,
            "constType":3
        },
        "value":{
            "type":8, // COMPOUND_EXPRESSION
            "children":[
                "$event => (",
                {
                    "type":4,
                    "content":"model",
                    "isStatic":false,
                    "constType":0,
                },
                " = $event)"
            ]
        }
    }
]

compiler-core 阶段的解析脑图: /img/vue3/compiler-core/pcg/pcg-08-v-model-cc.svg

从图中可以看出, v-model 指令的解析也是在 buildProps 中完成的,关于这个函数的脑 图也可以查看 buildProps(node, context) 如何构建 props ?

vue/baseCompile 解析之后的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const _Vue = Vue

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

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

vue/compile 经过 compile-dom package(未完成) 的 transformModel 之后的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(function anonymous(
) {
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 => (model = $event)
    }, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [
      [_vModelText, model]
    ])
  }
}
})

fix: v-model no value · gcclll/stb-vue-next@a537be0

修复之后(genNode 没有实现 8,COMPOUND_EXPRESSION 类型),测试

  1. 不带参数的 v-model

    1
    2
    3
    4
    5
    6
    7
    
     const {
         baseParse,
         baseCompile
     } = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
    
     const { code } = baseCompile(`<input v-model="model" />`)
     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", {
          modelValue: model,
          "onUpdate:modelValue": $event => (model = $event)
        }, null, 8 /* PROPS */, ["modelValue","onUpdate:modelValue"]))
      }
    }
    
  2. 指令 { prefixIdentifiers: true } 选项(需要 node 环境, TODO)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     const {
         baseParse,
         baseCompile
     } = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
    
     const { code } = baseCompile(`<input v-model="model" />`, {
       prefixIdentifiers: true
     })
     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", {
          modelValue: model,
          "onUpdate:modelValue": $event => (model = $event)
        }, null, 8 /* PROPS */, ["modelValue","onUpdate:modelValue"]))
      }
    }
    undefined
    
  3. 组合表达式(8,COMPOUND_EXPRESSION)

    1
    2
    3
    4
    5
    6
    7
    
    const {
    baseParse,
    baseCompile
    } = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
    
    const { code } = baseCompile(`<input v-model="model[index]" />`)
    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", {
          modelValue: model[index],
          "onUpdate:modelValue": $event => (model[index] = $event)
        }, null, 8 /* PROPS */, ["modelValue","onUpdate:modelValue"]))
      }
    }
    undefined
    
  4. 带参数

    1
    2
    3
    4
    5
    6
    7
    8
    
    
    const {
    baseParse,
    baseCompile
    } = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
    
    const { code } = baseCompile(`<input v-model:value="model" />`)
    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: model,
          "onUpdate:value": $event => (model = $event)
        }, null, 40 /* PROPS, HYDRATE_EVENTS */, ["value","onUpdate:value"]))
      }
    }
    undefined
    

    不带参数的时候参数名会给一个默认值: modelValue, 如果有自己的参数会直接使 用提供的参数名。

  5. 动态参数

    1
    2
    3
    4
    5
    6
    7
    8
    
    
    const {
    baseParse,
    baseCompile
    } = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
    
    const { code } = baseCompile(`<input v-model:[value]="model" />`)
    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]: model,
          : $event => (model = $event)
        }, null, 16 /* FULL_PROPS */))
      }
    }
    

    结果显示,动态属性的事件名没有被解析出来 : $event => (model = $event)

    修复之后结果(fix: v-model dynamic arg generate · gcclll/stb-vue-next@94a7a85):

    const _Vue = Vue
    
    return function render(_ctx, _cache) {
      with (_ctx) {
        const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue
    
        return (_openBlock(), _createBlock("input", {
          [value]: model,
          ["onUpdate:" + value]: $event => (model = $event)
        }, null, 16 /* FULL_PROPS */))
      }
    }
    
  6. 缓存事件回调函数(cacheHandlers: true, TODO)

    需要结合 prefixIdentifiers: true 使用。

针对 v-model 还需要 compile-runtime 阶段的支持,由于这里还没完成,所以这里的结果 和 vue-next 测试结果会有些出入,出入点在于 compiler-runtime 期会将 modelValue: model 删除。

更多测试用例(<f12>)打开控制台查看 ->> 。

bf18a84 add v-once transform

feat(add): v-once · gcclll/stb-vue-next@bf18a84

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const seen = new WeakSet()

export const transformOnce: NodeTransform = (node, context) => {
  if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
    // 缓存实现 v-once,就算有数据更新也不会重新生成 render 函数
    if (seen.has(node)) {
      return
    }
    seen.add(node)
    context.helper(SET_BLOCK_TRACKING)
    return () => {
      const cur = context.currentNode as ElementNode | IfNode | ForNode
      if (cur.codegenNode) {
        cur.codegenNode = context.cache(cur.codegenNode, true /* isVNode */)
      }
    }
  }
}

v-once 指令的实现看似挺简单的,将解析后的 node 节点缓存到 seen: WeakSet 中, 下次使用的时候直接取缓存(context.cache(...)),而不是重新生成 codegenNode

JS_CACHE_EXPRESSION 结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export function createCacheExpression(
  index: number,
  value: JSChildNode,
  isVNode: boolean = false
): CacheExpression {
  return {
    type: NodeTypes.JS_CACHE_EXPRESSION,
    index, // 在 context.cached 中的索引
    value, // v-once节点的 ast
    isVNode, // block 或 vnode ?
    loc: locStub
  }
}

generator 阶段实现:feat(add): v-once generator · gcclll/stb-vue-next@8bacf14

genNode() 中增加 JS_CACHE_EXPRESSION 类型的分支处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
  const { push, helper, indent, deindent, newline } = context
  if (node.isVNode) {
    indent()
    push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
    newline()
  }

  push(`_cache[${node.index}] = `)
  genNode(node.value, context)
  if (node.isVNode) {
    push(`,`)
    newline()
    push(`${helper(SET_BLOCK_TRACKING)}(1),`)
    newline()
    push(`_cache[${node.index}]`)
    deindent()
  }
  push(`)`)
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const c = ( tpl, desc ) => {
  console.log(desc)
  const { code } = baseCompile(tpl)
  console.log(code)
}

c(`<div :id="foo" v-once />`, `>>> <div :id="foo" v-once />`)
c(`<div><div :id="foo" v-once /></div>`, `>>> 标签中嵌套使用`)
c(`<div><Comp :id="foo" v-once /></div>`, `>>> 在自定义组件上`)
>>> <div :id="foo" v-once />
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { setBlockTracking : _setBlockTracking, createVNode : _createVNode } = _Vue

    return _cache[1] || (
      _setBlockTracking(-1),
      _cache[1] = _createVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"]),
      _setBlockTracking(1),
      _cache[1]
    )
  }
}
>>> 标签中嵌套使用
const _Vue = Vue

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

    return (_openBlock(), _createBlock("div", null, [
      _cache[1] || (
        _setBlockTracking(-1),
        _cache[1] = _createVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"]),
        _setBlockTracking(1),
        _cache[1]
      )
    ]))
  }
}
>>> 在自定义组件上
const _Vue = Vue

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

    return (_openBlock(), _createBlock("div", null, [
      _cache[1] || (
        _setBlockTracking(-1),
        _cache[1] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"]),
        _setBlockTracking(1),
        _cache[1]
      )
    ]))
  }
}
undefined

TODO 缺少: const _component_Comp = _resolveComponent("Comp")

更多测试用例(<f12>)打开控制台查看 ->> 。

acdea14 add v-if transform

v-if 指令源码脑图可参考: 05 v-if 指令(git:0a591b6)

对于 v-if|else|else-if 指令在 transform 阶段,转换收集 transformXxx 函数过程中, 会先针对指令进行处理,比如: v-else, v-else-if 指令的组件会被解析到 v-if 节 点的 node.branches[] 分支数组里面之后被删除,这些都是在收集 transformXxx 之前需要完成的。

包括 v-for 指令都需要经过 createStructuralDirectiveTransform() 函数封装一层 之后,返回对应的 transformXxx 函数。

 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

export function createStructuralDirectiveTransform(
  name: string | RegExp,
  fn: StructuralDirectiveTransform
): NodeTransform {
  const matches = isString(name)
    ? (n: string) => n === name
    : (n: string) => name.test(n)

  return (node, context) => {
    if (node.type === NodeTypes.ELEMENT) {
      const { props } = node
      // structural directive transforms are not concerned with slots
      // as they are handled separately in vSlot.ts
      if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
        return
      }
      const exitFns = []
      for (let i = 0; i < props.length; i++) {
        const prop = props[i]
        if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
          // structural directives are removed to avoid infinite recursion
          // also we remove them *before* applying so that it can further
          // traverse itself in case it moves the node around
          props.splice(i, 1)
          i--
          const onExit = fn(node, prop, context)
          if (onExit) exitFns.push(onExit)
        }
      }
      return exitFns
    }
  }
}

通过 for (...) 将所有 v-if/v-for 相关指令经过他们自己的处理函数(比如: processIf ) 之后得到最终的 onExit 收集到 exitFns 中,在处理过程中随时会出 现节点的删除操作(比如: v-else 节点会在解析完之后被删除),在正常的 traverse 过 程中这些节点都不会再存在。

PS: 正确理解应该属于移动操作,因为原始的 AST 结构并没改变,只不过是在原有的 AST 数结构中移除到新的 AST 节点下面了。

acdea14 v-if transform init

feat(init): v-if transform · gcclll/stb-vue-next@acdea14

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export const transformIf = createStructuralDirectiveTransform(
  /^(if|else|else-if)$/,
  (node, dir, context) => {
    return processIf(node, dir, context, (ifNode, branch, isRoot) => {
      // TODO
      console.log(ifNode, branch, isRoot)
      return () => {}
    })
  }
)

export function processIf(
  node: ElementNode,
  dir: DirectiveNode,
  context: TransformContext,
  processCodegen?: (
    node: IfNode,
    branch: IfBranchNode,
    isRoot: boolean
  ) => (() => void) | undefined
) {}

初始化 v-if process 函数, processIf 函数里面会针对 v-if 节点甚至它的兄弟节点做 一系列操作,比如将下一个是 v-else 的兄弟节点删除移到自己的 branches[] 里面。

9039a3e v-if transform processIf

feat: v-if processIf · gcclll/stb-vue-next@9039a3e

这里增加了两个函数的实现:

  1. processIf, 解析 if,创建 IF,9 类型的结构,替换 v-if 原来的 ast

    1
    2
    3
    4
    5
    
     const ifNode: IfNode = {
       type: NodeTypes.IF,
       loc: node.loc,
       branches: [branch]
     }
    

    其中 branches 保存着所有 v-else, v-else-if 分支节点,这里其实是创建了一个默认 的分支节点,因为 v-if 系列指令在 render 函数中是以三元运算符(?:)形式存 在的,所以 if 后面必须要有一个分支,即 condition ? node1 : node2 中的 node2 必须是个有效的值,才能正常使用 ?: 运算符。

    所以,如果只有 v-if 指令的时候三元符后面的值起始是个空值(好像是 null)

  2. createIfBranch, 创建 v-if 的分支节点的

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
    return {
     type: NodeTypes.IF_BRANCH,
     loc: node.loc,
     // condition ? v-if node : v-else node
     condition: dir.name === 'else' ? undefined : dir.exp,
     // 如果用的是 <template v-if="condition" ... 就需要 node.children
     // 因为 template 本身是不该被渲染的
     children:
       node.tagType === ElementTypes.TEMPLATE && !findDir(node, 'for')
         ? node.children
         : [node],
     // 对于 v-for, v-if/... 都应该给它个 key, 这里是用户编写是的提供的唯一 key
     // 如果没有解析器会默认生成一个全局唯一的 key
     userKey: findProp(node, `key`)
    }
    }
    

    注意看最后一个属性, v-if 分支也是需要一个 key 属性的。

44985b4 v-if transform createIfBranch

feat: v-if createIfBranch · gcclll/stb-vue-next@44985b4

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export function createConditionalExpression(
  test: ConditionalExpression['test'],
  consequent: ConditionalExpression['consequent'],
  alternate: ConditionalExpression['alternate'],
  newline = true
) {
  return {
    type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
    test,
    consequent,
    alternate,
    newline,
    loc: locStub
  }
}

这里的结构(v-if)在 render 函数中的对应关系:

test ? consequent : alternate

如果有 v-else-if 时候, alternate 结构会是个完整的 JS_CONDITIONAL_EXPRESSION ,即: alternate: { test, consequent, alternate, ...} 所以:

test ? consequent : test1 ? consequent 1 : alternate

fix: no v-if transform · gcclll/stb-vue-next@1e24eb7

到这里 v-if 指令 transform 阶段已经完成,测试结果:

1
2
3
4
5
6
7
8
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const { code, ast } = baseCompile(`<div v-if="ok"/>`)
console.log(`>>> ast.codegenNode 结果`)
console.log(ast.codegenNode)
>>> ast.codegenNode 结果
{
  type: 9,
  loc: {
    start: { column: 1, line: 1, offset: 0 },
    end: { column: 17, line: 1, offset: 16 },
    source: '<div v-if="ok"/>'
  },
  branches: [
    {
      type: 10,
      loc: [Object],
      condition: [Object],
      children: [Array],
      userKey: undefined
    }
  ],
  codegenNode: {
    type: 19,
    test: {
      type: 4,
      content: 'ok',
      isStatic: false,
      isConstant: false,
      loc: [Object]
    },
    consequent: {
      type: 13,
      tag: '"div"',
      props: [Object],
      children: undefined,
      patchFlag: undefined,
      dynamicProps: undefined,
      directives: undefined,
      isBlock: true,
      disableTracking: false,
      loc: [Object]
    },
    alternate: {
      type: 14,
      loc: [Object],
      callee: Symbol(createCommentVNode),
      arguments: [Array]
    },
    newline: true,
    loc: { source: '', start: [Object], end: [Object] }
  }
}
undefined

+RESULTS: 错误结果

const _Vue = Vue

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

    return (_openBlock(), _createBlock("div", { key: 0 }))
  }
}
>>> ast.codegenNode 结果
{
  type: 13,
  tag: '"div"',
  props: {
    type: 15,
    loc: { source: '', start: [Object], end: [Object] },
    properties: [ [Object] ]
  },
  children: undefined,
  patchFlag: undefined,
  dynamicProps: undefined,
  directives: undefined,
  isBlock: true,
  disableTracking: false,
  loc: {
    start: { column: 1, line: 1, offset: 0 },
    end: { column: 17, line: 1, offset: 16 },
    source: '<div v-if="ok"/>'
  }
}
undefined

结果显示是不对的,因为创建的 IF 结构没有替换 ast 🌲中原来的节点,追踪后发现是 漏掉了 context.replaceNode(node) 的实现。

fix: v-if codegenNode is incorrect · gcclll/stb-vue-next@47c30d2

traverseNode 中需要增加 case 9,IF 分支处理,遍历所有的 branches[]

fix: v-if branches no codegenNode · gcclll/stb-vue-next@179f06f

742757e v-if generator

feat: v-if generator · gcclll/stb-vue-next@742757e

genNode 增加 JS_CONDITIONAL_EXPRESSION 分支处理(genConditionalExpression)

 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
function genConditionalExpression(
  node: ConditionalExpression,
  context: CodegenContext
) {
  const { test, consequent, alternate, newline: needNewline } = node
  const { push, indent, deindent, newline } = context
  if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
    // 非简单的标识符需要用括号,可能是表达式,所以需要 (a + b) ? ... : ...
    const needsParams = !isSimpleIdentifier(test.content)
    needsParams && push(`(`)
    genExpression(test, context)
    needsParams && push(`)`)
  } else {
    push(`(`)
    genNode(test, context)
    push(`)`)
  }

  needNewline && indent()
  context.indentLevel++
  needNewline || push(` `)
  push(`? `)
  genNode(consequent, context)
  context.indentLevel--
  needNewline && newline()
  needNewline || push(` `)
  push(`: `)
  const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
  if (!isNested) {
    // 不是嵌套
    context.indentLevel++
  }
  genNode(alternate, context)
  if (!isNested) {
    context.indentLevel--
  }

  needNewline && deindent(true /* without newline */)
}

genConditionalExpression 处理分为三个部分

  1. test 生成条件表达式,这里是: ok ,如果是表达式需要括号: (a + b)

  2. consequent 用来生成 ? 后面的表达式,即 ok 结果为 truth 时执行

  3. alternate 用来生成 : 后面的表达式,即 ok 结果为 falsy 时执行

    alternate 中的结构可能也是个 JS_CONDITIONAL_EXPRESSION 结构,代表可能有 v-else-if 分支,如: (a + b) ? node1 : (c + d) ? node2 : othernode

测试:

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

const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const c = ( tpl, desc ) => {
  console.log(`>>> ` + desc)
  const { code } = baseCompile(tpl, { hoistStatic: true })
  console.log(code)
}

c(`<div v-if="ok"/>`, 'basic v-if')
c(`<template v-if="ok"><div/>hello<p/></template>`, 'template v-if')
>>> basic v-if
const _Vue = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue

const _hoisted_1 = { key: 0 }

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

    return ok
      ? (_openBlock(), _createBlock("div", _hoisted_1))
      : _createCommentVNode("v-if", true)
  }
}
>>> template v-if
const _Vue = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode } = _Vue

const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createTextVNode("hello")
const _hoisted_3 = /*#__PURE__*/_createVNode("p", null, null, -1 /* HOISTED */)

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode : _createVNode, createTextVNode : _createTextVNode, Fragment : _Fragment, openBlock : _openBlock, createBlock : _createBlock, createCommentVNode : _createCommentVNode } = _Vue

    return ok
      ? (_openBlock(), _createBlock(_Fragment, { key: 0 }, [
          _hoisted_1,
          _hoisted_2,
          _hoisted_3
        ], 64 /* STABLE_FRAGMENT */))
      : _createCommentVNode("v-if", true)
  }
}
undefined

BUG: 这里居然少了个 _hoisted_2 ???

1
2
3
4
5
[
  _hoisted_1,
  ,
  _hoisted_3
]

答: genNode() 中缺少对 4,TEXT_CALL 纯文本类型处理。

解:fix: v-if TEXT_CALL gen node · gcclll/stb-vue-next@2372b5f

更多测试用例(<f12>)打开控制台查看 ->> 。

fa77b51 v-else/v-else-if

feat(add): v-else · gcclll/stb-vue-next@fa77b51

修改点:

  1. processCodegen() 函数里面增加分支处理

    http://qiniu.ii6g.com/img/20201209164845.png 这里有一个需要注意的点: getParentCondition() 会一直查找 JS_CONDITIONAL_EXPRESSION 类型节点的 alternate ,如果它依旧是个 JS_CONDITIONAL_EXPRESSION 类型,说明是多级的 if/else 条件语句,直到找到最 后一个不是为止。

    相当于 : c1 ? cons1 : c2 ? cons2 : c3 ? cons3 : alt 会一直从 c1 节点开始 查找直到找到最后的那个 alt 节点为止,然后将新的分支挂到 alt 后面组织成新 的分支: c1 ? cons1 : c2 ? cons2 : c3 ? cons3 : c4 ? cons4 : newalt

    PS: c1, c2, c3, c4 分别代表分支节点的 test ,最后追加的 c4 ? cons4 : newalt 三个对象都属于新加的节点, {test -> c4, cons4 -> consequent, alternate -> newalt }

  2. processIf() 里增加分支处理

    新增代码里有个 while 循环去从当前的分支节点开始在它的兄弟节点里面往回找,直 到找到第一个 9,IF 节点,这中间不允许出现其他有效节点(除注释,空文本节点外), 因为 v-if/else 指令节点必须紧靠着。

    找到之后,要将当前分支节点删除,并且同时要去手动 traverseNode(branch) 一次, 因为他在原来的 ast 树种删除了,所以原来的 traverse 进程不会遍历它,因此需要手 动执行 traverse 去处理它及其孩子节点生成对应的 codegenNode 。

    然后将其 push 到 9,IF 节点的 node.branches 里面作为分支。

  3. isSameKey(a,b) 新增,检测两个 key 属性是不是相同

    几种判定为不相同的条件:

    1. key 类型不同 (a.type !== b.type)

    2. key 值不同 (a.value.content !== b.value.content)

    3. key 如果是指令类型,检测表达式类型,静态属性异同(isStatic)

  4. getParentCondition() 新增,递归 9,IF 节点的 node.alternate.alternate.alternate... 直到找到 alternate 不是 JS_CONDITIONAL_EXPRESSION 的情况

FIX: fix: v-else current node dont removed · gcclll/stb-vue-next@464d681

测试:

1
2
3
4
5
6
7
8

const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const { code } = baseCompile(`<div v-if="ok"/><p v-else/>`)
console.log(code)
const _Vue = Vue

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

    return ok
      ? (_openBlock(), _createBlock("div", { key: 0 }))
      : (_openBlock(), _createBlock("p", { key: 1 }))
  }
}
undefined

更多测试(<f12>)打开控制台查看 ->> 。

BUG: v-else-if 被解析成了 else 因为 parser 阶段匹配正则不对。 fix: parser v-else-if failed · gcclll/stb-vue-next@5b83d1c

6c82066 add v-for transform

feat(init): v-for · gcclll/stb-vue-next@3a1662e

feat: v-for directive · gcclll/stb-vue-next@6c82066

v-for 指令实现过程中需要用到的几个函数:

  • transformFor() 最终生成的 tranformXxx 函数

  • createStructuralDirectiveTransform()v-if 指令

  • processFor() 处理 v-for 指令入口

  • processCodegen()v-if 用来生成 codegenNode 的函数

  • parseForExpression()v-for="item in items" 表达式解析成 ForParseResult{source, value, key, index} 类型 AST 。

  • createAliasExpression()value, key, index 创建 SIMPLE_EXPRESSION 类型 结构。

  • createForLoopParams() 创建 _renderList 函数回调的参数 [value, key, index] ,如果没有使用默认变量: ___ ,如: (_, __, index)

其中 parseForExpression() 函数是解析 v-for 表达式的核心函数,里面使用了三个 正则,用来匹配指令表达式:

  1. const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/

    http://qiniu.ii6g.com/img/20201210155617.png

    匹配 v-for="item in items" 中的值部分

    1
    2
    3
    4
    5
    6
    7
    8
    
    const re = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
    const log = (params) => console.log(params.map((p, i) => `${i}, ${p}`).join(`\n`))
    log.title = console.log
    
    log.title(`>>> 匹配 item in items`)
    log("item in items".match(re))
    log.title(`>>> 匹配 (item, key) in items`)
    log("( item, key ) in items".match(re))
    
    >>> 匹配 item in items
    0, item in items
    1, item
    2, items
    >>> 匹配 (item, key) in items
    0, ( item, key ) in items
    1, ( item, key )
    2, items
    undefined
    
  2. const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/

    http://qiniu.ii6g.com/img/20201210161642.png

    这个正则表达式用来匹配 (item, key) in items 中的 itemkey

    1
    2
    3
    4
    5
    6
    7
    8
    
    const re = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ 
    const log = (params) => console.log(params.map((p, i) => `${i}, ${p}`).join(`\n`))
    log.title = console.log
    
    log.title(`>>> 匹配 'item, key, index' 中的 key 和 index`)
    log("item, key, index".match(re))
    log.title(`>>> 匹配 "item, key" 中的 key`)
    log("item, key".match(re))
    
    >>> 匹配 'item, key, index' 中的 key 和 index
    0, , key, index
    1,  key
    2,  index
    >>> 匹配 "item, key" 中的 key
    0, , key
    1,  key
    2, undefined
    undefined
    
  3. const stripParensRE = /^\(|\)$/g 这个用来匹配 (item, key, index) 前后括号

parseForExpression() 核心实现:

  1. source 数据源, forAliasRE 匹配后的 RHS

    1
    2
    3
    4
    5
    6
    7
    
    source: {
     type: 4, // SIMPLE_EXPRESSION
     loc: { source: 'obj', start: [Object], end: [Object] },
     isConstant: false,
     content: 'obj',
     isStatic: false
    }
  2. value 的取值,在 AST 中对应 valueAlias

    valueContent = valueContent.replace(forIteratorRE, '').trim()

    通过匹配 key, index 的正则,反向替换得到 value

    1
    2
    3
    4
    
    const re = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
    console.log(`item, key, index`.replace(re, '').trim())
    console.log(`>>> 支持解构`)
    console.log(`[ id, value ], key, index`.replace(re, '').trim())
    
    item
    >>> 支持解构
    [ id, value ]
    undefined
    

    解析后的结构:

    1
    2
    3
    4
    5
    6
    7
    
    valueAlias: {
        type: 4, // SIMPLE_EXPRESSION
        loc: { source: 'value', start: [Object], end: [Object] },
        isConstant: false,
        content: 'value',
        isStatic: false
    }
  3. key 取值处理,在 AST 中对应 keyAlias

    1
    2
    3
    4
    5
    6
    7
    
     keyAlias: {
         type: 4,
         loc: { source: 'key', start: [Object], end: [Object] },
         isConstant: false,
         content: 'key',
         isStatic: false
     }
    
  4. index 取值处理,在 AST中对应 objectIndexAlias

    1
    2
    3
    4
    5
    6
    7
    
     objectIndexAlias: {
         type: 4,
         loc: { source: 'index', start: [Object], end: [Object] },
         isConstant: false,
         content: 'index',
         isStatic: false
     }
    

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const { ast } = baseCompile(`<span v-for="(value, key, index) in obj" />`)
const { source, valueAlias, keyAlias, objectIndexAlias, type } = ast.codegenNode
console.log(`type: ${type}`)
console.log(`>>> 数据源`)
console.log(source)
console.log(`>>> value`)
console.log(valueAlias)
console.log(`>>> key`)
console.log(keyAlias)
console.log(`>>> index`)
console.log(objectIndexAlias)
console.log(`>>> _renderList(obj, (value, key, index) => {...}) 第二个参数`)
console.log(ast.codegenNode.codegenNode.children.arguments[1])
type: 11
>>> 数据源
{
  type: 4,
  loc: {
    source: 'obj',
    start: { column: 37, line: 1, offset: 36 },
    end: { column: 40, line: 1, offset: 39 }
  },
  isConstant: false,
  content: 'obj',
  isStatic: false
}
>>> value
{
  type: 4,
  loc: {
    source: 'value',
    start: { column: 15, line: 1, offset: 14 },
    end: { column: 20, line: 1, offset: 19 }
  },
  isConstant: false,
  content: 'value',
  isStatic: false
}
>>> key
{
  type: 4,
  loc: {
    source: 'key',
    start: { column: 22, line: 1, offset: 21 },
    end: { column: 25, line: 1, offset: 24 }
  },
  isConstant: false,
  content: 'key',
  isStatic: false
}
>>> index
{
  type: 4,
  loc: {
    source: 'index',
    start: { column: 27, line: 1, offset: 26 },
    end: { column: 32, line: 1, offset: 31 }
  },
  isConstant: false,
  content: 'index',
  isStatic: false
}
>>> _renderList(obj, (value, key, index) => {...}) 第二个参数
{
  type: 18, // JS_FUNCTION_EXPRESSION
  params: [
    {
      type: 4,
      loc: [Object],
      isConstant: false,
      content: 'value',
      isStatic: false
    },
    {
      type: 4,
      loc: [Object],
      isConstant: false,
      content: 'key',
      isStatic: false
    },
    {
      type: 4,
      loc: [Object],
      isConstant: false,
      content: 'index',
      isStatic: false
    }
  ],
  returns: {
    type: 13,
    tag: '"span"',
    props: undefined,
    children: undefined,
    patchFlag: undefined,
    dynamicProps: undefined,
    directives: undefined,
    isBlock: true,
    disableTracking: false,
    loc: {
      start: [Object],
      end: [Object],
      source: '<span v-for="(value, key, index) in obj" />'
    }
  },
  newline: true,
  isSlot: false,
  loc: {
    source: '',
    start: { line: 1, column: 1, offset: 0 },
    end: { line: 1, column: 1, offset: 0 }
  }
}
undefined

39a20fe add v-for generator

feat(add): v-for generator · gcclll/stb-vue-next@39a20fe

codegen 阶段新增对应的实现: 18,JS_FUNCTION_EXPRESSION

这个主要是用来解析 _renderList(source, (value, key, index) => { ... }) 函数的 第二个参数的,这是个用来 render 列表项的函数。

测试:

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

const { code } = baseCompile('<span v-for="(item) in items" />')
console.log(code)
const _Vue = Vue

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

    return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => {
      return (_openBlock(), _createBlock("span"))
    )), 256 /* UNKEYED_FRAGMENT */))
  }
}
undefined

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

7cb8908 add slot outlet transform

feat(add): v-slot transform · gcclll/stb-vue-next@7cb8908

transform <slot /> 标签。

<slot/> 在 render 函数中是以 _renderSlot($slot, name, props, children) 形式存在。

<slot> 上不允许自定义的指令存在?

相关函数/参数:

  1. transformSlotOutlet() 该阶段的 tranformXxx 函数

  2. SlotOutletProcessResult 类型定义 {slotName, slotProps}

  3. processSlotOutlet(), <slot/> 的处理过程

    首先是解析插槽名称(name 属性),该属性可以是动态(<slot :name="myslot"/>)也 可以是静态的(<slot name="myslot"/>)。

    然后解析出插槽上定义的一些属性(静态),除了 :name 之外插槽上 不允许有其他的 指令类型的属性存在

  4. children 参数

    <slot><div/><p/></slot>

    <slot> 标签内的所有元素(<div/><p/>)会被解析成 _renderSlot 的第四个参数。

测试:

1
2
3
4
5
6
7
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const { ast } = baseCompile(`<slot/>`)
console.log(ast.codegenNode)
{
  type: 1,
  ns: 0,
  tag: 'slot',
  tagType: 2,
  props: [],
  isSelfClosing: true,
  children: [],
  loc: {
    start: { column: 1, line: 1, offset: 0 },
    end: { column: 8, line: 1, offset: 7 },
    source: '<slot/>'
  },
  codegenNode: {
    type: 14, // JS_CALL_EXPRESSION
    loc: { start: [Object], end: [Object], source: '<slot/>' },
    callee: Symbol(renderSlot),
    arguments: [ '$slots', '"default"' ]
  }
}
undefined

更多 codegenNode 结果请 <f12> 打开控制台查看。

ebdb1ed add track slot scopes

feat: add track slot scopes · gcclll/stb-vue-next@ebdb1ed

还不知道干吗的❓

 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
// A NodeTransform that:
// 1. Tracks scope identifiers for scoped slots so that they don't get prefixed
//    by transformExpression. This is only applied in non-browser builds with
//    { prefixIdentifiers: true }.
// 2. Track v-slot depths so that we know a slot is inside another slot.
//    Note the exit callback is executed before buildSlots() on the same node,
//    so only nested slots see positive numbers.
export const trackSlotScopes: NodeTransform = (node, context) => {
  // <component> or <template>
  if (
    node.type === NodeTypes.ELEMENT &&
    (node.tagType === ElementTypes.COMPONENT ||
      node.tagType === ElementTypes.TEMPLATE)
  ) {
    // We are only checking non-empty v-slot here
    // since we only care about slots that introduce scope variables.
    const vSlot = findDir(node, 'slot')
    if (vSlot) {
      const slotProps = vSlot.exp
      if (!__BROWSER__ && context.prefixIdentifiers) {
        slotProps && context.addIdentifiers(slotProps)
      }

      context.scopes.vSlot++
      return () => {
        if (!__BROWSER__ && context.prefixIdentifiers) {
          slotProps && context.removeIdentifiers(slotProps)
        }
        context.scopes.vSlot--
      }
    }
  }
}

36c1f36 (v-slot)build user component as slots

组件是如何当做 slot 处理的

feat(add): user component treat as slot to build · gcclll/stb-vue-next@36c1f36

完整用户组件当 slot 处理流程图: /img/vue3/compiler-core/pcg/pcg-v-slot.svg

<Comp><div/></Comp>

这里 Comp 是组件类型(tagType=1,COMPONENT) 会在 transformElement 中被当做 slot 来处理调用 buildSlots()

更多测试用例(<f12>)打开控制台查看 ->> 。

fbed5cf add component with default slot without <template> transform

feat(add): default slot without template · gcclll/stb-vue-next@fbed5cf

<Comp><div/></Comp>

这种情况,用户组件上既没有 v-slot 孩子节点里面也没有 <template v-slot> 最后 的处理是 <Comp></Comp> 里面的所有 children 被当做默认插槽处理。

该示例符合:

  1. 组件上没有 v-slot 指令

  2. 孩子节点里面没有 <template v-slot:name="slotProps">

因此,这里的处理是将 <div/>comp.children 全部作为默认的 slot:default 来处理。

处理流程: <Comp> -> transformElement -> isComponent -> buildSlots() -> {slots, hasDynamicSlots} -> vnodeChildren -> {type: 13,VNODE_CALL, children: vnodeChildren, ... }

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

const { ast } = baseCompile(`<Comp><div/></Comp>`)
console.log(ast.codegenNode)
{
  type: 13,
  tag: '_component_Comp',
  props: undefined,
  children: {
    type: 15,
    loc: { start: [Object], end: [Object], source: '<Comp><div/></Comp>' },
    properties: [ [Object], [Object] ]
  },
  patchFlag: undefined,
  dynamicProps: undefined,
  directives: undefined,
  isBlock: true,
  disableTracking: false,
  loc: {
    start: { column: 1, line: 1, offset: 0 },
    end: { column: 20, line: 1, offset: 19 },
    source: '<Comp><div/></Comp>'
  }
}
undefined

另外在 transformElement() 函数中通过 isComponent = node.tagType === ElementTypes.COMPONENT 检测是不是用户组件,如果是继续解析该组件类型(resolveComponentType())。

这里最终得到的结果是: vnodeTag = _component_Comp 作为标签名,也是该 <Comp/> 组件在 Vue 实例过程中的存在的标签名(组件名称)。

8bed175 add component with default slot without <template> generator

feat(add): user component resolver generator · gcclll/stb-vue-next@8bed175

因为在 transform 阶段 transformElement 过程中,检测到 <Comp> 是个用户组件,所 以将其增加到了 context.components.add('Comp') 中了,在 generator 阶段会去检测 这个 components 用来解析组件得到组件的引用:

_component_Comp = _resolveComponent("Comp")

新增的代码主要由两部分:

  1. 新增 genAssets() 函数处理 context.components

    处理之后的结果就是增加 _component_Comp = _resolveComponent("Comp")

  2. generate() 中增加 ast.components 检测,如果有内容调用 genAssets() 解 析

 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
// generate() 中增加
 if (ast.components.length) {
    genAssets(ast.components, 'component', context)
    if (ast.directives.length || ast.temps > 0) {
      newline()
    }
  }

// 新增
function genAssets(
  assets: string[],
  type: 'component' | 'directive',
  { helper, push, newline }: CodegenContext
) {
  const resolver = helper(
    type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE
  )

  for (let i = 0; i < assets.length; i++) {
    const id = assets[i]
    push(
      `const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})`
    )
    if (i < assets.length - 1) {
      newline()
    }
  }
}

测试:

1
2
3
4
5
6
7
8

const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')

const { code } = baseCompile(`<Comp><div/></Comp>`, { hoistStatic: true })
console.log(code)
const _Vue = Vue
const { createVNode: _createVNode } = _Vue

const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */)

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

    const _component_Comp = _resolveComponent("Comp")

    return (_openBlock(), _createBlock(_component_Comp, null, {
      default: _withCtx(() => [
        _hoisted_1
      ]),
      _: 1
    }))
  }
}
undefined

缺少 1 /* STABLE */ 注释: fix: slot flag text · gcclll/stb-vue-next@08a6fca

注意 _createBlock() 的第三个参数变成了一个对象(children) 对象里面包含了两个 属性: default_ 分别代表了默认插槽下的孩子节点,和该插槽标识(1-STABLE,2-FORWARDED,3-DYNAMIC)

9f58154 fix: 文本节点没有合并(transformText)

fix: adjacent text node need merge · gcclll/stb-vue-next@9f58154

对于 `<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</Comp>` 用例得到的结果非预期。

vue-next 正确结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const {
  baseCompile
} = require(process.env.PWD + '/../../static/vue/vue.js')
 
const { ast } = baseCompile(`<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</Comp>`)
const returns = ast.codegenNode.children.properties[0].value.returns
console.log(returns)
console.log(`>>> 解析后的 {{ foo }} {{ bar }} 应该合并`)
console.log(returns[0].content)
console.log(`>>> 下面正是合并之后的两个插值`)
console.log(returns[0].content.children)
You are running a development build of Vue.
Make sure to use the production build (*.prod.js) when deploying for production.
[
  {
    type: 12,
    content: { type: 8, loc: [Object], children: [Array] },
    loc: { start: [Object], end: [Object], source: '{{ foo }}' },
    codegenNode: {
      type: 14,
      loc: [Object],
      callee: Symbol(createTextVNode),
      arguments: [Array]
    }
  }
]
>>> 解析后的 {{ foo }} {{ bar }} 应该合并
{
  type: 8,
  loc: {
    start: { column: 24, line: 1, offset: 23 },
    end: { column: 33, line: 1, offset: 32 },
    source: '{{ foo }}'
  },
  children: [
    { type: 5, content: [Object], loc: [Object] },
    ' + ',
    { type: 5, content: [Object], loc: [Object] }
  ]
}
>>> 下面正是合并之后的两个插值
[
  {
    type: 5,
    content: {
      type: 4,
      isStatic: false,
      constType: 0,
      content: 'foo',
      loc: [Object]
    },
    loc: { start: [Object], end: [Object], source: '{{ foo }}' }
  },
  ' + ',
  {
    type: 5,
    content: {
      type: 4,
      isStatic: false,
      constType: 0,
      content: 'bar',
      loc: [Object]
    },
    loc: { start: [Object], end: [Object], source: '{{ bar }}' }
  }
]
undefined

看下现阶段 stb-vue-next 输出结果:

1
2
3
4
5
6
7
8
9

const {
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const { ast } = baseCompile(`<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</Comp>`)
const returns = ast.codegenNode.children.properties[0].value.returns
console.log(returns[0].content)
console.log(returns[1].content)
{
  type: 5,
  content: {
    type: 4,
    isStatic: false,
    isConstant: false,
    content: 'foo',
    loc: { start: [Object], end: [Object], source: 'foo' }
  },
  loc: {
    start: { column: 24, line: 1, offset: 23 },
    end: { column: 33, line: 1, offset: 32 },
    source: '{{ foo }}'
  }
}
{
  type: 5,
  content: {
    type: 4,
    isStatic: false,
    isConstant: false,
    content: 'bar',
    loc: { start: [Object], end: [Object], source: 'bar' }
  },
  loc: {
    start: { column: 33, line: 1, offset: 32 },
    end: { column: 42, line: 1, offset: 41 },
    source: '{{ bar }}'
  }
}
undefined

stb-vue-next 明显 returns 里面包含了两个元素分别是: {{ foo }}{{ bar }}

这是因为在 transformText 里面没有进行文本(插值也算)合并。

0773374 add component nested slots scoping

fix: patchFlag should |= but != · gcclll/stb-vue-next@0773374

插槽嵌套使用。

1
2
3
4
5
6
7
8
`<Comp>
    <template #default="{ foo }">
        <Inner v-slot="{ bar }">
        {{ foo }}{{ bar }}{{ baz }}
        </Inner>
        {{ foo }}{{ bar }}{{ baz }}
    </template>
</Comp>`

插槽嵌套使用时的解析是先里后外,因为在 transform 阶段 ast 阶段转换之后,会进行回 溯,回溯过程是相反的。

如:

ast 结构 transform:

Comp.children -> [template] -> template.children -> [Inner, foo, bar, baz] -> Inner.children -> [foo, bar, baz]

ast回溯:

Inner.children -> Inner -> template.children -> template -> Comp.children -> Comp

所以首先执行 transformElement() 的是 Inner 所以它会先进入 buildSlots() 构 建插槽结构,完了之后是 <template> 最后是 <Comp>


render 推导过程:

  1. <Inner v-slot="{bar}">{{foo}}{{bar}}{{baz}}</Inner>

    是在用户组件上应用了 v-slot 且是默认插槽,因此它的所有孩子节点都会成为默认 插槽一部分。

    结果:

    1
    2
    3
    4
    5
    6
    
    _createVNode(_component_Inner, null, {
      default: _withCtx(({ bar }) => [
        _createTextVNode(_toDisplayString(foo) + _toDisplayString(bar) + _toDisplayString(baz), 1 /* TEXT */)
      ]),
      _: 2, /* DYNAMIC */
    }, 1024 /* DYNAMIC_SLOTS */)
    

    这里使用的是 _createVNode 因为 Inner 非唯一的孩子节点。

  2. <template #default="{foo}">...</template>

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    (_openBlock(), _createBlock(_component_Comp, null {
      default: _withCtx(({ foo }) => [
        _createVNode(_component_Inner, null, {
           default: _withCtx(({ bar }) => [
              _createTextVNode(_toDisplayString(foo) + _toDisplayString(bar) + _toDisplayString(baz), 1 /* TEXT */)
            ]),
           _: 2 /* DYNAMIC */
        }, 1024 /* DYNAMIC_SLOTS */),
        _createTextVNode(_toDisplayString(foo) + _toDisplayString(bar) + _toDisplayString(baz), 1 /* TEXT */)
      ]),
      _: 1 /* STABLE */
    }))
    

    这里有个判断 Inner 为动态 slot 的关键点: context.scopes.vSlot > 0 而这个 值是在收集 transformXxx 阶段递增 +1 而后回溯过程中 -1 的。

    在收集阶段 <template> 收集到 trackSlotScopes() 函数此时 context.scopes.vSlot = 1 然后递归 children执行到 Inner 收集阶段的时候 context.scopes.vSlot = 2 直到递归结束。

    开始回溯,先是在 Inner 上应用 transformElement 直到 Inner 回溯到执行 trackSlotScopes() 应为它也有 v-slot 指令,所以 Inner 能收集到这个函数, 它回溯结束执行 trackSlotScopes() 随之 context.scopes.vSlot-- 所以此时,在 回溯 Inner 结束之后在开始回溯 <template> 之前 context.scopes.vSlot = 1

    这就是 Inner 为什么没有动态属性名但是依旧会判断为动态插槽的原理。

    一句话:如果在 <template v-slot> 里面嵌套另一个 v-slot 那个这个不管有没有 动态属性名都会被当做动态插槽来处理。

c1ace74 add component with v-slot inside v-for

1
2
3
`<div v-for="i in list">
  <Comp v-slot="bar">foo</Comp>
</div>`

let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0;

根据这个判断决定 v-for 里面的 v-slot 为动态插槽。

cfef20e add named slot with v-if

feat(add): slot with v-if · gcclll/stb-vue-next@cfef20e

1
2
3
`<Comp>
  <template #one v-if="ok">hello</template>
</Comp>`

测试:

1
2
3
4
5
6
7
8
9
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')

const { code } = baseCompile(`<Comp>
  <template #one v-if="ok">hello</template>
</Comp>`)
console.log(code)
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createTextVNode : _createTextVNode, resolveComponent : _resolveComponent, withCtx : _withCtx, createSlots : _createSlots, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    const _component_Comp = _resolveComponent("Comp")

    return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [
      ok
        ? {
            name: "one",
            fn: _withCtx(() => [
              _createTextVNode("hello")
            ])
          }
        : undefined
    ]), 1024 /* DYNAMIC_SLOTS */))
  }
}
undefined

动态 slots 处理之后: _createSlots({ /* 静态 slots */, [ /* 动态 slots 列表 */ ] })

看下面的运行时的 createSlots(slots, dynamicSlots) 其实就是讲两者合并在一起了 v-if 是个对象 { fn, name } v-for 是该对象的数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export function createSlots(
  slots: Record<string, Slot>,
  dynamicSlots: (
    | CompiledSlotDescriptor
    | CompiledSlotDescriptor[]
    | undefined)[]
): Record<string, Slot> {
  for (let i = 0; i < dynamicSlots.length; i++) {
    const slot = dynamicSlots[i]
    // array of dynamic slot generated by <template v-for="..." #[...]>
    if (isArray(slot)) {
      for (let j = 0; j < slot.length; j++) {
        slots[slot[j].name] = slot[j].fn
      }
    } else if (slot) {
      // conditional single slot generated by <template v-if="..." #foo>
      slots[slot.name] = slot.fn
    }
  }
  return slots
}

拓展: v-else, v-else-if feat(add): v-else/v-else-if with v-slot · gcclll/stb-vue-next@e48d46a

与普通的 v-else/v-else-if 处理机制一样,首先是要找到他们兄弟节点前面的 v-if 节点, 然后将该节点挂接到 v-if 节点后面。

核心代码:

 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
// v-else/if on slot
let j = i
let prev
while (j--) {
    // 找到相邻的 v-if
    prev = children[j]
    // 往回找第一个非注释的节点
    if (prev.type !== NodeTypes.COMMENT) {
        break
    }
}

// 如果该节点是 v-if 合法,否则不合法使用情况
if (prev && isTemplateNode(prev) && findDir(prev, 'if')) {
    // remove node
    children.splice(i, 1)
    i--
    __TEST__ && assert(dynamicSlots.length > 0)
    // attach this slot to previous conditional
    let conditional = dynamicSlots[
        dynamicSlots.length - 1
    ] as ConditionalExpression

  // 这目的是找到 ?: 表达式最后的那个节点
    while (
        conditional.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
    ) {
        conditional = conditional.alternate
    }

  // 将当前的 v-else/v-else-if 挂到最后那个节点表达式位置
  // vElse.exp 检测是 v-else-if 还是 v-else(没有值exp)
    conditional.alternate = vElse.exp
        ? createConditionalExpression(
            vElse.exp,
            buildDynamicSlot(slotName, slotFunction),
            defaultFallback
        )
        : buildDynamicSlot(slotName, slotFunction)
} else {
    context.onError(
        createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc)
    )
}

c1ace74 add v-for on v-slot component

feat(add): v-slot inside v-for · gcclll/stb-vue-next@c1ace74

v-for 的处理和 v-if 原理是一样的,最后返回的都是 {name, fn} 类型,只不过 v-for 返回的是这个类型的数组。

v-for _renderList 和 slot 的结合:

 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
_renderList(list, (i) => {
  return {
    name: name,
    fn: _withCtx(() => [
      _createTextVNode(_toDisplayString(name), 1 /* TEXT */)
    ])
  }
})

// 等于是: [ { name, fn }, { name1, fn1 }]
// 然后 _renderSlots ->
_renderSlots({ _: 2 /* DYNAMIC */ }, [
  _renderList(list, ...)
])

// -> _createBlock
_createBlock(_component_Comp, null, _renderSlots({
  /* 静态,最终动态插槽会合并到该对象来 */
}, [
  /* ... 动态列表 */
]))

// 对比无动态插槽情况:
_createBlock(_component_Comp, null {
  default: _withCtx(() => [
    _createTextVNode(_toDisplayString(foo), 1 /* TEXT */)
  ]),
  _: 1 /* STABLE */
})

测试:

1
2
3
4
5
6
7
8
9
const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')
 
const { code } = baseCompile(`<Comp>
        <template v-for="name in list" #[name]>{{ name }}</template>
      </Comp>`)
console.log(code)
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { toDisplayString : _toDisplayString, createTextVNode : _createTextVNode, resolveComponent : _resolveComponent, withCtx : _withCtx, renderList : _renderList, createSlots : _createSlots, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

    const _component_Comp = _resolveComponent("Comp")

    return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [
      _renderList(list, (name) => {
        return {
          name: name,
          fn: _withCtx(() => [
            _createTextVNode(_toDisplayString(name), 1 /* TEXT */)
          ])
        }
      ))
    ]), 1024 /* DYNAMIC_SLOTS */))
  }
}
undefined

无非就是不同类型元素、组件 render 方式的组合。

小结:v-slot 几种用法

  1. <Comp><div/></Comp> , 用户组件上的默认插槽

    1
    2
    3
    4
    5
    6
    7
    
    const _hoisted_1 = /*# __PURE__ */_createVNode('div', null, null, -1 /* HOISTED */)
    (_openBlock(), _createBlock(_component_Comp, null, {
      default: _withCtx(() => [
        _hoisted_1
      ]),
      _: 1 /* STABLE */
    }))
    
  2. <Comp v-slot="slotProps"><div/></Comp>, 用户组件上带 slotProps 的默认插槽

    1
    2
    3
    4
    5
    6
    7
    
    const _hoisted_1 = /*# __PURE__ */_createVNode('div', null, null, -1 /* HOISTED */)
    (_openBlock(), _createBlock(_component_Comp, null, {
      default: _withCtx((slotProps) => [
        _hoisted_1
      ]),
      _: 1 /* STABLE */
    }))
    
  3. <Comp v-slot:named="slotProps">, 用户组件上具名插槽

    1
    2
    3
    4
    5
    6
    
    const _hoisted_1 = /*# __PURE__ */_createVNode('div', null, null, -1 /* HOISTED */)
    (_openBlock(), _createBlock(_component_Comp, null, {
      named: _withCtx((slotProps) => [
        _hoisted_1
      ])
    }))
    
  4. <Comp v-slot:[named]="slotProps">, 用户组件上动态具名插槽

    1
    2
    3
    4
    5
    6
    
    const _hoisted_1 = /*# __PURE__ */_createVNode('div', null, null, -1 /* HOISTED */)
    (_openBlock(), _createBlock(_component_Comp, null, {
      [named]: _withCtx((slotProps) => [
        _hoisted_1
      ])
    }))
    
  5. <template> 默认插槽

    <Comp><template v-slot="slotProps"><div/></template></Comp>

    <template> 上的默认插槽,这个时候 <Comp> 不能在使用 v-slot ,下同

  6. <template> 具名插槽

    <Comp><template v-slot:named="slotProps"></Comp>

    1
    2
    3
    4
    
    return (_openBlock(), _createBlock(_component_Comp, null, {
      named: _withCtx((slotProps) => []),
      _: 1 /* STABLE */
    }))
    
  7. <template> 动态具名插槽

    <Comp><template v-slot:[named]="slotProps"></Comp>

    1
    2
    3
    4
    
    return (_openBlock(), _createBlock(_component_Comp, null, {
      [named]: _withCtx((slotProps) => []),
      _: 1 /* STABLE */
    }))
    
  8. <template> 具名插槽,其余非 template 元素当做默认插槽处理

    <Comp><template v-slot:named="slotProps"><div/></template><div :id="defaultSlotId"/></Comp>

    <template> 上的具名插槽 + 默认插槽,在组件内的非 <template> 元素(即: <div/>)都会被动作默认插槽来处理

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    const _hoisted_1 = /*# __PURE__ */_createVNode("div", null, null, -1 /* HOISTED */)
    _createBlock(_component_Comp, null, {
      named: _withCtx((slotProps) => [
        _hoisted_1
      ]),
      default: _withCtx(() => [
        _createVNode('div', {
          id: defaultSlotId
        }, null, 8 /* PROPS */, ["id"])
      ])
      _: 1 /* STABLE */
    })
    
  9. v-slot + v-if 插槽使用

    <Comp><template v-if="ok" #named="slotProps">{{ bar }}</template></Comp>

    配合 v-if 使用的 slot template, 最后解析成 : ok ? { name: 'named', fn : _withCtx(() => [ _createTextVNode(_toDisplayString(bar)) ]) } : undefined

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    return (_openBlock(), _createBlock(_component_Comp, null, _renderSlots(
      { _: 2 /* DYNAMIC */ }, [
        ok ?
         {
           name: 'named',
           fn: _withCtx((slotProps) => [
             _createTextVNode(_toDisplayString(bar), 1 /* TEXT */)
           ])
         } : undefined
      ]
    )))
    
  10. v-slot + v-for 插槽上使用

    <Comp><template v-for="i in list" #named="{ prop }">{{ bar }}</template></Comp>

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    return (_openBlock(), _createBlock(_component_Comp, null, _renderSlots(
      { _: 2 /* DYNAMIC */ },
      _renderList(list, (i) => {
        return {
          name: 'named',
          fn: _withCtx(({ prop }) => [
            _createTextVNode(_toDisplayString(bar), 1 /* TEXT */)
          ])
        }
      })
    )))
    

插槽按状态分为:

  1. 静态插槽,动态插槽除外的插槽

  2. 动态插槽,有 v-if/v-for/v-else[-if] 指令或 v-slot:[named] 或作为 v-for 节点的孩子节点都视为动态插槽

update vue-next merge into stb-vue-next[2020-12-11 16:54:06]

更新 vue-next 合并到 stb-vue-next。

  1. ast.ts 更新

    • SimpleExpressionNode : 去掉 isConstant 属性,增加 constType

    • 去掉了 StaticType 增加 ConstantType

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      /**
      * Static types have several levels.
      * Higher levels implies lower levels. e.g. a node that can be stringified
      * can always be hoisted and skipped for patch.
      */
      export const enum ConstantTypes {
      NOT_CONSTANT = 0,
      CAN_SKIP_PATCH,
      CAN_HOIST,
      CAN_STRINGIFY
      }
      
  2. codegen.ts 更新

    fix: merge vue-next codegen.ts · gcclll/stb-vue-next@521b879

  3. parse.ts 更新

    ast 结构中的 isConstant 改成 ConstantTypes 类型值

    fix: merge vue-next parse.ts isConstant -> constType · gcclll/stb-vue-next@fdbdc4f

  4. transform.ts 更新

    fix: merge vue-next transform.ts · gcclll/stb-vue-next@22a8f0b

    • add filename

    • add isTS, inline

  5. transformElement.ts

    fix: merge vue-next transformElement.ts · gcclll/stb-vue-next@1d85990

  6. transformSlotOutlet.ts

    fix: merge vue-next transformSlotOutlet.ts · gcclll/stb-vue-next@58e87f3 修改 <slot></slot> 属性解析逻辑。

6efbc86 add expression transform(node-env)

feat: transformExpression -> processExpression · gcclll/stb-vue-next@6efbc86

transformExpression 处理的是复杂表达式情况,且需要在非浏览器环境,因为它需要依赖 一些 JavaScript 解析器来协助完成。

需要处理的两种类型:

  1. INTERPOLATION 插值类型

  2. ELEMENT 类型且没有 v-for 指令的标签,对于指令需要处理的有 arg, exp 如果 参数是动态的情况

两者最终都是调用 processExpression(node, context, asPrarms, asRawStatement) 这 个函数,这个函数有点复杂,需要慢慢消化🐕 🐕 🐕

TODOs(ssr render, node env, setup meta)

在此之前都是基于浏览器去完成和测试的,但是由于后面 compiler-dom 有些测试同样需要 node 环境支持,因此这里必须先完成所有浏览器的支持,这样 prefixIdentifierscacheHandlers 也将支持。

add generate imports and module mode: feat: gen imports · gcclll/stb-vue-next@ea7f16b

TODO ssr template literal: feat(add): gen template literal · gcclll/stb-vue-next@356bc93

TODO ssr if generate: feat(add): ssr gen if statement · gcclll/stb-vue-next@1c424be

TODO ssr assigment expression generate: 生成赋值表达式 feat(add): ssr gen assignment expression · gcclll/stb-vue-next@1f99a11

TODO ssr generate sequence exprssion: feat(add): ssr gen sequence expression · gcclll/stb-vue-next@81aa0b0

TODO ssr generate return statement: feat(add): ssr gen return statement · gcclll/stb-vue-next@c85406c

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

TODO setup mode v-model on ref: feat(add): setup mode v-model on ref · gcclll/stb-vue-next@20bb40d

TODO process expression in vIf.ts feat(add): v-if process expression · gcclll/stb-vue-next@56c49c7

TODO setup mode ref in transformElement feat(add): transform ref in setup mode in transformElement · gcclll/stb-vue-next@54f467f

non-browser testing(非浏览器环境测试)

本章节测试都是基于 node 环境进行测试的,测试代码位于各个 package 目录下的 __nests__ (node tests 缩写)里面。

由于测试输出结果较多,因此采用控制台形式输出,请 <f12> 打开控制台查看

jest 跑🏃用例

fix: jest errors · gcclll/stb-vue-next@2085dd6

@vue/runtime-dom (guessing 'runtimeDom')
created packages/vue/dist/vue.global.js in 5.1s
 PASS  packages/compiler-core/__tests__/transforms/transformElement.spec.ts (9.278 s)
 PASS  packages/compiler-core/__tests__/transforms/vModel.spec.ts
 PASS  packages/compiler-core/__tests__/transforms/vFor.spec.ts
 PASS  packages/compiler-core/__tests__/parse.spec.ts
 PASS  packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts
 PASS  packages/compiler-core/__tests__/codegen.spec.ts
 PASS  packages/compiler-core/__tests__/transforms/vIf.spec.ts
 PASS  packages/compiler-core/__tests__/transforms/vSlot.spec.ts
 PASS  packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts
 PASS  packages/compiler-core/__tests__/transforms/vOn.spec.ts
 PASS  packages/compiler-core/__tests__/transform.spec.ts
 PASS  packages/compiler-core/__tests__/compile.spec.ts
 PASS  packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts
 PASS  packages/compiler-core/__tests__/transforms/transformText.spec.ts
 PASS  packages/compiler-core/__tests__/transforms/vOnce.spec.ts
 PASS  packages/compiler-core/__tests__/scopeId.spec.ts
 PASS  packages/compiler-core/__tests__/transforms/vBind.spec.ts
 PASS  packages/compiler-core/__tests__/transforms/noopDirectiveTransform.spec.ts
 PASS  packages/compiler-core/__tests__/utils.spec.ts

Test Suites: 19 passed, 19 total
Tests:       480 passed, 480 total
Snapshots:   205 passed, 205 total
Time:        17.699 s

全部通过测试,期间出现两个小问题:

  1. transformElement 里面解析动态属性(vnodeDynamicProps)的时候会将这些属性组成 字符串数组,这里少了个逗号

    http://qiniu.ii6g.com/img/20201218162357.png

  2. genFunctionExpression() 里面将函数 } 错写成了 )

    http://qiniu.ii6g.com/img/20201218162509.png

综合测试

 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

const {
  baseParse,
  baseCompile
} = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js')

const log = (...args) => console.log.apply(console, args)
const c = tpl => baseCompile(tpl, {
  hoistStatic: true
}).code
let tpl = ``, code = ``
tpl = `
<div :id="status" class="status" :style="{ color: 'red' }">
  <button v-if="opened" @click="opened = !opened">关闭</button>
  <button v-else-if="invalid" v-on.enter="{ clicked: toggleValid }">有效</button>
  <button v-else @click.prevent="() => opened = true">打开</button>
  <p :name="vbind">v-bind 缩写</p>
  <p class="vbind-no-arg" v-bind="{ name: 'vbind' }">无参数的 v-bind</p>
</div>
<div id="list" :class="list" style="color:red;">
  <ul>
    <li v-for="(value, key, index) in list">{{ value }}</li>
  </ul>
</div>
<div id="once" :class="once">{{ once }}</div>
<div id="user-comp" :class="user-comp">
  <!-- 我是注释:用户组件 -->
  <List>{{ "我是 <List/> 的默认插槽" }}</List>
  <List v-slot="{ prop }"><p class="title">我是带 slotProps({{ prop }}) 的默认插槽</p></List>
  <List v-slot:title="{ prop }">"我是带名字{{ prop }}的插槽"</List>
  <List>
    <template v-slot:one="slotProps"><p>{{ slotProps.title }}</p></template>
    <p>我是默认插槽的一部分</p>
    <p>我也是默认插槽的一部分</p>
    <p>我也是。。。。。。。</p>
    <p>我们都是。。。。。。</p>
  </List>
  <List>
    <template>
      <p>我是默认插槽</p>
    </template>
    <template v-slot:one="{ prop }">
      <p>我是名字为 one 的具名插槽插槽({{ prop }})</p>
    </template>
  </List>
  <List v-if="ok" v-slot:one="slotProps"><p>我是带 v-if 指令的且 v-slot 引用在组件上的具名 "one" 插槽,标题属性: {{ slotProps.title }}</p></List>
  <List>
    <template><!-- 我是默认插槽,给你们注释用的!!!!!! --></template>
    <template v-if="ok" v-slot:one="{ prop }">
      <p>带 v-if + v-slot + template 的动态插槽,我应该要用到 _createSlots(staticSlots, dynamicSLots) 函数</p>
    </template>
    <template v-for="(value, key, index) in list" v-slot:two="slotProps">
      <p>带 v-for + v-slot + template 的动态插槽,我应该要用到 _createSlots(staticSlots, dynamicSLots) 函数</p>
    </template>
  </List>
</div>
`
code = c(tpl)
log(`>>> 复杂应用场景`)
log(code)
log(`\n> 注释\n`,
    `1. v-on 表达式为简单表达式时会被处理成一个箭头函数\n`,
    `2. v-on 如果没有参数时,会触发该组件上的所有属性合并(_mergeProps(...))\n`,
    `3. v-on 表达式为一个函数式,不需要额外处理\n`,
    `4. v-for 调用 _renderList(list, fn) 渲染列表,list 列表数据来源,fn 列表项渲染函数\n`,
    `5. v-slot 应用在用户组件上时,里面就不能在使用 v-slot\n`,
    `6. 只要在 template 上应用了 v-if/v-for 或者将用户组件放在 v-for 下面这些插槽都会被当做动态插槽处理\n`)
>>> 复杂应用场景
const _Vue = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode } = _Vue

const _hoisted_1 = { class: "title" }
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "我是默认插槽的一部分", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createVNode("p", null, "我也是默认插槽的一部分", -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createVNode("p", null, "我也是。。。。。。。", -1 /* HOISTED */)
const _hoisted_5 = /*#__PURE__*/_createVNode("p", null, "我们都是。。。。。。", -1 /* HOISTED */)
const _hoisted_6 = /*#__PURE__*/_createVNode("template", null, [
  /*#__PURE__*/_createVNode("p", null, "我是默认插槽")
], -1 /* HOISTED */)
const _hoisted_7 = /*#__PURE__*/_createVNode("template", null, [
  /*#__PURE__*/_createCommentVNode(" 我是默认插槽,给你们注释用的!!!!!! ")
], -1 /* HOISTED */)
const _hoisted_8 = /*#__PURE__*/_createVNode("p", null, "带 v-if + v-slot + template 的动态插槽,我应该要用到 _createSlots(staticSlots, dynamicSLots) 函数", -1 /* HOISTED */)
const _hoisted_9 = /*#__PURE__*/_createVNode("p", null, "带 v-for + v-slot + template 的动态插槽,我应该要用到 _createSlots(staticSlots, dynamicSLots) 函数", -1 /* HOISTED */)

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock, createCommentVNode : _createCommentVNode, toHandlers : _toHandlers, mergeProps : _mergeProps, renderList : _renderList, Fragment : _Fragment, toDisplayString : _toDisplayString, createTextVNode : _createTextVNode, resolveComponent : _resolveComponent, withCtx : _withCtx, createSlots : _createSlots } = _Vue

    const _component_List = _resolveComponent("List")

    return (_openBlock(), _createBlock(_Fragment, null, [
      _createVNode("div", {
        id: status,
        class: "status",
        style: { color: 'red' }
      }, [
        opened
          ? (_openBlock(), _createBlock("button", {
              key: 0,
              onClick: $event => (opened = !opened)
            }, "关闭", 8 /* PROPS */, ["onClick"]))
          : invalid
            ? (_openBlock(), _createBlock("button", _mergeProps({ key: 1 }, _toHandlers({ clicked: toggleValid })), "有效", 16 /* FULL_PROPS */))
            : (_openBlock(), _createBlock("button", {
                key: 2,
                onClick: () => opened = true
              }, "打开", 8 /* PROPS */, ["onClick"])),
        _createVNode("p", { name: vbind }, "v-bind 缩写", 8 /* PROPS */, ["name"]),
        _createVNode("p", _mergeProps({ class: "vbind-no-arg" }, { name: 'vbind' }), "无参数的 v-bind", 16 /* FULL_PROPS */)
      ], 12 /* STYLE, PROPS */, ["id"]),
      _createVNode("div", {
        id: "list",
        class: list,
        style: "color:red;"
      }, [
        _createVNode("ul", null, [
          (_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (value, key, index) => {
            return (_openBlock(), _createBlock("li", null, _toDisplayString(value), 1 /* TEXT */))
          )), 256 /* UNKEYED_FRAGMENT */))
        ])
      ], 2 /* CLASS */),
      _createVNode("div", {
        id: "once",
        class: once
      }, _toDisplayString(once), 3 /* TEXT, CLASS */),
      _createVNode("div", {
        id: "user-comp",
        class: user-comp
      }, [
        _createCommentVNode(" 我是注释:用户组件 "),
        _createVNode(_component_List, null, {
          default: _withCtx(() => [
            _createTextVNode(_toDisplayString("我是 <List/> 的默认插槽"), 1 /* TEXT */)
          ]),
          _: 1 /* STABLE */
        }),
        _createVNode(_component_List, null, {
          default: _withCtx(({ prop }) => [
            _createVNode("p", _hoisted_1, "我是带 slotProps(" + _toDisplayString(prop) + ") 的默认插槽", 1 /* TEXT */)
          ]),
          _: 1 /* STABLE */
        }),
        _createVNode(_component_List, null, {
          title: _withCtx(({ prop }) => [
            _createTextVNode("\"我是带名字" + _toDisplayString(prop) + "的插槽\"", 1 /* TEXT */)
          ]),
          _: 1 /* STABLE */
        }),
        _createVNode(_component_List, null, {
          one: _withCtx((slotProps) => [
            _createVNode("p", null, _toDisplayString(slotProps.title), 1 /* TEXT */)
          ]),
          default: _withCtx(() => [
            _hoisted_2,
            _hoisted_3,
            _hoisted_4,
            _hoisted_5
          ]),
          _: 1 /* STABLE */
        }),
        _createVNode(_component_List, null, {
          one: _withCtx(({ prop }) => [
            _createVNode("p", null, "我是名字为 one 的具名插槽插槽(" + _toDisplayString(prop) + ")", 1 /* TEXT */)
          ]),
          default: _withCtx(() => [
            _hoisted_6
          ]),
          _: 1 /* STABLE */
        }),
        ok
          ? (_openBlock(), _createBlock(_component_List, { key: 0 }, {
              one: _withCtx((slotProps) => [
                _createVNode("p", null, "我是带 v-if 指令的且 v-slot 引用在组件上的具名 \"one\" 插槽,标题属性: " + _toDisplayString(slotProps.title), 1 /* TEXT */)
              ]),
              _: 1 /* STABLE */
            }))
          : _createCommentVNode("v-if", true),
        _createVNode(_component_List, null, _createSlots({
          default: _withCtx(() => [
            _hoisted_7
          ]),
          _: 2 /* DYNAMIC */
        }, [
          ok
            ? {
                name: "one",
                fn: _withCtx(({ prop }) => [
                  _hoisted_8
                ])
              }
            : undefined,
          _renderList(list, (value, key, index) => {
            return {
              name: "two",
              fn: _withCtx((slotProps) => [
                _hoisted_9
              ])
            }
          ))
        ]), 1024 /* DYNAMIC_SLOTS */)
      ], 2 /* CLASS */)
    ], 64 /* STABLE_FRAGMENT */))
  }
}

> 注释
 1. v-on 表达式为简单表达式时会被处理成一个箭头函数
 2. v-on 如果没有参数时,会触发该组件上的所有属性合并(_mergeProps(...))
 3. v-on 表达式为一个函数式,不需要额外处理
 4. v-for 调用 _renderList(list, fn) 渲染列表,list 列表数据来源,fn 列表项渲染函数
 5. v-slot 应用在用户组件上时,里面就不能在使用 v-slot
 6. 只要在 template 上应用了 v-if/v-for 或者将用户组件放在 v-for 下面这些插槽都会被当做动态插槽处理

undefined