Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React #10

Open
WangShuXian6 opened this issue May 1, 2018 · 27 comments
Open

React #10

WangShuXian6 opened this issue May 1, 2018 · 27 comments
Labels

Comments

@WangShuXian6
Copy link
Owner

WangShuXian6 commented May 1, 2018

安装React和React-DOM

npm install react --save-dev
npm install react-dom --save-dev

或者建立快速项目

sudo npm install create-react-app -g
create-react-app my-app

cd my-app
npm start

推荐使用npx,防止以后使用了全局的没更新的老版本项目
或者使用npx建立快速项目

npx create-react-app my-app

cd my-app
npm start

You can now view react-app in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://192.168.1.129:3000/

Note that the development build is not optimized.
To create a production build, use npm run build.

从父组件向子组件传值
index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

App.js

import React, {Component} from 'react';
import logo from './logo.svg';
import './App.css';
import Box from './components/box'

let comments = [
  {'author': 'tom', 'comment': 'hello', 'text': '123'},
  {'author': 'lili', 'comment': 'bucuo', 'text': '555'},
]

class App extends Component {
  render() {
    return (
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo"/>
            <h1 className="App-title">Welcome to React</h1>
          </header>
          <p className="App-intro">
            To get started, edit <code>src/App.js</code> and save to reload.
          </p>
          <p>
            你好
          </p>
          <Box data={comments}/>
        </div>
    );
  }
}

export default App;

box.js

import React, {Component} from 'react'
import './box.css'
import Comment from './comment'

class Box extends Component {
  render() {
    return (
        <div className='box'>
          <p>hello box</p>
          <Comment author='tom' comment='不错' data={this.props.data}>hello</Comment>
        </div>
    )
  }
}

export default Box

comment.js

import React, {Component} from 'react'
import './comment.css'

class Comment extends Component {
  render() {
    let commentNodes = this.props.data.map((comment, index) => {
      return (
          <div key={index}>
            <p>{comment.author}</p>
            <p>{comment.comment}</p>
          </div>
      )
    })

    return (
        <div className='comment'>
          {commentNodes}
          {/*<p>{this.props.author}</p>*/}
          {/*<p>{this.props.comment}</p>*/}
          {/*<p className='child'>{this.props.children}</p>*/}
        </div>
    )
  }
}

export default Comment

从服务器获取数据并更新
App.js

import React, {Component} from 'react';
import logo from './logo.svg';
import './App.css';
import Box from './components/box'

class App extends Component {
  render() {
    return (
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo"/>
            <h1 className="App-title">Welcome to React</h1>
          </header>
          <p className="App-intro">
            To get started, edit <code>src/App.js</code> and save to reload.
          </p>
          <p>
            你好
          </p>
          <Box url='./comment.json'/>
        </div>
    );
  }
}

export default App;
//comment.json需要在public静态资源文件夹存放一份,否则热更新无法获取到该文件

box.js

import React, {Component} from 'react'
import './box.css'
import Comment from './comment'
import 'whatwg-fetch'

class Box extends Component {
  constructor(props) {
    super(props)
    this.state = {data: []}
    this.getComments()
    setInterval(() => {
      this.getComments()
    }, 5000)
  }

  getComments() {
    fetch(this.props.url, {
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      }
    }).then((response) => {
      return response.json()
    }).then((comments) => {
      this.setState({data: comments})
    })
  }

  render() {
    return (
        <div className='box'>
          <p>hello box</p>
          <Comment author='tom' comment='不错' data={this.state.data}>aaa</Comment>
        </div>
    )
  }
}

export default Box

comment.js

import React, {Component} from 'react'
import './comment.css'

class Comment extends Component {
  render() {
    let commentNodes = this.props.data.map((comment,index) => {
      return (
          <div key={index}>
            <p>{comment.author}</p>
            <p>{comment.comment}</p>
          </div>
      )
    })

    return (
        <div className='comment'>
          {commentNodes}
          {/*<p>{this.props.author}</p>*/}
          {/*<p>{this.props.comment}</p>*/}
          {/*<p className='child'>{this.props.children}</p>*/}
        </div>
    )
  }
}

export default Comment

comment.json

[
  {
    "author": "tom",
    "comment": "hello",
    "text": "123"
  },
  {
    "author": "lili",
    "comment": "bucuo",
    "text": "555"
  }
]

事件,ref

import React, {Component} from 'react'
import './comment.css'

class Comment extends Component {
  handleSubmit(event) {
    event.preventDefault()
    let text = this.refs.text.value
    console.log(text)
  }

  render() {
    let commentNodes = this.props.data.map((comment, index) => {
      return (
          <div key={index}>
            <p>{comment.author}</p>
            <p>{comment.comment}</p>
          </div>
      )
    })

    return (
        <div className='comment'>
          {commentNodes}
          <form onSubmit={this.handleSubmit.bind(this)}>
            <input type="text" name='name' ref='text'/>
            <button type="submit">sunmit</button>
          </form>
        </div>
    )
  }
}

export default Comment

将数据从子组件传给父组件
box.js

import React, {Component} from 'react'
import './box.css'
import Comment from './comment'
import 'whatwg-fetch'

class Box extends Component {
  constructor(props) {
    super(props)
    this.state = {data: []}
    this.getComments()
    setInterval(() => {
      this.getComments()
    }, 5000)
  }

  getComments() {
    fetch(this.props.url, {
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      }
    }).then((response) => {
      return response.json()
    }).then((comments) => {
      this.setState({data: comments})
    })
  }

  handleCommentSubmit(object) {
    console.log(object)
  }

  render() {
    return (
        <div className='box'>
          <p>hello box</p>
          <Comment onCommentSubmit={this.handleCommentSubmit.bind(this)}
                   author='tom' comment='不错'
                   data={this.state.data}>aaa</Comment>
        </div>
    )
  }
}

export default Box

comment.js

import React, {Component} from 'react'
import './comment.css'

class Comment extends Component {
  handleSubmit(event) {
    event.preventDefault()
    let text = this.refs.text.value
    console.log(text)
    this.props.onCommentSubmit({text})
  }

  render() {
    let commentNodes = this.props.data.map((comment, index) => {
      return (
          <div key={index}>
            <p>{comment.author}</p>
            <p>{comment.comment}</p>
          </div>
      )
    })

    return (
        <div className='comment'>
          {commentNodes}
          <form onSubmit={this.handleSubmit.bind(this)}>
            <input type="text" name='name' ref='text'/>
            <button type="submit">sunmit</button>
          </form>
        </div>
    )
  }
}

export default Comment

更新状态,重新渲染
box.js

import React, {Component} from 'react'
import './box.css'
import Comment from './comment'
import 'whatwg-fetch'

class Box extends Component {
  constructor(props) {
    super(props)
    this.state = {data: []}
    this.getComments()
  }

  getComments() {
    fetch(this.props.url, {
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      }
    }).then((response) => {
      return response.json()
    }).then((comments) => {
      this.setState({data: comments})
    })
  }

  handleCommentSubmit(object) {
    console.log(object)
    let comments = this.state.data
    let newComments = comments.concat(object)
    this.setState({data: newComments})
  }

  render() {
    return (
        <div className='box'>
          <p>hello box</p>
          <Comment onCommentSubmit={this.handleCommentSubmit.bind(this)}
                   author='tom' comment='不错'
                   data={this.state.data}>aaa</Comment>
        </div>
    )
  }
}

export default Box

comment.js

import React, {Component} from 'react'
import './comment.css'

class Comment extends Component {
  handleSubmit(event) {
    event.preventDefault()
    let author = this.refs.author.value
    let comment = this.refs.comment.value
    console.log(author)
    this.props.onCommentSubmit({author, comment})
  }

  render() {
    let commentNodes = this.props.data.map((comment, index) => {
      return (
          <div key={index}>
            <p>{comment.author}</p>
            <p>{comment.comment}</p>
          </div>
      )
    })

    return (
        <div className='comment'>
          {commentNodes}
          <form onSubmit={this.handleSubmit.bind(this)}>
            <input type="text" name='author' ref='author'/>
            <input type="text" name='comment' ref='comment'/>
            <button type="submit">sunmit</button>
          </form>
        </div>
    )
  }
}

export default Comment
@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Dec 29, 2018

React 最佳实践

在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。

例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。

在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
不建议在 getDefaultProps、getInitialState、shouldComponentUpdate、componentWillUpdate、render 和 componentWillUnmount 中调用 setState,特别注意:不能在 shouldComponentUpdate 和 componentWillUpdate中调用 setState,会导致循环调用。

组件接口设计的三个原则:
  • 保持接口小,props 数量要少
  • 根据数据边界来划分组件,利用组合(composition)
  • 把 state 尽量往上层组件提取
  • 避免 renderXXXX 函数
  • 给回调函数类型的 props 加统一前缀
  • 使用 propTypes 来定义组件的 props

无状态组件和类组件并不是对立的概念,一个类组件如果没有自己的state,一样是无状态组件,类组件和函数形式组件才是对立的概念。


  • 尽量每个组件都有自己专属的源代码文件;
  • 用解构赋值(destructuring assignment)的方法获取参数 props 的每个属性值;
  • 利用属性初始化(property initializer)来定义 state 和成员函数。

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Dec 29, 2018

react diff

from: https://zhuanlan.zhihu.com/p/20346379

React diff 作为 Virtual DOM 的加速器,其算法上的改进优化是 React 整个界面渲染的基础,以及性能提高的保障

React diff 会帮助我们计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行实际 DOM 操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染

  • React 通过分层求异的策略,对 tree diff 进行算法优化;
  • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
  • React 通过设置唯一 key的策略,对 element diff 进行算法优化;

传统 diff 算法

计算一棵树形结构转换成另一棵树形结构的最少操作,是一个复杂且值得研究的问题。传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。O(n^3) 到底有多可怕,这意味着如果要展示1000个节点,就要依次执行上十亿次的比较。这种指数型的性能消耗对于前端渲染场景来说代价太高了!现今的 CPU 每秒钟能执行大约30亿条指令,即便是最高效的实现,也不可能在一秒内计算出差异情况。

如果 React 只是单纯的引入 diff 算法而没有任何的优化改进,那么其效率是远远无法满足前端渲染所要求的性能。


React diff

React 通过制定大胆的策略,将 O(n^3) 复杂度的问题转换成 O(n) 复杂度的问题。

diff 策略
  • 1-Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
  • 2-拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  • 3-对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。


ree diff

基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。

既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

updateChildren: function(nextNestedChildrenElements, transaction, context) {
  updateDepth++;
  var errorThrown = true;
  try {
    this._updateChildren(nextNestedChildrenElements, transaction, context);
    errorThrown = false;
  } finally {
    updateDepth--;
    if (!updateDepth) {
      if (errorThrown) {
        clearQueue();
      } else {
        processQueue();
      }
    }
  }
}

当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作。

在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。


component diff

React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。

  • 1-如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
  • 2-如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
  • 3-对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。

element diff

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。

  • 1-INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。
  • 2-MOVE_EXISTING,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
  • 3-REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。

优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化!

新老集合所包含的节点,如下图所示,新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作,即可。

首先对新集合的节点进行循环遍历,for (name in nextChildren),通过唯一 key 可以判断新老集合中是否存在相同的节点,if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,if (child._mountIndex < lastIndex),则进行节点移动操作,否则不执行该操作。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Dec 29, 2018

生命周期

React 的主要思想是通过构建可复用组件来构建用户界面。
所谓组件其实就是 有限状态机,通过状态渲染对应的界面,且每个组件都有自己的生命周期,它规定了组件的状态和方法需要在哪个阶段进行改变和执行。

有限状态机(FSM),表示有限个状态以及在这些状态之间的转移和动作等行为的模型。
一般通过状态、事件、转换和动作来描述有限状态机,下面是描述组合锁状态机的模型图,包括5个状态、5个状态自转换、6个状态间转换和1个复位 RESET 转换到状态 S1。
状态机,能够记住目前所处的状态,根据当前的状态可以做出相应的决策,并且在进入不同的状态时,可以做不同的操作。
通过状态机将复杂的关系简单化,利用这种自然而直观的方式可以让代码更容易理解。
fsm

React 正是利用这一概念,通过管理状态来实现对组件的管理。
例如,某个组件有显示和隐藏两个状态,通常会设计两个方法 show() 和 hide() 来实现切换;
而 React 只需要设置状态 setState({ showed: true/false }) 即可实现。
同时,React 还引入了组件的生命周期概念。通过它就可以实现组件的状态机控制,从而达到 “生命周期-状态-组件” 的和谐画面。


生命周期在不同状态下的执行顺序:
  • 当首次装载组件时,按顺序执行 getDefaultProps、getInitialState、componentWillMount、render 和 componentDidMount;
  • 当卸载组件时,执行 componentWillUnmount;
  • 当重新装载组件时,此时按顺序执行 getInitialState、componentWillMount、render 和 componentDidMount,但并不执行 getDefaultProps;
  • 当再次渲染组件时,组件接受到更新状态,此时按顺序执行 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate。

react life


自定义组件(ReactCompositeComponent)的生命周期主要通过三种状态进行管理:MOUNTING、RECEIVE_PROPS、UNMOUNTING,它们负责通知组件当前所处的状态,应该执行生命周期中的哪个步骤,是否可以更新 state。

三个状态对应三种方法,分别为:mountComponent、updateComponent、unmountComponent,每个方法都提供了两种处理方法,

will 方法在进入状态之前调用,did 方法在进入状态之后调用,三种状态三种方法五种处理方法,此外还提供两种特殊状态的处理方法。

from : https://zhuanlan.zhihu.com/p/20312691


@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Dec 31, 2018

React 的设计思想

  • React 界面完全由数据驱动;
  • React 中一切都是组件;
  • props 是 React 组件之间通讯的基本方式。
UI = f(data)

等号左边的 UI 代表最终画出来的界面;等号右边的 f 是一个函数,也就是我们写的 React 相关代码;data 就是数据,在 React 中,data 可以是 state 或者 props。

UI 就是把 data 作为参数传递给 f 运算出来的结果。这个公式的含义就是,如果要渲染界面,不要直接去操纵 DOM 元素,而是修改数据,由数据去驱动 React 来修改界面。

我们开发者要做的,就是设计出合理的数据模型,让我们的代码完全根据数据来描述界面应该画成什么样子,而不必纠结如何去操作浏览器中的 DOM 树结构。

这样一种程序结构,是声明式编程(Declarative Programming)的方式,代码结构会更加容易理解和维护。


在 React 中一切皆为组件。这是因为:
  • 用户界面就是组件;
  • 组件可以嵌套包装组成复杂功能;
  • 组件可以用来实现副作用。
用户界面就是组件

在界面上看到的任何一个“块”,都需要代码来实现,而这部分代码最好是独立存在的,与其他代码之间的纠葛越少越好,所以要把这个“块”的相关代码封装在一个代码单元里。这样的代码单元,在 React 里就是一个“组件”。

btn

>在上面的图中,一个 Button 是一个界面元素,对应的就是一个 React 组件。在 React 中,一个组件可以是一个类,也可以是一个函数,这取决于这个组件是否有自己的状态。
组件可以嵌套包装组成复杂功能

现实中的应用是很复杂的,界面设计中包含很多元素,一个“块”套着另一个“块”,React 中的组件可以重复嵌套,就是为了支持现实中的用户界面需要。

组件可以用来实现副作用

并不是说组件必须要在界面画一些东西,一个组件可以什么都不画,或者把画界面的事情交给其他组件去做,自己做一些和界面无关的事情,比如获取数据。

下面是一个 Beacon 组件,它的 render 函数返回为空,所以它实际上并不渲染任何东西。

class Beacon extends React.Component {
  render() {
    return null;
  }
  
  componentDidMount() {
    const beacon = new Image();
    beacon.src = 'https://domain.name/beacon.gif';
  }
}

不过,Beacon 的 componentDidMount 函数中创造了一个 Image 对象,访问了一个特定的图片资源,这样就可以对应服务器上留下日志记录,用于记录这一次网页访问。

Beacon 组件的使用方式和普通组件别无二致,但是却能够轻松实现对网页访问的跟踪。

<div>
    <Beacon />
  </div>

组件之间的语言:props

如果一个父组件有话要对子组件说,也就是,想要传递数据给子组件,则应该通过 props。

当然,你可以给子组件增加一个新的函数,然后让父组件去调用这个函数,但是,这种方法很拙劣。如果直接调用子组件的函数,执行过程也处于 React 生命周期之外,所以,不应该使用这种方法。

同样,如果子组件有话要同父组件说,那应该支持函数类型的 props。身为 JavaScript 里一等公民的函数可以作为参数传递,当然也可以作为 props 传递。让父组件传递一个函数类型的 props 进来,当子组件要传递数据给父组件时,调用这个函数类型 props,就把信息传递给了父组件。

如果两个完全没有关系的组件之间有话说,情况就复杂了一点
就没法直接通过 props 来传递信息。

一个比较土的方法,就是通过 props 之间的逐步传递,来把这两个组件关联起来。如果之间跨越两三层的关系,这种方法还凑合,但是,如果这两个组件隔了十几层,或者说所处位置多变,那让 props 跨越千山万水来相会,实在是得不偿失。

另一个简单的方式,就是建立一个全局的对象,两个组件把想要说的话都挂在这个全局对象上。这种方法当然简单可行,但是,我们都知道全局变量的危害罄竹难书,如果不想将来被难以维护的代码折磨,我们最好对这种方法敬而远之。

一般,业界对于这种场景,往往会采用第三方数据管理工具来解决

其实,不依赖于第三方工具,React 也提供了自己的跨组件通讯方式,这种方式叫 Context

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Dec 31, 2018

定义清晰可维护的接口

React 的组件其实就就是软件设计中的模块,所以其设计原则也遵从通用的组件设计原则
要减少组件之间的耦合性(Coupling),让组件的界面简单,这样才能让整体系统易于理解、易于维护。

React 组件设计原则
  • 保持接口小,props 数量要少;
  • 根据数据边界来划分组件,充分利用组合(composition);
  • 把 state 往上层组件提取,让下层组件只需要实现为纯函数。

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Dec 31, 2018

秒表设计

class StopWatch extends React.Component {
  render() {
    return (
       <div>
          <MajorClock>
          <ControlButtons>
          <SplitTimes>
       </div>
    );
  }
}

const MajorClock = (props) => {
  //TODO: 返回数字时钟的JSX
};

const ControlButtons = (props) => {
  //TODO: 返回两个按钮的JSX
};
  
const SplitTimes = (props) => {
  //TODO: 返回所有计次时间的JSX
}
state 的位置

尽量把数据状态往上层组件提取。在秒表这个应用中,上层组件就是 StopWatch,如果我们让 StopWatch 来存储时间状态,那一切就会简单很多。

StopWatch 中利用 setTimeout 或者 setInterval 来更新 state,每一次更新会引发一次重新渲染,在重新渲染的时候,直接把当前时间值传递给 MajorClock 就完事了。

ControlButtons 对状态的控制,让 StopWatch 传递函数类型 props 给 ControlButtons,当特定按钮时间点击的时候回调这些函数,StopWatch 就知道何时停止更新或者启动 setTimeout 或者 setInterval,因为这一切逻辑都封装在 StopWatch 中,非常直观自然。

SplitTimes,它需要一个数组记录所有计次时间,这些数据也很自然应该放在 StopWatch 中维护,然后通过 props 传递给 SplitTimes,这样 SplitTimes 只单纯做渲染就足够。


组件 props 的设计

MajorClock,因为它依赖的数据只有当前时间,所以只需要一个 props。
传入的 props 是一个代表毫秒的数字,所以命名为 milliseconds
props的命名一定力求简洁而且清晰

const MajorClock = ({milliseconds}) => {
  //TODO: 返回数字时钟的JSX
};

MajorClock.propTypes = {
  milliseconds: PropTypes.number.isRequired
};

ControlButtons,这个组件需要根据当前是否是“启动”状态显示不同的按钮,所以需要一个 props 来表示是否“启动”,我们把它命名为 activated

StopWatch 还需要传递回调函数给 ControlButtons,所以还需要支持函数类型的 props,分别代表 ControlButtons 可以做的几个动作:

为了让开发者能够一眼认出回调函数类型的 props,这类 props 最好有一个统一的前缀,比如 on 或者 handle

const ControlButtons = (props) => {
  //TODO: 返回两个按钮的JSX
};

ControlButtons.propTypes = {
  activated: PropTypes.bool,
  onStart: PropTypes.func.isRquired,
  onPause: PropTypes.func.isRquired,
  onSplit: PropTypes.func.isRquired,
  onReset: PropTypes.func.isRquired,
};

SplitTimes,它需要接受一个数组类型的 props

const SplitTimes = (props) => {
  //TODO: 返回所有计次时间的JSX
}

SplitTimes.propTypes = {
  splits: PropTypes.arrayOf(PropTypes.number)
};

一个好的设计就是要在写代码之前就应用被证明最佳的原则,这样写代码的过程就会少走弯路。

构建
ControlButtons

从达到“代码整洁”的目的来说,应该每个组件都有一个独立的文件,然后这个文件用 export default 的方式导出单个组件

在 src 目录下为 ControlButtons 创建一个 ControlButtons.js 文件

import React from 'react';

const ControlButtons = () => {
  //TODO: 实现ControlButtons
};

export default ControlButtons;
import ControlButtons from './ControlButtons';

因为 ControlButtons 是一个函数类型的组件,所以 props 以参数形式传递进来,props 中的属性包含 activated 这样的值,利用大括号,就可以完成对 props 的“解构”,把 props.activated 赋值给同名的变量 activated。

const ControlButtons = (props) => {
  const {activated, onStart, onPause, onReset, onSplit} = props;
  if (activated) {
    return (
      <div>
        <button onClick={onSplit}>计次</button>
        <button onClick={onPause}>停止</button>
      </div>
    );
  } else {
    return (
      <div>
        <button onClick={onReset}>复位</button>
        <button onClick={onStart}>启动</button>
      </div>
    );
  }
};

可以更进一步,把解构赋值提到参数中,这样连 props 的对象都看不见

const ControlButtons = ({activated, onStart, onPause, onReset, onSplit}) => {

}

根据 activated 的值返回不同的 JSX,当 activated 为 true 时,返回的是“计次”和“停止”;当 activated 为 false 时,返回的是“复位”和“启动”,对应分别使用了传入的 on 开头的函数类型 props。

ControlButtons 除了显示内容和分配 props,没有做什么实质的工作,实质工作会在 StopWatch 中

MajorClock

如果使用 MajorClock 时没有传入 milliseconds 这个 props,那么 milliseconds 的值就是 0

const MajorClock = ({milliseconds=0}) => {
  return <h1>{ms2Time(milliseconds)}</h1>
};

因为把毫秒数转为 HH:mm:ss:mmm 这样的格式和 JSX 没什么关系,所以,我们不在组件中直接编写,而是放在 ms2Time 函数中

SplitTimes

利用循环或者数组 map 而产生的动态数量的 JSX 元件,必须要有 key 属性
一般来说,key 不应该取数组的序号,因为 key 要唯一而且稳定,也即是每一次渲染过程中,key 都能唯一标识一个内容。对于 StopWatch 这个例子,倒是可以直接使用数组序号,因为计次时间的数组顺序不会改变,使用数组序号足够唯一标识内容。

import MajorClock from './MajorClock';

const SplitTimes = ({value=[]}) => {
  return value.map((v, k) => (
    <MajorClock key={k} milliseconds={v} />
  ));
};
StopWatch 状态管理

把这些子组件串起来,这就是 StopWatch。
StopWatch 是一个有状态的组件,所以,不能只用一个函数实现,而是做成一个继承自 React.Component 的类
对于一个 React 组件类,最少要有一个 render 函数实现

class StopWatch extends React.Component {
  render() {
    return (
      <Fragment>
        <MajorClock />
        <ControlButtons />
        <SplitTimes />
      </Fragment>
    );
  }
}

React 组件的 state 需要初始化

constructor() {
    super(...arguments);

    this.state = {
      isStarted: false,
      startTime: null,
      currentTime: null,
      splits: [],
    };
  }

属性初始化方法

也可以完全避免编写 constructor 函数,而直接使用属性初始化(Property Initializer),也就是在 class 定义中直接初始化类的成员变量。

不用 constructor,可以这样初始化 state,效果是完全一样的:

class StopWatch extends React.Component {
  state = {
    isStarted: false,
    startTime: null,
    currentTime: null,
    splits: [],
  }
}

不要在 JSX 中写内联函数(inline function)
JSX 中应用的函数 props 应该尽量使用类成员函数,不要用内联函数。

render 这些生命周期函数,里面访问的 this 就是当前组件本身,完全是因为这些函数是 React 调用的,React 对它们进行了特殊处理,对于其他普通的成员函数,特殊处理就要靠我们自己了。

通常的处理方法,就是在构造函数中对函数进行绑定,然后把新产生的函数覆盖原有的函数,就像这样:

constructor() {
    super(...arguments);

    this.onSplit = this.onSplit.bind(this);
  }

如果可以使用 bind operator,也可以这样写:

    this.onSplit = ::this.onSplit;

更好的方法依然是使用属性初始化,就和初始化 state 一样,利用等号直接初始化 onSplit,代码如下:

onSplit = () => {
    this.setState({
      splits: [...this.state.splits, this.state.currentTime - this.state.startTime]
    });
  }

不需要 constructor,函数体内的 this 绝对就是当前组件对象。

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Dec 31, 2018

组件化样式

const clockStyle = {
  'font-family': 'monospace'
};

const MajorClock = ({milliseconds=0}) => {
  return <h1 style={clockStyle}>{ms2Time(milliseconds)}</h1>
}

导入一个同目录下的 ControlButtons.css 文件:

import "./ControlButtons.css";

组件化样式的实现方式很多,这里我们介绍最容易理解的一个库,叫做 styled-jsx。
npm install react-app-rewired styled-jsx

修改 scripts 部分
对应脚本中的 react-scripts 替换为 react-app-rewired,之后,当用 npm 执行这些指令的时候,就会使用 react-app-rewired。
package.json

"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test --env=jsdom",
    "eject": "react-scripts eject"
   }

要让 react-scripts 支持 styled-jsx,对应只需要在项目根目录增加一个 config-overrides.js 文件
把 styled-jsx/babel 注入到 react-scripts 的基本配置中去,然后,我们的应用就支持 styled-jsx 了。

const { injectBabelPlugin } = require('react-app-rewired');

module.exports = function override(config, env) {
  config = injectBabelPlugin(['styled-jsx/babel'], config);

  return config;
};

使用 styled-jsx 定制样式

给 MajorClock 中的 h1 增加 CSS 规则
style jsx 包裹的是一个字符串表达式,而这个字符串就是 CSS 规则。
在 MajorClock 中用 style jsx 添加的 CSS 规则,只作用于 MajorClock 的 JSX 中出现的元素,不会影响其他的组件。

const MajorClock = ({milliseconds=0}) => {
  return (
    <React.Fragment>
      <style jsx>{`
        h1 {
          font-family: monospace;
        }
      `}</style>
      <h1>
        {ms2Time(milliseconds)}
      </h1>
    </React.Fragment>
  );
};
动态 styled jsx

可以动态修改 styled jsx 中的值,因为 styled jsx 的内容就是字符串,我们只要修改其中的字符串,就修改了样式效果。

让 MajorClock 在开始计时状态显示红色,否则显示黑色

const MajorClock = ({milliseconds=0, activated=false}) => {
  return (
    <React.Fragment>
      <style jsx>{`
        h1 {
          color: ${activated? 'red' : 'black'};
          font-family: monospace;
        }
      `}</style>
      <h1>
        {ms2Time(milliseconds)}
      </h1>
    </React.Fragment>
  );
};

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Dec 31, 2018

组件设计模式 :聪明组件和傻瓜组件

这种模式的本质,就是把一个功能分配到两个组件中,形成父子关系,外层的父组件负责管理数据状态,内层的子组件只负责展示。

这个模式的名称很多

  • 容器组件和展示组件(Container and Presentational Components);
  • 胖组件和瘦组件;
  • 有状态组件和无状态组件。

软件设计中有一个原则,叫做“责任分离”(Separation of Responsibility),简单说就是让一个模块的责任尽量少,如果发现一个模块功能过多,就应该拆分为多个模块,让一个模块都专注于一个功能,这样更利于代码的维护。


使用 React 来做界面,无外乎就是获得驱动界面的数据,然后利用这些数据来渲染界面

最好把获取和管理数据这件事和界面渲染这件事分开

把获取和管理数据的逻辑放在父组件,也就是聪明组件;把渲染界面的逻辑放在子组件,也就是傻瓜组件。

这么做的好处,是可以灵活地修改数据状态管理方式,比如,最初你可能用 Redux 来管理数据,然后你想要修改为用 Mobx,如果按照这种模式分割组件,那么,你需要改的只有聪明组件,傻瓜组件可以保持原状。


随机笑话样例
傻瓜组件

功能可以分为两部分,第一部分是展示,也就是傻瓜组件
傻瓜组件 Joke 的功能很简单,显示一个笑脸,然后显示名为 value 的 props,也就是笑话的内容,如果没有 value 值,就显示一个“loading...”。

至于怎么获得笑话内容,不是 Joke 要操心的事,它只专注于显示笑话,所谓傻人有傻福,傻瓜组件虽然“傻”了一点,但是免去了数据管理的烦恼。

import SmileFace from './yaoming_simile.png';

const Joke = ({value}) => {
  return (
    <div>
      <img src={SmileFace} />
      {value || 'loading...' }
    </div>
  );
}

聪明组件

聪明组件,这个组件不用管渲染的逻辑,只负责拿到数据,然后把数据传递给傻瓜组件,由傻瓜组件来完成渲染。
RandomJoke

export default class RandomJoke extends React.Component {
  state = {
    joke: null
  }

  render() {
    return <Joke value={this.state.joke} />
  }

  componentDidMount() {
    fetch('https://icanhazdadjoke.com/',
      {headers: {'Accept': 'application/json'}}
    ).then(response => {
      return response.json();
    }).then(json => {
      this.setState({joke: json.joke});
    });
  }
}

RandomJoke 的 render 函数只做一件事,就是渲染 Joke,并把 this.state 中的值作为 props 传进去。聪明组件的 render 函数一般都这样简单,因为渲染不是他们操心的业务,他们的主业是获取数据。

当 RandomJoke 被第一次渲染的时候,它的 state 中的 joke 值为 null,所以它传给 Joke 的 value 也是 null,这时候,Joke 会渲染一 “loading...”。

但是,在第一次渲染完毕的时候,componentDidMount 被调用,一个 API 请求发出去,拿到一个随机笑话,更新 state 中的 joke 值。因为对一个组件 state 的更新会引发一个新的渲染过程,所以 RandomJoke 的 render 再一次被调用,所以 Joke 也会再一次被渲染,这一次,传入的 value 值是一个真正的笑话,所以,笑话也就出现了。

应用了这种方法之后,如果你要优化界面,只需要去修改傻瓜组件 Joke,
如果你想改进数据管理和获取,只需要去修改聪明组件 RandomJoke。


PureComponent

因为傻瓜组件一般没有自己的状态,所以,可以像上面的 Joke 一样实现为函数形式,
其实,可以进一步改进,利用 PureComponent 来提高傻瓜组件的性能。

函数形式的 React 组件,好处是不需要管理 state,占用资源少,但是,函数形式的组件无法利用 shouldComponentUpdate。

当 RandomJoke 要渲染 Joke 时,即使传入的 props 是一模一样的,Joke 也要走一遍完整的渲染过程,这就显得浪费了。

好一点的方法,是把 Joke 实现为一个类,而且定义 shouldComponentUpdate 函数,每次渲染过程中,在 render 函数执行之前 shouldComponentUpdate 会被调用,如果返回 true,那就继续,如果返回 false,那么渲染过程立刻停止,因为这代表不需要重画了。

对于傻瓜组件,因为逻辑很简单,界面完全由 props 决定,所以 shouldComponentUpdate 的实现方式就是比较这次渲染的 props 是否和上一次 props 相同。
当然,让每一个组件都实现一遍这样简单的 shouldComponentUpdate 也很浪费,所以,React 提供了一个简单的实现工具 PureComponent,可以满足绝大部分需求。

改进后

class Joke extends React.PureComponent {
  render() {
    return (
      <div>
        <img src={SmileFace} />
        {this.props.value || 'loading...' }
      </div>
    );
  }
}

PureComponent 中 shouldComponentUpdate 对 props 做得只是浅层比较,不是深层比较,如果 props 是一个深层对象,就容易产生问题。
比如,两次渲染传入的某个 props 都是同一个对象,但是对象中某个属性的值不同,这在 PureComponent 眼里,props 没有变化,不会重新渲染


React.memo

使用 React v16.6.0 之后的版本,可以使用一个新功能 React.memo 来完美实现 React 组件

React.memo 既利用了 shouldComponentUpdate,又不要求我们写一个 class,这也体现出 React 逐步向完全函数式编程前进。

const Joke = React.memo(() => (
    <div>
        <img src={SmileFace} />
        {this.props.value || 'loading...' }
    </div>
));

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Dec 31, 2018

高阶组件(HoC)

  • 高阶组件不能去修改作为参数的组件,高阶组件必须是一个纯函数,不应该有任何副作用。
  • 高阶组件返回的结果必须是一个新的 React 组件,这个新的组件的 JSX 部分肯定会包含作为参数的组件。
  • 高阶组件一般需要把传给自己的 props 转手传递给作为参数的组件。

对于很多网站应用,有些模块都需要在用户已经登录的情况下才显示。比如,对于一个电商类网站,“退出登录”按钮、“购物车”这些模块,就只有用户登录之后才显示,对应这些模块的 React 组件如果连“只有在登录时才显示”的功能都重复实现,那就浪费了。

这时候,我们就可以利用“高阶组件(HoC)”这种模式来解决问题。

基本形式

“高阶组件”名为“组件”,其实并不是一个组件,而是一个函数,只不过这个函数比较特殊
它接受至少一个 React 组件为参数,并且能够返回一个全新的 React 组件作为结果
这个新产生的 React 组件是对作为参数的组件的包装,所以,有机会赋予新组件一些增强的“神力”。

高阶组件的命名一般都带 with 前缀,命名中后面的部分代表这个高阶组件的功能。

const withDoNothing = (Component) => {
  const NewComponent = (props) => {
    return <Component {...props} />;
  };
  return NewComponent;
};

用高阶组件抽取共同逻辑

只有在登录时才显示

假设我们已经有一个函数 getUserId 能够从 cookies 中读取登录用户的 ID,如果用户未登录,这个 getUserId 就返回空,那么“退出登录按钮“就需要这么写:

const LogoutButton = () => {
  if (getUserId()) {
    return ...; // 显示”退出登录“的JSX
  } else {
    return null;
  }
};

购物车

const ShoppintCart = () => {
  if (getUserId()) {
    return ...; // 显示”购物车“的JSX
  } else {
    return null;
  }
};

两个组件明显有重复的代码,我们可以把重复代码抽取出来,形成 withLogin 这个高阶组件,

const withLogin = (Component) => {
  const NewComponent = (props) => {
    if (getUserId()) {
      return <Component {...props} />;
    } else {
      return null;
    }
  }

  return NewComponent;
};

只需要这样定义 LogoutButton 和 ShoppintCart

const LogoutButton = withLogin((props) => {
  return ...; // 显示”退出登录“的JSX
});

const ShoppingCart = withLogin(() => {
  return ...; // 显示”购物车“的JSX
});

避免了重复代码,以后如果要修改对用户是否登录的判断逻辑,也只需要修改 withLogin,而不用修改每个 React 组件。


高阶组件的高级用法

高阶组件只需要返回一个 React 组件即可,没人规定高阶组件只能接受一个 React 组件作为参数,完全可以传入多个 React 组件给高阶组件。

改进上面的 withLogin,让它接受两个 React 组件,根据用户是否登录选择渲染合适的组件。

const withLoginAndLogout = (ComponentForLogin, ComponentForLogout) => {
  const NewComponent = (props) => {
    if (getUserId()) {
      return <ComponentForLogin {...props} />;
    } else {
      return <ComponentForLogout{...props} />;
    }
  }
  return NewComponent;
};

有了上面的 withLoginAndLogout,就可以产生根据用户登录状态显示不同的内容。

const TopButtons = withLoginAndLogout(
  LogoutButton,
  LoginButton
);

链式调用高阶组件

高阶组件最巧妙的一点,是可以链式调用。

假设,你有三个高阶组件分别是 withOne、withTwo 和 withThree,那么,如果要赋予一个组件 X 某个高阶组件的超能力,那么,你要做的就是挨个使用高阶组件包装

const X1 = withOne(X);
const X2 = withTwo(X1);
const X3 = withThree(X2);
const SuperX = X3; //最终的SuperX具备三个高阶组件的超能力

直接连续调用高阶组件

const SuperX = withThree(withTwo(withOne(X)));

高阶组件本身就是一个纯函数,纯函数是可以组合使用的,
所以,可以把多个高阶组件组合为一个高阶组件,然后用这一个高阶组件去包装X

const hoc = compose(withThree, withTwo, withOne);
const SuperX = hoc(X);

compose,是函数式编程中很基础的一种方法,作用就是把多个函数组合为一个函数

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

React 组件可以当做积木一样组合使用,现在有了 compose,我们就可以把高阶组件也当做积木一样组合,进一步重用代码。

假如一个应用中多个组件都需要同样的多个高阶组件包装,那就可以用 compose 组合这些高阶组件为一个高阶组件,这样在使用多个高阶组件的地方实际上就只需要使用一个高阶组件了。


不要滥用高阶组件

高阶组件不得不处理 displayName,不然 debug 会很痛苦。
当 React 渲染出错的时候,靠组件的 displayName 静态属性来判断出错的组件类,而高阶组件总是创造一个新的 React 组件类,所以,每个高阶组件都需要处理一下 displayName。

如果要做一个最简单的什么增强功能都没有的高阶组件,也必须要写下面这样的代码:

const withExample = (Component) => {
  const NewComponent = (props) => {
    return <Component {...props} />;
  }
  
  NewComponent.displayName = `withExample(${Component.displayName || Component.name || 'Component'})`;
  
  return NewCompoennt;
};

对于 React 生命周期函数,高阶组件不用怎么特殊处理,但是,如果内层组件包含定制的静态函数,这些静态函数的调用在 React 生命周期之外,那么高阶组件就必须要在新产生的组件中增加这些静态函数的支持,这更加麻烦。

使用高阶组件,一定要非常小心,要避免重复产生 React 组件,比如,下面的代码是有问题的:

const Example = () => {
  const EnhancedFoo = withExample(Foo);
  return <EnhancedFoo />
}

每一次渲染 Example,都会用高阶组件产生一个新的组件,虽然都叫做 EnhancedFoo,但是对 React 来说是一个全新的东西,在重新渲染的时候不会重用之前的虚拟 DOM,会造成极大的浪费。

正确的写法是下面这样,自始至终只有一个 EnhancedFoo 组件类被创建:

const EnhancedFoo = withExample(Foo);

const Example = () => {
  return <EnhancedFoo />
}

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Dec 31, 2018

render props 模式

render props,指的是让 React 组件的 props 支持函数这种模式。因为作为 props 传入的函数往往被用来渲染一部分界面,所以这种模式被称为 render props。

一个最简单的 render props 组件 RenderAll
这个 RenderAll 预期子组件是一个函数,它所做的事情就是把子组件当做函数调用,调用参数就是传入的 props,然后把返回结果渲染出来,除此之外什么事情都没有做。

RenderAll 的内部元素即为 props.children ,调用 该内部元素并把 RenderAll 的所有 属性传给它

作为一个通用的render props,并不知道子组件需要用到哪些props,所以应该都传递过去,用不用在他,但是传不传在你。

const RenderAll = (props) => {
  return(
     <React.Fragment>
     	{props.children(props)}
     </React.Fragment>
  );
};

使用 RenderAll
RenderAll 的子组件,也就是夹在 RenderAll 标签之间的部分,其实是一个函数。这个函数渲染出 <h1>hello world</h1>,这就是上面使用 RenderAll 渲染出来的结果。

<RenderAll>
        {() => <h1>hello world</h1>}
      </RenderAll>

传递 props

render props 可以做很多的定制功能,我们还是以根据是否登录状态来显示一些界面元素为例,来实现一个 render props。

实现 render props 的 Login 组件,
render props 和高阶组件的第一个区别,就是 render props 是真正的 React 组件,而不是一个返回 React 组件的函数。

当用户处于登录状态,getUserName 返回当前用户名,否则返回空,然后我们根据这个结果决定是否渲染 props.children 返回的结果。

const Login = (props) => {
  const userName = getUserName();

  if (userName) {
    const allProps = {userName, ...props};
    return (
      <React.Fragment>
        {props.children(allProps)}
      </React.Fragment>
    );
  } else {
    return null;
  }
};

使用上面 Login 的 JSX 代码示例

<Login>
    {({userName}) => <h1>Hello {userName}</h1>}
  </Login>

“以函数为子组件(function as child)”的模式,这可以算是 render props 的一种具体形式,也就利用 children 这个 props 来作为函数传递。


render props 这个模式不必局限于 children 这一个 props,任何一个 props 都可以作为函数,也可以利用多个 props 来作为函数。

扩展 Login,不光在用户登录时显示一些东西,也可以定制用户没有登录时显示的东西,我们把这个组件叫做 Auth

const Auth= (props) => {
  const userName = getUserName();

  if (userName) {
    const allProps = {userName, ...props};
    return (
      <React.Fragment>
        {props.login(allProps)}
      </React.Fragment>
    );
  } else {
    <React.Fragment>
      {props.nologin(props)}
    </React.Fragment>
  }
};

使用 Auth 的话,可以分别通过 login 和 nologin 两个 props 来指定用户登录或者没登录时显示什么

<Auth
    login={({userName}) => <h1>Hello {userName}</h1>}
    nologin={() => <h1>Please login</h1>}
  />

依赖注入

render props 其实就是 React 世界中的“依赖注入”(Dependency Injection)。

所谓依赖注入,指的是解决这样一个问题:逻辑 A 依赖于逻辑 B,如果让 A 直接依赖于 B,当然可行,但是 A 就没法做得通用了。
依赖注入就是把 B 的逻辑以函数形式传递给 A,A 和 B 之间只需要对这个函数接口达成一致就行,如此一来,再来一个逻辑 C,也可以用一样的方法重用逻辑 A。

在上面的代码示例中,Login 和 Auth 组件就是上面所说的逻辑 A,而传递给组件的函数类型 props,就是逻辑 B 和 C。


render props 和高阶组件的比较

比对一下这两种重用 React 组件逻辑的模式

  • render props 模式的应用,就是做一个 React 组件,而高阶组件,虽然名为“组件”,其实只是一个产生 React 组件的函数
  • render props 不像上一小节中介绍的高阶组件有那么多毛病,如果说 render props 有什么缺点,那就是 render props 不能像高阶组件那样链式调用,当然,这并不是一个致命缺点。
  • render props 相对于高阶组件还有一个显著优势,就是对于新增的 props 更加灵活。

以登录状态为例,假如我们扩展 withLogin 的功能,让它给被包裹的组件传递用户名这个 props

const withLogin = (Component) => {
  const NewComponent = (props) => {
    const userName= getUserName();
    if (userName) {
      return <Component {...props} userName={userName}/>;
    } else {
      return null;
    }
  }

  return NewComponent;
};

这就要求被 withLogin 包住的组件要接受 userName 这个props。可是,假如有一个现成的 React 组件不接受 userName,却接受名为 name 的 props 作为用户名,这就麻烦了。我们就不能直接用 withLogin 包住这个 React 组件,还要再造一个组件来做 userName 到 name 的映射,十分费事。

对于应用 render props 的 Login,就不存在这个问题,接受 name 不接受 userName

<Login>
  {
    (props) => {
      const {userName} = props;
      return <TheComponent {...props} name={userName} />
    }
  }
</Login>

所以,当需要重用 React 组件的逻辑时,建议首先看这个功能是否可以抽象为一个简单的组件;

如果行不通的话,考虑是否可以应用 render props 模式;

再不行的话,才考虑应用高阶组件模式。

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Jan 1, 2019

React 中的“提供者模式”(Provider Pattern)

解决跨级的信息传递
避免 props 逐级传递,即是提供者的用途。

虽然这个模式叫做“提供者模式”,但是其实有两个角色,一个叫“提供者”(Provider),另一个叫“消费者”(Consumer),这两个角色都是 React 组件。
其中“提供者”在组件树上居于比较靠上的位置,“消费者”处于靠下的位置。

既然名为“提供者”,它可以提供一些信息,而且这些信息在它之下的所有组件,无论隔了多少层,都可以直接访问到,而不需要通过 props 层层传递。


如何实现提供者模式

实现提供者模式,需要 React 的 Context 功能,可以说,提供者模式只不过是让 Context 功能更好用一些而已。

所谓 Context 功能,就是能够创造一个“上下文”,在这个上下文笼罩之下的所有组件都可以访问同样的数据。

提供者模式的一个典型用例就是实现“样式主题”(Theme),由顶层的提供者确定一个主题,下面的样式就可以直接使用对应主题里的样式。
这样,当需要切换样式时,只需要修改提供者就行,其他组件不用修改。

React v16.3.0 之前的提供者模式

在 React v16.3.0 之前,要实现提供者,就要实现一个 React 组件,不过这个组件要做两个特殊处理。

  • 需要实现 getChildContext 方法,用于返回“上下文”的数据;
  • 需要定义 childContextTypes 属性,声明“上下文”的结构。
一个实现“提供者”的例子,组件名为 ThemeProvider
class ThemeProvider extends React.Component {
  getChildContext() {
    return {
      theme: this.props.value
    };
  }

  render() {
    return (
      <React.Fragment>
        {this.props.children}
      </React.Fragment>
    );
  }
}

ThemeProvider.childContextTypes = {
  theme: PropTypes.object
};

getChildContext 只是简单返回名为 value 的 props 值,但是,因为 getChildContext 是一个函数,它可以有更加复杂的操作,比如可以从 state 或者其他数据源获得数据。

对于 ThemeProvider,我们创造了一个上下文,这个上下文就是一个对象,结构是这样:

{
  theme: {
    //一个对象
  }
}

做两个消费(也就是使用)这个“上下文”的组件,第一个是 Subject,代表标题;第二个是 Paragraph,代表章节。

把 Subject 实现为一个类
class Subject extends React.Component {
  render() {
    const {mainColor} = this.context.theme;
    return (
      <h1 style={{color: mainColor}}>
        {this.props.children}
      </h1>
    );
  }
}

Subject.contextTypes = {
  theme: PropTypes.object
}

在 Subject 的 render 函数中,可以通过 this.context 访问到“上下文”数据,因为 ThemeProvider 提供的“上下文”包含 theme 字段,所以可以直接访问 this.context.theme。

Subject 必须增加 contextTypes 属性,必须和 ThemeProvider 的 childContextTypes 属性一致,不然,this.context 就不会得到任何值。

为什么要求“提供者”用 childContextTypes 定义一次上下文结构,又要求“消费者”再用 contextTypes 再重复定义一次呢?

React 这么要求,是考虑到“上下文”可能会嵌套,就是一个“提供者”套着另一个“提供者”,这时候,底层的消费者组件到底消费哪一个“提供者”呢?通过这种显示的方式指定。

也可以把消费者实现为一个纯函数组件,只不过访问“上下文”的方式有些不同,我们用纯函数的方式实现另一个消费者 Paragraph
const Paragraph = (props, context) => {
  const {textColor} = context.theme;
  return (
    <p style={{color: textColor}}>
      {props.children}
    </p>
  );
};

Paragraph.contextTypes = {
  theme: PropTypes.object
};

从上面的代码可以看到,因为 Paragraph 是一个函数形式,所以不可能访问 this.context,但是函数的第二个参数其实就是 context。

不要忘了设定 Paragraph 的 contextTypes,不然参数 context 也不会是上下文。

结合”提供者“和”消费者“。

做一个组件来使用 Subject 和 Paragraph,这个组件不需要帮助传递任何 props

const Page = () => (
  <div>
    <Subject>这是标题</Subject>
    <Paragraph>
      这是正文
    </Paragraph>
  </div>
);

上面的组件 Page 使用了 Subject 和 Paragraph,现在我们想要定制样式主题,只需要在 Page 或者任何需要应用这个主题的组件外面包上 ThemeProvider,对应的 JSX 代码

<ThemeProvider value={{mainColor: 'green', textColor: 'red'}} >
    <Page />
  </ThemeProvider>

当我们需要改变一个样式主题的时候,改变传给 ThemeProvider的 value 值就搞定了。


React v16.3.0 之后的提供者模式

需要让“提供者”和“消费者”共同依赖于一个 Context 对象
而且消费者也要使用 render props 模式。

首先,要用新提供的 createContext 函数创造一个“上下文”对象。

创造“提供者”极大简化了,不需要创造一个 React 组件类。

const ThemeContext = React.createContext();

这个“上下文”对象 ThemeContext 有两个属性,分别就是Provider 和 Consumer。

const ThemeProvider = ThemeContext.Provider;
const ThemeConsumer = ThemeContext.Consumer;
使用“消费者”

Subject

class Subject extends React.Component {
  render() {
    return (
      <ThemeConsumer>
        {
          (theme) => (
            <h1 style={{color: theme.mainColor}}>
              {this.props.children}
            </h1>
          )
        }
      </ThemeConsumer>
    );
  }
}

上面的 ThemeConsumer 其实就是一个应用了 render props 模式的组件,它要求子组件是一个函数,会把“上下文”的数据作为参数传递给这个函数,而这个函数里就可以通过参数访问“上下文”对象。

在新的 API 里,不需要设定组件的 childContextTypes 或者 contextTypes 属性

Subject 没有自己的状态,没必要实现为类,我们用纯函数的形式实现 Paragraph

const Paragraph = (props, context) => {
  return (
    <ThemeConsumer>
      {
        (theme) => (
          <p style={{color: theme.textColor}}>
            {props.children}
          </p>
          )
      }
    </ThemeConsumer>
  );
};
实现 Page 的方式并没有变化,而应用 ThemeProvider 的代码和之前也完全一样
<ThemeProvider value={{mainColor: 'green', textColor: 'red'}} >
    <Page />
  </ThemeProvider>

在新版 Context API 中,需要一个“上下文”对象(上面的例子中就是 ThemeContext),使用“提供者”的代码和“消费者”的代码往往分布在不同的代码文件中,那么,这个 ThemeContext 对象放在哪个代码文件中呢?
最好是放在一个独立的文件中

为了避免依赖关系复杂,每个应用都不要滥用“上下文”,应该限制“上下文”的使用个数。

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Jan 1, 2019

组合组件

组合组件模式要解决的是这样一类问题:父组件想要传递一些信息给子组件,但是,如果用 props 传递又显得十分麻烦。


很多界面都有 Tab 这样的元件,我们需要一个 Tabs 组件和 TabItem 组件,Tabs 是容器,TabItem 是一个一个单独的 Tab,因为一个时刻只有一个 TabItem 被选中,很自然希望被选中的 TabItem 样式会和其他 TabItem 不同。

TabItem

TabItem 有两个重要的 props:
active 代表自己是否被激活,onClick 是自己被点击时应该调用的回调函数,这就足够了。
TabItem 所做的就是根据这两个 props 渲染出 props.children,没有任何复杂逻辑,是一个活脱脱的“傻瓜组件”,所以,用一个纯函数实现就可以了。

const TabItem = (props) => {
  const {active, onClick} = props;
  const tabStyle = {
    'max-width': '150px',
    color: active ? 'red' : 'green',
    border: active ? '1px red solid' : '0px',
  };
  return (
    <h1 style={tabStyle} onClick={onClick}>
      {props.children}
    </h1>
  );
};

Tabs 如何把 active 和 onClick 传递给 TabItem
使用组合组件的 JSX 代码

<Tabs>
      <TabItem>One</TabItem>
      <TabItem>Two</TabItem>
      <TabItem>Three</TabItem>
    </Tabs>

Tabs 虽然可以访问到作为 props 的 children,但是到手的 children 已经是创造好的元素,而且是不可改变的,Tabs 是不可能把创造好的元素再强塞给 children 的。

如果 Tabs 并不去渲染 children,而是把 children 拷贝一份,就有机会去篡改这份拷贝,最后渲染这份拷贝就好了。

Tabs
class Tabs extends React.Component {
  state = {
    activeIndex:  0
  }

  render() {
    const newChildren = React.Children.map(this.props.children, (child, index) => {
      if (child.type) {
        return React.cloneElement(child, {
          active: this.state.activeIndex === index,
          onClick: () => this.setState({activeIndex: index})
        });
      } else {
        return child;
      }
    });

    return (
      <Fragment>
        {newChildren}
      </Fragment>
    );
  }
}

使用 React.Children.map,可以遍历 children 中所有的元素,因为 children 可能是一个数组嘛。

使用 React.cloneElement 可以复制某个元素。这个函数第一个参数就是被复制的元素,第二个参数可以增加新产生元素的 props,我们就是利用这个机会,把 active 和 onClick 添加了进去。

这两个 API 双剑合璧,就能实现不通过表面的 props 传递,完成两个组件的“组合”。

而维护哪个 TabItem 是当前选中的状态,则是 Tabs 的责任。

实际应用

对于组合组件这种实现方式,TabItem 非常简化;Tabs 稍微麻烦了一点,但是好处就是把复杂度都封装起来了,从使用者角度,连 props 都看不见。

应用组合组件的往往是共享组件库,把一些常用的功能封装在组件里,让应用层直接用就行。在 antd 和 bootstrap 这样的共享库中,都使用了组合组件这种模式。

如果你的某两个组件并不需要重用,那么就要谨慎使用组合组件模式,毕竟这让代码复杂了一些。

如果要开发需要关联的成对组件,可以采用这个方案。

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Jan 1, 2019

React 单元测试

对代码进行测试是最佳实践,可以保证代码质量

测试就是尽力发现软件中的缺陷(俗称 bug),当我们发现不了更多的 bug 时,说明这个软件质量可以接受了。

测试是尽力发现软件中的 bug。当我们发现 bug 数量和严重程度呈稳定的下降趋势,直到低于一个门槛(无须降低为 0,只需要降低到可接受的程度),没有更多更严重的 bug 出现,就说明这个软件的质量可以接受,可以上线了。

对”小块代码“的测试,也就是单元测试。


Jest

Mocha 之类老牌单元测试框架,把所有的单元测试都放在一个环境中执行,这就使所有单元测试访问的是同样一个全局变量空间,所以只要测试代码没写好,就会互相影响。而且,为了保证执行正常,所有的单元测试必须一个接一个地执行,这是体系架构决定的,没有办法。

Jest 不同,Jest 为每一个单元测试文件创造一个独立的运行环境,换句话说,Jest 会启动一个进程执行一个单元测试文件,运行结束之后,就把这个执行进程废弃了,这个单元测试文件即使写得比较差,把全局变量污染得一团糟,也不会影响其他单元测试文件,因为其他单元测试文件是用另一个进程来执行。

Jest 最重要的一个特性,就是支持并行执行

因为每个单元测试文件之间再无纠葛,Jest 可以启动多个进程同时运行不同的文件,这样就充分利用了电脑的多 CPU 多核

使用 create-react-app 产生的项目自带 Jest 作为测试框架,不奇怪,因为 Jest 和 React 一样都是出自 Facebook。

运行下面的命令,就可以进入交互式的”测试驱动开发“模式

npm test

Enzyme

虽然最好的 React 测试框架出自 Facebook 家,最受欢迎的 React 测试工具库却出自 Airbnb,这个工具库叫做 Enzyme。Enzyme 这个单词的含义是“酶”,至于命名原因已经无法考据,可能寓意着快速分解。

安装

npm i --save-dev enzyme enzyme-adapter-react-16

enzyme-adapter-react-16,这个库是用来作为适配器的。

  • 因为不同 React 版本有各自特点,所用的适配器也会不同,
  • 我们的项目中使用的是 16.4 之后的版本,所以用 enzyme-adapter-react-16;
  • 如果用 16.3 版本,需要用 enzyme-adapter-react-16.3;
  • 如果用 16.2 版本,需要用 enzyme-adapter-react-16.2;
  • 如果用更老的版本 15.5,需要用 enzyme-adapter-react-15。
  • 具体各个 React 版本对应什么样的 Adapter,请参考 enzyme官方文档。
  • https://airbnb.io/enzyme/#installation

单元测试

以之前秒表应用中的 ControlButtons 组件为例
创造一个 ControlButtons.test.js,来容纳对应的测试用例,因为所有后缀为 .test.js 的文件都会被 Jest 认作是测试用例文件。
在代码中,需要使用 Adapter

import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
``

>我们对 ControlButtons 组件的测试,就是要渲染它一次,看看渲染结果如何,enzyme 就能帮助我们做这件事。

>比如,我们想要保证渲染出来的内容必须包含两个按钮,其中一个按钮的 class 名是 left-btn,另一个是 right-btn,那么我们就需要下面的单元测试用例:
```ts
import {shallow} from 'enzyme';

it('renders without crashing', () => {
  const wrapper = shallow(<ControlButtons />);
  expect(wrapper.find('.left-btn')).toHaveLength(1);
  expect(wrapper.find('.right-btn')).toHaveLength(1);
});

shallow 和 mount 的区别,就是 shallow 只会渲染被测试的 React 组件这一层,不会渲染子组件;
而 mount 则是完整地渲染 React 组件包括其所有子组件,包括触发 componentDidMount 生命周期函数。

原则上,能用 shallow 就尽量用 shallow,首先是为了测试性能考虑,其次是可以减少组件之间的影响

const Foo = () => ()
    <div>
       {/* other logic */
       <Bar />
    </div>
)

如果用 mount 去渲染 Foo,会连带 Bar 一起完全渲染,如果 Bar 出了什么毛病,那 Foo 的单元测试也过不了;如果用 shallow,只知道 Bar 曾经被用,即使 Bar 哪里出了问题,也不影响 Foo 的单元测试。


代码覆盖率

代码覆盖率必须达到 100%,也就是说,一个应用不光所有的单元测试都要通过,而且所有单元测试都必须覆盖到代码 100% 的角落。

在 create-react-app 创造的应用中,已经自带了代码覆盖率的支持,运行下面的命令,不光会运行所有单元测试,也会得到覆盖率汇报。

npm test -- --coverage

代码覆盖率包含四个方面:

  • 语句覆盖率
  • 逻辑分支覆盖率
  • 函数覆盖率
  • 代码行覆盖率

只有四个方面都是 100%,才算真的 100%。

@WangShuXian6
Copy link
Owner Author

组件状态

UI = f(data)
f 的参数 data,除了 props,就是 state。props 是组件外传递进来的数据,state 代表的就是 React 组件的内部状态。

对于 React 组件而言,数据分为两种:

  • props
  • state

props 是外部传给组件的数据,而 state 是组件自己维护的数据,对外部是不可见的。

判断某个数据以 props 方式存在,还是以 state 方式存在,并不难,只需要判断这个状态是否是组件内部状态。

数据存在 this.foo 中,而不是存在 this.state.foo 中,当这个组件渲染的时候,当然 this.foo 的值也就被渲染出来了,问题是,更新 this.foo 并不会引发组件的重新渲染

判断一个数据应该放在哪里,用下面的原则:

  • 如果数据由外部传入,放在 props 中;
  • 如果是组件内部状态,是否这个状态更改应该立刻引发一次组件重新渲染?如果是,放在 state 中;不是,放在成员变量中。

state 不会被同步修改

简单说来,调用 setState 之后的下一行代码,读取 this.state 并不是修改之后的结果。

React 非常巧妙地用任务队列解决了这个问题,可以理解为每次 setState 函数调用都会往 React 的任务队列里放一个任务,多次 setState 调用自然会往队列里放多个任务。React 会选择时机去批量处理队列里执行任务,当批量处理开始时,React 会合并多个 setState 的操作

为什么 setTimeout 能够强迫 setState 同步更新 state
setTimeout(() => {
    this.setState({count: 2}); //这会立刻引发重新渲染
    console.log(this.state.count); //这里读取的count就是2
  }, 0);

当 React 调用某个组件的生命周期函数或者事件处理函数时,React 会想:“嗯,这一次函数可能调用多次 setState,我会先打开一个标记,只要这个标记是打开的,所有的 setState 调用都是往任务队列里放任务,当这一次函数调用结束的时候,我再去批量处理任务队列,然后把这个标记关闭。”

因为 setTimeout 是一个 JavaScript 函数,和 React 无关,对于 setTimeout 的第一个函数参数,这个函数参数的执行时机,已经不是 React 能够控制的了,换句话说,React 不知道什么时候这个函数参数会被执行,所以那个“标记”也没有打开。

当那个“标记”没有打开时,setState 就不会给任务列表里增加任务,而是强行立刻更新 state 和引发重新渲染。这种情况下,React 认为:“这个 setState 发生在自己控制能力之外,也许开发者就是想要强行同步更新呢,宁滥勿缺,那就同步更新了吧。”

React 选择不同步更新 state,是一种性能优化,如果你用上 setTimeout,就没机会让 React 优化了。

每当你觉得需要同步更新 state 的时候,往往说明你的代码设计存在问题,绝大部分情况下,你所需要的,并不是“state 立刻更新”,而是,“确定 state 更新之后我要做什么”


setState 的第二个参数

setState 的第二个参数可以是一个回调函数,当 state 真的被修改时,这个回调函数会被调用。
当 setState 的第二个参数被调用时,React 已经处理完了任务列表,所以 this.state 就是更新后的数据。

  console.log(this.state.count); // 0
  this.setState({count: 1}, () => {
    console.log(this.state.count); // 这里就是1了
  })
  console.log(this.state.count); // 依然为0
函数式 setState
this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});

上面的代码表面上看会让 this.state.count 增加 3,实际上只增加了 1,因为 setState 没有同步更新 this.state 啊,所以给任务队列加的三个任务都是给 this.state.count 同一个值而已。

当 setState 的第一个参数为函数时,任务列表上增加的就是一个可执行的任务函数了,React 每处理完一个任务,都会更新 this.state,然后把新的 state 传递给这个任务函数。

setState 第一个参数的形式如下:

function increment(state, props) {
  return {count: state.count + 1};
}

这是一个纯函数,不光接受当前的 state,还接受组件的 props,在这个函数中可以根据 state 和 props 任意计算,返回的结果会用于修改 this.state。

如此一来,我们就可以这样连续调用 setState:

this.setState(increment);
  this.setState(increment);
  this.setState(increment);

用这种函数式方式连续调用 setState,就真的能够让 this.state.count 增加 3,而不只是增加 1。

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Jan 1, 2019

Redux 使用模式

创建一个独立于这两个组件的对象,在这个对象中存放共享的数据,没错,这个对象,相当于一个 Store。

如果只是一个简单对象,那么任何人都可以修改 Store,这不大合适。所以我们做出一些限制,让 Store 只接受某些『事件』,如果要修改 Store 上的数据,就往 Store 上发送这些『事件』,Store 对这些『事件』的响应,就是修改状态。

这里所说的『事件』,就是 action,而对应修改状态的函数,就是 reducer。


第一步,看这个状态是否会被多个 React 组件共享。

所谓共享,就是多个组件需要读取或者修改这个状态,如果是,那不用多想,应该放在 Store 上,因为 Store 上状态方便被多个组件共用,避免组件之间传递数据;如果不是,继续看第二步。

第二步,看这个组件被 unmount 之后重新被 mount,之前的状态是否需要保留。

举个简单例子,一个对话框组件。用户在对话框打开的时候输入了一些内容,不做提交直接关闭这个对话框,这时候对话框就被 unmount 了,然后重新打开这个对话框(也就是重新 mount),需求是否要求刚才输入的内容依然显示?如果是,那么应该把状态放在 Store 上,因为 React 组件在 unmount 之后其中的状态也随之消失了,要想在重新 mount 时重获之前的状态,只能把状态放在组件之外,Store 当然是一个好的选择;如果需求不要求重新 mount 时保持 unmount 之前的状态,继续看第三步。

第三步,到这一步,基本上可以确定,这个状态可以放在 React 组件中了。


代码组织方式

更好的方法,是把源代码文件分类放在不同的目录中,根据分类方式,可以分为两种:

  • 基于角色的分类(role based)
  • 基于功能的分类(feature based)

基于角色的分类
把所有 reducer 放在一个目录(通常就叫做 reducers),把所有 action 放在另一个目录(通常叫 actions),最后,把所有的纯 React 组件放在另一个目录。

基于功能的分类方式,是把一个模块相关的所有源代码放在一个目录。
例如,对于博客系统,有 Post(博客文章)和 Comment(注释)两个基本模块,建立两个目录 Post 和 Comment,每个目录下都有各自的 action.js 和 reducer.js 文件,如下所示,每个目录都代表一个模块功能,这就是基于功能的分类方式。

Post -- action.js
     |_ reucer.js
     |_ view.js
Comment -- action.js
        |_ reucer.js
        |_ view.js     

基于功能的分类方式更优。因为每个目录是一个功能的封装,方便共享


react-redux

安装

npm install redux react-redux

react-redux 就是『提供者模式』的实践。在组件树的一个比较靠近根节点的位置,我们通过 Provider 来引入一个 store

import {createStore} from 'redux';
import {Provider} from 'react-redux';

const store = createStore(...);

// JSX
  <Provider store={store}>
    { // Provider之下的所有组件都可以connect到给定的store }
  </Provider>

这个 Provider 当然也是利用了 React 的 Context 功能。在这个 Provider 之下的所有组件,如果使用 connect,那么『链接』的就是 Provider 的 state。

connect 的用法,首先,我们需要一个『傻瓜组件』,可以由纯函数实现

const CounterView = ({count, onIncrement}) => {
  return (
    <div>
      <div>{count}</div>
      <button onClick={onIncrement}>+</button>
    </div>
  );
};

把 CounterView 和 store 连接起来

import {connect} from 'react-redux';

const mapStateToProps = (state) => {
  return {
    count: state.count
  };
}

const mapDispatchToProps = (dispatch) => ({
  onIncrement: () => dispatch({type: 'INCREMENT'})
});

const Counter = connect(mapStateToProps, mapDispatchToProps)(CounterView);

这里的 connect 函数接受两个参数,一个 mapStateToProps 是把 Store 上的 state 映射为 props;另一个 mapDispatchToProps 则是把回调函数类型的 props 映射为派发 action 的动作,connect 函数调用会产生一个『高阶组件』。

connect 产生的高阶组件产生了一个新的 React 组件 Counter,这个 Counter 其实就是一个『聪明组件』,它负责管理状态,而 CounterView 是一个『傻瓜组件』,只负责渲染。

在 react-redux 中,应用了三个 React 模式:

  • 提供者模式
  • 高阶组件
  • 聪明组件和傻瓜组件的分离

Redux 和 React 结合的最佳实践

1-Store 上的数据应该范式化。
所谓范式化,就是尽量减少冗余信息,像设计 MySQL 这样的关系型数据库一样设计数据结构。

2-使用 selector。
对于 React 组件,需要的是『反范式化』的数据,当从 Store 上读取数据得到的是范式化的数据时,需要通过计算来得到反范式化的数据。你可能会因此担心出现问题,这种担心不是没有道理,毕竟,如果每次渲染都要重复计算,这种浪费积少成多可能真会产生性能影响,所以,我们需要使用 seletor。业界应用最广的 selector 就是 reslector 。
reselector 的好处,是把反范式化分为两个步骤,第一个步骤是简单映射,第二个步骤是真正的重量级运算,如果第一个步骤发现产生的结果和上一次调用一样,那么第二个步骤也不用计算了,可以直接复用缓存的上次计算结果。

3-只 connect 关键点的 React 组件
当 Store 上状态发生改变的时候,所有 connect 上这个 Store 的 React 组件会被通知:『状态改变了!』

然后,这些组件会进行计算。connect 的实现方式包含 shouldComponentUpdate 的实现,可以阻挡住大部分不必要的重新渲染,但是,毕竟处理通知也需要消耗 CPU,所以,尽量让关键的 React 组件 connect 到 store 就行。

一个实际的例子就是,一个列表种可能包含几百个项,让每一个项都去 connect 到 Store 上不是一个明智的设计,最好是只让列表去 connect,然后把数据通过 props 传递给各个项。

使用 react-redux 的话,虽然 Provider 可以嵌套,但是,最里层的 Provider 提供的 store 才生效。


如何实现异步操作

最简单的 redux-thunk,代码量少,只有几行,用起来也很直观,但是开发者要写很多代码;

而比较复杂的 redux-observable 相当强大,可以只用少量代码就实现复杂功能,但是前提是你要学会 RxJS,RxJS 本身学习曲线很陡,内容需要 一本书 的篇幅来介绍,这就是代价

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Jan 1, 2019

React Router

要实现“单页应用”,一个最要紧的问题就是做好“路由”(Routing),也就是处理好下面两件事:

  • 把 URL 映射到对应的页面来处理;
  • 页面之间切换做到只需局部更新。;

react router v4 的动态路由

动态路由,指的是路由规则不是预先确定的,而是在渲染过程中确定的

安装
react-router-dom 依赖于 react-router ,所以 react-router 也会被自动安装上。

npm install react-router-dom

react-router 的工作方式,是在组件树顶层放一个 Router 组件,然后在组件树中散落着很多 Route 组件(注意比 Router 少一个“r”),顶层的 Router 组件负责分析监听 URL 的变化,在它保护伞之下的 Route 组件可以直接读取这些信息。

Router 和 Route 的配合,就是“提供者模式”,Router 是“提供者”,Route是“消费者”。

Router 其实也是一层抽象,让下面的 Route 无需各种不同 URL 设计的细节


BrowserRouter

第一种很自然,比如 / 对应 Home 页,/about 对应 About 页,但是这样的设计需要服务器端渲染,因为用户可能直接访问任何一个 URL,服务器端必须能对 /的访问返回 HTML,也要对 /about 的访问返回 HTML。


HashRouter

第二种看起来不自然,但是实现更简单。只有一个路径 /,通过 URL 后面的 # 部分来决定路由,/#/ 对应 Home 页,/#/about 对应 About 页。因为 URL 中#之后的部分是不会发送给服务器的,所以,无论哪个 URL,最后都是访问服务器的 / 路径,服务器也只需要返回同样一份 HTML 就可以,然后由浏览器端解析 # 后的部分,完成浏览器端渲染。

把 Router 用在 React 组件树的最顶层,这是最佳实践。

import {HashRouter} from 'react-router-dom';

ReactDOM.render(
  <HashRouter>
    <App />
  </HashRouter>,
  document.getElementById('root')
);

使用 Link

Navigation 导航栏

const ulStyle = {
  'list-style-type': 'none',
  margin: 0,
  padding: 0,
};

const liStyle = {
  display: 'inline-block',
  width: '60px',
};

const Navigation = () => (
  <header>
    <nav>
      <ul style={ulStyle}>
        <li style={liStyle}><Link to='/'>Home</Link></li>
        <li style={liStyle}><Link to='/about'>About</Link></li>
      </ul>
    </nav>
  </header>
)

使用 Route 和 Switch

Content 内容组件,这里会用到 react-router 最常用的两个组件 Route 和 Switch。

const Content = () => (
  <main>
    <Switch>
      <Route exact path='/' component={Home}/>
      <Route path='/about' component={About}/>
    </Switch>
  </main>
)

当访问 /about 页面时,不光匹配 /about,也配中 /,界面上会把 Home 和 About 都渲染出来的。
解决方法,可以在想要精确匹配的 Route 上加一个属性 exact,或者使用 Switch 组件。


动态路由

假设,我们增加一个新的页面叫 Product,对应路径为 /product,但是只有用户登录了之后才显示。如果用静态路由,我们在渲染之前就确定这条路由规则,这样即使用户没有登录,也可以访问 product,我们还不得不在 Product 组件中做用户是否登录的检查。

如果用动态路由,则只需要在代码中的一处涉及这个逻辑:

 <Switch>
      <Route exact path='/' component={Home}/>
      {
        isUserLogin() &&
        <Route exact path='/product' component={Product}/>,
      }  
      <Route path='/about' component={About}/>
    </Switch>

可以用任何条件决定 Route 组件实例是否渲染,比如,可以根据页面宽度、设备类型决定路由规则,动态路由有了最大的自由度。

@WangShuXian6
Copy link
Owner Author

纯原生 HTML 使用 ant design react 组件

依赖库打包
umd.zip

antd.DatePicker

<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <meta charset="UTF-8">
  <title>ant design in html</title>
  <link rel="stylesheet" href="./umd/antd.css">
  <script src="./umd/react.production.min.js"></script>
  <script src="./umd/react-dom.production.min.js"></script>
  <script src="./umd/browser5.8.24.js"></script>
  <script src="./umd/moment-with-locales.js"></script>
  <script src="./umd/antd-with-locales.js"></script>
  <script></script>
</head>
<body>

<div id="date"></div>

<script>
  moment.locale('zh-cn');

  const dateDom = document.querySelector('#date')

  const reactEle = React.createElement(antd.DatePicker, {
    onChange: (e) => {
      console.log(e)
      console.log(e._d)
      console.log(new Date(e._d).toLocaleTimeString())
    }
  })

  ReactDOM.render(reactEle, dateDom)
</script>
</body>
</html>

antd.Table

<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <meta charset="UTF-8">
  <title>ant design in html</title>
  <link rel="stylesheet" href="./umd/antd.css">
  <script src="./umd/react.production.min.js"></script>
  <script src="./umd/react-dom.production.min.js"></script>
  <script src="./umd/browser5.8.24.js"></script>
  <script src="./umd/moment-with-locales.js"></script>
  <script src="./umd/antd-with-locales.js"></script>
  <script></script>
</head>
<body>

<div id="date"></div>

<script>
  moment.locale('zh-cn');

  const dataSource = [
    {
      key: '1',
      name: '胡彦斌',
      age: 32,
      address: '西湖区湖底公园1号',
    },
    {
      key: '2',
      name: '胡彦祖',
      age: 42,
      address: '西湖区湖底公园1号',
    },
  ];

  const columns = [
    {
      title: '姓名',
      dataIndex: 'name',
      key: 'name',
    },
    {
      title: '年龄',
      dataIndex: 'age',
      key: 'age',
    },
    {
      title: '住址',
      dataIndex: 'address',
      key: 'address',
    },
  ];

  const dateDom = document.querySelector('#date')

  const reactEle = React.createElement(antd.Table, {
    dataSource,
    columns,
    onChange: (e) => {
      console.log(e)
      console.log(e._d)
      console.log(new Date(e._d).toLocaleTimeString())
    }
  })

  ReactDOM.render(reactEle, dateDom)
</script>
</body>
</html>

demo

<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <meta charset="UTF-8">
  <title>ant design in html</title>
  <link rel="stylesheet" href="./umd/antd.css">
  <script src="./umd/react.production.min.js"></script>
  <script src="./umd/react-dom.production.min.js"></script>
  <script src="./umd/browser5.8.24.js"></script>
  <script src="./umd/moment-with-locales.js"></script>
  <script src="./umd/antd-with-locales.js"></script>
  <script></script>
</head>
<body>

<div id="date"></div>


<script>
  const dateDom = document.querySelector('#date')

  class LikeButton extends React.Component {
    constructor(props) {
      super(props);
      this.state = {liked: false};
    }

    render() {
      if (this.state.liked) {
        return 'You liked this.';
      }

      return React.createElement(
        'button',
        {
          onClick: () => {
            console.log(123)
            this.setState({liked: true})
          }
        },
        'Like'
      );
    }
  }

  ReactDOM.render(React.createElement(LikeButton), dateDom);
</script>
</body>
</html>

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Aug 29, 2019

Hook

https://zh-hans.reactjs.org/docs/hooks-overview.html

Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。

Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。

React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序

只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。

Hook API

https://zh-hans.reactjs.org/docs/hooks-reference.html

基础 Hook

useState
useEffect
useContext

额外的 Hook

useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebugValue

什么时候我会用 Hook

在编写函数组件[无状态组件]并意识到需要向其添加一些 state

以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。

计数器示例

定义 ./src/component/State1.tsx

import React,{useState} from 'react'

export default function State1(){
    const [count,setCount]=useState(0)

    const update=()=>{
        setCount(count+1)
    }

    return (
        <div>
            <p>{count}</p>
            <button onClick={update}>click</button>
        </div>
    )
}

使用 ./src/App.tsx

import React from 'react';
import logo from './logo.svg';
import './App.css';
import State1 from './component/useState'

const App: React.FC = () => {
  return (
    <div className="App">
      <header className="App-header">
        <State1 />
      </header>
      
    </div>
  );
}

export default App;

Hook 使用规则

在你的 React 函数的最顶层调用他们,就能确保 Hook 在每一次渲染中都按照同样的顺序被调用

只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。

只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中)


State Hook

useState 就是一个 Hook

useState 是允许你在 React 函数组件中添加 state 的 Hook

通过在函数组件里调用它来给组件添加一些内部 state。

React 会在重复渲染时保留这个 state。

useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。

它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。

useState 唯一的参数就是初始 state

初始 state 参数只有在第一次渲染时会被用到

声明多个 state 变量

可以在一个组件中多次使用 State Hook

数组解构的语法让我们在调用 useState 时可以给 state 变量取不同的名字

function ExampleWithManyStates() {
  // 声明多个 state 变量!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}

Effect Hook

副作用(作用)

在 React 组件中执行过数据获取、订阅或者手动修改过 DOM

在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的

无需清除的 effect

在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志

在执行完这些操作之后,就可以忽略他们了

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
useEffect 做了什么

告诉 React 组件需要在渲染后执行某些操作

React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它

React 保证了每次运行 effect 的同时,DOM 都已经更新完毕

传递给 useEffect 的函数在每次渲染中都会有所不同,这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。

需要清除的 effect

effect 如果返回函数,将作为清除函数在下一次渲染后或组件卸载后执行

一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露

它会在调用一个新的 effect 之前对前一个 effect 进行清理

可能会产生的订阅和取消订阅操作调用序列:

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // 运行第一个 effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // 运行下一个 effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // 运行下一个 effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect
跳过对 effect 的调用

如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

上面这个示例中,我们传入 [count] 作为第二个参数。这个参数是什么作用呢?如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。

当渲染时,如果 count 的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。

对于有清除操作的 effect 同样适用:
useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅

未来版本,可能会在构建时自动添加第二个参数。

如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。

启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

useEffect 就是一个 Effect Hook

给函数组件增加了操作副作用的能力。

可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API

当你调用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。

由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。

默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候

“清除”副作用

副作用函数还可以通过返回一个函数来指定如何“清除”副作用。

如果 useEffect 第二个参数为常量,在下一次组件渲染之后, useEffect执行上一次返回的清理函数 或者 组件销毁时 执行,然后执行 useEffect 函数

import React, { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里

可以在组件中多次使用 useEffect

组件在 React 更新 DOM 后会设置一个页面标题:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 相当于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 使用浏览器的 API 更新页面标题
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

自定义 Hook

在组件之间重用一些状态逻辑

目前为止,有两种主流方案来解决这个问题:高阶组件和 render props。自定义 Hook 可以让你在不增加组件的情况下达到同样的目的

自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

如果函数的名字以 “use” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。

useSomething 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。

可以创建涵盖各种场景的自定义 Hook,如表单处理、动画、订阅声明、计时器,甚至可能还有更多我们没想到的场景

FriendStatus 的组件

它通过调用 useState 和 useEffect 的 Hook 来订阅一个好友的在线状态。

假设我们想在另一个组件里重用这个订阅逻辑。

它将 friendID 作为参数,并返回该好友是否在线:

useFriendStatus 的自定义 Hook
import React, { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}
在两个组件中使用它:
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

这两个组件的 state 是完全独立的。

Hook 是一种复用状态逻辑的方式,它不复用 state 本身。

Hook 的每次调用都有一个完全独立的 state —— 因此你可以在单个组件中多次调用同一个自定义 Hook。

在多个 Hook 之间传递信息

由于 Hook 本身就是函数,因此我们可以在它们之间传递信息。

const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);

useContext

不使用组件嵌套就可以订阅 React 的 Context

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}

useReducer

通过 reducer 来管理组件本地的复杂 state

已经将 useReducer 的 Hook 内置到 React 中

useReducer 的 Hook

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}

在组件中使用它,让 reducer 驱动它管理 state:

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer, []);

  function handleAddClick(text) {
    dispatch({ type: 'add', text });
  }

  // ...
}
function Todos() {
  const [todos, dispatch] = useReducer(todosReducer);
  // ...

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 8, 2021

React 最佳实践 [React best practice]

https://zh-hans.reactjs.org/docs/thinking-in-react.html


从设计稿开始

第一步:将设计好的 UI 划分为组件层级

在设计稿上用方框圈出每一个组件(包括它们的子组件),并且以合适的名称命名。

将组件当作一种函数或者是对象来考虑,根据单一功能原则来判定组件的范围。
也就是说,一个组件原则上只能负责一个功能。
如果它需要负责更多的功能,这时候就应该考虑将它拆分成更小的组件。

如果你的模型设计得恰当,UI(或者说组件结构)便会与数据模型一一对应,这是因为 UI 和数据模型都会倾向于遵守相同的信息结构。
将 UI 分离为组件,其中每个组件需与数据模型的某部分匹配。

设计稿中被其他组件包含的子组件,在层级上应该作为其子节点。

第二步:用 React 创建一个静态版本

将渲染 UI 和添加交互这两个过程分开。

这是因为,编写一个应用的静态版本时,往往要编写大量代码,而不需要考虑太多交互细节;
添加交互功能时则要考虑大量细节,而不需要编写太多代码。
所以,将这两个过程分开进行更为合适

在构建应用的静态版本时,我们需要创建一些会重用其他组件的组件,然后通过 props 传入所需的数据。
props 是父组件向子组件传递数据的方式。
即使你已经熟悉了 state 的概念,也完全不应该使用 state 构建静态版本。
state 代表了随时间会产生变化的数据,应当仅在实现交互时使用。所以构建应用的静态版本时,你不会用到它。

自上而下或者自下而上构建应用

自上而下意味着首先编写层级较高的组件(比如 FilterableProductTable),
自下而上意味着从最基本的组件开始编写(比如 ProductRow)。

当你的应用比较简单时,使用自上而下的方式更方便;

对于较为大型的项目来说,自下而上地构建,并同时为低层组件编写测试是更加简单的方式。

到此为止,你应该已经有了一个可重用的组件库来渲染你的数据模型

第三步:确定 UI state 的最小(且完整)表示

想要使你的 UI 具备交互功能,需要有触发基础数据模型改变的能力。React 通过实现 state 来完成这个任务。

只保留应用所需的可变 state 的最小集合,其他数据均由它们计算产生。

通过问自己以下三个问题,你可以逐个检查相应数据是否属于 state:

该数据是否是由父组件通过 props 传递而来的?如果是,那它应该不是 state。
该数据是否随时间的推移而保持不变?如果是,那它应该也不是 state。
你能否根据其他 state 或 props 计算出该数据的值?如果是,那它也不是 state。

第四步:确定 state 放置的位置

确定哪个组件能够改变这些 state,或者说拥有这些 state。
React 中的数据流是单向的,并顺着组件层级从上往下传递

对于应用中的每一个 state:

找到根据这个 state 进行渲染的所有组件。
找到他们的共同所有者(common owner)组件(在组件层级上高于所有需要该 state 的组件)。
该共同所有者组件或者比它层级更高的组件应该拥有该 state。
如果你找不到一个合适的位置来存放该 state,就可以直接创建一个新的组件来存放该 state,并将这一新组件置于高于共同所有者组件层级的位置。

第五步:添加反向数据流

让数据反向传递:处于较低层级的表单组件更新较高层级的中的 state。
这种需要显式声明的方法更有助于人们理解程序的运作方式。


避免不需要的状态 [Unneeded state]

永远记住单一事实来源原则 - 它可以使您的组件类更易于编写和维护。
请记住,每个非规范化状态字段都是一个可能的简单错误。

为什么使用状态?

状态是数据的子集,如果更改,会导致 React 组件重新渲染自身

为什么要保持状态尽可能小

因为您拥有的状态越多,就越需要保持同步。
拥有大状态会导致管理组件的大量开销。
如果您具有非规范化状态,则尤其如此- 这意味着您的状态具有可以从其余状态数据计算的数据。每次更新状态时,您也需要记住更新非规范化字段。这违反了单一事实来源原则。

单一事实来源

单一事实来源是您在考虑什么应该进入状态和什么不应该状态时需要具备的基本原则。在 React 组件中保留唯一的真实来源可以在使用 React 组件时为您省去很多麻烦。

如何避免不必要的状态

一些简单的启发式方法可以检查您是否有不需要的状态。

如果您发现要进行复杂的更新setState并且总是将两个或多个字段设置在一起,则您设置的一个字段很可能是另一个状态字段的非规范化形式。

如果您引入另一个领域,请考虑它将采用什么值。可能的值是有限的并且可以枚举的可能性很大 - 就像这个特定状态字段只能具有"x","y"或"z"值一样。如果您发现存在共享这组枚举值或某个子集的字段,请检查这些字段是否可以合并为一个。

当然,考虑一下组件本身是否可以在其生命周期中更改该字段 - 如果不能,您可以将此字段保留在 props 中或作为组件内的常量而不是状态。

检查不需要的状态 - 算法

大多数不需要的状态可以以结构化的方式检查 - 有一个算法。如果您想确保不需要或不需要某个状态字段x,您可以遵循它。

检查 x 的初始化。写下初始值。

查看所有setState并replaceState调用组件的代码。写下所有x可能具有的值。

看看那些可能的值。如果可能值集合中有一个变量,则跟踪对该值的所有赋值以及初始值,并将这些可能的变量值添加到集合中。从您的集合中删除变量。

如果您的可能值集中存在函数/方法调用,请对您调用的函数的所有可能return值进行分析。它只能根据函数参数来定义 - 因此,如果有局部变量,请将其删除并将该值的所有可能分配添加到该集合中。然后,在此函数的调用中扫描所有可能的参数并相应地修改可能设置的值。

重复该过程,直到您的可能值集中没有变量或函数调用。在此阶段,您只能拥有数据原语或状态引用(如0、new Foo(),{}{ x: 2 }或this.state.??)。

如果您的可能值集仅包含原始中性元素( 0, {}, null, undefined, []) 和状态引用,则不需要此状态字段。

<!DOCTYPE html>
<html>
<head>
<script src="//fb.me/react-0.13.3.js"></script>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div id="form"></div>
</body>
</html>
class List extends React.Component {
  render() {
    let { name, items } = this.props;
    let options = [];

    options.push(<option value={name}>{name}</option>);

    for(var index in items) {
      let item = items[index];
      options.push(<option value={item}>{item}</option>);
    }

    return (
      <span>
        <select onChange={this.props.handler} value={this.props.value ? this.props.value : "Model"}>
          {options}
        </select>
      </span>
    );
  }
}

class TwoLists extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      brand: null, model: null
    };
    this.brandChanged = this.brandChanged.bind(this);
    this.modelChanged = this.modelChanged.bind(this);
    this.buttonClicked = this.buttonClicked.bind(this);
    this.knownModel = this.knownModel.bind(this);
  }

  brandChanged(event) {
    let brand = event.target.value;
    if(this.knownBrand(brand)) {
      let models = this.data()[brand];
      this.setState({
        brand, model: null,
        models: models, buttonDisabled: true,
      });
    } else {
      this.setState({
        brand: null, model: null
      });
    }
  }

  modelChanged(event) {
    let model = event.target.value;
    if(this.knownModel(model)) {
      this.setState({ model });
    } else {
      this.setState({ model: null });
    }
  }

  buttonClicked(event) {
    let { brand, model } = this.state;
    console.log(this.state);
    console.log(`${brand} ${model} riding...`);
  }

  data() {
    return (
      {
        'Opel':   ['Agila', 'Astra', 'Corsa', 'Vectra'],
        'Škoda':  ['Fabia', 'Octavia', 'Superb', 'Yeti'],
        'Toyota': ['Auris', 'Avensis', 'Corolla', 'Prius']
      }
    );
  }
  
  buttonDisabled() {
    return this.state.model === null || this.state.brand === null;
  }
  
  models() {
    return this.state.brand ? this.data()  [this.state.brand] : [];
  }  

  brands() {
    return Object.keys(this.data());
  }

  knownBrand(brand) {
    return this.brands().indexOf(brand) !== -1
  }

  knownModel(model) {
    return this.models().indexOf(model) !== -1
  }

  render() {
    return (
      <div id={this.props.id}>
        <List name="Brand" items={this.brands()} handler={this.brandChanged} value={this.state.brand} />
        <List name="Model" items={this.models()} handler={this.modelChanged} value={this.state.model} />
        <button onClick={this.buttonClicked} disabled={this.buttonDisabled()}>Ride</button>
      </div>
    );
  }
}

React.render(
  <TwoLists id="two-lists"/>,
  document.getElementById("form"));

状态是一种反模式 State is an antipattern

[并不是说应用程序根本没有状态,只是说它没有内部状态(即 this.setState),它的行为有点像一个有限自动机,所有状态都可以是只用属性props描述。]

好的黑匣子是直观的,你可以不用看说明书就知道它们是做什么的

好的黑匣子是幂等的,对于任何一组输入,它们总是给你相同的输出

好的黑匣子是纯净的,它们不会以意想不到的方式运行

如果黑匣子必须有副作用,则必须有可能预见和/或关闭它们

React 组件基本上是黑盒子:

好的 React 组件是直观的

好的 React 组件是幂等的,对于任何一组 props,它们总是给你相同的 HTML

好的 React 组件是纯粹的,它们不会以意想不到的方式运行

如果 React 组件必须有副作用,它们必须是 Flux 动作

状态破坏了它们。

好的 React 组件是直观的

一个按钮组件,它最初是绿色的,但在您单击它后变为红色

getInitialState(){
  return {color: 'green'}
}

onClicked (e){
  this.setState({color: 'red'})
}

错误版本 Button2

看起来更漂亮

<Button2/>

正确版本 Button1

看起来很冗长 但是,我可以理解 Button1 是如何工作的,您只需向它传递一个颜色和一个 onClick 回调

<Button1 
  color={flux.store.wasTheButtonClicked() ? 'red' : 'green'}
  onClick={flux.actions.buttonClicked}
/>

好的 React 组件是幂等的

从 Button1 开始,我知道我可以传递我想要的任何颜色并且它会起作用,
而使用 Button2 我什至没有得到道具,我被迫将 nil 作为输入传递,并且取决于其不可预测的内部状态,对于同一组输入(nil),它可以返回绿色按钮或红色按钮,而我无法预测结果。

好的 React 组件是纯粹的

不纯粹的组件把问题隐藏在了表面之下

<Tweets2 hashtag="reactjs"/>

纯粹的组件具有更高的灵活性

//The store
getTweets (){
  if(this.tweets) return this.tweets;
  TweetLibrary.getTweetsFor("reactjs").then(function(tweets){
    this.tweets = tweets;
    this.emit("change");
  }.bind(this));
}

//The component
<Tweets1 tweets={flux.store.getTweets()}/>

如果 React 组件必须有副作用,它们必须是 Flux 动作[或其他回调]

React 组件不应该有副作用,但有时,别无他法。
每次您希望您的组件对用户交互做出反应时,您都不得不使用副作用,但是,如果您仍然必须这样做,至少要正确地做。

传递一个onPageChanged 回调是一个很好的做法,
我们应该将一个 Flux 动作传递给它,这样逻辑将驻留在动作创建者中,
数据来自store
我们不会用逻辑将我们的组件聚集在一起,因为它不属于那里,组件只是视图层。

使用状态导致的问题

破坏组件可重用性

破坏单向数据流,使其更难调试

破坏关注点的分离,无论用户是否登录都不应该是 的问题,它只应该从其props中填写登录名和密码字段并在用户尝试时回调改变那个。

PS:既然我们在谈论表单,那么看看 Facebook 如何在 React 中实现 <input/> 元素。
向它传递一个值和一个 onChange 回调,您有责任将该状态存储在某处,<input/> 拒绝在内部存储该状态。

结论:

组件只是视图层,不要在其中放置逻辑,而是使用 Flux

必须可以仅使用 props 描述组件的所有状态

使用 prop 属性

永远不要使用状态。如果你发现自己是被迫的,那你就做错了

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 8, 2021

React 计算属性 [React computed]

使用 “计算属性” 的方法

Class Component 类组件

直接使用 get 方法即可达成计算属性:

class ClassComponent extends Component {

  // 对于 props 的一些处理
  get computed() {
    return this.props.value
  }
  
  // 支持计算属性相互依赖
  // 在 vue 中,由于不支持 data 相互依赖,但支持 computed 相互依赖,这也是 vue 中非常好用的一点!
  get computed2() {
    return this.computed + '2'
  }

  render() {
    return (
      <div>{this.computed}, {this.computed2}</div>
    )
  }
}

function App() {

  const [state, setState] = useState('old')

  useEffect(() => {
    // 模拟一些异步处理,变化传入的 props
    setTimeout(() => {
      setState('new')
    }, 4000)
  }, [])

  return (
    <ClassComponent value={state}></ClassComponent>
  )
}

通过 class 的 get 方法,即可完成即时变化的计算属性,同时支持计算属性相互依赖。

Function Component 函数组件

useMemo : 传入一个回调函数和一个依赖列表,React会在依赖列表中的值变化时,调用这个回调函数,并将回调函数返回的结果进行缓存后,返回给上一层:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

一般使用 useMemo 进行计算属性的计算(当依赖列表变化时重新计算):

// 对 props 的处理函数
const handler = (v) => v

function FC({ value }) {

  // 通过依赖列表变化 useMemo 重新计算结果达到计算属性实时更新
  const computed = useMemo(() => handler(value), [value])

  return (
    <div>{computed}</div>
  )
}

function App() {

  const [state, setState] = useState('old')

  useEffect(() => {
    // 模拟一些异步处理,变化传入的 props
    setTimeout(() => {
      setState('new')
    }, 4000)
  }, [])

  return (
    <FC value={state}></FC>
  )
}

Function Component Async

当处理 props 的方法为异步时,useMemo 将不再适用,需要借用 useEffect 内部支持执行 async 函数的特性转化一步:

// 模拟异步处理 props 的逻辑
const asyncHandler = (v) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`已处理 ${v}`)
    }, 2000)
  })
}

/**
 * 处理 async 业务的 hooks 封装
 * @param {Function} func 异步逻辑函数
 * @param {Array} dep 依赖列表
 * @param {Object} initialValue 初始值
 */
function useAsyncComputed(func, dep, initialValue) {

  const [val, setVal] = useState(initialValue)
  
  // 借用 useEffect 执行异步逻辑
  useEffect(() => {
    let cancel = false

    const handler = async () => {
      const res = await func()

      if(!cancel) {
        setVal(res)
      }
    }
    handler()

	// 卸载时标记 cancel 已退出,在进程中的异步逻辑将不会再改变 val 值
    return () => {
      cancel = true
    }
  }, dep)

  return val
}

function AsyncFC({ value }) {

  const computed = useAsyncComputed(() => asyncHandler(value), [value], value)

  return (
    <div>{computed}</div>
  )
}

function App() {

  const [state, setState] = useState('old')

  useEffect(() => {
    // 模拟一些异步处理,变化传入的 props
    setTimeout(() => {
      setState('new')
    }, 4000)
  }, [])

  return (
    <AsyncFC value={state}></AsyncFC>
  )
}

在上文中我们封装了 useAsyncComputed 自定义 hooks 来处理含异步业务的计算属性,所以执行流程是:

初始显示传入的 value 值为 old

过 2 s 后,异步业务 asyncHandler 执行完毕,计算属性展示为 已处理 old

再过 2 s 后,传入的 props 改变为 new,触发异步业务 useAsyncComputed ,此时展示值仍为上一步的 已处理 old

再过 2 s 后,异步处理完毕,展示值为 已处理 new

实际处理中,可以增加 loading 效果,改进用户体验。

useAsyncMemo

https://github.com/awmleer/use-async-memo

npm install use-async-memo --save

yarn add use-async-memo
import {useAsyncMemo} from 'use-async-memo'

const replyMessage = useAsyncMemo(async () => ( // 这里的回调函数变成了async函数
  await api.fetchReply(message) // 这里也变成了await
), [message])

“文本框输入的同时自动搜索”

const [input, setInput] = useState()
const users = useAsyncMemo(async () => {
  if (input === '') return []
  return await apiService.searchUsers(input)
}, [input], [])

还可以加上防抖动的逻辑:

const [input, setInput] = useState()
const [debouncedInput] = useDebounce(input, 300)
const users = useAsyncMemo(async () => {
  if (debouncedInput === '') return []
  return await apiService.searchUsers(debouncedInput)
}, [debouncedInput], [])

@WangShuXian6
Copy link
Owner Author

React 的真正优势: 组合、单向数据流、免于 DSL、显式变异和静态心智模型

composition, unidirectional data flow, freedom from DSLs, explicit mutation and static mental model
React 的重点不是虚拟 DOM、自定义事件系统、服务器渲染或 JSX

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 19, 2021

设计纯函数组件 design purely functional components

React + Flux 和正确的思维方式

React + Flux 和正确的思维方式足以进行这样的设计。
组件不应该有内部状态

工作流程

首先用占位符填充应用程序,
然后开始向下实现功能,这意味着从层次结构的最低级别开始。[基础组件]
然后我描述一个组件的props和states,就好像应用程序的其余部分不存在一样。
例如

<UserBox showLoginMenu=true|false/>

但不这样设计

<UserBox isLoggedIn={true}/>

因为用户框不关心用户是否登录,它只应该在被告知时显示或不显示已登录的用户菜单。
当完成对组件状态的编码后,编写测试以确保它们按预期工作,
然后向上移动一级,依此类推,直到到达根组件。


无状态组件的处理

question :基于无状态组件的想法,它们必须对全局状态进行操作。你如何将这个状态注入到深度嵌套的组件中?
假设您有一个全局状态对象和一些深度嵌套的组件:您是否通过 props 将所需的状态一直向下传递到目标组件?这似乎是大量的样板文件并导致组件不必要的耦合?或者您是否使用上下文来确保所有组件都可以“隐式”访问全局状态?

answer : 从根组件一直传递到最低级别。
如果它变得过于冗长(或者我从一开始就知道我的应用程序会出错),我会执行以下操作:

<Breakfast coffee={coffeeSettings} eggs={eggsSettings}/>

然后在层次结构中的某个地方:

<Coffee {...coffeeSettings}/>

使用 GraphQL 有助于减少样板。
但另一方面,冗长的组件可以通过查看 props 更容易地找出它们的状态。

@WangShuXian6
Copy link
Owner Author

state 与 ref 的监测测试

测试 hooks 中的 模拟请求

node v22.4.0
"react": "^18.3.1"

测试无论嵌套多少hooks,组件,都可以监测 useState 值变更,无法监测useRef值变更

src/utils/mock.ts

// 模拟请求函数
export const mockApiCall = (params: unknown): Promise<string> => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(`Received params: ${JSON.stringify(params)}`);
      }, 2000);
    });
  }
  

src/utils/useQuery.ts

import { useState, useEffect } from 'react'

export interface QueryResult<T, P> {
  data: T | null
  error: Error | null
  isLoading: boolean
  exec: (p: P) => void
}

// 通用请求 hooks
const useQuery = <T, P>(queryFn: (params: P) => Promise<T>, params: P): QueryResult<T, P> => {
  const [data, setData] = useState<T | null>(null)
  const [error, setError] = useState<Error | null>(null)
  const [isLoading, setIsLoading] = useState<boolean>(false)

  const exec = async (newParams: P) => {
    setIsLoading(true)
    // try {
    //   const response = await queryFn(newParams || params)
    //   console.log(`response--${JSON.stringify(response)}`)
    //   setData(response)
    // } catch (error) {
    //   setError(error as Error)
    // } finally {
    //   setIsLoading(false)
    // }

    queryFn(newParams || params).then((response)=>{
      console.log(`queryFn response-${JSON.stringify(response)}`)
      setData(response)
    }).catch((error)=>{
      setError(error as Error)
    }).finally(()=>{
      setIsLoading(false)
    })
  }

  useEffect(() => {
    console.log('queryFn 123')
    exec(params)
  }, [])

  useEffect(() => {
    console.log(' queryFn 111111111data:', data)
  }, [data])

  return { data, error, isLoading, exec }
}

export default useQuery

src/hooks/useTestHookQuery.ts

//import useQuery from "../utils/useQuery";
import { mockApiCall } from "../utils/mock";
import { useEffect, useRef, useState } from "react";

const useTestHookQuery = (params: unknown, lazy = false) => {
  const [data, setData] = useState<unknown>();
  const dataRef = useRef<unknown>();
  const [error, setError] = useState<Error>();
  const [isLoading, setIsLoading] = useState<boolean>(false);

  const exec = async (newParams: unknown) => {
    setIsLoading(true);
    mockApiCall(newParams || params)
      .then((response) => {
        console.log(`响应 response:`);
        console.table(response);
        setData(response);
        dataRef.current = response;
      })
      .catch((error) => {
        setError(error as Error);
      })
      .finally(() => {
        setIsLoading(false);
      });
  };

  useEffect(() => {
    if (lazy) return;
    exec(params);
  }, []);

  useEffect(() => {
    // useState的值 可以监测变更
    console.log(`监测 useTestHookQuery  data:-data${data}}`);
    console.log(`监测 useTestHookQuery  data:-dataRef:${dataRef.current}}`);
  }, [data]);

  useEffect(() => {
    // useRef的值 无法监测变更
    console.log(`监测 useTestHookQuery  dataRef:-data${data}}`);
    console.log(`监测 useTestHookQuery  dataRef:-dataRef:${dataRef.current}}`);
  }, [dataRef]);

  return {
    data,
    error,
    isLoading,
    exec,
  };
};

export default useTestHookQuery;

src/components/TestQueryEffect.tsx

import React, { useEffect } from 'react';
import useQuery from '../utils/useQuery';
import   {mockApiCall} from '../utils/mock'

const TestSingQuery = () => {
  const { data, error, isLoading, exec } = useQuery<string, { userId: number }>(mockApiCall, { userId: 123 });

  useEffect(()=>{
    console.log(`监测 TestSingQuery data 更新:${data}`)
  },[data])

  return (
    <div>
      {isLoading && <p>Loading...</p>}
      {error && <p>Error: {error.message}</p>}
      {data && <p>Data: {data}</p>}
      <button onClick={() => exec({ userId: 456 })}>Refetch with new params</button>
    </div>
  );
}

export default TestSingQuery;

src/App.tsx

import { useEffect, useState } from "react";
import "./App.css";
import TestSingQuery from "./components/TestQueryEffect";
import useTestHookQuery from "./hooks/useTestHookQuery";

function App() {
  const [count, setCount] = useState(0);
  const { data, exec } = useTestHookQuery({ a: 2222 }, true);
  useEffect(() => {
    exec({ a: 333 });
  }, []);
  return (
    <>
      {/* <TestSingQuery /> */}
      {data}
    </>
  );
}

export default App;

image

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Aug 2, 2024

为什么react推荐使用新对象来更新state,而非直接修改源对象

在React中推荐使用新对象来更新state,而不是直接修改源对象,主要是为了确保React能够正确地检测到state的变化,从而有效地管理组件的更新。这里有几个关键原因:

  1. 不可变性

    • 不可变性指的是对象一旦创建就不能被改变。更新state时创建一个新对象,而不是修改现有对象,可以保持不可变性。
    • 不可变性使得调试和追踪state的变化更容易,因为每次state的变化都会产生一个新对象,这样就可以保留之前的state历史,从而更方便地进行状态管理和回滚操作。
  2. 性能优化

    • React的核心是通过虚拟DOM来优化实际DOM的更新。当state发生变化时,React会比较新旧虚拟DOM树,找出差异并最小化实际DOM的更新。
    • 如果直接修改原对象,React可能无法检测到对象的变化,因为它依赖于引用的变化(引用比较)。通过创建新对象,可以确保引用发生变化,从而让React准确地检测到需要更新的部分。
  3. 一致性

    • 使用新对象来更新state可以保证state的一致性和可预测性。直接修改源对象可能会导致不可预见的副作用,特别是在多个组件共享同一对象时。
    • 保持state不可变性可以防止意外的状态变化,使得应用更健壮和稳定。

示例代码

以下是一个简单的例子,说明如何在React中更新state时创建新对象:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  // 错误的做法:直接修改源对象(不会触发重新渲染)
  const incrementWrong = () => {
    count++;
  };

  // 正确的做法:创建一个新对象来更新state
  const incrementRight = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementWrong}>Wrong Increment</button>
      <button onClick={incrementRight}>Right Increment</button>
    </div>
  );
}

export default Counter;

在上述代码中,incrementWrong函数直接修改count的值,但这不会触发组件的重新渲染,因为React无法检测到state的引用发生了变化。相反,incrementRight函数创建了一个新的count值并调用setCount来更新state,从而触发重新渲染。

总结

通过保持state不可变性并使用新对象来更新state,React能够更准确地检测和管理组件的更新,确保应用性能和一致性。

是否建议使用map对象来保存,且直接修改map对象来更新state

在React中使用Map对象来保存state时,需要小心处理,因为直接修改Map对象可能会导致React无法正确检测到state的变化。为了确保React能够正确地管理组件的更新,仍然建议在更新Map对象时创建一个新对象或进行浅拷贝。以下是一些详细的原因和建议:

为什么不建议直接修改Map对象

  1. 引用不变性

    • React依赖于state引用的变化来检测是否需要重新渲染组件。如果直接修改Map对象的内容,Map对象的引用不会改变,React可能不会检测到state的变化,从而不会触发重新渲染。
  2. 不可变性原则

    • 尽管Map对象的内部实现可以方便地进行增删改查操作,但为了保持不可变性,仍然建议在更新Map时创建一个新的Map对象。这可以确保每次state的更新都是可追溯和可预测的。

示例代码

以下是如何在React中正确使用Map对象来更新state的示例:

import React, { useState } from 'react';

function MapComponent() {
  const [mapState, setMapState] = useState(new Map());

  // 错误的做法:直接修改Map对象(不会触发重新渲染)
  const updateMapWrong = (key, value) => {
    mapState.set(key, value);
    setMapState(mapState); // 这不会触发重新渲染
  };

  // 正确的做法:创建一个新的Map对象
  const updateMapRight = (key, value) => {
    const newMap = new Map(mapState);
    newMap.set(key, value);
    setMapState(newMap); // 这会触发重新渲染
  };

  return (
    <div>
      <p>Map size: {mapState.size}</p>
      <button onClick={() => updateMapWrong('key1', 'value1')}>Wrong Update</button>
      <button onClick={() => updateMapRight('key1', 'value1')}>Right Update</button>
    </div>
  );
}

export default MapComponent;

在上述示例中,updateMapWrong函数直接修改了mapState的内容,但没有改变mapState的引用,导致React不会检测到变化。而updateMapRight函数创建了一个新的Map对象,并更新了state,这样可以确保React正确地检测到state的变化并触发重新渲染。

总结

在React中,使用Map对象保存state时,为了确保React能够正确地检测到state的变化并管理组件的更新,建议在更新Map对象时创建一个新的Map对象,而不是直接修改原对象。这样可以保持不可变性,确保应用的状态管理更为可靠和可预测。

既然推荐直接用新对象更新,那么面对复杂对象,需要更新的只是其中一个属性怎么办?特别是一个嵌套很深的对象或数组

在React中更新嵌套很深的对象或数组时,依旧建议通过不可变性原则来更新状态,即创建一个新的对象,而不是直接修改原对象。这种做法在面对复杂的嵌套结构时可能会比较繁琐,但有几种策略可以帮助简化这个过程,包括使用扩展运算符、结合工具库(如lodashimmutability-helper),以及使用更现代的React状态管理工具(如Immer)。

使用扩展运算符更新嵌套对象

对于浅层次的嵌套对象,可以使用扩展运算符来更新特定的属性:

import React, { useState } from 'react';

function App() {
  const [state, setState] = useState({
    user: {
      name: 'John',
      address: {
        city: 'New York',
        zip: '10001'
      }
    }
  });

  const updateCity = () => {
    setState(prevState => ({
      ...prevState,
      user: {
        ...prevState.user,
        address: {
          ...prevState.user.address,
          city: 'Los Angeles'
        }
      }
    }));
  };

  return (
    <div>
      <p>City: {state.user.address.city}</p>
      <button onClick={updateCity}>Update City</button>
    </div>
  );
}

export default App;

使用工具库

对于嵌套更深的对象,可以使用lodashset函数或immutability-helper来简化更新过程。

Lodash

import React, { useState } from 'react';
import _ from 'lodash';

function App() {
  const [state, setState] = useState({
    user: {
      name: 'John',
      address: {
        city: 'New York',
        zip: '10001'
      }
    }
  });

  const updateCity = () => {
    const newState = _.cloneDeep(state);
    _.set(newState, 'user.address.city', 'Los Angeles');
    setState(newState);
  };

  return (
    <div>
      <p>City: {state.user.address.city}</p>
      <button onClick={updateCity}>Update City</button>
    </div>
  );
}

export default App;

Immutability-helper

import React, { useState } from 'react';
import update from 'immutability-helper';

function App() {
  const [state, setState] = useState({
    user: {
      name: 'John',
      address: {
        city: 'New York',
        zip: '10001'
      }
    }
  });

  const updateCity = () => {
    const newState = update(state, {
      user: {
        address: {
          city: { $set: 'Los Angeles' }
        }
      }
    });
    setState(newState);
  };

  return (
    <div>
      <p>City: {state.user.address.city}</p>
      <button onClick={updateCity}>Update City</button>
    </div>
  );
}

export default App;

使用Immer

Immer是一个更现代化的库,它使用“生产者函数”来处理状态的不可变更新,使得代码更加简洁和易读。

import React, { useState } from 'react';
import produce from 'immer';

function App() {
  const [state, setState] = useState({
    user: {
      name: 'John',
      address: {
        city: 'New York',
        zip: '10001'
      }
    }
  });

  const updateCity = () => {
    setState(produce(draft => {
      draft.user.address.city = 'Los Angeles';
    }));
  };

  return (
    <div>
      <p>City: {state.user.address.city}</p>
      <button onClick={updateCity}>Update City</button>
    </div>
  );
}

export default App;

总结

对于嵌套很深的对象或数组,推荐使用工具库或现代状态管理工具来简化不可变状态更新。lodashimmutability-helperImmer都是处理这种情况的好选择,能帮助保持代码的可读性和维护性。通过这些工具,可以在保持状态不可变性的同时简化更新复杂嵌套结构的代码。

嵌套很深的数组状态如何修改是最佳实践?

在React中处理嵌套很深的数组状态时,确保以不可变的方式修改状态是非常重要的,因为这样可以避免引发潜在的渲染和性能问题。以下是几种最佳实践来安全和有效地修改嵌套数组:

1. 使用不可变数据更新技术

对于简单的结构,可以使用扩展运算符或数组的方法(如mapfilter等)来创建新的数组,而不直接修改原数组。

示例:更新嵌套数组中的某一项

const updateItemInNestedArray = (data, itemIndex, arrayIndex, newValue) => {
  return data.map((item, index) => {
    if (index === itemIndex) {
      const newArray = [...item];
      newArray[arrayIndex] = newValue;
      return newArray;
    }
    return item;
  });
};

2. 使用 immer

对于更复杂的嵌套结构,使用像immer这样的库可以大大简化不可变数据更新的复杂性。immer允许你以一种看似可变的方式修改数据,而它实际上会为你背后生成不可变的更新。

示例:使用 Immer

import produce from 'immer';

const data = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

const newData = produce(data, draft => {
  draft[1][2] = 100; // 修改第二个数组的第三个元素
});

3. 状态拆分

如果数组很大且更新很频繁,考虑将数组拆分成更小的部分或只将状态放在使用它的组件中,这样可以避免不必要的渲染和复杂的状态逻辑。

4. 结合使用 Context 和 useReducer

在多个组件需要访问和修改相同数组时,将useReducerContextAPI结合使用可以提供一个更集中和高效的方式来管理和更新嵌套数组。

示例:使用 Context 和 useReducer

import React, { useReducer, useContext, createContext } from 'react';

const ArrayContext = createContext(null);

const reducer = (state, action) => {
  switch (action.type) {
    case 'update_value':
      return produce(state, draft => {
        draft[action.payload.itemIndex][action.payload.arrayIndex] = action.payload.newValue;
      });
    default:
      return state;
  }
};

const ArrayProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, [[1, 2], [3, 4], [5, 6]]);
  return (
    <ArrayContext.Provider value={{ state, dispatch }}>
      {children}
    </ArrayContext.Provider>
  );
};

const App = () => (
  <ArrayProvider>
    <NestedComponent />
  </ArrayProvider>
);

const NestedComponent = () => {
  const { state, dispatch } = useContext(ArrayContext);

  const updateValue = () => {
    dispatch({ type: 'update_value', payload: { itemIndex: 1, arrayIndex: 1, newValue: 100 } });
  };

  return (
    <>
      <div>Value: {state[1][1]}</div>
      <button onClick={updateValue}>Update</button>
    </>
  );
};

export default App;

总结

对于React中嵌套很深的数组状态的修改,最佳实践包括使用不可变的更新方法、利用辅助库如immer简化状态更新、适当拆分状态以及合理使用React的高级特性如ContextuseReducer。这些方法可以帮助你维持高性能的应用,并保持代码的清晰和可维护性。

@WangShuXian6
Copy link
Owner Author

在 React 中使用 JSX 时,一些属性名称(特别是包含点号的属性名称)会导致语法错误。要解决这个问题,可以将这些属性设置为对象属性,而不是直接在 JSX 中使用它们。

const ctl = $('#myWriterControl');
    ctl.attr('dctype', 'WriterControlForWASM');
    ctl.attr('RuleVisible', 'true');
    ctl.attr('RuleBackColor', 'rgb(213, 220, 223)');
    ctl.attr('imagedataembedinhtml', 'true');
    ctl.attr('RegisterCode', '05D7DFC4AA1E317860F50521D0FC0DF2FA9D179A9393535FE32CAE901F58C5C89F6D07B71387AE26203CC42786E9FE2E4040F0A102BD9F8F5F48477C648F2DD3B092F82F98A23A2E1AECC40B9226BFA5897E9216D95435D320E2898F1EAFE71740DAC50CED00DC4E8FACC196015BCA1B05');
    ctl.attr('DocumentOptions.BehaviorOptions.CompressLayoutForFieldBorder', 'true');

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Sep 11, 2024

React 权限控制

假设

const userInfo.permissions=['crm:contract-config:query','crm:contract-config:create']

通过hooks ref 获取组件来控制组件的显示或隐藏,

不适合循环创建的组件
hooks 规则 :https://react.dev/warnings/invalid-hook-call-warning

src\shared\hooks\usePermissionDirective.ts

import { useRef, useEffect } from 'react'
import { useSelector } from 'react-redux'

// 自定义 Hook 来处理权限
export const usePermissionDirective = () => {
  const { userInfo } = useSelector((store: any) => store.preBook)
  const refs = {} // 用对象来存储多个 ref

  // 内部生成唯一的 ref key
  const getRef = (permission) => {
    const uniqueKey = Symbol(permission) // 使用 Symbol 生成唯一 key

    if (!refs[uniqueKey]) {
      refs[uniqueKey] = useRef(null)
    }

    useEffect(() => {
      const el = refs[uniqueKey]?.current

      if (el) {
        if (!userInfo?.permissions || !userInfo?.permissions?.includes(permission)) {
          el.style.display = 'none' // 没有权限则隐藏
        } else {
          el.style.display = '' // 有权限时显示
        }
      }
    }, [permission, userInfo])

    return refs[uniqueKey]
  }

  return getRef
}

使用

import {usePermissionDirective} from '@/shared/hooks/usePermissionDirective'

const getPermissionRef = usePermissionDirective();

   <Button  ref={getPermissionRef("crm:contract-config:query")}  >

通过包装高阶组件HOC来控制组件的显示或隐藏

适合各种情况,可在循环创建的组件中使用

src\shared\hoc\withPermission.tsx

import React from 'react'
import { useSelector } from 'react-redux'

// 定义权限类型
interface UserInfo {
  permissions: string[]
}

// 从 Redux store 中选择用户信息
const useUserInfo = () => useSelector((store: any) => store.preBook.userInfo)

// 高阶组件的类型
interface WithPermissionProps {
  permissions: string
}

export const withPermission = <P extends object>(WrappedComponent: React.ComponentType<P>) => {
  const WithPermissionComponent: React.FC<P & WithPermissionProps> = (props) => {
    const userInfo = useUserInfo()
    const { permissions, ...restProps } = props as WithPermissionProps

    const hasPermission = userInfo?.permissions?.includes(permissions)

    // 如果没有权限,则返回 null(不渲染任何内容)
    if (!hasPermission) {
      return null
    }

    // 如果有权限,渲染包装组件
    return <WrappedComponent {...(restProps as P)} />
  }

  // 设置 displayName 以便于调试
  WithPermissionComponent.displayName = `WithPermission(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`

  return WithPermissionComponent
}

使用 HOC 包装按钮组件

const PermissionedButton = withPermission(Button);

const App: React.FC = () => (
  <div>
    <PermissionedButton permissions='crm:contract-config:query'>
      查看合同配置
    </PermissionedButton>

    <PermissionedButton permissions='erp:purchase-in:create'>
      创建采购单
    </PermissionedButton>
  </div>
);

export default App;

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 10, 2024

使用 useContext 管理应用程序状态。

第一步:创建上下文 (Context)

首先,我们需要创建一个上下文来存储共享的数据:

// src/contexts/AppContext.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface AppContextProps {
  userName: string;
  setUserName: (name: string) => void;
}

const AppContext = createContext<AppContextProps | undefined>(undefined);

export const AppProvider = ({ children }) => {
  const [userName, setUserName] = useState<string>('Guest');

  return (
    <AppContext.Provider value={{ userName, setUserName }}>
      {children}
    </AppContext.Provider>
  );
};

export const useAppContext = (): AppContextProps => {
  const context = useContext<AppContextProps | undefined>(AppContext);
  if (!context) {
    throw new Error('useAppContext must be used within an AppProvider');
  }
  return context;
};

第二步:将 AppProvider 包裹整个应用程序

接下来,需要在应用的根组件中使用 AppProvider 来包裹其他组件。这样,就可以确保所有子组件都可以访问上下文的数据。

// src/app.tsx
import React from 'react';
import { AppProvider } from './contexts/AppContext';
import { View } from '@tarojs/components';
import HomePage from './pages/HomePage';

const App: React.FC = () => {
  return (
    <AppProvider>
      <View>
        <HomePage />
      </View>
    </AppProvider>
  );
};

export default App;

第三步:在子组件中使用上下文

在子组件中使用 useAppContext 钩子来访问和更新上下文的数据。

// src/pages/HomePage.tsx
import React from 'react';
import { useAppContext } from '../contexts/AppContext';
import { Button, View, Text, Input } from '@tarojs/components';

const HomePage: React.FC = () => {
  const { userName, setUserName } = useAppContext();

  const handleChange = (e: any) => {
    setUserName(e.target.value);
  };

  return (
    <View>
      <Text>Hello, {userName}!</Text>
      <Input type="text" value={userName} onInput={handleChange} />
      <Button onClick={() => setUserName('Taro User')}>Set Default Name</Button>
    </View>
  );
};

export default HomePage;

关键点总结

  1. 使用 createContext 创建上下文:使用 createContext 来创建上下文,并设置默认值。

  2. 通过 AppProvider 提供上下文:创建一个 Provider 组件来管理上下文的状态,并在根组件中包裹它,这样整个应用都能访问该上下文。

  3. 自定义钩子简化上下文使用:定义一个自定义钩子(如 useAppContext)来简化上下文的使用,保证只在 Provider 内部使用上下文,从而避免在没有提供上下文的组件中出现错误。

  4. TypeScript 类型定义:通过定义接口(如 AppContextProps)来约束上下文的类型,确保上下文的数据和操作都符合预期。

这种方式不仅提高了代码的可读性和可维护性,还使得共享状态的管理更加方便和安全。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant