本系列会实现一个简单的react
,包含最基础的首次渲染,更新,hook
,lane
模型等等,本文是本系列的第一篇。这对于我也是一个很大的挑战,不过这也是一个学习和进步的过程,希望能坚持下去,一起加油!期待多多点赞!😘😘
本文致力于实现一个最简单的优化策略执行过程,代码均已上传至github
,期待star!✨:
本文是系列文章,阅读的联系性非常重要!!
手写mini-react!超万字实现mount首次渲染流程🎉🎉
进击的hooks!实现react(反应)(反应)中的hooks架构和useState 🚀🚀
面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭
react(反应)(反应)能力值+1!useTransition是如何实现的?
面试官惊呼内行!万字解析React Suspense实现原理 🚀🚀
面试被问react性能优化?直接实现 bailout 和 eagerState 优化策略 🚀🚀
期待点赞!😁😁
食用前指南!本文涉及到react的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!
上一篇文章使用bailout
策略和eagerState
策略实现了更新时减少多余的渲染次数。在react中同样提供了很多可以使开发者手动控制的优化手段。
React.memo
js
import { memo } from 'react';
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
memo
用来缓存组件的渲染,避免不必要的更新。
在一般情况下,只要父组件的某一个状态改变了,无论是否对子组件进行更新,所有子组件都会重新再次进行渲染,因此,这时候就需要使用React.memo
对当前组件进行缓存。
memo
可以检测从父组件接收的props
,并且在父组件改变state
的时候对比这个state
是否是本组件在使用,如果不是,不会重新触发渲染。
js
import React from 'react';
// 使用memo包裹
const MyComponent = React.memo(({ name }) => {
return <div>{name}</div>;
});
// 在父组件中使用
function ParentComponent() {
const [name, setName] = React.useState('mimi');
const handleClick = () => {
setName('Gu');
};
return (
<div>
<MyComponent name={name} />
<button onClick={handleClick}>change</button>
</div>
);
}
memo
实现的本质:在子组件与父组件之间增加一个MemoComponent
,MemoComponent
通过 「props的浅比较」 命中bailout
策略。
由于我们本质上是用memo
包裹需要性能优化的组件,所以memo
作为一个高阶组件使用。在react创建fiber
树时也会作为一个节点存在,所以新增一个$$typeof
类型REACT_MEMO_TYPE
用来标记memo
的element
节点:
函数组件作为type
属性传入,由于函数组件的函数体也是保存在函数组件element
对象的type
属性,所以相当于形成了对象type
属性的嵌套,所以在memo
节点上获取函数体需要使用memo.type.type
来获取。
js
export function memo(
type,
compare
) {
const fiberType = {
$$typeof: REACT_MEMO_TYPE,
type,
compare: compare === undefined ? null : compare
};
// memo fiber.type.type
return fiberType;
}
在创建fiber
节点时,创建MemoComponent
类型的节点,代表memo
组件。
js
export const MemoComponent = 15;
export function createFiberFromElement(element) {
const { type, key, props, ref } = element;
let fiberTag: WorkTag = FunctionComponent;
if (typeof type === 'string') {
// <div/> type: 'div'
fiberTag = HostComponent;
} else if (typeof type === 'object') {
// memo组件节点
switch (type.$$typeof) {
case REACT_MEMO_TYPE:
fiberTag = MemoComponent;
break;
default:
console.warn('未定义的type类型', element);
break;
}
}
// ...
else if (typeof type !== 'function' && __DEV__) {
console.warn('为定义的type类型', element);
}
// 创建fiber对象
const fiber = new FiberNode(fiberTag, props, key);
fiber.type = type;
fiber.ref = ref;
return fiber;
}
进入render
阶段后,根据不同的节点类型进入不同的处理逻辑。如果处理到memo
组件类型的节点,调用updateMemoComponent
函数处理,主要是判断props
是否发生变化。
js
export const beginWork = (wip, renderLane) => {
// bailout策略
didReceiveUpdate = false;
const current = wip.alternate;
// ...
// 比较,返回子fiberNode
switch (wip.tag) {
case HostRoot:
return updateHostRoot(wip, renderLane);
case HostComponent:
return updateHostComponent(wip);
case HostText:
return null;
case FunctionComponent:
return updateFunctionComponent(wip, wip.type, renderLane);
case Fragment:
return updateFragment(wip);
// ...
// memo组件类型
case MemoComponent:
return updateMemoComponent(wip, renderLane);
// ...
}
return null;
};
取出current
树的props
,与本次更新的props
进行浅比较。如果props
属性没有变化并且该fiber
节点上没有待执行的更新任务,命中性能优化策略,对子fiber
树进行复用。如果没有命中,正常进行FunctionComponent
类型节点的处理逻辑。详细过程见:
js
function updateMemoComponent(wip, renderLane) {
// props浅比较
const current = wip.alternate;
const nextProps = wip.pendingProps;
const Component = wip.type.type;
if (current !== null) {
// 获取上次更新的props
const prevProps = current.memoizedProps;
// 浅比较props
if (shallowEqual(prevProps, nextProps) && current.ref === wip.ref) {
// 命中性能优化策略
didReceiveUpdate = false;
wip.pendingProps = prevProps;
// 没有更新任务
if (!checkScheduledUpdateOrContext(current, renderLane)) {
wip.lanes = current.lanes;
// 不再重新创建子fiber树,复用子树
return bailouOnAlreadyFinishedWork(wip, renderLane);
}
}
}
// 没有命中,正常执行函数组件的处理逻辑
return updateFunctionComponent(wip, Component, renderLane);
}
浅比较两个对象:
- 基本类型使用
Object.is
判断是否相同 - 对两个对象取键组成的数组
- 比较长度
- 遍历取值,挨个进行对比
js
export function shallowEqual(a, b) {
// 基本类型使用Object.is
if (Object.is(a, b)) {
return true;
}
if (
typeof a !== 'object' ||
a === null ||
typeof b !== 'object' ||
b === null
) {
return false;
}
// 取对象的键组成的数组
const keysA = Object.keys(a);
const keysB = Object.keys(b);
// 判断长度是否一致
if (keysA.length !== keysB.length) {
return false;
}
// 遍历每个属性
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
// b没有key、 key不相等
if (!{}.hasOwnProperty.call(b, key) || !Object.is(a[key], b[key])) {
return false;
}
}
return true;
}
Object.is()
方法用来判断两个值是否相等,接收两个参数,分别是需要比较的两个值。
不会进行类型转换,返回一个 Boolean
值这两个值是否相等。
js
Object.is(123, 123); // true
Object.is(123, '123'); // false
Object.is([], []); // false
Object.is(NaN, NaN); // true
同样是比较两个值是否相同, ==
比较两个值是否相等,如果两边的值不是同一个类型的话,会将他们转为同一个类型后再进行比较。
js
123 == '123'; // true
'' == false; // true
false == 0; // true
NaN == NaN; // false
而===
不会对类型进行转换,两边的值必须相等且类型相同才会等到 true
。对于 0 和 NaN 的比较。无论 0 的正负,他们都是相等的,而 NaN 是与任何值都不相等的,包括他本身。
js
+0 === -0; // true
0 === -0; // true
+0 === 0; // true
NaN === NaN; // false
而 Object.is()
会将 NaN 与 NaN 视为相等,无符号的 0 归为整数。
js
Object.is(0, +0); // true
Object.is(-0, 0); // false
Object.is(-0, +0); // false
Object.is(NaN, NaN); // true
在实现useMemo
与useCallback
两个hook之前,先来了解一下react中的hook架构。
数据共享层
hook
架构在实现时,脱离了react部分的逻辑,在内部实现了一个数据共享层,类似于提供一个接口。任何满足了规范的函数都可以通过数据共享层接入处理hook
的逻辑。这样就可以与宿主环境解耦,灵活性更高。
js
// 内部数据共享层
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
currentDispatcher
};
const currentDispatcher = {
current: null
};
currentDispatcher
为我们本次实现的hook
。
所以对应到我们的render
流程以及hook
的应用,他们之间的调用关系是这样的:
hook
怎么知道当前是mount
还是update
?
我们在使用hook
时,react在内部通过 currentDispatcher.current
赋予不同的函数来处理不同阶段的调用,判断hooks 是否在函数组件内部调用。
hook架构
hook
可以看做是函数组件和与其对应的fiber
节点进行沟通和的操作的纽带。在react
中处于不同阶段的fiber
节点会被赋予不同的处理函数执行hooks
:
- 初始化阶段 ----->
HookDispatcherOnMount
- 更新阶段 ----->
HookDispatcherOnUpdate
js
const HookDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect,
useRef: mountRef,
useMemo: mountMemo,
useCallback: mountCallback
};
const HookDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect,
useRef: updateRef,
useMemo: updateMemo,
useCallback: updateCallback
};
但是实现之前,还有几个问题需要解决:
如何确定fiber对应的hook上下文?
还记得我们在处理函数组件类型的fiber
节点时,调用renderWithHooks
函数进行处理,在我们在执行hook
相关的逻辑时,将当前fiber
节点信息保存在一个全局变量中:
js
// 当前正在render的fiber
let currentlyRenderingFiber = null;
js
export function renderWithHooks(wip) {
// 赋值操作
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
中,我们在处理hook
的初始化及更新逻辑中就可以获取到当前的fiber
节点信息。
hook是如何存在的?保存在什么地方?
注意hook
函数只存在于函数组件中,但是一个函数组件的fiber
节点时如何保存hook
信息呢?
答案是:memoizedState
。
fiber
节点中保存着非常多的属性,有作为构造fiber
链表,用于保存位置信息的属性,有作为保存更新队列的属性等等。
而对于函数组件类型的fiber
节点,memoizedState
属性保存hook
信息。hook
在初始化时,会创建一个对象,保存此hook
所产生的计算值,更新队列,hook
链表。
js
const hook = {
// hooks计算产生的值 (初始化/更新)
memoizedState: "";
// 对此hook的更新行为
updateQueue: "";
// hooks链表指针
next: null;
}
多个hook如何处理?
例如有以下代码:
js
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>
);
}
在某个函数组件中存在多个hook
,此时每个hook
的信息该如何保存呢?这就是上文中hook
对象中next
属性的作用,它是一个链表指针。在hook
对象中,next
属性指向下一个hook
。
换句话说,如果在一个函数组件中存在多个hook
,那么在该fiber
节点的memoizedState
属性中保存该节点的hooks
链表。
函数组件对应 fiber
用 memoizedState
保存 hook
信息,每一个 hook
执行都会产生一个 hook
对象,hook
对象中,保存着当前 hook
的信息,不同 hook
保存的形式不同。每一个 hook
通过 next
链表建立起关系。
useMemo & useCallback
useMemo
:理念与 memo
相同,都是判断是否满足当前的条件来决定是否执行callback
函数。在依赖不变的情况下,会返回相同的引用,避免子组件进行无意义的重复渲染。
返回值:更新之后的数据源,即 fn 函数的返回值,如果 deps 中的依赖值发生改变,将重新执行 fn,否则取上一次的缓存值。
js
const cacheData = useMemo(fn, deps)
js
import { useState, useMemo } from "react";
const usePow2 = (list) => {
return list.map((item) => {
console.log("我是usePow2");
return item * 2;
});
};
// 被useMemo包裹
const usePow = (list) => {
return useMemo(
() =>
list.map((item) => {
console.log(1);
return Math.pow(item, 2);
}),
[],
);
};
const Index = () => {
let [flag, setFlag] = useState(true);
const data = usePow([1, 2, 3]);
const data2 = usePow2([1, 2, 3]);
return (
<>
<div>数字集合:{JSON.stringify(data)}</div>
<div>数字集合2:{JSON.stringify(data2)}</div>
<button onClick={() => setFlag((v) => !v)}>
状态切换{JSON.stringify(flag)}
</button>
</>
);
};
export default Index;
可以看到,即使传入的参数没有改变,未被useMemo
包裹的函数依然会执行。
与 useMemo
用法一致,唯一不同的点在于,useMemo
返回的是值,而 useCallback
返回的是函数。
由于在更新函数组件时,归根结底是执行函数。所以每次执行FunctionComponent
都是对函数组件内部定义的函数重新执行。这时候react提供useCallback
这个hook对某些不想每次更新都执行的函数进行缓存。
js
const res = useCallback(fn, deps)
返回值:即 fn 函数,如果 deps 中的依赖值发生改变,将重新执行 fn,否则取上一次的函数。
用法:
js
const Index = () => {
let [count, setCount] = useState(0);
let [flag, setFlag] = useState(true);
const add = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<>
<button onClick={() => setCount((v) => v + 1)}>普通点击</button>
<button onClick={add}>useCallback点击</button>
<div>数字:{count}</div>
<button onClick={() => setFlag((v) => !v)}>
切换{JSON.stringify(flag)}
</button>
</>
);
};
useMemo
和useCallback
内部的实现非常相似:
初始化:
首先在mountWorkInProgressHook
内部创建hook对象,然后加入该fiber
节点的hook链表。useMemo
根据用户传入的函数计算结果缓存,而useCallback
直接保存函数。
js
// mountMemo
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;、
// 执行传入的nextCreate函数,保存结果
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
js
// mountCallback
function mountCallback<T>(
callback: T,
deps: Array<mixed> | void | null
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 直接返回用户传入的callback,保存执行函数
hook.memoizedState = [callback, nextDeps];
return callback;
}
在初始化中,useMemo
首先创建一个 hook
,加入到该fiber
的hook
链表中,然后判断 deps
的类型,执行 nextCreate
,这个参数是需要缓存的值,然后将值与 deps 保存到 memoizedState 上。
而 useCallback
直接将 callback和 deps 存入到 memoizedState 里。
更新:
js
// updateMemo
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
// 判断新值
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
//之前保存的值
const prevDeps: Array<mixed> | null = prevState[1];
// 与useEffect判断deps一致
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
useMemo
通过判断两次的 deps 是否发生改变,如果发生改变,则重新执行 nextCreate()
,将得到的新值重新复制给 memoizedState
保存;如果没发生改变,则直接返回缓存的值。
js
// updateCallback
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
//之前保存的值
const prevDeps: Array<mixed> | null = prevState[1];
// 与useEffect判断deps一致
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
// 对比
function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null || nextDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(prevDeps[i], nextDeps[i])) {
continue;
}
return false;
}
return true;
}
可以看到useMemo
只是直接将依赖函数执行之后进行保存,而updateCallback
直接将依赖函数保存。
写在最后 ⛳
经过差不多前后三个月的时间,终于把react系列的核心功能实现并且输出文章了,刚开始的时候并没想到能写这么多篇。真的是耗费了很大的心血。但是也受益良多。经过输出十几篇文章,对整个react的运行机制有了更深刻的理解。
无论做什么事情,开始行动就已经胜利了一半。一直空想只会让你停滞不前。
未来可能继续输出antd
源码解析系列文章,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳