通过对 Vue2.0 源码阅读,想写一写自己的理解,能力有限故从尤大佬2016.4.11第一次提交开始读,准备陆续写:模版字符串转AST语法树AST语法树转render函数Vue双向绑定原理Vue虚拟dom比较原理其中包含自己的理解和源码的分析,尽量通俗易懂!由于是2.0的最早提交,所以和最新版本有很多差异、bug,后续将陆续补充,敬请谅解!包含中文注释的Vue源码已上传…开始今天要说的代码全在codegen文件夹中,在说实现原理前,还是先看个简单的例子!<div class=“container”> <span>{{msg}}</span> <button :class="{active: isActive}" @click=“handle”>change msg</button></div>上述类名为container的元素节点包含5个子节点(其中3个是换行文本节点),转化成的AST语法树:AST语法树转的render函数长这样:function render() { with (this) { return h( ‘div’, {staticClass: “container”}, [ " “, h(‘span’, {}, [String((msg))]), " “, h(‘button’, {class: {active: isActive},on:{“click”:handle}}, [“change msg”]), " " ] ) };}可以的看出,render函数做的事情很简单,就是把语法树每个节点的指令进行解析。看下render函数,它是由with函数包裹(为了改变作用域),要用的时候直接_render.call(vm);另外就是__h__函数,这个后面会说到,这个函数用于元素节点的解析,接收3个参数:元素节点标签名,节点数据,子节点数据。这个函数最后返回的就是虚拟dom了,不过今天先不深究,先说如何生成这样的render函数,主要是v-if、v-for、v-bind、v-on等指令的解析。源码解析这边解析的是从AST树转换成render函数部分的源码,由于vue2.0第一次提交的源码这部分不全,故做了部分更新,代码全在codegen文件夹中。入口整个AST语法树转render函数的起点是index.js文件中的generate()函数:export function generate (ast) { const code = genElement(ast); return new Function (with (this) { return ${code}}
);}明显看到,generate()函数传入参数为AST语法树,内部调用genElement()函数开始解析根节点(容器节点)。genElement()函数用于解析元素节点,它接收两个参数:AST对象和节点标识(v-for的key),最后返回形如__h_(‘div’, {}, [])的字符串,看一下内部逻辑:function genElement (el, key) { let exp; if (exp = getAndRemoveAttr(el, ‘v-for’)) { // 解析v-for指令 return genFor(el, exp); } else if (exp = getAndRemoveAttr(el, ‘v-if’)) { // 解析v-if指令 return genIf(el, exp, key); } else if (el.tag === ’template’) { // 解析子组件 return genChildren(el); } else { return __h__('${el.tag}', ${genData(el, key) }, ${genChildren(el)})
; }}genElement()函数内部依次调用getAndRemoveAttr()函数判断了v-for、v-if标签是否存在,若存在则删除并返回表达式;随后判断节点名为template就直接进入子节点解析;以上条件都不符合就返回__h__函数字符串,该字符串将使用到属性解析和子节点解析。function getAndRemoveAttr (el, attr) { let val; // 如果属性存在,则从AST对象的attrs和attrsMap移除 if (val = el.attrsMap[attr]) { el.attrsMap[attr] = null; for (let i = 0, l = el.attrs.length; i < l; i++) { if (el.attrs[i].name === attr) { el.attrs.splice(i, 1); break; } } } return val;}v-for 和 v-if 指令解析让我们先看看v-for的编译:function genFor (el, exp) { const inMatch = exp.match(/([a-zA-Z_][\w])\s+(?:in|of)\s+(.)/); if (!inMatch) { throw new Error(‘Invalid v-for expression: ‘+ exp); } const alias = inMatch[1].trim(); exp = inMatch[2].trim(); let key = getAndRemoveAttr(el, ’track-by’); // 后面用 :key 代替了 track-by if (!key) { key =‘undefined’; } else if (key !== ‘$index’) { key = alias + ‘[”’ + key + ‘”]’; } return (${exp}) && (${exp}).map(function (${alias}, $index) {return ${genElement(el, key)}})
;}该函数先进行正则匹配,如"item in items",将解析出别名(item)和表达式(items),再去看看当前节点是否含:key,如果有那就作为genElement()函数的参数解析子节点。举个????,对于模版<div v-for=“item in items” track-by=“id”></div>,将解析成:(items) && (items).map(function (item, $index) {return ${genElement(el, item["id"])}})
你会发现v-for解析完,通过mao循环对该节点继续解析,但此时该节点已经没有的v-for和:key属性了。继续看看v-if的解析:function genIf (el, exp, key) { return (${exp}) ? ${genElement(el, key)} : null
;}v-if的解析就很粗暴,直接通过条件运算符去决定继续解析该节点,还是直接返回 null。属性解析这里说的属性解析,包括了v-bind指令、v-on指令和v-model指令的解析,以及普通属性的解析。这些解析都在genData()函数中:function genData (el, key) { if (!el.attrs.length && !key) { return ‘{}’; } let data = ‘{’; let attrs = attrs:{
; let props = props:{
; let events = {}; let hasAttrs = false; let hasProps = false; let hasEvents = false; … if (hasAttrs) { data += attrs.slice(0, -1) + ‘},’; } if (hasProps) { data += props.slice(0, -1) + ‘},’; } if (hasEvents) { data += genEvents(events); // 事件解析 } return data.replace(/,$/, ‘’) + ‘}’;}看一下genData()函数整体,先是判断有没有属性,然后定义了多个变量:data是输出结果;attrs用于存储节点属性;props用于存储节点某些特殊属性;event用于存储事件;hasxxx是当前节点是否含xxx的标识。随后会进行属性的遍历计算,最后通过对hasxxx的判断来对data进行拼接输出。重点是中间属性的遍历、各种指令/属性的处理,先看看特殊的key和class:if (key) { data += key:${key},
;}const classBinding = getAndRemoveAttr(el, ‘:class’) || getAndRemoveAttr(el, ‘v-bind:class’);if (classBinding) { data += class: ${classBinding},
;}const staticClass = getAndRemoveAttr(el, ‘class’);if (staticClass) { data += staticClass: "${staticClass}",
;}这边也是调用getAndRemoveAttr()获取class属性,并以动态和静态进行存储,比较简单。再来看看其他属性的处理:for (let i = 0, l = el.attrs.length; i < l; i++) { let attr = el.attrs[i]; let name = attr.name; let value = attr.value; if (/^v-|^@|^:/.test(name)) { const modifiers = parseModifiers(name); // 事件修饰符(.stop/.prevent/.self) name = removeModifiers(name); if (/^:|^v-bind:/.test(name)) { // v-bind name = name.replace(/^:|^v-bind:/, ‘’); if (name === ‘style’) { data += style: ${value},
; } else if (/^(value|selected|checked|muted)$/.test(name)) { hasProps = true; props += "${name}": (${value}),
; } else { hasAttrs = true; attrs += "${name}": (${value}),
; } } else if (/^@|^v-on:/.test(name)) { // v-on hasEvents = true; name = name.replace(/^@|^v-on:/, ‘’); addHandler(events, name, value, modifiers); } else if (name === ‘v-model’) { // v-model hasProps = hasEvents = true; props += genModel(el, events, value) + ‘,’; } } else { hasAttrs = true; attrs += "${name}": (${JSON.stringify(attr.value)}),
; }}通过for循环对节点属性进行遍历,先用/^v-|^@|^:/正则判断当前属性是否为指令,若不是就直接添加到attrs中,若是就需要继续进行解析了。进入if后首先来到了事件修饰符的处理,主要用到了parseModifiers()、removeModifiers()两个函数,主要就是拿到事件修饰符并删除,如v-on:click.prevent.self,将返回[‘prevent’, ‘self’],简单看一下:function parseModifiers (name) { const match = name.match(/.[^.]+/g); if (match) { return match.map(m => m.slice(1)); }}function removeModifiers (name) { return name.replace(/.[^.]+/g, ‘’);}然后进入v-bind的处理,依次处理了:style、特殊属性、其他属性…这边特殊属性用正则/^(value|selected|checked|muted)$/去匹配,之所以特殊我的理解是:含有该属性的元素会在页面加载时给自身默认状态,如想默认选择复选框,给它加上checked=“checked"就行了,但是后续不能用setAttribute()修改,而是通过checkboxObject.checked=true|false更改状态。v-bind解析完了,进入v-on的解析,主要是用到了addHandler()函数,这部分在event.js中。function addHandler (events, name, value, modifiers) { const captureIndex = modifiers && modifiers.indexOf(‘capture’); if (captureIndex > -1) { modifiers.splice(captureIndex, 1); name = ‘!’ + name; } const newHandler = { value, modifiers }; const handlers = events[name]; if (isArray(handlers)) { handlers.push(newHandler); } else if (handlers) { events[name] = [handlers, newHandler]; } else { events[name] = newHandler; }}该函数先对capture事件修饰符(事件捕获模式)进行了判断,若有就给name前加个!标识;然后就去events里面找是否已经有name事件了,找到一种情况追加进去,所以events可能长这样:{click: change, mouseleave: [fn1, fn2]}。最后来说说v-model指令,实现原理就是v-bind和v-on的结合,例如你想对输入框进行双向绑定,你也可以写成<input :value=“val” @input=“fn”>{ data: { val: ’’ }, methods: { fn (e) { this.val = e.target.value; } }}所以对双向绑定的处理,就是对不同的元素节点采用不同的事件绑定而已,如对于select标签用onchange监听,对文本输入框用oninput监听…这部分的代码全在model.js文件中,看一下genModel()函数吧:function genModel (el, events, value) { if (el.tag === ‘select’) { if (el.attrsMap.multiple != null) { // 同时选择多个选项 return genMultiSelect(events, value, el) } else { return genSelect(events, value) } } else { switch (el.attrsMap.type) { case ‘checkbox’: return genCheckboxModel(events, value) case ‘radio’: return genRadioModel(events, value, el) default: return genDefaultModel(events, value) } }}依次找了select标签和input标签,这边还考虑到了下拉标签的多选情况,然后找对应函数去解析,这边就拿文本框的处理函数genDefaultModel()来举例:function genDefaultModel (events, value) { addHandler(events, ‘input’, ${value}=$event.target.value
); return value:(${value})
;}该函数先调用之前提到的addHandler()函数添加时间,再返回value属性追加到props中。其他下拉框、单选框等的处理函数也是类似…最后还有对事件的处理,我们前面只是把事件都存储到events对象中,需要处理后添加到data返回值中,主要用到的函数是genEvents():const simplePathRE = /^[A-Za-z_$][\w$](?:.[A-Za-z_$][\w$]|[’.?’]|[”.?"]|[\d+]|[[A-Za-z_$][\w$]])$/const modifierCode = { stop: ‘$event.stopPropagation();’, prevent: ‘$event.preventDefault();’, self: ‘if($event.target !== $event.currentTarget)return;’}function genEvents (events) { let res = ‘on:{’; for (let name in events) { res += "${name}":${genHandler(events[name])},
; } return res.slice(0, -1) + ‘}’;}function genHandler (handler) { if (!handler) { return function(){}
; } else if (isArray(handler)) { // handler为数组则循环调用 return [${handler.map(genHandler).join(',')}]
; } else if (!handler.modifiers || !handler.modifiers.length) { return simplePathRE.test(handler.value) ? handler.value : function($event){${handler.value}}
; } else { let code = ‘function($event){’; for (let i = 0; i < handler.modifiers.length; i++) { let modifier = handler.modifiers[i]; code += modifierCode[modifier]; } let handlerCode = simplePathRE.test(handler.value) ? handler.value + ‘()’ : handler.value; return code + handlerCode + ‘}’; }}simplePathRE正则用于看属性值是否是简单函数名,fn是简单函数名而fn(‘x’)不是;modifierCode对象用于存储事件修饰符对应的js代码;genEvents()函数对events对象进行遍历,调用genHandler()函数逐个解析;genHandler()函数内部是对不同的参数进行不同的处理,做的比较好的是:对是否是简单函数的处理,例如@click=“fn"会返回click: fn,@click=“fn(‘11’)“会返回click: function($event){fn(‘11’)},这将大大便利了后续dom事件的绑定;对是否含事件修饰符的处理,例如@click.stop=“fn”,将返回click: function($event){$event.stopPropagation();fn()}。到这里,所有属性都解析完毕了!返回的结果形如{key: …,class: …,staticClass: …,attrs: {…},props: {…},on: {…}}。子节点解析子节点的解析主要是用到了genChildren()函数:function genChildren (el) { if (!el.children.length) { return ‘undefined’; } return ‘[’ + el.children.map(node => { if (node.tag) { return genElement(node); } else { return genText(node); } }).join(’,’) + ‘]’;}通过map方法对子节点数组进行循环,依次判断节点标签是否存在,再分别解析元素节点和文本节点,最后将结果拼接成数组形式的字符串。元素节点的解析函数genElement()上面说过了,接下来说说文本节点的解析函数genText():function genText (text) { if (text === ’ ‘) { return ‘” “’; } else { const exp = parseText(text); if (exp) { return ‘String(’ + exp + ‘)’; } else { return JSON.stringify(text); } }}判断一波是否有文本,有就继续调用parseText()函数:const tagRE = /{{((?:.|\n)+?)}}/g;export function parseText (text) { if (!tagRE.test(text)) { return null; } var tokens = []; var lastIndex = tagRE.lastIndex = 0; var match, index, value; while (match = tagRE.exec(text)) { // 循环解析 {{}} index = match.index; // 把 ‘{{’ 之前的文本推入 if (index > lastIndex) { tokens.push(JSON.stringify(text.slice(lastIndex, index))); } // 把{{}}中间数据取出推入 value = match[1]; tokens.push(’(’ + match[1].trim() + ‘)’); lastIndex = index + match[0].length; } if (lastIndex < text.length) { tokens.push(JSON.stringify(text.slice(lastIndex))); } return tokens.join(’+’);}该函数通过循环调用tagRE正则匹配文本,依次匹配出 {{}},并推入数组,最后将数组转为字符串。例如文本hi,{{name}}!,将返回’hi’+(name)+’!’。总结到这也终于算是说完了,虽然这部分做的事情比较简单,主要就是指令解析,将AST树解析为render函数,但代码量感觉挺大的,这边还有很多地方等待完善,等后续继续补充…好困啊,晚安了