vue openlayer单击地图事件循环多次执行_12道vue高频原理面试题,你能答出几道?
前言
本文分享 12 道 vue 高頻原理面試題,覆蓋了 vue 核心實現(xiàn)原理,其實一個框架的實現(xiàn)原理一篇文章是不可能說完的,希望通過這 12 道問題,讓讀者對自己的 Vue 掌握程度有一定的認識(B 數(shù)),從而彌補自己的不足,更好的掌握 Vue ??
1. Vue 響應(yīng)式原理
vue-reactive核心實現(xiàn)類:
Observer : 它的作用是給對象的屬性添加 getter 和 setter,用于依賴收集和派發(fā)更新
Dep : 用于收集當前響應(yīng)式對象的依賴關(guān)系,每個響應(yīng)式對象包括子對象都擁有一個 Dep 實例(里面 subs 是 Watcher 實例數(shù)組),當數(shù)據(jù)有變更時,會通過 dep.notify()通知各個 watcher。
Watcher : 觀察者對象 , 實例分為渲染 watcher (render watcher),計算屬性 watcher (computed watcher),偵聽器 watcher(user watcher)三種
Watcher 和 Dep 的關(guān)系
watcher 中實例化了 dep 并向 dep.subs 中添加了訂閱者,dep 通過 notify 遍歷了 dep.subs 通知每個 watcher 更新。
依賴收集
initState 時,對 computed 屬性初始化時,觸發(fā) computed watcher 依賴收集
initState 時,對偵聽屬性初始化時,觸發(fā) user watcher 依賴收集
render()的過程,觸發(fā) render watcher 依賴收集
re-render 時,vm.render()再次執(zhí)行,會移除所有 subs 中的 watcer 的訂閱,重新賦值。
派發(fā)更新
組件中對響應(yīng)的數(shù)據(jù)進行了修改,觸發(fā) setter 的邏輯
調(diào)用 dep.notify()
遍歷所有的 subs(Watcher 實例),調(diào)用每一個 watcher 的 update 方法。
原理
當創(chuàng)建 Vue 實例時,vue 會遍歷 data 選項的屬性,利用 Object.defineProperty 為屬性添加 getter 和 setter 對數(shù)據(jù)的讀取進行劫持(getter 用來依賴收集,setter 用來派發(fā)更新),并且在內(nèi)部追蹤依賴,在屬性被訪問和修改時通知變化。
每個組件實例會有相應(yīng)的 watcher 實例,會在組件渲染的過程中記錄依賴的所有數(shù)據(jù)屬性(進行依賴收集,還有 computed watcher,user watcher 實例),之后依賴項被改動時,setter 方法會通知依賴與此 data 的 watcher 實例重新計算(派發(fā)更新),從而使它關(guān)聯(lián)的組件重新渲染。
一句話總結(jié):
vue.js 采用數(shù)據(jù)劫持結(jié)合發(fā)布-訂閱模式,通過 Object.defineproperty 來劫持各個屬性的 setter,getter,在數(shù)據(jù)變動時發(fā)布消息給訂閱者,觸發(fā)響應(yīng)的監(jiān)聽回調(diào)
2. computed 的實現(xiàn)原理
computed 本質(zhì)是一個惰性求值的觀察者。
computed 內(nèi)部實現(xiàn)了一個惰性的 watcher,也就是 computed watcher,computed watcher 不會立刻求值,同時持有一個 dep 實例。
其內(nèi)部通過 this.dirty 屬性標記計算屬性是否需要重新求值。
當 computed 的依賴狀態(tài)發(fā)生改變時,就會通知這個惰性的 watcher,
computed watcher 通過 this.dep.subs.length 判斷有沒有訂閱者,
有的話,會重新計算,然后對比新舊值,如果變化了,會重新渲染。(Vue 想確保不僅僅是計算屬性依賴的值發(fā)生變化,而是當計算屬性最終計算的值發(fā)生變化時才會觸發(fā)渲染 watcher 重新渲染,本質(zhì)上是一種優(yōu)化。)
沒有的話,僅僅把 this.dirty = true。(當計算屬性依賴于其他數(shù)據(jù)時,屬性并不會立即重新計算,只有之后其他地方需要讀取屬性的時候,它才會真正計算,即具備 lazy(懶計算)特性。)
3. computed 和 watch 有什么區(qū)別及運用場景?
區(qū)別
computed 計算屬性 : 依賴其它屬性值,并且 computed 的值有緩存,只有它依賴的屬性值發(fā)生改變,下一次獲取 computed 的值時才會重新計算 computed 的值。
watch 偵聽器 : 更多的是「觀察」的作用,無緩存性,類似于某些數(shù)據(jù)的監(jiān)聽回調(diào),每當監(jiān)聽的數(shù)據(jù)變化時都會執(zhí)行回調(diào)進行后續(xù)操作。
運用場景
運用場景:
當我們需要進行數(shù)值計算,并且依賴于其它數(shù)據(jù)時,應(yīng)該使用 computed,因為可以利用 computed 的緩存特性,避免每次獲取值時,都要重新計算。
當我們需要在數(shù)據(jù)變化時執(zhí)行異步或開銷較大的操作時,應(yīng)該使用 watch,使用 watch 選項允許我們執(zhí)行異步操作 ( 訪問一個 API ),限制我們執(zhí)行該操作的頻率,并在我們得到最終結(jié)果前,設(shè)置中間狀態(tài)。這些都是計算屬性無法做到的。
4. 為什么在 Vue3.0 采用了 Proxy,拋棄了 Object.defineProperty?
Object.defineProperty 本身有一定的監(jiān)控到數(shù)組下標變化的能力,但是在 Vue 中,從性能/體驗的性價比考慮,尤大大就棄用了這個特性(Vue 為什么不能檢測數(shù)組變動 )。為了解決這個問題,經(jīng)過 vue 內(nèi)部處理后可以使用以下幾種方法來監(jiān)聽數(shù)組
push();
pop();
shift();
unshift();
splice();
sort();
reverse();
由于只針對了以上 7 種方法進行了 hack 處理,所以其他數(shù)組的屬性也是檢測不到的,還是具有一定的局限性。
Object.defineProperty 只能劫持對象的屬性,因此我們需要對每個對象的每個屬性進行遍歷。Vue 2.x 里,是通過 遞歸 + 遍歷 data 對象來實現(xiàn)對數(shù)據(jù)的監(jiān)控的,如果屬性值也是對象那么需要深度遍歷,顯然如果能劫持一個完整的對象是才是更好的選擇。
Proxy 可以劫持整個對象,并返回一個新的對象。Proxy 不僅可以代理對象,還可以代理數(shù)組。還可以代理動態(tài)增加的屬性。
5. Vue 中的 key 到底有什么用?
key 是給每一個 vnode 的唯一 id,依靠 key,我們的 diff 操作可以更準確、更快速 (對于簡單列表頁渲染來說 diff 節(jié)點也更快,但會產(chǎn)生一些隱藏的副作用,比如可能不會產(chǎn)生過渡效果,或者在某些節(jié)點有綁定數(shù)據(jù)(表單)狀態(tài),會出現(xiàn)狀態(tài)錯位。)
diff 算法的過程中,先會進行新舊節(jié)點的首尾交叉對比,當無法匹配的時候會用新節(jié)點的 key 與舊節(jié)點進行比對,從而找到相應(yīng)舊節(jié)點.
更準確 : 因為帶 key 就不是就地復(fù)用了,在 sameNode 函數(shù) a.key === b.key 對比中可以避免就地復(fù)用的情況。所以會更加準確,如果不加 key,會導致之前節(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;
}
6. 談一談 nextTick 的原理
JS 運行機制
JS 執(zhí)行是單線程的,它是基于事件循環(huán)的。事件循環(huán)大致分為以下幾個步驟:
所有同步任務(wù)都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。
主線程之外,還存在一個"任務(wù)隊列"(task queue)。只要異步任務(wù)有了運行結(jié)果,就在"任務(wù)隊列"之中放置一個事件。
一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會讀取"任務(wù)隊列",看看里面有哪些事件。那些對應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進入執(zhí)行棧,開始執(zhí)行。
主線程不斷重復(fù)上面的第三步。
主線程的執(zhí)行過程就是一個 tick,而所有的異步結(jié)果都是通過 “任務(wù)隊列” 來調(diào)度。消息隊列中存放的是一個個的任務(wù)(task)。規(guī)范中規(guī)定 task 分為兩大類,分別是 macro task 和 micro task,并且每個 macro task 結(jié)束后,都要清空所有的 micro task。
for?(macroTask?of?macroTaskQueue)?{
??//?1.?Handle?current?MACRO-TASK
??handleMacroTask();
??//?2.?Handle?all?MICRO-TASK
??for?(microTask?of?microTaskQueue)?{
????handleMicroTask(microTask);
??}
}
在瀏覽器環(huán)境中 :
常見的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate
常見的 micro task 有 MutationObsever 和 Promise.then
異步更新隊列
可能你還沒有注意到,Vue 在更新 DOM 時是異步執(zhí)行的。只要偵聽到數(shù)據(jù)變化,Vue 將開啟一個隊列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更。
如果同一個 watcher 被多次觸發(fā),只會被推入到隊列中一次。這種在緩沖時去除重復(fù)數(shù)據(jù)對于避免不必要的計算和 DOM 操作是非常重要的。
然后,在下一個的事件循環(huán)“tick”中,Vue 刷新隊列并執(zhí)行實際 (已去重的) 工作。
Vue 在內(nèi)部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,如果執(zhí)行環(huán)境不支持,則會采用 setTimeout(fn, 0) 代替。
在 vue2.5 的源碼中,macrotask 降級的方案依次是:setImmediate、MessageChannel、setTimeout
vue 的 nextTick 方法的實現(xiàn)原理:
vue 用異步隊列的方式來控制 DOM 更新和 nextTick 回調(diào)先后執(zhí)行
microtask 因為其高優(yōu)先級特性,能確保隊列中的微任務(wù)在一次事件循環(huán)前被執(zhí)行完畢
microtask 因為其高優(yōu)先級特性,能確保隊列中的微任務(wù)在一次事件循環(huán)前被執(zhí)行完畢
考慮兼容問題,vue 做了 microtask 向 macrotask 的降級方案
7. vue 是如何對數(shù)組方法進行變異的 ?
我們先來看看源碼
const?arrayProto?=?Array.prototype;
export?const?arrayMethods?=?Object.create(arrayProto);
const?methodsToPatch?=?[
??"push",
??"pop",
??"shift",
??"unshift",
??"splice",
??"sort",
??"reverse"
];
/**?*?Intercept?mutating?methods?and?emit?events?*/
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;
??});
});
/**?*?Observe?a?list?of?Array?items.?*/
Observer.prototype.observeArray?=?function?observeArray(items)?{
??for?(var?i?=?0,?l?=?items.length;?i?<?l;?i++)?{
????observe(items[i]);
??}
};
簡單來說,Vue 通過原型攔截的方式重寫了數(shù)組的 7 個方法,首先獲取到這個數(shù)組的ob,也就是它的 Observer 對象,如果有新的值,就調(diào)用 observeArray 對新的值進行監(jiān)聽,然后手動調(diào)用 notify,通知 render watcher,執(zhí)行 update
8. Vue 組件 data 為什么必須是函數(shù) ?
new Vue()實例中,data 可以直接是一個對象,為什么在 vue 組件中,data 必須是一個函數(shù)呢?
因為組件是可以復(fù)用的,JS 里對象是引用關(guān)系,如果組件 data 是一個對象,那么子組件中的 data 屬性值會互相污染,產(chǎn)生副作用。
所以一個組件的 data 選項必須是一個函數(shù),因此每個實例可以維護一份被返回對象的獨立的拷貝。new Vue 的實例是不會被復(fù)用的,因此不存在以上問題。
9. 談?wù)?Vue 事件機制,手寫$on,$off,$emit,$once
Vue 事件機制 本質(zhì)上就是 一個 發(fā)布-訂閱 模式的實現(xiàn)。
class?Vue?{
??constructor()?{
????//??事件通道調(diào)度中心
????this._events?=?Object.create(null);
??}
??$on(event,?fn)?{
????if?(Array.isArray(event))?{
??????event.map(item?=>?{
????????this.$on(item,?fn);
??????});
????}?else?{
??????(this._events[event]?||?(this._events[event]?=?[])).push(fn);
????}
????return?this;
??}
??$once(event,?fn)?{
????function?on()?{
??????this.$off(event,?on);
??????fn.apply(this,?arguments);
????}
????on.fn?=?fn;
????this.$on(event,?on);
????return?this;
??}
??$off(event,?fn)?{
????if?(!arguments.length)?{
??????this._events?=?Object.create(null);
??????return?this;
????}
????if?(Array.isArray(event))?{
??????event.map(item?=>?{
????????this.$off(item,?fn);
??????});
??????return?this;
????}
????const?cbs?=?this._events[event];
????if?(!cbs)?{
??????return?this;
????}
????if?(!fn)?{
??????this._events[event]?=?null;
??????return?this;
????}
????let?cb;
????let?i?=?cbs.length;
????while?(i--)?{
??????cb?=?cbs[i];
??????if?(cb?===?fn?||?cb.fn?===?fn)?{
????????cbs.splice(i,?1);
????????break;
??????}
????}
????return?this;
??}
??$emit(event)?{
????let?cbs?=?this._events[event];
????if?(cbs)?{
??????const?args?=?[].slice.call(arguments,?1);
??????cbs.map(item?=>?{
????????args???item.apply(this,?args)?:?item.call(this);
??????});
????}
????return?this;
??}
}
10. 說說 Vue 的渲染過程
render調(diào)用 compile 函數(shù),生成 render 函數(shù)字符串 ,編譯過程如下:
parse 函數(shù)解析 template,生成 ast(抽象語法樹)
optimize 函數(shù)優(yōu)化靜態(tài)節(jié)點 (標記不需要每次都更新的內(nèi)容,diff 算法會直接跳過靜態(tài)節(jié)點,從而減少比較的過程,優(yōu)化了 patch 的性能)
generate 函數(shù)生成 render 函數(shù)字符串
調(diào)用 new Watcher 函數(shù),監(jiān)聽數(shù)據(jù)的變化,當數(shù)據(jù)發(fā)生變化時,Render 函數(shù)執(zhí)行生成 vnode 對象
調(diào)用 patch 方法,對比新舊 vnode 對象,通過 DOM diff 算法,添加、修改、刪除真正的 DOM 元素
11. 聊聊 keep-alive 的實現(xiàn)原理和緩存策略
export?default?{
??name:?"keep-alive",
??abstract:?true,?//?抽象組件屬性?,它在組件實例建立父子關(guān)系的時候會被忽略,發(fā)生在?initLifecycle?的過程中
??props:?{
????include:?patternTypes,?//?被緩存組件
????exclude:?patternTypes,?//?不被緩存組件
????max:?[String,?Number]?//?指定緩存大小
??},
??created()?{
????this.cache?=?Object.create(null);?//?緩存
????this.keys?=?[];?//?緩存的VNode的鍵
??},
??destroyed()?{
????for?(const?key?in?this.cache)?{
??????//?刪除所有緩存
??????pruneCacheEntry(this.cache,?key,?this.keys);
????}
??},
??mounted()?{
????//?監(jiān)聽緩存/不緩存組件
????this.$watch("include",?val?=>?{
??????pruneCache(this,?name?=>?matches(val,?name));
????});
????this.$watch("exclude",?val?=>?{
??????pruneCache(this,?name?=>?!matches(val,?name));
????});
??},
??render()?{
????//?獲取第一個子元素的?vnode
????const?slot?=?this.$slots.default;
????const?vnode:?VNode?=?getFirstComponentChild(slot);
????const?componentOptions:??VNodeComponentOptions?=
??????vnode?&&?vnode.componentOptions;
????if?(componentOptions)?{
??????//?name不在inlcude中或者在exlude中?直接返回vnode
??????//?check?pattern
??????const?name:??string?=?getComponentName(componentOptions);
??????const?{?include,?exclude?}?=?this;
??????if?(
????????//?not?included
????????(include?&&?(!name?||?!matches(include,?name)))?||
????????//?excluded
????????(exclude?&&?name?&&?matches(exclude,?name))
??????)?{
????????return?vnode;
??????}
??????const?{?cache,?keys?}?=?this;
??????//?獲取鍵,優(yōu)先獲取組件的name字段,否則是組件的tag
??????const?key:??string?=
????????vnode.key?==?null
????????????//?same?constructor?may?get?registered?as?different?local?components
????????????//?so?cid?alone?is?not?enough?(#3269)
????????????componentOptions.Ctor.cid?+
????????????(componentOptions.tag???`::${componentOptions.tag}`?:?"")
??????????:?vnode.key;
??????//?命中緩存,直接從緩存拿vnode?的組件實例,并且重新調(diào)整了?key?的順序放在了最后一個
??????if?(cache[key])?{
????????vnode.componentInstance?=?cache[key].componentInstance;
????????//?make?current?key?freshest
????????remove(keys,?key);
????????keys.push(key);
??????}
??????//?不命中緩存,把?vnode?設(shè)置進緩存
??????else?{
????????cache[key]?=?vnode;
????????keys.push(key);
????????//?prune?oldest?entry
????????//?如果配置了?max?并且緩存的長度超過了?this.max,還要從緩存中刪除第一個
????????if?(this.max?&&?keys.length?>?parseInt(this.max))?{
??????????pruneCacheEntry(cache,?keys[0],?keys,?this._vnode);
????????}
??????}
??????//?keepAlive標記位
??????vnode.data.keepAlive?=?true;
????}
????return?vnode?||?(slot?&&?slot[0]);
??}
};
原理
獲取 keep-alive 包裹著的第一個子組件對象及其組件名
根據(jù)設(shè)定的 include/exclude(如果有)進行條件匹配,決定是否緩存。不匹配,直接返回組件實例
根據(jù)組件 ID 和 tag 生成緩存 Key,并在緩存對象中查找是否已緩存過該組件實例。如果存在,直接取出緩存值并更新該 key 在 this.keys 中的位置(更新 key 的位置是實現(xiàn) LRU 置換策略的關(guān)鍵)
在 this.cache 對象中存儲該組件實例并保存 key 值,之后檢查緩存的實例數(shù)量是否超過 max 的設(shè)置值,超過則根據(jù) LRU 置換策略刪除最近最久未使用的實例(即是下標為 0 的那個 key)
最后組件實例的 keepAlive 屬性設(shè)置為 true,這個在渲染和執(zhí)行被包裹組件的鉤子函數(shù)會用到,這里不細說
LRU 緩存淘汰算法
LRU(Least recently used)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進行淘汰數(shù)據(jù),其核心思想是“如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高”。
LRUkeep-alive 的實現(xiàn)正是用到了 LRU 策略,將最近訪問的組件 push 到 this.keys 最后面,this.keys[0]也就是最久沒被訪問的組件,當緩存實例超過 max 設(shè)置值,刪除 this.keys[0]
12. vm.$set()實現(xiàn)原理是什么?
受現(xiàn)代 JavaScript 的限制 (而且 Object.observe 也已經(jīng)被廢棄),Vue 無法檢測到對象屬性的添加或刪除。
由于 Vue 會在初始化實例時對屬性執(zhí)行 getter/setter 轉(zhuǎn)化,所以屬性必須在 data 對象上存在才能讓 Vue 將它轉(zhuǎn)換為響應(yīng)式的。
對于已經(jīng)創(chuàng)建的實例,Vue 不允許動態(tài)添加根級別的響應(yīng)式屬性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套對象添加響應(yīng)式屬性。
那么 Vue 內(nèi)部是如何解決對象新增屬性不能響應(yīng)的問題的呢?
export?function?set(target:?Array<any>?|?Object,?key:?any,?val:?any):?any?{
??//?target?為數(shù)組
??if?(Array.isArray(target)?&&?isValidArrayIndex(key))?{
????//?修改數(shù)組的長度,?避免索引>數(shù)組長度導致splcie()執(zhí)行有誤
????target.length?=?Math.max(target.length,?key);
????//?利用數(shù)組的splice變異方法觸發(fā)響應(yīng)式
????target.splice(key,?1,?val);
????return?val;
??}
??//?target為對象,?key在target或者target.prototype上?且必須不能在?Object.prototype?上,直接賦值
??if?(key?in?target?&&?!(key?in?Object.prototype))?{
????target[key]?=?val;
????return?val;
??}
??//?以上都不成立,?即開始給target創(chuàng)建一個全新的屬性
??//?獲取Observer實例
??const?ob?=?(target:?any).__ob__;
??//?target?本身就不是響應(yīng)式數(shù)據(jù),?直接賦值
??if?(!ob)?{
????target[key]?=?val;
????return?val;
??}
??//?進行響應(yīng)式處理
??defineReactive(ob.value,?key,?val);
??ob.dep.notify();
??return?val;
}
如果目標是數(shù)組,使用 vue 實現(xiàn)的變異方法 splice 實現(xiàn)響應(yīng)式
如果目標是對象,判斷屬性存在,即為響應(yīng)式,直接賦值
如果 target 本身就不是響應(yīng)式,直接賦值
如果屬性不是響應(yīng)式,則調(diào)用 defineReactive 方法進行響應(yīng)式處理
完
?? 看完三件事
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容
關(guān)注公眾號【全棧前端精選】,不定期分享原創(chuàng)&精品技術(shù)文章。
公眾號內(nèi)回復(fù):【 1 】。加入全棧前端精選公眾號交流群。
覺得文章不錯可以分享到朋友圈讓更多的小伙伴看到哦~
客官!在看一下唄總結(jié)
以上是生活随笔為你收集整理的vue openlayer单击地图事件循环多次执行_12道vue高频原理面试题,你能答出几道?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 记账式国债可以提前支取吗?该如何计息?
- 下一篇: 4299元起 荣耀70 Pro+手机发布