本文分享12題高頻vue原理面試題,涵蓋了vue 核心實現原理,其實一個框架的實現原理一篇文章是不可能說完的,希望通過這12道問題,讓讀者對自己的Vue 掌握程度有一定的認識(B 數),從而彌補自己的不足,更好的掌握Vue。
【相關推薦:vue面試題#(2021)】
1. Vue 響應式原則
#核心實作類別:
Observer : 它的作用是為物件的屬性添加getter 和setter,用於依賴收集和派發更新
Dep : 用於收集當前響應式物件的依賴關係,每個響應式物件包括子物件都擁有一個Dep 實例(裡面subs 是Watcher 實例數組),當資料有變更時,會透過dep.notify()通知各個watcher。
Watcher : 觀察者物件, 實例分為渲染watcher (render watcher),計算屬性watcher (computed watcher),偵聽器watcher(user watcher)三種
Watcher 和Dep 的關係
watcher 中實例化了dep 並向dep.subs 中新增了訂閱者,dep 透過notify 遍歷了dep.subs 通知每個watcher 更新。
依賴收集
- initState 時,對computed 屬性初始化時,觸發computed watcher 依賴收集
- initState 時,對偵聽屬性初始化時,觸發user watcher依賴收集
- render()的過程,觸發render watcher 依賴收集
- re-render 時,vm.render()再次執行,會移除所有subs 中的watcer 的訂閱,重新賦值。
派發更新
- 元件中對回應的資料進行了修改,觸發setter 的邏輯
- 呼叫dep.notify()
- 遍歷所有的subs(Watcher 實例),呼叫每一個watcher 的update 方法。
原則
當建立Vue 實例時,vue 會遍歷data 選項的屬性,利用Object.defineProperty 為屬性添加getter 和setter 對資料的讀取進行劫持(getter 使用來依賴收集,setter 用來派發更新),並且在內部追蹤依賴,在屬性被存取和修改時通知變化。
每個元件實例會有對應的watcher 實例,會在元件渲染的過程中記錄依賴的所有資料屬性(進行依賴收集,還有computed watcher,user watcher 實例),之後依賴項會改變時,setter 方法會通知依賴與此data 的watcher 實例重新計算(派發更新),從而使它關聯的元件重新渲染。
一句話總結:
vue.js 採用資料劫持結合發布-訂閱模式,透過Object.defineproperty 來劫持各個屬性的setter,getter,在資料變動時發布訊息給訂閱者,觸發回應的監聽回呼
2. computed 的實作原理
computed 本質是一個惰性求值的觀察者。
computed 內部實作了一個惰性的 watcher,也就是 computed watcher,computed watcher 不會立刻求值,同時持有一個 dep 實例。
其內部透過 this.dirty 屬性標記計算屬性是否需要重新求值。
當computed 的依賴狀態改變時,就會通知這個惰性的watcher,
#computed watcher 透過this.dep.subs.length 判斷有沒有訂閱者,
有的話,會重新計算,然後對比新舊值,如果變化了,會重新渲染。 (Vue 想確保不僅僅是計算屬性所依賴的值發生變化,而是當計算屬性最終計算的值發生變化時才會觸發渲染watcher 重新渲染,本質上是一種優化。)
沒有的話,僅僅把this.dirty = true。 (當計算屬性依賴其他資料時,屬性並不會立即重新計算,只有之後其他地方需要讀取屬性的時候,它才會真正計算,即具備lazy(懶計算)特性。 )
3. computed 和watch 有什麼區別及運用場景?
##區別computed 計算屬性: 依賴其它屬性值,並且computed 的值有快取,只有它所依賴的屬性值發生改變,下次取得computed 的值時才會重新計算computed 的值。 watch 偵聽器: 更多的是「觀察」的作用,無緩存性,類似於某些資料的監聽回調,每當監聽的資料變化時都會執行回調進行後續操作。
運用場景運用場景:當我們需要進行數值計算,並且依賴其它資料時,應該使用computed,因為可以利用computed 的快取特性,避免每次取得值時,都要重新計算。當我們需要在資料變更時執行非同步或開銷較大的操作,應該使用watch,使用 watch 選項允許我們執行非同步操作( 存取一個API ),限制我們執行該操作的頻率,並在我們得到最終結果前,設定中間狀態。這些都是計算屬性無法做到的。
4. 為什麼在 Vue3.0 採用了 Proxy,拋棄了 Object.defineProperty?
Object.defineProperty 本身有一定的監控到數組下標變化的能力,但是在Vue 中,從性能/體驗的性價比考慮,尤大大就棄用了這個特性( Vue 為什麼不能偵測陣列變動 )。為了解決這個問題,經過vue 內部處理後可以使用以下幾種方法來監聽數組
push(); pop(); shift(); unshift(); splice(); sort(); reverse();
由於只針對了以上7 種方法進行了hack 處理,所以其他數組的屬性也是檢測不到的,還是具有一定的限制。
Object.defineProperty 只能劫持物件的屬性,因此我們需要對每個物件的每個屬性進行遍歷。 Vue 2.x 裡,是透過 遞歸 遍歷 data 物件來實現對資料的監控的,如果屬性值也是物件那麼需要深度遍歷,顯然如果能劫持一個完整的物件是才是更好的選擇。Proxy 可以劫持整個物件,並回傳一個新的物件。 Proxy 不僅可以代理物件,還可以代理數組。也可以代理動態增加的屬性。
5. Vue 中的 key 到底有什麼用?
key 是給每一個vnode 的唯一id,依靠key,我們的diff 操作可以更準確、更快速(對於簡單列表頁渲染來說diff 節點也更快,但會產生一些隱藏的副作用,例如可能不會產生過渡效果,或者在某些節點有綁定資料(表單)狀態,會出現狀態錯位。)
diff 演算法的過程中,先會進行新舊節點的首尾交叉對比,當無法匹配的時候會用新節點的key 與舊節點進行比對,從而找到相應舊節點.
更準確: 因為帶key 就不是就地復用了,在sameNode 函數 a.key === b.key 對比中可以避免就地重複使用的情況。所以會更準確,如果不加 key,會導致之前節點的狀態被保留下來,會產生一系列的 bug。
更快速: key 的唯一性可以被Map 資料結構充分利用,相比於遍歷查找的時間複雜度O(n),Map 的時間複雜度僅為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 執行是單執行緒的,它是基於事件循環的。事件循環大致分為以下步驟:
- 所有同步任務都在主執行緒上執行,形成執行堆疊(execution context stack)。
- 主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了運行結果,就在"任務隊列"之中放置一個事件。
- 一旦"執行堆疊"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
- 主執行緒不斷重複上面的第三步。
主執行緒的執行過程就是一個 tick,而所有的非同步結果都是透過 「任務佇列」 來調度。訊息佇列中存放的是一個個的任務(task)。規範中規定 task 分為兩大類,分別是 macro task 和 micro task,並且每個 macro task 結束後,都要清空所有的 micro task。
for (macroTask of macroTaskQueue) { // 1. Handle current MACRO-TASK handleMacroTask(); // 2. Handle all MICRO-TASK for (microTask of microTaskQueue) { handleMicroTask(microTask); } }
在瀏覽器環境中:
常見的macro task 有setTimeout、MessageChannel、postMessage、setImmediate
#常見的micro task 有MutationObsever 和Promise.then
非同步更新佇列
可能你還沒注意到,Vue 在更新DOM 時是異步執行的。只要偵聽到資料變化,Vue 將開啟一個佇列,並緩衝在同一事件循環中發生的所有資料變更。
如果同一個 watcher 被多次觸發,只會被推入到佇列中一次。這種在緩衝時去除重複資料對於避免不必要的計算和 DOM 操作是非常重要的。
然後,在下一個的事件循環「tick」中,Vue 刷新佇列並執行實際 (已去重的) 工作。
Vue 在內部對非同步佇列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,如果執行環境不支持,則會採用 setTimeout(fn, 0) 代替。
在 vue2.5 的原始碼中,macrotask 降級的方案依序是:setImmediate、MessageChannel、setTimeout
vue 的 nextTick 方法的实现原理:
- vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行
- microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
- 考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案
7. vue 是如何对数组方法进行变异的 ?
我们先来看看源码
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 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update
8. Vue 组件 data 为什么必须是函数 ?
new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?
因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。
所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。
9. 谈谈 Vue 事件机制,手写$on,$off,$emit,$once
Vue 事件机制 本质上就是 一个 发布-订阅 模式的实现。
class Vue { constructor() { // 事件通道调度中心 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 的渲染过程
- 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
- parse 函数解析 template,生成 ast(抽象语法树)
- optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)
- generate 函数生成 render 函数字符串
- 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
- 调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素
11. 聊聊 keep-alive 的实现原理和缓存策略
export default { name: "keep-alive", abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 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() { // 监听缓存/不缓存组件 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; // 获取键,优先获取组件的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 的组件实例,并且重新调整了 key 的顺序放在了最后一个 if (cache[key]) { vnode.componentInstance = cache[key].componentInstance; // make current key freshest remove(keys, key); keys.push(key); } // 不命中缓存,把 vnode 设置进缓存 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 包裹着的第一个子组件对象及其组件名
- 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
- 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
- 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)
- 最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说
LRU 缓存淘汰算法
LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]
12. vm.$set()实现原理是什么?
受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。
由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。
那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?
export function set(target: Array<any> | Object, key: any, val: any): any { // target 为数组 if (Array.isArray(target) && isValidArrayIndex(key)) { // 修改数组的长度, 避免索引>数组长度导致splice()执行有误 target.length = Math.max(target.length, key); // 利用数组的splice变异方法触发响应式 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创建一个全新的属性 // 获取Observer实例 const ob = (target: any).__ob__; // target 本身就不是响应式数据, 直接赋值 if (!ob) { target[key] = val; return val; } // 进行响应式处理 defineReactive(ob.value, key, val); ob.dep.notify(); return val; }
- 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
- 如果目标是对象,判断属性存在,即为响应式,直接赋值
- 如果 target 本身就不是响应式,直接赋值
- 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理
本文转载自:https://segmentfault.com/a/1190000021407782
推荐教程:《JavaScript视频教程》
以上是12個vue高頻原理面試題(附分析)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。1.Python以简洁语法和丰富库生态著称,适用于数据分析和Web开发。2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。

JavaScript不需要安裝,因為它已內置於現代瀏覽器中。你只需文本編輯器和瀏覽器即可開始使用。 1)在瀏覽器環境中,通過標籤嵌入HTML文件中運行。 2)在Node.js環境中,下載並安裝Node.js後,通過命令行運行JavaScript文件。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

MinGW - Minimalist GNU for Windows
這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

SAP NetWeaver Server Adapter for Eclipse
將Eclipse與SAP NetWeaver應用伺服器整合。

記事本++7.3.1
好用且免費的程式碼編輯器

Dreamweaver Mac版
視覺化網頁開發工具

SublimeText3 Linux新版
SublimeText3 Linux最新版