Cómo se compilan las plantillas de Vue

new Vue({
  render: h => h(App)
})

.vueTodo el mundo está familiarizado con esto, llamar a renderizar obtendrá el DOM virtual correspondiente a la plantilla entrante ( archivo), entonces, ¿de dónde viene este renderizado? ¿ Cómo convierte  .vue los archivos en código reconocible por el navegador?

Hay dos formas en que la función de renderizado proviene

  • El primero es generar una función de renderizado a través de la compilación de plantillas.
  • La segunda es que nosotros mismos definimos la función render en el componente, lo que omitirá el proceso de compilación de la plantilla.

Este artículo presentará estos dos tipos, así como los principios detallados del proceso de compilación.

Comprender la compilación de plantillas

Sabemos que  <template></template> se trata de una plantilla, no de HTML real. Los navegadores no reconocen plantillas, por lo que debemos compilarla en HTML nativo que los navegadores reconozcan.

El proceso principal de esta pieza es

  1. Extraiga el HTML nativo y el HTML no nativo en la plantilla, como atributos vinculados, eventos, instrucciones, etc.
  2. Generar una función de representación después de algún procesamiento
  3. La función de representación genera el vnode correspondiente a partir del contenido de la plantilla.
  4. Luego, realice el proceso de parche (Diff) para que el vnode se represente en la vista.
  5. Finalmente, cree un nodo DOM real basado en vnode, es decir, inserte HTML nativo en la vista para completar la representación.

Los elementos 1, 2 y 3 anteriores son el proceso de compilación de plantillas

Entonces, ¿cómo compila y finalmente genera la función de renderizado?

Explicación detallada de la compilación de plantillas - código fuente

baseCompile()

Esta es la función de entrada de la compilación de plantillas, que recibe dos parámetros

  • template: es la cadena de plantilla que se va a convertir
  • options: Es el parámetro requerido para la conversión

Hay tres pasos principales en el proceso de compilación:

  1. <template></template> Análisis de plantillas: extraiga los elementos de la etiqueta, los atributos, las variables y otra información de la plantilla  a través de la regularización y otros métodos  , y analícelos en un árbol de sintaxis abstracta.AST
  2. Optimización: recorra  AST para encontrar los nodos estáticos y los nodos raíz estáticos, y agregue etiquetas
  3. Generación de código: según  AST la función de renderizado generada render

Estos tres pasos corresponden a tres funciones respectivamente, que se presentarán una por una más adelante, primero mire  baseCompile dónde se llaman en el código fuente

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string, // 就是要转换的模板字符串
  options: CompilerOptions //就是转换时需要的参数
): CompiledResult {
  // 1. 进行模板解析,并将结果保存为 AST
  const ast = parse(template.trim(), options)

  // 没有禁用静态优化的话
  if (options.optimize !== false) {
    // 2. 就遍历 AST,并找出静态节点并标记
    optimize(ast, options)
  }
  // 3. 生成渲染函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render, // 返回渲染函数 render
    staticRenderFns: code.staticRenderFns
  }
})

Solo unas pocas líneas de código, tres pasos, llamar a tres métodos es muy claro

Echemos un vistazo a lo que se devuelve al final y luego profundicemos en el código fuente de los métodos llamados en los tres pasos anteriores, para que podamos saber más claramente qué deben hacer los tres pasos.

compilar resultado

Por ejemplo, existe una plantilla de este tipo.

<template>
    <div id="app">{
   
   {name}}</div>
</template>

Imprima el resultado compilado, que es el resultado de la devolución del código fuente anterior, y vea qué es

{
  ast: {
    type: 1,
    tag: 'div',
    attrsList: [ { name: 'id', value: 'app' } ],
    attrsMap: { id: 'app' },
    rawAttrsMap: {},
    parent: undefined,
    children: [
      {
        type: 2,
        expression: '_s(name)',
        tokens: [ { '@binding': 'name' } ],
        text: '{
   
   {name}}',
        static: false
      }
    ],
    plain: false,
    attrs: [ { name: 'id', value: '"app"', dynamic: undefined } ],
    static: false,
    staticRoot: false
  },
  render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`,
  staticRenderFns: [],
  errors: [],
  tips: []
}

No importa si no entiendes, solo presta atención a lo que han hecho los tres pasos mencionados anteriormente.

  • ast Campo, que se genera en el primer paso
  • static El campo, es decir, la etiqueta, se añade  en el segundo paso según  ast el type
  • render Campo, que se genera en el tercer paso.

Tengo una impresión general, y luego miro el código fuente.

1. analizar()

Dirección de la fuente:src/complier/parser/index.js - 79行

Este método es la función principal del analizador, que extrae  <template></template> toda la  taginformación propsde children la cadena de plantilla a través de la regularización y otros métodos, y genera un objeto ast con una estructura correspondiente.

parse toma dos parámetros

  • template : es la cadena de plantilla que se va a convertir
  • options: Es el parámetro requerido para la conversión. Contiene cuatro funciones de enlace, que se utilizan para  parseHTML extraer la cadena analizada y generar la correspondiente AST

Los pasos básicos son los siguientes:

Llame a  parseHTML la función para analizar la cadena de plantilla

  • Analizar la etiqueta de inicio, la etiqueta final, el texto y los comentarios para un procesamiento diferente
  • Cuando encuentre información de texto durante el análisis, llame a la  parseText función de analizador de texto para analizar texto
  • Cuando se encuentra un filtro de inclusión durante el proceso de análisis,  parseFilters se llama a la función de analizador de filtro para analizar

Los resultados de cada paso del análisis se fusionan en un objeto (es decir, el AST final)

El código fuente de este lugar es realmente demasiado largo, hay cientos de líneas de código, así que solo publicaré un resumen. Si está interesado, puede echar un vistazo a lo anterior. Al analizar el texto, la llamada se se agregará de acuerdo con los diferentes tipos de nodos para marcar  chars() los  typenodos  AST Tipo, este atributo se utilizará en el siguiente paso de marcado

tipo tipo de nodo AST
1 nodo de elemento
2 nodo de texto dinámico que contiene variables
3 Nodos de texto sin formato sin variables

2. optimizar()

Esta función es  AST para encontrar el nodo estático y el nodo raíz estático en él, y agregar una marca, de modo que la comparación del nodo estático se omita en el  patch proceso posterior, y una copia del pasado se clone directamente, optimizando así  patch el desempeño

La función externa llamada en la función no pegará el código, el proceso general es así

  • **marcar nodo estático(markStatic)**. Es para juzgar el tipo Los tres tipos con valores de 1, 2 y 3 se presentan arriba.

    • El valor de tipo es 1: es un nodo que contiene elementos secundarios, establezca static en falso y marque los nodos secundarios recursivamente hasta que todos los nodos secundarios estén marcados
    • el valor del tipo es 2: establecer estático en falso
    • El valor de tipo es 3: es un nodo de texto sin formato que no contiene nodos secundarios ni atributos dinámicos Establézcalo en estático = verdadero, y esto se omitirá al aplicar parches, y una copia se clonará directamente
  • **Marcar nodos raíz estáticos (markStaticRoots)**, el principio aquí es básicamente el mismo que el de marcar nodos estáticos, solo los nodos que cumplen las siguientes condiciones pueden contarse como nodos raíz estáticos

    • El nodo en sí debe ser un nodo estático.
    • debe tener nodos secundarios
    • Los nodos secundarios no pueden tener un solo nodo de texto

Dirección de la fuente:src/complier/optimizer.js - 21行

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // 标记静态节点
  markStatic(root)
  // 标记静态根节点
  markStaticRoots(root, false)
}

3. generar()

Esta es la función que genera el renderizado, lo que significa que eventualmente devolverá algo como lo siguiente

// 比如有这么个模板
<template>
    <div id="app">{
   
   {name}}</div>
</template>

// 上面模板编译后返回的 render 字段 就是这样的
render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`

// 把内容格式化一下,容易理解一点
with(this){
  return _c(
    'div',
    { attrs:{"id":"app"} },
    [  _v(_s(name))  ]
  )
}

¿Te resulta familiar esta estructura?

Si comprende el DOM virtual, puede ver que el renderizado anterior es exactamente la estructura del DOM virtual, que es dividir una etiqueta en  tag, props, children, no hay nada de malo

Antes de mirar  generate el código fuente, necesitamos entender  render qué significa el último campo devuelto arriba, y luego mirar  generate el código fuente, será mucho más fácil. De lo contrario, ni siquiera sabemos qué devuelve la función. ¿Cómo podemos entender esta función?Paño de lana

prestar

Traduzcamos lo compilado arriba render

Esto  se presenta en el primer volumen de with " JavaScript You Don't Know ", que es una palabra clave utilizada para engañar al alcance léxico, lo que nos permite referirnos a múltiples propiedades en un objeto más rápido.

ver un ejemplo

const name = '掘金'
const obj = { name:'沐华', age: 18 }
with(obj){
    console.log(name) // 沐华  不需要写 obj.name 了
    console.log(age) // 18   不需要写 obj.age 了
}

with(this){} Lo anterior  es  this la instancia del componente actual. Porque al  with cambiar el puntero del atributo en el ámbito léxico, es  name necesario usarlo directamente en la etiqueta, en lugar de  this.name esto

 ¿Qué es eso  _c_v y  ?_s

Esto se define en el código fuente, el formato es: ** _c(abreviatura) =  createElement(nombre de la función)**

Dirección de la fuente:src/core/instance/render-helpers/index.js - 15行

// 其实不止这几个,由于本文例子中没有用到就没都复制过来占位了
export function installRenderHelpers (target: any) {
  target._s = toString // 转字符串函数
  target._l = renderList // 生成列表函数
  target._v = createTextVNode // 创建文本节点函数
  target._e = createEmptyVNode // 创建空节点函数
}
// 补充
_c = createElement // 创建虚拟节点函数

A ver si se aclara

with(this){ // 欺骗词法作用域,将该作用域里所有属姓和方法都指向当前组件
  return _c( // 创建一个虚拟节点
    'div', // 标签为 div
    { attrs:{"id":"app"} }, // 有一个属性 id 为 'app'
    [  _v(_s(name))  ] // 是一个文本节点,所以把获取到的动态属性 name 转成字符串
  )
}

A continuación, veamos  generate() el código fuente.

generar

Dirección de la fuente:src/complier/codegen/index.js - 43行

Este proceso es muy simple, solo unas pocas líneas de código, es primero juzgar  AST si está vacío, si no está vacío, cree un vnode de acuerdo con el AST, de lo contrario, cree un vnode con un div vacío.

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // 就是先判断 AST 是不是为空,不为空就根据 AST 创建 vnode,否则就创建一个空div的 vnode
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'

  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

Se puede ver que se  genElement() crea principalmente a través de métodos  vnode , así que echemos un vistazo a su código fuente para ver cómo se creó.

genElement()

Dirección de la fuente:src/complier/codegen/index.js - 56行

La lógica aquí sigue siendo muy clara, es decir, un montón de  if/else juicios sobre los atributos de los nodos del elemento AST pasados ​​para ejecutar diferentes funciones de generación.

También se puede encontrar aquí que otro punto de conocimiento, v-for, tiene una prioridad más alta que v-if, porque el for se juzga primero.

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) { // v-once
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) { // v-for
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) { // v-if
    return genIf(el, state)

    // template 节点 && 没有插槽 && 没有 pre 标签
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') { // v-slot
    return genSlot(el, state)
  } else {
    // component or element
    let code
    // 如果有子组件
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      // 获取元素属性 props
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }
      // 获取元素子节点
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    // 返回上面作为 with 作用域执行的内容
    return code
  }
}

Las funciones de generación llamadas por cada tipo no se enumeran una por una. En general, solo hay tres tipos de nodos vnode creados al final, nodos de elementos, nodos de texto y nodos de comentarios.

renderizado personalizado

Déjame darte un ejemplo, las tres situaciones son las siguientes

// 1. test.vue
<template>
    <h1>我是沐华</h1>
</template>
<script>
  export default {}
</script>
// 2. test.vue
<script>
  export default {
    render(h){
      return h('h1',{},'我是沐华')
    }
  }
</script>
// 3. test.js
export default {
  render(h){
    return h('h1',{},'我是沐华')
  }
}

Los tres anteriores, la representación final es exactamente la misma , porque esta  h es la que se compila después de la plantilla anterior. _c

En este momento, alguien puede preguntar, ¿por qué escribirlo usted mismo, no se genera automáticamente mediante la compilación de plantillas?

¡Buena pregunta! Definitivamente es beneficioso escribir el suyo propio.

  1. Si escribe el vnode usted mismo, omitirá la compilación de la plantilla directamente y no necesitará analizar los atributos dinámicos, eventos, instrucciones, etc. en la plantilla, por lo que el rendimiento mejorará ligeramente. Esto se refleja en la prioridad de la representación a continuación.
  2. También hay algunas situaciones que pueden hacer que nuestra escritura de código sea más flexible, más conveniente y concisa, y no será redundante.

Por ejemplo,  Element-UI hay una gran cantidad de funciones de representación escritas directamente en el código fuente de los componentes internos.

A continuación, veamos cómo se reflejan estos dos puntos.

1. Prioridad de representación

Primer vistazo a la parte sobre la compilación de plantillas en el ciclo de vida del sitio web oficial

Como puede ver en la imagen, si hay  template, no se atenderá  el , por lo que  la plantilla tiene una prioridad más alta que el , por ejemplo

¿Y qué si escribimos nuestro propio render?

<div id='app'>
    <p>{
   
   { name }}</p>
</div>
<script>
    new Vue({
        el:'#app',
        data:{ name:'沐华' },
        template:'<div>掘金</div>',
        render(h){
            return h('div', {}, '好好学习,天天向上')
        }
    })
</script>

Después de ejecutar este código, la página se representa con solo <div>好好学习,天天向上</div>

Se puede concluir que  la función render tiene mayor prioridad

Porque no importa si está  el montado,   al final  template se compilará en  una función, y si ya hay  una función, se omitirá la compilación anterior.renderrender

Esto también se refleja en el código fuente.

Encuentra la respuesta en el código fuente:dist/vue.js - 11927行

 Vue.prototype.$mount = function ( el, hydrating ) {
    el = el && query(el);
    var options = this.$options;
    // 如果没有 render 
    if (!options.render) {
      var template = options.template;
      // 再判断,如果有 template
      if (template) {
        if (typeof template === 'string') {
          if (template.charAt(0) === '#') {
            template = idToTemplate(template);
          }
        } else if (template.nodeType) {
          template = template.innerHTML;
        } else {
          return this
        }
      // 再判断,如果有 el
      } else if (el) {
        template = getOuterHTML(el);
      }
    }
    return mount.call(this, el, hydrating)
  };

2. Escritura más flexible

Por ejemplo, cuando necesitamos escribir muchos juicios si

<template>
    <h1 v-if="level === 1">
      <a href="xxx">
        <slot></slot>
      </a>
    </h1>
    <h2 v-else-if="level === 2">
      <a href="xxx">
        <slot></slot>
      </a>
    </h2>
    <h3 v-else-if="level === 3">
      <a href="xxx">
        <slot></slot>
      </a>
    </h3>
</template>
<script>
  export default {
    props:['level']
  }
</script>

No sé si ha escrito un código similar al anterior.

Escribamos el mismo código que el anterior de otra manera, solo escriba render

<script>
  export default {
    props:['level'],
    render(h){
      return h('h' + this.level, this.$slots.default())
    }
  }
</script>

¡Hecho! ¡Eso es todo! ¿Eso es todo?

Así es, eso es todo!

O lo siguiente es muy conveniente cuando se llama varias veces

<script>
  export default {
    props:['level'],
    render(h){
      const tag = 'h' + this.level
      return (<tag>{this.$slots.default()}</tag>)
    }
  }
</script>

Supongo que te gusta

Origin blog.csdn.net/asfasfaf122/article/details/128784664
Recomendado
Clasificación