react常見面試題總結

react核心思想

簡單來講,就是virtual dom & react diff。
咱們都知道在前端開發中,js運行很快,dom操做很慢,而react充分利用了這個前提。在react中render的執行結果是樹形結構的javascript對象,當數據(state || props)發生變化時,會生成一個新的樹形結構的javascript對象,這兩個javascript對象咱們能夠稱之爲virtual dom。而後對比兩個virtual dom,找出最小的有變化的點,這個對比的過程咱們稱之爲react diff,將這個變化的部分(patch)加入到一個隊列中,最終批量更新這些patch到dom中。javascript

react執行render和setState進行渲染時主要有兩個階段

  • 調度階段(Reconciler):React 會自頂向下經過遞歸, 用新數據生成一顆新樹,遍歷虛擬dom,diff新老virtual dom樹,蒐集具體的UI差別,找到須要更新的元素(Patch),放到更新隊列中。
  • 渲染階段(Renderer):遍歷更新隊列,經過調用宿主環境的API(好比 DOM、Native、WebGL)實際更新渲染對應元素。

引入虛擬dom的好處是什麼?

  • js運行很快,dom操做很慢。配合react diff算法,經過對比virtual Dom,能夠快速找出真實dom的最小變化,這樣前端實際上是不須要去關注那個變化的點,把這個變化交給react來作就好,同時你也沒必要本身去完成屬性操做、事件處理、DOM更新,React會替你完成這一切,這讓咱們更關注咱們的業務邏輯而非DOM操做,基於以上兩點可大大提高咱們的開發效率。
  • 跨瀏覽器、跨平臺兼容。react基於virtual dom本身實現了一套本身的事件機制,本身模擬了事件冒泡和捕獲的過程,採用了事件代理,批量更新等方法,抹平了各個瀏覽器的事件兼容性問題。跨平臺virtual dom爲React帶來了跨平臺渲染的能力。以React Native爲例子。React根據virtual dom畫出相應平臺的ui層,只不過不一樣平臺畫的姿式不一樣而已。

react對性能的提高

關於提高性能,不少人說virtual dom能夠提高性能,這一說法其實是很片面的。由於咱們知道,直接操做dom是很是耗費性能的,可是即便咱們用了react,最終依然要去操做真實的dom。而react幫咱們作的事情就是儘可能用最佳的方式有操做dom。若是是首次渲染,virtual dom不具備任何優點,甚至它要進行更多的計算,消耗更多的內存。
react自己的優點在於react diff算法和批處理策略。react在頁面更新以前,提早計算好了如何進行更新和渲染DOM,實際上,這個計算過程咱們在直接操做DOM時,也是能夠本身判斷和實現的,可是必定會耗費很是多的精力和時間,並且每每咱們本身作的是不如React好的。因此,在這個過程當中React幫助咱們"提高了性能"。
因此,我更傾向於說,virtual dom幫助咱們提升了開發效率,在重複渲染時它幫助咱們計算如何更高效的更新,而不是它比DOM操做更快。html

什麼是jsx?

咱們在實現一個React組件時能夠選擇兩種編碼方式,第一種是使用JSX編寫,第二種是直接使用React.createElement編寫。實際上,上面兩種寫法是等價的,jsx只是爲React.createElemen方法的語法糖,最終全部的jsx都會被babel轉換成React.createElement。
可是請注意,babel在編譯時會判斷jsx中組件的首字母,當首字母爲小寫時,其被認定爲原生dom標籤,createElement的第一個變量被編譯爲字符串。當首字母爲大寫時,其被認定爲自定義組件,createElement的第一個變量被編譯爲對象。前端

react的生命週期是怎樣的?

在react16中,廢棄了三個will屬性componentWillMount,componentWillReceiveProps,comonentWillUpdate,可是目前還未刪除,react17計劃會刪除,同時經過UNSAFF_前綴向前兼容。
在 React 中,咱們能夠將其生命週期分爲三個階段。java

掛載階段

  • constructor()
    組件在掛載前,會調用它的構造函數,在構造函數內部必須執行一次super(props),不然不能在constructor內部使用this,constructor一般用於給this.state初始化內部狀態,爲事件處理函數綁定this。
  • static getDerivedStateFromProps(newProps,prevState)
    是一個靜態方法,父組件傳入的newProps和當前組件的prevState進行比較,判斷時須要更新state,返回值用做更新state,若是不須要則返回null。在render()方法以前調用,而且在初始掛載和後續更新時調用。
  • render()
    render()是組件中惟一必須實現的方法。須要返回如下類型,React元素、數組、fragments、Portals、字符串或者、值類型、布爾類型或null。同時render函數應該是純函數。不可以調用setState。
  • componentDidMount()

更新階段

  • static getDerivedStateFromProps(props,state)
  • shouldComponentUpate()
    當props或者state發生變化時,會在渲染前調用。根據父組件的props和當前的state進行對比,返回true/false。決定是否觸發後續的 UNSAFE_componentWillUpdate(),render()和componentDidUpdate()。。
  • render()
  • getSnapshotBeforeUpdate(prevProps,prevSteate)
    在render()以後componentDidUpdate()以前調用。此方法的返回值(snaphot)可做爲componentDidUpdate()的第三個參數使用。如不須要返回值則直接返回null。
  • componentDidUpdate(prevProps, prevState, snapshot)
    該方法會在更新完成後當即調用。首次渲染不會執行此方法,當組件更新後,能夠在此處對dom進行操做。能夠在此階段使用setState,觸發render()但必須包裹在一個條件語句裏,以免死循環。

卸載階段

  • componentWillUnmount()
    會在組件卸載和銷燬以前直接調用。此方法主要用來執行一些清理工做,例如:定時器,清除事件綁定,取消網絡請求。此階段不能調用setState,由於組件永遠不會從新渲染。

react diff解決什麼問題?是怎樣的實現思路?

react diff會幫助咱們計算出virtual dom中真正變化的部分,並只針對該部分進行實際dom操做,而非從新渲染整個頁面,從而保證了每次操做更新後頁面的高效渲染。傳統diff算法經過循環遞歸對節點進行依次對比,效率低下,算法複雜度達到 O(n^3)。react diff基於一下三個策略實現了O(n)的算法複雜度。react

  • Web UI中dom節點跨層級的移動操做特別少,能夠忽略不計。
  • 擁有相同類的兩個組件將會生成類似的樹形結構,擁有不一樣類的兩個組件將會生成不一樣的樹形結構。
  • 對於同一層級的一組子節點,它們能夠經過惟一id進行區分。

基於以上三個前提策略,React分別對tree diff、component diff以及element diff 進行算法優化,事實也證實這三個前提策略是合理且準確的,它保證了總體界面構建的性能。git

react中key的做用,能不能用index做爲Key。

首先說一下element diff的過程。好比有老的集合(A,B,C,D)和新的集合(B,A,D,C),咱們考慮在不增長空間複雜度的狀況下如何以O(n)的時間複雜度找出老集合中須要移動的元素。
在react裏的思路是這樣的,遍歷新集合,初始化lastIndex=0(表明訪問過的老集合中最右側的位置),表達式爲max(prev.mountIndex, lastIndex),若是當前節點在老集合中的位置即(prev.mountIndex)比lastIndex大說明當前訪問節點在老集合中就比上一個節點位置靠後則該節點不會影響其餘節點的位置,所以不用添加到差別隊列中,即不執行移動操做,只有當訪問的節點比 lastIndex 小時,才須要進行移動操做。
部分源碼爲github

var lastIndex = 0;
var nextIndex = 0;
for (name in nextChildren) {
    var prevChild = prevChildren && prevChildren[name]; // 老節點
    var nextChild = nextChildren[name]; // 新節點
    if (prevChild === nextChild) { // 若是新節點存在老節點集合裏
      // 移動節點
      this.moveChild(prevChild, nextIndex, lastIndex);
      lastIndex = Math.max(prevChild._mountIndex, lastIndex);
      prevChild._mountIndex = nextIndex;
    } else {
      if (prevChild) { // 若是不存在在
        lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        // 刪除節點
        this._unmountChild(prevChild);
      }
      // 初始化並建立節點
      this._mountChildAtIndex(
        nextChild, nextIndex, transaction, context
      );
    }
    nextIndex++;
}

// 移動節點
moveChild: function(child, toIndex, lastIndex) {
  if (child._mountIndex < lastIndex) {
    this.prepareToManageChildren();
    enqueueMove(this, child._mountIndex, toIndex);
  }
}

React 16有哪些新特性?

  • render支持返回數組和字符串
  • Error Boundaries
  • createPortal
  • rollup減少文件體積
  • fiber
  • Fragment
  • createRef
  • Strict Mode

React Fiber是什麼?解決什麼問題?

React Fiber是React對核心算法的一次從新實現。
在協調階段階段,之前因爲是採用的遞歸的遍歷方式,這種也被稱爲Stack Reconciler,主要是爲了區別Fiber Reconciler取的一個名字。這種方式有一個特色: 一旦任務開始進行,就沒法中斷,那麼js將一直佔用主線程,一直要等到整棵virtual dom樹計算完成以後,才能把執行權交給渲染引擎,那麼這就會致使一些用戶交互、動畫等任務沒法當即獲得處理,就會有卡頓,很是的影響用戶體驗。
頁面是一幀一幀繪製出來的,當每秒繪製的幀數(FPS)達到60時,頁面是流暢的,小於這個值時,用戶會感受到卡頓。1秒60幀,因此每一幀分到的時間是1000/60 ≈ 16ms。因此咱們書寫代碼時力求不讓一幀的工做量超過 16ms。若是任意一個步驟所佔用的時間過長,超過16ms了以後,用戶就能看到卡頓。web

Fiber如何實現

簡單來講就是時間分片 + 鏈表結構。而fiber就是維護每個分片的數據結構。
Fiber利用分片的思想,把一個耗時長的任務分紅不少小片,每個小片的運行時間很短,在每一個小片執行完以後,就把控制權交還給React負責任務協調的模塊,若是有緊急任務就去優先處理,若是沒有就繼續更新,這樣就給其餘任務一個執行的機會,惟一的線程就不會一直被獨佔。
所以,在組件更新時有可能一個更新任務尚未完成,就被另外一個更高優先級的更新過程打斷,優先級高的更新任務會優先處理完,而低優先級更新任務所作的工做則會徹底做廢,而後等待機會重頭再來。因此 React Fiber把一個更新過程分爲兩個階段:算法

  • 第一個階段 Reconciliation Phase,Fiber會找出須要更新的DOM,這個階段是能夠被打斷的。
  • 第二個階段 Commit Phase,是沒法別打斷,完成dom的更新並展現。

什麼是高階組件

高階組件(HOC)是React中用於複用組件邏輯的一種高級技巧。HOC自身不是React API的一部分,它是一種基於 React 的組合特性而造成的設計模式。具體而言,高階組件是參數爲組件,返回值爲新組件的函數。
請注意,HOC 不會修改傳入的組件,也不會使用繼承來複制其行爲。相反,HOC 經過將組件包裝在容器組件中來組成新組件。HOC 是純函數,沒有反作用。
我理解的高階組件是,將組件以參數的方式傳遞給另一個函數,在該函數中,對組件進行包裝,封裝了一些公用的組件邏輯,實現組件的邏輯複用,該函數被稱爲高階組件。可是請注意,高階組件不該修改傳入的組件行爲。
屬性代理segmentfault

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        user: currentLoggedInUser
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

反向繼承

function hoc(ComponentClass) {
    return class HOC extends ComponentClass {
        render() {
            if (this.state.success) {
                return super.render()
            }
            return <div>Loading...</div>
        }
    }
}

export default class ComponentClass extends Component {
    state = {
        success: false,
        data: null
    };
    async componentDidMount() {
        const result = await fetch(...請求);          
     this.setState({
            success: true,
            data: result.data
        });
    }
    render() {
        return <div>主要內容</div>
    }
}

什麼是渲染屬性

術語 「render prop」 是指一種技術,用於使用一個值爲函數的 prop 在 React 組件之間的代碼共享。
帶有渲染屬性(Render Props)的組件須要一個返回 React 元素並調用它的函數,而不是實現本身的渲染邏輯。
我理解的渲染屬性是,提供渲染頁面的props給子組件,共享能夠共享子組件的狀態,複用子組件的狀態,並告訴子組件如何進行渲染。

import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
// 與 HOC 不一樣,咱們可使用具備 render prop 的普通組件來共享代碼
class Mouse extends React.Component {
  static propTypes = {
    render: PropTypes.func.isRequired
  }
  state = { x: 0, y: 0 }
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  }
  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    )
  }
}
const App = React.createClass({
  render() {
    return (
      <div style={{ height: '100%' }}>
        <Mouse render={({ x, y }) => (
          // render prop 給了咱們所須要的 state 來渲染咱們想要的
          <h1>The mouse position is ({x}, {y})</h1>
        )}/>
      </div>
    )
  }
})
ReactDOM.render(<App/>, document.getElementById('app'))

什麼是React Hooks,它是爲了解決什麼問題?說一下它的實現原理!

React Hooks 是 React 16.7.0-alpha 版本推出的新特性,它可讓你在不編寫class的狀況下使用state以及其餘的 React特性。React Hooks要解決的問題是狀態共享,是繼render-props和hoc以後的第三種狀態共享方案,不會產生JSX嵌套地獄問題。這個狀態指的是狀態邏輯,因此稱爲狀態邏輯複用會更恰當,由於只共享數據處理邏輯,不會共享數據自己。

簡單實現

let memoizedState = []; // hooks 存放在這個數組
let cursor = 0; // 當前 memoizedState 下標

function useState(initialValue) {
  memoizedState[cursor] = memoizedState[cursor] || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memoizedState[currentCursor] = newState;
    render();
  }
  return [memoizedState[cursor++], setState]; // 返回當前 state,並把 cursor 加 1
}

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray;
  const deps = memoizedState[cursor];
  const hasChangedDeps = deps
    ? !depArray.every((el, i) => el === deps[i])
    : true;
  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedState[cursor] = depArray;
  }
  cursor++;
}

React爲何要在構造函數中調用super(props),爲何要bind(this)?

super表明父類的構造函數,javascript規定若是子類不調用super是不容許在子類中使用this的,這不是React的限制,而是javaScript的限制,同時你也必須給super傳入props,不然React.Component就無法初始化this.props
在 React 的類組件中,當咱們把事件處理函數引用做爲回調傳遞過去,事件處理程序方法會丟失其隱式綁定的上下文。當事件被觸發而且處理程序被調用時,this的值會回退到默認綁定,即值爲 undefined,這是由於類聲明和原型方法是以嚴格模式運行。

說一下react事件機制?

react爲何要用本身的事件機制

  • 減小內存消耗,提高性能,不須要註冊那麼多的事件了,一種事件類型只在document上註冊一次。
  • 統一規範,解決 ie 事件兼容問題,簡化事件邏輯。
  • 對開發者友好。

react的合成事件

SyntheticEvent是react合成事件的基類,定義了合成事件的基礎公共屬性和方法。react會根據當前的事件類型來使用不一樣的合成事件對象,好比鼠標單機事件 - SyntheticMouseEvent,焦點事件-SyntheticFocusEvent等,可是都是繼承自SyntheticEvent。在合成事件中主要作了如下三件事情。

  • 對原生事件的封裝
  • 對某些原生事件的升級和改造
  • 不一樣瀏覽器事件兼容的處理

事件註冊

組件掛載階段,根據組件內的聲明的事件類型-onclick,onchange等,給document上添加事件addEventListener,並指定統一的事件處理程序dispatchEvent。
經過virtual dom的props屬性拿到要註冊的事件名,回調函數,經過listenTo方法使用原生的addEventListener進行事件綁定。

事件存儲

事件存儲,就是把react組件內的全部事件統一的存放到一個二級map對象裏,緩存起來,爲了在觸發事件的時候能夠查找到對應的方法去執行。先查找事件名,而後找對對應的組件id相對應的事件。以下圖:
8081b073fb2c06f047538b75cc97fc6f.png

setState是異步的?爲何要這麼作?setState執行機制?

由執行機制看,setState自己並非異步的,而是在調用setState時,若是react正處於更新過程,當前更新會被暫存,等上一次更新執行後再執行,這個過程給人一種異步的假象。

ReactComponent.prototype.setState = function(partialState, callback) {
  //  將setState事務放進隊列中
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
enqueueSetState: function (publicInstance, partialState) {
     // 獲取當前組件的instance
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

     // 將要更新的state放入一個數組裏
     var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

     //  將要更新的component instance也放在一個隊列裏
    enqueueUpdate(internalInstance);
}
function enqueueUpdate(component) {
  // 若是沒有處於批量建立/更新組件的階段,則處理update state事務
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 若是正處於批量建立/更新組件的過程,將當前的組件放在dirtyComponents數組中
  dirtyComponents.push(component);
}

這裏的partialState能夠傳object,也能夠傳function,它會產生新的state以一種Object.assgine()的方式跟舊的state進行合併。
由這段代碼能夠看到,當前若是正處於建立/更新組件的過程,就不會馬上去更新組件,而是先把當前的組件放在dirtyComponent裏,因此不是每一次的setState都會更新組件。這段代碼就解釋了咱們常據說的:setState是一個異步的過程,它會集齊一批須要更新的組件而後一塊兒更新。而batchingStrategy 又是個什麼東西呢?
ReactDefaultBatchingStrategy.js

var ReactDefaultBatchingStrategy = {
  // 用於標記當前是否出於批量更新
  isBatchingUpdates: false,
  // 當調用這個方法時,正式開始批量更新
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // 若是當前事務正在更新過程在中,則調用callback,既enqueueUpdate
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
    // 不然執行更新事務
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  }
};

react-router原理

前端路由的原理思路大體上都是相同的,即實如今無刷新頁面的條件下切換顯示不一樣的頁面。而前端路由的本質就是頁面的URL發生改變時,頁面的顯示結果能夠根據URL的變化而變化,可是頁面不會刷新。目前實現前端路由有兩種方式:

經過Hash實現前端路由

路徑中hash值改變,並不會引發頁面刷新,同時咱們能夠經過hashchange事件,監聽hash的變化,從而實現咱們根據不一樣的hash值展現和隱藏不一樣UI顯示的功能,進而實現前端路由。

經過H5的history實現前端路由

HTML5的History接口,History對象是一個底層接口,不繼承於任何的接口。History接口容許咱們操做瀏覽器會話歷史記錄。
而history的pushState和repalce方法能夠實現改變當前頁面顯示的url,但都不會刷新頁面。

未完待續~
參考文檔:

react生命週期詳解
React diff
react 16新特性
react fiber1
react fiber2
react hooks
react 事件機制
setState機制1
setState機制2
react-router原理
集合

相關文章
相關標籤/搜索