目前社區有不少 Vue.js 的源碼解析文章,不少大牛寫的都很是詳細,但說到底。光看文章本身不去研究源碼和總結筆記,終究不會深刻了解和記憶。前端
本篇文章將本身研究 Vue.js源碼的一些內容作成筆記而且記錄下來。加深印象和理解,俗話說讀書百遍不如手寫一遍。vue
MVC
模式是指用戶操做會請求服務端路由,路由會調用對應的控制器來處理,控制器會獲取數據。將結果返回給前端,頁面從新渲染。而且前端會將數據手動的操做DOM渲染到頁面上,很是消耗性能。node
雖然沒有徹底遵循 MVVM 模型,可是 Vue 的設計也受到了它的啓發。Vue中則再也不須要用戶手動操做DOM元素,而是將數據綁定到viewModel
層上,數據會自動渲染到頁面上,視圖變化會通知viewModel層
更新數據。ViewModel
就是咱們MVVM
模式中的橋樑.react
Vue2.x版本響應式數據的原理是 Object.defineProperty(Es6筆記中有詳細介紹)web
Vue在初始化的時候,也就是new Vue()的時候,會調用底層的一個initData()方法,方法中有一個observe()會將初始化傳入的data進行數據響應式控制,其中會對data進行一系列操做,判斷是否已經被觀測過。判斷觀測的數據是對象仍是數組。
面試
假若觀測的是一個對象,會調用一個walk()方法其內部內就是調用Object.defineProperty進行觀測,假若對象內部的屬性仍是一個對象的話,就會進行遞歸觀測。ajax
這時當對當前對象取值的時候就會調用get方法,get方法中就進行依賴收集(watcher),若是對當前對象進行賦值操做,就會調用set方法,set方法中會判斷新舊值是否不同,不同就會調用一個notify方法去觸發數據對應的依賴收集進行更新。算法
假若觀測的是一個數組,數組不會走上面的方法進行依賴收集,Vue底層重寫了數組的原型方法,當前觀測的是數組時,Vue將數組的原型指向了本身定義的原型方法。而且只攔截瞭如下7個數組的方法。express
// 由於只有如下7中數組方法纔會去改變原數組。
push, pop, shift, unshift, splice, sort, reverse
複製代碼
原型方法內部採用的是函數劫持的方式,若是用戶操做的是以上7中數組方法,就會走Vue重寫的數組方法。這時候就能夠在數組發生變化時候,去手動調用notify方法去更新試圖。數組
固然在對數據進行數據更新的時候,也會對新增的數據進行依賴收集觀測。
若是數組中的數據也是對象,它會繼續調用Object.defineProperty對其進行觀測。
知道以上內容,你就能夠理解爲什麼數組經過下標修改數據,數據變化了可是視圖沒有更新的緣由。
觀測對象核心代碼
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend() // ** 收集依賴 ** /
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
val = newVal
childOb = !shallow && observe(newVal)
dep.notify() /** 通知相關依賴進行更新 **/
}
})
複製代碼
觀測數組核心代碼
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) { // 重寫原型方法
const original = arrayProto[method] // 調用原數組的方法
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify() // 當調用數組方法後,手動通知視圖更新
return result
})
})
this.observeArray(value) // 進行深度監控
複製代碼
以上內容最好下載Github上Vue源碼一塊兒看。
首先咱們要知道Vue是組件級更新。
當咱們操做某個組件中的方法進行數據更新的時候,例如
data() {
return {
msg: 'hello word',
name: '只會番茄炒蛋'
}
}
methods:{
add() {
this.msg = '我更改了 => hello word'
this.name = '我更改了 => 只會番茄炒蛋'
}
}
複製代碼
假若一旦更改數據就進行視圖的渲染(以上更改了兩次數據),必然會影響性能,所以Vue採用異步渲染方式,也就是多個數據在一個事件中同時被更改了,同一個watcher被屢次觸發,只會被推入到隊列中一次。當最後數據被更改完畢之後調用nexttick方法去異步更新視圖。
內部還有一些其餘的操做,例如添加 watcher 的時候給一個惟一的id, 更新的時候根據 id 進行一個排序,更新完畢還會調用對應的生命週期也就是 beforeUpdate 和 updated 方法等。
以上內容最好下載Github上Vue源碼一塊兒看。
在瞭解nextTick實現原理以前,你須要掌握什麼Event Loop,而且瞭解微任務和宏任務,這裏我簡單介紹一下。
你們也知道了當咱們執行 JS 代碼的時候其實就是往執行棧中放入函數,那麼遇到異步代碼的時候該怎麼辦?其實當遇到異步的代碼時,會被掛起並在須要執行的時候加入到 Task(有多種 Task) 隊列中。一旦執行棧爲空,Event Loop 就會從 Task 隊列中拿出須要執行的代碼並放入執行棧中執行,因此本質上來講 JS 中的異步仍是同步行爲。
不一樣的任務源會被分配到不一樣的 Task 隊列中,任務源能夠分爲 微任務(microtask) 和 宏任務(macrotask)。在 ES6 規範中,microtask 稱爲 jobs,macrotask 稱爲 task。
微任務包括 process.nextTick ,promise.then ,MutationObserver,其中 process.nextTick 爲 Node 獨有。
宏任務包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering。
簡單瞭解 Event Loop 以後繼續學習 Vue 中 nextTick 實現原理
Vue 在內部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,若是執行環境不支持,則會採用 setTimeout(fn, 0) 代替。
官方原話
當你設置vm.someData = 'new value',該組件不會當即從新渲染。當刷新隊列時,組件會在下一個事件循環「tick」中更新。多數狀況咱們不須要關心這個過程,可是若是你想基於更新後的 DOM 狀態來作點什麼,這就可能會有些棘手。雖然 Vue.js 一般鼓勵開發人員使用「數據驅動」的方式思考,避免直接接觸 DOM,可是有時咱們必需要這麼作。爲了在數據變化以後等待 Vue 完成更新 DOM,能夠在數據變化以後當即使用 Vue.nextTick(callback)。這樣回調函數將在 DOM 更新完成後被調用。
總結:nextTick方法主要是使用了宏任務和微任務,定義了一個異步方法,屢次調用nextTick會將方法存入隊列中,經過這個異步方法清空當前隊列。 因此這個nextTick方法就是異步方法
nextTick原理核心代碼
let timerFunc // 會定義一個異步方法
if (typeof Promise !== 'undefined' && isNative(Promise)) { // promise
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && ( // MutationObserver
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' ) { // setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => { // setTimeout
setTimeout(flushCallbacks, 0)
}
}
// nextTick實現
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
}
複製代碼
以上內容最好下載Github上Vue源碼一塊兒看。
通常面試題中都問會問到 computed 和 watch 的區別,實際上 computed 和 watch 的原理都是使用watcher實現的,而他倆的區別就是 computed 具備緩存的功能。
當咱們默認初始化建立計算屬性的時候,它會建立一個watcher, 而且這個watcher有兩個個屬性lazy:true,dirty: true
, 也就是說當建立一個計算屬性的時候,默認是不執行的,只有當用戶取值的時候(也就是在組件上使用的時候),它會判斷若是dirty: true
的話就會讓這個watcher執行去取值,而且在求值結束後,更改dirty: false
,這樣當你再次使用這個計算屬性的時候,判斷條件走到dirty: false
的時候,就不在執行watcher求值操做,而是直接返回上次求值的結果。
那麼何時會從新計算求職呢?
只有當計算屬性的值發生變化的時候,它會調用對應的update方法,而後更改dirty: true
,而後執行的時候根據條件從新執行watcher求值。
computed原理核心代碼
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) { // 若是依賴的值沒發生變化,就不會從新求值
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
複製代碼
以上內容最好下載Github上Vue源碼一塊兒看。
Vue官方關於watch的介紹
類型:{ [key: string]: string | Function | Object | Array }
詳細:一個對象,鍵是須要觀察的表達式,值是對應回調函數。值也能夠是方法名,或者包含選項的對象。Vue 實例將會在實例化時調用 $watch(),遍歷 watch 對象的每個屬性。
一般咱們在項目中通常使用watch來監聽路由或者data中的屬性發生變化時做出對應的處理方式。
那麼deep : true的使用場景就是當咱們監測的屬性是一個對象的時候,咱們會發現watch中監測的方法並無執行,緣由是受現代 JavaScript 的限制 (以及廢棄 Object.observe),Vue 不能檢測到對象屬性的添加或刪除。因爲 Vue 會在初始化實例時對屬性執行 getter/setter 轉化過程,因此屬性必須在 data 對象上存在才能讓 Vue 轉換它,這樣才能讓它是響應的。
deep的意思就是深刻觀察,監聽器會一層層的往下遍歷,給對象的全部屬性都加上這個監聽器,可是這樣性能開銷就會很是大了,任何修改obj裏面任何一個屬性都會觸發這個監聽器裏的 handler。
這時候咱們能夠優化這個問題,經過如下方式
// 使用字符串形式監聽具體對象中的某個值。
watch: {
'obj.a': {
handler(newName, oldName) {
console.log('obj.a changed');
},
immediate: true, // 當即執行一次handler方法
deep: true // 深度監測
}
}
複製代碼
須要注意的是,當咱們經過下標去修改數組中某個值的時候,也不會引發watch的變化,原理請看上面Vue中響應式數據的原理是什麼?
固然除了改變數組的方法能夠進行監測數組變化,Vue也提供來Vue.set()方法。
Watch中的 deep : true 核心代碼
get () {
pushTarget(this) // 先將當前依賴放到 Dep.target上
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) { // 若是須要深度監控
traverse(value) // 會對對象中的每一項取值,取值時會執行對應的get方法
}
popTarget()
}
return value
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
複製代碼
以上內容最好下載Github上Vue源碼一塊兒看。
附上Vue官方關於生命週期的介紹圖
在實例初始化之後,數據觀測(data observe)以前進行調用,此時獲取不到data中的數據。
複製代碼
在實例建立完成以後調用,這時候實例已經完成了數據觀測(data observe),屬性和方法的運算,watch/event 事件回調。
注意:這裏沒有$el
複製代碼
在掛載以前調用,相關的render函數首次被調用。
複製代碼
el綁定的元素被內部新建立的$el替換掉,而且掛載到實例上去以後調用。
複製代碼
數據更新時調用,發生在虛擬DOM從新渲染和打補丁以前。
複製代碼
因爲數據更改致使的虛擬 DOM 從新渲染和打補丁,在這以後會調用該鉤子。
複製代碼
實例銷燬以前調用。在這一步,實例仍然徹底可用
複製代碼
Vue實例銷燬後調用。調用後,Vue實例指示的全部東西都會解綁定,全部的事件監聽器會被移除。
全部的子實例也會被銷燬,該鉤子在服務器端渲染期間不被調用。
複製代碼
一般咱們在項目中會在created(){}生命週期中去調用ajax進行一些數據資源的請求,可是因爲當前生命週期沒法操做DOM
因此通常在項目中,全部的請求我都會統一放到mounted(){}生命週期中。
複製代碼
在當前生命週期中,實例已經掛載完成,一般我會將ajax請求放到這個生命週期函數中。
若是有一些須要根據獲取的數據並去初始化DOM操做,在這裏是最佳方案。
複製代碼
能夠在這個生命週期函數中進一步地更改狀態,這不會觸發附加的重渲染過程
複製代碼
能夠執行依賴於 DOM 的操做。然而在大多數狀況下,你應該避免在此期間更改狀態,由於這可能會致使更新無限循環。
該鉤子在服務器端渲染期間不被調用。
複製代碼
能夠執行一些優化操做,清空定時器,解除綁定事件
複製代碼
經過以上描述咱們能夠總結出如下結論
ajax請求放在一般放在created(){}或者mounted(){}生命週期中。 而且在created的時候,視圖中的dom
並無渲染出來,因此此時若是直接去操dom
節點,沒法找到相關的元素,在mounted中,因爲此時dom
已經渲染出來了,因此能夠直接操做dom
節點 ,通常狀況下都放到mounted
中,保證邏輯的統一性,由於生命週期是同步執行的,ajax
是異步執行的。
注意:服務端渲染不支持mounted方法,因此在服務端渲染的狀況下統一放到created中
假若當前組件中有定時器,使用了$on方法,綁定 scroll mousemove
等事件,須要在beforeDestroy鉤子中去清除。
查看源碼後發現,Vue在底層會調用一個parseHTML方法將模版轉爲AST語法樹(內部經過正則走一些方法),最後將AST語法樹轉爲render函數(渲染函數),渲染函數結合數據生成Virtual DOM樹,Diff和Patch後生成新的UI。
Vue的編譯器在編譯模板以後,會把這些模板編譯成一個渲染函數。而函數被調用的時候就會渲染而且返回一個虛擬DOM的樹。當咱們有了這個虛擬的樹以後,再交給一個Patch函數,負責把這些虛擬DOM真正施加到真實的DOM上。
在這個過程當中,Vue有自身的響應式系統來偵測在渲染過程當中所依賴到的數據來源。在渲染過程當中,偵測到數據來源以後就能夠精確感知數據源的變更。到時候就能夠根據須要從新進行渲染。當從新進行渲染以後,會生成一個新的樹,將新的樹與舊的樹進行對比,就能夠最終得出應施加到真實DOM上的改動。
最後再經過Patch函數施加改動。簡單點講,在Vue的底層實現上,Vue將模板編譯成虛擬DOM渲染函數。結合Vue自帶的響應系統,在應該狀態改變時,Vue可以智能地計算出從新渲染組件的最小代價並應到DOM操做上。
實際上這一部分的源碼仍是比較多的。這裏我簡單的理解了一些。
這裏我簡單描述一下二者的區別
若是當前條件判斷不成立,那麼當前指令所在節點的DOM元素不會渲染
複製代碼
當前指令所在節點的DOM元素始終會被渲染,只是根據當前條件去動態改變 display: none || block
從而達到DOM元素的顯示和隱藏。
複製代碼
Vue底層封裝了一些特殊的方法,代碼位於此處。 vue/packages/weex-vue-framework/factory.js
VueTemplateCompiler.compile(`<div v-if="true"><span v-for="i in 3">hello</span></div>`);
with(this) {
return (true) ? _c('div', _l((3), function (i) {
return _c('span', [_v("hello")])
}), 0) : _e() // _e()方法建立一個空的虛擬dom等等。
}
複製代碼
經過上述代碼,能夠得知若是當前條件判斷不成立,那麼當前指令所在節點的DOM元素不會渲染
v-show編譯出來裏面沒有任何東西,只有一個directives,它裏面有一個指令叫作v-show
VueTemplateCompiler.compile(`<div v-show="true"></div>`);
/**
with(this) {
return _c('div', {
directives: [{
name: "show",
rawName: "v-show",
value: (true),
expression: "true"
}]
})
}
複製代碼
只有在運行的時候它會去處理這個指令,代碼以下:
// v-show 操做的是樣式 定義在platforms/web/runtime/directives/show.js
bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
vnode = locateNode(vnode)
const transition = vnode.data && vnode.data.transition
const originalDisplay = el.__vOriginalDisplay =
el.style.display === 'none' ? '' : el.style.display
if (value && transition) {
vnode.data.show = true
enter(vnode, () => {
el.style.display = originalDisplay
})
} else {
el.style.display = value ? originalDisplay : 'none'
}
}
複製代碼
經過源碼咱們能夠清晰的看到它是在操做DOM的display屬性。
一樣能夠經過觀看源碼就知道緣由
VueTemplateCompiler.compile(`<div v-if="false" v-for="i in 3">hello</div>`);
with(this) {
return _l((3), function (i) {
return (false) ? _c('div', [_v("hello")]) : _e()
})
}
複製代碼
咱們知道 v-for 的優先級比 v-if 高,那麼在編譯階段會發現他給內部的每個元素都加了 v-if,這樣在運行的階段會走驗證,這樣很是的消耗性能。所以在項目中咱們要避免這樣的操做。
固然若是咱們有這樣的需求的話也是能夠實現的。
咱們能夠經過計算屬性來達到目的
<div v-for="i in computedNumber">hello</div>
export default {
data() {
return {
arr: [1, 2, 3]
}
},
computed: {
computedNumber() {
return arr.filter(item => item > 1)
}
}
}
複製代碼
關於解析指令的源碼,建議你們也去看看源碼的實現過程
在我理解來講,就是用一個對象來描述咱們的虛擬DOM結構,例如:
<div id="container">
<p></p>
</div>
// 簡單用對象來描述的虛擬DOM結構
let obj = {
tag: 'div',
data: {
id: "container"
},
children: [
{
tag: 'p',
data: {},
children: {}
}
]
}
複製代碼
固然在Vue中的實現是比較複雜的,我這裏添加來一些注視方便理解
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {
// 兼容不傳data的狀況
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// 若是alwaysNormalize是true
// 那麼normalizationType應該設置爲常量ALWAYS_NORMALIZE的值
if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
// 調用_createElement建立虛擬節點
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (context, tag, data, children, normalizationType) {
/**
* 若是存在data.__ob__,說明data是被Observer觀察的數據
* 不能用做虛擬節點的data
* 須要拋出警告,並返回一個空節點
*
* 被監控的data不能被用做vnode渲染的數據的緣由是:
* data在vnode渲染過程當中可能會被改變,這樣會觸發監控,致使不符合預期的操做
*/
if (data && data.__ob__) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// 當組件的is屬性被設置爲一個falsy的值
// Vue將不會知道要把這個組件渲染成什麼
// 因此渲染一個空節點
if (!tag) {
return createEmptyVNode()
}
// 做用域插槽
if (Array.isArray(children) && typeof children[0] === 'function') {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 根據normalizationType的值,選擇不一樣的處理方法
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
// 若是標籤名是字符串類型
if (typeof tag === 'string') {
let Ctor
// 獲取標籤名的命名空間
ns = config.getTagNamespace(tag)
// 判斷是否爲保留標籤
if (config.isReservedTag(tag)) {
// 若是是保留標籤,就建立一個這樣的vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
// 若是不是保留標籤,那麼咱們將嘗試從vm的components上查找是否有這個標籤的定義
} else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
// 若是找到了這個標籤的定義,就以此建立虛擬組件節點
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 兜底方案,正常建立一個vnode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
// 當tag不是字符串的時候,咱們認爲tag是組件的構造類
// 因此直接建立
} else {
vnode = createComponent(tag, data, context, children)
}
// 若是有vnode
if (vnode) {
// 若是有namespace,就應用下namespace,而後返回vnode
if (ns) applyNS(vnode, ns)
return vnode
// 不然,返回一個空節點
} else {
return createEmptyVNode()
}
}
}
複製代碼
以上內容最好下載Github上Vue源碼一塊兒看。
算法方面不是很瞭解,這邊也只是簡單看視頻和文章介紹描述一下。
兩個樹的徹底的diff
算法是一個時間複雜度爲 O(n3)
,Vue
進行了優化·O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題(只比較同級不考慮跨級問題) 在前端當中, 你不多會跨越層級地移動Dom元素。 因此 Virtual Dom只會對同一個層級的元素進行對比。
第一種狀況:同級比較
當新節點和舊節點不相同狀況,新節點直接替換舊節點。
第二種狀況:同級比較,節點一致,但一方有子節點,一方沒有
當新舊節點相同狀況下,若是新節點有子節點,但舊節點沒有,那麼舊會直接將新節點的子節點插入到舊節點中。
當新舊節點相同狀況下,若是新節點沒有子節點,但舊節點有子節點,那麼舊節點會直接刪除子節點。
第三種狀況:新舊節點相同,且都有子節點。(這時候舊用到了上圖雙指針比較方法。)
狀況一:
舊:1234
新:12345
當前雙指針指向新舊1,1和4,5,判斷首部節點一致,指針向後移繼續判斷,直到最後一項不相同,將新5插入到舊4後面。
狀況二:
舊:1234
新:01234
當前雙指針指向新舊1,0和4,4 發現不想等時會從最後的指針查看,這時候發現相同後,會從後面往前移動指針進行判斷。直到到達首部,將新0插入到舊1以前。
狀況三:
舊:1234
新:4123
當前發現頭部和頭部不想等,而且尾部和尾部不想等的時候,就混進行頭尾/尾頭的互相比較。這時候發現舊的4在新的第一位,就會將本身的4調整到1的前面。
狀況四:
舊:1234
新:2341
當前發現頭部和頭部不想等,而且尾部和尾部不想等的時候,就混進行頭尾/尾頭的互相比較。這時候發現舊的1在新的第四位,就會將本身的1調整到4的後面。
特殊狀況五:(也就是咱們循環數組時候須要加key值的緣由)
舊:1234
新:2456
這時候遞歸遍歷會拿新的元素的Key去舊的比較而後移動位置,若是舊的沒有就直接將新的放進去,反之將舊的中有,新的沒有的元素刪除掉。
複製代碼
經過上述內容咱們大體舊瞭解diff算法的一部分了。
核心源碼
core/vdom/patch.js
const oldCh = oldVnode.children // 老的兒子
const ch = vnode.children // 新的兒子
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 比較孩子
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) { // 新的兒子有 老的沒有
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) { // 若是老的有新的沒有 就刪除
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) { // 老的有文本 新的沒文本
nodeOps.setTextContent(elm, '') // 將老的清空
}
} else if (oldVnode.text !== vnode.text) { // 文本不相同替換
nodeOps.setTextContent(elm, vnode.text)
}
複製代碼
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, vnodeToMove, 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
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
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)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
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, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
複製代碼
例如當咱們利用for循環出三個chenckbox, 當咱們經過一個按鈕將當前循環數組的第一項刪除的時候,會發現第一項依舊是選中狀態,而最後一項被刪除了,緣由就是diff的過程當中。當對比新舊虛擬dom的時候,發現DOM一致,這時候內部複用了當前要刪除的第一項DOM(內容會是要現實的內容,而不是刪除的內容),作完比對後,將舊dom最後一項刪除了。
1 (1是選中狀態) 1 (1是選中狀態)
2 2
3 3 (被刪除了)
複製代碼
描述的可能有些混亂,你們能夠本身在項目中實踐一下。(ps:v-for循環必定要加上key且key不能爲index下標)
持續總結中。。。