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

阅读: 评论:0

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

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

简述过程
  • 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的深度
关于stackstack里的最后一项,永远是当前正在解析的元素的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 (!st(rest) &&!st(rest) &&!st(rest) &&!st(rest)) {// < in plain text, be forgiving and treat it as textnext = rest.indexOf('<', 1)if (next < 0) breaktextEnd += nextrest = 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 = elementstack.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=""></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 = '{'// keyif (el.key) {data += `key:${el.key},`}// refif (el.ref) {data += `ref:${el.ref},`}if (el.refInFor) {data += `refInFor:true,`}// ... 类似的还有很多种情况data = place(/,$/, '') + '}'return data
}

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

那么children怎么拼出来呢?

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

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

本文发布于:2024-01-31 13:33:21,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170667920228891.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:模板   过程   vue   template
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23