全國(guó)咨詢(xún)/投訴熱線(xiàn):400-618-4000

首頁(yè)技術(shù)文章正文

react異步setState思考

更新時(shí)間:2018-12-14 來(lái)源:黑馬程序員技術(shù)社區(qū) 瀏覽量:

一個(gè)異步請(qǐng)求,當(dāng)請(qǐng)求返回的時(shí)候,拿到數(shù)據(jù)馬上setState并把loading組件換掉,很常規(guī)的操作。但是,當(dāng)那個(gè)需要setState的組件被卸載的時(shí)候(切換路由、卸載上一個(gè)狀態(tài)組件)去setState就會(huì)警告: image 于是,一個(gè)很簡(jiǎn)單的方法也來(lái)了:


// 掛載
componentDidMount() {
  this._isMounted = true;
}

// 卸載
componentWillUnmount() {
   this._isMounted = false;
}

// 請(qǐng)求
request(url)
.then(res => {
  if (this._isMounted) {
    this.setState(...)
  }
})
問(wèn)題fix。

1. 不想一個(gè)個(gè)改了
項(xiàng)目肯定不是簡(jiǎn)簡(jiǎn)單單的,如果要考慮,所有的異步setState都要改,改到何年何日。最簡(jiǎn)單的方法,換用preact,它內(nèi)部已經(jīng)考慮到這個(gè)case,封裝了這些方法,隨便用?;蛘遚onsole它的組件this,有一個(gè)__reactstandin__isMounted的屬性,這個(gè)就是我們想要的_isMounted。

不過(guò),項(xiàng)目可能不是說(shuō)改技術(shù)棧就改的,我們只能回到原來(lái)的react項(xiàng)目中。不想一個(gè)個(gè)搞,那我們直接改原生的生命周期和setState吧。

// 我們讓setState更加安全,叫他safe吧
function safe(setState, ctx) {
  console.log(ctx, 666);
  return (...args) => {
    if (ctx._isMounted) {
      setState.bind(ctx)(...args);
    }
  }
}

// 在構(gòu)造函數(shù)里面做一下處理
constructor() {
  super();
  this.setState = a(this.setState, this);
}

// 掛載
componentDidMount() {
  this._isMounted = true;
}

// 卸載
componentWillUnmount() {
   this._isMounted = false;
}
2. 不想直接改
直接在構(gòu)造函數(shù)里面改,顯得有點(diǎn)耍流氓,而且不夠優(yōu)雅。本著代碼優(yōu)雅的目的,很自然地就想到了裝飾器@。如果項(xiàng)目的babel不支持的,安裝babel-plugin-transform-decorators-legacy,加入babel的配置中:

    "plugins": [
      "transform-decorators-legacy"
    ]
考慮到很多人用了create-react-app,這個(gè)腳手架原本不支持裝飾器,需要我們修改配置。使用命令npm run eject可以彈出個(gè)性化配置,這個(gè)過(guò)程不可逆,于是就到了webpack的配置了。如果我們不想彈出個(gè)性化配置,也可以找到它的配置文件:node_modules => babel-preset-react-app => create.js,在plugin數(shù)組加上require.resolve('babel-plugin-transform-decorators-legacy')再重新啟動(dòng)項(xiàng)目即可。

回到正題,如果想優(yōu)雅一點(diǎn),每一個(gè)想改的地方不用寫(xiě)太多代碼,想改就改,那么可以加上一個(gè)裝飾器給組件:

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 {
// ...
}
這樣子,就封裝了一個(gè)這樣的組件,對(duì)一個(gè)被卸載的組件setstate的時(shí)候并不會(huì)警告和報(bào)錯(cuò)。

但是需要注意的是,我們裝飾的只是一個(gè)類(lèi),所以類(lèi)的實(shí)例的this是拿不到的。在上面被改寫(xiě)過(guò)的函數(shù)有依賴(lài)this.state或者props的就導(dǎo)致報(bào)錯(cuò),直接修飾構(gòu)造函數(shù)以外的函數(shù)實(shí)際上是修飾原型鏈,而構(gòu)造函數(shù)也不可以被修飾,這些都是沒(méi)意義的而且讓你頁(yè)面全面崩盤(pán)。所以,最完美的還是直接在constructor里面修改this.xx,這樣子實(shí)例化的對(duì)象this就可以拿到,然后給實(shí)例加上生命周期。

// 構(gòu)造函數(shù)里面
    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. 添加業(yè)務(wù)生命周期
我們來(lái)玩一點(diǎn)更刺激的——給state賦值。

平時(shí),有一些場(chǎng)景,props下來(lái)的都是后臺(tái)數(shù)據(jù),可能你在前面一層組件處理過(guò),可能你在constructor里面處理,也可能在render里面處理。比如,傳入1至12數(shù)字,代表一年級(jí)到高三;后臺(tái)給stringify過(guò)的對(duì)象但你需要操作對(duì)象本身等等。有n種方法處理數(shù)據(jù),如果多個(gè)人開(kāi)發(fā),可能就亂了,畢竟大家風(fēng)格不一樣。是不是想過(guò)有一個(gè)beforeRender方法,在render之前處理一波數(shù)據(jù),render后再把它改回去。

// 首先函數(shù)在構(gòu)造函數(shù)里面改一波
this.render = render(this.render, this);

// 然后修飾器,我們希望beforeRender在render前面發(fā)生
function render(_render, ctx) {
  return function() {
    ctx.beforeRender && ctx.beforeRender.call(ctx);
    const r = _render.call(ctx);
    return r;
  }
}

// 接著就是用的問(wèn)題
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>
    )
  }
我們可以看見(jiàn)輸出的是101。改過(guò)人家的東西,那就得改回去,不然就是101了,你肯定不希望這樣子。didmpunt或者didupdate是可以搞定,但是需要你自己寫(xiě)。我們可以再封裝一波,在背后悄悄進(jìn)行:

// 加上render之后的操作:
function render(_render, ctx) {
  return function(...args) {
    ctx.beforeRender && ctx.beforeRender.call(ctx);
    const r = _render.call(ctx);
    // 這里只是一層對(duì)象淺遍歷賦值,實(shí)際上需要考慮深度遍歷
    Object.keys(ctx._state_).forEach(k => {
      ctx.state[k] = ctx._state_[k];
    })
    return r;
  }
}
一個(gè)很重要的問(wèn)題,千萬(wàn)不要this.state = this._state_,比如你前面的didmount在幾秒后打印this.state,它還是原來(lái)的state。因?yàn)槟菚r(shí)候持有對(duì)原state對(duì)象的引用,后來(lái)你賦值只是改變以后state的引用,對(duì)于前面的dimount是沒(méi)意義的。

// 補(bǔ)上componentDidMount可以測(cè)試一波
  componentDidMount() {
    setTimeout(() => {
      this.setState({ a: 2 })
    }, 500);
    setTimeout(() => {
      console.log(this.state.a, '5秒結(jié)果') // 要是前面的還原是this.state = this._state_,這里還是101
    }, 5000);
  }
當(dāng)然,這些都是突發(fā)奇想的。考慮性能與深度遍歷以及擴(kuò)展性,還是有挺多優(yōu)化的地方,什么時(shí)候要深度遍歷,什么時(shí)候要賦值,什么時(shí)候可以換一種姿勢(shì)遍歷或者什么時(shí)候完全不用遍歷,這些都是設(shè)計(jì)需要思考的點(diǎn)。

4. 更簡(jiǎn)單一些吧
能拿到實(shí)例的this,只能在構(gòu)造函數(shù),而構(gòu)造函數(shù)不能被修飾,怎么更簡(jiǎn)單呢?那就是高階組件了,封裝好我們前面的所有邏輯,成為一個(gè)被我們改造過(guò)的特殊高階組件:

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
    }
  }
}
利用繼承,我們?cè)僮约弘S意操作子類(lèi)constructor的this,滿(mǎn)足了我們的需求,而且也簡(jiǎn)單,改動(dòng)不大,一個(gè)import一個(gè)裝飾器。

5. 讓我們更瘋狂一點(diǎn)
想極致體驗(yàn),又不能改源碼,那就介于這兩者之間——經(jīng)過(guò)我們手里滋潤(rùn)一下下:

// 我們寫(xiě)一個(gè)myreact.js文件
import * as React from 'react';
// ...前面一堆代碼
function Wrap(Cmp) {}
export default React
export const Component = Wrap(React.Component)
我們?cè)僖胨鼈?br/>
import React, { Component } from './myreact'
// 下面的裝飾器也不用了,就是正常的react
// ...
不,這還不夠極致,我們還要改import路徑。最后,一種‘你懂的’眼光投向了webpack配置去:

resolve: {
  alias: {
    '_react': './myreact', // 為什么不直接'react': './myreact'?做人嘛,總要留一條底線(xiàn)的
  }
}
對(duì)于具有龐大用戶(hù)的create-react-app,它的配置在哪里?我們一步步來(lái)找:根路徑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,很快我們就看見(jiàn)熟悉的config,又找到了配置文件。打開(kāi)webpack.config.dev.js,加上我們的alias配置代碼,完事。 最后:

import React, { Component } from '_react'
最終我們可以做到不動(dòng)業(yè)務(wù)代碼,就植入人畜無(wú)害的自己改過(guò)的react代碼    

作者:黑馬程序員前端與移動(dòng)開(kāi)發(fā)培訓(xùn)學(xué)院    
首發(fā):http://web.itheima.com/?v2

分享到:
在線(xiàn)咨詢(xún) 我要報(bào)名
和我們?cè)诰€(xiàn)交談!