跳到主要内容

模拟实现

2023年06月11日
柏拉文
越努力,越幸运

一、实现


1.1 /packages/react-reconciler/fiberHooks.js

function readContext(context) {
const consumer = currentlyRenderingFiber;
if (consumer == null) {
throw new Error('请在函数组件内调用 useContext');
}
const value = context._currentValue;
return value;
}

function use(useable) {
if (useable !== null && typeof useable === 'object') {
if (typeof useable.then === 'function') {
const thenable = useable;
return trackUsedThenable(thenable);
} else if (useable.$$typeof === REACT_CONTEXT_TYPE) {
const context = useable;
return readContext(context);
}
}

throw new Error('不支持的 use 参数' + useable);
}

/**
* @description: Mount 阶段 Hooks 实现
*/
const HooksDispatcherOnMount = {
use,
useRef: mountRef,
useState: mountState,
useEffect: mountEffect,
useContext: readContext,
useTransition: mountTransition
};

/**
* @description: Update 阶段 Hooks 实现
*/
const HooksDispatcherOnUpdate = {
use,
useRef: updateRef,
useState: updateState,
useEffect: updateEffect,
useContext: readContext,
useTransition: updateTransition
};

1.2 /packages/react-reconciler/thenable.js

let suspendedThenable = null;

export const SuspenseException = new Error(
'这不是真实的错误, 是 Suspense 用来中断渲染的'
);

function noop() {}

export function getSuspenseThenable() {
if (suspendedThenable === null) {
throw new Error('应该存在 suspendedThenable, 这是一个 Bug');
}
const thenable = suspendedThenable;
suspendedThenable = null;
return thenable;
}

export function trackUsedThenable(thenable) {
switch (thenable.status) {
case 'fulfilled':
return thenable.value;
case 'rejected':
throw thenable.reason;
default:
if (typeof thenable.status === 'string') {
thenable.then(noop, noop);
} else {
const pending = thenable;
pending.status = 'pending';
pending.then(
val => {
if (pending.status === 'pending') {
const fulfilled = pending;
fulfilled.status = 'fulfilled';
fulfilled.value = val;
}
},
error => {
if (pending.status === 'pending') {
const rejected = pending;
rejected.status = 'rejected';
rejected.reason = error;
}
}
);
}
break;
}

suspendedThenable = thenable;
throw SuspenseException;
}

1.3 /packages/react-reconciler/workLoop.js

function unwindUnitOfWork(unitOfWork) {
let incompleteWork = unitOfWork;

do {
const next = unwindWork(incompleteWork);
if (next !== null) {
workInProgress = next;
return;
}

const returnFiber = incompleteWork.return;
if (returnFiber != null) {
returnFiber.deletions = null;
}

incompleteWork = returnFiber;
} while (incompleteWork !== null);

workInProgress = null;
workInProgressRootExitStatus = RootDidNotComplete;
}

……

function throwAndUnwindWorkLoop(root, unitOfWork, thrownValue, lane) {
// 1. 重置 FunctionComponent 的全局变量
resetHooksOnUnwind();
// 2. use 请求返回后重新触发更新 / ErrorBoundary 捕获错误
throwException(root, thrownValue, lane);
// 3. unwind 流程
unwindUnitOfWork(unitOfWork);
}

……

function handleThrow(root, thrownValue) {
if (thrownValue === SuspenseException) {
thrownValue = getSuspenseThenable();
workInProgressSuspendedReason = SuspendedOnData;
}

workInProgressThrownValue = thrownValue;
}

……

/**
* @description: 防止进入死循环
*/
let retries = 0;

function renderRoot(root, lane, shouldTimesSlice) {
if (workInProgressRootRenderLane !== lane) {
prepareFreshStack(root, lane);
}

do {
try {
if (
workInProgressSuspendedReason !== NotSuspended &&
workInProgress !== null
) {
const thrownValue = workInProgressThrownValue;
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
throwAndUnwindWorkLoop(root, workInProgress, thrownValue, lane);
}

shouldTimesSlice ? workLoopConcurrent() : workLoopSync();
break;
} catch (e) {
console.log('workLoop renderRoot 发生错误', e);

retries++;

if (retries > 20) {
console.log('此时已经进入死循环,不再执行 workLoop');
break;
}

handleThrow(root, e);
}
} while (true);

if (workInProgressRootExitStatus !== RootInProgress) {
return workInProgressRootExitStatus;
}

if (shouldTimesSlice && workInProgress !== null) {
return RootInComplete;
}

if (!shouldTimesSlice && workInProgress !== null) {
console.log('renderRoot 结束之后 workInProgress 不应该存在');
}

return RootCompleted;
}

1.4 /packages/react-reconciler/fiberThrow.js

import { ShouldCapture } from './fiberFlags';
import { markRootPinged } from './fiberLanes';
import { getSuspenseHandler } from './suspenseContext';
import { markRootUpdated, ensureRootIsScheduled } from './workLoop';

function attachPingListener(root, wakeable, lane) {
let pingCache = root.pingCache;
let threadIDs;

if (pingCache === null) {
threadIDs = new Set();
pingCache = root.pingCache = new WeakMap();
pingCache.set(wakeable, threadIDs);
} else {
threadIDs = pingCache.get(wakeable);
if (threadIDs == undefined) {
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
}
}

if (!threadIDs.has(lane)) {
threadIDs.add(lane);

function ping() {
if (pingCache !== null) {
pingCache.delete(wakeable);
}

markRootPinged(root, lane);
markRootUpdated(root, lane);
ensureRootIsScheduled(root);
}

wakeable.then(ping, ping);
}
}

export function throwException(root, value, lane) {
// Error Boundary
// thenable
if (
value !== null &&
typeof value === 'object' &&
typeof value.then === 'function'
) {
const wakeable = value;
const suspenseBoundary = getSuspenseHandler();
if (suspenseBoundary) {
suspenseBoundary.flags |= ShouldCapture;
}

attachPingListener(root, wakeable, lane);
}
}

1.5 /packages/react-reconciler/fiberUnwindWork.js

import { popProvider } from './fiberContext';
import { popSuspenseHandler } from './suspenseContext';
import { ContextProvider, SuspenseComponent } from './workTags';
import { DidCapture, NoFlags, ShouldCapture } from './fiberFlags';

export function unwindWork(workInProgress) {
const flags = workInProgress.flags;

switch (workInProgress.tag) {
case SuspenseComponent:
popSuspenseHandler();

if (
(flags & ShouldCapture) !== NoFlags &&
(flags & DidCapture) === NoFlags
) {
workInProgress.flags = (flags & ~ShouldCapture) | DidCapture;
return workInProgress;
}
break;
case ContextProvider:
const context = workInProgress.type._context;
popProvider(context);
return null;
default:
return null;
}

return null;
}

二、测试


2.1 use

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Mini KaSong use</title>
<script>
var process = {
env: {
NODE_ENV: 'development'
}
};
</script>
<script src="../dist/react.iife.js"></script>
</head>
<body>
<div id="root"></div>

<script>
const {
ReactDOM: { createRoot },
React: { use, Suspense, createElement }
} = React;

const delay = t =>
new Promise(r => {
setTimeout(r, t);
});

const cachePool = [];

function fetchData(id, timeout) {
const cache = cachePool[id];
if (cache) {
return cache;
}

return (cachePool[id] = delay(timeout).then(() => {
return { data: Math.random().toFixed(2) * 100 };
}));
}

function Child() {
const value = use(fetchData(0, 3000));

return createElement('div', {}, value.data);
}

function App() {
return createElement(Child);
}

const root = createRoot(document.getElementById('root'));
root.render(createElement(App));
</script>
</body>
</html>

2.2 Suspense use

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Mini KaSong use</title>
<script>
var process = {
env: {
NODE_ENV: 'development'
}
};
</script>
<script src="../dist/react.iife.js"></script>
</head>
<body>
<div id="root"></div>

<script>
const {
ReactDOM: { createRoot },
React: { use, createElement }
} = React;

function request() {
return new Promise(resolve => {
setTimeout(() => {
resolve('hello world');
}, 1000);
});
}

function App() {
const value = use(request());

console.log(value);

return createElement(
'div',
{},
createElement('div', {}, 'hello world')
);
}

const root = createRoot(document.getElementById('root'));
root.render(createElement(App));
</script>
</body>
</html>