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)
基本思路

- 数据劫持/数据代理
Vue 将遍历此对象所有的 property,并使用Object.defineProperty把这些 property 全部转为getter/setter。在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
data(){ return { key: value } }
:::warning
由于使用了Object.defineProperty, Vue 不能检测数组和对象的变化。但是提供了其他的解决方案, Vue3.0使用Proxy重构了。为什么 Vue3.0 不再使用 defineProperty 实现数据监听?
:::
2.数据依赖收集
compile函数中会遍历所有的node上的attribute,如果是v-model进行双向数据绑定,以冒号“:, v-bind”开头的属性我们进行相应数据的监听,如果是“@, v-on”开头的则添加该节点上相应的事件监听。动态模板{{ }}内的内容作为动态处理。
export const onRE = /^@|^v-on:/export const bindRE = /^:|^\.|^v-bind:/// 收集项<span>(双大括号): {{ msg }}</span><div id="item-{\{ id }\}"></div><div>{{ message.split('').reverse().join('') }}</div><div class="{{ className }}"></div><div :class :id :name ...></div><!-- 阻止单击事件冒泡 --><a @click.stop="doThisFunc"></a><!-- 修饰符可以串联 --><a v-on:click.stop.prevent="doThat"><input v-on:keyup.13="submitFuncName"><input v-on:keyup.enter="submitFuncName"><!-- 在 "change" 而不是 "input" 事件中更新 --><input v-model="msg" lazy><div v-for='item in ...' v-for="iten of ..." ></div>
3.模板与数据的绑定
数据依赖收集完毕, 数据改变时所关联的node需要进行更新,用一个watcher对象来进行node与数值之间的联系,并在数据改变时执行相关的更新视图操作。
其他
数组变异方法:
Vue仅仅依赖getter与setter,是无法做到在数组调用push,pop等方法时候触发数据响应的,因此Vue实际上是通过劫持这些方法,对这些方法进行包装变异来实现的。
const arrayProto = Array.prototypeexport const arrayMethods = Object.create(arrayProto)/**Vue 包装的编译数组方法*/const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']/*** 拦截变异方法并触发通知事件*/methodsToPatch.forEach(function (method) {// 缓存原始方法const original = arrayProto[method]def(arrayMethods, method, function mutator (...args) {const result = original.apply(this, args)const ob = this.__ob__let insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':inserted = args.slice(2)break}// 如果有新的数据插入,则插入的数据也要进行一个响应式if (inserted) ob.observeArray(inserted)// 通知更新ob.dep.notify()return result})})
模板解析
编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。它会用到大量正则表达式对字符串解析。
<ul :class="bindClass" class="list" v-if="isShow"><li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li></ul>
经过 parse 过程后,生成的 AST 如下:
ast = {'type': 1,'tag': 'ul','attrsList': [],'attrsMap': {':class': 'bindClass','class': 'list','v-if': 'isShow'},'if': 'isShow','ifConditions': [{'exp': 'isShow','block': // ul ast element}],'parent': undefined,'plain': false,'staticClass': 'list','classBinding': 'bindCls','children': [{'type': 1,'tag': 'li','attrsList': [{'name': '@click','value': 'clickItem(index)'}],'attrsMap': {'@click': 'clickItem(index)','v-for': '(item,index) in data'},'parent': // ul ast element'plain': false,'events': {'click': {'value': 'clickItem(index)'}},'hasBindings': true,'for': 'data','alias': 'item','iterator1': 'index','children': ['type': 2,'expression': '_s(item)+":"+_s(index)''text': '{{item}}:{{index}}','tokens': [{'@binding':'item'},':',{'@binding':'index'}]]}]}
可以看到,生成的 AST 是一个树状结构,每一个节点都是一个 ast element,除了它自身的一些属性,还维护了它的父子关系,如 parent 指向它的父节点,children 指向它的所有子节点。先对 AST 有一些直观的印象,那么接下来我们来分析一下这个 AST 是如何得到的。
#整体流程
首先来看一下 parse 的定义,在 src/compiler/parser/index.js 中:
export function parse (template: string,options: CompilerOptions): ASTElement | void {getFnsAndConfigFromOptions(options)parseHTML(template, {// options ...start (tag, attrs, unary) {let element = createASTElement(tag, attrs)processElement(element)treeManagement()},end () {treeManagement()closeElement()},chars (text: string) {handleText()createChildrenASTOfText()},comment (text: string) {createChildrenASTOfComment()}})return astRootElement}
#解析 HTML 模板
parseHTML(template, options)
对于 template 模板的解析主要是通过 parseHTML 函数,它的定义在 src/compiler/parser/html-parser 中:
export function parseHTML (html, options) {let lastTagwhile (html) {if (!lastTag || !isPlainTextElement(lastTag)){let textEnd = html.indexOf('<')if (textEnd === 0) {if(matchComment) {advance(commentLength)continue}if(matchDoctype) {advance(doctypeLength)continue}if(matchEndTag) {advance(endTagLength)parseEndTag()continue}if(matchStartTag) {parseStartTag()handleStartTag()continue}}handleText()advance(textLength)} else {handlePlainTextElement()parseEndTag()}}}
整体来说它的逻辑就是循环解析 template ,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。 在匹配的过程中会利用 advance 函数不断前进整个模板字符串,直到字符串末尾。
function advance (n) {index += nhtml = html.substring(n)}
为了更加直观地说明 advance 的作用,可以通过一副图表示:
调用 advance 函数:
advance(4)
得到结果:
匹配的过程中主要利用了正则表达式,如下:
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/const ncname = '[a-zA-Z_][\\w\\-\\.]*'const qnameCapture = `((?:${ncname}\\:)?${ncname})`const startTagOpen = new RegExp(`^<${qnameCapture}`)const startTagClose = /^\s*(\/?)>/const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)const doctype = /^<!DOCTYPE [^>]+>/iconst comment = /^<!\--/const conditionalComment = /^<!\[/
通过这些正则表达式,我们可以匹配注释节点、文档类型节点、开始闭合标签等。
- 注释节点、文档类型节点
对于注释节点和文档类型节点的匹配,如果匹配到我们仅仅做的是做前进即可。
if (comment.test(html)) {const commentEnd = html.indexOf('-->')if (commentEnd >= 0) {if (options.shouldKeepComment) {options.comment(html.substring(4, commentEnd))}advance(commentEnd + 3)continue}}if (conditionalComment.test(html)) {const conditionalEnd = html.indexOf(']>')if (conditionalEnd >= 0) {advance(conditionalEnd + 2)continue}}const doctypeMatch = html.match(doctype)if (doctypeMatch) {advance(doctypeMatch[0].length)continue}
对于注释和条件注释节点,前进至它们的末尾位置;对于文档类型节点,则前进它自身长度的距离。
开始标签
const startTagMatch = parseStartTag()if (startTagMatch) {handleStartTag(startTagMatch)if (shouldIgnoreFirstNewline(lastTag, html)) {advance(1)}continue}
首先通过
parseStartTag解析开始标签:function parseStartTag () {const start = html.match(startTagOpen)if (start) {const match = {tagName: start[1],attrs: [],start: index}advance(start[0].length)let end, attrwhile (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {advance(attr[0].length)match.attrs.push(attr)}if (end) {match.unarySlash = end[1]advance(end[0].length)match.end = indexreturn match}}}
对于开始标签,除了标签名之外,还有一些标签相关的属性。函数先通过正则表达式
startTagOpen匹配到开始标签,然后定义了match对象,接着循环去匹配开始标签中的属性并添加到match.attrs中,直到匹配的开始标签的闭合符结束。如果匹配到闭合符,则获取一元斜线符,前进到闭合符尾,并把当前索引赋值给match.end。parseStartTag对开始标签解析拿到match后,紧接着会执行handleStartTag对match做处理:function handleStartTag (match) {const tagName = match.tagNameconst unarySlash = match.unarySlashif (expectHTML) {if (lastTag === 'p' && isNonPhrasingTag(tagName)) {parseEndTag(lastTag)}if (canBeLeftOpenTag(tagName) && lastTag === tagName) {parseEndTag(tagName)}}const unary = isUnaryTag(tagName) || !!unarySlashconst l = match.attrs.lengthconst attrs = new Array(l)for (let i = 0; i < l; i++) {const args = match.attrs[i]if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {if (args[3] === '') { delete args[3] }if (args[4] === '') { delete args[4] }if (args[5] === '') { delete args[5] }}const value = args[3] || args[4] || args[5] || ''const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'? options.shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesattrs[i] = {name: args[1],value: decodeAttr(value, shouldDecodeNewlines)}}if (!unary) {stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })lastTag = tagName}if (options.start) {options.start(tagName, attrs, unary, match.start, match.end)}}
handleStartTag的核心逻辑很简单,先判断开始标签是否是一元标签,类似<img>、<br/>这样,接着对match.attrs遍历并做了一些处理,最后判断如果非一元标签,则往stack里 push 一个对象,并且把tagName赋值给lastTag。至于stack的作用,稍后我会介绍。
最后调用了options.start回调函数,并传入一些参数,这个回调函数的作用稍后我会详细介绍。闭合标签
const endTagMatch = html.match(endTag)if (endTagMatch) {const curIndex = indexadvance(endTagMatch[0].length)parseEndTag(endTagMatch[1], curIndex, index)continue}
先通过正则
endTag匹配到闭合标签,然后前进到闭合标签末尾,然后执行parseEndTag方法对闭合标签做解析。function parseEndTag (tagName, start, end) {let pos, lowerCasedTagNameif (start == null) start = indexif (end == null) end = indexif (tagName) {lowerCasedTagName = tagName.toLowerCase()}if (tagName) {for (pos = stack.length - 1; pos >= 0; pos--) {if (stack[pos].lowerCasedTag === lowerCasedTagName) {break}}} else {pos = 0}if (pos >= 0) {for (let i = stack.length - 1; i >= pos; i--) {if (process.env.NODE_ENV !== 'production' &&(i > pos || !tagName) &&options.warn) {options.warn(`tag <${stack[i].tag}> has no matching end tag.`)}if (options.end) {options.end(stack[i].tag, start, end)}}stack.length = poslastTag = pos && stack[pos - 1].tag} else if (lowerCasedTagName === 'br') {if (options.start) {options.start(tagName, [], true, start, end)}} else if (lowerCasedTagName === 'p') {if (options.start) {options.start(tagName, [], false, start, end)}if (options.end) {options.end(tagName, start, end)}}}
parseEndTag的核心逻辑很简单,在介绍之前我们回顾一下在执行handleStartTag的时候,对于非一元标签(有 endTag)我们都把它构造成一个对象压入到stack中,如图所示:
那么对于闭合标签的解析,就是倒序stack,找到第一个和当前endTag匹配的元素。如果是正常的标签匹配,那么stack的最后一个元素应该和当前的endTag匹配,但是考虑到如下错误情况:<div><span></div>
这个时候当
endTag为</div>的时候,从stack尾部找到的标签是<span>,就不能匹配,因此这种情况会报警告。匹配后把栈到pos位置的都弹出,并从stack尾部拿到lastTag。
最后调用了options.end回调函数,并传入一些参数,这个回调函数的作用稍后我会详细介绍。文本
let text, rest, nextif (textEnd >= 0) {rest = html.slice(textEnd)while (!endTag.test(rest) &&!startTagOpen.test(rest) &&!comment.test(rest) &&!conditionalComment.test(rest)) {next = rest.indexOf('<', 1)if (next < 0) breaktextEnd += nextrest = html.slice(textEnd)}text = html.substring(0, textEnd)advance(textEnd)}if (textEnd < 0) {text = htmlhtml = ''}if (options.chars && text) {options.chars(text)}
接下来判断
textEnd是否大于等于 0 的,满足则说明到从当前位置到textEnd位置都是文本,并且如果<是纯文本中的字符,就继续找到真正的文本结束的位置,然后前进到结束的位置。
再继续判断textEnd小于 0 的情况,则说明整个template解析完毕了,把剩余的html都赋值给了text。
最后调用了options.chars回调函数,并传text参数,这个回调函数的作用稍后我会详细介绍。
因此,在循环解析整个template的过程中,会根据不同的情况,去执行不同的回调函数,下面我们来看看这些回调函数的作用。#处理开始标签
对应伪代码:
start (tag, attrs, unary) {let element = createASTElement(tag, attrs)processElement(element)treeManagement()}
当解析到开始标签的时候,最后会执行
start回调函数,函数主要就做 3 件事情,创建 AST 元素,处理 AST 元素,AST 树管理。下面我们来分别来看这几个过程。创建 AST 元素
// check namespace.// inherit parent ns if there is oneconst ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)// handle IE svg bug/* istanbul ignore if */if (isIE && ns === 'svg') {attrs = guardIESVGBug(attrs)}let element: ASTElement = createASTElement(tag, attrs, currentParent)if (ns) {element.ns = ns}export function createASTElement (tag: string,attrs: Array<Attr>,parent: ASTElement | void): ASTElement {return {type: 1,tag,attrsList: attrs,attrsMap: makeAttrsMap(attrs),parent,children: []}}
通过
createASTElement方法去创建一个 AST 元素,并添加了 namespace。可以看到,每一个 AST 元素就是一个普通的 JavaScript 对象,其中,type表示 AST 元素类型,tag表示标签名,attrsList表示属性列表,attrsMap表示属性映射表,parent表示父的 AST 元素,children表示子 AST 元素集合。处理 AST 元素
if (isForbiddenTag(element) && !isServerRendering()) {element.forbidden = trueprocess.env.NODE_ENV !== 'production' && warn('Templates should only be responsible for mapping the state to the ' +'UI. Avoid placing tags with side-effects in your templates, such as ' +`<${tag}>` + ', as they will not be parsed.')}// apply pre-transformsfor (let i = 0; i < preTransforms.length; i++) {element = preTransforms[i](element, options) || element}if (!inVPre) {processPre(element)if (element.pre) {inVPre = true}}if (platformIsPreTag(element.tag)) {inPre = true}if (inVPre) {processRawAttrs(element)} else if (!element.processed) {// structural directivesprocessFor(element)processIf(element)processOnce(element)// element-scope stuffprocessElement(element, options)}
首先是对模块
preTransforms的调用,其实所有模块的preTransforms、transforms和postTransforms的定义都在src/platforms/web/compiler/modules目录中,这部分我们暂时不会介绍,之后会结合具体的例子说。接着判断element是否包含各种指令通过processXXX做相应的处理,处理的结果就是扩展 AST 元素的属性。这里我并不会一一介绍所有的指令处理,而是结合我们当前的例子,我们来看一下processFor和processIf:export function processFor (el: ASTElement) {let expif ((exp = getAndRemoveAttr(el, 'v-for'))) {const res = parseFor(exp)if (res) {extend(el, res)} else if (process.env.NODE_ENV !== 'production') {warn(`Invalid v-for expression: ${exp}`)}}}export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/const stripParensRE = /^\(|\)$/gexport function parseFor (exp: string): ?ForParseResult {const inMatch = exp.match(forAliasRE)if (!inMatch) returnconst res = {}res.for = inMatch[2].trim()const alias = inMatch[1].trim().replace(stripParensRE, '')const iteratorMatch = alias.match(forIteratorRE)if (iteratorMatch) {res.alias = alias.replace(forIteratorRE, '')res.iterator1 = iteratorMatch[1].trim()if (iteratorMatch[2]) {res.iterator2 = iteratorMatch[2].trim()}} else {res.alias = alias}return res}
processFor就是从元素中拿到v-for指令的内容,然后分别解析出for、alias、iterator1、iterator2等属性的值添加到 AST 的元素上。就我们的示例v-for="(item,index) in data"而言,解析出的的for是data,alias是item,iterator1是index,没有iterator2。function processIf (el) {const exp = getAndRemoveAttr(el, 'v-if')if (exp) {el.if = expaddIfCondition(el, {exp: exp,block: el})} else {if (getAndRemoveAttr(el, 'v-else') != null) {el.else = true}const elseif = getAndRemoveAttr(el, 'v-else-if')if (elseif) {el.elseif = elseif}}}export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {if (!el.ifConditions) {el.ifConditions = []}el.ifConditions.push(condition)}
processIf就是从元素中拿v-if指令的内容,如果拿到则给 AST 元素添加if属性和ifConditions属性;否则尝试拿v-else指令及v-else-if指令的内容,如果拿到则给 AST 元素分别添加else和elseif属性。AST 树管理
我们在处理开始标签的时候为每一个标签创建了一个 AST 元素,在不断解析模板创建 AST 元素的时候,我们也要为它们建立父子关系,就像 DOM 元素的父子关系那样。
AST 树管理相关代码如下:
function checkRootConstraints (el) {if (process.env.NODE_ENV !== 'production') {if (el.tag === 'slot' || el.tag === 'template') {warnOnce(`Cannot use <${el.tag}> as component root element because it may ` +'contain multiple nodes.')}if (el.attrsMap.hasOwnProperty('v-for')) {warnOnce('Cannot use v-for on stateful component root element because ' +'it renders multiple elements.')}}}// tree managementif (!root) {root = elementcheckRootConstraints(root)} else if (!stack.length) {// allow root elements with v-if, v-else-if and v-elseif (root.if && (element.elseif || element.else)) {checkRootConstraints(element)addIfCondition(root, {exp: element.elseif,block: element})} else if (process.env.NODE_ENV !== 'production') {warnOnce(`Component template should contain exactly one root element. ` +`If you are using v-if on multiple elements, ` +`use v-else-if to chain them instead.`)}}if (currentParent && !element.forbidden) {if (element.elseif || element.else) {processIfConditions(element, currentParent)} else if (element.slotScope) { // scoped slotcurrentParent.plain = falseconst name = element.slotTarget || '"default"';(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element} else {currentParent.children.push(element)element.parent = currentParent}}if (!unary) {currentParent = elementstack.push(element)} else {closeElement(element)}
AST 树管理的目标是构建一颗 AST 树,本质上它要维护 root 根节点和当前父节点 currentParent。为了保证元素可以正确闭合,这里也利用了 stack 栈的数据结构,和我们之前解析模板时用到的 stack 类似。
当我们在处理开始标签的时候,判断如果有 currentParent,会把当前 AST 元素 push 到 currentParent.chilldren 中,同时把 AST 元素的 parent 指向 currentParent。
接着就是更新 currentParent 和 stack ,判断当前如果不是一个一元标签,我们要把它生成的 AST 元素 push 到 stack 中,并且把当前的 AST 元素赋值给 currentParent。stack 和 currentParent 除了在处理开始标签的时候会变化,在处理闭合标签的时候也会变化,因此整个 AST 树管理要结合闭合标签的处理逻辑看。
#处理闭合标签
对应伪代码:
end () {treeManagement()closeElement()}
当解析到闭合标签的时候,最后会执行 end 回调函数:
// remove trailing whitespaceconst element = stack[stack.length - 1]const lastNode = element.children[element.children.length - 1]if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {element.children.pop()}// pop stackstack.length -= 1currentParent = stack[stack.length - 1]closeElement(element)
首先处理了尾部空格的情况,然后把 stack 的元素弹一个出栈,并把 stack 最后一个元素赋值给 currentParent,这样就保证了当遇到闭合标签的时候,可以正确地更新 stack 的长度以及 currentParent 的值,这样就维护了整个 AST 树。
最后执行了 closeElement(elment):
function closeElement (element) {// check pre stateif (element.pre) {inVPre = false}if (platformIsPreTag(element.tag)) {inPre = false}// apply post-transformsfor (let i = 0; i < postTransforms.length; i++) {postTransforms[i](element, options)}}
closeElement 逻辑很简单,就是更新一下 inVPre 和 inPre 的状态,以及执行 postTransforms 函数,这些我们暂时都不必了解。
#处理文本内容
对应伪代码:
chars (text: string) {handleText()createChildrenASTOfText()}
除了处理开始标签和闭合标签,我们还会在解析模板的过程中去处理一些文本内容:
const children = currentParent.childrentext = inPre || text.trim()? isTextTag(currentParent) ? text : decodeHTMLCached(text)// only preserve whitespace if its not right after a starting tag: preserveWhitespace && children.length ? ' ' : ''if (text) {let resif (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {children.push({type: 2,expression: res.expression,tokens: res.tokens,text})} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {children.push({type: 3,text})}}
文本构造的 AST 元素有 2 种类型,一种是有表达式的,type 为 2,一种是纯文本,type 为 3。在我们的例子中,文本就是 :,是个表达式,通过执行 parseText(text, delimiters) 对文本解析,它的定义在 src/compiler/parser/text-parsre.js 中:
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/gconst regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/gconst buildRegex = cached(delimiters => {const open = delimiters[0].replace(regexEscapeRE, '\\$&')const close = delimiters[1].replace(regexEscapeRE, '\\$&')return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')})export function parseText (text: string,delimiters?: [string, string]): TextParseResult | void {const tagRE = delimiters ? buildRegex(delimiters) : defaultTagREif (!tagRE.test(text)) {return}const tokens = []const rawTokens = []let lastIndex = tagRE.lastIndex = 0let match, index, tokenValuewhile ((match = tagRE.exec(text))) {index = match.index// push text tokenif (index > lastIndex) {rawTokens.push(tokenValue = text.slice(lastIndex, index))tokens.push(JSON.stringify(tokenValue))}// tag tokenconst 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))}return {expression: tokens.join('+'),tokens: rawTokens}}
parseText 首先根据分隔符(默认是 {{}})构造了文本匹配的正则表达式,然后再循环匹配文本,遇到普通文本就 push 到 rawTokens 和 tokens 中,如果是表达式就转换成 _s(${exp}) push 到 tokens 中,以及转换成 {@binding:exp} push 到 rawTokens 中。
对于我们的例子 :,tokens 就是 [_s(item),'":"',_s(index)];rawTokens 就是 [{'@binding':'item'},':',{'@binding':'index'}]。那么返回的对象如下:
return {expression: '_s(item)+":"+_s(index)',tokens: [{'@binding':'item'},':',{'@binding':'index'}]}
:::info
parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。
AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。
:::
:::info
当 AST 树构造完毕,下一步就是 optimize 优化这颗树。
:::
