"8 minutos para aprender el principio de Vue.js": 2. Principio de implementación de render() de compilación AST

En la sección anterior "1. Compilar cadenas de plantillas en el árbol de sintaxis abstracta AST" , implementamos la lógica de templaterepresentación AST, que está un paso más cerca del objetivo final de "representar DOM real".

En esta sección, continuemos implementando la compilación AST en la función de renderizado render().

Objetivos de esta sección

  • Compile el árbol de sintaxis abstracta AST en una función de representaciónrender()

Código completo: Ejemplo en línea: Compilación AST del árbol de sintaxis abstracta para la función de representación render() - JS Bin

imagen

Es decir , el objeto AST que compilamos en la sección anterior :

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

Compilar para representar la función render():

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

No es necesario entender el significado de la función de renderizado por el momento, y lo analizaremos más a fondo más adelante.

¿Qué es una función de renderizado render()?

Las funciones de representación son los medios intermedios de AST a los nodos DOM virtuales. Son esencialmente funciones JS. Después de la ejecución, devolverán objetos de nodos virtuales en función del "tiempo de ejecución".

En Vue.js 2, los nodos DOM virtuales se obtienen mediante la ejecución de "funciones de representación", que se utilizan para el nodo virtual Diff y finalmente generan DOM real.

Enlace de origen de Vue.js: lifecycle.js#L189-L191

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
复制代码

En las tres líneas de código fuente anteriores, vm._render()se llama a la "función de representación" y el valor de retorno es el "nodo DOM virtual".

Después de pasar el nodo virtual DOM como parámetro vm._update(), comienza el famoso "Virtual DOM Diff".

principio fundamental

1. Convierta el cuerpo de la función de cadena en una función

Al escribir JS, podemos crear funciones en forma de 声明o .表达式

Pero para "crear funciones" durante la ejecución de JS, necesitamos new Function()APIs , es decir, los constructores de funciones en 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))])}

Para convertir los atributos que todavía son cuerpos de funciones de cadena renderen funciones ejecutables, agregamos otra pieza de new Function(code)lógica,

Y coloque la createFunction (code)declaración en la VueCompilerclase para una llamada final fácil:

createFunction (code) {
  try {
    return new Function(code)
  } catch (err) {
    throw err
  }
}
复制代码

Finalmente, unifiquemos la llamada.

Agrega una instancia y llama a la VueCompilerclase :compile(template)CodeGeneratorthis.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
  }
}
复制代码

En base a lo que hemos escrito en la sección anterior this.compiler.compile(this.options.template), finalmente podemos ver la función de renderizado impresa por la consola render() = :

image.png

Código completo: Ejemplo en línea: Compilación AST del árbol de sintaxis abstracta para la función de representación render() - JS Bin


Serie "8 minutos para aprender el principio de Vue.js" , un total de 5 partes:

La actualización está en pleno apogeo, bienvenido a comunicarse ~ Bienvenido a actualizar urgentemente ~

Supongo que te gusta

Origin juejin.im/post/7085375799888445477
Recomendado
Clasificación