前言:用
create-react-app
脚手架搭建react
项目,跟着视频走,发现对不上,折腾了半天,发现两天前已经更新了react18
,所以脚手架默认直接搭建的就是18版本的,结果初次挂载会把组件运行两次,我以为是bug
,就去提issue
,结果回复说是新特性
如上文所述,直接脚手架搭建项目,其中场景会用到fetch
异步请求一些数据,然后通过函数式组件响应到页面,在试图找到问题所在的前提下,进行简化,先用同步代码测试,已经发现会重复调用函数体。但是在react 17
以及之前的版本就不会出现这种情况,所以我以为是bug,找了好久,因为是两天前才更新的18.1.0,网上也没讲这个的帖子,就去提了issue。
issue
地址:https://github.com/facebook/react/issues/24467 (随后被告知是新特性,已被关闭)
再现步骤:
create-react-app test-render --template typescript
useEffect( )
in the function before return, such as console.log('app')
in the callback functionnpm run start
再现测试代码 Link to code example: github.com/voiceu-zuix…
得到的结果:发现不管是组件函数体内还是useEffect
内,都运行了两边
根据issue中的回复链接,找到了如下文档资料,基本都是官方网站或者官方github
帖
资料 1:reactjs.org/blog/2022/0…
资料 2:github.com/reactwg/rea…
资料 3:github.com/reactwg/rea…
资料 4.1:zh-hans.reactjs.org/docs/strict…
资料 4.2(资料 1 的中文文档片段):zh-hans.reactjs.org/docs/strict…
资料 5(问题原因所在):zh-hans.reactjs.org/docs/strict…
总的来说,是入口文件index.tsx
的
导致,新特性在 资料5 中提到如下:
渲染阶段的生命周期包括以下 class 组件方法:
constructor
componentWillMount
(or UNSAFE_componentWillMount
)——这个导致我项目中componentWillReceiveProps
(or UNSAFE_componentWillReceiveProps
)componentWillUpdate
(or UNSAFE_componentWillUpdate
)getDerivedStateFromProps
shouldComponentUpdate
render
setState
更新函数(第一个参数)因为上述方法可能会被多次调用,所以不要在它们内部编写副作用相关的代码,这点非常重要。忽略此规则可能会导致各种问题的产生,包括内存泄漏和或出现无效的应用程序状态。不幸的是,这些问题很难被发现,因为它们通常具有非确定性。
严格模式
不能自动检测到你的副作用,但它可以帮助你发现它们,使它们更具确定性。通过故意两次调用(double-invoking) 以下函数来实现的该操作:
我的项目简化的代码如下,整体意思是:调用Test
组件函数体就会打印Test
,调用useEffect
就会打印res
import { useEffect, useState } from 'react'
const apiUrl = process.env.REACT_APP_API_URL
export const Test = () => {
const [param, setParam] = useState({ name: '' })
console.log('Test')
useEffect(() => {
fetch(`${apiUrl}/projects?${param}`).then(async (response) => {
if (response.ok) {
let res = await response.json()
console.log('res', res)
setParam(res)
}
})
console.log('useEffect')
}, [])
return 我是Test
}
打印结果:
结合上面的答案,可以知道我的代码,虽然是函数式组件,但是如果是类式组件构建的话,就会经历上面的一系列生命周期,在严格模式
下,这些周期方法就会被多次调用,但是却是函数式组件,所以React模拟这种情况,就多次调用函数体组件和setState
,具体多少次,按官方说的,应该是两次,但是却出现了不一样的情况,这一点我也有测试,多数情况是两次,少数情况不一致,可能是由于我的异步请求的原因吧,实在是找不到原因。
也出现过如下情况
为了看清楚这个步骤,我尝试了在外部加一个变量,渲染一次后就+1
import { useEffect, useState } from 'react'
const apiUrl = process.env.REACT_APP_API_URL
let n = 1
export const Test = () => {
const [param, setParam] = useState({ name: '' })
console.log('Test', n)
useEffect(() => {
fetch(`${apiUrl}/projects?${param}`).then(async (response) => {
if (response.ok) {
let res = await response.json()
console.log('res', res)
setParam(res)
}
})
console.log('useEffect', n)
}, [])
n += 1
console.log('n',n)
return 我是Test
}
结果如下:
useState
hook,其余的规则与此代码无关useEffect
,第二个参数是空数组,模拟的是componentDidMount
生命周期,是提交阶段,不受影响具体流程如下:
componentDidMount
生命周期useEffect
,所以n都是3第一个useEffect
内部调用useState
的useParam
,所以会两次调用该函数,该函数会导致页面出现渲染,所以函数组件体又被两次调用
第二个useEffect
同理。
如果是类式组件的话,componentDidMount
函数应该不会被调用两次,因为是函数式组件的整体调用,才让useEffect
被迫调用两次,下面进行测试
import React, { Component } from 'react'
let number_outside = 1
export default class TestClass extends Component {
constructor() {
super()
console.log('constructor', number_outside)
this.state = {
param: {
name: '',
num: 10
}
}
}
componentDidMount() {
this.setState({ param: { num: this.state.param.num + 1 } })
console.log('didMount-同步', number_outside, this.state.param.num)
}
onClick = () => {
this.setState({ param: { name: '22' } })
}
componentDidUpdate() {
console.log('didUpdate-同步', number_outside,this.state.param.num)
}
render() {
number_outside += 1
console.log('Test', number_outside)
return test-class
}
}
结果如下,componentDidMount
函数被调用两次,但是内部的setState
并没有生效两次,只生效一次,很费解,不过测试了所有打包后的生产环境,均无这些情况,很正常。
但是这种开发模式下的多次调用的情况,导致我想进行打印输出来判断哪里出了bug,可能会出现大问题,不太想用这种模式,看到了可以通过ref
来解决
const didLogRef = useRef(false);
useEffect(() => {
// In this case, whether we are mounting or remounting,
// we use a ref so that we only log an impression once.
if (didLogRef.current === false) {
didLogRef.current = true;
SomeTrackingAPI.logImpression();
}
}, []);
尽量别在上面提到的函数内部写effect
相关的代码吧,我在想要是有状态库,这些反复调用的话,会不会把状态反复操作了,比如+1
,在测试里是通过state
来进行,确实没有出问题,但是函数式组件例子里在外部的一个变量n却实实在在的改变了,因为函数体和useState
的存在,还不止改变了2次,所以担心redux
这些会不会有影响,比如我在刚刚的例子里不是操作n+1
,而是想在初次渲染就让redux
里的某个值+1
,这在开发环境下,怕不是直接加了好几次。
确实在测试中componentDidMount
也会跑两次,但是componentDidUpdate
只跑一次,而且两者都没被写进那个list里面,然后我就又去那个issue底下问了一下,回复我如下
所以,还是不要纠结这个了,估计后面的版本会陆续完善,毕竟18才刚出,他说他也觉得应该把这个加进docs去,后面看看会不会加吧
然后马上又回复了,效率是真高,
说已经委婉的提出了,但是没有明说,我们可以把这个放到具体的提示里面
But we can explicitly list what APIs are included in this to help discoverability (e.g. by using STRG+F “componentDidMount”). Opened reactjs/reactjs.org#4618 for that.
github.com/reactjs/rea…
github.com/reactjs/rea…
尝试了不用严格模式,直接写render(
,这样hook确实不会被重复调用,但是useState还是会把函数组件体外部的逻辑走一遍,会绕开hook,不懂为什么。
类式组件在不是严格模式下不会有问题,因为类式组件想要在内部写一点渲染就能运行的逻辑,必须是在生命周期钩子中,比如constructor,render这些函数里,些其他函数只能是定义,而无法运行,但是函数式组件就可以直接写,因此感觉最好是除了hook这些函数,就避免写直接运行的函数,毕竟是组件,还是写一些函数的定义,在触发的时候运行,这样就不会有bug了
所以react 17
的时候严格模式不会render2遍,react 18
会,并且尽量不要在函数式组件里写直接运行的函数逻辑,最好是定义函数 xxx=()=>{ ? ? ? }
,而不是写 xxx()
,不用严格模式就好
import ReactDOM from 'react-dom/client'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
// 之前的代码都是直接包裹路由,也没用严格模式,问题不大,只是少一些提醒
// react 17的时候严格模式不会render2遍,react 18会
//不用严格模式
//
//
//
)