vue编译过程分析(上)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: vue编译过程分析

前言


先正式讲解之前先看一张来自Vue官网的实例生命周期图。


网络异常,图片无法展示
|


由图中可知,当我们实例化一个Vue对象并完成初始化后,Vue会检查el和template属性,以获取模板字符串。然后将得到的模板编译成render函数。


只有当template未指定时,vue才会以所制定的el元素的outerHTML作为模板。


但如果这时我们还指定了自定义的render函数,vue不会再通过前两者去获取模板了。


本文重点聊聊vue是如何将通过template或者el获得的模板最终编译成render函数的。


将HTML模板解析为AST节点树


如上所述,vue在获取到模板字符串后,通过正则表达式对HTML模板逐字符解析,分别解析出元素节点以及每个元素节点上所设置的指令、attribute、事件绑定等,最终构建成一个完整描述HTML节点信息的AST节点树。


先认识一下几个核心的正则表达式和AST对象。


html字符匹配核心正则表达式


// 1. 匹配开始标签(不包括结尾的>),如匹配<div
const startTagOpen = /^<((?:[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)/
// 2. 匹配普通html属性,如id="app"
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 3. 匹配动态属性 如v-for="(item, index) in roles",v-bind:src="imageSrc" :[key]="value"
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
4. 匹配开始标签的尾部,如>或者/>
const startTagClose = /^\s*(\/?)>/
// 5. 匹配闭合标签,如</div>
const endTag = /^<\\/((?:[a-zA-Z_][\\-\\.0-9_a-zA-Z((?:[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)]*\\:)?[a-zA-Z_][\\-\\.0-9_a-zA-Z((?:[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)]*)[^>]*>/
// 6. 匹配DOCTYPE
const doctype = /^<!DOCTYPE [^>]+>/i
// 7. 匹配HTML注释
const comment = /^<!\--/
// 8. 匹配HTML条件注释
const conditionalComment = /^<!\[/


AST节点对象


下面一段代码是AST节点的创建函数。通过代码,我们可以对AST节点有个基本了解。简单来说,


AST节点是对HTML节点信息的描述。例如AST将HTML标签属性解析出来的内容保存到attrsList、attrsMap和rawAttrsMap中,在parent中保持对父节点的引用,通过children来指向自己的子节点。


{
    // 节点类型
    type: 1,
    // 标签名,如div
    tag: "div",
    // 节点所包含的属性
    attrsList: [],
    attrsMap: {},
    rawAttrsMap: {},
    // 父节点指针
    parent: undefined,
    // 子节点指针
    children: []
}


简单标签解析


假定有如下模板:


<div id="app">{{msg}}</div>


具体的解析过程是通过几个核心正则表达式分别捕获到标签名以及属性的名称和值,然后使用这些信息创建AST节点对象,主要代码如下:


// 1. 解析标签和属性
function parseStartTag () {
  // 匹配开始标签
  var start = html.match(startTagOpen);
  if (start) {
    var match = {
      tagName: start[1],
      attrs: [],
      start: index
    };
    // 将指针向前移动
    advance(start[0].length);
    var end, attr;
    // 匹配标签中的属性
    while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
      attr.start = index;
      advance(attr[0].length);
      attr.end = index;
      match.attrs.push(attr);
    }
    if (end) {
      match.unarySlash = end[1];
      advance(end[0].length);
      match.end = index;
      return match
    }
  }
}
...省略其他代码
// 2. 创建AST节点对象
var element = createASTElement(tag, attrs, currentParent);
...省略其他代码


经过以上解析,得到如下AST节点对象:


{
    attrsList: [{name: "id", value: "app", start: 5, end: 13}],
    attrsMap: {id: "app"},
    children: [],
    end: 14,
    parent: undefined,
    rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}},
    start: 0,
    tag: "div",
    type: 1
}


接下来需要处理的是div的子节点{{msg}}。由于其子节点是文本节点,这里使用parseText来处理文本节点,然后使用这些信息创建AST节点对象,主要代码如下:


// 1. 解析文本节点中的字符
function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  // ...省略其他代码
  const tokens = []
  const rawTokens = []
  // tagRE默认值为/\{\{((?:.|\r?\n)+?)\}\}/g,识别出文本中通过{{value}}插入的值
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    index = match.index
    if (index > lastIndex) {
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // 如果模板插值中使用了过滤器,需要先解析过滤器
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  // 解析结果为expression和tokens组成的对象
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}
//...省略其他代码
// 2. 创建文本AST对象
child = {
    type: 2,
    expression: res.expression,
    tokens: res.tokens,
    text: text
}
child.start = start;
child.end = end;
//...省略其他代码
// 添加到div的子节点数组里
children.push(child);


文本节点解析后的结果如下:


{
    type: 2, 
    expression: "_s(message)", 
    tokens: [{@binding: "message"}],
    text: "{{message}}"
}


解析完子节点后的结果根节点div的AST更新为:


{
    attrsList: [{name: "id", value: "app", start: 5, end: 13}]
    attrsMap: {id: "app"}
    children: [{type: 2, expression: "_s(msg)", tokens: [{@binding: "msg"}], text: "{{msg}}"}]
    end: 14
    parent: undefined
    rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}
    start: 0
    tag: "div"
    type: 1
}


最后是解析闭合标签</div>。 当解析器匹配到闭合标签后,意味着一个标签的匹配结束了。


因为在标签中除了会使用id="app",placeholder="edit me"等HTML attribute外,还有很多是使用了vue的指令属性,如v-for,v-on,v-model。解析器会对前面已经生成的AST节点对象,进一步处理。最终这部分信息会以directives、on、domProps等属性的形式添加到AST对象上。


// 节点的收尾处理
function closeElement (element) {
    // 去除元素的空子节点
    trimEndingWhitespace(element)
    // 处理AST,添加额外属性
    if (!inVPre && !element.processed) {
      element = processElement(element, options)
    }
    // ... 省略其他代码
}
// 给AST添加额外属性
function processElement (
  element: ASTElement,
  options: CompilerOptions
) {
  // 给AST添加key属性
  processKey(element)
  // 给AST添加plain属性
  element.plain = (
    !element.key &&
    !element.scopedSlots &&
    !element.attrsList.length
  )
  // 处理v-ref,给AST对象添加ref属性
  processRef(element)
  // 处理传递给组件的slot,给AST添加slotScope属性
  processSlotContent(element)
  // 处理slot标签,给AST添加slotName属性
  processSlotOutlet(element)
  // 给AST添加component或inlineTemplate属性
  processComponent(element)
  // 处理
  for (let i = 0; i < transforms.length; i++) {
    element = transforms[i](element, options) || element
  }
  // 根据属性的不同,给AST对象添加directives、events、props等属性
  processAttrs(element)
  return element
}


了解了简单标签解析过程,我们再来看下对于v-for、v-if、v-model、v-on几个常见指令的解析。


相关文章
|
6天前
|
JavaScript
vue使用iconfont图标
vue使用iconfont图标
51 1
|
17天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
2月前
|
JavaScript API 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
2月前
|
JavaScript 前端开发 开发者
Vue是如何劫持响应式对象的
Vue是如何劫持响应式对象的
35 1
|
2月前
|
JavaScript 前端开发 API
介绍一下Vue中的响应式原理
介绍一下Vue中的响应式原理
36 1
|
2月前
|
JavaScript 前端开发 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
2月前
|
存储 JavaScript 前端开发
介绍一下Vue的核心功能
介绍一下Vue的核心功能
|
2月前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
48 1
vue学习第一章
|
2月前
|
JavaScript 前端开发 索引
vue学习第三章
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中的v-bind指令,包括基本使用、动态绑定class及style等,希望能为你的前端学习之路提供帮助。持续关注,更多精彩内容即将呈现!🎉🎉🎉
34 1
|
2月前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
40 1
vue学习第四章

热门文章

最新文章