diff --git a/README.md b/README.md
index 774c215..8098776 100644
--- a/README.md
+++ b/README.md
@@ -6,9 +6,8 @@
- 💪 功能完备,当前可跑通官方测试用例数量:34
- 🚶 按`Git Tag`划分迭代步骤,记录从 0 实现的每个功能
-如果想加入项目对应的`源码交流群`,和 7000+小伙伴们一起交流`React`,可以加我微信,备注「开发」:
+如果想跟着我学习「如何从0到1实现React18」,可以[点击这里](https://qux.xet.tech/s/2wiFh1)
-
## TODO List
diff --git a/demos/fragment/index.html b/demos/fragment/index.html
new file mode 100644
index 0000000..e14a680
--- /dev/null
+++ b/demos/fragment/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ v11测试并发更新
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/fragment/main.tsx b/demos/fragment/main.tsx
new file mode 100644
index 0000000..b793fed
--- /dev/null
+++ b/demos/fragment/main.tsx
@@ -0,0 +1,23 @@
+import { useState, useEffect } from 'react';
+import { createRoot } from 'react-dom/client';
+
+function App() {
+ const [num, update] = useState(0);
+ function onClick() {
+ update(num + 1);
+ }
+
+ const arr =
+ num % 2 === 0
+ ? [a, b, d]
+ : [d, c, b];
+
+ return (
+
+ );
+}
+
+createRoot(document.getElementById('root') as HTMLElement).render();
diff --git a/demos/fragment/vite-env.d.ts b/demos/fragment/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/demos/fragment/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/demos/noop-renderer/index.html b/demos/noop-renderer/index.html
new file mode 100644
index 0000000..5d0dbe9
--- /dev/null
+++ b/demos/noop-renderer/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ noop-renderer测试
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/noop-renderer/main.tsx b/demos/noop-renderer/main.tsx
new file mode 100644
index 0000000..dac0cc7
--- /dev/null
+++ b/demos/noop-renderer/main.tsx
@@ -0,0 +1,21 @@
+import { useState, useEffect } from 'react';
+import * as ReactNoop from 'react-noop-renderer';
+
+const root = ReactNoop.createRoot();
+
+function Parent() {
+ return (
+ <>
+
+ hello world
+ >
+ );
+}
+
+function Child() {
+ return 'Child';
+}
+
+root.render();
+
+window.root = root;
diff --git a/demos/noop-renderer/vite-env.d.ts b/demos/noop-renderer/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/demos/noop-renderer/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/demos/ref/index.html b/demos/ref/index.html
new file mode 100644
index 0000000..5d0dbe9
--- /dev/null
+++ b/demos/ref/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ noop-renderer测试
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/ref/main.tsx b/demos/ref/main.tsx
new file mode 100644
index 0000000..801c24e
--- /dev/null
+++ b/demos/ref/main.tsx
@@ -0,0 +1,25 @@
+import { useState, useEffect, useRef } from 'react';
+import { createRoot } from 'react-dom/client';
+
+function App() {
+ const [isDel, del] = useState(false);
+ const divRef = useRef(null);
+
+ console.warn('render divRef', divRef.current);
+
+ useEffect(() => {
+ console.warn('useEffect divRef', divRef.current);
+ }, []);
+
+ return (
+ del(true)}>
+ {isDel ? null : }
+
+ );
+}
+
+function Child() {
+ return console.warn('dom is:', dom)}>Child
;
+}
+
+createRoot(document.getElementById('root') as HTMLElement).render();
diff --git a/demos/ref/vite-env.d.ts b/demos/ref/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/demos/ref/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/demos/suspense/component.tsx b/demos/suspense/component.tsx
new file mode 100644
index 0000000..278e6f9
--- /dev/null
+++ b/demos/suspense/component.tsx
@@ -0,0 +1,18 @@
+import { useState, useEffect } from 'react';
+
+export default function Comp() {
+ const [v, setv] = useState(1);
+ useEffect(() => {
+ console.log('acomp, ', v);
+ }, [v]);
+ return (
+ {
+ console.log('acomp, click');
+ setv(v + 1);
+ }}
+ >
+ async component - {v}
+
+ );
+}
diff --git a/demos/suspense/index.html b/demos/suspense/index.html
new file mode 100644
index 0000000..8869d84
--- /dev/null
+++ b/demos/suspense/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ Suspense
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/suspense/main.tsx b/demos/suspense/main.tsx
new file mode 100644
index 0000000..281b292
--- /dev/null
+++ b/demos/suspense/main.tsx
@@ -0,0 +1,36 @@
+import { useState, useEffect, lazy, Suspense } from 'react';
+import { createRoot } from 'react-dom/client';
+
+const delay = (t: number) =>
+ new Promise((r) => {
+ setTimeout(r, t);
+ });
+
+const Comp = lazy(() =>
+ import('./component').then((res) => {
+ return delay(1000).then(() => {
+ console.log('ready render Comp');
+ return res;
+ });
+ })
+);
+
+function App() {
+ const [num, setNum] = useState(0);
+ console.log('num', num);
+ return (
+
+
+ loading...
}>
+
+
+
+
+ );
+}
+
+function Child({ i }) {
+ return i am child {i}
;
+}
+
+createRoot(document.getElementById('root') as HTMLElement).render();
diff --git a/demos/suspense/vite-env.d.ts b/demos/suspense/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/demos/suspense/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/demos/v11/main.tsx b/demos/v11/main.tsx
index 70bfed0..e588260 100644
--- a/demos/v11/main.tsx
+++ b/demos/v11/main.tsx
@@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
function App() {
const [num, updateNum] = useState(0);
- const len = 1000;
+ const len = 8;
console.log('num', num);
return (
diff --git a/demos/vite.config.js b/demos/vite.config.js
index 5505acf..8ceff5b 100644
--- a/demos/vite.config.js
+++ b/demos/vite.config.js
@@ -22,10 +22,19 @@ export default defineConfig({
find: 'react-dom',
replacement: path.resolve(__dirname, '../packages/react-dom')
},
+ {
+ find: 'react-reconciler',
+ replacement: path.resolve(__dirname, '../packages/react-reconciler')
+ },
+ {
+ find: 'react-noop-renderer',
+ replacement: path.resolve(__dirname, '../packages/react-noop-renderer')
+ },
{
find: 'hostConfig',
replacement: path.resolve(
__dirname,
+ // '../packages/react-noop-renderer/src/hostConfig.ts'
'../packages/react-dom/src/hostConfig.ts'
)
}
diff --git a/package.json b/package.json
index b3a62f2..8badd58 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"build:dev": "rm -rf dist && rollup --config scripts/rollup/dev.config.js",
"demo": "vite serve demos/v11 --config demos/vite.config.js --force",
"lint": "eslint --ext .ts,.jsx,.tsx --fix --quiet ./packages",
- "test": "jest"
+ "test": "jest --config scripts/jest/jest.config.js"
},
"devDependencies": {
"@babel/core": "^7.18.6",
diff --git a/packages/react-dom/src/SyntheticEvent.ts b/packages/react-dom/src/SyntheticEvent.ts
index 40292b3..9cede2d 100644
--- a/packages/react-dom/src/SyntheticEvent.ts
+++ b/packages/react-dom/src/SyntheticEvent.ts
@@ -9,7 +9,7 @@ const { unstable_runWithPriority: runWithPriority } = Scheduler;
// 支持的事件类型
const validEventTypeList = ['click'];
-export const elementEventPropsKey = '__props';
+export const elementPropsKey = '__props';
type EventCallback = (e: SyntheticEvent) => void;
interface Paths {
@@ -17,20 +17,19 @@ interface Paths {
bubble: EventCallback[];
}
interface SyntheticEvent extends Event {
- type: string;
__stopPropagation: boolean;
}
-export interface PackagedElement extends Element {
- [elementEventPropsKey]: {
- [eventType: string]: EventCallback;
+export interface DOMElement extends Element {
+ [elementPropsKey]: {
+ [key: string]: any;
};
}
function createSyntheticEvent(e: Event): SyntheticEvent {
const syntheticEvent = e as SyntheticEvent;
syntheticEvent.__stopPropagation = false;
- const originStopPropagation = e.stopPropagation;
+ const originStopPropagation = e.stopPropagation.bind(e);
syntheticEvent.stopPropagation = () => {
syntheticEvent.__stopPropagation = true;
@@ -51,27 +50,8 @@ function getEventCallbackNameFromtEventType(
}
// 将支持的事件回调保存在DOM中
-export const updateFiberProps = (
- node: Element,
- props: any
-): PackagedElement => {
- (node as PackagedElement)[elementEventPropsKey] =
- (node as PackagedElement)[elementEventPropsKey] || {};
-
- validEventTypeList.forEach((eventType) => {
- const callbackNameList = getEventCallbackNameFromtEventType(eventType);
-
- if (!callbackNameList) {
- return;
- }
- callbackNameList.forEach((callbackName) => {
- if (Object.hasOwnProperty.call(props, callbackName)) {
- (node as PackagedElement)[elementEventPropsKey][callbackName] =
- props[callbackName];
- }
- });
- });
- return node as PackagedElement;
+export const updateFiberProps = (node: DOMElement, props: any) => {
+ (node as DOMElement)[elementPropsKey] = props;
};
const triggerEventFlow = (paths: EventCallback[], se: SyntheticEvent) => {
@@ -96,7 +76,7 @@ const dispatchEvent = (container: Container, eventType: string, e: Event) => {
}
const { capture, bubble } = collectPaths(
- targetElement as PackagedElement,
+ targetElement as DOMElement,
container,
eventType
);
@@ -115,7 +95,7 @@ const dispatchEvent = (container: Container, eventType: string, e: Event) => {
// 收集从目标元素到HostRoot之间所有目标回调函数
const collectPaths = (
- targetElement: PackagedElement,
+ targetElement: DOMElement,
container: Container,
eventType: string
): Paths => {
@@ -125,12 +105,12 @@ const collectPaths = (
};
// 收集事件回调是冒泡的顺序
while (targetElement && targetElement !== container) {
- const eventProps = targetElement[elementEventPropsKey];
- if (eventProps) {
+ const elementProps = targetElement[elementPropsKey];
+ if (elementProps) {
const callbackNameList = getEventCallbackNameFromtEventType(eventType);
if (callbackNameList) {
callbackNameList.forEach((callbackName, i) => {
- const eventCallback = eventProps[callbackName];
+ const eventCallback = elementProps[callbackName];
if (eventCallback) {
if (i === 0) {
// 反向插入捕获阶段的事件回调
@@ -143,7 +123,7 @@ const collectPaths = (
});
}
}
- targetElement = targetElement.parentNode as PackagedElement;
+ targetElement = targetElement.parentNode as DOMElement;
}
return paths;
};
diff --git a/packages/react-dom/src/hostConfig.ts b/packages/react-dom/src/hostConfig.ts
index ff64362..7bc6466 100644
--- a/packages/react-dom/src/hostConfig.ts
+++ b/packages/react-dom/src/hostConfig.ts
@@ -1,14 +1,15 @@
-import { PackagedElement, updateFiberProps } from './SyntheticEvent';
+import { DOMElement, updateFiberProps } from './SyntheticEvent';
import { FiberNode } from 'react-reconciler/src/fiber';
import { HostText } from 'react-reconciler/src/workTags';
-export type Container = PackagedElement;
-export type Instance = PackagedElement;
+export type Container = Element;
+export type Instance = DOMElement;
export type TextInstance = Text;
export const createInstance = (type: string, props: any): Instance => {
- const element = document.createElement(type);
- return updateFiberProps(element, props);
+ const element = document.createElement(type) as unknown;
+ updateFiberProps(element as DOMElement, props);
+ return element as DOMElement;
};
export const createTextInstance = (content: string) => {
diff --git a/packages/react-dom/src/root.ts b/packages/react-dom/src/root.ts
index 80ab3f2..7b89630 100644
--- a/packages/react-dom/src/root.ts
+++ b/packages/react-dom/src/root.ts
@@ -4,7 +4,7 @@ import {
createContainer
} from 'react-reconciler/src/fiberReconciler';
import { ReactElement } from 'shared/ReactTypes';
-import { initEvent, elementEventPropsKey } from './SyntheticEvent';
+import { initEvent, elementPropsKey } from './SyntheticEvent';
const containerToRoot = new Map();
@@ -14,7 +14,7 @@ function clearContainerDOM(container: Container) {
}
for (let i = 0; i < container.childNodes.length; i++) {
const childNode = container.childNodes[i];
- if (!Object.hasOwnProperty.call(childNode, elementEventPropsKey)) {
+ if (!Object.hasOwnProperty.call(childNode, elementPropsKey)) {
container.removeChild(childNode);
// 当移除节点时,再遍历时length会减少,所以相应i需要减少一个
i--;
diff --git a/packages/react-noop-renderer/src/ReactNoop.ts b/packages/react-noop-renderer/src/ReactNoop.ts
index d48222f..6f0a2f4 100644
--- a/packages/react-noop-renderer/src/ReactNoop.ts
+++ b/packages/react-noop-renderer/src/ReactNoop.ts
@@ -1,5 +1,5 @@
import { ReactElement } from 'shared/ReactTypes';
-import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols';
+import { REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE } from 'shared/ReactSymbols';
import Reconciler from 'react-reconciler';
import * as Scheduler from 'scheduler';
import { Container, Instance } from './hostConfig';
@@ -9,35 +9,34 @@ let idCounter = 0;
export function createRoot() {
const container: Container = {
rootID: idCounter++,
- pendingChildren: [],
children: []
};
const root = Reconciler.createContainer(container);
- function getChildren(root: Container) {
- if (root) {
- return root.children;
+ function getChildren(parent: Container | Instance) {
+ if (parent) {
+ return parent.children;
}
return null;
}
function getChildrenAsJSX(root: Container) {
const children = childToJSX(getChildren(root));
- if (children === null) {
- return null;
- }
if (Array.isArray(children)) {
- // 对应混合了Instance与TextInstance,应该用Fragment处理
- console.error('TODO Fragment的case,还未实现');
+ return {
+ $$typeof: REACT_ELEMENT_TYPE,
+ type: REACT_FRAGMENT_TYPE,
+ key: null,
+ ref: null,
+ props: { children },
+ __mark: 'KaSong'
+ };
}
return children;
}
// 递归将整棵子树变为JSX
function childToJSX(child: any): any {
- if (child === null) {
- return null;
- }
if (['string', 'number'].includes(typeof child)) {
return child;
}
@@ -58,7 +57,6 @@ export function createRoot() {
}
// 这是Instance
if (Array.isArray(child.children)) {
- // This is an instance.
const instance: Instance = child;
const children = childToJSX(instance.children);
const props = instance.props;
@@ -71,7 +69,7 @@ export function createRoot() {
type: instance.type,
key: null,
ref: null,
- props: props,
+ props,
__mark: 'KaSong'
};
}
diff --git a/packages/react-noop-renderer/src/hostConfig.ts b/packages/react-noop-renderer/src/hostConfig.ts
index 754cf2f..9c22e36 100644
--- a/packages/react-noop-renderer/src/hostConfig.ts
+++ b/packages/react-noop-renderer/src/hostConfig.ts
@@ -1,6 +1,5 @@
export interface Container {
rootID: number;
- pendingChildren: (Instance | TextInstance)[];
children: (Instance | TextInstance)[];
}
export interface Instance {
@@ -17,7 +16,6 @@ export interface TextInstance {
}
import { FiberNode } from 'react-reconciler/src/fiber';
-import { DefaultLane } from 'react-reconciler/src/fiberLanes';
import { HostText } from 'react-reconciler/src/workTags';
let instanceCounter = 0;
@@ -25,7 +23,7 @@ let instanceCounter = 0;
export const createInstance = (type: string, props: any): Instance => {
const instance = {
id: instanceCounter++,
- type: type,
+ type,
children: [],
parent: -1,
props
diff --git a/packages/react-reconciler/src/beginWork.ts b/packages/react-reconciler/src/beginWork.ts
index e923b17..8f59d8a 100644
--- a/packages/react-reconciler/src/beginWork.ts
+++ b/packages/react-reconciler/src/beginWork.ts
@@ -1,8 +1,20 @@
-import { ReactElement } from 'shared/ReactTypes';
+import {
+ Fragment,
+ LazyComponent,
+ OffscreenComponent,
+ SuspenseComponent
+} from 'react-reconciler/src/workTags';
+import { Props, ReactElement } from 'shared/ReactTypes';
import { mountChildFibers, reconcileChildFibers } from './childFiber';
-import { FiberNode } from './fiber';
+import {
+ FiberNode,
+ createFiberFromFragment,
+ createFiberFromOffscreen,
+ createWorkInProgress,
+ resolveLazyComponentTag
+} from './fiber';
import { renderWithHooks } from './fiberHooks';
-import { Lane, Lanes, NoLane } from './fiberLanes';
+import { Lane, Lanes, NoLane, NoLanes } from './fiberLanes';
import { processUpdateQueue, UpdateQueue } from './updateQueue';
import {
FunctionComponent,
@@ -10,8 +22,19 @@ import {
HostRoot,
HostText
} from './workTags';
+import {
+ Ref,
+ NoFlags,
+ DidCapture,
+ Placement,
+ ChildDeletion
+} from './fiberFlags';
+import { resolveDefaultProps } from './fiberLazyComponent';
+import { LazyComponent as LazyComponentType } from 'react/src/lazy';
+import { jsx } from 'react/src/jsx';
+import { OffscreenProps } from './fiberOffscreenComponent';
-export const beginWork = (workInProgress: FiberNode, renderLane: Lane) => {
+export const beginWork = (workInProgress: FiberNode, renderLanes: Lanes) => {
if (__LOG__) {
console.log('beginWork流程', workInProgress.type);
}
@@ -20,47 +43,69 @@ export const beginWork = (workInProgress: FiberNode, renderLane: Lane) => {
switch (workInProgress.tag) {
case HostRoot:
- return updateHostRoot(workInProgress, renderLane);
+ return updateHostRoot(workInProgress, renderLanes);
case HostComponent:
- return updateHostComponent(workInProgress);
+ return updateHostComponent(workInProgress, renderLanes);
case HostText:
return null;
case FunctionComponent:
- return updateFunctionComponent(workInProgress, renderLane);
+ return updateFunctionComponent(workInProgress, renderLanes);
+ case Fragment:
+ return updateFragment(workInProgress, renderLanes);
+ case LazyComponent:
+ return mountLazyComponent(workInProgress, renderLanes);
+ case SuspenseComponent:
+ return updateSuspenseComponent(workInProgress, renderLanes);
+ case OffscreenComponent:
+ return updateOffscreenComponent(workInProgress, renderLanes);
default:
console.error('beginWork未处理的情况');
return null;
}
};
-function updateFunctionComponent(workInProgress: FiberNode, renderLane: Lane) {
- const nextChildren = renderWithHooks(workInProgress, renderLane);
- reconcileChildren(workInProgress, nextChildren);
+function updateFragment(workInProgress: FiberNode, renderLanes: Lanes) {
+ const nextChildren = workInProgress.pendingProps;
+ reconcileChildren(workInProgress, nextChildren, renderLanes);
+ return workInProgress.child;
+}
+
+function updateFunctionComponent(
+ workInProgress: FiberNode,
+ renderLanes: Lanes
+) {
+ const nextChildren = renderWithHooks(workInProgress, renderLanes);
+ reconcileChildren(workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
-function updateHostComponent(workInProgress: FiberNode) {
+function updateHostComponent(workInProgress: FiberNode, renderLanes: Lanes) {
// 根据element创建fiberNode
const nextProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
- reconcileChildren(workInProgress, nextChildren);
+ markRef(workInProgress.alternate, workInProgress);
+ reconcileChildren(workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updateHostRoot(workInProgress: FiberNode, renderLanes: Lanes) {
const baseState = workInProgress.memoizedState;
- const updateQueue = workInProgress.updateQueue as UpdateQueue;
+ const updateQueue = workInProgress.updateQueue as UpdateQueue;
const pending = updateQueue.shared.pending;
updateQueue.shared.pending = null;
const { memoizedState } = processUpdateQueue(baseState, pending, renderLanes);
workInProgress.memoizedState = memoizedState;
const nextChildren = workInProgress.memoizedState;
- reconcileChildren(workInProgress, nextChildren);
+ reconcileChildren(workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
-function reconcileChildren(workInProgress: FiberNode, children?: ReactElement) {
+function reconcileChildren(
+ workInProgress: FiberNode,
+ children: any,
+ renderLanes: Lanes
+) {
const current = workInProgress.alternate;
if (current !== null) {
@@ -68,10 +113,250 @@ function reconcileChildren(workInProgress: FiberNode, children?: ReactElement) {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
- children
+ children,
+ renderLanes
);
} else {
// mount
- workInProgress.child = mountChildFibers(workInProgress, null, children);
+ workInProgress.child = mountChildFibers(
+ workInProgress,
+ null,
+ children,
+ renderLanes
+ );
+ }
+}
+
+function markRef(current: FiberNode | null, workInProgress: FiberNode) {
+ const ref = workInProgress.ref;
+
+ if (
+ (current === null && ref !== null) ||
+ (current !== null && current.ref !== ref)
+ ) {
+ workInProgress.flags |= Ref;
+ }
+}
+
+function mountLazyComponent(workInProgress: FiberNode, renderLanes: Lanes) {
+ const elementType = workInProgress.type;
+
+ const props = workInProgress.pendingProps;
+ const lazyComponent: LazyComponentType = elementType;
+ const payload = lazyComponent._payload;
+ const init = lazyComponent._init;
+
+ const Component = init(payload);
+ // 能到这里说明异步结束了
+ workInProgress.type = Component;
+ const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
+ workInProgress.pendingProps = resolveDefaultProps(Component, props);
+ switch (resolvedTag) {
+ case FunctionComponent:
+ return updateFunctionComponent(workInProgress, renderLanes);
+ default:
+ return null;
+ }
+}
+
+function updateSuspenseComponent(
+ workInProgress: FiberNode,
+ renderLanes: Lanes
+) {
+ const current = workInProgress.alternate;
+ const nextProps = workInProgress.pendingProps;
+
+ let showFallback = false;
+ const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
+
+ if (didSuspend) {
+ showFallback = true;
+ workInProgress.flags &= ~DidCapture;
+ }
+ const nextPrimaryChildren = nextProps.children;
+ const nextFallbackChildren = nextProps.fallback;
+
+ // 源码中会用Offline去保存状态
+ if (current === null) {
+ if (showFallback) {
+ const fallbackFragment = mountSuspenseFallbackChildren(
+ workInProgress,
+ nextPrimaryChildren,
+ nextFallbackChildren,
+ renderLanes
+ );
+ return fallbackFragment;
+ } else {
+ return mountSuspensePrimaryChildren(
+ workInProgress,
+ nextPrimaryChildren,
+ renderLanes
+ );
+ }
+ } else {
+ if (showFallback) {
+ const fallbackChildFragment = updateSuspenseFallbackChildren(
+ workInProgress,
+ nextPrimaryChildren,
+ nextFallbackChildren,
+ renderLanes
+ );
+ return fallbackChildFragment;
+ } else {
+ return updateSuspensePrimaryChildren(
+ workInProgress,
+ nextPrimaryChildren,
+ renderLanes
+ );
+ }
+ }
+}
+
+function mountSuspenseFallbackChildren(
+ workInProgress: FiberNode,
+ primaryChildren: any,
+ fallbackChildren: any,
+ renderLanes: Lanes
+) {
+ const primaryChildProps: OffscreenProps = {
+ mode: 'hidden',
+ children: primaryChildren
+ };
+ const primaryChildFragment = mountWorkInProgressOffscreenFiber(
+ primaryChildProps,
+ NoLanes
+ );
+ const fallbackFragment = createFiberFromFragment(
+ fallbackChildren,
+ renderLanes,
+ null
+ );
+ primaryChildFragment.return = fallbackFragment.return = workInProgress;
+ primaryChildFragment.sibling = fallbackFragment;
+ workInProgress.child = primaryChildFragment;
+ return fallbackFragment;
+}
+
+function mountSuspensePrimaryChildren(
+ workInProgress: FiberNode,
+ primaryChildren: any,
+ renderLanes: Lanes
+) {
+ const primaryChildProps: OffscreenProps = {
+ mode: 'visible',
+ children: primaryChildren
+ };
+ const primaryChildFragment = mountWorkInProgressOffscreenFiber(
+ primaryChildProps,
+ renderLanes
+ );
+ primaryChildFragment.return = workInProgress;
+ workInProgress.child = primaryChildFragment;
+ return primaryChildFragment;
+}
+function updateSuspenseFallbackChildren(
+ workInProgress: FiberNode,
+ primaryChildren: any,
+ fallbackChildren: any,
+ renderLanes: Lanes
+) {
+ const current = workInProgress.alternate!;
+ const currentPrimaryChildFragment = current.child as FiberNode;
+ const currentFallbackChildFragment: FiberNode | null =
+ currentPrimaryChildFragment.sibling;
+ const primaryChildProps: OffscreenProps = {
+ mode: 'hidden',
+ children: primaryChildren
+ };
+ const primaryChildFragment = updateWorkInProgressOffscreenFiber(
+ currentPrimaryChildFragment,
+ primaryChildProps
+ );
+ let fallbackChildFragment!: FiberNode;
+ if (currentFallbackChildFragment !== null) {
+ fallbackChildFragment = createWorkInProgress(
+ currentFallbackChildFragment,
+ fallbackChildren
+ );
+ } else {
+ fallbackChildFragment = createFiberFromFragment(
+ fallbackChildren,
+ renderLanes,
+ null
+ );
+ fallbackChildFragment.flags |= Placement;
+ }
+
+ workInProgress.deletions = null;
+ workInProgress.flags &= ~ChildDeletion;
+
+ fallbackChildFragment.return = workInProgress;
+ primaryChildFragment.return = workInProgress;
+ primaryChildFragment.sibling = fallbackChildFragment;
+ workInProgress.child = primaryChildFragment;
+
+ return fallbackChildFragment;
+}
+
+function updateSuspensePrimaryChildren(
+ workInProgress: FiberNode,
+ primaryChildren: any,
+ renderLanes: Lanes
+) {
+ const current = workInProgress.alternate!;
+ const currentPrimaryChildFragment = current.child as FiberNode;
+ const currentFallbackChildFragment = currentPrimaryChildFragment.sibling;
+
+ const primaryChildFragment = updateWorkInProgressOffscreenFiber(
+ currentPrimaryChildFragment,
+ {
+ mode: 'visible',
+ children: primaryChildren
+ }
+ );
+
+ primaryChildFragment.return = workInProgress;
+ primaryChildFragment.sibling = null;
+
+ if (currentFallbackChildFragment !== null) {
+ const deletions = workInProgress.deletions;
+ if (deletions === null) {
+ workInProgress.deletions = [currentFallbackChildFragment];
+ workInProgress.flags |= ChildDeletion;
+ } else {
+ deletions.push(currentFallbackChildFragment);
+ }
+ }
+
+ workInProgress.child = primaryChildFragment;
+ return primaryChildFragment;
+}
+
+function mountWorkInProgressOffscreenFiber(
+ offscreenProps: OffscreenProps,
+ renderLanes: Lanes
+) {
+ return createFiberFromOffscreen(offscreenProps, renderLanes, null);
+}
+
+function updateWorkInProgressOffscreenFiber(
+ current: FiberNode,
+ offscreenProps: OffscreenProps
+) {
+ return createWorkInProgress(current, offscreenProps);
+}
+
+function updateOffscreenComponent(
+ workInProgress: FiberNode,
+ renderLanes: Lanes
+) {
+ // debugger;
+ const nextProps: OffscreenProps = workInProgress.pendingProps;
+ const nextChildren = nextProps.children;
+ if (nextProps.mode === 'hidden') {
+ return null;
+ } else {
+ reconcileChildren(workInProgress, nextChildren, renderLanes);
+ return workInProgress.child;
}
}
diff --git a/packages/react-reconciler/src/childFiber.ts b/packages/react-reconciler/src/childFiber.ts
index 92d3f3d..8d3e566 100644
--- a/packages/react-reconciler/src/childFiber.ts
+++ b/packages/react-reconciler/src/childFiber.ts
@@ -1,12 +1,19 @@
-import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols';
+import {
+ REACT_ELEMENT_TYPE,
+ REACT_FRAGMENT_TYPE,
+ REACT_LAZY_TYPE
+} from 'shared/ReactSymbols';
import { Props, ReactElement } from 'shared/ReactTypes';
import {
createFiberFromElement,
+ createFiberFromFragment,
createWorkInProgress,
FiberNode
} from './fiber';
import { ChildDeletion, Placement } from './fiberFlags';
-import { HostText } from './workTags';
+import { Lanes } from './fiberLanes';
+import { Fragment, HostText } from './workTags';
+import { LazyComponent } from 'react/src/lazy';
/**
* mount/reconcile只负责 Placement(插入)/Placement(移动)/ChildDeletion(删除)
@@ -15,6 +22,12 @@ import { HostText } from './workTags';
type ExistingChildren = Map;
+function resolveLazy(lazyType: LazyComponent) {
+ const payload = lazyType._payload;
+ const init = lazyType._init;
+ return init(payload);
+}
+
function ChildReconciler(shouldTrackEffects: boolean) {
function deleteChild(returnFiber: FiberNode, childToDelete: FiberNode) {
if (!shouldTrackEffects) {
@@ -33,19 +46,19 @@ function ChildReconciler(shouldTrackEffects: boolean) {
currentFirstChild: FiberNode | null
) {
if (!shouldTrackEffects) {
- return null;
+ return;
}
let childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
- return null;
}
function reconcileSingleElement(
returnFiber: FiberNode,
currentFirstChild: FiberNode | null,
- element: ReactElement
+ element: ReactElement,
+ lanes: Lanes
) {
// 前:abc 后:a 删除bc
// 前:a 后:b 删除b、创建a
@@ -58,9 +71,18 @@ function ChildReconciler(shouldTrackEffects: boolean) {
// key相同,比较type
if (element.$$typeof === REACT_ELEMENT_TYPE) {
- if (current.type === element.type) {
+ if (
+ current.type === element.type ||
+ (typeof element.type === 'object' &&
+ element.type.$$typeof === REACT_LAZY_TYPE &&
+ resolveLazy(element.type) === currentFirstChild?.type)
+ ) {
// type相同 可以复用
- const existing = useFiber(current, element.props);
+ let props = element.props;
+ if (element.type === REACT_FRAGMENT_TYPE) {
+ props = element.props.children as Props;
+ }
+ const existing = useFiber(current, props);
existing.return = returnFiber;
// 当前节点可复用,其他兄弟节点都删除
deleteRemainingChildren(returnFiber, current.sibling);
@@ -80,7 +102,12 @@ function ChildReconciler(shouldTrackEffects: boolean) {
}
}
// 创建新的
- const fiber = createFiberFromElement(element);
+ let fiber;
+ if (element.type === REACT_FRAGMENT_TYPE) {
+ fiber = createFiberFromFragment(element.props.children, lanes, key);
+ } else {
+ fiber = createFiberFromElement(element, lanes);
+ }
fiber.return = returnFiber;
return fiber;
}
@@ -96,64 +123,96 @@ function ChildReconciler(shouldTrackEffects: boolean) {
returnFiber: FiberNode,
existingChildren: ExistingChildren,
index: number,
- element: ReactElement | string | number | null
+ element: any,
+ lanes: Lanes
): FiberNode | null {
- let keyToUse;
- if (
- element === null ||
- typeof element === 'string' ||
- typeof element === 'number'
- ) {
- keyToUse = index;
- } else {
- keyToUse = element.key !== null ? element.key : index;
- }
+ // 确定key
+ const keyToUse = element.key !== null ? element.key : index;
+
const before = existingChildren.get(keyToUse);
- if (
- element === null ||
- typeof element === 'string' ||
- typeof element === 'number'
- ) {
+ // 处理文本节点
+ if (typeof element === 'string' || typeof element === 'number') {
if (before) {
// fiber key相同,如果type也相同,则可复用
- existingChildren.delete(keyToUse);
if (before.tag === HostText) {
// 复用文本节点
+ existingChildren.delete(keyToUse);
return useFiber(before, { content: element + '' });
- } else {
- deleteChild(returnFiber, before);
}
}
-
- // 新建文本节点
- return element === null
- ? null
- : new FiberNode(HostText, { content: element }, null);
+ return new FiberNode(HostText, { content: element }, null);
}
+ // 处理ReactElement
if (typeof element === 'object' && element !== null) {
switch (element.$$typeof) {
case REACT_ELEMENT_TYPE:
+ if (element.type === REACT_FRAGMENT_TYPE) {
+ return updateFragment(
+ returnFiber,
+ before,
+ element,
+ lanes,
+ keyToUse,
+ existingChildren
+ );
+ }
if (before) {
// fiber key相同,如果type也相同,则可复用
- existingChildren.delete(keyToUse);
if (before.type === element.type) {
- // 复用
+ existingChildren.delete(keyToUse);
return useFiber(before, element.props);
- } else {
- deleteChild(returnFiber, before);
}
}
- return createFiberFromElement(element);
+ return createFiberFromElement(element, lanes);
+ }
+ // 处理Fragment
+ /**
+ * after可能还是array 考虑如下,其中list是个array:
+ *
+ * 这种情况我们应该视after为Fragment
+ */
+ if (Array.isArray(element)) {
+ return updateFragment(
+ returnFiber,
+ before,
+ element,
+ lanes,
+ keyToUse,
+ existingChildren
+ );
}
}
return null;
}
+ function updateFragment(
+ returnFiber: FiberNode,
+ current: FiberNode | undefined,
+ elements: any[],
+ lanes: Lanes,
+ key: string,
+ existingChildren: ExistingChildren
+ ): FiberNode {
+ let fiber;
+ if (!current || current.tag !== Fragment) {
+ fiber = createFiberFromFragment(elements, lanes, key);
+ } else {
+ existingChildren.delete(key);
+ fiber = useFiber(current, elements);
+ }
+ fiber.return = returnFiber;
+ return fiber;
+ }
+
function reconcileSingleTextNode(
returnFiber: FiberNode,
currentFirstChild: FiberNode | null,
- content: string
+ content: string,
+ lanes: Lanes
) {
// 前:b 后:a
// TODO 前:abc 后:a
@@ -173,6 +232,7 @@ function ChildReconciler(shouldTrackEffects: boolean) {
}
const created = new FiberNode(HostText, { content }, null);
+ created.lanes = lanes;
created.return = returnFiber;
return created;
}
@@ -180,7 +240,8 @@ function ChildReconciler(shouldTrackEffects: boolean) {
function reconcileChildrenArray(
returnFiber: FiberNode,
currentFirstChild: FiberNode | null,
- newChild: (ReactElement | string)[]
+ newChild: any[],
+ lanes: Lanes
) {
// 遍历到的最后一个可复用fiber在before中的index
let lastPlacedIndex = 0;
@@ -200,26 +261,15 @@ function ChildReconciler(shouldTrackEffects: boolean) {
// 遍历流程
for (let i = 0; i < newChild.length; i++) {
- /**
- * TODO after可能还是array 考虑如下,其中list是个array:
- *
- * 这种情况我们应该视after为Fragment
- */
const after = newChild[i];
- if (Array.isArray(after)) {
- console.error('TODO 还未实现嵌套Array情况下的diff');
- }
-
// after对应的fiber,可能来自于复用,也可能是新建
const newFiber = updateFromMap(
returnFiber,
existingChildren,
i,
- after
+ after,
+ lanes
) as FiberNode;
/**
@@ -272,30 +322,59 @@ function ChildReconciler(shouldTrackEffects: boolean) {
function reconcileChildFibers(
returnFiber: FiberNode,
currentFirstChild: FiberNode | null,
- newChild?: ReactElement
+ newChild: any,
+ lanes: Lanes
): FiberNode | null {
+ // 对于类似 这样内部直接使用<>作为Fragment的情况
+ const isUnkeyedTopLevelFragment =
+ typeof newChild === 'object' &&
+ newChild !== null &&
+ newChild.type === REACT_FRAGMENT_TYPE &&
+ newChild.key === null;
+ if (isUnkeyedTopLevelFragment) {
+ newChild = newChild.props.children;
+ }
+
// newChild 为 JSX
// currentFirstChild 为 fiberNode
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
- reconcileSingleElement(returnFiber, currentFirstChild, newChild)
+ reconcileSingleElement(
+ returnFiber,
+ currentFirstChild,
+ newChild,
+ lanes
+ )
);
}
+ // 第一层数组直接遍历,嵌套数组作为Fragment处理
+ // 如:
if (Array.isArray(newChild)) {
- return reconcileChildrenArray(returnFiber, currentFirstChild, newChild);
+ return reconcileChildrenArray(
+ returnFiber,
+ currentFirstChild,
+ newChild,
+ lanes
+ );
}
}
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
- reconcileSingleTextNode(returnFiber, currentFirstChild, newChild + '')
+ reconcileSingleTextNode(
+ returnFiber,
+ currentFirstChild,
+ newChild + '',
+ lanes
+ )
);
}
// 其他情况全部视为删除旧的节点
- return deleteRemainingChildren(returnFiber, currentFirstChild);
+ deleteRemainingChildren(returnFiber, currentFirstChild);
+ return null;
}
return reconcileChildFibers;
diff --git a/packages/react-reconciler/src/commitWork.ts b/packages/react-reconciler/src/commitWork.ts
index 7875ae9..2398070 100644
--- a/packages/react-reconciler/src/commitWork.ts
+++ b/packages/react-reconciler/src/commitWork.ts
@@ -2,12 +2,14 @@ import { FiberNode, FiberRootNode, PendingPassiveEffects } from './fiber';
import {
ChildDeletion,
Flags,
+ LayoutMask,
MutationMask,
NoFlags,
PassiveEffect,
PassiveMask,
Placement,
- Update
+ Update,
+ Ref
} from './fiberFlags';
import { Effect, FCUpdateQueue } from './fiberHooks';
import { HookHasEffect } from './hookEffectTags';
@@ -23,48 +25,57 @@ import {
FunctionComponent,
HostComponent,
HostRoot,
- HostText
+ HostText,
+ SuspenseComponent
} from './workTags';
+import { RetryQueue } from './fiberThrow';
+import { Wakeable } from 'shared/ReactTypes';
+import { SyncLane } from './fiberLanes';
+import {
+ ensureRootIsScheduled,
+ markRootUpdated,
+ markUpdateLaneFromFiberToRoot
+} from './workLoop';
let nextEffect: FiberNode | null = null;
// 以DFS形式执行
-export const commitMutationEffects = (
- finishedWork: FiberNode,
- root: FiberRootNode
+const commitEffects = (
+ phrase: 'mutation' | 'layout',
+ mask: Flags,
+ callback: (fiber: FiberNode, root: FiberRootNode) => void
) => {
- nextEffect = finishedWork;
+ return (finishedWork: FiberNode, root: FiberRootNode) => {
+ nextEffect = finishedWork;
- while (nextEffect !== null) {
- // 向下遍历
- const child: FiberNode | null = nextEffect.child;
+ while (nextEffect !== null) {
+ // 向下遍历
+ const child: FiberNode | null = nextEffect.child;
- if (
- (nextEffect.subtreeFlags & (MutationMask | PassiveMask)) !== NoFlags &&
- child !== null
- ) {
- nextEffect = child;
- } else {
- // 向上遍历
- up: while (nextEffect !== null) {
- commitMutationEffectsOnFiber(nextEffect, root);
- const sibling: FiberNode | null = nextEffect.sibling;
-
- if (sibling !== null) {
- nextEffect = sibling;
- break up;
+ if ((nextEffect.subtreeFlags & mask) !== NoFlags && child !== null) {
+ nextEffect = child;
+ } else {
+ // 向上遍历
+ up: while (nextEffect !== null) {
+ callback(nextEffect, root);
+ const sibling: FiberNode | null = nextEffect.sibling;
+
+ if (sibling !== null) {
+ nextEffect = sibling;
+ break up;
+ }
+ nextEffect = nextEffect.return;
}
- nextEffect = nextEffect.return;
}
}
- }
+ };
};
const commitMutationEffectsOnFiber = (
finishedWork: FiberNode,
root: FiberRootNode
) => {
- const flags = finishedWork.flags;
+ const { flags, tag } = finishedWork;
if ((flags & Placement) !== NoFlags) {
// 插入/移动
@@ -82,16 +93,75 @@ const commitMutationEffectsOnFiber = (
finishedWork.flags &= ~ChildDeletion;
}
if ((flags & Update) !== NoFlags) {
- commitUpdate(finishedWork);
finishedWork.flags &= ~Update;
+ if (tag === SuspenseComponent) {
+ const retryQueue = finishedWork.updateQueue as RetryQueue;
+ if (retryQueue !== null) {
+ finishedWork.updateQueue = null;
+ attachSuspenseRetryListeners(finishedWork, retryQueue);
+ }
+ } else {
+ commitUpdate(finishedWork);
+ }
}
if ((flags & PassiveEffect) !== NoFlags) {
// 收集因deps变化而需要执行的useEffect
commitPassiveEffect(finishedWork, root, 'update');
finishedWork.flags &= ~PassiveEffect;
}
+ if ((flags & Ref) !== NoFlags && tag === HostComponent) {
+ safelyDetachRef(finishedWork);
+ }
+};
+
+function safelyDetachRef(current: FiberNode) {
+ const ref = current.ref;
+ if (ref !== null) {
+ if (typeof ref === 'function') {
+ ref(null);
+ } else {
+ ref.current = null;
+ }
+ }
+}
+
+const commitLayoutEffectsOnFiber = (
+ finishedWork: FiberNode,
+ root: FiberRootNode
+) => {
+ const { flags, tag } = finishedWork;
+
+ if ((flags & Ref) !== NoFlags && tag === HostComponent) {
+ // 绑定新的ref
+ safelyAttachRef(finishedWork);
+ finishedWork.flags &= ~Ref;
+ }
};
+function safelyAttachRef(fiber: FiberNode) {
+ const ref = fiber.ref;
+ if (ref !== null) {
+ const instance = fiber.stateNode;
+ if (typeof ref === 'function') {
+ ref(instance);
+ } else {
+ ref.current = instance;
+ }
+ }
+}
+
+export const commitMutationEffects = commitEffects(
+ 'mutation',
+ MutationMask | PassiveMask,
+ commitMutationEffectsOnFiber
+);
+
+export const commitLayoutEffects = commitEffects(
+ 'layout',
+ LayoutMask,
+ commitLayoutEffectsOnFiber
+);
+
/**
* 难点在于目标fiber的hostSibling可能并不是他的同级sibling
* 比如: 其中:function B() {return } 所以A的hostSibling实际是B的child
@@ -214,6 +284,24 @@ function getHostParent(fiber: FiberNode) {
console.error('getHostParent未找到hostParent');
}
+function recordHostChildrenToDelete(
+ hostChildrenToDelete: FiberNode[],
+ unmountFiber: FiberNode
+) {
+ const lastOne = hostChildrenToDelete[hostChildrenToDelete.length - 1];
+ if (!lastOne) {
+ hostChildrenToDelete.push(unmountFiber);
+ } else {
+ let node = lastOne.sibling;
+ while (node !== null) {
+ if (unmountFiber === node) {
+ hostChildrenToDelete.push(unmountFiber);
+ }
+ node = node.sibling;
+ }
+ }
+}
+
/**
* 删除需要考虑:
* HostComponent:需要遍历他的子树,为后续解绑ref创造条件,HostComponent本身只需删除最上层节点即可
@@ -223,20 +311,18 @@ function commitDeletion(childToDelete: FiberNode, root: FiberRootNode) {
if (__LOG__) {
console.log('删除DOM、组件unmount', childToDelete);
}
- let firstHostFiber: FiberNode | null = null;
+ // 在Fragment之前,只需删除子树的根Host节点,但支持Fragment后,可能需要删除同级多个节点
+ const hostChildrenToDelete: FiberNode[] = [];
commitNestedUnmounts(childToDelete, (unmountFiber) => {
switch (unmountFiber.tag) {
case HostComponent:
- if (firstHostFiber === null) {
- firstHostFiber = unmountFiber;
- }
+ recordHostChildrenToDelete(hostChildrenToDelete, unmountFiber);
// 解绑ref
+ safelyDetachRef(unmountFiber);
return;
case HostText:
- if (firstHostFiber === null) {
- firstHostFiber = unmountFiber;
- }
+ recordHostChildrenToDelete(hostChildrenToDelete, unmountFiber);
return;
case FunctionComponent:
// effect相关操作
@@ -245,9 +331,11 @@ function commitDeletion(childToDelete: FiberNode, root: FiberRootNode) {
}
});
- if (firstHostFiber !== null) {
+ if (hostChildrenToDelete.length) {
const hostParent = getHostParent(childToDelete) as Container;
- removeChild((firstHostFiber as FiberNode).stateNode, hostParent);
+ hostChildrenToDelete.forEach((hostChild) => {
+ removeChild(hostChild.stateNode, hostParent);
+ });
}
childToDelete.return = null;
@@ -332,3 +420,36 @@ export function commitHookEffectListMount(flags: Flags, lastEffect: Effect) {
}
});
}
+
+function getRetryCache(finishedWork: FiberNode) {
+ switch (finishedWork.tag) {
+ case SuspenseComponent:
+ let retryCache = finishedWork.stateNode;
+ if (retryCache === null) {
+ retryCache = finishedWork.stateNode = new WeakSet();
+ }
+ return retryCache;
+ }
+}
+
+function resolveRetryWakeable(boundaryFiber: FiberNode) {
+ const root = markUpdateLaneFromFiberToRoot(boundaryFiber, SyncLane);
+ if (root !== null) {
+ markRootUpdated(root, SyncLane);
+ ensureRootIsScheduled(root);
+ }
+}
+
+function attachSuspenseRetryListeners(
+ finishedWork: FiberNode,
+ wakeables: RetryQueue
+) {
+ const retryCache = getRetryCache(finishedWork);
+ wakeables.forEach((wakeable) => {
+ const retry = resolveRetryWakeable.bind(null, finishedWork);
+ if (!retryCache.has(wakeable)) {
+ retryCache.add(wakeable);
+ wakeable.then(retry, retry);
+ }
+ });
+}
diff --git a/packages/react-reconciler/src/completeWork.ts b/packages/react-reconciler/src/completeWork.ts
index 49e61a6..7865468 100644
--- a/packages/react-reconciler/src/completeWork.ts
+++ b/packages/react-reconciler/src/completeWork.ts
@@ -1,6 +1,6 @@
import { updateFiberProps } from 'react-dom/src/SyntheticEvent';
import { FiberNode } from './fiber';
-import { NoFlags, Update } from './fiberFlags';
+import { NoFlags, Ref, Update, Visibility } from './fiberFlags';
import {
appendInitialChild,
createInstance,
@@ -8,11 +8,20 @@ import {
Instance
} from 'hostConfig';
import {
+ Fragment,
FunctionComponent,
HostComponent,
HostRoot,
- HostText
+ HostText,
+ LazyComponent,
+ OffscreenComponent,
+ SuspenseComponent
} from './workTags';
+import { RetryQueue } from './fiberThrow';
+
+function markRef(fiber: FiberNode) {
+ fiber.flags |= Ref;
+}
const appendAllChildren = (parent: Instance, workInProgress: FiberNode) => {
// 遍历workInProgress所有子孙 DOM元素,依次挂载
@@ -73,19 +82,29 @@ export const completeWork = (workInProgress: FiberNode) => {
// 不应该在此处调用updateFiberProps,应该跟着判断属性变化的逻辑,在这里打flag
// 再在commitWork中更新fiberProps,我准备把这个过程留到「属性变化」相关需求一起做
updateFiberProps(workInProgress.stateNode, newProps);
+ // 标记Ref
+ if (current.ref !== workInProgress.ref) {
+ markRef(workInProgress);
+ }
} else {
// 初始化DOM
const instance = createInstance(workInProgress.type, newProps);
// 挂载DOM
appendAllChildren(instance, workInProgress);
workInProgress.stateNode = instance;
-
+ // 标记Ref
+ if (workInProgress.ref !== null) {
+ markRef(workInProgress);
+ }
// TODO 初始化元素属性
}
// 冒泡flag
bubbleProperties(workInProgress);
return null;
+ case FunctionComponent:
case HostRoot:
+ case Fragment:
+ case LazyComponent:
bubbleProperties(workInProgress);
return null;
case HostText:
@@ -105,9 +124,19 @@ export const completeWork = (workInProgress: FiberNode) => {
// 冒泡flag
bubbleProperties(workInProgress);
return null;
- case FunctionComponent:
+ case SuspenseComponent:
+ const retryQueue = workInProgress.updateQueue as RetryQueue | null;
+ if (retryQueue !== null) {
+ workInProgress.flags |= Update;
+ }
bubbleProperties(workInProgress);
return null;
+ case OffscreenComponent:
+ const nextIsHidden = workInProgress.memoizedProps?.mode === 'hidden';
+ if (!nextIsHidden) {
+ bubbleProperties(workInProgress);
+ }
+ return null;
default:
console.error('completeWork未定义的fiber.tag', workInProgress);
return null;
diff --git a/packages/react-reconciler/src/fiber.ts b/packages/react-reconciler/src/fiber.ts
index 4d819e6..c15c85b 100644
--- a/packages/react-reconciler/src/fiber.ts
+++ b/packages/react-reconciler/src/fiber.ts
@@ -3,8 +3,22 @@ import { Flags, NoFlags } from './fiberFlags';
import { Effect } from './fiberHooks';
import { Lane, Lanes, NoLane, NoLanes } from './fiberLanes';
import { Container } from 'hostConfig';
-import { FunctionComponent, HostComponent, WorkTag } from './workTags';
+import {
+ Fragment,
+ FunctionComponent,
+ HostComponent,
+ WorkTag,
+ LazyComponent,
+ SuspenseComponent,
+ OffscreenComponent
+} from './workTags';
import { CallbackNode } from 'scheduler';
+import { REACT_LAZY_TYPE, REACT_SUSPENSE_TYPE } from 'shared/ReactSymbols';
+import {
+ OffscreenInstance,
+ OffscreenProps,
+ OffscreenVisible
+} from './fiberOffscreenComponent';
export class FiberNode {
pendingProps: Props;
@@ -33,7 +47,7 @@ export class FiberNode {
constructor(tag: WorkTag, pendingProps: Props, key: Key) {
// 实例
this.tag = tag;
- this.key = key;
+ this.key = key || null;
this.stateNode = null;
this.type = null;
@@ -103,21 +117,44 @@ export class FiberRootNode {
}
}
-export function createFiberFromElement(element: ReactElement): FiberNode {
- const { type, key, props } = element;
+export function createFiberFromElement(
+ element: ReactElement,
+ lanes: Lanes
+): FiberNode {
+ const { type, key, props, ref } = element;
let fiberTag: WorkTag = FunctionComponent;
if (typeof type === 'string') {
fiberTag = HostComponent;
+ } else if (typeof type === 'object' && type !== null) {
+ switch (type.$$typeof) {
+ case REACT_LAZY_TYPE:
+ fiberTag = LazyComponent;
+ break;
+ }
+ } else if (type === REACT_SUSPENSE_TYPE) {
+ fiberTag = SuspenseComponent;
} else if (typeof type !== 'function') {
console.error('未定义的type类型', element);
}
const fiber = new FiberNode(fiberTag, props, key);
fiber.type = type;
+ fiber.lanes = lanes;
+ fiber.ref = ref;
return fiber;
}
+export function createFiberFromFragment(
+ elements: ReactElement[],
+ lanes: Lanes,
+ key: Key
+): FiberNode {
+ const fiber = new FiberNode(Fragment, elements, key);
+ fiber.lanes = lanes;
+ return fiber;
+}
+
export const createWorkInProgress = (
current: FiberNode,
pendingProps: Props
@@ -147,8 +184,34 @@ export const createWorkInProgress = (
// 数据
wip.memoizedProps = current.memoizedProps;
wip.memoizedState = current.memoizedState;
+ wip.ref = current.ref;
wip.lanes = current.lanes;
return wip;
};
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+export function resolveLazyComponentTag(Component: Function): WorkTag {
+ if (typeof Component === 'function') {
+ // 不考虑class
+ return FunctionComponent;
+ }
+ throw '未知的tag';
+}
+
+export function createFiberFromOffscreen(
+ pendingProps: OffscreenProps,
+ lanes: Lanes,
+ key: null | string
+) {
+ const fiber = new FiberNode(OffscreenComponent, pendingProps, key);
+ fiber.lanes = lanes;
+ // TODO
+ const primaryChildInstance: OffscreenInstance = {
+ visibility: OffscreenVisible,
+ retryCache: null
+ };
+ fiber.stateNode = primaryChildInstance;
+ return fiber;
+}
diff --git a/packages/react-reconciler/src/fiberFlags.ts b/packages/react-reconciler/src/fiberFlags.ts
index a89941e..4f2a605 100644
--- a/packages/react-reconciler/src/fiberFlags.ts
+++ b/packages/react-reconciler/src/fiberFlags.ts
@@ -4,11 +4,16 @@ export const NoFlags = 0b00000000000000000000000000;
export const Placement = 0b00000000000000000000000010;
export const Update = 0b00000000000000000000000100;
export const ChildDeletion = 0b00000000000000000000010000;
+export const Visibility = 0b0000000000000010000000000000;
+
+export const DidCapture = 0b0000000000000000000010000000;
// useEffect
export const PassiveEffect = 0b00000000000000000000100000;
+export const Ref = 0b00000000000000000001000000;
-export const MutationMask = Placement | Update | ChildDeletion;
+export const MutationMask = Placement | Update | ChildDeletion | Ref;
+export const LayoutMask = Ref;
// 删除子节点可能触发useEffect destroy
export const PassiveMask = PassiveEffect | ChildDeletion;
diff --git a/packages/react-reconciler/src/fiberHooks.ts b/packages/react-reconciler/src/fiberHooks.ts
index c1552e3..9952414 100644
--- a/packages/react-reconciler/src/fiberHooks.ts
+++ b/packages/react-reconciler/src/fiberHooks.ts
@@ -1,4 +1,4 @@
-import { Dispatcher, Disptach } from 'react/src/currentDispatcher';
+import { Dispatcher, Dispatch } from 'react/src/currentDispatcher';
import { Action } from 'shared/ReactTypes';
import sharedInternals from 'shared/internals';
import { FiberNode } from './fiber';
@@ -68,17 +68,19 @@ export const renderWithHooks = (workInProgress: FiberNode, lane: Lane) => {
const HooksDispatcherOnMount: Dispatcher = {
useState: mountState,
- useEffect: mountEffect
+ useEffect: mountEffect,
+ useRef: mountRef
};
const HooksDispatcherOnUpdate: Dispatcher = {
useState: updateState,
- useEffect: updateEffect
+ useEffect: updateEffect,
+ useRef: updateRef
};
function mountState(
initialState: (() => State) | State
-): [State, Disptach] {
+): [State, Dispatch] {
const hook = mountWorkInProgressHook();
let memoizedState: State;
if (initialState instanceof Function) {
@@ -100,7 +102,7 @@ function mountState(
return [memoizedState, dispatch];
}
-function updateState(): [State, Disptach] {
+function updateState(): [State, Dispatch] {
const hook = updateWorkInProgressHook();
const queue = hook.updateQueue as UpdateQueue;
const baseState = hook.baseState;
@@ -150,7 +152,7 @@ function updateState(): [State, Disptach] {
hook.baseQueue = newBaseQueue;
}
- return [hook.memoizedState, queue.dispatch as Disptach];
+ return [hook.memoizedState, queue.dispatch as Dispatch];
}
function dispatchSetState(
@@ -226,6 +228,18 @@ function areHookInputsEqual(nextDeps: TEffectDeps, prevDeps: TEffectDeps) {
return true;
}
+function mountRef(initialValue: T): { current: T } {
+ const hook = mountWorkInProgressHook();
+ const ref = { current: initialValue };
+ hook.memoizedState = ref;
+ return ref;
+}
+
+function updateRef(initialValue: T): { current: T } {
+ const hook = updateWorkInProgressHook();
+ return hook.memoizedState;
+}
+
export interface Effect {
tag: Flags;
create: TEffectCallback | void;
diff --git a/packages/react-reconciler/src/fiberLazyComponent.ts b/packages/react-reconciler/src/fiberLazyComponent.ts
new file mode 100644
index 0000000..50b3c3f
--- /dev/null
+++ b/packages/react-reconciler/src/fiberLazyComponent.ts
@@ -0,0 +1,3 @@
+export function resolveDefaultProps(Component: any, baseProps: object): object {
+ return baseProps;
+}
diff --git a/packages/react-reconciler/src/fiberOffscreenComponent.ts b/packages/react-reconciler/src/fiberOffscreenComponent.ts
new file mode 100644
index 0000000..01652d2
--- /dev/null
+++ b/packages/react-reconciler/src/fiberOffscreenComponent.ts
@@ -0,0 +1,15 @@
+import { OffscreenMode, Wakeable } from 'shared/ReactTypes';
+import { FiberNode } from './fiber';
+
+export interface OffscreenProps {
+ mode?: OffscreenMode;
+ children?: FiberNode;
+}
+export type OffscreenInstance = {
+ visibility: OffscreenVisibility;
+ retryCache: WeakSet | Set | null;
+};
+
+export type OffscreenVisibility = number;
+
+export const OffscreenVisible = 0b01;
diff --git a/packages/react-reconciler/src/fiberThrow.ts b/packages/react-reconciler/src/fiberThrow.ts
new file mode 100644
index 0000000..f49534c
--- /dev/null
+++ b/packages/react-reconciler/src/fiberThrow.ts
@@ -0,0 +1,24 @@
+import { Wakeable } from 'shared/ReactTypes';
+import { FiberNode } from './fiber';
+import { DidCapture } from './fiberFlags';
+
+export type RetryQueue = Set>;
+
+export function throwException(unitOfWork: FiberNode, value: any) {
+ if (
+ value !== null &&
+ typeof value === 'object' &&
+ typeof value.then === 'function'
+ ) {
+ const weakable: Wakeable = value;
+ // 为了简化 假设一定是Suspense包裹一层lazy,
+ const suspenseBoundary = unitOfWork!.return!.return!;
+ suspenseBoundary.flags |= DidCapture;
+ const retryQueue = suspenseBoundary.updateQueue as RetryQueue | null;
+ if (retryQueue === null) {
+ suspenseBoundary.updateQueue = new Set([weakable]);
+ } else {
+ retryQueue.add(weakable);
+ }
+ }
+}
diff --git a/packages/react-reconciler/src/updateQueue.ts b/packages/react-reconciler/src/updateQueue.ts
index 0b61c09..39dc416 100644
--- a/packages/react-reconciler/src/updateQueue.ts
+++ b/packages/react-reconciler/src/updateQueue.ts
@@ -1,4 +1,4 @@
-import { Disptach } from 'react/src/currentDispatcher';
+import { Dispatch } from 'react/src/currentDispatcher';
import { Action } from 'shared/ReactTypes';
import { Update } from './fiberFlags';
import {
@@ -19,7 +19,7 @@ export interface UpdateQueue {
shared: {
pending: Update | null;
};
- dispatch: Disptach | null;
+ dispatch: Dispatch | null;
}
// 创建
diff --git a/packages/react-reconciler/src/workLoop.ts b/packages/react-reconciler/src/workLoop.ts
index 254de1f..f648fbc 100644
--- a/packages/react-reconciler/src/workLoop.ts
+++ b/packages/react-reconciler/src/workLoop.ts
@@ -3,6 +3,7 @@ import {
commitHookEffectListDestroy,
commitHookEffectListMount,
commitHookEffectListUnmount,
+ commitLayoutEffects,
commitMutationEffects
} from './commitWork';
import { completeWork } from './completeWork';
@@ -12,7 +13,7 @@ import {
FiberRootNode,
PendingPassiveEffects
} from './fiber';
-import { MutationMask, NoFlags, PassiveMask } from './fiberFlags';
+import { MutationMask, NoFlags, PassiveMask, DidCapture } from './fiberFlags';
import {
getHighestPriorityLane,
getNextLanes,
@@ -30,6 +31,7 @@ import { flushSyncCallbacks, scheduleSyncCallback } from './syncTaskQueue';
import { HostRoot } from './workTags';
import * as scheduler from 'scheduler';
import { HookHasEffect, Passive } from './hookEffectTags';
+import { throwException } from './fiberThrow';
const {
unstable_scheduleCallback: scheduleCallback,
@@ -57,6 +59,15 @@ const RootCompleted = 2;
// 与调度effect相关
let rootDoesHavePassiveEffects = false;
+// Suspense
+type SuspendedReason =
+ | typeof NotSuspended
+ | typeof SuspendedOnDeprecatedThrowPromise;
+const NotSuspended = 0;
+const SuspendedOnDeprecatedThrowPromise = 6;
+let workInProgressSuspendedReason: SuspendedReason = NotSuspended;
+let workInProgressThrownValue: any = null;
+
export function scheduleUpdateOnFiber(fiber: FiberNode, lane: Lane) {
if (__LOG__) {
console.log('开始schedule阶段', fiber, lane);
@@ -71,11 +82,11 @@ export function scheduleUpdateOnFiber(fiber: FiberNode, lane: Lane) {
ensureRootIsScheduled(root);
}
-function markRootUpdated(root: FiberRootNode, lane: Lane) {
+export function markRootUpdated(root: FiberRootNode, lane: Lane) {
root.pendingLanes = mergeLanes(root.pendingLanes, lane);
}
-function markUpdateLaneFromFiberToRoot(fiber: FiberNode, lane: Lane) {
+export function markUpdateLaneFromFiberToRoot(fiber: FiberNode, lane: Lane) {
let node = fiber;
let parent = node.return;
@@ -95,7 +106,7 @@ function markUpdateLaneFromFiberToRoot(fiber: FiberNode, lane: Lane) {
return null;
}
-function ensureRootIsScheduled(root: FiberRootNode) {
+export function ensureRootIsScheduled(root: FiberRootNode) {
const updateLanes = getNextLanes(root);
const existingCallback = root.callbackNode;
@@ -109,7 +120,6 @@ function ensureRootIsScheduled(root: FiberRootNode) {
}
const curPriority = getHighestPriorityLane(updateLanes);
const prevPriority = root.callbackPriority;
-
if (curPriority === prevPriority) {
// 有更新在进行,比较该更新与正在进行的更新的优先级
// 如果优先级相同,则不需要调度新的,退出调度
@@ -208,11 +218,22 @@ function renderRoot(
// render阶段具体操作
do {
try {
+ if (
+ workInProgressSuspendedReason !== NotSuspended &&
+ workInProgress !== null
+ ) {
+ const unitOfWork = workInProgress;
+ const thrownValue = workInProgressThrownValue;
+
+ workInProgressSuspendedReason = NotSuspended;
+ throwAndUnwindWorkLoop(unitOfWork, thrownValue);
+
+ workInProgress = workInProgress!.return!.return;
+ }
shouldTimeSlice ? workLoopConcurrent() : workLoopSync();
break;
} catch (e) {
- console.error('workLoop发生错误', e);
- workInProgress = null;
+ handleThrow(root, e);
}
} while (true);
@@ -338,6 +359,7 @@ function commitRoot(root: FiberRootNode) {
root.current = finishedWork;
// 阶段3/3:Layout
+ commitLayoutEffects(finishedWork, root);
executionContext = prevExecutionContext;
} else {
@@ -399,3 +421,21 @@ function completeUnitOfWork(fiber: FiberNode) {
workInProgress = node;
} while (node !== null);
}
+
+function handleThrow(root: FiberRootNode, thrownValue: any): void {
+ console.error('handleThrow', thrownValue, workInProgress);
+ workInProgressThrownValue = thrownValue;
+ if (
+ thrownValue &&
+ thrownValue.then &&
+ typeof thrownValue.then === 'function'
+ ) {
+ workInProgressSuspendedReason = SuspendedOnDeprecatedThrowPromise;
+ } else {
+ workInProgress = null;
+ }
+}
+
+function throwAndUnwindWorkLoop(unitOfWork: FiberNode, thrownValue: any) {
+ throwException(unitOfWork, thrownValue);
+}
diff --git a/packages/react-reconciler/src/workTags.ts b/packages/react-reconciler/src/workTags.ts
index f2ea036..93d4dab 100644
--- a/packages/react-reconciler/src/workTags.ts
+++ b/packages/react-reconciler/src/workTags.ts
@@ -2,9 +2,28 @@ export type WorkTag =
| typeof FunctionComponent
| typeof HostRoot
| typeof HostComponent
- | typeof HostText;
+ | typeof HostText
+ | typeof Fragment
+ | typeof SuspenseComponent
+ | typeof LazyComponent
+ | typeof OffscreenComponent;
export const FunctionComponent = 0;
export const HostRoot = 3;
export const HostComponent = 5;
export const HostText = 6;
+export const Fragment = 7;
+export const SuspenseComponent = 13;
+export const LazyComponent = 16;
+export const OffscreenComponent = 22;
+
+// "ReferenceError: Cannot access 'current2' before initialization
+// at completeWork (http://localhost:5173/@fs/Users/bytedance/projects/big-react/packages/react-reconciler/src/completeWork.ts?t=1687782804329:88:7)
+// at completeUnitOfWork (http://localhost:5173/@fs/Users/bytedance/projects/big-react/packages/react-reconciler/src/workLoop.ts?t=1687782804329:288:18)
+// at performUnitOfWork (http://localhost:5173/@fs/Users/bytedance/projects/big-react/packages/react-reconciler/src/workLoop.ts?t=1687782804329:280:5)
+// at workLoopSync (http://localhost:5173/@fs/Users/bytedance/projects/big-react/packages/react-reconciler/src/workLoop.ts?t=1687782804329:268:5)
+// at renderRoot (http://localhost:5173/@fs/Users/bytedance/projects/big-react/packages/react-reconciler/src/workLoop.ts?t=1687782804329:163:48)
+// at performSyncWorkOnRoot (http://localhost:5173/@fs/Users/bytedance/projects/big-react/packages/react-reconciler/src/workLoop.ts?t=1687782804329:185:22)
+// at http://localhost:5173/@fs/Users/bytedance/projects/big-react/packages/react-reconciler/src/syncTaskQueue.ts:14:39
+// at Array.forEach ()
+// at flushSyncCallbacks (http://localhost:5173/@fs/Users/bytedance/projects/big-react/packages/react-reconciler/src/syncTaskQueue.ts:14:17)"
\ No newline at end of file
diff --git a/packages/react/index.ts b/packages/react/index.ts
index 1964204..14e414d 100644
--- a/packages/react/index.ts
+++ b/packages/react/index.ts
@@ -5,6 +5,10 @@ import currentDispatcher, {
import { jsx, isValidElement as isValidElementFn } from './src/jsx';
+export { lazy } from './src/lazy';
+
+export { REACT_SUSPENSE_TYPE as Suspense } from 'shared/ReactSymbols';
+
export const useState: Dispatcher['useState'] = (initialState) => {
const dispatcher = resolveDispatcher() as Dispatcher;
return dispatcher.useState(initialState);
@@ -15,6 +19,11 @@ export const useEffect: Dispatcher['useEffect'] = (create, deps) => {
return dispatcher.useEffect(create, deps);
};
+export const useRef: Dispatcher['useRef'] = (initialValue) => {
+ const dispatcher = resolveDispatcher() as Dispatcher;
+ return dispatcher.useRef(initialValue);
+};
+
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
currentDispatcher
};
diff --git a/packages/react/jsx-dev-runtime.ts b/packages/react/jsx-dev-runtime.ts
index bd931d7..c7fa533 100644
--- a/packages/react/jsx-dev-runtime.ts
+++ b/packages/react/jsx-dev-runtime.ts
@@ -2,4 +2,4 @@
* 这个文件是为了方便demos下的示例调试用的
*/
-export { jsxDEV } from './src/jsx';
+export { jsxDEV, Fragment } from './src/jsx';
diff --git a/packages/react/src/currentDispatcher.ts b/packages/react/src/currentDispatcher.ts
index 3672736..0fc2ef9 100644
--- a/packages/react/src/currentDispatcher.ts
+++ b/packages/react/src/currentDispatcher.ts
@@ -1,11 +1,12 @@
import { Action } from 'shared/ReactTypes';
export type Dispatcher = {
- useState: (initialState: (() => T) | T) => [T, Disptach];
+ useState: (initialState: (() => T) | T) => [T, Dispatch];
useEffect: (callback: (() => void) | void, deps: any[] | void) => void;
+ useRef: (initialValue: T) => { current: T };
};
-export type Disptach = (action: Action) => void;
+export type Dispatch = (action: Action) => void;
const currentDispatcher: { current: null | Dispatcher } = {
current: null
diff --git a/packages/react/src/jsx.ts b/packages/react/src/jsx.ts
index 887b6a8..72abc65 100644
--- a/packages/react/src/jsx.ts
+++ b/packages/react/src/jsx.ts
@@ -1,4 +1,4 @@
-import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols';
+import { REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE } from 'shared/ReactSymbols';
import { Key, ElementType, Ref, Props, ReactElement } from 'shared/ReactTypes';
const ReactElement = function (
@@ -27,6 +27,8 @@ function hasValidRef(config: any) {
return config.ref !== undefined;
}
+export const Fragment = REACT_FRAGMENT_TYPE;
+
export const jsx = (type: ElementType, config: any, ...maybeChildren: any) => {
let key: Key = null;
const props: any = {};
diff --git a/packages/react/src/lazy.ts b/packages/react/src/lazy.ts
new file mode 100644
index 0000000..57ba286
--- /dev/null
+++ b/packages/react/src/lazy.ts
@@ -0,0 +1,92 @@
+import { REACT_LAZY_TYPE } from 'shared/ReactSymbols';
+import { Thenable, Wakeable } from 'shared/ReactTypes';
+
+const Uninitialized = -1;
+const Pending = 0;
+const Resolved = 1;
+const Rejected = 2;
+
+type UninitializedPayload = {
+ _status: -1;
+ _result: () => Thenable<{ default: T }>;
+};
+
+type PendingPayload = {
+ _status: 0;
+ _result: Wakeable;
+};
+
+type ResolvedPayload = {
+ _status: 1;
+ _result: { default: T };
+};
+
+type RejectedPayload = {
+ _status: 2;
+ _result: Err;
+};
+
+type Payload =
+ | UninitializedPayload
+ | PendingPayload
+ | ResolvedPayload
+ | RejectedPayload;
+
+export type LazyComponent = {
+ $$typeof: symbol | number;
+ _payload: P;
+ _init: (payload: P) => T;
+};
+
+function lazyInitializer(payload: Payload): T {
+ if (payload._status === Uninitialized) {
+ const ctor = payload._result;
+ const thenable = ctor();
+ const status = payload._status as number;
+ thenable.then(
+ (moduleObject) => {
+ if (status === Pending || status === Uninitialized) {
+ const resolved = payload as unknown as ResolvedPayload;
+ resolved._status = Resolved;
+ resolved._result = moduleObject;
+ }
+ },
+ (error) => {
+ if (status === Pending || status === Uninitialized) {
+ const rejected = payload as unknown as RejectedPayload;
+ rejected._status = Rejected;
+ rejected._result = error;
+ }
+ }
+ );
+ if (payload._status === Uninitialized) {
+ const pending = payload as unknown as PendingPayload;
+ pending._status = Pending;
+ pending._result = thenable;
+ }
+ }
+
+ if (payload._status === Resolved) {
+ const moduleObject = payload._result;
+ return moduleObject.default;
+ } else {
+ throw payload._result;
+ }
+}
+
+export function lazy(
+ ctor: () => Thenable<{ default: T }>
+): LazyComponent> {
+ const payload: Payload = {
+ _status: Uninitialized,
+ _result: ctor
+ };
+
+ const lazyType: LazyComponent> = {
+ $$typeof: REACT_LAZY_TYPE,
+ _payload: payload,
+ _init: lazyInitializer
+ };
+
+ return lazyType;
+}
diff --git a/packages/shared/ReactSymbols.ts b/packages/shared/ReactSymbols.ts
index f87da96..d29966e 100644
--- a/packages/shared/ReactSymbols.ts
+++ b/packages/shared/ReactSymbols.ts
@@ -3,3 +3,19 @@ const supportSymbol = typeof Symbol === 'function' && Symbol.for;
export const REACT_ELEMENT_TYPE = supportSymbol
? Symbol.for('react.element')
: 0xeac7;
+
+export const REACT_FRAGMENT_TYPE = supportSymbol
+ ? Symbol.for('react.fragment')
+ : 0xeacb;
+
+export const REACT_LAZY_TYPE = supportSymbol
+ ? Symbol.for('react.lazy')
+ : 0xead4;
+
+export const REACT_SUSPENSE_TYPE = supportSymbol
+ ? Symbol.for('react.suspense')
+ : 0xead1;
+
+export const REACT_OFFSCREEN_TYPE = supportSymbol
+ ? Symbol.for('react.offscreen')
+ : 0xeae2;
diff --git a/packages/shared/ReactTypes.ts b/packages/shared/ReactTypes.ts
index 2eb6a75..dcb922f 100644
--- a/packages/shared/ReactTypes.ts
+++ b/packages/shared/ReactTypes.ts
@@ -1,9 +1,9 @@
-export type Ref = any;
+export type Ref = { current: any } | ((instance: any) => void);
export type ElementType = any;
export type Key = string | null;
export type Props = {
[key: string]: any;
- children?: ReactElement;
+ children?: any;
};
export interface ReactElement {
@@ -16,3 +16,47 @@ export interface ReactElement {
}
export type Action = State | ((prevState: State) => State);
+
+export interface Wakeable {
+ then(
+ onFulfill: () => Result,
+ onReject: () => Result
+ ): void | Wakeable;
+}
+
+interface ThenableImpl {
+ then(
+ onFulfill: (value: T) => Result,
+ onReject: (error: Err) => Result
+ ): void | Wakeable;
+}
+
+interface UntrackedThenable
+ extends ThenableImpl {
+ status?: void;
+}
+
+export interface PendingThenable
+ extends ThenableImpl {
+ status: 'pending';
+}
+
+export interface FulfilledThenable
+ extends ThenableImpl {
+ status: 'fulfilled';
+ value: T;
+}
+
+export interface RejectedThenable
+ extends ThenableImpl {
+ status: 'rejected';
+ reason: Err;
+}
+
+export type Thenable =
+ | UntrackedThenable
+ | PendingThenable
+ | FulfilledThenable
+ | RejectedThenable;
+
+export type OffscreenMode = 'hidden' | 'visible';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b7ddff9..ff2d4af 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -84,9 +84,11 @@ importers:
packages/react-dom:
specifiers:
react-reconciler: workspace:*
+ scheduler: ^0.23.0
shared: workspace:*
dependencies:
react-reconciler: link:../react-reconciler
+ scheduler: 0.23.0
shared: link:../shared
packages/react-noop-renderer:
diff --git a/jest.config.js b/scripts/jest/jest.config.js
similarity index 95%
rename from jest.config.js
rename to scripts/jest/jest.config.js
index 695a412..b7b6829 100644
--- a/jest.config.js
+++ b/scripts/jest/jest.config.js
@@ -2,6 +2,7 @@ const { defaults } = require('jest-config');
module.exports = {
...defaults,
+ rootDir: process.cwd(),
modulePathIgnorePatterns: ['/.history'],
moduleDirectories: [
// 对于 React ReactDOM
diff --git a/tsconfig.json b/tsconfig.json
index 0ea5b22..fa64390 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,8 +12,8 @@
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
"noImplicitReturns": false,
"skipLibCheck": true,
"typeRoots": ["./types", "./node_modules/@types/"],