深入理解react——3. 函数组件与useState
前两篇文章已经将fiber的基本工作原理介绍完成,接下来我们添加函数式组件与useState,将miniReact正式完结。
一,函数组件
1.1 performUnitOfWork(工作单元计算)
从前两篇文章我们知道,每个组件经过编译后其实都是一个对象,那么函数组件自然也就是执行函数 return 出来的jsx对象,所以我们对原来的performUnitOfWork做一点修改。
export function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
// 3. 返回下一个工作单元
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
基本逻辑不变,只是做一个if的判断,将函数式组件与非函数式组件分开处理。这里值得注意的地方是,函数式组件的fiber.type就是函数本身。也就是React.createElement(MyComponent, { propKey: propValue })]
function updateFunctionComponent(fiber) {
console.log("updateFunctionComponent执行");
wipFiber = fiber;
wipFiber.hooks = [];
hookIndex = 0;
// TODO 更新函数组件
const children = [fiber.type(fiber.props)]; // 执行函数组件,直到此时,函数中的setState才会被调用
reconcileChildren(fiber, children);
}
function updateHostComponent(fiber) {
// 添加节点元素到dom
// 如果没有dom属性,根据fiber新构建
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// 遍历节点的children属性创建Fiber对象
const elements = fiber.props.children;
// 调和fiber对象,设置状态:添加、更新和删除
reconcileChildren(fiber, elements);
}
1.2 commitWork
另外在提交节点commitWork函数也需要做一些修改,因为函数式组件并没有对应的dom节点,所以新增和删除实际上需要向上或者向下递归寻找到真实的dom节点。
/**
* commitWork 函数
* 递归提交Fiber节点的DOM更新
* @param {Object} fiber - 当前的Fiber节点
*/
export function commitWork(fiber) {
if (!fiber) {
return;
}
// 函数组件本身没有dom属性,需要向上寻找
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom; // 获取父DOM节点
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}
if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}
if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
return;
}
commitWork(fiber.child); // 递归提交子节点
commitWork(fiber.sibling); // 递归提交兄弟节点
}
export function commitDeletion(fiber, domParent) {
// 找不到dom,继续向下寻找
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}
至此,函数式组件就添加完成了,我们来测试一下。
function MyComponent(props) {
return React.createElement(
"div",
null,
`hellow,${props.name}`
);
}
const element = React.createElement(MyComponent, { name: "World" })
render(element, rootDOM);
二,useState
上面我们已经添加了函数组件了,那么hook自然需要提上日程,不然函数组件就没法用。我们先添加最常用的useState。
export function useState(initial) {
console.log("useState执行");
const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
};
// 执行所有setState的回调函数
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action) => {
hook.state = action(hook.state);
});
const setState = (action) => {
// 推入队列
hook.queue.push(action);
// 将下一次任务设为当前根fiber
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
};
deletions = [];
nextUnitOfWork = wipRoot;
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
这里有几个非常值得注意的点,我们一个一个来说:
- 首先是执行时机,useState在函数组件中执行的时机其实是函数组件本身被执行它才被执行
- 那么我们可以回去看看处理函数组件组件fiber的时候做了什么,我们有一个全局的wipFiber指向的正是当前正在计算的fiber,所以上面的wipFiber.hooks.push(hook);实际上可以简单的看出每一个函数组件的hook都是存在当前组件的fiber中
- 另外由于useState函数本身并没有带什么特殊标识,在寻找oldHook时是凭借全局的hookIndex最后再做返回
return [hook.state, setState];。这也引出一个常问的面试问题,为什么hook不能放在if条件中使用。 - 还有一个问题,useState到底是同步还是异步的,从这里可以看出,函数肯定是同步执行,但是相对于渲染是异步的,因为实际state值的更改实际上是下一次fiber计算和构建时才会触发。如果将useState放在定时器中,那么他又将是在下一次事件循环中的宏任务中去执行。(所以看博客不能知其所以然的地方,亲自写过源码后才会更加的清晰。ps:听说更加高版本的react批处理已经把定时器的情况也考虑进去了,我这里没有了解过,有知道的大佬可以评论区分享一下)
到这里为止,我们的useState就算是完成了。老规矩,搞点测试。
function MyComponent(props) {
const [count, setCount] = useState(0);
const handleClick_1 = () => {
setCount(count + 1);
setCount(count + 1);
setCount((count) => count + 1);
console.log("handleClick_1 clicked:", count);
};
return React.createElement(
"div",
null,
React.createElement("button", { onClick: handleClick_1, className: "button1" }, `Count, ${count}`),
`hellow,${props.name}`
);
}
const element = React.createElement(MyComponent, {})
render(element, rootDOM);
大功告成,完成并理解useState后再添加别的hook就会变得简单很多。
三,useEffect
既然正在兴头上,那我们就再写一个useEffect
export function useEffect(callback, deps) {
console.log("useEffect执行");
const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
const hasChanged = !oldHook || !deps || deps.some((dep, i) => dep !== oldHook.deps[i]);
const hook = {
deps,
};
if (hasChanged) {
if (oldHook && oldHook.cleanup) {
oldHook.cleanup(); // 清理上一次的副作用
}
hook.cleanup = callback(); // 执行副作用,并保存清理函数
}
wipFiber.hooks.push(hook);
hookIndex++;
}
很简单就完成了,逻辑都是一样的,我们再来测试一下
function MyComponent(props) {
const [count, setCount] = useState(0);
const handleClick_1 = () => {
setCount(count + 1);
setCount(count + 1);
setCount((count) => count + 1);
console.log("handleClick_1 clicked:", count);
};
useEffect(()=>{
console.log("我监听到count的变化了",count)
return ()=>{
console.log("我正在执行清理")
}
},[count])
return React.createElement(
"div",
null,
React.createElement("button", { onClick: handleClick_1, className: "button1" }, `Count, ${count}`),
`hellow,${props.name}`
);
}
const element = React.createElement(MyComponent, {})
render(element, rootDOM);
至此,深入理解react这个小专栏到这里就算是结束了,源码请查看:github.com/time-is-cod…