useState:
//这是一个全局变量,用来记录hook的值
let hookState = [];
//存放当前hook的索引值
let hookIndex = 0;
export function useState(initialState){
hookState[hookIndex]=hookState[hookIndex]||initialState;//hookState[0]=10
let currentIndex = hookIndex;
function setState(newState){
hookState[currentIndex]=newState;//currentIndex指向hookIndex赋值的时候的那个值 0
scheduleUpdate();//状态变化后,要执行调度更新任务
}
return [hookState[hookIndex++],setState];
}
let scheduleUpdate;
function render(vdom, parentDOM) {
mount(vdom, parentDOM);
//在React里不管在哪里触发的更新,真正的调度都是从根节点开始的
scheduleUpdate = ()=>{
hookIndex = 0;//把索引重置为0
//从根节点执行完整的dom-diff 进行组件的更新
compareTwoVdom(parentDOM,vdom,vdom);
}
}
注:
1、第一次render完后,hookState存储了从根节点到各级最底层子节点所有的状态
2、每触发任何一次更新,都会从根节点开始更新,典型的场合是请求成功后、事件回调里setXXX了一堆钩子,然后就会走到scheduleUpdate,重置hookIndex到0,再通过compareTwoVdom对比dom进行更新
3、更新时hookState里已经存了所有索引对应的state了,所以走到组件里时,再调用useState就会拿到最新的值,具体代码体现在:
hookState[hookIndex]=hookState[hookIndex]||initialState;
memo、useMemo、useCallback:
组件的props的值不改变时,我们不希望组件被再次渲染,这种情况下,我们需要用到React.memo,例如下面的例子中:
function Child({data,handleClick}){
console.log('child render');
return <button onClick={handleClick}>{data.number}</button>
}
function App(){
const [name,setName] = React.useState('zhufeng');
const [number,setNumber] = React.useState(0);
//缓存对象的 第1个参数是创建对象的工厂函数,第2个参数是依赖变量的数组,如果依赖数组中的任何一个变量发生改变,就会重新调用工厂方法创建新的对象,则否则就会重用上次的对象
let data = {number};
//缓存回调函数的
let handleClick = ()=>setNumber(number+1);
return (
<div>
<input type="text" value={name} onChange={event=>setName(event.target.value)}/>
<Child data={data} handleClick={handleClick}/>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
用户在input中输入数字的时候,传给Child组件的number属性和handleClick属性并没有改变,但通过测试我们发现在input中输入内容的过程中,console.log('child render');还是会执行
为了避免这种情况下的重复渲染,用了React.memo包装起来:
function Child({data,handleClick}){
console.log('child render');
return <button onClick={handleClick}>{data.number}</button>
}
let MemoChild = React.memo(Child);
function App(){
const [name,setName] = React.useState('zhufeng');
const [number,setNumber] = React.useState(0);
//缓存对象的 第1个参数是创建对象的工厂函数,第2个参数是依赖变量的数组,如果依赖数组中的任何一个变量发生改变,就会重新调用工厂方法创建新的对象,则否则就会重用上次的对象
let data = {number};
//缓存回调函数的
let handleClick = ()=>setNumber(number+1);
return (
<div>
<input type="text" value={name} onChange={event=>setName(event.target.value)}/>
<MemoChild data={data} handleClick={handleClick}/>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
但是,经过上述修改之后,可以发现,并没有起到作用,原因是data是对象类型,handleClick是函数,每次重新渲染时,它们都有不同的引用,因此,react还会认为这是两个不一样的对象,还是会重新渲染
这时就需要用useCallback和useMemo来改造:
function Child({data,handleClick}){
console.log('Child render');
return <button onClick={handleClick}>{data.number}</button>
}
//可缓存的Child,如果一个组件它的属性没有变化,就不会重新渲染
let MemoChild = React.memo(Child);
function App(){
console.log('App render');
const [name,setName] = React.useState('zhufeng');
const [number,setNumber] = React.useState(0);
//缓存对象的 第1个参数是创建对象的工厂函数,第2个参数是依赖变量的数组,如果依赖数组中的任何一个变量发生改变,就会重新调用工厂方法创建新的对象,则否则就会重用上次的对象
let data = React.useMemo(()=>({number}),[number]);
//缓存回调函数的
let handleClick = React.useCallback(()=>setNumber(number+1),[number]);
return (
<div>
<input type="text" value={name} onChange={event=>setName(event.target.value)}/>
<MemoChild data={data} handleClick={handleClick}/>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
useReducer:
import React from './react';
import ReactDOM from './react-dom';
function reducer(state={number:0}, action) {
switch (action.type) {
case 'ADD':
return {number: state.number + 1};
case 'MINUS':
return {number: state.number - 1};
default:
return state;
}
}
function Counter(){
const [state, dispatch] = React.useReducer(reducer,{number:0});
return (
<div>
Count: {state.number}
<button onClick={() => dispatch({type: 'ADD'})}>+</button>
<button onClick={() => dispatch({type: 'MINUS'})}>-</button>
</div>
)
}
ReactDOM.render(
<Counter/>,
document.getElementById('root')
);
export function useReducer(reducer,initialState){
hookState[hookIndex]=hookState[hookIndex]||initialState;//hookState[0]=10
let currentIndex = hookIndex;
function dispatch(action){
action = typeof action === 'function'?action(hookState[currentIndex]):action;
hookState[currentIndex]=reducer?reducer(hookState[currentIndex],action):action;
scheduleUpdate();//状态变化后,要执行调度更新任务
}
return [hookState[hookIndex++],dispatch];
}
useContext:
function useContext(context){
return context._currentValue;
}
useEffect:
useEffect里的函数会在当前的组件渲染到页而之后执行
useEffect不会阻塞当前页面的渲染
如果没有添加依赖,则每次组件更新时都会执行这个effect
这种情况下虽然可以拿到最新的number值,但会重新再开一个定时器,导致页面效果混乱
import React from 'react';
import ReactDOM from 'react-dom';
function Counter(){
const [number, setNumber] = React.useState(0);
React.useEffect(()=>{
const timer = setInterval(()=>{
setNumber(number+1);
},1000);
// 如果没有添加依赖,则每次组件更新时都会执行这个effect
});
return <div>{number}</div>
}
ReactDOM.render(<Counter/>, document.getElementById('root'));
添加空的依赖之后,就可以保证这个effect只执行一次,这样定时器就可以只开一个
但是受到闭包的影响,这里的number每次都是0,所以没法自增
import React from 'react';
import ReactDOM from 'react-dom';
function Counter(){
const [number, setNumber] = React.useState(0);
React.useEffect(()=>{
const timer = setInterval(()=>{
setNumber(number+1);
},1000);
},[]);
return <div>{number}</div>
}
ReactDOM.render(<Counter/>, document.getElementById('root'));
有一种更改的办法是给setNumber传函数,基于老值计算新值
import React from 'react';
import ReactDOM from 'react-dom';
function Counter(){
const [number, setNumber] = React.useState(0);
React.useEffect(()=>{
const timer = setInterval(()=>{
// 基于老值计算新值
setNumber(number=>number+1);
},1000);
},[]);
return <div>{number}</div>
}
ReactDOM.render(<Counter/>, document.getElementById('root'));
也可以在组件销毁时将定时器也取消,下面的写法中,每次更新时effect也都会执行,定时器会重新开启,但会被销毁,下次再开新的定时器
组件销毁时机是在下一次更新的时候销毁,即组件第一次渲染时,console中log的顺序为:
Counter render
开启定时器
执行定时器
下一次渲染时,console中log的顺序为:
Counter render
销毁定时器
开启定时器
执行定时器
function Counter(){
console.log('Counter render');
const [number,setNumber] = React.useState(0);
React.useEffect(()=>{
console.log('开启定时器');
const timer = setInterval(()=>{
console.log('执行定时器');
setNumber(number=>number+1);
},1000);
return ()=>{
console.log('销毁定时器');
clearInterval(timer)
}
});
return <div>{number}</div>
}
ReactDOM.render(<Counter/>, document.getElementById('root'));
export function useEffect(effect,deps){
//先判断是不是初次渲染
if(hookState[hookIndex]){
let [lastDestroy,lastDeps] = hookState[hookIndex];
let same = deps&&deps.every((item,index)=>item === lastDeps[index]);
if(same){
hookIndex++;
}else{
//如果有任何一个值不一样,则执行上一个销毁函数
lastDestroy&&lastDestroy();
//开启一个新的宏任务
setTimeout(()=>{
let destroy = effect();
hookState[hookIndex++]=[destroy,deps]
});
}
}else{
//如果是第一次执行执行到此
setTimeout(()=>{
let destroy = effect();
hookState[hookIndex++]=[destroy,deps]
});
}
}
useLayoutEffect:
export function useLayoutEffect(effect,deps){
//先判断是不是初次渲染
if(hookState[hookIndex]){
let [lastDestroy,lastDeps] = hookState[hookIndex];
let same = deps&&deps.every((item,index)=>item === lastDeps[index]);
if(same){
hookIndex++;
}else{
//如果有任何一个值不一样,则执行上一个销毁函数
lastDestroy&&lastDestroy();
//开启一个新的宏任务
queueMicrotask(()=>{
let destroy = effect();
hookState[hookIndex++]=[destroy,deps]
});
}
}else{
//如果是第一次执行执行到此
queueMicrotask(()=>{
let destroy = effect();
hookState[hookIndex++]=[destroy,deps]
});
}
}
通过一个简单的案例,来区分useEffect和useLayoutEffect
作者:buuug
链接:https://zhuanlan.zhihu.com/p/348701319\
来源:知乎
import React, { useEffect, useLayoutEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
const [state, setState] = useState("hello world")
useEffect(() => {
let i = 0;
while(i <= 100000000) { i++; };
setState("world hello");
}, []);
// useLayoutEffect(() => {
// let i = 0;
// while(i <= 100000000) {
// i++;
// };
// setState("world hello");
// }, []);
return ( <> <div>{state}</div> </> );
}
export default App;
上面的案例中,如果使用useEffect,页面会先渲染初始的hello world,在useEffect里的while循环卡了一段时间后,再渲染出来world hello,即useEffect是异步执行的
通过测试,发现有一个点需要注意,这个案例必须要用ReactDOM.createRoot(xxx).render(xxx)的方式测试才有效果,即在concurrent模式下才会体现出效果,主要原因是因为在react17中,或者说在非concurrent模式下,react内部虽然也有调度器,但是它是以sync同步的形式工作的,因此在layout阶段后执行的遍历useEffect回调也是以同步方式执行
而如果使用useLayoutEffect,则不会出现先渲染hello world,再渲染world hello的情况,而是直接渲染出来了world hello,即useLayoutEffect是同步执行的
useImperativeHandle:
import React from './react';
import ReactDOM from './react-dom';
function Child(props,forwardRef){
const inputRef = React.useRef();
//这个方法可以定制暴露给父组件ref值 forwardRef.current = {focus}
React.useImperativeHandle(forwardRef,()=>{
return {
focus(){
inputRef.current.focus();
}
}
});
return <input ref={inputRef}/>
}
let ForwaredChild = React.forwardRef(Child);
function Parent(){
const inputRef = React.useRef();//{current:undefined}
const getFocus = ()=>{
inputRef.current.focus();
//如果子组件的把自己的内部的真实DOM完整暴露给父组件的,父组件可以对此DOM元素进行任何操作
//inputRef.current.remove();
//inputRef.current.value = 'xx';
}
return (
<div>
<ForwaredChild ref={inputRef}/>
<button onClick={getFocus}>获得焦点</button>
</div>
)
}
ReactDOM.render(<Parent/>, document.getElementById('root'));
export function useImperativeHandle(ref,handler){
ref.current = handler();
}