vue中template模板编译的过程全面剖析

目录
  • 简述过程
  • vue的渲染过程
  • parse
  • parse过程总结
  • generate生成render函数

简述过程

vue template模板编译的过程经过parse()生成ast(抽象语法树),optimize对静态节点优化,generate()生成render字符串

之后调用new Watcher()函数,用来监听数据的变化,render 函数就是数据监听的回调所调用的,其结果便是重新生成 vnode。

当这个 render 函数字符串在第一次 mount、或者绑定的数据更新的时候,都会被调用,生成 Vnode。

如果是数据的更新,那么 Vnode 会与数据改变之前的 Vnode 做 diff,对内容做改动之后,就会更新到 我们真正的 DOM

vue的渲染过程

parse

在了解 parse 的过程之前,我们需要了解 AST,AST 的全称是 Abstract Syntax Tree,也就是所谓抽象语法树,用来表示代码的数据结构。

在Vue中我把它理解为嵌套的、携带标签名、属性和父子关系的 JS 对象,以树来表现 DOM 结构。

vue中的ast类型有以下3种

ASTElement = {  // AST标签元素
  type: 1;
  tag: string;
  attrsList: Array<{ name: string; value: any }>;
  attrsMap: { [key: string]: any };
  parent: ASTElement | void;
  children: Array<ASTNode>

  ...
}
ASTExpression = { // AST表达式 {{ }}
  type: 2;
  expression: string;
  text: string;
  tokens: Array<string | Object>;
  static?: boolean;
};
ASTText = {  // AST文本
  type: 3;
  text: string;
  static?: boolean;
  isComment?: boolean;
};

通过children字段来形成一种层层嵌套的树状结构。vue中定义了许多正则(判断标签开始、结束、属性、vue指令、文本),通过对html内容进行递归正则匹配,对满足条件的字符串进行截取。把字符串类型的html转换位AST结构

parse函数的作用就是把字符串型的template转化为AST结构

如,假设我们有一个元素

texttext,在 parse 完之后会变成如下的结构并返回:

  ele1 = {
    type: 1,
    tag: "div",
    attrsList: [{name: "id", value: "test"}],
    attrsMap: {id: "test"},
    parent: undefined,
    children: [{
        type: 3,
        text: 'texttext'
      }
    ],
    plain: true,
    attrs: [{name: "id", value: "'test'"}]
  }

那么它具体是怎么解析、截取的呢?

举个例子

<div>
    <p>我是{{name}}</p>
</div>

他的截取过程,主要如下

// 初始
<div>
    <p>我是{{name}}</p>
</div>
// 第一次截取剩余(包括空格)
    <p>我是{{name}}</p>
</div>
// 第二次截取
<p>我是{{name}}</p>
</div>
// 第三次截取
我是{{name}}</p>
</div>
// 第四次截取
</p>
</div>
//

</div>
//
</div>

那么,他的截取规则是什么呢?

vue中截取规则主要是通过判断模板中html.indexof(’<’)的值,来确定我们是要截取标签还是文本.

  • 等于 0:这就代表这是注释、条件注释、doctype、开始标签、结束标签中的某一种
  • 大于等于 0:这就说明是文本、表达式
  • 小于 0:表示 html 标签解析完了,可能会剩下一些文本、表达式

若等于0

若等于0,则进行正则匹配看是否为开始标签、结束标签、注释、条件注释、doctype中的一种。若是开始标签,则截取对应的开始标签,并定义ast的基本结构,并且解析标签上带的属性(attrs, tagName)、指令等等。
当然,这里的attrs也是通过正则匹配出来的,具体做法就是通过匹配标签上对应的属性,然后把他push到attrs里。

匹配时候的正则表达式如下。

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
  • 同时,需要注意的一点是,vue中还需要维护一个stack(可以理解为一个数组),用来标记DOM的深度

关于stack

stack里的最后一项,永远是当前正在解析的元素的parentNode。

通过stack解析器会把当前解析的元素和stack里的最后一个元素建立父子关系。即把当前节点push到stack的最后一个节点的children里,同时将它自身的parent设为stack的最后一个节点。

当然,因为我们的标签中存在一种自闭和的标签(如input),这种类型的标签没有子元素,所以不会push到stack中。

  • 若是结束标签,则需要通过这个结束标签的tagName从后到前匹配stack中每一项的tagName,将匹配到的那一项之后的所有项全部删除,表示这一段已经解析完成。
  • 若不是以上5种中的一种,则表示他是文本

等于0或大于0

若等于0且不满足以上五种条件或大于0,则表示它是文本或表达式。

  • 此时,它会判断它的剩余部分是否符合标签的格式,
  • 如果不符合,则继续再剩余部分判断’<'的位置,并继续1的判断,直到剩余部分有符合标签的格式出现。
let textEnd = html.indexOf('<')
let text, rest, next
if (textEnd >= 0) {
  rest = html.slice(textEnd)
  // 剩余部分的 HTML 不符合标签的格式那肯定就是文本
  // 并且还是以 < 开头的文本
  while (
    !endTag.test(rest) &&
    !startTagOpen.test(rest) &&
    !comment.test(rest) &&
    !conditionalComment.test(rest)
  ) {
    // < in plain text, be forgiving and treat it as text
    next = rest.indexOf('<', 1)
    if (next < 0) break
    textEnd += next
    rest = html.slice(textEnd)
  }
  text = html.substring(0, textEnd)
  html = html.substring(0, textEnd)
}

关于文本的截取

文本一般分为2种

  • 实打实</div>
  • 我是{{name}}</div>

如果文本中含有表达式,则需要对文本中的变量进行解析

const expression = parseText(text, delimiters) // 对变量解析 {{name}} => _s(name)
children.push({
  type: 2,
  expression,
  text
})
// 上例中解析过后形成如下的结构
{
  expression: "_s(name)",
  text: "我是{{name}}",
  type: 2
}

现在我们再来看最开始的例子

<div>
    <p>我是{{name}}</p>
</div>

1.首先第一次判断<的位置,等于0,且可以匹配上开始标签,则截取这个标签。

// 第一次截取后剩余
    <p>我是{{name}}</p>
</div>

2.继续判断<的位置,大于0(因为有空格),判断为文本,截取这个文本

// 第二次截取后剩余
<p>我是{{name}}</p>
</div>

3.继续判断<位置,等于0,且为开始标签,截取这一部分,并且维护stack,把当前的解析的元素的parnet置为stack中的最后一项,并且在stack的最后一项的children里push当前解析的元素

// 这里有个判断,因为非自闭和标签才会有children,所以非自闭标签才往stack里push
if (!unary) {
  currentParent = element
  stack.push(element)
}
// 设立父子关系
currentParent.children.push(element)
element.parent = currentParent
// 此时stack
[divAst,pAst]
//  第三次截取后剩余
我是{{name}}</p>
</div>

4.继续判断<的位置,大于0,判断剩余部分是否属于标签的一种,这里剩余部分可以匹配结束标签,则表明为文本

// 第四次截取后剩余
</p>
</div>

5.继续判断<的位置,等于0,且匹配为结束标签,此时会再stack里寻找满足tagName和当前标签名相同的最后一项,把它之后项的全部删除。

// 此时stack
[divAst]
// 第五次截取剩余
</div>

6.继续通过以上方式截取,直到全部截取完毕。

parse过程总结

简单来说,template的parse过程,其实就是不断的截取字符串并解析它们的过程。

在此过程中,如果截取到非闭合标签就push到stack中,如果截取道结束标签就把这个标签pop出来。

optimize优化

optimize的作用主要是对生成的AST进行静态内容的优化,标记静态节点。所谓静态内容,指的是和数据没有关系,不需要每次都更新的内容。

标记静态节点的作用的作用是为了之后dom diff时,是否需要patch,diff算法会直接跳过静态节点,从而减少了比较的过程,优化了patch的性能。

  • 1.如果是表达式AST节点,直接返回 false
  • 2.如果是文本AST节点,直接返回 true
  • 3.如果元素是元素节点,阶段有 v-pre 指令 ||

1.没有任何指令、数据绑定、事件绑定等 &&

2.没有 v-if 和 v-for &&

3.不是 slot 和 component &&

4.是 HTML 保留标签 &&

5.不是 template 标签的直接子元素并且没有包含在 for 循环中则返回 true

简单来说,没有使用vue独有的语法的节点就可以称为静态节点

判断一个父级元素是静态节点,则需要判断它的所有子节点都是静态节点,否则就不是静态节点

标记静态节点的过程是一个不断递归的过程

for (let i = 0, l = node.children.length; i < l; i++) {
  const child = node.children[i]
  markStatic(child)
  if (!child.static) {
    node.static = false
  }
}

markStatic方法是用来标记静态节点的方法,它会不断的循环children,如果children还有children,则走相同的逻辑。这样所有的节点都会被打上标记。

在循环中会判断,子节点是否为静态节点,如果不是则其父节点不是静态节点。

generate生成render函数

generate是将AST转化成render funtion字符串的过程,他递归了AST,得到结果是render的字符串。

render函数的就是返回一个_c(‘tagName’,data,children)的方法

1.第一个参数是标签名

2.第二个参数是他的一些数据,包括属性/指令/方法/表达式等等。

3.第三个参数是当前标签的子标签,同样的,每一个子标签的格式也是_c(‘tagName’,data,children)。

generate就是通过不断递归形成了这么一种树形结构。

  • genElement:用来生成基本的render结构或者叫createElement结构
  • genData: 处理ast结构上的一些属性,用来生成data
  • genChildren:处理ast的children,并在内部调用genElement,形成子元素的_c()方法

render字符串内部有几种方法

几种内部方法

  • _c:对应的是 createElement 方法,顾名思义,它的含义是创建一个元素(Vnode)
  • _v:创建一个文本结点。
  • _s:把一个值转换为字符串。(eg: {{data}})
  • _m:渲染静态内容
<template>
  <div id="app">
    {{val}}
    <img src="http://xx.jpg">
  </div>
</template>
{
  render: with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_v("\n" + _s(val) + "\n"),
        _c('img', {
              attrs: {
                "src": ""
              }
            })
        ]
    )
  }
}

那么问题来了,_c(‘tagName’,data,children)如何拼接的,data是如何拼接的,children又是如何拼接的?

// genElement方法用来拼接每一项_c('tagName',data,children)
function genElement (el: ASTElement, state: CodegenState) {
  const data = el.plain ? undefined : genData(el, state)
  const children = el.inlineTemplate ? null : genChildren(el, state, true)

  let code = `_c('${el.tag}'${
    data ? `,${data}` : '' // data
  }${
    children ? `,${children}` : '' // children
  })`

  return code
}

线来看data的拼接逻辑

//
function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // ... 类似的还有很多种情况
  data = data.replace(/,$/, '') + '}'
  return data
}

从上面可以看出来,data的拼接过程就是不断的判读ast上一些属性是否存在,然后拼在data上,最后把这个data返回。

那么children怎么拼出来呢?

function genChildren (
  el: ASTElement,
  state: CodegenState
): string | void {
  const children = el.children
  if (children.length) {
    return `[${children.map(c => genNode(c, state)).join(',')}]`
  }
}
function genNode (node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    return genElement(node, state)
  } if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}

最后执行render函数就会形成虚拟DOM.

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 深入了解Vue3模板编译原理

    目录 Parse Transform cacheHandlers hoistStatic prefixIdentifiers PatchFlags hoists type 变化 Codegen 代码生成模式 静态节点 帮助函数 helpers helpers 是怎么使用的呢? 如何生成代码? Vue 的编译模块包含 4 个目录: compiler-core compiler-dom // 浏览器 compiler-sfc // 单文件组件 compiler-ssr // 服务端渲染 其中 com

  • 详解Vue template 如何支持多个根结点

    如果你试图创建一个没有根结点的 Vue template,像这样: <template> <div>Node 1</div> <div>Node 2</div> </template> 不出意外的话你会得到一个编译错误或者运行时错误,因为 template 必须有一个根元素. 通常你可以在外面套一个div容器来解决.这个容器元素没有显示上的作用,只是为了满足模板编译的单个根节点的要求. <template> <div

  • 深入解析Vue源码实例挂载与编译流程实现思路详解

    在正文开始之前,先了解vue基于源码构建的两个版本,一个是 runtime only ,另一个是 runtime加compiler 的版本,两个版本的主要区别在于后者的源码包括了一个编译器. 什么是编译器,百度百科上面的解释是 简单讲,编译器就是将"一种语言(通常为高级语言)"翻译为"另一种语言(通常为低级语言)"的程序.一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) →

  • Vue源码解析之Template转化为AST的实现方法

    什么是AST 在Vue的mount过程中,template会被编译成AST语法树,AST是指抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式. Virtual Dom Vue的一个厉害之处就是利用Virtual DOM模拟DOM对象树来优化DOM操作的一种技术或思路. Vue源码中虚拟DOM构建经历 template编译成AST语法树 -> 再转换为render函数 最终返回一个VNode(VNod

  • 详解使用vue-admin-template的优化历程

    前言 公司有好几个项目都有后台管理系统,为了方便开发,所以选择了 vue 中比较火的后台模板作为基础模板进行开发.但是,开始用的时候,作者并没有对此进行优化,到项目上线的时候,才发现,打包出来的文件都十分之大,就一个 vendor 就有 770k 的体积(下图是基础模板,什么都没加打包后的文件信息): 通过 webpack-bundle-analyzer 进行分析可得,体积主要来源于饿了么UI(体积为 500k),因为没对其进行部分引入拆分组件,导致 webpack 把整个组件库都打包进去了.其

  • vue中template模板编译的过程全面剖析

    目录 简述过程 vue的渲染过程 parse parse过程总结 generate生成render函数 简述过程 vue template模板编译的过程经过parse()生成ast(抽象语法树),optimize对静态节点优化,generate()生成render字符串 之后调用new Watcher()函数,用来监听数据的变化,render 函数就是数据监听的回调所调用的,其结果便是重新生成 vnode. 当这个 render 函数字符串在第一次 mount.或者绑定的数据更新的时候,都会被调

  • Vue 中 template 有且只能一个 root的原因解析(源码分析)

    引言 今年, 疫情 并没有影响到各种面经的正常出现,可谓是络绎不绝(学不动...).然后,在前段时间也看到一个这样的关于 Vue 的问题, 为什么每个组件 template 中有且只能一个 root? 可能,大家在平常开发中,用的较多就是 template 写 html 的形式.当然,不排除用 JSX 和 render() 函数的.但是,究其本质,它们最终都会转化成 render() 函数.然后,再由 render() 函数转为 Vritual DOM (以下统称 VNode ).而 rende

  • vue.js template模板的使用(仿饿了么布局)

    使用template实现如下页面(仿饿了么布局) 如上图.使用了4个组件,分别是header.vue,goods.vue,ratings.vue,seller.vue header.vue代码如下 <template> <div class="header"> 我是header头部 </div> </template> <script type="text/ecmascript-6"> export def

  • vue中template的三种写法示例

    第一种(字符串模板写法): 直接写在vue构造器里,这种写法比较直观,适用于html代码不多的场景,但是如果模板里html代码太多,不便于维护,不建议这么写. <!DOCTYPE html> <html> <!-- WARNING! Make sure that you match all Quasar related tags to the same version! (Below it's "@1.7.4") --> <head> &

  • 在Vue中使用Avue、配置过程及实际应用小结

    目录 1.使用Avue的原因 2.Avue的官网 3.安装使用 3.1 安装 3.2 在main.js中引入 4 使用Avue组件库 4.1 基本样式 4.2 实际应用 4.3 效果 在新项目中用到一个新的小玩意.还挺不错的.立马安装使用到自己的项目中.哈哈哈 1.使用Avue的原因 在项目中遇到通过点击加号实现输入框的增加.以及对该输入框的输入内容进行验证.有感而发 2.Avue的官网 官网地址:https://avuejs.com/ 3.安装使用 可以直接根据官网的教程来 以下介绍我成功安装

  • VUE中template的三种写法

    一.直接写在构造器中 <!-- 第一种写法:直接写在构造器里 --> <div id ="app1"> </div> <script> var vm1 = new Vue({ el: '#app1', data: {}, methods: {}, template:`<h3>在构造器中的文字</h3>` }); </script> 二.写在HTML自带的<template>标签中 <!

  • vue中使用iconfont图标的过程

    目录 vue引入iconfont图标 引入在线链接文件 vue使用iconfont多色图标 vue引入iconfont图标 引入在线链接文件 如果开发过程中需要不断更新图标,为了避免多次下载文件到本地,可以先选择使用在线链接的图标文件 前面的步骤就不赘述了,直接讲如何在vue中引入 查看项目在线链接 我 选的是 Unicode 的形式 在项目中的 assets/css 文件夹下新建 global.css 文件,复制刚才生成的 font-face 代码,如何定义iconfont 类 @font-f

  • vue中封装echarts公共组件过程

    目录 1.安装echarts 2.在mian.js中全局引入 3.下面开始封装图表 4.接下来只需要在需要显示图表的地方引入Echart.vue 定义图表公共样式是为了统一同一网站各页面图表的基础样式baseOption.js(轴线.区域.色系.字体),统一封装后页面需要直接引入,传入所需参即可覆盖基础样式. 以下示例封装图表组件Echart.vue. 1.安装echarts npm install echarts --save npm install lodash --save // 若已安装

  • Vue 中的compile操作方法

    在 Vue 里,模板编译也是非常重要的一部分,里面也非常复杂,这次探究不会深入探究每一个细节,而是走一个全景概要,来吧,大家和我一起去一探究竟. 初体验 我们看了 Vue 的初始化函数就会知道,在最后一步,它进行了 vm.$mount(el) 的操作,而这个 $mount 在两个地方定义过,分别是在 entry-runtime-with-compiler.js(简称:eMount) 和 runtime/index.js(简称:rMount) 这两个文件里,那么这两个有什么区别呢? // entr

  • Vue中为什么要引入render函数的实现

    目录 前言 背景 原理 后记 前言 使用Vue脚手架创建项目的入口文件main.js中,默认代码如下: import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false new Vue({ render: h => h(App), }).$mount('#app') 可以看到,代码中通过import引入了App组件,但是却并没有通过components注册,还使用了一个render函数,而没有利用

随机推荐