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 = {