- 因为
没有依赖
,是无法触发页面的unmount
和componentDidUpdate
的, 只会在componentDidMount
时运行一次 - 同样的值传入
setState
多次,只有前两次会引起re-render; - 为什么
count
的值一直是0
?- 每次渲染都是一次
snapshot
,会有自己state
的一个version
- 因为
没有依赖
,所以就没有unmount
的发生,所以clean
不会发生 clean
没有发生,全局就只
有一次setInterval
,会把count闭包
闭进去- 闭包是闭了
count
这个变量
,而不是
它的值
- 但是
count
这个变量永远停留
在当时渲染的那个状态
(Immutable) - 所以
setInterval
中读到的count
永远都是0
.
- 每次渲染都是一次
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
console.log('render');
useEffect(() => {
console.log('componentDidUpdate')
const id = setInterval(() => {
console.log(count)
setCount(count + 1)
}, 1000);
return () => {
clearInterval(id); // 只会在页面退出时执行
console.log('clean');
}
}, []); // 是无法触发页面的unmount和componentDidUpdate的, 只会在componentDidMount时运行一次
return (
<h1>
{count}
</h1>
)
}
- 如果传入
count
,会发生什么?- 因为设置了
count
为dependency
, 每次count
的变动都会引起unmount
,re-render
和useEffect
- 即clean上一次
setInterval
, 重新渲染(更新count
的值), 和重新setInterval
(闭包闭了新的count
) - 因此
表现起来
会很正常
- 因为设置了
- 传入useEffect的dependencies:
- state: 如果useEffect中又会引起state的变化,将会陷入无限循环当中
- function:
- 如果
function
定义在useEffect
里面,则无需添加到依赖中 - 如果
function
定义在component
里面,将会陷入无限循环的渲染当中, 因为function
每次渲染-引用都会发生变化 - 如果
function
定义在component
外面,则只会渲染一次 - 如果必须把
function
定义在component
里面,但是又不想无限循环,则在使用useCallback
或者useMemo
- 如果
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
console.log('render');
useEffect(() => {
console.log('componentDidUpdate')
const id = setInterval(() => {
console.log(count)
setCount(count + 1)
}, 1000);
return () => {
clearInterval(id);
console.log('clean');
}
}, [count]); // 每次变化将会触发unmount和componentDidUpdate
return (
<h1>
{count}
</h1>
)
}
export default Counter;
Solution 1
有什么问题?- 按照直觉,状态的更新引起页面的重新渲染我们可以理解
- 但是状态的更新却同时引起了页面的
unmount
和componentDidUpdate
, 其实是没有必要的过程 - 我们希望只有一个
setInterval
,但是页面依然会根据状态变化而重新熏染
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
console.log('render');
useEffect(() => {
console.log('componentDidUpdate')
const id = setInterval(() => {
setCount(count => count + 1); // 仅仅描述了一种行为,每次执行时都会自动读取当时最新count值
}, 1000);
return () => {
clearInterval(id);
console.log('clean');
}
}, []); // 是无法触发页面的unmount和componentDidUpdate的
return (
<h1>
{count}
</h1>
)
}
export default Counter;
- 这种情况适用于: state相互之间存在依赖,需要传入多个进入
useEffect
的dependency
- 建议: 能把
initialState
和reducer
都放在component
外面的,就尽量放在外面 - 因为每次渲染都会形成新的
initialState
和reducer
, 是没有必要的 - 你会发现这里
state
的变化也不会引起unmount
的发生
import React, { useEffect, useReducer } from 'react';
const initialState = { count: 0, step: 1 };
const reducer = (state, action) => {
switch (action.type) {
case 'TICK': return { ...state,
count: state.count + state.step };
case 'STEP': return { ...state,
count: state.count + state.step + action.payload,
step: state.step + action.payload }
default: throw new Error();
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
console.log('render');
useEffect(() => {
console.log('componentDidUpdate')
const id = setInterval(() => {
dispatch({ type: 'TICK' }); // 读取上一次最新的state交给了userReducer去完成
}, 1000);
return () => {
clearInterval(id);
console.log('clean');
}
}, []); // 是无法触发页面的unmount和componentDidUpdate的
return (
<h1>
{state.count} <br/>
<button onClick={() => { dispatch({ type: 'STEP', payload: 1 }) }}>Step Up</button>
</h1>
)
}
export default Counter;
- 这种情形跟
case 1
类似:- 如果不将
step
例如dependency
, 那么不会触发unmount
,setInterval
全局只有一个,闭包闭的step
的值一直停留在最初的值上面,因为props
的变化并不能引起count
的变化 - 如果将
step
列入dependency
, 将会触发unmount
,setInterval
一直会clean
和重建
,读取到的props也是最新的,但是有一定性能的损耗
.
- 如果不将
import React, { useEffect, useReducer } from 'react';
const initialState = { count: 0 };
const reducer = (state, action) => {
switch (action.type) {
case 'TICK': return { ...state, count: state.count + action.step };
default: throw new Error();
}
}
const Counter = (props: any) => {
const { step } = props;
const [state, dispatch] = useReducer(reducer, initialState);
console.log('render');
useEffect(() => {
console.log('componentDidUpdate')
const id = setInterval(() => {
dispatch({ type: 'TICK', step }); // 读取上一次最新的state交给了userReducer去完成
}, 1000);
return () => {
clearInterval(id);
console.log('clean');
}
}, [step]); // react 保证dispatch在每次渲染中都是一样的
return (
<h1>
{state.count} <br />
{/* <button onClick={() => { dispatch({ type: 'STEP', payload: step }) }}>Step Up</button> */}
</h1>
)
}
export default Counter;
- 因为
dependency
为[]
, 因此全局只有一个setInterval
props
的更新本身就会引起重新渲染
,因此reducer
里面读到的永远是最新的props
import React, { useEffect, useReducer } from 'react';
const initialState = { count: 0 };
const Counter = (props: any) => {
const { step } = props;
const reducer = (state, action) => {
switch (action.type) {
case 'TICK': return { ...state, count: state.count + step };
default: throw new Error();
}
}
const [state, dispatch] = useReducer(reducer, initialState);
console.log('render');
useEffect(() => {
console.log('componentDidUpdate')
const id = setInterval(() => {
dispatch({ type: 'TICK' });
}, 1000);
return () => {
clearInterval(id);
console.log('clean');
}
}, []);
return (
<h1>
{state.count} <br />
</h1>
)
}
export default Counter;
- 特征:
函数
本身的值不会发生变化(stable)- 函数本身
未引用
任何外部变量,
useEffect
中引用外部
的变量(仅仅是变量不包括函数)会被提醒
要加入到dependency
中- 所以这里的
fetchData
并未被要求加入到dep
s中 - 因为
fetchData
定义是在component
内部,useEffect
的外部,如果加入到dep
s中,一定会引起页面的无限刷新
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const SearchResult = () => {
const [data, setData] = useState({ hits: [] });
const fetchData = async () => {
const result = await axios('https://hn.algolia.com/api/v1/search?query=react')
setData(result.data);
}
useEffect(() => {
// 将fetchData放到这里其实效果一样,性能也基本上没有差异
fetchData(); // 未引用任何变量,所以不会被提醒加入到deps中
}, []) // 如果把函数加入到deps中,反而会造成页面的无限循环
return (
<div>{data.hits.map((item: any) => (
<div>{item.title}</div>
))}</div>
)
}
export default SearchResult;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const SearchResult = () => {
const [query, setQuery] = useState('react');
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => { // define inside useEffect
const result = await axios('https://hn.algolia.com/api/v1/search?query='+ query);
setData(result.data);
}
fetchData();
}, [query]) // add external variables here
return (
<div>
<ul>
{data.hits.map((item: any) => (
<li key={item.objectID}>{item.title}</li>
))}
</ul>
{query}
</div>
)
}
export default SearchResult;
- 特征:
- 如果有一个函数必须放到useEffect之外,例如想要在多个useEffect中复用这个函数
- 该函数要依赖外部变量时
- 解决:
- useCallback的使用能够使得函数在每次页面渲染时候保持稳定,因此不会引起页面的再次渲染
- useCallback的本质其实是将原本定义在useEffect中的函数分离出来的一层,最终还是依赖于变量的
- 问题:
- 只是'缓存'了变量,并未缓存api请求的结果,可以进一步优化
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
const SearchResult = () => {
const [query, setQuery] = useState('react');
const [data1, setData1] = useState({ hits: [] });
const [data2, setData2] = useState({ hits: [] });
const MemorizedGetFetchUrl = useCallback(() => 'https://hn.algolia.com/api/v1/search?query=' + query, [query]);
useEffect(() => {
const fetchData = async () => {
const result = await axios(MemorizedGetFetchUrl());
setData1(result.data);
}
fetchData();
}, [MemorizedGetFetchUrl])
useEffect(() => {
const fetchData = async () => {
const result = await axios(MemorizedGetFetchUrl())
setData2(result.data);
}
fetchData()
}, [MemorizedGetFetchUrl])
return (
<div>
<ul>
{data1.hits.map((item: any) => (
<li key={item.objectID}>{item.title}</li>
))}
</ul>
<br />
<ul>
{data2.hits.map((item: any) => (
<li key={item.objectID}>{item.title}</li>
))}
</ul>
</div>
)
}
export default SearchResult;
- 这里优化,本质上是因为deps的缘故,只有在
query
改变时才会进行api请求
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
const SearchResult = () => {
const [query1, setQuery1] = useState('react');
const [query2, setQuery2] = useState('redux');
const [refresh, setRefresh] = useState(0)
const [data1, setData1] = useState({ hits: [] });
const [data2, setData2] = useState({ hits: [] });
const MemorizedGetFetchUrl = useCallback((query, id) => {
const fetchData = async () => {
const result = await axios('https://hn.algolia.com/api/v1/search?query=' + query);
if (id === 1) setData1(result.data);
if (id === 2) setData2(result.data);
}
fetchData()
}, []);
useEffect(() => {
MemorizedGetFetchUrl(query1, 1);
}, [MemorizedGetFetchUrl, query1])
useEffect(() => {
MemorizedGetFetchUrl(query2, 2);
}, [MemorizedGetFetchUrl, query2])
return (
<div>
<ul>
{data1.hits.map((item: any) => (
<li key={item.objectID}>{item.title}</li>
))}
</ul>
<br />
<ul>
{data2.hits.map((item: any) => (
<li key={item.objectID}>{item.title}</li>
))}
</ul>
<button onClick={() => { setRefresh(refresh + 1) }}>Re-render Page</button>
</div>
)
}
export default SearchResult;
- 何时需要
cleanup
函数?- useEffect存在
deps
且deps
会变化的时候 - 例如在
useEffect
中的api请求依赖于某个变量
。- 因为无法保证api请求的返回顺序,我们需要在新的请求之前,禁止上一次请求的结果对页面元素产生影响!
- useEffect存在
- 代码分析:
- 本质原因是js的
闭包
,闭包闭的variable而不是值 - 因此在第一次
useEffect
执行未完成,但是unmount
已经触发的情况下,第一次·的值会被修改为· - 这时候
setData
的操作就不会被出发
- 本质原因是js的
const SearchResult = () => {
const [query, setQuery] = useState('');
const [search, setSearch] = useState('')
const [data, setData] = useState({ hits: [] });
const MemorizedGetFetchUrl = useCallback((didCancel) => {
const fetchData = async () => {
const result = await axios('https://hn.algolia.com/api/v1/search?query=' + search);
!didCancel && setData(result.data);
!!didCancel && console.log('cancel setting result');
}
search !== '' && fetchData()
}, [search]);
useEffect(() => {
let didCancel = false;
!didCancel && MemorizedGetFetchUrl(didCancel);
return () => {
didCancel = true;
}
}, [MemorizedGetFetchUrl])
return (
<div>
<ul>
{data.hits.map((item: any) => (
<li key={item.objectID}>{item.title}</li>
))}
</ul>
<input type="text" value={query} onChange={evt => setQuery(evt.target.value)} />
<button onClick={() => setSearch(query)}>Search</button>
</div>
)
}
export default SearchResult;
useCallback
可以直接传参数,而useMemo
不能直接
传参数, 但是二者都可以直接读取定义在文件内的变量
useMemo
重点在与缓存了昂贵计算
函数运行的结果,而useCallback
的重点在于缓存了函数本身; 使用useCallback无法达到缓存昂贵计算的结果;useCallback
中可以进行api
请求,而useMemo
内部传入函数时只适合做用在渲染过程
中的昂贵计算上,比如重交互的图表和动画等- 结论: 单纯为了持久化函数或者多个
useEffect
函数复用一个函数,使用useCallback
; 为了能够减少昂贵计算的次数,使用useMemo
useEffect
依赖的变动会引起unmount
render
componentDidUpdate
- 什么会引起
componentWillUnmount
?- 页面
退出
useEffect
中依赖值
的变动
- 页面
- setState会引起什么?
render
componentDidUpdate
- 不会引起
unmount
- props的改变会引起什么?
- 子组件的
render
,如果是不必要的,可以使用memo + shallowEqual的方式避免子组件的重新渲染 - 子组件的
componentDidUpdate
- 不会引起
unmount
- 不会引起子组件state的reset
- 子组件的
import React, { useState, memo } from "react";
import shallowCompare from "react-addons-shallow-compare";
const App = () => {
const [a, setA] = useState(0);
console.log("app render");
return (
<div>
<div>Hello World</div>
{a}
<button onClick={() => setA(a => a + 1)}>change app</button>
<Counter a={a} />
</div>
);
};
const Counter = memo(({ a }: any) => {
const [b, setB] = useState(a);
console.log("counter render");
return (
<div>
{b}
<button onClick={() => setB(b => b + 1)}>change counter</button>
</div>
);
}, shallowCompare);
export default App;
- 为什么要减少dependency?
- 为了减少dependency变化带来的
componentWillUnmount
的执行
- 为了减少dependency变化带来的
- useEffect会提醒你把什么东西加入到deps中?
- 外部变量
- 包含外部变量的
函数
- 不提醒你把不包含外部变量的
函数
加入
- 什么样的变量适合放入
state
中?- 用来沟通
html
和js
的变量
- 用来沟通
- 定义在
hooks
之外,组件之内的变量和函数有什么特征?- 在每次渲染后,
引用
都会发生改变
- 在每次渲染后,
- 经验之谈:
- 放在
deps
里面的不是props
就是state
, 共同特征: 会引起页面的重新渲染 - 因为
component
中的hooks
之外的变量和函数都是不稳定的,会引起无限渲染 - 如果要放在
component
之外,说明不需要被复用,其实可以直接放入到hooks
里面 - 如果没办法放在component之内,有必须在hooks之外的(复用),则需要使用
useCallback
,useMemo
来原始化
- 放在
- 本质: hooks的本质就是在immutable中使用mutable
- useReducer vs useState: 前者具备了数据在上一次和此次沟通的能力,也便利了状态之间的沟通