背景
今天早上在脈脈上看到一個關於BN
的前端二面分享,作者出於純粹的目的分享了一下最近的面試題。
我覺得這是一套不錯的面試題,於是分享給了大家。
為什麼會有這套面試題
前端界,到底什麼樣子的專案,會用到這型別的面試題背後蘊含的知識?
我有幸從0 - 1
參與過幾個專案,例如:
- 桌面端IM專案(Electron、React、Node.js),端到端加密,主打20萬人群聊功能
- 幾個大的SAAS系統(React)
- 小程式(Taro)
- 混合APP
- 微信公眾號
- 一些web3專案(流動池幾千萬,solidity React TypeScript Node.js)
等等..
裡面有些需要一定技術深度背後蘊含的知識有:
- 通訊,基於TCP的端到端加密長連結通訊,
- 安全,使用者隱私,安全,像Telegram一樣的方向
- 效能:資料量大的處理與展示,前端任務排程,re-render控制等
- 設計模式的理解與實踐和麵向物件程式設計:例如單例模式,控制反轉,依賴注入
- 對react和Vue關鍵節點原始碼的閱讀與理解
- 對ES6非同步實現的理解
- 瀏覽器的渲染原理
- Node.js
- Linux、docker、K8s、nginx等基礎運維知識
等等...這裡不展開是因為寫這篇文章時候中午還沒吃飯。很餓,況且大部分人根本用不到其他冷門的知識
假設一個場景
例如每秒同時有兩個人給你發訊息,你的客戶端(前端)是不需要做任務排程。
假如每秒同時有一千個人給你發很多訊息,這個時候就要做任務排程了,因為這裡面涉及到網路層、DB層、快取層(前端記憶體,例如redux等),以及資料流向、更新頻次與時機控制。
交易,同理。例如一個幣價一秒鐘內波動劇烈,由於是IM場景,雙工通訊,可能一秒你接收到多次推送。這個頻次如果根據使用者實際場景拆解做精細化,是一個極度複雜的需求。這裡就不展開講了
那麼這個時候,你就會用到我在上面提到的大部分知識,在做效能優化的時候,當你的知識足夠全面豐富,其實更像是在下棋,子落後不可反悔。有利有弊
隨著網際網路的推進,我認為前端會越來越像是一個完整的客戶端,現在有webContainer技術和webasm等技術,只要谷歌解決native-socket和安全的一些關鍵節點問題,就是完整的客戶端。不再需要Electron之類的
大概講講題目
1.React的時間切片思想
可以結合我三年前文章 手寫mini-react原始碼看看
https://github.com/JinJieTan/Peter-/tree/master/mini-React
- 先看看
cpu
排程時間片
時間片即CPU分配給各個程式的時間,每個執行緒被分配一個時間段,稱作它的時間片,即該程式允許執行的時間,使各個程式從表面上看是同時進行的。如果在時間片結束時程式還在執行,則CPU將被剝奪並分配給另一個程式。如果程式在時間片結束前阻塞或結束,則CPU當即進行切換。而不會造成CPU資源浪費。在巨集觀上:我們可以同時開啟多個應用程式,每個程式並行不悖,同時執行。但在微觀上:由於只有一個CPU,一次只能處理程式要求的一部分,如何處理公平,一種方法就是引入時間片,每個程式輪流執行
- 那麼react的時間切片思想是什麼呢?
兩年前,我們公司一個專案從react0.14版本升級上來react16,記得當時給公司一些同事科普過一次。react16引入了fiber,其實這個時間切片思想,就是react16的fiber。
當時react0.14版本的專案有一個問題,就是會出現卡頓,因為react16版本之前,是一口氣完成更新。如果這個過程很長,就會導致等待(卡頓)的時間很長
react16版本後,react更新,會有一個Reconcilation階段,這個階段是會遍歷虛擬dom樹,找出更新的節點,完成一系列操作。這個階段計算比較多,就會長時間佔用cpu.而這個Reconcilation階段是可以中斷的(暫時掛起),讓瀏覽器先響應高優先順序事件,例如使用者互動等。這就是所謂的時間切片思想,本質上是任務排程
- 2.為什麼不用
requestIdleCallback
在程式碼裡面我有備註過,我測試過requestIdleCallback
,當時我在做1秒鐘1000個人頻繁發訊息的效能優化,就在結合手寫react做任務排程。
原因是:requestIdleCallback的相容性不好,對於使用者互動頻繁多次合併更新來說,requestAnimation更有及時性高優先順序,requestIdleCallback則適合處理可以延遲渲染的任務
我們可以發現,很多優化思想,來自於對作業系統本身的認知,對事物的本身認知決定了發展的天花板。
useMemo之類的原理和優化原理
背後使用了Object.js方法遍歷淺對比了傳入的dependencys的prev和current值。
使用簡單的比較,省去不必要的render
react的副作用
比較籠統的問題,這個問題我就不回答了
vue的nextTick
vue2有一個優雅降級的過程
- 先是promise.then
- 而後是MutationObserver
- 然後是setImmediate
最後是setTimeout
let timerFunc // nextTick非同步實現fn if (typeof Promise !== 'undefined' && isNative(Promise)) { // Promise方案 const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // 將flushCallbacks包裝進Promise.then中 } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // MutationObserver方案 let counter = 1 const observer = new MutationObserver(flushCallbacks) // 將flushCallbacks作為觀測變化的cb const textNode = document.createTextNode(String(counter)) // 建立文字節點 // 觀測文字節點變化 observer.observe(textNode, { characterData: true }) // timerFunc改變文字節點的data,以觸發觀測的回撥flushCallbacks timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // setImmediate方案 timerFunc = () => { setImmediate(flushCallbacks) } } else { // 最終降級方案setTimeout timerFunc = () => { setTimeout(flushCallbacks, 0) } }
出這個問題,是想知道面試者對Vue框架的資料更新 - 渲染非同步是否真的理解,並非只是這個nextTick而已。
剩下的巨集任務和微任務,可以跟第六題一起回答。
什麼是控制反轉和依賴注入
出這個題目,說明面試官比較崇尚這種風格模式,不然不會問這個特殊問題,但是要注意的是,既然問了這方面的,肯定會擴充發散,問你實際的使用和其他設計模式等。所以背面試題,對於稍微上點檔次的面試,是不靠譜的。
我個人反對背面試題,更看重過往專案經驗和基礎知識掌握與實踐思考
- 控制反轉(IoC):
在單一職責原則的設計下,很少有單獨一個物件就能完成的任務。大多數任務都需要複數的物件來協作完成,這樣物件與物件之間就有了依賴。一開始物件之間的依賴關係是自己解決的,需要什麼物件了就New一個出來用,控制權是在物件本身。但是這樣耦合度就非常高,可能某個物件的一點小修改就會引起連鎖反應,需要把依賴的物件一路修改過去。
經典的控制反轉(IoC)原則:
上層模組不應該依賴於下層模組,他們共同依賴於一個抽象,抽象不能夠依賴於具體 ,具體必須依賴於抽象。
放在TypeScript中,上面這句話可以理解為,多個class遵循一個interface,這些class的對應資料值不同,但是欄位和型別都是一樣的。
當需要被單獨、組合使用時,直接使用這些class即可
控制反轉此時的好處:如果後面要更新進化,只要新的interface相容現有的interface即可,不需要改動現有class程式碼去做相容。這涉及到Ts的協變和逆變,感興趣的去了解下
- 依賴注入(DI—Dependency Injection):
把物件之間的依賴關係提到外部去管理,可是還如果元件之間依賴關係由容器在執行期決定,形象的說,即由容器動態的將某個依賴關係注入到元件之中
例如react的Context,使用Context.Provider注入資料
例如裝飾器
@Foo()
智慧合約內部也有修飾器,例如access control
裡面的
modifier onlyOnwer(){
require(msg.sender == onwer,'msg.sender not onwer');
__;
}
function _mint () public onlyOnwer(){
//dosomething
}
依賴注入,本質上幫助簡化組裝依賴過程。
asyncpool實現
前端併發控制的庫 asyncpol
ES7實現版本
async function asyncPool(poolLimit, array, iteratorFn) {
const ret = []; // 儲存所有的非同步任務
const executing = []; // 儲存正在執行的非同步任務
for (const item of array) {
// 呼叫iteratorFn函式建立非同步任務
const p = Promise.resolve().then(() => iteratorFn(item, array));
ret.push(p); // 儲存新的非同步任務
// 當poolLimit值小於或等於總任務個數時,進行併發控制
if (poolLimit <= array.length) {
// 當任務完成後,從正在執行的任務陣列中移除已完成的任務
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e); // 儲存正在執行的非同步任務
if (executing.length >= poolLimit) {
await Promise.race(executing); // 等待較快的任務執行完成
}
}
}
return Promise.all(ret);
}
ES6實現版本:
function asyncPool(poolLimit, array, iteratorFn) {
let i = 0;
const ret = []; // 儲存所有的非同步任務
const executing = []; // 儲存正在執行的非同步任務
const enqueue = function () {
if (i === array.length) {
return Promise.resolve();
}
const item = array[i++]; // 獲取新的任務項
const p = Promise.resolve().then(() => iteratorFn(item, array));
ret.push(p);
let r = Promise.resolve();
// 當poolLimit值小於或等於總任務個數時,進行併發控制
if (poolLimit <= array.length) {
// 當任務完成後,從正在執行的任務陣列中移除已完成的任務
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
r = Promise.race(executing);
}
}
// 正在執行任務列表 中較快的任務執行完成之後,才會從array陣列中獲取新的待辦任務
return r.then(() => enqueue());
};
return enqueue().then(() => Promise.all(ret));
}
總結
面試題出得比較貼近實際,看中對框架原理和前端非同步以及基礎的考察,這些知識點跟框架開發中複雜功能的debug息息相關。學習原始碼是必不可少的進階過程,有可能當時學了沒用,但是真的理解精髓以後你會發現,大部分優秀的框架原始碼都差不多,包括他們的使用,思路和理念等,原始碼最重要的是幫助你在未來做複雜場景需求debug時使用。
當然,這些都是基於我很久沒有更新的前端知識的認知基礎寫的,如果有問題,歡迎你指出。
寫於2022年5月31日
一個寫智慧合約的web2.5軟體工程師
如果感覺寫得不錯,可以點個贊,幫忙關注下公眾號:前端巔峰