Skip to content

第十三章:compiler 编译器 - 构建 compile 编译器

01:前言

在上一章我们了解了 compiler 的作用和大致流程之后,那么这一章我们将要在 vue-next-mini 中 实现一个自己的编译器。

但是我们也知道,compiler 是一个非常复杂的概念,我们无法在有限的课程中实现一个完善的编译器,所以,我们将会和之前一样,严格遵循:没有使用就当做不存在最少代码的实现逻辑 这两个标准。只关注 核心当前业务 相关的内容,而忽略其他。

那么明确好了以上内容之后,接下来,就让我们进入编辑器的学习吧~~~

02:扩展知识:JavaScript与有限自动状态机

我们知道想要实现 compiler 第一步是构建 AST 对象。那么想要构建 AST,就需要利用到 有限状态机 的概念。

有限状态机也被叫做 有限自动状态机,表示:有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型

光看概念,可能难以理解,那么下面我们来看一个具体的例子:

根据 packages/compiler-core/src/compile.ts 中的代码可知,ast 对象的生成是通过 baseParse 方法得到的。

而对于 baseParse 方法而言,接收一个 template 作为参数,返回一个 ast 对象。

即:通过 parse 方法,解析 template,得到 ast 对象。 中间解析的过程,就需要使用到 有限自动状态机。

我们来如下模板 (template) :

html
<div>hello world</div>

vue 想要把该模板解析成 AST,那么就需要利用有限自动状态机对该模板进行分析,分析的过程中主要包含了三个特性:

摘自:http://www.ruanyifeng.com/blog/2013/09/finite-state_machine_for_javascript.html

  1. 状态总数是有限的
    1. 初始状态
    2. 标签开始状态
    3. 标签名称状态
    4. 文本状态
    5. 结束标签状态
    6. 结束标签名称状态
  2. 任一时刻,只处在一种状态之中
  3. 某种条件下,会从一种状态转变到另一种状态
    1. 比如:从 12 意味着从初始状态切换到了标签开始状态

如下图所示:

image-20230813111602103

  1. 解析 <:由 初始状态 进入 标签开始状态
  2. 解析 div:由 标签开始状态 进入 标签名称状态
  3. 解析 >:由 标签名称状态 进入 初始状态
  4. 解析 hello world:由 初始状态 进入 文本状态
  5. 解析 <:由 文本状态 进入 标签开始状态
  6. 解析 /:由 标签开始状态 进入 结束标签状态
  7. 解析 div:由 结束标签状态 进入 结束标签名称状态
  8. 解析 >:由 结束标签名称状态 进入 初始状态

经过这样一些列的解析,对于:

html
<div>hello world</div>

而言,我们将得到三个 token

html
开始标签:<div>
文本节点:hello world
结束标签:</div>

而这样一个利用有限自动状态机的状态迁移,来获取 tokens 的过程,可以叫做:对模板的标记化

总结

那么这一小节,我们了解了什么是有限自动状态机,也知道了它的三个特性。

vue 利用它来实现了对模板的标记化,得到了对应的 token

那么这些 token 有什么用呢?我们下一小节再说。

03:扩展知识:扫描 tokens 构建 AST 结构的方案

在上一小节中,我们已经知道可以通过自动状态机解析模板为 tokens,那么解析出来的 tokens 就是生成 AST 的关键。

生成 AST 的过程,就是 tokens 扫描的过程

我们以以下 html 结构为例:

html
<div>
  <p>hello</p>  
  <p>world</p>  
</div>

html 可以被解析为如下 tokens

html
开始标签:<div>
开始标签:<p>
文本节点:hello
结束标签:</p>
开始标签:<p>
文本节点:world
结束标签:</p>
结束标签:</div>

具体的扫描过程为(文档中仅显示初始状态和结束状态,具体扫描流程可以查看 课程资料 PPT 第 7 页):

初始状态:

image-20230813111808407

结束状态:

image-20230813111841180

在刚才的图示中,我们通过 递归下降算法? 这样的一种扫描形式把 tokens 通过 解析成了 AST(抽象语法树)

04:源码阅读:编译器第一步:依据模板,生成 AST 抽象语法树

那么这一小节我们就来看一下 vue 中生成 AST 的代码。该部分代码全部被放入到了 packages/compiler-core/src/parse.ts 中,从这个文件可以看出,整体 parse 的逻辑非常复杂,整体文件有 1173 行代码。

所以我们再去看这一块逻辑的时候,同样会按照之前的方式,只去关注当前业务下的逻辑,而忽略其他逻辑,依次来降低整体的复杂度。

通过 packages/compiler-core/src/compile.ts 中的 baseCompile 方法可以知道,整个 parse 的过程是从 baseParse 开始的,所以我们可以直接从这个方法开始进行 debugger

测试实例为:

js
<script>
  const { compile, h, render } = Vue
  // 创建 template
  const template = `<div>hello world</div>`

  // 生成 render 函数
  const renderFn = compile(template)
</script>

当前的 template 对应的目标极简 AST 为(这意味着我们将不再关注其他的属性生成):

js
const ast = {
  "type": 0,
  "children": [
    {
      "type": 1,
      "tag": "div",
      "tagType": 0,
      // 属性,目前我们没有做任何处理。但是需要添加上,否则,生成的 ats 放到 vue 源码中会抛出错误
      "props": [],
      "children": [{ "type": 2, "content": " hello world " }]
    }
  ],
  // loc:位置,这个属性并不影响渲染,但是它必须存在,否则会报错。所以我们给了他一个 {}
  "loc": {}
}

模板解析的 token 流程为(以 <div>hello world</div> 为例):

1. <div
2. >
3. hello world
4. </div
5. >

明确好以上内容之后,我们开始。

  1. 进入 baseParse 方法

  2. 执行 createParserContext生成 context 上下文对象

    1. 进入 createParserContext 方法
    2. 该方法中返回了一个 ParserContext 类型的对象:
      1. ParserContext 是一个解析器上下文对象,里面包含了非常多的解析器属性
      2. 具体可查看 packages/compiler-core/src/parse.ts 中 第 92
    3. 该对象比较复杂,我们只需要关注 source(模板源代码) 这一个属性即可
  3. 此时 context.source = "<div> hello world </div>"

  4. 执行 getCursor(context) 方法,该方法主要获取 loc (即:location 位置),与我们的极简 AST 无关,无需关注

  5. 执行 parseChildren 方法(解析子节点),这个方法 非常重要,是生成 AST 的核心方法:

    1. 进入 parseChildren 方法

    2. 执行 const nodes: TemplateChildNode[] = [],创建 nodes 变量,这个 nodes 就是生成的 AST 中的 children

    3. 执行 while 循环,循环解析模板数据:

      1. 循环的判断条件为 !isEnd(context, mode, ancestors) ,我们进入到 isEnd 方法进行查看

        1. 执行 const s = context.source,获取 s,此时 s = <div> hello world </div>
        2. 不符合 isEnd 的条件,返回 false进入循环
      2. 执行 const s = context.source ,此时的 s = <div> hello world </div>

      3. 执行 let node,声明 node,这个 node 就是 children 中的元素

      4. 执行 if (mode === TextModes.DATA || mode === TextModes.RCDATA) {...}else if (mode === TextModes.DATA && s[0] === '<') ,因为当前的 s = <div> hello world </div> ,所以 满足条件。表示为:标签开始

        1. 执行 else if (/[a-z]/i.test(s[1]))满足条件。表示为:< 开始,后面跟 a-z 表示,这是一个标签的开始

        2. 执行 node = parseElement(context, ancestors) 方法。即:parseElement 的返回值为 node,我们知道 nodechildren 下的元素,所以说:parseElement 即为解析 element ,生成 children 下元素的方法

          1. 进入 parseElement 方法,开始解析 element,此时 context.source = <div> hello world </div>

            1. 整个 parseElement 的解析分为三步:

              1. 开始标签:例如 <div>
              2. 子节点:例如 hello world
              3. 结束标签:例如 </div>
            2. 首先执行 开始标签 <div> 的解析:

              1. 执行 const element = parseTag(context, TagType.Start, parent) 方法,parseTag 表示为 解析标签

                1. 整个 parseTag 方法解析标签共分为两步:
                  1. 标签开始:例如:<div
                  2. 标签结束:例如:>
              2. 进入 parseTag 方法,该方法为 解析标签 的方法,主要做了 四件 事情:

                1. 首先处理 标签开始

                  1. 代码执行:const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
                  2. 代码执行:const tag = match[1]。利用 match 这个正则,拿到 tag 标签名,此时标签名为 tag = div
                  3. 执行 advanceBy(context, match[0].length) 方法,此处的 advanceBy 方法 非常重要
                    1. 该方法的的作用,主要为:解析模板
                    2. 针对于 <div>hello world</div> 而言,一共会被解析 5 次,解析的顺序为:
                    3. 第一次解析:<div:此时 context.source = >hello world</div>
                    4. 第二次解析:>:此时 context.source = hello world</div>
                    5. 第三次解析:hello world:此时 context.source = </div>
                    6. 第四次解析:</div:此时 context.source = >
                    7. 第五次解析:>:此时 context.source = ''
                    8. 此时为 第一次解析:<div:此时 context.source = >hello world</div>
                2. 接下来处理 标签结束

                  1. 执行 let isSelfClosing,创建 isSelfClosing 变量,该变量表示为关闭标签。
                  2. 执行 isSelfClosing = startsWith(context.source, '/>')。即:context.source 此时以 /> 即为,则 isSelfClosing = true。否则为 false
                  3. 再次执行 advanceBy(context, isSelfClosing ? 2 : 1),此时为: 第一次解析:>:此时 context.source = hello world</div>
                3. 标记 标签类型

                  1. 执行 let tagType = ElementTypes.ELEMENT。标记当前的 tagTypeelement 类型。
                4. 返回 element 对象

                  1. 此时返回的对象为 element 对象,值为:

                    image-20230813113153259

                  2. 此时 context.source 已经被处理了:<div> 两个 token,剩余的 context.source 为:

                    image-20230813113227724

            3. 此时 parseTag 执行完成,标记着 开始标签:例如 <div> 处理完成

            4. 接下来处理 子节点 children

              1. 代码执行 ancestors.push(element)

                1. 其中 ancestors 表示父节点的意思
                2. 即:把刚才得到的 element 对象放入到 ancestors
              2. 执行 parseChildren 方法,处理子节点。注意: parseChildren 此时 第二次 被调用,用于处理 子节点 hello world

                1. 再次进入 parseChildren 方法

                2. 执行同样的逻辑,但是,因为此时的 s: " hello world </div>"。所以会执行 node = parseText(context, mode),表示:处理文本节点(hello world)

                  1. 进入 parseText 方法

                  2. 执行 const endTokens = xxx。此时 endTokens 的值为 ['<', '{{']

                    1. endTokens 表示:普通文本的结束 token。例如:hello world </div>,那么文本结束的标记就为 <
                    2. PS:这也意味着如果你渲染了一个 <div> hell<o </div> 的标签,那么你将得到一个 错误
                  3. 执行 let endIndex = context.source.length。其中 endIndex 表示 普通文本结束的位置

                  4. 执行 for 循环,计算 endIndex 的值。计算的逻辑为:从 context.source 中分别获取 '<', '{{'的下标,取最小值为 endIndex

                  5. 代码执行完成之后:endIndex = 13

                  6. 执行 const content = parseTextData(context, endIndex, mode),获取 文本内容。该方法主要做了三件事:

                    1. 获取文本内容:
                    2. 执行 context.source.slice(0, length) 方法
                    3. 触发 第三次解析:
                    4. 触发 advanceBy 方法进行 第三次解析,此时解析的内容为: hello world:此时 context.source = </div>
                    5. 返回文本:
                    6. 执行 return rawText
                  7. 至此 parseText 方法执行完成,它将返回一个 children,值为:

                    image-20230813113455585

                  8. 此时 context.source = </div>

                3. 执行 pushNode(nodes, node) ,把 node 作为 nodes 的子节点,此时 nodes 的值为:

                  image-20230813113617645

              3. 返回 parseElement 方法中,得到的 children 即为 nodes

              4. 最后在 ancestorspop 出子节点

            5. 此时,子节点 hello world 处理完成

            6. 执行 element.children = children,即:

              image-20230813113709841

            7. 最后处理 结束标签 </div>:

              1. 执行 f (startsWithEndTagOpen(context.source, element.tag)) 方法
                1. 该方法比较简单,其作用是:判断当前是否为《标签结束的开始》。比如 </div> 就是 div 标签结束的开始
              2. 再次执行 parseTag 方法处理结束标签:
                1. 再次进入 parseTag 方法,此次进入我们将处理结束标签 </div>
                  1. 标签开始
                    1. 执行 const tag = match[1],拿到的 tag = div
                    2. 执行 advanceBy 方法,此时为 第四次解析:</div:此时 context.source = >
                  2. 标签结束
                    1. 执行 isSelfClosing = startsWith(context.source, '/>'),结果 isSelfClosing = false
                    2. 执行 advanceBy 方法,此时为 第五次解析:>:此时 context.source = ''
            8. 至此:整个 template 已经全部解析完成

            9. 这也标记着 parseElement 方法执行完成,得到的 element 为:

              image-20230813113902517

            10. element 将被作为返回值返回

        3. 至此:node = parseElement(context, ancestors) 执行完成,此时的 node 为上图 element

        4. 执行 pushNode(nodes, node),赋值给 nodes

        5. 此时的 nodes 为:

          image-20230813113957342

      5. 至此,整个 parseChildren 执行完成,得到 nodes,并返回

  6. parseChildren 方法执行完成:children = nodes(上图)

  7. 最后执行 createRoot 方法:

    1. 进入 createRoot 方法,该方法就比较简单了。
    2. 只是返回了一个:NodeTypes.ROOT 为根节点,nodeschildrenAST 对象
  8. 至此我们 成功 得到了 AST 对象

由以上代码可知:

  1. 整个 AST 生成的核心就是 parseChildren 方法。
  2. 生成的过程中,对整个 template<div> hello world </div> 进行了解析,整个解析分为 5 步(第二小节的讲解):
    1. 第一次解析:<div:此时 context.source = >hello world</div>
    2. 第二次解析:>:此时 context.source = hello world</div>
    3. 第三次解析:hello world:此时 context.source = </div>
    4. 第四次解析:</div:此时 context.source = >
    5. 第五次解析:>:此时 context.source = ''
  3. 在这个解析过程中,我们逐步扫描(第三小节的讲解)对应的每次 token,得到了一个对应的 AST 对象

vue 源码中的 parse 逻辑是非常复杂的,我们当前只是针对 <div>hello world</div> 这一种类型的 element 类型进行了处理。

其他的比如 <pre><img /> 这些标签类型的处理,大家可以根据本小节的内容,自己进行测试,我们在课程中就不会一一进行讲解了。

05:框架实现:构建 parse 方法,生成 context 实例

从这一小节开始,我们将实现 vue-next-mini 中的编辑器模块。首先我们第一步要做的就是生成 AST 对象。但是我们知道 AST 对象的生成颇为复杂,所以我们把整个过程分为成三步进行处理。

  1. 构建 parse 方法,生成 context 实例
  2. 构建 parseChildren ,处理所有子节点(最复杂
    1. 构建有限自动状态机解析模板
    2. 扫描 token 生成 AST 结构
  3. 生成 AST,构建测试

那么本小节,我们就先处理第一步。

  1. 创建 packages/compiler-core/src/compile.ts 模块,写入如下代码:

    js
    export function baseCompile(template: string, options) {
    	return {}
    }
  2. 创建 packages/compiler-dom/src/index.ts 模块,导出 compile 方法:

    js
    import { baseCompile } from 'packages/compiler-core/src/compile'
    
    export function compile(template: string, options) {
    	return baseCompile(template, options)
    }
  3. packages/vue/src/index.ts 中,导出 compile 方法:

    js
    export { compile } from '@vue/compiler-dom'
  4. 创建 packages/compiler-core/src/parse.ts 模块下创建 baseParse 方法:

    js
     /**
      * 基础的 parse 方法,生成 AST
      * @param content tempalte 模板
      * @returns
      */
     export function baseParse(content: string) {
     	return {}
     }
  5. packages/compiler-core/src/compile.ts 模块下的 baseCompile 中,使用 baseParse 方法:

    js
    import { baseParse } from './parse'
    
    export function baseCompile(template: string, options) {
    	const ast = baseParse(template)
    	console.log(JSON.stringify(ast))
    
    	return {}
    }

那么至此,我们就成功的触发了 baseParse。接下来我们去生成 context 上下文对象。

  1. packages/compiler-core/src/parse.ts 中创建 createParserContext 方法,用来生成上下文对象:

    js
     /**
      * 创建解析器上下文
      */
     function createParserContext(content: string): ParserContext {
     	// 合成 context 上下文对象
     	return {
     		source: content
     	}
     }
  2. 创建 ParserContext 接口:

    js
     /**
      * 解析器上下文
      */
     export interface ParserContext {
     	// 模板数据源
     	source: string
     }
  3. baseParse 中触发该方法:

    js
    export function baseParse(content: string) {
    	// 创建 parser 对象,未解析器的上下文对象
    	const context = createParserContext(content)
      console.log(context)
    	return {}
    }

那么至此我们成功得到了 context 上下文对象。

我们可以创建测试实例 packages/vue/examples/compiler/compiler-ast.html

html
<script>
  const { compile } = Vue
  // 创建 template
  const template = `<div> hello world </div>`

  // 生成 render 函数
  const renderFn = compile(template)
</script>

可以成功打印 context

06:框架实现:构建有限自动状态机解析模板,扫描 token 生成 AST 结构

接下来我们通过 parseChildren 方法处理所有的子节点,整个处理的过程分为两大块:

  1. 构建有限自动状态机解析模板
  2. 扫描 token 生成 AST 结构

接下来我们来进行实现:

  1. 创建 parseChildren 方法:

    js
     /**
      * 解析子节点
      * @param context 上下文
      * @param mode 文本模型
      * @param ancestors 祖先节点
      * @returns
      */
     function parseChildren(context: ParserContext, ancestors) {
     	// 存放所有 node节点数据的数组
     	const nodes = []
     
     	/**
     	 * 循环解析所有 node 节点,可以理解为对 token 的处理。
     	 * 例如:<div>hello world</div>,此时的处理顺序为:
     	 * 1. <div
     	 * 2. >
     	 * 3. hello world
     	 * 4. </
     	 * 5. div>
     	 */
     	while (!isEnd(context, ancestors)) {
     		/**
     		 * 模板源
     		 */
     		const s = context.source
     		// 定义 node 节点
     		let node
     
     		if (startsWith(s, '{{')) {
     		}
     		// < 意味着一个标签的开始
     		else if (s[0] === '<') {
     			// 以 < 开始,后面跟a-z 表示,这是一个标签的开始
     			if (/[a-z]/i.test(s[1])) {
     				// 此时要处理 Element
     				node = parseElement(context, ancestors)
     			}
     		}
     
     		// node 不存在意味着上面的两个 if 都没有进入,那么我们就认为此时的 token 为文本节点
     		if (!node) {
     			node = parseText(context)
     		}
     
     		pushNode(nodes, node)
     	}
     
     	return nodes
     }
  2. 以上代码中涉及到了 个方法:

    1. isEnd:判断是否为结束节点
    2. startsWith:判断是否以指定文本开头
    3. pushNode:为 array 执行 push 方法
    4. 复杂:parseElement:解析 element
    5. 复杂:parseText:解析 text
  3. 我们先实现前三个简单方法:

  4. 创建 startsWith 方法:

    js
     /**
      * 是否以指定文本开头
      */
     function startsWith(source: string, searchString: string): boolean {
     	return source.startsWith(searchString)
     }
  5. 创建 isEnd 方法:

    js
     /**
      * 判断是否为结束节点
      */
     function isEnd(context: ParserContext, ancestors): boolean {
     	const s = context.source
     
     	// 解析是否为结束标签
     	if (startsWith(s, '</')) {
     		for (let i = ancestors.length - 1; i >= 0; --i) {
     			if (startsWithEndTagOpen(s, ancestors[i].tag)) {
     				return true
     			}
     		}
     	}
     	return !s
     }
  6. isEnd 方法中使用了 startsWithEndTagOpen 方法,所以我们要实现它:

    js
     /**
      * 判断当前是否为《标签结束的开始》。比如 </div> 就是 div 标签结束的开始
      * @param source 模板。例如:</div>
      * @param tag 标签。例如:div
      * @returns
      */
     function startsWithEndTagOpen(source: string, tag: string): boolean {
     	return (
     		startsWith(source, '</') &&
     		source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase() &&
     		/[\t\r\n\f />]/.test(source[2 + tag.length] || '>')
     	)
     }
  7. 创建 pushNode 方法:

    js
     /**
      * nodes.push(node)
      */
     function pushNode(nodes, node): void {
     	nodes.push(node)
     }

至此三个简单的方法都被构建完成。

接下来我们来处理 parseElement,在处理的过程中,我们需要使用到 NodeTypesElementTypes 这两个 enum 对象,所以我们需要先构建它们(直接复制即可):

  1. 创建 packages/compiler-core/src/ast.ts 模块:

    js
     /**
      * 节点类型(我们这里复制了所有的节点类型,但是我们实际上只用到了极少的部分)
      */
     export const enum NodeTypes {
     	ROOT,
     	ELEMENT,
     	TEXT,
     	COMMENT,
     	SIMPLE_EXPRESSION,
     	INTERPOLATION,
     	ATTRIBUTE,
     	DIRECTIVE,
     	// containers
     	COMPOUND_EXPRESSION,
     	IF,
     	IF_BRANCH,
     	FOR,
     	TEXT_CALL,
     	// codegen
     	VNODE_CALL,
     	JS_CALL_EXPRESSION,
     	JS_OBJECT_EXPRESSION,
     	JS_PROPERTY,
     	JS_ARRAY_EXPRESSION,
     	JS_FUNCTION_EXPRESSION,
     	JS_CONDITIONAL_EXPRESSION,
     	JS_CACHE_EXPRESSION,
     
     	// ssr codegen
     	JS_BLOCK_STATEMENT,
     	JS_TEMPLATE_LITERAL,
     	JS_IF_STATEMENT,
     	JS_ASSIGNMENT_EXPRESSION,
     	JS_SEQUENCE_EXPRESSION,
     	JS_RETURN_STATEMENT
     }
     
     /**
      * Element 标签类型
      */
     export const enum ElementTypes {
     	/**
     	 * element,例如:<div>
     	 */
     	ELEMENT,
     	/**
     	 * 组件
     	 */
     	COMPONENT,
     	/**
     	 * 插槽
     	 */
     	SLOT,
     	/**
     	 * template
     	 */
     	TEMPLATE
     }

    下面就可以构建 parseElement 方法了 ,该方法的作用主要为了解析 Element 元素:

    1. 创建 parseElement

      js
       /**
        * 解析 Element 元素。例如:<div>
        */
       function parseElement(context: ParserContext, ancestors) {
       	// -- 先处理开始标签 --
       	const element = parseTag(context, TagType.Start)
       
       	//  -- 处理子节点 --
       	ancestors.push(element)
       	// 递归触发 parseChildren
       	const children = parseChildren(context, ancestors)
       	ancestors.pop()
       	// 为子节点赋值
       	element.children = children
       
       	//  -- 最后处理结束标签 --
       	if (startsWithEndTagOpen(context.source, element.tag)) {
       		parseTag(context, TagType.End)
       	}
       
       	// 整个标签处理完成
       	return element
       }
    2. 构建 TagType enum

      js
       /**
        * 标签类型,包含:开始和结束
        */
       const enum TagType {
       	Start,
       	End
       }
    3. 处理开始标签,构建 parseTag

      js
       /**
        * 解析标签
        */
       function parseTag(context: any, type: TagType): any {
       	// -- 处理标签开始部分 --
       
       	// 通过正则获取标签名
       	const match: any = /^<\/?([a-z][^\r\n\t\f />]*)/i.exec(context.source)
       	// 标签名字
       	const tag = match[1]
       
       	// 对模板进行解析处理
       	advanceBy(context, match[0].length)
       
       	// -- 处理标签结束部分 --
       
       	// 判断是否为自关闭标签,例如 <img />
       	let isSelfClosing = startsWith(context.source, '/>')
       	// 《继续》对模板进行解析处理,是自动标签则处理两个字符 /> ,不是则处理一个字符 >
       	advanceBy(context, isSelfClosing ? 2 : 1)
       
       	// 标签类型
       	let tagType = ElementTypes.ELEMENT
       
       	return {
       		type: NodeTypes.ELEMENT,
       		tag,
       		tagType,
       		// 属性,目前我们没有做任何处理。但是需要添加上,否则,生成的 ats 放到 vue 源码中会抛出错误
       		props: []
       	}
       }
    4. 解析标签的过程,其实就是一个自动状态机不断读取的过程,我们需要构建 advanceBy 方法,来标记进入下一步:

      js
       /**
        * 前进一步。多次调用,每次调用都会处理一部分的模板内容
        * 以 <div>hello world</div> 为例
        * 1. <div
        * 2. >
        * 3. hello world
        * 4. </div
        * 5. >
        */
       function advanceBy(context: ParserContext, numberOfCharacters: number): void {
       	// template 模板源
       	const { source } = context
       	// 去除开始部分的无效数据
       	context.source = source.slice(numberOfCharacters)
       }

      至此 parseElement 构建完成。此处的代码虽然不多,但是逻辑非常复杂。在解析的过程中,会再次触发 parseChildren,这次触发表示触发 文本解析,所以下面我们要处理 parseText 方法。

      1. 创建 parseText 方法,解析文本:

        js
         /**
          * 解析文本。
          */
         function parseText(context: ParserContext) {
         	/**
         	 * 定义普通文本结束的标记
         	 * 例如:hello world </div>,那么文本结束的标记就为 <
         	 * PS:这也意味着如果你渲染了一个 <div> hell<o </div> 的标签,那么你将得到一个错误
         	 */
         	const endTokens = ['<', '{{']
         	// 计算普通文本结束的位置
         	let endIndex = context.source.length
         
         	// 计算精准的 endIndex,计算的逻辑为:从 context.source 中分别获取 '<', '{{' 的下标,取最小值为 endIndex
         	for (let i = 0; i < endTokens.length; i++) {
         		const index = context.source.indexOf(endTokens[i], 1)
         		if (index !== -1 && endIndex > index) {
         			endIndex = index
         		}
         	}
         
         	// 获取处理的文本内容
         	const content = parseTextData(context, endIndex)
         
         	return {
         		type: NodeTypes.TEXT,
         		content
         	}
         }
      2. 解析文本的过程需要获取到文本内容,此时我们需要构建 parseTextData 方法:

        js
         /**
          * 从指定位置(length)获取给定长度的文本数据。
          */
         function parseTextData(context: ParserContext, length: number): string {
         	// 获取指定的文本数据
         	const rawText = context.source.slice(0, length)
         	// 《继续》对模板进行解析处理
         	advanceBy(context, length)
         	// 返回获取到的文本
         	return rawText
         }

      最后在 baseParse 中触发 parseChildren 方法:

      js
      /**
       * 基础的 parse 方法,生成 AST
       * @param content tempalte 模板
       * @returns
       */
      export function baseParse(content: string) {
      	// 创建 parser 对象,未解析器的上下文对象
      	const context = createParserContext(content)
      	const children = parseChildren(context, [])
      	console.log(children)
      	return {}
      }

      此时运行测试实例,应该可以打印出如下内容:

      json
       [
          {
            "type": 1,
            "tag": "div",
            "tagType": 0,
            "props": [],
            "children": [{ "type": 2, "content": " hello world " }]
          }
        ]

07:框架实现:生成 AST,构建测试

parseChildren 处理完成之后,我们可以到 children,那么最后我们就只需要利用 createRoot 方法,把 children 放到 ROOT 节点之下即可。

  1. 创建 createRoot 方法:

    js
     /**
      * 生成 root 节点
      */
     export function createRoot(children) {
     	return {
     		type: NodeTypes.ROOT,
     		children,
     		// loc:位置,这个属性并不影响渲染,但是它必须存在,否则会报错。所以我们给了他一个 {}
     		loc: {}
     	}
     }
  2. baseParse 中使用该方法:

    js
     /**
      * 基础的 parse 方法,生成 AST
      * @param content tempalte 模板
      * @returns
      */
     export function baseParse(content: string) {
     	// 创建 parser 对象,未解析器的上下文对象
     	const context = createParserContext(content)
     	const children = parseChildren(context, [])
     	return createRoot(children)
     }

至此整个 parse 解析流程完成。我们可以在 packages/compiler-core/src/compile.ts 中打印得到的 AST

js
export function baseCompile(template: string, options) {
	const ast = baseParse(template)
	console.log(JSON.stringify(ast))

	return {}
}

得到的内容为:

json
{
  "type": 0,
  "children": [
    {
      "type": 1,
      "tag": "div",
      "tagType": 0,
      "props": [],
      "children": [{ "type": 2, "content": " hello world " }]
    }
  ],
  "loc": {}
}

我们可以把得到的该 AST 放入到 vue 的源码中进行解析,以此来验证是否正确。

vue 源码的 packages/compiler-core/src/compile.ts 模块下 baseCompile 方法中:

diff
export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  ...

-  const ast = isString(template) ? baseParse(template, options) : template
+  const ast = {
+    type: 0,
+    children: [
+      {
+        type: 1,
+        tag: 'div',
+        tagType: 0,
+        props: [],
+        children: [{ type: 2, content: ' hello world ' }]
+      }
+    ],
+    loc: {}
+  }

  ...
}

运行源码的 compile 方法,浏览器中应该可以渲染 hello world

html
<script>
  const { compile, h, render } = Vue
  // 创建 template
  const template = ``

  // 生成 render 函数
  const renderFn = compile(template)

  // 创建组件
  const component = {
    render: renderFn
  }

  // 通过 h 函数,生成 vnode
  const vnode = h(component)

  // 通过 render 函数渲染组件
  render(vnode, document.querySelector('#app'))
</script>

成功运行,标记着我们的 AST 处理完成。

08:扩展知识:AST 到 JavaScript AST 的转化策略和注意事项

在生成了 AST 之后,我们知道接下来就需要把 AST 转化为 JavaScript AST 了,但是在转化的过程中,有一些对应的策略和注意事项,我们需要在本小节中进行描述。

转化策略

我们知道从 AST 转化为 JavaScript AST 本质上是一个对象结构的变化,变化的本质是为了后面更方便的解析对象,生成 render 函数。

在转化的过程中,我们需要遵循如下策略:

  1. 深度优先
  2. 转化函数分离
  3. 上下文对象

深度优先

我们知道 AST 而言,它是包含层级的,比如:

  1. 最外层是 ROOT
  2. children 是根节点

这样的结构下,就会存在一个自上而下的层级,那么针对这样的一个层级而言,我们需要遵循 深度优先,自下而上 的一个转化方案。

因为父节点的状态往往需要根据子节点的情况才能够进行确定,比如:

html
<div>hello world</div>
<div>hello {{ msg }}</div>

转化函数分离

在处理 AST 的时候,我们知道,针对于不同的 token,那么会使用不同的 parseXXX 方法进行处理,那么同样的,在 transform 的过程中,我们也会通过不同的 transformXXX 方法进行转化。

但是为了防止 transform 模块过于臃肿,所以我们会通过 options的方式对 transformXXX 方法进行注入(类似于 render 的 option)。

所有注入的方法,会生成一个 nodeTransforms 数组,通过 options 传入。

上下文对象

上下文对象即 context,对于上下文对象而言我们其实并不陌生。比如在 parse 时、 setup 函数中vuex 的 action 上,都出现过 context 对象。

对于 context 而言,我们可以把它理解为一个 全局变量 或者 单例的全局变量,它是一个多模块都可以访问的唯一对象。

transform 的策略中,因为存在 转化函数分离 这样的一个特性,所以我们我们必须要构建出这样的一个 context 对象,用来保存 当前的 node 节点 等数据

注意事项

说完了转化策略之后,我们来看下注意事项。

对于 transform 转化方法而言,vue 本身的实现非常复杂,比如:指令、 … 都会在这里处理。

但是对于我们当前而言,我们不考虑这些复杂的情况,仅查看最简单的静态数据渲染,以此来简化整体逻辑。

09:源码阅读:编译器第二步:转化 AST,得到 JavaScript AST 对象

这一小节,我们就来看下对应的 transform 逻辑,我们创建如下测试实例:

html
<script>
  const { compile } = Vue
  // 创建 template
  const template = `<div> hello world </div>`

  // 生成 render 函数
  const renderFn = compile(template)
</script>

进入 baseCompile 方法,触发 debugger

  1. 进入 baseCompile 方法:

  2. 执行 transform注意: 此时 tansform 方法传递了两个参数:

    1. ast:这是我们在 parse 时生成的 AST 对象
    2. options:这是一个配置对象,里面包含了上一小节说的 nodeTransforms
  3. 触发 transform 方法,该方法即为转化 JavaScript AST 的核心方法

    1. 进入 transform 方法

    2. 执行 createTransformContext,该方法主要为生成 context 上下文对象

      1. 进入 createTransformContext 方法

      2. 该方法内部的代码很多,但是整体逻辑比较简单,核心就是创建了一个对象 context,然后执行了 return

      3. 在生成的对象中,我们只需要关注如下几个核心属性即可:

        js
        {
        	/**
        	 * AST 根节点
        	 */
        	root
        	/**
        	 * 每次转化时记录的父节点
        	 */
        	parent: ParentNode | null
        	/**
        	 * 每次转化时记录的子节点索引
        	 */
        	childIndex: number
        	/**
        	 * 当前处理的节点
        	 */
        	currentNode
        	/**
        	 * 协助创建 JavaScript AST 属性 helpers,该属性是一个数组,值为 Symbol(方法名),表示 render 函数中创建 节点 的方法
        	 */
        	helpers: Map<symbol, number>
        	helper<T extends symbol>(name: T): T
        	/**
        	 * 转化方法集合
        	 */
        	nodeTransforms: any[]
        }
    3. 得到 context 上下文对象

    4. 之后,执行 traverseNode 方法,该方法为转化的 核心方法

      1. 进入 traverseNode,此时参数为:

        1. nodeast 对象
        2. context:上下文对象
      2. 执行 context.currentNode = node,标记当前处理的节点。此时 currentNode 为最外层的 root 节点

      3. 执行 const { nodeTransforms } = context,获取 nodeTransforms 数组,该数组中封装了所有的节点转化方法

      4. 执行 const exitFns = []for 循环,该循环的主要作用是:exitFns 数组中依次放入 nodeTransform 转化方法

        1. 进入该循环
        2. 执行const onExit = nodeTransforms[i](node, context),获取转化方法
          1. 进入 nodeTransforms[i](node, context) 中进行查看
          2. 可以发现虽然执行了很多不同的 transformXXX 方法,但是这些方法都有一个共同点就是:return 了一个方法。即:transformXXX 方法是一个闭包函数,对外返回了一个待执行的方法
          3. 返回的方法现在未执行,将来会在 exitFns[i]() 的时候被触发。
          4. 所以现在我们不着急查看
      5. 循环执行完成。exitFns 中将放入所有的 transformXXX 方法,此时对应的 noderoot,即:最顶层对象。当前 exitFns 中的值为:

        js
        [
        	// 转化 element
          postTransformElement(),
          // 转化 text
          transformText()
        ]
      6. 执行 switch (node.type) ,进入 switch

        1. 执行 traverseChildren(node, context),该方法会循环处理所有的子节点
          1. 进入 traverseChildren 方法
          2. 可以看到方法本身的逻辑比较简单,会遍历所有的子节点,以触发 traverseNode
          3. 我们尝试再次进入 traverseNode 来进行查看
          4. 再次进入traverseNode,此时参数为:
            1. noderootchildren 的第一个节点,即:**type = ELEMENTdiv **
            2. context:上下文对象
          5. 再次执行 for 循环逻辑,填充 exitFns。与上次不同的是此时对应的 nodetype = ELEMENTdiv,即:children[1]
          6. 再次触发 switch,执行 traverseChildren(node, context) 方法,循环处理子节点
      7. 依次迭代,当所有的迭代执行完成之后,代码继续往下执行

      8. 代码通过 switch,继续往下执行时,将是从最底层(text 节点)开始处理的

      9. 此时将执行 依次退出 逻辑,在依次退出时,会依次触发 exitFns 中保存的方法:

        js
        let i = exitFns.length
        while (i--) {
        	exitFns[i]()
        }

        这里大家需要注意:这是一个 i-- 的逻辑,这样的逻辑意味着 保存的方法将从后往前执行

      10. 之前的时候我们说过,exitFns[i]() 中保存着所有的 transformXXX 方法,每次 exitFns[i]() 执行都意味着一个 transformXXX 触发,即:一个节点被转化

那么至此,我们的 traverseNode 方法的讲解就算是完成了,下面我们就需要进入 transformXXX 函数的处理,我们这里主要使用到了两个 transformXXX 方法,分别为:

  1. packages/compiler-core/src/transforms/transformElement.ts 中的 transformElement 方法
  2. packages/compiler-core/src/transforms/transformText.ts 中的 transformText 方法

那么下面我们依次来看这两个方法,这两个方法会被多次触发,所以我们可以直接在 exitFns[i]() 中增加断点 :

  1. 执行 context.currentNode = node明确当前的执行节点

  2. 第一次触发

    1. 第一次触发 transformElement 方法,其实我们这里触发的验证来说应该是 returnpostTransformElement 函数

    2. node = context.currentNode!,利用我们的 context 的上下文对象,获取到当前的 node 节点,此时的 node 节点是:

      image-20230813125128394

    3. 该节点为 最底层 的节点,满足我们之前所说的深度优先

    4. 代码触发 if,不符合条件,直接 renturn

  3. 第二次触发

    1. 第二次触发 transformText 方法

    2. 执行 const children = node.children,此时的 children 为:

      image-20230813125203771

    3. 针对于 transformText 方法而言,代码非常多,但是并不复杂,当前我们的代码没有办法满足它的运行场景,所以我们直接描述一下它的逻辑:

      js
      /**
       * 方法的作用:将相邻的文本节点和表达式合并为一个表达式。
       *
       * 例如:
       * <div>hello {{ msg }}</div>
       * 上述模板包含两个节点:
       * 1. hello:TEXT 文本节点
       * 2. {{ msg }}:INTERPOLATION 表达式节点
       * 这两个节点在生成 render 函数时,需要被合并: 'hello' + _toDisplayString(_ctx.msg)
       * 那么在合并时就要多出来这个 + 加号。
       * 例如:
       * children:[
       * 	{ TEXT 文本节点 },
       *  " + ",
       *  { INTERPOLATION 表达式节点 }
       * ]
       */
    4. 因为我们当前没有 ,所以我们无法观察后续执行,后续代码直接跳过

  4. 第三次触发

    1. 第三次触发 transformElement 方法,此时的 node 节点为:

      image-20230813125331778

    2. 满足条件,触发逻辑

    3. 对于该方法而言,内部的代码非常多,但是处理我们无需关注,比如: propschildren

    4. 我们直接关注 node.codegenNode = createVNodeCall(...) 的逻辑

      1. 进入 createVNodeCall 方法
      2. 执行 context.helper(getVNodeHelper(context.inSSR, isComponent))
        1. 在这里就利用到了我们生成 context 的时候,创建的 helper 方法和 getVNodeHelper 方法,我们分别进入这两个方法来看一下
          1. 进入 getVNodeHelper 方法
            1. 内部的逻辑非常简单,只是一个三元表达式,直接返回了一个 CREATE_ELEMENT_VNODE 的常量,对应的值为 Symbol(createElementVNode)
            2. 该常量在生成 render 函数时代表了 createElementVNode 方法:
          2. 进入 helper 方法
            1. 该方法也非常简单,只是把刚才的 CREATE_ELEMENT_VNODE 这个常量放入到了 helpers 对象中。
            2. 针对于 helpers 对象:
              1. key:函数名
              2. value:索引
      3. 最后 return 一个对象
    5. 该对象即为 codegenNode 对象

那么至此,我们看到了两个主要的 transformXXX 方法的执行,后面还会存在多次的执行,我们就不在一个一个去看了。

当所有的 transformXXX 执行完成之后,意味着整个 traverseNode 全部执行完成。traverseNode 执行完成标记着此时:

  1. rootchildren 中将包含有 codegenNode 对象
  2. 所有的 文本节点和表达式 也都完成了合并

此时所有的 children 都有了 codegenNode 对象,但是对于最外层的 root 还不存在 codegenNode,所有接下来我们要处理最外层的 codegenNode

那么此时我们可以再回到 transform 方法中,继续往下执行:

  1. 重新回到 transform 方法

  2. 执行 createRootCodegen(root, context) 方法:

    1. 进入 createRootCodegen
    2. 执行 const { children } = root 拿到 children
    3. 我们当前 只有一个根节点,所以 children.length = 0
    4. 执行 if (isSingleElementRoot(root, child) && child.codegenNode),确认当前只存在一个根节点
    5. 拿到第一个子节点的 codegenNode,使其为 rootcodegenNode
  3. 至此 rootcodegenNode 存在值,值为 第一个子节点的 codegenNode

  4. 最后执行:

    js
      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

    完成各中值的初始化即可

由以上代码可知:

  1. 整个 transform 的逻辑可以大致分为两部分:
    1. 深度优先排序,通过 traverseNode 方法,完成排序逻辑
    2. 通过保存在 nodeTransforms 中的 transformXXX 方法,针对不同的节点,完成不同的处理
  2. 期间创建的 context 上下文,承担了一个全局单例的作用

10:框架实现:转化 JavaScript AST,构建深度优先的 AST 转化逻辑

明确好了 transform 的大致逻辑之后,这一小节我们就开始实现一下对应的代码,我们代码的逻辑实现我们分成两个小节来讲:

  1. 深度优先排序
  2. 完成具体的节点转化

这一小节,我们先来完成深度优先排序:

  1. packages/compiler-core/src/compile.tsbaseCompile 中,增加 transform 的方法触发:

    js
    export function baseCompile(template: string, options = {}) {
    	const ast = baseParse(template)
    
    	transform(
    		ast,
    		extend(options, {
    			nodeTransforms: [transformElement, transformText]
    		})
    	)
    	console.log(JSON.stringify(ast))
    
    	return {}
    }
  2. 创建 packages/compiler-core/src/transforms/transformElement.ts 模块,导出 transformElement 方法:

    js
    /**
     * 对 element 节点的转化方法
     */
    export const transformElement = (node, context) => {
    	return function postTransformElement() {
    		
    	}
    }
  3. 创建 packages/compiler-core/src/transforms/transformText.ts 模块,导出 transformText 方法:

    js
    export const transformText = (node, context) => {
    	if (
    		node.type === NodeTypes.ROOT ||
    		node.type === NodeTypes.ELEMENT ||
    		node.type === NodeTypes.FOR ||
    		node.type === NodeTypes.IF_BRANCH
    	) {
    		return () => {
    		}
    	}
    }
  4. 创建 packages/compiler-core/src/transform.ts 模块,创建 transform 方法:

    js
    /**
     * 根据 AST 生成 JavaScript AST
     * @param root AST
     * @param options 配置对象
     */
    export function transform(root, options) {
    	// 创建 transform 上下文
    	const context = createTransformContext(root, options)
      // 按照深度优先依次处理 node 节点转化
      traverseNode(root, context)
    }
  5. 创建 createTransformContext 生成上下文对象:

    js
    /**
     * transform 上下文对象
     */
    export interface TransformContext {
    	/**
    	 * AST 根节点
    	 */
    	root
    	/**
    	 * 每次转化时记录的父节点
    	 */
    	parent: ParentNode | null
    	/**
    	 * 每次转化时记录的子节点索引
    	 */
    	childIndex: number
    	/**
    	 * 当前处理的节点
    	 */
    	currentNode
    	/**
    	 * 协助创建 JavaScript AST 属性 helpers,该属性是一个 Map,key 值为 Symbol(方法名),表示 render 函数中创建 节点 的方法
    	 */
    	helpers: Map<symbol, number>
    	helper<T extends symbol>(name: T): T
    	/**
    	 * 转化方法集合
    	 */
    	nodeTransforms: any[]
    }
    
    /**
     * 创建 transform 上下文
     */
    export function createTransformContext(
    	root,
    	{ nodeTransforms = [] }
    ): TransformContext {
    	const context: TransformContext = {
    		// options
    		nodeTransforms,
    
    		// state
    		root,
    		helpers: new Map(),
    		currentNode: root,
    		parent: null,
    		childIndex: 0,
    
    		// methods
    		helper(name) {
    			const count = context.helpers.get(name) || 0
    			context.helpers.set(name, count + 1)
    			return name
    		}
    	}
    
    	return context
    }
  6. 创建 traverseNode 方法:

    js
    /**
     * 遍历转化节点,转化的过程一定要是深度优先的(即:孙 -> 子 -> 父),因为当前节点的状态往往需要根据子节点的情况来确定。
     * 转化的过程分为两个阶段:
     * 1. 进入阶段:存储所有节点的转化函数到 exitFns 中
     * 2. 退出阶段:执行 exitFns 中缓存的转化函数,且一定是倒叙的。因为只有这样才能保证整个处理过程是深度优先的
     */
    export function traverseNode(node, context: TransformContext) {
    	// 通过上下文记录当前正在处理的 node 节点
    	context.currentNode = node
    	// 获取当前所有 node 节点的 transform 方法
    	const { nodeTransforms } = context
    	// 存储转化函数的数组
    	const exitFns: any = []
    	// 循环获取节点的 transform 方法,缓存到 exitFns 中
    	for (let i = 0; i < nodeTransforms.length; i++) {
    		const onExit = nodeTransforms[i](node, context)
    		if (onExit) {
    			exitFns.push(onExit)
    		}
    	}
    
    	// 继续转化子节点
    	switch (node.type) {
    		case NodeTypes.ELEMENT:
    		case NodeTypes.ROOT:
    			traverseChildren(node, context)
    			break
    	}
    
    	// 在退出时执行 transform
    	context.currentNode = node
    	let i = exitFns.length
    	while (i--) {
    		exitFns[i]()
    	}
    }
    
    /**
     * 循环处理子节点
     */
    export function traverseChildren(parent, context: TransformContext) {
    	parent.children.forEach((node, index) => {
    		context.parent = parent
    		context.childIndex = index
    		traverseNode(node, context)
    	})
    }

那么至此,一个按照深度优先依次处理 node 节点转化的逻辑就已经完成

11:框架实现:构建 transformXXX 方法,转化对应节点

在上一小节,我们会依次触发 exitFns[i]() 方法,我们知道这些方法其实是 transformXXX 方法,那么我们依次进行实现:

首先是 transformElement 方法:

  1. packages/compiler-core/src/transforms/transformElement.ts 模块中实现 transformElement 方法:

    js
    /**
     * 对 element 节点的转化方法
     */
    export const transformElement = (node, context) => {
    	return function postTransformElement() {
    		node = context.currentNode!
    
    		// 仅处理 ELEMENT 类型
    		if (node.type !== NodeTypes.ELEMENT) {
    			return
    		}
    
    		const { tag } = node
    
    		let vnodeTag = `"${tag}"`
    		let vnodeProps = []
    		let vnodeChildren = node.children
    
    		node.codegenNode = createVNodeCall(
    			context,
    			vnodeTag,
    			vnodeProps,
    			vnodeChildren
    		)
    	}
    }
  2. packages/compiler-core/src/ast.ts 中,创建 createVNodeCall 方法:

    js
    export function createVNodeCall(context, tag, props?, children?) {
    	if (context) {
    		context.helper(CREATE_ELEMENT_VNODE)
    	}
    
    	return {
    		type: NodeTypes.VNODE_CALL,
    		tag,
    		props,
    		children
    	}
    }
  3. 创建 packages/compiler-core/src/runtimeHelpers.ts 模块:

    js
    export const CREATE_ELEMENT_VNODE = Symbol('createElementVNode')
    export const CREATE_VNODE = Symbol('createVNode')
    
    /**
     * const {xxx} = Vue
     * 即:从 Vue 中可以被导出的方法,我们这里统一使用  createVNode
     */
    export const helperNameMap = {
    	// 在 renderer 中,通过 export { createVNode as createElementVNode }
    	[CREATE_ELEMENT_VNODE]: 'createElementVNode',
    	[CREATE_VNODE]: 'createVNode'
    }

其次是 transformText 方法:

  1. packages/compiler-core/src/transforms/transformText.ts 中,完成 transformText 方法:

    js
       /**
        * 将相邻的文本节点和表达式合并为一个表达式。
        *
        * 例如:
        * <div>hello {{ msg }}</div>
        * 上述模板包含两个节点:
        * 1. hello:TEXT 文本节点
        * 2. {{ msg }}:INTERPOLATION 表达式节点
        * 这两个节点在生成 render 函数时,需要被合并: 'hello' + _toDisplayString(_ctx.msg)
        * 那么在合并时就要多出来这个 + 加号。
        * 例如:
        * children:[
        * 	{ TEXT 文本节点 },
        *  " + ",
        *  { INTERPOLATION 表达式节点 }
        * ]
        */
       export const transformText = (node, context) => {
       	if (
       		node.type === NodeTypes.ROOT ||
       		node.type === NodeTypes.ELEMENT ||
       		node.type === NodeTypes.FOR ||
       		node.type === NodeTypes.IF_BRANCH
       	) {
       		return () => {
       			// 获取所有的子节点
       			const children = node.children
       			// 当前容器
       			let currentContainer
       			// 循环处理所有的子节点
       			for (let i = 0; i < children.length; i++) {
       				const child = children[i]
       				if (isText(child)) {
       					// j = i + 1 表示下一个节点
       					for (let j = i + 1; j < children.length; j++) {
       						const next = children[j]
       						// 当前节点 child 和 下一个节点 next 都是 Text 节点
       						if (isText(next)) {
       							if (!currentContainer) {
       								// 生成一个复合表达式节点
       								currentContainer = children[i] = createCompoundExpression(
       									[child],
       									child.loc
       								)
       							}
       							// 在 当前节点 child 和 下一个节点 next 中间,插入 "+" 号
       							currentContainer.children.push(` + `, next)
       							// 把下一个删除
       							children.splice(j, 1)
       							j--
       						}
       						// 当前节点 child 是 Text 节点,下一个节点 next 不是 Text 节点,则把 currentContainer 置空即可
       						else {
       							currentContainer = undefined
       							break
       						}
       					}
       				}
       			}
       		}
       	}
       }
  2. packages/compiler-core/src/ast.ts 中,创建 createCompoundExpression 方法:

    js
       /**
        * return hello {{ msg }} 复合表达式
        */
       export function createCompoundExpression(children, loc) {
       	return {
       		type: NodeTypes.COMPOUND_EXPRESSION,
       		loc,
       		children
       	}
       }
  3. 创建 packages/compiler-core/src/utils.ts模块,创建 isText 方法:

    js
     export function isText(node) {
      return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
     }

至此,两个 transformXXX 方法,都已经创建完成。

此时创建测试实例:

html
<script>
  const { compile } = Vue
  // 创建 template
  const template = `<div> hello world </div>`

  // 生成 render 函数
  const renderFn = compile(template)
</script>

应该可以打印出 root 之外的 childrencodegen

12:框架实现:处理根节点的转化,生成 JavaScript AST

那么最后我们就只剩下根节点的处理了。

  1. transform 方法中:

    js
    export function transform(root, options) {
    	....
    	createRootCodegen(root)
    	root.helpers = [...context.helpers.keys()]
    	root.components = []
    	root.directives = []
    	root.imports = []
    	root.hoists = []
    	root.temps = []
    	root.cached = []
    }
  2. 创建 createRootCodegen 方法:

    js
       /**
        * 生成 root 节点下的 codegen
        */
       function createRootCodegen(root) {
       	const { children } = root
       
       	// 仅支持一个根节点的处理
       	if (children.length === 1) {
       		// 获取单个根节点
       		const child = children[0]
       		if (isSingleElementRoot(root, child) && child.codegenNode) {
       			const codegenNode = child.codegenNode
       			root.codegenNode = codegenNode
       		}
       	}
       }
  3. 创建 packages/compiler-core/src/hoistStatic.ts 模块,创建 isSingleElementRoot 方法:

    js
       /**
        * 单个元素的根节点
        */
       export function isSingleElementRoot(root, child) {
       	const { children } = root
       	return children.length === 1 && child.type === NodeTypes.ELEMENT
       }

此时,整个 transform 处理完成。运行测试实例,可以得到如下打印:

json
{
  type: 0,
  children: [
    {
      type: 1,
      tag: 'div',
      tagType: 0,
      props: [],
      children: [{ type: 2, content: ' hello world ' }],
      codegenNode: {
        type: 13,
        tag: '"div"',
        props: [],
        children: [{ type: 2, content: ' hello world ' }]
      }
    }
  ],
  loc: {},
  codegenNode: {
    type: 13,
    tag: '"div"',
    props: [],
    children: [{ type: 2, content: ' hello world ' }]
  },
  helpers: [null],
  components: [],
  directives: [],
  imports: [],
  hoists: [],
  temps: [],
  cached: []
}

我们可以把如上打印放入到 vue 源代码中的 packages/compiler-core/src/compile.tsbaseCompile 方法中。

注意: 需要把 helpers: [null] 改为 helpers: [CREATE_ELEMENT_VNODE]

此时,在 vue 源码中运行如下测试实例:

html
<script>
  const { compile, h, render } = Vue
  // 创建 template
  const template = ``

  // 生成 render 函数
  const renderFn = compile(template)

  // 创建组件
  const component = {
    render: renderFn
  }

  // 通过 h 函数,生成 vnode
  const vnode = h(component)

  // 通过 render 函数渲染组件
  render(vnode, document.querySelector('#app'))
</script>

发现可以正常渲染 <div>hello world</div>

13:扩展知识:render 函数的生成方案

当我们得到了 JavaScript AST 之后,下面我们就可以生成对应的 render 函数了。

那么我们如何根据 JavaScript AST 来生成对应的 render 函数呢?

我们先来看 vue 源码生成的 render

  1. packages/compiler-core/src/compile.ts 中的 baseCompile 方法下,使用此代码(咱们自己生成的 JavaScript AST ):

    js
    return generate(
      {
        type: 0,
        children: [
          {
            type: 1,
            tag: 'div',
            tagType: 0,
            props: [],
            children: [{ type: 2, content: ' hello world ' }],
            codegenNode: {
              type: 13,
              tag: '"div"',
              props: [],
              children: [{ type: 2, content: ' hello world ' }]
            }
          }
        ],
        loc: {},
        codegenNode: {
          type: 13,
          tag: '"div"',
          props: [],
          children: [{ type: 2, content: ' hello world ' }]
        },
        // 此处需要主动写入
        helpers: [CREATE_ELEMENT_VNODE],
        components: [],
        directives: [],
        imports: [],
        hoists: [],
        temps: [],
        cached: []
      },
      extend({}, options, {
        prefixIdentifiers
      })
    )

    代替原有的 generate 方法调用。

    packages/compiler-core/src/codegen.ts 文件中的 generate 方法的最后位置,打印 context.code

    js
    console.log(context.code)

运行测试实例,可以得到如下打印:

js
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createElementVNode: _createElementVNode } = _Vue
    return _createElementVNode("div", [], [" hello world "])
  }
}

该函数就是通过 generate 方法转化得到的 render 函数,在该 render 中存在一个 with (_ctx) 这个代码在我们最终期望得到的 render 函数中是不需要的。所以我们最终期望得到的 render 函数为:

js
const _Vue = Vue

return function render(_ctx, _cache) {
    const { createElementVNode: _createElementVNode } = _Vue
    return _createElementVNode("div", [], [" hello world "])
}

那么下面我们来分析一下上面这个函数的生成,即:生成方案。

函数的生成方案,分为三部分:

  1. 函数本质上就是一段字符
  2. 字符串的拼接方式
  3. 字符串拼接的格式处理

函数本质上就是一段字符

函数本质上就是一段字符,所以我们可以把以上函数比较一个大的 字符串

那么想要生成这样的一个大字符串,本质上就是各个小的字符串的拼接。

例如,我们可以期望如下的拼接:

js
context.code = `
	const _Vue = Vue \n\n return function render(_ctx, _cache) { \n\n const { createElementVNode: _createElementVNode } = _Vue \n\n return _createElementVNode("div", [], [" hello world "]) \n\n }
`

把以上字符串处理之后,我们就可以得到一样函数格式的字符:

js
context.code = `
	const _Vue = Vue
  
  return function render(_ctx, _cache) {
  	const { createElementVNode: _createElementVNode } = _Vue
    return _createElementVNode("div", [], [" hello world "]) 
  }
`

字符拼接的方式

当我们明确好了函数本身就是字符,这样的概念之后,那么接下来就是如何拼接这样的字符。

我们把上面的函数分成 4 个部分:

  1. 函数的前置代码:const _Vue = Vue

  2. 函数名:function render

  3. 函数的参数:_ctx, _cache

  4. 函数体:

    js
    const { createElementVNode: _createElementVNode } = _Vue
    return _createElementVNode("div", [], [" hello world "])

我们只需要把以上的内容拼接到一起,那么就可以得到最终的目标结果。

那么为了完成对应的拼接,我们可以提供一个 push 函数:

js
function push (code) {
	context.code += code
}

以此来完成对应的拼接

关于字符串的格式

在去处理这样的一个字符串的过程中,我们不光需要处理拼接,还需要处理对应的格式问题,比如:

js
context.code = `
	const _Vue = Vue
  (换行)
  return function render(_ctx, _cache) {
 (缩进)const { createElementVNode: _createElementVNode } = _Vue
    return _createElementVNode("div", [], [" hello world "]) 
  }
`

对于字符串而言,我们知道换行可以通过 \n 来进行表示,缩进就是 空格的处理。

所以我们需要再提供对应的方法,来进行对应的处理,比如:

js
context.indentLevel = 0 // 表示缩进

// 换行
function newline(n: number) {
		newline(context.indentLevel)
}

// 缩进+换行
function indent(n: number) {
		newline(++context.indentLevel)
}

// 取消缩进 + 换行
function deindent(n: number) {
		newline(--context.indentLevel)
}

function newline(n: number) {
		context.code += '\n' + `  `.repeat(n)
}

14:源码阅读:编译器第三步:生成 render 函数

我们知道生成 render 函数的代码,主要是 packages/compiler-core/src/codegen.ts 中的 generate 方法,所以我们可以直接在该方法中打断点,进入 debugger注意:此时我们使用的是 vuex-next-mini 生成 JavaScript AST):

  1. 进入 generate 方法:

  2. 执行 const context = createCodegenContext(ast, options) 得到 context 上下文:

    1. 进入 createCodegenContext 方法,对于 context 而言,我们现在是比较熟悉的了,知道它就是一个全局变量

    2. 观察该方法,可以发现 context 内部存在很多属性和方法,这些属性和方法很多,但是我们不需要全部关注,只需要关注如下内容即可:

      js
      	const context = {
      		// render 函数代码字符串
      		code: ``,
      		// 运行时全局的变量名
      		runtimeGlobalName: 'Vue',
      		// 模板源
      		source: ast.loc.source,
      		// 缩进级别
      		indentLevel: 0,
      		// 需要触发的方法,关联 JavaScript AST 中的 helpers
      		helper(key) {
      			return `_${helperNameMap[key]}`
      		},
      		/**
      		 * 插入代码
      		 */
      		push(code) {
      			context.code += code
      		},
      		/**
      		 * 新的一行
      		 */
      		newline() {
      			newline(context.indentLevel)
      		},
      		/**
      		 * 控制缩进 + 换行
      		 */
      		indent() {
      			newline(++context.indentLevel)
      		},
      		/**
      		 * 控制缩进 + 换行
      		 */
      		deindent() {
      			newline(--context.indentLevel)
      		}
      	}
    3. 这些代码相对而言,比较简单,我们在上一小节也提到过对应的作用,这里就不在赘述了。

  3. 执行完成该方法之后,我们可以得到一个 context.code 目前值为 “”

  4. 接下来的代码执行,就是不断往 context.code 填充内容的过程

  5. 代码执行 genFunctionPreamble(ast, preambleContext)

    1. 进入 genFunctionPreamble 方法
    2. 执行 if (ast.helpers.length > 0) 满足条件
      1. 执行 push(const _Vue = ${VueBinding}\n)
      2. 当前的 VueBinding = Vue,所以以上等同于 push(const _Vue = Vue\n)
      3. 此时,context.code = "const _Vue = Vue\n"
    3. 执行 newline(),此时,context.code = "const _Vue = Vue\n\n"
    4. 执行 push(return),此时,context.code = "const _Vue = Vue\n\nreturn"
  6. genFunctionPreamble 执行完成,此时,context.code = "const _Vue = Vue\n\nreturn"

  7. 代码继续执行,生成 functionNameargs

  8. 执行 push(function functionName(signature}) {),此时,context.code = "“const _Vue = Vue\n\nreturn function render(_ctx, _cache) {”"

  9. 执行 indent() ,此时,context.code = "const _Vue = Vue\n\nreturn function render(_ctx, _cache) {\n "

  10. 执行 push(with (_ctx) {)

    1. 此时,context.code = "const _Vue = Vue\n\nreturn function render(_ctx, _cache) {\n with (_ctx) {"
  11. 执行 indent() 。此时,context.code = "const _Vue = Vue\n\nreturn function render(_ctx, _cache) {\n with (_ctx) { \n"

  12. 执行:

    js
    push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = _Vue`)
    push(`\n`)
    newline()
  13. 此时,

    js
    context.code =  "const _Vue = Vue\n\nreturn function render(_ctx, _cache) {\n  with (_ctx) {\n    const { createElementBlock: _createElementBlock } = _Vue\n\n    "
  14. 执行 push(return )

  15. 此时:

    js
    context.code =  "const _Vue = Vue\n\nreturn function render(_ctx, _cache) {\n  with (_ctx) {\n    const { createElementBlock: _createElementBlock } = _Vue\n\n    return"
  16. 那么到此为止,对于 code 而言,就只剩下最后一块内容,也就是 :

    js
    _createElementVNode("div", [], [" hello world "])
  17. 而这里,也是整个 generate 最复杂的一块逻辑

  18. 这块逻辑由 genNode(ast.codegenNode, context) 开始,我们进入到 genNode 方法

    1. 进入 genNode 方法,目前的参数 node 为:

      image-20230813131145602

    2. 代码执行 switch

      1. 当前的 type13,对应 case NodeTypes.VNODE_CALL

      2. 所以触发 genVNodeCall 方法

        1. 进入 genVNodeCall 方法

        2. 代码执行 const callHelper: symbol = xxx,这里的 isBlock = undefined,所以会触发 getVNodeHelper(context.inSSR, isComponent) 方法:

          1. 进入 getVNodeHelper 方法:

          2. 该方法的内部执行非常简单:

            js
            return ssr || isComponent ? CREATE_VNODE : CREATE_ELEMENT_VNODE
          3. 返回了两个 Symbol,分别对应 createVNodecreateElementVNode 方法

          4. 此处返回 CREATE_ELEMENT_VNODE

        3. 执行 push(helper(callHelper) +(, node)

        4. 此时:

          js
          context.code =  "const _Vue = Vue\n\nreturn function render(_ctx, _cache) {\n  with (_ctx) {\n    const { createElementBlock: _createElementBlock } = _Vue\n\n    return  _createElementVNode("
        5. 接下来我们就需要为 方法填充参数:

        6. 执行 const args = genNullableArgs(...)

          1. 进入 genNullableArgs 方法,此时的 arg 参数为:

            image-20230813131343202

          2. 执行 for 循环,最终返回值为:

            image-20230813131407698

        7. 代码执行 genNodeList(args, context)处理参数的 push

          1. 执行 for 循环,循环会被触发 3 次:

            1. 第一次触发: node = 'div'

              1. 直接执行 push(node)
              2. 执行 push(', ')
            2. 第二次触发:node =[]

              1. 执行 genNodeListAsArray(node, context)
                1. 执行 context.push([)
                2. 执行 context.push(])
              2. 跳出方法,执行 push(', ')
            3. 第三次触发:

              image-20230813131559738

              1. 执行 genNodeListAsArray(node, context)

                1. 执行 context.push([)

                2. 执行 genNodeList(nodes, context, multilines)

                  1. 通常触发 for 循环,此时: node 的值为:

                    image-20230813131648657

              2. 执行 genNode(node, context)

                1. 进入 genNode
                2. 执行 genText(node, context)
                3. 进入 genText
                4. 执行 context.push(JSON.stringify(node.content), node) 插入
                5. 执行 context.push(])
                6. 跳出方法,执行 push(', ')
        8. 整个 genNodeList 方法执行完成

        9. 此时:

          js
          context.code = "const _Vue = Vue
          
          return function render(_ctx, _cache) {
            with (_ctx) {
              const { createElementVNode: _createElementVNode } = _Vue
              return _createElementVNode("div", [], [" hello world "]"
  19. 最后返回到 generate 方法,处理最后的文本即可

那么由以上代码可知:

  1. 整个的 generate 处理就已经完成了,其中最复杂的部分就是 函数参数的拼接,即 genNode(ast.codegenNode, context) 的处理
    1. 这里的处理会涉及到一个迭代的循环处理,根据 [tag, props, children, patchFlag, dynamicProps] 的值来进行循环的参数处理
  2. 而对于其他的内容而言,本质上就只是一个 字符串的拼接,这些拼接将通过 context 上下文对象中的方法:
    1. push
    2. newine
    3. indent
    4. deindent
  3. 来进行实现。

15:框架实现:构建 CodegenContext 上下文对象

对于 generate 的构建,我们将分成两部分来进行实现:

  1. 构建 context 上下文对象
  2. 利用 context 完成函数拼接

那么这一小节,我们先实现第一部分

  1. packages/compiler-core/src/compile.tsbaseCompile 方法中,完成 generate 的调用:

    js
    export function baseCompile(template: string, options = {}) {
    	...
    	return generate(ast)
    }
  2. 创建 packages/compiler-core/src/codegen.ts 模块,构建 generatecreateCodegenContext 方法:

    js
    /**
     * 根据 JavaScript AST 生成
     */
    export function generate(ast) {
    	// 生成上下文 context
    	const context = createCodegenContext(ast)
    
    	// 获取 code 拼接方法
    	const { push, newline, indent, deindent } = context
    
    	...
    }
    js
    function createCodegenContext(ast) {
    	const context = {
    		// render 函数代码字符串
    		code: ``,
    		// 运行时全局的变量名
    		runtimeGlobalName: 'Vue',
    		// 模板源
    		source: ast.loc.source,
    		// 缩进级别
    		indentLevel: 0,
    		// 需要触发的方法,关联 JavaScript AST 中的 helpers
    		helper(key) {
    			return `_${helperNameMap[key]}`
    		},
    		/**
    		 * 插入代码
    		 */
    		push(code) {
    			context.code += code
    		},
    		/**
    		 * 新的一行
    		 */
    		newline() {
    			newline(context.indentLevel)
    		},
    		/**
    		 * 控制缩进 + 换行
    		 */
    		indent() {
    			newline(++context.indentLevel)
    		},
    		/**
    		 * 控制缩进 + 换行
    		 */
    		deindent() {
    			newline(--context.indentLevel)
    		}
    	}
    
    	function newline(n: number) {
    		context.code += '\n' + `  `.repeat(n)
    	}
    
    	return context
    }

那么至此,我们就完成了 CodegenContext 上下文对象的构建

16:框架实现:解析 JavaScript AST,拼接 render 函数

我们最终解析之后的目标函数如下:

js
const _Vue = Vue

return function render(_ctx, _cache) {
    const { createElementVNode: _createElementVNode } = _Vue

    return _createElementVNode("div", [], [" hello world "])
}

依次,首先我们先生成 除参数之外 部分:

  1. generate 中:

    js
     /**
      * 根据 JavaScript AST 生成
      */
     export function generate(ast) {
     	// 生成上下文 context
     	const context = createCodegenContext(ast)
     
     	// 获取 code 拼接方法
     	const { push, newline, indent, deindent } = context
     
     	// 生成函数的前置代码:const _Vue = Vue
     	genFunctionPreamble(context)
     
     	// 创建方法名称
     	const functionName = `render`
     	// 创建方法参数
     	const args = ['_ctx', '_cache']
     	const signature = args.join(', ')
     
     	// 利用方法名称和参数拼接函数声明
     	push(`function ${functionName}(${signature}) {`)
     
     	// 缩进 + 换行
     	indent()
     
     	// 明确使用到的方法。如:createVNode
     	const hasHelpers = ast.helpers.length > 0
     	if (hasHelpers) {
     		push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = _Vue`)
     		push(`\n`)
     		newline()
     	}
     
     	// 最后拼接 return 的值
     	newline()
     	push(`return `)
     
     	....
     }
  2. 创建 genFunctionPreamble 方法:

    js
     /**
      * 生成 "const _Vue = Vue\n\nreturn "
      */
     function genFunctionPreamble(context) {
     	const { push, newline, runtimeGlobalName } = context
     
     	const VueBinding = runtimeGlobalName
     	push(`const _Vue = ${VueBinding}\n`)
     
     	newline()
     	push(`return `)
     }
  3. 创建 aliasHelper

    js
    const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
  4. 运行此时的代码,我们应该可以得到这样的函数生成:

    js
    const _Vue = Vue
    
    return function render(_ctx, _cache) {
      const { createElementVNode: _createElementVNode } = _Vue
      return

那么接下来我们就处理最后 renturn 函数的部分:

  1. 补全 generate 中的代码:

    js
     /**
      * 根据 JavaScript AST 生成
      */
     export function generate(ast) {
     	...
     	// 处理 renturn 结果。如:_createElementVNode("div", [], [" hello world "])
     	if (ast.codegenNode) {
     		genNode(ast.codegenNode, context)
     	} else {
     		push(`null`)
     	}
     
     	// 收缩缩进 + 换行
     	deindent()
     	push(`}`)
     
     	return {
     		ast,
     		code: context.code
     	}
     }
  2. 创建 genNode 函数

    js
     /**
      * 区分节点进行处理
      */
     function genNode(node, context) {
     	switch (node.type) {
     		case NodeTypes.VNODE_CALL:
     			genVNodeCall(node, context)
     			break
     		case NodeTypes.TEXT:
     			genText(node, context)
     			break
     	}
     }
  3. 创建 genText 函数:

    js
     /**
      * 处理 TEXT 节点
      */
     function genText(node, context) {
     	context.push(JSON.stringify(node.content), node)
     }
  4. 创建 genVNodeCall 函数:

    js
     /**
      * 处理 VNODE_CALL 节点
      */
     function genVNodeCall(node, context) {
     	const { push, helper } = context
     	const { tag, props, children, patchFlag, dynamicProps, isComponent } = node
     
     	// 返回 vnode 生成函数
     	const callHelper = getVNodeHelper(context.inSSR, isComponent)
     	push(helper(callHelper) + `(`, node)
     
     	// 获取函数参数
     	const args = genNullableArgs([tag, props, children, patchFlag, dynamicProps])
     
     	// 处理参数的填充
     	genNodeList(args, context)
     
     	push(`)`)
     }
  5. 创建 packages/compiler-core/src/utils.ts 模块,添加 getVNodeHelper 方法:

    js
     /**
      * 返回 vnode 生成函数
      */
     export function getVNodeHelper(ssr: boolean, isComponent: boolean) {
     	return ssr || isComponent ? CREATE_VNODE : CREATE_ELEMENT_VNODE
     }
  6. 创建 genNullableArgs 函数:

    js
     /**
      * 处理 createXXXVnode 函数参数
      */
     function genNullableArgs(args: any[]) {
     	let i = args.length
     	while (i--) {
     		if (args[i] != null) break
     	}
     	return args.slice(0, i + 1).map((arg) => arg || `null`)
     }
  7. 创建 genNodeList 函数

    js
     /**
      * 处理参数的填充
      */
     function genNodeList(nodes, context) {
     	const { push, newline } = context
     	for (let i = 0; i < nodes.length; i++) {
     		const node = nodes[i]
     		// 字符串直接 push 即可
     		if (isString(node)) {
     			push(node)
     		}
     		// 数组需要 push "[" "]"
     		else if (isArray(node)) {
     			genNodeListAsArray(node, context)
     		}
     		// 对象需要区分 node 节点类型,递归处理
     		else {
     			genNode(node, context)
     		}
     		if (i < nodes.length - 1) {
     			push(', ')
     		}
     	}
     }
  8. 创建 genNodeListAsArray 函数:

    js
    function genNodeListAsArray(nodes, context) {
    	context.push(`[`)
    	genNodeList(nodes, context)
    	context.push(`]`)
    }

至此函数生成完成。接下来我们就来测试一下函数是否可用。

创建如下测试实例:

js
<script>
  const { compile, h, render } = Vue
  // 创建 template
  const template = `<div> hello world </div>`

  // 生成 render 函数
  const { code } = compile(template)

  console.log(code);

  const renderFn = new Function(code)()


  // 创建组件
  const component = {
    render: renderFn
  }

  // 通过 h 函数,生成 vnode
  const vnode = h(component)

  // 通过 render 函数渲染组件
  render(vnode, document.querySelector('#app'))
</script>

打印当前的 code 为:

js
const _Vue = Vue

return function render(_ctx, _cache) {
  const { createElementVNode: _createElementVNode } = _Vue
  return _createElementVNode("div", [], [" hello world "])
}

由以上代码可知,render 函数使用到了 createElementVNode 方法,所以我们需要在 runtime 时,导出该方法:

  1. packages/runtime-core/src/vnode.ts 中,新增:

    js
    // createElementVNode 实际调用的是  createVNode
    export { createVNode as createElementVNode }
  2. packages/runtime-core/src/index.ts 中,增加 createElementVNode 的导出:

    js
    export { ..., createElementVNode } from './vnode'
  3. packages/vue/src/index.ts 中,增加 createElementVNode 的导出

    js
    export {
    	...
    	createElementVNode
    } from '@vue/runtime-core'

此时,浏览器中,应该可以成功渲染。

17:框架实现:新建 compat 模块,把 render 转化为 function

此时,我们的 render 函数构建,已经可以完成了。但是我们当前的 render 本质上还是一个 字符串 ,所以我们需要通过 new Function 来把它变为函数。

那么这样的一个 new Function 的过程,我们其实可以在 vue 中完成。

  1. 创建 packages/vue-compat/src/index.ts 模块

  2. 新增 compileToFunction 方法:

    js
    import { compile } from '@vue/compiler-dom'
    
    function compileToFunction(template, options?) {
    	const { code } = compile(template, options)
    
    	const render = new Function(code)()
    
    	return render
    }
    export { compileToFunction as compile }
  3. packages/vue/src/index.ts 中修改 compile 的导出:

    js
    // export { compile } from '@vue/compiler-dom'
    export { compile } from '@vue/vue-compat'
  4. 修改测试实例:

    html
    <script>
      const { compile, h, render } = Vue
      // 创建 template
      const template = `<div> hello world </div>`
    
      // 生成 render 函数
      const renderFn = compile(template)
    
      // 创建组件
      const component = {
        render: renderFn
      }
    
      // 通过 h 函数,生成 vnode
      const vnode = h(component)
    
      // 通过 render 函数渲染组件
      render(vnode, document.querySelector('#app'))
    </script>

至此,compile 处理完成。

18:总结

到这里我们就已经完成了一个基础的编辑器处理。

我们知道整个编辑器的处理过程分成了三部分:

  1. 解析模板 templateAST
    1. 在这一步过程中,我们使用了
      1. 有限自动状态机解析模板得到了 tokens
      2. 通过扫描 tokens 最终得到了 AST
  2. 转化 ASTJavaScript AST
    1. 这一步是为了最终生成 render 函数做准备
    2. 利用了深度优先的方式,进行了自下向上的逐层转化
  3. 生成 render 函数
    1. 这一步是最后的解析环节,我们需要对 JavaScript AST 进行处理,得到最终的 render 函数

整个一套编辑器的流程非常复杂,我们目前只完成了最基础的编辑逻辑,目前只能支持 <div>文本</div> 的处理。那么如果我们想要处理更复杂的逻辑,比如:

  1. 响应性数据
  2. 多个子节点
  3. 指令

的话,对比编辑器而言,还需要做更多的事情才可以。

下一章,我们会深入编辑器,来看一下,以上问题应该如何进行处理。

Released under the MIT License.