1. 程式人生 > 實用技巧 >前端大廠面試

前端大廠面試

前端大廠面試

CSS

1. 盒模型

頁面渲染時,dom 元素所採用的佈局模型。可通過box-sizing進行設定。根據計算寬高的區域可分為:

  • content-box(W3C 標準盒模型)
  • border-box(IE 盒模型)
  • padding-box
  • margin-box(瀏覽器未實現)

2. BFC

塊級格式化上下文,是一個獨立的渲染區域,讓處於 BFC 內部的元素與外部的元素相互隔離,使內外元素的定位不會相互影響。

IE下為 Layout,可通過 zoom:1 觸發

  • 觸發條件:
    • 根元素
    • position: absolute/fixed
    • display: inline-block / table
    • float元素
    • ovevflow!==visible
    • 規則:
      • 屬於同一個 BFC 的兩個相鄰 Box 垂直排列
      • 屬於同一個 BFC 的兩個相鄰 Box 的 margin 會發生重疊
      • BFC 中子元素的 margin box 的左邊, 與包含塊 (BFC) border box的左邊相接觸 (子元素 absolute 除外)
      • BFC 的區域不會與 float 的元素區域重疊
      • 計算 BFC 的高度時,浮動子元素也參與計算
      • 文字層不會被浮動層覆蓋,環繞於周圍
      • 應用:
        • 阻止margin重疊
        • 可以包含浮動元素 —— 清除內部浮動(清除浮動的原理是兩個div都位於同一個 BFC 區域之中)
        • 自適應兩欄佈局
        • 可以阻止元素被浮動元素覆蓋

3.層疊上下文

元素提升為一個比較特殊的圖層,在三維空間中(z軸)高出普通元素一等。

  • 觸發條件
    • 根層疊上下文(html)
    • position
    • css3屬性
      • flex
      • transform
      • opacity
      • filter
      • will-change
      • -webkit-overflow-scrolling
      • 層疊等級:層疊上下文在z軸上的排序
        • 在同一層疊上下文中,層疊等級才有意義
        • z-index的優先順序最高

4. 居中佈局

  • 水平居中
    • 行內元素:text-align: center
    • 塊級元素:margin: 0 auto
    • absolute + transform
    • flex + justify-content: center
    • 垂直居中
      • line-height: height
      • absolute + transform
      • flex + align-items: center
      • table
      • 水平垂直居中
        • absolute + transform
        • flex + justify-content + align-items

5. 選擇器優先順序

  • !important> 行內樣式 >#id>.class>tag> * > 繼承 > 預設
  • 選擇器從右往左解析

6.去除浮動影響,防止父級高度塌陷

  • 通過增加尾元素清除浮動
    • :after / <br> : clear: both
    • 建立父級 BFC
    • 父級設定高度

7.link 與 @import 的區別

  • link功能較多,可以定義 RSS,定義 Rel 等作用,而@import只能用於載入 css
  • 當解析到link時,頁面會同步載入所引的 css,而@import所引用的 css 會等到頁面載入完才被載入
  • @import需要 IE5 以上才能使用
  • link可以使用 js 動態引入,@import不行

8. CSS前處理器(Sass/Less/Postcss)

CSS前處理器的原理: 是將類 CSS 語言通過Webpack 編譯轉成瀏覽器可讀的真正 CSS。在這層編譯之上,便可以賦予 CSS 更多更強大的功能,常用功能:

  • 巢狀
  • 變數
  • 迴圈語句
  • 條件語句
  • 自動字首
  • 單位轉換
  • mixin複用

面試中一般不會重點考察該點,一般介紹下自己在實戰專案中的經驗即可~

9.CSS動畫

  • transition: 過渡動畫
    • transition-property: 屬性
    • transition-duration: 間隔
    • transition-timing-function: 曲線
    • transition-delay: 延遲
    • 常用鉤子:transitionend
    • animation / keyframes
      • animation-name: 動畫名稱,對應@keyframes
      • animation-duration: 間隔
      • animation-timing-function: 曲線
      • animation-delay: 延遲
      • animation-iteration-count: 次數
        • infinite: 迴圈動畫
  • animation-direction: 方向
    • alternate: 反向播放
  • animation-fill-mode: 靜止模式
    • forwards: 停止時,保留最後一幀
    • backwards: 停止時,回到第一幀
    • both: 同時運用forwards / backwards
  • 常用鉤子:animationend
  • 動畫屬性: 儘量使用動畫屬性進行動畫,能擁有較好的效能表現
    • translate
    • scale
    • rotate
    • skew
    • opacity
    • color

經驗

通常,CSS 並不是重點的考察領域,但這其實是由於現在國內業界對 CSS 的專注不夠導致的,真正精通並專注於 CSS 的團隊和人才並不多。因此如果能在 CSS 領域有自己的見解和經驗,反而會為相當的加分和脫穎而出。

JavaScript

1. 原型 / 建構函式 / 例項

  • 原型(prototype): 一個簡單的物件,用於實現物件的屬性繼承。可以簡單的理解成物件的爹。在 Firefox 和 Chrome 中,每個JavaScript物件中都包含一個__proto__(非標準)的屬性指向它爹(該物件的原型),可obj.__proto__進行訪問。
  • 建構函式: 可以通過new來新建一個物件的函式。
  • 例項: 通過建構函式和new創建出來的物件,便是例項。例項通過__proto__指向原型,通過constructor指向建構函式

說了一大堆,大家可能有點懵逼,這裡來舉個栗子,以Object為例,我們常用的Object便是一個建構函式,因此我們可以通過它構建例項。

// 例項

const instance = new Object()

複製程式碼

則此時,例項為instance,建構函式為Object,我們知道,建構函式擁有一個prototype的屬性指向原型,因此原型為:

// 原型

const prototype = Object.prototype

複製程式碼

這裡我們可以來看出三者的關係:

例項.__proto__ === 原型

原型.constructor === 建構函式

建構函式.prototype === 原型

// 這條線其實是是基於原型進行獲取的,可以理解成一條基於原型的對映線

// 例如:

// const o = new Object()

// o.constructor === Object --> true

// o.__proto__ = null;

// o.constructor === Object --> false

例項.constructor === 建構函式

複製程式碼

此處感謝 caihaihong 童鞋的指出。

放大來看,我畫了張圖供大家徹底理解:

2.原型鏈:

原型鏈是由原型物件組成,每個物件都有__proto__屬性,指向了建立該物件的建構函式的原型,__proto__將物件連線起來組成了原型鏈。是一個用來實現繼承和共享屬性的有限的物件鏈。

  • 屬性查詢機制: 當查詢物件的屬性時,如果例項物件自身不存在該屬性,則沿著原型鏈往上一級查詢,找到時則輸出,不存在時,則繼續沿著原型鏈往上一級查詢,直至最頂級的原型物件Object.prototype,如還是沒找到,則輸出undefined;
  • 屬性修改機制: 只會修改例項物件本身的屬性,如果不存在,則進行新增該屬性,如果需要修改原型的屬性時,則可以用:b.prototype.x = 2;但是這樣會造成所有繼承於該物件的例項的屬性發生改變。

3. 執行上下文(EC)

執行上下文可以簡單理解為一個物件:

  • 它包含三個部分:
    • 變數物件(VO)
    • 作用域鏈(詞法作用域)
    • this指向
    • 它的型別:
      • 全域性執行上下文
      • 函式執行上下文
      • eval執行上下文
      • 程式碼執行過程:
        • 建立全域性上下文(global EC)
        • 全域性執行上下文 (caller) 逐行自上而下執行。遇到函式時,函式執行上下文(callee) 被push到執行棧頂層
        • 函式執行上下文被啟用,成為 active EC, 開始執行函式中的程式碼,caller 被掛起
        • 函式執行完後,callee 被pop移除出執行棧,控制權交還全域性上下文 (caller),繼續執行

2.變數物件

變數物件,是執行上下文中的一部分,可以抽象為一種資料作用域,其實也可以理解為就是一個簡單的物件,它儲存著該執行上下文中的所有變數和函式宣告(不包含函式表示式)

活動物件 (AO): 當變數物件所處的上下文為 active EC 時,稱為活動物件。

3. 作用域

執行上下文中還包含作用域鏈。理解作用域之前,先介紹下作用域。作用域其實可理解為該上下文中宣告的變數和宣告的作用範圍。可分為塊級作用域函式作用域

特性:

  • 宣告提前: 一個宣告在函式體內都是可見的, 函式優先於變數
  • 非匿名自執行函式,函式變數為只讀狀態,無法修改

let foo = function() { console.log(1) };

(function foo() {

foo = 10 // 由於foo在函式中只為可讀,因此賦值無效

console.log(foo)

}())

// 結果列印: ƒ foo() { foo = 10 ; console.log(foo) }

複製程式碼

4.作用域鏈

我們知道,我們可以在執行上下文中訪問到父級甚至全域性的變數,這便是作用域鏈的功勞。作用域鏈可以理解為一組物件列表,包含父級和自身的變數物件,因此我們便能通過作用域鏈訪問到父級裡宣告的變數或者函式。

  • 由兩部分組成:
    • [[scope]]屬性: 指向父級變數物件和作用域鏈,也就是包含了父級的[[scope]]和AO
    • AO: 自身活動物件

如此[[scopr]]包含[[scope]],便自上而下形成一條鏈式作用域

5. 閉包

閉包屬於一種特殊的作用域,稱為靜態作用域。它的定義可以理解為:父函式被銷燬的情況下,返回出的子函式的[[scope]]中仍然保留著父級的單變數物件和作用域鏈,因此可以繼續訪問到父級的變數物件,這樣的函式稱為閉包。

  • 閉包會產生一個很經典的問題:
    • 多個子函式的[[scope]]都是同時指向父級,是完全共享的。因此當父級的變數物件被修改時,所有子函式都受到影響。
    • 解決:
      • 變數可以通過函式引數的形式傳入,避免使用預設的[[scope]]向上查詢
      • 使用setTimeout包裹,通過第三個引數傳入
      • 使用塊級作用域,讓變數成為自己上下文的屬性,避免共享

6. script 引入方式:

  • html 靜態<script>引入
  • js 動態插入<script>
  • <script defer>: 延遲載入,元素解析完成後執行
  • <script async>: 非同步載入,但執行時會阻塞元素渲染

7. 物件的拷貝

  • 淺拷貝: 以賦值的形式拷貝引用物件,仍指向同一個地址,修改時原物件也會受到影響
    • Object.assign
    • 展開運算子(...)
    • 深拷貝: 完全拷貝一個新物件,修改時原物件不再受到任何影響
      • JSON.parse(JSON.stringify(obj)): 效能最快
        • 具有迴圈引用的物件時,報錯
        • 當值為函式、undefined、或symbol時,無法拷貝
  • 遞迴進行逐一賦值

8. new運算子的執行過程

  • 新生成一個物件
  • 連結到原型:obj.__proto__ = Con.prototype
  • 繫結this:apply
  • 返回新物件(如果建構函式有自己 retrun 時,則返回該值)

9. instanceof原理

能在例項的原型物件鏈中找到該建構函式的prototype屬性所指向的原型物件,就返回true。即:

// __proto__: 代表原型物件鏈

instance.[__proto__...] === instance.constructor.prototype

// return true

複製程式碼

10. 程式碼的複用

當你發現任何程式碼開始寫第二遍時,就要開始考慮如何複用。一般有以下的方式:

  • 函式封裝
  • 繼承
  • 複製extend
  • 混入mixin
  • 借用apply/call

11. 繼承

在 JS 中,繼承通常指的便是原型鏈繼承,也就是通過指定原型,並可以通過原型鏈繼承原型上的屬性或者方法。

  • 最優化:聖盃模式

var inherit = (function(c,p){

var F = function(){};

return function(c,p){

F.prototype = p.prototype;

c.prototype = new F();

c.uber = p.prototype;

c.prototype.constructor = c;

}

})();

複製程式碼

  • 使用 ES6 的語法糖class / extends

12. 型別轉換

大家都知道 JS 中在使用運算子號或者對比符時,會自帶隱式轉換,規則如下:

  • -、*、/、% :一律轉換成數值後計算
  • +:
    • 數字 + 字串 = 字串, 運算順序是從左到右
    • 數字 + 物件, 優先呼叫物件的valueOf->toString
    • 數字 +boolean/null-> 數字
    • 數字 +undefined->NaN
    • [1].toString() === '1'
    • {}.toString() === '[object object]'
    • NaN!==NaN、+undefined 為 NaN

13. 型別判斷

判斷 Target 的型別,單單用 typeof 並無法完全滿足,這其實並不是 bug,本質原因是 JS 的萬物皆物件的理論。因此要真正完美判斷時,我們需要區分對待:

  • 基本型別(null): 使用String(null)
  • 基本型別(string / number / boolean / undefined) +function: 直接使用typeof即可
  • 其餘引用型別(Array / Date / RegExp Error): 呼叫toString後根據[object XXX]進行判斷

很穩的判斷封裝:

let class2type = {}

'Array Date RegExp Object Error'.split(' ').forEach(e => class2type[ '[object ' + e + ']' ] = e.toLowerCase())

function type(obj) {

if (obj == null) return String(obj)

return typeof obj === 'object' ? class2type[ Object.prototype.toString.call(obj) ] || 'object' : typeof obj

}

複製程式碼

14. 模組化

模組化開發在現代開發中已是必不可少的一部分,它大大提高了專案的可維護、可拓展和可協作性。通常,我們在瀏覽器中使用 ES6 的模組化支援,在 Node 中使用 commonjs 的模組化支援。

  • 分類:
    • es6:import / export
    • commonjs:require / module.exports / exports
    • amd:require / defined
    • require與import的區別
      • require支援動態匯入,import不支援,正在提案 (babel 下可支援)
      • require是同步匯入,import屬於非同步匯入
      • require是值拷貝,匯出值變化不會影響匯入值;import指向記憶體地址,匯入值會隨匯出值而變化

15. 防抖與節流

防抖與節流函式是一種最常用的高頻觸發優化方式,能對效能有較大的幫助。

  • 防抖 (debounce): 將多次高頻操作優化為只在最後一次執行,通常使用的場景是:使用者輸入,只需再輸入完成後做一次輸入校驗即可。

function debounce(fn, wait, immediate) {

let timer = null

return function() {

let args = arguments

let context = this

if (immediate && !timer) {

fn.apply(context, args)

}

if (timer) clearTimeout(timer)

timer = setTimeout(() => {

fn.apply(context, args)

}, wait)

}

}

複製程式碼

  • 節流(throttle): 每隔一段時間後執行一次,也就是降低頻率,將高頻操作優化成低頻操作,通常使用場景: 滾動條事件 或者 resize 事件,通常每隔 100~500 ms執行一次即可。

function throttle(fn, wait, immediate) {

let timer = null

let callNow = immediate

return function() {

let context = this,

args = arguments

if (callNow) {

fn.apply(context, args)

callNow = false

}

if (!timer) {

timer = setTimeout(() => {

fn.apply(context, args)

timer = null

}, wait)

}

}

}

複製程式碼

16. 函式執行改變this

由於 JS 的設計原理: 在函式中,可以引用執行環境中的變數。因此就需要一個機制來讓我們可以在函式體內部獲取當前的執行環境,這便是this。

因此要明白this指向,其實就是要搞清楚 函式的執行環境,說人話就是,誰呼叫了函式。例如:

  • obj.fn(),便是obj呼叫了函式,既函式中的this === obj
  • fn(),這裡可以看成window.fn(),因此this === window

但這種機制並不完全能滿足我們的業務需求,因此提供了三種方式可以手動修改this的指向:

  • call: fn.call(target, 1, 2)
  • apply: fn.apply(target, [1, 2])
  • bind: fn.bind(target)(1,2)

17. ES6/ES7

由於 Babel 的強大和普及,現在 ES6/ES7 基本上已經是現代化開發的必備了。通過新的語法糖,能讓程式碼整體更為簡潔和易讀。

  • 宣告
    • let / const: 塊級作用域、不存在變數提升、暫時性死區、不允許重複宣告
    • const: 宣告常量,無法修改
    • 解構賦值
    • class / extend: 類宣告與繼承
    • Set / Map: 新的資料結構
    • 非同步解決方案:
      • Promise的使用與實現
      • generator:
        • yield: 暫停程式碼
        • next(): 繼續執行程式碼
        • function* helloWorld() {
        • yield 'hello';
        • yield 'world';
        • return 'ending';
        • }
        • const generator = helloWorld();
        • generator.next() // { value: 'hello', done: false }
        • generator.next() // { value: 'world', done: false }
        • generator.next() // { value: 'ending', done: true }
        • generator.next() // { value: undefined, done: true }

複製程式碼

  • await / async: 是generator的語法糖, babel中是基於promise實現。

async function getUserByAsync(){

let user = await fetchUser();

return user;

}

const user = await getUserByAsync()

console.log(user)

複製程式碼

18. AST

抽象語法樹 (Abstract Syntax Tree),是將程式碼逐字母解析成樹狀物件的形式。這是語言之間的轉換、程式碼語法檢查,程式碼風格檢查,程式碼格式化,程式碼高亮,程式碼錯誤提示,程式碼自動補全等等的基礎。例如:

function square(n){

return n * n

}

複製程式碼

通過解析轉化成的AST如下圖:

19. babel編譯原理

  • babylon 將 ES6/ES7 程式碼解析成 AST
  • babel-traverse 對 AST 進行遍歷轉譯,得到新的 AST
  • 新 AST 通過 babel-generator 轉換成 ES5

20. 函式柯里化

在一個函式中,首先填充幾個引數,然後再返回一個新的函式的技術,稱為函式的柯里化。通常可用於在不侵入函式的前提下,為函式預置通用引數,供多次重複呼叫。

const add = function add(x) {

return function (y) {

return x + y

}

}

const add1 = add(1)

add1(2) === 3

add1(20) === 21

複製程式碼

21. 陣列(array)

  • map: 遍歷陣列,返回回撥返回值組成的新陣列
  • forEach: 無法break,可以用try/catch中throw new Error來停止
  • filter: 過濾
  • some: 有一項返回true,則整體為true
  • every: 有一項返回false,則整體為false
  • join: 通過指定連線符生成字串
  • push / pop: 末尾推入和彈出,改變原陣列, 返回推入/彈出項
  • unshift / shift: 頭部推入和彈出,改變原陣列,返回操作項
  • sort(fn) / reverse: 排序與反轉,改變原陣列
  • concat: 連線陣列,不影響原陣列, 淺拷貝
  • slice(start, end): 返回截斷後的新陣列,不改變原陣列
  • splice(start, number, value...): 返回刪除元素組成的陣列,value 為插入項,改變原陣列
  • indexOf / lastIndexOf(value, fromIndex): 查詢陣列項,返回對應的下標
  • reduce / reduceRight(fn(prev, cur), defaultPrev): 兩兩執行,prev 為上次化簡函式的return值,cur 為當前值(從第二項開始)
  • 陣列亂序:

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

arr.sort(function () {

return Math.random() - 0.5;

});

複製程式碼

  • 陣列拆解: flat: [1,[2,3]] --> [1, 2, 3]

Array.prototype.flat = function() {

return this.toString().split(',').map(item => +item )

}

複製程式碼

瀏覽器

1. 跨標籤頁通訊

不同標籤頁間的通訊,本質原理就是去運用一些可以共享的中間介質,因此比較常用的有以下方法:

  • 通過父頁面window.open()和子頁面postMessage
    • 非同步下,通過window.open('about: blank')和tab.location.href = '*'
    • 設定同域下共享的localStorage與監聽window.onstorage
      • 重複寫入相同的值無法觸發
      • 會受到瀏覽器隱身模式等的限制
      • 設定共享cookie與不斷輪詢髒檢查(setInterval)
      • 藉助服務端或者中間層實現

2. 瀏覽器架構

  • 使用者介面
  • 主程序
  • 核心
    • 渲染引擎
    • JS 引擎
      • 執行棧
  • 事件觸發執行緒
    • 訊息佇列
      • 微任務
      • 巨集任務
  • 網路非同步執行緒
  • 定時器執行緒

3. 瀏覽器下事件迴圈(Event Loop)

事件迴圈是指: 執行一個巨集任務,然後執行清空微任務列表,迴圈再執行巨集任務,再清微任務列表

  • 微任務microtask(jobs):promise / ajax / Object.observe(該方法已廢棄)
  • 巨集任務macrotask(task):setTimout / script / IO / UI Rendering

4. 從輸入 url 到展示的過程

  • DNS 解析
  • TCP 三次握手
  • 傳送請求,分析 url,設定請求報文(頭,主體)
  • 伺服器返回請求的檔案 (html)
  • 瀏覽器渲染
    • HTML parser --> DOM Tree
      • 標記化演算法,進行元素狀態的標記
      • dom 樹構建
  • CSS parser --> Style Tree
    • 解析 css 程式碼,生成樣式樹
  • attachment --> Render Tree
    • 結合 dom樹 與 style樹,生成渲染樹
  • layout: 佈局
  • GPU painting: 畫素繪製頁面

5. 重繪與迴流

當元素的樣式發生變化時,瀏覽器需要觸發更新,重新繪製元素。這個過程中,有兩種型別的操作,即重繪與迴流。

  • 重繪(repaint): 當元素樣式的改變不影響佈局時,瀏覽器將使用重繪對元素進行更新,此時由於只需要UI層面的重新畫素繪製,因此損耗較少
  • 迴流(reflow): 當元素的尺寸、結構或觸發某些屬性時,瀏覽器會重新渲染頁面,稱為迴流。此時,瀏覽器需要重新經過計算,計算後還需要重新頁面佈局,因此是較重的操作。會觸發迴流的操作:
    • 頁面初次渲染
    • 瀏覽器視窗大小改變
    • 元素尺寸、位置、內容發生改變
    • 元素字型大小變化
    • 新增或者刪除可見的 dom 元素
    • 啟用 CSS 偽類(例如::hover)
    • 查詢某些屬性或呼叫某些方法
      • clientWidth、clientHeight、clientTop、clientLeft
      • offsetWidth、offsetHeight、offsetTop、offsetLeft
      • scrollWidth、scrollHeight、scrollTop、scrollLeft
      • getComputedStyle()
      • getBoundingClientRect()
      • scrollTo()

迴流必定觸發重繪,重繪不一定觸發迴流。重繪的開銷較小,迴流的代價較高。

最佳實踐:

  • css
    • 避免使用table佈局
    • 將動畫效果應用到position屬性為absolute或fixed的元素上
    • javascript
      • 避免頻繁操作樣式,可彙總後統一一次修改
      • 儘量使用class進行樣式修改
      • 減少dom的增刪次數,可使用字串或者documentFragment一次性插入
      • 極限優化時,修改樣式可將其display: none後修改
      • 避免多次觸發上面提到的那些會觸發迴流的方法,可以的話儘量用變數存住

6. 儲存

我們經常需要對業務中的一些資料進行儲存,通常可以分為 短暫性儲存 和 永續性儲存。

  • 短暫性的時候,我們只需要將資料存在記憶體中,只在執行時可用
  • 永續性儲存,可以分為 瀏覽器端 與 伺服器端
    • 瀏覽器:
      • cookie: 通常用於儲存使用者身份,登入狀態等
        • http 中自動攜帶, 體積上限為 4K, 可自行設定過期時間
        • localStorage / sessionStorage: 長久儲存/視窗關閉刪除, 體積限制為 4~5M
        • indexDB
  • 伺服器:
    • 分散式快取 redis
    • 資料庫

7. Web Worker

現代瀏覽器為JavaScript創造的多執行緒環境。可以新建並將部分任務分配到worker執行緒並行執行,兩個執行緒可獨立執行,互不干擾,可通過自帶的訊息機制相互通訊。

基本用法:

// 建立 worker

const worker = new Worker('work.js');

// 向主程序推送訊息

worker.postMessage('Hello World');

// 監聽主程序來的訊息

worker.onmessage = function (event) {

console.log('Received message ' + event.data);

}

複製程式碼

限制:

  • 同源限制
  • 無法使用document/window/alert/confirm
  • 無法載入本地資源

8. V8垃圾回收機制

垃圾回收: 將記憶體中不再使用的資料進行清理,釋放出記憶體空間。V8 將記憶體分成新生代空間老生代空間

  • 新生代空間: 用於存活較短的物件
    • 又分成兩個空間: from 空間 與 to 空間
    • Scavenge GC演算法: 當 from 空間被佔滿時,啟動 GC 演算法
      • 存活的物件從 from space 轉移到 to space
      • 清空 from space
      • from space 與 to space 互換
      • 完成一次新生代GC
      • 老生代空間: 用於存活時間較長的物件
        • 從 新生代空間 轉移到 老生代空間 的條件
          • 經歷過一次以上 Scavenge GC 的物件
          • 當 to space 體積超過25%
  • 標記清除演算法: 標記存活的物件,未被標記的則被釋放
    • 增量標記: 小模組標記,在程式碼執行間隙執,GC 會影響效能
    • 併發標記(最新技術): 不阻塞 js 執行
  • 壓縮演算法: 將記憶體中清除後導致的碎片化物件往記憶體堆的一端移動,解決記憶體的碎片化

9. 記憶體洩露

  • 意外的全域性變數: 無法被回收
  • 定時器: 未被正確關閉,導致所引用的外部變數無法被釋放
  • 事件監聽: 沒有正確銷燬 (低版本瀏覽器可能出現)
  • 閉包: 會導致父級中的變數無法被釋放
  • dom 引用: dom 元素被刪除時,記憶體中的引用未被正確清空

可用 chrome 中的 timeline 進行記憶體標記,視覺化檢視記憶體的變化情況,找出異常點。

服務端與網路

1. http/https 協議

  • 1.0 協議缺陷:
    • 無法複用連結,完成即斷開,重新慢啟動和 TCP 3次握手
    • head of line blocking:線頭阻塞,導致請求之間互相影響
    • 1.1 改進:
      • 長連線(預設 keep-alive),複用
      • host 欄位指定對應的虛擬站點
      • 新增功能:
        • 斷點續傳
        • 身份認證
        • 狀態管理
        • cache 快取
          • Cache-Control
          • Expires
          • Last-Modified
          • Etag
          • 2.0:
            • 多路複用
            • 二進位制分幀層: 應用層和傳輸層之間
            • 首部壓縮
            • 服務端推送
            • https: 較為安全的網路傳輸協議
              • 證書(公鑰)
              • SSL 加密
              • 埠 443
              • TCP:
                • 三次握手
                • 四次揮手
                • 滑動視窗: 流量控制
                • 擁塞處理
                  • 慢開始
                  • 擁塞避免
                  • 快速重傳
                  • 快速恢復
                  • 快取策略: 可分為強快取協商快取
                    • Cache-Control/Expires: 瀏覽器判斷快取是否過期,未過期時,直接使用強快取,Cache-Control的 max-age 優先順序高於 Expires
                    • 當快取已經過期時,使用協商快取
                      • 唯一標識方案: Etag(response 攜帶) & If-None-Match(request攜帶,上一次返回的 Etag): 伺服器判斷資源是否被修改,
                      • 最後一次修改時間: Last-Modified(response) & If-Modified-Since (request,上一次返回的Last-Modified)
                        • 如果一致,則直接返回 304 通知瀏覽器使用快取
                        • 如不一致,則服務端返回新的資源
  • Last-Modified 缺點:
    • 週期性修改,但內容未變時,會導致快取失效
    • 最小粒度只到 s, s 以內的改動無法檢測到
  • Etag 的優先順序高於 Last-Modified

2. 常見狀態碼

  • 1xx: 接受,繼續處理
  • 200: 成功,並返回資料
  • 201: 已建立
  • 202: 已接受
  • 203: 成為,但未授權
  • 204: 成功,無內容
  • 205: 成功,重置內容
  • 206: 成功,部分內容
  • 301: 永久移動,重定向
  • 302: 臨時移動,可使用原有URI
  • 304: 資源未修改,可使用快取
  • 305: 需代理訪問
  • 400: 請求語法錯誤
  • 401: 要求身份認證
  • 403: 拒絕請求
  • 404: 資源不存在
  • 500: 伺服器錯誤

3. get / post

  • get: 快取、請求長度受限、會被歷史儲存記錄
    • 無副作用(不修改資源),冪等(請求次數與資源無關)的場景
    • post: 安全、大資料、更多編碼型別

兩者詳細對比如下圖:

4. Websocket

Websocket 是一個持久化的協議, 基於 http , 服務端可以主動 push

  • 相容:
    • FLASH Socket
    • 長輪詢: 定時傳送 ajax
    • long poll: 傳送 --> 有訊息時再 response
    • new WebSocket(url)
    • ws.onerror = fn
    • ws.onclose = fn
    • ws.onopen = fn
    • ws.onmessage = fn
    • ws.send()

5. TCP三次握手

建立連線前,客戶端和服務端需要通過握手來確認對方:

  • 客戶端傳送 syn(同步序列編號) 請求,進入 syn_send 狀態,等待確認
  • 服務端接收並確認 syn 包後傳送 syn+ack 包,進入 syn_recv 狀態
  • 客戶端接收 syn+ack 包後,傳送 ack 包,雙方進入 established 狀態

6. TCP四次揮手

  • 客戶端 -- FIN --> 服務端, FIN—WAIT
  • 服務端 -- ACK --> 客戶端, CLOSE-WAIT
  • 服務端 -- ACK,FIN --> 客戶端, LAST-ACK
  • 客戶端 -- ACK --> 服務端,CLOSED

7. Node 的 Event Loop: 6個階段

  • timer 階段: 執行到期的setTimeout / setInterval佇列回撥
  • I/O 階段: 執行上輪迴圈殘流的callback
  • idle, prepare
  • poll: 等待回撥
  1. 執行回撥
  1. 執行定時器
  • 如有到期的setTimeout / setInterval, 則返回 timer 階段
  • 如有setImmediate,則前往 check 階段
  • check
    • 執行setImmediate
    • close callbacks

跨域

  • JSONP: 利用<script>標籤不受跨域限制的特點,缺點是隻能支援 get 請求

function jsonp(url, jsonpCallback, success) {

const script = document.createElement('script')

script.src = url

script.async = true

script.type = 'text/javascript'

window[jsonpCallback] = function(data) {

success && success(data)

}

document.body.appendChild(script)

}

複製程式碼

  • 設定 CORS: Access-Control-Allow-Origin:*
  • postMessage

安全

  • XSS攻擊: 注入惡意程式碼
    • cookie 設定 httpOnly
    • 轉義頁面上的輸入內容和輸出內容
    • CSRF: 跨站請求偽造,防護:
      • get 不修改資料
      • 不被第三方網站訪問到使用者的 cookie
      • 設定白名單,不被第三方網站請求
      • 請求校驗

框架:Vue

1. nextTick

在下次dom更新迴圈結束之後執行延遲迴調,可用於獲取更新後的dom狀態

  • 新版本中預設是microtasks,v-on中會使用macrotasks
  • macrotasks任務的實現:
    • setImmediate / MessageChannel / setTimeout

2. 生命週期

  • _init_
    • initLifecycle/Event,往vm上掛載各種屬性
    • callHook: beforeCreated: 例項剛建立
    • initInjection/initState: 初始化注入和 data 響應性
    • created: 建立完成,屬性已經繫結, 但還未生成真實dom
    • 進行元素的掛載:$el / vm.$mount()
    • 是否有template: 解析成render function
      • *.vue檔案:vue-loader會將<template>編譯成render function
  • beforeMount: 模板編譯/掛載之前
  • 執行render function,生成真實的dom,並替換到dom tree中
  • mounted: 元件已掛載
  • update:
    • 執行diff演算法,比對改變是否需要觸發UI更新
    • flushScheduleQueue
      • watcher.before: 觸發beforeUpdate鉤子 -watcher.run(): 執行watcher中的notify,通知所有依賴項更新UI
  • 觸發updated鉤子: 元件已更新
  • actived / deactivated(keep-alive): 不銷燬,快取,元件啟用與失活
  • destroy:
    • beforeDestroy: 銷燬開始
    • 銷燬自身且遞迴銷燬子元件以及事件監聽
      • remove(): 刪除節點
      • watcher.teardown(): 清空依賴
      • vm.$off(): 解綁監聽
  • destroyed: 完成後觸發鉤子

上面是vue的宣告週期的簡單梳理,接下來我們直接以程式碼的形式來完成vue的初始化

new Vue({})

// 初始化Vue例項

function _init() {

// 掛載屬性

initLifeCycle(vm)

// 初始化事件系統,鉤子函式等

initEvent(vm)

// 編譯slot、vnode

initRender(vm)

// 觸發鉤子

callHook(vm, 'beforeCreate')

// 新增inject功能

initInjection(vm)

// 完成資料響應性 props/data/watch/computed/methods

initState(vm)

// 新增 provide 功能

initProvide(vm)

// 觸發鉤子

callHook(vm, 'created')

// 掛載節點

if (vm.$options.el) {

vm.$mount(vm.$options.el)

}

}

// 掛載節點實現

function mountComponent(vm) {

// 獲取 render function

if (!this.options.render) {

// template to render

// Vue.compile = compileToFunctions

let { render } = compileToFunctions()

this.options.render = render

}

// 觸發鉤子

callHook('beforeMounte')

// 初始化觀察者

// render 渲染 vdom,

vdom = vm.render()

// update: 根據 diff 出的 patchs 掛載成真實的 dom

vm._update(vdom)

// 觸發鉤子

callHook(vm, 'mounted')

}

// 更新節點實現

funtion queueWatcher(watcher) {

nextTick(flushScheduleQueue)

}

// 清空佇列

function flushScheduleQueue() {

// 遍歷佇列中所有修改

for(){

// beforeUpdate

watcher.before()

// 依賴區域性更新節點

watcher.update()

callHook('updated')

}

}

// 銷燬例項實現

Vue.prototype.$destory = function() {

// 觸發鉤子

callHook(vm, 'beforeDestory')

// 自身及子節點

remove()

// 刪除依賴

watcher.teardown()

// 刪除監聽

vm.$off()

// 觸發鉤子

callHook(vm, 'destoryed')

}

複製程式碼

3. 資料響應(資料劫持)

看完生命週期後,裡面的watcher等內容其實是資料響應中的一部分。資料響應的實現由兩部分構成:觀察者( watcher )依賴收集器( Dep ),其核心是defineProperty這個方法,它可以重寫屬性的 get 與 set方法,從而完成監聽資料的改變。

  • Observe (觀察者)觀察 props 與 state
    • 遍歷 props 與 state,對每個屬性建立獨立的監聽器( watcher )
    • 使用defineProperty重寫每個屬性的 get/set(defineReactive)
      • get: 收集依賴
        • Dep.depend()
          • watcher.addDep()
  • set: 派發更新
    • Dep.notify()
    • watcher.update()
    • queenWatcher()
    • nextTick
    • flushScheduleQueue
    • watcher.run()
    • updateComponent()

大家可以先看下面的資料相應的程式碼實現後,理解後就比較容易看懂上面的簡單脈絡了。

let data = {a: 1}

// 資料響應性

observe(data)

// 初始化觀察者

new Watcher(data, 'name', updateComponent)

data.a = 2

// 簡單表示用於資料更新後的操作

function updateComponent() {

vm._update() // patchs

}

// 監視物件

function observe(obj) {

// 遍歷物件,使用 get/set 重新定義物件的每個屬性值

Object.keys(obj).map(key => {

defineReactive(obj, key, obj[key])

})

}

function defineReactive(obj, k, v) {

// 遞迴子屬性

if (type(v) == 'object') observe(v)

// 新建依賴收集器

let dep = new Dep()

// 定義get/set

Object.defineProperty(obj, k, {

enumerable: true,

configurable: true,

get: function reactiveGetter() {

// 當有獲取該屬性時,證明依賴於該物件,因此被新增進收集器中

if (Dep.target) {

dep.addSub(Dep.target)

}

return v

},

// 重新設定值時,觸發收集器的通知機制

set: function reactiveSetter(nV) {

v = nV

dep.nofify()

},

})

}

// 依賴收集器

class Dep {

constructor() {

this.subs = []

}

addSub(sub) {

this.subs.push(sub)

}

notify() {

this.subs.map(sub => {

sub.update()

})

}

}

Dep.target = null

// 觀察者

class Watcher {

constructor(obj, key, cb) {

Dep.target = this

this.cb = cb

this.obj = obj

this.key = key

this.value = obj[key]

Dep.target = null

}

addDep(Dep) {

Dep.addSub(this)

}

update() {

this.value = this.obj[this.key]

this.cb(this.value)

}

before() {

callHook('beforeUpdate')

}

}

複製程式碼

4. virtual dom 原理實現

  • 建立 dom 樹
  • 樹的diff,同層對比,輸出patchs(listDiff/diffChildren/diffProps)
    • 沒有新的節點,返回
    • 新的節點tagName與key不變, 對比props,繼續遞迴遍歷子樹
      • 對比屬性(對比新舊屬性列表):
        • 舊屬性是否存在與新屬性列表中
        • 都存在的是否有變化
        • 是否出現舊列表中沒有的新屬性
  • tagName和key值變化了,則直接替換成新節點
  • 渲染差異
    • 遍歷patchs, 把需要更改的節點取出來
    • 區域性更新dom

// diff演算法的實現

function diff(oldTree, newTree) {

// 差異收集

let pathchs = {}

dfs(oldTree, newTree, 0, pathchs)

return pathchs

}

function dfs(oldNode, newNode, index, pathchs) {

let curPathchs = []

if (newNode) {

// 當新舊節點的 tagName 和 key 值完全一致時

if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {

// 繼續比對屬性差異

let props = diffProps(oldNode.props, newNode.props)

curPathchs.push({ type: 'changeProps', props })

// 遞迴進入下一層級的比較

diffChildrens(oldNode.children, newNode.children, index, pathchs)

} else {

// 當 tagName 或者 key 修改了後,表示已經是全新節點,無需再比

curPathchs.push({ type: 'replaceNode', node: newNode })

}

}

// 構建出整顆差異樹

if (curPathchs.length) {

if(pathchs[index]){

pathchs[index] = pathchs[index].concat(curPathchs)

} else {

pathchs[index] = curPathchs

}

}

}

// 屬性對比實現

function diffProps(oldProps, newProps) {

let propsPathchs = []

// 遍歷新舊屬性列表

// 查詢刪除項

// 查詢修改項

// 查詢新增項

forin(olaProps, (k, v) => {

if (!newProps.hasOwnProperty(k)) {

propsPathchs.push({ type: 'remove', prop: k })

} else {

if (v !== newProps[k]) {

propsPathchs.push({ type: 'change', prop: k , value: newProps[k] })

}

}

})

forin(newProps, (k, v) => {

if (!oldProps.hasOwnProperty(k)) {

propsPathchs.push({ type: 'add', prop: k, value: v })

}

})

return propsPathchs

}

// 對比子級差異

function diffChildrens(oldChild, newChild, index, pathchs) {

// 標記子級的刪除/新增/移動

let { change, list } = diffList(oldChild, newChild, index, pathchs)

if (change.length) {

if (pathchs[index]) {

pathchs[index] = pathchs[index].concat(change)

} else {

pathchs[index] = change

}

}

// 根據 key 獲取原本匹配的節點,進一步遞迴從頭開始對比

oldChild.map((item, i) => {

let keyIndex = list.indexOf(item.key)

if (keyIndex) {

let node = newChild[keyIndex]

// 進一步遞迴對比

dfs(item, node, index, pathchs)

}

})

}

// 列表對比,主要也是根據 key 值查詢匹配項

// 對比出新舊列表的新增/刪除/移動

function diffList(oldList, newList, index, pathchs) {

let change = []

let list = []

const newKeys = getKey(newList)

oldList.map(v => {

if (newKeys.indexOf(v.key) > -1) {

list.push(v.key)

} else {

list.push(null)

}

})

// 標記刪除

for (let i = list.length - 1; i>= 0; i--) {

if (!list[i]) {

list.splice(i, 1)

change.push({ type: 'remove', index: i })

}

}

// 標記新增和移動

newList.map((item, i) => {

const key = item.key

const index = list.indexOf(key)

if (index === -1 || key == null) {

// 新增

change.push({ type: 'add', node: item, index: i })

list.splice(i, 0, key)

} else {

// 移動

if (index !== i) {

change.push({

type: 'move',

form: index,

to: i,

})

move(list, index, i)

}

}

})

return { change, list }

}

複製程式碼

5. Proxy 相比於 defineProperty 的優勢

  • 陣列變化也能監聽到
  • 不需要深度遍歷監聽

let data = { a: 1 }

let reactiveData = new Proxy(data, {

get: function(target, name){

// ...

},

// ...

})

複製程式碼

6. vue-router

  • mode
    • hash
    • history
    • 跳轉
      • this.$router.push()
      • <router-link to=""></router-link>
      • 佔位
        • <router-view></router-view>

7. vuex

  • state: 狀態中心
  • mutations: 更改狀態
  • actions: 非同步更改狀態
  • getters: 獲取狀態
  • modules: 將state分成多個modules,便於管理

演算法

其實演算法方面在前端的實際專案中涉及得並不多,但還是需要精通一些基礎性的演算法,一些公司還是會有這方面的需求和考核,建議大家還是需要稍微準備下,這屬於加分題。

1. 五大演算法

  • 貪心演算法: 區域性最優解法
  • 分治演算法: 分成多個小模組,與原問題性質相同
  • 動態規劃: 每個狀態都是過去歷史的一個總結
  • 回溯法: 發現原先選擇不優時,退回重新選擇
  • 分支限界法

2. 基礎排序演算法

  • 氣泡排序: 兩兩比較

function bubleSort(arr) {

var len = arr.length;

for (let outer = len ; outer >= 2; outer--) {

for(let inner = 0; inner <=outer - 1; inner++) {

if(arr[inner] > arr[inner + 1]) {

[arr[inner],arr[inner+1]] = [arr[inner+1],arr[inner]]

}

}

}

return arr;

}

複製程式碼

  • 選擇排序: 遍歷自身以後的元素,最小的元素跟自己調換位置

function selectSort(arr) {

var len = arr.length;

for(let i = 0 ;i < len - 1; i++) {

for(let j = i ; j<len; j++) {

if(arr[j] < arr[i]) {

[arr[i],arr[j]] = [arr[j],arr[i]];

}

}

}

return arr

}

複製程式碼

  • 插入排序: 即將元素插入到已排序好的陣列中

function insertSort(arr) {

for(let i = 1; i < arr.length; i++) { //外迴圈從1開始,預設arr[0]是有序段

for(let j = i; j > 0; j--) { //j = i,將arr[j]依次插入有序段中

if(arr[j] < arr[j-1]) {

[arr[j],arr[j-1]] = [arr[j-1],arr[j]];

} else {

break;

}

}

}

return arr;

}

複製程式碼

3. 高階排序演算法

  • 快速排序
    • 選擇基準值(base),原陣列長度減一(基準值),使用 splice
    • 迴圈原陣列,小的放左邊(left陣列),大的放右邊(right陣列);
    • concat(left, base, right)
    • 遞迴繼續排序 left 與 right

function quickSort(arr) {

if(arr.length <= 1) {

return arr; //遞迴出口

}

var left = [],

right = [],

current = arr.splice(0,1);

for(let i = 0; i < arr.length; i++) {

if(arr[i] < current) {

left.push(arr[i]) //放在左邊

} else {

right.push(arr[i]) //放在右邊

}

}

return quickSort(left).concat(current,quickSort(right));

}

複製程式碼

  • 希爾排序:不定步數的插入排序,插入排序
  • 口訣: 插冒歸基穩定,快選堆希不穩定

穩定性: 同大小情況下是否可能會被交換位置, 虛擬dom的diff,不穩定性會導致重新渲染;

4. 遞迴運用(斐波那契數列): 爬樓梯問題

初始在第一級,到第一級有1種方法(s(1) = 1),到第二級也只有一種方法(s(2) = 1), 第三級(s(3) = s(1) + s(2))

function cStairs(n) {

if(n === 1 || n === 2) {

return 1;

} else {

return cStairs(n-1) + cStairs(n-2)

}

}

複製程式碼

5. 資料樹

  • 二叉樹: 最多隻有兩個子節點
    • 完全二叉樹
    • 滿二叉樹
      • 深度為 h, 有 n 個節點,且滿足 n = 2^h - 1
      • 二叉查詢樹: 是一種特殊的二叉樹,能有效地提高查詢效率
        • 小值在左,大值在右
        • 節點 n 的所有左子樹值小於 n,所有右子樹值大於 n

  • 遍歷節點
    • 前序遍歷
  1. 根節點
  1. 訪問左子節點,回到 1
  1. 訪問右子節點,回到 1
  • 中序遍歷
  1. 先訪問到最左的子節點
  1. 訪問該節點的父節點
  1. 訪問該父節點的右子節點, 回到 1
  • 後序遍歷
  1. 先訪問到最左的子節點
  1. 訪問相鄰的右節點
  1. 訪問父節點, 回到 1
  • 插入與刪除節點

6. 天平找次品

有n個硬幣,其中1個為假幣,假幣重量較輕,你有一把天平,請問,至少需要稱多少次能保證一定找到假幣?

  • 三等分演算法:
  1. 將硬幣分成3組,隨便取其中兩組天平稱量
  • 平衡,假幣在未上稱的一組,取其回到 1 繼續迴圈
  • 不平衡,假幣在天平上較輕的一組, 取其回到 1 繼續迴圈

進階知識

框架: React

React 也是現如今最流行的前端框架,也是很多大廠面試必備。React 與 Vue 雖有不同,但同樣作為一款 UI 框架,雖然實現可能不一樣,但在一些理念上還是有相似的,例如資料驅動、元件化、虛擬 dom 等。這裡就主要列舉一些 React 中獨有的概念。

1. Fiber

React 的核心流程可以分為兩個部分:

  • reconciliation (排程演算法,也可稱為 render):
    • 更新 state 與 props;
    • 呼叫生命週期鉤子;
    • 生成 virtual dom;
      • 這裡應該稱為 Fiber Tree 更為符合;
    • 通過新舊 vdom 進行 diff 演算法,獲取 vdom change;
    • 確定是否需要重新渲染
  • commit:
    • 如需要,則操作 dom 節點更新;

要了解 Fiber,我們首先來看為什麼需要它?

  • 問題: 隨著應用變得越來越龐大,整個更新渲染的過程開始變得吃力,大量的元件渲染會導致主程序長時間被佔用,導致一些動畫或高頻操作出現卡頓和掉幀的情況。而關鍵點,便是 同步阻塞。在之前的排程演算法中,React 需要例項化每個類元件,生成一顆元件樹,使用 同步遞迴 的方式進行遍歷渲染,而這個過程最大的問題就是無法 暫停和恢復
  • 解決方案: 解決同步阻塞的方法,通常有兩種: 非同步任務分割。而 React Fiber 便是為了實現任務分割而誕生的。
  • 簡述:
  • 核心:
    • 連結串列樹遍歷演算法: 通過 節點儲存與對映,便能夠隨時地進行 停止和重啟,這樣便能達到實現任務分割的基本前提;
    • 任務分割,React 中的渲染更新可以分成兩個階段:
    • 分散執行: 任務分割後,就可以把小任務單元分散到瀏覽器的空閒期間去排隊執行,而實現的關鍵是兩個新API: requestIdleCallbackrequestAnimationFrame
    • 在 React V16 將排程演算法進行了重構, 將之前的 stack reconciler 重構成新版的 fiber reconciler,變成了具有連結串列和指標的 單鏈表樹遍歷演算法。通過指標對映,每個單元都記錄著遍歷當下的上一步與下一步,從而使遍歷變得可以被暫停和重啟。
    • 這裡我理解為是一種 任務分割排程演算法,主要是 將原先同步更新渲染的任務分割成一個個獨立的 小任務單位,根據不同的優先順序,將小任務分散到瀏覽器的空閒時間執行,充分利用主程序的事件迴圈機制。
    • Fiber 這裡可以具象為一個 資料結構:
· class Fiber {
·  constructor(instance) {
·  this.instance = instance
·  // 指向第一個 child 節點
·  this.child = child
·  // 指向父節點
·  this.return = parent
·  // 指向第一個兄弟節點
·  this.sibling = previous
·  } 
· }
複製程式碼
      • 1、首先通過不斷遍歷子節點,到樹末尾;
      • 2、開始通過 sibling 遍歷兄弟節點;
      • 3、return 返回父節點,繼續執行2;
      • 4、直到 root 節點後,跳出遍歷;
      • reconciliation 階段: vdom 的資料對比,是個適合拆分的階段,比如對比一部分樹後,先暫停執行個動畫呼叫,待完成後再回來繼續比對。
      • Commit 階段: 將 change list 更新到 dom 上,並不適合拆分,才能保持資料與 UI 的同步。否則可能由於阻塞 UI 更新,而導致資料更新和 UI 不一致的情況。
      • 低優先順序的任務交給requestIdleCallback處理,這是個瀏覽器提供的事件迴圈空閒期的回撥函式,需要 pollyfill,而且擁有 deadline 引數,限制執行事件,以繼續切分任務;
      • 高優先順序的任務交給requestAnimationFrame處理;
// 類似於這樣的方式
requestIdleCallback((deadline) => {
 // 當有空閒時間時,我們執行一個元件渲染;
 // 把任務塞到一個個碎片時間中去;
 while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {
 nextComponent = performWork(nextComponent);
 }
});
複製程式碼
    • 優先順序策略: 文字框輸入 > 本次排程結束需完成的任務 > 動畫過渡 > 互動反饋 > 資料更新 > 不會顯示但以防將來會顯示的任務

Tips:

Fiber 其實可以算是一種程式設計思想,在其它語言中也有許多應用(Ruby Fiber)。核心思想是 任務拆分和協同,主動把執行權交給主執行緒,使主執行緒有時間空擋處理其他高優先順序任務。

當遇到程序阻塞的問題時,任務分割非同步呼叫快取策略 是三個顯著的解決思路。

感謝 @Pengyuan 童鞋,在評論中指出了幾個 Fiber 中最核心的理念,感恩!!

2. 生命週期

在新版本中,React 官方對生命週期有了新的 變動建議:

  • 使用getDerivedStateFromProps 替換componentWillMount
  • 使用getSnapshotBeforeUpdate替換componentWillUpdate
  • 避免使用componentWillReceiveProps

其實該變動的原因,正是由於上述提到的 Fiber。首先,從上面我們知道 React 可以分成 reconciliation 與 commit 兩個階段,對應的生命週期如下:

  • reconciliation:
  • commit:
    • componentWillMount
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
    • componentDidMount
    • componentDidUpdate
    • componentWillUnmount

在 Fiber 中,reconciliation 階段進行了任務分割,涉及到 暫停 和 重啟,因此可能會導致 reconciliation 中的生命週期函式在一次更新渲染迴圈中被 多次呼叫 的情況,產生一些意外錯誤。

新版的建議生命週期如下:

class Component extends React.Component {
 // 替換 `componentWillReceiveProps` ,
 // 初始化和 update 時被呼叫
 // 靜態函式,無法使用 this
 static getDerivedStateFromProps(nextProps, prevState) {}
 
// 判斷是否需要更新元件
 // 可以用於元件效能優化
 shouldComponentUpdate(nextProps, nextState) {}
 
// 元件被掛載後觸發
 componentDidMount() {}
 
// 替換 componentWillUpdate
 // 可以在更新之前獲取最新 dom 資料
 getSnapshotBeforeUpdate() {}
 
// 元件更新後呼叫
 componentDidUpdate() {}
 
// 元件即將銷燬
 componentWillUnmount() {}
 
// 元件已銷燬
 componentDidUnMount() {}
}
複製程式碼
  • 使用建議:
    • constructor初始化 state;
    • componentDidMount中進行事件監聽,並在componentWillUnmount中解綁事件;
    • componentDidMount中進行資料的請求,而不是在componentWillMount
    • 需要根據 props 更新 state 時,使用getDerivedStateFromProps(nextProps, prevState)
      • 舊 props 需要自己儲存,以便比較;
· public static getDerivedStateFromProps(nextProps, prevState) {
·  // 當新 props 中的 data 發生變化時,同步更新到 state 上
·  if (nextProps.data !== prevState.data) {
·  return {
·  data: nextProps.data
·  }
·  } else {
·  return null1
·  }
· }
複製程式碼
    • 可以在componentDidUpdate監聽 props 或者 state 的變化,例如:
componentDidUpdate(prevProps) {
 // 當 id 發生變化時,重新獲取資料
 if (this.props.id !== prevProps.id) {
 this.fetchData(this.props.id);
 }
}
複製程式碼
    • componentDidUpdate使用setState時,必須加條件,否則將進入死迴圈;
    • getSnapshotBeforeUpdate(prevProps, prevState)可以在更新之前獲取最新的渲染資料,它的呼叫是在 render 之後, update 之前;
    • shouldComponentUpdate: 預設每次呼叫setState,一定會最終走到 diff 階段,但可以通過shouldComponentUpdate的生命鉤子返回false來直接阻止後面的邏輯執行,通常是用於做條件渲染,優化渲染的效能。

3. setState

在瞭解setState之前,我們先來簡單瞭解下 React 一個包裝結構: Transaction:

  • 事務 (Transaction):
    • 是 React 中的一個呼叫結構,用於包裝一個方法,結構為: initialize - perform(method) - close。通過事務,可以統一管理一個方法的開始與結束;處於事務流中,表示程序正在執行一些操作;

  • setState: React 中用於修改狀態,更新檢視。它具有以下特點:
  • 非同步與同步: setState並不是單純的非同步或同步,這其實與呼叫時的環境相關:
  • 批量更新: 在 合成事件生命週期鉤子 中,setState更新佇列時,儲存的是 合併狀態(Object.assign)。因此前面設定的 key 值會被後面所覆蓋,最終只會執行一次更新;
  • 函式式: 由於 Fiber 及 合併 的問題,官方推薦可以傳入 函式 的形式。setState(fn),在fn中返回新的state物件即可,例如this.setState((state, props) => newState);
  • 注意事項:
    • 合成事件生命週期鉤子(除 componentDidUpdate) 中,setState是"非同步"的;
      • 原因: 因為在setState的實現中,有一個判斷: 當更新策略正在事務流的執行中時,該元件更新會被推入dirtyComponents佇列中等待執行;否則,開始執行batchedUpdates佇列更新;
        • 在生命週期鉤子呼叫中,更新策略都處於更新之前,元件仍處於事務流中,而componentDidUpdate是在更新之後,此時元件已經不在事務流中了,因此則會同步執行;
        • 在合成事件中,React 是基於 事務流完成的事件委託機制 實現,也是處於事務流中;
      • 問題: 無法在setState後馬上從this.state上獲取更新後的值。
      • 解決: 如果需要馬上同步去獲取新值,setState其實是可以傳入第二個引數的。setState(updater, callback),在回撥中即可獲取最新值;
    • 原生事件setTimeout 中,setState是同步的,可以馬上獲取更新後的值;
      • 原因: 原生事件是瀏覽器本身的實現,與事務流無關,自然是同步;而setTimeout是放置於定時器執行緒中延後執行,此時事務流已結束,因此也是同步;
    • 使用函式式,可以用於避免setState的批量更新的邏輯,傳入的函式將會被 順序呼叫
    • setState 合併,在 合成事件 和 生命週期鉤子 中多次連續呼叫會被優化為一次;
    • 當元件已被銷燬,如果再次呼叫setState,React 會報錯警告,通常有兩種解決辦法:
      • 將資料掛載到外部,通過 props 傳入,如放到 Redux 或 父級中;
      • 在元件內部維護一個狀態量 (isUnmounted),componentWillUnmount中標記為 true,在setState前進行判斷;

4. HOC(高階元件)

HOC(Higher Order Componennt) 是在 React 機制下社群形成的一種元件模式,在很多第三方開源庫中表現強大。

  • 簡述:
  • 用法:
    • 屬性代理 (Props Proxy): 返回出一個元件,它基於被包裹元件進行 功能增強
    • 高階元件不是元件,是 增強函式,可以輸入一個元元件,返回出一個新的增強元件;
    • 高階元件的主要作用是 程式碼複用操作 狀態和引數;
      • 預設引數: 可以為元件包裹一層預設引數;
o function proxyHoc(Comp) {
o  return class extends React.Component {
o  render() {
o  const newProps = {
o  name: 'tayde',
o  age: 1,
o  }
o  return <Comp {...this.props} {...newProps} />
o  }
o  }
o }
複製程式碼
      • 提取狀態: 可以通過 props 將被包裹元件中的 state 依賴外層,例如用於轉換受控元件:
function withOnChange(Comp) {
 return class extends React.Component {
 constructor(props) {
 super(props)
 this.state = {
 name: '',
 }
 }
 onChangeName = () => {
 this.setState({
 name: 'dongdong',
 })
 }
 render() {
 const newProps = {
 value: this.state.name,
 onChange: this.onChangeName,
 }
  return <Comp {...this.props} {...newProps} />
 }
 }
}
複製程式碼

使用姿勢如下,這樣就能非常快速的將一個 Input 元件轉化成受控元件。

const NameInput = props => (<input name="name" {...props} />)
export default withOnChange(NameInput)
複製程式碼
      • 包裹元件: 可以為被包裹元素進行一層包裝,
function withMask(Comp) {
 return class extends React.Component {
 render() {
  return (
  <div>
  <Comp {...this.props} />
 <div style={{
  width: '100%',
  height: '100%',
  backgroundColor: 'rgba(0, 0, 0, .6)',
  }} 
 </div>
  )
  }
 }
}
複製程式碼
  • 反向繼承 (Inheritance Inversion): 返回出一個元件,繼承於被包裹元件,常用於以下操作:
    • 渲染劫持 (Render Highjacking)
    • 操作狀態 (Operate State): 可以直接通過 this.state 獲取到被包裹元件的狀態,並進行操作。但這樣的操作容易使 state 變得難以追蹤,不易維護,謹慎使用。
  • 應用場景:
  • 使用注意:
o function IIHoc(Comp) {
o return class extends Comp {
o render() {
o return super.render();
o }
o };
o }
複製程式碼
        • 條件渲染: 根據條件,渲染不同的元件
§ function withLoading(Comp) {
§ return class extends Comp {
§ render() {
§ if(this.props.isLoading) {
§ return <Loading />
§ } else {
§ return super.render()
§ }
§ }
§ };
§ }
複製程式碼
        • 可以直接修改被包裹元件渲染出的 React 元素樹
    • 許可權控制,通過抽象邏輯,統一對頁面進行許可權判斷,按不同的條件進行頁面渲染:
· function withAdminAuth(WrappedComponent) {
· return class extends React.Component {
·  constructor(props){
·  super(props)
·  this.state = {
·   isAdmin: false,
·  }
·  } 
·  async componentWillMount() {
·  const currentRole = await getCurrentUserRole();
·  this.setState({
·  isAdmin: currentRole === 'Admin',
·  });
·  }
·  render() {
·  if (this.state.isAdmin) {
·  return <Comp {...this.props} />;
·  } else {
·  return (<div>您沒有許可權檢視該頁面,請聯絡管理員!</div>);
·  }
·  }
· };
· }
複製程式碼
    • 效能監控,包裹元件的生命週期,進行統一埋點:
function withTiming(Comp) {
 return class extends Comp {
 constructor(props) {
 super(props);
 this.start = Date.now();
 this.end = 0;
 }
 componentDidMount() {
 super.componentDidMount && super.componentDidMount();
 this.end = Date.now();
 console.log(`${WrappedComponent.name} 元件渲染時間為 ${this.end - this.start} ms`);
 }
 render() {
 return super.render();
 }
 };
}
複製程式碼
    • 程式碼複用,可以將重複的邏輯進行抽象。
      1. 純函式: 增強函式應為純函式,避免侵入修改元元件;
      • 避免用法汙染: 理想狀態下,應透傳元元件的無關引數與事件,儘量保證用法不變;
      1. 名稱空間: 為 HOC 增加特異性的元件名稱,這樣能便於開發除錯和查詢問題;
      1. 引用傳遞: 如果需要傳遞元元件的 refs 引用,可以使用React.forwardRef
      1. 靜態方法: 元元件上的靜態方法並無法被自動傳出,會導致業務層無法呼叫;解決:
      • 函式匯出
      • 靜態方法賦值
      1. 重新渲染: 由於增強函式每次呼叫是返回一個新元件,因此如果在 Render 中使用增強函式,就會導致每次都重新渲染整個HOC,而且之前的狀態會丟失;

5. Redux

Redux 是一個 資料管理中心,可以把它理解為一個全域性的 data store 例項。它通過一定的使用規則和限制,保證著資料的健壯性、可追溯和可預測性。它與 React 無關,可以獨立運行於任何 JavaScript 環境中,從而也為同構應用提供了更好的資料同步通道。

  • 核心理念:
  • 大致的資料結構如下所示:
    • 單一資料來源: 整個應用只有唯一的狀態樹,也就是所有 state 最終維護在一個根級 Store 中;
    • 狀態只讀: 為了保證狀態的可控性,最好的方式就是監控狀態的變化。那這裡就兩個必要條件:
      • Redux Store 中的資料無法被直接修改;
      • 嚴格控制修改的執行;
    • 純函式: 規定只能通過一個純函式 (Reducer) 來描述修改;

  • 理念實現:
    • Store: 全域性 Store 單例, 每個 Redux 應用下只有一個 store, 它具有以下方法供使用:
      • getState: 獲取 state;
      • dispatch: 觸發 action, 更新 state;
      • subscribe: 訂閱資料變更,註冊監聽器;
· // 建立
· const store = createStore(Reducer, initStore)
複製程式碼
    • Action: 它作為一個行為載體,用於對映相應的 Reducer,並且它可以成為資料的載體,將資料從應用傳遞至 store 中,是 store 唯一的資料來源
// 一個普通的 Action
const action = {
 type: 'ADD_LIST',
 item: 'list-item-1',
}
// 使用:
store.dispatch(action)
// 通常為了便於呼叫,會有一個 Action 建立函式 (action creater)
funtion addList(item) {
 return const action = {
 type: 'ADD_LIST',
 item,
 }
}
// 呼叫就會變成:
dispatch(addList('list-item-1'))
複製程式碼
    • Reducer: 用於描述如何修改資料的純函式,Action 屬於行為名稱,而 Reducer 便是修改行為的實質;
// 一個常規的 Reducer
// @param {state}: 舊資料
// @param {action}: Action 物件
// @returns {any}: 新資料
const initList = []
function ListReducer(state = initList, action) {
 switch (action.type) {
 case 'ADD_LIST':
 return state.concat([action.item])
 break
 defalut:
 return state
 }
}
複製程式碼

注意:

  1. 遵守資料不可變,不要去直接修改 state,而是返回出一個 新物件,可以使用 assign / copy / extend / 解構 等方式建立新物件;
    1. 預設情況下需要 返回原資料,避免資料被清空;
    2. 最好設定 初始值,便於應用的初始化及資料穩定;
  • 進階:
    • React-Redux: 結合 React 使用;
      • <Provider>: 將 store 通過 context 傳入元件中;
      • connect: 一個高階元件,可以方便在 React 元件中使用 Redux;
          1. store通過mapStateToProps進行篩選後使用props注入元件

根據mapDispatchToProps建立方法,當元件呼叫時使用dispatch觸發對應的action

    • Reducer 的拆分與重構:
      • 隨著專案越大,如果將所有狀態的 reducer 全部寫在一個函式中,將會 難以維護
      • 可以將 reducer 進行拆分,也就是 函式分解,最終再使用combineReducers()進行重構合併;
    • 非同步 Action: 由於 Reducer 是一個嚴格的純函式,因此無法在 Reducer 中進行資料的請求,需要先獲取資料,再dispatch(Action)即可,下面是三種不同的非同步實現:

6. React Hooks

React 中通常使用 類定義 或者 函式定義 建立元件:

在類定義中,我們可以使用到許多 React 特性,例如 state、 各種元件生命週期鉤子等,但是在函式定義中,我們卻無能為力,因此 React 16.8 版本推出了一個新功能 (React Hooks),通過它,可以更好的在函式定義元件中使用 React 特性。

  • 好處:
  • 注意:
  • 重要鉤子*:
    • 1、跨元件複用: 其實 render props / HOC 也是為了複用,相比於它們,Hooks 作為官方的底層 API,最為輕量,而且改造成本小,不會影響原來的元件層次結構和傳說中的巢狀地獄;
    • 2、類定義更為複雜:
      • 不同的生命週期會使邏輯變得分散且混亂,不易維護和管理;
      • 時刻需要關注this的指向問題;
      • 程式碼複用代價高,高階元件的使用經常會使整個元件樹變得臃腫;
    • 3、狀態與UI隔離: 正是由於 Hooks 的特性,狀態邏輯會變成更小的粒度,並且極容易被抽象成一個自定義 Hooks,元件中的狀態和 UI 變得更為清晰和隔離。
    • 避免在 迴圈/條件判斷/巢狀函式 中呼叫 hooks,保證呼叫順序的穩定;
    • 只有 函式定義元件 和 hooks 可以呼叫 hooks,避免在 類元件 或者 普通函式 中呼叫;
    • 不能在useEffect中使用useState,React 會報錯提示;
    • 類元件不會被替換或廢棄,不需要強制改造類元件,兩種方式能並存;
    • 狀態鉤子 (useState): 用於定義元件的 State,其到類定義中this.state的功能;
· // useState 只接受一個引數: 初始狀態
· // 返回的是元件名和更改該元件對應的函式
· const [flag, setFlag] = useState(true);
· // 修改狀態
· setFlag(false)
·  
· // 上面的程式碼對映到類定義中:
· this.state = {
·  flag: true 
· }
· const flag = this.state.flag
· const setFlag = (bool) => {
· this.setState({
· flag: bool,
· })
· }
複製程式碼
    • 生命週期鉤子 (useEffect):

類定義中有許多生命週期函式,而在 React Hooks 中也提供了一個相應的函式 (useEffect),這裡可以看做componentDidMountcomponentDidUpdatecomponentWillUnmount的結合。

    • useEffect(callback, [source])接受兩個引數
      • callback: 鉤子回撥函式;
      • source: 設定觸發條件,僅當 source 發生改變時才會觸發;
      • useEffect鉤子在沒有傳入[source]引數時,預設在每次 render 時都會優先呼叫上次儲存的回撥中返回的函式,後再重新呼叫回撥;
useEffect(() => {
 // 元件掛載後執行事件繫結
 console.log('on')
 addEventListener()
 
 // 元件 update 時會執行事件解綁
 return () => {
 console.log('off')
 removeEventListener()
 }
}, [source]);
// 每次 source 發生改變時,執行結果(以類定義的生命週期,便於大家理解):
// --- DidMount ---
// 'on'
// --- DidUpdate ---
// 'off'
// 'on'
// --- DidUpdate ---
// 'off'
// 'on'
// --- WillUnmount --- 
// 'off'
複製程式碼
  • 通過第二個引數,我們便可模擬出幾個常用的生命週期:
  • 其它內建鉤子:
    • useContext: 獲取 context 物件
    • useReducer: 類似於 Redux 思想的實現,但其並不足以替代 Redux,可以理解成一個元件內部的 redux:
    • useCallback: 快取回撥函式,避免傳入的回撥每次都是新的函式例項而導致依賴元件重新渲染,具有效能優化的效果;
    • useMemo: 用於快取傳入的 props,避免依賴的元件每次都重新渲染;
    • useRef: 獲取元件的真實節點;
    • useLayoutEffect:
  • 自定義鉤子(useXxxxx): 基於 Hooks 可以引用其它 Hooks 這個特性,我們可以編寫自定義鉤子,如上面的useMounted。又例如,我們需要每個頁面自定義標題:
      • componentDidMount: 傳入[]時,就只會在初始化時呼叫一次;
o const useMount = (fn) => useEffect(fn, [])
複製程式碼
      • componentWillUnmount: 傳入[],回撥中的返回的函式也只會被最終執行一次;
const useUnmount = (fn) => useEffect(() => fn, [])
複製程式碼
      • mounted: 可以使用 useState 封裝成一個高度可複用的 mounted 狀態;
const useMounted = () => {
 const [mounted, setMounted] = useState(false);
 useEffect(() => {
 !mounted && setMounted(true);
 return () => setMounted(false);
 }, []);
 return mounted;
}
複製程式碼
      • componentDidUpdate: useEffect每次均會執行,其實就是排除了 DidMount 後即可;
const mounted = useMounted() 
useEffect(() => {
 mounted && fn()
})
複製程式碼
      • 並不是持久化儲存,會隨著元件被銷燬而銷燬;
      • 屬於元件內部,各個元件是相互隔離的,單純用它並無法共享資料;
      • 配合useContext的全域性性,可以完成一個輕量級的 Redux;(easy-peasy)
      • DOM更新同步鉤子。用法與useEffect類似,只是區別於執行時間點的不同。
      • useEffect屬於非同步執行,並不會等待 DOM 真正渲染後執行,而useLayoutEffect則會真正渲染後才觸發;
      • 可以獲取更新後的 state;
function useTitle(title) {
 useEffect(
 () => {
 document.title = title;
 });
}
// 使用:
function Home() {
 const title = '我是首頁'
 useTitle(title)
 
 return (
 <div>{title}</div>
 )
}
複製程式碼

7. SSR

SSR,俗稱 服務端渲染 (Server Side Render),講人話就是: 直接在服務端層獲取資料,渲染出完成的 HTML 檔案,直接返回給使用者瀏覽器訪問。

  • 前後端分離: 前端與服務端隔離,前端動態獲取資料,渲染頁面。
  • 痛點:
    • 首屏渲染效能瓶頸:
    • SEO 問題: 由於頁面初始狀態為空,因此爬蟲無法獲取頁面中任何有效資料,因此對搜尋引擎不友好。
      • 空白延遲: HTML下載時間 + JS下載/執行時間 + 請求時間 + 渲染時間。在這段時間內,頁面處於空白的狀態。
      • 雖然一直有在提動態渲染爬蟲的技術,不過據我瞭解,大部分國內搜尋引擎仍然是沒有實現。

最初的服務端渲染,便沒有這些問題。但我們不能返璞歸真,既要保證現有的前端獨立的開發模式,又要由服務端渲染,因此我們使用 React SSR。

  • 原理:
  • 條件: Node 中間層、 React / Vue 等框架。 結構大概如下:
    • Node 服務: 讓前後端運行同一套程式碼成為可能。
    • Virtual Dom: 讓前端程式碼脫離瀏覽器執行。

  • 開發流程: (此處以 React + Router + Redux + Koa 為例)
    • 1、在同個專案中,搭建 前後端部分,常規結構:
    • 2、server 中使用 Koa 路由監聽 頁面訪問:
    • 4、通過頁面路由的配置進行 資料獲取。通常可以在頁面路由中增加 SSR 相關的靜態配置,用於抽象邏輯,可以保證服務端邏輯的通用性,如:
      • build
      • public
      • src
        • client
        • server
· import * as Router from 'koa-router'
· 
· const router = new Router()
· // 如果中間也提供 Api 層
· router.use('/api/home', async () => {
·  // 返回資料
· })
· 
· router.get('*', async (ctx) => {
·  // 返回 HTML
· })
複製程式碼
    • 3、通過訪問 url 匹配 前端頁面路由:
// 前端頁面路由
import { pages } from '../../client/app'
import { matchPath } from 'react-router-dom'
// 使用 react-router 庫提供的一個匹配方法
const matchPage = matchPath(ctx.req.url, page)
複製程式碼
o class HomePage extends React.Component{
o  public static ssrConfig = {
o  cache: true,
o fetch() {
o  // 請求獲取資料
o }
o }
o }
複製程式碼

獲取資料通常有兩種情況:

      • 中間層也使用 http 獲取資料,則此時 fetch 方法可前後端共享;
const data = await matchPage.ssrConfig.fetch()
複製程式碼
      • 中間層並不使用 http,是通過一些 內部呼叫,例如 Rpc 或 直接讀資料庫 等,此時也可以直接由服務端呼叫對應的方法獲取資料。通常,這裡需要在 ssrConfig 中配置特異性的資訊,用於匹配對應的資料獲取方法。
// 頁面路由
class HomePage extends React.Component{
 public static ssrConfig = {
 fetch: {
  url: '/api/home',
 }
 }
}
// 根據規則匹配出對應的資料獲取方法
// 這裡的規則可以自由,只要能匹配出正確的方法即可
const controller = matchController(ssrConfig.fetch.url)
// 獲取資料
const data = await controller(ctx)
複製程式碼
  • 5、建立 Redux store,並將資料dispatch到裡面:
  • 7、將 AppString 包裝成完整的 html 檔案格式;
  • 8、此時,已經能生成完整的 HTML 檔案。但只是個純靜態的頁面,沒有樣式沒有互動。接下來我們就是要插入 JS 與 CSS。我們可以通過訪問前端打包後生成的asset-manifest.json檔案來獲取相應的檔案路徑,並同樣注入到 Html 中引用。
import { createStore } from 'redux'
// 獲取 Clinet層 reducer
// 必須複用前端層的邏輯,才能保證一致性;
import { reducers } from '../../client/store'
// 建立 store
const store = createStore(reducers)
 
// 獲取配置好的 Action
const action = ssrConfig.action
// 儲存資料 
store.dispatch(createAction(action)(data))
複製程式碼
    • 6、注入 Store, 呼叫renderToString將 React Virtual Dom 渲染成 字串:
import * as ReactDOMServer from 'react-dom/server'
import { Provider } from 'react-redux'
// 獲取 Clinet 層根元件
import { App } from '../../client/app'
const AppString = ReactDOMServer.renderToString(
 <Provider store={store}>
 <StaticRouter
 location={ctx.req.url}
 context={{}}>
 <App />
 </StaticRouter>
 </Provider>
)
複製程式碼
const html = `
 <!DOCTYPE html>
 <html lang="zh">
 <head></head>
 <link href="${cssPath}" rel="stylesheet" />
 <body>
 <div id="App">${AppString}</div>
 <script src="${scriptPath}"></script>
 </body>
 </html>
`
複製程式碼
    • 9、進行 資料脫水: 為了把服務端獲取的資料同步到前端。主要是將資料序列化後,插入到 html 中,返回給前端。
import serialize from 'serialize-javascript'
// 獲取資料
const initState = store.getState()
const html = `
 <!DOCTYPE html>
 <html lang="zh">
 <head></head>
 <body>
 <div id="App"></div>
 <script type="application/json" id="SSR_HYDRATED_DATA">${serialize(initState)}</script>
 </body>
 </html>
`
ctx.status = 200
ctx.body = html
複製程式碼

Tips:

這裡比較特別的有兩點:

16. 使用了serialize-javascript序列化 store, 替代了JSON.stringify,保證資料的安全性,避免程式碼注入和 XSS 攻擊;

17. 使用 json 進行傳輸,可以獲得更快的載入速度;

    • 10、Client 層 資料吸水: 初始化 store 時,以脫水後的資料為初始化資料,同步建立 store。
const hydratedEl = document.getElementById('SSR_HYDRATED_DATA')
const hydrateData = JSON.parse(hydratedEl.textContent)
// 使用初始 state 建立 Redux store
const store = createStore(reducer, hydrateData)
複製程式碼

8. 函數語言程式設計

函數語言程式設計是一種 程式設計正規化,你可以理解為一種軟體架構的思維模式。它有著獨立一套理論基礎與邊界法則,追求的是 更簡潔、可預測、高複用、易測試。其實在現有的眾多知名庫中,都蘊含著豐富的函數語言程式設計思想,如 React / Redux 等。

  • 常見的程式設計正規化:
  • 函數語言程式設計的理念:
    • 純函式(確定性函式): 是函數語言程式設計的基礎,可以使程式變得靈活,高度可拓展,可維護;
      • 優勢:
      • 條件:
      • 我們常用到的許多 API 或者工具函式,它們都具有著純函式的特點, 如split / join / map
      • 函式複合: 將多個函式進行組合後呼叫,可以實現將一個個函式單元進行組合,達成最後的目標;
        • 扁平化巢狀: 首先,我們一定能想到組合函式最簡單的操作就是 包裹,因為在 JS 中,函式也可以當做引數:
        • 結果傳遞: 如果想實現上面的方式,那也就是xxx函式要實現的便是: 執行結果在各個函式之間的執行傳遞;
        • 使用: 實現一個 駝峰命名 轉 中劃線命名 的功能:
        • 好處:
    • 指令式程式設計(過程化程式設計): 更關心解決問題的步驟,一步步以語言的形式告訴計算機做什麼;
    • 事件驅動程式設計: 事件訂閱與觸發,被廣泛用於 GUI 的程式設計設計中;
    • 面向物件程式設計: 基於類、物件與方法的設計模式,擁有三個基礎概念: 封裝性、繼承性、多型性;
    • 函數語言程式設計
      • 換成一種更高階的說法,面向數學程式設計。怕不怕~