完全是不可能滴, 這輩子都不可能完全! -- 來自某非著名碼農
本文總結 React 實用的特性,部分實驗性和不實用的功能將不會納入進來,或許未來可期~
1、setState
- 面試題
class App extends Component { state = { val: 0 } // 震驚!隔壁老王也有失手的時候~ componentDidMount() { this.setState({val: this.state.val + 1}); console.log(this.state.val); // ? this.setState((prevState) => ({val: prevState.val + 1})); console.log(this.state.val); // ? setTimeout(() => { this.setState({val: this.state.val + 1}); console.log(this.state.val); // ? this.setState({val: this.state.val + 1}); console.log(this.state.val); // ? }, 1000) } render() { return <h2>App元件</h2>; } } 複製程式碼
- 總結
setState
只在 React 合成事件和鉤子函式中是“非同步”的,在原生DOM事件和定時器中都是同步的。- 如果需要獲取“非同步”場景的
setState
的值 -->this.setState(partial, callback)
在 callback 中拿到最新的值 - 如果要在“非同步”場景保證同步更新多次
setState
-->this.setState((prevState, props) => {return newState})
- 能保證同步更新, 但是外面獲取的值還是之前的值
2、Fragment
-
before
- 程式碼
export default class App extends Component { render() { return ( <div> <h2>App元件</h2> <p>這是App元件的內容</p> </div> ); } } 複製程式碼
- 效果
- 程式碼
-
after
- 程式碼
export default class App extends Component { render() { return ( <Fragment> <h2>App元件</h2> <p>這是App元件的內容</p> </Fragment> ); } } 複製程式碼
- 效果
- 程式碼
-
總結:使用 Fragment ,可以不用新增額外的DOM節點
3、React效能優化
-
shouldComponentUpdate
// 舉個例子: shouldComponentUpdate(nextProps, nextState) { if (nextProps !== this.props) { return true; // 允許更新 } if (nextState !== this.state) { return true; } return false; // 不允許更新 } 複製程式碼
-
PureComponent
元件- 使用
// 實現了對 state 和 props 的淺比較 // 相等就不更新,不相等才更新 class App extends PureComponent {} 複製程式碼
- 淺比較原始碼
// 實現 Object.is() 方法, 判斷x y是否完全相等 function is(x, y) { // (x !== 0 || 1 / x === 1 / y) 用於判斷 0 和 -0 不相等 // x !== x && y !== y 用於判斷 NaN 等於 NaN return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y ; } // 提取了hasOwnProperty方法,快取 var hasOwnProperty$1 = Object.prototype.hasOwnProperty; // 返回false為更新,true為不更新 function shallowEqual(objA, objB) { // 如果A和B完全相等,返回true if (is(objA, objB)) { return true; } // 如果A和B不相等,並且不是物件,說明就是普通值,返回false if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false; } // 提取A和B的所有屬性 var keysA = Object.keys(objA); var keysB = Object.keys(objB); // 如果長度不相等,返回false if (keysA.length !== keysB.length) { return false; } // 檢測 A 的屬性 和 B 的屬性是否一樣 for (var i = 0; i < keysA.length; i++) { if (!hasOwnProperty$1.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { return false; } } return true; } 複製程式碼
- 使用
-
問題:如果使用
pureComponent
只能進行淺比較,如果修改了原資料再更新,就會導致地址值一樣從而不會更新。但實際需要更新。 -
解決:
- 保證每次都是新的值
- 使用
immutable-js
庫,這個庫保證生成的值都是唯一的var map1 = Immutable.Map({ a: 1, b: 2, c: 3 }); // 設定值 var map2 = map1.set('a', 66); // 讀取值 map1.get('a'); // 1 map2.get('a'); // 66 複製程式碼
-
總結:使用以上方式,可以減少不必要的重複渲染。
4、React 高階元件
- 基本使用
// WrappedComponent 就是傳入的包裝元件 function withHoc(WrappedComponent) { return class extends Component { render () { return <WrappedComponent />; } } } // 使用 withHoc(App) 複製程式碼
- 向其中傳參
function withHoc(params) { return (WrappedComponent) => { return class extends Component { render () { return <WrappedComponent />; } } } } // 使用 withHoc('hello hoc')(App) 複製程式碼
- 接受props
function withHoc(params) { return (WrappedComponent) => { return class extends Component { render () { // 將接受的 props 傳遞給包裝元件使用 return <WrappedComponent {...this.props}/>; } } } } 複製程式碼
- 定義元件名稱
function withHoc(params) { return (WrappedComponent) => { return class extends Component { // 定義靜態方法,修改元件在除錯工具中顯示的名稱 static displayName = `Form(${getDisplayName(WrappedComponent)})` render () { return <WrappedComponent {...this.props}/>; } } } } // 封裝獲取包裝元件的 displayName 的方法 function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } 複製程式碼
5、render props
原文太長,直接上 連結
官網真香, 建議大家將 React 官網過一遍~
6、React 懶載入
react-loadable
import Loadable from 'react-loadable'; import Loading from './components/loading' const LoadableComponent = Loadable({ loader: () => import('./components/home'), loading: Loading, }); export default class App extends Component { render() { return ( <div> <h2>App元件</h2> <LoadableComponent /> </div> ); } } 複製程式碼
Suspense
和lazy
import React, {Component, Suspense, lazy} from 'react'; import Loading from './components/loading'; const LazyComponent = lazy(() => import('./components/home')); export default class App extends Component { render() { return ( <div> <h2>App元件</h2> <Suspense fallback={<Loading />}> <LazyComponent /> </Suspense> </div> ); } } 複製程式碼
- 區別
react-loadable
是民間 --> 需要額外下載引入Suspense
和lazy
是官方 --> 只需引入react-loadable
支援伺服器渲染Suspense
和lazy
不支援伺服器渲染
- 總結:使用 create-react-app 會將其單獨提取成一個bundle輸出,從而資源可以懶載入和重複利用。
7、虛擬DOM diff演算法
-
虛擬DOM diff演算法主要就是對以下三種場景進行優化:
-
tree diff
- 對樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。(因為 DOM 節點跨層級的移動操作少到可以忽略不計)
- 如果父節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用於進一步的比較。
- 注意:
- React 官方建議不要進行 DOM 節點跨層級的操作,非常影響 React 效能。
- 在開發元件時,保持穩定的 DOM 結構會有助於效能的提升。例如,可以通過 CSS 隱藏或顯示節點,而不是真的移除或新增 DOM 節點。
-
component diff
- 如果是同一型別的元件,按照原策略繼續比較 virtual DOM tree(tree diff)。
- 對於同一型別的元件,有可能其 Virtual DOM 沒有任何變化,如果能夠確切的知道這點那可以節省大量的 diff 運算時間,因此 React 允許使用者通過 shouldComponentUpdate() 來判斷該元件是否需要進行 diff。
- 如果不是,直接替換整個元件下的所有子節點。
- 如果是同一型別的元件,按照原策略繼續比較 virtual DOM tree(tree diff)。
-
element diff
- 對處於同一層級的節點進行對比。
- 這時 React 建議:新增唯一 key 進行區分。雖然只是小小的改動,效能上卻發生了翻天覆地的變化!
- 如: A B C D --> B A D C
- 新增 key 之前: 發現 B != A,則建立並插入 B 至新集合,刪除老集合 A;以此類推,建立並插入 A、D 和 C,刪除 B、C 和 D。
- 新增 key 之後: B、D 不做任何操作,A、C 進行移動操作,即可。
- 建議:在開發過程中,儘量減少類似將最後一個節點移動到列表首部的操作,當節點數量過大或更新操作過於頻繁時,在一定程度上會影響 React 的渲染效能。
-
總結
- React 通過制定大膽的 diff 策略,將 O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題;
- React 通過分層求異的策略,對 tree diff 進行演算法優化;
- React 通過相同類生成相似樹形結構,不同類生成不同樹形結構的策略,對 component diff 進行演算法優化;
- React 通過設定唯一 key的策略,對 element diff 進行演算法優化;
- 建議,在開發元件時,保持穩定的 DOM 結構會有助於效能的提升;
- 建議,在開發過程中,儘量減少類似將最後一個節點移動到列表首部的操作,當節點數量過大或更新操作過於頻繁時,在一定程度上會影響 React 的渲染效能。
8、Fiber
Fiber
是為了解決 React 專案的效能問題和之前的一些痛點而誕生的。Fiber
的核心流程可以分為兩個部分:- 可中斷的 render/reconciliation 通過構造 workInProgress tree 得出 change。
- 不可中斷的 commit 應用這些 DOM change。
- 非同步實現不同優先順序任務的協調執行:
requestIdleCallback
: 線上程空閒時期排程執行低優先順序函式;requestAnimationFrame
: 在下一個動畫幀排程執行高優先順序函式;
- 總結
- 可切分,可中斷任務。
- 可重用各分階段任務,且可以設定優先順序。
- 可以在父子元件任務間前進/後退切換任務。
- render方法可以返回多元素(即可以返回陣列)。
- 支援異常邊界處理異常。
9、Redux
- 作用: 集中管理多個元件共享的狀態
- 特點: 單一資料來源、純函式、只讀state
- redux 核心模組定義:
- store.js
import { createStore, applyMiddleware } from 'redux'; // 非同步actions使用的中介軟體 import thunk from 'redux-thunk'; // redux開發chrome除錯外掛 import { composeWithDevTools } from 'redux-devtools-extension'; import reducers from './reducers'; export default createStore(reducers, composeWithDevTools(applyMiddleware(thunk))); 複製程式碼
- reducers.js
import { combineReducers } from 'redux'; import { TEST1, TEST2 } from './action-types'; function a(prevState = 0, action) { switch (action.type) { case TEST1 : return action.data + 1; default : return prevState; } } function b(prevState = 0, action) { switch (action.type) { case TEST2 : return action.data + 1; default : return prevState; } } // 組合兩個reducer函式並暴露出去 export default combineReducers({a, b}); 複製程式碼
- actions.js
import { TEST1, TEST2 } from './action-types'; // 同步action creator,返回值為action物件 export const test1 = (data) => ({type: TEST1, data}); export const test2 = (data) => ({type: TEST2, data}); // 非同步action creator,返回值為函式 export const test2Async = (data) => { return (dispatch) => { setTimeout(() => { dispatch(test2(data)); }, 1000) } }; 複製程式碼
- action-types.js
export const TEST1 = 'test1'; export const TEST2 = 'test2'; 複製程式碼
- store.js
- 元件內使用:
- App.jsx
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import {test1, test2Async} from './redux/actions'; class App extends Component { static propTypes = { a: PropTypes.number.isRequired, b: PropTypes.number.isRequired, test1: PropTypes.func.isRequired, test2Async: PropTypes.func.isRequired, } componentDidMount() { const { a, b, test1, test2Async } = this.props; // 測試 test1(a + 1); test2Async(b + 1); } render() { return ( <div>App元件</div> ); } } /* =============== redux相關程式碼 ================== */ // 將狀態資料對映為屬性以props方式傳入元件 const mapStateToProps = (state) => ({a: state.a, b: state.b}); // 將操作狀態資料的方法對映為屬性以props方式傳入元件 const mapDispatchToProps = (dispatch) => { return { test1(data) { dispatch(test1(data)); }, test2Async(data) { dispatch(test2Async(data)); } } } // connect就是一個典型的HOC export default connect(mapStateToProps, mapDispatchToProps)(App); /* // 上面寫的太複雜了,但是好理解。而以下就是上面的簡寫方式 export default connect( (state) => ({...state}), { test1, test2Async } )(App); */ 複製程式碼
- index.js
// 入口檔案的配置 import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import App from './App'; import store from './redux/store'; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root')); 複製程式碼
- App.jsx
- 總結:
- 我們會發現使用
Redux
會變得更加複雜,以及多了很多模板程式碼(例如: action creators) - 但是,這是
Redux
能幫助我們更好操作狀態,追蹤和除錯錯誤等。 - 並且
Redux
有著一整套豐富的生態圈,這些你都能在 官方文件 找到答案 - 總之,目前比起世面上
mobx
等庫,更適用於大型專案開發~
- 我們會發現使用
10、未來可期
其實還有很多技術沒有說,像 context
和 React Hooks
等,但受限於筆者的眼界,目前沒有發現大規模使用的場景(如果有,請小夥伴們指正),所以就不談了~有興趣的小夥伴去找找看吧~