https://juejin.im/post/5ceb36dd51882530be7b1585
一.
对于class component :
- 首先state是Immutable的, setState后一定会生成一个全新的state引用。
- 但是class component是通过this.state的方式读取的,这导致每次代码执行都会拿到最新的state引用。
对于Function component:
- useState产生的数据也是immutable的,通过数组第二个参数set一个新值后, 原来的值会在下次渲染时形成一个新的引用。
- 但由于对state的读取没有通过this.的方式,使得每次setTimeout都读取了当时渲染时闭包环境里的数据,虽然state跟着最新的渲染变了,但旧的渲染里,状态依然是旧值。
function Counter() {
const [count, setCount] = useState();
const log = () => {
setCount(count+1);
setTimeout(() => {
console.log(count);
}, 3000);
};
return (
<div>
<p>you clickeed {count} times</p>
<button onclick={log}>click me</button>
</div>
);
}
打印 0、1、2
二.
useRef: 通过useRef创建的对象,其值只有一份,而且在所有Rerender之间共享。
所以我们对count.current赋值或读取,读到的永远是其最新的值,而与渲染闭包无关。
function Counter() {
const count = useRef(0);
const log = () => {
count.current++;
setTimeout(() => {
console.log(count.current);
}, 3000);
};
return (
<div>
<p>you clicked {count.current} times</p>
<button onClick={log}>click me</button>
</div>
);
}
答案: 3、3、3
三.
useEffect: 使用useRef会改变原始值,为了不改变原始值且达到相同效果,可以使用useEffect。useEffect是处理副作用的,其执行时机是在每次渲染完毕后,实际是在真实dom操作完成后。他的第二个参数是一个数组定义了依赖, 只要所有依赖项不变, useEffect就不会执行, 如果传入[], 则只在初始化时执行一次。 因此我们可以新建一个useRef的值给setTimeout用,其他部分用原始的state count。
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useRef(count);
useEffect(() => {
currentCount.current = count;
}, [count]);
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
}
return (
<div>
<p>you clicked {count.current} times</p>
<button onClick={log}>click me</button>
</div>
);
};
四.
封装下useRef作为一个自定义的hooks
自定义hooks: 以use开头且返回非jsx元素。
function useCurrentValue(value) {
const ref = useRef(0);
useEffect(() => {
ref.current = value;
}, [value]);
return ref;
};
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useCurrentValue(count);
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
}
return (
<div>
<p>you clicked {count.current} times</p>
<button onClick={log}>click me</button>
</div>
);
};
五.
useEffect可以返回一个函数,这个函数是在useEffect下次调用前调用的(比如第二次调用useEffect的时候会先执行return函数)
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
return <h1>{count}</h1>;
}
答案: 123 (每次重新渲染如果count变化就先执行clearInterval, 然后重新实例化一个计时器)
六.
setCount的第二种用法是传入一个函数,参数是最新的count值。
所以如果不想每次都实例化一个新的计时器可以传入回调函数
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c+1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
这样就做到了
- 对依赖诚实(useEffect里的回调方法用到什么state, 就在[]里监控什么state)
- 依赖项是[] , 就只会执行一次useEffect
七.
useReducer: 就是把一堆state放在一起,可以替换 在一个setXXState的回调函数里面想用除了XX以外别的State的情况
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const {count, step} = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({type: 'tick'});
},1000);
return () => clearInterval(id);
}, [dispatch]);
return <h1>{count}</h1>
};
function reducer(state, action) {
switch (action.type) {
case 'tick':
return {
...state,
count: state.count + state.step
};
}
}
对于依赖dispatch,他的引用永远不会变,因此可以忽略他的影响。
八.
为了避免遗漏依赖,尽量将useEffect里用到的函数写在useEffect的内部。
如果想要抽到外部,可以使用useCallback.
把抽出去的函数用useCallback打包一下(打包时需要传入依赖项)得到一个新的函数,这个新的函数要作为useEffect的依赖项放入[]中,然后就可以在useEffect的回调函数利用了。
function Counter() {
const {count, setCount} = useState(0);
const fetchUrl = useCallback(() => {
return 'https://ssssss' + count;
}, [count]);
useEffect(()=>{
const url = fetchUrl();
}, [fetchUrl]);
return <h1>{count}</h1>
}
useEffect对业务的抽象:
- 参数变化,列表变化:依赖项是查询参数,那么只要查询参数变化了,列表就会自动取数刷新。注意我们将取数据时机从触发端改成了接收端。
- 当列表更新后,重新注册一遍拖拽响应事件:依赖参数是列表。只要列表变化,拖拽响应就会重新初始化。
- 只要数据流某个数据变化,标题就同步修改
九.
可以利用自定义hooks,将一个功能抽到整个组件的外部
但是如果抽象出的hooks重新执行成本很高,依赖又不是中心功能所需要的(比如只是打日志), 可以通过将依赖转化成ref
十.
把依赖绑在ref上,这样依赖不会变,却能每次都拿到新值: 也就是多加一个useEffect把传进来的state绑在ref上,useCallback的依赖使用ref。 但是官方还是推荐使用reducer
十一.
render优化可以使用memo(props => {useEffect…; return (//…)}); 等价于pureComponent。
useMemo可做局部的PureRender
return useMemo(() => {
//...
},[])
十二.
可以使用Context做批量透传
const store = createContext(null);
function Parent() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const fetchData = useFetch(count, step);
return (
<Store.Provider value={{setCount, setStep, fetchData}}>
<Child />
</Store.Provider>
)
}
const Child = useMemo( props => {
const dispatch = useContext(Store);
function onClick(){
dispatch({
type: 'countInc'
});
}
return (//...);
});
也可以将state通过Provider注入,但会有性能问题,需要注意:
如果有多个子组件使用了state, 那么任意一个set某state的操作都会让使用state的子组件rerender。这是因为即使使用了memo包裹子组件,memo也只能挡在外层,而通过useContext的数据注入发生在函数内部,会绕过memo,所以当出发dispatch导致state变化时,所有使用了state的组件内部都会强制重新刷新。要想对渲染次数优化,可以用useMemo, useMemo的第一个参数是原子组件返回的jsx,第二个参数是jsx依赖项的数组。
如果使用useContext而没有用props, 就可以使用useMemo代替memo做性能优化。
十三.
异步操作: 写在useEffect里。可封装成一个自定义hook:
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({type: 'FETCH_INIT});
try {
const result = await axios(url);
if(!didCancel) {
dispatch({type: 'FETCH_SUCCESS', payload: result.data});
}
} catch (error) {
if(didCancel) {
dispatch({type: 'FETCH_FAILURE'});
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
const doFetch = url => setUrl(url);
return { ...state, doFetch };
}
function App() {
const { data, isLoading, isError } = useDataApi("http://v", {
showLog: true
});
useEffect(() => {
dispatch({
type: 'updateLoading',
data,
isLoading,
isError
});
}, [dispatch, data, isLoading, isError]);
}
十四.
Child.defaultProps = {};
十五.
给子组件传对象的时候,为了避免对象的引用不断变化,可以将对象挂在ref上: const scheme = useRef({b: 1});