详解React 的数据流和生命周期
作者:彭加李
数据流和生命周期
如何处理 React 中的数据,组件之间如何通信,数据在 React 中如何流动?
常用的 React 生命周期方法以及开源项目 spug 中使用了哪些生命周期方法?
数据和数据流
虽然也有很多静态网站,但人们使用的大多数网站都充满了随时间变化的数据。
state
和 props
是 React 组件处理数据和彼此通信
的两种主要方法。
React 提供了两个数据相关的 api:state 和 props。
前面我们已经知道,不要直接修改 state,而是使用 setState(updater[, callback])
updater 函数签名:(preState, props) => stateChange
Tip: 建议传递函数
而非对象。React 16 以前的版本允许传递对象而不是函数给 setState 的第一个参数。传递对象给 state 暗示是同步的,实际却并非如此,而使用函数,更符合 React 的异步范式,React 会更新,但不保证时间。
笔者测试,下面两种写法都生效:
this.setState({ date: new Date() }) this.setState(() => ({ date: new Date() }))
假设我们想根据 props.step 来增加 state,可以这样:
this.setState((state, props) => { return {counter: state.counter + props.step}; });
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 个子组件中的数据来自父组件。请看示例:
const MyComponent = (props) => ( <div> <MyComponentSub1 content={props.cnt1}/> <MyComponentSub2 content={props.cnt2}/> </div> ) const MyComponentSub1 = (props) => ( <p>sub1: {props.content}</p> ) const MyComponentSub2 = (props) => ( <p>sub2: {props.content}</p> ) ReactDOM.render( <MyComponent cnt1="apple" cnt2="orange" />, document.getElementById('root') );
页面显示:
sub1: apple 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。没有发送数据请求。摘录如下:
constructor(props) { super(props); this.state = { groupMap: {} } }
constructor(props) { super(props); this.textView = null; this.JSONView = null; this.state = { view: '1' } }
constructor(props) { super(props); this.input = null; this.state = { fetching: false, showDot: false, uploading: false, uploadStatus: 'active', pwd: [], objects: [], percent: 0 } }
componentDidMount
共 29 处。都是用于发送 ajax 请求数据。摘录如下:
store.fetchRecords();
http.post('/api/config/history/'
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。就像这样:
// 相当于 componentDidMount() React.useEffect(() => { console.log('a') }, [])
// 相当于 componentDidMount()、componentDidUpdate() React.useEffect(() => { console.log('a') })
// 相当于 componentDidMount、componentWillUnmount React.useEffect(() => { console.log('a') return () => { console.log('b') } }, [])
Tip: 有关 React.useEffect 的详细介绍请看 这里 -> 体验 useEffect
搜索 useEffect
,发现 spug 有 126 处。作用就这 3 点:
- 模拟 componentDidMount 发送网络请求,或初始化数据,或设置定时器
- 模拟 componentWillUnmount 删除定时器
- 模拟 componentDidUpdate
摘录如下:
// 模拟 componentDidMount,用于发送网络请求 useEffect(() => { setFetching(true); http.get('/api/app/deploy/') .then(res => setDeploys(res)) .finally(() => setFetching(false)) if (!envStore.records.length) { envStore.fetchRecords().then(_initEnv) } else { _initEnv() } }, [])
// 模拟 componentDidMount 发送网络请求、添加定时器,模拟 componentWillUnmount 删除定时器 useEffect(() => { fetch(); listen(); timer = setInterval(() => { if (ws.readyState === 1) { ws.send('ping') } else if (ws.readyState === 3) { listen() } }, 10000) return () => { if (timer) clearInterval(timer); if (ws.close) ws.close() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [])
React.useEffect(() => { store.fetchGitlabList() store.fetchProjectName() }, [])
// 模拟 componentDidMount()、componentDidUpdate() useEffect(() => { setLoading(true); http.get('/api/home/alarm/', {params}) .then(res => setRes(res)) .finally(() => setLoading(false)) }, [params])
useEffect(() => { let socket; initialTerm() http.get(`/api/repository/${store.record.id}/`) .then(res => { term.write(res.data) setStep(res.step) if (res.status === '1') { socket = _makeSocket(res.index) } else { setStatus('wait') } }) .finally(() => setFetching(false)) return () => socket && socket.close() // eslint-disable-next-line react-hooks/exhaustive-deps }, [])
useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
// 写多个 useEffect 也可以 useEffect(() => { if (!loading) { http.get('/api/exec/history/') .then(res => setHistories(res)) } }, [loading]) useEffect(() => { return () => { store.host_ids = [] if (store.showConsole) { store.switchConsole() } } }, [])
// 挂载时注册事件 resize,卸载时注销事件 resize useEffect(() => { store.tag = '' gCurrent = current const fitPlugin = new FitAddon() term.setOption('disableStdin', false) term.setOption('fontFamily', 'Source Code Pro, Courier New, Courier, Monaco, monospace, PingFang SC, Microsoft YaHei') term.setOption('theme', {background: '#f0f0f0', foreground: '#000', selection: '#999', cursor: '#f0f0f0'}) term.loadAddon(fitPlugin) term.open(el.current) fitPlugin.fit() term.write('\x1b[36m### WebSocket connecting ...\x1b[0m') const resize = () => fitPlugin.fit(); window.addEventListener('resize', resize) setTerm(term) return () => window.removeEventListener('resize', resize); // eslint-disable-next-line react-hooks/exhaustive-deps }, [])
父子组件的生命周期
笔者创建父子两个组件,加入常见的生命周期方法(如:componentDidMount、shouldComponentUpdate、componentDidUpdate、componentWillUnmount、componentDidCatch),功能很简单,子组件的文案来自父组件的 state,父组件的 state.text 能被用户通过 input 修改。
代码如下:
<script type="text/babel"> class ChildComponent extends React.Component { static propTypes = { name: PropTypes.string }; static defaultProps = (function () { console.log("ChildComponent : defaultProps。孩子的 defaultProps 是"); return {}; })(); constructor(props) { super(props); console.log("ChildComponent: state。孩子的 state 是"); this.state = { text: "peng" }; } // 组件挂载后(插入 DOM 树中)立即调用。常做网络请求 componentDidMount() { console.log("ChildComponent : componentDidMount"); } shouldComponentUpdate(nextProps, nextState) { console.log("<ChildComponent/> - shouldComponentUpdate()"); return true; } // React 更新 dom 和 refs 后调用 componentDidUpdate(previousProps, previousState) { console.log("ChildComponent: componentDidUpdate"); } componentWillUnmount() { console.log("ChildComponent: componentWillUnmount"); } render() { if (this.props.content === 'jiali12') { throw new Error("模拟组件报错"); } console.log("ChildComponent: render"); // 下面两种写法等效。一个是单根一个是多根。 // return <div className="subClass"> // sub // <p>{this.props.content}</p> // </div> // 下面这种数组的写法需要给每个元素添加 key,否则报错如下: // Warning: Each child in a list should have a unique "key" prop. // React 要求每个被迭代项传递一个 key,对于 render() 方法返回的任何数组组件亦如此。 return [ <div key="name" className="subClass">sub</div>, <p key="content">{this.props.content}</p> ] } } class ParentComponent extends React.Component { static defaultProps = (function () { console.log("ParentComponent: defaultProps。我的 defaultProps 是"); return { true: false }; })(); constructor(props) { super(props); console.log("ParentComponent: state。我的 state 是"); this.state = { text: "jiali" }; this.onInputChange = this.onInputChange.bind(this); } onInputChange(e) { const text = e.target.value; this.setState(() => ({ text: text })); } componentDidMount() { console.log("ParentComponent: componentDidMount"); } componentDidUpdate(previousProps, previousState) { console.log("ParentComponent: componentDidUpdate"); } componentWillUnmount() { console.log("ParentComponent: componentWillUnmount"); } // 此生命周期在后代组件抛出错误后被调用 componentDidCatch(err, errorInfo) { console.log("componentDidCatch"); this.setState(() => ({ err, errorInfo })); } render() { if (this.state.err) { return <div>降级处理</div> } console.log("ParentComponent: render"); return <div className="parentClass"> <p>parent</p> <input key="input" value={this.state.text} onChange={this.onInputChange} /> <ChildComponent content={this.state.text} /> </div> } } ReactDOM.render( <ParentComponent />, document.getElementById('root') ); </script>
浏览器中生成的页面结构如下:
<div id="root"> <div class="parentClass"> <p>parent</p> <input value="jiali"> <!-- 子组件 --> <div class="subClass">sub</div> <p>jiali</p> <!-- /子组件 --> </div> </div>
控制台输出:
ChildComponent : defaultProps。孩子的 defaultProps 是
ParentComponent: defaultProps。我的 defaultProps 是
ParentComponent: state。我的 state 是
ParentComponent: render
ChildComponent: state。孩子的 state 是
ChildComponent: render
ChildComponent : componentDidMount
ParentComponent: componentDidMount
Tip:尽管初始 state 和属性并不使用特定的生命周期方法,但他们为组件提供数据方面发挥了重要作用,所以有必要把它们作为生命周期的一部分
此刻是初次渲染,也就是挂载时,根据输出日志我们知道:
- 先 defaultProps 后 state
- 先 render 后 componentDidMount
- 先 render 父组件,后 render 子组件
- 先挂载子组件,后挂载父组件
为何先 render 父组件,又先挂载子组件?
Tip: 其实 vue 也是这样,请看 这里
笔者推测如下:
入口是这里:
ReactDOM.render( <ParentComponent />, document.getElementById('root') );
先渲染父组件,发现里面有子组件,接着渲染子组件。
我们将虚拟 Dom
看做真实 Dom
,虚拟 Dom 如何生成真实 Dom 的我们暂时不去管它,这是 React 做的事情。虚拟 DOM 树的生成过程前文我们已经说了:像一个小孩不停的问“里面是什么?”,”里面是什么?“,直到搞懂所有的子孙组件,就可以形成一颗完整的树。
接着往 input 中输入 1(此刻显示jiali1
),控制台输出:
ParentComponent: render
<ChildComponent/> - shouldComponentUpdate()
ChildComponent: render
ChildComponent: componentDidUpdate
ParentComponent: componentDidUpdate
同样,先执行父组件的 render 再执行子组件的 render,先调用子组件的 componentDidUpdate 再调用父组件的 componentDidUpdate。可以翻译成:
- 要渲染父组件
- 里面有子组件需要渲染
- 子组件更新完毕
- 父组件更新完毕
接着往 input 中输入 2(此刻显示jiali12
),触发子组件 render 里面报错,父组件中的 componentDidCatch 被执行,实现降级处理,页面显示 降级处理
。
到此这篇关于React 的数据流和生命周期的文章就介绍到这了,更多相关React 生命周期内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!