目前社区有很多 Vue.js 的源码解读文章,但是质量层次不齐,不够系统和全面,最近我将从各个细枝末节解读 Vue.js 的实现原理,针对 vue3 版本之前,让同学们可以彻底掌握 Vue.js。 友情提示:阅读本文大概需要** 30****分钟**
前言
Vue.Js 是采用虚拟DOM的前端框架之一,其中每一个vue对象都有 template,它类似于html又不完全相同,究竟Vue.js是如何将data中的数据渲染到真实的宿主环境中的?又是如何通过“响应式”修改数据的?template是如何被编译成真实环境中可用的HTML的?AST语法树又是什么? 本篇文章将从AST语法树的基础上,详细讲解从 template 到 DOM 的全过程。
$mount
在Vue的mount过程中,template会被编译成AST语法树,AST是指抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式。然后,经过 generate(将AST语法树转化成 render function 字符串的过程)得到render函数,返回VNode。VNode是Vue的虚拟DOM节点,里面包含标签名、子节点、文本等信息。
首先看一下 mount 的代码:
/*把原本不带编译的$mount方法保存下来,在最后会调用。*/
const mount = Vue.prototype.$mount
/*挂载组件,带模板编译*/
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to html or body - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
/*处理模板templete,编译成render函数,render不存在的时候才会编译template,否则优先使用render*/
if (!options.render) {
let template = options.template
/*template存在的时候取template,不存在的时候取el的outerHTML*/
if (template) {
/*当template是字符串的时候*/
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
/*当template为DOM节点的时候*/
template = template.innerHTML
} else {
/*报错*/
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
/*获取element的outerHTML*/
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
/*将template编译成render函数,这里会有render以及staticRenderFns两个返回,这是vue的编译时优化,static静态不需要在VNode更新时进行patch,优化性能*/
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`${this._name} compile`, 'compile', 'compile end')
}
}
}
/*Github:https://github.com/answershuto*/
/*调用const mount = Vue.prototype.$mount保存下来的不带编译的mount*/
return mount.call(this, el, hydrating)
}
通过 mount 代码我们可以看到,在mount的过程中,如果 render 函数不存在(render函数存在会优先使用render)会将template进行compileToFunctions 得到 render 以及 staticRenderFns。比如说手写组件时加入了template 的情况都会在运行时进行编译。而 render function 在运行后会返回 VNode 节点,供页面的渲染以及在 update 的时候 patch。接下来我们来看一下 template 是如何编译的。
AST语法树
首先,我们需要提前了解AST,因为template会被编译成AST,那么AST是什么?
在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。AST会经过 generate 得到 render 函数,render 的返回值是 VNode,VNode是Vue的虚拟DOM节点(关于虚拟DOM将在后续解读),具体定义代码如下:
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?ArrayVNode;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
functionalContext: Component | void; // only for functional component root nodes
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
/*Github:https://github.com/answershuto*/
constructor (
tag?: string,
data?: VNodeData,
children?: ?ArrayVNode,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空间*/
this.ns = undefined
/*编译作用域*/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*静态节点标志*/
this.isStatic = false
/*是否作为跟节点插入*/
this.isRootInsert = true
/*是否为注释节点*/
this.isComment = false
/*是否为克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
CreateCompiler
/*编译,将模板template编译成AST、render函数以及staticRenderFns函数*/
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
finalOptions.warn = (msg, tip) = {
(tip ? tips : errors).push(msg)
}
/*做下面这些merge的目的因为不同平台可以提供自己本身平台的一个baseOptions,内部封装了平台自己的实现,然后把共同的部分抽离开来放在这层compiler中,所以在这里需要merge一下*/
if (options) {
// merge custom modules
/*合并modules*/
if (options.modules) {
finalOptions.modules = (baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
/*合并directives*/
finalOptions.directives = extend(
Object.create(baseOptions.directives),
options.directives
)
}
// copy other options
for (const key in options) {
/*合并其余的options,modules与directives已经在上面做了特殊处理了*/
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
/*基础模板编译,得到编译结果*/
const compiled = baseCompile(template, finalOptions)
if (process.env.NODE_ENV !== 'production') {
errors.push.apply(errors, detectErrors(compiled.ast))
}
compiled.errors = errors
compiled.tips = tips
return compiled
}
compile主要做了两件事,一件是合并option(前面说的将平台自有的option与传入的option进行合并),另一件是baseCompile,进行模板template的编译。
baseCompile
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
/*parse解析得到AST*/
const ast = parse(template.trim(), options)
/*
将AST进行优化
优化的目标:生成模板AST,检测不需要进行DOM改变的静态子树。
一旦检测到这些静态树,我们就能做以下这些事情:
1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。
2.在patch的过程中直接跳过。
*/
optimize(ast, options)
/*根据AST生成所需的code(内部包含render与staticRenderFns)*/
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
baseCompile首先会将模板template进行parse得到一个AST,再通过optimize做一些优化,最后通过generate得到render以及staticRenderFns。
parse
// 1.parse 解析模板字符串生成ast,源码中有2千多行
// const ast = parse(template.trim(), options)
// 对模板做解析,生成ast,对源代码抽象语法结构的树状表现形式
// 利用大量的正则表达式对字符串解析。
// tag:ul
// attrsMap:装属性
// events:装事件 'click':{'value':''}
// 除了一些自身属性,还维护了他的父子关系。parent指向父节点,children指向子节点
// html-parse函数解析模板
// 循环解析template,用正则进行各种匹配,对不同的情况分别处理,直到整个template被解析完毕,在匹配过程中利用advance函数不断的前进整个模板字符串,直到字符串末尾
// 匹配过程中主要使用了正则表达式来进行操作
// attr的匹配表达式
// 标签的开始符,标签的结束符
// 注释节点,文档类型节点
parse会用正则等方式解析template模板中的指令、class、style等数据,parse函数实现了这个转换的步骤,通过各种正则适配将 html 解析成AST。
optimize
optimize,翻译成中文是优化,而optimize完成的正是优化的过程,optimize通过对节点添加标志,从而在页面重新刷新进行vnode比对时,跳过静态节点,提高性能。
// 2.optimize(ast, options)
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)
}
主要执行两个方法,markStatic和 markStaticRoots,root即为通过parse阶段生成的ast抽象语法树。
// 2.1 markStatic
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (let i = 0, l = node.children.length; i l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
//2.1.1 首先调用isStatic
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
可以看到,如果node 是表达式,返回false,如果是文本节点,返回true,如果有v-for click等的属性也返回false,然后回到markStatic,可以看到会对node的children子节点递归调用markStaic,如果子节点不是static,就会把当前节点也置为非static,这就是markStatic的主要过程,下面我们进入markstaticRoot。
// 2.2
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
optimize的主要作用是标记static静态节点,这是Vue在编译过程中的一处优化,后面当update更新界面时,会有一个patch的过程,diff算法会直接跳过静态节点,从而减少了比较的过程,优化了patch的性能。
generate
编译的最后一个步骤是generate,这里是真正的生成render函数,最后返回的是一个字符串,可由new Function或eval进行执行。
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
主要执行了genElement方法,我们进入genElement方法。
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
可以看到,这时会根据不同的节点属性执行不同的方法,例如如果只是普通的节点会或返回一个code字符串,内容为’_c(xx’,_c这些方法是什么呢,其实这些方法都定义在辅助函数中。
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}
因为情况非常多,我们就拿 v-for 来举例说明一下执行过程,实例 templete为。
// 举个栗子
ulli v-for="item of [1,2,3]"{{item}}/li/ul
v-for源码
export function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
if (process.env.NODE_ENV !== 'production' &&
state.maybeComponent(el) &&
el.tag !== 'slot' &&
el.tag !== 'template' &&
!el.key
) {
state.warn(
`${el.tag} v-for="${alias} in ${exp}": component lists rendered with ` +
`v-for should have explicit keys. ` +
`See https://vuejs.org/guide/list.html#key for more info.`,
el.rawAttrsMap['v-for'],
true /* tip */
)
}
el.forProcessed = true // avoid recursion
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
可以看到首先对templete类型的key进行了判断,如果没有,会进行警告,最后返回了一个_l作为函数名的执行函数,参数为定义的v-for值,函数内容为另一个genElement,最后整个templete返回的render函数内容为:
"with(this){return _c('ul',_l(([1,2,3]),function(item){return _c('li',[_v(_s(item))])}),0)}"
generate就是将AST转化成render funtion字符串的过程,得到结果是render的字符串以及staticRenderFns字符串。最后render函数在执行时就会把_c和_l等替换成真正的函数,从而返回一个vnode,再继续完成patch,插入真实dom树完成渲染。
总结
到现在为止,templete 编译过程已经完成。我们的template模板已经被转化成了我们所需的AST、render function字符串以及staticRenderFns字符串。下面借用网友的demo(源地址:http://www.jianshu.com/p/63ac3b9ede5d):
// 举个栗子
div class="main" :class="bindClass"
div{{text}}/div
divhello world/div
div v-for="(item, index) in arr"
p{{item.name}}/p
p{{item.value}}/p
p{{index}}/p
p---/p
/div
div v-if="text"
{{text}}
/div
div v-else/div
/div
转化后得到AST,如下图:
我们可以看到最外层的div是这颗AST的根节点,节点上有许多数据代表这个节点的形态,比如static表示是否是静态节点,staticClass表示静态class属性(非bind:class)。children代表该节点的子节点,可以看到children是一个长度为4的数组,里面包含的是该节点下的四个div子节点。children里面的节点与父节点的结构类似,层层往下形成一棵AST。
再来看看由AST得到的render函数:
with(this){
return _c( 'div',
{
/*static class*/
staticClass:"main",
/*bind class*/
class:bindClass
},
[
_c( 'div', [_v(_s(text))]),
_c('div',[_v("hello world")]),
/*这是一个v-for循环*/
_l(
(arr),
function(item,index){
return _c( 'div',
[_c('p',[_v(_s(item.name))]),
_c('p',[_v(_s(item.value))]),
_c('p',[_v(_s(index))]),
_c('p',[_v("---")])]
)
}
),
/*这是v-if*/
(text)?_c('div',[_v(_s(text))]):_c('div',[_v("no text")])],
2
)
}
看了render function字符串,发现有大量的_c,_v,_s,_q,这些函数究竟是什么?直接翻源码看看。
/* @flow */
/*
内部处理render的函数
这些函数会暴露在Vue原型上以减小渲染函数大小
*/
/*处理v-once的渲染函数*/
Vue.prototype._o = markOnce
/*将字符串转化为数字,如果转换失败会返回原字符串*/
Vue.prototype._n = toNumber
/*将val转化成字符串*/
Vue.prototype._s = toString
/*处理v-for列表渲染*/
Vue.prototype._l = renderList
/*处理slot的渲染*/
Vue.prototype._t = renderSlot
/*检测两个变量是否相等*/
Vue.prototype._q = looseEqual
/*检测arr数组中是否包含与val变量相等的项*/
Vue.prototype._i = looseIndexOf
/*处理static树的渲染*/
Vue.prototype._m = renderStatic
/*处理filters*/
Vue.prototype._f = resolveFilter
/*从config配置中检查eventKeyCode是否存在*/
Vue.prototype._k = checkKeyCodes
/*合并v-bind指令到VNode中*/
Vue.prototype._b = bindObjectProps
/*创建一个文本节点*/
Vue.prototype._v = createTextVNode
/*创建一个空VNode节点*/
Vue.prototype._e = createEmptyVNode
/*处理ScopedSlots*/
Vue.prototype._u = resolveScopedSlots
}
通过源码展示的函数,render函数最后会返回一个VNode节点,在_update的时候,经过patch与之前的VNode节点进行比较,得出差异后将这些差异渲染到真实的DOM上,这里和React的DIFF算法类似。
补充
前面总结了这么多,无非是从源码角度了解到Template转换成DOM的实现,下面将手写一套函数来模拟实现 htmlParse 解析器的功能,首先要分析一下需要对哪些元素进行解析:
实现思路:
匹配单个字符 - 匹配标签 - 匹配属性 **- **匹配文本 - 匹配结束标签
实现步骤:
// 模拟 template
// 转化HTML至AST对象
function parse(template){
var currentParent; //当前父节点
var root; //最终生成的AST对象
var stack = []; //插入栈
var startStack = []; //开始标签栈
var endStack = []; //结束标签栈
//console.log(template);
parseHTML(template,{
start:function start(targetName,attrs,unary,start,end,type,text){//标签名 ,attrs,是否结束标签,文本开始位置,文本结束位置,type,文本,
var element = { //我们想要的对象
tag:targetName,
attrsList:attrs,
parent:currentParent, //需要记录父对象吧
type:type,
children:[]
}
if(!root){ //根节点哈
root = element;
}
if(currentParent && !unary){ //有父节点并且不是结束标签?
currentParent.children.push(element); //插入到父节点去
element.parent = currentParent; //记录父节点
}
if (!unary) { //不是结束标签?
if(type == 1){
currentParent = element;//不是结束标签,当前父节点就要切换到现在匹配到的这个开始标签哈,后面再匹配到
startStack.push(element); //推入开始标签栈
}
stack.push(element); //推入总栈
}else{
endStack.push(element); //推入结束标签栈
currentParent = startStack[endStack.length-1].parent; //结束啦吧当前父节点切到上一个开始标签,这能理解吧,当前这个已经结束啦
}
//console.log(stack,"currentstack")
},
end:function end(){
},
chars:function chars(){
}
});
console.log(root,"root");
return root;
};
// Regular Expressions for parsing tags and attributes
var singleAttrIdentifier = /([^s"'/=]+)/;
var singleAttrAssign = /(?:=)/;
var singleAttrValues = [
// attr value double quotes
/"([^"]*)"+/.source,
// attr value, single quotes
/'([^']*)'+/.source,
// attr value, no quotes
/([^s"'=`]+)/.source
];
var attribute = new RegExp(
'^\s*' + singleAttrIdentifier.source +
'(?:\s*(' + singleAttrAssign.source + ')' +
'\s*(?:' + singleAttrValues.join('|') + '))?'
);
// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
// but for Vue templates we can enforce a simple charset
var ncname = '[a-zA-Z_][\w\-\.]*';
var qnameCapture = '((?:' + ncname + '\:)?' + ncname + ')';
var startTagOpen = new RegExp('^' + qnameCapture);
var startTagClose = /^s*(/?)/;
var endTag = new RegExp('^\/' + qnameCapture + '[^]*');
var doctype = /^!DOCTYPE [^]+/i;
var comment = /^!--/;
var conditionalComment = /^![/;
//偷懒哈 上面的正则是我在vue上拿下来的,这个后期可以研究,下面的话简单的写两个用用,和vue原版的是有一些差别的
//{{变量}}
var varText = new RegExp('{{' + ncname + '}}');
//空格与换行符
var space = /^s/;
var checline = /^[rn]/;
/**
type 1普通标签
type 2代码
type 3普通文本
*/
function parseHTML(html,options){
var stack = []; //内部也要有一个栈
var index = 0; //记录的是html当前找到那个索引啦
var last; //用来比对,当这些条件都走完后,如果last==html 说明匹配不到啦,结束while循环
var isUnaryTag = false;
while(html){
last = html;
var textEnd = html.indexOf('');
if(textEnd === 0){ //这一步如果第一个字符是那么就只有两种情况,1开始标签 2结束标签
//结束标签
var endTagMatch = html.match(endTag); //匹配
if(endTagMatch){
console.log(endTagMatch,"endTagMatch");
isUnaryTag = true;
var start = index;
advance(endTagMatch[0].length); //匹配完要删除匹配到的,并且更新index,给下一次匹配做工作
options.start(null,null,isUnaryTag,start,index,1);
continue;
}
//初始标签
var startMatch = parseStartTag();
if(startMatch){
parseStartHandler(startMatch);//封装处理下
console.log(stack,"startMatch");
continue;
}
}
if(html === last){
console.log(html,"html");
break;
}
}
function advance (n) {
index += n;
html = html.substring(n);
}
//处理起始标签 主要的作用是生成一个match 包含初始的attr标签
function parseStartTag(){
var start = html.match(startTagOpen);
if(start){
var match = {
tagName: start[1], // 标签名(div)
attrs: [], // 属性
start: index // 游标索引(初始为0)
};
advance(start[0].length);
var end, attr;
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {//在endClose之前寻找attribute
advance(attr[0].length);
match.attrs.push(attr);
}
if (end) {
advance(end[0].length); // 标记结束位置
match.end = index; //这里的index 是在 parseHTML就定义 在advance里面相加
return match // 返回匹配对象 起始位置 结束位置 tagName attrs
}
}
}
//对match进行二次处理,生成对象推入栈
function parseStartHandler(match){
var _attrs = new Array(match.attrs.length);
for(var i=0,len=_attrs.length;ilen;i++){ //这儿就是找attrs的代码哈
var args = match.attrs[i];
var value = args[3] || args[4] || args[5] || '';
_attrs[i] = {
name:args[1],
value:value
}
}
stack.push({tag: match.tagName,type:1, lowerCasedTag: match.tagName.toLowerCase(), attrs: _attrs}); //推栈
options.start(match.tagName, _attrs,false, match.start, match.end,1); //匹配开始标签结束啦。
}
}
总结
vue中的templete最后都会编译成render函数来执行,编译的过程主要由parse,optimize,generate组成。最后手写一个 htmlParse 解析器作为总结。本文的实例代码地址:https://github.com/GitLuoSiyu/vue-learn
最后
今天的 Vue2总结(八)AST语法树 就分享到这里,我的公众号没有留言功能哈,有问题大家心里默念,我能感受到,谢谢 ~
原文始发于微信公众号(程序员思语):