經過對 Vue2.0 源碼閱讀,想寫一寫本身的理解,能力有限故從尤大佬2016.4.11第一次提交開始讀,準備陸續寫:javascript
其中包含本身的理解和源碼的分析,儘可能通俗易懂!因爲是2.0的最先提交,因此和最新版本有不少差別、bug,後續將陸續補充,敬請諒解!包含中文註釋的Vue源碼已上傳...html
今天要說的代碼全在codegen文件夾中,在說實現原理前,仍是先看個簡單的例子!vue
<div class="container"> <span>{{msg}}</span> <button :class="{active: isActive}" @click="handle">change msg</button> </div>
上述類名爲container
的元素節點包含5個子節點(其中3個是換行文本節點),轉化成的AST語法樹:java
AST語法樹轉的render函數長這樣:node
function _render() { with (this) { return __h__( 'div', {staticClass: "container"}, [ " ", __h__('span', {}, [String((msg))]), " ", __h__('button', {class: {active: isActive},on:{"click":handle}}, ["change msg"]), " " ] ) }; }
能夠的看出,render函數作的事情很簡單,就是把語法樹每一個節點的指令進行解析。git
看下render函數,它是由with函數包裹(爲了改變做用域),要用的時候直接_render.call(vm)
;另外就是__h__
函數,這個後面會說到,這個函數用於元素節點的解析,接收3個參數:元素節點標籤名,節點數據,子節點數據。這個函數最後返回的就是虛擬dom了,不過今天先不深究,先說如何生成這樣的render函數,主要是v-if
、v-for
、v-bind
、v-on
等指令的解析。github
這邊解析的是從AST樹轉換成render函數部分的源碼,因爲vue2.0第一次提交的源碼這部分不全,故作了部分更新,代碼全在codegen文件夾中。express
整個AST語法樹轉render函數的起點是index.js
文件中的generate()
函數:segmentfault
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
的編譯:
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函數,但代碼量感受挺大的,這邊還有不少地方等待完善,等後續繼續補充...
好睏啊,晚安了