Vue源碼後記-其余內置指令(2)
……
指令這個講起來還有點復雜,先把html弄上來:
<body> <div id=‘app‘> <div v-if="vIfIter" v-bind:style="styleObject"> <input v-show="vShowIter" v-model=‘vModel‘ /> <span v-once>{{msg}}</span> <div v-html="html"></div> </div> <div class=‘on‘>empty Node</div> </div> </body> <script src=‘./vue.js‘></script> <script> var app = new Vue({ el: ‘#app‘, data: { vIfIter: true, vShowIter: true, vModel: 1, styleObject: { color: ‘red‘ }, msg: ‘Hello World‘, html: ‘<span>v-html</span>‘ }, }); </script>
上一節是解析完了input標簽的2個屬性,並將其打包進了directives屬性中返回。
接著會繼續跑genData函數,如下:
function genData(el) { var data = ‘{‘; // 從這出來 var dirs = genDirectives(el); if (dirs) { data += dirs + ‘,‘; } // code... // DOM props if (el.props) { data += "domProps:{" + (genProps(el.props)) + "},"; } // event handlers if (el.events) { data += (genHandlers(el.events, false, warn$3)) + ","; } // code... data = data.replace(/,$/, ‘‘) + ‘}‘; // v-bind data wrap if (el.wrapData) { data = el.wrapData(data); } return data }
由於在處理v-model的時候給el添加了props與events屬性,所以之後會跑進兩個處理函數。
首先看看genProps:
// props => {name:value,value:(vModel)} function genProps(props) { var res = ‘‘; for (var i = 0; i < props.length; i++) { var prop = props[i]; res += "\"" + (prop.name) + "\":" + (transformSpecialNewlines(prop.value)) + ","; } return res.slice(0, -1) } // 替換新換行符 function transformSpecialNewlines(text) { return text .replace(/\u2028/g, ‘\\u2028‘) .replace(/\u2029/g, ‘\\u2029‘) }
比較簡單,直接看返回的字符串:
接下來是處理添加的events:
// event handlers if (el.events) { data += (genHandlers(el.events, false, warn$3)) + ","; } function genHandlers(events, native, warn) { var res = native ? ‘nativeOn:{‘ : ‘on:{‘; for (var name in events) { var handler = events[name]; // click.right => contextmenu // 右鍵並不能觸發click事件 建議使用H5新屬性 然而這個屬性兼容性捉急 res += "\"" + name + "\":" + (genHandler(name, handler)) + ","; } return res.slice(0, -1) + ‘}‘; } // name => inpout // handler => {modifiers => null,value:‘if(...)...‘} function genHandler(name, handler) { if (!handler) { return ‘function(){}‘ } if (Array.isArray(handler)) { return ("[" + (handler.map(function(handler) { return genHandler(name, handler); }).join(‘,‘)) + "]") } // simplePathRE => /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[‘.*?‘]|\[".*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*\s*$/; // 這是匹配的啥玩意啊! var isMethodPath = simplePathRE.test(handler.value); // fnExpRE => /^\s*([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/; // 匹配箭頭函數或function var isFunctionExpression = fnExpRE.test(handler.value); // 匹配完 兩個都是false // 包裝成函數形式返回 if (!handler.modifiers) { return isMethodPath || isFunctionExpression ? handler.value : ("function($event){" + (handler.value) + "}") // inline statement } // 處理事件後綴 else { // code... } }
事件處理方面也很明了,首先判斷是原生事件還是自定義,然後根據表達式的形式進行處理返回,本例會被包裝為一個普通的函數返回,如圖:
字符串代表DOM上有一個input事件,值為"on:..."
至此,input標簽AST轉換完畢,返回的code字符串如圖:
"_c(‘input‘, {directives:[{name:"show",rawName:"v-show",value:(vShowIter),expression:"vShowIter"},{name:"model",rawName:"v-model",value:(vModel),expression:"vModel"}], domProps:{"value":(vModel)}, on:{"input":function($event){if($event.target.composing)return;vModel=$event.target.value}}})"
包含tag名、內置指令v-model/v-show、props、events。
下面處理span標簽,包含一個v-once:
<span v-once>{{msg}}</span>
這個el有個once屬性,會進入genOnce函數:
function genOnce(el) { el.onceProcessed = true; // 優先處理v-if if (el.if && !el.ifProcessed) { return genIf(el) } else if (el.staticInFor) { // 同時出現v-once與v-for } else { return genStatic(el) } } // hoist static sub-trees out function genStatic(el) { el.staticProcessed = true; // 該節點屬於靜態dom staticRenderFns.push(("with(this){return " + (genElement(el)) + "}")); return ("_m(" + (staticRenderFns.length - 1) + (el.staticInFor ? ‘,true‘ : ‘‘) + ")") } function genElement(el) { if (el.staticRoot && !el.staticProcessed) { // once/for/if/templte/slot } else { // component or element var code; if (el.component) { code = genComponent(el.component, el); } else { var data = el.plain ? undefined : genData(el); var children = el.inlineTemplate ? null : genChildren(el, true); code = "_c(‘" + (el.tag) + "‘" + (data ? ("," + data) : ‘‘) + (children ? ("," + children) : ‘‘) + ")"; } // module transforms => return code } }
由於v-once是一次性渲染,所以會被歸類於靜態渲染,render函數會被彈入staticRenderFns數組。
這裏還有一個值得註意的是,由於span節點只有v-once,並沒有其余的屬性,沒有必要進入genData進行屬性切割,所以早在parseHTML階段,該el的plain屬性被置為true,跳過genData階段。
// parseHTML => processRawAttrs function processRawAttrs(el) { var l = el.attrsList.length; if (l) { // code... } else if (!el.pre) { el.plain = true; } }
呃……
沒有屬性處理會genChildren,子節點是一個表達式,會返回一個如圖字符串:,最開始跑源碼的時候見過_v、_s,不用的是該表達式被彈入了staticRenderFns數組。
至此,span標簽解析完畢。
下面開始解析下一個div標簽:
<div v-html="html"></div>
該標簽包含一個內置指令,所以會進入genDirectives分支,並調用對應的gen函數處理v-html:
function html(el, dir) { // dir.value => html if (dir.value) { addProp(el, ‘innerHTML‘, ("_s(" + (dir.value) + ")")); } }
代碼很簡單,將v-html對應的值用_s包裹起來,添加到props屬性中,如圖:
這裏需要註意的是,該函數並未返回任何值,因為v-html不需要弄進directives屬性,所以默認返回的是undefined,直接會跳過後面條件判斷返回。
因為設置了props屬性,所以後面的genProps也要跑,過程不看了直接看結果:
至此,v-html的標簽解析完了,返回一個render函數:
下面解析根元素的下一個子元素,靜態div:
<div class=‘on‘>empty Node</div>
調用流程是這樣的:generate => genChildren => genNode => genElement => genData && genChildren
節點包含一個靜態的類以及字符串子節點,所以會調用最後的genData與genChildren返回一個render字符串,流程比較簡單,所以直接放結果:
到此為止,所有的AST都被轉化為render函數,可以看一下結構:
_c(‘div‘ /*<div id=‘app‘>*/ , { attrs: { "id": "app" } }, [(vIfIter) /*v-if條件*/ ? // 條件為真渲染下面的DOM _c(‘div‘ /*<div v-if="vIfIter" v-bind:style="styleObject">*/ , { style: (styleObject) }, [_c(‘input‘ /*<input v-show="vShowIter" v-model=‘vModel‘ />*/ , { directives: [{ name: "show", rawName: "v-show", value: (vShowIter), expression: "vShowIter" }, { name: "model", rawName: "v-model", value: (vModel), expression: "vModel" }], domProps: { "value": (vModel) }, on: { "input": function($event) { if ($event.target.composing) return; vModel = $event.target.value } } }), _v(" ") /*這些是回車換行符*/ , _m(0) /*<span v-once>{{msg}}</span>*/ , _v(" "), _c(‘div‘ /*<div v-html="html"></div>*/ , { domProps: { "innerHTML": _s(html) } }) ]) : // 否則渲染一個空的div _e() /*<div></div>*/ , _v(" "), _c(‘div‘ /*<div class=‘on‘>empty Node</div>*/ , { staticClass: "on" }, [_v("empty Node")]) ])
這樣看起來結構就非常清晰了,render函數針對v-if是使用一個三元表示式處理的,該函數的整體框架結構為_c(tagName,attrs,childrens),其中childrens可能嵌套多個_c。
其中有個小點需要註意的是裏面並沒有v-once的那個span標簽,取而代之的是一個_m(0),這是因為span標簽是靜態節點,所以被彈入staticRenderFns數組並作為第一個元素,這裏將來會將其取出來並渲染。
這個patch下次再講,最近被項目搞的腦袋疼。
Vue源碼後記-其余內置指令(2)