這篇文章算是對最近寫的一系列Vue.js源碼的文章(https://github.com/answershuto/learnVue)的總結吧,在閱讀源碼的過程當中也確實受益不淺,但願本身的這些產出也會對一樣想要學習Vue.js源碼的小夥伴有所幫助。以前這篇文章一樣在我司(大搜車)的技術博客中發表過,歡迎你們關注我司的技術博客,給個傳送門https://blog.souche.com/。javascript
由於對Vue.js很感興趣,並且平時工做的技術棧也是Vue.js,這幾個月花了些時間研究學習了一下Vue.js源碼,並作了總結與輸出。html
文章的原地址:https://github.com/answershuto/learnVue。vue
在學習過程當中,爲Vue加上了中文的註釋https://github.com/answershuto/learnVue/tree/master/vue-src,但願能夠對其餘想學習Vue源碼的小夥伴有所幫助。java
可能會有理解存在誤差的地方,歡迎提issue指出,共同窗習,共同進步。node
let vm = new Vue({ el: '#app', /*some options*/ });
不少同窗好奇,在new一個Vue對象的時候,內部究竟發生了什麼?git
究竟Vue.js是如何將data中的數據渲染到真實的宿主環境環境中的?github
又是如何經過「響應式」修改數據的?web
template是如何被編譯成真實環境中可用的HTML的?算法
Vue指令又是執行的?json
帶着這些疑問,咱們從Vue的構造類開始看起。
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) { warn('Vue is a constructor and should be called with the `new` keyword') } /*初始化*/ this._init(options) }
Vue的構造類只作了一件事情,就是調用_init函數進行
來看一下init的代碼
Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-init:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed /*一個防止vm實例自身被觀察的標誌位*/ vm._isVue = true // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm /*初始化生命週期*/ initLifecycle(vm) /*初始化事件*/ initEvents(vm) /*初始化render*/ initRender(vm) /*調用beforeCreate鉤子函數而且觸發beforeCreate鉤子事件*/ callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props /*初始化props、methods、data、computed與watch*/ initState(vm) initProvide(vm) // resolve provide after data/props /*調用created鉤子函數而且觸發created鉤子事件*/ callHook(vm, 'created') /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { /*格式化組件名*/ vm._name = formatComponentName(vm, false) mark(endTag) measure(`${vm._name} init`, startTag, endTag) } if (vm.$options.el) { /*掛載組件*/ vm.$mount(vm.$options.el) } }
_init主要作了這兩件事:
1.初始化(包括生命週期、事件、render函數、state等)。
2.$mount組件。
在生命鉤子beforeCreate與created之間會初始化state,在此過程當中,會依次初始化props、methods、data、computed與watch,這也就是Vue.js對options中的數據進行「響應式化」(即雙向綁定)的過程。對於Vue.js響應式原理不瞭解的同窗能夠先看一下筆者的另外一片文章《Vue.js響應式原理》。
/*初始化props、methods、data、computed與watch*/ export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options /*初始化props*/ if (opts.props) initProps(vm, opts.props) /*初始化方法*/ if (opts.methods) initMethods(vm, opts.methods) /*初始化data*/ if (opts.data) { initData(vm) } else { /*該組件沒有data的時候綁定一個空對象*/ observe(vm._data = {}, true /* asRootData */) } /*初始化computed*/ if (opts.computed) initComputed(vm, opts.computed) /*初始化watchers*/ if (opts.watch) initWatch(vm, opts.watch) }
以initData爲例,對option的data的數據進行雙向綁定Oberver,其餘option參數雙向綁定的核心原理是一致的。
function initData (vm: Component) { /*獲得data數據*/ let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} /*判斷是不是對象*/ if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) } // proxy data on instance /*遍歷data對象*/ const keys = Object.keys(data) const props = vm.$options.props let i = keys.length //遍歷data中的數據 while (i--) { /*保證data中的key不與props中的key重複,props優先,若是有衝突會產生warning*/ if (props && hasOwn(props, keys[i])) { process.env.NODE_ENV !== 'production' && warn( `The data property "${keys[i]}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(keys[i])) { /*判斷是不是保留字段*/ /*這裏是咱們前面講過的代理,將data上面的屬性代理到了vm實例上*/ proxy(vm, `_data`, keys[i]) } } /*Github:https://github.com/answershuto*/ // observe data /*從這裏開始咱們要observe了,開始對數據進行綁定,這裏有尤大大的註釋asRootData,這步做爲根數據,下面會進行遞歸observe進行對深層對象的綁定。*/ observe(data, true /* asRootData */) }
observe會經過defineReactive對data中的對象進行雙向綁定,最終經過Object.defineProperty對對象設置setter以及getter的方法。getter的方法主要用來進行依賴收集,對於依賴收集不瞭解的同窗能夠參考筆者的另外一篇文章《依賴收集》。setter方法會在對象被修改的時候觸發(不存在添加屬性的狀況,添加屬性請用Vue.set),這時候setter會通知閉包中的Dep,Dep中有一些訂閱了這個對象改變的Watcher觀察者對象,Dep會通知Watcher對象更新視圖。
若是是修改一個數組的成員,該成員是一個對象,那隻須要遞歸對數組的成員進行雙向綁定便可。但這時候出現了一個問題,?若是咱們進行pop、push等操做的時候,push進去的對象根本沒有進行過雙向綁定,更別說pop了,那麼咱們如何監聽數組的這些變化呢? Vue.js提供的方法是重寫push、pop、shift、unshift、splice、sort、reverse這七個數組方法。修改數組原型方法的代碼能夠參考observer/array.js以及observer/index.js。
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { //....... if (Array.isArray(value)) { /* 若是是數組,將修改後能夠截獲響應的數組方法替換掉該數組的原型中的原生方法,達到監聽數組數據變化響應的效果。 這裏若是當前瀏覽器支持__proto__屬性,則直接覆蓋當前數組對象原型上的原生數組方法,若是不支持該屬性,則直接覆蓋數組對象的原型。 */ const augment = hasProto ? protoAugment /*直接覆蓋原型的方法來修改目標對象*/ : copyAugment /*定義(覆蓋)目標對象或數組的某一個方法*/ augment(value, arrayMethods, arrayKeys) /*若是是數組則須要遍歷數組的每個成員進行observe*/ this.observeArray(value) } else { /*若是是對象則直接walk進行綁定*/ this.walk(value) } } } /** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */ /*直接覆蓋原型的方法來修改目標對象或數組*/ function protoAugment (target, src: Object) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ } /** * Augment an target Object or Array by defining * hidden properties. */ /* istanbul ignore next */ /*定義(覆蓋)目標對象或數組的某一個方法*/ function copyAugment (target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }
/* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */ import { def } from '../util/index' /*取得原生數組的原型*/ const arrayProto = Array.prototype /*建立一個新的數組對象,修改該對象上的數組的七個方法,防止污染原生數組方法*/ export const arrayMethods = Object.create(arrayProto) /** * Intercept mutating methods and emit events */ /*這裏重寫了數組的這些方法,在保證不污染原生數組原型的狀況下重寫數組的這些方法,截獲數組的成員發生的變化,執行原生數組操做的同時dep通知關聯的全部觀察者進行響應式處理*/ ;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // cache original method /*將數組的原生方法緩存起來,後面要調用*/ const original = arrayProto[method] def(arrayMethods, method, function mutator () { // avoid leaking arguments: // http://jsperf.com/closure-with-arguments let i = arguments.length const args = new Array(i) while (i--) { args[i] = arguments[i] } /*調用原生的數組方法*/ const result = original.apply(this, args) /*數組新插入的元素須要從新進行observe才能響應式*/ const ob = this.__ob__ let inserted switch (method) { case 'push': inserted = args break case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change /*dep通知全部註冊的觀察者進行響應式處理*/ ob.dep.notify() return result }) })
從數組的原型新建一個Object.create(arrayProto)對象,經過修改此原型能夠保證原生數組方法不被污染。若是當前瀏覽器支持__proto__這個屬性的話就能夠直接覆蓋該屬性則使數組對象具備了重寫後的數組方法。若是沒有該屬性的瀏覽器,則必須經過遍歷def全部須要重寫的數組方法,這種方法效率較低,因此優先使用第一種。
在保證不污染不覆蓋數組原生方法添加監聽,主要作了兩個操做,第一是通知全部註冊的觀察者進行響應式處理,第二是若是是添加成員的操做,須要對新成員進行observe。
可是修改了數組的原生方法之後咱們仍是無法像原生數組同樣直接經過數組的下標或者設置length來修改數組,Vue.js提供了$set()及$remove()方法。
對於更具體的講解數據雙向綁定以及Dep、Watcher的實現能夠參考筆者的文章《從源碼角度再看數據綁定》。
在$mount過程當中,若是是獨立構建構建,則會在此過程當中將template編譯成render function。固然,你也能夠採用運行時構建。具體參考運行時-編譯器-vs-只包含運行時。
template是如何被編譯成render function的呢?
function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { /*parse解析獲得ast樹*/ const ast = parse(template.trim(), options) /* 將AST樹進行優化 優化的目標:生成模板AST樹,檢測不須要進行DOM改變的靜態子樹。 一旦檢測到這些靜態樹,咱們就能作如下這些事情: 1.把它們變成常數,這樣咱們就不再須要每次從新渲染時建立新的節點了。 2.在patch的過程當中直接跳過。 */ optimize(ast, options) /*根據ast樹生成所需的code(內部包含render與staticRenderFns)*/ const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } }
baseCompile首先會將模板template進行parse獲得一個AST語法樹,再經過optimize作一些優化,最後經過generate獲得render以及staticRenderFns。
parse的源碼能夠參見https://github.com/answershuto/learnVue/blob/master/vue-src/compiler/parser/index.js#L53。
parse會用正則等方式解析template模板中的指令、class、style等數據,造成AST語法樹。
optimize的主要做用是標記static靜態節點,這是Vue在編譯過程當中的一處優化,後面當update更新界面時,會有一個patch的過程,diff算法會直接跳過靜態節點,從而減小了比較的過程,優化了patch的性能。
generate是將AST語法樹轉化成render funtion字符串的過程,獲得結果是render的字符串以及staticRenderFns字符串。
具體的template編譯實現請參考《聊聊Vue.js的template編譯》。
Watcher對象會經過調用updateComponent方法來達到更新視圖的目的。這裏提一下,其實Watcher並非實時更新視圖的,Vue.js默認會將Watcher對象存在一個隊列中,在下一個tick時更新異步更新視圖,完成了性能優化。關於nextTick感興趣的小夥伴能夠參考《Vue.js異步更新DOM策略及nextTick》。
updateComponent = () => { vm._update(vm._render(), hydrating) }
updateComponent就執行一句話,_render函數會返回一個新的Vnode節點,傳入_update中與舊的VNode對象進行對比,通過一個patch的過程獲得兩個VNode節點的差別,最後將這些差別渲染到真實環境造成視圖。
什麼是VNode?
在刀耕火種的年代,咱們須要在各個事件方法中直接操做DOM來達到修改視圖的目的。可是當應用一大就會變得難以維護。
那咱們是否是能夠把真實DOM樹抽象成一棵以JavaScript對象構成的抽象樹,在修改抽象樹數據後將抽象樹轉化成真實DOM重繪到頁面上呢?因而虛擬DOM出現了,它是真實DOM的一層抽象,用屬性描述真實DOM的各個特性。當它發生變化的時候,就會去修改視圖。
可是這樣的JavaScript操做DOM進行重繪整個視圖層是至關消耗性能的,咱們是否是能夠每次只更新它的修改呢?因此Vue.js將DOM抽象成一個以JavaScript對象爲節點的虛擬DOM樹,以VNode節點模擬真實DOM,能夠對這顆抽象樹進行建立節點、刪除節點以及修改節點等操做,在這過程當中都不須要操做真實DOM,只須要操做JavaScript對象,大大提高了性能。修改之後通過diff算法得出一些須要修改的最小單位,再將這些小單位的視圖進行更新。這樣作減小了不少不須要的DOM操做,大大提升了性能。
Vue就使用了這樣的抽象節點VNode,它是對真實DOM的一層抽象,而不依賴某個平臺,它能夠是瀏覽器平臺,也能夠是weex,甚至是node平臺也能夠對這樣一棵抽象DOM樹進行建立刪除修改等操做,這也爲先後端同構提供了可能。
先來看一下Vue.js源碼中對VNode類的定義。
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope functionalContext: Component | void; // only for functional component root nodes key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions ) { /*當前節點的標籤名*/ this.tag = tag /*當前節點對應的對象,包含了具體的一些數據信息,是一個VNodeData類型,能夠參考VNodeData類型中的數據信息*/ this.data = data /*當前節點的子節點,是一個數組*/ this.children = children /*當前節點的文本*/ this.text = text /*當前虛擬節點對應的真實dom節點*/ this.elm = elm /*當前節點的名字空間*/ this.ns = undefined /*編譯做用域*/ this.context = context /*函數化組件做用域*/ this.functionalContext = undefined /*節點的key屬性,被看成節點的標誌,用以優化*/ this.key = data && data.key /*組件的option選項*/ this.componentOptions = componentOptions /*當前節點對應的組件的實例*/ this.componentInstance = undefined /*當前節點的父節點*/ this.parent = undefined /*簡而言之就是是否爲原生HTML或只是普通文本,innerHTML的時候爲true,textContent的時候爲false*/ this.raw = false /*靜態節點標誌*/ this.isStatic = false /*是否做爲跟節點插入*/ this.isRootInsert = true /*是否爲註釋節點*/ this.isComment = false /*是否爲克隆節點*/ this.isCloned = false /*是否有v-once指令*/ this.isOnce = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } }
這是一個最基礎的VNode節點,做爲其餘派生VNode類的基類,裏面定義了下面這些數據。
tag: 當前節點的標籤名
data: 當前節點對應的對象,包含了具體的一些數據信息,是一個VNodeData類型,能夠參考VNodeData類型中的數據信息
children: 當前節點的子節點,是一個數組
text: 當前節點的文本
elm: 當前虛擬節點對應的真實dom節點
ns: 當前節點的名字空間
context: 當前節點的編譯做用域
functionalContext: 函數化組件做用域
key: 節點的key屬性,被看成節點的標誌,用以優化
componentOptions: 組件的option選項
componentInstance: 當前節點對應的組件的實例
parent: 當前節點的父節點
raw: 簡而言之就是是否爲原生HTML或只是普通文本,innerHTML的時候爲true,textContent的時候爲false
isStatic: 是否爲靜態節點
isRootInsert: 是否做爲跟節點插入
isComment: 是否爲註釋節點
isCloned: 是否爲克隆節點
isOnce: 是否有v-once指令
打個比方,好比說我如今有這麼一個VNode樹
{ tag: 'div' data: { class: 'test' }, children: [ { tag: 'span', data: { class: 'demo' } text: 'hello,VNode' } ] }
渲染以後的結果就是這樣的
<div class="test"> <span class="demo">hello,VNode</span> </div>
更多操做VNode的方法,請參考《VNode節點》。
最後_update會將新舊兩個VNode進行一次patch的過程,得出兩個VNode最小的差別,而後將這些差別渲染到視圖上。
首先說一下patch的核心diff算法,diff算法是經過同層的樹節點進行比較而非對樹進行逐層搜索遍歷的方式,因此時間複雜度只有O(n),是一種至關高效的算法。
這兩張圖表明舊的VNode與新VNode進行patch的過程,他們只是在同層級的VNode之間進行比較獲得變化(第二張圖中相同顏色的方塊表明互相進行比較的VNode節點),而後修改變化的視圖,因此十分高效。
在patch的過程當中,若是兩個VNode被認爲是同一個VNode(sameVnode),則會進行深度的比較,得出最小差別,不然直接刪除舊有DOM節點,建立新的DOM節點。
什麼是sameVnode?
咱們來看一下sameVnode的實現。
/* 判斷兩個VNode節點是不是同一個節點,須要知足如下條件 key相同 tag(當前節點的標籤名)相同 isComment(是否爲註釋節點)相同 是否data(當前節點對應的對象,包含了具體的一些數據信息,是一個VNodeData類型,能夠參考VNodeData類型中的數據信息)都有定義 當標籤是<input>的時候,type必須相同 */ function sameVnode (a, b) { return ( a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) } // Some browsers do not support dynamically changing type for <input> // so they need to be treated as different nodes /* 判斷當標籤是<input>的時候,type是否相同 某些瀏覽器不支持動態修改<input>類型,因此他們被視爲不一樣類型 */ function sameInputType (a, b) { if (a.tag !== 'input') return true let i const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type return typeA === typeB }
當兩個VNode的tag、key、isComment都相同,而且同時定義或未定義data的時候,且若是標籤爲input則type必須相同。這時候這兩個VNode則算sameVnode,能夠直接進行patchVnode操做。
patchVnode的規則是這樣的:
1.若是新舊VNode都是靜態的,同時它們的key相同(表明同一節點),而且新的VNode是clone或者是標記了once(標記v-once屬性,只渲染一次),那麼只須要替換elm以及componentInstance便可。
2.新老節點均有children子節點,則對子節點進行diff操做,調用updateChildren,這個updateChildren也是diff的核心。
3.若是老節點沒有子節點而新節點存在子節點,先清空老節點DOM的文本內容,而後爲當前DOM節點加入子節點。
4.當新節點沒有子節點而老節點有子節點的時候,則移除該DOM節點的全部子節點。
5.當新老節點都無子節點的時候,只是文本的替換。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, elmToMove, refElm // removeOnly is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { /*前四種狀況實際上是指定key的時候,斷定爲同一個VNode,則直接patchVnode便可,分別比較oldCh以及newCh的兩頭節點2*2=4種狀況*/ patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { /* 生成一個key與舊VNode的key對應的哈希表(只有第一次進來undefined的時候會生成,也爲後面檢測重複的key值作鋪墊) 好比childre是這樣的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2 結果生成{key0: 0, key1: 1, key2: 2} */ if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) /*若是newStartVnode新的VNode節點存在key而且這個key在oldVnode中能找到則返回這個節點的idxInOld(即第幾個節點,下標)*/ idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null if (isUndef(idxInOld)) { // New element /*newStartVnode沒有key或者是該key沒有在老節點中找到則建立一個新的節點*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { /*獲取同key的老節點*/ elmToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !elmToMove) { /*若是elmToMove不存在說明以前已經有新節點放入過這個key的DOM中,提示可能存在重複的key,確保v-for的時候item有惟一的key值*/ warn( 'It seems there are duplicate keys that is causing an update error. ' + 'Make sure each v-for item has a unique key.' ) } if (sameVnode(elmToMove, newStartVnode)) { /*Github:https://github.com/answershuto*/ /*若是新VNode與獲得的有相同key的節點是同一個VNode則進行patchVnode*/ patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) /*由於已經patchVnode進去了,因此將這個老節點賦值undefined,以後若是還有新節點與該節點key相同能夠檢測出來提示已有重複的key*/ oldCh[idxInOld] = undefined /*當有標識位canMove實能夠直接插入oldStartVnode對應的真實DOM節點前面*/ canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // same key but different element. treat as new element /*當新的VNode與找到的一樣key的VNode不是sameVNode的時候(好比說tag不同或者是有不同type的input標籤),建立一個新的節點*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } } if (oldStartIdx > oldEndIdx) { /*所有比較完成之後,發現oldStartIdx > oldEndIdx的話,說明老節點已經遍歷完了,新節點比老節點多,因此這時候多出來的新節點須要一個一個建立出來加入到真實DOM中*/ refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { /*若是所有比較完成之後發現newStartIdx > newEndIdx,則說明新節點已經遍歷完了,老節點多餘新節點,這個時候須要將多餘的老節點從真實DOM中移除*/ removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
直接看源碼可能比較難以捋清其中的關係,咱們經過圖來看一下。
首先,在新老兩個VNode節點的左右頭尾兩側都有一個變量標記,在遍歷過程當中這幾個變量都會向中間靠攏。當oldStartIdx <= oldEndIdx或者newStartIdx <= newEndIdx時結束循環。
索引與VNode節點的對應關係: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode
在遍歷中,若是存在key,而且知足sameVnode,會將該DOM節點進行復用,不然則會建立一個新的DOM節點。
首先,oldStartVnode、oldEndVnode與newStartVnode、newEndVnode兩兩比較一共有2*2=4種比較方法。
當新老VNode節點的start或者end知足sameVnode時,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接將該VNode節點進行patchVnode便可。
若是oldStartVnode與newEndVnode知足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。
這時候說明oldStartVnode已經跑到了oldEndVnode後面去了,進行patchVnode的同時還須要將真實DOM節點移動到oldEndVnode的後面。
若是oldEndVnode與newStartVnode知足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。
這說明oldEndVnode跑到了oldStartVnode的前面,進行patchVnode的同時真實的DOM節點移動到了oldStartVnode的前面。
若是以上狀況均不符合,則經過createKeyToOldIdx會獲得一個oldKeyToIdx,裏面存放了一個key爲舊的VNode,value爲對應index序列的哈希表。從這個哈希表中能夠找到是否有與newStartVnode一致key的舊的VNode節點,若是同時知足sameVnode,patchVnode的同時會將這個真實DOM(elmToMove)移動到oldStartVnode對應的真實DOM的前面。
固然也有可能newStartVnode在舊的VNode節點找不到一致的key,或者是即使key相同卻不是sameVnode,這個時候會調用createElm建立一個新的DOM節點。
到這裏循環已經結束了,那麼剩下咱們還須要處理多餘或者不夠的真實DOM節點。
1.當結束時oldStartIdx > oldEndIdx,這個時候老的VNode節點已經遍歷完了,可是新的節點尚未。說明了新的VNode節點實際上比老的VNode節點多,也就是比真實DOM多,須要將剩下的(也就是新增的)VNode節點插入到真實DOM節點中去,此時調用addVnodes(批量調用createElm的接口將這些節點加入到真實DOM中去)。
2。同理,當newStartIdx > newEndIdx時,新的VNode節點已經遍歷完了,可是老的節點還有剩餘,說明真實DOM節點多餘了,須要從文檔中刪除,這時候調用removeVnodes將這些多餘的真實DOM刪除。
更詳細的diff實現參考筆者的文章VirtualDOM與diff(Vue.js實現)。
因爲Vue使用了虛擬DOM,因此虛擬DOM能夠在任何支持JavaScript語言的平臺上操做,譬如說目前Vue支持的瀏覽器平臺或是weex,在虛擬DOM的實現上是一致的。那麼最後虛擬DOM如何映射到真實的DOM節點上呢?
Vue爲平臺作了一層適配層,瀏覽器平臺見/platforms/web/runtime/node-ops.js以及weex平臺見/platforms/weex/runtime/node-ops.js。不一樣平臺之間經過適配層對外提供相同的接口,虛擬DOM進行操做真實DOM節點的時候,只須要調用這些適配層的接口便可,而內部實現則不須要關心,它會根據平臺的改變而改變。
如今又出現了一個問題,咱們只是將虛擬DOM映射成了真實的DOM。那如何給這些DOM加入attr、class、style等DOM屬性呢?
這要依賴於虛擬DOM的生命鉤子。虛擬DOM提供了以下的鉤子函數,分別在不一樣的時期會進行調用。
const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] /*構建cbs回調函數,web平臺上見/platforms/web/runtime/modules*/ for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } }
同理,也會根據不一樣平臺有本身不一樣的實現,咱們這裏以Web平臺爲例。Web平臺的鉤子函數見/platforms/web/runtime/modules。裏面有對attr、class、props、events、style以及transition(過渡狀態)的DOM屬性進行操做。
以attr爲例,代碼很簡單。
/* @flow */ import { isIE9 } from 'core/util/env' import { extend, isDef, isUndef } from 'shared/util' import { isXlink, xlinkNS, getXlinkProp, isBooleanAttr, isEnumeratedAttr, isFalsyAttrValue } from 'web/util/index' /*更新attr*/ function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) { /*若是舊的以及新的VNode節點均沒有attr屬性,則直接返回*/ if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { return } let key, cur, old /*VNode節點對應的Dom實例*/ const elm = vnode.elm /*舊VNode節點的attr*/ const oldAttrs = oldVnode.data.attrs || {} /*新VNode節點的attr*/ let attrs: any = vnode.data.attrs || {} // clone observed objects, as the user probably wants to mutate it /*若是新的VNode的attr已經有__ob__(表明已經被Observe處理過了), 進行深拷貝*/ if (isDef(attrs.__ob__)) { attrs = vnode.data.attrs = extend({}, attrs) } /*遍歷attr,不一致則替換*/ for (key in attrs) { cur = attrs[key] old = oldAttrs[key] if (old !== cur) { setAttr(elm, key, cur) } } // #4391: in IE9, setting type can reset value for input[type=radio] /* istanbul ignore if */ if (isIE9 && attrs.value !== oldAttrs.value) { setAttr(elm, 'value', attrs.value) } for (key in oldAttrs) { if (isUndef(attrs[key])) { if (isXlink(key)) { elm.removeAttributeNS(xlinkNS, getXlinkProp(key)) } else if (!isEnumeratedAttr(key)) { elm.removeAttribute(key) } } } } /*設置attr*/ function setAttr (el: Element, key: string, value: any) { if (isBooleanAttr(key)) { // set attribute for blank value // e.g. <option disabled>Select one</option> if (isFalsyAttrValue(value)) { el.removeAttribute(key) } else { el.setAttribute(key, key) } } else if (isEnumeratedAttr(key)) { el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true') } else if (isXlink(key)) { if (isFalsyAttrValue(value)) { el.removeAttributeNS(xlinkNS, getXlinkProp(key)) } else { el.setAttributeNS(xlinkNS, key, value) } } else { if (isFalsyAttrValue(value)) { el.removeAttribute(key) } else { el.setAttribute(key, value) } } } export default { create: updateAttrs, update: updateAttrs }
attr只須要在create以及update鉤子被調用時更新DOM的attr屬性便可。
至此,咱們已經從template到真實DOM的整個過程梳理完了。如今再去看這張圖,是否是更清晰了呢?
做者:染陌
Email:answershuto@gmail.com or answershuto@126.com
Github: https://github.com/answershuto
Blog:http://answershuto.github.io/
知乎主頁:https://www.zhihu.com/people/cao-yang-49/activities
知乎專欄:https://zhuanlan.zhihu.com/ranmo
掘金: https://juejin.im/user/58f87ae844d9040069ca7507
osChina:https://my.oschina.net/u/3161824/blog
轉載請註明出處,謝謝。
歡迎關注個人公衆號