其他章节请看:

react实战 系列

数据流和生命周期

如何处理 React 中的数据,组件之间如何通信,数据在 React 中如何流动?

常用的 React 生命周期方法以及开源项目 spug 中使用了哪些生命周期方法?

数据和数据流

虽然也有很多静态网站,但人们使用的大多数网站都充满了随时间变化的数据。

stateprops 是 React 组件处理数据和彼此通信的两种主要方法。

React 提供了两个数据相关的 api:state 和 props。

前面我们已经知道,不要直接修改 state,而是使用 setState(updater[, callback])

updater 函数签名:(preState, props) => stateChange

Tip: 建议传递函数而非对象。React 16 以前的版本允许传递对象而不是函数给 setState 的第一个参数。传递对象给 state 暗示是同步的,实际却并非如此,而使用函数,更符合 React 的异步范式,React 会更新,但不保证时间。

笔者测试,下面两种写法都生效:

  1. this.setState({
  2. date: new Date()
  3. })
  4. this.setState(() => ({
  5. date: new Date()
  6. }))

假设我们想根据 props.step 来增加 state,可以这样:

  1. this.setState((state, props) => {
  2. return {counter: state.counter + props.step};
  3. });

setState 是浅合并,而非替换。请看实验:合并还是替换

如果需要在应用更新后触发某些动作,可以使用 setState 的回调函数(setState(updater, callback)

不可变状态 props

在 React 中,属性是传递不可变数据的主要方式,所有组件都可以接收属性,并能在构造函数、render()和生命周期方法中使用。

属性通常来自父组件或自身的默认属性值(defaultProps)。

是否可以将父组件的 state 作为属性传给子组件呢?是的。一个组件的状态可以是另一个组件的属性。

可以将任何有效的 js 数据作为属性传递给子组件

Tip: 前面 我们已经测试过了,如果修改 props(this.props.name = 'aName'),控制台会报错。

属性可以随时间改变,但不是从组件内部改变。这是单向数据流的一部分,下面会提到。

只使用 props 的组件

倘若我们希望将状态保存在一个中心位置,而不是分散在各个组件中,比如 Flux、Redux等。这时我们可以创建无状态函数组件(或称之为函数组件)。

无状态不是没有任何种类的状态,而是它不会获得 React 进行管理的支撑实例,意味着没有生命周期方法,没有 state。

无状态函数组件与有支撑实例的父组件结合使用时非常强大。与其在多个组件间设置状态,不如创建单个有状态的父组件并让其余部分使用轻量级子组件。例如 Redux 会将这个模式提升到全新的水平。

Tip:请练习,使用一个组件的状态修改另一个组件的属性。(下文父子组件有使用)

组件通信

现在我们能够用子组件轻易的构建新组件,能够很容易地表示组件间的 has-a、is-a 的关系。如何让他们通信呢?

在 React 中,如果想让组件彼此通信,需要传递属性,开发人员会做两件事:

  • 访问父组件中的数据(状态或属性)
  • 传递数据给子组件

下面定义 1 个父组件,2 个子组件,其中 2 个子组件中的数据来自父组件。请看示例:

  1. const MyComponent = (props) => (
  2. <div>
  3. <MyComponentSub1 content={props.cnt1}/>
  4. <MyComponentSub2 content={props.cnt2}/>
  5. </div>
  6. )
  7. const MyComponentSub1 = (props) => (
  8. <p>sub1: {props.content}</p>
  9. )
  10. const MyComponentSub2 = (props) => (
  11. <p>sub2: {props.content}</p>
  12. )
  13. ReactDOM.render(
  14. <MyComponent cnt1="apple" cnt2="orange" />,
  15. document.getElementById('root')
  16. );

页面显示:

  1. sub1: apple
  2. sub2: orange

单向数据流

数据如何流经应用的不同部分?

在 React 中,UI 是数据投射到视图中的数据,当数据变化时,视图随之变化。

React 中,数据流是单向的。上层通过 props 传递数据给子组件,子组件通过回调函数将数据传递给上层。单向数据流让思考数据在应用中的流动变得更简单

也得益于组件的层次结构以及将属性和状态局限于组件,预测数据在应用中如何移动也更加简单。

数据在 React 中是按一个方向流动的。属性由父组件传递给子组件(从所有者到拥有者),子组件不能编辑父组件的状态或属性。每个拥有支撑实例的组件都能修改自身的 state 但无法修改超出自身的东西,除了设置子组件的属性。

如果允许从应用的部分随意修改想要修改的东西,听上去好像不错,实际上可能会导致难以捉摸的应用并且可能会造成调试困难。

React 中的数据流是单向的,从父组件流向子组件,子组件通过回调函数将数据回送给父组件,但它不能直接修改父组件的状态,而父组件也不能直接修改子组件的状态,组件间通信通过属性完成。

渲染和生命周期

渲染就是 React 创建和管理用户界面所做的工作,也就是让应用展现到屏幕上。

和 vue 中生命周期类似,react 也可以分 4 部分:

  • 初始 - 组件被实例化的时候
  • 挂载 - 组件插入到 dom
  • 更新 - 通过状态或属性用新数据更新组件
  • 卸载 - 组件从 dom 中移除

生命周期方法简介

笔者在前文已经分析过一次 react 生命周期相关方法,这里再次总结一下:

挂载时的顺序:constructor()、render()、componentDidMount()(组件挂载后立即调用)

Tip:

  • 挂载是 React 将组件插入到 dom 的过程。React 在实际 dom 中创建组件之前,组件只存在虚拟 dom 中。
  • 容易犯的一个错误是把不该放到 render() 中的东西放到了 render() 里面。render() 通常会在组件的生命周期内调用多次,也无法确定 React 会何时调用 render(),因为出于性能 React 会批量更新。
  • 挂载(ReactDOM.render)和卸载(ReactDOM.unmountComponentAtNode)由 React.DOM 从外部控制。没有 ReactDOM 的帮助,组件不能卸载自己。

更新时的顺序:shouldComponentUpdate、render()、componentDidUpdate(组件更新后被立即调用)

卸载时:componentWillUnmount

过时的生命周期有:componentWillMount、componentWillReceiveProps、componentWillUpdate。官网不建议在新代码中使用它们。

避免使用:forceUpdate、shouldComponentUpdate。

Tip:React 采用了复杂的、先进的方法确定应该更新什么以及何时更新。如果最终使用了 shouldComponentUpdate,则应该是那些方法由于某种原因不够用的情况下。

新增生命周期方法:getDerivedStateFromProps、getSnapshotBeforeUpdate。都属于不常见的情形。

spug 使用了哪些生命周期方法

React 有那么多生命周期相关方法,那么像 spug 这种运维相关的开源项目用到了哪些,是否都用到了?

答:只用到了挂载、更新和卸载 3 个方法。

请看笔者统计:

constructor

共 25 处

几乎都是初始化 state。没有发送数据请求。摘录如下:

  1. constructor(props) {
  2. super(props);
  3. this.state = {
  4. groupMap: {}
  5. }
  6. }
  1. constructor(props) {
  2. super(props);
  3. this.textView = null;
  4. this.JSONView = null;
  5. this.state = {
  6. view: '1'
  7. }
  8. }
  1. constructor(props) {
  2. super(props);
  3. this.input = null;
  4. this.state = {
  5. fetching: false,
  6. showDot: false,
  7. uploading: false,
  8. uploadStatus: 'active',
  9. pwd: [],
  10. objects: [],
  11. percent: 0
  12. }
  13. }
componentDidMount

共 29 处。都是用于发送 ajax 请求数据。摘录如下:

  1. store.fetchRecords();
  1. http.post('/api/config/history/'
  1. this.updateValue()

Tip: 感觉请求数据时机有点迟。因为 vue 中请求数据我们通常会写在 created 生命周期中,能更快获取到服务端数据,减少页面loading 时间。

0 处使用

以下生命周期方法都在 spug 中使用。

  • shouldComponentUpdate
  • componentDidUpdate
  • componentWillUnmount
  • componentDidCatch(构造函数、渲染、生命周期中未捕获的错误)
  • 过时的生命周期:componentWillMount、componentWillReceiveProps、componentWillUpdate。
  • 不建议使用:forceUpdate、shouldComponentUpdate
  • 新增生命周期方法:getDerivedStateFromProps、getSnapshotBeforeUpdate
React.useEffect

我们知道 React.useEffect 可以在函数组件中模拟 componentDidMount、componentDidUpdate、componentWillUnmount。就像这样:

  1. // 相当于 componentDidMount()
  2. React.useEffect(() => {
  3. console.log('a')
  4. }, [])
  1. // 相当于 componentDidMount()、componentDidUpdate()
  2. React.useEffect(() => {
  3. console.log('a')
  4. })
  1. // 相当于 componentDidMount、componentWillUnmount
  2. React.useEffect(() => {
  3. console.log('a')
  4. return () => {
  5. console.log('b')
  6. }
  7. }, [])

Tip: 有关 React.useEffect 的详细介绍请看 这里 -> 体验 useEffect

搜索 useEffect,发现 spug 有 126 处。作用就这 3 点:

  • 模拟 componentDidMount 发送网络请求,或初始化数据,或设置定时器
  • 模拟 componentWillUnmount 删除定时器
  • 模拟 componentDidUpdate

摘录如下:

  1. // 模拟 componentDidMount,用于发送网络请求
  2. useEffect(() => {
  3. setFetching(true);
  4. http.get('/api/app/deploy/')
  5. .then(res => setDeploys(res))
  6. .finally(() => setFetching(false))
  7. if (!envStore.records.length) {
  8. envStore.fetchRecords().then(_initEnv)
  9. } else {
  10. _initEnv()
  11. }
  12. }, [])
  1. // 模拟 componentDidMount 发送网络请求、添加定时器,模拟 componentWillUnmount 删除定时器
  2. useEffect(() => {
  3. fetch();
  4. listen();
  5. timer = setInterval(() => {
  6. if (ws.readyState === 1) {
  7. ws.send('ping')
  8. } else if (ws.readyState === 3) {
  9. listen()
  10. }
  11. }, 10000)
  12. return () => {
  13. if (timer) clearInterval(timer);
  14. if (ws.close) ws.close()
  15. }
  16. // eslint-disable-next-line react-hooks/exhaustive-deps
  17. }, [])
  1. React.useEffect(() => {
  2. store.fetchGitlabList()
  3. store.fetchProjectName()
  4. }, [])
  1. // 模拟 componentDidMount()、componentDidUpdate()
  2. useEffect(() => {
  3. setLoading(true);
  4. http.get('/api/home/alarm/', {params})
  5. .then(res => setRes(res))
  6. .finally(() => setLoading(false))
  7. }, [params])
  1. useEffect(() => {
  2. let socket;
  3. initialTerm()
  4. http.get(`/api/repository/${store.record.id}/`)
  5. .then(res => {
  6. term.write(res.data)
  7. setStep(res.step)
  8. if (res.status === '1') {
  9. socket = _makeSocket(res.index)
  10. } else {
  11. setStatus('wait')
  12. }
  13. })
  14. .finally(() => setFetching(false))
  15. return () => socket && socket.close()
  16. // eslint-disable-next-line react-hooks/exhaustive-deps
  17. }, [])
  1. useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
  1. // 写多个 useEffect 也可以
  2. useEffect(() => {
  3. if (!loading) {
  4. http.get('/api/exec/history/')
  5. .then(res => setHistories(res))
  6. }
  7. }, [loading])
  8. useEffect(() => {
  9. return () => {
  10. store.host_ids = []
  11. if (store.showConsole) {
  12. store.switchConsole()
  13. }
  14. }
  15. }, [])
  1. // 挂载时注册事件 resize,卸载时注销事件 resize
  2. useEffect(() => {
  3. store.tag = ''
  4. gCurrent = current
  5. const fitPlugin = new FitAddon()
  6. term.setOption('disableStdin', false)
  7. term.setOption('fontFamily', 'Source Code Pro, Courier New, Courier, Monaco, monospace, PingFang SC, Microsoft YaHei')
  8. term.setOption('theme', {background: '#f0f0f0', foreground: '#000', selection: '#999', cursor: '#f0f0f0'})
  9. term.loadAddon(fitPlugin)
  10. term.open(el.current)
  11. fitPlugin.fit()
  12. term.write('\x1b[36m### WebSocket connecting ...\x1b[0m')
  13. const resize = () => fitPlugin.fit();
  14. window.addEventListener('resize', resize)
  15. setTerm(term)
  16. return () => window.removeEventListener('resize', resize);
  17. // eslint-disable-next-line react-hooks/exhaustive-deps
  18. }, [])

父子组件的生命周期

笔者创建父子两个组件,加入常见的生命周期方法(如:componentDidMount、shouldComponentUpdate、componentDidUpdate、componentWillUnmount、componentDidCatch),功能很简单,子组件的文案来自父组件的 state,父组件的 state.text 能被用户通过 input 修改。

代码如下:

  1. <script type="text/babel">
  2. class ChildComponent extends React.Component {
  3. static propTypes = {
  4. name: PropTypes.string
  5. };
  6. static defaultProps = (function () {
  7. console.log("ChildComponent : defaultProps。孩子的 defaultProps 是");
  8. return {};
  9. })();
  10. constructor(props) {
  11. super(props);
  12. console.log("ChildComponent: state。孩子的 state 是");
  13. this.state = {
  14. text: "peng"
  15. };
  16. }
  17. // 组件挂载后(插入 DOM 树中)立即调用。常做网络请求
  18. componentDidMount() {
  19. console.log("ChildComponent : componentDidMount");
  20. }
  21. shouldComponentUpdate(nextProps, nextState) {
  22. console.log("<ChildComponent/> - shouldComponentUpdate()");
  23. return true;
  24. }
  25. // React 更新 dom 和 refs 后调用
  26. componentDidUpdate(previousProps, previousState) {
  27. console.log("ChildComponent: componentDidUpdate");
  28. }
  29. componentWillUnmount() {
  30. console.log("ChildComponent: componentWillUnmount");
  31. }
  32. render() {
  33. if (this.props.content === 'jiali12') {
  34. throw new Error("模拟组件报错");
  35. }
  36. console.log("ChildComponent: render");
  37. // 下面两种写法等效。一个是单根一个是多根。
  38. // return <div className="subClass">
  39. // sub
  40. // <p>{this.props.content}</p>
  41. // </div>
  42. // 下面这种数组的写法需要给每个元素添加 key,否则报错如下:
  43. // Warning: Each child in a list should have a unique "key" prop.
  44. // React 要求每个被迭代项传递一个 key,对于 render() 方法返回的任何数组组件亦如此。
  45. return [
  46. <div key="name" className="subClass">sub</div>,
  47. <p key="content">{this.props.content}</p>
  48. ]
  49. }
  50. }
  51. class ParentComponent extends React.Component {
  52. static defaultProps = (function () {
  53. console.log("ParentComponent: defaultProps。我的 defaultProps 是");
  54. return {
  55. true: false
  56. };
  57. })();
  58. constructor(props) {
  59. super(props);
  60. console.log("ParentComponent: state。我的 state 是");
  61. this.state = { text: "jiali" };
  62. this.onInputChange = this.onInputChange.bind(this);
  63. }
  64. onInputChange(e) {
  65. const text = e.target.value;
  66. this.setState(() => ({ text: text }));
  67. }
  68. componentDidMount() {
  69. console.log("ParentComponent: componentDidMount");
  70. }
  71. componentDidUpdate(previousProps, previousState) {
  72. console.log("ParentComponent: componentDidUpdate");
  73. }
  74. componentWillUnmount() {
  75. console.log("ParentComponent: componentWillUnmount");
  76. }
  77. // 此生命周期在后代组件抛出错误后被调用
  78. componentDidCatch(err, errorInfo) {
  79. console.log("componentDidCatch");
  80. this.setState(() => ({ err, errorInfo }));
  81. }
  82. render() {
  83. if (this.state.err) {
  84. return <div>降级处理</div>
  85. }
  86. console.log("ParentComponent: render");
  87. return <div className="parentClass">
  88. <p>parent</p>
  89. <input
  90. key="input"
  91. value={this.state.text}
  92. onChange={this.onInputChange}
  93. />
  94. <ChildComponent content={this.state.text} />
  95. </div>
  96. }
  97. }
  98. ReactDOM.render(
  99. <ParentComponent />,
  100. document.getElementById('root')
  101. );
  102. </script>

浏览器中生成的页面结构如下:

  1. <div id="root">
  2. <div class="parentClass">
  3. <p>parent</p>
  4. <input value="jiali">
  5. <!-- 子组件 -->
  6. <div class="subClass">sub</div>
  7. <p>jiali</p>
  8. <!-- /子组件 -->
  9. </div>
  10. </div>

控制台输出:

  1. ChildComponent : defaultProps。孩子的 defaultProps
  2. ParentComponent: defaultProps。我的 defaultProps
  3. ParentComponent: state。我的 state
  4. ParentComponent: render
  5. ChildComponent: state。孩子的 state
  6. ChildComponent: render
  7. ChildComponent : componentDidMount
  8. ParentComponent: componentDidMount

Tip:尽管初始 state 和属性并不使用特定的生命周期方法,但他们为组件提供数据方面发挥了重要作用,所以有必要把它们作为生命周期的一部分

此刻是初次渲染,也就是挂载时,根据输出日志我们知道:

  • 先 defaultProps 后 state
  • 先 render 后 componentDidMount
  • 先 render 父组件,后 render 子组件
  • 先挂载子组件,后挂载父组件

为何先 render 父组件,又先挂载子组件?

Tip: 其实 vue 也是这样,请看 这里

笔者推测如下:

入口是这里:

  1. ReactDOM.render(
  2. <ParentComponent />,
  3. document.getElementById('root')
  4. );

先渲染父组件,发现里面有子组件,接着渲染子组件。

我们将虚拟 Dom看做真实 Dom,虚拟 Dom 如何生成真实 Dom 的我们暂时不去管它,这是 React 做的事情。虚拟 DOM 树的生成过程前文我们已经说了:像一个小孩不停的问“里面是什么?”,”里面是什么?“,直到搞懂所有的子孙组件,就可以形成一颗完整的树。

接着往 input 中输入 1(此刻显示jiali1),控制台输出:

  1. ParentComponent: render
  2. <ChildComponent/> - shouldComponentUpdate()
  3. ChildComponent: render
  4. ChildComponent: componentDidUpdate
  5. ParentComponent: componentDidUpdate

同样,先执行父组件的 render 再执行子组件的 render,先调用子组件的 componentDidUpdate 再调用父组件的 componentDidUpdate。可以翻译成:

  • 要渲染父组件
  • 里面有子组件需要渲染
  • 子组件更新完毕
  • 父组件更新完毕

接着往 input 中输入 2(此刻显示jiali12),触发子组件 render 里面报错,父组件中的 componentDidCatch 被执行,实现降级处理,页面显示 降级处理

其他章节请看:

react实战 系列

react实战 系列 —— React 的数据流和生命周期的更多相关文章

  1. react实战系列 —— React 中的表单和路由的原理

    其他章节请看: react实战 系列 React 中的表单和路由的原理 React 中的表单是否简单好用,受控组件和非受控是指什么? React 中的路由原理是什么,如何更好的理解 React 应用的 ...

  2. React 深入系列4:组件的生命周期

    文:徐超,<React进阶之路>作者 授权发布,转载请注明作者及出处 React 深入系列4:组件的生命周期 React 深入系列,深入讲解了React中的重点概念.特性和模式等,旨在帮助 ...

  3. react实战系列 —— react 的第一个组件

    react 的第一个组件 写了 react 有一个半月,现在又有半个月没写了,感觉对其仍旧比较陌生. 本文分两部分,首先聊一下 react 的相关概念,然后不使用任何语法糖(包括 jsx)或可能隐藏底 ...

  4. react实战系列 —— 起步(mockjs、第一个模块、docusaurus)

    其他章节请看: react实战 系列 起步 本篇我们首先引入 mockjs ,然后进入 spug 系统,接着模仿"任务计划"模块实现一个类似的一级导航页面("My任务计划 ...

  5. react实战系列 —— 我的仪表盘(bizcharts、antd、moment)

    其他章节请看: react实战 系列 My Dashboard 上一篇我们在 spug 项目中模仿"任务计划"模块实现一个类似的一级导航页面("My任务计划") ...

  6. 七天接手react项目 系列 —— react 脚手架创建项目

    其他章节请看: 七天接手react项目 系列 react 脚手架创建项目 前面我们一直通过 script 的方式学习 react 基础知识,而真实项目通常是基于脚手架进行开发. 本篇首先通过 reac ...

  7. 七天接手react项目 系列 —— react 路由

    其他章节请看: 七天接手react项目 系列 react 路由 本篇首先讲解路由原理,接着以一个基础路由示例为起点讲述路由最基础的知识,然后讲解嵌套路由.路由传参,最后讲解路由组件和一般组件的区别,以 ...

  8. 实战Asp.Net Core:DI生命周期

    title: 实战Asp.Net Core:DI生命周期 date: 2018-11-30 21:54:52 --- 1.前言 Asp.Net Core 默认支持 DI(依赖注入) 软件设计模式,那使 ...

  9. React Native填坑之旅--组件生命周期

    这次我们来填React Native生命周期的坑.这一点非常重要,需要有一个清晰的认识.如果你了解Android或者iOS的话,你会非常熟悉我们今天要说的的内容. 基本上一个React Native的 ...

随机推荐

  1. e2fsck-磁盘分区修复

    检查 ext2/ext3/ext4 类型文件系统. 语法 e2fsck [-panyrcdfvtDFV] [-b superblock] [-B blocksize] [-I inode_buffer ...

  2. .net 获取IP地址的几种方式

    1.获取服务器IP地址: 1) Local_Addr var Local_Addr = Request.ServerVariables.Get("Local_Addr").ToSt ...

  3. 【转载】k8s入坑之路(2)kubernetes架构详解

    每个微服务通过 Docker 进行发布,随着业务的发展,系统中遍布着各种各样的容器.于是,容器的资源调度,部署运行,扩容缩容就是我们要面临的问题. 基于 Kubernetes 作为容器集群的管理平台被 ...

  4. 老子云AMRT全新三维格式正式上线,其性能全面超越现有的三维数据格式

    9月16日,老子云AMRT全新三维格式正式上线,其性能远超现有的三维数据格式.目前已有含国家超算长沙中心.中科院空间所.中车集团等上百家政企事业单位的项目中使用了AMRT格式,大大提升了可视化项目的开 ...

  5. 强化学习-Windows安装gym、atari和box2d环境

    安装gym pip3 install gym pip3 install gym[accept-rom-license] 安装atari环境[可选] 下载安装VS build tools 如果出现 OS ...

  6. 教你如何用网页开发APP

    用到的工具: HBuilderX app开发版1.首先你得网站必须是上线的,然后明确这一点后,点击打开HBuilderX.在文件里找到新建项目,选择wap2App,将下面信息填写完整,然后创建. 2. ...

  7. 使用Playbook批量部署多台LAMP环境

    1. 安装ansible yum install epel-release -y yum install ansible -y Playbook是一个不同于使用ansible命令行执行方式的模式,功能 ...

  8. Tapdata “设擂招贤”携手 LeetCode 举办全球极客技术竞赛

      2021年11月28日 Tapdata 专场全球极客技术竞赛将在 LeetCode 平台开赛,面向程序员"设擂招贤",打擂成功的前50名挑战者将优先获得 Tapdata 高端技 ...

  9. 选择结构-穿透的switch语句和循环结构-循环概述

     case的穿透性 在switch语句中,如果case的后面不写break,将出现穿透现象,也就是不会在判断下一个case的值,直接向后运 行,直到遇到break,或者整体switch结束 publi ...

  10. 看起来是线程池的BUG,但是我认为是源码设计不合理。

    你好呀,我是歪歪. 前几天看到一个 JDK 线程池的 BUG,我去了解了一下,摸清楚了它的症结所在之后,我觉得这个 BUG 是属于一种线程池方法设计不合理的地方,而且官方在知道这个 BUG 之后表示: ...