经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Vue.js » 查看文章
Vue2模版编译(AST、Optimize 、Render)
来源:cnblogs  作者:柏成  时间:2023/3/27 14:50:15  对本文有异议

在Vue $mount过程中,我们需要把模版编译成render函数,整体实现可以分为三部分:

  1. parse:解析模版 template生成 AST语法树
  2. optimize: 优化 AST语法树,标记静态节点
  3. codegen: 把优化后的 AST语法树转换生成render方法代码字符串,利用模板引擎生成可执行的 render函数( render执行后返回的结果就是虚拟DOM,即以 VNode节点作为基础的树 )

Vue.js 提供了 2 个版本,一个是 Runtime + Compiler 的,一个是 Runtime only 的,前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,需要借助 webpack 的 vue-loader 事先把模板编译成 render函数。

下一章我们将介绍 render 和 patch 过程。关于 render函数如何生成虚拟DOM,以及如何将 vnode转化成真实DOM并挂载?

入口

  1. Vue.prototype.$mount = function (el) {
  2. ...
  3. // 这里需要对模板进行编译
  4. const render = compileToFunction(template)
  5. }
  6. export function compileToFunction(template) {
  7. // 1.解析模版template生成 AST语法树
  8. let ast = parseHTML(template)
  9. // 2.优化AST语法树,标记静态节点
  10. optimize(ast)
  11. // 3.把优化后的 AST语法树转换生成render方法代码字符串,利用模板引擎生成可执行的 render函数回的结果就是 虚拟DOM)
  12. let code = codegen(ast)
  13. code = `with(this){return ${code}}`
  14. let render = new Function(code)
  15. return render
  16. }

parse

AST做的是语法层面的转化,就是用对象去描述语法本身,例如经过 parse过程后,对 html的描述如下

可以看到,生成的 AST 是一个树状结构,每一个节点都是一个 ast element,除了它自身的一些属性,还维护了它的父子关系,如 parent指向它的父节点,children指向它的所有子节点

我们也可以利用AST的可视化工具网站 - AST Exploer ,使用各种parse对代码进行AST转换

在 Vue的 $mount过程中,编译过程首先就是调用 parseHTML方法,解析 template模版,生成 AST语法树

在这个过程,我们会用到正则表达式对字符串解析,匹配开始标签、文本内容和闭合标签等

  1. const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
  2. const qnameCapture = `((?:${ncname}\\:)?${ncname})`
  3. // 匹配的是 <xxx 第一个分组就是开始标签的名字
  4. const startTagOpen = new RegExp(`^<${qnameCapture}`)
  5. // 匹配的是 </xxxx> 第一个分组就是结束标签的名字
  6. const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
  7. // 分组1: 属性的key 分组2: = 分组3/分组4/分组5: value值
  8. const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 匹配属性
  9. const startTagClose = /^\s*(\/?)>/ // 匹配开始标签的结束 > 或 /> <div id = 'app' > <br/>

使用 while 循环html字符串,利用正则去匹配开始标签、文本内容和闭合标签,然后执行 advance方法将匹配到的内容在原html字符串中剔除,直到html字符串为空,结束循环

  1. export function parseHTML(html) {
  2. // 创建一颗抽象语法树
  3. function createASTElement(tag, attrs) { }
  4. // 处理开始标签,利用栈型结构来构造一颗树
  5. function start(tag, attrs) { }
  6. // 处理文本
  7. function chars(text) { }
  8. // 处理结束标签
  9. function end(tag) { }
  10. // 剔除 template 已匹配的内容
  11. function advance(n) {
  12. html = html.substring(n)
  13. }
  14. // 解析开始标签
  15. function parseStartTag() {
  16. const start = html.match(startTagOpen)
  17. if (start) {
  18. const match = {
  19. tagName: start[1], // 标签名
  20. attrs: [],
  21. }
  22. advance(start[0].length)
  23. let attr, end
  24. // 如果不是开始标签的结束 就一直匹配下去
  25. while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
  26. advance(attr[0].length)
  27. match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] || true })
  28. }
  29. // 如果不是开始标签的结束
  30. if (end) {
  31. advance(end[0].length)
  32. }
  33. return match
  34. }
  35. return false
  36. }
  37. // 循环html字符串,直到其为空停止
  38. while (html) {
  39. // 如果textEnd = 0 说明是一个开始标签或者结束标签
  40. // 如果textEnd > 0 说明就是文本的结束位置
  41. let textEnd = html.indexOf('<')
  42. if (textEnd == 0) {
  43. // 开始标签的解析結果,包括 标签名 和 属性
  44. const startTagMatch = parseStartTag()
  45. if (startTagMatch) {
  46. start(startTagMatch.tagName, startTagMatch.attrs)
  47. continue
  48. }
  49. // 匹配结束标签
  50. let endTagMatch = html.match(endTag)
  51. if (endTagMatch) {
  52. advance(endTagMatch[0].length)
  53. end(endTagMatch[1])
  54. continue
  55. }
  56. }
  57. if (textEnd > 0) {
  58. let text = html.substring(0, textEnd) // 截取文本内容
  59. if (text) {
  60. chars(text)
  61. advance(text.length)
  62. }
  63. }
  64. }
  65. return root
  66. }

当我们使用正则匹配到开始标签、文本内容和闭合标签时,分别执行start、chars、end方法去处理,利用 stack 栈型数据结构,最终构造一颗AST树,即root

  1. 匹配到开始标签时,就创建一个 ast元素,判断如果有 currentParent,会把当前 ast元素 push到 currentParent.chilldren 中,同时把 ast元素的 parent 指向 currentParent,ast元素入栈并更新 currentParent
  2. 匹配到文本时,就给 currentParent.children push一个文本 ast元素
  3. 匹配到结束标签时,就弹出栈中最后一个 ast元素,更新 currentParent

currentParent:指向的是栈中的最后一个 ast节点

注意:栈中的当前 ast节点永远是下一个 ast节点的父节点

  1. const ELEMENT_TYPE = 1 // 元素类型
  2. const TEXT_TYPE = 3 // 文本类型
  3. const stack = [] // 用于存放元素的栈
  4. let currentParent // 指向的是栈中的最后一个
  5. let root
  6. // 最终需要转化成一颗抽象语法树
  7. function createASTElement(tag, attrs) {
  8. return {
  9. tag, // 标签名
  10. type: ELEMENT_TYPE, // 类型
  11. attrs, // 属性
  12. parent: null,
  13. children: [],
  14. }
  15. }
  16. // 处理开始标签,利用栈型结构 来构造一颗树
  17. function start(tag, attrs) {
  18. let node = createASTElement(tag, attrs) // 创造一个 ast节点
  19. if (!root) {
  20. root = node // 如果root为空,则当前是树的根节点
  21. }
  22. if (currentParent) {
  23. node.parent = currentParent // 只赋予了parent属性
  24. currentParent.children.push(node) // 还需要让父亲记住自己
  25. }
  26. stack.push(node)
  27. currentParent = node // currentParent为栈中的最后一个
  28. }
  29. // 处理文本
  30. function chars(text) {
  31. text = text.replace(/\s/g, '')
  32. // 文本直接放到当前指向的节点中
  33. if (text) {
  34. currentParent.children.push({
  35. type: TEXT_TYPE,
  36. text,
  37. parent: currentParent,
  38. })
  39. }
  40. }
  41. // 处理结束标签
  42. function end(tag) {
  43. stack.pop() // 弹出栈中最后一个ast节点
  44. currentParent = stack[stack.length - 1]
  45. }

当 AST 树构造完毕,下一步就是 optimize 优化这颗树

optimeize

当我们解析 template模版,生成 AST语法树之后,需要对这棵树进行 optimize优化,在编译阶段把一些 AST 节点优化成静态节点

深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则标记 static: true

为什么要有优化过程,因为我们知道 Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的比对,这对运行时对模板的更新起到极大的优化作用。

codegen

编译的最后一步就是把优化后的 AST树转换成可执行的 render代码。此过程包含两部分,第一部分是使用 codegen方法生成 render代码字符串,第二部分是利用模板引擎转换成可执行的 render代码

render方法代码字符串格式如下

_c: 执行 createElement创建虚拟节点;_v: 执行 createTextVNode创建文本虚拟节点;_s: 处理变量
我们会在Vue原型上扩展这些方法

让我们来实现一个简单的codegen方法,深度遍历AST树去生成render代码字符串

  1. function codegen(ast) {
  2. let children = genChildren(ast.children)
  3. let code = `_c('${ast.tag}',${ast.attrs.length > 0 ? genProps(ast.attrs) : 'null'}${ast.children.length ? `,${children}` : ''})`
  4. return code
  5. }
  6. // 根据ast语法树的 children对象 生成相对应的 children字符串
  7. function genChildren(children) {
  8. return children.map(child => gen(child)).join(',')
  9. }
  10. const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // 匹配到的内容就是我们表达式的变量,例如 {{ name }}
  11. function gen(node) {
  12. if (node.type === 1) { // 元素
  13. return codegen(node)
  14. } else { // 文本
  15. let text = node.text
  16. if (!defaultTagRE.test(text)) {
  17. // _v('hello')
  18. return `_v(${JSON.stringify(text)})`
  19. } else {
  20. //_v( _s(name) + 'hello' + _s(age))
  21. ... 拼接 _s
  22. return `_v(${tokens.join('+')})`
  23. }
  24. }
  25. }
  26. // 根据ast语法树的 attrs属性对象 生成相对应的属性字符串
  27. function genProps(attrs) {
  28. let str = ''
  29. for (let i = 0; i < attrs.length; i++) {
  30. let attr = attrs[i]
  31. str += `${attr.name}:${JSON.stringify(attr.value)},` // id:'app',class:'app-inner',
  32. }
  33. return `{${str.slice(0, -1)}}`
  34. }

模板引擎的实现原理就是 with + new Function(),转换成可执行的函数,最终赋值给vm.options.render

  1. let code = codegen(ast)
  2. code = `with(this){return ${code}}`
  3. let render = new Function(code)

尤大大亲自解读: Vue2模板编译为何使用with

with 的作用域和模板的作用域正好契合,可以极大地简化模板编译过程。用 with 代码量可以很少,而且把作用域的处理交给 js 引擎来做也更可靠
用 with 的主要副作用是生成的代码不能在 strict mode / ES module 中运行,但直接在浏览器里编译的时候因为用了 new Function(),等同于 eval,不受这一点影响

参考文档

编译 | Vue.js 技术揭秘

原文链接:https://www.cnblogs.com/burc/p/17254659.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号