前端面試的那些事兒(16)~ 掌握這些知識就不怕面試官問React - React框架基礎

React 快速入門

React 是一個用於構建用戶界面的 JavaScript 庫。數據改變時 React 能有效地更新並正確地渲染組件。javascript

咱們寫 React 就是建立擁有各自狀態的組件,再由這些組件構成更加複雜的 UI 界面。java

第一個組件:react

import React from 'react';

class HelloReact extends React.Component {
  render() {
    return (
      <div> Hello {this.props.name} </div>
    );
  }
}

export default HelloReact;
複製代碼

組件擁有動態渲染 name 的功能,name 字段是使用該組件的地方傳入的。git

<HelloReact name={"第一個組件"}/>
複製代碼

所以咱們就能夠正確渲染出一個DOM結構github

image.png
就是經過這種不斷去組合各種有狀態的組件,構建一個複雜的工業項目。

本文代碼託管地址>>>請點擊面試

JSX

接觸 React 想必 JSX 應該會是你接觸的第一個新概念,由於你多是第一次在 js 文件中直接編寫「HTML」。編程

return (
  <div> Hello {this.props.name} </div>
);
複製代碼

JSX 是一個 JavaScript 的語法擴展,它能夠生成 React 「元素」。數組

它的寫法徹底等價於:瀏覽器

import React from 'react';

class HelloJSX extends React.Component {
  render() {
    return React.createElement("div", null, "Hello ", this.props.name);
  }
}

export default HelloJSX;
複製代碼

其實 React 最終須要的就是 React.createElement("div", null, "Hello ", this.props.name)  經過它,React 能夠建立一系列數據結構來表示 React 中的元素,最終再把它轉換成真實的 DOM 插入到頁面中。緩存

所以 JSX 的結構與真實的 DOM 結構只能說是類似,它們之間還須要 React 去作一系列轉換。

JSX的本質是什麼?

JSX 自己在 React 項目中只是語法糖,不能直接被瀏覽器識別的。須要通過編譯,那麼這個編譯後的就是它的本質了。

JSX 在 React 項目中被編譯成了 React 元素,就是一個樹狀的數據結構。也就是咱們常說的虛擬 DOM。

組件傳值

既然 Reac t是經過組件的組合來實現複雜工程的構建。那麼組件之間的第一個問題就是它們之間如何通訊。

props

經過 props 向子組件傳遞數據,而且全部 React 組件都必須像純函數同樣保護它們的 props 不被更改。

image.png

import React from 'react';

class Child extends React.Component {
  render() {
    const { list } = this.props

    return <ul>{list.map((item, index) => { return <li key={item.id}> <span>{item.title}</span> </li> })}</ul>
  }
}

class Parent extends React.Component{
  constructor(props) {
    super(props)
    this.state = {
      list: [
        {
          id: 'id-1',
          title: '標題1'
        },
        {
          id: 'id-2',
          title: '標題2'
        },
        {
          id: 'id-3',
          title: '標題3'
        }
      ]
    }
  }
  render() {
    return <Child list={this.state.list} /> } } export default Parent; 複製代碼

代碼解釋:

  1. Parent 父組件把自身的 list 狀態傳遞給子組件 <Child list={this.state.list} />
  2. Child 子組件經過 props 獲取到父組件傳遞的數據併成功渲染 const { list } = this.props

這個就是父組件向子組件傳遞數據。

思考一個問題,當咱們的組件層級嵌套很深,例如 Parent 組件須要向 Child 的 Child 也就是孫子組件,甚至乎更加深的層級去傳遞一些數據該如何作呢?能夠經過context去實現。

Context

Context 提供了一種在組件之間共享此類值的方式,而沒必要顯式地經過組件樹的逐層傳遞 props。

咱們經常用它來傳遞應用級別的配置數據,例如APP的主題、用戶喜愛之類的。

image.png

咱們來實現一個切換主題的簡單場景:

import React from 'react'

// 建立 Context 填入默認值(任何一個 js 變量)
const ThemeContext = React.createContext('light') // {1}

// 函數式組件可使用 Consumer
function ThemeLink (props) {
  return <ThemeContext.Consumer> // {3}
    { value => <p>link's theme is {value}</p> }
  </ThemeContext.Consumer>
}

class ThemedButton extends React.Component {
  render() {
    const theme = this.context // React 會往上找到最近的 theme Provider,而後使用它的值。
    return <div>
      <p>button's theme is {theme}</p>
    </div>
  }
}
ThemedButton.contextType = ThemeContext // 指定 contextType 讀取當前的 theme context。

// 中間的組件不再必指明往下傳遞 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
      <ThemeLink />
    </div>
  )
}

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      theme: 'light'
    }
  }
  render() {
    return <ThemeContext.Provider value={this.state.theme}> // {2}
      <Toolbar />
      <hr/>
      <button onClick={this.changeTheme}>change theme</button>
    </ThemeContext.Provider>
  }
  changeTheme = () => {
    this.setState({
      theme: this.state.theme === 'light' ? 'dark' : 'light'
    })
  }
}

export default App
複製代碼

代碼解釋:

  • {1} const ThemeContext = React.createContext('light') 建立一個context,返回一個ThemeContext 對象
  • {2} <ThemeContext.Provider value={this.state.theme}></ThemeContext.Provider>  ThemeContext 的提供者
  • {3} <ThemeContext.Consumer></ThemeContext.Consumer> ThemeContext 的消費者
React.createContext 源碼
export function createContext( defaultValue, calculateChangedBits ){
  const context = {
    $$typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    Provider:null,
    Consumer:null ,
  };

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };

  return context;
}
複製代碼

這是通過刪減的源碼,調用 createContext 函數返回的就是一個context對象,咱們使用的 ThemeContext.Provider 也就是該對象返回的其中一個屬性。

state

經過上面咱們已經知道組件之間是如何進行通訊的,那麼組件如何維護自身的狀態呢?就是 state 了,由於它的一些特性,也讓它成爲了面試必考。

import React from 'react'

class StateDemo extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  add = ()=>{
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return <div> <p>{this.state.count}</p> <button onClick={this.add}>增長</button> </div>
  }
}

export default StateDemo
複製代碼

代碼解釋:

  1. 在 constructor 中初始化組件的 state 對象
  2. 當點擊按鈕時經過 setState 改變 state 的狀態

這就是組件的 state,以及如何改變 state,關於它的常見面試題有:

  1. setState 是同步仍是異步的?
  2. 爲何有時連續兩次 setState只有一次生效?
  3. 爲何 state 必須爲不可變值

setState 異步狀況

1.1 React 生命週期中 setState

componentDidMount() {
  console.log('SetState調用setState');
  this.setState({
    index: this.state.index + 1
  })
  console.log('state', this.state.index); // 0

  console.log('SetState調用setState');
  this.setState({
    index: this.state.index + 1
  })
  console.log('state', this.state.index); // 0
}
複製代碼

兩次打印都是0,沒有當即更新

1.2 React 合成事件中 setState

add = ()=>{
    console.log('合成事件中調用setState');

    this.setState({
      count: this.state.count + 1
    })

    console.log('count', this.state.count); // 0

    console.log('合成事件中調用setState');

    this.setState({
      count: this.state.count + 1
    })

    console.log('count', this.state.count); // 0

  }
複製代碼
  • 兩次輸出都爲0沒有當即更新
  • 這裏進行了兩次設置,可是頁面只增長了1,說明被合併了

setState 同步狀況

1.1 異步函數中執行 setState

componentDidMount() {
    setTimeout(()=>{
      console.log('SetState調用setState');
      this.setState({
        index: this.state.index + 1
      })
      console.log('state', this.state.index); // 1

      console.log('SetState調用setState');
      this.setState({
        index: this.state.index + 1
      })
      console.log('state', this.state.index); // 2
    },0);
  }
複製代碼

咱們使用 setTimeout 異步函數包裝下,發現,setState 同步執行了。

1.2 原生事件中執行 setState

componentDidMount(){
    document.body.addEventListener('click', this.bodyClickHandler); // 在生命週期中綁定原生事件
}

bodyClickHandler = ()=>{
  console.log('原生事件中調用setState');

  this.setState({
    count: this.state.count + 1
  })

  console.log('count', this.state.count); // 1

  console.log('原生事件中調用setState');

  this.setState({
    count: this.state.count + 1
  })

  console.log('count', this.state.count); // 2
}
複製代碼

在原生事件中執行 setState 也同步執行了。

異步狀況:

  1. React 生命週期中 setState
  2. React 合成事件中 setState

緣由:

React的生命週期和合成事件中,React仍然處於他的更新機制中,這時isBatchingUpdates 爲 true 。 這時不管調用多少次setState,都會不會執行更新,而是將要更新的state存入_pendingStateQueue,將要更新的組件存入dirtyComponent

當上一次更新機制執行完畢,以生命週期爲例,全部組件,即最頂層組件didmount後會將 isBatchingUpdates 設置爲 false 。這時將執行以前累積的setState

同步狀況:

  1. 異步函數中執行 setState
  2. 原生事件中執行 setState

緣由:

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

在生命週期,根據JS的異步機制,會將異步函數先暫存,等全部同步代碼執行完畢後再執行,這時上一次更新過程已經執行完畢,isBatchingUpdates被設置爲 false ,根據上面的流程,這時再調用setState便可當即執行更新,拿到更新結果。

簡單理解:當isBatchingUpdatestrue 時,加入異步隊列,下一輪執行;當isBatchingUpdatesfalse 時,不加入異步隊列,當即執行。能夠理解成相似 JavaScript EventLoop  機制。

image.png
這是 React-16.6.0 的源碼

if (!isBatchingUpdates && !isRendering) {
  performSyncWork();
}
複製代碼

這句話的大概意思是,isBatchingUpdates變量爲 false 時當即執行performSyncWork方法。從該方法名的意思是「執行同步任務」。

Batch模式下React不會馬上修改state,而是把這個對象放到一個更新隊列中,稍後纔會從隊列中把新的狀態提取出來合併到state中,而後再觸發組件更新,這樣的設計主要目的是爲了提升 UI 更新的性能。

setState 合併

從 React 源碼中也能夠看出 setState 第一個參數其實能夠接受兩種類型的,一種是對象,一種是函數。對象型是會進行合併處理 Object.assagin ,而函數型是不會的。

Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼

咱們再來看看源碼中 enqueueSetState 是如何進行處理的:

for (let i = oldReplace ? 1 : 0; i < oldQueue.length; i++) {
  let partial = oldQueue[i];
  let partialState =
      typeof partial === 'function'
  ? partial.call(inst, nextState, element.props, publicContext)
  : partial;
  if (partialState != null) {
    if (dontMutate) {
      dontMutate = false;
      nextState = Object.assign({}, nextState, partialState);
    } else {
      Object.assign(nextState, partialState);
    }
  }
}
複製代碼

這是其中處理 state queue 的源碼片斷,咱們關注重點便可

if(partial === 'function'){
  partial.call(inst, nextState, element.props, publicContext)
}
if (partialState != null) {
  Object.assign(nextState, partialState);
}

複製代碼

若是 queue 中的 state 是函數則直接執行,若是是非函數,且不是null,則先進行對象合併操做。

咱們來測試下傳入函數是否真的不會合並

componentDidMount() {
    console.log('SetState傳入函數');

    this.setState((state, props) => ({
      index: state.index + 1
    }));

    console.log('state', this.state.index); // 0

    console.log('SetState傳入函數');

    this.setState((state, props) => ({
      index: state.index + 1
    }));

    console.log('state', this.state.index); // 0

    setTimeout(()=>{
      console.log('state', this.state.index); // 2
    },0);
  }
複製代碼

從結果能夠看出來,咱們同時更新了兩次index,說明傳入函數並不會被合併。

state必須爲不可變值

咱們那上面的 add 方法作例子,改造下:

add = ()=>{
    this.state.count++;
    this.setState({
      count: this.state.count
    })
  }
複製代碼
  1. 咱們先直接改變 state 對象中的 count 值
  2. 而後再去 setState

咱們發現效果是同樣的,是否是表示咱們這樣作也能夠呢?答案是否認的。

必須得這樣寫:

add = ()=>{
    this.setState({
      count: this.state.count + 1
    })
  }
複製代碼

不可變值在對象和數組中的應用:

import React from 'react'

class StateDemo1 extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      list:[
        {
          id:1,
          name:"a"
        },
        {
          id:2,
          name:"b"
        },
        {
          id:3,
          name:"C"
        },
      ]
    }
  }

render() {
    return (
      <div> <ul> { this.state.list.map((item)=>{ return <li key={item.id}> <span>{item.name}</span> </li> }) } </ul> <button onClick={this.deletePop}>刪除最後一條</button> </div>
    )
  }
}

export default StateDemo1
複製代碼

image.png

當咱們須要對這個列表進行刪除時, this.state.list  就必須遵循不可變值。

deletePop = ()=>{
    const newList = [...this.state.list];
    newList.pop();
    this.setState({
      list: newList
    })
  }
複製代碼
  • 首先對 this.state.list  進行淺複製
  • 再操做新的 list 數組
  • 在 setState 中進行賦值操做

在這裏咱們沒有去改變原 state.list 值,而是淺複製了一個副本出來再去操做的。這就是在 React 編程中要很是注意的「不可變值」。

至於爲何必需要不可變值呢,這個其實跟React性能優化有很是大的關係,在下一篇文章中會詳細講述其中緣由。

React 事件

合成事件

Virtual DOM 在內存中是以對象的形式存在的,若是想要在這些對象上添加事件,就會很是簡單。React 基於 Virtual DOM 實現了一個 SyntheticEvent (合成事件)層,咱們所定義的事件處理器會接收到一個 SyntheticEvent 對象的實例,它徹底符合 W3C 標準,不會存在任何 IE 標準的兼容性問題。而且與原生的瀏覽器事件同樣擁有一樣的接口,一樣支持事件的冒泡機制,咱們可使用 stopPropagation() 和 preventDefault() 來中斷它。

合成事件的實現機制

在 React 底層,主要對合成事件作了兩件事:事件委派和自動綁定。

1. 事件委派

在使用 React 事件前,必定要熟悉它的事件代理機制。它並不會把事件處理函數直接綁定到真實的節點上,而是把全部事件綁定到結構的最外層,使用一個統一的事件監聽器,這個事件監聽器上維持了一個映射來保存全部組件內部的事件監聽和處理函數。當組件掛載或卸載時,只是在這個統一的事件監聽器上插入或刪除一些對象;當事件發生時,首先被這個統一的事件監聽器處理,而後在映射裏找到真正的事件處理函數並調用。這樣作簡化了事件處理和回收機制,效率也有很大提高。

2. 自動綁定

在 React 組件中,每一個方法的上下文都會指向該組件的實例,即自動綁定 this 爲當前組件。並且 React 還會對這種引用進行緩存,以達到 CPU 和內存的最優化。 實際上,React 的合成事件系統只是原生 DOM 事件系統的一個子集。它僅僅實現了 DOM Level 3 的事件接口,而且統一了瀏覽器間的兼容問題。有些事件 React 並無實現,或者受某些限制沒辦法去實現,好比 windowresize 事件。

對於沒法使用 React 合成事件的場景,咱們還須要使用原生事件來完成。

爲何要手動綁定this

ES6類的方法內部若是含有this,它默認指向類的實例。可是,必須很是當心,一旦單獨使用該方法,極可能報錯。而React中執行事件回調方法放入一個隊列中,當事件被觸發時執行相應的回調,所以該事件回調方法並未與React組件實例綁定在一塊兒,因此咱們須要進行手動綁定上下文。

bind this 綁定

import React from 'react'

class EventDemo extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  add(){
    this.setState({
    	count: this.state.count + 1
    })   
  }

  render() {
    return <div> <p>{this.state.count}</p> <button onClick={this.add.bind(this)}>增長</button> </div>
  }
}
複製代碼

或者在 constructor 中 bind this

constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
    this.add = this.add.bind(this);
  }
複製代碼

箭頭函數

使用箭頭函數就不須要使用 bind this 進行綁定了

add = ()=>{
    this.setState({
    	count: this.state.count + 1
    })   
  }
複製代碼

合成事件與原生事件的區別

  1. React 事件使用駝峯命名,而不是所有小寫;
  2. 阻止原生事件傳播須要使用 e.preventDefault(),不過對於不支持該方法的瀏覽器(IE9 如下),只能使用 e.cancelBubble = true 來阻止。而在 React 合成事件中,只須要使用 e.preventDefault() 便可;
  3. React本身實現了一套事件機制,本身模擬了事件冒泡和捕獲的過程,採用了事件代理,批量更新等方法,而且抹平了各個瀏覽器的兼容性問題。

React事件和原生事件的執行順序

import React from 'react'

class EventDemo extends React.Component {
  constructor(props) {
    super(props);
    this.parent = React.createRef();
    this.child = React.createRef();
  }
  componentDidMount() {
    this.parent.current.addEventListener('click', (e) => {
      console.log('dom parent');
    })
    this.child.current.addEventListener('click', (e) => {
      console.log('dom child');
    })
    document.addEventListener('click', (e) => {
      console.log('document');
    })
  }

  childClick = (e) => {
    console.log('react child');
  }

  parentClick = (e) => {
    console.log('react parent');
  }

  render() {
    return (
      <div onClick={this.parentClick} ref={this.parent}> <div onClick={this.childClick} ref={this.child}> test Event </div> </div>)
  }
}

export default EventDemo
複製代碼

執行結果:

dom child
dom parent
react child
react parent
document
複製代碼

由上面的流程咱們能夠理解:

  • React的全部事件都掛載在document
  • 當真實dom觸發後冒泡到document後纔會對React事件進行處理
  • 因此原生的事件會先執行
  • 而後執行React合成事件
  • 最後執行真正在document上掛載的事件

React事件和原生事件能夠混用嗎?

React事件和原生事件最好不要混用。

原生事件中若是執行了stopPropagation方法,則會致使其餘React事件失效。由於全部元素的事件將沒法冒泡到document上。

受控組件與非受控組件

受控組件與非受控組件主要是針對表單元素

受控組件

咱們先來看一段React處理表單的代碼:

import React from 'react'

class FormDemo extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      name: 'frank'
    }
  }
  render() {
    return <div> <p>{this.state.name}</p> <input id="inputName" value={this.state.name} onChange={this.onInputChange}/> </div> } onInputChange = (e) => { this.setState({ name: e.target.value }) } } export default FormDemo 複製代碼

你心中必定會有疑問,爲什麼 <input>  要綁定一個 change 事件呢?

在 HTML 中,表單元素(如<input><textarea><select>)之類的表單元素一般本身維護 state,並根據用戶輸入進行更新。而在 React 中,可變狀態一般保存在組件的 state 屬性中,而且只能經過使用 setState()來更新。被 React 以這種方式控制取值的表單輸入元素就叫作「受控組件」。

總結下 React 受控組件更新 state 的流程: (1) 能夠經過在初始 state 中設置表單的默認值。 (2) 每當表單的值發生變化時,調用 onChange 事件處理器。 (3) 事件處理器經過合成事件對象 e 拿到改變後的狀態,並更新應用的 state。 (4) setState 觸發視圖的從新渲染,完成表單組件值的更新。

非受控組件

先來看一段代碼:

class FormDemo1 extends React.Component {
  constructor(props) {
    super(props)
    this.content = React.createRef();
  }
  handleSubmit=(e)=>{
    e.preventDefault();
    const { value } = this.content.current;
    console.log(value);
  }

  render() {
    return <form onSubmit={this.handleSubmit}> <input ref={this.content} type="text" defaultValue="frank" /> <button type="submit">Submit</button> </form> } } 複製代碼

在 React 中,非受控組件是一種反模式,它的值不受組件自身的 state 或 props 控制。一般,須要經過爲其添加 ref 來訪問渲染後的底層 DOM 元素。

受控組件和非受控組件的最大區別是:非受控組件的狀態並不會受應用狀態的控制,而受控組件的值來自於組件的 state。

小結

因爲React在面試中佔有很是的比重,所以關於React技術棧的面試文章將分解爲多篇進行講解

相關文章
相關標籤/搜索