hey🖐! 我是小黄瓜😊😊。一枚小透明,期待关注➕ 点赞,共同成长~
写在前面
本系列会实现一个简单的react
,包含最基础的首次渲染,更新,hook
,lane
模型等等,本文是本系列的第一篇。这对于我也是一个很大的挑战,不过这也是一个学习和进步的过程,希望能坚持下去,一起加油!期待多多点赞!😘😘
本文致力于实现一个最简单的首次渲染流程,代码均已上传至github
,期待star!✨: github.com/kongyich/ti…
本文是系列文章,阅读的联系性非常重要!!
手写mini-react!超万字实现mount首次渲染流程🎉🎉
期待点赞!😁😁
食用前指南!本文涉及到react的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!
上文中我们已经是实现了一个最基本的单节点dom结构的jsx
的初始化渲染逻辑。接下来应该就实现节点更新的流程,但是在此之前我们需要先实现触发更新的“行为”。
我们的"mini-react"采用的是函数式组件的写法,不同于传统的类组件的写法,函数组件更加轻量,减少了很多样板代码。而且高阶组件可以很灵活的满足各种个性化配置需求。
在hooks
这一特性出来之前,函数式组件基本只能承载一些ui渲染的任务,所有的组件生命周期及状态变更只能在类组件中处理,函数组件只能被动的接收状态,而在hooks
这一特性诞生之后,函数组件也拥有了与类组件同样的能力。依托官方提供的各类hooks
,我们在函数组件中也可以处理各种状态。这也使得函数组件在react
开发中也逐渐占据主流。
接下来我们搭建一个hooks
架构,然后实现useState
以便于后续对jsx
进行更新。
一. useState基本用法
useState用于在函数组件中定义变量,具备类组件的 state
,让函数组件拥有更新视图的能力。
import { useState } from 'react'
const [state, setState] = useState(initial)
参数:
-
initial
参数有两种类型- 函数类型:将
initial
函数的执行结果作为state
的初始值 - 非函数类型:直接讲
initial
作为state
的初始值
- 函数类型:将
返回值:
-
state
数据源,用于渲染UI 层
的数据源
-
setState
改变数据源的函数,用于数据源状态的更新,类似于类组件的 this.setState
方法。
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('gua');
const [todos, setTodos] = useState(() => createTodos());
}
基本用法
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
add
</button>
);
}
这个例子会在点击add
按钮后为count
增加1。
此外setState
函数也可以接收一个函数。state
的值会作为参数传入setState
函数,返回结果将会作为新的值赋值给state
。
function handleClick() {
setCount(count => count + 1);
}
二. 数据共享层
hook
架构在实现时,脱离了react部分的逻辑,在内部实现了一个数据共享层,类似于提供一个接口。任何满足了规范的函数都可以通过数据共享层接入处理hook
的逻辑。这样就可以与宿主环境解耦,灵活性更高。
// 内部数据共享层
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
currentDispatcher
};
const currentDispatcher = {
current: null
};
currentDispatcher
为我们本次实现的hook
。
所以对应到我们的render
流程以及hooks
的应用,他们之间的调用关系是这样的:
hooks
怎么知道当前是mount
还是update
?
我们在使用hooks
时,react在内部通过**currentDispatcher.current**
赋予不同的函数来处理不同阶段的调用,判断hooks 是否在函数组件内部调用。
三. hooks
hooks
可以看做是函数组件和与其对应的fiber
节点进行沟通和的操作的纽带。在react
中处于不同阶段的fiber
节点会被赋予不同的处理函数执行hooks
:
-
初始化阶段 ----->
HookDispatcherOnMount
-
更新阶段 ----->
HookDispatcherOnUpdate
const HookDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect
};
const HookDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect
};
但是实现之前,还有几个问题需要解决:
如何确定fiber对应的hooks上下文?
还记得我们在处理函数组件类型的fiber
节点时,调用renderWithHooks
函数进行处理,在我们在执行hooks
相关的逻辑时,将当前fiber
节点信息保存在一个全局变量中:
// 当前正在render的fiber
let currentlyRenderingFiber = null;
export function renderWithHooks(wip: FiberNode) {
// 赋值操作
currentlyRenderingFiber = wip;
// 重置
wip.memoizedState = null;
const current = wip.alternate;
if (current !== null) {
// update
// hooks更新阶段
} else {
// mount
// hooks初始化阶段
}
const Component = wip.type;
const props = wip.pendingProps;
const children = Component(props);
// 重置操作
// 处理完当前fiber节点后清空currentlyRenderingFiber
currentlyRenderingFiber = null;
return children;
}
将当前正在处理的fiber
节点保存在全局变量currentlyRenderingFiber
中,我们在处理hooks
的初始化及更新逻辑中就可以获取到当前的fiber
节点信息。
hooks是如何存在的?保存在什么地方?
注意hooks
只存在于函数组件中,但是一个函数组件的fiber
节点时如何保存hooks
信息呢?
答案是:memoizedState
。
fiber
节点中保存着非常多的属性(参考上一篇文章),有作为构造fiber
链表,用于保存位置信息的属性,有作为保存更新队列的属性等等。
而对于函数组件类型的fiber
节点,memoizedState
属性保存hooks
信息。hook
在初始化时,会创建一个对象,保存此hook
所产生的计算值,更新队列,hooks
链表。
const hook = {
// hooks计算产生的值 (初始化/更新)
memoizedState: "";
// 对此hook的更新行为
updateQueue: "";
// hooks链表指针
next: null;
}
多个hook如何处理?
例如有以下代码:
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const [age, setAge] = useState(10);
function handleClick() {
setCount(count + 1);
}
function handleAgeClick() {
setCount(age + 18);
}
return (
<button onClick={handleClick}>
add
</button>
<button onClick={handleAgeClick}>
age
</button>
);
}
在某个函数组件中存在多个hooks
,此时每个hook
的信息该如何保存呢?这就是上文中hook
对象中next
属性的作用,它是一个链表指针。在hook
对象中,next
属性指向下一个hook
。
换句话说,如果在一个函数组件中存在多个hook
,那么在该fiber
节点的memoizedState
属性中保存该节点的hooks
链表。
函数组件对应 fiber
用 memoizedState
保存 hooks
信息,每一个 hooks
执行都会产生一个 hooks
对象,hooks
对象中,保存着当前 hooks
的信息,不同 hooks
保存的形式不同。每一个 hooks
通过 next
链表建立起关系。
对应我们上文的Counter
这个函数组件的fiber
节点与hooks
之间的关系:
四. useState
在上一篇介绍react初始化流程中,首先在render
阶段调用beginWork
开始构建fiber
节点,而对于不同的节点,react会进行不同的处理:
export const beginWork = (wip) => {
// 返回子fiberNode
switch (wip.tag) {
// 根节点
case HostRoot:
return updateHostRoot(wip);
// 原生dom节点
case HostComponent:
return updateHostComponent(wip);
// 文本节点
case HostText:
return null;
// 函数组件
case FunctionComponent:
return updateFunctionComponent(wip);
default:
if (__DEV__) {
console.warn('beginWork未实现的类型');
}
break;
}
return null;
};
针对函数节点,我们调用updateFunctionComponent
函数进行处理,在初始化时,主要任务是执行函数组件,然后生成此函数组件的fiber
节点,在上文中我们使用了两个函数来执行这两个任务:
function updateFunctionComponent(wip) {
// 执行函数组件,生成fiber节点
const nextChildren = renderWithHooks(wip);
// 根据fiber生成真实DOM节点
reconcilerChildren(wip, nextChildren);
return wip.child;
}
而针对于 renderWithHooks
函数来说,在我们加入hooks
相关的逻辑后,显然它需要被承载更重要的能力,就是根据不同的处理阶段来为**currentDispatcher.current**
赋值不同的hooks
函数,之所以会在这个函数中处理,是因为可以直接避免在其他无关的环境里调用hooks
函数。
currentDispatcher.current
在其他类型的fiber
节点被处理时值都为null
,这样就保证了hooks
函数只在函数组件中被调用。
// 定义useState函数
export const useState = (initialState) => {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
};
当currentDispatcher.current
为null时,说明当前并非函数组件。
const currentDispatcher = {
current: null
};
// 错误处理,当currentDispatcher.current为null时,说明当前并非函数组件
export const resolveDispatcher = () => {
const dispatcher = currentDispatcher.current;
if (dispatcher === null) {
throw new Error('hook只能在函数组件中执行');
}
return dispatcher;
};
在renderWithHooks
中依旧根据alternate
的存在判断当前为初始化/更新?
// 当前正在render的fiber
let currentlyRenderingFiber = null;
// 当前处理中的hooks
let workInProgressHook = null;
// 当前current树中的hooks
let currentHook = null;
export function renderWithHooks(wip) {
// 赋值操作
currentlyRenderingFiber = wip;
// 重置memoizedState
wip.memoizedState = null;
const current = wip.alternate;
if (current !== null) {
// update
currentDispatcher.current = HookDispatcherOnUpdate;
} else {
// mount
currentDispatcher.current = HookDispatcherOnMount;
}
// ....
// 重置操作
currentlyRenderingFiber = null;
workInProgressHook = null;
currentHook = null;
}
在renderWithHooks
函数中定义了三个全局变量:currentlyRenderingFiber
代表当前正在处理的fiber
节点。workInProgressHook
代表当前处理中的hooks
,也就是workInProgress
树中的。currentHook
代表当前的current
树中的hooks
。(上一篇文章双缓存树知识)
currentDispatcher.current
在初始化阶段和更新阶段分别被赋值给了HookDispatcherOnMount
和HookDispatcherOnUpdate
,分别执行不同的逻辑。
const HookDispatcherOnMount = {
useState: mountState
};
const HookDispatcherOnUpdate = {
useState: updateState
};
HookDispatcherOnMount
和HookDispatcherOnUpdate
是一个对象,useState
属性中保存真正的执行函数,这也就对应了在usestate
函数中的调用方式;
dispatcher.useState(initialState);
五. Mount阶段
根据useState
的使用规则,useState
返回一个数组。其中第一项为数据源,第二项为触发更新的函数,这也就意味着用户在调用第二个更新函数时,也需要触发react整体的更新流程。
而参数有两种形态,普通值或函数。后者需要将执行结果作为初始值。
到这里在mount
阶段的任务已经很明确了:
- 生成
hooks
链表,将当前hooks
加入到链表中 - 处理初始化数据,其中如果参数为函数,将执行结果作为初始值
- 构建该
hooks
的更新队列
mountWorkInprogressHook
函数用于构造在mount
阶段的hooks
链表,分为两种情况,如果workInProgressHook
为null
,说明当前还没有处理hooks
,当前为处理的第一个hooks
,则将workInProgressHook
赋值为当前的hooks
对象,最后保存hooks
链表到memoizedState
属性中。
如果workInProgressHook
有值,说明当前处理的是后续的hooks
,那么将当前 workInProgressHook
的next
指针指向当前hooks
,然后更新workInProgressHook
。
function mountWorkInprogressHook() {
const hook = {
memoizedState: null,
updateQueue: null,
next: null
};
if (workInProgressHook == null) {
// mount 时 第一个hook
if (currentlyRenderingFiber == null) {
throw new Error('请在函数组件内调用hook');
} else {
workInProgressHook = hook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
// mount后续的hook
workInProgressHook.next = hook;
workInProgressHook = hook;
}
return workInProgressHook;
}
createUpdateQueue
创建更新队列(详情上一篇)
const hook = mountWorkInprogressHook();
// 处理初始化数据
let memoizedState;
// 如果初始值为函数,执行,将返回值作为初始值
if (initialState instanceof Function) {
memoizedState = initialState();
} else {
memoizedState = initialState;
}
// 构建更新函数
const queue = createUpdateQueue();
hook.updateQueue = queue;
hook.memoizedState = memoizedState;
如上整个hooks
的初始化和初始值就已经处理完毕了,接下来还需要创建一个更新函数,由用户来触发,更新hooks
保存的值,并触发更新流程。
const dispatch = dispatchSetState().bind(null, currentlyRenderingFiber, queue);
// 保存更新函数
queue.dispatch = dispatch;
dispatchSetState
这个函数与react更新流程的触发方式相似,创建一个更新函数,加入更新队列,然后调用scheduleUpdateOnFiber
函数开启更新流程:
(更新相关的逻辑可查看上一篇文章)
dispatchSetState
函数在初始化及更新阶段都不会使用,是提供给用户调用的。
function dispatchSetState(
fiber,
updateQueue,
action
) {
// 创建更新任务
const update = createUpdate(action);
// 入队
enqueueUpdate(updateQueue, update);
scheduleUpdateOnFiber(fiber);
}
这里有一个地方需要注意一下,dispatchSetState
函数是使用bind
进行绑定的,这样做的好处是可以提前传递currentlyRenderingFiber
和queue
参数。
我们在使用useState
的时候是这样子的:
setCount(count => count + 1);
而使用bind
绑定的方式可以提前传递一些参数,而用户定义的副作用函数被当作第三个参数action
来处理。
至此,整个mountState
函数如下:
function mountState<State>(initialState) {
// 找到当前useState对应的hook数据
// 构建hooks链表
const hook = mountWorkInprogressHook();
// 处理初始化数据
let memoizedState;
if (initialState instanceof Function) {
memoizedState = initialState();
} else {
memoizedState = initialState;
}
// 构建更新函数
const queue = createUpdateQueue();
hook.updateQueue = queue;
hook.memoizedState = memoizedState;
const dispatch = dispatchSetState().bind(null, currentlyRenderingFiber, queue);
queue.dispatch = dispatch;
return [memoizedState, dispatch];
}
六. Update阶段
update
阶段也需要处理hooks
链表,只不过稍微有点区别,在mount
阶段中需要构造一个新的hooks
对象。而update
阶段我们需要从current
树中复用已有的hooks
对象。形成新的 hooks
链表关系。
-
当前
fiber
存在alternate
-
currentHook
的值为null
- 获取
current
树中原始的hooks
链表,获取memoizedState
属性,第一个hooks
- 获取
-
currentHook
有值-
currentHook.next
取后续的hooks
-
-
-
当前
fiber
不存在alternate
- 与初始化的逻辑类似
function updateWorkInprogressHook(): Hook {
let nextCurrentHook: Hook | null;
if (currentHook == null) {
// 第一个hook链表
const current = currentlyRenderingFiber?.alternate;
if (current !== null) {
nextCurrentHook = current?.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 后续的hook
nextCurrentHook = currentHook.next;
}
if (nextCurrentHook === null) {
throw new Error(
`组件${currentlyRenderingFiber?.type}本次执行时多了一个hook`
);
}
currentHook = nextCurrentHook as Hook;
// 复用原始的hooks
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
updateQueue: currentHook.updateQueue,
next: null
};
if (workInProgressHook == null) {
// mount 时 第一个hook
if (currentlyRenderingFiber == null) {
throw new Error('请在函数组件内调用hook');
} else {
workInProgressHook = newHook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
// mount后续的hook
workInProgressHook.next = newHook;
workInProgressHook = newHook;
}
return workInProgressHook;
}
其实从更新阶段的hooks
取值的逻辑也就不难看出,为什么在react中的hooks
函数不可以在条件语句中使用的原因了,如果我们有以下使用场景:
export default function App(){
let number, setNumber;
let isShow = true
if(isShow) {
const [number, setNumber] = useState(0)
}
const [count, setCount] = useState(10)
const [age, setAge] = useState(18)
}
isShow
使其中一个useState
变为有条件使用,所以如果某一次更新isShow
变为false
,那么更新时hooks
链表在复用时:
直接导致整条hooks
链表错乱。
hooks
中的memoizedState
也需要更新,执行processUpdateQueue
函数对保存在更新队列的函数。
// 计算更新新state的逻辑
const queue = hook.updateQueue;
const pending = queue.shared.pending;
if (pending !== null) {
// 执行更新函数
const { memoizedState } = processUpdateQueue(hook.memoizedState, pending);
// 执行后的新值,更新memoizedState属性
hook.memoizedState = memoizedState;
}
export const processUpdateQueue = (
baseState,
pendingUpdate
) => {
const result = {
memoizedState: baseState
};
if (pendingUpdate !== null) {
const action = pendingUpdate.action;
if (action instanceof Function) {
// baseState 1 update (x) => 4x -> memoizeState 4
result.memoizedState = action(baseState);
} else {
// baseState 1 update 2 -> memoizeState 2
result.memoizedState = action;
}
}
return result;
};
至此,整个更新阶段的hooks
函数就完成了:
function updateState() {
// 构建新的hooks链表
const hook = updateWorkInprogressHook();
// 计算更新新state的逻辑
const queue = hook.updateQueue;
const pending = queue.shared.pending;
if (pending !== null) {
const { memoizedState } = processUpdateQueue(hook.memoizedState, pending);
hook.memoizedState = memoizedState;
}
return [hook.memoizedState, queue.dispatch];
}
写在最后 ⛳
未来可能会更新实现mini-react
和antd
源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳