前言
react-hooks
是 React 16.8 的新增特性。它可以让我们在函数组件中使用 state 、生命周期以及其他 react 特性,而不仅限于 class 组件。这篇文章通过手写 react-hooks
的核心原理来带大家轻松掌握 react-hooks
,希望能够打动屏幕面前的你。
useState
useState
会返回一个数组:一个 state,一个更新 state 的函数。
useState
类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并,而是直接替换
需要注意的是:
useState 支持我们在调用的时候直接传入一个值,来指定 state 的默认值,比如这样 useState(0), useState({ a: 1 }), useState([ 1, 2 ]),还支持我们传入一个函数,来通过逻辑计算出默认值
在state需要更新的时候也有两种方式 一个是通过一个新的 state 值更新,一个是通过函数式更新返回新的 state
// 保存状态的数组
let hookStates = [];
// 索引
let hookIndex = 0;
function useState(initState) {
//判断传入的参数是否是函数
if (typeof initState === "function") {
hookStates[hookIndex] = initState();
} else {
hookStates[hookIndex] = hookStates[hookIndex] || initState;
}
// 使用闭包维护函数调用位置
let currentIndex = hookIndex;
function setState(newState) {
// 判断传入的state是否为函数,如果是把prevState传入
if (typeof newState === "function") {
// 重新复制给newState
newState = newState(hookStates[currentIndex]);
}
// 更新state
hookStates[currentIndex] = newState;
// 触发视图更新的函数
render();
}
// 返回数组形式
return [hookStates[hookIndex++], setState];
}
useEffect
useEffect
就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途
,只不过被合并成了一个 API。与 componentDidMount 或 componentDidUpdate 不同的是,使用 useEffect 调度的 effect 不会阻塞浏览器更新视图
,这让你的应用看起来响应更快。
需要注意的是useEffect
中第二个参数的作用:
什么都不传,组件每次 render 之后 useEffect 都会调用
传入一个空数组 [], useEffect 只会调用一次
传入一个非空数组,其中包括变量,只有这些变量变动时,useEffect 才会执行
//保存状态的数组
let hookStates = [];
//索引
let hookIndex = 0;
function useEffect(callback, dependiencies) {
if (hookStates[hookIndex]) {
// 非初始调用
let lastdependiencies = hookStates[hookIndex];
// 判断传入依赖项跟上一次是否相同
let same = dependiencies.every((item, i) => {
//判断item是不是对象
if (
typeof item === "object" &&
typeof item !== "function" &&
item !== null
) {
return isObjectValueEqual(item, lastdependiencies[i]);
} else {
return item === lastdependiencies[i];
}
});
if (same) {
hookIndex++;
} else {
hookStates[hookIndex++] = dependiencies;
callback();
}
} else {
// 初始调用
hookStates[hookIndex++] = dependiencies;
callback();
}
}
//比较两个对象是否一致
function isObjectValueEqual(a, b) {
var aProps = Object.getOwnPropertyNames(a);
var bProps = Object.getOwnPropertyNames(b);
if (aProps.length != bProps.length) {
return false;
}
for (var i = 0; i < aProps.length; i++) {
var propName = aProps[i];
var propA = a[propName];
var propB = b[propName];
//b里面没有propName这个key名
if (!b.hasOwnProperty(propName)) return false;
// 判断两边都有相同键名
if (propA instanceof Object) {
if (this.isObjectValueEqual(propA, propB)) {
// 这里不能return ,后面的对象还没判断
} else {
return false;
}
} else if (propA !== propB) {
return false;
} else {
}
}
return true;
}
useMemo
useMemo
把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
需要注意的是useMemo
中第二个参数的作用:
不传数组,每次更新都会重新计算
空数组,只会计算一次
依赖对应的值,当对应的值发生变化时,才会重新计算
// 保存状态的数组
let hookStates = [];
// 索引
let hookIndex = 0;
function useMemo(factory, dependencies) {
if (hookStates[hookIndex]) {
// 非首次调用
let [lastMemo, lastDependencies] = hookStates[hookIndex];
// 判断传入依赖项跟上一次是否相同
let same = dependiencies.every((item, i) => {
//判断item是不是对象
if (
typeof item === "object" &&
typeof item !== "function" &&
item !== null
) {
return isObjectValueEqual(item, lastdependiencies[i]);
} else {
return item === lastdependiencies[i];
}
});
if (same) {
hookIndex++;
return lastMemo;
} else {
// 只要有一个依赖变量不一样的话
let newMemo = factory();
hookStates[hookIndex++] = [newMemo, dependencies];
return newMemo;
}
} else {
// 首次调用
let newMemo = factory();
hookStates[hookIndex++] = [newMemo, dependencies];
return newMemo;
}
}
//比较两个对象是否一致
function isObjectValueEqual(a, b) {
...
}
useCallback
useCallback
接收一个内联回调函数参数和一个依赖项数组(子组件依赖父组件的状态,即子组件会使用到父组件的值) ,useCallback 会返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新
useCallback
把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
需要注意的是useCallback
中第二个参数的作用:
不传数组,每次更新都会重新计算
空数组,只会计算一次
依赖对应的值,当对应的值发生变化时,才会重新计算
// 保存状态的数组
let hookStates = [];
// 索引
let hookIndex = 0;
function useCallback(callback, dependencies) {
if (hookStates[hookIndex]) {
// 非首次
let [lastCallback, lastDependencies] = hookStates[hookIndex];
// 判断传入依赖项跟上一次是否相同
let same = dependiencies.every((item, i) => {
//判断item是不是对象
if (
typeof item === "object" &&
typeof item !== "function" &&
item !== null
) {
return isObjectValueEqual(item, lastdependiencies[i]);
} else {
return item === lastdependiencies[i];
}
});
if (same) {
hookIndex++;
return lastCallback;
} else {
// 只要有一个依赖变量不一样的话
hookStates[hookIndex++] = [callback, dependencies];
return callback;
}
} else {
// 首次调用
hookStates[hookIndex++] = [callback, dependencies];
return callback;
}
}
//比较两个对象是否一致
function isObjectValueEqual(a, b) {
...
}
memo
memo
类似于PureCompoent 作用是优化组件性能,防止组件触发重渲染, memo
针对 一个组件的渲染是否重复执行
function memo(OldFunComp) {
return class extends React.PureComponent {
render() {
return <OldFunComp {...this.props} />;
}
};
}
useContext
useContext
接收一个 context 对象(React.createContext 的返回值
)并返回该 context 的当前值useContext(MyContext)
, 只是让你能够读取 context 的值以及订阅 context 的变化。仍然需要在上层组件树中使用 <MyContext.Provider>
来为下层组件提供 context。
function useContext(context) {
return context._currentValue;
}
// 父组件
const CountCtx = React.createContext();
function ParentComp() {
const [state, setState] = React.useState({ number: 0 });
return (
<CountCtx.Provider value={{ state, setState }}>
<Child />
</CountCtx.Provider>
);
}
// 子组件
function Child() {
let { state, setState } = useContext(CountCtx);
return (
<div>
<p>{state.number}</p>
<button onClick={() => setState({ number: state.number + 1 })}>
add
</button>
</div>
);
}
useRef
useRef
返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数
useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的 ref 对象都是同一个(注意使用 React.createRef ,每次重新渲染组件都会重新创建 ref)
let lastRef;
function useRef(value) {
lastRef = lastRef || { current: value };
return lastRef;
}
useReducer
useReducer
接受类型为 (state, action) => newState 的 reducer,并返回与 dispatch 方法配对的当前状态。useReducer
和 redux 中 reducer 很像 useState 内部就是靠 useReducer 来实现的
// 保存状态的数组
let hookStates = [];
// 索引
let hookIndex = 0;
function useReducer(reducer, initState) {
hookStates[hookIndex] = hookStates[hookIndex] || initState;
let currentIndex = hookIndex;
function dispatch(action) {
hookStates[currentIndex] = reducer
? reducer(hookStates[currentIndex], action)
: action;
// 触发视图更新
render();
}
return [hookStates[hookIndex++], dispatch];
}
// useState可以使用useReducer改写
function useState(initState) {
return useReducer(null, initState);
}
hook 使用规则
使用 Hooks 的时候必须遵守 2 条规则:
只能在代码的第一层调用 Hooks,不能在循环、条件分支或者嵌套函数中调用 Hooks。
只能在Function Component或者自定义 Hook 中调用 Hooks,不能在普通的 JS 函数中调用。