- 因为
没有依赖,是无法触发页面的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并未被要求加入到deps中 - 因为
fetchData定义是在component内部,useEffect的外部,如果加入到deps中,一定会引起页面的无限刷新
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依赖的变动会引起unmountrendercomponentDidUpdate
- 什么会引起
componentWillUnmount?- 页面
退出 useEffect中依赖值的变动
- 页面
- setState会引起什么?
rendercomponentDidUpdate- 不会引起
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: 前者具备了数据在上一次和此次沟通的能力,也便利了状态之间的沟通
