让我们来手写一个简易版react hooks
前言
今天开始和大家一起深入的学习和了解react相关知识;从useState简易版源码开始
了解react源码我们能学到什么
- 性能相关:例如;对代码进行优化再也不会无从下手,不使用相关优化API也可以写出性能很好的代码,chrome dev tools performance需要对源码流程熟悉才可以顺利的定位问题;
- 使用子库:例如,有用到时间分片相关的功能可以看下scheduler这个库
- 设计模式:对框架设计模式有更深入的认识,在前端中我们所说的MVC是 Model-View-Controller,严格说这三个加起来以后才是三层架构中的UI层,Model代表的是数据的变化也就是state的变化,V就是视图,Controller负责调度 model 和 view,在react中我们的C实际上是react中schedule这个阶段,通过setState触发schedule阶段进而触发视图的更新
- 面试:面试会问
- 解决开发中的疑惑,例;为什么引入我们没有使用的createElement,为什么会有hook规则
hooks理念
hooks的实现就是为了践行代数效应,将副作用从我们的函数组件中进行抽离 ,使我们的函数组件变的纯粹
function getTotalUser(tenantIdA, tenantIdB) {
const num1 = getUser(user1);
const num2 = getUser(user2);
return num1 + num2;
}
getTotalUser(xiangrong,xiangwang)
// 因为getUser是异步请求,为了保证getTotalUser的调用方式不变,我们想到了async await,但它是具有传染性的
function TotalUser(tenantIdA, tenantIdB) {
const num1 = useGetUser(user1);
const num2 = useGetUser(user2);
return num1 + num2;
}
function useGetUser(id) {
const [num, setNum] = useState(0);
useEffect(() => {
request(id).then((res)=>{
setNum(res)
})
}, [id]);
return num;
}
// 我们通过hooks实现TotalUser,发现TotalUser并不区分是同步或是异步的
由于react的视图与数据不是一一对应的,所以要遵守Hook 规则,同样的vue与solid中的hooks就可以在条件判断中使用,Hook规则是react的问题,我们可以这么理解,vue比起react更好的践行了代数效应;
useState有什么特性
function fn() {
const [num, setNum] = useState(0);
// useState是一个方法,接收一个任意形式的参数,并返回一个数组
// 不能在条件判断或方法内执行useState方法
const [num1, setNum1] = useState(10);
window.updateNum = setNum;
// 调用window.updateNum(1)可以更新num
useEffect(() => {
request(id).then((res)=>{
setNum(res)
setNum(res+1)
setNum(num=>num+1)
setNum1(res)
// 同一个setState可以多次调用
})
setTimeout(()=>{
setNum(num+1) // 一秒中点击五次的结果实际还是1,换成setNum(num=>num+1)就会达成我们想要的效果
},1000)
}, []);
}
hooks如何保存数据
FunctionComponent的 render本身只是函数调用。那么在render内部调用的hook是如何获取到对应数据呢?
每个组件有个对应的fiber节点(可以理解为虚拟DOM),用于保存组件相关信息,每次render时,全局变量currentlyRenderingFiber都会被赋值为该FunctionComponent对应的fiber节点。所以,hook内部其实是从currentlyRenderingFiber中获取状态信息的。
useState简易实现
大体思路:首次render之后,在我们调用setState时函数组件App将会重新渲染,即重新调用了App函数,两次调用函数组件App不同之处是,第一次属于挂载(mount),第二次则属于更新(update),也就是说两次调用useState的含义也是不同的,说白了就是有两个大分支,第一次走mount,后面都走update,update时会调用schedule及其以后的阶段;
let isMount = true; // 模拟组件生命周期
let workInprogressHook = null; // 工作的任务链表,指针指向当前处理的hook
const fiber = {
stateNode: App, // 保存实例本身
memoizedState: null, // 保存的是一个链表,保存的是每一个hook对应的数据,当前头节点指向null
};
/** 阅读顺序四 start */
// 调度-->把不同优先级的更新进行排序
function schedule() {
workInprogressHook = fiber.memoizedState; // 这部分是把workInprogressHook运行的指针重新指向第一个memoizedState
const app = fiber.stateNode(); // 对应render阶段
// todo render commit
isMount = false; // 首次调用之后变为update模式,源码中hooks调用时,mount阶段和update阶段调用的是两个方法
return app;
}
/** 阅读顺序四 end */
// 计算一个新的状态,并返回一个改变当前状态的方法 return [state, setState]
/** 阅读顺序一 start */
function useState(initialState) {
let hook;
// 在mount时每一个useState都会调用这个方法创建一个hook,通过next指针把所有useState关联起来形成一个链表
if (isMount) {
// 首次渲染的时候fiber.memoizedState的指针指向null ,因此我们需要创建一个hook
hook = {
memoizedState: initialState, // 链表的头节点是我们传入的参数
next: null, // 可以理解为链表的下一个指针指向null
queue: {
// 保存的是改变的状态,可能一个setState调用多次,所以用链表保存,
pending: null, // 当前头节点指向null
},
};
// 如果指针指向null,证明当前fiber.memoizedState对象里面没有hook节点
if (fiber.memoizedState === null) {
fiber.memoizedState = hook; // 那么就把我们创建的hook当作头节点
workInprogressHook = hook; // 当前处理的hook就是我们定义的hook,workInprogressHook在这边被初始化为一个链表
} else {
// 如果不是第一个调用用的hook,就把下一个任务存到我们的工作任务链表内
workInprogressHook.next = hook;
// 此时workInprogressHook的结构是这样的
/*workInprogressHook={
memoizedState: initialState,
next: {
memoizedState: initialState,
next: {...},
queue: {
pending: null,
},
},
queue: {
pending: null,
},
}*/
}
} else {
// 此时update阶段我们已经有一个hook的链表,把工作链表赋值给hook,然后让当前的workInprogressHook指向下一个工作任务
hook = workInprogressHook;
workInprogressHook = workInprogressHook.next; // 这步就相当于清除头节点,把最外层对象干掉,因为执行完了就不再保留当前任务,所以指向下一个任务
}
/** 阅读顺序一 end */
/** 阅读顺序三 start */
// 当前任务的基本状态,如果是第一次的时候,baseState就是我们传的参数
let baseState = hook.memoizedState;
if (hook.queue.pending) {
// 找到第一次的update
let firstUpdate = hook.queue.pending.next;
do {// while是先判断条件再执行循环体,do while是先执行循环体再判断条件
const action = firstUpdate.action;
baseState = action(baseState); // 此时是调用完一次setState后的值了
firstUpdate = firstUpdate.next; // 当前状态更新完,把指针指向下一个setState,用于setState的链式调用,比如setNumber(1);setNumber(2);
} while (firstUpdate !== hook.queue.pending.next); // 遍历完整个链表,hook.queue.pending.next此时指向空
// 将链表清空
hook.queue.pending = null;
}
// 更改当前状态
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
/** 阅读顺序三 end */
}
// 就是setState方法,源码中的useState实际上调用的useReducer,只不过useState会给useReducer一个默认的reducer,而调用useReducer需要我们自己创建
/** 阅读顺序二 start */
function dispatchAction(queue, action) {
// 考虑到优先级的问题所以update这边是个环状列表
const update = {
action, // 就是我们自己写的updateNum的参数
next: null,
};
// 代表当前没有能触发的更新,那么我们创建的update就是将要触发的第一个更新
if (queue.pending === null) {
// 自己指向自己,形成一个环状链表 head->head
// update = {
// action,
// next: {
// action,
// next: {...},
// },
// };
update.next = update;
} else {
// 插入链表的操作queue.pending是最后一个update,所以他的next指向第一个update
// head->update->head 插入节点形成环状列表
// 假设第一次走完if
// queue.pending = {
// action:oldAction,
// next: {
// action:oldAction,
// next: null,
// },
// };
// 此时的update = {
// action,
// next: null,
// };
update.next = queue.pending.next;
// 此时update = {
// action:newAction,
// next: {
// action:oldAction, 上一次的更新
// next: null,
// },
// };
queue.pending.next = update;
// 此时queue.pending = {
// action:oldAction,
// next: {
// action:newAction,
// next: {
// action:oldAction, 上一次的更新
// next: null,
// },
// };
// };
// 这样就构成了一个双向链表
}
// 每次这个方法创建的update都是这个环状链表的最后一个更新
// 此时queue.pending = {
// action:newAction,
// next: {
// action:oldAction, 上一次的更新
// next: null,
// },
// };
queue.pending = update;
/** 阅读顺序二 end */
schedule();
}
function App() {
const [num, updateNum] = useState(0);
const [num1, updateNum1] = useState(10);
return {
onClick() {
console.log(1)
updateNum((num) => num + 1);
},
onClick1() {
updateNum1((num) => num + 10);
},
};
}
window.app = schedule();
转载自:https://juejin.cn/post/7078270117808701470