0. 前言
一個非同步請求,當請求返回的時候,拿到資料馬上setState並把loading元件換掉,很常規的操作。但是,當那個需要setState的元件被解除安裝的時候(切換路由、解除安裝上一個狀態元件)去setState就會警告:
於是,一個很簡單的方法也來了:// 掛載
componentDidMount() {
this._isMounted = true;
}
// 解除安裝
componentWillUnmount() {
this._isMounted = false;
}
// 請求
request(url)
.then(res => {
if (this._isMounted) {
this.setState(...)
}
})
複製程式碼
問題fix。
1. 不想一個個改了
專案肯定不是簡簡單單的,如果要考慮,所有的非同步setState都要改,改到何年何日。最簡單的方法,換用preact,它內部已經考慮到這個case,封裝了這些方法,隨便用。或者console它的元件this,有一個__reactstandin__isMounted
的屬性,這個就是我們想要的_isMounted
。
不過,專案可能不是說改技術棧就改的,我們只能回到原來的react專案中。不想一個個搞,那我們直接改原生的生命週期和setState吧。
// 我們讓setState更加安全,叫他safe吧
function safe(setState, ctx) {
console.log(ctx, 666);
return (...args) => {
if (ctx._isMounted) {
setState.bind(ctx)(...args);
}
}
}
// 在建構函式裡面做一下處理
constructor() {
super();
this.setState = a(this.setState, this);
}
// 掛載
componentDidMount() {
this._isMounted = true;
}
// 解除安裝
componentWillUnmount() {
this._isMounted = false;
}
複製程式碼
2. 不想直接改
直接在建構函式裡面改,顯得有點耍流氓,而且不夠優雅。本著程式碼優雅的目的,很自然地就想到了裝飾器@
。如果專案的babel不支援的,安裝babel-plugin-transform-decorators-legacy
,加入babel的配置中:
"plugins": [
"transform-decorators-legacy"
]
複製程式碼
考慮到很多人用了create-react-app
,這個腳手架原本不支援裝飾器,需要我們修改配置。使用命令npm run eject
可以彈出個性化配置,這個過程不可逆,於是就到了webpack的配置了。如果我們不想彈出個性化配置,也可以找到它的配置檔案:node_modules => babel-preset-react-app => create.js
,在plugin陣列加上require.resolve('babel-plugin-transform-decorators-legacy')
再重新啟動專案即可。
回到正題,如果想優雅一點,每一個想改的地方不用寫太多程式碼,想改就改,那麼可以加上一個裝飾器給元件:
function safe(_target_) {
const target = _target_.prototype;
const {
componentDidMount,
componentWillUnmount,
setState,
} = target;
target.componentDidMount = () => {
componentDidMount.call(target);
target._isMounted = true;
}
target.componentWillUnmount = () => {
componentWillUnmount.call(target);
target._isMounted = false;
}
target.setState = (...args) => {
if (target._isMounted) {
setState.call(target, ...args);
}
}
}
@safe
export default class Test extends Component {
// ...
}
複製程式碼
這樣子,就封裝了一個這樣的元件,對一個被解除安裝的元件setstate的時候並不會警告和報錯。
但是需要注意的是,我們裝飾的只是一個類,所以類的例項的this是拿不到的。在上面被改寫過的函式有依賴this.state或者props的就導致報錯,直接修飾建構函式以外的函式實際上是修飾原型鏈,而建構函式也不可以被修飾,這些都是沒意義的而且讓你頁面全面崩盤。所以,最完美的還是直接在constructor裡面修改this.xx,這樣子例項化的物件this就可以拿到,然後給例項加上生命週期。
// 建構函式裡面
this.setState = safes(this.setState, this);
this.componentDidMount = did(this.componentDidMount, this)
this.componentWillUnmount = will(this.componentWillUnmount, this)
// 修飾器
function safes(setState, ctx) {
return (...args) => {
if (ctx._isMounted) {
setState.bind(ctx)(...args);
}
}
}
function did(didm, ctx) {
return(...args) => {
ctx._isMounted = true;
didm.call(ctx);
}
}
function will(willu, ctx) {
return (...args) => {
ctx._isMounted = false;
willu.call(ctx);
}
}
複製程式碼
3. 新增業務生命週期
我們來玩一點更刺激的——給state賦值。
平時,有一些場景,props下來的都是後臺資料,可能你在前面一層元件處理過,可能你在constructor裡面處理,也可能在render裡面處理。比如,傳入1至12數字,代表一年級到高三;後臺給stringify過的物件但你需要操作物件本身等等。有n種方法處理資料,如果多個人開發,可能就亂了,畢竟大家風格不一樣。是不是想過有一個beforeRender方法,在render之前處理一波資料,render後再把它改回去。
// 首先函式在建構函式裡面改一波
this.render = render(this.render, this);
// 然後修飾器,我們希望beforeRender在render前面發生
function render(_render, ctx) {
return function() {
ctx.beforeRender && ctx.beforeRender.call(ctx);
const r = _render.call(ctx);
return r;
}
}
// 接著就是用的問題
constructor() {
super()
this.state = {
a: 1
}
this.render = render(this.render, this);
}
beforeRender() {
this._state_ = { ...this.state };
this.state.a += 100;
}
render() {
return (
<div>
{this.state.a}
</div>
)
}
複製程式碼
我們可以看見輸出的是101。改過人家的東西,那就得改回去,不然就是101了,你肯定不希望這樣子。didmpunt或者didupdate是可以搞定,但是需要你自己寫。我們可以再封裝一波,在背後悄悄進行:
// 加上render之後的操作:
function render(_render, ctx) {
return function(...args) {
ctx.beforeRender && ctx.beforeRender.call(ctx);
const r = _render.call(ctx);
// 這裡只是一層物件淺遍歷賦值,實際上需要考慮深度遍歷
Object.keys(ctx._state_).forEach(k => {
ctx.state[k] = ctx._state_[k];
})
return r;
}
}
複製程式碼
一個很重要的問題,千萬不要this.state = this._state_
,比如你前面的didmount在幾秒後列印this.state,它還是原來的state。因為那時候持有對原state物件的引用,後來你賦值只是改變以後state的引用,對於前面的dimount是沒意義的。
// 補上componentDidMount可以測試一波
componentDidMount() {
setTimeout(() => {
this.setState({ a: 2 })
}, 500);
setTimeout(() => {
console.log(this.state.a, '5秒結果') // 要是前面的還原是this.state = this._state_,這裡還是101
}, 5000);
}
複製程式碼
當然,這些都是突發奇想的。考慮效能與深度遍歷以及擴充套件性,還是有挺多優化的地方,什麼時候要深度遍歷,什麼時候要賦值,什麼時候可以換一種姿勢遍歷或者什麼時候完全不用遍歷,這些都是設計需要思考的點。
4. 更簡單一些吧
能拿到例項的this,只能在建構函式,而建構函式不能被修飾,怎麼更簡單呢?那就是高階元件了,封裝好我們前面的所有邏輯,成為一個被我們改造過的特殊高階元件:
function Wrap(Cmp) {
return class extends Cmp {
constructor() {
super()
this.setState = safes(this.setState, this);
this.componentDidMount = did(this.componentDidMount, this)
this.componentWillUnmount = will(this.componentWillUnmount, this)
this.render = render(this.render, this);
}
}
}
// 我們只需要這樣就可以使用
@Wrap
export default class Footer extends Component {
constructor() {
super()
this.state = {
a: 123
}
}
}
複製程式碼
利用繼承,我們再自己隨意操作子類constructor的this,滿足了我們的需求,而且也簡單,改動不大,一個import一個裝飾器。
5. 讓我們更瘋狂一點
想極致體驗,又不能改原始碼,那就介於這兩者之間——經過我們手裡滋潤一下下:
// 我們寫一個myreact.js檔案
import * as React from 'react';
// ...前面一堆程式碼
function Wrap(Cmp) {}
export default React
export const Component = Wrap(React.Component)
複製程式碼
我們再引入它們
import React, { Component } from './myreact'
// 下面的裝飾器也不用了,就是正常的react
// ...
複製程式碼
不,這還不夠極致,我們還要改import路徑。最後,一種‘你懂的’眼光投向了webpack配置去:
resolve: {
alias: {
'_react': './myreact', // 為什麼不直接'react': './myreact'?做人嘛,總要留一條底線的
}
}
複製程式碼
對於具有龐大使用者的create-react-app
,它的配置在哪裡?我們一步步來找:根路徑package.json裡面script是這樣:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
複製程式碼
都知道它的配置是藏著node_modules 裡面的,我們找到了react-scripts
,很快我們就看見熟悉的config,又找到了配置檔案。開啟webpack.config.dev.js,加上我們的alias配置程式碼,完事。
最後:
import React, { Component } from '_react'
複製程式碼
最終我們可以做到不動業務程式碼,就植入人畜無害的自己改過的react程式碼