生命周期的概念在各个领域中都广泛存在,广义来说生命周期泛指自然界和人类社会中各种客观事物的阶段性变化及其规律,在React框架中则用来描述组件挂载(创建)、更新(存在)、卸载(销毁)3个阶段。生命周期的每个阶段总是伴随着一些方法的调用,这些方法就是生命周期的钩子函数,它们为开发人员在不同阶段操作组件提供了执行时机。
虽然现在最新的React版本都推荐使用函数组件结合Hooks的方式来组织应用,但是对类组件生命周期的理解能够帮助我们以追根溯源的方式,更全面地建立对React框架的认知,帮助我们编写正确且高效的代码。
当class组件实例被创建并插入DOM中时,其生命周期函数调用顺序如下:
l constructor():在React组件挂载之前,会调用该构造函数。此函数只会执行一次,并返回一个组件实例。在构造函数中,我们可以对this.state进行赋值,以完成组件的初始化工作。其他生命周期函数只能通过?his.setState修改state,不能直接为this.state赋值。
l componentWillMount():组件将要挂载时调用该函数。官方在React 16.3版本中不建议使用该函数,在v17.0版本中移除该函数。
l getDerivedStateFromProps():它是一个静态方法,接收props和state两个参数。它会在调用render()函数之前被调用,并且不管是在初始挂载时还是在后续组件更新时都会被调用。这个钩子函数在React 16.3版本之后出现,可以作为componentWillMount、componentWillUpdate和componentWillReceiveProps的替代方案。此生命周期钩子不常用,如果可以的话,我们也尽可能不使用它。
l render():是class组件中唯一必须实现的函数,它的返回值将作为页面渲染的视图。render函数应该为纯函数,也就是对于相同的state和props,它总是返回相同的渲染结果。
l componentDidMount():在组件挂载(插入DOM树中)后,且在浏览器更新视图之前调用,只会执行一次。一般在这个生命周期函数中发送网络请求、添加订阅等。
每当组件的state或props发生变化时,组件就会更新。组件更新的生命周期函数调用顺序如下:
shouldComponentUpdate():当props或state发生变化时,shouldComponentUpdate()会在渲染执行之前被调用。
getSnapshotBeforeUpdate():在最近一次渲染输出(提交到 DOM节点)之前调用。
componentDidUpdate():在组件更新后会被立即调用。
当组件从DOM中移除时会调用如下方法:
componentWillUnmount():在组件卸载及销毁之前直接调用。
Hooks API的出现可以帮助函数式组件获得生命周期函数。Hooks API比较简单,本书案例计算机选购配置系统的项目工程名software-labs-client采用的就是React Hooks组件的写法。可以利用useState和useEffect()这两个函数来模拟实现生命周期。
函数组件没有构造函数constructor,可以通过调用useState来初始化state,代码如下:
const [num, UpdateNum] = useState(0)
函数式组件中的componentDidMount、componentDidUpdate和componentWillUnmount使用useEffect实现,使用方法如下:
方法1:
useEffect(()=>{
}, [])
上面的代码使用useEffect函数,当第二个参数是空数组时,第一个函数中执行的代码相当于class组件在componentDidMount中执行的内容。
方法2:
useEffect(() => {
}, [count]);
上面的代码使用useEffect函数,当count发生变化时,执行第一个函数,相当于class组件在componentDidMount中执行的内容,以及count更改时componentDidUpdate执行的内容。
方法3:
01 useEffect(()=>{
02 //code here
03 return function cleanup() {
04 //code here
05 }
06 }, [])
上面的代码中,第02行相当于class组件在componentDidMount中执行的内容,第04行相当于class组件在componentWillUnmount中执行的内容。
在software-labs-client工程中,打开src/features/bar/components/Step1.js,代码如下:
01 import React, { useEffect } from 'react';
02 import { useSelector, useDispatch } from 'react-redux';
03 import _ from 'lodash';
04 import { getSoftwareLists, getLocales } from '../actions';
05 import { selectPreferences} from '../../wrappers/selector';
06 import { selectLocales } from '../selector';
07 import { getBrowserLocal } from '../../../help/utils';
08
09 const Step1 = () => {
10 const preferences = useSelector(selectPreferences);
11 const countryCode = preferences.countryCode;
12 const region = preferences.region;
13 const locales = useSelector(selectLocales);
14 const dispatch = useDispatch();
15
16 useEffect(() => {
17 if(!_.isEmpty(locales)){
18 return;
19 }
20 dispatch(getLocales());
21 }, [])
22
23 useEffect(() => {
24 if (!_.isEmpty(locales) && !region) {
25 const {countryCode} = getBrowserLocal();
26 dispatch(getSoftwareLists({ countryCode }));
27 }
28 }, [locales])
29
30 const setRegion = (value) => { };
31
32 const changeCountry = (value) => { };
33
34 const changeLanguage = (value) => { };
35
36 return (
37 <div className="stepOneWrap"></div>
38 );
39 }
40
41 export default Step1;
代码解析:
l 第01行引入React和useEffect。
l 第02行从React Redux中引入useSelector和useDispatch,分别用于从Redux的store中获取数据和发送action。
l 第03行引入lodash工具库。
l 第04行从actions.js文件中引入getSoftwareLists和getLocales函数。通常,actions.js文件中的函数都是发起action的函数,有同步和异步action。异步action指发起ajax请求,从服务器端获取数据。
l 第05行从selector文件中通过selectPreferences获取preferences数据,这个数据在software-labs-client项目中是指用户的国家、语言、地区等数据。
l 第06行是从selector文件中通过selectLocales函数获取locales数据。locales数据是一个由很多国家、语言、国家编码、语言编码组成的对象集合,用于在页面中用下拉列表展示区域、国家和语言。
l 第07行从help/utils.js工具函数文件中引入getBrowserLocal,这个函数用于获取用户当前浏览器的国家和语言信息,并初始化页面上展示的区域、国家和语言。
l 第09~39行是Step1组件的内容。这个组件就是页面上的区域、国家和语言的展示。选择区域时,国家列表更新响应区域下的国家,例如区域是北美,国家列表中就是加拿大和美国,切换区域时,国家列表更新。当选择国家时,当前国家的语言列表更新,例如选择法国时,语言列表中就是法语和英语。
l 第09行定义了组件的名字,组件名字的首字母要大写。
l 第10~13行是从Redux的store中取出组件渲染或代码逻辑需要的数据,例如preferences和locales。
l 第14行使用hook函数useDispatch得到dispatch方法。该方法用于发送action。
l 第16~21行是useEffect函数的使用。在这个useEffect函数中,第一个参数是空数组,意味着useEffect函数的第一个回调函数会在组件挂载后、浏览器渲染页面之前执行,相当于class组件的生命周期钩子函数componentDidMount的执行时机。可以看到,第17~19行进行了判断,如果locales不为空,也就是说locales已经有数据,则return,即不执行后面的代码,否则执行第20行代码,dispatch(getLocales()),即发送ajax请求,从服务器端取locales数据。
l 第23~28行也是useEffect的使用。这里第一个参数是locales,意味着当locales值发生变化时,执行该useEffect的第一个回调函数。
l 第24~27行是指当locales和region都不为空时,发起ajax请求,即执行dispatch(getSoftwareLists({ countryCode })),从服务器端获取softwares列表数据。该列表数据用于渲染页面上的软件列表。
l 第36~38行是返回JSX语法编写的组件DOM。JSX的DOM最外层必须只有一个父级,中间的内容此处省略。
l 第41行导出Step1组件。
Step1组件如图5.9所示,getLocales获取的数据用于渲染国家和语言列表,当国家和语言变更后,
调用getSoftwareLists重新获取软件列表数据。preferences中保存的数据用于渲染界面,展示区域、国家和语言。
打开src/features/wrappers/index.js,文件中定义了Wrappers组件。该组件中也使用了useEffect钩子函数,部分代码如下:
01 import React, { useState, useEffect } from 'react';
02 import { useSelector, useDispatch } from 'react-redux';
03 import {Outlet} from "react-router-dom";
04 import {IntlProvider} from 'react-intl';
05 import { ConfigProvider, message } from 'antd';
06 import { selectError, selectPreferences } from './selector';
07 import {getBrowserLocal, getLocaleMessages} from '../../help/utils';
08 import localeData from '../../help/localeData';
09 import {FormattedMessage} from 'react-intl';
10
11 const Wrappers = (props) => {
12 const [messageApi, contextHolder] = message.useMessage();
13 const preferences = useSelector(selectPreferences);
14 const error = useSelector(selectError);
15 const {languageCode} = getBrowserLocal();
16 const locale = preferences.languageCode || languageCode;
17 const antdLocale = localeData[locale];
18 const messages = getLocaleMessages(locale);
19 useEffect(() => {
20 if(error){
21 messageApi.open({
22 type: 'error',
23 content: <FormattedMessage id={error}/>,
24 });
25 }
26 }, [error])
27
28 return (
29 ...
30 );
31 }
32 export default Wrappers;
代码解析:
l 第19~26行使用useEffect方法,当error发生变化时,传入useEffect的第一个参数,该参数是一个匿名函数。在该函数中进行判断,如果error数据存在,则执行第21~24行的代码。
l 第21~24行调用antd框架的messageApi.open方法,表示打开一个错误提示弹窗,弹窗中显示的内容为error。
本节选自《React.js+Node.js+MongoDB企业级全栈开发实践》,获出版社和作者授权共享。