前言
hook是React16.8的新特性,他可以让你在不编写class的情况下去使用state以及其他特性
最后附完整代码
准备阶段
我们先在 App 组件返回的一个对象去模拟用户点击 App().onClick()
function App() {
const [num, updateNum] = useState(0);
console.log('num: ' + num)
return {
onClick() {
updateNum(pre => pre + 1);
},
}
}
准备这三个全局变量
let isMount = true;
let workInProgressHook = null; // 代表着当前工作的节点
const fiber = {
stateNode: App, //当前这个节点
memoizedState: null // 初始值
}
-
isMount用来判断当前是否为初始化阶段 -
因为
fiber是个链表结构,所以workInProgressHook用来记录当前工作到某个节点的标志 -
fiber中的stateNode表示当前的组件 也就是App,memoizedState会存着当前这个组件上的状态
如果说第一次执行是 n 个 hook 。第二次执行是 n-1 个 hook (也就是说第二次少了一个hook,造成的原因可能是在 if 里写 hook ) 这种情况 react 就会报错误,原理也就是他是 .next 一直访问下一次hook 通过链表的形式记录每一次是哪个 hook,这样会出现 hook 的值错乱的情况
在
hooks当中存在着挂载阶段和更新阶段
我们用 mountWorkInProgressHook 表示挂载阶段需要做的事
// 用来获取初始化阶段的hook 同时将workInprogressHook 指针指向当前的hook
function mountWorkInProgressHook() {
let hook;
hook = {
next: null, // 代表的下一个 hook
queue: { // 是一个队列的原因是因为 我们有可能在一次更新调用多次更新hook
pending: null, // 保存改变的状态
}
}
if (!fiber.memoizedState) {
// 如果是第一个hook
fiber.memoizedState = hook; //将fiber的初始值设置为当前的hook
} else {
// 如果不是第一个hook 此时workInProgress代表上一次的hook
workInProgressHook.next = hook // 给当前节点的 next 添加本次的 hook ps: 因为存在着互相引用的关系 这里给workInProgressHook添加next 根fiber会更改
}
workInProgressHook = hook // 将当前工作的节点更改为现在的hook
return hook
}
这个函数主要做的任务就是 生成 hook 对象 同时将 workInprogressHook 指针指向当前的 hook
- 因为会有多个
hook调用, 如果当前调用的是第一个hook也就是fiber.memoizedState是null, 创建hook节点,将hook节点赋值给fiber.memoizedState - 如果不是第一个
hook则将当前的hook插入到上一次的节点后 也就是workInProgressHook.next = hook
我们 updateWorkInProgressHook 表示更新阶段需要做的事
// 用来获取更新阶段的hook 同时将workInprogressHook 指针指向当前的hook
function updateWorkInProgressHook() {
let hook;
hook = workInProgressHook; // 更新阶段可以直接从workInprogressHook里取当前的hook
workInProgressHook = workInProgressHook.next // 当前工作节点hook后移
return hook
}
在这个函数里我们吧 workInProgressHook 赋值给当前的 hook 并且指针后移, 返回 hook
用 schedule 来模拟更新组件 ,更新完成,改变 isMount = false
// 用来模拟更新组件
function schedule() {
workInProgressHook = fiber.memoizedState; // 每次重新执行将当前的工作进度指向 根 fiber 的 memoizedState
const app = fiber.stateNode(); // 模拟组件重新执行
isMount = false
return app
}
window.app = schedule()
因为更新组件需要将 workInProgressHook 指向根 state 所以 workInProgressHook = fiber.memoizedState;
为了方便调用 在这里将 App 调用后的结果赋值给 app 返回 并且 我们可以在控制台用 app.onClick() 去模拟组件点击
useState
function useState(initialState) {
let hook;
if (isMount) {
// 如果是初始化阶段, 初始化 当前这个 hook
hook = mountWorkInProgressHook()
hook.memoizedState = initialState; // 设置初始值
} else {
// 更新阶段 这里只是将最新的hook
hook = updateWorkInProgressHook()
}
let baseState = hook.memoizedState; // 当前的状态
if (hook.queue.pending) {
// 表示是需要更新 state
let firstUpdate = hook.queue.pending; // 这个变量表示当前移动到的action位置
do {
const action = firstUpdate.action; // 取到当前的action逻辑
if (typeof action !== 'function') {
baseState = action // 如果传递过来的是值 则直接赋值给baseState
} else {
baseState = action(baseState) // action 传入上一次的值 计算出新的值
}
firstUpdate = firstUpdate.next // 移动当前action指向的位置
} while (firstUpdate !== hook.queue.pending); // 如果当前的update是指向的第一个update 则表示遍历完毕 (用到了下面创建update为环状链表)
hook.queue.pending = null; // 清除本次调用后存储的更新
}
hook.memoizedState = baseState; //更改本次值为更新后的值
return [
baseState,
dispatchAction.bind(null, hook.queue)
]
}
// setState
function dispatchAction(queue, action) {
// 本次更新
const update = {
action, // 状态改变的action
next: null
}
// 这里存储为环状链表 会在上面用到判断
if (queue.pending === null) {
// 表示在本次更新 当前这个state是第一次更新
update.next = update; // 环状链表 u0.next -> u0
} else {
// u1 -> u0 -> u1
// 不是第一次更新 就将本次的更新插入到队列的最前面,此时update是收集了之前更新后最新的 链表
update.next = queue.pending.next;
queue.pending.next = update; // 将队列指向最新的 update
}
// 将当前队列中的 pendding 指向本次更新创建的update
queue.pending = update
// 触发一次更新
schedule()
}
-
先看
setState,也就是dispatchAction在这里创建了update也是一个链表结构的 因为有可能在一次更新中会调用多次更新,这里用链表收集 -
在
useState中我们用了baseState去记录当前的状态,并且利用当前节点queue.queue.pending有无值来判断当前是需要更新还是...
调用 onClick 后 可以看到 pending 用来收集更新的 action 依次调用,结构为环状链表
useRef
function useRef(initialValue) {
let hook;
if (isMount) {
hook = mountWorkInProgressHook();
hook.memoizedState = {
current: initialValue
}
} else {
hook = updateWorkInProgressHook()
}
return hook.memoizedState
}
useRef 的代码很简单:如果当前是mount阶段 创建{current: initValue},否则 取到内存中的 这块对象
因为每个useRef只会创建一个相同的对象 也就是 {current: initValue} 所以说ref在组件的生命周期内引用不变
useMemo
// 用来判断前后依赖性是否一样
function areHookInputsEqual(nextDeps, prevDeps) {
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
/**
* 如果说两个数组长度不一样 则只会去判断共有的长度的数组值
*
* 也就是说 第一次为 [1, 2] 第二次为 [1] 则不会去更新
*
* 第一次为[1, 2] 第二次为[2]会去更新
*/
if (Object.is(nextDeps[i], prevDeps[i])) {
// 如果两个值是一样的 ,跳出本次循环
continue;
}
// 如果不一样返回false
return false;
}
// 遍历完还没跳出函数 则两个deps为一样的 返回true
return true;
}
function useMemo(nextCreate, deps) {
let hook;
const nextDeps = deps === undefined ? null : deps; //默认不传递第二个参数会当null处理
if (isMount) {
hook = mountWorkInProgressHook();
} else {
hook = updateWorkInProgressHook();
const prevState = hook.memoizedState;
if (prevState[0] !== null && nextDeps !== null) {
// 如果上一次的值和这一次的依赖项不是null
const prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果一样,返回上一次的值
return prevState[0];
}
}
}
// 如果是mount 阶段或者是 更新阶段值发生变化了 则调用本次的fn 同时更新fiber中存储的状态和依赖
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
useCallback
// useCallback和useMemo的区别就是一个会吧函数调用之后的结果存下来 一个会存函数本身
function useCallback(nextCreate, deps) {
let hook;
const nextDeps = deps === undefined ? null : deps; //默认不传递第二个参数会当null处理
if (isMount) {
hook = mountWorkInProgressHook();
} else {
hook = updateWorkInProgressHook();
const prevState = hook.memoizedState;
if (prevState[0] !== null && nextDeps !== null) {
// 如果上一次的值和这一次的依赖项不是null
const prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果一样,返回上一次的值
return prevState[0];
}
}
}
// 如果是mount 阶段或者是 更新阶段值发生变化了 则调用本次的fn 同时更新fiber中存储的状态和依赖
hook.memoizedState = [nextCreate, nextDeps];
return hook.memoizedState[0];
}
完整代码
有兴趣的可以去自己试一试写一写
let isMount = true;
let workInProgressHook = null; // 代表着当前工作的节点
const fiber = {
stateNode: App, //当前这个节点
memoizedState: null // 初始值
}
// 用来获取初始化阶段的hook 同时将workInprogressHook 指针指向当前的hook
function mountWorkInProgressHook() {
let hook;
hook = {
next: null, // 代表的下一个 hook
queue: { // 是一个队列的原因是因为 我们有可能在一次更新调用多次更新hook操作
pending: null, // 保存改变的状态
}
}
if (!fiber.memoizedState) {
// 如果是第一个hook
fiber.memoizedState = hook; //将fiber的初始值设置为当前的hook
} else {
// 如果不是第一个hook 此时workInProgress代表上一次的hook
workInProgressHook.next = hook // 给当前节点的 next 添加本次的 hook ps: 因为存在着互相引用的关系 这里给workInProgressHook添加next 根fiber会更改
}
workInProgressHook = hook // 将当前工作的节点更改为现在的hook
return hook
}
// 用来获取更新阶段的hook 同时将workInprogressHook 指针指向当前的hook
function updateWorkInProgressHook() {
let hook;
hook = workInProgressHook; // 更新阶段可以直接从workInprogressHook里取当前的hook
workInProgressHook = workInProgressHook.next // 当前工作节点hook后移
return hook
}
// 用来模拟更新组件
function schedule() {
workInProgressHook = fiber.memoizedState; // 每次重新执行将当前的工作进度指向 根 fiber 的 memoizedState
const app = fiber.stateNode(); // 模拟组件重新执行
isMount = false
return app
}
function useState(initialState) {
let hook;
if (isMount) {
// 如果是初始化阶段, 初始化 当前这个 hook
hook = mountWorkInProgressHook()
hook.memoizedState = initialState; // 设置初始值
} else {
// 更新阶段 这里只是将最新的hook
hook = updateWorkInProgressHook()
}
let baseState = hook.memoizedState; // 当前的状态
s
if (hook.queue.pending) {
// 表示是需要更新 state
let firstUpdate = hook.queue.pending; // 这个变量表示当前移动到的action位置
do {
const action = firstUpdate.action; // 取到当前的action逻辑
if (typeof action !== 'function') {
baseState = action // 如果传递过来的是值 则直接赋值给baseState
} else {
baseState = action(baseState) // action 传入上一次的值 计算出新的值
}
firstUpdate = firstUpdate.next // 移动当前action指向的位置
} while (firstUpdate !== hook.queue.pending); // 如果当前的update是指向的第一个update 则表示遍历完毕 (用到了下面创建update为环状链表)
hook.queue.pending = null; // 清除本次调用后存储的更新
}
hook.memoizedState = baseState; //更改本次值为更新后的值
return [
baseState,
dispatchAction.bind(null, hook.queue)
]
}
// setState
function dispatchAction(queue, action) {
// 本次更新
const update = {
action, // 状态改变的action
next: null
}
// 这里存储为环状链表 会在上面用到判断
if (queue.pending === null) {
// 表示在本次更新 当前这个state是第一次更新
update.next = update; // 环状链表 u0.next -> u0
} else {
// u1 -> u0 -> u1
// 不是第一次更新 就将本次的更新插入到队列的最前面,此时update是收集了之前更新后最新的 链表
update.next = queue.pending.next;
queue.pending.next = update; // 将队列指向最新的 update
}
// 将当前队列中的 pendding 指向本次更新创建的update
queue.pending = update
// 触发一次更新
schedule()
}
function useRef(initialValue) {
let hook;
if (isMount) {
hook = mountWorkInProgressHook();
hook.memoizedState = {
current: initialValue
}
} else {
hook = updateWorkInProgressHook()
}
return hook.memoizedState
}
// 用来判断前后依赖性是否一样
function areHookInputsEqual(nextDeps, prevDeps) {
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
/**
* 如果说两个数组长度不一样 则只会去判断共有的长度的数组值
*
* 也就是说 第一次为 [1, 2] 第二次为 [1] 则不会去更新
*
* 第一次为[1, 2] 第二次为[2]会去更新
*/
if (Object.is(nextDeps[i], prevDeps[i])) {
// 如果两个值是一样的 ,跳出本次循环
continue;
}
// 如果不一样返回false
return false;
}
// 遍历完还没跳出函数 则两个deps为一样的 返回true
return true;
}
function useMemo(nextCreate, deps) {
let hook;
const nextDeps = deps === undefined ? null : deps; //默认不传递第二个参数会当null处理
if (isMount) {
hook = mountWorkInProgressHook();
} else {
hook = updateWorkInProgressHook();
const prevState = hook.memoizedState;
if (prevState[0] !== null && nextDeps !== null) {
// 如果上一次的值和这一次的依赖项不是null
const prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果一样,返回上一次的值
return prevState[0];
}
}
}
// 如果是mount 阶段或者是 更新阶段值发生变化了 则调用本次的fn 同时更新fiber中存储的状态和依赖
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
// useCallback和useMemo的区别就是一个会吧函数调用之后的结果存下来 一个会存函数本身
function useCallback(nextCreate, deps) {
let hook;
const nextDeps = deps === undefined ? null : deps; //默认不传递第二个参数会当null处理
if (isMount) {
hook = mountWorkInProgressHook();
} else {
hook = updateWorkInProgressHook();
const prevState = hook.memoizedState;
if (prevState[0] !== null && nextDeps !== null) {
// 如果上一次的值和这一次的依赖项不是null
const prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果一样,返回上一次的值
return prevState[0];
}
}
}
// 如果是mount 阶段或者是 更新阶段值发生变化了 则调用本次的fn 同时更新fiber中存储的状态和依赖
hook.memoizedState = [nextCreate, nextDeps];
return hook.memoizedState[0];
}
function App() {
const [num, updateNum] = useState(0);
console.log('num: ' + num)
return {
onClick() {
updateNum(pre => pre + 1);
},
}
}
window.app = schedule()
缺陷
- 上面手动实现
hook不能达到批量更新的效果。当多次调用updateNnm会多次render - 没有优先级的情况,不能中断渲染