1. 程式人生 > 其它 >高頻前端面試題彙總之React篇(下)

高頻前端面試題彙總之React篇(下)

六、Redux

1. 對 Redux 的理解,主要解決什麼問題

React是檢視層框架。Redux是一個用來管理資料狀態和UI狀態的JavaScript應用工具。隨著JavaScript單頁應用(SPA)開發日趨複雜, JavaScript需要管理比任何時候都要多的state(狀態), Redux就是降低管理難度的。(Redux支援React、Angular、jQuery甚至純JavaScript)。

在 React 中,UI 以元件的形式來搭建,元件之間可以巢狀組合。但 React 中元件間通訊的資料流是單向的,頂層元件可以通過 props 屬性向下層元件傳遞資料,而下層元件不能向上層元件傳遞資料,兄弟元件之間同樣不能。這樣簡單的單向資料流支撐起了 React 中的資料可控性。

當專案越來越大的時候,管理資料的事件或回撥函式將越來越多,也將越來越不好管理。管理不斷變化的 state 非常困難。如果一個 model 的變化會引起另一個 model 變化,那麼當 view 變化時,就可能引起對應 model 以及另一個 model 的變化,依次地,可能會引起另一個 view 的變化。直至你搞不清楚到底發生了什麼。state 在什麼時候,由於什麼原因,如何變化已然不受控制。 當系統變得錯綜複雜的時候,想重現問題或者新增新功能就會變得舉步維艱。如果這還不夠糟糕,考慮一些來自前端開發領域的新需求,如更新調優、服務端渲染、路由跳轉前請求資料等。state 的管理在大專案中相當複雜。

Redux 提供了一個叫 store 的統一倉儲庫,元件通過 dispatch 將 state 直接傳入store,不用通過其他的元件。並且元件通過 subscribe 從 store獲取到 state 的改變。使用了 Redux,所有的元件都可以從 store 中獲取到所需的 state,他們也能從store 獲取到 state 的改變。這比元件之間互相傳遞資料清晰明朗的多。

主要解決的問題: 單純的Redux只是一個狀態機,是沒有UI呈現的,react- redux作用是將Redux的狀態機和React的UI呈現繫結在一起,當你dispatch action改變state的時候,會自動更新頁面。

2. Redux 原理及工作流程

(1)原理 Redux原始碼主要分為以下幾個模組檔案

  • compose.js 提供從右到左進行函數語言程式設計
  • createStore.js 提供作為生成唯一store的函式
  • combineReducers.js 提供合併多個reducer的函式,保證store的唯一性
  • bindActionCreators.js 可以讓開發者在不直接接觸dispacth的前提下進行更改state的操作
  • applyMiddleware.js 這個方法通過中介軟體來增強dispatch的功能
const actionTypes = {
    ADD: 'ADD',
    CHANGEINFO: 'CHANGEINFO',
}

const initState = {
    info: '初始化',
}

export default function initReducer(state=initState, action) {
    switch(action.type) {
        case actionTypes.CHANGEINFO:
            return {
                ...state,
                info: action.preload.info || '',
            }
        default:
            return { ...state };
    }
}

export default function createStore(reducer, initialState, middleFunc) {

    if (initialState && typeof initialState === 'function') {
        middleFunc = initialState;
        initialState = undefined;
    }

    let currentState = initialState;

    const listeners = [];

    if (middleFunc && typeof middleFunc === 'function') {
        // 封裝dispatch 
        return middleFunc(createStore)(reducer, initialState);
    }

    const getState = () => {
        return currentState;
    }

    const dispatch = (action) => {
        currentState = reducer(currentState, action);

        listeners.forEach(listener => {
            listener();
        })
    }

    const subscribe = (listener) => {
        listeners.push(listener);
    }

    return {
        getState,
        dispatch,
        subscribe
    }
}

(2)工作流程

  • const store= createStore(fn)生成資料;
  • action: {type: Symble('action01), payload:'payload' }定義行為;
  • dispatch發起action:store.dispatch(doSomething('action001'));
  • reducer:處理action,返回新的state;

通俗點解釋:

  • 首先,使用者(通過View)發出Action,發出方式就用到了dispatch方法
  • 然後,Store自動呼叫Reducer,並且傳入兩個引數:當前State和收到的Action,Reducer會返回新的State
  • State—旦有變化,Store就會呼叫監聽函式,來更新View

以 store 為核心,可以把它看成資料儲存中心,但是他要更改資料的時候不能直接修改,資料修改更新的角色由Reducers來擔任,store只做儲存,中間人,當Reducers的更新完成以後會通過store的訂閱來通知react component,元件把新的狀態重新獲取渲染,元件中也能主動傳送action,建立action後這個動作是不會執行的,所以要dispatch這個action,讓store通過reducers去做更新React Component 就是react的每個元件。

3. Redux 中非同步的請求怎麼處理

可以在 componentDidmount 中直接進⾏請求⽆須藉助redux。但是在⼀定規模的項⽬中,上述⽅法很難進⾏非同步流的管理,通常情況下我們會藉助redux的非同步中介軟體進⾏非同步處理。redux非同步流中介軟體其實有很多,當下主流的非同步中介軟體有兩種redux-thunk、redux-saga。

(1)使用react-thunk中介軟體

redux-thunk優點:

  • 體積⼩: redux-thunk的實現⽅式很簡單,只有不到20⾏程式碼
  • 使⽤簡單: redux-thunk沒有引⼊像redux-saga或者redux-observable額外的正規化,上⼿簡單

redux-thunk缺陷:

  • 樣板程式碼過多: 與redux本身⼀樣,通常⼀個請求需要⼤量的程式碼,⽽且很多都是重複性質的
  • 耦合嚴重: 非同步操作與redux的action偶合在⼀起,不⽅便管理
  • 功能孱弱: 有⼀些實際開發中常⽤的功能需要⾃⼰進⾏封裝

使用步驟:

  • 配置中介軟體,在store的建立中配置
import {createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk'

// 設定除錯工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
// 設定中介軟體
const enhancer = composeEnhancers(
  applyMiddleware(thunk)
);

const store = createStore(reducer, enhancer);

export default store;
  • 新增一個返回函式的actionCreator,將非同步請求邏輯放在裡面
/**
  傳送get請求,並生成相應action,更新store的函式
  @param url {string} 請求地址
  @param func {function} 真正需要生成的action對應的actionCreator
  @return {function} 
*/
// dispatch為自動接收的store.dispatch函式 
export const getHttpAction = (url, func) => (dispatch) => {
    axios.get(url).then(function(res){
        const action = func(res.data)
        dispatch(action)
    })
}
  • 生成action,併發送action
componentDidMount(){
    var action = getHttpAction('/getData', getInitTodoItemAction)
    // 傳送函式型別的action時,該action的函式體會自動執行
    store.dispatch(action)
}

(2)使用redux-saga中介軟體

redux-saga優點:

  • 非同步解耦: 非同步操作被被轉移到單獨 saga.js 中,不再是摻雜在 action.js 或 component.js 中
  • action擺脫thunk function: dispatch 的引數依然是⼀個純粹的 action (FSA),⽽不是充滿 “⿊魔法” thunk function
  • 異常處理: 受益於 generator function 的 saga 實現,程式碼異常/請求失敗 都可以直接通過 try/catch 語法直接捕獲處理
  • 功能強⼤: redux-saga提供了⼤量的Saga 輔助函式和Effect 建立器供開發者使⽤,開發者⽆須封裝或者簡單封裝即可使⽤
  • 靈活: redux-saga可以將多個Saga可以串⾏/並⾏組合起來,形成⼀個⾮常實⽤的非同步flow
  • 易測試,提供了各種case的測試⽅案,包括mock task,分⽀覆蓋等等

redux-saga缺陷:

  • 額外的學習成本: redux-saga不僅在使⽤難以理解的 generator function,⽽且有數⼗個API,學習成本遠超redux-thunk,最重要的是你的額外學習成本是隻服務於這個庫的,與redux-observable不同,redux-observable雖然也有額外學習成本但是背後是rxjs和⼀整套思想
  • 體積龐⼤: 體積略⼤,程式碼近2000⾏,min版25KB左右
  • 功能過剩: 實際上併發控制等功能很難⽤到,但是我們依然需要引⼊這些程式碼
  • ts⽀持不友好: yield⽆法返回TS型別

redux-saga可以捕獲action,然後執行一個函式,那麼可以把非同步程式碼放在這個函式中,使用步驟如下:

  • 配置中介軟體
import {createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducer';
import createSagaMiddleware from 'redux-saga'
import TodoListSaga from './sagas'

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const sagaMiddleware = createSagaMiddleware()

const enhancer = composeEnhancers(
  applyMiddleware(sagaMiddleware)
);

const store = createStore(reducer, enhancer);
sagaMiddleware.run(TodoListSaga)

export default store;
  • 將非同步請求放在sagas.js中
import {takeEvery, put} from 'redux-saga/effects'
import {initTodoList} from './actionCreator'
import {GET_INIT_ITEM} from './actionTypes'
import axios from 'axios'

function* func(){
    try{
        // 可以獲取非同步返回資料
        const res = yield axios.get('/getData')
        const action = initTodoList(res.data)
        // 將action傳送到reducer
        yield put(action)
    }catch(e){
        console.log('網路請求失敗')
    }
}

function* mySaga(){
    // 自動捕獲GET_INIT_ITEM型別的action,並執行func
    yield takeEvery(GET_INIT_ITEM, func)
}

export default mySaga
  • 傳送action
componentDidMount(){
  const action = getInitTodoItemAction()
  store.dispatch(action)
}

4. Redux 怎麼實現屬性傳遞,介紹下原理

react-redux 資料傳輸∶ view-->action-->reducer-->store-->view。看下點選事件的資料是如何通過redux傳到view上:

  • view 上的AddClick 事件通過mapDispatchToProps 把資料傳到action ---> click:()=>dispatch(ADD)
  • action 的ADD 傳到reducer上
  • reducer傳到store上 const store = createStore(reducer);
  • store再通過 mapStateToProps 對映穿到view上text:State.text

程式碼示例∶

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';
class App extends React.Component{
    render(){
        let { text, click, clickR } = this.props;
        return(
            <div>
                <div>資料:已有人{text}</div>
                <div onClick={click}>加人</div>
                <div onClick={clickR}>減人</div>
            </div>
        )
    }
}
const initialState = {
    text:5
}
const reducer = function(state,action){
    switch(action.type){
        case 'ADD':
            return {text:state.text+1}
        case 'REMOVE':
            return {text:state.text-1}
        default:
            return initialState;
    }
}

let ADD = {
    type:'ADD'
}
let Remove = {
    type:'REMOVE'
}

const store = createStore(reducer);

let mapStateToProps = function (state){
    return{
        text:state.text
    }
}

let mapDispatchToProps = function(dispatch){
    return{
        click:()=>dispatch(ADD),
        clickR:()=>dispatch(Remove)
    }
}

const App1 = connect(mapStateToProps,mapDispatchToProps)(App);

ReactDOM.render(
    <Provider store = {store}>
        <App1></App1>
    </Provider>,document.getElementById('root')
)

5. Redux 中介軟體是什麼?接受幾個引數?柯里化函式兩端的引數具體是什麼?

Redux 的中介軟體提供的是位於 action 被髮起之後,到達 reducer 之前的擴充套件點,換而言之,原本 view -→> action -> reducer -> store 的資料流加上中介軟體後變成了 view -> action -> middleware -> reducer -> store ,在這一環節可以做一些"副作用"的操作,如非同步請求、列印日誌等。

applyMiddleware原始碼:

export default function applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        // 利用傳入的createStore和reducer和建立一個store
        const store = createStore(...args)
        let dispatch = () => {
            throw new Error()
        }
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }
        // 讓每個 middleware 帶著 middlewareAPI 這個引數分別執行一遍
        const chain = middlewares.map(middleware => middleware(middlewareAPI))
        // 接著 compose 將 chain 中的所有匿名函式,組裝成一個新的函式,即新的 dispatch
        dispatch = compose(...chain)(store.dispatch)
        return {
            ...store,
            dispatch
        }
    }
}

從applyMiddleware中可以看出∶

  • redux中介軟體接受一個物件作為引數,物件的引數上有兩個欄位 dispatch 和 getState,分別代表著 Redux Store 上的兩個同名函式。
  • 柯里化函式兩端一個是 middewares,一個是store.dispatch

6. Redux 請求中介軟體如何處理併發

使用redux-Saga redux-saga是一個管理redux應用非同步操作的中介軟體,用於代替 redux-thunk 的。它通過建立 Sagas 將所有非同步操作邏輯存放在一個地方進行集中處理,以此將react中的同步操作與非同步操作區分開來,以便於後期的管理與維護。 redux-saga如何處理併發:

  • takeEvery

可以讓多個 saga 任務並行被 fork 執行。

import {
    fork,
    take
} from "redux-saga/effects"

const takeEvery = (pattern, saga, ...args) => fork(function*() {
    while (true) {
        const action = yield take(pattern)
        yield fork(saga, ...args.concat(action))
    }
})
  • takeLatest

takeLatest 不允許多個 saga 任務並行地執行。一旦接收到新的發起的 action,它就會取消前面所有 fork 過的任務(如果這些任務還在執行的話)。 在處理 AJAX 請求的時候,如果只希望獲取最後那個請求的響應, takeLatest 就會非常有用。

import {
    cancel,
    fork,
    take
} from "redux-saga/effects"

const takeLatest = (pattern, saga, ...args) => fork(function*() {
    let lastTask
    while (true) {
        const action = yield take(pattern)
        if (lastTask) {
            yield cancel(lastTask) // 如果任務已經結束,則 cancel 為空操作
        }
        lastTask = yield fork(saga, ...args.concat(action))
    }
})

7. Redux 狀態管理器和變數掛載到 window 中有什麼區別

兩者都是儲存資料以供後期使用。但是Redux狀態更改可回溯——Time travel,資料多了的時候可以很清晰的知道改動在哪裡發生,完整的提供了一套狀態管理模式。

隨著 JavaScript 單頁應用開發日趨複雜,JavaScript 需要管理比任何時候都要多的 state (狀態)。 這些 state 可能包括伺服器響應、快取資料、本地生成尚未持久化到伺服器的資料,也包括 UI狀態,如啟用的路由,被選中的標籤,是否顯示載入動效或者分頁器等等。

管理不斷變化的 state 非常困難。如果一個 model 的變化會引起另一個 model 變化,那麼當 view 變化時,就可能引起對應 model 以及另一個model 的變化,依次地,可能會引起另一個 view 的變化。直至你搞不清楚到底發生了什麼。state 在什麼時候,由於什麼原因,如何變化已然不受控制。 當系統變得錯綜複雜的時候,想重現問題或者新增新功能就會變得舉步維艱。 如果這還不夠糟糕,考慮一些來自前端開發領域的新需求,如更新調優、服務端渲染、路由跳轉前請求資料等等。前端開發者正在經受前所未有的複雜性,難道就這麼放棄了嗎?當然不是。

這裡的複雜性很大程度上來自於:我們總是將兩個難以理清的概念混淆在一起:變化和非同步。 可以稱它們為曼妥思和可樂。如果把二者分開,能做的很好,但混到一起,就變得一團糟。一些庫如 React 檢視在檢視層禁止非同步和直接操作 DOM來解決這個問題。美中不足的是,React 依舊把處理 state 中資料的問題留給了你。Redux就是為了幫你解決這個問題。

8. mobox 和 redux 有什麼區別?

(1)共同點

  • 為了解決狀態管理混亂,無法有效同步的問題統一維護管理應用狀態;
  • 某一狀態只有一個可信資料來源(通常命名為store,指狀態容器);
  • 操作更新狀態方式統一,並且可控(通常以action方式提供更新狀態的途徑);
  • 支援將store與React元件連線,如react-redux,mobx- react;

(2)區別 Redux更多的是遵循Flux模式的一種實現,是一個 JavaScript庫,它關注點主要是以下幾方面∶

  • Action∶ 一個JavaScript物件,描述動作相關資訊,主要包含type屬性和payload屬性∶

    o type∶ action 型別; o payload∶ 負載資料;
    
  • Reducer∶ 定義應用狀態如何響應不同動作(action),如何更新狀態;

  • Store∶ 管理action和reducer及其關係的物件,主要提供以下功能∶

    o 維護應用狀態並支援訪問狀態(getState());
    o 支援監聽action的分發,更新狀態(dispatch(action)); 
    o 支援訂閱store的變更(subscribe(listener));
    
  • 非同步流∶ 由於Redux所有對store狀態的變更,都應該通過action觸發,非同步任務(通常都是業務或獲取資料任務)也不例外,而為了不將業務或資料相關的任務混入React元件中,就需要使用其他框架配合管理非同步任務流程,如redux-thunk,redux-saga等;

Mobx是一個透明函式響應式程式設計的狀態管理庫,它使得狀態管理簡單可伸縮∶

  • Action∶定義改變狀態的動作函式,包括如何變更狀態;
  • Store∶ 集中管理模組狀態(State)和動作(action)
  • Derivation(衍生)∶ 從應用狀態中派生而出,且沒有任何其他影響的資料

對比總結:

  • redux將資料儲存在單一的store中,mobx將資料儲存在分散的多個store中
  • redux使用plain object儲存資料,需要手動處理變化後的操作;mobx適用observable儲存資料,資料變化後自動處理響應的操作
  • redux使用不可變狀態,這意味著狀態是隻讀的,不能直接去修改它,而是應該返回一個新的狀態,同時使用純函式;mobx中的狀態是可變的,可以直接對其進行修改
  • mobx相對來說比較簡單,在其中有很多的抽象,mobx更多的使用面向物件的程式設計思維;redux會比較複雜,因為其中的函數語言程式設計思想掌握起來不是那麼容易,同時需要藉助一系列的中介軟體來處理非同步和副作用
  • mobx中有更多的抽象和封裝,除錯會比較困難,同時結果也難以預測;而redux提供能夠進行時間回溯的開發工具,同時其純函式以及更少的抽象,讓除錯變得更加的容易

9. Redux 和 Vuex 有什麼區別,它們的共同思想

(1)Redux 和 Vuex區別

  • Vuex改進了Redux中的Action和Reducer函式,以mutations變化函式取代Reducer,無需switch,只需在對應的mutation函式裡改變state值即可
  • Vuex由於Vue自動重新渲染的特性,無需訂閱重新渲染函式,只要生成新的State即可
  • Vuex資料流的順序是∶View呼叫store.commit提交對應的請求到Store中對應的mutation函式->store改變(vue檢測到資料變化自動渲染)

通俗點理解就是,vuex 弱化 dispatch,通過commit進行 store狀態的一次更變;取消了action概念,不必傳入特定的 action形式進行指定變更;弱化reducer,基於commit引數直接對資料進行轉變,使得框架更加簡易;

(2)共同思想

  • 單—的資料來源
  • 變化可以預測

本質上∶ redux與vuex都是對mvvm思想的服務,將資料從檢視中抽離的一種方案。

10. Redux 中介軟體是怎麼拿到store 和 action? 然後怎麼處理?

redux中介軟體本質就是一個函式柯里化。redux applyMiddleware Api 原始碼中每個middleware 接受2個引數, Store 的getState 函式和dispatch 函式,分別獲得store和action,最終返回一個函式。該函式會被傳入 next 的下一個 middleware 的 dispatch 方法,並返回一個接收 action 的新函式,這個函式可以直接呼叫 next(action),或者在其他需要的時刻呼叫,甚至根本不去呼叫它。呼叫鏈中最後一個 middleware 會接受真實的 store的 dispatch 方法作為 next 引數,並藉此結束呼叫鏈。所以,middleware 的函式簽名是({ getState,dispatch })=> next => action。

11. Redux中的connect有什麼作用

connect負責連線React和Redux

(1)獲取state

connect 通過 context獲取 Provider 中的 store,通過 store.getState() 獲取整個store tree 上所有state

(2)包裝原元件

將state和action通過props的方式傳入到原元件內部 wrapWithConnect 返回—個 ReactComponent 對 象 Connect,Connect 重 新 render 外部傳入的原元件 WrappedComponent ,並把 connect 中傳入的 mapStateToProps,mapDispatchToProps與元件上原有的 props合併後,通過屬性的方式傳給WrappedComponent

(3)監聽store tree變化

connect快取了store tree中state的狀態,通過當前state狀態 和變更前 state 狀態進行比較,從而確定是否呼叫 this.setState()方法觸發Connect及其子元件的重新渲染

七、Hooks

1. 對 React Hook 的理解,它的實現原理是什麼

React-Hooks 是 React 團隊在 React 元件開發實踐中,逐漸認知到的一個改進點,這背後其實涉及對類元件函式元件兩種元件形式的思考和側重。

(1)類元件: 所謂類元件,就是基於 ES6 Class 這種寫法,通過繼承 React.Component 得來的 React 元件。以下是一個類元件:

class DemoClass extends React.Component {
  state = {
    text: ""
  };
  componentDidMount() {
    //...
  }
  changeText = (newText) => {
    this.setState({
      text: newText
    });
  };

  render() {
    return (
      <div className="demoClass">
        <p>{this.state.text}</p>
        <button onClick={this.changeText}>修改</button>
      </div>
    );
  }
}

可以看出,React 類元件內部預置了相當多的“現成的東西”等著我們去排程/定製,state 和生命週期就是這些“現成東西”中的典型。要想得到這些東西,難度也不大,只需要繼承一個 React.Component 即可。

當然,這也是類元件的一個不便,它太繁雜了,對於解決許多問題來說,編寫一個類元件實在是一個過於複雜的姿勢。複雜的姿勢必然帶來高昂的理解成本,這也是我們所不想看到的。除此之外,由於開發者編寫的邏輯在封裝後是和元件粘在一起的,這就使得類元件內部的邏輯難以實現拆分和複用。

(2)函式元件:函式元件就是以函式的形態存在的 React 元件。早期並沒有 React-Hooks,函式元件內部無法定義和維護 state,因此它還有一個別名叫“無狀態元件”。以下是一個函式元件:

function DemoFunction(props) {
  const { text } = props
  return (
    <div className="demoFunction">
      <p>{`函式元件接收的內容:[${text}]`}</p>
    </div>
  );
}

相比於類元件,函式元件肉眼可見的特質自然包括輕量、靈活、易於組織和維護、較低的學習成本等。

通過對比,從形態上可以對兩種元件做區分,它們之間的區別如下:

  • 類元件需要繼承 class,函式元件不需要;
  • 類元件可以訪問生命週期方法,函式元件不能;
  • 類元件中可以獲取到例項化後的 this,並基於這個 this 做各種各樣的事情,而函式元件不可以;
  • 類元件中可以定義並維護 state(狀態),而函式元件不可以;

除此之外,還有一些其他的不同。通過上面的區別,我們不能說誰好誰壞,它們各有自己的優勢。在 React-Hooks 出現之前,類元件的能力邊界明顯強於函式元件。

實際上,類元件和函式元件之間,是面向物件和函數語言程式設計這兩套不同的設計思想之間的差異。而函式元件更加契合 React 框架的設計理念: React 元件本身的定位就是函式,一個輸入資料、輸出 UI 的函式。作為開發者,我們編寫的是宣告式的程式碼,而 React 框架的主要工作,就是及時地把宣告式的程式碼轉換為命令式的 DOM 操作,把資料層面的描述對映到使用者可見的 UI 變化中去。這就意味著從原則上來講,React 的資料應該總是緊緊地和渲染繫結在一起的,而類元件做不到這一點。函式元件就真正地將資料和渲染繫結到了一起。函式元件是一個更加匹配其設計理念、也更有利於邏輯拆分與重用的元件表達形式。

為了能讓開發者更好的的去編寫函式式元件。於是,React-Hooks 便應運而生。

React-Hooks 是一套能夠使函式元件更強大、更靈活的“鉤子”。

函式元件比起類元件少了很多東西,比如生命週期、對 state 的管理等。這就給函式元件的使用帶來了非常多的侷限性,導致我們並不能使用函式這種形式,寫出一個真正的全功能的元件。而React-Hooks 的出現,就是為了幫助函式元件補齊這些(相對於類元件來說)缺失的能力。

如果說函式元件是一臺輕巧的快艇,那麼 React-Hooks 就是一個內容豐富的零部件箱。“重灌戰艦”所預置的那些裝置,這個箱子裡基本全都有,同時它還不強制你全都要,而是允許你自由地選擇和使用你需要的那些能力,然後將這些能力以 Hook(鉤子)的形式“鉤”進你的元件裡,從而定製出一個最適合你的“專屬戰艦”。

2. 為什麼 useState 要使用陣列而不是物件

useState 的用法:

const [count, setCount] = useState(0)

可以看到 useState 返回的是一個數組,那麼為什麼是返回陣列而不是返回物件呢?

這裡用到了解構賦值,所以先來看一下ES6 的解構賦值:

陣列的解構賦值
const foo = [1, 2, 3];
const [one, two, three] = foo;
console.log(one);	// 1
console.log(two);	// 2
console.log(three);	// 3
物件的解構賦值
const user = {
  id: 888,
  name: "xiaoxin"
};
const { id, name } = user;
console.log(id);	// 888
console.log(name);	// "xiaoxin"

看完這兩個例子,答案應該就出來了:

  • 如果 useState 返回的是陣列,那麼使用者可以對陣列中的元素命名,程式碼看起來也比較乾淨
  • 如果 useState 返回的是物件,在解構物件的時候必須要和 useState 內部實現返回的物件同名,想要使用多次的話,必須得設定別名才能使用返回值

下面來看看如果 useState 返回物件的情況:

// 第一次使用
const { state, setState } = useState(false);
// 第二次使用
const { state: counter, setState: setCounter } = useState(0) 

這裡可以看到,返回物件的使用方式還是挺麻煩的,更何況實際專案中會使用的更頻繁。 總結:useState 返回的是 array 而不是 object 的原因就是為了降低使用的複雜度,返回陣列的話可以直接根據順序解構,而返回物件的話要想使用多次就需要定義別名了。

3. React Hooks 解決了哪些問題?

React Hooks 主要解決了以下問題:

(1)在元件之間複用狀態邏輯很難

React 沒有提供將可複用性行為“附加”到元件的途徑(例如,把元件連線到 store)解決此類問題可以使用 render props 和 高階元件。但是這類方案需要重新組織元件結構,這可能會很麻煩,並且會使程式碼難以理解。由 providers,consumers,高階元件,render props 等其他抽象層組成的元件會形成“巢狀地獄”。儘管可以在 DevTools 過濾掉它們,但這說明了一個更深層次的問題:React 需要為共享狀態邏輯提供更好的原生途徑。

可以使用 Hook 從元件中提取狀態邏輯,使得這些邏輯可以單獨測試並複用。Hook 使我們在無需修改元件結構的情況下複用狀態邏輯。 這使得在元件間或社群內共享 Hook 變得更便捷。

(2)複雜元件變得難以理解

在元件中,每個生命週期常常包含一些不相關的邏輯。例如,元件常常在 componentDidMount 和 componentDidUpdate 中獲取資料。但是,同一個 componentDidMount 中可能也包含很多其它的邏輯,如設定事件監聽,而之後需在 componentWillUnmount 中清除。相互關聯且需要對照修改的程式碼被進行了拆分,而完全不相關的程式碼卻在同一個方法中組合在一起。如此很容易產生 bug,並且導致邏輯不一致。

在多數情況下,不可能將元件拆分為更小的粒度,因為狀態邏輯無處不在。這也給測試帶來了一定挑戰。同時,這也是很多人將 React 與狀態管理庫結合使用的原因之一。但是,這往往會引入了很多抽象概念,需要你在不同的檔案之間來回切換,使得複用變得更加困難。

為了解決這個問題,Hook 將元件中相互關聯的部分拆分成更小的函式(比如設定訂閱或請求資料),而並非強制按照生命週期劃分。你還可以使用 reducer 來管理元件的內部狀態,使其更加可預測。

(3)難以理解的 class

除了程式碼複用和程式碼管理會遇到困難外,class 是學習 React 的一大屏障。我們必須去理解 JavaScript 中 this 的工作方式,這與其他語言存在巨大差異。還不能忘記繫結事件處理器。沒有穩定的語法提案,這些程式碼非常冗餘。大家可以很好地理解 props,state 和自頂向下的資料流,但對 class 卻一籌莫展。即便在有經驗的 React 開發者之間,對於函式元件與 class 元件的差異也存在分歧,甚至還要區分兩種元件的使用場景。

為了解決這些問題,Hook 使你在非 class 的情況下可以使用更多的 React 特性。 從概念上講,React 元件一直更像是函式。而 Hook 則擁抱了函式,同時也沒有犧牲 React 的精神原則。Hook 提供了問題的解決方案,無需學習複雜的函式式或響應式程式設計技術

4. React Hook 的使用限制有哪些?

React Hooks 的限制主要有兩條:

  • 不要在迴圈、條件或巢狀函式中呼叫 Hook;
  • 在 React 的函式元件中呼叫 Hook。

那為什麼會有這樣的限制呢?Hooks 的設計初衷是為了改進 React 元件的開發模式。在舊有的開發模式下遇到了三個問題。

  • 元件之間難以複用狀態邏輯。過去常見的解決方案是高階元件、render props 及狀態管理框架。
  • 複雜的元件變得難以理解。生命週期函式與業務邏輯耦合太深,導致關聯部分難以拆分。
  • 人和機器都很容易混淆類。常見的有 this 的問題,但在 React 團隊中還有類難以優化的問題,希望在編譯優化層面做出一些改進。

這三個問題在一定程度上阻礙了 React 的後續發展,所以為了解決這三個問題,Hooks 基於函式元件開始設計。然而第三個問題決定了 Hooks 只支援函式元件。

那為什麼不要在迴圈、條件或巢狀函式中呼叫 Hook 呢?因為 Hooks 的設計是基於陣列實現。在呼叫時按順序加入陣列中,如果使用迴圈、條件或巢狀函式很有可能導致陣列取值錯位,執行錯誤的 Hook。當然,實質上 React 的原始碼裡不是陣列,是連結串列。

這些限制會在編碼上造成一定程度的心智負擔,新手可能會寫錯,為了避免這樣的情況,可以引入 ESLint 的 Hooks 檢查外掛進行預防。

5. useEffect 與 useLayoutEffect 的區別

(1)共同點

  • 運用效果: useEffect 與 useLayoutEffect 兩者都是用於處理副作用,這些副作用包括改變 DOM、設定訂閱、操作定時器等。在函式元件內部操作副作用是不被允許的,所以需要使用這兩個函式去處理。
  • 使用方式: useEffect 與 useLayoutEffect 兩者底層的函式簽名是完全一致的,都是呼叫的 mountEffectImpl方法,在使用上也沒什麼差異,基本可以直接替換。

(2)不同點

  • 使用場景: useEffect 在 React 的渲染過程中是被非同步呼叫的,用於絕大多數場景;而 useLayoutEffect 會在所有的 DOM 變更之後同步呼叫,主要用於處理 DOM 操作、調整樣式、避免頁面閃爍等問題。也正因為是同步處理,所以需要避免在 useLayoutEffect 做計算量較大的耗時任務從而造成阻塞。
  • 使用效果: useEffect是按照順序執行程式碼的,改變螢幕畫素之後執行(先渲染,後改變DOM),當改變螢幕內容時可能會產生閃爍;useLayoutEffect是改變螢幕畫素之前就執行了(會推遲頁面顯示的事件,先改變DOM後渲染),不會產生閃爍。useLayoutEffect總是比useEffect先執行。

在未來的趨勢上,兩個 API 是會長期共存的,暫時沒有刪減合併的計劃,需要開發者根據場景去自行選擇。React 團隊的建議非常實用,如果實在分不清,先用 useEffect,一般問題不大;如果頁面有異常,再直接替換為 useLayoutEffect 即可。

6. React Hooks在平時開發中需要注意的問題和原因

(1)不要在迴圈,條件或巢狀函式中呼叫Hook,必須始終在 React函式的頂層使用Hook

這是因為React需要利用呼叫順序來正確更新相應的狀態,以及呼叫相應的鉤子函式。一旦在迴圈或條件分支語句中呼叫Hook,就容易導致呼叫順序的不一致性,從而產生難以預料到的後果。

(2)使用useState時候,使用push,pop,splice等直接更改陣列物件的坑

使用push直接更改陣列無法獲取到新值,應該採用析構方式,但是在class裡面不會有這個問題。程式碼示例:

function Indicatorfilter() {
  let [num,setNums] = useState([0,1,2,3])
  const test = () => {
    // 這裡坑是直接採用push去更新num
    // setNums(num)是無法更新num的
    // 必須使用num = [...num ,1]
    num.push(1)
    // num = [...num ,1]
    setNums(num)
  }
return (
    <div className='filter'>
      <div onClick={test}>測試</div>
        <div>
          {num.map((item,index) => (
              <div key={index}>{item}</div>
          ))}
      </div>
    </div>
  )
}

class Indicatorfilter extends React.Component<any,any>{
  constructor(props:any){
      super(props)
      this.state = {
          nums:[1,2,3]
      }
      this.test = this.test.bind(this)
  }

  test(){
      // class採用同樣的方式是沒有問題的
      this.state.nums.push(1)
      this.setState({
          nums: this.state.nums
      })
  }

  render(){
      let {nums} = this.state
      return(
          <div>
              <div onClick={this.test}>測試</div>
                  <div>
                      {nums.map((item:any,index:number) => (
                          <div key={index}>{item}</div>
                      ))}
                  </div>
          </div>

      )
  }
}

(3)useState設定狀態的時候,只有第一次生效,後期需要更新狀態,必須通過useEffect

TableDeail是一個公共元件,在呼叫它的父元件裡面,我們通過set改變columns的值,以為傳遞給TableDeail 的 columns是最新的值,所以tabColumn每次也是最新的值,但是實際tabColumn是最開始的值,不會隨著columns的更新而更新:

const TableDeail = ({
    columns,
}:TableData) => {
    const [tabColumn, setTabColumn] = useState(columns) 
}

// 正確的做法是通過useEffect改變這個值
const TableDeail = ({
    columns,
}:TableData) => {
    const [tabColumn, setTabColumn] = useState(columns) 
    useEffect(() =>{setTabColumn(columns)},[columns])
}

(4)善用useCallback

父元件傳遞給子元件事件控制代碼時,如果我們沒有任何引數變動可能會選用useMemo。但是每一次父元件渲染子元件即使沒變化也會跟著渲染一次。

(5)不要濫用useContext

可以使用基於 useContext 封裝的狀態管理工具。

7. React Hooks 和生命週期的關係?

函式元件 的本質是函式,沒有 state 的概念的,因此不存在生命週期一說,僅僅是一個 render 函式而已。 但是引入 Hooks 之後就變得不同了,它能讓元件在不使用 class 的情況下擁有 state,所以就有了生命週期的概念,所謂的生命週期其實就是 useStateuseEffect()useLayoutEffect()

即:Hooks 元件(使用了Hooks的函式元件)有生命週期,而函式元件(未使用Hooks的函式元件)是沒有生命週期的

下面是具體的 class 與 Hooks 的生命週期對應關係

  • constructor:函式元件不需要建構函式,可以通過呼叫 **useState 來初始化 state**。如果計算的代價比較昂貴,也可以傳一個函式給 useState
const [num, UpdateNum] = useState(0)
  • getDerivedStateFromProps:一般情況下,我們不需要使用它,可以在渲染過程中更新 state,以達到實現 getDerivedStateFromProps 的目的。
function ScrollView({row}) {
  let [isScrollingDown, setIsScrollingDown] = useState(false);
  let [prevRow, setPrevRow] = useState(null);
  if (row !== prevRow) {
    // Row 自上次渲染以來發生過改變。更新 isScrollingDown。
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }
  return `Scrolling down: ${isScrollingDown}`;
}

React 會立即退出第一次渲染並用更新後的 state 重新執行元件以避免耗費太多效能。

  • shouldComponentUpdate:可以用 **React.memo** 包裹一個元件來對它的 props 進行淺比較
const Button = React.memo((props) => {  // 具體的元件});

注意:**React.memo 等效於 **``**PureComponent**,它只淺比較 props。這裡也可以使用 useMemo 優化每一個節點。

  • render:這是函式元件體本身。
  • componentDidMount, componentDidUpdateuseLayoutEffect 與它們兩的呼叫階段是一樣的。但是,我們推薦你一開始先用 useEffect,只有當它出問題的時候再嘗試使用 useLayoutEffectuseEffect 可以表達所有這些的組合。
// componentDidMount
useEffect(()=>{
  // 需要在 componentDidMount 執行的內容
}, [])
useEffect(() => { 
  // 在 componentDidMount,以及 count 更改時 componentDidUpdate 執行的內容
  document.title = `You clicked ${count} times`; 
  return () => {
    // 需要在 count 更改時 componentDidUpdate(先於 document.title = ... 執行,遵守先清理後更新)
    // 以及 componentWillUnmount 執行的內容       
  } // 當函式中 Cleanup 函式會按照在程式碼中定義的順序先後執行,與函式本身的特性無關
}, [count]); // 僅在 count 更改時更新

請記得 React 會等待瀏覽器完成畫面渲染之後才會延遲呼叫 ,因此會使得額外操作很方便

  • componentWillUnmount:相當於 useEffect 裡面返回的 cleanup 函式
// componentDidMount/componentWillUnmount
useEffect(()=>{
  // 需要在 componentDidMount 執行的內容
  return function cleanup() {
    // 需要在 componentWillUnmount 執行的內容      
  }
}, [])
  • componentDidCatch and getDerivedStateFromError:目前還沒有這些方法的 Hook 等價寫法,但很快會加上。
class 元件 Hooks 元件
constructor useState
getDerivedStateFromProps useState 裡面 update 函式
shouldComponentUpdate useMemo
render 函式本身
componentDidMount useEffect
componentDidUpdate useEffect
componentWillUnmount useEffect 裡面返回的函式
componentDidCatch
getDerivedStateFromError

八、虛擬DOM

1. 對虛擬 DOM 的理解?虛擬 DOM 主要做了什麼?虛擬 DOM 本身是什麼?

從本質上來說,Virtual Dom是一個JavaScript物件,通過物件的方式來表示DOM結構。將頁面的狀態抽象為JS物件的形式,配合不同的渲染工具,使跨平臺渲染成為可能。通過事務處理機制,將多次DOM修改的結果一次性的更新到頁面上,從而有效的減少頁面渲染的次數,減少修改DOM的重繪重排次數,提高渲染效能。

虛擬DOM是對DOM的抽象,這個物件是更加輕量級的對DOM的描述。它設計的最初目的,就是更好的跨平臺,比如node.js就沒有DOM,如果想實現SSR,那麼一個方式就是藉助虛擬dom,因為虛擬dom本身是js物件。 在程式碼渲染到頁面之前,vue或者react會把程式碼轉換成一個物件(虛擬DOM)。以物件的形式來描述真實dom結構,最終渲染到頁面。在每次資料發生變化前,虛擬dom都會快取一份,變化之時,現在的虛擬dom會與快取的虛擬dom進行比較。在vue或者react內部封裝了diff演算法,通過這個演算法來進行比較,渲染時修改改變的變化,原先沒有發生改變的通過原先的資料進行渲染。

另外現代前端框架的一個基本要求就是無須手動操作DOM,一方面是因為手動操作DOM無法保證程式效能,多人協作的專案中如果review不嚴格,可能會有開發者寫出效能較低的程式碼,另一方面更重要的是省略手動DOM操作可以大大提高開發效率。

為什麼要用 Virtual DOM:

(1)保證效能下限,在不進行手動優化的情況下,提供過得去的效能

下面對比一下修改DOM時真實DOM操作和Virtual DOM的過程,來看一下它們重排重繪的效能消耗∶

  • 真實DOM∶ 生成HTML字串+ 重建所有的DOM元素
  • Virtual DOM∶ 生成vNode+ DOMDiff+必要的DOM更新

Virtual DOM的更新DOM的準備工作耗費更多的時間,也就是JS層面,相比於更多的DOM操作它的消費是極其便宜的。尤雨溪在社群論壇中說道∶ 框架給你的保證是,你不需要手動優化的情況下,我依然可以給你提供過得去的效能。 (2)跨平臺 Virtual DOM本質上是JavaScript的物件,它可以很方便的跨平臺操作,比如服務端渲染、uniapp等。

2. React diff 演算法的原理是什麼?

實際上,diff 演算法探討的就是虛擬 DOM 樹發生變化後,生成 DOM 樹更新補丁的方式。它通過對比新舊兩株虛擬 DOM 樹的變更差異,將更新補丁作用於真實 DOM,以最小成本完成檢視更新。 具體的流程如下:

  • 真實的 DOM 首先會對映為虛擬 DOM;
  • 當虛擬 DOM 發生變化後,就會根據差距計算生成 patch,這個 patch 是一個結構化的資料,內容包含了增加、更新、移除等;
  • 根據 patch 去更新真實的 DOM,反饋到使用者的介面上。

一個簡單的例子:

import React from 'react'
export default class ExampleComponent extends React.Component {
  render() {
    if(this.props.isVisible) {
       return <div className="visible">visbile</div>;
    }
     return <div className="hidden">hidden</div>;
  }
}

這裡,首先假定 ExampleComponent 可見,然後再改變它的狀態,讓它不可見 。對映為真實的 DOM 操作是這樣的,React 會建立一個 div 節點。

<div class="visible">visbile</div>

當把 visbile 的值變為 false 時,就會替換 class 屬性為 hidden,並重寫內部的 innerText 為 hidden。這樣一個生成補丁、更新差異的過程統稱為 diff 演算法。

diff演算法可以總結為三個策略,分別從樹、元件及元素三個層面進行復雜度的優化:

策略一:忽略節點跨層級操作場景,提升比對效率。(基於樹進行對比)

這一策略需要進行樹比對,即對樹進行分層比較。樹比對的處理手法是非常“暴力”的,即兩棵樹只對同一層次的節點進行比較,如果發現節點已經不存在了,則該節點及其子節點會被完全刪除掉,不會用於進一步的比較,這就提升了比對效率。

策略二:如果元件的 class 一致,則預設為相似的樹結構,否則預設為不同的樹結構。(基於元件進行對比)

在元件比對的過程中:

  • 如果元件是同一型別則進行樹比對;
  • 如果不是則直接放入補丁中。

只要父元件型別不同,就會被重新渲染。這也就是為什麼 shouldComponentUpdate、PureComponent 及 React.memo 可以提高效能的原因。

策略三:同一層級的子節點,可以通過標記 key 的方式進行列表對比。(基於節點進行對比)

元素比對主要發生在同層級中,通過標記節點操作生成補丁。節點操作包含了插入、移動、刪除等。其中節點重新排序同時涉及插入、移動、刪除三個操作,所以效率消耗最大,此時策略三起到了至關重要的作用。通過標記 key 的方式,React 可以直接移動 DOM 節點,降低內耗。

3. React key 是幹嘛用的 為什麼要加?key 主要是解決哪一類問題的

Keys 是 React 用於追蹤哪些列表中元素被修改、被新增或者被移除的輔助標識。在開發過程中,我們需要保證某個元素的 key 在其同級元素中具有唯一性。

在 React Diff 演算法中 React 會藉助元素的 Key 值來判斷該元素是新近建立的還是被移動而來的元素,從而減少不必要的元素重渲染此外,React 還需要藉助 Key 值來判斷元素與本地狀態的關聯關係。

注意事項:

  • key值一定要和具體的元素—一對應;
  • 儘量不要用陣列的index去作為key;
  • 不要在render的時候用隨機數或者其他操作給元素加上不穩定的key,這樣造成的效能開銷比不加key的情況下更糟糕。

4. 虛擬 DOM 的引入與直接操作原生 DOM 相比,哪一個效率更高,為什麼

虛擬DOM相對原生的DOM不一定是效率更高,如果只修改一個按鈕的文案,那麼虛擬 DOM 的操作無論如何都不可能比真實的 DOM 操作更快。在首次渲染大量DOM時,由於多了一層虛擬DOM的計算,虛擬DOM也會比innerHTML插入慢。它能保證效能下限,在真實DOM操作的時候進行鍼對性的優化時,還是更快的。所以要根據具體的場景進行探討。

在整個 DOM 操作的演化過程中,其實主要矛盾並不在於效能,而在於開發者寫得爽不爽,在於研發體驗/研發效率。虛擬 DOM 不是別的,正是前端開發們為了追求更好的研發體驗和研發效率而創造出來的高階產物。虛擬 DOM 並不一定會帶來更好的效能,React 官方也從來沒有把虛擬 DOM 作為效能層面的賣點對外輸出過。虛擬 DOM 的優越之處在於,它能夠在提供更爽、更高效的研發模式(也就是函式式的 UI 程式設計方式)的同時,仍然保持一個還不錯的效能。

5. React 與 Vue 的 diff 演算法有何不同?

diff 演算法是指生成更新補丁的方式,主要應用於虛擬 DOM 樹變化後,更新真實 DOM。所以 diff 演算法一定存在這樣一個過程:觸發更新 → 生成補丁 → 應用補丁。

React 的 diff 演算法,觸發更新的時機主要在 state 變化與 hooks 呼叫之後。此時觸發虛擬 DOM 樹變更遍歷,採用了深度優先遍歷演算法。但傳統的遍歷方式,效率較低。為了優化效率,使用了分治的方式。將單一節點比對轉化為了 3 種類型節點的比對,分別是樹、元件及元素,以此提升效率。

  • 樹比對:由於網頁檢視中較少有跨層級節點移動,兩株虛擬 DOM 樹只對同一層次的節點進行比較。
  • 元件比對:如果元件是同一型別,則進行樹比對,如果不是,則直接放入到補丁中。
  • 元素比對:主要發生在同層級中,通過標記節點操作生成補丁,節點操作對應真實的 DOM 剪裁操作。

以上是經典的 React diff 演算法內容。自 React 16 起,引入了 Fiber 架構。為了使整個更新過程可隨時暫停恢復,節點與樹分別採用了 FiberNode 與 FiberTree 進行重構。fiberNode 使用了雙鏈表的結構,可以直接找到兄弟節點與子節點。整個更新過程由 current 與 workInProgress 兩株樹雙緩衝完成。workInProgress 更新完成後,再通過修改 current 相關指標指向新節點。

Vue 的整體 diff 策略與 React 對齊,雖然缺乏時間切片能力,但這並不意味著 Vue 的效能更差,因為在 Vue 3 初期引入過,後期因為收益不高移除掉了。除了高幀率動畫,在 Vue 中其他的場景幾乎都可以使用防抖和節流去提高響應效能。

九、其他

1. React元件命名推薦的方式是哪個?

通過引用而不是使用來命名元件displayName。

使用displayName命名元件:

export default React.createClass({  displayName: 'TodoApp',  // ...})

React推薦的方法:

export default class TodoApp extends React.Component {  // ...}

2. react 最新版本解決了什麼問題,增加了哪些東西

React 16.x的三大新特性 Time Slicing、Suspense、 hooks

  • Time Slicing(解決CPU速度問題)使得在執行任務的期間可以隨時暫停,跑去幹別的事情,這個特性使得react能在效能極其差的機器跑時,仍然保持有良好的效能
  • Suspense (解決網路IO問題) 和lazy配合,實現非同步載入元件。 能暫停當前元件的渲染, 當完成某件事以後再繼續渲染,解決從react出生到現在都存在的「非同步副作用」的問題,而且解決得非的優雅,使用的是 T非同步但是同步的寫法,這是最好的解決非同步問題的方式
  • 提供了一個內建函式componentDidCatch,當有錯誤發生時,可以友好地展示 fallback 元件; 可以捕捉到它的子元素(包括巢狀子元素)丟擲的異常; 可以複用錯誤元件。

(1)React16.8 加入hooks,讓React函式式元件更加靈活,hooks之前,React存在很多問題:

  • 在元件間複用狀態邏輯很難
  • 複雜元件變得難以理解,高階元件和函式元件的巢狀過深。
  • class元件的this指向問題
  • 難以記憶的生命週期

hooks很好的解決了上述問題,hooks提供了很多方法

  • useState 返回有狀態值,以及更新這個狀態值的函式
  • useEffect 接受包含命令式,可能有副作用程式碼的函式。
  • useContext 接受上下文物件(從 React.createContext返回的值)並返回當前上下文值,
  • useReducer useState 的替代方案。接受型別為 (state,action)=> newState的reducer,並返回與dispatch方法配對的當前狀態。
  • useCalLback 返回一個回憶的memoized版本,該版本僅在其中一個輸入發生更改時才會更改。純函式的輸入輸出確定性 o useMemo 純的一個記憶函式 o useRef 返回一個可變的ref物件,其Current 屬性被初始化為傳遞的引數,返回的 ref 物件在元件的整個生命週期內保持不變。
  • useImperativeMethods 自定義使用ref時公開給父元件的例項值
  • useMutationEffect 更新兄弟元件之前,它在React執行其DOM改變的同一階段同步觸發
  • useLayoutEffect DOM改變後同步觸發。使用它來從DOM讀取佈局並同步重新渲染

(2)React16.9

  • 重新命名 Unsafe 的生命週期方法。新的 UNSAFE_字首將有助於在程式碼 review 和 debug 期間,使這些有問題的字樣更突出
  • 廢棄 javascrip:形式的 URL。以javascript:開頭的URL 非常容易遭受攻擊,造成安全漏洞。
  • 廢棄"Factory"元件。 工廠元件會導致 React 變大且變慢。
  • act()也支援非同步函式,並且你可以在呼叫它時使用 await。
  • 使用 <React.ProfiLer> 進行效能評估。在較大的應用中追蹤效能迴歸可能會很方便

(3)React16.13.0

  • 支援在渲染期間呼叫setState,但僅適用於同一組件
  • 可檢測衝突的樣式規則並記錄警告
  • 廢棄 unstable_createPortal,使用CreatePortal
  • 將元件堆疊新增到其開發警告中,使開發人員能夠隔離bug並除錯其程式,這可以清楚地說明問題所在,並更快地定位和修復錯誤。

3. react 實現一個全域性的 dialog

import React, { Component } from 'react';
import { is, fromJS } from 'immutable';
import ReactDOM from 'react-dom';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import './dialog.css';
let defaultState = {
  alertStatus:false,
  alertTip:"提示",
  closeDialog:function(){},
  childs:''
}
class Dialog extends Component{
  state = {
    ...defaultState
  };
  // css動畫元件設定為目標元件
  FirstChild = props => {
    const childrenArray = React.Children.toArray(props.children);
    return childrenArray[0] || null;
  }
  //開啟彈窗
  open =(options)=>{
    options = options || {};
    options.alertStatus = true;
    var props = options.props || {};
    var childs = this.renderChildren(props,options.childrens) || '';
    console.log(childs);
    this.setState({
      ...defaultState,
      ...options,
      childs
    })
  }
  //關閉彈窗
  close(){
    this.state.closeDialog();
    this.setState({
      ...defaultState
    })
  }
  renderChildren(props,childrens) {
    //遍歷所有子元件
    var childs = [];
    childrens = childrens || [];
    var ps = {
        ...props,  //給子元件繫結props
        _close:this.close  //給子元件也繫結一個關閉彈窗的事件    
       };
    childrens.forEach((currentItem,index) => {
        childs.push(React.createElement(
            currentItem,
            {
                ...ps,
                key:index
            }
        ));
    })
    return childs;
  }
  shouldComponentUpdate(nextProps, nextState){
    return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState))
  }
   
  render(){
    return (
      <ReactCSSTransitionGroup
        component={this.FirstChild}
        transitionName='hide'
        transitionEnterTimeout={300}
        transitionLeaveTimeout={300}>
        <div className="dialog-con" style={this.state.alertStatus? {display:'block'}:{display:'none'}}>
            {this.state.childs}
        </div>
      </ReactCSSTransitionGroup>
    );
  }
}
let div = document.createElement('div');
let props = {
   
};
document.body.appendChild(div);
let Box = ReactD

子類:

//子類jsx
import React, { Component } from 'react';
class Child extends Component {
    constructor(props){
        super(props);
        this.state = {date: new Date()};
  }
  showValue=()=>{
    this.props.showValue && this.props.showValue()
  }
  render() {
    return (
      <div className="Child">
        <div className="content">
           Child
           <button onClick={this.showValue}>呼叫父的方法</button>
        </div>
      </div>
    );
  }
}
export default Child;

css:

.dialog-con{
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.3);
}

4. React 資料持久化有什麼實踐嗎?

封裝資料持久化元件:

】let storage={
    // 增加
    set(key, value){
        localStorage.setItem(key, JSON.stringify(value));
    },
    // 獲取
    get(key){
        return JSON.parse(localStorage.getItem(key));
    },
    // 刪除
    remove(key){
        localStorage.removeItem(key);
    }
};
export default Storage;

在React專案中,通過redux儲存全域性資料時,會有一個問題,如果使用者重新整理了網頁,那麼通過redux儲存的全域性資料就會被全部清空,比如登入資訊等。這時就會有全域性資料持久化儲存的需求。首先想到的就是localStorage,localStorage是沒有時間限制的資料儲存,可以通過它來實現資料的持久化儲存。

但是在已經使用redux來管理和儲存全域性資料的基礎上,再去使用localStorage來讀寫資料,這樣不僅是工作量巨大,還容易出錯。那麼有沒有結合redux來達到持久資料儲存功能的框架呢?當然,它就是redux-persist。redux-persist會將redux的store中的資料快取到瀏覽器的localStorage中。其使用步驟如下:

(1)首先要安裝redux-persist:

npm i redux-persist

(2)對於reducer和action的處理不變,只需修改store的生成程式碼,修改如下:

import {createStore} from 'redux'
import reducers from '../reducers/index'
import {persistStore, persistReducer} from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
const persistConfig = {
    key: 'root',
    storage: storage,
    stateReconciler: autoMergeLevel2 // 檢視 'Merge Process' 部分的具體情況
};
const myPersistReducer = persistReducer(persistConfig, reducers)
const store = createStore(myPersistReducer)
export const persistor = persistStore(store)
export default store

(3)在index.js中,將PersistGate標籤作為網頁內容的父標籤:

import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux'
import store from './redux/store/store'
import {persistor} from './redux/store/store'
import {PersistGate} from 'redux-persist/lib/integration/react';
ReactDOM.render(<Provider store={store}>
            <PersistGate loading={null} persistor={persistor}>
                {/*網頁內容*/}
            </PersistGate>
        </Provider>, document.getElementById('root'));

這就完成了通過redux-persist實現React持久化本地資料儲存的簡單應用。

5. 對 React 和 Vue 的理解,它們的異同

相似之處:

  • 都將注意力集中保持在核心庫,而將其他功能如路由和全域性狀態管理交給相關的庫
  • 都有自己的構建工具,能讓你得到一個根據最佳實踐設定的專案模板。
  • 都使用了Virtual DOM(虛擬DOM)提高重繪效能
  • 都有props的概念,允許元件間的資料傳遞
  • 都鼓勵元件化應用,將應用分拆成一個個功能明確的模組,提高複用性

不同之處:

1)資料流

Vue預設支援資料雙向繫結,而React一直提倡單向資料流

2)虛擬DOM

Vue2.x開始引入"Virtual DOM",消除了和React在這方面的差異,但是在具體的細節還是有各自的特點。

  • Vue宣稱可以更快地計算出Virtual DOM的差異,這是由於它在渲染過程中,會跟蹤每一個元件的依賴關係,不需要重新渲染整個元件樹。
  • 對於React而言,每當應用的狀態被改變時,全部子元件都會重新渲染。當然,這可以通過 PureComponent/shouldComponentUpdate這個生命週期方法來進行控制,但Vue將此視為預設的優化。

3)元件化

React與Vue最大的不同是模板的編寫。

  • Vue鼓勵寫近似常規HTML的模板。寫起來很接近標準 HTML元素,只是多了一些屬性。
  • React推薦你所有的模板通用JavaScript的語法擴充套件——JSX書寫。

具體來講:React中render函式是支援閉包特性的,所以我們import的元件在render中可以直接呼叫。但是在Vue中,由於模板中使用的資料都必須掛在 this 上進行一次中轉,所以 import 完元件之後,還需要在 components 中再宣告下。

4)監聽資料變化的實現原理不同

  • Vue 通過 getter/setter 以及一些函式的劫持,能精確知道資料變化,不需要特別的優化就能達到很好的效能
  • React 預設是通過比較引用的方式進行的,如果不優化(PureComponent/shouldComponentUpdate)可能導致大量不必要的vDOM的重新渲染。這是因為 Vue 使用的是可變資料,而React更強調資料的不可變。

5)高階元件

react可以通過高階元件(Higher Order Components-- HOC)來擴充套件,而vue需要通過mixins來擴充套件。

原因高階元件就是高階函式,而React的元件本身就是純粹的函式,所以高階函式對React來說易如反掌。相反Vue.js使用HTML模板建立檢視元件,這時模板無法有效的編譯,因此Vue不採用HOC來實現。

6)構建工具

兩者都有自己的構建工具

  • React ==> Create React APP
  • Vue ==> vue-cli

7)跨平臺

  • React ==> React Native
  • Vue ==> Weex

6. 可以使用TypeScript寫React應用嗎?怎麼操作?

(1)如果還未建立 Create React App 專案

  • 直接建立一個具有 typescript 的 Create React App 專案:
 npx create-react-app demo --typescript

(2)如果已經建立了 Create React App 專案,需要將 typescript 引入到已有專案中

  • 通過命令將 typescript 引入專案:
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
  • 將專案中任何 字尾名為 ‘.js’ 的 JavaScript 檔案重新命名為 TypeScript 檔案即字尾名為 ‘.tsx’(例如 src/index.js 重新命名為 src/index.tsx )

7. React 設計思路,它的理念是什麼?

(1)編寫簡單直觀的程式碼

React最大的價值不是高效能的虛擬DOM、封裝的事件機制、伺服器端渲染,而是宣告式的直觀的編碼方式。react文件第一條就是宣告式,React 使建立互動式 UI 變得輕而易舉。為應用的每一個狀態設計簡潔的檢視,當資料改變時 React 能有效地更新並正確地渲染元件。 以宣告式編寫 UI,可以讓程式碼更加可靠,且方便除錯。

(2)簡化可複用的元件

React框架裡面使用了簡化的元件模型,但更徹底地使用了元件化的概念。React將整個UI上的每一個功能模組定義成元件,然後將小的元件通過組合或者巢狀的方式構成更大的元件。React的元件具有如下的特性∶

  • 可組合:簡單元件可以組合為複雜的元件
  • 可重用:每個元件都是獨立的,可以被多個元件使用
  • 可維護:和元件相關的邏輯和UI都封裝在了元件的內部,方便維護
  • 可測試:因為元件的獨立性,測試元件就變得方便很多。

(3) Virtual DOM

真實頁面對應一個 DOM 樹。在傳統頁面的開發模式中,每次需要更新頁面時,都要手動操作 DOM 來進行更新。 DOM 操作非常昂貴。在前端開發中,效能消耗最大的就是 DOM 操作,而且這部分程式碼會讓整體專案的程式碼變得難 以維護。React 把真實 DOM 樹轉換成 JavaScript 物件樹,也就是 Virtual DOM,每次資料更新後,重新計算 Virtual DOM,並和上一次生成的 Virtual DOM 做對比,對發生變化的部分做批量更新。React 也提供了直觀的 shouldComponentUpdate 生命週期回撥,來減少資料變化後不必要的 Virtual DOM 對比過程,以保證效能。

(4)函數語言程式設計

React 把過去不斷重複構建 UI 的過程抽象成了元件,且在給定引數的情況下約定渲染對應的 UI 介面。React 能充分利用很多函式式方法去減少冗餘程式碼。此外,由於它本身就是簡單函式,所以易於測試。

(5)一次學習,隨處編寫

無論現在正在使用什麼技術棧,都可以隨時引入 React來開發新特性,而不需要重寫現有程式碼。

React 還可以使用 Node 進行伺服器渲染,或使用 React Native 開發原生移動應用。因為 React 元件可以對映為對應的原生控制元件。在輸出的時候,是輸出 Web DOM,還是 Android 控制元件,還是 iOS 控制元件,就由平臺本身決定了。所以,react很方便和其他平臺整合

8. React中props.children和React.Children的區別

在React中,當涉及元件巢狀,在父元件中使用props.children把所有子元件顯示出來。如下:

function ParentComponent(props){
	return (
		<div>
			{props.children}
		</div>
	)
}

如果想把父元件中的屬性傳給所有的子元件,需要使用React.Children方法。

比如,把幾個Radio組合起來,合成一個RadioGroup,這就要求所有的Radio具有同樣的name屬性值。可以這樣:把Radio看做子元件,RadioGroup看做父元件,name的屬性值在RadioGroup這個父元件中設定。

首先是子元件:

//子元件
function RadioOption(props) {
  return (
    <label>
      <input type="radio" value={props.value} name={props.name} />
      {props.label}
    </label>
  )
}

然後是父元件,不僅需要把它所有的子元件顯示出來,還需要為每個子元件賦上name屬性和值:

//父元件用,props是指父元件的props
function renderChildren(props) {
    
  //遍歷所有子元件
  return React.Children.map(props.children, child => {
    if (child.type === RadioOption)
      return React.cloneElement(child, {
        //把父元件的props.name賦值給每個子元件
        name: props.name
      })
    else
      return child
  })
}
//父元件
function RadioGroup(props) {
  return (
    <div>
      {renderChildren(props)}
    </div>
  )
}
function App() {
  return (
    <RadioGroup name="hello">
      <RadioOption label="選項一" value="1" />
      <RadioOption label="選項二" value="2" />
      <RadioOption label="選項三" value="3" />
    </RadioGroup>
  )
}
export default App;

以上,React.Children.map讓我們對父元件的所有子元件又更靈活的控制。

9. React的狀態提升是什麼?使用場景有哪些?

React的狀態提升就是使用者對子元件操作,子元件不改變自己的狀態,通過自己的props把這個操作改變的資料傳遞給父元件,改變父元件的狀態,從而改變受父元件控制的所有子元件的狀態,這也是React單項資料流的特性決定的。官方的原話是:共享 state(狀態) 是通過將其移動到需要它的元件的最接近的共同祖先元件來實現的。 這被稱為“狀態提升(Lifting State Up)”。

概括來說就是將多個元件需要共享的狀態提升到它們最近的父元件上在父元件上改變這個狀態然後通過props分發給子元件。

一個簡單的例子,父元件中有兩個input子元件,如果想在第一個輸入框輸入資料,來改變第二個輸入框的值,這就需要用到狀態提升。

class Father extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            Value1: '',
            Value2: ''
        }
    }
    value1Change(aa) {
        this.setState({
            Value1: aa
        })
    }
    value2Change(bb) {
        this.setState({
            Value2: bb
        })
    }
    render() {
        return (
            <div style={{ padding: "100px" }}>
                <Child1 value1={this.state.Value1} onvalue1Change={this.value1Change.bind(this)} />
                

                <Child2 value2={this.state.Value1} />
            </div>
        )
    }
}
class Child1 extends React.Component {
    constructor(props) {
        super(props)
    }
    changeValue(e) {
        this.props.onvalue1Change(e.target.value)
    }
    render() {
        return (
            <input value={this.props.Value1} onChange={this.changeValue.bind(this)} />
        )
    }
}
class Child2 extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return (
            <input value={this.props.value2} />
        )
    }
}
 
ReactDOM.render(
    <Father />,
    document.getElementById('root')
)

10. React中constructor和getInitialState的區別?

兩者都是用來初始化state的。前者是ES6中的語法,後者是ES5中的語法,新版本的React中已經廢棄了該方法。

getInitialState是ES5中的方法,如果使用createClass方法建立一個Component元件,可以自動呼叫它的getInitialState方法來獲取初始化的State物件,

var APP = React.creatClass ({
  getInitialState() {
    return { 
        userName: 'hi',
        userId: 0
     };
 }
})

React在ES6的實現中去掉了getInitialState這個hook函式,規定state在constructor中實現,如下:

Class App extends React.Component{
    constructor(props){
      super(props);
      this.state={};
    }
  }

11. React的嚴格模式如何使用,有什麼用處?

StrictMode 是一個用來突出顯示應用程式中潛在問題的工具。與 Fragment 一樣,StrictMode 不會渲染任何可見的 UI。它為其後代元素觸發額外的檢查和警告。 可以為應用程式的任何部分啟用嚴格模式。例如:

import React from 'react';
function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>        
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>      
      <Footer />
    </div>
  );
}

在上述的示例中,不會對 HeaderFooter 元件執行嚴格模式檢查。但是,ComponentOneComponentTwo 以及它們的所有後代元素都將進行檢查。

StrictMode 目前有助於:

  • 識別不安全的生命週期
  • 關於使用過時字串 ref API 的警告
  • 關於使用廢棄的 findDOMNode 方法的警告
  • 檢測意外的副作用
  • 檢測過時的 context API

12. 在React中遍歷的方法有哪些?

(1)遍歷陣列:map && forEach

import React from 'react';

class App extends React.Component {
  render() {
    let arr = ['a', 'b', 'c', 'd'];
    return (
      <ul>
        {
          arr.map((item, index) => {
            return <li key={index}>{item}</li>
          })
        }
      </ul>
    )
  }
}

class App extends React.Component {
  render() {
    let arr = ['a', 'b', 'c', 'd'];
    return (
      <ul>
        {
          arr.forEach((item, index) => {
            return <li key={index}>{item}</li>
          })
        }
      </ul>
    )
  }
}

(2)遍歷物件:map && for in

class App extends React.Component {
  render() {
    let obj = {
      a: 1,
      b: 2,
      c: 3
    }
    return (
      <ul>
        {
          (() => {
            let domArr = [];
            for(const key in obj) {
              if(obj.hasOwnProperty(key)) {
                const value = obj[key]
                domArr.push(<li key={key}>{value}</li>)
              }
            }
            return domArr;
          })()
        }
      </ul>
    )
  }
}

// Object.entries() 把物件轉換成陣列
class App extends React.Component {
  render() {
    let obj = {
      a: 1,
      b: 2,
      c: 3
    }
    return (
      <ul>
        {
          Object.entries(obj).map(([key, value], index) => {   // item是一個數組,把item解構,寫法是[key, value]
            return <li key={key}>{value}</li>
          }) 
        }
      </ul>
    )
  }
}

13. 在React中頁面重新載入時怎樣保留資料?

這個問題就設計到了資料持久化, 主要的實現方式有以下幾種:

  • Redux: 將頁面的資料儲存在redux中,在重新載入頁面時,獲取Redux中的資料;
  • data.js: 使用webpack構建的專案,可以建一個檔案,data.js,將資料儲存data.js中,跳轉頁面後獲取;
  • sessionStorge: 在進入選擇地址頁面之前,componentWillUnMount的時候,將資料儲存到sessionStorage中,每次進入頁面判斷sessionStorage中有沒有儲存的那個值,有,則讀取渲染資料;沒有,則說明資料是初始化的狀態。返回或進入除了選擇地址以外的頁面,清掉儲存的sessionStorage,保證下次進入是初始化的資料
  • history API: History API 的 pushState 函式可以給歷史記錄關聯一個任意的可序列化 state,所以可以在路由 push 的時候將當前頁面的一些資訊存到 state 中,下次返回到這個頁面的時候就能從 state 裡面取出離開前的資料重新渲染。react-router 直接可以支援。這個方法適合一些需要臨時儲存的場景。

14. 同時引用這三個庫react.js、react-dom.js和babel.js它們都有什麼作用?

  • react:包含react所必須的核心程式碼
  • react-dom:react渲染在不同平臺所需要的核心程式碼
  • babel:將jsx轉換成React程式碼的工具

15. React必須使用JSX嗎?

React 並不強制要求使用 JSX。當不想在構建環境中配置有關 JSX 編譯時,不在 React 中使用 JSX 會更加方便。

每個 JSX 元素只是呼叫 React.createElement(component, props, ...children) 的語法糖。因此,使用 JSX 可以完成的任何事情都可以通過純 JavaScript 完成。

例如,用 JSX 編寫的程式碼:

class Hello extends React.Component {
  render() {
    return <div>Hello {this.props.toWhat}</div>;
  }
}
ReactDOM.render(
  <Hello toWhat="World" />,
  document.getElementById('root')
);

可以編寫為不使用 JSX 的程式碼:

class Hello extends React.Component {
  render() {
    return React.createElement('div', null, `Hello ${this.props.toWhat}`);
  }
}
ReactDOM.render(
  React.createElement(Hello, {toWhat: 'World'}, null),
  document.getElementById('root')
);

16. 為什麼使用jsx的元件中沒有看到使用react卻需要引入react?

本質上來說JSX是React.createElement(component, props, ...children)方法的語法糖。在React 17之前,如果使用了JSX,其實就是在使用React, babel 會把元件轉換為 CreateElement 形式。在React 17之後,就不再需要引入,因為 babel 已經可以幫我們自動引入react。

17. 在React中怎麼使用async/await?

async/await是ES7標準中的新特性。如果是使用React官方的腳手架建立的專案,就可以直接使用。如果是在自己搭建的webpack配置的專案中使用,可能會遇到 regeneratorRuntime is not defined 的異常錯誤。那麼我們就需要引入babel,並在babel中配置使用async/await。可以利用babel的 transform-async-to-module-method 外掛來轉換其成為瀏覽器支援的語法,雖然沒有效能的提升,但對於程式碼編寫體驗要更好。

18. React.Children.map和js的map有什麼區別?

JavaScript中的map不會對為null或者undefined的資料進行處理,而React.Children.map中的map可以處理React.Children為null或者undefined的情況。

19. 對React SSR的理解

服務端渲染是資料與模版組成的html,即 HTML = 資料 + 模版。將元件或頁面通過伺服器生成html字串,再發送到瀏覽器,最後將靜態標記"混合"為客戶端上完全互動的應用程式。頁面沒使用服務渲染,當請求頁面時,返回的body裡為空,之後執行js將html結構注入到body裡,結合css顯示出來;

SSR的優勢:

  • 對SEO友好
  • 所有的模版、圖片等資源都存在伺服器端
  • 一個html返回所有資料
  • 減少HTTP請求
  • 響應快、使用者體驗好、首屏渲染快

1)更利於SEO

不同爬蟲工作原理類似,只會爬取原始碼,不會執行網站的任何指令碼使用了React或者其它MVVM框架之後,頁面大多數DOM元素都是在客戶端根據js動態生成,可供爬蟲抓取分析的內容大大減少。另外,瀏覽器爬蟲不會等待我們的資料完成之後再去抓取頁面資料。服務端渲染返回給客戶端的是已經獲取了非同步資料並執行JavaScript指令碼的最終HTML,網路爬中就可以抓取到完整頁面的資訊。

2)更利於首屏渲染

首屏的渲染是node傳送過來的html字串,並不依賴於js檔案了,這就會使使用者更快的看到頁面的內容。尤其是針對大型單頁應用,打包後文件體積比較大,普通客戶端渲染載入所有所需檔案時間較長,首頁就會有一個很長的白屏等待時間。

SSR的侷限:

1)服務端壓力較大

本來是通過客戶端完成渲染,現在統一到服務端node服務去做。尤其是高併發訪問的情況,會大量佔用服務端CPU資源;

2)開發條件受限

在服務端渲染中,只會執行到componentDidMount之前的生命週期鉤子,因此專案引用的第三方的庫也不可用其它生命週期鉤子,這對引用庫的選擇產生了很大的限制;

3)學習成本相對較高 除了對webpack、MVVM框架要熟悉,還需要掌握node、 Koa2等相關技術。相對於客戶端渲染,專案構建、部署過程更加複雜。

時間耗時比較:

1)資料請求

由服務端請求首屏資料,而不是客戶端請求首屏資料,這是"快"的一個主要原因。服務端在內網進行請求,資料響應速度快。客戶端在不同網路環境進行資料請求,且外網http請求開銷大,導致時間差

  • 客戶端資料請求
  • 服務端資料請求

2)html渲染 服務端渲染是先向後端伺服器請求資料,然後生成完整首屏 html返回給瀏覽器;而客戶端渲染是等js程式碼下載、載入、解析完成後再請求資料渲染,等待的過程頁面是什麼都沒有的,就是使用者看到的白屏。就是服務端渲染不需要等待js程式碼下載完成並請求資料,就可以返回一個已有完整資料的首屏頁面。

  • 非ssr html渲染
  • ssr html渲染

20. 為什麼 React 要用 JSX?

JSX 是一個 JavaScript 的語法擴充套件,或者說是一個類似於 XML 的 ECMAScript 語法擴充套件。它本身沒有太多的語法定義,也不期望引入更多的標準。

其實 React 本身並不強制使用 JSX。在沒有 JSX 的時候,React 實現一個元件依賴於使用 React.createElement 函式。程式碼如下:

class Hello extends React.Component {
  render() {
    return React.createElement(
        'div',
        null, 
        `Hello ${this.props.toWhat}`
      );
  }
}
ReactDOM.render(
  React.createElement(Hello, {toWhat: 'World'}, null),
  document.getElementById('root')
);

而 JSX 更像是一種語法糖,通過類似 XML 的描述方式,描寫函式物件。在採用 JSX 之後,這段程式碼會這樣寫:

class Hello extends React.Component {
  render() {
    return <div>Hello {this.props.toWhat}</div>;
  }
}
ReactDOM.render(
  <Hello toWhat="World" />,
  document.getElementById('root')
);

通過對比,可以清晰地發現,程式碼變得更為簡潔,而且程式碼結構層次更為清晰。

因為 React 需要將元件轉化為虛擬 DOM 樹,所以在編寫程式碼時,實際上是在手寫一棵結構樹。而XML 在樹結構的描述上天生具有可讀性強的優勢。

但這樣可讀性強的程式碼僅僅是給寫程式的同學看的,實際上在執行的時候,會使用 Babel 外掛將 JSX 語法的程式碼還原為 React.createElement 的程式碼。

總結: JSX 是一個 JavaScript 的語法擴充套件,結構類似 XML。JSX 主要用於宣告 React 元素,但 React 中並不強制使用 JSX。即使使用了 JSX,也會在構建過程中,通過 Babel 外掛編譯為 React.createElement。所以 JSX 更像是 React.createElement 的一種語法糖。

React 團隊並不想引入 JavaScript 本身以外的開發體系。而是希望通過合理的關注點分離保持元件開發的純粹性。

21. HOC相比 mixins 有什麼優點?

HOC 和 Vue 中的 mixins 作用是一致的,並且在早期 React 也是使用 mixins 的方式。但是在使用 class 的方式建立元件以後,mixins 的方式就不能使用了,並且其實 mixins 也是存在一些問題的,比如:

  • 隱含了一些依賴,比如我在元件中寫了某個 state 並且在 mixin 中使用了,就這存在了一個依賴關係。萬一下次別人要移除它,就得去 mixin 中查詢依賴
  • 多個 mixin 中可能存在相同命名的函式,同時代碼元件中也不能出現相同命名的函式,否則就是重寫了,其實我一直覺得命名真的是一件麻煩事。。
  • 雪球效應,雖然我一個元件還是使用著同一個 mixin,但是一個 mixin 會被多個元件使用,可能會存在需求使得 mixin 修改原本的函式或者新增更多的函式,這樣可能就會產生一個維護成本

HOC 解決了這些問題,並且它們達成的效果也是一致的,同時也更加的政治正確(畢竟更加函式式了)。

22. React 中的高階元件運用了什麼設計模式?

使用了裝飾模式,高階元件的運用:

function withWindowWidth(BaseComponent) {
  class DerivedClass extends React.Component {
    state = {
      windowWidth: window.innerWidth,
    }
    onResize = () => {
      this.setState({
        windowWidth: window.innerWidth,
      })
    }
    componentDidMount() {
      window.addEventListener('resize', this.onResize)
    }
    componentWillUnmount() {
      window.removeEventListener('resize', this.onResize);
    }
    render() {
      return <BaseComponent {...this.props} {...this.state}/>
    }
  }
  return DerivedClass;
}
const MyComponent = (props) => {
  return <div>Window width is: {props.windowWidth}</div>
};
export default withWindowWidth(MyComponent);

裝飾模式的特點是不需要改變 被裝飾物件 本身,而只是在外面套一個外殼介面。JavaScript 目前已經有了原生裝飾器的提案,其用法如下:

@testable
   class MyTestableClass {
}


轉自:https://juejin.cn/post/6940942549305524238