摘要:根據(jù)樹生成所需的內(nèi)部包含與首先會將模板進(jìn)行得到一個語法樹,再通過做一些優(yōu)化,最后通過得到以及。會用正則等方式解析模板中的指令等數(shù)據(jù),形成語法樹。是將語法樹轉(zhuǎn)化成字符串的過程,得到結(jié)果是的字符串以及字符串。
寫在前面
這篇文章算是對最近寫的一系列Vue.js源碼的文章(https://github.com/answershuto/learnVue)的總結(jié)吧,在閱讀源碼的過程中也確實受益匪淺,希望自己的這些產(chǎn)出也會對同樣想要學(xué)習(xí)Vue.js源碼的小伙伴有所幫助。之前這篇文章同樣在我司(大搜車)的技術(shù)博客中發(fā)表過,歡迎大家關(guān)注我司的技術(shù)博客,給個傳送門https://blog.souche.com/。
因為對Vue.js很感興趣,而且平時工作的技術(shù)棧也是Vue.js,這幾個月花了些時間研究學(xué)習(xí)了一下Vue.js源碼,并做了總結(jié)與輸出。
文章的原地址:https://github.com/answershuto/learnVue。
在學(xué)習(xí)過程中,為Vue加上了中文的注釋https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以對其他想學(xué)習(xí)Vue源碼的小伙伴有所幫助。
可能會有理解存在偏差的地方,歡迎提issue指出,共同學(xué)習(xí),共同進(jìn)步。
從new一個Vue對象開始let vm = new Vue({ el: "#app", /*some options*/ });
很多同學(xué)好奇,在new一個Vue對象的時候,內(nèi)部究竟發(fā)生了什么?
究竟Vue.js是如何將data中的數(shù)據(jù)渲染到真實的宿主環(huán)境環(huán)境中的?
又是如何通過“響應(yīng)式”修改數(shù)據(jù)的?
template是如何被編譯成真實環(huán)境中可用的HTML的?
Vue指令又是執(zhí)行的?
帶著這些疑問,我們從Vue的構(gòu)造類開始看起。
Vue構(gòu)造類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的構(gòu)造類只做了一件事情,就是調(diào)用_init函數(shù)進(jìn)行
來看一下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實例自身被觀察的標(biāo)志位*/ 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) /*調(diào)用beforeCreate鉤子函數(shù)并且觸發(fā)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 /*調(diào)用created鉤子函數(shù)并且觸發(fā)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函數(shù)、state等)。
2.$mount組件。
在生命鉤子beforeCreate與created之間會初始化state,在此過程中,會依次初始化props、methods、data、computed與watch,這也就是Vue.js對options中的數(shù)據(jù)進(jìn)行“響應(yīng)式化”(即雙向綁定)的過程。對于Vue.js響應(yīng)式原理不了解的同學(xué)可以先看一下筆者的另一片文章《Vue.js響應(yīng)式原理》。
/*初始化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的數(shù)據(jù)進(jìn)行雙向綁定Oberver,其他option參數(shù)雙向綁定的核心原理是一致的。
function initData (vm: Component) { /*得到data數(shù)據(jù)*/ 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: " + "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中的數(shù)據(jù) while (i--) { /*保證data中的key不與props中的key重復(fù),props優(yōu)先,如果有沖突會產(chǎn)生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了,開始對數(shù)據(jù)進(jìn)行綁定,這里有尤大大的注釋asRootData,這步作為根數(shù)據(jù),下面會進(jìn)行遞歸observe進(jìn)行對深層對象的綁定。*/ observe(data, true /* asRootData */) }
observe會通過defineReactive對data中的對象進(jìn)行雙向綁定,最終通過Object.defineProperty對對象設(shè)置setter以及getter的方法。getter的方法主要用來進(jìn)行依賴收集,對于依賴收集不了解的同學(xué)可以參考筆者的另一篇文章《依賴收集》。setter方法會在對象被修改的時候觸發(fā)(不存在添加屬性的情況,添加屬性請用Vue.set),這時候setter會通知閉包中的Dep,Dep中有一些訂閱了這個對象改變的Watcher觀察者對象,Dep會通知Watcher對象更新視圖。
如果是修改一個數(shù)組的成員,該成員是一個對象,那只需要遞歸對數(shù)組的成員進(jìn)行雙向綁定即可。但這時候出現(xiàn)了一個問題,?如果我們進(jìn)行pop、push等操作的時候,push進(jìn)去的對象根本沒有進(jìn)行過雙向綁定,更別說pop了,那么我們?nèi)绾伪O(jiān)聽數(shù)組的這些變化呢?
Vue.js提供的方法是重寫push、pop、shift、unshift、splice、sort、reverse這七個數(shù)組方法。修改數(shù)組原型方法的代碼可以參考o(jì)bserver/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)) { /* 如果是數(shù)組,將修改后可以截獲響應(yīng)的數(shù)組方法替換掉該數(shù)組的原型中的原生方法,達(dá)到監(jiān)聽數(shù)組數(shù)據(jù)變化響應(yīng)的效果。 這里如果當(dāng)前瀏覽器支持__proto__屬性,則直接覆蓋當(dāng)前數(shù)組對象原型上的原生數(shù)組方法,如果不支持該屬性,則直接覆蓋數(shù)組對象的原型。 */ const augment = hasProto ? protoAugment /*直接覆蓋原型的方法來修改目標(biāo)對象*/ : copyAugment /*定義(覆蓋)目標(biāo)對象或數(shù)組的某一個方法*/ augment(value, arrayMethods, arrayKeys) /*如果是數(shù)組則需要遍歷數(shù)組的每一個成員進(jìn)行observe*/ this.observeArray(value) } else { /*如果是對象則直接walk進(jìn)行綁定*/ this.walk(value) } } } /** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */ /*直接覆蓋原型的方法來修改目標(biāo)對象或數(shù)組*/ 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 */ /*定義(覆蓋)目標(biāo)對象或數(shù)組的某一個方法*/ function copyAugment (target: Object, src: Object, keys: Array) { 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" /*取得原生數(shù)組的原型*/ const arrayProto = Array.prototype /*創(chuàng)建一個新的數(shù)組對象,修改該對象上的數(shù)組的七個方法,防止污染原生數(shù)組方法*/ export const arrayMethods = Object.create(arrayProto) /** * Intercept mutating methods and emit events */ /*這里重寫了數(shù)組的這些方法,在保證不污染原生數(shù)組原型的情況下重寫數(shù)組的這些方法,截獲數(shù)組的成員發(fā)生的變化,執(zhí)行原生數(shù)組操作的同時dep通知關(guān)聯(lián)的所有觀察者進(jìn)行響應(yīng)式處理*/ ;[ "push", "pop", "shift", "unshift", "splice", "sort", "reverse" ] .forEach(function (method) { // cache original method /*將數(shù)組的原生方法緩存起來,后面要調(diào)用*/ 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] } /*調(diào)用原生的數(shù)組方法*/ const result = original.apply(this, args) /*數(shù)組新插入的元素需要重新進(jìn)行observe才能響應(yīng)式*/ 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通知所有注冊的觀察者進(jìn)行響應(yīng)式處理*/ ob.dep.notify() return result }) })
從數(shù)組的原型新建一個Object.create(arrayProto)對象,通過修改此原型可以保證原生數(shù)組方法不被污染。如果當(dāng)前瀏覽器支持__proto__這個屬性的話就可以直接覆蓋該屬性則使數(shù)組對象具有了重寫后的數(shù)組方法。如果沒有該屬性的瀏覽器,則必須通過遍歷def所有需要重寫的數(shù)組方法,這種方法效率較低,所以優(yōu)先使用第一種。
在保證不污染不覆蓋數(shù)組原生方法添加監(jiān)聽,主要做了兩個操作,第一是通知所有注冊的觀察者進(jìn)行響應(yīng)式處理,第二是如果是添加成員的操作,需要對新成員進(jìn)行observe。
但是修改了數(shù)組的原生方法以后我們還是沒法像原生數(shù)組一樣直接通過數(shù)組的下標(biāo)或者設(shè)置length來修改數(shù)組,Vue.js提供了$set()及$remove()方法。
對于更具體的講解數(shù)據(jù)雙向綁定以及Dep、Watcher的實現(xiàn)可以參考筆者的文章《從源碼角度再看數(shù)據(jù)綁定》。
template編譯在$mount過程中,如果是獨立構(gòu)建構(gòu)建,則會在此過程中將template編譯成render function。當(dāng)然,你也可以采用運行時構(gòu)建。具體參考運行時-編譯器-vs-只包含運行時。
template是如何被編譯成render function的呢?
function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { /*parse解析得到ast樹*/ const ast = parse(template.trim(), options) /* 將AST樹進(jìn)行優(yōu)化 優(yōu)化的目標(biāo):生成模板AST樹,檢測不需要進(jìn)行DOM改變的靜態(tài)子樹。 一旦檢測到這些靜態(tài)樹,我們就能做以下這些事情: 1.把它們變成常數(shù),這樣我們就再也不需要每次重新渲染時創(chuàng)建新的節(jié)點了。 2.在patch的過程中直接跳過。 */ optimize(ast, options) /*根據(jù)ast樹生成所需的code(內(nèi)部包含render與staticRenderFns)*/ const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } }
baseCompile首先會將模板template進(jìn)行parse得到一個AST語法樹,再通過optimize做一些優(yōu)化,最后通過generate得到render以及staticRenderFns。
parseparse的源碼可以參見https://github.com/answershuto/learnVue/blob/master/vue-src/compiler/parser/index.js#L53。
parse會用正則等方式解析template模板中的指令、class、style等數(shù)據(jù),形成AST語法樹。
optimizeoptimize的主要作用是標(biāo)記static靜態(tài)節(jié)點,這是Vue在編譯過程中的一處優(yōu)化,后面當(dāng)update更新界面時,會有一個patch的過程,diff算法會直接跳過靜態(tài)節(jié)點,從而減少了比較的過程,優(yōu)化了patch的性能。
generategenerate是將AST語法樹轉(zhuǎn)化成render funtion字符串的過程,得到結(jié)果是render的字符串以及staticRenderFns字符串。
具體的template編譯實現(xiàn)請參考《聊聊Vue.js的template編譯》。
Watcher到視圖Watcher對象會通過調(diào)用updateComponent方法來達(dá)到更新視圖的目的。這里提一下,其實Watcher并不是實時更新視圖的,Vue.js默認(rèn)會將Watcher對象存在一個隊列中,在下一個tick時更新異步更新視圖,完成了性能優(yōu)化。關(guān)于nextTick感興趣的小伙伴可以參考《Vue.js異步更新DOM策略及nextTick》。
updateComponent = () => { vm._update(vm._render(), hydrating) }
updateComponent就執(zhí)行一句話,_render函數(shù)會返回一個新的Vnode節(jié)點,傳入_update中與舊的VNode對象進(jìn)行對比,經(jīng)過一個patch的過程得到兩個VNode節(jié)點的差異,最后將這些差異渲染到真實環(huán)境形成視圖。
什么是VNode?
VNode在刀耕火種的年代,我們需要在各個事件方法中直接操作DOM來達(dá)到修改視圖的目的。但是當(dāng)應(yīng)用一大就會變得難以維護(hù)。
那我們是不是可以把真實DOM樹抽象成一棵以JavaScript對象構(gòu)成的抽象樹,在修改抽象樹數(shù)據(jù)后將抽象樹轉(zhuǎn)化成真實DOM重繪到頁面上呢?于是虛擬DOM出現(xiàn)了,它是真實DOM的一層抽象,用屬性描述真實DOM的各個特性。當(dāng)它發(fā)生變化的時候,就會去修改視圖。
但是這樣的JavaScript操作DOM進(jìn)行重繪整個視圖層是相當(dāng)消耗性能的,我們是不是可以每次只更新它的修改呢?所以Vue.js將DOM抽象成一個以JavaScript對象為節(jié)點的虛擬DOM樹,以VNode節(jié)點模擬真實DOM,可以對這顆抽象樹進(jìn)行創(chuàng)建節(jié)點、刪除節(jié)點以及修改節(jié)點等操作,在這過程中都不需要操作真實DOM,只需要操作JavaScript對象,大大提升了性能。修改以后經(jīng)過diff算法得出一些需要修改的最小單位,再將這些小單位的視圖進(jìn)行更新。這樣做減少了很多不需要的DOM操作,大大提高了性能。
Vue就使用了這樣的抽象節(jié)點VNode,它是對真實DOM的一層抽象,而不依賴某個平臺,它可以是瀏覽器平臺,也可以是weex,甚至是node平臺也可以對這樣一棵抽象DOM樹進(jìn)行創(chuàng)建刪除修改等操作,這也為前后端同構(gòu)提供了可能。
先來看一下Vue.js源碼中對VNode類的定義。
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array; 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 , text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions ) { /*當(dāng)前節(jié)點的標(biāo)簽名*/ this.tag = tag /*當(dāng)前節(jié)點對應(yīng)的對象,包含了具體的一些數(shù)據(jù)信息,是一個VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息*/ this.data = data /*當(dāng)前節(jié)點的子節(jié)點,是一個數(shù)組*/ this.children = children /*當(dāng)前節(jié)點的文本*/ this.text = text /*當(dāng)前虛擬節(jié)點對應(yīng)的真實dom節(jié)點*/ this.elm = elm /*當(dāng)前節(jié)點的名字空間*/ this.ns = undefined /*編譯作用域*/ this.context = context /*函數(shù)化組件作用域*/ this.functionalContext = undefined /*節(jié)點的key屬性,被當(dāng)作節(jié)點的標(biāo)志,用以優(yōu)化*/ this.key = data && data.key /*組件的option選項*/ this.componentOptions = componentOptions /*當(dāng)前節(jié)點對應(yīng)的組件的實例*/ this.componentInstance = undefined /*當(dāng)前節(jié)點的父節(jié)點*/ this.parent = undefined /*簡而言之就是是否為原生HTML或只是普通文本,innerHTML的時候為true,textContent的時候為false*/ this.raw = false /*靜態(tài)節(jié)點標(biāo)志*/ this.isStatic = false /*是否作為跟節(jié)點插入*/ this.isRootInsert = true /*是否為注釋節(jié)點*/ this.isComment = false /*是否為克隆節(jié)點*/ 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 } }
這是一個最基礎(chǔ)的VNode節(jié)點,作為其他派生VNode類的基類,里面定義了下面這些數(shù)據(jù)。
tag: 當(dāng)前節(jié)點的標(biāo)簽名
data: 當(dāng)前節(jié)點對應(yīng)的對象,包含了具體的一些數(shù)據(jù)信息,是一個VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息
children: 當(dāng)前節(jié)點的子節(jié)點,是一個數(shù)組
text: 當(dāng)前節(jié)點的文本
elm: 當(dāng)前虛擬節(jié)點對應(yīng)的真實dom節(jié)點
ns: 當(dāng)前節(jié)點的名字空間
context: 當(dāng)前節(jié)點的編譯作用域
functionalContext: 函數(shù)化組件作用域
key: 節(jié)點的key屬性,被當(dāng)作節(jié)點的標(biāo)志,用以優(yōu)化
componentOptions: 組件的option選項
componentInstance: 當(dāng)前節(jié)點對應(yīng)的組件的實例
parent: 當(dāng)前節(jié)點的父節(jié)點
raw: 簡而言之就是是否為原生HTML或只是普通文本,innerHTML的時候為true,textContent的時候為false
isStatic: 是否為靜態(tài)節(jié)點
isRootInsert: 是否作為跟節(jié)點插入
isComment: 是否為注釋節(jié)點
isCloned: 是否為克隆節(jié)點
isOnce: 是否有v-once指令
打個比方,比如說我現(xiàn)在有這么一個VNode樹
{ tag: "div" data: { class: "test" }, children: [ { tag: "span", data: { class: "demo" } text: "hello,VNode" } ] }
渲染之后的結(jié)果就是這樣的
hello,VNode
更多操作VNode的方法,請參考《VNode節(jié)點》。
patch最后_update會將新舊兩個VNode進(jìn)行一次patch的過程,得出兩個VNode最小的差異,然后將這些差異渲染到視圖上。
首先說一下patch的核心diff算法,diff算法是通過同層的樹節(jié)點進(jìn)行比較而非對樹進(jìn)行逐層搜索遍歷的方式,所以時間復(fù)雜度只有O(n),是一種相當(dāng)高效的算法。
這兩張圖代表舊的VNode與新VNode進(jìn)行patch的過程,他們只是在同層級的VNode之間進(jìn)行比較得到變化(第二張圖中相同顏色的方塊代表互相進(jìn)行比較的VNode節(jié)點),然后修改變化的視圖,所以十分高效。
在patch的過程中,如果兩個VNode被認(rèn)為是同一個VNode(sameVnode),則會進(jìn)行深度的比較,得出最小差異,否則直接刪除舊有DOM節(jié)點,創(chuàng)建新的DOM節(jié)點。
什么是sameVnode?
我們來看一下sameVnode的實現(xiàn)。
/* 判斷兩個VNode節(jié)點是否是同一個節(jié)點,需要滿足以下條件 key相同 tag(當(dāng)前節(jié)點的標(biāo)簽名)相同 isComment(是否為注釋節(jié)點)相同 是否data(當(dāng)前節(jié)點對應(yīng)的對象,包含了具體的一些數(shù)據(jù)信息,是一個VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息)都有定義 當(dāng)標(biāo)簽是的時候,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 // so they need to be treated as different nodes /* 判斷當(dāng)標(biāo)簽是的時候,type是否相同 某些瀏覽器不支持動態(tài)修改類型,所以他們被視為不同類型 */ 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 }
當(dāng)兩個VNode的tag、key、isComment都相同,并且同時定義或未定義data的時候,且如果標(biāo)簽為input則type必須相同。這時候這兩個VNode則算sameVnode,可以直接進(jìn)行patchVnode操作。
patchVnode的規(guī)則是這樣的:
1.如果新舊VNode都是靜態(tài)的,同時它們的key相同(代表同一節(jié)點),并且新的VNode是clone或者是標(biāo)記了once(標(biāo)記v-once屬性,只渲染一次),那么只需要替換elm以及componentInstance即可。
2.新老節(jié)點均有children子節(jié)點,則對子節(jié)點進(jìn)行diff操作,調(diào)用updateChildren,這個updateChildren也是diff的核心。
3.如果老節(jié)點沒有子節(jié)點而新節(jié)點存在子節(jié)點,先清空老節(jié)點DOM的文本內(nèi)容,然后為當(dāng)前DOM節(jié)點加入子節(jié)點。
4.當(dāng)新節(jié)點沒有子節(jié)點而老節(jié)點有子節(jié)點的時候,則移除該DOM節(jié)點的所有子節(jié)點。
5.當(dāng)新老節(jié)點都無子節(jié)點的時候,只是文本的替換。
updateChildrenfunction 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// 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的兩頭節(jié)點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對應(yīng)的哈希表(只有第一次進(jìn)來undefined的時候會生成,也為后面檢測重復(fù)的key值做鋪墊) 比如childre是這樣的 [{xx: xx, key: "key0"}, {xx: xx, key: "key1"}, {xx: xx, key: "key2"}] beginIdx = 0 endIdx = 2 結(jié)果生成{key0: 0, key1: 1, key2: 2} */ if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) /*如果newStartVnode新的VNode節(jié)點存在key并且這個key在oldVnode中能找到則返回這個節(jié)點的idxInOld(即第幾個節(jié)點,下標(biāo))*/ idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null if (isUndef(idxInOld)) { // New element /*newStartVnode沒有key或者是該key沒有在老節(jié)點中找到則創(chuàng)建一個新的節(jié)點*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { /*獲取同key的老節(jié)點*/ elmToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== "production" && !elmToMove) { /*如果elmToMove不存在說明之前已經(jīng)有新節(jié)點放入過這個key的DOM中,提示可能存在重復(fù)的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的節(jié)點是同一個VNode則進(jìn)行patchVnode*/ patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) /*因為已經(jīng)patchVnode進(jìn)去了,所以將這個老節(jié)點賦值undefined,之后如果還有新節(jié)點與該節(jié)點key相同可以檢測出來提示已有重復(fù)的key*/ oldCh[idxInOld] = undefined /*當(dāng)有標(biāo)識位canMove實可以直接插入oldStartVnode對應(yīng)的真實DOM節(jié)點前面*/ canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // same key but different element. treat as new element /*當(dāng)新的VNode與找到的同樣key的VNode不是sameVNode的時候(比如說tag不一樣或者是有不一樣type的input標(biāo)簽),創(chuàng)建一個新的節(jié)點*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } } if (oldStartIdx > oldEndIdx) { /*全部比較完成以后,發(fā)現(xiàn)oldStartIdx > oldEndIdx的話,說明老節(jié)點已經(jīng)遍歷完了,新節(jié)點比老節(jié)點多,所以這時候多出來的新節(jié)點需要一個一個創(chuàng)建出來加入到真實DOM中*/ refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { /*如果全部比較完成以后發(fā)現(xiàn)newStartIdx > newEndIdx,則說明新節(jié)點已經(jīng)遍歷完了,老節(jié)點多余新節(jié)點,這個時候需要將多余的老節(jié)點從真實DOM中移除*/ removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
直接看源碼可能比較難以捋清其中的關(guān)系,我們通過圖來看一下。
首先,在新老兩個VNode節(jié)點的左右頭尾兩側(cè)都有一個變量標(biāo)記,在遍歷過程中這幾個變量都會向中間靠攏。當(dāng)oldStartIdx <= oldEndIdx或者newStartIdx <= newEndIdx時結(jié)束循環(huán)。
索引與VNode節(jié)點的對應(yīng)關(guān)系:
oldStartIdx => oldStartVnode
oldEndIdx => oldEndVnode
newStartIdx => newStartVnode
newEndIdx => newEndVnode
在遍歷中,如果存在key,并且滿足sameVnode,會將該DOM節(jié)點進(jìn)行復(fù)用,否則則會創(chuàng)建一個新的DOM節(jié)點。
首先,oldStartVnode、oldEndVnode與newStartVnode、newEndVnode兩兩比較一共有2*2=4種比較方法。
當(dāng)新老VNode節(jié)點的start或者end滿足sameVnode時,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接將該VNode節(jié)點進(jìn)行patchVnode即可。
如果oldStartVnode與newEndVnode滿足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。
這時候說明oldStartVnode已經(jīng)跑到了oldEndVnode后面去了,進(jìn)行patchVnode的同時還需要將真實DOM節(jié)點移動到oldEndVnode的后面。
如果oldEndVnode與newStartVnode滿足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。
這說明oldEndVnode跑到了oldStartVnode的前面,進(jìn)行patchVnode的同時真實的DOM節(jié)點移動到了oldStartVnode的前面。
如果以上情況均不符合,則通過createKeyToOldIdx會得到一個oldKeyToIdx,里面存放了一個key為舊的VNode,value為對應(yīng)index序列的哈希表。從這個哈希表中可以找到是否有與newStartVnode一致key的舊的VNode節(jié)點,如果同時滿足sameVnode,patchVnode的同時會將這個真實DOM(elmToMove)移動到oldStartVnode對應(yīng)的真實DOM的前面。
當(dāng)然也有可能newStartVnode在舊的VNode節(jié)點找不到一致的key,或者是即便key相同卻不是sameVnode,這個時候會調(diào)用createElm創(chuàng)建一個新的DOM節(jié)點。
到這里循環(huán)已經(jīng)結(jié)束了,那么剩下我們還需要處理多余或者不夠的真實DOM節(jié)點。
1.當(dāng)結(jié)束時oldStartIdx > oldEndIdx,這個時候老的VNode節(jié)點已經(jīng)遍歷完了,但是新的節(jié)點還沒有。說明了新的VNode節(jié)點實際上比老的VNode節(jié)點多,也就是比真實DOM多,需要將剩下的(也就是新增的)VNode節(jié)點插入到真實DOM節(jié)點中去,此時調(diào)用addVnodes(批量調(diào)用createElm的接口將這些節(jié)點加入到真實DOM中去)。
2。同理,當(dāng)newStartIdx > newEndIdx時,新的VNode節(jié)點已經(jīng)遍歷完了,但是老的節(jié)點還有剩余,說明真實DOM節(jié)點多余了,需要從文檔中刪除,這時候調(diào)用removeVnodes將這些多余的真實DOM刪除。
更詳細(xì)的diff實現(xiàn)參考筆者的文章VirtualDOM與diff(Vue.js實現(xiàn)).MarkDown)。
映射到真實DOM由于Vue使用了虛擬DOM,所以虛擬DOM可以在任何支持JavaScript語言的平臺上操作,譬如說目前Vue支持的瀏覽器平臺或是weex,在虛擬DOM的實現(xiàn)上是一致的。那么最后虛擬DOM如何映射到真實的DOM節(jié)點上呢?
Vue為平臺做了一層適配層,瀏覽器平臺見/platforms/web/runtime/node-ops.js以及weex平臺見/platforms/weex/runtime/node-ops.js。不同平臺之間通過適配層對外提供相同的接口,虛擬DOM進(jìn)行操作真實DOM節(jié)點的時候,只需要調(diào)用這些適配層的接口即可,而內(nèi)部實現(xiàn)則不需要關(guān)心,它會根據(jù)平臺的改變而改變。
現(xiàn)在又出現(xiàn)了一個問題,我們只是將虛擬DOM映射成了真實的DOM。那如何給這些DOM加入attr、class、style等DOM屬性呢?
這要依賴于虛擬DOM的生命鉤子。虛擬DOM提供了如下的鉤子函數(shù),分別在不同的時期會進(jìn)行調(diào)用。
const hooks = ["create", "activate", "update", "remove", "destroy"] /*構(gòu)建cbs回調(diào)函數(shù),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]]) } } }
同理,也會根據(jù)不同平臺有自己不同的實現(xiàn),我們這里以Web平臺為例。Web平臺的鉤子函數(shù)見/platforms/web/runtime/modules。里面有對attr、class、props、events、style以及transition(過渡狀態(tài))的DOM屬性進(jìn)行操作。
以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節(jié)點均沒有attr屬性,則直接返回*/ if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { return } let key, cur, old /*VNode節(jié)點對應(yīng)的Dom實例*/ const elm = vnode.elm /*舊VNode節(jié)點的attr*/ const oldAttrs = oldVnode.data.attrs || {} /*新VNode節(jié)點的attr*/ let attrs: any = vnode.data.attrs || {} // clone observed objects, as the user probably wants to mutate it /*如果新的VNode的attr已經(jīng)有__ob__(代表已經(jīng)被Observe處理過了), 進(jìn)行深拷貝*/ 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) } } } } /*設(shè)置attr*/ function setAttr (el: Element, key: string, value: any) { if (isBooleanAttr(key)) { // set attribute for blank value // e.g. 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鉤子被調(diào)用時更新DOM的attr屬性即可。
最后至此,我們已經(jīng)從template到真實DOM的整個過程梳理完了。現(xiàn)在再去看這張圖,是不是更清晰了呢?
關(guān)于作者:染陌
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
轉(zhuǎn)載請注明出處,謝謝。
歡迎關(guān)注我的公眾號
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/89080.html
摘要:實際上,我在看代碼的過程中順手提交了這個,作者眼明手快,當(dāng)天就進(jìn)行了修復(fù),現(xiàn)在最新的代碼里已經(jīng)不是這個樣子了而且狀態(tài)機(jī)標(biāo)識由字符串換成了數(shù)字常量,解析更準(zhǔn)確的同時執(zhí)行效率也會更高。 最近饒有興致的又把最新版?Vue.js?的源碼學(xué)習(xí)了一下,覺得真心不錯,個人覺得 Vue.js 的代碼非常之優(yōu)雅而且精辟,作者本身可能無 (bu) 意 (xie) 提及這些。那么,就讓我來吧:) 程序結(jié)構(gòu)梳...
摘要:實際上,我在看代碼的過程中順手提交了這個,作者眼明手快,當(dāng)天就進(jìn)行了修復(fù),現(xiàn)在最新的代碼里已經(jīng)不是這個樣子了而且狀態(tài)機(jī)標(biāo)識由字符串換成了數(shù)字常量,解析更準(zhǔn)確的同時執(zhí)行效率也會更高。 最近饒有興致的又把最新版?Vue.js?的源碼學(xué)習(xí)了一下,覺得真心不錯,個人覺得 Vue.js 的代碼非常之優(yōu)雅而且精辟,作者本身可能無 (bu) 意 (xie) 提及這些。那么,就讓我來吧:) 程序結(jié)構(gòu)梳...
摘要:圖在中應(yīng)用三數(shù)據(jù)渲染過程數(shù)據(jù)綁定實現(xiàn)邏輯本節(jié)正式分析從到數(shù)據(jù)渲染到頁面的過程,在中定義了一個的構(gòu)造函數(shù)。一、概述 vue已是目前國內(nèi)前端web端三分天下之一,也是工作中主要技術(shù)棧之一。在日常使用中知其然也好奇著所以然,因此嘗試閱讀vue源碼并進(jìn)行總結(jié)。本文旨在梳理初始化頁面時data中的數(shù)據(jù)是如何渲染到頁面上的。本文將帶著這個疑問一點點追究vue的思路。總體來說vue模版渲染大致流程如圖1所...
摘要:當(dāng)我們的視圖和數(shù)據(jù)任何一方發(fā)生變化的時候,我們希望能夠通知對方也更新,這就是所謂的數(shù)據(jù)雙向綁定。返回值返回傳入函數(shù)的對象,即第一個參數(shù)該方法重點是描述,對象里目前存在的屬性描述符有兩種主要形式數(shù)據(jù)描述符和存取描述符。 前言 談起當(dāng)前前端最熱門的 js 框架,必少不了 Vue、React、Angular,對于大多數(shù)人來說,我們更多的是在使用框架,對于框架解決痛點背后使用的基本原理往往關(guān)注...
閱讀 1557·2021-11-17 09:33
閱讀 1106·2021-11-12 10:36
閱讀 2418·2019-08-30 15:54
閱讀 2443·2019-08-30 13:14
閱讀 2918·2019-08-26 14:05
閱讀 3294·2019-08-26 11:32
閱讀 3006·2019-08-26 10:09
閱讀 3001·2019-08-26 10:09