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

《8分钟学会 Vue.js 原理》:二、AST 编译 render() 实现原理 #8

Open
JuniorTour opened this issue Feb 17, 2022 · 0 comments
Labels

Comments

@JuniorTour
Copy link
Owner

JuniorTour commented Feb 17, 2022

在上一节《一、template 字符串编译为抽象语法树 AST》中,我们实现了template渲染为AST的逻辑,距离最终目标「渲染为真实DOM」更近了一步!

这一节,我们来继续实现 AST 编译为渲染函数 render() 。

本节目标

  • 将抽象语法树 AST 编译为渲染函数render()

完整代码:在线示例:抽象语法树 AST 编译为渲染函数render() - JS Bin

image

也就是将上一节我们编译出来的 AST 对象

{
    "type": 1,
    "tag": "div",
    "children": [
        {
            "type": 2,
            "expression": "_s(msg)",
            "tokens": [
                {
                    "@binding": "msg"
                }
            ],
            "text": "{{msg}}"
        }
    ]
}

编译为渲染函数render()

function render() {
  with(this) {
   return _c('div',[_v(_s(msg))])
  }
}

暂时不必理解渲染函数的含义,后续我们会深入了解。

什么是渲染函数render()

渲染函数是 AST 到虚拟 DOM 节点的中间媒介,本质上就是 JS 的函数,执行后会基于『运行时』返回虚拟节点的对象。

在 Vue.js 2 中,通过执行「渲染函数」获得了虚拟 DOM 节点,用于虚拟节点 Diff 并最终生成真实 DOM。

Vue.js 源码链接:lifecycle.js#L189-L191

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

上述3行源码中,调用的vm._render()即是「渲染函数」,其返回值即为「虚拟 DOM 节点」。

将虚拟 DOM 节点作为参数传给vm._update()后,就开始了著名的『虚拟 DOM Diff』。

核心原理

1. 把字符串函数体转化为函数

写 JS 时,我们可以通过声明表达式的形式创造函数。

但是在 JS 的执行过程中「创造函数」我们需要new Function() API,即JS中函数的构造函数。

通过调用函数的构造函数,我们可以将「字符串」类型的函数体,转化为一个可执行的JS函数:

const func = new Function('console.log(`新函数`)')

/* 
func ===
ƒ anonymous() {
  console.log(`新函数`)
}
*/

func() // 打印 `新函数`

通过new Function()API,我们就拥有了在 JS 执行过程中生成函数体,并最终声明函数的能力。

2. 基于AST生成字符串格式的函数体

有了声明函数的能力,我们就可以把 AST 编译为「字符串格式的函数体」,再将之转化为可执行的函数。

例如,我们有一个<div />对应的 AST:

{
    "type": 1,
    "tag": "div",
    "children": [],
}

想要把 AST 编译为渲染函数的函数体:_c('div')

我们只需要对 AST 进行遍历,根据tag属性就可以拼接出想要的函数体:

function generate(ast) {
  if (ast.tag) {
    return `_c('${ast.tag}')`
  }
}

如果 AST 的children属性不为空,我们继续对其进行深度优先递归搜索,就可继续增加渲染函数的函数体,最终生成各种复杂的渲染函数,渲染出复杂的 DOM,例如:

const render = function () {
  with (this) {
    return _c(
      'div', {attrs: {"id": "app"}},
      [
        _c('h1', [_v("Hello vue-template-babel-compiler")]),
        _v(" "),
        (optional?.chaining)
          ? _c('h2', [_v("\\n      Optional Chaining enabled: " + _s(optional?.chaining) + "\\n    ")])
          : _e()
      ]
    )
  }
}

如果有兴趣,可以找到自己项目中的node_modules/vue-template-compiler/build.js第4815行:var code = generate(ast, options);
加上console.log(code)npm run serve运行后,就可以在控制台中看到自己写的.vue文件编译出的渲染函数。

具体步骤

这次的代码逻辑更加简单,总共只需要写 41 行代码。

1. 增加CodeGenerator类及其调用

我们用CodeGenerator封装编译AST为渲染函数的逻辑,其带有一个generate(ast)方法,

传入 AST 作为参数,调用后会返回带有 render() 函数作为属性值的对象:

class CodeGenerator {
    generate(ast) {
      debugger
      var code = this.genElement(ast)

      return {
        render: ("with(this){return " + code + "}"),
      }
    }
}

拼接render时的with(this) {}有什么用?

with(this)关键字就是 Vue.js 单文件组件(.vue 文件,SFC)中不用写this关键字,就能渲染出this.msg的秘密。

with 关键字文档 - MDN

通过在渲染函数中使用with(this)关键字,可以把this作为其中作用域的全局变量(类似于window, global),{}花括号内的变量都会直接取this对应的属性。

例如:

with (Math) {
  val = random()
}
console.log(val) // 调用Math.random()的返回值

2. 编译 AST 中的父元素

我们再为类添加一个genElement方法,

这个方法接受一个 AST 节点,做2件事:

  • 继续编译 AST 节点的子节点children
  • 拼接字符串,将当前 AST 节点编译为渲染函数
genElement(el) {
  var children = this.genChildren(el)
  const code = `_c('${el.tag}'${children ? `,${children}` : ''})`
  return code
}

genElement用于将AST:

{
    "type": 1,
    "tag": "div",
    "children": [],
}

编译为字符串函数体:_c('div')

3. 编译 AST 中的子元素

接下来我们编译子元素ast.children

children是一个数组,可能有多个子元素,所以我们需要对其进行.map()遍历,分别处理每一个子元素。

genChildren (el, state) {
  var children = el.children
  if (children.length) {
    return `[${children.map(c => this.genNode(c, state)).join(',')}]`
  }
}

我们再为类添加一个genElement方法,用于调用genChildren

  genElement(el) {
    debugger
    var children = this.genChildren(el)
    const code = `_c('${el.tag}'${children ? `,${children}` : ''})`
    return code
  }

4. 分别处理每一个子元素

我们用genNode(node)方法处理子元素,

生产环境中,子元素有多种,可能是文本、注释、HTML元素,所以需要用if (node.type === 2)判断类型,在分情况处理。

genNode(node) {
  if (node.type === 2) {
    return this.genText(node)
  }
  // TODO else if (node.type === otherType) {}
}

我们此次需要处理的只有「文本」(node.type === 2)这一种,所以我们再增加一个genText(text)来处理。

genText(text) {
  return `_v(${text.expression})`
}

在编译 AST 阶段,我们已经把{{msg}}编译为了一个 JS 对象:

  {
    "type": 2,
    "expression": "_s(msg)",
    "tokens": [
        {
           "@binding": "msg"
        }
    ],
    "text": "{{msg}}"
  }

现在我们只要取expression属性,就是其对应的渲染函数。

简而言之_s()是 Vue.js 内置的一个方法,可以把传入的字符串生成一个对应的虚拟 DOM 节点。

后续我们将详细介绍_s(msg)的含义及其实现。

5. 拼接为字符串函数体、生成渲染函数

经过以上各步骤,我们已将 AST 对象解析成了渲染函数的函数体字符串:with(this){return _c('div',[_v(_s(msg))])}

为了将仍然是字符串函数体的render属性,转化为可执行的函数,我们再增加一段new Function(code)逻辑,

并把createFunction (code)声明到VueCompiler类,以便于最终调用:

createFunction (code) {
  try {
    return new Function(code)
  } catch (err) {
    throw err
  }
}

最后我们来统一调用。

VueCompiler类的compile(template)中添加CodeGenerator实例及this.CodeGenerator.generate(ast)调用:

class VueCompiler {
  HTMLParser = new HTMLParser()
  CodeGenerator = new CodeGenerator()

  compile(template) {
    const ast = this.parse(template)
    console.log(`一、《template 字符串编译为抽象语法树 AST》`)
    console.log(`ast = ${JSON.stringify(ast, null, 2)}`)

    const code = this.CodeGenerator.generate(ast)
    const render = this.createFunction(code.render)
    console.log(`二、《抽象语法树 AST 编译为渲染函数 render()》`)
    console.log(`render() = ${render}`)
    return render
  }
}

基于我们前一节已经写好的this.compiler.compile(this.options.template),最终我们就能看到控制台打印出来的渲染函数render() =

image.png

完整代码:在线示例:抽象语法树 AST 编译为渲染函数render() - JS Bin


《8分钟学会 Vue.js 原理》系列,共计5部分:

正在热火朝天更新中,欢迎交流~ 欢迎催更~

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

No branches or pull requests

1 participant