vue编译过程分析(上)

简介: 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几个常见指令的解析。


相关文章
|
2月前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
227 2
|
9天前
|
缓存 JavaScript
vue中的keep-alive问题(2)
vue中的keep-alive问题(2)
221 137
|
5月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
659 0
|
5月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
4月前
|
JavaScript 安全
在 Vue 中,如何在回调函数中正确使用 this?
在 Vue 中,如何在回调函数中正确使用 this?
162 0
|
4月前
|
人工智能 JSON JavaScript
VTJ.PRO 首发 MasterGo 设计智能识别引擎,秒级生成 Vue 代码
VTJ.PRO发布「AI MasterGo设计稿识别引擎」,成为全球首个支持解析MasterGo原生JSON文件并自动生成Vue组件的AI工具。通过双引擎架构,实现设计到代码全流程自动化,效率提升300%,助力企业降本增效,引领“设计即生产”新时代。
320 1
|
5月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
240 1
|
7月前
|
JavaScript
vue实现任务周期cron表达式选择组件
vue实现任务周期cron表达式选择组件
898 4
|
6月前
|
JavaScript 数据可视化 前端开发
基于 Vue 与 D3 的可拖拽拓扑图技术方案及应用案例解析
本文介绍了基于Vue和D3实现可拖拽拓扑图的技术方案与应用实例。通过Vue构建用户界面和交互逻辑,结合D3强大的数据可视化能力,实现了力导向布局、节点拖拽、交互事件等功能。文章详细讲解了数据模型设计、拖拽功能实现、组件封装及高级扩展(如节点类型定制、连接样式优化等),并提供了性能优化方案以应对大数据量场景。最终,展示了基础网络拓扑、实时更新拓扑等应用实例,为开发者提供了一套完整的实现思路和实践经验。
657 77
|
7月前
|
缓存 JavaScript 前端开发
Vue 基础语法介绍
Vue 基础语法介绍
下一篇
开通oss服务