src源码目录

  • compiler 编译相关
  • core 核心代码
  • platforms 对不同平台的支持
  • server 服务端渲染
  • sfc .vue文件解析
  • shared 共享工具方法

    compiler

    compiler 目录包含 Vue.js 所有编译相关的代码。它包括把模板解析成 AST 语法树,AST语法树优化,代码生成等功能。

    core

    core 目录包含了 Vue.js 的核心代码,包括有内置组件、全局 API 封装,Vue 实例化、Obsever、Virtual DOM、工具函数 Util 等等。

    platform

    Vue.js 是一个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 native 客户端上。platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。

    server

    Vue.js 2.0 支持了服务端渲染,所有服务端渲染相关的逻辑都在这个目录下。注意:这部分代码是跑在服务端的 Node.js,不要和跑在浏览器端的 Vue.js 混为一谈。
    服务端渲染主要的工作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记”混合”为客户端上完全交互的应用程序。

    sfc

    通常我们开发 Vue.js 都会借助 webpack 构建, 然后通过 .vue 单文件来编写组件。
    这个目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象。

    shared

    Vue.js 会定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的。

主要的功能部分

  • 数据劫持/数据代理:数据改变时通知相关函数进行更新操作
  • 数据依赖收集:建立保存dom节点与数据的关联关系
  • 模板与数据之间的绑定:接收到新数据时对dom节点进行更新(Watcher)

基本思路

image.png

  1. 数据劫持/数据代理

Vue 将遍历此对象所有的 property,并使用Object.defineProperty把这些 property 全部转为getter/setter。在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

  1. data(){ return { key: value } }

:::warning 由于使用了Object.defineProperty, Vue 不能检测数组和对象的变化。但是提供了其他的解决方案, Vue3.0使用Proxy重构了。为什么 Vue3.0 不再使用 defineProperty 实现数据监听? ::: 2.数据依赖收集
compile函数中会遍历所有的node上的attribute,如果是v-model进行双向数据绑定,以冒号“:, v-bind”开头的属性我们进行相应数据的监听,如果是“@, v-on”开头的则添加该节点上相应的事件监听。动态模板{{ }}内的内容作为动态处理。

  1. export const onRE = /^@|^v-on:/
  2. export const bindRE = /^:|^\.|^v-bind:/
  3. // 收集项
  4. <span>(双大括号): {{ msg }}</span>
  5. <div id="item-{\{ id }\}"></div>
  6. <div>{{ message.split('').reverse().join('') }}</div>
  7. <div class="{{ className }}"></div>
  8. <div :class :id :name ...></div>
  9. <!-- 阻止单击事件冒泡 -->
  10. <a @click.stop="doThisFunc"></a>
  11. <!-- 修饰符可以串联 -->
  12. <a v-on:click.stop.prevent="doThat">
  13. <input v-on:keyup.13="submitFuncName">
  14. <input v-on:keyup.enter="submitFuncName">
  15. <!-- "change" 而不是 "input" 事件中更新 -->
  16. <input v-model="msg" lazy>
  17. <div v-for='item in ...' v-for="iten of ..." ></div>

3.模板与数据的绑定
数据依赖收集完毕, 数据改变时所关联的node需要进行更新,用一个watcher对象来进行node与数值之间的联系,并在数据改变时执行相关的更新视图操作。

其他


数组变异方法:

Vue仅仅依赖gettersetter,是无法做到在数组调用push,pop等方法时候触发数据响应的,因此Vue实际上是通过劫持这些方法,对这些方法进行包装变异来实现的。

  1. const arrayProto = Array.prototype
  2. export const arrayMethods = Object.create(arrayProto)
  3. /**
  4. Vue 包装的编译数组方法
  5. */
  6. const methodsToPatch = [
  7. 'push',
  8. 'pop',
  9. 'shift',
  10. 'unshift',
  11. 'splice',
  12. 'sort',
  13. 'reverse'
  14. ]
  15. /**
  16. * 拦截变异方法并触发通知事件
  17. */
  18. methodsToPatch.forEach(function (method) {
  19. // 缓存原始方法
  20. const original = arrayProto[method]
  21. def(arrayMethods, method, function mutator (...args) {
  22. const result = original.apply(this, args)
  23. const ob = this.__ob__
  24. let inserted
  25. switch (method) {
  26. case 'push':
  27. case 'unshift':
  28. inserted = args
  29. break
  30. case 'splice':
  31. inserted = args.slice(2)
  32. break
  33. }
  34. // 如果有新的数据插入,则插入的数据也要进行一个响应式
  35. if (inserted) ob.observeArray(inserted)
  36. // 通知更新
  37. ob.dep.notify()
  38. return result
  39. })
  40. })

模板解析

编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。它会用到大量正则表达式对字符串解析。

  1. <ul :class="bindClass" class="list" v-if="isShow">
  2. <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
  3. </ul>

经过 parse 过程后,生成的 AST 如下:

  1. ast = {
  2. 'type': 1,
  3. 'tag': 'ul',
  4. 'attrsList': [],
  5. 'attrsMap': {
  6. ':class': 'bindClass',
  7. 'class': 'list',
  8. 'v-if': 'isShow'
  9. },
  10. 'if': 'isShow',
  11. 'ifConditions': [{
  12. 'exp': 'isShow',
  13. 'block': // ul ast element
  14. }],
  15. 'parent': undefined,
  16. 'plain': false,
  17. 'staticClass': 'list',
  18. 'classBinding': 'bindCls',
  19. 'children': [{
  20. 'type': 1,
  21. 'tag': 'li',
  22. 'attrsList': [{
  23. 'name': '@click',
  24. 'value': 'clickItem(index)'
  25. }],
  26. 'attrsMap': {
  27. '@click': 'clickItem(index)',
  28. 'v-for': '(item,index) in data'
  29. },
  30. 'parent': // ul ast element
  31. 'plain': false,
  32. 'events': {
  33. 'click': {
  34. 'value': 'clickItem(index)'
  35. }
  36. },
  37. 'hasBindings': true,
  38. 'for': 'data',
  39. 'alias': 'item',
  40. 'iterator1': 'index',
  41. 'children': [
  42. 'type': 2,
  43. 'expression': '_s(item)+":"+_s(index)'
  44. 'text': '{{item}}:{{index}}',
  45. 'tokens': [
  46. {'@binding':'item'},
  47. ':',
  48. {'@binding':'index'}
  49. ]
  50. ]
  51. }]
  52. }

可以看到,生成的 AST 是一个树状结构,每一个节点都是一个 ast element,除了它自身的一些属性,还维护了它的父子关系,如 parent 指向它的父节点,children 指向它的所有子节点。先对 AST 有一些直观的印象,那么接下来我们来分析一下这个 AST 是如何得到的。

#整体流程

首先来看一下 parse 的定义,在 src/compiler/parser/index.js 中:

  1. export function parse (
  2. template: string,
  3. options: CompilerOptions
  4. ): ASTElement | void {
  5. getFnsAndConfigFromOptions(options)
  6. parseHTML(template, {
  7. // options ...
  8. start (tag, attrs, unary) {
  9. let element = createASTElement(tag, attrs)
  10. processElement(element)
  11. treeManagement()
  12. },
  13. end () {
  14. treeManagement()
  15. closeElement()
  16. },
  17. chars (text: string) {
  18. handleText()
  19. createChildrenASTOfText()
  20. },
  21. comment (text: string) {
  22. createChildrenASTOfComment()
  23. }
  24. })
  25. return astRootElement
  26. }

#解析 HTML 模板

  1. parseHTML(template, options)

对于 template 模板的解析主要是通过 parseHTML 函数,它的定义在 src/compiler/parser/html-parser 中:

  1. export function parseHTML (html, options) {
  2. let lastTag
  3. while (html) {
  4. if (!lastTag || !isPlainTextElement(lastTag)){
  5. let textEnd = html.indexOf('<')
  6. if (textEnd === 0) {
  7. if(matchComment) {
  8. advance(commentLength)
  9. continue
  10. }
  11. if(matchDoctype) {
  12. advance(doctypeLength)
  13. continue
  14. }
  15. if(matchEndTag) {
  16. advance(endTagLength)
  17. parseEndTag()
  18. continue
  19. }
  20. if(matchStartTag) {
  21. parseStartTag()
  22. handleStartTag()
  23. continue
  24. }
  25. }
  26. handleText()
  27. advance(textLength)
  28. } else {
  29. handlePlainTextElement()
  30. parseEndTag()
  31. }
  32. }
  33. }

整体来说它的逻辑就是循环解析 template ,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。 在匹配的过程中会利用 advance 函数不断前进整个模板字符串,直到字符串末尾。

  1. function advance (n) {
  2. index += n
  3. html = html.substring(n)
  4. }

为了更加直观地说明 advance 的作用,可以通过一副图表示:
Vue源码初探 - 图2
调用 advance 函数:

  1. advance(4)

得到结果:
Vue源码初探 - 图3
匹配的过程中主要利用了正则表达式,如下:

  1. const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
  2. const ncname = '[a-zA-Z_][\\w\\-\\.]*'
  3. const qnameCapture = `((?:${ncname}\\:)?${ncname})`
  4. const startTagOpen = new RegExp(`^<${qnameCapture}`)
  5. const startTagClose = /^\s*(\/?)>/
  6. const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
  7. const doctype = /^<!DOCTYPE [^>]+>/i
  8. const comment = /^<!\--/
  9. const conditionalComment = /^<!\[/

通过这些正则表达式,我们可以匹配注释节点、文档类型节点、开始闭合标签等。

  • 注释节点、文档类型节点

对于注释节点和文档类型节点的匹配,如果匹配到我们仅仅做的是做前进即可。

  1. if (comment.test(html)) {
  2. const commentEnd = html.indexOf('-->')
  3. if (commentEnd >= 0) {
  4. if (options.shouldKeepComment) {
  5. options.comment(html.substring(4, commentEnd))
  6. }
  7. advance(commentEnd + 3)
  8. continue
  9. }
  10. }
  11. if (conditionalComment.test(html)) {
  12. const conditionalEnd = html.indexOf(']>')
  13. if (conditionalEnd >= 0) {
  14. advance(conditionalEnd + 2)
  15. continue
  16. }
  17. }
  18. const doctypeMatch = html.match(doctype)
  19. if (doctypeMatch) {
  20. advance(doctypeMatch[0].length)
  21. continue
  22. }

对于注释和条件注释节点,前进至它们的末尾位置;对于文档类型节点,则前进它自身长度的距离。

  • 开始标签

    1. const startTagMatch = parseStartTag()
    2. if (startTagMatch) {
    3. handleStartTag(startTagMatch)
    4. if (shouldIgnoreFirstNewline(lastTag, html)) {
    5. advance(1)
    6. }
    7. continue
    8. }

    首先通过 parseStartTag 解析开始标签:

    1. function parseStartTag () {
    2. const start = html.match(startTagOpen)
    3. if (start) {
    4. const match = {
    5. tagName: start[1],
    6. attrs: [],
    7. start: index
    8. }
    9. advance(start[0].length)
    10. let end, attr
    11. while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
    12. advance(attr[0].length)
    13. match.attrs.push(attr)
    14. }
    15. if (end) {
    16. match.unarySlash = end[1]
    17. advance(end[0].length)
    18. match.end = index
    19. return match
    20. }
    21. }
    22. }

    对于开始标签,除了标签名之外,还有一些标签相关的属性。函数先通过正则表达式 startTagOpen 匹配到开始标签,然后定义了 match 对象,接着循环去匹配开始标签中的属性并添加到 match.attrs 中,直到匹配的开始标签的闭合符结束。如果匹配到闭合符,则获取一元斜线符,前进到闭合符尾,并把当前索引赋值给 match.end
    parseStartTag 对开始标签解析拿到 match 后,紧接着会执行 handleStartTagmatch 做处理:

    1. function handleStartTag (match) {
    2. const tagName = match.tagName
    3. const unarySlash = match.unarySlash
    4. if (expectHTML) {
    5. if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
    6. parseEndTag(lastTag)
    7. }
    8. if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
    9. parseEndTag(tagName)
    10. }
    11. }
    12. const unary = isUnaryTag(tagName) || !!unarySlash
    13. const l = match.attrs.length
    14. const attrs = new Array(l)
    15. for (let i = 0; i < l; i++) {
    16. const args = match.attrs[i]
    17. if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
    18. if (args[3] === '') { delete args[3] }
    19. if (args[4] === '') { delete args[4] }
    20. if (args[5] === '') { delete args[5] }
    21. }
    22. const value = args[3] || args[4] || args[5] || ''
    23. const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
    24. ? options.shouldDecodeNewlinesForHref
    25. : options.shouldDecodeNewlines
    26. attrs[i] = {
    27. name: args[1],
    28. value: decodeAttr(value, shouldDecodeNewlines)
    29. }
    30. }
    31. if (!unary) {
    32. stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
    33. lastTag = tagName
    34. }
    35. if (options.start) {
    36. options.start(tagName, attrs, unary, match.start, match.end)
    37. }
    38. }

    handleStartTag 的核心逻辑很简单,先判断开始标签是否是一元标签,类似 <img>、<br/> 这样,接着对 match.attrs 遍历并做了一些处理,最后判断如果非一元标签,则往 stack 里 push 一个对象,并且把 tagName 赋值给 lastTag。至于 stack 的作用,稍后我会介绍。
    最后调用了 options.start 回调函数,并传入一些参数,这个回调函数的作用稍后我会详细介绍。

  • 闭合标签

    1. const endTagMatch = html.match(endTag)
    2. if (endTagMatch) {
    3. const curIndex = index
    4. advance(endTagMatch[0].length)
    5. parseEndTag(endTagMatch[1], curIndex, index)
    6. continue
    7. }

    先通过正则 endTag 匹配到闭合标签,然后前进到闭合标签末尾,然后执行 parseEndTag 方法对闭合标签做解析。

    1. function parseEndTag (tagName, start, end) {
    2. let pos, lowerCasedTagName
    3. if (start == null) start = index
    4. if (end == null) end = index
    5. if (tagName) {
    6. lowerCasedTagName = tagName.toLowerCase()
    7. }
    8. if (tagName) {
    9. for (pos = stack.length - 1; pos >= 0; pos--) {
    10. if (stack[pos].lowerCasedTag === lowerCasedTagName) {
    11. break
    12. }
    13. }
    14. } else {
    15. pos = 0
    16. }
    17. if (pos >= 0) {
    18. for (let i = stack.length - 1; i >= pos; i--) {
    19. if (process.env.NODE_ENV !== 'production' &&
    20. (i > pos || !tagName) &&
    21. options.warn
    22. ) {
    23. options.warn(
    24. `tag <${stack[i].tag}> has no matching end tag.`
    25. )
    26. }
    27. if (options.end) {
    28. options.end(stack[i].tag, start, end)
    29. }
    30. }
    31. stack.length = pos
    32. lastTag = pos && stack[pos - 1].tag
    33. } else if (lowerCasedTagName === 'br') {
    34. if (options.start) {
    35. options.start(tagName, [], true, start, end)
    36. }
    37. } else if (lowerCasedTagName === 'p') {
    38. if (options.start) {
    39. options.start(tagName, [], false, start, end)
    40. }
    41. if (options.end) {
    42. options.end(tagName, start, end)
    43. }
    44. }
    45. }

    parseEndTag 的核心逻辑很简单,在介绍之前我们回顾一下在执行 handleStartTag 的时候,对于非一元标签(有 endTag)我们都把它构造成一个对象压入到 stack 中,如图所示:
    Vue源码初探 - 图4
    那么对于闭合标签的解析,就是倒序 stack,找到第一个和当前 endTag 匹配的元素。如果是正常的标签匹配,那么 stack 的最后一个元素应该和当前的 endTag 匹配,但是考虑到如下错误情况:

    1. <div><span></div>

    这个时候当 endTag</div> 的时候,从 stack 尾部找到的标签是 <span>,就不能匹配,因此这种情况会报警告。匹配后把栈到 pos 位置的都弹出,并从 stack 尾部拿到 lastTag
    最后调用了 options.end 回调函数,并传入一些参数,这个回调函数的作用稍后我会详细介绍。

  • 文本

    1. let text, rest, next
    2. if (textEnd >= 0) {
    3. rest = html.slice(textEnd)
    4. while (
    5. !endTag.test(rest) &&
    6. !startTagOpen.test(rest) &&
    7. !comment.test(rest) &&
    8. !conditionalComment.test(rest)
    9. ) {
    10. next = rest.indexOf('<', 1)
    11. if (next < 0) break
    12. textEnd += next
    13. rest = html.slice(textEnd)
    14. }
    15. text = html.substring(0, textEnd)
    16. advance(textEnd)
    17. }
    18. if (textEnd < 0) {
    19. text = html
    20. html = ''
    21. }
    22. if (options.chars && text) {
    23. options.chars(text)
    24. }

    接下来判断 textEnd 是否大于等于 0 的,满足则说明到从当前位置到 textEnd 位置都是文本,并且如果 < 是纯文本中的字符,就继续找到真正的文本结束的位置,然后前进到结束的位置。
    再继续判断 textEnd 小于 0 的情况,则说明整个 template 解析完毕了,把剩余的 html 都赋值给了 text
    最后调用了 options.chars 回调函数,并传 text 参数,这个回调函数的作用稍后我会详细介绍。
    因此,在循环解析整个 template 的过程中,会根据不同的情况,去执行不同的回调函数,下面我们来看看这些回调函数的作用。

    #处理开始标签

    对应伪代码:

    1. start (tag, attrs, unary) {
    2. let element = createASTElement(tag, attrs)
    3. processElement(element)
    4. treeManagement()
    5. }

    当解析到开始标签的时候,最后会执行 start 回调函数,函数主要就做 3 件事情,创建 AST 元素,处理 AST 元素,AST 树管理。下面我们来分别来看这几个过程。

  • 创建 AST 元素

    1. // check namespace.
    2. // inherit parent ns if there is one
    3. const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
    4. // handle IE svg bug
    5. /* istanbul ignore if */
    6. if (isIE && ns === 'svg') {
    7. attrs = guardIESVGBug(attrs)
    8. }
    9. let element: ASTElement = createASTElement(tag, attrs, currentParent)
    10. if (ns) {
    11. element.ns = ns
    12. }
    13. export function createASTElement (
    14. tag: string,
    15. attrs: Array<Attr>,
    16. parent: ASTElement | void
    17. ): ASTElement {
    18. return {
    19. type: 1,
    20. tag,
    21. attrsList: attrs,
    22. attrsMap: makeAttrsMap(attrs),
    23. parent,
    24. children: []
    25. }
    26. }

    通过 createASTElement 方法去创建一个 AST 元素,并添加了 namespace。可以看到,每一个 AST 元素就是一个普通的 JavaScript 对象,其中,type 表示 AST 元素类型,tag 表示标签名,attrsList 表示属性列表,attrsMap 表示属性映射表,parent 表示父的 AST 元素,children 表示子 AST 元素集合。

  • 处理 AST 元素

    1. if (isForbiddenTag(element) && !isServerRendering()) {
    2. element.forbidden = true
    3. process.env.NODE_ENV !== 'production' && warn(
    4. 'Templates should only be responsible for mapping the state to the ' +
    5. 'UI. Avoid placing tags with side-effects in your templates, such as ' +
    6. `<${tag}>` + ', as they will not be parsed.'
    7. )
    8. }
    9. // apply pre-transforms
    10. for (let i = 0; i < preTransforms.length; i++) {
    11. element = preTransforms[i](element, options) || element
    12. }
    13. if (!inVPre) {
    14. processPre(element)
    15. if (element.pre) {
    16. inVPre = true
    17. }
    18. }
    19. if (platformIsPreTag(element.tag)) {
    20. inPre = true
    21. }
    22. if (inVPre) {
    23. processRawAttrs(element)
    24. } else if (!element.processed) {
    25. // structural directives
    26. processFor(element)
    27. processIf(element)
    28. processOnce(element)
    29. // element-scope stuff
    30. processElement(element, options)
    31. }

    首先是对模块 preTransforms 的调用,其实所有模块的 preTransformstransformspostTransforms 的定义都在 src/platforms/web/compiler/modules 目录中,这部分我们暂时不会介绍,之后会结合具体的例子说。接着判断 element 是否包含各种指令通过 processXXX 做相应的处理,处理的结果就是扩展 AST 元素的属性。这里我并不会一一介绍所有的指令处理,而是结合我们当前的例子,我们来看一下 processForprocessIf

    1. export function processFor (el: ASTElement) {
    2. let exp
    3. if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    4. const res = parseFor(exp)
    5. if (res) {
    6. extend(el, res)
    7. } else if (process.env.NODE_ENV !== 'production') {
    8. warn(
    9. `Invalid v-for expression: ${exp}`
    10. )
    11. }
    12. }
    13. }
    14. export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
    15. export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
    16. const stripParensRE = /^\(|\)$/g
    17. export function parseFor (exp: string): ?ForParseResult {
    18. const inMatch = exp.match(forAliasRE)
    19. if (!inMatch) return
    20. const res = {}
    21. res.for = inMatch[2].trim()
    22. const alias = inMatch[1].trim().replace(stripParensRE, '')
    23. const iteratorMatch = alias.match(forIteratorRE)
    24. if (iteratorMatch) {
    25. res.alias = alias.replace(forIteratorRE, '')
    26. res.iterator1 = iteratorMatch[1].trim()
    27. if (iteratorMatch[2]) {
    28. res.iterator2 = iteratorMatch[2].trim()
    29. }
    30. } else {
    31. res.alias = alias
    32. }
    33. return res
    34. }

    processFor 就是从元素中拿到 v-for 指令的内容,然后分别解析出 foraliasiterator1iterator2 等属性的值添加到 AST 的元素上。就我们的示例 v-for="(item,index) in data" 而言,解析出的的 fordataaliasitemiterator1index,没有 iterator2

    1. function processIf (el) {
    2. const exp = getAndRemoveAttr(el, 'v-if')
    3. if (exp) {
    4. el.if = exp
    5. addIfCondition(el, {
    6. exp: exp,
    7. block: el
    8. })
    9. } else {
    10. if (getAndRemoveAttr(el, 'v-else') != null) {
    11. el.else = true
    12. }
    13. const elseif = getAndRemoveAttr(el, 'v-else-if')
    14. if (elseif) {
    15. el.elseif = elseif
    16. }
    17. }
    18. }
    19. export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
    20. if (!el.ifConditions) {
    21. el.ifConditions = []
    22. }
    23. el.ifConditions.push(condition)
    24. }

    processIf 就是从元素中拿 v-if 指令的内容,如果拿到则给 AST 元素添加 if 属性和 ifConditions 属性;否则尝试拿 v-else 指令及 v-else-if 指令的内容,如果拿到则给 AST 元素分别添加 elseelseif 属性。

  • AST 树管理

我们在处理开始标签的时候为每一个标签创建了一个 AST 元素,在不断解析模板创建 AST 元素的时候,我们也要为它们建立父子关系,就像 DOM 元素的父子关系那样。
AST 树管理相关代码如下:

  1. function checkRootConstraints (el) {
  2. if (process.env.NODE_ENV !== 'production') {
  3. if (el.tag === 'slot' || el.tag === 'template') {
  4. warnOnce(
  5. `Cannot use <${el.tag}> as component root element because it may ` +
  6. 'contain multiple nodes.'
  7. )
  8. }
  9. if (el.attrsMap.hasOwnProperty('v-for')) {
  10. warnOnce(
  11. 'Cannot use v-for on stateful component root element because ' +
  12. 'it renders multiple elements.'
  13. )
  14. }
  15. }
  16. }
  17. // tree management
  18. if (!root) {
  19. root = element
  20. checkRootConstraints(root)
  21. } else if (!stack.length) {
  22. // allow root elements with v-if, v-else-if and v-else
  23. if (root.if && (element.elseif || element.else)) {
  24. checkRootConstraints(element)
  25. addIfCondition(root, {
  26. exp: element.elseif,
  27. block: element
  28. })
  29. } else if (process.env.NODE_ENV !== 'production') {
  30. warnOnce(
  31. `Component template should contain exactly one root element. ` +
  32. `If you are using v-if on multiple elements, ` +
  33. `use v-else-if to chain them instead.`
  34. )
  35. }
  36. }
  37. if (currentParent && !element.forbidden) {
  38. if (element.elseif || element.else) {
  39. processIfConditions(element, currentParent)
  40. } else if (element.slotScope) { // scoped slot
  41. currentParent.plain = false
  42. const name = element.slotTarget || '"default"'
  43. ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
  44. } else {
  45. currentParent.children.push(element)
  46. element.parent = currentParent
  47. }
  48. }
  49. if (!unary) {
  50. currentParent = element
  51. stack.push(element)
  52. } else {
  53. closeElement(element)
  54. }

AST 树管理的目标是构建一颗 AST 树,本质上它要维护 root 根节点和当前父节点 currentParent。为了保证元素可以正确闭合,这里也利用了 stack 栈的数据结构,和我们之前解析模板时用到的 stack 类似。
当我们在处理开始标签的时候,判断如果有 currentParent,会把当前 AST 元素 push 到 currentParent.chilldren 中,同时把 AST 元素的 parent 指向 currentParent
接着就是更新 currentParentstack ,判断当前如果不是一个一元标签,我们要把它生成的 AST 元素 push 到 stack 中,并且把当前的 AST 元素赋值给 currentParent
stackcurrentParent 除了在处理开始标签的时候会变化,在处理闭合标签的时候也会变化,因此整个 AST 树管理要结合闭合标签的处理逻辑看。

#处理闭合标签

对应伪代码:

  1. end () {
  2. treeManagement()
  3. closeElement()
  4. }

当解析到闭合标签的时候,最后会执行 end 回调函数:

  1. // remove trailing whitespace
  2. const element = stack[stack.length - 1]
  3. const lastNode = element.children[element.children.length - 1]
  4. if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
  5. element.children.pop()
  6. }
  7. // pop stack
  8. stack.length -= 1
  9. currentParent = stack[stack.length - 1]
  10. closeElement(element)

首先处理了尾部空格的情况,然后把 stack 的元素弹一个出栈,并把 stack 最后一个元素赋值给 currentParent,这样就保证了当遇到闭合标签的时候,可以正确地更新 stack 的长度以及 currentParent 的值,这样就维护了整个 AST 树。
最后执行了 closeElement(elment)

  1. function closeElement (element) {
  2. // check pre state
  3. if (element.pre) {
  4. inVPre = false
  5. }
  6. if (platformIsPreTag(element.tag)) {
  7. inPre = false
  8. }
  9. // apply post-transforms
  10. for (let i = 0; i < postTransforms.length; i++) {
  11. postTransforms[i](element, options)
  12. }
  13. }

closeElement 逻辑很简单,就是更新一下 inVPreinPre 的状态,以及执行 postTransforms 函数,这些我们暂时都不必了解。

#处理文本内容

对应伪代码:

  1. chars (text: string) {
  2. handleText()
  3. createChildrenASTOfText()
  4. }

除了处理开始标签和闭合标签,我们还会在解析模板的过程中去处理一些文本内容:

  1. const children = currentParent.children
  2. text = inPre || text.trim()
  3. ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
  4. // only preserve whitespace if its not right after a starting tag
  5. : preserveWhitespace && children.length ? ' ' : ''
  6. if (text) {
  7. let res
  8. if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
  9. children.push({
  10. type: 2,
  11. expression: res.expression,
  12. tokens: res.tokens,
  13. text
  14. })
  15. } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
  16. children.push({
  17. type: 3,
  18. text
  19. })
  20. }
  21. }

文本构造的 AST 元素有 2 种类型,一种是有表达式的,type 为 2,一种是纯文本,type 为 3。在我们的例子中,文本就是 :,是个表达式,通过执行 parseText(text, delimiters) 对文本解析,它的定义在 src/compiler/parser/text-parsre.js 中:

  1. const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
  2. const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g
  3. const buildRegex = cached(delimiters => {
  4. const open = delimiters[0].replace(regexEscapeRE, '\\$&')
  5. const close = delimiters[1].replace(regexEscapeRE, '\\$&')
  6. return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
  7. })
  8. export function parseText (
  9. text: string,
  10. delimiters?: [string, string]
  11. ): TextParseResult | void {
  12. const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  13. if (!tagRE.test(text)) {
  14. return
  15. }
  16. const tokens = []
  17. const rawTokens = []
  18. let lastIndex = tagRE.lastIndex = 0
  19. let match, index, tokenValue
  20. while ((match = tagRE.exec(text))) {
  21. index = match.index
  22. // push text token
  23. if (index > lastIndex) {
  24. rawTokens.push(tokenValue = text.slice(lastIndex, index))
  25. tokens.push(JSON.stringify(tokenValue))
  26. }
  27. // tag token
  28. const exp = parseFilters(match[1].trim())
  29. tokens.push(`_s(${exp})`)
  30. rawTokens.push({ '@binding': exp })
  31. lastIndex = index + match[0].length
  32. }
  33. if (lastIndex < text.length) {
  34. rawTokens.push(tokenValue = text.slice(lastIndex))
  35. tokens.push(JSON.stringify(tokenValue))
  36. }
  37. return {
  38. expression: tokens.join('+'),
  39. tokens: rawTokens
  40. }
  41. }

parseText 首先根据分隔符(默认是 {{}})构造了文本匹配的正则表达式,然后再循环匹配文本,遇到普通文本就 push 到 rawTokenstokens 中,如果是表达式就转换成 _s(${exp}) push 到 tokens 中,以及转换成 {@binding:exp} push 到 rawTokens 中。
对于我们的例子 :tokens 就是 [_s(item),'":"',_s(index)]rawTokens 就是 [{'@binding':'item'},':',{'@binding':'index'}]。那么返回的对象如下:

  1. return {
  2. expression: '_s(item)+":"+_s(index)',
  3. tokens: [{'@binding':'item'},':',{'@binding':'index'}]
  4. }

:::info parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。
AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。 ::: :::info 当 AST 树构造完毕,下一步就是 optimize 优化这颗树。 :::