React Hooks

TIP

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

一、使用

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

1 useState

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

模拟原理

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 useReducer

  • useReducer 的作用:在无状态组件中使用redux
  • 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'));

模拟原理

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];
}

源码中useState是useReducer的语法糖

function useState(initialState){
	return useReducer(null, initialState)
}

3 useEffect

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

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

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

模拟原理

/**
 * @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];
		});
	}
}

4 useLayoutEffect

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

模拟原理

/**
 * @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];
		});
	}
}

6 useRef

  • useRef 获取元素DOM
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'));

模拟原理

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

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

模拟原理

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

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

8 useMemo和useCallback

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

let Child = ({ data, handleClick }) => {
	console.log('Child render');
	return (
		<button onClick={handleClick}>{data.number}</button>
	)
}
Child = React.memo(Child);

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

	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)} />
			<Child data={data} handleClick={handleClick} />
		</div>
	)
}

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

模拟原理

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;
	}
}

9 forwardRef+useImperativeHandle

-forwardRef将ref从父组件中转发到子组件中的dom元素上,子组件接受props和ref作为参数

  • useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值
// 通过点击父组件的按钮,然后获取子组件的输入框焦点,而且不能更改子组件中的其他操作

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

模拟原理

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();
}

二、原理