vue2.x高階問題,你能答多少
有句老話說,在父母那里我們永遠(yuǎn)是孩子,同樣在各位大佬這里,我永遠(yuǎn)是菜雞??????。不管怎樣,學(xué)習(xí)的激情永遠(yuǎn)不可磨滅。答案如有錯誤,感謝指教??
首發(fā)個人博客
種一棵樹,最好的時機是十年前,其次是現(xiàn)在

vue源碼中值得學(xué)習(xí)的點
柯里化: 一個函數(shù)原本有多個參數(shù), 只傳入一個參數(shù), 生成一個新函數(shù), 由新函數(shù)接收剩下的參數(shù)來運行得到結(jié)構(gòu)偏函數(shù): 一個函數(shù)原本有多個參數(shù), 只傳入一部分參數(shù), 生成一個新函數(shù), 由新函數(shù)接收剩下的參數(shù)來運行得到結(jié)構(gòu)高階函數(shù): 一個函數(shù)參數(shù)是一個函數(shù), 該函數(shù)對參數(shù)這個函數(shù)進行加工, 得到一個函數(shù), 這個加工用的函數(shù)就是高階函數(shù)...
vue 響應(yīng)式系統(tǒng)
簡述:vue 初始化時會用Object.defineProperty()給data中每一個屬性添加getter和setter,同時創(chuàng)建dep和watcher進行依賴收集與派發(fā)更新,最后通過diff算法對比新老vnode差異,通過patch即時更新DOM
簡易圖解:

詳細(xì)版本
可以參考下圖片引用地址: 圖解 Vue 響應(yīng)式原理
Vue的數(shù)據(jù)為什么頻繁變化但只會更新一次
檢測到數(shù)據(jù)變化 開啟一個隊列 在同一事件循環(huán)中緩沖所有數(shù)據(jù)改變 如果同一個 watcher (watcherId相同)被多次觸發(fā),只會被推入到隊列中一次
不優(yōu)化,每一個數(shù)據(jù)變化都會執(zhí)行: setter->Dep->Watcher->update->run
優(yōu)化后:執(zhí)行順序update -> queueWatcher -> 維護觀察者隊列(重復(fù)id的Watcher處理) -> waiting標(biāo)志位處理 -> 處理$nextTick(在為微任務(wù)或者宏任務(wù)中異步更新DOM)
vue使用Object.defineProperty() 的缺陷
數(shù)組的length屬性被初始化configurable false,所以想要通過get/set方法來監(jiān)聽length屬性是不可行的。
vue中通過重寫了七個能改變原數(shù)組的方法來進行數(shù)據(jù)監(jiān)聽
對象還是使用Object.defineProperty()添加get和set來監(jiān)聽
參考
為什么defineProperty不能檢測到數(shù)組長度的變化
Vue.nextTick()原理
在下次DOM更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào)。在修改數(shù)據(jù)之后立即使用這個方法,獲取更新后的DOM。
源碼實現(xiàn):Promise > MutationObserver > setImmediate > setTimeout
參考文章:淺析Vue.nextTick()原理
computed 的實現(xiàn)原理
computed 本質(zhì)是一個惰性求值的觀察者computed watcher。其內(nèi)部通過 this.dirty 屬性標(biāo)記計算屬性是否需要重新求值。
當(dāng) computed 的依賴狀態(tài)發(fā)生改變時,就會通知這個惰性的 watcher, computed watcher通過this.dep.subs.length判斷有沒有訂閱者,有的話,會重新計算,然后對比新舊值,如果變化了,會重新渲染。(Vue 想確保不僅僅是計算屬性依賴的值發(fā)生變化,而是當(dāng)計算屬性 最終計算的值發(fā)生變化時才會觸發(fā)渲染 watcher重新渲染,本質(zhì)上是一種優(yōu)化。)沒有的話,僅僅把 this.dirty = true(當(dāng)計算屬性依賴于其他數(shù)據(jù)時,屬性并不會立即重新計算,只有之后其他地方需要讀取屬性的時候,它才會真正計算,即具備 lazy(懶計算)特性。)
watch 的理解
watch沒有緩存性,更多的是觀察的作用,可以監(jiān)聽某些數(shù)據(jù)執(zhí)行回調(diào)。當(dāng)我們需要深度監(jiān)聽對象中的屬性時,可以打開deep:true選項,這樣便會對對象中的每一項進行監(jiān)聽。這樣會帶來性能問題,優(yōu)化的話可以使用字符串形式監(jiān)聽
注意:Watcher : 觀察者對象 , 實例分為渲染 watcher (render watcher),計算屬性 watcher (computed watcher),偵聽器 watcher(user watcher)三種
vue diff 算法
只對比父節(jié)點相同的新舊子節(jié)點(比較的是Vnode),時間復(fù)雜度只有O(n) 在 diff 比較的過程中,循環(huán)從兩邊向中間收攏
新舊節(jié)點對比過程
1、先找到 不需要移動的相同節(jié)點,借助key值找到可復(fù)用的節(jié)點是,消耗最小
2、再找相同但是需要移動的節(jié)點,消耗第二小
3、最后找不到,才會去新建刪除節(jié)點,保底處理
注意:新舊節(jié)點對比過程,不會對這兩棵Vnode樹進行修改,而是以比較的結(jié)果直接對 真實DOM 進行修改
Vue的patch是即時的,并不是打包所有修改最后一起操作DOM(React則是將更新放入隊列后集中處理)
參考文章:Vue 虛擬dom diff原理詳解
vue 渲染過程

調(diào)用 compile函數(shù),生成 render 函數(shù)字符串 ,編譯過程如下:
parse 使用大量的正則表達式對template字符串進行解析,將標(biāo)簽、指令、屬性等轉(zhuǎn)化為抽象語法樹AST。 模板 -> AST (最消耗性能)optimize 遍歷AST,找到其中的一些靜態(tài)節(jié)點并進行標(biāo)記,方便在頁面重渲染的時候進行diff比較時,直接跳過這一些靜態(tài)節(jié)點, 優(yōu)化runtime的性能generate 將最終的AST轉(zhuǎn)化為render函數(shù)字符串
調(diào)用 new Watcher函數(shù),監(jiān)聽數(shù)據(jù)的變化,當(dāng)數(shù)據(jù)發(fā)生變化時,Render 函數(shù)執(zhí)行生成 vnode 對象調(diào)用 patch方法,對比新舊 vnode 對象,通過 DOM diff 算法,添加、修改、刪除真正的 DOM 元素
結(jié)合源碼,談一談vue生命周期
vue 生命周期官方圖解
Vue 中的 key 到底有什么用?
key 是給每一個 vnode 的唯一 id,依靠 key,我們的 diff 操作可以更準(zhǔn)確、更快速
更準(zhǔn)確 : 因為帶 key 就不是就地復(fù)用了,在 sameNode 函數(shù) a.key === b.key 對比中可以避免就地復(fù)用的情況。所以會更加準(zhǔn)確,如果不加 key,會導(dǎo)致之前節(jié)點的狀態(tài)被保留下來,會產(chǎn)生一系列的 bug。
更快速 : key 的唯一性可以被 Map 數(shù)據(jù)結(jié)構(gòu)充分利用,相比于遍歷查找的時間復(fù)雜度 O(n),Map 的時間復(fù)雜度僅僅為 O(1),源碼如下:
function createKeyToOldIdx(children, beginIdx, endIdx) {
let i, key;
const map = {};
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key;
if (isDef(key)) map[key] = i;
}
return map;
}
注意:在沒有key的情況下,會更快。感謝評論區(qū)老哥fengyangyang的提醒:引用官網(wǎng)的話:key 的特殊 attribute 主要用在 Vue 的虛擬 DOM 算法,在新舊 nodes 對比時辨識 VNodes。如果不使用 key,Vue 會使用一種最大限度減少動態(tài)元素并且盡可能的嘗試就地修改/復(fù)用相同類型元素的算法。而使用 key 時,它會基于 key 的變化重新排列元素順序,并且會移除 key 不存在的元素。
vue-router 路由模式有幾種
默認(rèn)值: "hash" (瀏覽器環(huán)境) | "abstract" (Node.js 環(huán)境)
可選值: "hash" | "history" | "abstract"
配置路由模式:
hash: 使用 URL hash 值來作路由。支持所有瀏覽器,包括不支持 HTML5 History Api 的瀏覽器。history: 依賴HTML5 History API和服務(wù)器配置。abstract: 支持所有 JavaScript 運行環(huán)境,如 Node.js 服務(wù)器端。如果發(fā)現(xiàn)沒有瀏覽器的 API,路由會自動強制進入這個模式.
說一說keep-alive實現(xiàn)原理
定義
keep-alive組件接受三個屬性參數(shù):include、exclude、max
include指定需要緩存的組件name集合,參數(shù)格式支持String, RegExp, Array。當(dāng)為字符串的時候,多個組件名稱以逗號隔開。exclude指定不需要緩存的組件name集合,參數(shù)格式和include一樣。max指定最多可緩存組件的數(shù)量,超過數(shù)量刪除第一個。參數(shù)格式支持String、Number。
原理
keep-alive實例會緩存對應(yīng)組件的VNode,如果命中緩存,直接從緩存對象返回對應(yīng)VNode
LRU(Least recently used)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進行淘汰數(shù)據(jù),其核心思想是“如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高”。(墨菲定律:越擔(dān)心的事情越會發(fā)生)
對對象屬性訪問的解析方法
eg:訪問 a.b.c.d
函數(shù)柯里化 + 閉包 + 遞歸
function createGetValueByPath( path ) {
let paths = path.split( '.' ); // [ xxx, yyy, zzz ]
return function getValueByPath( obj ) {
let res = obj;
let prop;
while( prop = paths.shift() ) {
res = res[ prop ];
}
return res;
}
}
let getValueByPath = createGetValueByPath( 'a.b.c.d' );
var o = {
a: {
b: {
c: {
d: {
e: '正確了'
}
}
}
}
};
var res = getValueByPath( o );
console.log( res );
vue中針對7個數(shù)組方法的重寫
Vue 通過原型攔截的方式重寫了數(shù)組的 7 個方法,首先獲取到這個數(shù)組的Observer。如果有新的值,就調(diào)用 observeArray 對新的值進行監(jiān)聽,然后調(diào)用 notify,通知 render watcher,執(zhí)行 update
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
methodsToPatch.forEach(function(method) {
// cache original 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;
});
});
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
vue處理響應(yīng)式 defineReactive 實現(xiàn)
// 簡化后的版本
function defineReactive( target, key, value, enumerable ) {
// 折中處理后, this 就是 Vue 實例
let that = this;
// 函數(shù)內(nèi)部就是一個局部作用域, 這個 value 就只在函數(shù)內(nèi)使用的變量 ( 閉包 )
if ( typeof value === 'object' && value != null && !Array.isArray( value ) ) {
// 是非數(shù)組的引用類型
reactify( value ); // 遞歸
}
Object.defineProperty( target, key, {
configurable: true,
enumerable: !!enumerable,
get () {
console.log( `讀取 ${key} 屬性` ); // 額外
return value;
},
set ( newVal ) {
console.log( `設(shè)置 ${key} 屬性為: ${newVal}` ); // 額外
value = reactify( newVal );
}
} );
}
vue響應(yīng)式 reactify 實現(xiàn)
// 將對象 o 響應(yīng)式化
function reactify( o, vm ) {
let keys = Object.keys( o );
for ( let i = 0; i < keys.length; i++ ) {
let key = keys[ i ]; // 屬性名
let value = o[ key ];
if ( Array.isArray( value ) ) {
// 數(shù)組
value.__proto__ = array_methods; // 數(shù)組就響應(yīng)式了
for ( let j = 0; j < value.length; j++ ) {
reactify( value[ j ], vm ); // 遞歸
}
} else {
// 對象或值類型
defineReactive.call( vm, o, key, value, true );
}
}
}
為什么訪問data屬性不需要帶data
vue中訪問屬性代理this.data.xxx 轉(zhuǎn)換 this.xxx的實現(xiàn)
/** 將 某一個對象的屬性 訪問 映射到 對象的某一個屬性成員上 */
function proxy( target, prop, key ) {
Object.defineProperty( target, key, {
enumerable: true,
configurable: true,
get () {
return target[ prop ][ key ];
},
set ( newVal ) {
target[ prop ][ key ] = newVal;
}
} );
}
原文地址
https://juejin.cn/post/6921911974611664903]
