一個異步請求,當請求返回的時候,拿到數據立刻setState並把loading組件換掉,很常規的操做。可是,當那個須要setState的組件被卸載的時候(切換路由、卸載上一個狀態組件)去setState就會警告: javascript
因而,一個很簡單的方法也來了:// 掛載
componentDidMount() {
this._isMounted = true;
}
// 卸載
componentWillUnmount() {
this._isMounted = false;
}
// 請求
request(url)
.then(res => {
if (this._isMounted) {
this.setState(...)
}
})
複製代碼
問題fix。java
項目確定不是簡簡單單的,若是要考慮,全部的異步setState都要改,改到何年何日。最簡單的方法,換用preact,它內部已經考慮到這個case,封裝了這些方法,隨便用。或者console它的組件this,有一個__reactstandin__isMounted
的屬性,這個就是咱們想要的_isMounted
。node
不過,項目可能不是說改技術棧就改的,咱們只能回到原來的react項目中。不想一個個搞,那咱們直接改原生的生命週期和setState吧。react
// 咱們讓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;
}
複製代碼
直接在構造函數裏面改,顯得有點耍流氓,並且不夠優雅。本着代碼優雅的目的,很天然地就想到了裝飾器@
。若是項目的babel不支持的,安裝babel-plugin-transform-decorators-legacy
,加入babel的配置中:webpack
"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')
再從新啓動項目便可。web
回到正題,若是想優雅一點,每個想改的地方不用寫太多代碼,想改就改,那麼能夠加上一個裝飾器給組件:npm
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的時候並不會警告和報錯。json
可是須要注意的是,咱們裝飾的只是一個類,因此類的實例的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);
}
}
複製代碼
咱們來玩一點更刺激的——給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);
}
複製代碼
固然,這些都是突發奇想的。考慮性能與深度遍歷以及擴展性,仍是有挺多優化的地方,何時要深度遍歷,何時要賦值,何時能夠換一種姿式遍歷或者何時徹底不用遍歷,這些都是設計須要思考的點。
能拿到實例的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一個裝飾器。
想極致體驗,又不能改源碼,那就介於這二者之間——通過咱們手裏滋潤一下下:
// 咱們寫一個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代碼