(中篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠
各位大佬在評論中指出的種種問題小弟萬分感謝。由于這一年來,出了不少變動,是以才一直耽擱,現已修複各位大佬指出的問題和建議。請大家放心食用!感恩~🥳
沒想到上篇文章能這麼受大家的喜歡,激動不已。🤩。但是卻也是誠惶誠恐,這也意味着責任。下篇許多知識點都需要比較深入的研究和了解,部落客也是水準有限,擔心自己無法承擔大家的期待。不過終究還是需要擺正心态,放下情緒,一字一字用心專注,不負自己,也不負社群。與各位小夥伴互相學習,共同成長,以此共勉!
最近業務繁忙,精力有限,雖然我盡量嚴謹和反複修訂,但文章也定有疏漏。上篇文章中,許多小夥伴們指出了不少的問題,為此我也是深表抱歉,我也會虛心接受和糾正錯誤。也非常感激那麼多通過微信或公衆号與我探讨的小夥伴,感謝大家的支援和鼓勵。
引言
大家知道,React 現在已經在前端開發中占據了主導的地位。優異的性能,強大的生态,讓其無法阻擋。部落客面的 5 家公司,全部是 React 技術棧。據我所知,大廠也大部分以 React 作為主技術棧。React 也成為了面試中并不可少的一環。
中篇主要從以下幾個方面對 React 展開闡述:
- Fiber
- 生命周期
- SetState
- HOC(高階元件)
- Redux
- React Hooks
- SSR
- 函數式程式設計
本來是計劃隻有上下兩篇,可是寫着寫着越寫越多,受限于篇幅,也為了有更好的閱讀體驗,隻好拆分出中篇,希望各位童鞋别介意。🙃,另外,下篇還有 Hybrid App / Webpack / 性能優化 / Nginx 等方面的知識,敬請期待。
建議還是先從上篇基礎開始哈~有個循序漸進的過程:
- (上篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠
- (中篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠
- (下篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠
進階知識
架構: 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 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 節點後,跳出周遊;
- 任務分割,React 中的渲染更新可以分成兩個階段:
- reconciliation 階段: vdom 的資料對比,是個适合拆分的階段,比如對比一部分樹後,先暫停執行個動畫調用,待完成後再回來繼續比對。
- Commit 階段: 将 change list 更新到 dom 上,并不适合拆分,才能保持資料與 UI 的同步。否則可能由于阻塞 UI 更新,而導緻資料更新和 UI 不一緻的情況。
- 分散執行: 任務分割後,就可以把小任務單元分散到浏覽器的空閑期間去排隊執行,而實作的關鍵是兩個新API:
與requestIdleCallback
requestAnimationFrame
- 低優先級的任務交給
處理,這是個浏覽器提供的事件循環空閑期的回調函數,需要 pollyfill,而且擁有 deadline 參數,限制執行事件,以繼續切分任務;requestIdleCallback
- 高優先級的任務交給
處理;requestAnimationFrame
// 類似于這樣的方式
requestIdleCallback((deadline) => {
// 當有空閑時間時,我們執行一個元件渲染;
// 把任務塞到一個個碎片時間中去;
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {
nextComponent = performWork(nextComponent);
}
});
複制代碼
- 低優先級的任務交給
- 優先級政策: 文本框輸入 > 本次排程結束需完成的任務 > 動畫過渡 > 互動回報 > 資料更新 > 不會顯示但以防将來會顯示的任務
-
Tips:
Fiber 其實可以算是一種程式設計思想,在其它語言中也有許多應用(Ruby Fiber)。核心思想是 任務拆分和協同,主動把執行權交給主線程,使主線程有時間空擋處理其他高優先級任務。
當遇到程序阻塞的問題時,任務分割、異步調用 和 緩存政策 是三個顯著的解決思路。
感謝 @Pengyuan 童鞋,在評論中指出了幾個 Fiber 中最核心的理念,感恩!!
2. 生命周期
在新版本中,React 官方對生命周期有了新的 變動建議:
- 使用
替換getDerivedStateFromProps
與componentWillMount
;componentWillReceiveProps
- 使用
替換getSnapshotBeforeUpdate
;componentWillUpdate
- 避免使用
;componentWillReceiveProps
其實該變動的原因,正是由于上述提到的 Fiber。首先,從上面我們知道 React 可以分成 reconciliation 與 commit 兩個階段,對應的生命周期如下:
- reconciliation:
-
componentWillMount
-
componentWillReceiveProps
-
shouldComponentUpdate
-
componentWillUpdate
-
- commit:
-
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() {}
}
複制代碼
- 使用建議:
- 在
初始化 state;constructor
- 在
中進行事件監聽,并在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
-
可以在更新之前擷取最新的渲染資料,它的調用是在 render 之後, update 之前;getSnapshotBeforeUpdate(prevProps, prevState)
-
: 預設每次調用shouldComponentUpdate
,一定會最終走到 diff 階段,但可以通過setState
的生命鈎子傳回shouldComponentUpdate
來直接阻止後面的邏輯執行,通常是用于做條件渲染,優化渲染的性能。false
- 在
3. setState
在了解
setState
之前,我們先來簡單了解下 React 一個包裝結構: Transaction:
- 事務 (Transaction):
- 是 React 中的一個調用結構,用于包裝一個方法,結構為: initialize - perform(method) - close。通過事務,可以統一管理一個方法的開始與結束;處于事務流中,表示程序正在執行一些操作;
-
: React 中用于修改狀态,更新視圖。它具有以下特點:setState
- 異步與同步:
并不是單純的異步或同步,這其實與調用時的環境相關:setState
- 在 合成事件 和 生命周期鈎子(除 componentDidUpdate) 中,
是"異步"的;setState
- 原因: 因為在
的實作中,有一個判斷: 當更新政策正在事務流的執行中時,該元件更新會被推入setState
隊列中等待執行;否則,開始執行dirtyComponents
隊列更新;batchedUpdates
- 在生命周期鈎子調用中,更新政策都處于更新之前,元件仍處于事務流中,而
是在更新之後,此時元件已經不在事務流中了,是以則會同步執行;componentDidUpdate
- 在合成事件中,React 是基于 事務流完成的事件委托機制 實作,也是處于事務流中;
- 在生命周期鈎子調用中,更新政策都處于更新之前,元件仍處于事務流中,而
- 問題: 無法在
後馬上從setState
上擷取更新後的值。this.state
- 解決: 如果需要馬上同步去擷取新值,
其實是可以傳入第二個參數的。setState
,在回調中即可擷取最新值;setState(updater, callback)
- 原因: 因為在
- 在 原生事件 和 setTimeout 中,
是同步的,可以馬上擷取更新後的值;setState
- 原因: 原生事件是浏覽器本身的實作,與事務流無關,自然是同步;而
是放置于定時器線程中延後執行,此時事務流已結束,是以也是同步;setTimeout
- 原因: 原生事件是浏覽器本身的實作,與事務流無關,自然是同步;而
- 在 合成事件 和 生命周期鈎子(除 componentDidUpdate) 中,
- 批量更新: 在 合成事件 和 生命周期鈎子 中,
更新隊列時,存儲的是 合并狀态(setState
)。是以前面設定的 key 值會被後面所覆寫,最終隻會執行一次更新;Object.assign
- 函數式: 由于 Fiber 及 合并 的問題,官方推薦可以傳入 函數 的形式。
,在setState(fn)
中傳回新的fn
對象即可,例如state
this.setState((state, props) => newState);
- 使用函數式,可以用于避免
的批量更新的邏輯,傳入的函數将會被 順序調用;setState
- 使用函數式,可以用于避免
- 注意事項:
- setState 合并,在 合成事件 和 生命周期鈎子 中多次連續調用會被優化為一次;
- 當元件已被銷毀,如果再次調用
,React 會報錯警告,通常有兩種解決辦法:setState
- 将資料挂載到外部,通過 props 傳入,如放到 Redux 或 父級中;
- 在元件内部維護一個狀态量 (isUnmounted),
中标記為 true,在componentWillUnmount
前進行判斷;setState
4. HOC(高階元件)
HOC(Higher Order Componennt) 是在 React 機制下社群形成的一種元件模式,在很多第三方開源庫中表現強大。
- 簡述:
- 高階元件不是元件,是 增強函數,可以輸入一個元元件,傳回出一個新的增強元件;
- 高階元件的主要作用是 代碼複用,操作 狀态和參數;
- 用法:
- 屬性代理 (Props Proxy): 傳回出一個元件,它基于被包裹元件進行 功能增強;
-
預設參數: 可以為元件包裹一層預設參數;
function proxyHoc(Comp) {
return class extends React.Component {
render() {
const newProps = {
name: ‘tayde’,
age: 1,
}
return <Comp {…this.props} {…newProps} />
}
}
}
複制代碼
-
提取狀态: 可以通過 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 (
<Comp {…this.props} />
<div style={{
width: ‘100%’,
height: ‘100%’,
backgroundColor: ‘rgba(0, 0, 0, .6)’,
}}
)
}
}
}
複制代碼
-
- 反向繼承 (Inheritance Inversion): 傳回出一個元件,繼承于被包裹元件,常用于以下操作:
function IIHoc(Comp) { return class extends Comp { render() { return super.render(); } }; } 複制代碼
- 渲染劫持 (Render Highjacking)
-
條件渲染: 根據條件,渲染不同的元件
function withLoading(Comp) {
return class extends Comp {
render() {
if(this.props.isLoading) {
return
} else {
return super.render()
}
}
};
}
複制代碼
- 可以直接修改被包裹元件渲染出的 React 元素樹
-
- 操作狀态 (Operate State): 可以直接通過
擷取到被包裹元件的狀态,并進行操作。但這樣的操作容易使 state 變得難以追蹤,不易維護,謹慎使用。this.state
- 渲染劫持 (Render Highjacking)
- 屬性代理 (Props Proxy): 傳回出一個元件,它基于被包裹元件進行 功能增強;
- 應用場景:
-
權限控制,通過抽象邏輯,統一對頁面進行權限判斷,按不同的條件進行頁面渲染:
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 (
您沒有權限檢視該頁面,請聯系管理者! );
}
}
};
}
複制代碼
-
性能監控,包裹元件的生命周期,進行統一埋點:
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();
}
};
}
複制代碼
- 代碼複用,可以将重複的邏輯進行抽象。
-
- 使用注意:
-
- 純函數: 增強函數應為純函數,避免侵入修改元元件;
-
- 避免用法污染: 理想狀态下,應透傳元元件的無關參數與事件,盡量保證用法不變;
-
- 命名空間: 為 HOC 增加特異性的元件名稱,這樣能便于開發調試和查找問題;
-
- 引用傳遞: 如果需要傳遞元元件的 refs 引用,可以使用
;React.forwardRef
- 引用傳遞: 如果需要傳遞元元件的 refs 引用,可以使用
-
- 靜态方法: 元元件上的靜态方法并無法被自動傳出,會導緻業務層無法調用;解決:
- 函數導出
- 靜态方法指派
-
- 重新渲染: 由于增強函數每次調用是傳回一個新元件,是以如果在 Render 中使用增強函數,就會導緻每次都重新渲染整個HOC,而且之前的狀态會丢失;
-
5. Redux
Redux 是一個 資料管理中心,可以把它了解為一個全局的 data store 執行個體。它通過一定的使用規則和限制,保證着資料的健壯性、可追溯和可預測性。它與 React 無關,可以獨立運作于任何 JavaScript 環境中,進而也為同構應用提供了更好的資料同步通道。
- 核心理念:
- 單一資料源: 整個應用隻有唯一的狀态樹,也就是所有 state 最終維護在一個根級 Store 中;
- 狀态隻讀: 為了保證狀态的可控性,最好的方式就是監控狀态的變化。那這裡就兩個必要條件:
- Redux Store 中的資料無法被直接修改;
- 嚴格控制修改的執行;
- 純函數: 規定隻能通過一個純函數 (Reducer) 來描述修改;
- 大緻的資料結構如下所示:
- 理念實作:
- Store: 全局 Store 單例, 每個 Redux 應用下隻有一個 store, 它具有以下方法供使用:
-
: 擷取 state;getState
-
: 觸發 action, 更新 state;dispatch
-
: 訂閱資料變更,注冊監聽器;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
}
}
複制代碼
注意:
- 遵守資料不可變,不要去直接修改 state,而是傳回出一個 新對象,可以使用
等方式建立新對象;assign / copy / extend / 解構
- 預設情況下需要 傳回原資料,避免資料被清空;
- 最好設定 初始值,便于應用的初始化及資料穩定;
- Store: 全局 Store 單例, 每個 Redux 應用下隻有一個 store, 它具有以下方法供使用:
- 進階:
- React-Redux: 結合 React 使用;
-
: 将 store 通過 context 傳入元件中;<Provider>
-
: 一個高階元件,可以友善在 React 元件中使用 Redux;connect
-
- 将
通過store
進行篩選後使用mapStateToProps
注入元件props
- 将
-
- 根據
建立方法,當元件調用時使用mapDispatchToProps
觸發對應的dispatch
action
- 根據
-
-
- Reducer 的拆分與重構:
- 随着項目越大,如果将所有狀态的 reducer 全部寫在一個函數中,将會 難以維護;
- 可以将 reducer 進行拆分,也就是 函數分解,最終再使用
進行重構合并;combineReducers()
- 異步 Action: 由于 Reducer 是一個嚴格的純函數,是以無法在 Reducer 中進行資料的請求,需要先擷取資料,再
即可,下面是三種不同的異步實作:dispatch(Action)
- redex-thunk
- redux-saga
- redux-observable
- React-Redux: 結合 React 使用;
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
,React 會報錯提示;useState
- 類元件不會被替換或廢棄,不需要強制改造類元件,兩種方式能并存;
- 重要鈎子*:
- 狀态鈎子 (
): 用于定義元件的 State,其到類定義中useState
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
),這裡可以看做useEffect
、componentDidMount
和componentDidUpdate
的結合。componentWillUnmount
-
接受兩個參數useEffect(callback, [source])
-
: 鈎子回調函數;callback
-
: 設定觸發條件,僅當 source 發生改變時才會觸發;source
-
鈎子在沒有傳入useEffect
參數時,預設在每次 render 時都會優先調用上次儲存的回調中傳回的函數,後再重新調用回調;[source]
useEffect(() => {
// 元件挂載後執行事件綁定
console.log(‘on’)
addEventListener()
// 元件 update 時會執行事件解綁 return () => { console.log('off') removeEventListener() }
}, [source]);
// 每次 source 發生改變時,執行結果(以類定義的生命周期,便于大家了解):
// — DidMount —
// ‘on’
// — DidUpdate —
// ‘off’
// ‘on’
// — DidUpdate —
// ‘off’
// ‘on’
// — WillUnmount —
// ‘off’
複制代碼
-
- 通過第二個參數,我們便可模拟出幾個常用的生命周期:
-
: 傳入componentDidMount
[]
時,就隻會在初始化時調用一次;
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()
})
複制代碼
-
- 狀态鈎子 (
- 其它内置鈎子:
-
: 擷取 context 對象useContext
-
: 類似于 Redux 思想的實作,但其并不足以替代 Redux,可以了解成一個元件内部的 redux:useReducer
- 并不是持久化存儲,會随着元件被銷毀而銷毀;
- 屬于元件内部,各個元件是互相隔離的,單純用它并無法共享資料;
- 配合
的全局性,可以完成一個輕量級的 Redux;(easy-peasy)useContext
-
: 緩存回調函數,避免傳入的回調每次都是新的函數執行個體而導緻依賴元件重新渲染,具有性能優化的效果;useCallback
-
: 用于緩存傳入的 props,避免依賴的元件每次都重新渲染;useMemo
-
: 擷取元件的真實節點;useRef
-
:useLayoutEffect
- DOM更新同步鈎子。用法與
類似,隻是差別于執行時間點的不同。useEffect
-
屬于異步執行,并不會等待 DOM 真正渲染後執行,而useEffect
則會真正渲染後才觸發;useLayoutEffect
- 可以擷取更新後的 state;
- DOM更新同步鈎子。用法與
-
- 自定義鈎子(
): 基于 Hooks 可以引用其它 Hooks 這個特性,我們可以編寫自定義鈎子,如上面的useXxxxx
useMounted
。又例如,我們需要每個頁面自定義标題:
function useTitle(title) {
useEffect(
() => {
document.title = title;
});
}
// 使用:
function Home() {
const title = ‘我是首頁’
useTitle(title)
return ( <div>{title}</div> )
}
複制代碼
7. SSR
SSR,俗稱 服務端渲染 (Server Side Render),講人話就是: 直接在服務端層擷取資料,渲染出完成的 HTML 檔案,直接傳回給使用者浏覽器通路。
- 前後端分離: 前端與服務端隔離,前端動态擷取資料,渲染頁面。
- 痛點:
- 首屏渲染性能瓶頸:
- 空白延遲: HTML下載下傳時間 + JS下載下傳/執行時間 + 請求時間 + 渲染時間。在這段時間内,頁面處于空白的狀态。
- SEO 問題: 由于頁面初始狀态為空,是以爬蟲無法擷取頁面中任何有效資料,是以對搜尋引擎不友好。
- 雖然一直有在提動态渲染爬蟲的技術,不過據我了解,大部分國内搜尋引擎仍然是沒有實作。
- 首屏渲染性能瓶頸:
最初的服務端渲染,便沒有這些問題。但我們不能返璞歸真,既要保證現有的前端獨立的開發模式,又要由服務端渲染,是以我們使用 React SSR。
- 原理:
- Node 服務: 讓前後端運作同一套代碼成為可能。
- Virtual Dom: 讓前端代碼脫離浏覽器運作。
- 條件: Node 中間層、 React / Vue 等架構。 結構大概如下:
- 開發流程: (此處以 React + Router + Redux + Koa 為例)
- 1、在同個項目中,搭建 前後端部分,正常結構:
- build
- public
- src
- client
- server
-
2、server 中使用 Koa 路由監聽 頁面通路:
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)
複制代碼
- 4、通過頁面路由的配置進行 資料擷取。通常可以在頁面路由中增加 SSR 相關的靜态配置,用于抽象邏輯,可以保證服務端邏輯的通用性,如:
擷取資料通常有兩種情況:class HomePage extends React.Component{ public static ssrConfig = { cache: true, fetch() { // 請求擷取資料 } } } 複制代碼
-
中間層也使用 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
到裡面:
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(
)
複制代碼
- 7、将 AppString 包裝成完整的 html 檔案格式;
- 8、此時,已經能生成完整的 HTML 檔案。但隻是個純靜态的頁面,沒有樣式沒有互動。接下來我們就是要插入 JS 與 CSS。我們可以通過通路前端打包後生成的
asset-manifest.json
檔案來擷取相應的檔案路徑,并同樣注入到 Html 中引用。
const html =
複制代碼<!DOCTYPE html> <html > <head></head> <link href="${cssPath}" target="_blank" rel="external nofollow" 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 > <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:
這裡比較特别的有兩點:
- 使用了
序列化 store, 替代了serialize-javascript
,保證資料的安全性,避免代碼注入和 XSS 攻擊;JSON.stringify
- 使用 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)
複制代碼
- 1、在同個項目中,搭建 前後端部分,正常結構:
8. 函數式程式設計
函數式程式設計是一種 程式設計範式,你可以了解為一種軟體架構的思維模式。它有着獨立一套理論基礎與邊界法則,追求的是 更簡潔、可預測、高複用、易測試。其實在現有的衆多知名庫中,都蘊含着豐富的函數式程式設計思想,如 React / Redux 等。
- 常見的程式設計範式:
- 指令式程式設計(過程化程式設計): 更關心解決問題的步驟,一步步以語言的形式告訴計算機做什麼;
- 事件驅動程式設計: 事件訂閱與觸發,被廣泛用于 GUI 的程式設計設計中;
- 面向對象程式設計: 基于類、對象與方法的設計模式,擁有三個基礎概念: 封裝性、繼承性、多态性;
- 函數式程式設計
- 換成一種更高端的說法,面向數學程式設計。怕不怕~🥴
- 函數式程式設計的理念:
- 純函數(确定性函數): 是函數式程式設計的基礎,可以使程式變得靈活,高度可拓展,可維護;
- 優勢:
- 完全獨立,與外部解耦;
- 高度可複用,在任意上下文,任意時間線上,都可執行并且保證結果穩定;
- 可測試性極強;
- 條件:
- 不修改參數;
- 不依賴、不修改任何函數外部的資料;
- 完全可控,參數一樣,傳回值一定一樣: 例如函數不能包含
或者new Date()
等這種不可控因素;Math.rando()
- 引用透明;
- 我們常用到的許多 API 或者工具函數,它們都具有着純函數的特點, 如
;split / join / map
- 優勢:
- 函數複合: 将多個函數進行組合後調用,可以實作将一個個函數單元進行組合,達成最後的目标;
- 扁平化嵌套: 首先,我們一定能想到組合函數最簡單的操作就是 包裹,因為在 JS 中,函數也可以當做參數:
-
: 嵌套地獄,可讀性低,當函數複雜後,容易讓人一臉懵逼;f(g(k(x)))
- 理想的做法:
xxx(f, g, k)(x)
-
- 結果傳遞: 如果想實作上面的方式,那也就是
函數要實作的便是: 執行結果在各個函數之間的執行傳遞;xxx
- 這時我們就能想到一個原生提供的數組方法:
,它可以按數組的順序依次執行,傳遞執行結果;reduce
- 是以我們就能夠實作一個方法
pipe
,用于函數組合:
// …fs: 将函數組合成數組;
// Array.prototype.reduce 進行組合;
// p: 初始參數;
const pipe = (…fs) => p => fs.reduce((v, f) => f(v), p)
複制代碼
- 這時我們就能想到一個原生提供的數組方法:
-
使用: 實作一個 駝峰命名 轉 中劃線命名 的功能:
// ‘Guo DongDong’ --> ‘guo-dongdong’
// 函數組合式寫法
const toLowerCase = str => str.toLowerCase()
const join = curry((str, arr) => arr.join(str))
const split = curry((splitOn, str) => str.split(splitOn));
const toSlug = pipe(
toLowerCase,
split(’ ‘),
join(’_’),
encodeURIComponent,
);
console.log(toSlug(‘Guo DongDong’))
複制代碼
- 好處:
- 隐藏中間參數,不需要臨時變量,避免了這個環節的出錯幾率;
- 隻需關注每個純函數單元的穩定,不再需要關注命名,傳遞,調用等;
- 可複用性強,任何一個函數單元都可被任意複用群組合;
-
可拓展性強,成本低,例如現在加個需求,要檢視每個環節的輸出:
const log = curry((label, x) => {
console.log(
${ label }: ${ x }
);
return x;
});
const toSlug = pipe(
toLowerCase,
log(‘toLowerCase output’),
split(’ ‘),
log(‘split output’),
join(’_’),
log(‘join output’),
encodeURIComponent,
);
複制代碼
Tips:
一些工具純函數可直接引用
,例如lodash/fp
等,并不需要像我們上面這樣自己實作;curry/map/split
- 扁平化嵌套: 首先,我們一定能想到組合函數最簡單的操作就是 包裹,因為在 JS 中,函數也可以當做參數:
- 資料不可變性(immutable): 這是一種資料理念,也是函數式程式設計中的核心理念之一:
- 倡導: 一個對象再被建立後便不會再被修改。當需要改變值時,是傳回一個全新的對象,而不是直接在原對象上修改;
- 目的: 保證資料的穩定性。避免依賴的資料被未知地修改,導緻了自身的執行異常,能有效提高可控性與穩定性;
- 并不等同于
。使用const
建立一個對象後,它的屬性仍然可以被修改;const
- 更類似于
: 當機對象,但Object.freeze
仍無法保證深層的屬性不被串改;freeze
-
: js 中的資料不可變庫,它保證了資料不可變,在 React 生态中被廣泛應用,大大提升了性能與穩定性;immutable.js
-
資料結構:trie
- 一種資料結構,能有效地深度當機對象,保證其不可變;
- 結構共享: 可以共用不可變對象的記憶體引用位址,減少記憶體占用,提高資料操作性能;
-
- 避免不同函數之間的 狀态共享,資料的傳遞使用複制或全新對象,遵守資料不可變原則;
- 避免從函數内部 改變外部狀态,例如改變了全局作用域或父級作用域上的變量值,可能會導緻其它機關錯誤;
- 避免在單元函數内部執行一些 副作用,應該将這些操作抽離成更獨立的工具單元;
- 日志輸出
- 讀寫檔案
- 網絡請求
- 調用外部程序
- 調用有副作用的函數
- 純函數(确定性函數): 是函數式程式設計的基礎,可以使程式變得靈活,高度可拓展,可維護;
- 高階函數: 是指 以函數為參數,傳回一個新的增強函數 的一類函數,它通常用于:
- 将邏輯行為進行 隔離抽象,便于快速複用,如處理資料,相容性等;
- 函數組合,将一系列單元函數清單組合成功能更強大的函數;
- 函數增強,快速地拓展函數功能,
- 函數式程式設計的好處:
- 函數副作用小,所有函數獨立存在,沒有任何耦合,複用性極高;
- 不關注執行時間,執行順序,參數,命名等,能專注于資料的流動與處理,能有效提高穩定性與健壯性;
- 追求單元化,粒度化,使其重構和改造成本降低,可維護、可拓展性較好;
- 更易于做單元測試。
- 總結:
- 函數式程式設計其實是一種程式設計思想,它追求更細的粒度,将應用拆分成一組組極小的單元函數,組合調用操作資料流;
- 它提倡着 純函數 / 函數複合 / 資料不可變, 謹慎對待函數内的 狀态共享 / 依賴外部 / 副作用;
Tips:
其實我們很難也不需要在面試過程中去完美地闡述出整套思想,這裡也隻是淺嘗辄止,一些個人了解而已。部落客也是初級小菜鳥,停留在表面而已,隻求對大家能有所幫助,輕噴🤣;
我個人覺得: 這些程式設計範式之間,其實并不沖突,各有各的 優劣勢。
了解和學習它們的理念與優勢,合理地 設計融合,将優秀的軟體程式設計思想用于提升我們應用;
所有設計思想,最終的目标一定是使我們的應用更加 解耦顆粒化、易拓展、易測試、高複用,開發更為高效和安全;
有一些庫能讓大家很快地接觸和運用函數思想:
/
Underscore.js
/
Lodash/fp
等。
Rxjs
- (上篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠
- (中篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠
- (下篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠