Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【看的见的思考】compiler-core #27

Open
cuixiaorui opened this issue Aug 24, 2021 · 0 comments
Open

【看的见的思考】compiler-core #27

cuixiaorui opened this issue Aug 24, 2021 · 0 comments

Comments

@cuixiaorui
Copy link
Owner

cuixiaorui commented Aug 24, 2021

看的见的思考

先从compile 看起, compile 里面的 baseCompile 是整个 core 的入口函数

先看看单元测试吧

  test('function mode', () => {
    const { code, map } = compile(source, {
      sourceMap: true,
      filename: `foo.vue`
    })

    expect(code).toMatchSnapshot()
    expect(map!.sources).toEqual([`foo.vue`])
    expect(map!.sourcesContent).toEqual([source])

    const consumer = new SourceMapConsumer(map as RawSourceMap)

    expect(
      consumer.originalPositionFor(getPositionInCode(code, `id`))
    ).toMatchObject(getPositionInCode(source, `id`))

这个测试相当的大,先一点点的看

先看input source 是

  const source = `
<div id="foo" :class="bar.baz">
  {{ world.burn() }}
  <div v-if="ok">yes</div>
  <template v-else>no</template>
  <div v-for="(value, index) in list"><span>{{ value + index }}</span></div>
</div>
`.trim()

这个就是 template 了。 然后第二个参数是对应的 CompilerOptions 先不用管

在去看看输出 output

下面是 code

 const _Vue = Vue
    
    return function render(_ctx, _cache) {
      with (_ctx) {
        const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, renderList: _renderList, createElementVNode: _createElementVNode, normalizeClass: _normalizeClass } = _Vue
    
        return (_openBlock(), _createElementBlock("div", {
          id: "foo",
          class: _normalizeClass(bar.baz)
        }, [
          _createTextVNode(_toDisplayString(world.burn()) + " ", 1 /* TEXT */),
          ok
            ? (_openBlock(), _createElementBlock("div", { key: 0 }, "yes"))
            : (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [
                _createTextVNode("no")
              ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)),
          (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (value, index) => {
            return (_openBlock(), _createElementBlock("div", null, [
              _createElementVNode("span", null, _toDisplayString(value + index), 1 /* TEXT */)
            ]))
          }), 256 /* UNKEYED_FRAGMENT */))
        ], 2 /* CLASS */))
      }
    }

就是生成的 render 函数

那map 是什么?

  {
      version: 3,
      sources: [ 'foo.vue' ],
      names: [],
      mappings: ';;;;;;0BAAA,oBAKM;MALD,EAAE,EAAC,KAAK;MAAE,KAAK,EAApB,gBAAsB,OAAO;;MAA7B,kCACK,YAAY,IAAG,GAClB;MAAW,EAAE;yBAAb,oBAAwB,SAF1B,KAAA,KAEiB,KAAG;yBAClB,oBAA8B,aAHhC,KAAA;YAAA,iBAGmB,IAAE;;yBACnB,oBAA0E,iBAJ5E,YAIgC,IAAI,EAJpC,CAIe,KAAK,EAAE,KAAK;8BAAzB,oBAA0E;UAAtC,oBAAgC,+BAAvB,aAAa',
      sourcesContent: [
        '<div id="foo" :class="bar.baz">\n' +
          '  {{ world.burn() }}\n' +
          '  <div v-if="ok">yes</div>\n' +
          '  <template v-else>no</template>\n' +
          '  <div v-for="(value, index) in list"><span>{{ value + index }}</span></div>\n' +
          '</div>'
      ]
    }

应该是 sourcemap

接下来这个 单元测试所以的点都是测试 sourcemap 的点,所以我们先不看了


那接着我们看看 baseCompile 函数是通过几个步骤生成的 code

baseCompile

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  const onError = options.onError || defaultOnError
  const isModuleMode = options.mode === 'module'
  /* istanbul ignore if */
  const ast = isString(template) ? baseParse(template, options) : template
  const [nodeTransforms, directiveTransforms] =
    getBaseTransformPreset(prefixIdentifiers)
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )

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

好,最主要的就是三个部分

  1. 生成 ast - 通过 baseParse

  2. 调用 transform - 来处理 ast

  3. 使用 generate 生成 code


baseParse

那我们先看是如何生成ast的把

也就是baseParse,还是先看单元测试

下面的逻辑是只测试的 TextNode

    test('simple text', () => {
      const ast = baseParse('some text')
      const text = ast.children[0] as TextNode

      expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some text',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 9, line: 1, column: 10 },
          source: 'some text'
        }
      })
    })

可以看到 node 对象的关键的几个属性了

接着看看是如何解析出来的把

export function baseParse(
  content: string,
  options: ParserOptions = {}
): RootNode {
  const context = createParserContext(content, options)
  const start = getCursor(context)
  return createRoot(
    parseChildren(context, TextModes.DATA, []),
    getSelection(context, start)
  )
}

这里的重点是 context 是什么

继续去看 createParserContext

function createParserContext(
  content: string,
  rawOptions: ParserOptions
): ParserContext {
  const options = extend({}, defaultParserOptions)

  let key: keyof ParserOptions
  for (key in rawOptions) {
    // @ts-ignore
    options[key] =
      rawOptions[key] === undefined
        ? defaultParserOptions[key]
        : rawOptions[key]
  }
  return {
    options,
    column: 1,
    line: 1,
    offset: 0,
    originalSource: content,
    source: content,
    inPre: false,
    inVPre: false,
    onWarn: options.onWarn
  }
}

只是生成了一个配置对象

    {
      options: {
        delimiters: [ '{{', '}}' ],
        getNamespace: [Function: getNamespace],
        getTextMode: [Function: getTextMode],
        isVoidTag: [Function: NO],
        isPreTag: [Function: NO],
        isCustomElement: [Function: NO],
        decodeEntities: [Function: decodeEntities],
        onError: [Function: defaultOnError],
        onWarn: [Function: defaultOnWarn],
        comments: true
      },
      column: 1,
      line: 1,
      offset: 0,
      originalSource: 'some text',
      source: 'some text',
      inPre: false,
      inVPre: false,
      onWarn: [Function: defaultOnWarn]
    }

接着是调用了 getCursor

function getCursor(context: ParserContext): Position {
  const { column, line, offset } = context
  return { column, line, offset }
}

数据是来自 context 里面的

继续最后一个逻辑

  return createRoot(
    parseChildren(context, TextModes.DATA, []),
    getSelection(context, start)
  )

先看 parseChildren

function parseChildren(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[]
): TemplateChildNode[] {
  const parent = last(ancestors)
  const ns = parent ? parent.ns : Namespaces.HTML
  const nodes: TemplateChildNode[] = []

  while (!isEnd(context, mode, ancestors)) {
    __TEST__ && assert(context.source.length > 0)
    const s = context.source
    let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined

    if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
      if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
        // '{{'
        node = parseInterpolation(context, mode)
      } else if (mode === TextModes.DATA && s[0] === '<') {

太多了,就不copy过来了。不过激动的是这里和我们之前去刷编译原理时候处理语法的时候逻辑是一致的,解析成功的话,那么就创建一个 node 节点对象

而且是用 nodes 把所有node对象都收集起来

至于解析的规则的话,就是按照 html 的规则来的

回头去刷编译原理就好了,这里去解析 html 的套路都是一样的

现在我们只需要知道返回一个处理完的 nodes就ok了

继续去看下一个逻辑点


getSelection

function getSelection(
  context: ParserContext,
  start: Position,
  end?: Position
): SourceLocation {
  end = end || getCursor(context)
  return {
    start,
    end,
    source: context.originalSource.slice(start.offset, end.offset)
  }
}

这里就是返回对应这段代码的信息的


createRoot

export function createRoot(
  children: TemplateChildNode[],
  loc = locStub
): RootNode {
  return {
    type: NodeTypes.ROOT,
    children,
    helpers: [],
    components: [],
    directives: [],
    hoists: [],
    imports: [],
    cached: 0,
    temps: 0,
    codegenNode: undefined,
    loc
  }
}

这里的 createRoot 就是直接创建一个 root 节点给外面就ok了

而关键的 children 就是通过parseChildren生成的 nodes。

transform

看看 transform 阶段是做了什么

  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )

是基于 ast 来做处理,第二个参数就是 transformOptions 了

export function transform(root: RootNode, options: TransformOptions) {
  const context = createTransformContext(root, options)
  traverseNode(root, context)
  if (options.hoistStatic) {
    hoistStatic(root, context)
  }
  if (!options.ssr) {
    createRootCodegen(root, context)
  }
  // finalize meta information
  root.helpers = [...context.helpers.keys()]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = context.imports
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached

  if (__COMPAT__) {
    root.filters = [...context.filters!]
  }
}

还是先处理配置 context

看看长什么样子

 {
      selfName: null,
      prefixIdentifiers: false,
      hoistStatic: false,
      cacheHandlers: false,
      nodeTransforms: [ [Function: plugin] ],
      directiveTransforms: {},
      transformHoist: null,
      isBuiltInComponent: [Function: NOOP],
      isCustomElement: [Function: NOOP],
      expressionPlugins: [],
      scopeId: null,
      slotted: true,
      ssr: false,
      inSSR: false,
      ssrCssVars: '',
      bindingMetadata: {},
      inline: false,
      isTS: false,
      onError: [Function: defaultOnError],
      onWarn: [Function: defaultOnWarn],
      compatConfig: undefined,
      root: {
        type: 0,
        children: [ [Object] ],
        helpers: [],
        components: [],
        directives: [],
        hoists: [],
        imports: [],
        cached: 0,
        temps: 0,
        codegenNode: undefined,
        loc: {
          start: [Object],
          end: [Object],
          source: '<div>hello {{ world }}</div>'
        }
      },
      helpers: Map(0) {},
      components: Set(0) {},
      directives: Set(0) {},
      hoists: [],
      imports: [],
      constantCache: Map(0) {},
      temps: 0,
      cached: 0,
      identifiers: [Object: null prototype] {},
      scopes: { vFor: 0, vSlot: 0, vPre: 0, vOnce: 0 },
      parent: null,
      currentNode: {
        type: 0,
        children: [ [Object] ],
        helpers: [],
        components: [],
        directives: [],
        hoists: [],
        imports: [],
        cached: 0,
        temps: 0,
        codegenNode: undefined,
        loc: {
          start: [Object],
          end: [Object],
          source: '<div>hello {{ world }}</div>'
        }
      },
      childIndex: 0,
      inVOnce: false,
      helper: [Function: helper],
      removeHelper: [Function: removeHelper],
      helperString: [Function: helperString],
      replaceNode: [Function: replaceNode],
      removeNode: [Function: removeNode],
      onNodeRemoved: [Function: onNodeRemoved],
      addIdentifiers: [Function: addIdentifiers],
      removeIdentifiers: [Function: removeIdentifiers],
      hoist: [Function: hoist],
      cache: [Function: cache],
      filters: Set(0) {}
    }

这里的好多属性看起来都是 vue 特有的

第二步的时候就是 调用 traverseNode

后面的逻辑是处理一些特殊key 的

  root.helpers = [...context.helpers.keys()]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = context.imports
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached

这里是把一些额外的方法给到了 root 上面,而root 是 AST 的root node

traverseNode

在看这个函数之前,先找个测试看看

  test('context state', () => {
    const ast = baseParse(`<div>hello {{ world }}</div>`)

    // manually store call arguments because context is mutable and shared
    // across calls
    const calls: any[] = []
    const plugin: NodeTransform = (node, context) => {
      calls.push([node, { ...context }])
    }

    transform(ast, {
      nodeTransforms: [plugin]
    })

    const div = ast.children[0] as ElementNode
    expect(calls.length).toBe(4)
    expect(calls[0]).toMatchObject([
      ast,
      {
        parent: null,
        currentNode: ast
      }
    ])
    expect(calls[1]).toMatchObject([
      div,
      {
        parent: ast,
        currentNode: div
      }
    ])
    expect(calls[2]).toMatchObject([
      div.children[0],
      {
        parent: div,
        currentNode: div.children[0]
      }
    ])
    expect(calls[3]).toMatchObject([
      div.children[1],
      {
        parent: div,
        currentNode: div.children[1]
      }
    ])
  })

通过这个测试可以知道,transform 是在本身的 AST 的基础上直接修改数据的

而这里的执行模式应该和 babel 的 plugin 的形式也差不多,通过 visit 的处理方式来调用

上面的 nodeTransforms:[plugin] 就是处理方式,看起来是当所有的node调用的时候,就会执行这个 nodeTransforms 里面给的函数

在看看第二个测试

  test('context.replaceNode', () => {
    const ast = baseParse(`<div/><span/>`)
    const plugin: NodeTransform = (node, context) => {
      if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
        // change the node to <p>
        context.replaceNode(
          Object.assign({}, node, {
            tag: 'p',
            children: [
              {
                type: NodeTypes.TEXT,
                content: 'hello',
                isEmpty: false
              }
            ]
          })
        )
      }
    }
    const spy = jest.fn(plugin)
    transform(ast, {
      nodeTransforms: [spy]
    })

    expect(ast.children.length).toBe(2)
    const newElement = ast.children[0] as ElementNode
    expect(newElement.tag).toBe('p')
    expect(spy).toHaveBeenCalledTimes(4)
    // should traverse the children of replaced node
    expect(spy.mock.calls[2][0]).toBe(newElement.children[0])
    // should traverse the node after the replaced node
    expect(spy.mock.calls[3][0]).toBe(ast.children[1])
  })

在 plugin 的实现里面可以看到就是通过替换node来达到修改代码的效果

而context 是什么?哦,看起来 context 是用来处理 node 的

traverseNode

使用的基本逻辑明白了 那接着看看 traverseNode 内部是如何实现的把

export function traverseNode(
  node: RootNode | TemplateChildNode,
  context: TransformContext
) {
  context.currentNode = node
  // apply transform plugins
  const { nodeTransforms } = context
  const exitFns = []
  for (let i = 0; i < nodeTransforms.length; i++) {
    const onExit = nodeTransforms[i](node, context)
    if (onExit) {
      if (isArray(onExit)) {
        exitFns.push(...onExit)
      } else {
        exitFns.push(onExit)
      }
    }
    if (!context.currentNode) {
      // node was removed
      return
    } else {
      // node may have been replaced
      node = context.currentNode
    }
  }

  switch (node.type) {
    case NodeTypes.COMMENT:
      if (!context.ssr) {
        // inject import for the Comment symbol, which is needed for creating
        // comment nodes with `createVNode`
        context.helper(CREATE_COMMENT)
      }
      break
    case NodeTypes.INTERPOLATION:
      // no need to traverse, but we need to inject toString helper
      if (!context.ssr) {
        context.helper(TO_DISPLAY_STRING)
      }
      break

    // for container types, further traverse downwards
    case NodeTypes.IF:
      for (let i = 0; i < node.branches.length; i++) {
        traverseNode(node.branches[i], context)
      }
      break
    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context)
      break
  }

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

这里的 context 就是通过 createTransformContext 生成的,里面有好多方法可以处理 node

第一步是先调用用户通过 config 注入的 nodeTransforms 里面的函数,也就是单测里面给的 plugin 函数

参数就是把 node 和 context 给到,所以用户可以在 plugin 里面通过 context 提供的方法来处理 node

这里的细节是, plugin 是可以返回一个函数的,这个函数就做 onExit

接着会基于 node 的类型做不同的处理

switch (node.type) {
    case NodeTypes.COMMENT:
      if (!context.ssr) {
        // inject import for the Comment symbol, which is needed for creating
        // comment nodes with `createVNode`
        context.helper(CREATE_COMMENT)
      }
      break
    case NodeTypes.INTERPOLATION:
      // no need to traverse, but we need to inject toString helper
      if (!context.ssr) {
        context.helper(TO_DISPLAY_STRING)
      }
      break

    // for container types, further traverse downwards
    case NodeTypes.IF:
      for (let i = 0; i < node.branches.length; i++) {
        traverseNode(node.branches[i], context)
      }
      break
    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context)
      break
  }
  • NodeTypes.COMMENT → context.helper(CREATE_COMMENT)

  • NodeTypes.INTERPOLATION → context.helper(TO_DISPLAY_STRING)

  • NodeTypes.IF → traverseNode(node.branches[i], context)

  • NodeTypes.IF_BRANCH || NodeTypes.FOR || NodeTypes.ELEMENT || NodeTypes.ROOT:

    traverseChildren(node, context)

这个处理完成后在统一的调用 exitFn

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

这里应该是方便让用户做一些清理逻辑


createTransformContext - context

继续来分析一下 context

他里面有几个关键的方法

  helper(name) {
 
    },
    removeHelper(name) {
  
    },
    helperString(name) {
    
    },
    replaceNode(node) {
    },
    removeNode(node) {
    onNodeRemoved: () => {},
    addIdentifiers(exp) {
     
    },
    removeIdentifiers(exp) {
      
    },
    hoist(exp) {
     
    },
    cache(exp, isVNode = false) {
      
    }

先来看看 helper

    helper(name) {
      const count = context.helpers.get(name) || 0
      context.helpers.set(name, count + 1)
      return name
    },

逻辑是加一个 count ,那么是用在哪里的呢?

没找到 继续看看 removeHelper

    removeHelper(name) {
      const count = context.helpers.get(name)
      if (count) {
        const currentCount = count - 1
        if (!currentCount) {
          context.helpers.delete(name)
        } else {
          context.helpers.set(name, currentCount)
        }
      }
    },

和 helper 是对应的,这里是删除一个 count

在看 helperString

    helperString(name) {
    return `_${helperNameMap[context.helper(name)]}`
    },

这里的重点是 helperNameMap ,而 context.helper(name ) 是基于 name 计数了一下,然后把 name 返回。

那看看 helperNameMap

export const helperNameMap: any = {
  [FRAGMENT]: `Fragment`,
  [TELEPORT]: `Teleport`,
  [SUSPENSE]: `Suspense`,
  [KEEP_ALIVE]: `KeepAlive`,
  [BASE_TRANSITION]: `BaseTransition`,
  [OPEN_BLOCK]: `openBlock`,
  [CREATE_BLOCK]: `createBlock`,
  [CREATE_ELEMENT_BLOCK]: `createElementBlock`,
  [CREATE_VNODE]: `createVNode`,
  [CREATE_ELEMENT_VNODE]: `createElementVNode`,
  [CREATE_COMMENT]: `createCommentVNode`,

太多了,截取了一部分,可以看到,这里存储的都是对应的处理函数,也就是所谓的 helper

那可以说是 helperString 就是返回对应 helper 的名称

继续看replaceNode

    replaceNode(node) {
      context.parent!.children[context.childIndex] = context.currentNode = node
    },

替换节点, 替换的是当前 context 的父级的孩子节点(基于 childIndex 获取的孩子)

下面的是removeNode

    removeNode(node) {

      const list = context.parent!.children
      const removalIndex = node
        ? list.indexOf(node)
        : context.currentNode
        ? context.childIndex
        : -1

      if (!node || node === context.currentNode) {
        // current node removed
        context.currentNode = null
        context.onNodeRemoved()
      } else {
        // sibling node removed
        if (context.childIndex > removalIndex) {
          context.childIndex--
          context.onNodeRemoved()
        }
      }
      context.parent!.children.splice(removalIndex, 1)
    },

处理的也是 context 父级的孩子节点, 直接给删除当前的节点

这里有个回调,删除完成后会执行 context 里面的 onNodeRemoved

addIdentifiers

    addIdentifiers(exp) {
      // identifier tracking only happens in non-browser builds.
      if (!__BROWSER__) {
        if (isString(exp)) {
          addId(exp)
        } else if (exp.identifiers) {
          exp.identifiers.forEach(addId)
        } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
          addId(exp.content)
        }
      }
    },

这里的关键是 addId 函数,等会在看 TODO

后面是 removeIdentifiers

    removeIdentifiers(exp) {
      if (!__BROWSER__) {
        if (isString(exp)) {
          removeId(exp)
        } else if (exp.identifiers) {
          exp.identifiers.forEach(removeId)
        } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
          removeId(exp.content)
        }
      }
    },

这里是对Id 的删除

hoist 处理静态提升

把静态的标签都缓存起来

    hoist(exp) {
      if (isString(exp)) exp = createSimpleExpression(exp)
      context.hoists.push(exp)
      const identifier = createSimpleExpression(
        `_hoisted_${context.hoists.length}`,
        false,
        exp.loc,
        ConstantTypes.CAN_HOIST
      )
      identifier.hoisted = exp
      return identifier
    },

这个主要是做优化的,可以作为TODO

cache 方法也是一样的,后续在看

    cache(exp, isVNode = false) {
      return createCacheExpression(context.cached++, exp, isVNode)
    }
 

总结来看的话,context 有这么几个职责

  • 处理 helper

    • helper 是对应的处理函数,这个应该是由 runtime-core 导出的
  • 处理 node

    • node就是 AST 节点,可以替换和删除
  • 处理 Identifiers

    • identifiers 是做什么的,还不知道

    • 添加和删除

  • 以及2个用于优化的方法

    • hoist

    • cache


接着我们回到traverseNode 函数内看下面的逻辑

    case NodeTypes.IF:
      for (let i = 0; i < node.branches.length; i++) {
        traverseNode(node.branches[i], context)
      }
      break

如果是 if 分支的话,那么需要2个分支都处理继续调用

在看 下面

    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context)
      break
export function traverseChildren(
  parent: ParentNode,
  context: TransformContext
) {
  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
    context.onNodeRemoved = nodeRemoved
    traverseNode(child, context)
  }
}

traverseChildren 的逻辑很简单,就是标准的 for children 然后再调用 traverseNode

至此,所有的 node 都会被执行到 nodeTransforms 这个里面的函数内

并且还收集完了 helper , 以及做好了 count 计数


generate

看看如何做代码生成

export function generate(
  ast: RootNode,
  options: CodegenOptions & {
    onContextCreated?: (context: CodegenContext) => void
  } = {}
): CodegenResult {
  const context = createCodegenContext(ast, options)
  if (options.onContextCreated) options.onContextCreated(context)
  const {
    mode,
    push,
    prefixIdentifiers,
    indent,
    deindent,
    newline,
    scopeId,
    ssr
  } = context

  const hasHelpers = ast.helpers.length > 0
  const useWithBlock = !prefixIdentifiers && mode !== 'module'
  const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
  const isSetupInlined = !__BROWSER__ && !!options.inline

  // preambles
  // in setup() inline mode, the preamble is generated in a sub context
  // and returned separately.
  const preambleContext = isSetupInlined
    ? createCodegenContext(ast, options)
    : context
  if (!__BROWSER__ && mode === 'module') {
    genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
  } else {
    genFunctionPreamble(ast, preambleContext)
  }
  // enter render function
  const functionName = ssr ? `ssrRender` : `render`
  const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
  if (!__BROWSER__ && options.bindingMetadata && !options.inline) {
    // binding optimization args
    args.push('$props', '$setup', '$data', '$options')
  }
  const signature =
    !__BROWSER__ && options.isTS
      ? args.map(arg => `${arg}: any`).join(',')
      : args.join(', ')

  if (isSetupInlined) {
    push(`(${signature}) => {`)
  } else {
    push(`function ${functionName}(${signature}) {`)
  }
  indent()

  if (useWithBlock) {
    push(`with (_ctx) {`)
    indent()
    // function mode const declarations should be inside with block
    // also they should be renamed to avoid collision with user properties
    if (hasHelpers) {
      push(
        `const { ${ast.helpers
          .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
          .join(', ')} } = _Vue`
      )
      push(`\n`)
      newline()
    }
  }

  // generate asset resolution statements
  if (ast.components.length) {
    genAssets(ast.components, 'component', context)
    if (ast.directives.length || ast.temps > 0) {
      newline()
    }
  }
  if (ast.directives.length) {
    genAssets(ast.directives, 'directive', context)
    if (ast.temps > 0) {
      newline()
    }
  }
  if (__COMPAT__ && ast.filters && ast.filters.length) {
    newline()
    genAssets(ast.filters, 'filter', context)
    newline()
  }

  if (ast.temps > 0) {
    push(`let `)
    for (let i = 0; i < ast.temps; i++) {
      push(`${i > 0 ? `, ` : ``}_temp${i}`)
    }
  }
  if (ast.components.length || ast.directives.length || ast.temps) {
    push(`\n`)
    newline()
  }

  // generate the VNode tree expression
  if (!ssr) {
    push(`return `)
  }
  if (ast.codegenNode) {
    genNode(ast.codegenNode, context)
  } else {
    push(`null`)
  }

  if (useWithBlock) {
    deindent()
    push(`}`)
  }

  deindent()
  push(`}`)

  return {
    ast,
    code: context.code,
    preamble: isSetupInlined ? preambleContext.code : ``,
    // SourceMapGenerator does have toJSON() method but it's not in the types
    map: context.map ? (context.map as any).toJSON() : undefined
  }
}

去找个单元测试

  test('module mode preamble', () => {
    const root = createRoot({
      helpers: [CREATE_VNODE, RESOLVE_DIRECTIVE]
    })
    const { code } = generate(root, { mode: 'module' })
    expect(code).toMatch(
      `import { ${helperNameMap[CREATE_VNODE]} as _${helperNameMap[CREATE_VNODE]}, ${helperNameMap[RESOLVE_DIRECTIVE]} as _${helperNameMap[RESOLVE_DIRECTIVE]} } from "vue"`
    )
    expect(code).toMatchSnapshot()
  })

code 长这个样子

import { createVNode as _createVNode, resolveDirective as _resolveDirective } from "vue"
    
    export function render(_ctx, _cache) {
      return null
    }

那问题

import 上面的代码是怎么生成的?

在这里

  // preambles
  // in setup() inline mode, the preamble is generated in a sub context
  // and returned separately.
  const preambleContext = isSetupInlined
    ? createCodegenContext(ast, options)
    : context
  if (!__BROWSER__ && mode === 'module') {
    genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
  } else {
    genFunctionPreamble(ast, preambleContext)
  }

这里是基于不同的 mode 来生成不同的代码

而代码这里其实就是 string 的拼接

我们先看 genModulePreamble

function genModulePreamble(
  ast: RootNode,
  context: CodegenContext,
  genScopeId: boolean,
  inline?: boolean
) {
  const { push, newline, optimizeImports, runtimeModuleName } = context

  if (genScopeId) {
    ast.helpers.push(WITH_SCOPE_ID)
    if (ast.hoists.length) {
      ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
    }
  }

  // generate import statements for helpers
  if (ast.helpers.length) {
    if (optimizeImports) {
      // when bundled with webpack with code-split, calling an import binding
      // as a function leads to it being wrapped with `Object(a.b)` or `(0,a.b)`,
      // incurring both payload size increase and potential perf overhead.
      // therefore we assign the imports to variables (which is a constant ~50b
      // cost per-component instead of scaling with template size)
      push(
        `import { ${ast.helpers
          .map(s => helperNameMap[s])
          .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
      )
      push(
        `\n// Binding optimization for webpack code-split\nconst ${ast.helpers
          .map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
          .join(', ')}\n`
      )
    } else {
      push(
        `import { ${ast.helpers
          .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
          .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
      )
    }
  }

  if (ast.ssrHelpers && ast.ssrHelpers.length) {
    push(
      `import { ${ast.ssrHelpers
        .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
        .join(', ')} } from "@vue/server-renderer"\n`
    )
  }

  if (ast.imports.length) {
    genImports(ast.imports, context)
    newline()
  }

  genHoists(ast.hoists, context)
  newline()

  if (!inline) {
    push(`export `)
  }
}

最简单的分支是

      push(
        `import { ${ast.helpers
          .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
          .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
      )

ast.helpers 是调用的时候给的,然后通知 helperNameMap 映射对应的函数名,最后拼成一个字符串即可

push 的话是 context 内部的方法,其实就是吧所有的 string 都收集进去


接着看 render 函数的生成

  // enter render function
  const functionName = ssr ? `ssrRender` : `render`
  const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
  if (!__BROWSER__ && options.bindingMetadata && !options.inline) {
    // binding optimization args
    args.push('$props', '$setup', '$data', '$options')
  }
  const signature =
    !__BROWSER__ && options.isTS
      ? args.map(arg => `${arg}: any`).join(',')
      : args.join(', ')

  if (isSetupInlined) {
    push(`(${signature}) => {`)
  } else {
    push(`function ${functionName}(${signature}) {`)
  }

基于参数来生成不同的 render string

具体后面所有生成代码的逻辑,都是基于 ast 上面的options 来做处理,有什么就生成什么样子的代码

现在基本上明白了。在回头看看 codegen return 的数据

  return {
    ast,
    code: context.code,
    preamble: isSetupInlined ? preambleContext.code : ``,
    // SourceMapGenerator does have toJSON() method but it's not in the types
    map: context.map ? (context.map as any).toJSON() : undefined
  }

ast 对象不用说了

code 就是基于 ast 生成的代码

preamble 是前导码头部的代码

map 是处理 sourcemap

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant