React高頻面試題梳理,看看怎麼答?(上)

我真的好想學習啊發表於2019-09-24

前段時間準備面試,總結了很多,下面是我在準備React面試時,結合自己的實際面試經歷,以及我以前原始碼分析的文章,總結出來的一些 React高頻面試題目。

以前我寫的原始碼分析的文章,並沒有很多人看,因為大部分情況下你不需要深入原始碼也能懂得其中原理,並解決實際問題,這也是我總結這些面試題的原因,讓你在更短的時間內獲得更大的收益。

由於是以面試題的角度來討論,所以某些點可能不能非常深入,我在問題下面都貼了相關連結,如果想深入理解,請點選這些文章。

由於題目較多,分為上、下兩篇,本篇文章我們先來討論如下19個題目:

  • React生命週期有哪些,16版本生命週期發生了哪些變化?

  • setState是同步的還是非同步的?

  • 為什麼有時連續多次 setState只有一次生效?

  • React如何實現自己的事件機制?

  • 為何 React事件要自己繫結 this

  • 原生事件和 React事件的區別?

  • React的合成事件是什麼?

  • React和原生事件的執行順序是什麼?可以混用嗎?

  • 虛擬Dom是什麼?

  • 虛擬Dom普通Dom更快嗎?

  • 虛擬Dom中的 $$typeof屬性的作用是什麼?

  • React元件的渲染流程是什麼?

  • 為什麼程式碼中一定要引入 React

  • 為什麼 React元件首字母必須大寫?

  • React在渲染 真實Dom時做了哪些效能優化?

  • 什麼是高階元件?如何實現?

  • HOC在業務場景中有哪些實際應用場景?

  • 高階元件( HOC)和 Mixin的異同點是什麼?

  • Hook有哪些優勢?

React生命週期有哪些,16版本生命週期發生了哪些變化?

15生命週期

React高頻面試題梳理,看看怎麼答?(上)

  • 初始化階段

    • constructor 建構函式

    • getDefaultProps props預設值

    • getInitialState state預設值

  • 掛載階段

    • componentWillMount 元件初始化渲染前呼叫

    • render 元件渲染

    • componentDidMount元件掛載到 DOM後呼叫

  • 更新階段

    • componentWillReceiveProps 元件將要接收新 props前呼叫

    • shouldComponentUpdate 元件是否需要更新

    • componentWillUpdate 元件更新前呼叫

    • render 元件渲染

    • componentDidUpdate 元件更新後呼叫

  • 解除安裝階段

    • componentWillUnmount 元件解除安裝前呼叫

16生命週期

React高頻面試題梳理,看看怎麼答?(上)

  • 初始化階段

    • constructor 建構函式

    • getDefaultProps props預設值

    • getInitialState state預設值

  • 掛載階段

    • staticgetDerivedStateFromProps(props,state)

    • render

    • componentDidMount

getDerivedStateFromProps:元件每次被 rerender的時候,包括在元件構建之後(虛擬 dom之後,實際 dom掛載之前),每次獲取新的 propsstate之後;每次接收新的props之後都會返回一個物件作為新的 state,返回null則說明不需要更新 state;配合 componentDidUpdate,可以覆蓋 componentWillReceiveProps的所有用法

  • 更新階段

    • staticgetDerivedStateFromProps(props,state)

    • shouldComponentUpdate

    • render

    • getSnapshotBeforeUpdate(prevProps,prevState)

    • componentDidUpdate

getSnapshotBeforeUpdate:觸發時間: update發生的時候,在 render之後,在元件 dom渲染之前;返回一個值,作為 componentDidUpdate的第三個引數;配合 componentDidUpdate, 可以覆蓋 componentWillUpdate的所有用法

  • 解除安裝階段

    • componentWillUnmount

  • 錯誤處理

    • componentDidCatch

React16新的生命週期棄用了 componentWillMount、componentWillReceivePorps,componentWillUpdate新增了 getDerivedStateFromProps、getSnapshotBeforeUpdate來代替棄用的三個鉤子函式。

React16並沒有刪除這三個鉤子函式,但是不能和新增的鉤子函式混用, React17將會刪除這三個鉤子函式,新增了對錯誤的處理( componentDidCatch

setState是同步的還是非同步的?

  • 生命週期和合成事件中

React的生命週期和合成事件中, React仍然處於他的更新機制中,這時無論呼叫多少次 setState,都會不會立即執行更新,而是將要更新的·存入 _pendingStateQueue,將要更新的元件存入 dirtyComponent

當上一次更新機制執行完畢,以生命週期為例,所有元件,即最頂層元件 didmount後會將批處理標誌設定為 false。這時將取出 dirtyComponent中的元件以及 _pendingStateQueue中的 state進行更新。這樣就可以確保元件不會被重新渲染多次。

  componentDidMount() {    this.setState({      index: this.state.index + 1    })    console.log('state', this.state.index);  }複製程式碼

所以,如上面的程式碼,當我們在執行 setState後立即去獲取 state,這時是獲取不到更新後的 state的,因為處於 React的批處理機制中, state被暫存起來,待批處理機制完成之後,統一進行更新。

所以。setState本身並不是非同步的,而是 React的批處理機制給人一種非同步的假象。

  • 非同步程式碼和原生事件中

  componentDidMount() {    setTimeout(() => {      console.log('呼叫setState');      this.setState({        index: this.state.index + 1      })      console.log('state', this.state.index);    }, 0);  }複製程式碼

如上面的程式碼,當我們在非同步程式碼中呼叫 setState時,根據 JavaScript的非同步機制,會將非同步程式碼先暫存,等所有同步程式碼執行完畢後在執行,這時 React的批處理機制已經走完,處理標誌設被設定為 false,這時再呼叫 setState即可立即執行更新,拿到更新後的結果。

在原生事件中呼叫 setState並不會出發 React的批處理機制,所以立即能拿到最新結果。

  • 最佳實踐

setState的第二個引數接收一個函式,該函式會在 React的批處理機制完成之後呼叫,所以你想在呼叫 setState後立即獲取更新後的值,請在該回撥函式中獲取。

   this.setState({ index: this.state.index + 1 }, () => {      console.log(this.state.index);    })複製程式碼

推薦閱讀:由實際問題探究setState的執行機制

為什麼有時連續多次setState只有一次生效?

例如下面的程式碼,兩次列印出的結果是相同的:

  componentDidMount() {    this.setState({ index: this.state.index + 1 }, () => {      console.log(this.state.index);    })    this.setState({ index: this.state.index + 1 }, () => {      console.log(this.state.index);    })  }複製程式碼

原因就是 React會批處理機制中儲存的多個 setState進行合併,來看下 React原始碼中的 _assign函式,類似於 Objectassign

 _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);複製程式碼

如果傳入的是物件,很明顯會被合併成一次,所以上面的程式碼兩次列印的結果是相同的:

Object.assign(  nextState,  {index: state.index+ 1},  {index: state.index+ 1})複製程式碼

注意, assign函式中對函式做了特殊處理,處理第一個引數傳入的是函式,函式的引數 preState是前一次合併後的結果,所以計算結果是準確的:

  componentDidMount() {    this.setState((state, props) => ({        index: state.index + 1    }), () => {      console.log(this.state.index);    })    this.setState((state, props) => ({        index: state.index + 1    }), () => {      console.log(this.state.index);    })  }複製程式碼

所以上面的程式碼兩次列印的結果是不同的。

  • 最佳實踐

React會對多次連續的 setState進行合併,如果你想立即使用上次 setState後的結果進行下一次 setState,可以讓 setState 接收一個函式而不是一個物件。這個函式用上一個 state 作為第一個引數,將此次更新被應用時的 props 做為第二個引數。

React如何實現自己的事件機制?

React事件並沒有繫結在真實的 Dom節點上,而是通過事件代理,在最外層的 document上對事件進行統一分發。

React高頻面試題梳理,看看怎麼答?(上)

元件掛載、更新時:

  • 通過 lastPropsnextProps判斷是否新增、刪除事件分別呼叫事件註冊、解除安裝方法。

  • 呼叫 EventPluginHubenqueuePutListener進行事件儲存

  • 獲取 document物件。

  • 根據事件名稱(如 onClickonCaptureClick)判斷是進行冒泡還是捕獲。

  • 判斷是否存在 addEventListener方法,否則使用 attachEvent(相容IE)。

  • document註冊原生事件回撥為 dispatchEvent(統一的事件分發機制)。

事件初始化:

  • EventPluginHub負責管理 React合成事件的 callback,它將 callback儲存在 listenerBank中,另外還儲存了負責合成事件的 Plugin

  • 獲取繫結事件的元素的唯一標識 key

  • callback根據事件型別,元素的唯一標識 key儲存在 listenerBank中。

  • listenerBank的結構是: listenerBank[registrationName][key]

觸發事件時:

  • 觸發 document註冊原生事件的回撥 dispatchEvent

  • 獲取到觸發這個事件最深一級的元素

  • 遍歷這個元素的所有父元素,依次對每一級元素進行處理。

  • 構造合成事件。

  • 將每一級的合成事件儲存在 eventQueue事件佇列中。

  • 遍歷 eventQueue

  • 通過 isPropagationStopped判斷當前事件是否執行了阻止冒泡方法。

  • 如果阻止了冒泡,停止遍歷,否則通過 executeDispatch執行合成事件。

  • 釋放處理完成的事件。

React在自己的合成事件中重寫了 stopPropagation方法,將 isPropagationStopped設定為 true,然後在遍歷每一級事件的過程中根據此遍歷判斷是否繼續執行。這就是 React自己實現的冒泡機制。

推薦閱讀:【React深入】React事件機制

為何React事件要自己繫結this?

在上面提到的事件處理流程中, Reactdocument上進行統一的事件分發, dispatchEvent通過迴圈呼叫所有層級的事件來模擬事件冒泡和捕獲。

React原始碼中,當具體到某一事件處理函式將要呼叫時,將呼叫 invokeGuardedCallback方法。

function invokeGuardedCallback(name, func, a) {  try {    func(a);  } catch (x) {    if (caughtError === null) {      caughtError = x;    }  }}複製程式碼

可見,事件處理函式是直接呼叫的,並沒有指定呼叫的元件,所以不進行手動繫結的情況下直接獲取到的 this是不準確的,所以我們需要手動將當前元件繫結到 this上。

原生事件和React事件的區別?

  • React 事件使用駝峰命名,而不是全部小寫。

  • 通過 JSX , 你傳遞一個函式作為事件處理程式,而不是一個字串。

  • React 中你不能通過返回 false 來阻止預設行為。必須明確呼叫 preventDefault

React的合成事件是什麼?

React 根據 W3C 規範定義了每個事件處理函式的引數,即合成事件。

事件處理程式將傳遞 SyntheticEvent 的例項,這是一個跨瀏覽器原生事件包裝器。它具有與瀏覽器原生事件相同的介面,包括 stopPropagation()preventDefault(),在所有瀏覽器中他們工作方式都相同。

React合成的 SyntheticEvent採用了事件池,這樣做可以大大節省記憶體,而不會頻繁的建立和銷燬事件物件。

另外,不管在什麼瀏覽器環境下,瀏覽器會將該事件型別統一建立為合成事件,從而達到了瀏覽器相容的目的。

React和原生事件的執行順序是什麼?可以混用嗎?

React的所有事件都通過 document進行統一分發。當真實 Dom觸發事件後冒泡到 document後才會對 React事件進行處理。

所以原生的事件會先執行,然後執行 React合成事件,最後執行真正在 document上掛載的事件

React事件和原生事件最好不要混用。原生事件中如果執行了 stopPropagation方法,則會導致其他 React事件失效。因為所有元素的事件將無法冒泡到 document上,導致所有的 React事件都將無法被觸發。。

虛擬Dom是什麼?

React高頻面試題梳理,看看怎麼答?(上)

在原生的 JavaScript程式中,我們直接對 DOM進行建立和更改,而 DOM元素通過我們監聽的事件和我們的應用程式進行通訊。

React會先將你的程式碼轉換成一個 JavaScript物件,然後這個 JavaScript物件再轉換成真實 DOM。這個 JavaScript物件就是所謂的虛擬 DOM

當我們需要建立或更新元素時, React首先會讓這個 VitrualDom物件進行建立和更改,然後再將 VitrualDom物件渲染成真實DOM。

當我們需要對 DOM進行事件監聽時,首先對 VitrualDom進行事件監聽, VitrualDom會代理原生的 DOM事件從而做出響應。

推薦閱讀:【React深入】深入分析虛擬DOM的渲染過程和特性

虛擬Dom比普通Dom更快嗎?

很多文章說 VitrualDom可以提升效能,這一說法實際上是很片面的。

直接操作 DOM是非常耗費效能的,這一點毋庸置疑。但是 React使用 VitrualDom也是無法避免操作 DOM的。

如果是首次渲染, VitrualDom不具有任何優勢,甚至它要進行更多的計算,消耗更多的記憶體。

VitrualDom的優勢在於 ReactDiff演算法和批處理策略, React在頁面更新之前,提前計算好了如何進行更新和渲染 DOM。實際上,這個計算過程我們在直接操作 DOM時,也是可以自己判斷和實現的,但是一定會耗費非常多的精力和時間,而且往往我們自己做的是不如 React好的。所以,在這個過程中 React幫助我們"提升了效能"。

所以,我更傾向於說, VitrualDom幫助我們提高了開發效率,在重複渲染時它幫助我們計算如何更高效的更新,而不是它比 DOM操作更快。

虛擬Dom中的$$typeof屬性的作用是什麼?

ReactElement中有一個 $$typeof屬性,它被賦值為 REACT_ELEMENT_TYPE

var REACT_ELEMENT_TYPE =  (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||  0xeac7;複製程式碼

可見, $$typeof是一個 Symbol型別的變數,這個變數可以防止 XSS

如果你的伺服器有一個漏洞,允許使用者儲存任意 JSON物件, 而客戶端程式碼需要一個字串,這可能會成為一個問題:

// JSONlet expectedTextButGotJSON = {  type: 'div',  props: {    dangerouslySetInnerHTML: {      __html: '/* put your exploit here */'    },  },};let message = { text: expectedTextButGotJSON };<p>  {message.text}</p>複製程式碼

JSON中不能儲存 Symbol型別的變數。

ReactElement.isValidElement函式用來判斷一個 React元件是否是有效的,下面是它的具體實現。

ReactElement.isValidElement = function (object) {  return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;};複製程式碼

可見 React渲染時會把沒有 $$typeof標識,以及規則校驗不通過的元件過濾掉。

當你的環境不支援 Symbol時, $$typeof被賦值為 0xeac7,至於為什麼, React開發者給出了答案:

0xeac7看起來有點像 React

React元件的渲染流程是什麼?


  • 使用 React.createElementJSX編寫 React元件,實際上所有的 JSX程式碼最後都會轉換成 React.createElement(...)Babel幫助我們完成了這個轉換的過程。



  • createElement函式對 keyref等特殊的 props進行處理,並獲取 defaultProps對預設 props進行賦值,並且對傳入的孩子節點進行處理,最終構造成一個 ReactElement物件(所謂的虛擬 DOM)。



  • ReactDOM.render將生成好的虛擬 DOM渲染到指定容器上,其中採用了批處理、事務等機制並且對特定瀏覽器進行了效能優化,最終轉換為真實 DOM


為什麼程式碼中一定要引入React?

JSX只是為 React.createElement(component,props,...children)方法提供的語法糖。

所有的 JSX程式碼最後都會轉換成 React.createElement(...)Babel幫助我們完成了這個轉換的過程。

所以使用了 JSX的程式碼都必須引入 React

為什麼React元件首字母必須大寫?

babel在編譯時會判斷 JSX中元件的首字母,當首字母為小寫時,其被認定為原生 DOM標籤, createElement的第一個變數被編譯為字串;當首字母為大寫時,其被認定為自定義元件, createElement的第一個變數被編譯為物件;

React在渲染真實Dom時做了哪些效能優化?

IE(8-11)Edge瀏覽器中,一個一個插入無子孫的節點,效率要遠高於插入一整個序列化完整的節點樹。

React通過 lazyTree,在 IE(8-11)Edge中進行單個節點依次渲染節點,而在其他瀏覽器中則首先將整個大的 DOM結構構建好,然後再整體插入容器。

並且,在單獨渲染節點時, React還考慮了 fragment等特殊節點,這些節點則不會一個一個插入渲染。

什麼是高階元件?如何實現?

高階元件可以看作 React對裝飾模式的一種實現,高階元件就是一個函式,且該函式接受一個元件作為引數,並返回一個新的元件。

高階元件( HOC)是 React中的高階技術,用來重用元件邏輯。但高階元件本身並不是 ReactAPI。它只是一種模式,這種模式是由 React自身的組合性質必然產生的。

function visible(WrappedComponent) {  return class extends Component {    render() {      const { visible, ...props } = this.props;      if (visible === false) return null;      return <WrappedComponent {...props} />;    }  }}複製程式碼

上面的程式碼就是一個 HOC的簡單應用,函式接收一個元件作為引數,並返回一個新元件,新組建可以接收一個 visible props,根據 visible的值來判斷是否渲染Visible。

React高頻面試題梳理,看看怎麼答?(上)

我們可以通過以下兩種方式實現高階元件:

屬性代理

函式返回一個我們自己定義的元件,然後在 render中返回要包裹的元件,這樣我們就可以代理所有傳入的 props,並且決定如何渲染,實際上 ,這種方式生成的高階元件就是原元件的父元件,上面的函式 visible就是一個 HOC屬性代理的實現方式。

function proxyHOC(WrappedComponent) {  return class extends Component {    render() {      return <WrappedComponent {...this.props} />;    }  }}複製程式碼

對比原生元件增強的項:

  • 可操作所有傳入的 props

  • 可操作元件的生命週期

  • 可操作元件的 static方法

  • 獲取 refs

反向繼承

返回一個元件,繼承原元件,在 render中呼叫原元件的 render。由於繼承了原元件,能通過this訪問到原元件的 生命週期、props、state、render等,相比屬性代理它能操作更多的屬性。

function inheritHOC(WrappedComponent) {  return class extends WrappedComponent {    render() {      return super.render();    }  }}複製程式碼

對比原生元件增強的項:

  • 可操作所有傳入的 props

  • 可操作元件的生命週期

  • 可操作元件的 static方法

  • 獲取 refs

  • 可操作 state

  • 可以渲染劫持

推薦閱讀:【React深入】從Mixin到HOC再到Hook

HOC在業務場景中有哪些實際應用場景?

HOC可以實現的功能:

  • 組合渲染

  • 條件渲染

  • 操作 props

  • 獲取 refs

  • 狀態管理

  • 操作 state

  • 渲染劫持

HOC在業務中的實際應用場景:

  • 日誌打點

  • 許可權控制

  • 雙向繫結

  • 表單校驗

具體實現請參考我這篇文章:https://juejin.im/post/5cad39b3f265da03502b1c0a

高階元件(HOC)和Mixin的異同點是什麼?

MixinHOC都可以用來解決 React的程式碼複用問題。

React高頻面試題梳理,看看怎麼答?(上)

圖片來源於網路

  • Mixin 可能會相互依賴,相互耦合,不利於程式碼維護

  • 不同的 Mixin中的方法可能會相互衝突

  • Mixin非常多時,元件是可以感知到的,甚至還要為其做相關處理,這樣會給程式碼造成滾雪球式的複雜性

HOC的出現可以解決這些問題:

  • 高階元件就是一個沒有副作用的純函式,各個高階元件不會互相依賴耦合

  • 高階元件也有可能造成衝突,但我們可以在遵守約定的情況下避免這些行為

  • 高階元件並不關心資料使用的方式和原因,而被包裹的元件也不關心資料來自何處。高階元件的增加不會為原元件增加負擔

Hook有哪些優勢?

  • 減少狀態邏輯複用的風險

HookMixin在用法上有一定的相似之處,但是 Mixin引入的邏輯和狀態是可以相互覆蓋的,而多個 Hook之間互不影響,這讓我們不需要在把一部分精力放在防止避免邏輯複用的衝突上。在不遵守約定的情況下使用 HOC也有可能帶來一定衝突,比如 props覆蓋等等,使用 Hook則可以避免這些問題。

  • 避免地獄式巢狀

大量使用 HOC的情況下讓我們的程式碼變得巢狀層級非常深,使用 HOC,我們可以實現扁平式的狀態邏輯複用,而避免了大量的元件巢狀。

  • 讓元件更容易理解

在使用 class元件構建我們的程式時,他們各自擁有自己的狀態,業務邏輯的複雜使這些元件變得越來越龐大,各個生命週期中會呼叫越來越多的邏輯,越來越難以維護。使用 Hook,可以讓你更大限度的將公用邏輯抽離,將一個元件分割成更小的函式,而不是強制基於生命週期方法進行分割。

  • 使用函式代替class

相比函式,編寫一個 class可能需要掌握更多的知識,需要注意的點也越多,比如 this指向、繫結事件等等。另外,計算機理解一個 class比理解一個函式更快。Hooks讓你可以在 classes之外使用更多 React的新特性。

下篇預告:

  • ReactDiff演算法的策略是什麼?

  • Reactkey的作用是什麼?

  • ReactFiber是什麼?為什麼要引入?

  • 為什麼推薦在 componentDidMount中發起網路請求?

  • React程式碼優化?

  • React元件設計要掌握哪些原則?

  • Redux的核心原理是什麼?

  • 什麼是 Redux中介軟體?

  • Reduxconnect函式的實現策略?

  • Mox的核心原理是什麼?

  • ReduxMobx的異同點,如何選擇?


轉載 檢視原文


相關文章