Skip to content

React Hooks

TIP

react-hooks是react16.8以后,react新增的钩子API,目的是增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷,设计的初衷,可能是想把组件颗粒化、单元化,形成独立的渲染环境,减少渲染次数,优化性能

出现的目的:为解决类组件的一些问题

  • this指向不明
  • 业务逻辑分散在不同的生命周期方法中
  • 复用逻辑不方便比较复杂

函数组件虽然简单,但是它没有状态。为了让函数组件可以有状态,就出现ReactHooks

1. 说明

  • React Hooks
  • 解决的问题:
    • 解决了函数式组件中没有状态、声明周期、数据管理
    • 解决了Class组件中this的不确定性、组件之间逻辑难以复用、大型复杂组件中Class产生的实例所带来的性能消耗和不好维护性

2. useReducer

  • useReducer 的作用:在无状态组件中使用redux
  • useReducer 返回的是一个键值对数组,第一个为状态值,第二个为派发函数

1.1 基本使用

  • useReducer 是 React Hooks 中的一个非常有用的 Hook,它是一个更复杂的版本的 useState。当你的 state 逻辑变得更加复杂或需要之前的状态来计算下一个状态时,useReducer 是非常有用的。
  • useReducer接受一个reducer函数和一个初始状态作为参数,返回当前的state和一个与该reducer函数关联的dispatch方法
js
const [state, dispatch] = useReducer(reducer, initialState);

1.2 为什么使用 useReducer?

  • 更加可预测: 由于它是基于 reducer 的,因此 useReducer 允许你的 state 逻辑以更可预测的方式运行,这在处理更复杂的 state 逻辑时特别有用。

  • 更好的组织: 当有多种状态更新逻辑时,使用 useReducer 可以帮助你更好地组织代码。

  • 中间件和增强器: 像 Redux 这样的库允许你使用中间件来增强 reducer 的功能。虽然 React 的 useReducer 没有这个功能,但你可以模仿类似的行为。

  • 更好地处理副作用: 当与 useEffect 结合使用时,useReducer 可以更好地处理和 orchestrate 副作用。

1.3 使用案例

js
import React from './react';
import ReactDOM from './react-dom/client';

//定义一个初始状态
const initialState = { count: 0 };

/**
 * 计算新状态的函数
 * @param {*} state 老状态 
 * @param {*} action 动作 动作就是一个描述你想干啥的对象 {type:'ADD'} {type:"MINUS"}
 */
function reducer(state = initialState, action) {
	switch (action.type) {
		case 'ADD':
			return { count: state.count + 1 };
		case 'MINUS':
			return { count: state.count - 1 };
		default:
			return state;
	}
}

function Counter(){
	const [state1,dispatch1] = React.useReducer(reducer,initialState);
	const [state2,dispatch2] = React.useReducer(reducer,initialState);

	return (
		<div>
			<p>state1.count:{state1.count}</p>
			<button onClick={()=>dispatch1({type:'ADD'})}>addState1</button>
			<button onClick={()=>dispatch1({type:'MINUS'})}>-</button>
			<hr/>
			<p>state2.count:{state2.count}</p>
			<button onClick={()=>dispatch2({type:'ADD'})}>addState2</button>
			<button onClick={()=>dispatch2({type:'MINUS'})}>-</button>
		</div>
	)
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Counter />);

1.4 模拟原理

js
function useReducer(reducer, initialState) {
	hookState[hookIndex] = hookState[hookIndex] || initialState;
	let currentIndex = hookIndex;
	// 派发函数
	function dispatch(action) {
		// 需要判断是否编写了reducer
		hookState[currentIndex] = reducer ? reducer(hookState[currentIndex], action) : action;
		scheduleUpdate();
	}
	return [hookState[hookIndex++], dispatch];
}

1.5 实现原理

src\react-dom\client.js

js
// 定义三个全局变量
// 当前正在渲染的函数组件
let currentVdom = null;
// 当前的根节点
let currentRoot = null;
// 当前根节点的虚拟dom
let currentRootVdom = null;

/**
 * 派发动作,然后经过reducer计算获取新状态,可以很好的封装计算新状态的逻辑
 * @param {*} reducer 新状态的计算函数
 * @param {*} initialState 初始状态
 * @returns 数组,包括当前的状态,和派发状态的方法
 */
export function useReducer(reducer, initialState) {
    const { hooks } = currentVdom;//获取hooks对象
    //当第一更新的时候hookIndex=0,hookStates[0]={count:1}
    const { hookIndex, hookStates } = hooks;//获取hooks对象上保存的索引和状态数组
    const oldState = hookStates[hookIndex];//获取当前的索引对应的状态

    //如果hookState不存在,表示可能是第一次挂载
    if (isUndefined(oldState)) {
        //赋默认值
        hookStates[hookIndex] = initialState;//{count:0}
    }

    //这是一个派发动作的函数
    function dispatch(action) {
        const oldState = hookStates[hookIndex];
        //根据老状态和派发的动作计算新状态,并且覆盖hookStates中对应的索引
        const newState = reducer(oldState, action);
        //用计算出来的新状态和老状态进行对比,如果状态不一样才进行更新逻辑,如果状态一样则不更新
        if (newState !== oldState) {
            hookStates[hookIndex] = newState;
            //如果新的状态和老状态是一样的话,则不更新
            //调用根节点的更新方法
            currentRoot.update();
        }
    }
    //当执行完useReducer函数的时候,索引就要++了
    return [hookStates[hooks.hookIndex++], dispatch];
}
/**
 * 创建DOM容器
 * @param {*} container 
 */
function createRoot(container) {
    const root = {
        //把虚拟DOM变成真实DOM并且插入容器container
        render(rootVdom) {
            currentRoot = root;//此处保存当前的根容器节点
            currentRootVdom = rootVdom;//根虚拟DOM
            mountVdom(rootVdom, container);
            //设置事件代理 
            setupEventDelegation(container);
        },
        update() {
            //比较新老的DOM-DIFF
            //container父DOM节点 currentRootVdom老的根虚拟DOM currentRootVdom新的根虚拟DOM
            compareVdom(container, currentRootVdom, currentRootVdom);
        }
    }
    return root;
}

/**
 * 进行深度的DOM-DIFF
 * @param {*} parentDOM 真实的父DOM oldVdom对应的真实DOM的父节点
 * @param {*} oldVdom 上一次render渲染出来的虚拟DOM
 * @param {*} newVdom 最新的render渲染出来的虚拟DOM
 */
export function compareVdom(parentDOM, oldVdom, newVdom, nextDOMElement) {
	//...
	//新老都有,并且类型也一样,那就可以进入深度对比属性和子节点
	updateVdom(oldVdom, newVdom);
}

/**
 * 更新虚拟DOM
 */
function updateVdom(oldVdom, newVdom) {
	// ...
	return updateFunctionComponent(oldVdom, newVdom)
}

/**
 * 更新函数组件 
 * @param {*} oldVdom 
 * @param {*} newVdom 
 */
function updateFunctionComponent(oldVdom, newVdom) {
	// 此处将hookIndex索引置为0 ,并更新虚拟dom的指向,保证更新为同一个状态对象
	const hooks = (newVdom.hooks = oldVdom.hooks);
	//再重新执行函数前把hookIndex重置为0
	hooks.hookIndex = 0;
	//让当前的虚拟DOM等于新的函数组件的虚拟DOM
	currentVdom = newVdom;

	//获取新的虚拟DOM对应的类型和属性
	const { type, props } = newVdom;
	//计算新的虚拟DOM
	const renderVdom = type(props);
	//进行虚拟DOM的对比
	compareVdom(getParentDOMByVdom(oldVdom), oldVdom.oldRenderVdom, renderVdom);
	newVdom.oldRenderVdom = renderVdom;
}

执行逻辑

  1. 初始化useReducer,利用全局变量指向当前的组件虚拟dom,并用数组记录下初始状态
  2. 定义dispatch派发方法
  3. 状态更新后,要重新从根节点开始更新整个dom树
  4. 利用compareVdom进行全量比较,触发函数组件的更新操作,从将状态索引置为0,重新开始对比渲染

2 useState

  • useState的作用:数据存储、派发更新
  • useState 返回的是一个键值对数组,第一个为状态值,第二个为修改函数
  • useState和setState的区别:useState不会合并对象,每次执行利用闭包生成一个新的上下文对象,setState会把新旧state进行合并

2.1 基本使用

js
import React from 'react';
import ReactDOM from 'react-dom';

function Counter() {
	// 返回初始值和修改值的函数
	const [number, setNumber] = React.useState(0)

	const handlerClick = () => {
		setNumber(number + 1)
	}
	return (
		<div>
			<p>{number}</p>
			<button onClick={handlerClick}>+</button>
		</div>
	)
}
ReactDOM.render(<Counter />, document.getElementById('root'));

2.2 模拟原理

js
let hookState = [];//这里存放着所有的状态
let hookIndex = 0;//当前的执行的hook的索引
let scheduleUpdate;//调度更新方法

function render(vdom, container) {
	mount(vdom, container);
	scheduleUpdate = () => {
		hookIndex = 0;//vdom并不指向当前的更新,而是指向根元素
		compareTwoVdom(container, vdom, vdom);
	}
}

function useState(initialState) {
	hookState[hookIndex] = hookState[hookIndex] || initialState;
	let currentIndex = hookIndex;
	function setState(newState) {
		hookState[currentIndex] = newState;
		scheduleUpdate();
	}
	return [hookState[hookIndex++], setState];
}

2.4 实现原理

源码中useState是useReducer的语法糖

js
//如果action是一个函数,则把老状态传入函数返回计算的新的状态,否则 就是直接把action当成新状态
const defaultReducer = (state, action) => typeof action === 'function' ? action(state) : action;
//其实在源码里面useState仅仅是一种特殊 的useReducer
//特殊在于直接给状态,不需要经过计算
export function useState(initialState) {
    //useState内置一个特殊的reducer,这个reducer会把dispatch派发动作当成新的状态
    return useReducer(defaultReducer, initialState);
}

3 useMemo和useCallback

  • useMemo和useCallback接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值
  • 区别在于useMemo返回的是函数运行的结果,useCallback返回的是函数,这个回调函数是经过处理后的也就是说父组件传递一个函数给子组件的时候,由于是无状态组件每一次都会重新生成新的props函数,这样就使得每一次传递给子组件的函数都发生了变化,这时候就会触发子组件的更新,这些更新是没有必要的,此时我们就可以通过usecallback来处理此函数,然后作为props传递给子组件
  • 把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新
  • 把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算

3.1 基本使用

  • useMemo:返回一个 memoized 值。只有当依赖项改变时,它才会重新计算这个值。
  • useCallback:返回一个 memoized 的 callback。它会返回一个不变的函数,直到依赖项改变。
js
import React from 'react';
import ReactDOM from 'react-dom';

// 现在希望如果改变的是输入框的值,ChildButton组件并不依赖输入框的值,所以ChildButton不要重新渲染
// 要想让ChildButton不重新渲染有两个条件
// 第一个条件需要使用React.memo
let Child = ({ data, handleClick }) => {
	console.log('Child render');
	return (
		<button onClick={handleClick}>{data.number}</button>
	)
}

// 使用了React.memo之后返回一个新组件,这个新组件可以保证如果新组件的属性不变,则不需要得新渲染这个新组件
Child = React.memo(Child);

function App() {
	console.log('App render');
	const [name, setName] = React.useState('test');
	const [number, setNumber] = React.useState(0);

	// 使用React.memo可以缓存对象,此对象可做到在App组件多次渲染执行的时候保持引用地址不变
	// 第二个参数是依赖值 的数组,当依赖数组中的值不变时,则会复用上一次的值,如果变化了,则重新计算新的对象
	const data = React.useMemo(() => ({ number }), [number]);
	// 使用React.useCallback可以缓存回调函数callback,此函数会在App组件多次渲染执行的时候保持引用地址不变
	const handleClick = React.useCallback(() => setNumber(number + 1), [number]);

	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'));

3.2 模拟原理

js
export function useMemo(factory, deps) {
	if (hookState[hookIndex]) {//说明不是第一次是更新
		let [lastMemo, lastDeps] = hookState[hookIndex];
		let everySame = deps.every((item, index) => item === lastDeps[index]);
		if (everySame) {
			hookIndex++;
			return lastMemo;
		} else {
			let newMemo = factory();
			hookState[hookIndex++] = [newMemo, deps];
			return newMemo;
		}
	} else {
		let newMemo = factory();
		hookState[hookIndex++] = [newMemo, deps];
		return newMemo;
	}
}
export function useCallback(callback, deps) {
	if (hookState[hookIndex]) {//说明不是第一次是更新
		let [lastCallback, lastDeps] = hookState[hookIndex];
		let everySame = deps.every((item, index) => item === lastDeps[index]);
		if (everySame) {
			hookIndex++;
			return lastCallback;
		} else {
			hookState[hookIndex++] = [callback, deps];
			return callback;
		}
	} else {
		hookState[hookIndex++] = [callback, deps];
		return callback;
	}
}

3.3 实现原理

js
/**
 * 可以记忆对象
 * @param {*} factory  创建对象的函数
 * @param {*} deps 依赖的值数组,如果依赖的值有任意某项发生变化,会重新创建,如果依赖的值全都没有变化,则不会重新创建
 */
export function useMemo(factory, deps) {
  const { hooks } = currentVdom
  const { hookIndex, hookStates } = hooks
  // 获取状态数组中的hookIndex对应的值 [newMemo, deps]
  const prevHook = hookStates[hookIndex]
  // 如果有值,说明当前处于更新的过程
  if (prevHook) {
    // 取出上一个对象和上一个依赖数组
    const [prevMemo, prevDeps] = prevHook
    // 对比新的依赖数组和老的依赖数组中的每一项,如果全部相等的话
    if (deps.every((dep, index) => dep === prevDeps[index])) {
      // 直接索引加1,往后走下一个hook,并且返回上一次缓存的对象
      hooks.hookIndex++
      return prevMemo
    }
  }

  // 第一次肯定是执行工厂方法,返回newMemo
  const newMemo = factory()
  // 把计算出来的对象和依赖数组保存在hookStates数组中
  hookStates[hookIndex] = [newMemo, deps]
  hooks.hookIndex++
  return newMemo
}

export function useCallback(callback, deps) {
  // 可以使用useMemo很方便的实现useCallback
  return useMemo(() => callback, deps)
}

4 useContext

  • useContext 在函数组件中自由获取context对象
  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context

4.1 基本使用

React.useContext 是 React 的一个 Hook,它允许你无需明确地传递 props,就能让组件订阅 context 的变化。

为了理解 useContext,首先你需要知道 React 的 Context API。Context 提供了一种在组件之间共享此类值的方式,而不必明确地通过组件树的每个层级传递 props。

Context 主要由两个核心组件组成:Provider 和 Consumer。

  • Provider:这是一个 React 组件,它允许消费组件订阅 context 的变化。
  • Consumer:这是 React context 的消费者。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树的最近的那个匹配的 Provider 读取到当前的 context 值。

然而,在函数组件中,你不必使用 Consumer。你可以简单地使用 useContext Hook

js
import React from 'react';
import ReactDOM from 'react-dom';

let Context = React.createContext();

/* 用useContext方式 */
const ChildContext1 = () => {
	const value = React.useContext(Context)
	return <div> name: {value.name}  age: {value.age}</div>
}

/* 用Context.Consumer 方式 */
const ChildContext2 = () => {
	return <Context.Consumer>
		{(value) => <div> name: {value.name}  age: {value.age}</div>}
	</Context.Consumer>
}

function App() {
	return (
		<Context.Provider value={{ name: 'test', age: 1 }}>
			<ChildContext1 />
			<ChildContext2 />
		</Context.Provider>
	)
}
ReactDOM.render(<Counter />, document.getElementById('root'));

4.2 模拟原理

js
function mountProviderComponent(vdom) {
	//在渲染Provider组件的时候,拿到属性中的value,赋给context._currentValue
	type._context._currentValue = props.value;
}

function useContext(context) {
	return context._currentValue;
}

4.3 实现原理

React.createContext 方法

js
function createContext(defaultValue) {
  const context = {
    _currentValue: defaultValue, //这个表示当前的值
    Provider: (props) => {
      //把属性中接收过来的value属性保存在context的_currentValue属性上
      context._currentValue = props.value
      return props.children //真正就是它的子元素
    },
    Consumer: (props) => {
      // Consumer也是一个函数组件,
      //它渲染的是子组件函数返回的结果 。函数的参数是context的当前值
      return props.children(context._currentValue)
    },
  }
  return context
}

React.useContext 方法

js
function useContext(context) {
  return context._currentValue
}

5 useRef

下面是 useRef 的一些核心点:

  1. 创建 Ref: 当你调用 useRef(),它会返回一个像这样的对象: { current: initialValue }。你可以为它提供一个初始值,例如 useRef(0),但这并不是必需的。

  2. 持久性: 不像 useState,每次组件重新渲染时都会返回一个新的 state,useRef 会在整个组件生命周期中保持其对象不变。

  3. 使用场景:

    • 访问 DOM 元素: 可以使用 useRef 创建一个 ref,然后将它附加到 JSX 元素上,从而在组件内部访问该 DOM 元素。
    • 保留不触发渲染的可变数据: 如果你有一个值,你不希望它的变化导致组件重新渲染,可以将它存储在 ref 中。
    • 跟踪上一次的 props 或 state: 你可以使用 useRef 来跟踪前一个渲染周期中的值。

5.2 基本使用

  • useRef 获取元素DOM
js
import React from 'react';
import ReactDOM from 'react-dom';

function App() {
	const ref = React.useRef();

	let handleClick = () => {
		console.log(ref.current)
	}
	return (
		<div>
			{/* ref 标记当前dom节点 */}
			<div ref={ref} >表单组件</div>
			<button onClick={() => handleClick()} >提交</button>
		</div>
	)
}

ReactDOM.render(<App />, document.getElementById('root'));

5.2 模拟原理

js
function useRef(initialState) {
	if (hookState[hookIndex]) {
		return hookState[hookIndex++];
	} else {
		hookState[hookIndex] = { current: initialState };
		return hookState[hookIndex++];
	}
}
// 然后这个对象会在虚拟dom执行的时候给current赋值

5.3 实现原理

js
export function useRef(initialValue) {
  const { hooks } = currentVdom
  const { hookIndex, hookStates } = hooks
  if (isUndefined(hookStates[hookIndex])) {
    hookStates[hookIndex] = { current: initialValue }
  }
  return hookStates[hooks.hookIndex++]
}

6 useEffect

  • useEffect 的作用:在函数组件更新后操作副作用(改变 DOM、添加订阅、设置定时器、记录日志等)钩子
  • useEffect 跟相当于 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 三个声明周期函数钩子的作用,只不过被合并成了一个 API
  • useEffect 两个参数:第一个渲染函数,第二个为依赖项,如果依赖项发生变化,渲染函数会重新执行

6.1 基本使用

React.useEffect 是 React 的一个 Hook,允许你在函数组件中执行有副作用的操作。它与类组件中的生命周期方法类似,如 componentDidMount、componentDidUpdate 和 componentWillUnmount,但更加强大和灵活。

使用方法

js
useEffect(() => {
  // 副作用操作
  return () => {
    // 清除副作用,类似于 componentWillUnmount
  };
}, [依赖项]);

参数解释

  • 函数体:第一个参数是你的副作用函数。这里面的代码在默认情况下在每次渲染后都会执行。
  • 依赖数组:这是一个可选参数。当提供这个数组时,useEffect 只会在这些依赖发生变化时执行。

注意事项

  • 不要在循环、条件或嵌套函数中调用 useEffect:它应该总是在你的 React 函数的顶层被调用。
  • 清除副作用:useEffect 可以返回一个函数,这个函数会在组件卸载前或重新执行副作用前被调用。这是清除副作用(如事件监听器、取消网络请求、清除定时器)的好地方。
js
import React from 'react';
import ReactDOM from 'react-dom';

// 一个错乱的定时器:结果会乱跳
// 理由是setNumber每次修改都会重新渲染页面,重新渲染又会开启新的定时器,会产生多个定时器,导致错乱
function Counter() {
	const [number, setNumber] = React.useState(0);
	
	// effect函数会在当前的组件渲染到DOM后执行
	React.useEffect(() => {
		console.log('开启一个新的定时器')
		const $timer = setInterval(() => {
			// setNumber(number => number + 1);
			setNumber(number + 1);
		}, 1000);
	});
	return <p>{number}</p>
}
ReactDOM.render(<Counter />, document.getElementById('root'));

改为确保只有一个定时器的两个方案

js
import React from 'react';
import ReactDOM from 'react-dom';

// 1 加入一个[]依赖项,让渲染函数执行一次,只产生一个定时器
// 然后setNumber中number当参数传入每次就会+1正确执行
function Counter() {
	const [number, setNumber] = React.useState(0);
	
	React.useEffect(() => {
		console.log('开启一个新的定时器')
		const $timer = setInterval(() => {
			console.log('执行定时器', number); // number一直是0
			setNumber(number => number + 1);
		}, 1000);
	}, []);
	// 如果是[]依赖项不发生变化,定时器里面number一直是0
	return <p>{number}</p>
}
ReactDOM.render(<Counter />, document.getElementById('root'));

// 2 传入依赖项[number],每次变化渲染函数重新执行,产生一个新的定时器
// 然后return一个函数清楚定时器,在下一次effect执行前就销毁该函数,这样也只会有一个定时器
function Counter() {
	const [number, setNumber] = React.useState(0);

	React.useEffect(() => {
		console.log('开启一个新的定时器')
		const $timer = setInterval(() => {
			console.log('执行定时器', number);
			// 在这里,下面两种方式都可以,因为每次都是产生新的
			setNumber(number => number + 1);
			// setNumber(number + 1);
		}, 1000);

		return () => {//在执行下一次的effect之前要执行销毁函数
			console.log('清空定时器', number);
			clearInterval($timer);
		}
	}, [number]);
	// 如果是[number] 发生了变化,每次才会重新执行,改变number的值
	return <p>{number}</p>
}
ReactDOM.render(<Counter />, document.getElementById('root'));

6.2 模拟原理

js
/**
 * @param {*} callback 当前渲染完成之后下一个宏任务
 * @param {*} deps 依赖数组,
 */
function useEffect(callback, deps) {
	if (hookState[hookIndex]) {
		let [destroy, lastDeps] = hookState[hookIndex];
		// 判断依赖项是否发生了变化
		let everySame = deps.every((item, index) => item === lastDeps[index]);
		if (everySame) {
			hookIndex++;
		} else {
			// 销毁函数每次都是在下一次执行的时候才会触发执行
			destroy && destroy(); // 先执行销毁函数,然后产生新的宏任务
			setTimeout(() => {
				let destroy = callback();
				hookState[hookIndex++] = [destroy, deps];
			});
		}
	} else {
		//初次渲染的时候,开启一个宏任务,在宏任务里执行callback,保存销毁函数和依赖数组
		setTimeout(() => {
			let destroy = callback();
			hookState[hookIndex++] = [destroy, deps];
		});
	}
}

6.3 实现原理

见useLayoutEffect 的7.3 实现原理

7 useLayoutEffect

  • useLayoutEffect 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
  • useEffect不会阻塞浏览器渲染,而 useLayoutEffect 会浏览器渲染
  • useEffect会在浏览器渲染结束后执行,useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行

7.1 基本使用

useLayoutEffect 是 React 的一个 Hook,它和 useEffect 有相同的函数签名,但它有一些关键的差异。让我们深入了解这两者之间的差异以及 useLayoutEffect 的工作原理:

执行时机

  • useEffect: 是在浏览器绘制后异步执行的,这意味着执行这个 effect 可能会导致延迟。
  • useLayoutEffect: 是在浏览器执行绘制之前同步执行的。由于这个原因,如果你在 useLayoutEffect 中执行一些会导致额外绘制的代码(例如修改 DOM),它不会导致额外的浏览器绘制。

用途

  • 当你需要在 React 更新 DOM 之后,但在浏览器绘制之前,读取或修改 DOM,你应该使用 useLayoutEffect。这通常用于读取例如布局或尺寸等与视觉相关的属性。
  • 如果你不关心绘制,而只是需要在某些事情发生后执行一些代码,使用 useEffect 更为合适。
js
import React from 'react';
import ReactDOM from 'react-dom';

function Counter() {
	const ref = React.useRef();
	// useLayoutEffect 在浏览器绘制前执行的,所以没有下面的动画
	React.useLayoutEffect(() => {
		ref.current.style.WebkitTransform = `translate(500px)`;
		ref.current.style.transition = `all 500ms`;
	});
	let style = {
		width: '100px',
		height: '100px',
		backgroundColor: 'red'
	}
	return <div style={style} ref={ref}>我是内容</div>
}
ReactDOM.render(<Counter />, document.getElementById('root'));

7.2 模拟原理

js
/**
 * @param {*} callback 当前渲染完成之前的一个微任务
 * @param {*} deps 依赖数组,
 */
function useLayoutEffect(callback, deps) {
	if (hookState[hookIndex]) {
		let [destroy, lastDeps] = hookState[hookIndex];
		let everySame = deps.every((item, index) => item === lastDeps[index]);
		if (everySame) {
			hookIndex++;
		} else {
			//销毁函数每次都是在下一次执行的时候才会触发执行
			destroy && destroy();//先执行销毁函数
			queueMicrotask(() => {
				let destroy = callback();
				hookState[hookIndex++] = [destroy, deps];
			});
		}
	} else {
		//初次渲染的时候,开启一个微任务,在微任务里执行callback,保存销毁函数和依赖数组
		queueMicrotask(() => {
			let destroy = callback();
			hookState[hookIndex++] = [destroy, deps];
		});
	}
}

7.3 实现原理

js
// 宏任务来实现
export function useEffect(effect, deps) {
  applyEffectHook(effect, deps, setTimeout)
}

// 微任务来实现
export function useLayoutEffect(effect, deps) {
  applyEffectHook(effect, deps, queueMicrotask)
}

applyEffectHook 实现方法

js
export function applyEffectHook(effect, deps, schedule) {
  //获取当前的函数组件虚拟DOM内部存放的hooks
  const { hooks } = currentVdom
  //从此对象上解构出当前hook的索引,和hookStates数组
  const { hookIndex, hookStates } = hooks
  //是否要执行effect函数
  let shouldRunEffect = true
  //尝试读取老的useEffect数据状态
  const previousHookState = hookStates[hookIndex]
  let previousCleanup
  //第一次执行的时候,它的值肯定是undefined
  if (previousHookState) {
    //第二次的时候,previousHookState已经保存了一次执行effect返回的销毁函数和上次执行effect时的依赖数组
    const { cleanup, prevDeps } = previousHookState
    previousCleanup = cleanup
    if (deps) {
      //判断新的依赖数组中是否有某一项和老的依赖数组对应的值不相等
      //判断新数组中每一项和旧数组中的每一项是否完全相同,如果完全相同,则不需要重新执行false。如果有某一项
      //不同,则就需要重新执行
      //只有一某一项不相等就要重新执行,就要把shouldRunEffect设置为true
      shouldRunEffect = deps.some((dep, index) => !Object.is(dep, prevDeps[index]))
      //如果每一项都一样,那就不需要执行了,所以需要对结果 取反
      //shouldRunEffect=!deps.every((deps,index)=>Object.is(deps[index],prevDeps[index]));
    }
  }
  //如果shouldRunEffect的值为true,表示要重新执行effect,如果为false,则不执行
  if (shouldRunEffect) {
    //其实effect并不是立刻执行的,而是会包装成一个宏任务,在浏览器绘制渲染页面之后执行
    schedule(() => {
      previousCleanup?.() //如果上一次执行effect函数返回一个清理函数,则需要再执行下次的effect之前行执行清理函数
      //执行副作用函数,返回一个清理函数
      const cleanup = effect()
      //在hooksState数组中保存执行effect得到的清理函数,以及当前的依赖数组
      hookStates[hookIndex] = { cleanup, prevDeps: deps }
    })
  }
  //完事后让当前的hook索引向后走一步
  hooks.hookIndex++
}

8 forwardRef+useImperativeHandle

  • forwardRef将ref从父组件中转发到子组件中的dom元素上,子组件接受props和ref作为参数
  • useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值

8.1 基本使用

useImperativeHandle 是 React 的一个高级 Hook,通常与 forwardRef 配合使用。它允许你在使用 ref 时,自定义暴露给父组件的实例值,而不是组件的默认实例。

这个 Hook 主要在你想给组件的外部用户更多的控制权,或当你想隐藏一些组件内部的细节时使用。

使用方法

js
useImperativeHandle(ref, createHandle, [deps]);
  • ref: 通常是从 forwardRef 传递过来的。
  • createHandle: 一个函数,返回一个对象,这个对象的内容将被暴露给 ref。
  • [deps]: (可选)依赖数组,只有在这些依赖发生变化时,才会重新定义 ref 的实例值。
js
// 通过点击父组件的按钮,然后获取子组件的输入框焦点,而且不能更改子组件中的其他操作

import React from 'react';
import ReactDOM from 'react-dom';

function Child(props, childRef) {
	let inputRef = React.createRef();
	// 儿子只给父亲暴露的功能函数
	React.useImperativeHandle(childRef, () => (
		{
			focus() {
				inputRef.current.focus();
			}
		}
	));
	return <input ref={inputRef} />;
}

const ForwardedChild = React.forwardRef(Child);

function Parent(props) {
	let childRef = React.createRef();
	let getFocus = () => {
		childRef.current.focus();
		// childRef.current.remove(); // 子组件没有暴露,调用会报错
	}
	return (
		<div>
			<ForwardedChild ref={childRef} />
			<button onClick={getFocus}>获取焦点</button>
		</div>
	)
}
ReactDOM.render(<Parent />, document.getElementById('root'));

8.2 模拟原理

js
function forwardRef(FunctionComponent) {
	return class extends Component {
		render() {//this好像类的实例
			if (FunctionComponent.length < 2) {
				console.error(`forwardRef render functions accept exactly two parameters: props and ref. Did you forget to use the ref parameter?`);
			}
			return FunctionComponent(this.props, this.ref);
		}
	}
}
function useImperativeHandle(ref, factory) {
	ref.current = factory();
}

8.3 forwardRef实现代码

js
/**
 * 转发ref,可以实现ref的转发,可以接收ref,并且转发给函数组件
 * @param {*} render 是一个函数组件,也就是一个渲染函数
 */
function forwardRef(render) {
  //type 字符串原生组件 函数组 可能是一个类
  return {
    $$typeof: REACT_FORWARD_REF,
    render,
  }
}

8.4 useImperativeHandle 实现代码

js
function useImperativeHandle(ref, factory) {
  ref.current = factory()
}

Released under the MIT License.