摘要:寫文章不容易,點個贊唄兄弟專注源碼分享,文章分為白話版和源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于版本如果你覺得排版難看,請點擊下面鏈接或者拉到下面關注公眾號也可以吧原理源碼版之屬性解析哈哈哈,今天終
寫文章不容易,點個贊唄兄弟
專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧
研究基于 Vue版本 【2.5.17】
如果你覺得排版難看,請點擊 下面鏈接 或者 拉到 下面關注公眾號也可以吧
【Vue原理】Compile - 源碼版 之 屬性解析
哈哈哈,今天終于到了屬性解析的部分了,之前已經講過了 parse 流程,標簽解析,最后就只剩下 屬性解析了 (′???`)
如果你對 compile 不感興趣的就先不看把,畢竟不會馬上起到什么作用~~ヾ(●′?`●)
如果你們沒看過前面兩篇文章的,十分建議看一下~
Compile 之 Parse 主要流程
Compile 之 標簽解析
如果看了,你們應該知道《屬性解析》在哪部分中,沒錯,在處理 頭標簽的 部分 parse-start 中
那么我們就來到 parse - start 這個函數中!
看到下面的源碼中,帶有 process 的函數都是用于處理 屬性的
function parse(template){ parseHTML(template,{ start:(...抽出放下面) }) } function start(tag, attrs, unary) { // 創建 AST 節點 var element = createASTElement(tag, attrs, currentParent); // 節點需要解析,并沒有還沒有處理 if (!element.processed) { processFor(element); processIf(element); processSlot(element); for (var i = 0; i < transforms.length; i++) { element = transforms[i](element, options) || element; } processAttrs(element); } .... 省略部分不重要代碼 // 父節點就是上一個節點,直接放入 上一個節點的 children 數組中 if (currentParent) { // 說明前面節點有 v-if if (element.elseif || element.else) { processIfConditions(element, currentParent); } else { currentParent.children.push(element); element.parent = currentParent; } } }
看完了吧,上面處理屬性的函數大概有幾個
沒啥難的,就是內容多了點
1、processFor,解析 v-for 2、processIf,解析 v-if 3、processSlot,解析 slot 4、processAttrs,解析其他屬性 5、transforms,解析樣式屬性
并且只有 element.processed 為 false 的時候,才會進行解析
因為 element.processed 表示屬性已經解析完畢,一開始 element.processed 的值是 undefined
下面就會逐個說明上面的方法
先明確下 element 是什么?parse 流程中說過了,element 是 通過解析得到的 tag 信息,生成的 ast
下面會逐個分析下上面的四個函數,并會附上相應的 element 例子作為參考
其實還有很多其他處理函數,為了維持文章的長度,所以我去掉了
開篇之前,大家需要先了解 getAndRemoveAttr 這個函數,下面很多地方都會使用到
作用就是從 el.attrList 中查找某個屬性,返回返回屬性值
function getAndRemoveAttr(el, name, removeFromMap) { var val =el.attrsMap[name]; if (removeFromMap) { delete el.attrsMap[name]; } return val }parse-start 中的 tramsforms
在parse -start 這個函數的 開頭,我們看到有一個 transfroms 的東西
transforms 是一個數組,存放兩個函數,一個是處理 動靜態的 class,一個處理 動靜態的 style
兩種處理都很簡單的,我們來簡單看看處理結果就好了
處理 classfunction transformNode(el, options) { var staticClass = getAndRemoveAttr(el, "class"); if (staticClass) { el.staticClass = JSON.stringify(staticClass); } // :class="b" 直接返回 b var classBinding = getBindingAttr(el, "class", false); if (classBinding) { el.classBinding = classBinding; } }
{ classBinding: "b" staticClass: ""a"" tag: "span" type: 1 }處理 style
function transformNode$1(el, options) { var staticStyle = getAndRemoveAttr(el, "style"); if (staticStyle) { // 比如綁定 style="height:0;width:0" // parseStyleText 解析得到對象 { height:0,width:0 } el.staticStyle = JSON.stringify(parseStyleText(staticStyle)); } // :style="{height:a}" 解析得 {height:a} var styleBinding = getBindingAttr(el, "style", false); if (styleBinding) { el.styleBinding = styleBinding; } }
{ staticStyle: "{"width":"0"}" styleBinding: "{height:a}" tag: "span" type: 1 }解析 v-for
在 parse - start 這個函數中,看到了 processFor,沒錯,就是解析 v-for 指令的!
function processFor(el) { var exp = getAndRemoveAttr(el, "v-for") if (exp) { // 比如指令是 v-for="(item,index) in arr" // res = {for: "arr", alias: "item", iterator1: "index"} var res = parseFor(exp); if (res) { // 把 res 和 el 屬性合并起來 extend(el, res); } } }
沒有什么難度,直接看模板 和最終結果好了
{ alias: "item", for: "arr", iterator1: "index", tag: "div", type: 1, }解析 v-if
在 parse - start 這個函數中,看到了 processFor,沒錯,就是解析 v-if 指令的!
function processIf(el) { var exp = getAndRemoveAttr(el, "v-if"); if (exp) { el.if = exp; (el.ifConditions || el.ifConditions=[]) .push({ exp: exp, block: el }) } else { if (getAndRemoveAttr(el, "v-else") != null) { el.else = true; } var elseif = getAndRemoveAttr(el, "v-else-if"); if (elseif) { el.elseif = elseif; } } }
處理 v-if 上是這樣的,需要把 v-if 的 表達式 和 節點都保存起來
而 v-else ,只需要設置 el.else 為 true,v-else-if 同樣需要保存 表達式
在這里 v-else 和 v-else-if 并沒有做太多處理,而是在最前面的 parse-start 中有處理
if (element.elseif || element.else) { processIfConditions(element, currentParent); }
當經過 processIf 之后,該屬性存在 elseif 或 else
那么會調用一個方法,如下
function processIfConditions(el, parent) { var prev = findPrevElement(parent.children); if (prev && prev.if) { (prev.ifConditions ||prev.ifConditions=[]) .push({ exp: el.elseif, block: el }) } }
這個方法主要是把 帶有 v-else-if 和 v-else 的節點掛靠在 帶有 v-if 的節點上
先來看掛靠后的結果
{ tag: "header", type: 1, children:[{ tag: "header", type: 1, if: "a", ifCondition:[ {exp: "a", block: {header的ast 節點}} {exp: "b", block: {strong的ast 節點}} {exp: undefined, block: {span的ast節點}} ] },{ tag: "p" type: 1 }] }
我們可以看到,原來寫的兩個子節點,strong 和 span 都不在 div 的children 中
而是跑到了 header 的 ifCondition 里面
現在看看 processIfConditions , 這個方法是只會處理 帶有 v-else-if 和 v-else 的節點的
并且需要找到 v-if 的節點掛靠,怎么找的呢?你可以看到一個方法
function findPrevElement(children) { var i = children.length; while (i--) { if (children[i].type === 1) { return children[i] } else { children.pop(); } } }
從同級子節點中結尾開始找,當type ==1 的時候,這個節點就是帶有 v-if 的節點
那么 v-else 那兩個就可以直接掛靠在上面了
你會問,為什么從結尾不是返回 span 節點,為什么 type ==1 就是帶有 v-if?
首先,你并不能從正常解析完的角度去分析,要從標簽逐個解析的角度去分析
比如現在已經解析完了 v-if 的節點,并且添加進了 父節點的 children
然后解析下一個節點,比如這個節點是帶有 v-else-if 的節點,此時,再去 parent.children 找最后一個節點(也就是剛剛添加進去的 v-if 節點)
肯定返回的是 v-if 的節點,自然能正確掛靠了
v-else 同理
如果你說 v-if 和 v-else-if 隔了一個其他節點,那 v-else-if 就無法掛靠在 v-if 了呢
那你肯定是刁民,v-else-if 必須跟著 v-if 的,否則都會報錯,錯誤就不討論了
解析 slot在 parse - start 這個函數中,看到了 processSlot,沒錯,就是解析 slot 相關
function processSlot(el) { if (el.tag === "slot") { el.slotName = el.attrsMap.name } else { var slotScope = getAndRemoveAttr(el, "slot-scope") el.slotScope = slotScope; // slot 的名字 var slotTarget = el.attrsMap.slot if (slotTarget) { el.slotTarget = slotTarget === """" ? ""default"" : slotTarget; } } }
這個好像也沒什么好講的,就簡單記錄一下 解析的結果好了
子組件模板
解析成
{ tag: "span" type: 1 children:[{ attrsMap: {name: " header", :a: "num", :b: "num"} slotName: "" header"" tag: "slot" type: 1 }] }
父組件模板
{{ c }}
解析成
{ children: [{ tag: "child", type: 1, children: [{ slotScope: "c", slotTarget: ""header "", tag: "p", type: 1 }] }], tag: "div", type: 1 }
下面內容很多,但是不難
解析其他屬性這一塊內容很多,但是總的來說沒有難度,就是看得煩了一些,然后把源碼放到了最后,打算先寫解析
這里集中處理了剩下的其他類型的屬性,大致分了兩種情況
1Vue 自帶屬性比如 帶有 "v-" , ":" , " @" 三種符號的屬性名,這三種每種都會分開處理
而在這三種屬性開始處理前,會把屬性名帶有的 modifiers 給提取出來
比如帶有 modifiers 的指令
v-bind.a.b.c = "xxxx"
經過處理,會提取出 modifiers 對象,如下
{a: true, b: true, c: true}
以供指令使用
之后就開始處理三種類型屬性
1 " : "我們都知道 " : " 等于 "v-bind" ,所有當匹配到這種屬性名的時候,會進入這里的處理
大致看一遍之后,可以看到,經過這部分的處理
屬性會存放進 el.props 或者 el.attrs
那么問題來了?
怎么判斷屬性放入 el.props 還是 el.attrs 呢?
有兩種條件
1、modifiers.prop
當你給指令添加了 .prop 的時候,比如
那么 sex 這個屬性,就會被存放到 el.props
2、表單
你看到這一句代碼
!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
第一,不能是組件
第二,是表單元素,且是表單重要屬性
來看看 platformMustUseProp 吧,很容易
當元素是 input,textarea,option,select,progress
屬性是 selected ,checked ,value 等之類的話
都要存放到 el.props 中
function a(tag, type, attr) { return ( (attr === "value" && "input,textarea,option,select,progress".indexOf(tag)>-1) && type !== "button" || (attr === "selected" && tag === "option") || (attr === "checked" && tag === "input") || (attr === "muted" && tag === "video") ) };
或許你會問
el.props 和 el.attrs 有什么區別呢?
props 是直接添加到 dom 屬性上的,而不會顯示在標簽上
attrs 則是用于顯示到到 標簽屬性上的
還有一個問題
添加進 el.props 的屬性,為什么要轉換成駝峰命名?
你看到的,所有屬性名,都會通過一個 camelize 的方法,為什么呢?
因為 DOM 的屬性都是駝峰命名的,不存在橫桿的命名
所以要把 a-b 的命名都轉成 aB,隨便截了一張圖
然而 innerHTML 比較特殊,駝峰都不行,所以做了特殊處理,你也看到的
駝峰的方法應該挺有用的,放上來吧
var camelize = function(str) { return str.replace(/-(w)/g, function(_, c) { return c ? c.toUpperCase() : ""; }) })
modifiers.sync
之后,你應該還發現了一塊寶藏,沒錯就是 sync
相信你應該用過吧,用于父子通信的,子組件想修改父組件傳入的 prop
通過事件的方式,間接修改 父組件的數據,從而更新 props
為了避免大家不記得了,在這里貼一個使用例子
父組件 給 子組件 傳入 name ,加入 sync 可以雙向修改子組件想修改 父組件傳入的 name,直接觸發事件并傳入參數就可以了 this.$emit("update:name", 222)
于是現在我們來看他在屬性解析時是怎么實現的
addHandler(el, "update:" + camelize(name), genAssignmentCode(value, "$event") );
看看這段代碼做了什么
首先
camelize(name)
把名字變成駝峰寫法,比如 get-name,轉換成 getName
然后下面這段代碼 執行
genAssignmentCode(value, "$event")
解析返回 "value = $event"
然后 addHandler 就是把 事件名和事件回調保存到 el.events 中,如下
保存的 events 后面會被繼續解析,value 會被包一層 function
相當于給子組件監聽事件
@update:name ="function($event){ xxx = $event }"
$event 就是子組件觸發事件時 傳入的值
xxx 是 父組件的數據,賦值之后,就相當于子組件修改父組件數據了
要是想了解 event 的內部原理,可以看 Event - 源碼版 之 綁定組件自定義事件
2 " @ "當匹配到 @ 或者 v-on 的時候,屬于添加事件,這里沒有太多處理
addHandler 就是把所有事件保存到 el.events
3 " v- "剩下 帶有 v- 的屬性,都會放到這里處理
匹配參數的,源碼中注釋也說清楚了,這里不解釋了
然后統統保存到 el.directives 中
2普通屬性沒啥說的,普通屬性,直接存放進 el.attrs
下面就是處理其他屬性的源碼,你別看很長,其實很簡單的!
var onRE = /^@|^v-on:/; var dirRE = /^v-|^@|^:/; var bindRE = /^:|^v-bind:/; var modifierRE = /.[^.]+/g; var argRE = /:(.*)$/; function processAttrs(el) { var list = el.attrsList; var i, l, name, rawName, value, modifiers, isProp; for (i = 0, l = list.length; i < l; i++) { name = rawName = list[i].name; value = list[i].value; // 判斷屬性是否帶有 "v-" , "@" , ":" if (dirRE.test(name)) { // mark element as dynamic el.hasBindings = true; // 比如 v-bind.a.b.c = "xxzxxxx" // 那么 modifiers = {a: true, b: true, c: true} modifiers = parseModifiers(name); // 抽取出純名字 if (modifiers) { // name = "v-bind.a.b.c = "xxzxxxx" " // 那么 name= v-bind name = name.replace(modifierRE, ""); } // 收集動態屬性,v-bind,可能是綁定的屬性,可能是傳入子組件的props // bindRE = /^:|^v-bind:/ if (bindRE.test(name)) { // 抽取出純名字,比如 name= v-bind // 替換之后,name = bind name = name.replace(bindRE, ""); isProp = false; if (modifiers) { // 直接添加到 dom 的屬性上 if (modifiers.prop) { isProp = true; // 變成駝峰命名 name = camelize(name); if (name === "innerHtml") name = "innerHTML"; } // 子組件同步修改 if (modifiers.sync) { addHandler(el, // 得到駝峰命名 "update:" + camelize(name), // 得到 "value= $event" genAssignmentCode(value, "$event") ); } } // el.props 的作用上面有說,這里有部分是 表單的必要屬性都要保存在 el.props 中 if ( isProp || // platformMustUseProp 判斷這個屬性是不是要放在 el.props 中 // 比如表單元素 input 等,屬性是value selected ,checked 等 // 比如 tag=input,name=value,那么value 屬性要房子啊 el.props 中 (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)) ) { (el.props || (el.props = [])).push({ name, value }); } // 其他屬性放在 el.attrs 中 else { (el.attrs || (el.attrs = [])).push({ name, value }); } } // 收集事件,v-on , onRE = /^@|^v-on:/ else if (onRE.test(name)) { // 把 v-on 或者 @ 去掉,拿到真正的 指令名字 // 比如 name ="@click" , 替換后 name = "click" name = name.replace(onRE, ""); addHandler(el, name, value, modifiers, false); } // 收集其他指令,比如 "v-once", else { // 把v- 去掉,拿到真正的 指令名字 name = name.replace(dirRE, ""); // name = "bind:key" , argMatch = [":a", "a"] var argMatch = name.match(argRE); var arg = argMatch && argMatch[1]; if (arg) { // 比如 name = "bind:key" ,去掉 :key // 然后 name = "bind" name = name.slice(0, -(arg.length + 1)); } (el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers }); } } else { (el.attrs || (el.attrs = [])).push({ name, value }); } } }最后
鑒于本人能力有限,難免會有疏漏錯誤的地方,請大家多多包涵,如果有任何描述不當的地方,歡迎后臺聯系本人,有重謝
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106587.html
摘要:當字符串開頭是時,可以匹配匹配尾標簽。從結尾,找到所在位置批量閉合。 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請點擊 下面鏈接 或者 拉到 下面關注公眾號也可以吧 【Vue原理】Compile - 源碼版 之 標簽解析...
寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請點擊 下面鏈接 或者 拉到 下面關注公眾號也可以吧 【Vue原理】Compile - 源碼版 之 Parse 主要流程 本文難度較繁瑣,需要耐心觀看,如果你對 compile 源碼暫時...
摘要:寫文章不容易,點個贊唄兄弟專注源碼分享,文章分為白話版和源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于版本如果你覺得排版難看,請點擊下面鏈接或者拉到下面關注公眾號也可以吧原理源碼版之節點數據拼接上一篇我們 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究...
摘要:頁面這個實例,按理就需要解析兩次,但是有緩存之后就不會理清思路也就是說,其實內核就是不過是經過了兩波包裝的第一波包裝在中的內部函數中內部函數的作用是合并公共和自定義,但是相關代碼已經省略,另一個就是執行第二波包裝在中,目的是進行緩存 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 ...
摘要:一旦我們檢測到這些子樹,我們可以把它們變成常數,這樣我們就不需要了在每次重新渲染時為它們創建新的節點在修補過程中完全跳過它們。否則,吊裝費用將會增加好處大于好處,最好總是保持新鮮。 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,...
閱讀 1837·2023-04-25 14:49
閱讀 3117·2021-09-30 09:47
閱讀 3101·2021-09-06 15:00
閱讀 2224·2019-08-30 13:16
閱讀 1436·2019-08-30 10:48
閱讀 2668·2019-08-29 15:11
閱讀 1287·2019-08-26 14:06
閱讀 1663·2019-08-26 13:30