全球頂級交易所前端二面

Peter譚老師發表於2022-06-02

背景

今天早上在脈脈上看到一個關於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軟體工程師

如果感覺寫得不錯,可以點個贊,幫忙關注下公眾號:前端巔峰

相關文章