一个异步请求,当请求返回的时候,拿到数据立刻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代码