本系列文章在實現一個 cpreact 的同時幫助大家理順 React 框架的核心內容(JSX/虛擬DOM/元件/生命週期/diff演算法/setState/PureComponent/HOC/...) 專案地址
- 從 0 到 1 實現 React 系列 —— JSX 和 Virtual DOM
- 從 0 到 1 實現 React 系列 —— 元件和 state|props
- 從 0 到 1 實現 React 系列 —— 生命週期和 diff 演算法
- 從 0 到 1 實現 React 系列 —— 優化 setState 和 ref 的實現
- 從 0 到 1 實現 React 系列 —— PureComponent 實現 && HOC 探幽
PureComponent 精髓
使用 PureComponent 是優化 React 效能的一種常用手段,相較於 Component, PureComponent 會在 render 之前自動執行一次 shouldComponentUpdate() 函式,根據返回的 bool 值判斷是否進行 render。其中有個重點是 PureComponent 在 shouldComponentUpdate() 的時候會進行 shallowEqual(淺比較)。
PureComponent 的淺比較策略如下:
對 prevState/nextState 以及 prevProps/nextProps 這兩組資料進行淺比較:
1.物件第一層資料未發生改變,render 方法不會觸發; 2.物件第一層資料發生改變(包括第一層資料引用的改變),render 方法會觸發;
PureComponent 的實現
照著上述思路我們來實現 PureComponent 的邏輯
function PureComponent(props) {
this.props = props || {}
this.state = {}
isShouldComponentUpdate.call(this) // 為每個 PureComponent 繫結 shouldComponentUpdate 方法
}
PureComponent.prototype.setState = function(updater, cb) {
isShouldComponentUpdate.call(this) // 呼叫 setState 時,讓 this 指向子類的例項,目的取到子類的 this.state
asyncRender(updater, this, cb)
}
function isShouldComponentUpdate() {
const cpState = this.state
const cpProps = this.props
this.shouldComponentUpdate = function (nextProps, nextState) {
if (!shallowEqual(cpState, nextState) || !shallowEqual(cpProps, nextProps)) {
return true // 只要 state 或 props 淺比較不等的話,就進行渲染
} else {
return false // 淺比較相等的話,不渲染
}
}
}
// 淺比較邏輯
const shallowEqual = function(oldState, nextState) {
const oldKeys = Object.keys(oldState)
const newKeys = Object.keys(nextState)
if (oldKeys.length !== newKeys.length) {
return false
}
let flag = true
for (let i = 0; i < oldKeys.length; i++) {
if (!nextState.hasOwnProperty(oldKeys[i])) {
flag = false
break
}
if (nextState[oldKeys[i]] !== oldState[oldKeys[i]]) {
flag = false
break
}
}
return flag
}
複製程式碼
測試用例
測試用例用 在 React 上提的一個 issue 中的案例,我們期望點選增加按鈕後,頁面上顯示的值能夠加 1。
class B extends PureComponent {
constructor(props) {
super(props)
this.state = {
count: 0
}
this.click = this.click.bind(this)
}
click() {
this.setState({
count: ++this.state.count,
})
}
render() {
return (
<div>
<button onClick={this.click}>增加</button>
<div>{this.state.count}</div>
</div>
)
}
}
複製程式碼
然而,我們點選上述程式碼,頁面上顯示的 0 分毫不動!!!
揭祕如下:
click() {
const t = ++this.state.count
console.log(t === this.state.count) // true
this.setState({
count: t,
})
}
複製程式碼
當點選增加按鈕,控制檯顯示 t === this.state.count
為 true, 也就說明了 setState 前後的狀態是統一的,所以 shallowEqual(淺比較) 返回的是 true,致使 shouldComponentUpdate 返回了 false,頁面因此沒有渲染。
類似的,如下寫法也是達不到目標的,留給讀者思考了。
click() {
this.setState({
count: this.state.count++,
})
}
複製程式碼
那麼如何達到我們期望的目標呢。揭祕如下:
click() {
this.setState({
count: this.state.count + 1
})
}
複製程式碼
感悟:小小的一行程式碼裡蘊藏著無數的 bug。
HOC 實踐
高階元件(Higher Order Component) 不屬於 React API 範疇,但是它在 React 中也是一種實用的技術,它可以將常見任務抽象成一個可重用的部分
。這個小節算是番外篇,會結合 cpreact(前文實現的類 react 輪子) 與 HOC 進行相關的實踐。
它可以用如下公式表示:
y = f(x),
// x:原有元件
// y:高階元件
// f():
複製程式碼
f()
的實現有兩種方法,下面進行實踐。
屬性代理(Props Proxy)
這類實現也是裝飾器模式的一種運用,通過裝飾器函式給原來函式賦能。下面例子在裝飾器函式中給被裝飾的元件傳遞了額外的屬性 { a: 1, b: 2 }。
宣告:下文所展示的 demo 均已在 cpreact 測試通過
function ppHOC(WrappedComponent) {
return class extends Component {
render() {
const obj = { a: 1, b: 2 }
return (
<WrappedComponent { ...this.props } { ...obj } />
)
}
}
}
@ppHOC
class B extends Component {
render() {
return (
<div>
{ this.props.a + this.props.b } { /* 輸出 3 */ }
</div>
)
}
}
複製程式碼
要是將 { a: 1, b: 2 } 替換成全域性共享物件,那麼不就是 react-redux 中的 Connect 了麼?
改進上述 demo,我們就可以實現可插拔的受控元件,程式碼示意如下:
function ppDecorate(WrappedComponent) {
return class extends Component {
constructor() {
super()
this.state = {
value: ''
}
this.onChange = this.onChange.bind(this)
}
onChange(e) {
this.setState({
value: e.target.value
})
}
render() {
const obj = {
onChange: this.onChange,
value: this.state.value,
}
return (
<WrappedComponent { ...this.props } { ...obj } />
)
}
}
}
@ppDecorate
class B extends Component {
render() {
return (
<div>
<input { ...this.props } />
<div>{ this.props.value }</div>
</div>
)
}
}
複製程式碼
效果如下圖:
這裡有個坑點,當我們在輸入框輸入字元的時候,並不會立馬觸發 onChange 事件(我們想要讓事件立即觸發,然而現在要按下Enter鍵或者點下滑鼠才觸發),在 react 中有個合成事件 的知識點,下篇文章會進行探究。
順帶一提在這個 demo 中似乎看到了雙向繫結的效果,但是實際中 React 並沒有雙向繫結的概念,但是我們可以運用 HOC 的知識點結合 setState 在 React 表單中實現偽雙向繫結的效果。
繼承反轉(Inheritance Inversion)
繼承反轉的核心是:傳入 HOC 的元件會作為返回類的父類來使用。然後在 render 中呼叫 super.render()
來呼叫父類的 render 方法。
在 《ES6 繼承與 ES5 繼承的差異》中我們提到了作為物件使用的 super 指向父類的例項。
function iiHOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
const parentRender = super.render()
if (parentRender.nodeName === 'span') {
return (
<span>繼承反轉</span>
)
}
}
}
}
@iiHOC
class B extends Component {
render() {
return (
<span>Inheritance Inversion</span>
)
}
}
複製程式碼
在這個 demo 中,在 HOC 內實現了渲染劫持,頁面上最終顯示如下:
可能會有疑惑,使用
屬性代理
的方式貌似也能實現渲染劫持呀,但是那樣做沒有繼承反轉
這種方式純粹。
鳴謝
Especially thank simple-react for the guidance function of this library. At the meantime,respect for preact and react