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/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..c2c2a1b 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,9 @@ "license": "MIT", "scripts": { "build:dev": "rm -rf dist && rollup --config scripts/rollup/dev.config.js", - "demo": "vite serve demos/v11 --config demos/vite.config.js --force", + "demo": "vite serve demos/ref --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..e8a67d8 100644 --- a/packages/react-reconciler/src/beginWork.ts +++ b/packages/react-reconciler/src/beginWork.ts @@ -1,3 +1,4 @@ +import { Fragment } from 'react-reconciler/src/workTags'; import { ReactElement } from 'shared/ReactTypes'; import { mountChildFibers, reconcileChildFibers } from './childFiber'; import { FiberNode } from './fiber'; @@ -10,8 +11,9 @@ import { HostRoot, HostText } from './workTags'; +import { Ref } from './fiberFlags'; -export const beginWork = (workInProgress: FiberNode, renderLane: Lane) => { +export const beginWork = (workInProgress: FiberNode, renderLanes: Lanes) => { if (__LOG__) { console.log('beginWork流程', workInProgress.type); } @@ -20,47 +22,63 @@ 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); 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 updateHostComponent(workInProgress: FiberNode) { +function updateFunctionComponent( + workInProgress: FiberNode, + renderLanes: Lanes +) { + const nextChildren = renderWithHooks(workInProgress, renderLanes); + reconcileChildren(workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +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 +86,27 @@ 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; } } diff --git a/packages/react-reconciler/src/childFiber.ts b/packages/react-reconciler/src/childFiber.ts index 92d3f3d..4933e83 100644 --- a/packages/react-reconciler/src/childFiber.ts +++ b/packages/react-reconciler/src/childFiber.ts @@ -1,12 +1,14 @@ -import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols'; +import { REACT_ELEMENT_TYPE, REACT_FRAGMENT_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'; /** * mount/reconcile只负责 Placement(插入)/Placement(移动)/ChildDeletion(删除) @@ -33,19 +35,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 @@ -60,7 +62,11 @@ function ChildReconciler(shouldTrackEffects: boolean) { if (element.$$typeof === REACT_ELEMENT_TYPE) { if (current.type === element.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 +86,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 +107,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 +216,7 @@ function ChildReconciler(shouldTrackEffects: boolean) { } const created = new FiberNode(HostText, { content }, null); + created.lanes = lanes; created.return = returnFiber; return created; } @@ -180,7 +224,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 +245,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 +306,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..1185c71 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,49 @@ import { FunctionComponent, HostComponent, HostRoot, - HostText + HostText, + Fragment } from './workTags'; 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) { // 插入/移动 @@ -90,8 +93,59 @@ const commitMutationEffectsOnFiber = ( 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 +268,30 @@ function getHostParent(fiber: FiberNode) { console.error('getHostParent未找到hostParent'); } +function isHostTypeFiberNode(fiber: FiberNode) { + const tag = fiber.tag; + return [HostComponent, HostRoot, HostText].includes(tag); +} + +function recordHostChildrenToDelete(beginNode: FiberNode): FiberNode[] { + if (isHostTypeFiberNode(beginNode)) return [beginNode]; + const hostChildrenToDelete: FiberNode[] = []; + const processQueue: FiberNode[] = [beginNode]; + while (processQueue.length) { + const node = processQueue.shift(); + if (node && isHostTypeFiberNode(node)) { + hostChildrenToDelete.push(node); + continue; + } + let childNode = node?.child; + while (childNode) { + processQueue.push(childNode); + childNode = childNode.sibling; + } + } + return hostChildrenToDelete; +} + /** * 删除需要考虑: * HostComponent:需要遍历他的子树,为后续解绑ref创造条件,HostComponent本身只需删除最上层节点即可 @@ -223,20 +301,17 @@ function commitDeletion(childToDelete: FiberNode, root: FiberRootNode) { if (__LOG__) { console.log('删除DOM、组件unmount', childToDelete); } - let firstHostFiber: FiberNode | null = null; + // 在Fragment之前,只需删除子树的根Host节点,但支持Fragment后,可能需要删除同级多个节点 + const hostChildrenToDelete: FiberNode[] = + recordHostChildrenToDelete(childToDelete); commitNestedUnmounts(childToDelete, (unmountFiber) => { switch (unmountFiber.tag) { case HostComponent: - if (firstHostFiber === null) { - firstHostFiber = unmountFiber; - } // 解绑ref + safelyDetachRef(unmountFiber); return; case HostText: - if (firstHostFiber === null) { - firstHostFiber = unmountFiber; - } return; case FunctionComponent: // effect相关操作 @@ -245,9 +320,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; diff --git a/packages/react-reconciler/src/completeWork.ts b/packages/react-reconciler/src/completeWork.ts index 49e61a6..f8dc673 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 } from './fiberFlags'; import { appendInitialChild, createInstance, @@ -8,12 +8,17 @@ import { Instance } from 'hostConfig'; import { + Fragment, FunctionComponent, HostComponent, HostRoot, HostText } from './workTags'; +function markRef(fiber: FiberNode) { + fiber.flags |= Ref; +} + const appendAllChildren = (parent: Instance, workInProgress: FiberNode) => { // 遍历workInProgress所有子孙 DOM元素,依次挂载 let node = workInProgress.child; @@ -73,19 +78,28 @@ 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: bubbleProperties(workInProgress); return null; case HostText: @@ -105,9 +119,6 @@ export const completeWork = (workInProgress: FiberNode) => { // 冒泡flag bubbleProperties(workInProgress); return null; - case FunctionComponent: - 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..23b35d3 100644 --- a/packages/react-reconciler/src/fiber.ts +++ b/packages/react-reconciler/src/fiber.ts @@ -3,7 +3,12 @@ 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 +} from './workTags'; import { CallbackNode } from 'scheduler'; export class FiberNode { @@ -33,7 +38,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,8 +108,11 @@ 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') { @@ -114,10 +122,22 @@ export function createFiberFromElement(element: ReactElement): FiberNode { } 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,6 +167,7 @@ export const createWorkInProgress = ( // 数据 wip.memoizedProps = current.memoizedProps; wip.memoizedState = current.memoizedState; + wip.ref = current.ref; wip.lanes = current.lanes; diff --git a/packages/react-reconciler/src/fiberFlags.ts b/packages/react-reconciler/src/fiberFlags.ts index a89941e..7ac59c3 100644 --- a/packages/react-reconciler/src/fiberFlags.ts +++ b/packages/react-reconciler/src/fiberFlags.ts @@ -7,8 +7,10 @@ export const ChildDeletion = 0b00000000000000000000010000; // 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/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..ac25f10 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'; @@ -338,6 +339,7 @@ function commitRoot(root: FiberRootNode) { root.current = finishedWork; // 阶段3/3:Layout + commitLayoutEffects(finishedWork, root); executionContext = prevExecutionContext; } else { diff --git a/packages/react-reconciler/src/workTags.ts b/packages/react-reconciler/src/workTags.ts index f2ea036..63a63f9 100644 --- a/packages/react-reconciler/src/workTags.ts +++ b/packages/react-reconciler/src/workTags.ts @@ -2,9 +2,11 @@ export type WorkTag = | typeof FunctionComponent | typeof HostRoot | typeof HostComponent - | typeof HostText; + | typeof HostText + | typeof Fragment; export const FunctionComponent = 0; export const HostRoot = 3; export const HostComponent = 5; export const HostText = 6; +export const Fragment = 7; diff --git a/packages/react/index.ts b/packages/react/index.ts index 1964204..88e19b0 100644 --- a/packages/react/index.ts +++ b/packages/react/index.ts @@ -15,6 +15,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/shared/ReactSymbols.ts b/packages/shared/ReactSymbols.ts index f87da96..7608cf6 100644 --- a/packages/shared/ReactSymbols.ts +++ b/packages/shared/ReactSymbols.ts @@ -3,3 +3,7 @@ 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; diff --git a/packages/shared/ReactTypes.ts b/packages/shared/ReactTypes.ts index 2eb6a75..f45e4dc 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 { 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/"],