React面試祕籍

轉自(中篇)中高級前端大廠面試祕籍,寒冬中爲您保駕護航,直通大廠

引言

你們知道,React 如今已經在前端開發中佔據了主導的地位。優異的性能,強大的生態,讓其沒法阻擋。博主面的 5 家公司,所有是 React 技術棧。據我所知,大廠也大部分以 React 做爲主技術棧。React 也成爲了面試中並不可少的一環。javascript

中篇主要從如下幾個方面對 React 展開闡述:css

原本是計劃只有上下兩篇,但是寫着寫着越寫越多,受限於篇幅,也爲了有更好的閱讀體驗,只好拆分出中篇,但願各位童鞋別介意。🙃,另外,下篇還有 Hybrid App / Webpack / 性能優化 / Nginx 等方面的知識,敬請期待。html

建議仍是先從上篇基礎開始哈~有個按部就班的過程: 面試上篇。🤑前端

進階知識

框架: React

React 也是現現在最流行的前端框架,也是不少大廠面試必備。React 與 Vue 雖有不一樣,但一樣做爲一款 UI 框架,雖然實現可能不同,但在一些理念上仍是有類似的,例如數據驅動、組件化、虛擬 dom 等。這裏就主要列舉一些 React 中獨有的概念。java

1. Fiber

React 的核心流程能夠分爲兩個部分:react

  • reconciliation (調度算法,也可稱爲 render):
    • 更新 state 與 props;
    • 調用生命週期鉤子;
    • 生成 virtual dom;
    • 經過新舊 vdom 進行 diff 算法,獲取 vdom change;
    • 肯定是否須要從新渲染
  • commit:
    • 如須要,則操做 dom 節點更新;

要了解 Fiber,咱們首先來看爲何須要它?git

  • 問題: 隨着應用變得愈來愈龐大,整個更新渲染的過程開始變得吃力,大量的組件渲染會致使主進程長時間被佔用,致使一些動畫或高頻操做出現卡頓和掉幀的狀況。而關鍵點,即是 同步阻塞。在以前的調度算法中,React 須要實例化每一個類組件,生成一顆組件樹,使用 同步遞歸 的方式進行遍歷渲染,而這個過程最大的問題就是沒法 暫停和恢復github

  • 解決方案: 解決同步阻塞的方法,一般有兩種: 異步任務分割。而 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
    	}	
    }
    複製代碼複製代碼
    • 鏈表樹遍歷算法: 經過 節點保存與映射,便可以隨時地進行 中止和重啓,這樣便能達到實現任務分割的基本前提;

      • 一、首先經過不斷遍歷子節點,到樹末尾;
      • 二、開始經過 sibling 遍歷兄弟節點;
      • 三、return 返回父節點,繼續執行2;
      • 四、直到 root 節點後,跳出遍歷;
    • 任務分割,React 中的渲染更新能夠分紅兩個階段:

      • reconciliation 階段: vdom 的數據對比,是個適合拆分的階段,好比對比一部分樹後,先暫停執行個動畫調用,待完成後再回來繼續比對。
      • Commit 階段: 將 change list 更新到 dom 上,不適合拆分,由於使用 vdom 的意義就是爲了節省傳說中最耗時的 dom 操做,把全部操做一次性更新,若是在這裏又拆分,那不是又懵了麼。🙃
    • 分散執行: 任務分割後,就能夠把小任務單元分散到瀏覽器的空閒期間去排隊執行,而實現的關鍵是兩個新API: requestIdleCallbackrequestAnimationFrame

      • 低優先級的任務交給requestIdleCallback處理,這是個瀏覽器提供的事件循環空閒期的回調函數,須要 pollyfill,並且擁有 deadline 參數,限制執行事件,以繼續切分任務;
      • 高優先級的任務交給requestAnimationFrame處理;
    // 相似於這樣的方式
    requestIdleCallback((deadline) => {
        // 當有空閒時間時,咱們執行一個組件渲染;
        // 把任務塞到一個個碎片時間中去;
        while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {
            nextComponent = performWork(nextComponent);
        }
    });
    複製代碼複製代碼
    • 優先級策略: 文本框輸入 > 本次調度結束需完成的任務 > 動畫過渡 > 交互反饋 > 數據更新 > 不會顯示但以防未來會顯示的任務

Tips:

Fiber 其實能夠算是一種編程思想,在其它語言中也有許多應用(Ruby Fiber)。當遇到進程阻塞的問題時,任務分割異步調用緩存策略 是三個顯著的解決思路。

2. 生命週期

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

  • 使用getDerivedStateFromProps 替換componentWillMount
  • 使用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() {} } 複製代碼複製代碼

  • 使用建議:

    • 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 以後, mounted 以前;
    • shouldComponentUpdate: 默認每次調用setState,必定會最終走到 diff 階段,但能夠經過shouldComponentUpdate的生命鉤子返回false來直接阻止後面的邏輯執行,一般是用於作條件渲染,優化渲染的性能。

3. setState

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

  • 事務 (Transaction):
    • 是 React 中的一個調用結構,用於包裝一個方法,結構爲: initialize - perform(method) - close。經過事務,能夠統一管理一個方法的開始與結束;處於事務流中,表示進程正在執行一些操做;

  • setState: React 中用於修改狀態,更新視圖。它具備如下特色:

  • 異步與同步: setState並非單純的異步或同步,這其實與調用時的環境相關:

    • 合成事件生命週期鉤子(除 componentDidUpdate) 中,setState是"異步"的;
      • 緣由: 由於在setState的實現中,有一個判斷: 當更新策略正在事務流的執行中時,該組件更新會被推入dirtyComponents隊列中等待執行;不然,開始執行batchedUpdates隊列更新;
        • 在生命週期鉤子調用中,更新策略都處於更新以前,組件仍處於事務流中,而componentDidUpdate是在更新以後,此時組件已經不在事務流中了,所以則會同步執行;
        • 在合成事件中,React 是基於 事務流完成的事件委託機制 實現,也是處於事務流中;
      • 問題: 沒法在setState後立刻從this.state上獲取更新後的值。
      • 解決: 若是須要立刻同步去獲取新值,setState實際上是能夠傳入第二個參數的。setState(updater, callback),在回調中便可獲取最新值;
    • 原生事件setTimeout 中,setState是同步的,能夠立刻獲取更新後的值;
      • 緣由: 原生事件是瀏覽器自己的實現,與事務流無關,天然是同步;而setTimeout是放置於定時器線程中延後執行,此時事務流已結束,所以也是同步;
  • 批量更新: 在 合成事件生命週期鉤子 中,setState更新隊列時,存儲的是 合併狀態(Object.assign)。所以前面設置的 key 值會被後面所覆蓋,最終只會執行一次更新;

  • 函數式: 因爲 Fiber 及 合併 的問題,官方推薦能夠傳入 函數 的形式。setState(fn),在fn中返回新的state對象便可,例如this.state((state, props) => newState);

    • 使用函數式,能夠用於避免setState的批量更新的邏輯,傳入的函數將會被 順序調用
  • 注意事項:

    • setState 合併,在 合成事件 和 生命週期鉤子 中屢次連續調用會被優化爲一次;
    • 當組件已被銷燬,若是再次調用setState,React 會報錯警告,一般有兩種解決辦法:
      • 將數據掛載到外部,經過 props 傳入,如放到 Redux 或 父級中;
      • 在組件內部維護一個狀態量 (isUnmounted),componentWillUnmount中標記爲 true,在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 (
      		      <div>
      				  <Comp {...this.props} />
      					<div style={{
      					  width: '100%',
      					  height: '100%',
      					  backgroundColor: 'rgba(0, 0, 0, .6)',
      				  }} 
      			  </div>
      		  )
      	  }
        }
      }
      複製代碼複製代碼
    • 反向繼承 (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 <Loading /> } else { return super.render() } } }; } 複製代碼複製代碼
        • 能夠直接修改被包裹組件渲染出的 React 元素樹
      • 操做狀態 (Operate State): 能夠直接經過 this.state 獲取到被包裹組件的狀態,並進行操做。但這樣的操做容易使 state 變得難以追蹤,不易維護,謹慎使用。

  • 應用場景:

    • 權限控制,經過抽象邏輯,統一對頁面進行權限判斷,按不一樣的條件進行頁面渲染:
    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. 避免用法污染: 理想狀態下,應透傳元組件的無關參數與事件,儘可能保證用法不變;
      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 / 解構 等方式建立新對象;
    2. 默認狀況下須要 返回原數據,避免數據被清空;
    3. 最好設置 初始值,便於應用的初始化及數據穩定;
  • 進階:

    • React-Redux: 結合 React 使用;
      • <Provider>: 將 store 經過 context 傳入組件中;
      • connect: 一個高階組件,能夠方便在 React 組件中使用 Redux;
          1. store經過mapStateToProps進行篩選後使用props注入組件
          1. 根據mapDispatchToProps建立方法,當組件調用時使用dispatch觸發對應的action
    • Reducer 的拆分與重構:
      • 隨着項目越大,若是將全部狀態的 reducer 所有寫在一個函數中,將會 難以維護
      • 能夠將 reducer 進行拆分,也就是 函數分解,最終再使用combineReducers()進行重構合併;
    • 異步 Action: 因爲 Reducer 是一個嚴格的純函數,所以沒法在 Reducer 中進行數據的請求,須要先獲取數據,再dispatch(Action)便可,下面是三種不一樣的異步實現:

6. React Hooks

React 中一般使用 類定義 或者 函數定義 建立組件:

在類定義中,咱們可使用到許多 React 特性,例如 state、 各類組件生命週期鉤子等,可是在函數定義中,咱們卻無能爲力,所以 React 16.8 版本推出了一個新功能 (React Hooks),經過它,能夠更好的在函數定義組件中使用 React 特性。

  • 好處:

    • 一、跨組件複用: 其實 render props / HOC 也是爲了複用,相比於它們,Hooks 做爲官方的底層 API,最爲輕量,並且改形成本小,不會影響原來的組件層次結構和傳說中的嵌套地獄;
    • 二、類定義更爲複雜:
      • 不一樣的生命週期會使邏輯變得分散且混亂,不易維護和管理;
      • 時刻須要關注this的指向問題;
      • 代碼複用代價高,高階組件的使用常常會使整個組件樹變得臃腫;
    • 三、狀態與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()
    <span class="hljs-comment">// 組件 update 時會執行事件解綁</span>
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
    	<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'off'</span>)
    	removeEventListener()
    }
    複製代碼

    }, [source]);

    複製代碼<span class="hljs-comment">// 組件 update 時會執行事件解綁</span> <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'off'</span>) removeEventListener() } 複製代碼// 每次 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()
      })
      複製代碼複製代碼
  • 其它內置鉤子:

    • useContext: 獲取 context 對象

    • useReducer: 相似於 Redux 思想的實現,但其並不足以替代 Redux,能夠理解成一個組件內部的 redux:

      • 並非持久化存儲,會隨着組件被銷燬而銷燬;
      • 屬於組件內部,各個組件是相互隔離的,單純用它並沒有法共享數據;
      • 配合useContext的全局性,能夠完成一個輕量級的 Redux;(easy-peasy)
    • useCallback: 緩存回調函數,避免傳入的回調每次都是新的函數實例而致使依賴組件從新渲染,具備性能優化的效果;

    • useMemo: 用於緩存傳入的 props,避免依賴的組件每次都從新渲染;

    • useRef: 獲取組件的真實節點;

    • useLayoutEffect:

      • DOM更新同步鉤子。用法與useEffect相似,只是區別於執行時間點的不一樣。
      • useEffect屬於異步執行,並不會等待 DOM 真正渲染後執行,而useLayoutEffect則會真正渲染後才觸發;
      • 能夠獲取更新後的 state;
  • 自定義鉤子(useXxxxx): 基於 Hooks 能夠引用其它 Hooks 這個特性,咱們能夠編寫自定義鉤子,如上面的useMounted。又例如,咱們須要每一個頁面自定義標題:

function useTitle(title) { useEffect( () => { document.title = title; }); }

// 使用: function Home() { const title = '我是首頁' useTitle(title)

<span class="hljs-keyword">return</span> (
	<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>{title}<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
)
複製代碼
複製代碼<span class="hljs-keyword">return</span> ( <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>{title}<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span> ) 複製代碼} 複製代碼複製代碼

7. SSR

SSR,俗稱 服務端渲染 (Server Side Render),講人話就是: 直接在服務端層獲取數據,渲染出完成的 HTML 文件,直接返回給用戶瀏覽器訪問。

  • 先後端分離: 前端與服務端隔離,前端動態獲取數據,渲染頁面。

  • 痛點:

    • 首屏渲染性能瓶頸:

      • 空白延遲: HTML下載時間 + JS下載/執行時間 + 請求時間 + 渲染時間。在這段時間內,頁面處於空白的狀態。
    • SEO 問題: 因爲頁面初始狀態爲空,所以爬蟲沒法獲取頁面中任何有效數據,所以對搜索引擎不友好。

      • 雖然一直有在提動態渲染爬蟲的技術,不過據我瞭解,大部分國內搜索引擎仍然是沒有實現。

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

  • 原理:

    • Node 服務: 讓先後端運行同一套代碼成爲可能。
    • Virtual Dom: 讓前端代碼脫離瀏覽器運行。
  • 條件: Node 中間層、 React / Vue 等框架。 結構大概以下:

  • 開發流程: (此處以 React + Router + Redux + Koa 爲例)

    • 一、在同個項目中,搭建 先後端部分,常規結構:

      • build
      • public
      • src
        • client
        • server
    • 二、server 中使用 Koa 路由監聽 頁面訪問:

    import * as Router from 'koa-router'

    const router = new Router() // 若是中間也提供 Api 層 router.use('/api/home', async () => { // 返回數據 })

    複製代碼router.get('*', async (ctx) => { // 返回 HTML }) 複製代碼複製代碼

    • 三、經過訪問 url 匹配 前端頁面路由:

    // 前端頁面路由 import { pages } from '../../client/app' import { matchPath } from 'react-router-dom' 複製代碼// 使用 react-router 庫提供的一個匹配方法 const matchPage = matchPath(ctx.req.url, page) 複製代碼複製代碼

    • 四、經過頁面路由的配置進行 數據獲取。一般能夠在頁面路由中增長 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) 複製代碼複製代碼
    • 五、建立 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)) 複製代碼複製代碼

    • 六、注入 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> ) 複製代碼複製代碼

    • 七、將 AppString 包裝成完整的 html 文件格式;

    • 八、此時,已經能生成完整的 HTML 文件。但只是個純靜態的頁面,沒有樣式沒有交互。接下來咱們就是要插入 JS 與 CSS。咱們能夠經過訪問前端打包後生成的asset-manifest.json文件來獲取相應的文件路徑,並一樣注入到 Html 中引用。

    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> `
    複製代碼複製代碼
    • 九、進行 數據脫水: 爲了把服務端獲取的數據同步到前端。主要是將數據序列化後,插入到 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:

    這裏比較特別的有兩點:

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

    2. 使用 json 進行傳輸,能夠得到更快的加載速度;

    • 十、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 等。

  • 常見的編程範式:

    • 命令式編程(過程化編程): 更關心解決問題的步驟,一步步以語言的形式告訴計算機作什麼;
    • 事件驅動編程: 事件訂閱與觸發,被普遍用於 GUI 的編程設計中;
    • 面向對象編程: 基於類、對象與方法的設計模式,擁有三個基礎概念: 封裝性、繼承性、多態性;
    • 函數式編程
      • 換成一種更高端的說法,面向數學編程。怕不怕~🥴
  • 函數式編程的理念:

    • 純函數(肯定性函數): 是函數式編程的基礎,可使程序變得靈活,高度可拓展,可維護;

      • 優點:

        • 徹底獨立,與外部解耦;
        • 高度可複用,在任意上下文,任意時間線上,均可執行而且保證結果穩定;
        • 可測試性極強;
      • 條件:

        • 不修改參數;
        • 不依賴、不修改任何函數外部的數據;
        • 徹底可控,參數同樣,返回值必定同樣: 例如函數不能包含new Date()或者Math.randon()等這種不可控因素;
        • 引用透明;
      • 咱們經常使用到的許多 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等,並不須要像咱們上面這樣本身實現;

    • 數據不可變性(immutable): 這是一種數據理念,也是函數式編程中的核心理念之一:

      • 倡導: 一個對象再被建立後便不會再被修改。當須要改變值時,是返回一個全新的對象,而不是直接在原對象上修改;
      • 目的: 保證數據的穩定性。避免依賴的數據被未知地修改,致使了自身的執行異常,能有效提升可控性與穩定性;
      • 並不等同於const。使用const建立一個對象後,它的屬性仍然能夠被修改;
      • 更相似於Object.freeze: 凍結對象,但freeze仍沒法保證深層的屬性不被串改;
      • immutable.js: js 中的數據不可變庫,它保證了數據不可變,在 React 生態中被普遍應用,大大提高了性能與穩定性;
        • trie數據結構:
          • 一種數據結構,能有效地深度凍結對象,保證其不可變;
          • 結構共享: 能夠共用不可變對象的內存引用地址,減小內存佔用,提升數據操做性能;
    • 避免不一樣函數之間的 狀態共享,數據的傳遞使用複製或全新對象,遵照數據不可變原則;

    • 避免從函數內部 改變外部狀態,例如改變了全局做用域或父級做用域上的變量值,可能會致使其它單位錯誤;

    • 避免在單元函數內部執行一些 反作用,應該將這些操做抽離成更獨立的工具單元;

      • 日誌輸出
      • 讀寫文件
      • 網絡請求
      • 調用外部進程
      • 調用有反作用的函數
  • 高階函數: 是指 以函數爲參數,返回一個新的加強函數 的一類函數,它一般用於:

    • 將邏輯行爲進行 隔離抽象,便於快速複用,如處理數據,兼容性等;
    • 函數組合,將一系列單元函數列表組合成功能更強大的函數;
    • 函數加強,快速地拓展函數功能,
  • 函數式編程的好處:

    • 函數反作用小,全部函數獨立存在,沒有任何耦合,複用性極高;
    • 不關注執行時間,執行順序,參數,命名等,能專一於數據的流動與處理,能有效提升穩定性與健壯性;
    • 追求單元化,粒度化,使其重構和改形成本下降,可維護、可拓展性較好;
    • 更易於作單元測試。
  • 總結:

    • 函數式編程實際上是一種編程思想,它追求更細的粒度,將應用拆分紅一組組極小的單元函數,組合調用操做數據流;
    • 它提倡着 純函數 / 函數複合 / 數據不可變, 謹慎對待函數內的 狀態共享 / 依賴外部 / 反作用;

Tips:

其實咱們很難也不須要在面試過程當中去完美地闡述出整套思想,這裏也只是淺嘗輒止,一些我的理解而已。博主也是初級小菜鳥,停留在表面而已,只求對你們能有所幫助,輕噴🤣;

我我的以爲: 這些編程範式之間,其實並不矛盾,各有各的 優劣勢

理解和學習它們的理念與優點,合理地 設計融合,將優秀的軟件編程思想用於提高咱們應用;

全部設計思想,最終的目標必定是使咱們的應用更加 解耦顆粒化、易拓展、易測試、高複用,開發更爲高效和安全

有一些庫能讓你們很快地接觸和運用函數思想: Underscore.js / Lodash/fp / Rxjs 等。

結語

到此,想必你們會發現已經開始深刻一些理論和原理層面了,並不像上篇那麼的淺顯易懂了。但這也是個必經之路,不可能永遠停留在 5分鐘掌握的技術 上。再也不停留在語言的表面,而是理解更深刻的原理,模式,架構,因果,你就會忽然發現你成爲高級軟件工程師了。😁。

但願各位小夥伴能沉下心來,一些理論、概念雖然枯燥,但反覆琢磨後再本身實踐嘗試下,就能有本身的理解。

當你開始面試高級工程師時,面試官便再也不重點關注你會不會寫stopPropagation或者會不會水平居中了,而是更在意你本身的思考和研究能力了。表現出本身深刻理解研究的成果,定會讓面試官另眼相看。

相關文章
相關標籤/搜索