草庐IT

Vue2模版编译(AST、Optimize 、Render)

柏成 2023-03-28 原文

在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并挂载?

入口

Vue.prototype.$mount = function (el) {
  ...
  // 这里需要对模板进行编译
  const render = compileToFunction(template)
}

export function compileToFunction(template) {
  // 1.解析模版template生成 AST语法树
  let ast = parseHTML(template)

  // 2.优化AST语法树,标记静态节点
  optimize(ast)

  // 3.把优化后的 AST语法树转换生成render方法代码字符串,利用模板引擎生成可执行的 render函数回的结果就是 虚拟DOM)
  let code = codegen(ast)
  code = `with(this){return ${code}}`
  let render = new Function(code) 

  return render
}

parse

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

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

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

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

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

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

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

export function parseHTML(html) {
  // 创建一颗抽象语法树
  function createASTElement(tag, attrs) { }

  // 处理开始标签,利用栈型结构来构造一颗树
  function start(tag, attrs) { }

  // 处理文本
  function chars(text) { }

  // 处理结束标签
  function end(tag) { }

  // 剔除 template 已匹配的内容
  function advance(n) {
    html = html.substring(n)
  }

  // 解析开始标签
  function parseStartTag() {
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1], // 标签名
        attrs: [],
      }
      advance(start[0].length)

      let attr, end
      // 如果不是开始标签的结束 就一直匹配下去
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        advance(attr[0].length)
        match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] || true })
      }

      // 如果不是开始标签的结束
      if (end) {
        advance(end[0].length)
      }
      return match
    }
    return false
  }

  // 循环html字符串,直到其为空停止
  while (html) {
    // 如果textEnd = 0 说明是一个开始标签或者结束标签
    // 如果textEnd > 0 说明就是文本的结束位置
    let textEnd = html.indexOf('<')
    if (textEnd == 0) {
      // 开始标签的解析結果,包括 标签名 和 属性
      const startTagMatch = parseStartTag()

      if (startTagMatch) {
        start(startTagMatch.tagName, startTagMatch.attrs)
        continue
      }

      // 匹配结束标签
      let endTagMatch = html.match(endTag)
      if (endTagMatch) {
        advance(endTagMatch[0].length)
        end(endTagMatch[1])
        continue
      }
    }
    if (textEnd > 0) {
      let text = html.substring(0, textEnd) // 截取文本内容
      if (text) {
        chars(text)
        advance(text.length)
      }
    }
  }

  return root
}

当我们使用正则匹配到开始标签、文本内容和闭合标签时,分别执行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节点的父节点

const ELEMENT_TYPE = 1 // 元素类型
const TEXT_TYPE = 3 // 文本类型
const stack = [] // 用于存放元素的栈
let currentParent // 指向的是栈中的最后一个
let root

// 最终需要转化成一颗抽象语法树
function createASTElement(tag, attrs) {
  return {
    tag, // 标签名
    type: ELEMENT_TYPE, // 类型
    attrs, // 属性
    parent: null,
    children: [],
  }
}

// 处理开始标签,利用栈型结构 来构造一颗树
function start(tag, attrs) {
  let node = createASTElement(tag, attrs) // 创造一个 ast节点
  if (!root) {
    root = node // 如果root为空,则当前是树的根节点
  }
  if (currentParent) {
    node.parent = currentParent // 只赋予了parent属性
    currentParent.children.push(node) // 还需要让父亲记住自己
  }
  stack.push(node)
  currentParent = node // currentParent为栈中的最后一个
}

// 处理文本
function chars(text) {
  text = text.replace(/\s/g, '')
  // 文本直接放到当前指向的节点中
  if (text) {
    currentParent.children.push({
      type: TEXT_TYPE,
      text,
      parent: currentParent,
    })
  }
}

// 处理结束标签
function end(tag) {
  stack.pop() // 弹出栈中最后一个ast节点
  currentParent = stack[stack.length - 1]
}

当 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代码字符串

function codegen(ast) {
  let children = genChildren(ast.children)
  let code = `_c('${ast.tag}',${ast.attrs.length > 0 ? genProps(ast.attrs) : 'null'}${ast.children.length ? `,${children}` : ''})`
  return code
}

// 根据ast语法树的 children对象 生成相对应的 children字符串
function genChildren(children) {
  return children.map(child => gen(child)).join(',')
}

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // 匹配到的内容就是我们表达式的变量,例如 {{ name }}
function gen(node) {
  if (node.type === 1) {  // 元素
    return codegen(node)
  } else {  // 文本
    let text = node.text
    if (!defaultTagRE.test(text)) {
      // _v('hello')
      return `_v(${JSON.stringify(text)})`
    } else {
      //_v( _s(name) + 'hello' + _s(age))
      ... 拼接 _s
      return `_v(${tokens.join('+')})`
    }
  }
}

// 根据ast语法树的 attrs属性对象 生成相对应的属性字符串
function genProps(attrs) {
  let str = ''
  for (let i = 0; i < attrs.length; i++) {
    let attr = attrs[i]
    str += `${attr.name}:${JSON.stringify(attr.value)},` // id:'app',class:'app-inner',
  }
  return `{${str.slice(0, -1)}}`
}

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

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

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

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

参考文档

编译 | Vue.js 技术揭秘

有关Vue2模版编译(AST、Optimize 、Render)的更多相关文章

  1. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

    我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

  2. ruby - Sinatra set cache_control to static files in public folder编译错误 - 2

    我不知道为什么,但是当我设置这个设置时它无法编译设置:static_cache_control,[:public,:max_age=>300]这是我得到的syntaxerror,unexpectedtASSOC,expecting']'(SyntaxError)set:static_cache_control,[:public,:max_age=>300]^我只想将“过期”header设置为css、javaascript和图像文件。谢谢。 最佳答案 我猜您使用的是Ruby1.8.7。Sinatra文档中显示的语法似乎是在Ruby1.

  3. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  4. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  5. ruby - Ruby 的 AST 中的 'send' 关键字是什么意思? - 2

    我正在尝试学习Ruby词法分析器和解析器(whitequarkparser)以了解更多有关从Ruby脚本进一步生成机器代码的过程。在解析以下Ruby代码字符串时。defadd(a,b)returna+bendputsadd1,2它导致以下S表达式符号。s(:begin,s(:def,:add,s(:args,s(:arg,:a),s(:arg,:b)),s(:return,s(:send,s(:lvar,:a),:+,s(:lvar,:b)))),s(:send,nil,:puts,s(:send,nil,:add,s(:int,1),s(:int,3))))任何人都可以向我解释生成的

  6. .net - 是否有 Ruby .NET 编译器? - 2

    是否有适用于Ruby语言的.NETFramework编译器?我听说过DLR(动态语言运行时),这是否将使Ruby能够用于.NET开发? 最佳答案 IronRuby是Microsoft支持的项目,建立在动态语言运行时之上。 关于.net-是否有Ruby.NET编译器?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/199638/

  7. python - 使用 Python、Ruby 和 Perl 重新编译 MacPort 版本的 MacVim - 2

    关闭。这个问题是off-topic.它目前不接受答案。想改进这个问题吗?Updatethequestion所以它是on-topic用于堆栈溢出。关闭10年前。ImprovethisquestionLinux专家正在转向Mac(10.8)。因为我懒...我使用MacPorts安装MacVim。它似乎安装没有错误。我只需要mvim中的python、ruby和perl支持。$/opt/local/bin/mvim--version|egrep'patches|python|ruby|perl'Includedpatches:1-244,246-646+multi_lang-mzscheme+

  8. ruby - 为什么 `middleman serve` 有效,但是 `middleman build` 编译这个 Sass 失败? - 2

    当我刚刚运行middleman时服务,all.css编译得很好,只包含对+box-shadow(none)的调用:/*line1,/home/yang/asdf/source/stylesheets/content.css.sass*/div{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}但是当我构建网站时,我得到了这个Sass/Compass错误:$middlemanbuildSlim::EmbeddedEngineisdeprecated,itiscalledSlim::EmbeddedinSlim2.0

  9. (附源码)vue3.0+.NET6实现聊天室(实时聊天SignalR) - 2

    参考文章搭建文章gitte源码在线体验可以注册两个号来测试演示图:一.整体介绍  介绍SignalR一种通讯模型Hub(中心模型,或者叫集线器模型),调用这个模型写好的方法,去发送消息。  内容有:    ①:Hub模型的方法介绍    ②:服务器端代码介绍    ③:前端vue3安装并调用后端方法    ④:聊天室样例整体流程:1、进入网站->调用连接SignalR的方法2、与好友发送消息->调用SignalR的自定义方法 前端通过,signalR内置方法.invoke()  去请求接口3、监听接受方法(渲染消息)通过new signalR.HubConnectionBuilder().on

  10. ruby - 有没有办法在 Ruby 中执行编译时类型检查? - 2

    我知道Ruby是动态和强类型的,但据我所知,由于每个参数缺少显式类型表示法(或契约),当前语法不允许在编译时检查参数类型。如果我想执行编译时类型检查,我有哪些(实际成熟的)选项?更新我的意思是类型检查类似于典型的静态类型语言。比如C。例如,C函数表示每个参数的类型,编译器检查传入的参数是否正确。voidfunc1(structAAAaaa){structBBBbbb;func1(bbb);//Wrongtype.Compiletimeerror.}作为另一个例子,Objective-C通过放置显式类型信息来做到这一点。-(id)method1:(AAA*)aaa{BBB*bbb=[[A

随机推荐