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: + *
      + *
    • + * {list} + *
    + * 这种情况我们应该视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: - *
      - *
    • - * {list} - *
    - * 这种情况我们应该视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/"],