From 65d60bf761fc6997413053de50ef8b9cb0fcfb1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E9=A2=82?= <313439271@qq.com> Date: Sat, 26 Nov 2022 20:33:24 +0800 Subject: [PATCH 01/15] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 774c215..960ab77 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ - 💪 功能完备,当前可跑通官方测试用例数量:34 - 🚶 按`Git Tag`划分迭代步骤,记录从 0 实现的每个功能 -如果想加入项目对应的`源码交流群`,和 7000+小伙伴们一起交流`React`,可以加我微信,备注「开发」: +如果想看我是如何从0到1实现React18的,可以购买如下课程: + +卡颂 -卡颂的微信 ## TODO List From 52a6aa8c6290e9d8fa2b6a99f761d4ef98f8dc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E9=A2=82?= <313439271@qq.com> Date: Sat, 26 Nov 2022 20:34:32 +0800 Subject: [PATCH 02/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 960ab77..e10b914 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - 💪 功能完备,当前可跑通官方测试用例数量:34 - 🚶 按`Git Tag`划分迭代步骤,记录从 0 实现的每个功能 -如果想看我是如何从0到1实现React18的,可以购买如下课程: +如果想跟着我学习「如何从0到1实现React18」,可以购买如下课程: 卡颂 From ec06bc53309945fb57713761f6d33ba642ba91a5 Mon Sep 17 00:00:00 2001 From: kasong <313439271@qq.com> Date: Tue, 6 Dec 2022 11:49:54 +0800 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4jest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- pnpm-lock.yaml | 2 ++ jest.config.js => scripts/jest/jest.config.js | 1 + tsconfig.json | 4 ++-- 4 files changed, 6 insertions(+), 3 deletions(-) rename jest.config.js => scripts/jest/jest.config.js (95%) 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/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/"], From 5bfdb7afbc5b562213c8ad851fcc8bffc3b54a3a Mon Sep 17 00:00:00 2001 From: kasong <313439271@qq.com> Date: Mon, 19 Dec 2022 11:43:40 +0800 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20=E4=BA=8B=E4=BB=B6=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demos/v11/main.tsx | 2 +- demos/vite.config.js | 4 +++ packages/react-dom/src/SyntheticEvent.ts | 44 +++++++----------------- packages/react-dom/src/hostConfig.ts | 11 +++--- packages/react-dom/src/root.ts | 4 +-- 5 files changed, 25 insertions(+), 40 deletions(-) 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..d862717 100644 --- a/demos/vite.config.js +++ b/demos/vite.config.js @@ -22,6 +22,10 @@ export default defineConfig({ find: 'react-dom', replacement: path.resolve(__dirname, '../packages/react-dom') }, + { + find: 'react-reconciler', + replacement: path.resolve(__dirname, '../packages/react-reconciler') + }, { find: 'hostConfig', replacement: path.resolve( diff --git a/packages/react-dom/src/SyntheticEvent.ts b/packages/react-dom/src/SyntheticEvent.ts index 40292b3..a928ea0 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,13 +17,12 @@ 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; }; } @@ -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--; From d4cf6b6de1eec37a015199b057af8cb9e0f5f73a Mon Sep 17 00:00:00 2001 From: kasong <313439271@qq.com> Date: Sat, 24 Dec 2022 10:52:33 +0800 Subject: [PATCH 05/15] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0Fragment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demos/fragment/index.html | 16 ++ demos/fragment/main.tsx | 23 +++ demos/fragment/vite-env.d.ts | 1 + package.json | 2 +- packages/react-reconciler/src/beginWork.ts | 48 +++-- packages/react-reconciler/src/childFiber.ts | 175 ++++++++++++------ packages/react-reconciler/src/commitWork.ts | 35 +++- packages/react-reconciler/src/completeWork.ts | 6 +- packages/react-reconciler/src/fiber.ts | 25 ++- packages/react-reconciler/src/fiberHooks.ts | 8 +- packages/react-reconciler/src/workTags.ts | 4 +- packages/react/jsx-dev-runtime.ts | 2 +- packages/react/src/jsx.ts | 4 +- packages/shared/ReactSymbols.ts | 4 + packages/shared/ReactTypes.ts | 2 +- 15 files changed, 262 insertions(+), 93 deletions(-) create mode 100644 demos/fragment/index.html create mode 100644 demos/fragment/main.tsx create mode 100644 demos/fragment/vite-env.d.ts 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/package.json b/package.json index 8badd58..522663e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "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/fragment --config demos/vite.config.js --force", "lint": "eslint --ext .ts,.jsx,.tsx --fix --quiet ./packages", "test": "jest --config scripts/jest/jest.config.js" }, diff --git a/packages/react-reconciler/src/beginWork.ts b/packages/react-reconciler/src/beginWork.ts index e923b17..5ee9021 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'; @@ -11,7 +12,7 @@ import { HostText } from './workTags'; -export const beginWork = (workInProgress: FiberNode, renderLane: Lane) => { +export const beginWork = (workInProgress: FiberNode, renderLanes: Lanes) => { if (__LOG__) { console.log('beginWork流程', workInProgress.type); } @@ -20,30 +21,41 @@ 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); + reconcileChildren(workInProgress, nextChildren, renderLanes); return workInProgress.child; } @@ -56,11 +68,15 @@ function updateHostRoot(workInProgress: FiberNode, renderLanes: Lanes) { 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 +84,16 @@ 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 + ); } } diff --git a/packages/react-reconciler/src/childFiber.ts b/packages/react-reconciler/src/childFiber.ts index 92d3f3d..c6a3536 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 + let 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..cbdc492 100644 --- a/packages/react-reconciler/src/commitWork.ts +++ b/packages/react-reconciler/src/commitWork.ts @@ -214,6 +214,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 +241,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[] = []; commitNestedUnmounts(childToDelete, (unmountFiber) => { switch (unmountFiber.tag) { case HostComponent: - if (firstHostFiber === null) { - firstHostFiber = unmountFiber; - } + recordHostChildrenToDelete(hostChildrenToDelete, unmountFiber); // 解绑ref return; case HostText: - if (firstHostFiber === null) { - firstHostFiber = unmountFiber; - } + recordHostChildrenToDelete(hostChildrenToDelete, unmountFiber); return; case FunctionComponent: // effect相关操作 @@ -245,9 +260,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..499e748 100644 --- a/packages/react-reconciler/src/completeWork.ts +++ b/packages/react-reconciler/src/completeWork.ts @@ -8,6 +8,7 @@ import { Instance } from 'hostConfig'; import { + Fragment, FunctionComponent, HostComponent, HostRoot, @@ -85,7 +86,9 @@ export const completeWork = (workInProgress: FiberNode) => { // 冒泡flag bubbleProperties(workInProgress); return null; + case FunctionComponent: case HostRoot: + case Fragment: bubbleProperties(workInProgress); return null; case HostText: @@ -105,9 +108,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..3f419a5 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,7 +108,10 @@ export class FiberRootNode { } } -export function createFiberFromElement(element: ReactElement): FiberNode { +export function createFiberFromElement( + element: ReactElement, + lanes: Lanes +): FiberNode { const { type, key, props } = element; let fiberTag: WorkTag = FunctionComponent; @@ -114,10 +122,21 @@ export function createFiberFromElement(element: ReactElement): FiberNode { } const fiber = new FiberNode(fiberTag, props, key); fiber.type = type; + fiber.lanes = lanes; 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 diff --git a/packages/react-reconciler/src/fiberHooks.ts b/packages/react-reconciler/src/fiberHooks.ts index c1552e3..49667fb 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'; @@ -78,7 +78,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { function mountState( initialState: (() => State) | State -): [State, Disptach] { +): [State, Dispatch] { const hook = mountWorkInProgressHook(); let memoizedState: State; if (initialState instanceof Function) { @@ -100,7 +100,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 +150,7 @@ function updateState(): [State, Disptach] { hook.baseQueue = newBaseQueue; } - return [hook.memoizedState, queue.dispatch as Disptach]; + return [hook.memoizedState, queue.dispatch as Dispatch]; } function dispatchSetState( 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/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/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..79f3388 100644 --- a/packages/shared/ReactTypes.ts +++ b/packages/shared/ReactTypes.ts @@ -3,7 +3,7 @@ export type ElementType = any; export type Key = string | null; export type Props = { [key: string]: any; - children?: ReactElement; + children?: any; }; export interface ReactElement { From 58116b88fc31a9f60f53b2e03ac34ef1744b385f Mon Sep 17 00:00:00 2001 From: kasong <313439271@qq.com> Date: Sat, 24 Dec 2022 11:02:45 +0800 Subject: [PATCH 06/15] feat: let 2 const --- packages/react-reconciler/src/childFiber.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/childFiber.ts b/packages/react-reconciler/src/childFiber.ts index c6a3536..4933e83 100644 --- a/packages/react-reconciler/src/childFiber.ts +++ b/packages/react-reconciler/src/childFiber.ts @@ -111,7 +111,7 @@ function ChildReconciler(shouldTrackEffects: boolean) { lanes: Lanes ): FiberNode | null { // 确定key - let keyToUse = element.key !== null ? element.key : index; + const keyToUse = element.key !== null ? element.key : index; const before = existingChildren.get(keyToUse); From 92c9d3ced964a74cf0c5ad50d36384a1f28af602 Mon Sep 17 00:00:00 2001 From: liyigang Date: Sat, 24 Dec 2022 22:17:39 +0800 Subject: [PATCH 07/15] fix: dispatch --- packages/react-reconciler/src/updateQueue.ts | 4 ++-- packages/react/src/currentDispatcher.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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/src/currentDispatcher.ts b/packages/react/src/currentDispatcher.ts index 3672736..802cf05 100644 --- a/packages/react/src/currentDispatcher.ts +++ b/packages/react/src/currentDispatcher.ts @@ -1,11 +1,11 @@ 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; }; -export type Disptach = (action: Action) => void; +export type Dispatch = (action: Action) => void; const currentDispatcher: { current: null | Dispatcher } = { current: null From 616cc9325dd9a49f59ea93fabb068d93def411fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E9=A2=82?= <313439271@qq.com> Date: Tue, 3 Jan 2023 13:51:32 +0800 Subject: [PATCH 08/15] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index e10b914..8098776 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,7 @@ - 💪 功能完备,当前可跑通官方测试用例数量:34 - 🚶 按`Git Tag`划分迭代步骤,记录从 0 实现的每个功能 -如果想跟着我学习「如何从0到1实现React18」,可以购买如下课程: - -卡颂 +如果想跟着我学习「如何从0到1实现React18」,可以[点击这里](https://qux.xet.tech/s/2wiFh1) ## TODO List From 725e09df94589af675d29517fb96e2c8dacaee8d Mon Sep 17 00:00:00 2001 From: kasong <313439271@qq.com> Date: Tue, 17 Jan 2023 18:04:36 +0800 Subject: [PATCH 09/15] fix: noop-renderer support Fragment --- demos/noop-renderer/index.html | 16 +++++++++++ demos/noop-renderer/main.tsx | 21 ++++++++++++++ demos/noop-renderer/vite-env.d.ts | 1 + demos/vite.config.js | 7 ++++- package.json | 2 +- packages/react-dom/src/SyntheticEvent.ts | 2 +- packages/react-noop-renderer/src/ReactNoop.ts | 28 +++++++++---------- .../react-noop-renderer/src/hostConfig.ts | 4 +-- 8 files changed, 60 insertions(+), 21 deletions(-) create mode 100644 demos/noop-renderer/index.html create mode 100644 demos/noop-renderer/main.tsx create mode 100644 demos/noop-renderer/vite-env.d.ts 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/vite.config.js b/demos/vite.config.js index d862717..34c4583 100644 --- a/demos/vite.config.js +++ b/demos/vite.config.js @@ -26,11 +26,16 @@ export default defineConfig({ 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-dom/src/hostConfig.ts' + '../packages/react-noop-renderer/src/hostConfig.ts' + // '../packages/react-dom/src/hostConfig.ts' ) } ] diff --git a/package.json b/package.json index 522663e..937b779 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "MIT", "scripts": { "build:dev": "rm -rf dist && rollup --config scripts/rollup/dev.config.js", - "demo": "vite serve demos/fragment --config demos/vite.config.js --force", + "demo": "vite serve demos/noop-renderer --config demos/vite.config.js --force", "lint": "eslint --ext .ts,.jsx,.tsx --fix --quiet ./packages", "test": "jest --config scripts/jest/jest.config.js" }, diff --git a/packages/react-dom/src/SyntheticEvent.ts b/packages/react-dom/src/SyntheticEvent.ts index a928ea0..9cede2d 100644 --- a/packages/react-dom/src/SyntheticEvent.ts +++ b/packages/react-dom/src/SyntheticEvent.ts @@ -29,7 +29,7 @@ export interface DOMElement extends Element { 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; 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 From 66a27ffaf75f7a1ffc487c30ac79abbb13b2209c Mon Sep 17 00:00:00 2001 From: HHX <903040380@qq.com> Date: Mon, 30 Jan 2023 22:03:06 +0800 Subject: [PATCH 10/15] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DupdateHostRoot?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E7=B1=BB=E5=9E=8B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/react-reconciler/src/beginWork.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/beginWork.ts b/packages/react-reconciler/src/beginWork.ts index 5ee9021..9dc66a8 100644 --- a/packages/react-reconciler/src/beginWork.ts +++ b/packages/react-reconciler/src/beginWork.ts @@ -61,7 +61,7 @@ function updateHostComponent(workInProgress: FiberNode, renderLanes: Lanes) { 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); From 41e1b4aff567804a872a19674c1a6efcce07abf6 Mon Sep 17 00:00:00 2001 From: kasong <313439271@qq.com> Date: Thu, 20 Apr 2023 22:52:52 +0800 Subject: [PATCH 11/15] feat: ref --- demos/ref/index.html | 16 +++ demos/ref/main.tsx | 25 +++++ demos/ref/vite-env.d.ts | 1 + demos/vite.config.js | 4 +- package.json | 2 +- packages/react-reconciler/src/beginWork.ts | 13 +++ packages/react-reconciler/src/commitWork.ts | 104 +++++++++++++----- packages/react-reconciler/src/completeWork.ts | 15 ++- packages/react-reconciler/src/fiber.ts | 4 +- packages/react-reconciler/src/fiberFlags.ts | 4 +- packages/react-reconciler/src/fiberHooks.ts | 18 ++- packages/react-reconciler/src/workLoop.ts | 2 + packages/react/index.ts | 5 + packages/react/src/currentDispatcher.ts | 1 + packages/shared/ReactTypes.ts | 2 +- 15 files changed, 181 insertions(+), 35 deletions(-) create mode 100644 demos/ref/index.html create mode 100644 demos/ref/main.tsx create mode 100644 demos/ref/vite-env.d.ts 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/vite.config.js b/demos/vite.config.js index 34c4583..8ceff5b 100644 --- a/demos/vite.config.js +++ b/demos/vite.config.js @@ -34,8 +34,8 @@ export default defineConfig({ find: 'hostConfig', replacement: path.resolve( __dirname, - '../packages/react-noop-renderer/src/hostConfig.ts' - // '../packages/react-dom/src/hostConfig.ts' + // '../packages/react-noop-renderer/src/hostConfig.ts' + '../packages/react-dom/src/hostConfig.ts' ) } ] diff --git a/package.json b/package.json index 937b779..c2c2a1b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "MIT", "scripts": { "build:dev": "rm -rf dist && rollup --config scripts/rollup/dev.config.js", - "demo": "vite serve demos/noop-renderer --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 --config scripts/jest/jest.config.js" }, diff --git a/packages/react-reconciler/src/beginWork.ts b/packages/react-reconciler/src/beginWork.ts index 9dc66a8..e8a67d8 100644 --- a/packages/react-reconciler/src/beginWork.ts +++ b/packages/react-reconciler/src/beginWork.ts @@ -11,6 +11,7 @@ import { HostRoot, HostText } from './workTags'; +import { Ref } from './fiberFlags'; export const beginWork = (workInProgress: FiberNode, renderLanes: Lanes) => { if (__LOG__) { @@ -55,6 +56,7 @@ function updateHostComponent(workInProgress: FiberNode, renderLanes: Lanes) { // 根据element创建fiberNode const nextProps = workInProgress.pendingProps; const nextChildren = nextProps.children; + markRef(workInProgress.alternate, workInProgress); reconcileChildren(workInProgress, nextChildren, renderLanes); return workInProgress.child; } @@ -97,3 +99,14 @@ function reconcileChildren( ); } } + +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/commitWork.ts b/packages/react-reconciler/src/commitWork.ts index cbdc492..d7f8cd5 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'; @@ -29,42 +31,42 @@ import { 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 +92,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 @@ -249,6 +302,7 @@ function commitDeletion(childToDelete: FiberNode, root: FiberRootNode) { case HostComponent: recordHostChildrenToDelete(hostChildrenToDelete, unmountFiber); // 解绑ref + safelyDetachRef(unmountFiber); return; case HostText: recordHostChildrenToDelete(hostChildrenToDelete, unmountFiber); diff --git a/packages/react-reconciler/src/completeWork.ts b/packages/react-reconciler/src/completeWork.ts index 499e748..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, @@ -15,6 +15,10 @@ import { HostText } from './workTags'; +function markRef(fiber: FiberNode) { + fiber.flags |= Ref; +} + const appendAllChildren = (parent: Instance, workInProgress: FiberNode) => { // 遍历workInProgress所有子孙 DOM元素,依次挂载 let node = workInProgress.child; @@ -74,13 +78,20 @@ 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 diff --git a/packages/react-reconciler/src/fiber.ts b/packages/react-reconciler/src/fiber.ts index 3f419a5..23b35d3 100644 --- a/packages/react-reconciler/src/fiber.ts +++ b/packages/react-reconciler/src/fiber.ts @@ -112,7 +112,7 @@ export function createFiberFromElement( element: ReactElement, lanes: Lanes ): FiberNode { - const { type, key, props } = element; + const { type, key, props, ref } = element; let fiberTag: WorkTag = FunctionComponent; if (typeof type === 'string') { @@ -123,6 +123,7 @@ export function createFiberFromElement( const fiber = new FiberNode(fiberTag, props, key); fiber.type = type; fiber.lanes = lanes; + fiber.ref = ref; return fiber; } @@ -166,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 49667fb..9952414 100644 --- a/packages/react-reconciler/src/fiberHooks.ts +++ b/packages/react-reconciler/src/fiberHooks.ts @@ -68,12 +68,14 @@ 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( @@ -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/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/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/src/currentDispatcher.ts b/packages/react/src/currentDispatcher.ts index 802cf05..0fc2ef9 100644 --- a/packages/react/src/currentDispatcher.ts +++ b/packages/react/src/currentDispatcher.ts @@ -3,6 +3,7 @@ import { Action } from 'shared/ReactTypes'; export type Dispatcher = { useState: (initialState: (() => T) | T) => [T, Dispatch]; useEffect: (callback: (() => void) | void, deps: any[] | void) => void; + useRef: (initialValue: T) => { current: T }; }; export type Dispatch = (action: Action) => void; diff --git a/packages/shared/ReactTypes.ts b/packages/shared/ReactTypes.ts index 79f3388..f45e4dc 100644 --- a/packages/shared/ReactTypes.ts +++ b/packages/shared/ReactTypes.ts @@ -1,4 +1,4 @@ -export type Ref = any; +export type Ref = { current: any } | ((instance: any) => void); export type ElementType = any; export type Key = string | null; export type Props = { From ec2c53d04b1b890401f67112cca44a89401cc816 Mon Sep 17 00:00:00 2001 From: Russellwzr Date: Tue, 4 Jul 2023 11:24:29 +0800 Subject: [PATCH 12/15] fix: fix recordHostChildrenToDelete for fragment deletion --- packages/react-reconciler/src/commitWork.ts | 69 +++++++++++---------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/packages/react-reconciler/src/commitWork.ts b/packages/react-reconciler/src/commitWork.ts index d7f8cd5..ec42195 100644 --- a/packages/react-reconciler/src/commitWork.ts +++ b/packages/react-reconciler/src/commitWork.ts @@ -25,7 +25,8 @@ import { FunctionComponent, HostComponent, HostRoot, - HostText + HostText, + Fragment } from './workTags'; let nextEffect: FiberNode | null = null; @@ -267,22 +268,23 @@ 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; +function recordHostChildrenToDelete(beginNode: FiberNode): FiberNode[] { + if (beginNode.tag !== Fragment) return [beginNode]; + const hostChildrenToDelete: FiberNode[] = []; + const processQueue: FiberNode[] = [beginNode]; + while (processQueue.length) { + const node = processQueue.shift(); + if (node && node.tag !== Fragment) { + hostChildrenToDelete.push(node); + continue; + } + let childNode = node?.child; + while (childNode) { + processQueue.push(childNode); + childNode = childNode.sibling; } } + return hostChildrenToDelete; } /** @@ -295,24 +297,25 @@ function commitDeletion(childToDelete: FiberNode, root: FiberRootNode) { console.log('删除DOM、组件unmount', childToDelete); } // 在Fragment之前,只需删除子树的根Host节点,但支持Fragment后,可能需要删除同级多个节点 - const hostChildrenToDelete: FiberNode[] = []; - - commitNestedUnmounts(childToDelete, (unmountFiber) => { - switch (unmountFiber.tag) { - case HostComponent: - recordHostChildrenToDelete(hostChildrenToDelete, unmountFiber); - // 解绑ref - safelyDetachRef(unmountFiber); - return; - case HostText: - recordHostChildrenToDelete(hostChildrenToDelete, unmountFiber); - return; - case FunctionComponent: - // effect相关操作 - commitPassiveEffect(unmountFiber, root, 'unmount'); - return; - } - }); + const hostChildrenToDelete: FiberNode[] = + recordHostChildrenToDelete(childToDelete); + + for (let i = 0; i < hostChildrenToDelete.length; i++) { + commitNestedUnmounts(hostChildrenToDelete[i], (unmountFiber) => { + switch (unmountFiber.tag) { + case HostComponent: + // 解绑ref + safelyDetachRef(unmountFiber); + return; + case HostText: + return; + case FunctionComponent: + // effect相关操作 + commitPassiveEffect(unmountFiber, root, 'unmount'); + return; + } + }); + } if (hostChildrenToDelete.length) { const hostParent = getHostParent(childToDelete) as Container; From c819b8dabb0e5d79e26f407ad1eaf6d3d972ec5a Mon Sep 17 00:00:00 2001 From: Russellwzr Date: Thu, 6 Jul 2023 19:49:47 +0800 Subject: [PATCH 13/15] fix: add function component judgment for recordHostChildrenToDelete to get host node --- packages/react-reconciler/src/commitWork.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/commitWork.ts b/packages/react-reconciler/src/commitWork.ts index ec42195..773af9b 100644 --- a/packages/react-reconciler/src/commitWork.ts +++ b/packages/react-reconciler/src/commitWork.ts @@ -269,12 +269,13 @@ function getHostParent(fiber: FiberNode) { } function recordHostChildrenToDelete(beginNode: FiberNode): FiberNode[] { - if (beginNode.tag !== Fragment) return [beginNode]; + if (beginNode.tag !== Fragment && beginNode.tag !== FunctionComponent) + return [beginNode]; const hostChildrenToDelete: FiberNode[] = []; const processQueue: FiberNode[] = [beginNode]; while (processQueue.length) { const node = processQueue.shift(); - if (node && node.tag !== Fragment) { + if (node && node.tag !== Fragment && node.tag !== FunctionComponent) { hostChildrenToDelete.push(node); continue; } From e622d80a0124985d59e97f56bfd04eed63de8c83 Mon Sep 17 00:00:00 2001 From: Russellwzr Date: Thu, 6 Jul 2023 20:06:31 +0800 Subject: [PATCH 14/15] fix: replace host node judement with isHostTypeFiberNode --- packages/react-reconciler/src/commitWork.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/commitWork.ts b/packages/react-reconciler/src/commitWork.ts index 773af9b..7d92e5e 100644 --- a/packages/react-reconciler/src/commitWork.ts +++ b/packages/react-reconciler/src/commitWork.ts @@ -268,14 +268,18 @@ 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 (beginNode.tag !== Fragment && beginNode.tag !== FunctionComponent) - return [beginNode]; + if (isHostTypeFiberNode(beginNode)) return [beginNode]; const hostChildrenToDelete: FiberNode[] = []; const processQueue: FiberNode[] = [beginNode]; while (processQueue.length) { const node = processQueue.shift(); - if (node && node.tag !== Fragment && node.tag !== FunctionComponent) { + if (node && isHostTypeFiberNode(node)) { hostChildrenToDelete.push(node); continue; } From 9d4b73eb7667eefdace7792b5eba5362d0faff8c Mon Sep 17 00:00:00 2001 From: Russellwzr Date: Thu, 6 Jul 2023 20:19:17 +0800 Subject: [PATCH 15/15] fix: change hostChildren to childToDelete for FC unmount --- packages/react-reconciler/src/commitWork.ts | 30 ++++++++++----------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/react-reconciler/src/commitWork.ts b/packages/react-reconciler/src/commitWork.ts index 7d92e5e..1185c71 100644 --- a/packages/react-reconciler/src/commitWork.ts +++ b/packages/react-reconciler/src/commitWork.ts @@ -305,22 +305,20 @@ function commitDeletion(childToDelete: FiberNode, root: FiberRootNode) { const hostChildrenToDelete: FiberNode[] = recordHostChildrenToDelete(childToDelete); - for (let i = 0; i < hostChildrenToDelete.length; i++) { - commitNestedUnmounts(hostChildrenToDelete[i], (unmountFiber) => { - switch (unmountFiber.tag) { - case HostComponent: - // 解绑ref - safelyDetachRef(unmountFiber); - return; - case HostText: - return; - case FunctionComponent: - // effect相关操作 - commitPassiveEffect(unmountFiber, root, 'unmount'); - return; - } - }); - } + commitNestedUnmounts(childToDelete, (unmountFiber) => { + switch (unmountFiber.tag) { + case HostComponent: + // 解绑ref + safelyDetachRef(unmountFiber); + return; + case HostText: + return; + case FunctionComponent: + // effect相关操作 + commitPassiveEffect(unmountFiber, root, 'unmount'); + return; + } + }); if (hostChildrenToDelete.length) { const hostParent = getHostParent(childToDelete) as Container;