摘要:先說遍歷,很簡單,如下行左右代碼就足夠遍歷一個對象了遇到普通數據屬性,直接處理,遇到對象,遍歷屬性之后遞歸進去處理屬性,遇到數組,遞歸進去處理數組元素。這樣改進之后我就不需要對數組元素進行響應式處理,只是遇到數組的時候把數組的方法變異即可。
用了Vue很久了,最近決定系統性的看看Vue的源碼,相信看源碼的同學不在少數,但是看的時候卻發現挺有難度,Vue雖然足夠精簡,但是怎么說現在也有10k行的代碼量了,深入進去逐行查看的時候感覺內容龐雜并且搞不懂代碼的目的,同時網上的深入去仔細闡述Vue的compile/link/ expression parse/依賴訂閱和收集/batcher的文章卻不多,我自己讀源碼時,深感在這些環節可供參考的資料稀缺。網上較多的文章都在講getter/setter、Mutation Observer和LRU緩存。所以我趁著寒假詳細的閱讀了Vue構建整個響應式過程的代碼,基本包括數據observe到模板解析、transclude、compile、link、指令的bind、update、dom批處理更新、數組diff等等環節,并用這篇文章詳細的介紹出來,希望能幫到想學習Vue源碼或者想參與Vue維護、提交pr的同學。
Vue源碼詳解系列文章和配套的我整理的Vue源碼注釋版已經在git上開項:Vue源碼注釋版及詳解,歡迎大家在git上查看,并配合注釋版源碼使用。訂閱文章更新請watch。
注釋版源碼主要注釋了本文中涉及的部分,依然有很多沒有涉及,我個人精力有限,歡迎大家提pr,如果您喜歡,多謝您的star~
本文介紹的源碼版本是當前(17年2月23日)1.x版本的最新版v1.0.26,2.x版本的源碼我先學學虛擬dom之后再進行。
源碼整體概覽Vue源碼構造實例的過程就一行this._init(options),用你的參數對象去執行init初始化函數。init函數中先進行了大量的參數初始化操作this.xxx = blabla,然后剩下這么幾行代碼(后文所有的英文注釋是尤雨溪所寫,中文是我添加的,英文注釋極其精確、簡潔,請勿忽略)
this._data = {} // call init hook this._callHook("init") // initialize data observation and scope inheritance. this._initState() // setup event system and option events. this._initEvents() // call created hook this._callHook("created") // if `el` option is passed, start compilation. if (options.el) { this.$mount(options.el) }
基本就是觸發init鉤子,初始化一些狀態,初始化event,然后觸發created鉤子,最后掛載到具體的元素上面去。_initState()方法中包含了數據的初始化操作,也就是讓數據變成響應式的,讓Vue能夠監聽到數據的變動。而this.$mount()方法則承載了絕大部分的代碼量,負責模板的嵌入、編譯、link、指令和watcher的生成、批處理的執行等等。
從數據的響應化說起嗯,是的,雖然這個observe數據的部分已經被很多文章說爛了,但是我并不只是講getter/setter,這里應該會有你沒看過的部分,比如Vue是如何解決"getter/setter無法監聽屬性的添加和刪除"的。
熟悉Vue的同學都了解Vue的響應式特性,對于data對象的幾乎任何更改我們都能夠監聽到。這是MVVM的基礎,基本思路就是遍歷每一個屬性,然后使用Object.defineProperty將這個屬性設置為響應式的(即我能監聽到他的改動)。
先說遍歷,很簡單,如下10行左右代碼就足夠遍歷一個對象了:
function touch (obj) { if (typeof obj === "object") if (Array.isArray(obj)) { for (let i = 0,l = obj.length; i < l; i++) { touch(obj[i]) } } else { let keys = Object.keys(obj) for (let key of keys) touch(obj[key]) } console.log(obj) }
遇到普通數據屬性,直接處理,遇到對象,遍歷屬性之后遞歸進去處理屬性,遇到數組,遞歸進去處理數組元素(console.log)。
遍歷完就到處理了,也就是Object.defineProperty部分了,對于一個對象,我們可以用這個來改寫它屬性的getter/setter,這樣,當你改屬性的值我就有辦法監聽到。但是對于數組就有問題了。
你也許想到可以遍歷當前存在的下標,然后執行Object.defineProperty。這種處理方法先不說性能問題,很多時候我們操作數組是采用push、pop、splice、unshift等方法來操作的,光是push你就沒辦法監聽,更不要說pop后你設置的getter/setter就直接沒了。
所以,Vue的方法是,改寫數組的push、pop等8個方法,讓他們在執行之后通知我數組更新了(這種方法帶來的后果就是你不能直接修改數組的長度或者通過下標去修改數組。參見官網)。這樣改進之后我就不需要對數組元素進行響應式處理,只是遇到數組的時候把數組的方法變異即可。于是在用戶使用數組的push、pop等方法會改變數組本身的方法時,可以監聽到數組變動。
此外,當數組內部元素是對象時,設置getter/setter是可以監聽對象的,所以對于數組元素還是要遍歷一下的。如果不是對象,比如a[0]是字符串、數字?那就沒辦法了,但是vue為數組提供了$set和$remove,方便我們可以通過下標去響應式的改動數組元素,這里后文再說。
我們先說說怎么“變異”數組的push等方法,并且找出數組元素中的對象,讓對象響應式。我們結合我的注釋版源碼來看一下。
Vue.prototype._initData = function () { // 初始化數據,其實一方面把data的內容代理到vm實例上, // 另一方面改造data,變成reactive的 // 即get時觸發依賴收集(將訂閱者加入Dep實例的subs數組中),set時notify訂閱者 var dataFn = this.$options.data var data = this._data = dataFn ? dataFn() : {} var props = this._props // proxy data on instance var keys = Object.keys(data) var i, key i = keys.length while (i--) { key = keys[i] // 將data屬性的內容代理到vm上面去,使得vm訪問指定屬性即可拿到_data內的同名屬性 // 實現vm.prop === vm._data.prop, // 這樣當前vm的后代實例就能直接通過原型鏈查找到父代的屬性 // 比如v-for指令會為數組的每一個元素創建一個scope,這個scope就繼承自vm或上級數組元素的scope, // 這樣就可以在v-for的作用域中訪問父級的數據 this._proxy(key) } // observe data //重點來了 observe(data, this) }
(注釋里的依賴收集、Dep什么的大家看不懂沒關系,請跳過,后面會細說)
代碼中間做了_proxy操作,注釋里我已經寫明原因。_proxy操作也很簡單想了解的話大家自己查看源碼即可。
代理完了之后就開始observe這個data:
export function observe (value, vm) { if (!value || typeof value !== "object") { // 保證只有對象會進入到這個函數 return } var ob if ( //如果這個數據身上已經有ob實例了,那observe過了,就直接返回那個ob實例 hasOwn(value, "__ob__") && value.__ob__ instanceof Observer ) { ob = value.__ob__ } else if ( shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // 是對象(包括數組)的話就深入進去遍歷屬性,observe每個屬性 ob = new Observer(value) } if (ob && vm) { // 把vm加入到ob的vms數組當中,因為有的時候我們會對數據手動執行$set/$delete操作, // 那么就要提示vm實例這個行為的發生(讓vm代理這個新$set的數據,和更新界面) ob.addVm(vm) } return ob }
代碼的執行過程一般都是進入到那個else if里,執行new Observer(value),至于shouldConvert和后續的幾個判斷則是為了防止value不是單純的對象而是Regexp或者函數之類的,或者是vm實例再或者是不可擴展的,shouldConvert則是某些特殊情況下為false,它的解釋參見源碼里尤雨溪的注釋。
那好,現在就進入到拿當前的data對象去new Observer(value),現在你可能會疑惑,遞歸遍歷的過程不是應該是純命令式的、面向過程的嗎?怎么代碼跑著跑著跑出來一句new一個對象了,嗯先不用管,我們先理清代碼執行過程,先帶著這個疑問。同時,我們注意到代碼最后return了ob,結合代碼,我們可以理解為如果return的是undifned,那么說明傳進來的value不是對象,反之return除了一個ob,則說明這個value是對象或數組,他可以添加或刪除屬性,這一點我們先記著,這個東西后面有用。
我們先看看Observer構造函數:
/** * Observer class that are attached to each observed * object. Once attached, the observer converts target * object"s property keys into getter/setters that * collect dependencies and dispatches updates. * * @param {Array|Object} value * @constructor */ function Observer (value) { this.value = value this.dep = new Dep() def(value, "__ob__", this) //value的__ob__屬性指向這個Ob實例 if (isArray(value)) { var augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { // 如果是對象則使用walk遍歷每個屬性 this.walk(value) } }observe一個數組
上述代碼中,如果遇到數組data中的數組實例增加了一些“變異”的push、pop等方法,這些方法會在數組原本的push、pop方法執行后發出消息,表明發生了改動。聽起來這好像可以用繼承的方式實現: 繼承數組然后在這個子類的原型上附加上變異的方法。
但是你需要知道的是在es5及更低版本的js里,無法完美繼承數組,主要原因是Array.call(this)時,Array根本不是像一般的構造函數那樣對你傳進去this進行改造,而是直接返回一個新的數組。所以一般的繼承方式就沒法實現了。參見這篇文章,所以出現了新建一個iframe,然后直接拿那個iframe里的數組的原型進行修改,添加自定義方法,諸如此類的hack方法,在此按下不表。
但是如果當前瀏覽器里存在__proto__這個非標準屬性的話(大部分都有),那又可以有方法繼承,就是創建一個繼承自Array.prototype的Object: Object.create(Array.prototype),在這個繼承了數組原生方法的對象上添加方法或者覆蓋原有方法,然后創建一個數組,把這個數組的__proto__指向這個對象,這樣這個數組的響應式的length屬性又得以保留,又獲得了新的方法,而且無侵入,不會改變本來的數組原型。
Vue就是基于這個思想,先判斷__proto__能不能用(hasProto),如果能用,則把那個一個繼承自Array.prototype的并且添加了變異方法的Object (arrayMethods),設置為當前數組的__proto__,完成改造,如果__proto__不能用,那么就只能遍歷arrayMethods就一個個的把變異方法def到數組實例上面去,這種方法效率不高,所以優先使用改造__proto__的那個方法。
源碼里后面那句this.observeArray非常簡單,for遍歷傳進去的value,然后對每個元素執行observe,處理之前說的數組的元素為對象或者數組的情況。好了,對于數組的討論先打住,至于數組的變異方法怎么通知我他進行了更改之類的我們不說了,我們先說清楚對象的情況,對象說清楚了,再去看源碼就一目了然了。
observe 對象對于對象,上面的代碼執行this.walk(value),他“游走”對象的每個屬性,對屬性和屬性值執行defineReactive函數。
function Dep () { this.id = uid++ this.subs = [] } Dep.prototype.depend = function () { Dep.target.addDep(this) } Dep.prototype.notify = function () { // stablize the subscriber list first var subs = toArray(this.subs) for (var i = 0, l = subs.length; i < l; i++) { subs[i].update() } } function defineReactive (obj, key, val) { // 生成一個新的Dep實例,這個實例會被閉包到getter和setter中 var dep = new Dep() var property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get var setter = property && property.set // 對屬性的值繼續執行observe,如果屬性的值是一個對象,那么則又遞歸進去對他的屬性執行defineReactive // 保證遍歷到所有層次的屬性 var childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val // 只有在有Dep.target時才說明是Vue內部依賴收集過程觸發的getter // 那么這個時候就需要執行dep.depend(),將watcher(Dep.target的實際值)添加到dep的subs數組中 // 對于其他時候,比如dom事件回調函數中訪問這個變量導致觸發的getter并不需要執行依賴收集,直接返回value即可 if (Dep.target) { dep.depend() if (childOb) { //如果value是對象,那就讓生成的Observer實例當中的dep也收集依賴 childOb.dep.depend() } if (isArray(value)) { for (var e, i = 0, l = value.length; i < l; i++) { e = value[i] //如果數組元素也是對象,那么他們observe過程也生成了ob實例,那么就讓ob的dep也收集依賴 e && e.__ob__ && e.__ob__.dep.depend() } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val if (newVal === value) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } // observe這個新set的值 childOb = observe(newVal) // 通知訂閱我這個dep的watcher們:我更新了 dep.notify() } }) }
我們來說說這個Dep,Dep類的定義極其簡單,一個id,一個數組,他就是一個很基本的發布者-觀察者模式的實現,作為一個發布者,他的subs屬性用來存放了訂閱他的觀察者,也就是后面我們會說到的watcher。
defineProperty是用來將對象的屬性轉化為響應式的getter/setter的,defineProperty函數執行過程中新建了一個Dep,閉包在了屬性的getter和setter中,因此每個屬性都有一個唯一的Dep與其對應,我們暫且可以把屬性和他對應的Dep理解為一體的。
Dep其實是dependence依賴的縮寫,我之前一直沒能理解依賴、依賴收集是什么,其實對于我們的一個模板{{a+b}},我們會說他的依賴有a和b,其實就是依賴了data的a和b屬性,更精確的說是依賴了a屬性中閉包的dep實例和b屬性中閉包的那個dep實例。
詳細來說:我們的這個{{a+b}}在dom里最終會被"a+b"表達式的真實值所取代,所以存在一個求出這個“a+b”的表達式的過程,求值的過程就會自然的分別觸發a和b的getter,而在getter中,我們看到執行了dep.depend(),這個函數實際上回做dep.addSub(Dep.target),即在dep的訂閱者數組中存放了Dep.target,讓Dep.target訂閱dep。
那Dep.target是什么?他就是我們后面介紹的Watcher實例,為什么要放在Dep.target里呢?是因為getter函數并不能傳參,dep可以通過閉包的形式放進去,那watcher可就不行了,watcher內部存放了a+b這個表達式,也是由watcher計算a+b的值,在計算前他會把自己放在一個公開的地方(Dep.target),然后計算a+b,從而觸發表達式中所有遇到的依賴的getter,這些getter執行過程中會把Dep.target加到自己的訂閱列表中。等整個表達式計算成功,Dep.target又恢復為null.這樣就成功的讓watcher分發到了對應的依賴的訂閱者列表中,訂閱到了自己的所有依賴。
我們可以看到這是極其精妙的一筆!在一個表達式的求值過程中隱式的完成依賴訂閱。
上面完成的是訂閱的過程,而上面setter代碼里的dep.notify就負責完成數據變動時通知訂閱者的功能。而且數據變化時,后文會說明只有依賴他的那些dom會精確更新,不會出現一些介紹mvvm的文章里雖然實現了訂閱更新但是重新計算整個視圖的情況。
于是一整個對象訂閱、notify的過程就結束了。
Observer類?現在我們明白了Dep的作用和收集訂閱依賴的過程,但是對于watcher是什么肯定還是云里霧里的,先別急。我們先解決之前的疑問:為什么命令式的監聽過程中出現了個new Observer()?而且構造函數第一行就創建了一個dep(這個dep不是defineReactive里的那個閉包dep,注意區分),在defineReactive函數的getter中還執行了childOb.dep.depend(),去完成了這個dep的watcher添加?
我們考慮一下這樣的情況,比如我的data:{a:{b:true}},這個時候,如果頁面有dom上有個指令:class="a",而我想響應式的刪除data.a的b屬性,此時我就沒有辦法了,因為defineReactive中的getter/setter都不會執行(他們甚至還會在delete a.b時被清空),閉包里的那個dep就無法通知對應的watcher。
這就是getter和setter存在的缺陷:只能監聽到屬性的更改,不能監聽到屬性的刪除與添加。
Vue的解決辦法是提供了響應式的api: vm.$set/vm.$delete/ Vue.set/ Vue.delete /數組的$set/數組的$remove。
具體方法是為所有的對象和數組(只有這倆哥們才可能delete和新建屬性),也創建一個dep,也完成收集依賴的過程。我們回到源碼defineReactive再看一遍,在執行defineReactive(data,"a",{b:true})時,他首先創造了那個閉包在getter/setter中的dep,然后var childOb = observe(val),val是{b:true},那就會為這個對象new Observer(val),并放在val.__ob__上,而這個ob實例上存放了一個Dep實例。現在我們看到,有兩個Dep實例,一個是閉包里的dep,一個是為{b:true}創建的ob上的這個dep。而:class="a"生成的watcher的求值過程中會觸發到a的getter,那就會執行:
dep.depend() if (childOb) { //如果value是對象,那就讓生成的Observer實例當中的dep也收集依賴 childOb.dep.depend() }
這一步,:class="a"的watcher既會訂閱閉包dep,也會訂閱ob的dep。
當我們執行Vue.delete(this.a,"b"),內部會執行del函數,他會找到要刪除屬性的那個對象,也是{b:true},它的__ob__屬性存放了ob,現在先刪除屬性,然后執行ob.dep.notify,通知所有依賴這個對象的watcher重新計算,這個時候屬性已經刪除了,重新計算的值(為空)就會刷新到頁面上,完成dom響應式更新。參見此處源碼。
不僅對于屬性的刪除這樣,屬性的的添加也是類似的,都是為了彌補getter和setter存在的缺陷,都會找到這個dep執行notify。不過data的頂級屬性略有不同,涉及到digest,此處不表。
同時我們再回到之前遍歷數組的代碼,我們數組的響應化代碼甚至都里沒有getter/setter,他連那個閉包的dep都沒有,代碼只是變異了一下push/pop方法。他有的只是那個childOb上的dep,所以數組的響應式過程都是notify的這個dep,不管是數組的變異方法),還是數組的$set/$remove里我們都會看到是在這個dep上觸發notify,通知訂閱了整個數組的watcher進行更新。所以你知道這個dep的重要性了把。當然這也就有問題了,我一個watcher訂閱整個數組,當數組的元素有改動我就會收到消息,但我不知道變動的是哪個,難道我要用整個數組重新構造一下dom?所以這就是數組diff算法的使用場景了。
至于Observer,這個額外的實例上存放了一個dep,這個dep配合Observer的addVm、removeVm、vms等屬性來一起搞定data的頂級屬性的新增或者刪除,至于為什么不直接在數據上存放dep,而是搞個Observer,并把dep定義在上面,我覺得是Observer的那些方法和vms等屬性,并不是所有的dep都應該具有的,作為dep的實例屬性是不應該的,所以就抽象了個Observer這么個東東吧,順便把walk、convert之類的函數變成方法掛在Observer上了,抽象出個專門用來observe的類而已,這部分純屬個人臆測。
_compile介紹完響應式的部分,算是開了個頭了,后面的內容很多,但是層層遞進,最終完成響應式精確訂閱和批處理更新的整個過程,過程比較流程,內容耦合度也高,所以我們先來給后文的概覽,介紹一下大體過程。
我們最開始的代碼里提到了Vue處理完數據和event之后就到了$mount,而$mount就是在this._compile后觸發編譯完成的鉤子而已,所以核心就是Vue.prototype._compile。
_compile包含了Vue構建的三個階段,transclude,compile,link。而link階段其實是放在linkAndCapture里執行的,這里又包含了watcher的生成,指令的bind、update等操作。
我先簡單講講什么是指令,雖然Vue文檔里說的指令是v-if,v-for等這種HTML的attribute,其實在Vue內部,只要是被Vue處理的dom上的東西都是指令,比如dom內容里的{{a}},最終會轉換成一個v-text的指令和一個textNode,而一個子組件
回過頭來,_compile部分大致分為如下幾個部分
transclude
transclude的意思是內嵌,這個步驟會把你template里給出的模板轉換成一段dom,然后抽取出你el選項指定的dom里的內容(即子元素,因為模板里可能有slot),把這段模板dom嵌入到el里面去,當然,如果replace為true,那他就是直接替換el,而不是內嵌。我們大概明白transclude這個名字的意義了,但其實更關鍵的是把template轉換為dom的過程(如`{{a}}
`字符串轉為真正的段落元素),這里為后面的編譯準備好了dom。
compile
compile的的過程具體就是**遍歷模板解析出模板里的指令**。更精確的說是解析后生成了指令描述對象。 同時,compile函數是一個高階函數,他執行完成之后的返回值是另一個函數:link,所以compile函數的第一個階段是編譯,返回出去的這個函數完成另一個階段:link。
link
compile階段將指令解析成為指令描述對象(descriptor),閉包在了link函數里,link函數會把descriptor傳入Directive構造函數,創建出真正的指令實例。此外link函數是作為參數傳入linkAndCaptrue中的,后者負責執行link,同時取出這些新生成的指令,先按照指令的預置的優先級從高到低排好順序,然后遍歷指令執行指令的_bind方法,這個方法會為指令創建watcher,并計算表達式的值,完成前面描述的依賴收集。并最后執行對應指令的bind和update方法,使指令生效、界面更新。 此外link函數最終的返回值是unlink函數,負責在vm卸載時取消對應的dom到數據的綁定。
是時候回過頭來看看Vue官網這張經典的圖了,以前我剛學Vue時也是對于Watcher,Directive之類的概念云里霧里。但是現在大家看這圖是不是很清晰明了?
模板中每個指令/數據綁定都有一個對應的 watcher 對象,在計算過程中它把屬性記錄為依賴。之后當依賴的 setter 被調用時,會觸發 watcher 重新計算 ,也就會導致它的關聯指令更新 DOM。 --Vue官網
上代碼:
Vue.prototype._compile = function (el) { var options = this.$options // transclude and init element // transclude can potentially replace original // so we need to keep reference; this step also injects // the template and caches the original attributes // on the container node and replacer node. var original = el el = transclude(el, options) // 在el這個dom上掛一些參數,并觸發"beforeCompile"鉤子,為compile做準備 this._initElement(el) // handle v-pre on root node (#2026) // v-pre指令的話就什么都不用做了。 if (el.nodeType === 1 && getAttr(el, "v-pre") !== null) { return } // root is always compiled per-instance, because // container attrs and props can be different every time. var contextOptions = this._context && this._context.$options var rootLinker = compileRoot(el, options, contextOptions) // resolve slot distribution // 具體是將各個slot存儲到vm._slotContents的對應屬性里面去, // 然后后面的compile階段會把slot解析為指令然后進行處理 resolveSlots(this, options._content) // compile and link the rest var contentLinkFn var ctor = this.constructor // component compilation can be cached // as long as it"s not using inline-template // 這里是組件的情況才進入的,大家先忽略此段代碼 if (options._linkerCachable) { contentLinkFn = ctor.linker if (!contentLinkFn) { contentLinkFn = ctor.linker = compile(el, options) } } // link phase // make sure to link root with prop scope! var rootUnlinkFn = rootLinker(this, el, this._scope) // compile和link一并做了 var contentUnlinkFn = contentLinkFn ? contentLinkFn(this, el) : compile(el, options)(this, el) // register composite unlink function // to be called during instance destruction this._unlinkFn = function () { rootUnlinkFn() // passing destroying: true to avoid searching and // splicing the directives contentUnlinkFn(true) } // finally replace original if (options.replace) { replace(original, el) } this._isCompiled = true this._callHook("compiled") }
尤雨溪的注釋已經極盡詳細,上面的代碼很清晰(如果你用過angular,那你會感覺很熟悉,angular里也是有transclude,compile和link的,雖然實際差別很大)。我們在具體進入各部分代碼前先說說為什么dom的編譯要分成compile和link兩個phase。
在組件的多個實例、v-for數組等場合,我們會出現同一個段模板要綁定不同的數據然后分發到dom里面去的需求。這也是mvvm性能考量的主要場景:大數據量的重復渲染生成。而重復渲染的模板是一致的,不一致的是他們需要綁定的數據,因此compile階段找出指令的過程是不用重復計算的,只需要link函數(和里面閉包的指令),而模板生成的dom使用原生的cloneNode方法即可復制出一份新的dom。現在,復制出的新dom+ link+具體的數據即可完成渲染,所以分離compile、并緩存link使得Vue在渲染時避免大量重復的性能消耗。
transclude函數這里大家可以考慮一下,我給你一個空的documentFragment和一段html字符串,讓你把html生成dom放進fragment里,你應該怎么做?innerHTML?documentFragment可是沒有innerHtml的哦。那先建個div再innerHTML?那萬一我的html字符串的是tr元素呢?tr并不能直接放進div里哦,那直接用outerHTML?沒有parent Node的元素是不能設置outerHTML的哈(parent是fragment也不行),那我先用正則提取第一個標簽,先createElement這個標簽然后在寫他的innerHTML總可以了吧?并不行,我沒告訴你我給你的這段HTML最外層就一個元素啊,萬一是個片段實例呢(也就是包含多個頂級元素,如
1
2
),所以我才說給你一個fragment當容器,讓你把dom裝進去。
上面這個例子說明了實際轉換dom過程中,可能遇到的一個小坑,只是想說明字符串轉dom并不是看起來那么一行innerHTML的事。
/** * Process an element or a DocumentFragment based on a * instance option object. This allows us to transclude * a template node/fragment before the instance is created, * so the processed fragment can then be cloned and reused * in v-for. * * @param {Element} el * @param {Object} options * @return {Element|DocumentFragment} */ export function transclude (el, options) { // extract container attributes to pass them down // to compiler, because they need to be compiled in // parent scope. we are mutating the options object here // assuming the same object will be used for compile // right after this. if (options) { options._containerAttrs = extractAttrs(el) } // for template tags, what we want is its content as // a documentFragment (for fragment instances) if (isTemplate(el)) { el = parseTemplate(el) } if (options) { // 如果當前是component,并且沒有模板,只有一個殼 // 那么只需要處理內容的嵌入 if (options._asComponent && !options.template) { options.template = "" } if (options.template) { //基本都會進入到這里 options._content = extractContent(el) el = transcludeTemplate(el, options) } } if (isFragment(el)) { // anchors for fragment instance // passing in `persist: true` to avoid them being // discarded by IE during template cloning prepend(createAnchor("v-start", true), el) el.appendChild(createAnchor("v-end", true)) } return el }
我們看上面的代碼,先options._containerAttrs = extractAttrs(el),這樣就把el元素上的所有attributes抽取出來存放在了選項對象的_containerAttrs屬性上。因為我們前面說過,這些屬性是vm實際掛載的根元素上的,如果vm是一個組件之類的,那么他們應該是在父組件的作用于編譯/link的,所以需要預先提取出來,因為如果replace為true,el元素會被模板元素替換,但是他上面的屬性是會編譯link后merge到模板元素上面去。
然后進入到那個兩層的if里, extractContent(el),將el的內容(子元素和文本節點)抽取出來,因為如果模板里有slot,那么他們要分發到對應的slot里。
然后就到el = transcludeTemplate(el, options):
/** * Process the template option. * If the replace option is true this will swap the $el. * * @param {Element} el * @param {Object} options * @return {Element|DocumentFragment} */ function transcludeTemplate (el, options) { var template = options.template var frag = parseTemplate(template, true) if (frag) { // 對于非片段實例情況且replace為true的情況下,frag的第一個子節點就是最終el元素的替代者 var replacer = frag.firstChild var tag = replacer.tagName && replacer.tagName.toLowerCase() if (options.replace) { /* istanbul ignore if */ if (el === document.body) { process.env.NODE_ENV !== "production" && warn( "You are mounting an instance with a template to " + ". This will replace entirely. You " + "should probably use `replace: false` here." ) } // there are many cases where the instance must // become a fragment instance: basically anything that // can create more than 1 root nodes. if ( // multi-children template frag.childNodes.length > 1 || // non-element template replacer.nodeType !== 1 || // single nested component tag === "component" || resolveAsset(options, "components", tag) || hasBindAttr(replacer, "is") || // element directive resolveAsset(options, "elementDirectives", tag) || // for block replacer.hasAttribute("v-for") || // if block replacer.hasAttribute("v-if") ) { return frag } else { // 抽取replacer自帶的屬性,他們將在自身作用域下編譯 options._replacerAttrs = extractAttrs(replacer) // 把el的所有屬性都轉移到replace上面去,因為我們后面將不會再處理el直至他最后被replacer替換 mergeAttrs(el, replacer) return replacer } } else { el.appendChild(frag) return el } } else { process.env.NODE_ENV !== "production" && warn( "Invalid template option: " + template ) } }
首先執行解析parseTemplate(template, true),得到一段存放在documentFragment里的真實dom,然后就判斷是否需要replace。(若replace為false)之后判斷是否是片段實例,官網已經講述哪幾種情況對應片段實例,而代碼里那幾個判斷就是對應的處理。若不是,那就進入后續的情況,我已經注釋代碼作用,就不再贅述。我們來說說parseTemplate,因為vue支持template選項寫#app這樣的HTML選擇符,也支持直接存放模板字符串、document fragment、dom元素等等,所以針對各種情況作了區分,如果是一個已經好的dom那幾乎不用處理,否則大部分情況下都是執行stringToFragment:
function stringToFragment (templateString, raw) { // 緩存機制 // try a cache hit first var cacheKey = raw ? templateString : templateString.trim() var hit = templateCache.get(cacheKey) if (hit) { return hit } //這三個正則分別是/<([w:-]+)/ 和/?w+?;/和/