├── packages ├── react-dom │ ├── events │ │ ├── PluginModuleType.ts │ │ ├── ReactSyntheticEventType.ts │ │ ├── EventSystemFlags.ts │ │ ├── getEventTarget.ts │ │ ├── SyntheticEvent.ts │ │ ├── ReactDOMComponentTree.ts │ │ ├── EventRegistry.ts │ │ ├── EventListener.ts │ │ ├── getListener.ts │ │ ├── plugins │ │ │ ├── SimpleEventPlugin.ts │ │ │ └── ChangeEventPlugin.ts │ │ ├── DOMEventProperties.ts │ │ ├── DOMEventNames.ts │ │ ├── ReactDOMEventListener.ts │ │ └── DOMPluginEventSystem.ts │ ├── index.ts │ ├── shared │ │ └── HTMLNodeType.ts │ ├── setTextContent.ts │ ├── ReactDOMComponentTree.ts │ ├── DOMPropertyOperations.ts │ ├── ReactDOMLegacy.ts │ ├── CSSPropertyOperations.ts │ ├── inputValueTracking.ts │ ├── ReactDomRoot.ts │ ├── ReactDOMInput.ts │ ├── ReactDOMComponent.ts │ └── ReactDomHostConfig.ts ├── scheduler │ ├── index.ts │ ├── SchedulerPriorities.ts │ ├── SchedulerMinHeap.ts │ └── SchedulerDOM.ts ├── shared │ ├── ReactSharedInternals.ts │ ├── ReactSymbols.ts │ ├── shallowEqual.ts │ └── ReactTypes.ts ├── react-reconciler │ ├── ReactFiberHostConfig.ts │ ├── ReactRootTags.ts │ ├── ReactHookEffectTags.ts │ ├── ReactTypeOfMode.ts │ ├── Scheduler.ts │ ├── ReactWorkTags.ts │ ├── ReactFiberRoot.ts │ ├── ReactFiberInterleavedUpdates.ts │ ├── ReactFiberFlags.ts │ ├── ReactFiberReconciler.ts │ ├── ReactFiberSyncTaskQueue.ts │ ├── ReactEventPriorities.ts │ ├── ReactInternalTypes.ts │ ├── ReactFiberCompleteWork.ts │ ├── ReactFiber.ts │ ├── ReactUpdateQueue.ts │ ├── ReactFiberLane.ts │ └── ReactFiberBeginWork.ts └── react │ ├── ReactSharedInternals.ts │ ├── ReactCurrentDispatcher.ts │ ├── ReactMemo.ts │ ├── index.ts │ ├── ReactHooks.ts │ └── ReactElement.ts ├── jest.config.js ├── .gitignore ├── docs └── pre-requisite │ ├── index.md │ ├── circular-linked-list.md │ ├── bit-manipulation.md │ ├── priority-queue.md │ └── dfs.md ├── examples ├── PriorityScheduling.tsx ├── StateAndEffect.tsx ├── index.tsx ├── TimeSlicing.tsx ├── ChildrenReconciler.tsx ├── TodoList.tsx ├── MemorizedComponent.tsx └── LayoutEffect.tsx ├── package.json ├── rollup.config.js ├── test ├── ReactJSXElement.test.tsx ├── ReactElement.test.tsx └── Scheduler.test.ts ├── tsconfig.json └── README.md /packages/react-dom/events/PluginModuleType.ts: -------------------------------------------------------------------------------- 1 | export type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | TouchEvent 2 | -------------------------------------------------------------------------------- /packages/scheduler/index.ts: -------------------------------------------------------------------------------- 1 | import * as Scheduler from './SchedulerDOM' 2 | export * from './SchedulerDOM' 3 | export default Scheduler 4 | -------------------------------------------------------------------------------- /packages/shared/ReactSharedInternals.ts: -------------------------------------------------------------------------------- 1 | export { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED as ReactSharedInternals } from '../react/index' 2 | -------------------------------------------------------------------------------- /packages/scheduler/SchedulerPriorities.ts: -------------------------------------------------------------------------------- 1 | export type PriorityLevel = 3 | 1 2 | 3 | export const ImmediatePriority = 1 4 | export const NormalPriority = 3 5 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactFiberHostConfig.ts: -------------------------------------------------------------------------------- 1 | //和宿主环境相关的信息,由于我们使用的是ReactDom进行渲染,直接导出ReactDOMHostConfig就行 2 | export * from '../react-dom/ReactDOMHostConfig' 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test/'], 3 | transform: { 4 | '^.+\\.(ts|tsx)$': 'ts-jest', 5 | }, 6 | testEnvironment: 'jsdom', 7 | } 8 | -------------------------------------------------------------------------------- /packages/shared/ReactSymbols.ts: -------------------------------------------------------------------------------- 1 | const symbolFor = Symbol.for 2 | 3 | export let REACT_ELEMENT_TYPE = symbolFor('react.element') 4 | export let REACT_MEMO_TYPE = symbolFor('react.memo') 5 | -------------------------------------------------------------------------------- /packages/react-dom/index.ts: -------------------------------------------------------------------------------- 1 | import { createRoot } from './ReactDomRoot' 2 | import { render } from './ReactDOMLegacy' 3 | 4 | export { createRoot, render } 5 | export default { 6 | createRoot, 7 | render, 8 | } 9 | -------------------------------------------------------------------------------- /packages/react/ReactSharedInternals.ts: -------------------------------------------------------------------------------- 1 | import { ReactCurrentDispatcher } from './ReactCurrentDispatcher' 2 | 3 | const ReactSharedInternals = { 4 | ReactCurrentDispatcher, 5 | } 6 | 7 | export { ReactSharedInternals } 8 | -------------------------------------------------------------------------------- /packages/react-dom/events/ReactSyntheticEventType.ts: -------------------------------------------------------------------------------- 1 | export type UnknownReactSyntheticEvent = {} 2 | 3 | export type KnownReactSyntheticEvent = {} 4 | 5 | export type ReactSyntheticEvent = 6 | | KnownReactSyntheticEvent 7 | | UnknownReactSyntheticEvent 8 | -------------------------------------------------------------------------------- /packages/react-dom/shared/HTMLNodeType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HTML nodeType values that represent the type of the node 3 | */ 4 | export const ELEMENT_NODE = 1 5 | export const TEXT_NODE = 3 6 | export const COMMENT_NODE = 8 7 | export const DOCUMENT_NODE = 9 8 | export const DOCUMENT_FRAGMENT_NODE = 11 9 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactRootTags.ts: -------------------------------------------------------------------------------- 1 | export type RootTag = 0 | 1 | 2 2 | 3 | /** 4 | * 通过React.render调用时创建的FiberRoot为该值 5 | */ 6 | export const LegacyRoot = 0 7 | export const BlockingRoot = 1 8 | /** 9 | * 通过React.createRoot调用时创建的FiberRoot为该值 10 | */ 11 | export const ConcurrentRoot = 2 12 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactHookEffectTags.ts: -------------------------------------------------------------------------------- 1 | export type HookFlags = number 2 | 3 | export const NoFlags = /* */ 0b000 4 | 5 | // 表示了是否因该触发改effect 6 | export const HasEffect = /* */ 0b001 7 | 8 | //表示了effect触发是所处的阶段 9 | export const Layout = /* */ 0b010 10 | export const Passive = /* */ 0b100 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /public/ 4 | /.pnp 5 | .pnp.js 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* -------------------------------------------------------------------------------- /packages/react-reconciler/ReactTypeOfMode.ts: -------------------------------------------------------------------------------- 1 | export type TypeOfMode = number; 2 | 3 | export const NoMode = /* */ 0b000000; 4 | // TODO: Remove BlockingMode and ConcurrentMode by reading from the root tag instead 5 | export const BlockingMode = /* */ 0b000001; 6 | export const ConcurrentMode = /* */ 0b000010; 7 | -------------------------------------------------------------------------------- /packages/react-dom/events/EventSystemFlags.ts: -------------------------------------------------------------------------------- 1 | export type EventSystemFlags = number 2 | export const IS_EVENT_HANDLE_NON_MANAGED_NODE = 1 3 | export const IS_NON_DELEGATED = 1 << 1 4 | export const IS_CAPTURE_PHASE = 1 << 2 5 | 6 | export const SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS = 7 | IS_EVENT_HANDLE_NON_MANAGED_NODE | IS_NON_DELEGATED | IS_CAPTURE_PHASE 8 | -------------------------------------------------------------------------------- /packages/react-dom/events/getEventTarget.ts: -------------------------------------------------------------------------------- 1 | import { TEXT_NODE } from '../shared/HTMLNodeType' 2 | import { AnyNativeEvent } from './PluginModuleType' 3 | 4 | export const getEventTarget = (nativeEvent: AnyNativeEvent) => { 5 | const target: Element = (nativeEvent.target || window) as Element 6 | 7 | return target.nodeType === TEXT_NODE ? target.parentNode : target 8 | } 9 | -------------------------------------------------------------------------------- /docs/pre-requisite/index.md: -------------------------------------------------------------------------------- 1 | # 开始看源码前你需要知道的算法和数据结构知识 2 | 3 | > :warning: 注意,在这里不会探讨链表,优先队列是怎么实现的,位运算符的作用是什么,我们会把重心放在,用这些数据结构解决实际的问题,来帮你能灵活的运用这些数据结构和技巧,让你在源码中看到相关代码时能轻松的理解他的作用,所以下面的会内容假设你已经有一定的数据结构基础,如果你还不了解上面的数据结构的话可以先去自行了解 4 | 5 | 6 | ## 内容列表 7 | - [位运算](./bit-manipulation.md) 8 | - [优先队列](./priority-queue.md) 9 | - [循环链表](./circular-linked-list.md) 10 | - [dfs](./dfs.md) -------------------------------------------------------------------------------- /packages/react/ReactCurrentDispatcher.ts: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from '../react-reconciler/ReactInternalTypes' 2 | 3 | /** 4 | * 用来保存当前的Dispatcher比如,初次渲染时保存的dispatcher就为HooksDispatcherOnMount 5 | * 组件更新时就为HooksDispatcherOnUpdate, 6 | * 具体逻辑可以查看react-reconciler/ReactFiberHooks下的renderWithHooks函数 7 | */ 8 | const ReactCurrentDispatcher: { 9 | current: null | Dispatcher 10 | } = { 11 | current: null, 12 | } 13 | 14 | export { ReactCurrentDispatcher } 15 | -------------------------------------------------------------------------------- /packages/react/ReactMemo.ts: -------------------------------------------------------------------------------- 1 | import { REACT_MEMO_TYPE } from '../shared/ReactSymbols' 2 | import { ReactElement } from '../shared/ReactTypes' 3 | 4 | export const memo = ( 5 | type: (props: Props) => ReactElement, 6 | compare?: (oldProps: Props, newProps: Props) => boolean 7 | ): any => { 8 | const elementType = { 9 | $$typeof: REACT_MEMO_TYPE, 10 | type, 11 | compare: compare === undefined ? null : compare, 12 | } 13 | 14 | return elementType 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-dom/setTextContent.ts: -------------------------------------------------------------------------------- 1 | import { TEXT_NODE } from './shared/HTMLNodeType' 2 | 3 | export const setTextContent = (node: Element, text: string): void => { 4 | if (text) { 5 | const firstChild = node.firstChild 6 | 7 | if ( 8 | firstChild && 9 | firstChild === node.lastChild && 10 | firstChild.nodeType === TEXT_NODE 11 | ) { 12 | firstChild.nodeValue = text 13 | return 14 | } 15 | } 16 | node.textContent = text 17 | } 18 | -------------------------------------------------------------------------------- /packages/react/index.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from './ReactElement' 2 | import { ReactSharedInternals } from './ReactSharedInternals' 3 | import { useState, useEffect, useLayoutEffect } from './ReactHooks' 4 | import { memo } from './ReactMemo' 5 | 6 | export { 7 | useState, 8 | useEffect, 9 | createElement, 10 | useLayoutEffect, 11 | memo, 12 | ReactSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, 13 | } 14 | 15 | export default { 16 | createElement, 17 | } 18 | -------------------------------------------------------------------------------- /packages/react-dom/ReactDOMComponentTree.ts: -------------------------------------------------------------------------------- 1 | import { Fiber } from '../react-reconciler/ReactInternalTypes' 2 | import { Container } from './ReactDomRoot' 3 | 4 | const randomKey = Math.random().toString(36).slice(2) 5 | 6 | const internalContainerInstanceKey = '__reactContainer$' + randomKey 7 | 8 | /** 9 | * 将该dom节点标记为容器节点(整个React App挂载在的节点) 10 | * @param hostRoot 当前fiber树的根节点 11 | * @param node dom节点 12 | */ 13 | export const markContainerAsRoot = (hostRoot: Fiber, node: Container) => { 14 | ;(node as any)[internalContainerInstanceKey] = hostRoot 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-reconciler/Scheduler.ts: -------------------------------------------------------------------------------- 1 | import * as Scheduler from '../scheduler' 2 | import { PriorityLevel } from '../scheduler/SchedulerPriorities' 3 | 4 | export const now = Scheduler.unstable_now 5 | 6 | export type SchedulerCallback = (isSync: boolean) => SchedulerCallback | null 7 | export const scheduleCallback = Scheduler.unstable_scheduleCallback 8 | export const NormalPriority: PriorityLevel = Scheduler.unstable_NormalPriority 9 | export const ImmediatePriority: PriorityLevel = Scheduler.unstable_ImmediatePriority 10 | export const shouldYield = Scheduler.unstable_shouldYield 11 | export const cancelCallback = Scheduler.unstable_cancelCallback -------------------------------------------------------------------------------- /packages/shared/shallowEqual.ts: -------------------------------------------------------------------------------- 1 | const hasOwnProperty = Object.prototype.hasOwnProperty 2 | const is = Object.is 3 | 4 | export const shallowEqual = (objA: unknown, objB: unknown): boolean => { 5 | if (is(objA, objB)) return true 6 | 7 | if ( 8 | typeof objA !== 'object' || 9 | objA === null || 10 | typeof objB !== 'object' || 11 | objB === null 12 | ) 13 | return false 14 | 15 | const keysA = Object.keys(objA) 16 | const keysB = Object.keys(objB) 17 | 18 | if (keysA.length !== keysB.length) return false 19 | 20 | for (let i = 0; i < keysA.length; ++i) { 21 | if ( 22 | !hasOwnProperty.call(objB, keysA[i]) || 23 | !is((objA as any)[keysA[i]], (objB as any)[keysA[i]]) 24 | ) { 25 | return false 26 | } 27 | } 28 | 29 | return true 30 | } 31 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactWorkTags.ts: -------------------------------------------------------------------------------- 1 | export type WorkTag = 2 | | 0 3 | | 1 4 | | 2 5 | | 3 6 | | 4 7 | | 5 8 | | 6 9 | | 7 10 | | 8 11 | | 9 12 | | 10 13 | | 11 14 | | 12 15 | | 13 16 | | 14 17 | | 15 18 | | 16 19 | | 17 20 | | 18 21 | | 19 22 | | 20 23 | | 21 24 | | 22 25 | | 23 26 | | 24 27 | 28 | export const FunctionComponent = 0 29 | 30 | /** 31 | * FiberRoot.current 32 | */ 33 | export const HostRoot = 3 // Root of a host tree. Could be nested inside another node. 34 | /** 35 | * 文字节点 36 | */ 37 | export const HostText = 6 38 | /** 39 | * 在每经过reconcile之前class和function都是该类组件 40 | */ 41 | export const IndeterminateComponent = 2 // Before we know whether it is function or class 42 | export const ClassComponent = 1 43 | /** 44 | * div span之类的组件 45 | */ 46 | export const HostComponent = 5 47 | export const MemoComponent = 14 48 | export const SimpleMemoComponent = 15 49 | -------------------------------------------------------------------------------- /packages/shared/ReactTypes.ts: -------------------------------------------------------------------------------- 1 | export type ReactEmpty = null | void | boolean 2 | 3 | export type ReactText = string | number 4 | 5 | export type Key = string | number 6 | 7 | export interface ReactElement< 8 | P = any, 9 | T extends string | JSXElementConstructor = 10 | | string 11 | | JSXElementConstructor 12 | > { 13 | type: T 14 | props: P 15 | key: Key | null 16 | } 17 | 18 | export type JSXElementConstructor

= (props: P) => ReactElement | null 19 | 20 | export type ReactNode = ReactElement | ReactText 21 | 22 | export type ReactNodeList = ReactEmpty | ReactElement 23 | 24 | export type EventPriority = 0 | 1 | 2 25 | 26 | /** 27 | * click、keydown、focusin等,这些事件的触发不是连续的,优先级为0。 28 | */ 29 | export const DiscreteEvent: EventPriority = 0 30 | /** 31 | * drag、scroll、mouseover等,特点是连续触发,阻塞渲染,优先级为1。 32 | */ 33 | export const UserBlockingEvent: EventPriority = 1 34 | /** 35 | * canplay、error、audio标签的timeupdate和canplay,优先级最高,为2。 36 | */ 37 | export const ContinuousEvent: EventPriority = 2 38 | -------------------------------------------------------------------------------- /packages/react-dom/events/SyntheticEvent.ts: -------------------------------------------------------------------------------- 1 | import { Fiber } from '../../react-reconciler/ReactInternalTypes' 2 | 3 | export const createSyntheticEvent = () => { 4 | class SyntheticBaseEvent { 5 | _reactName: string | null = null 6 | _targetInst: Fiber 7 | type: string 8 | nativeEvent: { [key: string]: unknown } 9 | target: null | EventTarget 10 | 11 | constructor( 12 | reactName: string | null, 13 | reactEventType: string, 14 | targetInst: Fiber, 15 | nativeEvent: { [key: string]: unknown }, 16 | nativeEventTarget: null | EventTarget 17 | ) { 18 | this._reactName = reactName 19 | this._targetInst = targetInst 20 | this.type = reactEventType 21 | this.nativeEvent = nativeEvent 22 | this.target = nativeEventTarget 23 | } 24 | } 25 | 26 | return SyntheticBaseEvent 27 | } 28 | 29 | export const SyntheticEvent = createSyntheticEvent() 30 | 31 | export const SyntheticMouseEvent = createSyntheticEvent() 32 | 33 | export const SyntheticKeyboardEvent = createSyntheticEvent() 34 | -------------------------------------------------------------------------------- /examples/PriorityScheduling.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 优先级调度的展示,高优先级的任务(点击事件产生的更新)会打断低 3 | * 优先级的任务(直接指向setState的更新) 4 | */ 5 | import React, { useState, useEffect } from '../packages/react' 6 | 7 | export const PriorityScheduling = () => { 8 | const [count, updateCount] = useState(0) 9 | 10 | const onClick = () => { 11 | updateCount((count) => count + 2) 12 | } 13 | 14 | console.log({ count }) 15 | 16 | useEffect(() => { 17 | //暂时不支持ref直接用选择器获取 18 | const button = document.getElementById('discretEventDispatcher')! 19 | setTimeout(() => updateCount(1), 1000) 20 | setTimeout(() => { 21 | button.click() 22 | //根据机能给第二个setTimeout一个合适的时间,或者适当的加长数组的长度 23 | //以保证点击事件触发时,前一个低优先级的更新的render阶段还没有完成 24 | //才能看到插队情况发生 25 | }, 1030) 26 | }, []) 27 | 28 | return ( 29 |

30 | 33 |
34 | {Array.from(new Array(10000)).map((v, index) => ( 35 | {count} 36 | ))} 37 |
38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-dom/events/ReactDOMComponentTree.ts: -------------------------------------------------------------------------------- 1 | import { Fiber } from '../../react-reconciler/ReactInternalTypes' 2 | import { Props } from '../ReactDOMHostConfig' 3 | 4 | const randomKey = Math.random().toString(36).slice(2) 5 | 6 | const internalPropsKey = '__reactProps$' + randomKey 7 | const internalInstanceKey = '__reactFiber$' + randomKey 8 | 9 | export const getFiberCurrentPropsFromNode = (node: Element): Props => { 10 | return (node as any)[internalPropsKey] 11 | } 12 | 13 | export const getClosestInstanceFromNode = (targetNode: Node): Fiber | null => { 14 | const targetInst = (targetNode as any)[internalInstanceKey] 15 | return targetInst ?? null 16 | } 17 | 18 | export const precacheFiberNode = (hostInst: Fiber, node: Element) => { 19 | ;(node as any)[internalInstanceKey] = hostInst 20 | } 21 | 22 | /** 23 | * 将jsx的props挂载到对应的dom节点上,待会该dom触发事件时 24 | * ReactDOM就能从event.target中获取到事件的handlers 25 | * @param node 要挂再属性的dom节点 26 | * @param props 要挂载的属性比如 {onClick: () => {}} 27 | */ 28 | export const updateFiberProps = (node: Element, props: Props): void => { 29 | ;(node as any)[internalPropsKey] = props 30 | } 31 | -------------------------------------------------------------------------------- /packages/react-dom/events/EventRegistry.ts: -------------------------------------------------------------------------------- 1 | import { DOMEventName } from './DOMEventNames' 2 | 3 | export const allNativeEvents: Set = new Set() 4 | 5 | /** 6 | * Mapping from registration name to event name 7 | */ 8 | export const registrationNameDependencies: Record = {} 9 | 10 | export const registerDirectEvent = ( 11 | registrationName: string, 12 | dependencies: DOMEventName[] 13 | ) => { 14 | if (registrationNameDependencies[registrationName]) { 15 | console.error( 16 | 'EventRegistry: More than one plugin attempted to publish the same ' + 17 | 'registration name, `%s`.', 18 | registrationName 19 | ) 20 | } 21 | 22 | registrationNameDependencies[registrationName] = dependencies 23 | 24 | for (let i = 0; i < dependencies.length; ++i) { 25 | allNativeEvents.add(dependencies[i]) 26 | } 27 | } 28 | 29 | export const registerTwoPhaseEvent = ( 30 | registrationName: string, 31 | dependencies: DOMEventName[] 32 | ): void => { 33 | registerDirectEvent(registrationName, dependencies) 34 | registerDirectEvent(registrationName + 'Capture', dependencies) 35 | } 36 | -------------------------------------------------------------------------------- /packages/react-dom/events/EventListener.ts: -------------------------------------------------------------------------------- 1 | export const addEventCaptureListenerWithPassiveFlag = ( 2 | target: EventTarget, 3 | eventType: string, 4 | listener: Function, 5 | passive: boolean 6 | ) => { 7 | target.addEventListener(eventType, listener as EventListener, { 8 | capture: true, 9 | passive, 10 | }) 11 | 12 | return listener 13 | } 14 | 15 | export const addEventCaptureListener = ( 16 | target: EventTarget, 17 | eventType: string, 18 | listener: Function 19 | ) => { 20 | target.addEventListener(eventType, listener as EventListener, true) 21 | return listener 22 | } 23 | 24 | export const addEventBubbleListenerWithPassiveFlag = ( 25 | target: EventTarget, 26 | eventType: string, 27 | listener: Function, 28 | passive: boolean 29 | ) => { 30 | target.addEventListener(eventType, listener as EventListener, { 31 | passive, 32 | }) 33 | 34 | return listener 35 | } 36 | 37 | export const addEventBubbleListener = ( 38 | target: EventTarget, 39 | eventType: string, 40 | listener: Function 41 | ) => { 42 | target.addEventListener(eventType, listener as EventListener, false) 43 | 44 | return listener 45 | } 46 | -------------------------------------------------------------------------------- /examples/StateAndEffect.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 简单的useState和useEffect例子 3 | */ 4 | import React, { useState, useEffect } from '../packages/react' 5 | 6 | export const StateEffectDemo = () => { 7 | const [num, setNum] = useState(0) 8 | const [num1, setNum1] = useState(0) 9 | const [num2, setNum2] = useState(0) 10 | useEffect(() => { 11 | console.log('num', num) 12 | }, [num]) 13 | 14 | useEffect(() => { 15 | console.log('num1', num1) 16 | }, [num1]) 17 | 18 | useEffect(() => { 19 | console.log('num2', num2) 20 | }, [num2]) 21 | 22 | console.log('render') 23 | 24 | return ( 25 | 26 | 27 |
28 | 37 |
38 | 49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /examples/index.tsx: -------------------------------------------------------------------------------- 1 | import React from '../packages/react' 2 | import { createRoot, render } from '../packages/react-dom' 3 | import { LayoutEffectDemo } from './LayoutEffect' 4 | import { PriorityScheduling } from './PriorityScheduling' 5 | import { StateEffectDemo } from './StateAndEffect' 6 | import { TimeSlicingDemo } from './TimeSlicing' 7 | import { TodoList } from './TodoList' 8 | import { ChildrenReconcilerDemo } from './ChildrenReconciler' 9 | import { MemorizedComponentDemo } from './MemorizedComponent' 10 | 11 | createRoot(document.querySelector('#app')!).render() 12 | // createRoot(document.querySelector('#app')!).render() 13 | // createRoot(document.querySelector('#app')!).render() 14 | // createRoot(document.querySelector('#app')!).render() 15 | // createRoot(document.querySelector('#app')!).render() 16 | // createRoot(document.querySelector('#app')!).render() 17 | // createRoot(document.querySelector('#app')!).render() 18 | // render(, document.querySelector('#app')!) 19 | // render(, document.querySelector('#app')!) 20 | // render(, document.querySelector('#app')!) 21 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactFiberRoot.ts: -------------------------------------------------------------------------------- 1 | import { createHostRootFiber } from './ReactFiber' 2 | import { createLaneMap, NoLane, NoLanes, NoTimestamp } from './ReactFiberLane' 3 | import { FiberRoot } from './ReactInternalTypes' 4 | import { RootTag } from './ReactRootTags' 5 | import { initializeUpdateQueue } from './ReactUpdateQueue' 6 | 7 | class FiberRootNode { 8 | callbackNode = null 9 | pendingLanes = NoLanes 10 | expiredLanes = NoLanes 11 | finishedWork = null 12 | current = null as any 13 | eventTimes = createLaneMap(NoLanes) 14 | expirationTimes = createLaneMap(NoTimestamp) 15 | callbackPriority = NoLane 16 | constructor(public containerInfo: any, public tag: RootTag) {} 17 | } 18 | 19 | /** 20 | * 21 | * @param containerInfo 当前创建fiber树所在的dom节点由createRoot方法传入 22 | * @param tag 决定fiber树是以什么模式创建的(concurrent,blocking) 23 | * @returns 返回FiberRoot(整个应用的根节点,其中current保存有当前页面所对应的fiber树) 24 | */ 25 | export const createFiberRoot = ( 26 | containerInfo: any, 27 | tag: RootTag 28 | ): FiberRoot => { 29 | const root: FiberRoot = new FiberRootNode(containerInfo, tag) 30 | 31 | const uninitializedFiber = createHostRootFiber(tag) 32 | root.current = uninitializedFiber 33 | uninitializedFiber.stateNode = root 34 | 35 | initializeUpdateQueue(uninitializedFiber) 36 | 37 | return root 38 | } 39 | -------------------------------------------------------------------------------- /packages/react/ReactHooks.ts: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from '../react-reconciler/ReactInternalTypes' 2 | import { ReactCurrentDispatcher } from './ReactCurrentDispatcher' 3 | 4 | type BasicStateAction = ((a: S) => S) | S 5 | type Dispatch = (a: A) => void 6 | 7 | /** 8 | * 取得此时因该使用的Dispatcher,比如首次mount时的dispatcher就为 9 | * 就为HooksDispatcherOnMount 10 | * 组件更新时就为HooksDispatcherOnUpdate, 11 | * 具体逻辑可以查看react-reconciler/ReactFiberHooks下的renderWithHooks函数 12 | * @returns 13 | */ 14 | const resolveDispatcher = (): Dispatcher => { 15 | const dispatcher = ReactCurrentDispatcher.current 16 | 17 | return dispatcher! 18 | } 19 | 20 | /** 21 | * 更具当前的dispatcher调用对应的useState 22 | * @param initialState 初始状态 23 | * @returns 24 | */ 25 | export const useState = ( 26 | initialState: (() => S) | S 27 | ): [S, Dispatch>] => { 28 | const dispatcher = resolveDispatcher() 29 | 30 | return dispatcher.useState(initialState) 31 | } 32 | 33 | export const useEffect = ( 34 | create: () => (() => void) | void, 35 | deps: unknown[] | void | null 36 | ): void => { 37 | const dispatcher = resolveDispatcher() 38 | return dispatcher.useEffect(create, deps) 39 | } 40 | 41 | export const useLayoutEffect = ( 42 | create: () => (() => void) | void, 43 | deps: unknown[] | void | null 44 | ): void => { 45 | const dispatcher = resolveDispatcher() 46 | return dispatcher.useLayoutEffect(create, deps) 47 | } 48 | -------------------------------------------------------------------------------- /packages/react-dom/DOMPropertyOperations.ts: -------------------------------------------------------------------------------- 1 | const reservedProps = new Set([ 2 | 'children', 3 | 'dangerouslySetInnerHTML', 4 | // TODO: This prevents the assignment of defaultValue to regular 5 | // elements (not just inputs). Now that ReactDOMInput assigns to the 6 | // defaultValue property -- do we need this? 7 | 'defaultValue', 8 | 'defaultChecked', 9 | 'innerHTML', 10 | 'suppressContentEditableWarning', 11 | 'suppressHydrationWarning', 12 | 'style', 13 | ]) 14 | 15 | const attributeNameMap = new Map([ 16 | ['acceptCharset', 'accept-charset'], 17 | ['className', 'class'], 18 | ['htmlFor', 'for'], 19 | ['httpEquiv', 'http-equiv'], 20 | ]) 21 | 22 | const shouldIgnoreAttribute = (name: string) => { 23 | if (reservedProps.has(name)) return true 24 | 25 | if ( 26 | name.length > 2 && 27 | (name[0] === 'o' || name[0] === 'O') && 28 | (name[1] === 'n' || name[1] === 'N') 29 | ) { 30 | return true 31 | } 32 | 33 | return false 34 | } 35 | 36 | /** 37 | * 为dom元素设置属性,比如将className,data-*设置为dom属性 38 | * @param node 要设置属性的dom 39 | * @param name 属性的名称 40 | * @param value 属性的值 41 | */ 42 | export const setValueForProperty = ( 43 | node: Element, 44 | name: string, 45 | value: unknown 46 | ) => { 47 | if (shouldIgnoreAttribute(name)) return 48 | 49 | const attributeName = attributeNameMap.get(name) ?? name 50 | if (value === null) { 51 | node.removeAttribute(attributeName) 52 | } else { 53 | node.setAttribute(attributeName, value + '') 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/react-dom/events/getListener.ts: -------------------------------------------------------------------------------- 1 | import { Fiber } from '../../react-reconciler/ReactInternalTypes' 2 | import { Props } from '../ReactDOMHostConfig' 3 | import { getFiberCurrentPropsFromNode } from './ReactDOMComponentTree' 4 | 5 | const isInteractive = (tag: string): boolean => { 6 | return ( 7 | tag === 'button' || 8 | tag === 'input' || 9 | tag === 'select' || 10 | tag === 'textarea' 11 | ) 12 | } 13 | 14 | const shouldPreventMouseEvent = ( 15 | name: string, 16 | type: string, 17 | props: Props 18 | ): boolean => { 19 | switch (name) { 20 | case 'onClick': 21 | case 'onClickCapture': 22 | case 'onDoubleClick': 23 | case 'onDoubleClickCapture': 24 | case 'onMouseDown': 25 | case 'onMouseDownCapture': 26 | case 'onMouseMove': 27 | case 'onMouseMoveCapture': 28 | case 'onMouseUp': 29 | case 'onMouseUpCapture': 30 | case 'onMouseEnter': 31 | return !!(props.disabled && isInteractive(type)) 32 | default: 33 | return false 34 | } 35 | } 36 | 37 | export const getListener = ( 38 | instance: Fiber, 39 | registrationName: string 40 | ): Function | null => { 41 | const stateNode = instance.stateNode 42 | 43 | if (stateNode === null) return null 44 | 45 | const props = getFiberCurrentPropsFromNode(stateNode) 46 | if (props === null) return null 47 | const listener = (props as any)[registrationName] 48 | if (shouldPreventMouseEvent(registrationName, instance.type, props)) 49 | return null 50 | 51 | return listener ?? null 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactFiberInterleavedUpdates.ts: -------------------------------------------------------------------------------- 1 | import { UpdateQueue as HookQueue } from './ReactFiberHooks' 2 | 3 | let interleavedQueues: HookQueue[] | null = null 4 | 5 | /** 6 | * 向InterleavedQueues加入一个包含interleaved update的queue 7 | * @param queue 要加入的queue 8 | */ 9 | export const pushInterleavedQueue = (queue: HookQueue) => { 10 | if (interleavedQueues === null) { 11 | interleavedQueues = [queue] 12 | } else { 13 | interleavedQueues.push(queue) 14 | } 15 | } 16 | 17 | /** 18 | * 将interleaved queue中的update转移到pending queue中 19 | * 该队列形成的条件可以看react-reconciler\ReactFiberHooks.ts下的 20 | * dispatchAction 21 | */ 22 | export const enqueueInterleavedUpdates = () => { 23 | //将interleaved的updates转移到main queue,每一个queue都有一个interleaved和一个pending 24 | //字段他们分别指向一个循环链表中的最后一个节点,我们需要将interleaved链表加到pending链表的最后 25 | if (interleavedQueues !== null) { 26 | for (let i = 0; i < interleavedQueues.length; ++i) { 27 | const queue = interleavedQueues[i] 28 | 29 | const lastInterleavedUpdate = queue.interleaved 30 | if (lastInterleavedUpdate !== null) { 31 | queue.interleaved = null 32 | const firstInterleavedUpdate = lastInterleavedUpdate.next 33 | const lastPendingUpdate = queue.pending 34 | if (lastPendingUpdate !== null) { 35 | const firstPendingUpdate = lastPendingUpdate.next 36 | lastPendingUpdate.next = firstInterleavedUpdate 37 | lastInterleavedUpdate.next = firstPendingUpdate 38 | } 39 | queue.pending = lastInterleavedUpdate 40 | } 41 | } 42 | interleavedQueues = null 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/TimeSlicing.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 打开performance查看Concurrent Mode下render阶段的时间切片 3 | * 点击一下按钮然后会开始BitList的render阶段,可以看到处于render阶段(也就是点击按钮后的前几秒)时input 4 | * 是可以输入的,但是由于要渲染的东西太多,到commit阶段时就会开始卡住, 5 | * 所以此时会卡顿的瓶颈在浏览器渲染太耗时,而不是在react 6 | */ 7 | 8 | import React, { useState } from '../packages/react' 9 | 10 | const data = Array.from({ length: 50e4 }, (_, i) => i) 11 | const CHUNK_SIZE = 1e4 / 10 12 | 13 | /** 14 | * fiber是最小的工作粒度,如果要保证render过程中能保证浏览器能 15 | * 处于交互的状态就得保证一个fiber render的过程不会太耗 16 | * 事件,所以可以根据机能设置合适的CHUNK_SIZE 17 | * @param param0 18 | * @returns 19 | */ 20 | const Chunk = ({ start }: { start: number }): any => { 21 | const end = Math.min(data.length, start + CHUNK_SIZE) 22 | const children = Array.from({ length: end - start }) 23 | for (let i = start; i < end; ++i) { 24 | children[i - start] =
{i}
25 | } 26 | 27 | return children 28 | } 29 | 30 | const BigList = () => { 31 | const children = [] 32 | 33 | for (let i = 0; i < data.length; i += CHUNK_SIZE) { 34 | children.push() 35 | } 36 | return
{children}
37 | } 38 | 39 | export const TimeSlicingDemo = () => { 40 | const [isShowBigList, setIsShowBigList] = useState(false) 41 | 42 | return ( 43 |
44 | 55 |
56 | 57 |
58 | {isShowBigList ? : null} 59 |
60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /packages/react-dom/ReactDOMLegacy.ts: -------------------------------------------------------------------------------- 1 | import { updateContainer } from '../react-reconciler/ReactFiberReconciler' 2 | import { unbatchedUpdates } from '../react-reconciler/ReactFiberWorkLoop' 3 | import { FiberRoot } from '../react-reconciler/ReactInternalTypes' 4 | import { ReactElement, ReactNodeList } from '../shared/ReactTypes' 5 | import { Container, createLegacyRoot, RootType } from './ReactDomRoot' 6 | 7 | type Component = unknown 8 | 9 | const legacyCreateRootFromDOMContainer = (container: Container): RootType => { 10 | return createLegacyRoot(container) 11 | } 12 | 13 | const legacyRenderSubtreeIntoContainer = ( 14 | parentComponent: Component | null, 15 | children: ReactNodeList, 16 | container: Container, 17 | callback?: Function 18 | ) => { 19 | let root: RootType | undefined = container._reactRootContainer 20 | 21 | let fiberRoot: FiberRoot 22 | if (!root) { 23 | //首次挂载 24 | root = container._reactRootContainer = legacyCreateRootFromDOMContainer( 25 | container 26 | ) 27 | fiberRoot = root._internalRoot 28 | if (typeof callback === 'function') { 29 | const originalCallback = callback 30 | callback = () => { 31 | const instance = fiberRoot.current.child?.stateNode ?? null 32 | originalCallback(instance) 33 | } 34 | } 35 | 36 | unbatchedUpdates(() => { 37 | updateContainer(children, fiberRoot) 38 | }, null) 39 | } else { 40 | throw new Error('Not Implement') 41 | } 42 | 43 | return fiberRoot.current.child?.stateNode 44 | } 45 | 46 | export const render = ( 47 | element: ReactElement, 48 | container: Container, 49 | callback?: Function 50 | ) => { 51 | return legacyRenderSubtreeIntoContainer(null, element, container, callback) 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactFiberFlags.ts: -------------------------------------------------------------------------------- 1 | export type Flags = number 2 | 3 | export const NoFlags = /* */ 0b00000000000000000000000 4 | 5 | export const Placement = /* */ 0b00000000000000000000010 6 | export const Update = /* */ 0b00000000000000000000100 7 | export const PlacementAndUpdate = /* */ Placement | Update 8 | export const Deletion = /* */ 0b00000000000000000001000 9 | export const ChildDeletion = /* */ 0b00000000000000000010000 10 | export const ContentReset = /* */ 0b00000000000000000100000 11 | export const Passive = /* */ 0b00000000000010000000000 12 | 13 | export const MutationMask = Placement | Update | ChildDeletion | ContentReset 14 | export const LayoutMask = Update 15 | 16 | export const BeforeMutationMask = Update 17 | 18 | export const PassiveMask = Passive | ChildDeletion 19 | 20 | // Static tags describe aspects of a fiber that are not specific to a render, 21 | // e.g. a fiber uses a passive effect (even if there are no updates on this particular render). 22 | // This enables us to defer more work in the unmount case, 23 | // since we can defer traversing the tree during layout to look for Passive effects, 24 | // and instead rely on the static flag as a signal that there may be cleanup work. 25 | export const RefStatic = /* */ 0b00001000000000000000000 26 | export const LayoutStatic = /* */ 0b00010000000000000000000 27 | export const PassiveStatic = /* */ 0b00100000000000000000000 28 | 29 | // Union of tags that don't get reset on clones. 30 | // This allows certain concepts to persist without recalculting them, 31 | // e.g. whether a subtree contains passive effects or portals. 32 | export const StaticMask = LayoutStatic | PassiveStatic | RefStatic 33 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactFiberReconciler.ts: -------------------------------------------------------------------------------- 1 | import { Container } from '../react-dom/ReactDomRoot' 2 | import { ReactNodeList } from '../shared/ReactTypes' 3 | import { createFiberRoot } from './ReactFiberRoot' 4 | import { 5 | requestEventTime, 6 | requestUpdateLane, 7 | scheduleUpdateOnFiber, 8 | } from './ReactFiberWorkLoop' 9 | import { Fiber, FiberRoot } from './ReactInternalTypes' 10 | import { RootTag } from './ReactRootTags' 11 | import { createUpdate, enqueueUpdate } from './ReactUpdateQueue' 12 | import { discreteUpdates, batchedEventUpdates } from './ReactFiberWorkLoop' 13 | 14 | /** 15 | * 16 | * @param containerInfo 当前创建的React App所挂载在的dom节点,在concurrent模式下由createRoot方法传入 17 | * @param tag 决定fiber树是以什么模式创建的(concurrent,legacy) 18 | * @returns 返回FiberRoot(整个应用的根节点,其中current保存有当前页面所对应的fiber树) 19 | */ 20 | export const createContainer = ( 21 | containerInfo: Container, 22 | tag: RootTag 23 | ): FiberRoot => { 24 | return createFiberRoot(containerInfo, tag) 25 | } 26 | 27 | /** 28 | * 29 | * @param element 由react.createElement创建的jsx对象在legacy模式下由ReactDom.render方法第一个参数传入 30 | * @param container 整个应用的根节点(类型为FiberRoot),其current属性(类型为Fiber,是否为Fiber树根节点由tag是否为HostRoot决定)保存有当前页面所对应的fiber树 31 | */ 32 | export const updateContainer = ( 33 | element: ReactNodeList, 34 | container: FiberRoot 35 | ) => { 36 | const current: Fiber = container.current 37 | const eventTime = requestEventTime() 38 | //获得该次更新的优先级如果不处于ConcurrentMode下的话优先级永远都为Sync 39 | const lane = requestUpdateLane(current) 40 | //创建一个更新,由于我们只实现了Function类型的组件 41 | //这种类型的update就只有HostRoot用到了 42 | const update = createUpdate() 43 | 44 | update.payload = { element } 45 | enqueueUpdate(current, update) 46 | 47 | /** 48 | * 调度该fiber节点上的更新 49 | */ 50 | scheduleUpdateOnFiber(current, lane, eventTime) 51 | } 52 | 53 | export { discreteUpdates, batchedEventUpdates } 54 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactFiberSyncTaskQueue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DiscreteEventPriority, 3 | getCurrentUpdatePriority, 4 | setCurrentUpdatePriority, 5 | } from './ReactEventPriorities' 6 | import { 7 | scheduleCallback, 8 | SchedulerCallback, 9 | ImmediatePriority, 10 | } from './Scheduler' 11 | 12 | let syncQueue: Array | null = null 13 | let includesLegacySyncCallbacks: boolean = false 14 | let isFlushingSyncQueue: boolean = false 15 | 16 | export const scheduleSyncCallback = (callback: SchedulerCallback) => { 17 | if (syncQueue === null) { 18 | syncQueue = [callback] 19 | } else { 20 | syncQueue.push(callback) 21 | } 22 | } 23 | 24 | export const scheduleLegacySyncCallback = (callback: SchedulerCallback) => { 25 | includesLegacySyncCallbacks = true 26 | scheduleSyncCallback(callback) 27 | } 28 | 29 | export const flushSyncCallbacks = () => { 30 | if (!isFlushingSyncQueue && syncQueue !== null) { 31 | //防止二次进入 32 | isFlushingSyncQueue = true 33 | let i = 0 34 | const previousUpdatePriority = getCurrentUpdatePriority() 35 | try { 36 | const isSync = true 37 | const queue = syncQueue 38 | setCurrentUpdatePriority(DiscreteEventPriority) 39 | 40 | for (; i < queue.length; ++i) { 41 | let callback: SchedulerCallback | null = queue[i] 42 | do { 43 | callback = callback(isSync) 44 | } while (callback !== null) 45 | } 46 | 47 | syncQueue = null 48 | includesLegacySyncCallbacks = false 49 | } catch (error) { 50 | /** 51 | * 如果一个任务发生异常,则跳过他接着调度他后面的任务 52 | */ 53 | if (syncQueue !== null) { 54 | syncQueue = syncQueue.slice(i + 1) 55 | } 56 | scheduleCallback(ImmediatePriority, flushSyncCallbacks, null) 57 | throw error 58 | } finally { 59 | setCurrentUpdatePriority(previousUpdatePriority) 60 | isFlushingSyncQueue = false 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/react-dom/events/plugins/SimpleEventPlugin.ts: -------------------------------------------------------------------------------- 1 | import { Fiber } from '../../../react-reconciler/ReactInternalTypes' 2 | import { DOMEventName } from '../DOMEventNames' 3 | import { 4 | registerSimpleEvents, 5 | topLevelEventsToReactNames, 6 | } from '../DOMEventProperties' 7 | import { 8 | accumulateSinglePhaseListeners, 9 | DispatchQueue, 10 | } from '../DOMPluginEventSystem' 11 | import { EventSystemFlags, IS_CAPTURE_PHASE } from '../EventSystemFlags' 12 | import { AnyNativeEvent } from '../PluginModuleType' 13 | import { SyntheticEvent, SyntheticKeyboardEvent, SyntheticMouseEvent } from '../SyntheticEvent' 14 | 15 | const extractEvents = ( 16 | dispatchQueue: DispatchQueue, 17 | domEventName: DOMEventName, 18 | targetInst: null | Fiber, 19 | nativeEvent: AnyNativeEvent, 20 | nativeEventTarget: null | EventTarget, 21 | eventSystemFlags: EventSystemFlags, 22 | targetContainer: EventTarget 23 | ): void => { 24 | let SyntheticEventCtor = SyntheticEvent 25 | switch (domEventName) { 26 | case 'keydown': 27 | case 'keyup': 28 | SyntheticEventCtor = SyntheticKeyboardEvent 29 | break 30 | case 'click': 31 | SyntheticEventCtor = SyntheticMouseEvent 32 | default: 33 | break 34 | } 35 | 36 | const reactName = topLevelEventsToReactNames.get(domEventName) ?? null 37 | 38 | const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0 39 | const accumulateTargetOnly = !inCapturePhase && domEventName === 'scroll' 40 | 41 | const listeners = accumulateSinglePhaseListeners( 42 | targetInst, 43 | reactName, 44 | inCapturePhase, 45 | accumulateTargetOnly 46 | ) 47 | 48 | if (listeners.length) { 49 | const event = new SyntheticEventCtor( 50 | reactName, 51 | '', 52 | null as any, 53 | nativeEvent as any, 54 | nativeEventTarget 55 | ) 56 | dispatchQueue.push({ event, listeners }) 57 | } 58 | } 59 | 60 | export { registerSimpleEvents as registerEvents, extractEvents } 61 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactEventPriorities.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Lane, 3 | NoLane, 4 | SyncLane, 5 | DefaultLane, 6 | Lanes, 7 | getHighestPriorityLane, 8 | InputContinuousLane, 9 | includesNonIdleWork, 10 | IdleLane, 11 | } from './ReactFiberLane' 12 | 13 | export type EventPriority = Lane 14 | 15 | export const DiscreteEventPriority: EventPriority = SyncLane 16 | export const DefaultEventPriority: EventPriority = DefaultLane 17 | export const ContinuousEventPriority: EventPriority = InputContinuousLane 18 | export const IdleEventPriority: EventPriority = IdleLane 19 | 20 | let currentUpdatePriority: EventPriority = NoLane 21 | 22 | /** 23 | * 当前更新的优先级比如一个click事件产生的更新就为DiscreteEventPriority 24 | * @returns 当前更新的优先级 25 | */ 26 | export const getCurrentUpdatePriority = (): EventPriority => { 27 | return currentUpdatePriority 28 | } 29 | 30 | /** 31 | * 设置当前更新的优先级,比如点击事件产生后,就会调用该方法将其设置为DiscreteEventPriority 32 | * @param newPriority 当前更新的优先级 33 | */ 34 | export const setCurrentUpdatePriority = (newPriority: EventPriority): void => { 35 | currentUpdatePriority = newPriority 36 | } 37 | 38 | /** 39 | * 判断a的优先级是否比b大 40 | * @param a a优先级 41 | * @param b b优先级 42 | * @returns 43 | */ 44 | const isHigherEventPriority = (a: EventPriority, b: EventPriority): boolean => { 45 | return a !== 0 && a < b 46 | } 47 | 48 | /** 49 | * 将lanes转换为与其优先级相符的事件优先级 50 | * @param lanes 要转换的lanes 51 | * @returns 对应的事件优先级 52 | */ 53 | export const lanesToEventPriority = (lanes: Lanes): EventPriority => { 54 | const lane = getHighestPriorityLane(lanes) 55 | //lane的优先级不小于DiscreteEventPriority,直接返回DiscreteEventPriority 56 | if (!isHigherEventPriority(DiscreteEventPriority, lane)) 57 | return DiscreteEventPriority 58 | 59 | //和上面同理 60 | if (!isHigherEventPriority(ContinuousEventPriority, lane)) { 61 | return ContinuousEventPriority 62 | } 63 | 64 | //有lane被占用,但是优先级没有上面的两个高,返回DefaultEventPriority 65 | if (includesNonIdleWork(lane)) return DefaultEventPriority 66 | 67 | return IdleEventPriority 68 | } 69 | -------------------------------------------------------------------------------- /packages/scheduler/SchedulerMinHeap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 最小优先队列实现,具体原理可以自行了解 3 | */ 4 | 5 | type Heap = Array 6 | export type Node = { 7 | id: number 8 | sortIndex: number 9 | } 10 | 11 | const compare = (a: Node, b: Node): number => { 12 | const diff = a.sortIndex - b.sortIndex 13 | return diff !== 0 ? diff : a.id - b.id 14 | } 15 | 16 | const siftUp = (heap: Heap, node: Node, i: number): void => { 17 | let index = i 18 | while (index > 0) { 19 | const parentIndex = (index - 1) >>> 1 20 | const parent = heap[parentIndex] 21 | 22 | if (compare(parent, node) > 0) { 23 | heap[parentIndex] = node 24 | heap[index] = parent 25 | index = parentIndex 26 | } else return 27 | } 28 | } 29 | 30 | export const push = (heap: Heap, node: Node) => { 31 | const index = heap.length 32 | heap.push(node) 33 | siftUp(heap, node, index) 34 | } 35 | 36 | export const peek = (heap: Heap): Node | null => { 37 | return heap.length === 0 ? null : heap[0] 38 | } 39 | 40 | const siftDown = (heap: Heap, node: Node, i: number): void => { 41 | let index = i 42 | const length = heap.length 43 | const halfLength = length >>> 1 44 | while (index < halfLength) { 45 | const leftIndex = (index + 1) * 2 - 1 46 | const left = heap[leftIndex] 47 | const rightIndex = leftIndex + 1 48 | const right = heap[rightIndex] 49 | 50 | if (compare(left, node) < 0) { 51 | if (rightIndex < length && compare(right, left) < 0) { 52 | heap[index] = right 53 | heap[rightIndex] = node 54 | index = rightIndex 55 | } else { 56 | heap[index] = left 57 | heap[leftIndex] = node 58 | index = leftIndex 59 | } 60 | } else if (rightIndex < length && compare(right, node) < 0) { 61 | heap[index] = right 62 | heap[rightIndex] = node 63 | index = rightIndex 64 | } else return 65 | } 66 | } 67 | 68 | export const pop = (heap: Heap): Node | null => { 69 | if (heap.length === 0) return null 70 | 71 | const first = heap[0] 72 | const last = heap.pop()! 73 | 74 | if (last !== first) { 75 | heap[0] = last 76 | siftDown(heap, last, 0) 77 | } 78 | 79 | return first 80 | } 81 | -------------------------------------------------------------------------------- /examples/ChildrenReconciler.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 用来测试子元素的diff是否正确 3 | */ 4 | 5 | import React, { useState } from '../packages/react' 6 | 7 | const NotReuseFiberWhenTypeChange = () => { 8 | const [isShowDiv, setIsShowDiv] = useState(true) 9 | 10 | return ( 11 |
12 | {isShowDiv ?
test
:

test

} 13 | 20 |
21 | ) 22 | } 23 | 24 | const TriggerUpdate = () => { 25 | const [count, setCount] = useState(0) 26 | 27 | return ( 28 |
29 | {count} 30 | 37 |
38 | ) 39 | } 40 | 41 | const BubbleFlagsOnChildrenOfBailoutComponent = () => { 42 | return ( 43 |
    44 | 45 |
46 | ) 47 | } 48 | 49 | const ResetContentWhenTextChildrenChangeToOther = () => { 50 | const [isShowText, setIsShowText] = useState(false) 51 | 52 | return ( 53 |
54 | 61 |
{isShowText ?
div
: 'Directed Text'}
62 |
63 | ) 64 | } 65 | 66 | const UpdateTextNodeDemo = () => { 67 | const [count, setCount] = useState(0) 68 | return ( 69 |
70 |
{count}-
71 | 78 |
79 | ) 80 | } 81 | 82 | export const ChildrenReconcilerDemo = () => { 83 | return ( 84 |
85 | NotReuseFiberWhenTypeChange 86 | 87 |
88 | ResetContentWhenNormalChildrenChangeToText 89 | 90 |
91 | UpdateTextNodeDemo 92 | 93 |
94 | BubbleFlagsOnChildrenOfBailoutHostComponent 95 | 96 |
97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /packages/react-dom/events/DOMEventProperties.ts: -------------------------------------------------------------------------------- 1 | import { DOMEventName } from './DOMEventNames' 2 | import { registerTwoPhaseEvent } from './EventRegistry' 3 | 4 | const simpleEventPluginEvents = [ 5 | 'abort', 6 | 'auxClick', 7 | 'cancel', 8 | 'canPlay', 9 | 'canPlayThrough', 10 | 'click', 11 | 'close', 12 | 'contextMenu', 13 | 'copy', 14 | 'cut', 15 | 'drag', 16 | 'dragEnd', 17 | 'dragEnter', 18 | 'dragExit', 19 | 'dragLeave', 20 | 'dragOver', 21 | 'dragStart', 22 | 'drop', 23 | 'durationChange', 24 | 'emptied', 25 | 'encrypted', 26 | 'ended', 27 | 'error', 28 | 'gotPointerCapture', 29 | 'input', 30 | 'invalid', 31 | 'keyDown', 32 | 'keyPress', 33 | 'keyUp', 34 | 'load', 35 | 'loadedData', 36 | 'loadedMetadata', 37 | 'loadStart', 38 | 'lostPointerCapture', 39 | 'mouseDown', 40 | 'mouseMove', 41 | 'mouseOut', 42 | 'mouseOver', 43 | 'mouseUp', 44 | 'paste', 45 | 'pause', 46 | 'play', 47 | 'playing', 48 | 'pointerCancel', 49 | 'pointerDown', 50 | 'pointerMove', 51 | 'pointerOut', 52 | 'pointerOver', 53 | 'pointerUp', 54 | 'progress', 55 | 'rateChange', 56 | 'reset', 57 | 'seeked', 58 | 'seeking', 59 | 'stalled', 60 | 'submit', 61 | 'suspend', 62 | 'timeUpdate', 63 | 'touchCancel', 64 | 'touchEnd', 65 | 'touchStart', 66 | 'volumeChange', 67 | 'scroll', 68 | 'toggle', 69 | 'touchMove', 70 | 'waiting', 71 | 'wheel', 72 | ] 73 | 74 | export const topLevelEventsToReactNames: Map = new Map() 75 | 76 | const registerSimpleEvent = ( 77 | domEventName: DOMEventName, 78 | reactName: string 79 | ): void => { 80 | topLevelEventsToReactNames.set(domEventName, reactName) 81 | registerTwoPhaseEvent(reactName, [domEventName]) 82 | } 83 | 84 | export const registerSimpleEvents = () => { 85 | for (let i = 0; i < simpleEventPluginEvents.length; ++i) { 86 | const eventName = simpleEventPluginEvents[i] 87 | const domEventName = eventName.toLowerCase() as DOMEventName 88 | const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1) 89 | 90 | registerSimpleEvent(domEventName, 'on' + capitalizedEvent) 91 | } 92 | 93 | registerSimpleEvent('focusin', 'onFocus') 94 | registerSimpleEvent('focusout', 'onBlur') 95 | } 96 | -------------------------------------------------------------------------------- /packages/react/ReactElement.ts: -------------------------------------------------------------------------------- 1 | import { REACT_ELEMENT_TYPE } from '../shared/ReactSymbols' 2 | import { Key } from '../shared/ReactTypes' 3 | 4 | /** 5 | * JSX对象类型 6 | */ 7 | type ReactElement = { 8 | /** 9 | * 该属性的意义[https://overreacted.io/zh-hans/why-do-react-elements-have-typeof-property/] 10 | */ 11 | $$typeof: Symbol 12 | /** 13 | * createElement的第一个参数如果是浏览器标签比如div那么就为一个字符串如果时Function组件那么就为一个函数 14 | * 如果为React的内置组件类型,比如Fragment,StrictMode,那么就为一个Symbol 15 | */ 16 | type: any 17 | /** 18 | * 该节点的key用来提高dom的复用的正确率 19 | */ 20 | key: Key | null 21 | /** 22 | * 该jsx上的属性比如onClick,value等等 23 | */ 24 | props: any 25 | } 26 | 27 | const hasOwnProperty = Object.prototype.hasOwnProperty 28 | 29 | /** 30 | * 保留属性,以下属性不会加入到props中,比如
31 | * 构建出来的jsx对象就是这样的 {key: "1", ref: null, props: {foo: 1}} 32 | */ 33 | const RESERVED_PROPS = { 34 | key: true, 35 | ref: true, 36 | } 37 | 38 | /** 39 | * jsx转换为javascript时调用的函数比如`
`就会被转换为React.createElement('div', null) 40 | * @param type 该组件的类型,如果时div,p这种浏览器标签就为字符串,如果时Function组件那么就为一个函数 41 | * @param config 初始props,包含key和ref经过该函数后会将key和ref抽出 42 | * @param children 该组件的children 43 | * @returns 返回一个JSX对象 44 | */ 45 | export function createElement( 46 | type: any, 47 | config?: Record, 48 | ...children: any[] 49 | ): ReactElement { 50 | const props: Record = {} 51 | let key: Key | null = null 52 | 53 | for (const propName in config) { 54 | if ( 55 | hasOwnProperty.call(config, propName) && 56 | !RESERVED_PROPS.hasOwnProperty(propName) 57 | ) { 58 | props[propName] = config[propName] 59 | } 60 | } 61 | 62 | if (type?.defaultProps) { 63 | const defaultProps = type.defaultProps 64 | for (const propName in defaultProps) { 65 | if (props[propName] === undefined) { 66 | props[propName] = defaultProps[propName] 67 | } 68 | } 69 | } 70 | 71 | if (config?.key !== undefined) { 72 | key = '' + config?.key 73 | } 74 | 75 | if (children.length === 1) { 76 | props.children = children[0] 77 | } else if (children.length > 1) { 78 | props.children = children 79 | } 80 | 81 | const element: ReactElement = { 82 | $$typeof: REACT_ELEMENT_TYPE, 83 | type: type, 84 | key: key, 85 | props, 86 | } 87 | 88 | return element 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-react", 3 | "version": "1.0.0", 4 | "description": "基于react17精简而来的tiny-react", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --watchAll", 8 | "build": "rimraf public && cross-env NODE_ENV=production rollup -c", 9 | "start": "rimraf public && cross-env NODE_ENV=production rollup -c -w" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/PiNengShaoNian/tiny-react.git" 14 | }, 15 | "author": "quguiyou1076@qq.com", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/PiNengShaoNian/tiny-react/issues" 19 | }, 20 | "homepage": "https://github.com/PiNengShaoNian/tiny-react#readme", 21 | "devDependencies": { 22 | "@babel/core": "^7.8.7", 23 | "@babel/plugin-proposal-class-properties": "^7.8.3", 24 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3", 25 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 26 | "@babel/plugin-transform-runtime": "^7.8.3", 27 | "@babel/preset-env": "^7.8.7", 28 | "@babel/preset-react": "^7.8.3", 29 | "@babel/preset-typescript": "^7.8.3", 30 | "@babel/runtime-corejs3": "^7.8.7", 31 | "@rollup/plugin-commonjs": "^11.0.2", 32 | "@rollup/plugin-html": "^0.1.1", 33 | "@rollup/plugin-node-resolve": "^7.1.1", 34 | "@rollup/plugin-replace": "^2.3.1", 35 | "@types/jest": "^26.0.23", 36 | "@types/node": "^13.7.7", 37 | "@types/react": "^16.9.23", 38 | "@types/react-dom": "^16.9.5", 39 | "@typescript-eslint/eslint-plugin": "^4.24.0", 40 | "@typescript-eslint/parser": "^4.24.0", 41 | "babel-eslint": "^10.0.0", 42 | "babel-plugin-react-require": "^3.1.3", 43 | "cross-env": "^7.0.2", 44 | "eslint": "^7.5.0", 45 | "jest": "^27.0.1", 46 | "rimraf": "^3.0.2", 47 | "rollup": "^1.32.0", 48 | "rollup-plugin-babel": "^4.3.3", 49 | "rollup-plugin-livereload": "^1.0.4", 50 | "rollup-plugin-scss": "^2.1.0", 51 | "rollup-plugin-serve": "^1.0.1", 52 | "rollup-plugin-sourcemaps": "^0.6.3", 53 | "rollup-plugin-terser": "^5.2.0", 54 | "serve": "^11.3.0", 55 | "ts-jest": "^27.0.1", 56 | "typescript": "^4.2.4" 57 | }, 58 | "eslintConfig": { 59 | "parser": "@typescript-eslint/parser", 60 | "plugins": [ 61 | "@typescript-eslint" 62 | ], 63 | "rules": { 64 | "@typescript-eslint/no-unused-expressions": "error" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/react-dom/events/DOMEventNames.ts: -------------------------------------------------------------------------------- 1 | export type DOMEventName = 2 | | 'abort' 3 | | 'afterblur' // Not a real event. This is used by event experiments. 4 | // These are vendor-prefixed so you should use the exported constants instead: 5 | // 'animationiteration' | 6 | // 'animationend | 7 | // 'animationstart' | 8 | | 'beforeblur' // Not a real event. This is used by event experiments. 9 | | 'beforeinput' 10 | | 'blur' 11 | | 'canplay' 12 | | 'canplaythrough' 13 | | 'cancel' 14 | | 'change' 15 | | 'click' 16 | | 'close' 17 | | 'compositionend' 18 | | 'compositionstart' 19 | | 'compositionupdate' 20 | | 'contextmenu' 21 | | 'copy' 22 | | 'cut' 23 | | 'dblclick' 24 | | 'auxclick' 25 | | 'drag' 26 | | 'dragend' 27 | | 'dragenter' 28 | | 'dragexit' 29 | | 'dragleave' 30 | | 'dragover' 31 | | 'dragstart' 32 | | 'drop' 33 | | 'durationchange' 34 | | 'emptied' 35 | | 'encrypted' 36 | | 'ended' 37 | | 'error' 38 | | 'focus' 39 | | 'focusin' 40 | | 'focusout' 41 | | 'fullscreenchange' 42 | | 'gotpointercapture' 43 | | 'hashchange' 44 | | 'input' 45 | | 'invalid' 46 | | 'keydown' 47 | | 'keypress' 48 | | 'keyup' 49 | | 'load' 50 | | 'loadstart' 51 | | 'loadeddata' 52 | | 'loadedmetadata' 53 | | 'lostpointercapture' 54 | | 'message' 55 | | 'mousedown' 56 | | 'mouseenter' 57 | | 'mouseleave' 58 | | 'mousemove' 59 | | 'mouseout' 60 | | 'mouseover' 61 | | 'mouseup' 62 | | 'paste' 63 | | 'pause' 64 | | 'play' 65 | | 'playing' 66 | | 'pointercancel' 67 | | 'pointerdown' 68 | | 'pointerenter' 69 | | 'pointerleave' 70 | | 'pointermove' 71 | | 'pointerout' 72 | | 'pointerover' 73 | | 'pointerup' 74 | | 'popstate' 75 | | 'progress' 76 | | 'ratechange' 77 | | 'reset' 78 | | 'scroll' 79 | | 'seeked' 80 | | 'seeking' 81 | | 'select' 82 | | 'selectstart' 83 | | 'selectionchange' 84 | | 'stalled' 85 | | 'submit' 86 | | 'suspend' 87 | | 'textInput' // Intentionally camelCase. Non-standard. 88 | | 'timeupdate' 89 | | 'toggle' 90 | | 'touchcancel' 91 | | 'touchend' 92 | | 'touchmove' 93 | | 'touchstart' 94 | // These are vendor-prefixed so you should use the exported constants instead: 95 | // 'transitionend' | 96 | | 'volumechange' 97 | | 'waiting' 98 | | 'wheel' 99 | 100 | -------------------------------------------------------------------------------- /packages/react-dom/CSSPropertyOperations.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 不需要加单位的css属性 3 | */ 4 | const isUnitlessNumber = { 5 | animationIterationCount: true, 6 | aspectRatio: true, 7 | borderImageOutset: true, 8 | borderImageSlice: true, 9 | borderImageWidth: true, 10 | boxFlex: true, 11 | boxFlexGroup: true, 12 | boxOrdinalGroup: true, 13 | columnCount: true, 14 | columns: true, 15 | flex: true, 16 | flexGrow: true, 17 | flexPositive: true, 18 | flexShrink: true, 19 | flexNegative: true, 20 | flexOrder: true, 21 | gridArea: true, 22 | gridRow: true, 23 | gridRowEnd: true, 24 | gridRowSpan: true, 25 | gridRowStart: true, 26 | gridColumn: true, 27 | gridColumnEnd: true, 28 | gridColumnSpan: true, 29 | gridColumnStart: true, 30 | fontWeight: true, 31 | lineClamp: true, 32 | lineHeight: true, 33 | opacity: true, 34 | order: true, 35 | orphans: true, 36 | tabSize: true, 37 | widows: true, 38 | zIndex: true, 39 | zoom: true, 40 | 41 | // SVG-related properties 42 | fillOpacity: true, 43 | floodOpacity: true, 44 | stopOpacity: true, 45 | strokeDasharray: true, 46 | strokeDashoffset: true, 47 | strokeMiterlimit: true, 48 | strokeOpacity: true, 49 | strokeWidth: true, 50 | } 51 | 52 | /** 53 | * 根据CSS属性名称和CSS值为他加上合适的单位 54 | * @param name CSS属性名 55 | * @param value CSS值 56 | * @param isCustomProperty 是否时自定义属性比如 `--bg-color`这种 57 | * @returns 返回加上单位后的CSS值 58 | */ 59 | const dangerousStyleValue = ( 60 | name: string, 61 | value: unknown, 62 | isCustomProperty: boolean 63 | ): string => { 64 | const isEmpty = value === null || typeof value === 'boolean' || value === '' 65 | 66 | if (isEmpty) return '' 67 | 68 | if ( 69 | !isCustomProperty && 70 | typeof value === 'number' && 71 | value !== 0 && 72 | !(isUnitlessNumber.hasOwnProperty(name) && (isUnitlessNumber as any)[name]) 73 | ) { 74 | return value + 'px' 75 | } 76 | 77 | return ('' + value).trim() 78 | } 79 | 80 | /** 81 | * 根据style属性里面的对象,为dom节点设置样式 82 | */ 83 | export const setValueForStyles = ( 84 | node: HTMLElement, 85 | styles: Record 86 | ) => { 87 | const style = node.style 88 | for (let styleName in styles) { 89 | if (!styles.hasOwnProperty(styleName)) continue 90 | const isCustomProperty = styleName.indexOf('--') === 0 91 | 92 | const styleValue = dangerousStyleValue( 93 | styleName, 94 | styles[styleName], 95 | isCustomProperty 96 | ) 97 | 98 | if (styleName === 'float') { 99 | styleName = 'cssFloat' 100 | } 101 | 102 | style[styleName as any] = styleValue 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /docs/pre-requisite/circular-linked-list.md: -------------------------------------------------------------------------------- 1 | # 循环链表 2 | 3 | 循环链表可能是React中使用最频繁的数据结构了,在这里我们会实现一个类似的数据结构 4 | 首先我们先定义LinkedList的结构 5 | ```ts 6 | type ListNode = { 7 | /** 8 | * 该节点存储的值 9 | */ 10 | value: TValue 11 | next: ListNode 12 | } 13 | 14 | type CircularLinkedList = { 15 | /** 16 | * 链表的尾节点 17 | */ 18 | last: null | ListNode 19 | } 20 | ``` 21 | 下面在为该循环链表实现几个函数 22 | 23 | `add`函数用来为链表添加一个节点 24 | ```ts 25 | const add = (list: CircularLinkedList, value: T): void => { 26 | const node: ListNode = { 27 | value, 28 | next: null as any, 29 | } 30 | 31 | if (!list.last) { 32 | //node是第一个加入的节点 33 | node.next = node 34 | } else { 35 | //node为现在的最后一个节点 36 | const first = list.last.next 37 | node.next = first 38 | list.last.next = node 39 | } 40 | 41 | list.last = node 42 | } 43 | ``` 44 | `traverse`函数用来遍历该循环链表 45 | ```ts 46 | const traverse = ( 47 | list: CircularLinkedList, 48 | callback: (v: ListNode) => void 49 | ): void => { 50 | if (!list.last) return 51 | const first = list.last.next 52 | let node = first 53 | 54 | do { 55 | callback(node) 56 | node = node.next 57 | } while (node !== first) 58 | } 59 | ``` 60 | 61 | `merge`函数用来将第二个链表合并到第一个链表上 62 | ```ts 63 | const merge = ( 64 | list1: CircularLinkedList, 65 | list2: CircularLinkedList 66 | ): void => { 67 | const last1 = list1.last 68 | const last2 = list2.last 69 | 70 | if (last1 === null || last2 === null) return 71 | 72 | const first1 = last1.next 73 | const first2 = last2.next 74 | 75 | //将第二个链表的头接到第一个链表的末尾 76 | last1.next = first2 77 | //现在last2为最后的节点,将他的next指向整个链表的头也就是first1 78 | last2.next = first1 79 | 80 | //list1.last指向合并后链表的最后节点,而现在last2才是最后节点 81 | list1.last = last2 82 | //清除list2 83 | list2.last = null 84 | } 85 | ``` 86 | 上面的函数,就是React所有用到的循环链表操作了,我们在为他写点测试 87 | ```ts 88 | import * as assert from 'assert' 89 | 90 | const main = (): void => { 91 | const list1: CircularLinkedList = { 92 | last: null, 93 | } 94 | 95 | add(list1, 1) 96 | add(list1, 2) 97 | add(list1, 3) 98 | const actual: number[] = [] 99 | traverse(list1, (v) => { 100 | actual.push(v.value) 101 | }) 102 | assert.deepStrictEqual(actual, [1, 2, 3]) 103 | 104 | const list2: CircularLinkedList = { 105 | last: null, 106 | } 107 | add(list2, 4) 108 | add(list2, 5) 109 | add(list2, 6) 110 | merge(list1, list2) 111 | assert.strictEqual(list2.last, null) 112 | actual.length = 0 113 | traverse(list1, (v) => { 114 | actual.push(v.value) 115 | }) 116 | assert.deepStrictEqual(actual, [1, 2, 3, 4, 5, 6]) 117 | } 118 | 119 | main() 120 | ``` -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import replace from '@rollup/plugin-replace' 2 | import resolve from '@rollup/plugin-node-resolve' 3 | import commonjs from '@rollup/plugin-commonjs' 4 | import babel from 'rollup-plugin-babel' 5 | import html from '@rollup/plugin-html' 6 | import { terser } from 'rollup-plugin-terser' 7 | import serve from 'rollup-plugin-serve' 8 | import livereload from 'rollup-plugin-livereload' 9 | import sourcemaps from 'rollup-plugin-sourcemaps' 10 | 11 | const isProd = process.env.NODE_ENV === 'development' 12 | const extensions = ['.js', '.ts', '.tsx'] 13 | 14 | export default { 15 | input: 'examples/index.tsx', 16 | output: { 17 | file: 'public/index.js', 18 | format: 'iife', 19 | sourcemap: true, 20 | }, 21 | plugins: [ 22 | replace({ 23 | 'process.env.NODE_ENV': JSON.stringify( 24 | isProd ? 'production' : 'development' 25 | ), 26 | preventAssignment: true, 27 | }), 28 | sourcemaps(), 29 | resolve({ 30 | extensions, 31 | }), 32 | commonjs({ 33 | include: /node_modules/, 34 | }), 35 | babel({ 36 | extensions, 37 | exclude: /node_modules/, 38 | babelrc: false, 39 | runtimeHelpers: true, 40 | presets: [ 41 | '@babel/preset-env', 42 | '@babel/preset-react', 43 | '@babel/preset-typescript', 44 | ], 45 | plugins: [ 46 | 'react-require', 47 | '@babel/plugin-syntax-dynamic-import', 48 | '@babel/plugin-proposal-class-properties', 49 | [ 50 | '@babel/plugin-proposal-object-rest-spread', 51 | { 52 | useBuiltIns: true, 53 | }, 54 | ], 55 | [ 56 | '@babel/plugin-transform-runtime', 57 | { 58 | corejs: 3, 59 | helpers: true, 60 | regenerator: true, 61 | useESModules: false, 62 | }, 63 | ], 64 | ], 65 | }), 66 | html({ 67 | fileName: 'index.html', 68 | title: 'Rollup + TypeScript + React = ❤️', 69 | template: ({ title }) => { 70 | return ` 71 | 72 | 73 | 74 | 75 | ${title} 76 | 77 | 78 |
79 | 80 | 81 | 82 | ` 83 | }, 84 | }), 85 | // scss({ 86 | // output: 'public/index.css', 87 | // }), 88 | isProd && terser(), 89 | !isProd && 90 | serve({ 91 | host: 'localhost', 92 | port: 3000, 93 | open: true, 94 | contentBase: ['public'], 95 | }), 96 | !isProd && 97 | livereload({ 98 | watch: 'public', 99 | }), 100 | ], 101 | } 102 | -------------------------------------------------------------------------------- /packages/react-dom/inputValueTracking.ts: -------------------------------------------------------------------------------- 1 | type ValueTracker = { 2 | getValue(): string 3 | setValue(value: string): void 4 | stopTracking(): void 5 | } 6 | type WrapperState = { _valueTracker?: ValueTracker } 7 | type ElementWithValueTracker = HTMLInputElement & WrapperState 8 | 9 | const getTracker = ( 10 | node: ElementWithValueTracker 11 | ): ValueTracker | undefined => { 12 | return node._valueTracker 13 | } 14 | 15 | const isCheckable = (elem: HTMLInputElement) => { 16 | const type = elem.type 17 | 18 | const nodeName = elem.nodeName 19 | 20 | return ( 21 | nodeName && 22 | nodeName.toLowerCase() === 'input' && 23 | (type === 'checkbox' || type === 'radio') 24 | ) 25 | } 26 | 27 | const detachTracker = (node: ElementWithValueTracker): void => { 28 | node._valueTracker = undefined 29 | } 30 | 31 | const trackValueOnNode = (node: any): ValueTracker | undefined => { 32 | const valueField = isCheckable(node) ? 'checked' : 'value' 33 | 34 | const descriptor = Object.getOwnPropertyDescriptor( 35 | node.constructor.prototype, 36 | valueField 37 | ) 38 | 39 | let currentValue = '' + node[valueField] 40 | 41 | if (!descriptor) return 42 | 43 | const { get, set } = descriptor 44 | 45 | Object.defineProperty(node, valueField, { 46 | configurable: true, 47 | get() { 48 | return get?.call(this) 49 | }, 50 | set(value) { 51 | currentValue = '' + value 52 | set?.call(this, value) 53 | }, 54 | }) 55 | 56 | const tracker: ValueTracker = { 57 | getValue() { 58 | return currentValue 59 | }, 60 | setValue(value) { 61 | currentValue = '' + value 62 | }, 63 | stopTracking() { 64 | detachTracker(node) 65 | delete node[valueField] 66 | }, 67 | } 68 | 69 | return tracker 70 | } 71 | 72 | export const track = (node: ElementWithValueTracker) => { 73 | if (getTracker(node)) return 74 | 75 | node._valueTracker = trackValueOnNode(node) 76 | } 77 | 78 | const getValueFromNode = (node: HTMLInputElement): string => { 79 | let value = '' 80 | if (!node) { 81 | return value 82 | } 83 | 84 | if (isCheckable(node)) value = node.checked ? 'true' : 'false' 85 | else { 86 | value = node.value 87 | } 88 | 89 | return value 90 | } 91 | 92 | export const updateValueIfChanged = ( 93 | node: ElementWithValueTracker 94 | ): boolean => { 95 | if (!node) return false 96 | 97 | const tracker = getTracker(node) 98 | 99 | //如果到这个时刻还没有tracker,如果此时不更新,那么以后也不太可能会正常更新了 100 | if (!tracker) return true 101 | 102 | const lastValue = tracker.getValue() 103 | const nextValue = getValueFromNode(node) 104 | if (nextValue !== lastValue) { 105 | tracker.setValue(nextValue) 106 | return true 107 | } 108 | 109 | return false 110 | } 111 | -------------------------------------------------------------------------------- /test/ReactJSXElement.test.tsx: -------------------------------------------------------------------------------- 1 | import React from '../packages/react' 2 | 3 | describe('ReactJSXElement', () => { 4 | let Component: Function 5 | beforeEach(() => { 6 | Component = () => { 7 | return
8 | } 9 | }) 10 | it('returns a complete element according to spec', () => { 11 | const element = 12 | expect(element.type).toBe(Component) 13 | expect(element.key).toBe(null) 14 | const expectation = {} 15 | Object.freeze(expectation) 16 | expect(element.props).toEqual(expectation) 17 | }) 18 | 19 | it('allows a lower-case to be passed as the string type', () => { 20 | const element =
21 | expect(element.type).toBe('div') 22 | expect(element.key).toBe(null) 23 | const expectation = {} 24 | Object.freeze(expectation) 25 | expect(element.props).toEqual(expectation) 26 | }) 27 | 28 | it('allows a string to be passed as the type', () => { 29 | const TagName = 'div' 30 | const element = 31 | expect(element.type).toBe('div') 32 | expect(element.key).toBe(null) 33 | const expectation = {} 34 | Object.freeze(expectation) 35 | expect(element.props).toEqual(expectation) 36 | }) 37 | 38 | it('does not reuse the object that is spread into props', () => { 39 | const config = { foo: 1 } 40 | const element = 41 | expect(element.props.foo).toBe(1) 42 | config.foo = 2 43 | expect(element.props.foo).toBe(1) 44 | }) 45 | 46 | it('extracts key and ref from the rest of the props', () => { 47 | const element = 48 | expect(element.type).toBe(Component) 49 | expect(element.key).toBe('12') 50 | const expectation = { foo: '56' } 51 | Object.freeze(expectation) 52 | expect(element.props).toEqual(expectation) 53 | }) 54 | 55 | it('coerces the key to a string', () => { 56 | const element = 57 | expect(element.type).toBe(Component) 58 | expect(element.key).toBe('12') 59 | const expectation = { foo: '56' } 60 | Object.freeze(expectation) 61 | expect(element.props).toEqual(expectation) 62 | }) 63 | 64 | it('does not override children if no JSX children are provided', () => { 65 | const element = 66 | expect(element.props.children).toBe('text') 67 | }) 68 | 69 | it('merges JSX children onto the children prop in an array', () => { 70 | const a = 1 71 | const b = 2 72 | const c = 3 73 | const element = ( 74 | 75 | {a} 76 | {b} 77 | {c} 78 | 79 | ) 80 | expect(element.props.children).toEqual([1, 2, 3]) 81 | }) 82 | 83 | it('is indistinguishable from a plain object', () => { 84 | const element =
85 | const object = {} 86 | expect(element.constructor).toBe(object.constructor) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /examples/TodoList.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 简单的代办应用 3 | */ 4 | import { CSSProperties } from 'react' 5 | import React, { useEffect } from '../packages/react' 6 | import { useState } from '../packages/react' 7 | 8 | type TodoItem = { 9 | label: string 10 | status: boolean 11 | } 12 | 13 | export const TodoList = () => { 14 | const [todoList, setTodoList] = useState([]) 15 | const [todo, setTodo] = useState('') 16 | 17 | useEffect(() => { 18 | console.log(todo) 19 | }, [todo]) 20 | 21 | const handleCompleteClick = (index: number) => { 22 | const next = [...todoList] 23 | next[index] = { 24 | ...todoList[index], 25 | status: true, 26 | } 27 | setTodoList(next) 28 | } 29 | 30 | const handleDeleteClick = (index: number) => { 31 | const next: TodoItem[] = [...todoList] 32 | 33 | console.log({ index }) 34 | 35 | next.splice(index, 1) 36 | setTodoList(next) 37 | } 38 | 39 | useEffect(() => { 40 | console.log(todoList) 41 | }, [todoList.length]) 42 | 43 | return ( 44 |
45 | 请输入代办项,并按回车 46 |
47 | { 50 | if ( 51 | e.nativeEvent.key === 'Enter' && 52 | todo && 53 | !todoList.find((v) => v.label === todo) 54 | ) { 55 | setTodoList([ 56 | ...todoList, 57 | { 58 | status: false, 59 | label: todo, 60 | }, 61 | ]) 62 | setTodo('') 63 | } 64 | }} 65 | onChange={(e) => { 66 | setTodo(e.target.value) 67 | }} 68 | /> 69 |
    70 | {todoList.map((v, i) => ( 71 |
  • 77 |
    {v.label}
    78 |
    79 | 80 | 81 |
    82 |
  • 83 | ))} 84 |
85 |
86 | ) 87 | } 88 | 89 | const baseTodoItemStyle: CSSProperties = { 90 | height: 30, 91 | background: 'indigo', 92 | margin: 0, 93 | padding: 0, 94 | marginBottom: 5, 95 | color: '#fff', 96 | listStyle: 'none', 97 | display: 'flex', 98 | alignItems: 'center', 99 | justifyContent: 'space-between', 100 | } 101 | 102 | const completedTodoItemStyle: CSSProperties = { 103 | ...baseTodoItemStyle, 104 | background: 'red', 105 | } 106 | 107 | const styles: Record< 108 | 'todoItem' | 'listContainer' | 'completedTodoItem', 109 | CSSProperties 110 | > = { 111 | listContainer: { 112 | margin: 0, 113 | padding: 0, 114 | }, 115 | todoItem: baseTodoItemStyle, 116 | completedTodoItem: completedTodoItemStyle, 117 | } 118 | -------------------------------------------------------------------------------- /packages/react-dom/ReactDomRoot.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createContainer, 3 | updateContainer, 4 | } from '../react-reconciler/ReactFiberReconciler' 5 | import { FiberRoot } from '../react-reconciler/ReactInternalTypes' 6 | import { 7 | ConcurrentRoot, 8 | LegacyRoot, 9 | RootTag, 10 | } from '../react-reconciler/ReactRootTags' 11 | import { ReactNodeList } from '../shared/ReactTypes' 12 | import { listenToAllSupportedEvents } from './events/DOMPluginEventSystem' 13 | import { markContainerAsRoot } from './ReactDOMComponentTree' 14 | import { COMMENT_NODE } from './shared/HTMLNodeType' 15 | 16 | export type Container = 17 | | (Element & { _reactRootContainer?: RootType }) 18 | | (Document & { _reactRootContainer?: RootType }) 19 | 20 | export type RootType = { 21 | render(children: ReactNodeList): void 22 | unmount(): void 23 | _internalRoot: FiberRoot 24 | } 25 | 26 | /** 27 | * createRoot创建节点时使用的类(ConcurrentRoot) 28 | */ 29 | class ReactDomRoot { 30 | _internalRoot: FiberRoot 31 | constructor(container: Container) { 32 | this._internalRoot = createRootImpl(container, ConcurrentRoot) 33 | } 34 | 35 | render(children: ReactNodeList): void {} 36 | 37 | unmount() {} 38 | } 39 | 40 | /** 41 | * ReactDOM.render创建FiberRoot的时使用的类 42 | */ 43 | class ReactDOMLegacyRoot { 44 | _internalRoot: FiberRoot 45 | constructor(container: Container) { 46 | this._internalRoot = createRootImpl(container, LegacyRoot) 47 | } 48 | 49 | unmount() {} 50 | 51 | render(children: ReactNodeList): void {} 52 | } 53 | 54 | /** 55 | * 将JSX对象渲染为Dom并挂载到container上 56 | */ 57 | ReactDomRoot.prototype.render = ReactDOMLegacyRoot.prototype.render = function ( 58 | children: ReactNodeList 59 | ) { 60 | const root = this._internalRoot 61 | 62 | updateContainer(children, root) 63 | } 64 | 65 | export const createRoot = (container: Container) => { 66 | return new ReactDomRoot(container) 67 | } 68 | 69 | /** 70 | * 71 | * @param container createRoot的第一个参数,一个dom元素,表示该React App要改在的容器 72 | * @param tag 该Root的类型用createRoot创建的为ConcurrentRoot, 73 | * 用ReactDOM.render创建的为LegacyRoot 74 | *该标签对以后的流程有深远的影响 75 | * @returns 返回一个FiberRoot,一个在并不对应任何DOM的最上层节点, 76 | * 所有的fiber节点的根节点,注意HostRoot(Fiber树根节点)可以有多个, 77 | * 但是FiberRoot只有一个 78 | */ 79 | const createRootImpl = (container: Container, tag: RootTag): FiberRoot => { 80 | const root = createContainer(container, tag) 81 | markContainerAsRoot(root.current, container) 82 | 83 | const rootContainerElement = 84 | container.nodeType === COMMENT_NODE ? container.parentNode! : container 85 | 86 | //在container上初始化事件系统,在这里将ReactDom接入react,保证了 87 | //基于fiber树的事件代理,以及基于不同事件优先级调度能正常工作 88 | listenToAllSupportedEvents(rootContainerElement) 89 | return root 90 | } 91 | 92 | /** 93 | * 创建一个LegacyRoot也就是ReactDOM.render所创建出的root 94 | * 该模式没有优先级调度,以及时间切片功能 95 | * @param container 挂载ReactApp 的dom容器 96 | * @returns 97 | */ 98 | export const createLegacyRoot = (container: Container): RootType => { 99 | return new ReactDOMLegacyRoot(container) 100 | } 101 | -------------------------------------------------------------------------------- /packages/react-dom/ReactDOMInput.ts: -------------------------------------------------------------------------------- 1 | type InputWithWrapperState = HTMLInputElement & { 2 | _wrapperState: { 3 | initialValue: unknown 4 | initialChecked?: boolean 5 | controlled?: boolean 6 | } 7 | } 8 | 9 | export const getHostProps = (element: Element, props: Object) => { 10 | const node = element as InputWithWrapperState 11 | const checked = (props as any).checked 12 | const hostProps = Object.assign({}, props, { 13 | defaultChecked: undefined, 14 | defaultValue: undefined, 15 | value: undefined, 16 | checked: checked != null ? checked : node._wrapperState.initialChecked, 17 | }) 18 | 19 | return hostProps 20 | } 21 | 22 | type ToStringValue = boolean | number | Object | string | null | void 23 | 24 | export function getToStringValue(value: unknown): ToStringValue { 25 | switch (typeof value) { 26 | case 'boolean': 27 | case 'number': 28 | case 'object': 29 | case 'string': 30 | case 'undefined': 31 | return value 32 | default: 33 | // function, symbol are assigned as empty strings 34 | return '' 35 | } 36 | } 37 | 38 | function isControlled(props: Record) { 39 | const usesChecked = props.type === 'checkbox' || props.type === 'radio' 40 | return usesChecked ? props.checked != null : props.value != null 41 | } 42 | 43 | export const initWrapperState = ( 44 | element: Element, 45 | props: Record 46 | ) => { 47 | let node: InputWithWrapperState = element as any 48 | const defaultValue = 49 | (props as any).defaultValue == null ? '' : (props as any).defaultValue 50 | node._wrapperState = { 51 | initialChecked: 52 | props.checked != null ? props.checked : props.defaultChecked, 53 | initialValue: getToStringValue( 54 | props.value != null ? props.value : defaultValue 55 | ), 56 | controlled: isControlled(props), 57 | } 58 | } 59 | 60 | export const postMountWrapper = ( 61 | element: Element, 62 | props: Record 63 | ) => { 64 | const node: InputWithWrapperState = element as any 65 | 66 | if (props.hasOwnProperty('value') || props.hasOwnProperty('defaultValue')) { 67 | const initialValue = node._wrapperState.initialValue + '' 68 | 69 | if (initialValue !== node.value) { 70 | node.value = initialValue 71 | } 72 | 73 | node.defaultValue = initialValue 74 | node.defaultChecked = !!node._wrapperState.initialChecked 75 | } 76 | } 77 | 78 | export const updateChecked = (element: Element, props: Record) => { 79 | const node: InputWithWrapperState = element as any 80 | const checked = props.checked 81 | if (checked != null) { 82 | node.setAttribute('checked', checked + '') 83 | } 84 | } 85 | 86 | export const updateWrapper = (element: Element, props: Record) => { 87 | const node: InputWithWrapperState = element as any 88 | 89 | updateChecked(element, props) 90 | 91 | const value = getToStringValue(props.value) 92 | 93 | if (value != null) { 94 | node.value = value + '' 95 | } 96 | 97 | if (props.hasOwnProperty('value')) { 98 | node.defaultValue = value + '' 99 | } else if (props.hasOwnProperty('defaultValue')) { 100 | node.defaultValue = props.defaultValue + '' 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/MemorizedComponent.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | import React, { memo, useState, useEffect } from '../packages/react' 3 | 4 | type Color = '#fff' | 'green' 5 | type Component = 6 | | 'NestedComponent' 7 | | 'NormalComponent' 8 | | 'MemorizedComponentWithUnstableProps' 9 | | 'MemorizedNestedComponent' 10 | | 'MemorizedComponentWithCustomCompareFunc' 11 | | 'MemorizedComponent' 12 | 13 | const prevColors: Record = { 14 | NestedComponent: 'green', 15 | NormalComponent: 'green', 16 | MemorizedComponentWithUnstableProps: 'green', 17 | MemorizedNestedComponent: 'green', 18 | MemorizedComponentWithCustomCompareFunc: 'green', 19 | MemorizedComponent: 'green', 20 | } 21 | 22 | const useCurrentOutlineStyle = (componentName: Component): CSSProperties => { 23 | const currColor = prevColors[componentName] === '#fff' ? 'green' : '#fff' 24 | prevColors[componentName] = currColor 25 | return { 26 | outline: `1px solid ${currColor}`, 27 | } 28 | } 29 | 30 | const NormalComponent = () => { 31 | return ( 32 |
33 | NormalComponent 34 | 35 | 36 |
37 | ) 38 | } 39 | 40 | const NestedComponent = () => { 41 | const outlineStyle = useCurrentOutlineStyle('NestedComponent') 42 | return
-- NestedComponent
43 | } 44 | 45 | const MemorizedNestedComponent = memo(() => { 46 | const outlineStyle = useCurrentOutlineStyle('MemorizedNestedComponent') 47 | 48 | return
-- MemorizedNestedComponent
49 | }) 50 | 51 | const MemorizedComponent = memo(() => { 52 | const outlineStyle = useCurrentOutlineStyle('MemorizedComponent') 53 | 54 | return
MemorizedComponent
55 | }) 56 | 57 | const MemorizedComponentWithUnstableProps = memo<{ count: number }>( 58 | ({ count }) => { 59 | const outlineStyle = useCurrentOutlineStyle( 60 | 'MemorizedComponentWithUnstableProps' 61 | ) 62 | 63 | return ( 64 |
65 | MemorizedComponentWithUnstableProps {count} 66 |
67 | ) 68 | } 69 | ) 70 | 71 | const MemorizedComponentWithCustomCompareFunc = memo<{ text: string }>( 72 | ({ text }) => { 73 | const outlineStyle = useCurrentOutlineStyle( 74 | 'MemorizedComponentWithCustomCompareFunc' 75 | ) 76 | 77 | return
最大字符长度-8 {text}
78 | }, 79 | (oldProps, newProps) => newProps.text.length > 8 80 | ) 81 | 82 | export const MemorizedComponentDemo = () => { 83 | const [count, setCount] = useState(0) 84 | const [text, setText] = useState('') 85 | return ( 86 |
87 | 88 |
89 | 90 |
91 | 92 |
93 | 94 |
95 | 102 |
103 | { 106 | setText(e.target.value) 107 | }} 108 | /> 109 |
110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /docs/pre-requisite/bit-manipulation.md: -------------------------------------------------------------------------------- 1 | # 位运算相关知识,能让你更容易理解lanes的相关代码 2 | 3 | > :warning: 注意,在这里已经假设你知道位运算符的作用了,如果还不知道可以看[MDN的解释](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Expressions_and_Operators#%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6) 4 | 5 | ___ 6 | ## 问题1 .实现`gitBit(num: number, i: number):boolean `函数 7 | 该函数会会根据数字num,第i个bit的值返回一个布尔值,如果第i个bit为1则返回true否则返回false(i从零开始) 8 | 9 | ### 示例1 10 | 输入: `num = 0b1110` , `i = 0` 11 | 返回: `false` 12 | 解释: `0b1110`的第零位为`0` 13 | 14 | ### 示例2 15 | 输入: `num = 0b1110` , `i = 1` 16 | 返回: `true` 17 | 解释: `0b1110`的第一位为`1` 18 | 19 | ### 示例3 20 | 输入: `num = 0b1110` , `i = 2` 21 | 返回: `true` 22 | 解释: `0b1110`的第二位为`1` 23 | 24 | ### 参考答案 25 | 26 | ```js 27 | const getBit = (num: number, i: number): boolean => { 28 | return (num & (1 << i)) !== 0 29 | } 30 | ``` 31 | ### 解释 32 | 要取num的第i位bit,只需让num和除了第i位为1其他位全为0的数按位与,然后在判断他是否为零即可,如果为零表示该bit为0,如果不为零则表示为1 33 | 这种数可以通过`<<`轻松获得 34 | `1 << 0` 等价于 `0b0000000000000000000000000000001` 35 | `1 << 1` 等价于 `0b0000000000000000000000000000010` 36 | `1 << 2` 等价于 `0b0000000000000000000000000000100` 37 | `1 << 3` 等价于 `0b0000000000000000000000000001000` 38 | ___ 39 | 40 | ## 问题2. 实现`setBit(num: number, i: number): number`函数 41 | 该函数会将数字num,第i位bit置为1并返回 42 | 43 | ### 示例1 44 | 输入: `num = 0b00000` , `i = 0` 45 | 返回: `0b00001` 46 | 47 | ### 示例2 48 | 输入: `num = 0b00000` , `i = 1` 49 | 返回: `0b00010` 50 | 51 | ### 示例3 52 | 输入: `num = 0b01010` , `i = 2` 53 | 返回: `0b01110` 54 | 55 | ### 参考答案 56 | 57 | ```ts 58 | const setBit = (num: number, i: number): number => { 59 | return (1 << i) | num 60 | } 61 | ``` 62 | ### 解释 63 | 要想将num的第i位bit设为1,只需让num和除了第i位为1其他位全为0的数按位或即可 64 | 这种数可以通过`<<`轻松获得 65 | `1 << 0` 等价于 `0b0000000000000000000000000000001` 66 | `1 << 1` 等价于 `0b0000000000000000000000000000010` 67 | `1 << 2` 等价于 `0b0000000000000000000000000000100` 68 | `1 << 3` 等价于 `0b0000000000000000000000000001000` 69 | ___ 70 | 71 | ## 问题3. 实现`clearBit(num: number, i: number): number`函数 72 | 该函数会将数字num,第i位bit置为0并返回 73 | 74 | ### 示例1 75 | 输入: `num = 0b00000` , `i = 0` 76 | 返回: `0b00000` 77 | 78 | ### 示例2 79 | 输入: `num = 0b00010` , `i = 1` 80 | 返回: `0b00000` 81 | 82 | ### 示例3 83 | 输入: `num = 0b01110` , `i = 2` 84 | 返回: `0b01010` 85 | 86 | ### 参考答案 87 | 88 | ```ts 89 | const clearBit = (num: number, i: number): number => { 90 | const mask = ~(1 << i) 91 | return num & mask 92 | } 93 | ``` 94 | ### 解释 95 | 要想将num的第i位bit设为0,只需让num和除了第i位为0其他位全为1的数按位与即可 96 | 这种数可以通过两个步骤获得,先通过`<<`获得除了第i位为1其他全为零的数再通过`~`将他反转即可 97 | 第一步 98 | `1 << 3` 等价于 `0b0000000000000000000000000001000` 99 | 第二步 100 | `~(1<<3)`等价于`0b1111111111111111111111111110111` 101 | ___ 102 | 103 | ## 问题4. 二进制数组转整数 104 | 给你一个数组。数组中的值不是 0 就是 1。已知此数组是一个整数数字的二进制表示形式。 105 | 请你返回该数组所表示数字的 十进制值 。 106 | 107 | ### 示例1 108 | 输入: `[1,0,1]` 109 | 返回: `5` 110 | 111 | ### 示例2 112 | 输入: `[1,0,0,1,0,0,1,1,1,0,0,0,0,0,0]` 113 | 返回: `18880` 114 | 115 | ### 示例3 116 | 输入: `[0,0]` 117 | 返回: `0` 118 | 119 | ### 参考答案 120 | 121 | ```ts 122 | const getDecimalValue = (head: number[]): number => { 123 | let ans = 0 124 | 125 | for (let i = 0; i < head.length; ++i) { 126 | ans = (ans << 1) | head[i] 127 | } 128 | 129 | return ans 130 | } 131 | ``` 132 | ### 解释 133 | 通过位移和或操作注意将二进制位设置到ans上即可 134 | 考虑如下例子,当输入为`[1,1]`时 135 | 第一轮循环先将0位设置1 此时的 `ans = 0b1` 136 | 第二轮先将ans左移一位此时`ans = 0b10`,再将ans的第零位设为1此时`ans = 0b11` 137 | ___ 138 | 139 | ## 相关引用 140 | - [程序员面试金典 第6版 P94 位操作](http://product.dangdang.com/27941258.html) 141 | - [leetcode 1290. 二进制链表转整数](https://leetcode-cn.com/problems/convert-binary-number-in-a-linked-list-to-integer/) 142 | -------------------------------------------------------------------------------- /packages/react-dom/ReactDOMComponent.ts: -------------------------------------------------------------------------------- 1 | import { track } from './inputValueTracking' 2 | import { setTextContent } from './setTextContent' 3 | import { 4 | initWrapperState as ReactDOMInputInitWrapperState, 5 | getHostProps as ReactDOMInputGetHostProps, 6 | postMountWrapper as ReactDOMInputPostMountWrapper, 7 | updateWrapper as ReactDOMInputUpdateWrapper, 8 | } from './ReactDOMInput' 9 | import { setValueForStyles } from './CSSPropertyOperations' 10 | import { setValueForProperty } from './DOMPropertyOperations' 11 | 12 | const STYLE = 'style' 13 | const CHILDREN = 'children' 14 | 15 | const setInitialDOMProperties = ( 16 | tag: string, 17 | domElement: Element, 18 | nextProps: Record 19 | ) => { 20 | for (const propKey in nextProps) { 21 | if (!nextProps.hasOwnProperty(propKey)) continue 22 | 23 | const nextProp = nextProps[propKey] 24 | 25 | if (propKey === STYLE) { 26 | setValueForStyles(domElement as any, nextProp) 27 | } else if (propKey === CHILDREN) { 28 | if (typeof nextProp === 'string') { 29 | const canSetTextContent = tag !== 'textarea' || nextProp !== '' 30 | 31 | if (canSetTextContent) { 32 | setTextContent(domElement, nextProp) 33 | } 34 | } else if (typeof nextProp === 'number') { 35 | setTextContent(domElement, nextProp + '') 36 | } 37 | } else if (nextProp != null) { 38 | setValueForProperty(domElement, propKey, nextProp) 39 | //todo 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * 初始化dom属性 46 | * @param domElement dom元素 47 | * @param tag dom的tag对应React.createElement的第一个参数 48 | * @param rawProps 对应了React.createElement的第二个参数(包含children) 49 | */ 50 | export const setInitialProperties = ( 51 | domElement: Element, 52 | tag: string, 53 | rawProps: Object 54 | ) => { 55 | let props: Object = rawProps 56 | switch (tag) { 57 | case 'input': 58 | ReactDOMInputInitWrapperState(domElement, rawProps) 59 | props = ReactDOMInputGetHostProps(domElement, rawProps) 60 | break 61 | 62 | default: 63 | break 64 | } 65 | 66 | setInitialDOMProperties(tag, domElement, props) 67 | 68 | switch (tag) { 69 | case 'input': 70 | track(domElement as HTMLInputElement) 71 | ReactDOMInputPostMountWrapper(domElement, rawProps) 72 | break 73 | case 'textarea': 74 | case 'option': 75 | case 'select': 76 | throw new Error('Not Implement') 77 | default: 78 | break 79 | } 80 | } 81 | 82 | const updateDOMProperties = (domElement: Element, updatePayload: any[]) => { 83 | for (let i = 0; i < updatePayload.length; i += 2) { 84 | const propKey = updatePayload[i] 85 | const propValue = updatePayload[i + 1] 86 | 87 | if (propKey === STYLE) { 88 | setValueForStyles(domElement as HTMLElement, propValue) 89 | } else if (propKey === CHILDREN) { 90 | setTextContent(domElement, propValue) 91 | } else { 92 | throw new Error('Not Implement') 93 | } 94 | } 95 | } 96 | 97 | export const updateProperties = ( 98 | domElement: Element, 99 | updatePayload: any[], 100 | tag: string, 101 | lastRawProps: Record & Object, 102 | nextRawProps: Record & Object 103 | ): void => { 104 | if ( 105 | tag === 'input' && 106 | nextRawProps.type === 'radio' && 107 | nextRawProps.name != null 108 | ) { 109 | throw new Error('Not Implement') 110 | } 111 | 112 | updateDOMProperties(domElement, updatePayload) 113 | 114 | switch (tag) { 115 | case 'input': 116 | ReactDOMInputUpdateWrapper(domElement, nextRawProps) 117 | break 118 | case 'textarea': 119 | case 'select': 120 | throw new Error('Not Implement') 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /packages/react-dom/events/plugins/ChangeEventPlugin.ts: -------------------------------------------------------------------------------- 1 | import { Fiber } from '../../../react-reconciler/ReactInternalTypes' 2 | import { updateValueIfChanged } from '../../inputValueTracking' 3 | import { DOMEventName } from '../DOMEventNames' 4 | import { 5 | accumulateTwoPhaseListeners, 6 | DispatchQueue, 7 | } from '../DOMPluginEventSystem' 8 | import { registerTwoPhaseEvent } from '../EventRegistry' 9 | import { EventSystemFlags } from '../EventSystemFlags' 10 | import { AnyNativeEvent } from '../PluginModuleType' 11 | import { SyntheticEvent } from '../SyntheticEvent' 12 | 13 | const registerEvents = () => { 14 | registerTwoPhaseEvent('onChange', [ 15 | 'change', 16 | 'click', 17 | 'focusin', 18 | 'focusout', 19 | 'input', 20 | 'keydown', 21 | 'keyup', 22 | 'selectionchange', 23 | ]) 24 | } 25 | 26 | const shouldUseChangeEvent = (elem: Element) => { 27 | const nodeName = elem.nodeName && elem.nodeName.toLowerCase() 28 | 29 | return ( 30 | nodeName === 'select' || 31 | (nodeName === 'input' && (elem as any).type === 'file') 32 | ) 33 | } 34 | 35 | const isTextInputElement = (elem: HTMLElement) => { 36 | const nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase() 37 | 38 | if (nodeName === 'input' || nodeName === 'textarea') { 39 | return true 40 | } 41 | 42 | return false 43 | } 44 | 45 | const getInstIfValueChanged = (targetInst: Fiber) => { 46 | const targetNode = targetInst.stateNode 47 | 48 | if (updateValueIfChanged(targetNode)) { 49 | return targetInst 50 | } 51 | } 52 | 53 | const getTargetInstForInputOrChangeEvent = ( 54 | domEventName: DOMEventName, 55 | targetInst: Fiber 56 | ) => { 57 | if (domEventName === 'input' || domEventName === 'change') { 58 | return getInstIfValueChanged(targetInst) 59 | } 60 | } 61 | 62 | const createAndAccumulateChangeEvent = ( 63 | dispatchQueue: DispatchQueue, 64 | inst: Fiber, 65 | nativeEvent: AnyNativeEvent, 66 | target: EventTarget | null 67 | ) => { 68 | const listeners = accumulateTwoPhaseListeners(inst, 'onChange') 69 | if (listeners.length > 0) { 70 | const event = new SyntheticEvent( 71 | 'onChange', 72 | 'change', 73 | null as any, 74 | nativeEvent as any, 75 | target 76 | ) 77 | 78 | dispatchQueue.push({ event, listeners }) 79 | } 80 | } 81 | 82 | const shouldUseClickEvent = (elem: HTMLElement) => { 83 | const nodeName = elem.nodeName 84 | 85 | return ( 86 | nodeName && 87 | nodeName.toLowerCase() === 'input' && 88 | ((elem as HTMLInputElement).type === 'checkbox' || 89 | (elem as HTMLInputElement).type === 'radio') 90 | ) 91 | } 92 | 93 | const extractEvents = ( 94 | dispatchQueue: DispatchQueue, 95 | domEventName: DOMEventName, 96 | targetInst: null | Fiber, 97 | nativeEvent: AnyNativeEvent, 98 | nativeEventTarget: null | EventTarget, 99 | eventSystemFlags: EventSystemFlags, 100 | targetContainer: null | EventTarget 101 | ) => { 102 | const targetNode = targetInst ? targetInst.stateNode : window 103 | 104 | let getTargetInstFunc: undefined | Function, handleEventFunc 105 | 106 | if (shouldUseChangeEvent(targetNode)) { 107 | throw new Error('Not Implement') 108 | } else if (isTextInputElement(targetNode as any)) { 109 | getTargetInstFunc = getTargetInstForInputOrChangeEvent 110 | } else if (shouldUseClickEvent(targetNode)) { 111 | throw new Error('Not Implement') 112 | } 113 | 114 | if (getTargetInstFunc) { 115 | const inst: Fiber | null = getTargetInstFunc(domEventName, targetInst) 116 | if (inst) { 117 | createAndAccumulateChangeEvent( 118 | dispatchQueue, 119 | inst, 120 | nativeEvent, 121 | nativeEventTarget 122 | ) 123 | 124 | return 125 | } 126 | } 127 | } 128 | 129 | export { extractEvents, registerEvents } 130 | -------------------------------------------------------------------------------- /docs/pre-requisite/priority-queue.md: -------------------------------------------------------------------------------- 1 | # 优先队列的应用,能让你更容易理解Scheduler模块中优先级调度的相关代码 2 | 3 | ## 题目1.数据流的中位数 4 | 中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。 5 | 6 | 例如, 7 | 8 | [2,3,4] 的中位数是 3 9 | 10 | [2,3] 的中位数是 (2 + 3) / 2 = 2.5 11 | 12 | 设计一个支持以下两种操作的数据结构: 13 | 14 | void addNum(int num) - 从数据流中添加一个整数到数据结构中。 15 | double findMedian() - 返回目前所有元素的中位数。 16 | 17 | ### 示例: 18 | ``` 19 | addNum(1) 20 | addNum(2) 21 | findMedian() -> 1.5 22 | addNum(3) 23 | findMedian() -> 2 24 | ``` 25 | 26 | ### 参考答案 27 | ```ts 28 | interface Comparable { 29 | compareTo(that: T): number 30 | equals(that: T): boolean 31 | } 32 | 33 | class PriorityQueue> { 34 | private pq: E[] = [] 35 | private _size = 0 36 | 37 | constructor(comparator?: (a: E, b: E) => boolean) { 38 | if (comparator) { 39 | this.less = (i: number, j: number) => { 40 | return comparator(this.pq[i], this.pq[j]) 41 | } 42 | } 43 | } 44 | 45 | size() { 46 | return this._size 47 | } 48 | 49 | isEmpty() { 50 | return this._size === 0 51 | } 52 | 53 | insert(e: E) { 54 | this.pq[++this._size] = e 55 | this.swim(this._size) 56 | } 57 | 58 | remove() { 59 | const min = this.pq[1] 60 | 61 | this.exch(1, this._size) 62 | this._size-- 63 | this.pq.length = this._size + 1 64 | 65 | this.sink(1) 66 | 67 | return min 68 | } 69 | 70 | private exch(i: number, j: number) { 71 | const t = this.pq[i] 72 | this.pq[i] = this.pq[j] 73 | this.pq[j] = t 74 | } 75 | 76 | private sink(k: number) { 77 | while (2 * k <= this._size) { 78 | let j = 2 * k 79 | 80 | if (j + 1 <= this._size && this.less(j + 1, j)) j++ 81 | 82 | if (this.less(k, j)) break 83 | 84 | this.exch(k, j) 85 | k = j 86 | } 87 | } 88 | 89 | private swim(k: number) { 90 | let j: number 91 | while (k > 1 && this.less(k, (j = Math.floor(k / 2)))) { 92 | this.exch(j, k) 93 | k = j 94 | } 95 | } 96 | 97 | private less(i: number, j: number): boolean { 98 | if (typeof this.pq[i] === 'string' || typeof this.pq[i] === 'number') { 99 | return this.pq[i] < this.pq[j] 100 | } else { 101 | return (this.pq[i] as Comparable).compareTo(this.pq[j]) < 0 102 | } 103 | } 104 | 105 | peek(): null | E { 106 | return this.pq[1] ?? null 107 | } 108 | } 109 | 110 | class MedianFinder { 111 | private minPQ: PriorityQueue = new PriorityQueue() 112 | private maxPQ: PriorityQueue = new PriorityQueue((a, b) => { 113 | return a > b 114 | }) 115 | 116 | size(): number { 117 | return this.minPQ.size() + this.maxPQ.size() 118 | } 119 | 120 | addNum(v: number): void { 121 | if (this.size() % 2 === 0) { 122 | const max = this.maxPQ.peek() 123 | 124 | if (max === null || max < v) { 125 | this.minPQ.insert(v) 126 | } else { 127 | this.minPQ.insert(this.maxPQ.remove()) 128 | this.maxPQ.insert(v) 129 | } 130 | } else { 131 | const min = this.minPQ.peek() 132 | 133 | if (min === null || v < min) { 134 | this.maxPQ.insert(v) 135 | } else { 136 | this.maxPQ.insert(this.minPQ.remove()) 137 | this.minPQ.insert(v) 138 | } 139 | } 140 | } 141 | 142 | findMedian(): number | null { 143 | const size = this.size() 144 | 145 | if (size === 0) return null 146 | 147 | if (size % 2 === 0) { 148 | return (this.minPQ.peek()! + this.maxPQ.peek()!) / 2 149 | } else { 150 | return this.minPQ.peek() 151 | } 152 | } 153 | } 154 | ``` 155 | ### 解释 156 | 使用两个优先级堆(priority heap),即一个大顶堆,存放小于中位数的值,以 157 | 及一个小顶堆,存放大于中位数的值。这会将所有元素大致分为两半,中间的两个元素位于两个堆的堆顶。这样一来,要找出中间值就是小事一桩。 158 | 159 | ## 相关引用 160 | - [leetcode 295. 数据流的中位数](https://leetcode-cn.com/problems/find-median-from-data-stream/) -------------------------------------------------------------------------------- /examples/LayoutEffect.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 切换useEffect和useLayoutEffect查看有什么区别 3 | */ 4 | import { CSSProperties } from 'react' 5 | import React, { useState, useEffect, useLayoutEffect } from '../packages/react' 6 | 7 | const shuffleArray = (array: number[]) => { 8 | for (let i = array.length - 1; i > 0; i--) { 9 | let j = Math.floor(Math.random() * (i + 1)) 10 | let temp = array[i] 11 | array[i] = array[j] 12 | array[j] = temp 13 | } 14 | } 15 | 16 | const previousLayoutInfo: DOMRect[] = Array.from({ length: 25 }) 17 | const colors = [ 18 | '#0351C1', 19 | '#01142F', 20 | '#51EAFF', 21 | '#45D09E', 22 | '#116315', 23 | '#CBE724', 24 | '#FFD600', 25 | '#F85C50', 26 | '#FFDFDC', 27 | '#EF2FA2', 28 | '#2F3640', 29 | '#7367F0', 30 | '#0070A5', 31 | '#9CBAED', 32 | '#74F6F7', 33 | '#FDDB92', 34 | '#00C6FF', 35 | '#98E5D9', 36 | '#89D405', 37 | '#511414', 38 | '#B4B0BE', 39 | '#FF9966', 40 | '#00C9FF', 41 | '#2F0166', 42 | '#4A00E0', 43 | ] 44 | 45 | export const LayoutEffectDemo = () => { 46 | const [list, setList] = useState(Array.from({ length: 25 }, (_, i) => i)) 47 | 48 | useLayoutEffect(() => { 49 | if (!previousLayoutInfo[0]) return 50 | const currentLayoutInfo: DOMRect[] = Array.from({ length: 25 }) 51 | const gridItems = document.querySelectorAll('.grid-item') 52 | const indexKeyToElementMap: Map = new Map() 53 | for (let i = 0; i < gridItems.length; ++i) { 54 | currentLayoutInfo[gridItems[i].textContent as any] = 55 | gridItems[i].getBoundingClientRect() 56 | indexKeyToElementMap.set(+gridItems[i].textContent!, gridItems[i]) 57 | } 58 | 59 | for (let i = 0; i < currentLayoutInfo.length; ++i) { 60 | const firstRect = previousLayoutInfo[i] 61 | const lastRect = currentLayoutInfo[i] 62 | const lastEl = indexKeyToElementMap.get(i)! 63 | lastEl.animate( 64 | [ 65 | { 66 | transform: ` 67 | translateX(${firstRect.left - lastRect.left}px) 68 | translateY(${firstRect.top - lastRect.top}px) 69 | scale(${firstRect.width / lastRect.width}) 70 | `, 71 | }, 72 | { 73 | transform: ` 74 | translateX(0) 75 | translateY(0) 76 | scale(1) 77 | `, 78 | }, 79 | ], 80 | { 81 | duration: 600, 82 | easing: 'cubic-bezier(0.2, 0, 0.2, 1)', 83 | } 84 | ) 85 | } 86 | }, [list]) 87 | 88 | const shuffle = () => { 89 | const next = [...list] 90 | //在做dom变更前先记录下此时的layout信息 91 | const gridItems = document.querySelectorAll('.grid-item') 92 | 93 | for (let i = 0; i < gridItems.length; ++i) { 94 | previousLayoutInfo[gridItems[i].textContent as any] = 95 | gridItems[i].getBoundingClientRect() 96 | } 97 | shuffleArray(next) 98 | setList(next) 99 | } 100 | 101 | return ( 102 |
103 | 104 |
105 | {list.map((v) => { 106 | return ( 107 |
108 | {v} 109 |
110 | ) 111 | })} 112 |
113 |
114 | ) 115 | } 116 | 117 | const styles: { [key: string]: CSSProperties } = { 118 | grid: { 119 | display: 'grid', 120 | gridTemplateColumns: 'repeat(5, 1fr)', 121 | rowGap: 20, 122 | columnGap: 20, 123 | maxWidth: 800, 124 | }, 125 | gridItem: { 126 | height: 100, 127 | width: 100, 128 | background: 'indigo', 129 | color: '#fff', 130 | display: 'flex', 131 | alignItems: 'center', 132 | justifyContent: 'center', 133 | }, 134 | } 135 | 136 | const gridItemStyles = colors.map((v) => ({ 137 | ...styles.gridItem, 138 | background: v, 139 | })) 140 | -------------------------------------------------------------------------------- /test/ReactElement.test.tsx: -------------------------------------------------------------------------------- 1 | import React from '../packages/react' 2 | 3 | describe('ReactElement', () => { 4 | const FunctionComponent = () => { 5 | return
6 | } 7 | it('returns a complete element according to spec', () => { 8 | const element = React.createElement(FunctionComponent) 9 | expect(element.type).toBe(FunctionComponent) 10 | expect(element.key).toBe(null) 11 | 12 | expect(element.props).toEqual({}) 13 | }) 14 | 15 | it('allows a string to be passed as the type', () => { 16 | const element = React.createElement('div') 17 | expect(element.type).toBe('div') 18 | expect(element.key).toBe(null) 19 | expect(element.props).toEqual({}) 20 | }) 21 | 22 | it('does not reuse the original config object', () => { 23 | const config = { foo: 1 } 24 | const element = React.createElement(FunctionComponent, config) 25 | expect(element.props.foo).toBe(1) 26 | config.foo = 2 27 | expect(element.props.foo).toBe(1) 28 | }) 29 | 30 | it('does not fail if config has no prototype', () => { 31 | const config = Object.create(null, { foo: { value: 1, enumerable: true } }) 32 | const element = React.createElement(FunctionComponent, config) 33 | expect(element.props.foo).toBe(1) 34 | }) 35 | 36 | it('extracts key and ref from the config', () => { 37 | const element = React.createElement(FunctionComponent, { 38 | key: '12', 39 | ref: '34', 40 | foo: '56', 41 | }) 42 | expect(element.type).toBe(FunctionComponent) 43 | expect(element.key).toBe('12') 44 | expect(element.props).toEqual({ foo: '56' }) 45 | }) 46 | 47 | it('extracts null key and ref', () => { 48 | const element = React.createElement(FunctionComponent, { 49 | key: null, 50 | ref: null, 51 | foo: '12', 52 | }) 53 | expect(element.type).toBe(FunctionComponent) 54 | expect(element.key).toBe('null') 55 | expect(element.props).toEqual({ foo: '12' }) 56 | }) 57 | 58 | it('ignores undefined key and ref', () => { 59 | const props = { 60 | foo: '56', 61 | key: undefined, 62 | ref: undefined, 63 | } 64 | const element = React.createElement(FunctionComponent, props) 65 | expect(element.type).toBe(FunctionComponent) 66 | expect(element.key).toBe(null) 67 | expect(element.props).toEqual({ foo: '56' }) 68 | }) 69 | 70 | it('coerces the key to a string', () => { 71 | const element = React.createElement(FunctionComponent, { 72 | key: 12, 73 | foo: '56', 74 | }) 75 | expect(element.type).toBe(FunctionComponent) 76 | expect(element.key).toBe('12') 77 | expect(element.props).toEqual({ foo: '56' }) 78 | }) 79 | 80 | it('merges an additional argument onto the children prop', () => { 81 | const a = 1 82 | const element = React.createElement( 83 | FunctionComponent, 84 | { 85 | children: 'text', 86 | }, 87 | a 88 | ) 89 | expect(element.props.children).toBe(a) 90 | }) 91 | 92 | it('does not override children if no rest args are provided', () => { 93 | const element = React.createElement(FunctionComponent, { 94 | children: 'text', 95 | }) 96 | expect(element.props.children).toBe('text') 97 | }) 98 | 99 | it('overrides children if null is provided as an argument', () => { 100 | const element = React.createElement( 101 | FunctionComponent, 102 | { 103 | children: 'text', 104 | }, 105 | null 106 | ) 107 | expect(element.props.children).toBe(null) 108 | }) 109 | 110 | it('merges rest arguments onto the children prop in an array', () => { 111 | const a = 1 112 | const b = 2 113 | const c = 3 114 | const element = React.createElement(FunctionComponent, null as any, a, b, c) 115 | expect(element.props.children).toEqual([1, 2, 3]) 116 | }) 117 | 118 | // NOTE: We're explicitly not using JSX here. This is intended to test 119 | // classic JS without JSX. 120 | it('is indistinguishable from a plain object', () => { 121 | const element = React.createElement('div', { className: 'foo' }) 122 | const object = {} 123 | expect(element.constructor).toBe(object.constructor) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactInternalTypes.ts: -------------------------------------------------------------------------------- 1 | import { Flags } from './ReactFiberFlags' 2 | import { Lane, LaneMap, Lanes } from './ReactFiberLane' 3 | import { RootTag } from './ReactRootTags' 4 | import { TypeOfMode } from './ReactTypeOfMode' 5 | import { WorkTag } from './ReactWorkTags' 6 | 7 | /** 8 | * 应用根节点 9 | */ 10 | export type FiberRoot = { 11 | /** 12 | * 当前完成render阶段构建完成的workInProgress树根节点 13 | */ 14 | finishedWork: Fiber | null 15 | /** 16 | * 当前页面所对应的fiber树,其alternate属性指向workInProgress fiber树 17 | */ 18 | current: Fiber 19 | 20 | /** 21 | * 当前应用所挂载在的dom节点,在legacy模式中为ReactDom.render方法的第二个参数 22 | * 在concurrent模式中为createRoot的参数 23 | */ 24 | containerInfo: any 25 | 26 | /** 27 | * Scheduler.scheduleCallback的返回值,代表了下次执行render的task 28 | */ 29 | callbackNode: unknown 30 | callbackPriority: Lane 31 | 32 | pendingLanes: Lanes 33 | expiredLanes: Lanes 34 | expirationTimes: LaneMap 35 | eventTimes: LaneMap 36 | 37 | /** 38 | * root的类型(legacy, batched,concurrent等) 39 | */ 40 | tag: RootTag 41 | } 42 | 43 | export type Fiber = { 44 | /** 45 | * 该fiber节点处于同级兄弟节点的第几位 46 | */ 47 | index: number 48 | /** 49 | * 此次commit中需要删除的fiber节点 50 | */ 51 | deletions: Fiber[] | null 52 | /** 53 | * 子树带有的更新操作,用于减少查找fiber树上更新的时间复杂度 54 | */ 55 | subtreeFlags: Flags 56 | /** 57 | *一个Bitset代表该fiber节点上带有的更新操作,比如第二位为1就代表该节点需要插入 58 | */ 59 | flags: Flags 60 | /** 61 | * 新创建jsx对象的第二个参数,像HostRoot这种内部自己创建的Fiber节点为null 62 | */ 63 | pendingProps: any 64 | /** 65 | * 上一轮更新完成后的props 66 | */ 67 | memoizedProps: any 68 | /** 69 | *其子节点为单链表结构child指向了他的第一个子节点后续子节点可通过child.sibling获得 70 | */ 71 | child: Fiber | null 72 | 73 | /** 74 | * 该fiber节点的兄弟节点,他们都有着同一个父fiber节点 75 | */ 76 | sibling: Fiber | null 77 | /** 78 | * 在我们的实现中只有Function组件对应的fiber节点使用到了该属性 79 | * function组件会用他来存储hook组成的链表,在react中很多数据结构 80 | * 都有该属性,注意不要弄混了 81 | */ 82 | memoizedState: any 83 | /** 84 | * 该fiber节点对于的相关节点(类组件为为类实例,dom组件为dom节点) 85 | */ 86 | stateNode: any 87 | 88 | /** 89 | * 存放了该fiber节点上的更新信息,其中HostRoot,FunctionComponent, HostComponent 90 | * 的updateQueue各不相同,函数的组件的updateQueue是一个存储effect的链表 91 | * 比如一个函数组件内有若干个useEffect,和useLayoutEffect,那每个effect 92 | * 就会对应这样的一个数据结构 93 | * { 94 | * tag: HookFlags //如果是useEffect就是Passive如果是useLayoutEffect就是Layout 95 | * create: () => (() => void) | void //useEffect的第一个参数 96 | * destroy: (() => void) | void //useEffect的返回值 97 | * deps: unknown[] | null //useEffect的第二个参数 98 | * next: Effect 99 | * } 100 | * 各个effect会通过next连接起来 101 | * HostComponent的updateQueue表示了该节点所要进行的更新, 102 | * 比如他可能长这样 103 | * ['children', 'new text', 'style', {background: 'red'}] 104 | * 代表了他对应的dom需要更新textContent和style属性 105 | */ 106 | updateQueue: unknown 107 | 108 | /** 109 | * 表示了该节点的类型,比如HostComponent,FunctionComponent,HostRoot 110 | * 详细信息可以查看react-reconciler\ReactWorkTags.ts 111 | */ 112 | tag: WorkTag 113 | 114 | /** 115 | * 该fiber节点父节点(以HostRoot为tag的fiber节点return属性为null) 116 | */ 117 | return: Fiber | null 118 | 119 | /** 120 | * 该节点链接了workInPrgress树和current fiber树之间的节点 121 | */ 122 | alternate: Fiber | null 123 | 124 | /** 125 | * 用于多节点children进行diff时提高节点复用的正确率 126 | */ 127 | key: string | null 128 | 129 | /** 130 | * 如果是自定义组件则该属性就是和该fiber节点关联的function或class 131 | * 如果是div,span则就是一个字符串 132 | */ 133 | type: any 134 | 135 | /** 136 | * 表示了元素的类型,fiber的type属性会在reconcile的过程中改变,但是 137 | * elementType是一直不变的,比如Memo组件的type在jsx对象中为 138 | * { 139 | * $$typeof: REACT_MEMO_TYPE, 140 | * type, 141 | * compare: compare === undefined ? null : compare, 142 | * } 143 | * 在经过render阶段后会变为他包裹的函数,所以在render前后是不一致的 144 | * 而我们在diff是需要判断一个元素的type有没有改变, 145 | * 以判断能不能复用该节点,这时候elementType就派上用场 146 | * 了,因为他是一直不变的 147 | */ 148 | elementType: any 149 | 150 | /** 151 | * 描述fiber节点及其子树属性BitSet 152 | * 当一个fiber被创建时他的该属性和父节点一致 153 | * 当以ReactDom.render创建应用时mode为LegacyMode, 154 | * 当以createRoot创建时mode为ConcurrentMode 155 | */ 156 | mode: TypeOfMode 157 | 158 | /** 159 | * 用来判断该Fiber节点是否存在更新,以及改更新的优先级 160 | */ 161 | lanes: Lanes 162 | /** 163 | * 用来判断该节点的子节点是否存在更新 164 | */ 165 | childLanes: Lanes 166 | } 167 | 168 | type Dispatch
= (a: A) => void 169 | type BasicStateAction = ((a: S) => S) | S 170 | 171 | export type Dispatcher = { 172 | useState(initialState: (() => S) | S): [S, Dispatch>] 173 | useEffect( 174 | create: () => (() => void) | void, 175 | deps: unknown[] | void | null 176 | ): void 177 | useLayoutEffect( 178 | create: () => (() => void) | void, 179 | deps: unknown[] | void | null 180 | ): void 181 | } 182 | -------------------------------------------------------------------------------- /packages/react-dom/events/ReactDOMEventListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContinuousEventPriority, 3 | DefaultEventPriority, 4 | DiscreteEventPriority, 5 | getCurrentUpdatePriority, 6 | setCurrentUpdatePriority, 7 | } from '../../react-reconciler/ReactEventPriorities' 8 | import { Lane } from '../../react-reconciler/ReactFiberLane' 9 | import { discreteUpdates } from '../../react-reconciler/ReactFiberReconciler' 10 | import { Container } from '../ReactDomRoot' 11 | import { DOMEventName } from './DOMEventNames' 12 | import { dispatchEventForPluginEventSystem } from './DOMPluginEventSystem' 13 | import { EventSystemFlags } from './EventSystemFlags' 14 | import { getEventTarget } from './getEventTarget' 15 | import { AnyNativeEvent } from './PluginModuleType' 16 | import { getClosestInstanceFromNode } from './ReactDOMComponentTree' 17 | 18 | const dispatchDiscreteEvent = ( 19 | domEventName: DOMEventName, 20 | eventSymtemFlags: EventSystemFlags, 21 | container: EventTarget, 22 | nativeEvent: Event 23 | ) => { 24 | discreteUpdates( 25 | dispatchEvent, 26 | domEventName, 27 | eventSymtemFlags, 28 | container, 29 | nativeEvent 30 | ) 31 | } 32 | 33 | const attemptToDispatchEvent = ( 34 | domEventName: DOMEventName, 35 | eventSystemFlags: EventSystemFlags, 36 | targetContainer: EventTarget, 37 | nativeEvent: AnyNativeEvent 38 | ): null | Container => { 39 | const nativeEventTarget = getEventTarget(nativeEvent) 40 | const targetInst = getClosestInstanceFromNode(nativeEventTarget!) 41 | 42 | dispatchEventForPluginEventSystem( 43 | domEventName, 44 | eventSystemFlags, 45 | nativeEvent, 46 | targetInst, 47 | targetContainer 48 | ) 49 | 50 | return null 51 | } 52 | 53 | export const dispatchEvent = ( 54 | domEventName: DOMEventName, 55 | eventSystemFlags: EventSystemFlags, 56 | targetContainer: EventTarget, 57 | nativeEvent: AnyNativeEvent 58 | ): void => { 59 | attemptToDispatchEvent( 60 | domEventName, 61 | eventSystemFlags, 62 | targetContainer, 63 | nativeEvent 64 | ) 65 | } 66 | 67 | const dispatchContinuousEvent = ( 68 | domEventName: DOMEventName, 69 | eventSystemFlags: EventSystemFlags, 70 | targetContainer: EventTarget, 71 | nativeEvent: AnyNativeEvent 72 | ) => { 73 | const previousPriority = getCurrentUpdatePriority() 74 | 75 | try { 76 | setCurrentUpdatePriority(ContinuousEventPriority) 77 | dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) 78 | } finally { 79 | setCurrentUpdatePriority(previousPriority) 80 | } 81 | } 82 | 83 | export const createEventListenerWrapperWithPriority = ( 84 | targetContainer: EventTarget, 85 | domEventName: DOMEventName, 86 | eventSymtemFlags: EventSystemFlags 87 | ): Function => { 88 | const eventPriority = getEventPriority(domEventName) 89 | 90 | let listenerWrapper 91 | 92 | switch (eventPriority) { 93 | case DiscreteEventPriority: 94 | listenerWrapper = dispatchDiscreteEvent 95 | break 96 | 97 | case DefaultEventPriority: 98 | listenerWrapper = dispatchEvent 99 | break 100 | case ContinuousEventPriority: 101 | listenerWrapper = dispatchContinuousEvent 102 | break 103 | default: 104 | throw new Error('Not Implement') 105 | } 106 | 107 | return listenerWrapper.bind( 108 | null, 109 | domEventName, 110 | eventSymtemFlags, 111 | targetContainer 112 | ) 113 | } 114 | 115 | export const getEventPriority = (domEventName: DOMEventName): Lane => { 116 | switch (domEventName) { 117 | case 'cancel': 118 | case 'click': 119 | case 'close': 120 | case 'contextmenu': 121 | case 'copy': 122 | case 'cut': 123 | case 'auxclick': 124 | case 'dblclick': 125 | case 'dragend': 126 | case 'dragstart': 127 | case 'drop': 128 | case 'focusin': 129 | case 'focusout': 130 | case 'input': 131 | case 'invalid': 132 | case 'keydown': 133 | case 'keypress': 134 | case 'keyup': 135 | case 'mousedown': 136 | case 'mouseup': 137 | case 'paste': 138 | case 'pause': 139 | case 'play': 140 | case 'pointercancel': 141 | case 'pointerdown': 142 | case 'pointerup': 143 | case 'ratechange': 144 | case 'reset': 145 | case 'seeked': 146 | case 'submit': 147 | case 'touchcancel': 148 | case 'touchend': 149 | case 'touchstart': 150 | case 'volumechange': 151 | // Used by polyfills: 152 | // eslint-disable-next-line no-fallthrough 153 | case 'change': 154 | case 'selectionchange': 155 | case 'textInput': 156 | case 'compositionstart': 157 | case 'compositionend': 158 | case 'compositionupdate': 159 | // Only enableCreateEventHandleAPI: 160 | // eslint-disable-next-line no-fallthrough 161 | case 'beforeblur': 162 | case 'afterblur': 163 | // Not used by React but could be by user code: 164 | // eslint-disable-next-line no-fallthrough 165 | case 'beforeinput': 166 | case 'blur': 167 | case 'fullscreenchange': 168 | case 'focus': 169 | case 'hashchange': 170 | case 'popstate': 171 | case 'select': 172 | case 'selectstart': 173 | return DiscreteEventPriority 174 | case 'drag': 175 | case 'dragenter': 176 | case 'dragexit': 177 | case 'dragleave': 178 | case 'dragover': 179 | case 'mousemove': 180 | case 'mouseout': 181 | case 'mouseover': 182 | case 'pointermove': 183 | case 'pointerout': 184 | case 'pointerover': 185 | case 'scroll': 186 | case 'toggle': 187 | case 'touchmove': 188 | case 'wheel': 189 | // Not used by React but could be by user code: 190 | // eslint-disable-next-line no-fallthrough 191 | case 'mouseenter': 192 | case 'mouseleave': 193 | case 'pointerenter': 194 | case 'pointerleave': 195 | return ContinuousEventPriority 196 | case 'message': 197 | throw new Error('Not Implement') 198 | default: 199 | return DefaultEventPriority 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 5 | 6 | /* Basic Options */ 7 | // "incremental": true, /* Enable incremental compilation */ 8 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 9 | "module": "CommonJS" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 10 | // "lib": [], /* Specify library files to be included in the compilation. */ 11 | // "allowJs": true, /* Allow javascript files to be compiled. */ 12 | // "checkJs": true, /* Report errors in .js files. */ 13 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 14 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 15 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 16 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 17 | // "outFile": "./", /* Concatenate and emit output to single file. */ 18 | "outDir": "./build/", /* Redirect output structure to the directory. */ 19 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 20 | // "composite": true, /* Enable project compilation */ 21 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 22 | // "removeComments": true, /* Do not emit comments to output. */ 23 | // "noEmit": true, /* Do not emit outputs. */ 24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 27 | 28 | /* Strict Type-Checking Options */ 29 | "strict": true /* Enable all strict type-checking options. */, 30 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 31 | // "strictNullChecks": true, /* Enable strict null checks. */ 32 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 33 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 34 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 35 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 36 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 37 | 38 | /* Additional Checks */ 39 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 40 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 41 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 42 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true /* Skip type checking of declaration files. */, 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tiny-react是一个基于React17精简而来的仓库 2 | 为了简化react源码学习的库,和react17的区别就是少了很多功能,只实现了核心的逻辑,和preact这种react-like库有着根本区别,preact更像是一个和react有着相同的接口但是实现细节却不尽相同的react,而tiny-react是从react官方仓库精简而来,它更像官方react的阉割版,所以每一行代码,每一个函数都能在react最新的官方仓库中找到出处,而且总共的代码只有6千多行,刨除掉ReactDOM只有4000多行,能让React源码学习的难度大大降低 3 | 4 | ## 使用指南 5 | * 在阅读源码前,你需要对react的大体原理有一定的了解在这里强烈推荐去通读一下卡颂老师的[React技术揭秘](https://react.iamkasong.com/) 6 | * 对react的大体原理有一定了解后就可以开始看源码了,不过有些同学可能对一些源码中使用频繁的的数据结构和算法还不怎么了解,这时候就可以看一下下面的[看源码前需要了解的数据结构和算法](#看源码前需要了解的数据结构和算法),如果你已经非常了解这些知识则可以跳过 7 | * react-dom这个模块,可以不用太关注,虽然他的代码有2000多行,但是大量的代码都是dom操作和事件委托相关函数,对学习React核心原理影响不大,不过其中 [浏览器事件优先级](https://github.com/PiNengShaoNian/tiny-react/blob/main/packages/react-dom/events/ReactDOMEventListener.ts) 相关的代码还是要注意一下 8 | * react-reconciler这个模块需要重点关注特别是其中的 [ReactFiberWorkLoop](https://github.com/PiNengShaoNian/tiny-react/blob/main/packages/react-reconciler/ReactFiberWorkLoop.ts) 他是React源码的骨干,把所有的模块连接到了一起 9 | * scheduler这个模块是代码最少,最简单的模块了,而且基本没有和其他模块耦合,可以直接单独看他的源码 10 | 11 | ## [看源码前需要了解的数据结构和算法](./docs/pre-requisite/index.md) 12 | - ### [位运算](./docs/pre-requisite/bit-manipulation.md) 13 | - ### [优先队列](./docs/pre-requisite/priority-queue.md) 14 | - ### [循环链表](./docs/pre-requisite/circular-linked-list.md) 15 | - ### [dfs](./docs/pre-requisite/dfs.md) 16 | 17 | ## Feature 18 | 19 | ### 时间切片 20 | ![image](https://i.loli.net/2021/06/06/x91pyG2MNhe3TBo.png) 21 | 22 | ### useState和useEffect 23 | ![image](https://i.loli.net/2021/06/06/nPSNCFK25UVbTAR.gif) 24 | 25 | ### useLayoutEffect 26 | ![image](https://i.loli.net/2021/06/06/VcGABLKUYC6QMfR.gif) 27 | 28 | ### 优先级调度 29 | ```ts 30 | import React, { useState, useEffect } from '../packages/react' 31 | 32 | export const PriorityScheduling = () => { 33 | const [count, updateCount] = useState(0) 34 | 35 | const onClick = () => { 36 | updateCount((count) => count + 2) 37 | } 38 | 39 | console.log({ count }) 40 | 41 | useEffect(() => { 42 | //暂时不支持ref直接用选择器获取 43 | const button = document.getElementById('discretEventDispatcher')! 44 | setTimeout(() => updateCount(1), 1000) 45 | setTimeout(() => { 46 | button.click() 47 | //根据机能给第二个setTimeout一个合适的时间,或者适当的加长数组的长度 48 | //以保证点击事件触发时,前一个低优先级的更新的render阶段还没有完成 49 | //才能看到插队情况发生 50 | }, 1030) 51 | }, []) 52 | 53 | return ( 54 |
55 | 58 |
59 | {Array.from(new Array(10000)).map((v, index) => ( 60 | {count} 61 | ))} 62 |
63 |
64 | ) 65 | } 66 | ``` 67 | ![image](https://i.loli.net/2021/06/06/PfXslp4dkytA6nr.gif) 68 | 69 | ### memo 70 | ```ts 71 | import { CSSProperties } from 'react' 72 | import React, { memo, useState, useEffect } from '../packages/react' 73 | 74 | type Color = '#fff' | 'green' 75 | type Component = 76 | | 'NestedComponent' 77 | | 'NormalComponent' 78 | | 'MemorizedComponentWithUnstableProps' 79 | | 'MemorizedNestedComponent' 80 | | 'MemorizedComponentWithCustomCompareFunc' 81 | | 'MemorizedComponent' 82 | 83 | const prevColors: Record = { 84 | NestedComponent: 'green', 85 | NormalComponent: 'green', 86 | MemorizedComponentWithUnstableProps: 'green', 87 | MemorizedNestedComponent: 'green', 88 | MemorizedComponentWithCustomCompareFunc: 'green', 89 | MemorizedComponent: 'green', 90 | } 91 | 92 | const useCurrentOutlineStyle = (componentName: Component): CSSProperties => { 93 | const currColor = prevColors[componentName] === '#fff' ? 'green' : '#fff' 94 | prevColors[componentName] = currColor 95 | return { 96 | outline: `1px solid ${currColor}`, 97 | } 98 | } 99 | 100 | const NormalComponent = () => { 101 | return ( 102 |
103 | NormalComponent 104 | 105 | 106 |
107 | ) 108 | } 109 | 110 | const NestedComponent = () => { 111 | const outlineStyle = useCurrentOutlineStyle('NestedComponent') 112 | return
-- NestedComponent
113 | } 114 | 115 | const MemorizedNestedComponent = memo(() => { 116 | const outlineStyle = useCurrentOutlineStyle('MemorizedNestedComponent') 117 | 118 | return
-- MemorizedNestedComponent
119 | }) 120 | 121 | const MemorizedComponent = memo(() => { 122 | const outlineStyle = useCurrentOutlineStyle('MemorizedComponent') 123 | 124 | return
MemorizedComponent
125 | }) 126 | 127 | const MemorizedComponentWithUnstableProps = memo<{ count: number }>( 128 | ({ count }) => { 129 | const outlineStyle = useCurrentOutlineStyle( 130 | 'MemorizedComponentWithUnstableProps' 131 | ) 132 | 133 | return ( 134 |
135 | MemorizedComponentWithUnstableProps {count} 136 |
137 | ) 138 | } 139 | ) 140 | 141 | const MemorizedComponentWithCustomCompareFunc = memo<{ text: string }>( 142 | ({ text }) => { 143 | const outlineStyle = useCurrentOutlineStyle( 144 | 'MemorizedComponentWithCustomCompareFunc' 145 | ) 146 | 147 | return
最大字符长度-8 {text}
148 | }, 149 | (oldProps, newProps) => newProps.text.length > 8 150 | ) 151 | 152 | export const MemorizedComponentDemo = () => { 153 | const [count, setCount] = useState(0) 154 | const [text, setText] = useState('') 155 | return ( 156 |
157 | 158 |
159 | 160 |
161 | 162 |
163 | 164 |
165 | 172 |
173 | { 176 | setText(e.target.value) 177 | }} 178 | /> 179 |
180 | ) 181 | } 182 | ``` 183 | ![image](https://i.loli.net/2021/06/12/12bquaWQsBpGrDh.gif) 184 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactFiberCompleteWork.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTextInstance, 3 | finalizeInitialChildren, 4 | prepareUpdate, 5 | Props, 6 | Type, 7 | } from '../react-dom/ReactDOMHostConfig' 8 | import { NoFlags, StaticMask, Update } from './ReactFiberFlags' 9 | import { appendInitialChild, createInstance } from './ReactFiberHostConfig' 10 | import { mergeLanes, NoLanes } from './ReactFiberLane' 11 | import { Fiber } from './ReactInternalTypes' 12 | import { 13 | FunctionComponent, 14 | HostComponent, 15 | HostRoot, 16 | HostText, 17 | IndeterminateComponent, 18 | MemoComponent, 19 | SimpleMemoComponent, 20 | } from './ReactWorkTags' 21 | 22 | /** 23 | * 将以workInProgress为根的fiber子树,其中包含的所有dom节点,其中最顶层dom 24 | * 节点加到parent dom节点的子节点中 25 | * @param parent 26 | * @param workInProgress 27 | * @returns 28 | */ 29 | const appendAllChildren = (parent: Element, workInProgress: Fiber): void => { 30 | let node: Fiber | null = workInProgress.child 31 | while (node !== null) { 32 | if (node.tag === HostComponent || node.tag === HostText) { 33 | appendInitialChild(parent, node.stateNode) 34 | } else if (node.child !== null) { 35 | //如果该子节点不是一个HostComponent则继续向下找 36 | node.child.return = node 37 | node = node.child 38 | continue 39 | } 40 | 41 | if (node === workInProgress) { 42 | return 43 | } 44 | 45 | while (node.sibling === null) { 46 | //该层级所有以node为父节点的子树中离parent最近的dom已经完成追加,是时候返回到上层了 47 | /** 48 | * 比如下面的例子 49 | * FunctionComp A 50 | * FunctionCompB FunctionCompC FunctionCompD 51 | * domE 52 | * 如果进入循环时此时node为domE,一轮循环后当node被赋值为FunctionCompC后就会跳出这个循环 53 | * 然后继续进行FunctionCompD的工作 54 | * 55 | */ 56 | if (node.return === null || node.return === workInProgress) return 57 | 58 | node = node?.return ?? null 59 | } 60 | 61 | //以该node为父节点的子树中离parent最近的dom已经完成追加,尝试对同级中其他fiber节点执行相同操作 62 | node.sibling.return = node.return 63 | node = node.sibling 64 | } 65 | } 66 | 67 | /** 68 | * 将该节点的子节点上的lanes,和flags全部冒泡到他的childLanes和subtreeFlags中 69 | * 只用冒泡一级就行,因为我们对每层的节点都会进行该操作 70 | * @param completedWork 其子节点需要冒泡的节点 71 | * @returns 72 | */ 73 | const bubbleProperties = (completedWork: Fiber): boolean => { 74 | //didBailout用来判断completedWork是否为静态节点,如果一个节点为静态节点 75 | //那么该节点会经过bailoutOnAlreadyFinishedWork并且他的childLanes为NoLanes 76 | //此时两棵fiber树中他子节点对于的fiber节点是严格相等的 77 | //详细逻辑可以查看react-reconciler\ReactFiber.ts下的 78 | //createWorkInProgress函数 79 | const didBailout = 80 | completedWork.alternate !== null && 81 | completedWork.alternate.child === completedWork.child 82 | let subtreeFlags = NoFlags 83 | let newChildLanes = NoLanes 84 | 85 | //在这会根据是否didBailout选择是否只保留该节点 86 | //subtreeFlags,flags中的StaticMask我们的实现中并没有 87 | //使用到StaticMask所以只保留StaticMask相当于把subtreeFlags,flags 88 | //清除 89 | if (!didBailout) { 90 | let child = completedWork.child 91 | 92 | while (child !== null) { 93 | newChildLanes = mergeLanes( 94 | newChildLanes, 95 | mergeLanes(child.lanes, child.childLanes) 96 | ) 97 | 98 | subtreeFlags |= child.subtreeFlags 99 | subtreeFlags |= child.flags 100 | child.return = completedWork 101 | 102 | child = child.sibling 103 | } 104 | completedWork.subtreeFlags |= subtreeFlags 105 | } else { 106 | let child = completedWork.child 107 | 108 | while (child !== null) { 109 | newChildLanes = mergeLanes( 110 | newChildLanes, 111 | mergeLanes(child.lanes, child.childLanes) 112 | ) 113 | 114 | subtreeFlags |= child.subtreeFlags & StaticMask 115 | subtreeFlags |= child.flags & StaticMask 116 | 117 | child.return = completedWork 118 | 119 | child = child.sibling 120 | } 121 | 122 | completedWork.subtreeFlags |= subtreeFlags 123 | } 124 | 125 | completedWork.childLanes = newChildLanes 126 | return didBailout 127 | } 128 | 129 | /** 130 | * 为一个fiber节点打上更新标记,待会commit阶段会根据flags的类型 131 | * 进行相应的操做 132 | * @param workInProgress 133 | */ 134 | const markUpdate = (workInProgress: Fiber) => { 135 | workInProgress.flags |= Update 136 | } 137 | 138 | /** 139 | * 判断该文本节点更新前后的文本有没有发生改变, 140 | * 如果改变了就把他打上更新标记 141 | * @param current 142 | * @param workInProgress 143 | * @param oldText 144 | * @param newText 145 | */ 146 | const updateHostText = ( 147 | current: Fiber, 148 | workInProgress: Fiber, 149 | oldText: string, 150 | newText: string 151 | ) => { 152 | if (oldText !== newText) { 153 | markUpdate(workInProgress) 154 | } 155 | } 156 | 157 | const updateHostComponent = ( 158 | current: Fiber, 159 | workInProgress: Fiber, 160 | type: Type, 161 | newProps: Props 162 | ) => { 163 | const oldProps = current.memoizedProps 164 | if (oldProps === newProps) { 165 | //更新前后的props没有变化该host component不需要进行工作 166 | return 167 | } 168 | 169 | const instance: Element = workInProgress.stateNode 170 | 171 | //前后的属性不一样,找出那些属性需要进行更新 172 | const updatePayload = prepareUpdate(instance, type, oldProps, newProps) 173 | 174 | workInProgress.updateQueue = updatePayload 175 | if (updatePayload) { 176 | markUpdate(workInProgress) 177 | } 178 | } 179 | 180 | export const completeWork = ( 181 | current: Fiber | null, 182 | workInProgress: Fiber 183 | ): Fiber | null => { 184 | const newProps = workInProgress.pendingProps 185 | 186 | switch (workInProgress.tag) { 187 | case IndeterminateComponent: 188 | case FunctionComponent: 189 | case SimpleMemoComponent: 190 | case MemoComponent: 191 | bubbleProperties(workInProgress) 192 | return null 193 | case HostRoot: { 194 | //todo 195 | // const fiberRoot = workInProgress.stateNode 196 | bubbleProperties(workInProgress) 197 | return null 198 | } 199 | case HostComponent: { 200 | const type = workInProgress.type 201 | if (current !== null && workInProgress.stateNode != null) { 202 | updateHostComponent(current, workInProgress, type, newProps) 203 | } else { 204 | const instance = createInstance(type, newProps, workInProgress) 205 | 206 | //由于是深度优先遍历,当workInProgress进行归阶段时, 207 | //其所有子节点都已完成了递和归阶段,也就是意味着其子树的所有dom节点已经创建 208 | //所以只需要把子树中离instance最近的dom节点追加到instance上即可 209 | appendAllChildren(instance, workInProgress) 210 | workInProgress.stateNode = instance 211 | 212 | if (finalizeInitialChildren(instance, type, newProps)) { 213 | throw new Error('Not Implement') 214 | } 215 | } 216 | 217 | bubbleProperties(workInProgress) 218 | return null 219 | } 220 | case HostText: { 221 | const newText = newProps 222 | 223 | if (current && workInProgress.stateNode !== null) { 224 | /** 225 | * 如果我们复用了改节点,那么意味着我们需要一个side-effect去做这个更新 226 | */ 227 | 228 | //此时current的memoizedProps和pendingProps字段都存储着更新前的文本 229 | //workInProgress的memoizedProps和pendingProps字段都存储着更新后的文本 230 | const oldText = current.memoizedProps 231 | updateHostText(current, workInProgress, oldText, newText) 232 | } else { 233 | workInProgress.stateNode = createTextInstance(newText) 234 | } 235 | bubbleProperties(workInProgress) 236 | return null 237 | } 238 | } 239 | 240 | throw new Error('Not implement') 241 | } 242 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactFiber.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from '../shared/ReactTypes' 2 | import { Fiber } from './ReactInternalTypes' 3 | import { ConcurrentRoot, RootTag } from './ReactRootTags' 4 | import { 5 | WorkTag, 6 | HostRoot, 7 | IndeterminateComponent, 8 | HostComponent, 9 | HostText, 10 | MemoComponent, 11 | } from './ReactWorkTags' 12 | import { 13 | BlockingMode, 14 | ConcurrentMode, 15 | NoMode, 16 | TypeOfMode, 17 | } from './ReactTypeOfMode' 18 | import { Flags, NoFlags } from './ReactFiberFlags' 19 | import { Lanes, NoLanes } from './ReactFiberLane' 20 | import { REACT_MEMO_TYPE } from '../shared/ReactSymbols' 21 | 22 | /** 23 | * 属性含义可查看react-reconciler\ReactInternalTypes.ts 24 | * 下的Fiber Type 25 | */ 26 | class FiberNode { 27 | stateNode: any = null 28 | updateQueue: unknown = null 29 | return: Fiber | null = null 30 | alternate: Fiber | null = null 31 | memoizedState: any = null 32 | child: Fiber | null = null 33 | sibling: Fiber | null = null 34 | type: any = null 35 | memoizedProps: any = null 36 | flags: Flags = 0 37 | subtreeFlags: Flags = 0 38 | deletions: Fiber[] | null = null 39 | index: number = 0 40 | lanes = NoLanes 41 | childLanes = NoLanes 42 | elementType = null 43 | 44 | constructor( 45 | public tag: WorkTag, 46 | public pendingProps: unknown, 47 | public key: null | string, 48 | public mode: TypeOfMode 49 | ) {} 50 | } 51 | 52 | /** 53 | * 54 | * @param tag 标志着该fiber树是以什么模式创建的 55 | * @returns 返回一个以HostRoot为tag的fiber节点(表示一颗fiber树的根节点) 56 | */ 57 | export const createHostRootFiber = (tag: RootTag): Fiber => { 58 | let mode 59 | 60 | if (tag === ConcurrentRoot) { 61 | mode = ConcurrentMode | BlockingMode 62 | } else { 63 | mode = NoMode 64 | } 65 | 66 | return new FiberNode(HostRoot, null, null, mode) 67 | } 68 | 69 | /** 70 | * 创建一个fiber节点 71 | * @param tag 72 | * @param pendingProps 73 | * @param key 74 | * @param mode 75 | * @returns 76 | */ 77 | export const createFiber = ( 78 | tag: WorkTag, 79 | pendingProps: unknown, 80 | key: string | null, 81 | mode: TypeOfMode 82 | ) => { 83 | return new FiberNode(tag, pendingProps, key, mode) 84 | } 85 | 86 | /** 87 | * 88 | * @param current 更具当前界面上的current fiber节点创建一个新的fiber节点去进行工作 89 | * @param pendingProps 该fiber节点新的props 90 | */ 91 | export const createWorkInProgress = ( 92 | current: Fiber, 93 | pendingProps: any 94 | ): Fiber => { 95 | let workInProgress = current.alternate 96 | 97 | if (workInProgress === null) { 98 | //我们在这里使用了双缓存技巧,因为知道只需要两个版本的树 99 | //我们可以把当前树中没有用到的节点拿出来复用,并且这些节点是只有需要时才创建的 100 | //去避免去为那些永远不会更新的节点创建额外的对象, 101 | //如果需要的话这也让我们可以再利用内存 102 | /** 103 | * 考虑下面的App组件,帮你理解上面的话 104 | * const TriggerUpdate = () => { 105 | * const [count, setCount] = useState(0) 106 | * 107 | * return ( 108 | *
109 | * {count} 110 | * 117 | *
118 | * ) 119 | * } 120 | * 121 | * const App = () => { 122 | * return ( 123 | *
124 | *
129 | * Static Node 130 | *
Static Node
131 | *
132 | * 133 | *
134 | * ) 135 | *} 136 | * 下面会使用jquery选择器的方式指明我们说的时哪个fiber节点 137 | * 比如$('#container')就表示哪个id为container的div所对应的fiber 138 | * 虽然App中的TriggerUpdate会触发更新但是他冒泡的lanes 139 | * 并不会影响到并不在他冒泡路径上和他同级的 140 | * $('#static')节点所以他的lanes和childLanes一直都是NoLanes, 141 | * 因为也知道他的父级节点$('#container') div也没有更新, 142 | * 所以在创建$('#static') div节点时复用前一次的props, 143 | * 当接下来进行$('#static')的beginWork时,由于前后props没变且不包含lanes 144 | * 会执行他的bailoutOnAlreadyFinishedWork逻辑 145 | * 即使此次更新中$('#static')对应的jsx对象的style属性是全新的对象, 146 | * 在执行bailout逻辑中发现他的childLanes为NoLanes所以直接返回,不在复制他的child 147 | * 进行render工作了,所以$('#static')节点的子节点只有在首次mount时会被创建一次, 148 | * 对于这些静态节点,在整个过程中,两颗fiber树始终指向相同的对象 149 | */ 150 | 151 | workInProgress = createFiber( 152 | current.tag, 153 | pendingProps, 154 | current.key, 155 | current.mode 156 | ) 157 | 158 | workInProgress.elementType = current.elementType 159 | workInProgress.type = current.type 160 | workInProgress.stateNode = current.stateNode 161 | 162 | workInProgress.alternate = current 163 | current.alternate = workInProgress 164 | } else { 165 | workInProgress.pendingProps = pendingProps 166 | workInProgress.type = current.type 167 | workInProgress.flags = NoFlags 168 | workInProgress.subtreeFlags = NoFlags 169 | workInProgress.deletions = null 170 | } 171 | 172 | workInProgress.lanes = current.lanes 173 | workInProgress.updateQueue = current.updateQueue 174 | workInProgress.childLanes = current.childLanes 175 | workInProgress.flags = current.flags 176 | workInProgress.child = current.child 177 | workInProgress.memoizedProps = current.memoizedProps 178 | workInProgress.memoizedState = current.memoizedState 179 | 180 | return workInProgress 181 | } 182 | 183 | /** 184 | * 根据JSX对象的type和props创建一个fiber节点 185 | * @param type 可以为string比如div,p可以为函数,比如函数组件 186 | * 可以为类比如类组件可以为Symbol比如React.Fragment 187 | * @param key 188 | * @param pendingProps 189 | * @param mode fiber树的模式比如Concurrent,Legacy 190 | * @param lanes 191 | * @returns 192 | */ 193 | export const createFiberFromTypeAndProps = ( 194 | type: any, 195 | key: null | string, 196 | pendingProps: any, 197 | mode: TypeOfMode, 198 | lanes: Lanes 199 | ) => { 200 | let fiberTag: WorkTag = IndeterminateComponent 201 | let resolvedType = type 202 | 203 | if (typeof type === 'function') { 204 | } else if (typeof type === 'string') { 205 | fiberTag = HostComponent 206 | } else { 207 | getTag: switch (type) { 208 | default: { 209 | if (typeof type === 'object' && type !== null) { 210 | switch (type.$$typeof) { 211 | case REACT_MEMO_TYPE: { 212 | fiberTag = MemoComponent 213 | break getTag 214 | } 215 | default: { 216 | throw new Error('Not Implement') 217 | } 218 | } 219 | } else { 220 | throw new Error('Not Implement') 221 | } 222 | } 223 | } 224 | } 225 | 226 | const fiber = createFiber(fiberTag, pendingProps, key, mode) 227 | fiber.type = resolvedType 228 | fiber.lanes = lanes 229 | fiber.elementType = type 230 | return fiber 231 | } 232 | 233 | export const createFiberFromElement = ( 234 | element: ReactElement, 235 | mode: TypeOfMode, 236 | lanes: Lanes 237 | ): Fiber => { 238 | const type = element.type 239 | const key = element.key as any 240 | const pendingProps = element.props 241 | 242 | const fiber = createFiberFromTypeAndProps( 243 | type, 244 | key, 245 | pendingProps, 246 | mode, 247 | lanes 248 | ) 249 | 250 | return fiber 251 | } 252 | 253 | /** 254 | * 创建一个HostText类型的Fiber节点 255 | * @param content 会作为pendingProps 256 | * @param mode 257 | * @returns 258 | */ 259 | export const createFiberFromText = ( 260 | content: string, 261 | mode: TypeOfMode, 262 | lanes: Lanes 263 | ): Fiber => { 264 | const fiber = createFiber(HostText, content, null, mode) 265 | fiber.lanes = lanes 266 | return fiber 267 | } 268 | 269 | export const isSimpleFunctionComponent = (type: any): boolean => { 270 | return typeof type === 'function' && type.defaultProps === undefined 271 | } 272 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactUpdateQueue.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 当首次mount时HostRoot会用到,Class Component也会用到该类型的updateQueue 3 | * Function Component使用的时另外的逻辑 4 | */ 5 | 6 | import { Fiber } from './ReactInternalTypes' 7 | 8 | export type SharedQueue = { 9 | /** 10 | * 存储着本次更新的update队列,是实际的updateQueue。 11 | * shared的意思是current节点与workInProgress节点共享一条更新队列。 12 | */ 13 | pending: Update | null 14 | } 15 | 16 | export type UpdateQueue = { 17 | /** 18 | * 前一次更新计算得出的状态,它是第一个被跳过的update之前的那些update计算得出的state。会以它为基础计算本次的state 19 | */ 20 | baseState: State 21 | shared: SharedQueue 22 | /** 23 | * 前一次更新时updateQueue中第一个被跳过的update对象 24 | */ 25 | firstBaseUpdate: Update | null 26 | /** 27 | *lastBaseUpdate相当于,前一次更新中,updateQueue中以第一个被跳过的update为起点一直到的最后一个update的形成的链表,截取最后一个而获得的update 28 | */ 29 | lastBaseUpdate: Update | null 30 | } 31 | 32 | export const UpdateState = 0 33 | 34 | export type Update = { 35 | /** 36 | * 更新所携带的状态。 37 | * 类组件中:有两种可能,对象({}),和函数((prevState, nextProps):newState => {}) 38 | *根组件中:是React.createElement创建的JSX对象,即ReactDOM.render的第一个参数 39 | */ 40 | payload: any 41 | 42 | /** 43 | * update之间通过next形成一条链表,后创建的update插在链表前端, 44 | * 其中最后一个update(最先创建的)的next指向第一个update形成一个环 45 | * 所以updateQueue.shared.pending为最后创建的update 46 | */ 47 | next: null | Update 48 | 49 | tag: 0 | 1 | 2 | 3 50 | } 51 | 52 | /** 53 | *初始化fiber节点的updateQueue 54 | * 55 | * @param fiber 要初始化updateQueue的fiber 56 | */ 57 | export const initializeUpdateQueue = (fiber: Fiber): void => { 58 | const queue: UpdateQueue = { 59 | baseState: fiber.memoizedState, 60 | shared: { 61 | pending: null, 62 | }, 63 | lastBaseUpdate: null, 64 | firstBaseUpdate: null, 65 | } 66 | 67 | fiber.updateQueue = queue 68 | } 69 | 70 | /** 71 | * 72 | * @returns 创建一个全新的update 73 | */ 74 | export const createUpdate = (): Update => { 75 | const update: Update = { 76 | payload: null, 77 | next: null, 78 | tag: UpdateState, 79 | } 80 | return update 81 | } 82 | 83 | /** 84 | * 将一个update添加fiber节点上 85 | * 86 | * @param fiber 要添加update的fiber节点 87 | * @param update 该update会被添加到fiber节点的updateQueue上 88 | */ 89 | export const enqueueUpdate = (fiber: Fiber, update: Update): void => { 90 | const updateQueue = fiber.updateQueue 91 | if (!updateQueue) return 92 | 93 | const sharedQueue: SharedQueue = (updateQueue as any).shared 94 | 95 | const pending: Update | null = sharedQueue.pending 96 | 97 | if (pending === null) { 98 | //第一个创建的update,创建一个循环链表 99 | update.next = update 100 | } else { 101 | //sharedQueue.pending 始终为最后一个创建的update 102 | //sharedQueue.pending.next指向第一个创建的update,遍历update都是从此开始 103 | //按顺序1,2,3插入update则构成的链表为 104 | //______________ 105 | //| ↓ 106 | //3 <- 2 <- 1 107 | 108 | //update3的next指向最早创建的update1 109 | update.next = pending.next 110 | 111 | //update2的next指向当前创建的update3 112 | pending.next = update 113 | } 114 | 115 | sharedQueue.pending = update 116 | } 117 | 118 | /** 119 | * 从current fiber上克隆一个updateQueue 120 | * @param current 121 | * @param workInProgress 122 | */ 123 | export const cloneUpdateQueue = ( 124 | current: Fiber, 125 | workInProgress: Fiber 126 | ): void => { 127 | const queue: UpdateQueue = workInProgress.updateQueue as any 128 | 129 | const currentQueue: UpdateQueue = current.updateQueue as any 130 | 131 | if (queue === currentQueue) { 132 | const clone: UpdateQueue = { 133 | shared: currentQueue.shared, 134 | firstBaseUpdate: currentQueue.firstBaseUpdate, 135 | lastBaseUpdate: currentQueue.lastBaseUpdate, 136 | baseState: currentQueue.baseState, 137 | } 138 | 139 | workInProgress.updateQueue = clone 140 | } 141 | } 142 | 143 | export const processUpdateQueue = ( 144 | workInProgress: Fiber, 145 | props: any, 146 | instance: any 147 | ) => { 148 | const queue: UpdateQueue = workInProgress.updateQueue as any 149 | 150 | 151 | let firstBaseUpdate = queue.firstBaseUpdate 152 | let lastBaseUpdate = queue.lastBaseUpdate 153 | 154 | //检测shared.pending是否存在进行中的update将他们转移到baseQueue 155 | let pendingQueue = queue.shared.pending 156 | if (pendingQueue !== null) { 157 | queue.shared.pending = null 158 | 159 | const lastPendingUpdate = pendingQueue 160 | const firstPendingUpdate = lastPendingUpdate.next 161 | //断开最后一个update和第一个update之间的连接 162 | lastPendingUpdate.next = null 163 | 164 | //将shared.pending上的update接到baseUpdate链表上 165 | if (lastBaseUpdate === null) { 166 | firstBaseUpdate = firstPendingUpdate 167 | } else { 168 | lastBaseUpdate.next = firstPendingUpdate 169 | } 170 | 171 | lastBaseUpdate = lastPendingUpdate 172 | 173 | //如果current存在则进行相同的工作 174 | const current = workInProgress.alternate 175 | if (current !== null) { 176 | const currentQueue: UpdateQueue = current.updateQueue as any 177 | const currentLastBaseUpdate = currentQueue.lastBaseUpdate 178 | 179 | if (currentLastBaseUpdate !== lastBaseUpdate) { 180 | if (currentLastBaseUpdate === null) { 181 | currentQueue.firstBaseUpdate = firstPendingUpdate 182 | } else { 183 | currentLastBaseUpdate.next = firstPendingUpdate 184 | } 185 | currentQueue.lastBaseUpdate = lastPendingUpdate 186 | } 187 | } 188 | } 189 | 190 | if (firstBaseUpdate !== null) { 191 | let newState = queue.baseState 192 | 193 | let newBaseState = null 194 | let newFirstBaseUpdate = null 195 | let newLastBaseUpdate = null 196 | 197 | let update: Update | null = firstBaseUpdate 198 | 199 | do { 200 | //暂时假设,所有更新都是一样的优先级,每次都从所有update计算状态 201 | newState = getStateFromUpdate( 202 | workInProgress, 203 | queue, 204 | update!, 205 | newState, 206 | props, 207 | instance 208 | ) 209 | update = update!.next 210 | 211 | //当前更新以全部遍历完,但是有可能payload为函数在计算state的过程中又在 212 | //updateQueue.shared.pending上添加了新的更新,继续迭代直到没有新更新产生为止 213 | if (update === null) { 214 | pendingQueue = queue.shared.pending 215 | //没产生新的更新 216 | if (pendingQueue === null) break 217 | else { 218 | //产生了新的更新 219 | const lastPendingUpdate = pendingQueue 220 | const firstPendingUpdate = lastPendingUpdate.next 221 | lastPendingUpdate.next = null 222 | 223 | update = firstPendingUpdate 224 | queue.lastBaseUpdate = lastPendingUpdate 225 | queue.shared.pending = null 226 | } 227 | } 228 | } while (true) 229 | 230 | //暂时没有会被跳过的update始终不成立 231 | if (newLastBaseUpdate === null) { 232 | newBaseState = newState 233 | } 234 | 235 | queue.baseState = newBaseState as any 236 | 237 | queue.firstBaseUpdate = newFirstBaseUpdate 238 | queue.lastBaseUpdate = newLastBaseUpdate 239 | 240 | workInProgress.memoizedState = newState 241 | } 242 | } 243 | 244 | export const getStateFromUpdate = ( 245 | _workInProgress: Fiber, 246 | _queue: UpdateQueue, 247 | update: Update, 248 | prevState: State, 249 | nextProps: any, 250 | instance: any 251 | ): any => { 252 | switch (update.tag) { 253 | case UpdateState: { 254 | const payload = update.payload 255 | let partialState 256 | if (typeof payload === 'function') { 257 | partialState = payload.call(instance, prevState, nextProps) 258 | } else { 259 | partialState = payload 260 | } 261 | 262 | if (partialState === null || partialState === undefined) { 263 | //如果是null或者undefined就说明什么都不用更新,什么也不干 264 | return prevState 265 | } 266 | 267 | return Object.assign({}, prevState, partialState) 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactFiberLane.ts: -------------------------------------------------------------------------------- 1 | import { FiberRoot } from './ReactInternalTypes' 2 | 3 | export const TotalLanes = 31 4 | 5 | export type Lanes = number 6 | export type Lane = number 7 | export type LaneMap = Array 8 | 9 | export const NoLanes: Lane = /* */ 0b0000000000000000000000000000000 10 | export const NoLane: Lane = /* */ 0b0000000000000000000000000000000 11 | 12 | export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001 13 | export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100 14 | export const DefaultLane: Lanes = /* */ 0b0000000000000000000000000010000 15 | 16 | export const IdleLane: Lanes = /* */ 0b0100000000000000000000000000000 17 | 18 | const NonIdleLanes = /* */ 0b0001111111111111111111111111111 19 | 20 | export const NoTimestamp = -1 21 | 22 | const clz32 = Math.clz32 23 | 24 | const pickArbitraryLaneIndex = (lanes: Lanes): number => { 25 | return 31 - clz32(lanes) 26 | } 27 | 28 | /** 29 | * 根据任务的优先级为其计算一个过期时间 30 | * @param lane 优先级 31 | * @param currentTime 当前的时间 32 | * @returns 33 | */ 34 | const computeExpirationTime = (lane: Lane, currentTime: number): number => { 35 | switch (lane) { 36 | case SyncLane: 37 | return currentTime + 250 38 | case DefaultLane: 39 | return currentTime + 5000 40 | default: { 41 | throw new Error('Not Implement') 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * 将已经过期的任务标记出来 48 | * @param root FiberRoot 49 | * @param currentTime 当前的时间 50 | */ 51 | export const markStarvedLanesAsExpired = ( 52 | root: FiberRoot, 53 | currentTime: number 54 | ): void => { 55 | const pendingLanes = root.pendingLanes 56 | const expirationTimes = root.expirationTimes 57 | 58 | let lanes = pendingLanes 59 | 60 | while (lanes > 0) { 61 | const index = pickArbitraryLaneIndex(lanes) 62 | const lane = 1 << index 63 | 64 | const expirationTime = expirationTimes[index] 65 | 66 | if (expirationTime === NoTimestamp) { 67 | /** 68 | * 还没有相关的时间戳,帮他计算一个 69 | */ 70 | expirationTimes[index] = computeExpirationTime(lane, currentTime) 71 | } else if (expirationTime <= currentTime) { 72 | //已经过期将其加入到expiredLanes中 73 | root.expiredLanes |= lane 74 | } 75 | 76 | //从lanes中移除该lane,下一轮循环就能开始检测下一个lane了 77 | lanes &= ~lane 78 | } 79 | } 80 | 81 | /** 82 | * 返回现在被占用的lanes中最高优先级的lane 83 | * 也就是获得一个数中以最低位1所形成的数字,原理可以去查看负数的表示 84 | * 比如输入 0b111 就返回 0b001 85 | * 0b101 -> 0b001 86 | * 0b100 -> 0b100 87 | * 0b1000001000 -> 0b0000001000 88 | * 0b1111111110 -> 0b0000000010 89 | * @param lanes 90 | * @returns 91 | */ 92 | export const getHighestPriorityLane = (lanes: Lanes): Lane => { 93 | return lanes & -lanes 94 | } 95 | 96 | /** 97 | * 返回现有的lanes中最高优先级的lane 98 | * @param lanes 99 | * @returns 100 | */ 101 | const getHighestPriorityLanes = (lanes: Lanes | Lane): Lanes => { 102 | switch (getHighestPriorityLane(lanes)) { 103 | case SyncLane: 104 | return SyncLane 105 | case DefaultLane: 106 | return DefaultLane 107 | default: { 108 | throw new Error('Not Implement') 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * 根据当前root的pendingLanes和workInProgressLanes返回这次更新的lanes 115 | * @param root 116 | * @param wipLanes 117 | * @returns 118 | */ 119 | export const getNextLanes = (root: FiberRoot, wipLanes: Lanes): Lanes => { 120 | const pendingLanes = root.pendingLanes 121 | 122 | //提前退出,如果没有待进行的工作 123 | if (pendingLanes === NoLanes) return NoLanes 124 | 125 | let nextLanes = NoLanes 126 | 127 | const nonIdlePendingLanes = pendingLanes & NonIdleLanes 128 | 129 | //存在lanes被占用,找出哪个最高优先级的 130 | if (nonIdlePendingLanes !== NoLanes) { 131 | //返回现被占用的lanes中最高优先级的lane 132 | nextLanes = getHighestPriorityLanes(nonIdlePendingLanes) 133 | } else { 134 | throw new Error('Not Implement') 135 | } 136 | 137 | if (nextLanes === NoLanes) { 138 | return NoLanes 139 | } 140 | 141 | /** 142 | * 如果已经处于render阶段,切换lanes会导致丢失进度 143 | * 我们只应该在新的lane拥有更高的优先级的时候这样做 144 | */ 145 | if (wipLanes !== NoLanes && wipLanes !== nextLanes) { 146 | const nextLane = getHighestPriorityLane(nextLanes) 147 | const wipLane = getHighestPriorityLane(wipLanes) 148 | 149 | //如果该次任务的优先级低于现存任务的优先级则workInProgressLanes不变 150 | if (nextLane >= wipLane) { 151 | return wipLanes 152 | } 153 | } 154 | 155 | return nextLanes 156 | } 157 | 158 | export const includesSomeLane = (a: Lanes | Lane, b: Lanes | Lane): boolean => { 159 | return (a & b) !== NoLanes 160 | } 161 | 162 | export const mergeLanes = (a: Lanes | Lane, b: Lanes | Lane): Lanes => { 163 | return a | b 164 | } 165 | 166 | /** 167 | * 返回该lane所在bit位在bitset中index 168 | * 比如 169 | * 0b001 就会返回0 170 | * 0b010 就会返回1 171 | * 0b100 就会返回2 172 | * 173 | * @param lane 174 | * @returns 175 | */ 176 | const laneToIndex = (lane: Lane): number => { 177 | return pickArbitraryLaneIndex(lane) 178 | } 179 | 180 | /** 181 | * 把该次更新的lane并到root的pendingLanes中,以及记录该更新对应lane的发生的时间, 182 | * 方便以后可以判断该更新是否已经过期需要立即执行该更新 183 | * @param root FiberRoot 184 | * @param updateLane 该更新对应的lane 185 | * @param eventTime 该更新发生的时间 186 | */ 187 | export const markRootUpdated = ( 188 | root: FiberRoot, 189 | updateLane: Lane, 190 | eventTime: number 191 | ): void => { 192 | //root上包含的更新他们所对应的lanes 193 | root.pendingLanes |= updateLane 194 | 195 | //一个三十一位的数组,分别对应着31位lane 196 | const eventTimes = root.eventTimes 197 | 198 | const index = laneToIndex(updateLane) 199 | eventTimes[index] = eventTime 200 | } 201 | 202 | export const createLaneMap = (initial: T): LaneMap => { 203 | const laneMap = [] 204 | for (let i = 0; i < TotalLanes; ++i) { 205 | laneMap.push(initial) 206 | } 207 | 208 | return laneMap 209 | } 210 | 211 | /** 212 | * subset bitset是否是 set bitset的子集 213 | * @param set 214 | * @param subset 215 | * @returns 216 | */ 217 | export const isSubsetOfLanes = (set: Lanes, subset: Lanes | Lane) => { 218 | return (set & subset) === subset 219 | } 220 | 221 | export const includesNonIdleWork = (lanes: Lanes): boolean => { 222 | return (lanes & NonIdleLanes) !== NonIdleLanes 223 | } 224 | 225 | /** 226 | * 是否开启默认同步模式 227 | */ 228 | const enableSyncDefaultUpdates = false 229 | 230 | /** 231 | * 是否开启时间切片,React中默认开启了同步模式(enableSyncDefaultUpdates),所以不会 232 | * 开启时间切片,我们这为了学习的目的把他关闭 233 | * @param root 234 | * @param lanes 235 | * @returns 236 | */ 237 | export const shouldTimeSlice = (root: FiberRoot, lanes: Lanes): boolean => { 238 | if ((lanes & root.expiredLanes) !== NoLanes) { 239 | //至少有一个lane已经过期了,为了防止更多的lane过期 240 | //因该尽快完成渲染,而不把控制权交给浏览器 241 | return false 242 | } 243 | 244 | if (enableSyncDefaultUpdates) { 245 | const SyncDefaultLanes = InputContinuousLane | DefaultLane 246 | 247 | return (lanes & SyncDefaultLanes) === NoLanes 248 | } else { 249 | return true 250 | } 251 | } 252 | 253 | /** 254 | * 进行本轮更新的收尾工作,将完成工作的lane time重置,并将他们 255 | * 从pendingLanes,expiredLanes去除 256 | * @param root 257 | * @param remainingLanes 剩余要进行工作的lanes 258 | */ 259 | export const markRootFinished = (root: FiberRoot, remainingLanes: Lanes) => { 260 | const noLongerPendingLanes = root.pendingLanes & ~remainingLanes 261 | 262 | root.pendingLanes = remainingLanes 263 | root.expiredLanes &= remainingLanes 264 | 265 | const eventTimes = root.eventTimes 266 | const expirationTimes = root.expirationTimes 267 | 268 | let lanes = noLongerPendingLanes 269 | 270 | while (lanes > 0) { 271 | const index = pickArbitraryLaneIndex(lanes) 272 | const lane = 1 << index 273 | eventTimes[index] = NoTimestamp 274 | expirationTimes[index] = NoTimestamp 275 | lanes &= ~lane 276 | } 277 | } 278 | 279 | export const removeLanes = (set: Lanes, subset: Lanes | Lane): Lanes => { 280 | return set & ~subset 281 | } 282 | -------------------------------------------------------------------------------- /test/Scheduler.test.ts: -------------------------------------------------------------------------------- 1 | import { PriorityLevel } from '../packages/scheduler/SchedulerPriorities' 2 | let Scheduler: any 3 | let runtime: { 4 | advanceTime: Function 5 | fireMessageEvent: Function 6 | log: Function 7 | isLogEmpty: Function 8 | assertLog: Function 9 | } 10 | let performance: { 11 | now: Function 12 | } 13 | let cancelCallback: Function 14 | let scheduleCallback: Function 15 | let NormalPriority: PriorityLevel 16 | 17 | // The Scheduler implementation uses browser APIs like `MessageChannel` and 18 | // `setTimeout` to schedule work on the main thread. Most of our tests treat 19 | // these as implementation details; however, the sequence and timing of these 20 | // APIs are not precisely specified, and can vary across browsers. 21 | // 22 | // To prevent regressions, we need the ability to simulate specific edge cases 23 | // that we may encounter in various browsers. 24 | // 25 | // This test suite mocks all browser methods used in our implementation. It 26 | // assumes as little as possible about the order and timing of events. 27 | describe('SchedulerBrowser', () => { 28 | beforeEach(() => { 29 | jest.resetModules() 30 | runtime = installMockBrowserRuntime() 31 | // jest.unmock('scheduler') 32 | 33 | Scheduler = require('../packages/scheduler') 34 | performance = global.performance as any 35 | cancelCallback = Scheduler.unstable_cancelCallback 36 | scheduleCallback = Scheduler.unstable_scheduleCallback 37 | NormalPriority = Scheduler.unstable_NormalPriority 38 | }) 39 | 40 | afterEach(() => { 41 | delete (global as any).performance 42 | 43 | if (!runtime.isLogEmpty()) { 44 | throw Error('Test exited without clearing log.') 45 | } 46 | }) 47 | 48 | function installMockBrowserRuntime() { 49 | let hasPendingMessageEvent = false 50 | 51 | let timerIDCounter = 0 52 | // let timerIDs = new Map(); 53 | 54 | let eventLog: string[] = [] 55 | 56 | let currentTime = 0 57 | 58 | ;(global as any).performance = { 59 | now() { 60 | return currentTime 61 | }, 62 | } 63 | 64 | // Delete node provide setImmediate so we fall through to MessageChannel. 65 | delete (global as any).setImmediate 66 | 67 | global.setTimeout = ((cb: Function, delay: number) => { 68 | const id = timerIDCounter++ 69 | log(`Set Timer`) 70 | // TODO 71 | return id 72 | }) as any 73 | global.clearTimeout = ((id: number) => { 74 | // TODO 75 | }) as any 76 | 77 | const port1: { onmessage: Function } = {} as any 78 | const port2 = { 79 | postMessage() { 80 | if (hasPendingMessageEvent) { 81 | throw Error('Message event already scheduled') 82 | } 83 | log('Post Message') 84 | hasPendingMessageEvent = true 85 | }, 86 | } 87 | global.MessageChannel = function MessageChannel(this: any) { 88 | this.port1 = port1 89 | this.port2 = port2 90 | } as any 91 | 92 | function ensureLogIsEmpty() { 93 | if (eventLog.length !== 0) { 94 | throw Error('Log is not empty. Call assertLog before continuing.') 95 | } 96 | } 97 | function advanceTime(ms: number) { 98 | currentTime += ms 99 | } 100 | function fireMessageEvent() { 101 | ensureLogIsEmpty() 102 | if (!hasPendingMessageEvent) { 103 | throw Error('No message event was scheduled') 104 | } 105 | hasPendingMessageEvent = false 106 | const onMessage = port1.onmessage 107 | log('Message Event') 108 | onMessage() 109 | } 110 | function log(val: string) { 111 | eventLog.push(val) 112 | } 113 | function isLogEmpty() { 114 | return eventLog.length === 0 115 | } 116 | function assertLog(expected: string[]) { 117 | const actual = eventLog 118 | eventLog = [] 119 | expect(actual).toEqual(expected) 120 | } 121 | return { 122 | advanceTime, 123 | fireMessageEvent, 124 | log, 125 | isLogEmpty, 126 | assertLog, 127 | } 128 | } 129 | 130 | it('task that finishes before deadline', () => { 131 | scheduleCallback(NormalPriority, () => { 132 | runtime.log('Task') 133 | }) 134 | runtime.assertLog(['Post Message']) 135 | runtime.fireMessageEvent() 136 | runtime.assertLog(['Message Event', 'Task']) 137 | }) 138 | 139 | it('task with continuation', () => { 140 | scheduleCallback(NormalPriority, () => { 141 | runtime.log('Task') 142 | while (!Scheduler.unstable_shouldYield()) { 143 | runtime.advanceTime(1) 144 | } 145 | runtime.log(`Yield at ${performance.now()}ms`) 146 | return () => { 147 | runtime.log('Continuation') 148 | } 149 | }) 150 | runtime.assertLog(['Post Message']) 151 | 152 | runtime.fireMessageEvent() 153 | runtime.assertLog(['Message Event', 'Task', 'Yield at 5ms', 'Post Message']) 154 | 155 | runtime.fireMessageEvent() 156 | runtime.assertLog(['Message Event', 'Continuation']) 157 | }) 158 | 159 | it('multiple tasks', () => { 160 | scheduleCallback(NormalPriority, () => { 161 | runtime.log('A') 162 | }) 163 | scheduleCallback(NormalPriority, () => { 164 | runtime.log('B') 165 | }) 166 | runtime.assertLog(['Post Message']) 167 | runtime.fireMessageEvent() 168 | runtime.assertLog(['Message Event', 'A', 'B']) 169 | }) 170 | 171 | it('multiple tasks with a yield in between', () => { 172 | scheduleCallback(NormalPriority, () => { 173 | runtime.log('A') 174 | runtime.advanceTime(4999) 175 | }) 176 | scheduleCallback(NormalPriority, () => { 177 | runtime.log('B') 178 | }) 179 | runtime.assertLog(['Post Message']) 180 | runtime.fireMessageEvent() 181 | runtime.assertLog([ 182 | 'Message Event', 183 | 'A', 184 | // Ran out of time. Post a continuation event. 185 | 'Post Message', 186 | ]) 187 | runtime.fireMessageEvent() 188 | runtime.assertLog(['Message Event', 'B']) 189 | }) 190 | 191 | it('cancels tasks', () => { 192 | const task = scheduleCallback(NormalPriority, () => { 193 | runtime.log('Task') 194 | }) 195 | runtime.assertLog(['Post Message']) 196 | cancelCallback(task) 197 | runtime.assertLog([]) 198 | }) 199 | 200 | it('throws when a task errors then continues in a new event', () => { 201 | scheduleCallback(NormalPriority, () => { 202 | runtime.log('Oops!') 203 | throw Error('Oops!') 204 | }) 205 | scheduleCallback(NormalPriority, () => { 206 | runtime.log('Yay') 207 | }) 208 | runtime.assertLog(['Post Message']) 209 | 210 | expect(() => runtime.fireMessageEvent()).toThrow('Oops!') 211 | runtime.assertLog(['Message Event', 'Oops!', 'Post Message']) 212 | 213 | runtime.fireMessageEvent() 214 | runtime.assertLog(['Message Event', 'Yay']) 215 | }) 216 | 217 | it('schedule new task after queue has emptied', () => { 218 | scheduleCallback(NormalPriority, () => { 219 | runtime.log('A') 220 | }) 221 | 222 | runtime.assertLog(['Post Message']) 223 | runtime.fireMessageEvent() 224 | runtime.assertLog(['Message Event', 'A']) 225 | 226 | scheduleCallback(NormalPriority, () => { 227 | runtime.log('B') 228 | }) 229 | runtime.assertLog(['Post Message']) 230 | runtime.fireMessageEvent() 231 | runtime.assertLog(['Message Event', 'B']) 232 | }) 233 | 234 | it('schedule new task after a cancellation', () => { 235 | const handle = scheduleCallback(NormalPriority, () => { 236 | runtime.log('A') 237 | }) 238 | 239 | runtime.assertLog(['Post Message']) 240 | cancelCallback(handle) 241 | 242 | runtime.fireMessageEvent() 243 | runtime.assertLog(['Message Event']) 244 | 245 | scheduleCallback(NormalPriority, () => { 246 | runtime.log('B') 247 | }) 248 | runtime.assertLog(['Post Message']) 249 | runtime.fireMessageEvent() 250 | runtime.assertLog(['Message Event', 'B']) 251 | }) 252 | }) 253 | -------------------------------------------------------------------------------- /packages/scheduler/SchedulerDOM.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ImmediatePriority, 3 | NormalPriority, 4 | PriorityLevel, 5 | } from './SchedulerPriorities' 6 | import { push, Node, peek, pop } from './SchedulerMinHeap' 7 | 8 | type Task = { 9 | id: number 10 | /** 11 | * 该任务的要执行的操作,最常见的就是performConcurrentWorkOnRoot 12 | * 时间分片就靠此实现 13 | */ 14 | callback: Function | null 15 | /** 16 | * 该任务的优先级 17 | */ 18 | priorityLevel: PriorityLevel 19 | /** 20 | * 该任务的开始时间 21 | */ 22 | startTime: number 23 | /** 24 | * 该任务的过期时间 25 | */ 26 | expirationTime: number 27 | /** 28 | * 最小优先队列中会按照该字段将一个节点放到队列合适的位置和expirationTime是同一个值 29 | */ 30 | sortIndex: number 31 | } 32 | 33 | const getCurrentTime = () => performance.now() 34 | /** 35 | * NORMAL级别任务过期时间的计算标准,比如现在时间为0, 36 | * 有一个Normal级别的任务被调度了,那么他的过期时间就为 37 | * `当前时间 +NORMAL_PRIORITY_TIMEOUT` 38 | * 等于5000 39 | */ 40 | const NORMAL_PRIORITY_TIMEOUT = 5000 41 | /** 42 | * IMMEDIATE级别任务过期时间计算标准,和上面同理 43 | */ 44 | const IMMEDIATE_PRIORITY_TIMEOUT = -1 45 | 46 | let taskIdCounter = 1 47 | /** 48 | * 延时任务队列,我们的代码中没有用到 49 | */ 50 | const timerQueue: Node[] = [] 51 | /** 52 | * 存放任务的最小有限队列 53 | */ 54 | const taskQueue: Node[] = [] 55 | let isHostCallbackScheduled = false 56 | let isHostTimeoutScheduled = false 57 | 58 | let currentPriorityLevel = NormalPriority 59 | let currentTask: null | Task = null 60 | 61 | let scheduledHostCallback: null | Function = null 62 | 63 | //当在执行工作的时候设置为true,防止二次进入 64 | let isPerformingWork = false 65 | 66 | let isMessageLoopRunning = false 67 | 68 | let dealine = 0 69 | let yieldInterval = 5 70 | 71 | let needsPaint = false 72 | 73 | let taskTimeoutID = -1 74 | 75 | const performWorkUntilDeadline = () => { 76 | if (scheduledHostCallback !== null) { 77 | const currentTime = getCurrentTime() 78 | dealine = currentTime + yieldInterval 79 | const hasTimeRemaining = true 80 | 81 | let hasMoreWork = true 82 | 83 | try { 84 | /** 85 | * 在我们的代码中scheduledHostCallback一直都是flushWork 86 | * 下面这行代码执行了flushWork 87 | */ 88 | hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime) 89 | } finally { 90 | if (hasMoreWork) { 91 | /** 92 | * 如果任务队列还不为空,就注册一个宏任务待会回来继续执行任务 93 | * 时间分片实现的关键 94 | */ 95 | schedulePerformWorkUntilDeadline() 96 | } else { 97 | isMessageLoopRunning = false 98 | scheduledHostCallback = null 99 | } 100 | } 101 | } else { 102 | isMessageLoopRunning = false 103 | } 104 | 105 | //在将控制权交给浏览器后他将有机会去渲染,所以我们可以重置这个变量 106 | needsPaint = false 107 | } 108 | 109 | /** 110 | * 使用postMessage注册一个宏任务 111 | * @param callback 112 | */ 113 | const requestHostCallback = (callback: Function): void => { 114 | scheduledHostCallback = callback 115 | 116 | if (!isMessageLoopRunning) { 117 | isMessageLoopRunning = true 118 | schedulePerformWorkUntilDeadline() 119 | } 120 | } 121 | 122 | let schedulePerformWorkUntilDeadline: () => void 123 | 124 | { 125 | const channel = new MessageChannel() 126 | const port = channel.port2 127 | channel.port1.onmessage = performWorkUntilDeadline 128 | schedulePerformWorkUntilDeadline = () => { 129 | port.postMessage(null) 130 | } 131 | } 132 | 133 | /** 134 | * 将那些延期到时的任务从timerQueue移动到taskQueue 135 | * @param currentTime 当前的时间 136 | */ 137 | const advanceTimers = (currentTime: number) => { 138 | let timer = peek(timerQueue) as Task 139 | while (timer !== null) { 140 | if (timer.callback === null) { 141 | //该任务被取消 142 | pop(timerQueue) 143 | } else if (timer.startTime <= currentTime) { 144 | pop(timerQueue) 145 | timer.sortIndex = timer.expirationTime 146 | push(taskQueue, timer) 147 | } else { 148 | // 剩余的任务都还没有到时 149 | return 150 | } 151 | timer = peek(timerQueue) as any 152 | } 153 | } 154 | 155 | /** 156 | * 是否将控制权交还给浏览器 157 | * 为了更好的知道归还时机 158 | * facebook甚至还和Chrome团队联合实现了一个 159 | * isInputPending[https://web.dev/isinputpending/] 160 | * 这个api默认是关闭的所以在这里我没有添加进来 161 | * 更详细的实现可以去看官方仓库 162 | * 现在的逻辑是一个切片的时间是5ms超过这个时间就把render工作 163 | * 暂停,然后在下一个切片中继续工作 164 | * @returns 165 | */ 166 | const shouldYieldToHost = (): boolean => { 167 | return getCurrentTime() >= dealine 168 | } 169 | 170 | const requestHostTimeout = (callback: Function, ms: number): void => { 171 | taskTimeoutID = setTimeout(() => { 172 | callback(getCurrentTime()) 173 | }, ms) as unknown as number 174 | } 175 | 176 | const handleTimeout = (currentTime: number) => { 177 | isHostTimeoutScheduled = false 178 | advanceTimers(currentTime) 179 | 180 | if (!isHostCallbackScheduled) { 181 | if (peek(taskQueue) !== null) { 182 | isHostCallbackScheduled = true 183 | requestHostCallback(flushWork) 184 | } else { 185 | const firstTimer = peek(timerQueue) as Task 186 | if (firstTimer !== null) { 187 | requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime) 188 | } 189 | } 190 | } 191 | } 192 | 193 | const workLoop = (hasTimeRemaining: boolean, initialTime: number) => { 194 | let currentTime = initialTime 195 | advanceTimers(currentTime) 196 | currentTask = peek(taskQueue) as any 197 | 198 | while (currentTask !== null) { 199 | if ( 200 | currentTask.expirationTime > currentTime && 201 | (!hasTimeRemaining || shouldYieldToHost()) 202 | ) { 203 | //如果当前的任务还没有过期而且已经期限了直接break 204 | break 205 | } 206 | 207 | const callback = currentTask.callback 208 | 209 | if (typeof callback === 'function') { 210 | currentTask.callback = null 211 | currentPriorityLevel = currentTask.priorityLevel 212 | const didUserCallbackTimeout = currentTask.expirationTime <= currentTime 213 | 214 | const continuationCallback = callback(didUserCallbackTimeout) 215 | currentTime = getCurrentTime() 216 | if (typeof continuationCallback === 'function') { 217 | currentTask.callback = continuationCallback 218 | } else { 219 | if (currentTask === peek(taskQueue)) { 220 | pop(taskQueue) 221 | } 222 | } 223 | 224 | advanceTimers(currentTime) 225 | } else { 226 | pop(taskQueue) 227 | } 228 | 229 | currentTask = peek(taskQueue) as any 230 | } 231 | 232 | if (currentTask !== null) { 233 | return true 234 | } else { 235 | const firstTimer = peek(timerQueue) as Task 236 | if (firstTimer !== null) { 237 | requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime) 238 | } 239 | return false 240 | } 241 | } 242 | 243 | const flushWork = (hasTimeRemaining: boolean, initialTime: number) => { 244 | //将该变量设置为false,让以后的host callback能被规划 245 | isHostCallbackScheduled = false 246 | 247 | if (isHostTimeoutScheduled) { 248 | throw new Error('Not Implement') 249 | } 250 | 251 | isPerformingWork = true 252 | 253 | const previousPriorityLevel = currentPriorityLevel 254 | 255 | try { 256 | return workLoop(hasTimeRemaining, initialTime) 257 | } finally { 258 | currentTask = null 259 | currentPriorityLevel = previousPriorityLevel 260 | isPerformingWork = false 261 | } 262 | } 263 | 264 | /** 265 | * 调度一个任务,根据其优先级为其设置一个过期时间, 266 | * 然后将他放入一个以过期时间为排序标准的最小优先队列 267 | * 比如调度了一个Normal和一个Sync级别的任务,且当前时间为0 268 | * 所以Normal级别的任务的过期时间为5000,而Sync级别的为-1 269 | * 所以Sync级别的过期时间被Normal级别的小,会被先出队执行 270 | * @param priorityLevel 该任务的优先级,决定了该任务的过期时间 271 | * @param callback 要调度的任务最常见的就是performConcurrentWorkOnRoot 272 | * @param options 273 | * @returns 返回该任务节点,持有该任务节点的模块可在其执行前将其取消 274 | */ 275 | const unstable_scheduleCallback = ( 276 | priorityLevel: PriorityLevel, 277 | callback: Function, 278 | options: { 279 | delay: number 280 | } | null 281 | ): Task => { 282 | const currentTime = getCurrentTime() 283 | let startTime 284 | 285 | if (typeof options === 'object' && options !== null) { 286 | const delay = options.delay 287 | if (typeof delay === 'number' && delay > 0) { 288 | startTime = currentTime + delay 289 | } else { 290 | startTime = currentTime 291 | } 292 | } else { 293 | startTime = currentTime 294 | } 295 | 296 | let timeout: number 297 | 298 | switch (priorityLevel) { 299 | case ImmediatePriority: 300 | timeout = IMMEDIATE_PRIORITY_TIMEOUT 301 | break 302 | case NormalPriority: 303 | timeout = NORMAL_PRIORITY_TIMEOUT 304 | break 305 | default: 306 | throw new Error('Not Implement') 307 | } 308 | 309 | const expirationTime = startTime + timeout 310 | const newTask: Task = { 311 | id: taskIdCounter++, 312 | callback, 313 | priorityLevel, 314 | startTime, 315 | expirationTime, 316 | sortIndex: -1, 317 | } 318 | 319 | if (startTime > currentTime) { 320 | //这是个延时任务 321 | // newTask.sortIndex = startTime 322 | // push(timerQueue, newTask) 323 | throw new Error('Not Implement') 324 | } else { 325 | newTask.sortIndex = expirationTime 326 | //入队 327 | push(taskQueue, newTask) 328 | 329 | //用postMessage注册一个宏任务,在该宏任务中执行任务队列中的任务 330 | //并设置相关变量防止二次注册 331 | if (!isHostCallbackScheduled && !isPerformingWork) { 332 | isHostCallbackScheduled = true 333 | requestHostCallback(flushWork) 334 | } 335 | } 336 | 337 | return newTask 338 | } 339 | 340 | /** 341 | * 取消一个优先队列中的任务 342 | * @param task 要取消的任务 343 | */ 344 | const unstable_cancelCallback = (task: Task) => { 345 | /** 346 | * 要删除一个优先队列中的随机元素,需要O(N)的时间复杂度很不划算 347 | * 不如直接把他的callback直接赋值为null然后在pop出来的时候在判断一下 348 | * 是否因该忽略他就行 349 | */ 350 | task.callback = null 351 | } 352 | 353 | export { 354 | NormalPriority as unstable_NormalPriority, 355 | ImmediatePriority as unstable_ImmediatePriority, 356 | getCurrentTime as unstable_now, 357 | unstable_scheduleCallback, 358 | shouldYieldToHost as unstable_shouldYield, 359 | unstable_cancelCallback, 360 | } 361 | -------------------------------------------------------------------------------- /packages/react-dom/ReactDomHostConfig.ts: -------------------------------------------------------------------------------- 1 | import { DefaultEventPriority } from '../react-reconciler/ReactEventPriorities' 2 | import { Lane } from '../react-reconciler/ReactFiberLane' 3 | import { Fiber } from '../react-reconciler/ReactInternalTypes' 4 | import { registrationNameDependencies } from './events/EventRegistry' 5 | import { 6 | precacheFiberNode, 7 | updateFiberProps, 8 | } from './events/ReactDOMComponentTree' 9 | import { getEventPriority } from './events/ReactDOMEventListener' 10 | import { setInitialProperties, updateProperties } from './ReactDOMComponent' 11 | import { Container } from './ReactDomRoot' 12 | import { getHostProps as ReactDOMInputGetHostProps } from './ReactDOMInput' 13 | import { setTextContent } from './setTextContent' 14 | 15 | const STYLE = 'style' 16 | const CHILDREN = 'children' 17 | 18 | export type Props = { 19 | autoFocus?: boolean 20 | children?: unknown 21 | disabled?: boolean 22 | hidden?: boolean 23 | suppressHydrationWarning?: boolean 24 | dangerouslySetInnerHTML?: unknown 25 | style?: object & { display?: string } 26 | bottom?: null | number 27 | left?: null | number 28 | right?: null | number 29 | top?: null | number 30 | } 31 | 32 | export type UpdatePayload = unknown[] 33 | 34 | export type Type = string 35 | 36 | /** 37 | * 判断该节点是否可以直接将children当作直接文本节点处理 38 | * 比如节点的类型为textarea时,或者children的类型为string或者number 39 | * @param type 40 | * @param props 41 | * @returns 42 | */ 43 | export const shouldSetTextContent = (type: string, props: Props): boolean => { 44 | return ( 45 | type === 'textarea' || 46 | type === 'option' || 47 | type === 'noscript' || 48 | typeof props.children === 'string' || 49 | typeof props.children === 'number' || 50 | (typeof props.dangerouslySetInnerHTML === 'object' && 51 | props.dangerouslySetInnerHTML !== null && 52 | (props.dangerouslySetInnerHTML as any).__html !== null) 53 | ) 54 | } 55 | 56 | export const createInstance = ( 57 | type: string, 58 | props: Props, 59 | internalInstanceHandle: Fiber 60 | ) => { 61 | const domElement: Element = document.createElement(type) 62 | //todo 63 | //updateFiberProps(domElement, props) 64 | 65 | precacheFiberNode(internalInstanceHandle, domElement) 66 | updateFiberProps(domElement, props) 67 | 68 | return domElement 69 | } 70 | 71 | export const appendInitialChild = ( 72 | parentInstance: Element, 73 | child: Element | Text 74 | ) => { 75 | parentInstance.appendChild(child) 76 | } 77 | 78 | export const insertBefore = ( 79 | parentInstance: Element, 80 | child: Element, 81 | beforeChild: Element 82 | ): void => { 83 | parentInstance.insertBefore(child, beforeChild) 84 | } 85 | 86 | export const appendChild = (parentInstance: Element, child: Element): void => { 87 | parentInstance.appendChild(child) 88 | } 89 | 90 | const COMMENT_NODE = 8 91 | /** 92 | * 和appendChild一样,只是多了个判断是否是注释节点 93 | * @param container React.render第二个参数 94 | * @param child 要添加的dom 95 | * @param beforeChild 96 | */ 97 | export const insertInContainerBefore = ( 98 | container: Container, 99 | child: Element, 100 | beforeChild: Element 101 | ) => { 102 | if (container.nodeType === COMMENT_NODE) { 103 | container.parentNode?.insertBefore(child, beforeChild) 104 | } else { 105 | container.insertBefore(child, beforeChild) 106 | } 107 | } 108 | 109 | export const appendChildToContainer = ( 110 | container: Container, 111 | child: Element 112 | ): void => { 113 | let parentNode 114 | 115 | if (container.nodeType === COMMENT_NODE) { 116 | parentNode = container.parentNode 117 | parentNode?.insertBefore(child, container) 118 | } else { 119 | parentNode = container 120 | parentNode.appendChild(child) 121 | } 122 | } 123 | 124 | /** 125 | * 在首次mount时,为HostComponent初始化属性 126 | * @param domElement 要初始化的dom 127 | * @param type 128 | * @param props 新的props 129 | * @returns 130 | */ 131 | export const finalizeInitialChildren = ( 132 | domElement: Element, 133 | type: string, 134 | props: Props 135 | ): boolean => { 136 | setInitialProperties(domElement, type, props) 137 | 138 | //shouldAutoFocusHostComponent 139 | return false 140 | } 141 | 142 | export const createTextInstance = (text: string): Text => { 143 | const instance = document.createTextNode(text) 144 | 145 | return instance 146 | } 147 | 148 | export const scheduleMicrotask = queueMicrotask 149 | 150 | const diffProperties = ( 151 | domElement: Element, 152 | tag: string, 153 | lastRawProps: Record, 154 | nextRawProps: Record 155 | ): null | Array => { 156 | let updatePayload: null | any[] = [] 157 | 158 | let lastProps: Record 159 | let nextProps: Record 160 | 161 | switch (tag) { 162 | case 'input': 163 | lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps) 164 | nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps) 165 | updatePayload = [] 166 | break 167 | case 'option': 168 | case 'select': 169 | case 'textarea': 170 | throw new Error('Not Implement') 171 | default: { 172 | lastProps = lastRawProps 173 | nextProps = nextRawProps 174 | } 175 | } 176 | 177 | let propKey 178 | let styleName 179 | let styleUpdates: Record | null = null 180 | 181 | for (propKey in lastProps) { 182 | //该循环只处理被删除的prop 183 | if ( 184 | nextProps.hasOwnProperty(propKey) || 185 | (!lastProps.hasOwnProperty(propKey) && lastProps[propKey] == null) 186 | ) { 187 | continue 188 | } 189 | 190 | const nextProp = nextProps[propKey] 191 | if (propKey === STYLE) { 192 | throw new Error('Not Implement') 193 | } else if (propKey === CHILDREN) { 194 | } else { 195 | ;(updatePayload = updatePayload || []).push(propKey, null) 196 | } 197 | } 198 | 199 | for (propKey in nextProps) { 200 | //该循环会处理增加和被修改的属性 201 | const nextProp = nextProps[propKey] 202 | const lastProp = lastProps !== null ? lastProps[propKey] : undefined 203 | 204 | if ( 205 | !nextProps.hasOwnProperty(propKey) || 206 | nextProp === lastProp || 207 | (nextProp === null && lastProp === null) 208 | ) { 209 | continue 210 | } 211 | 212 | if (propKey === STYLE) { 213 | if (lastProp) { 214 | for (styleName in lastProp) { 215 | //处理删除的style 216 | if ( 217 | lastProp.hasOwnProperty(styleName) && 218 | (!nextProp || !nextProp.hasOwnProperty(styleName)) 219 | ) { 220 | if (!styleUpdates) { 221 | styleUpdates = {} 222 | } 223 | styleUpdates[styleName] = '' 224 | } 225 | } 226 | 227 | //处理新增或者更新的style 228 | for (styleName in nextProp) { 229 | if ( 230 | nextProp.hasOwnProperty(styleName) && 231 | lastProp[styleName] !== nextProp[styleName] 232 | ) { 233 | if (!styleUpdates) { 234 | styleUpdates = {} 235 | } 236 | styleUpdates[styleName] = nextProp[styleName] 237 | } 238 | } 239 | } else { 240 | if (!styleUpdates) { 241 | if (!updatePayload) { 242 | updatePayload = [] 243 | } 244 | updatePayload.push(propKey, styleUpdates) 245 | } 246 | styleUpdates = nextProp 247 | } 248 | } else if (registrationNameDependencies.hasOwnProperty(propKey)) { 249 | if (!updatePayload) updatePayload = [] 250 | } else if (propKey === CHILDREN) { 251 | //这里是直接文本节点能正常更新的关键,因为他们没有对应的fiber节点 252 | //所以不能靠打上Update标签这种形式去更新他自身的文本,他只能在 253 | //父节点的updateQueue(也就是这的updatePayload)中加上 children属性 254 | //待会该节点会更具updateQueue中children的新内容重新设置文本 255 | if (typeof nextProp === 'string' || typeof nextProp === 'number') { 256 | ;(updatePayload = updatePayload || []).push(propKey, '' + nextProp) 257 | } 258 | } else { 259 | ;(updatePayload = updatePayload || []).push(propKey, nextProp) 260 | } 261 | } 262 | 263 | if (styleUpdates) { 264 | ;(updatePayload = updatePayload || []).push(STYLE, styleUpdates) 265 | } 266 | 267 | return updatePayload 268 | } 269 | 270 | 271 | /** 272 | * 会返回类似这样的一个数组 ['style', {background: 'red'}, 'children', 'newText'] 273 | * 2n存储属性名,2n+1存储新的属性值 274 | * 该数组里面的属性都是dom真正拥有的属性, 275 | * 如果是类似于onClick这种react事件不会在数组中添加相关的属性,只会返回一个空数组 276 | * 待会更新的时候会判断到updateQueue不为null所以会进行该节点的更新流程 277 | * onClick的handler会通过updateFiberProps得到更新 278 | * @param domElement 279 | * @param type 280 | * @param oldProps 281 | * @param newProps 282 | * @returns 283 | */ 284 | export const prepareUpdate = ( 285 | domElement: Element, 286 | type: string, 287 | oldProps: Props, 288 | newProps: Props 289 | ): null | unknown[] => { 290 | return diffProperties(domElement, type, oldProps, newProps) 291 | } 292 | 293 | export const commitTextUpdate = ( 294 | textInstance: Text, 295 | oldText: string, 296 | newText: string 297 | ): void => { 298 | textInstance.nodeValue = newText 299 | } 300 | 301 | export const commitUpdate = ( 302 | domElement: Element, 303 | updatePayload: unknown[], 304 | type: string, 305 | oldProps: Props, 306 | newProps: Props, 307 | internalInstanceHandle: Object 308 | ): void => { 309 | /** 310 | * 更新fiber属性,ReactDOM事件系统能正常工作的关键 311 | * 比如如下代码 312 | * const Foo = () => { 313 | * const [count, setCount] = useState(0) 314 | * 315 | * return
{ 316 | * setCount(count + 1) 317 | * }}>{count}
318 | * } 319 | * 如果不更新props的话,ReactDOM中事件机制执行时 320 | * 从dom对应fiber提取到的onClick事件的handler将永远是首次mount时 321 | * 的handler,这意味着他闭包中捕获到的count值永远都是0,所以不管你点击多少次div 322 | * 他都等价于setCount(0 + 1),所以会导致不能正常更新 323 | * 而调用了下面的updateFiberProps就不一样了,每次更新后handler里面闭包捕获到的count 324 | * 都是最新值所以能正常更新 325 | */ 326 | updateFiberProps(domElement, newProps) 327 | updateProperties(domElement, updatePayload, type, oldProps, newProps) 328 | } 329 | 330 | /** 331 | * 更具当前的事件返回对应的优先级 332 | * @returns 333 | */ 334 | export const getCurrentEventPriority = (): Lane => { 335 | const currentEvent = window.event 336 | if (currentEvent === undefined) { 337 | return DefaultEventPriority 338 | } 339 | 340 | return getEventPriority(currentEvent.type as any) 341 | } 342 | 343 | export const removeChild = ( 344 | parentInstance: HTMLElement, 345 | child: HTMLElement | Text 346 | ) => { 347 | parentInstance.removeChild(child) 348 | } 349 | 350 | export const resetTextContent = (domElement: Element): void => { 351 | setTextContent(domElement, '') 352 | } 353 | -------------------------------------------------------------------------------- /docs/pre-requisite/dfs.md: -------------------------------------------------------------------------------- 1 | # DFS(深度优先遍历) 2 | 3 | DFS可能是React中使用最频繁的算法,在阅读源码前,请务必把下面的内容搞懂 4 | 5 | 假设有以下数据结构,我们的目的就是写一个函数来深度优先的遍历这种数据结构 6 | ```ts 7 | type TreeNode = { 8 | value: T 9 | children: TreeNode[] 10 | } 11 | ``` 12 | ## 第一版. 递归实现 13 | 由于递归自带dfs属性我们可以轻松写出以下代码 14 | ```ts 15 | const traverseRecursively = () => { 16 | for (let i = 0; i < root.children.length; ++i) { 17 | traverseRecursively(root.children[i], callback) 18 | } 19 | 20 | callback(root) 21 | } 22 | ``` 23 | ## 第二版. 迭代版本 24 | 实际上在React中所有dfs的实现都是基于实现迭代版本实现,那也不难我们可以使用栈这种数据结构 25 | ```ts 26 | const traverseIteratively = ( 27 | root: TreeNode, 28 | callback: (v: TreeNode) => void 29 | ): void => { 30 | const stack: Command[] = [] 31 | type Command = { 32 | type: 'GO' | 'PERFORM_WORK' 33 | node: TreeNode 34 | } 35 | 36 | stack.push({ 37 | type: 'GO', 38 | node: root, 39 | }) 40 | while (stack.length) { 41 | const { type, node } = stack.pop()! 42 | 43 | switch (type) { 44 | case 'GO': 45 | { 46 | //由于栈式先进后出的任务,所以越靠后的任务需要越先入栈 47 | stack.push({ 48 | node, 49 | type: 'PERFORM_WORK', 50 | }) 51 | 52 | for (let i = node.children.length - 1; i >= 0; --i) { 53 | stack.push({ 54 | type: 'GO', 55 | node: node.children[i], 56 | }) 57 | } 58 | } 59 | break 60 | case 'PERFORM_WORK': { 61 | callback(node) 62 | } 63 | } 64 | } 65 | } 66 | ``` 67 | ## 第三版. 迭代优化版本 68 | 上面的迭代版本好像除了在树的层级过深时不会报`Maximum call stack size exceeded`错误外和递归版本比一无是处,因为他的空间复杂度还是O(N),下面我们就实现一个O(1)空间复杂度的迭代版本的DFS,为了实现这个版本我们需要对上面的树节点的数据结构做一些改变,能让一个树节点知道他还有没有下一个兄弟节点,还有知道他的父节点是什么 69 | ```ts 70 | type Fiber = { 71 | /** 72 | * 节点上存储的值 73 | */ 74 | value: T 75 | /** 76 | * 他的下一个同级兄弟节点 77 | */ 78 | sibling: Fiber | null 79 | /** 80 | * 他的父节点 81 | */ 82 | return: Fiber | null 83 | /** 84 | * 他的第一个子节点,其他子节点可以通过fiber.child.sibling获得 85 | */ 86 | child: Fiber | null 87 | } 88 | ``` 89 | 然后根据的数据可以写出下面的代码 90 | ```ts 91 | const traverseIterativelyPlus = ( 92 | root: Fiber, 93 | callback: (v: Fiber) => void 94 | ): void => { 95 | let workInProgress: null | Fiber = root 96 | 97 | while (workInProgress !== null) { 98 | while (workInProgress.child !== null) { 99 | //只要还有子节点就继续向下走 100 | workInProgress = workInProgress.child 101 | continue 102 | } 103 | 104 | //已经到达最底层,该节点没有子节点了执行他的callback 105 | callback(workInProgress) 106 | 107 | //已经返回至最上层,直接退出函数 108 | if (workInProgress === root) return 109 | 110 | while (workInProgress.sibling === null) { 111 | if (workInProgress.return === null) return 112 | 113 | workInProgress = workInProgress.return 114 | //他的子节点已经全部完成工作了,现在执行他的callback 115 | callback(workInProgress) 116 | } 117 | 118 | workInProgress = workInProgress.sibling 119 | } 120 | } 121 | ``` 122 | 123 | ## 第四版. 迭代优化版本,并且区分出工作阶段 124 | 上面的版本既不会爆栈,也能把空间复杂度控制在O(1),但是他的callback只照顾到后序遍历的情况,即只有对所有子节点都调用callback后,才会执行父节点的callback,现在我们要将递归细分成两个阶段,一个为**递阶段**:此阶段处于第一次访问该节点,一个为**归阶段**:该阶段处于一个节点的所有子节点都完成了两个阶段又返回他,我们上面的代码只处理到了第二个阶段(归阶段),我们把**递阶段**命名为begin,**归阶段**命名为complete,所以可以写出下面的代码 125 | 126 | ```ts 127 | const traverseIterativelyPlusMax = ( 128 | root: Fiber, 129 | beginCallback: (v: Fiber) => void, 130 | completeCallback: (v: Fiber) => void 131 | ): void => { 132 | /** 133 | * 执行一个节点的递阶段 134 | * @param workInProgress 要执行递阶段的节点 135 | * @returns 下一个要执行递阶段的节点如果为null则表示要执行workInProgress 136 | * 的归阶段了 137 | */ 138 | const beginWork = (workInProgress: Fiber): Fiber | null => { 139 | //第一次访问该节点,执行他的begin回调 140 | beginCallback(workInProgress) 141 | 142 | //返回他的子节点,如果他存在子节点,就会开始该子节点的递阶段(begin phase) 143 | //如果不存在就会开始他的归阶段(complete phase) 144 | return workInProgress.child 145 | } 146 | 147 | /** 148 | * 执行一个节点的归阶段 149 | * @param workInProgress 要执行归阶段的节点 150 | */ 151 | const completeWork = (workInProgress: Fiber) => { 152 | completeCallback(workInProgress) 153 | } 154 | 155 | const workLoop = () => { 156 | let next: Fiber | null = null 157 | while (workInProgress !== null) { 158 | next = beginWork(workInProgress) 159 | 160 | if (next === null) { 161 | //workInProgress没有子节点了,该进行他的归阶段了 162 | 163 | let completedWork: Fiber | null = workInProgress 164 | do { 165 | completeWork(completedWork) 166 | 167 | //上面的节点已经完成了他的归阶段,该进行他同级兄弟节点的递阶段了 168 | //如果有的话 169 | const siblingFiber = completedWork.sibling 170 | 171 | if (siblingFiber !== null) { 172 | workInProgress = siblingFiber 173 | break 174 | } else { 175 | //他没有同级兄弟节点,意味着该进行他父节点的归阶段了 176 | completedWork = workInProgress.return! 177 | workInProgress = completedWork 178 | } 179 | } while (completedWork !== null) 180 | } else { 181 | workInProgress = next 182 | } 183 | } 184 | } 185 | 186 | let workInProgress: Fiber | null = root 187 | 188 | workLoop() 189 | } 190 | ``` 191 | 好了现在第四版中这个dfs就是React中workLoop的大体流程了,让我们为他们,写一些测试为了方便,我们可以实现一个将`TreeNode`转换为`Fiber`的函数,他的原理不用了解 192 | ```ts 193 | const transformTreeNodeToFiber = (treeNode: TreeNode): Fiber => { 194 | const ans: Fiber = { 195 | value: treeNode.value, 196 | child: null, 197 | sibling: null, 198 | return: null, 199 | } 200 | 201 | const dfs = (vertex1: TreeNode, vertex2: Fiber) => { 202 | if (vertex1.children.length === 0) return 203 | 204 | let currentTreeNode: TreeNode 205 | let currentFiber: Fiber | null = null 206 | 207 | for (let i = 0; i < vertex1.children.length; ++i) { 208 | currentTreeNode = vertex1.children[i] 209 | const newFiber: Fiber = { 210 | value: currentTreeNode.value, 211 | sibling: null, 212 | child: null, 213 | return: vertex2, 214 | } 215 | 216 | if (currentFiber === null) { 217 | //他是第一个节点,将他接到父节点的child中 218 | vertex2.child = newFiber 219 | } else { 220 | //不是第一个节点,将他接到前一个节点的sibling中 221 | currentFiber.sibling = newFiber 222 | } 223 | 224 | currentFiber = newFiber 225 | newFiber.return = vertex2 226 | 227 | dfs(currentTreeNode, currentFiber) 228 | } 229 | } 230 | 231 | dfs(treeNode, ans) 232 | 233 | return ans 234 | } 235 | ``` 236 | 237 | ```ts 238 | import * as assert from 'assert' 239 | 240 | const main = (): void => { 241 | const root1: TreeNode = { 242 | value: 4, 243 | children: [ 244 | { 245 | value: 2, 246 | children: [ 247 | { 248 | value: 1, 249 | children: [], 250 | }, 251 | ], 252 | }, 253 | { 254 | value: 3, 255 | children: [], 256 | }, 257 | ], 258 | } 259 | const root2: TreeNode = { 260 | value: 1, 261 | children: [], 262 | } 263 | const root3: TreeNode = { 264 | value: 'div', 265 | children: [ 266 | { 267 | value: 'ul', 268 | children: [ 269 | { value: 'li', children: [] }, 270 | { value: 'li', children: [] }, 271 | ], 272 | }, 273 | { 274 | value: '666', 275 | children: [], 276 | }, 277 | { 278 | value: 'p', 279 | children: [], 280 | }, 281 | ], 282 | } 283 | const fiber1 = transformTreeNodeToFiber(root1) 284 | const fiber2 = transformTreeNodeToFiber(root2) 285 | const fiber3 = transformTreeNodeToFiber(root3) 286 | 287 | const acutal: Array = [] 288 | 289 | //递归版dfs测试 290 | traverseRecursively(root1, (v) => { 291 | acutal.push(v.value) 292 | }) 293 | assert.deepStrictEqual(acutal, [1, 2, 3, 4]) 294 | 295 | acutal.length = 0 296 | traverseRecursively(root2, (v) => { 297 | acutal.push(v.value) 298 | }) 299 | assert.deepStrictEqual(acutal, [1]) 300 | 301 | acutal.length = 0 302 | traverseRecursively(root3, (v) => { 303 | acutal.push(v.value as any) 304 | }) 305 | assert.deepStrictEqual(acutal, ['li', 'li', 'ul', '666', 'p', 'div']) 306 | 307 | //迭代版dfs测试 308 | acutal.length = 0 309 | traverseIteratively(root1, (v) => { 310 | acutal.push(v.value) 311 | }) 312 | assert.deepStrictEqual(acutal, [1, 2, 3, 4]) 313 | 314 | acutal.length = 0 315 | traverseIteratively(root2, (v) => { 316 | acutal.push(v.value) 317 | }) 318 | assert.deepStrictEqual(acutal, [1]) 319 | 320 | acutal.length = 0 321 | traverseIteratively(root3, (v) => { 322 | acutal.push(v.value as any) 323 | }) 324 | assert.deepStrictEqual(acutal, ['li', 'li', 'ul', '666', 'p', 'div']) 325 | 326 | //优化空间复杂度迭代版dfs测试 327 | acutal.length = 0 328 | traverseIterativelyPlus(fiber1, (v) => { 329 | acutal.push(v.value) 330 | }) 331 | assert.deepStrictEqual(acutal, [1, 2, 3, 4]) 332 | 333 | acutal.length = 0 334 | traverseIterativelyPlus(fiber2, (v) => { 335 | acutal.push(v.value) 336 | }) 337 | assert.deepStrictEqual(acutal, [1]) 338 | 339 | acutal.length = 0 340 | traverseIterativelyPlus(fiber3, (v) => { 341 | acutal.push(v.value as any) 342 | }) 343 | assert.deepStrictEqual(acutal, ['li', 'li', 'ul', '666', 'p', 'div']) 344 | 345 | //优化空间复杂度,切分执行时期迭代版dfs测试 346 | acutal.length = 0 347 | traverseIterativelyPlusMax( 348 | fiber1, 349 | (v) => { 350 | acutal.push(v.value + '-begin') 351 | }, 352 | (v) => { 353 | acutal.push(v.value + '-complete') 354 | } 355 | ) 356 | assert.deepStrictEqual(acutal, [ 357 | '4-begin', 358 | '2-begin', 359 | '1-begin', 360 | '1-complete', 361 | '2-complete', 362 | '3-begin', 363 | '3-complete', 364 | '4-complete', 365 | ]) 366 | 367 | acutal.length = 0 368 | traverseIterativelyPlusMax( 369 | fiber2, 370 | (v) => { 371 | acutal.push(v.value + '-begin') 372 | }, 373 | (v) => { 374 | acutal.push(v.value + '-complete') 375 | } 376 | ) 377 | assert.deepStrictEqual(acutal, ['1-begin', '1-complete']) 378 | 379 | acutal.length = 0 380 | traverseIterativelyPlusMax( 381 | fiber3, 382 | (v) => { 383 | acutal.push(v.value + '-begin') 384 | }, 385 | (v) => { 386 | acutal.push(v.value + '-complete') 387 | } 388 | ) 389 | assert.deepStrictEqual(acutal, [ 390 | 'div-begin', 391 | 'ul-begin', 392 | 'li-begin', 393 | 'li-complete', 394 | 'li-begin', 395 | 'li-complete', 396 | 'ul-complete', 397 | '666-begin', 398 | '666-complete', 399 | 'p-begin', 400 | 'p-complete', 401 | 'div-complete', 402 | ]) 403 | } 404 | 405 | main() 406 | ``` -------------------------------------------------------------------------------- /packages/react-dom/events/DOMPluginEventSystem.ts: -------------------------------------------------------------------------------- 1 | import { Fiber } from '../../react-reconciler/ReactInternalTypes' 2 | import { HostComponent } from '../../react-reconciler/ReactWorkTags' 3 | import { DOMEventName } from './DOMEventNames' 4 | import { 5 | addEventBubbleListener, 6 | addEventBubbleListenerWithPassiveFlag, 7 | addEventCaptureListener, 8 | addEventCaptureListenerWithPassiveFlag, 9 | } from './EventListener' 10 | import { allNativeEvents } from './EventRegistry' 11 | import { 12 | EventSystemFlags, 13 | IS_CAPTURE_PHASE, 14 | SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS, 15 | } from './EventSystemFlags' 16 | import { getEventTarget } from './getEventTarget' 17 | import { getListener } from './getListener' 18 | import { AnyNativeEvent } from './PluginModuleType' 19 | import * as SimpleEventPlugin from './plugins/SimpleEventPlugin' 20 | import * as ChangeEventPlugin from './plugins/ChangeEventPlugin' 21 | import { createEventListenerWrapperWithPriority } from './ReactDOMEventListener' 22 | import { ReactSyntheticEvent } from './ReactSyntheticEventType' 23 | import { batchedEventUpdates } from '../../react-reconciler/ReactFiberReconciler' 24 | 25 | const listeningMarker = '_reactListening' + Math.random().toString(36).slice(2) 26 | 27 | type DispatchListener = { 28 | instance: Fiber | null 29 | listener: Function 30 | currentTarget: EventTarget 31 | } 32 | type DispatchEntry = { 33 | event: ReactSyntheticEvent 34 | listeners: DispatchListener[] 35 | } 36 | 37 | export type DispatchQueue = DispatchEntry[] 38 | 39 | SimpleEventPlugin.registerEvents() 40 | ChangeEventPlugin.registerEvents() 41 | 42 | /** 43 | * 我们不因该在container代理这些事件,而是因该把他们添加到真正的目标dom上 44 | * 主要是因为这些事件的冒泡不具有一致性 45 | */ 46 | export const nonDelegatedEvents: Set = new Set([ 47 | 'cancel' as DOMEventName, 48 | 'close' as DOMEventName, 49 | 'invalid' as DOMEventName, 50 | 'load' as DOMEventName, 51 | 'scroll' as DOMEventName, 52 | 'toggle' as DOMEventName, 53 | ]) 54 | 55 | const addTrappedEventListener = ( 56 | targetContainer: EventTarget, 57 | domEventName: DOMEventName, 58 | eventSystemFlags: EventSystemFlags, 59 | isCapturePhaseListener: boolean 60 | ) => { 61 | const listener = createEventListenerWrapperWithPriority( 62 | targetContainer, 63 | domEventName, 64 | eventSystemFlags 65 | ) 66 | 67 | let isPassiveListener: undefined | boolean = undefined 68 | 69 | if ( 70 | domEventName === 'wheel' || 71 | domEventName === 'touchmove' || 72 | domEventName === 'touchstart' 73 | ) { 74 | isPassiveListener = true 75 | } 76 | 77 | let unsubscribeListener 78 | 79 | if (isCapturePhaseListener) { 80 | if (isPassiveListener !== undefined) { 81 | unsubscribeListener = addEventCaptureListenerWithPassiveFlag( 82 | targetContainer, 83 | domEventName, 84 | listener, 85 | isPassiveListener 86 | ) 87 | } else { 88 | unsubscribeListener = addEventCaptureListener( 89 | targetContainer, 90 | domEventName, 91 | listener 92 | ) 93 | } 94 | } else { 95 | if (isPassiveListener !== undefined) { 96 | unsubscribeListener = addEventBubbleListenerWithPassiveFlag( 97 | targetContainer, 98 | domEventName, 99 | listener, 100 | isPassiveListener 101 | ) 102 | } else { 103 | unsubscribeListener = addEventBubbleListener( 104 | targetContainer, 105 | domEventName, 106 | listener 107 | ) 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * 在EventTarget注册一个事件 114 | * @param domEventName 事件名称 115 | * @param isCapturePhaseListener 是否为捕获阶段的事件 116 | * @param target container 117 | */ 118 | const listenToNativeEvent = ( 119 | domEventName: DOMEventName, 120 | isCapturePhaseListener: boolean, 121 | target: EventTarget 122 | ) => { 123 | let eventSystemFlags = 0 124 | 125 | if (isCapturePhaseListener) { 126 | eventSystemFlags |= IS_CAPTURE_PHASE 127 | } 128 | 129 | addTrappedEventListener( 130 | target, 131 | domEventName, 132 | eventSystemFlags, 133 | isCapturePhaseListener 134 | ) 135 | } 136 | 137 | /** 138 | * 将所有支持的事件在container上全都注册上 139 | * @param rootContainerElement container 140 | */ 141 | export const listenToAllSupportedEvents = ( 142 | rootContainerElement: EventTarget 143 | ) => { 144 | if (!(rootContainerElement as any)[listeningMarker]) { 145 | allNativeEvents.forEach((domEventName) => { 146 | /** 147 | * 单独处理selectionchange因为他不会冒泡,而且需要设置在document上 148 | */ 149 | if (domEventName !== 'selectionchange') { 150 | if (!nonDelegatedEvents.has(domEventName)) { 151 | listenToNativeEvent(domEventName, false, rootContainerElement) 152 | } 153 | 154 | listenToNativeEvent(domEventName, true, rootContainerElement) 155 | } 156 | }) 157 | } 158 | } 159 | 160 | const extractEvents = ( 161 | dispatchQueue: DispatchQueue, 162 | domEventName: DOMEventName, 163 | targetInst: null | Fiber, 164 | nativeEvent: AnyNativeEvent, 165 | nativeEventTarget: null | EventTarget, 166 | eventSystemFlags: EventSystemFlags, 167 | targetContainer: EventTarget 168 | ) => { 169 | SimpleEventPlugin.extractEvents( 170 | dispatchQueue, 171 | domEventName, 172 | targetInst, 173 | nativeEvent, 174 | nativeEventTarget, 175 | eventSystemFlags, 176 | targetContainer 177 | ) 178 | 179 | const shouldProcessPolyfillPlugins = 180 | (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0 181 | 182 | if (shouldProcessPolyfillPlugins) { 183 | ChangeEventPlugin.extractEvents( 184 | dispatchQueue, 185 | domEventName, 186 | targetInst, 187 | nativeEvent, 188 | nativeEventTarget, 189 | eventSystemFlags, 190 | targetContainer 191 | ) 192 | } 193 | } 194 | 195 | const createDispatchListener = ( 196 | instance: Fiber | null, 197 | listener: Function, 198 | currentTarget: EventTarget 199 | ): DispatchListener => { 200 | return { 201 | instance, 202 | listener, 203 | currentTarget, 204 | } 205 | } 206 | 207 | export const accumulateTwoPhaseListeners = ( 208 | targetFiber: Fiber | null, 209 | reactName: string 210 | ): DispatchListener[] => { 211 | const captureName = reactName + 'Capture' 212 | const listeners: Array = [] 213 | let instance = targetFiber 214 | 215 | while (instance !== null) { 216 | const { stateNode, tag } = instance 217 | 218 | if (tag === HostComponent && stateNode !== null) { 219 | const currentTarget = stateNode 220 | const captureListener = getListener(instance, captureName) 221 | 222 | if (captureListener !== null) { 223 | listeners.unshift( 224 | createDispatchListener(instance, captureListener, currentTarget) 225 | ) 226 | } 227 | 228 | const bubbleListener = getListener(instance, reactName) 229 | if (bubbleListener !== null) { 230 | listeners.push( 231 | createDispatchListener(instance, bubbleListener, currentTarget) 232 | ) 233 | } 234 | } 235 | 236 | instance = instance.return 237 | } 238 | 239 | return listeners 240 | } 241 | 242 | export const accumulateSinglePhaseListeners = ( 243 | targetFiber: Fiber | null, 244 | reactName: string | null, 245 | inCapturePhase: boolean, 246 | accumulateTargetOnly: boolean 247 | ) => { 248 | const captureName = reactName !== null ? reactName + 'Capture' : null 249 | const reactEventName = inCapturePhase ? captureName : reactName 250 | let listeners: DispatchListener[] = [] 251 | 252 | let instance = targetFiber 253 | let lastHostComponent = null 254 | 255 | while (instance !== null) { 256 | const { tag, stateNode } = instance 257 | 258 | if (tag === HostComponent && stateNode !== null) { 259 | lastHostComponent = stateNode 260 | if (reactEventName !== null) { 261 | const listener = getListener(instance, reactEventName) 262 | 263 | if (listener !== null) { 264 | listeners.push( 265 | createDispatchListener(instance, listener, lastHostComponent) 266 | ) 267 | } 268 | } 269 | } 270 | 271 | if (accumulateTargetOnly) break 272 | 273 | instance = instance.return 274 | } 275 | 276 | return listeners 277 | } 278 | 279 | const executeDispatch = ( 280 | event: ReactSyntheticEvent, 281 | listener: Function, 282 | currentTarget: EventTarget 283 | ): void => { 284 | listener(event) 285 | } 286 | 287 | const processDispatchQueueItemsInOrder = ( 288 | event: ReactSyntheticEvent, 289 | dispatchListeners: DispatchListener[], 290 | inCapturePhase: boolean 291 | ): void => { 292 | if (inCapturePhase) { 293 | for (let i = dispatchListeners.length - 1; i >= 0; --i) { 294 | const { instance, currentTarget, listener } = dispatchListeners[i] 295 | //todo isPropagationStopped 296 | executeDispatch(event, listener, currentTarget) 297 | } 298 | } else { 299 | for (let i = 0; i < dispatchListeners.length; ++i) { 300 | const { instance, currentTarget, listener } = dispatchListeners[i] 301 | //todo isPropagationStopped 302 | executeDispatch(event, listener, currentTarget) 303 | } 304 | } 305 | } 306 | 307 | export const processDispatchQueue = ( 308 | dispatchQueue: DispatchQueue, 309 | eventSystemFlags: EventSystemFlags 310 | ): void => { 311 | const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0 312 | for (let i = 0; i < dispatchQueue.length; ++i) { 313 | const { event, listeners } = dispatchQueue[i] 314 | 315 | processDispatchQueueItemsInOrder(event, listeners, inCapturePhase) 316 | } 317 | } 318 | 319 | const dispatchEventsForPlugins = ( 320 | domEventName: DOMEventName, 321 | eventSystemFlags: EventSystemFlags, 322 | nativeEvent: AnyNativeEvent, 323 | targetInst: null | Fiber, 324 | targetContainer: EventTarget 325 | ) => { 326 | const nativeEventTarget = getEventTarget(nativeEvent) 327 | 328 | const dispatchQueue: DispatchQueue = [] 329 | 330 | extractEvents( 331 | dispatchQueue, 332 | domEventName, 333 | targetInst, 334 | nativeEvent, 335 | nativeEventTarget, 336 | eventSystemFlags, 337 | targetContainer 338 | ) 339 | processDispatchQueue(dispatchQueue, eventSystemFlags) 340 | } 341 | 342 | export const dispatchEventForPluginEventSystem = ( 343 | domEventName: DOMEventName, 344 | eventSystemFlags: EventSystemFlags, 345 | nativeEvent: AnyNativeEvent, 346 | targetInst: null | Fiber, 347 | targetContainer: EventTarget 348 | ) => { 349 | const ancestorInst = targetInst 350 | batchedEventUpdates( 351 | () => 352 | dispatchEventsForPlugins( 353 | domEventName, 354 | eventSystemFlags, 355 | nativeEvent, 356 | ancestorInst, 357 | targetContainer 358 | ), 359 | null, 360 | ) 361 | } 362 | -------------------------------------------------------------------------------- /packages/react-reconciler/ReactFiberBeginWork.ts: -------------------------------------------------------------------------------- 1 | import { shouldSetTextContent } from './ReactFiberHostConfig' 2 | import { 3 | cloneChildFibers, 4 | mountChildFibers, 5 | reconcileChildFibers, 6 | } from './ReactChildFiber' 7 | import { bailoutHooks, renderWithHooks } from './ReactFiberHooks' 8 | import { Fiber } from './ReactInternalTypes' 9 | import { cloneUpdateQueue, processUpdateQueue } from './ReactUpdateQueue' 10 | import { 11 | FunctionComponent, 12 | HostComponent, 13 | HostRoot, 14 | HostText, 15 | IndeterminateComponent, 16 | MemoComponent, 17 | SimpleMemoComponent, 18 | } from './ReactWorkTags' 19 | import { includesSomeLane, Lanes, NoLanes } from './ReactFiberLane' 20 | import { ContentReset } from './ReactFiberFlags' 21 | import { 22 | createFiberFromTypeAndProps, 23 | createWorkInProgress, 24 | isSimpleFunctionComponent, 25 | } from './ReactFiber' 26 | import { shallowEqual } from '../shared/shallowEqual' 27 | 28 | let didReceiveUpdate = false 29 | 30 | const updateFunctionComponent = ( 31 | current: Fiber | null, 32 | workInProgress: Fiber, 33 | Component: Function, 34 | nextProps: any, 35 | renderLanes: Lanes 36 | ): Fiber | null => { 37 | const nextChildren = renderWithHooks( 38 | current, 39 | workInProgress, 40 | Component as any, 41 | nextProps, 42 | null, 43 | renderLanes 44 | ) 45 | 46 | if (current !== null && !didReceiveUpdate) { 47 | bailoutHooks(current, workInProgress, renderLanes) 48 | return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) 49 | } 50 | 51 | reconcileChildren(current, workInProgress, nextChildren, renderLanes) 52 | return workInProgress.child 53 | } 54 | 55 | /** 56 | * 优化路径,该fiber节点没有要进行的工作,看看他的子树有没有工作要做,如果 57 | * 有就返回子节点继续子节点的render过程,如果没有就直接返回null,此时以workInProgress 58 | * 为根的fiber子树的render过程就直接完成了 59 | * @param current 60 | * @param workInProgress 61 | * @param renderLanes 此次render的优先级 62 | * @returns 63 | */ 64 | const bailoutOnAlreadyFinishedWork = ( 65 | current: Fiber | null, 66 | workInProgress: Fiber, 67 | renderLanes: Lanes 68 | ): Fiber | null => { 69 | //检查该节点的children是否存在待进行的工作 70 | if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { 71 | /** 72 | * children也没有待进行的工作,我们可以直接跳过他们的render工作 73 | */ 74 | return null 75 | } 76 | 77 | //该节点没有工作,但是他的子节点有,从current Fiber树中克隆他的子节点,然后继续 78 | cloneChildFibers(current, workInProgress) 79 | return workInProgress.child 80 | } 81 | 82 | /** 83 | * 更新HostRoot节点,此函数只会在首次渲染时使用 84 | * 其他情况下HostRoot走的都是bailout逻辑 85 | * @param current 86 | * @param workInProgress 87 | * @returns 88 | */ 89 | const updateHostRoot = ( 90 | current: Fiber, 91 | workInProgress: Fiber, 92 | renderLanes: Lanes 93 | ) => { 94 | cloneUpdateQueue(current, workInProgress) 95 | //当第一次mount时payload为 {element: jsx对象} 96 | const prevState = workInProgress.memoizedState 97 | const prevChildren = prevState !== null ? prevState.element : null 98 | //HostRoot的pendingProps为null 99 | const nextProps = workInProgress.pendingProps 100 | processUpdateQueue(workInProgress, nextProps, null) 101 | const nextState = workInProgress.memoizedState 102 | 103 | const nextChildren = nextState.element 104 | 105 | if (nextChildren === prevChildren) { 106 | //todo 前后jsx对象没有变 107 | return null 108 | } 109 | 110 | reconcileChildren(current, workInProgress, nextChildren, renderLanes) 111 | 112 | return workInProgress.child 113 | } 114 | 115 | const reconcileChildren = ( 116 | current: Fiber | null, 117 | workInProgress: Fiber, 118 | nextChildren: any, 119 | renderLanes: Lanes 120 | ) => { 121 | if (current === null) { 122 | workInProgress.child = mountChildFibers( 123 | workInProgress, 124 | null, 125 | nextChildren, 126 | renderLanes 127 | ) 128 | } else { 129 | workInProgress.child = reconcileChildFibers( 130 | workInProgress, 131 | current.child, 132 | nextChildren, 133 | renderLanes 134 | ) 135 | } 136 | } 137 | 138 | /** 139 | * 因为函数组件的fiber在创建时会被赋值为IndeterminateComponent 140 | * 所以首次渲染时Function组件会走这个逻辑 141 | * 详细逻辑可以看 react-reconciler\ReactFiber.ts下的 142 | * createFiberFromTypeAndProps函数 143 | * @param current 144 | * @param workInProgress 145 | * @param Component 函数组件 146 | * @param renderLanes 147 | * @returns 148 | */ 149 | const mountIndeterminateComponent = ( 150 | current: Fiber | null, 151 | workInProgress: Fiber, 152 | Component: any, 153 | renderLanes: Lanes 154 | ): Fiber | null => { 155 | const props = workInProgress.pendingProps 156 | //value为该Function Component返回的JSX对象 157 | const value = renderWithHooks( 158 | current, 159 | workInProgress, 160 | Component, 161 | props, 162 | null, 163 | renderLanes 164 | ) 165 | 166 | workInProgress.tag = FunctionComponent 167 | reconcileChildren(null, workInProgress, value, renderLanes) 168 | 169 | return workInProgress.child 170 | } 171 | 172 | const updateHostComponent = ( 173 | current: Fiber | null, 174 | workInProgress: Fiber, 175 | renderLanes: Lanes 176 | ) => { 177 | const type = workInProgress.type 178 | const nextProps = workInProgress.pendingProps 179 | const prevProps = current !== null ? current.memoizedProps : null 180 | 181 | let nextChildren = nextProps.children 182 | //子节点是否可以直接设置成字符串而不用继续reconcile 183 | const isDirectTextChild = shouldSetTextContent(type, nextProps) 184 | 185 | if (isDirectTextChild) { 186 | /** 187 | * 我们把子节点为文本这种情况特别处理,这是一种非常常见的情况 188 | * 在这不会为该文本创建实际的fiber节点而是只把他放到props.children 189 | * 待会更新props时会直接setTextContent把他设置到dom上,以避免还要创建 190 | * 一个fiber节点,并遍历他 191 | * 注意只有
sdfsd dsfsd
,或者
{1}
这种才算时 192 | * 直接文本子节点
{1}{2}
这种children类型是数组其中1,和2都会 193 | * 创建一个fiber节点与之对应,更多例子可以上[https://babeljs.io/repl/] 194 | * 自行把玩 195 | */ 196 | nextChildren = null 197 | } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) { 198 | /** 199 | * 此次更新时,当前需要被替换的节点一个单纯的文本节点,他没有对应的fiber节点 200 | * 所以不能靠reconcile过程把他删除,所以我们在这直接把他的父节点打上ContentReset 201 | * 标签待会commit阶段的时候它会被`textContent = ''`删除,这样他就能正常的被新内容替换,否则他将不会被清除一直存在在 202 | * 他的父节点上 203 | */ 204 | workInProgress.flags |= ContentReset 205 | } 206 | 207 | reconcileChildren(current, workInProgress, nextChildren, renderLanes) 208 | return workInProgress.child 209 | } 210 | 211 | const updateSimpleMemoComponent = ( 212 | current: Fiber | null, 213 | workInProgress: Fiber, 214 | Component: any, 215 | nextProps: any, 216 | updateLanes: Lanes, 217 | renderLanes: Lanes 218 | ): null | Fiber => { 219 | if (current !== null) { 220 | const prevProps = current.memoizedProps 221 | 222 | if (shallowEqual(prevProps, nextProps)) { 223 | didReceiveUpdate = false 224 | if (!includesSomeLane(renderLanes, updateLanes)) { 225 | //在beginWork中workInProgress pending中的lanes会被置为 226 | //NoLanes,进入该逻辑表明,这轮render以workInProgress为根的子树没有工作要做 227 | //但是可能他下一轮render可能有工作要做, 228 | //为了保证它pending中的工作能在下一轮render中,能被正常的执行 229 | //需要在这里将他current节点里的lanes赋值给workInProgress,以确保他待会 230 | //他pending中的lanes会在completeWork中被冒泡到root上 231 | /** 232 | * 考虑以下代码 233 | * let hasDispatched = false 234 | * const Foo = memo(() => { 235 | * const [list, setList] = useState([]) 236 | * 237 | * setTimeout(() => { 238 | * if (hasDispatched) return 239 | * 240 | * hasDispatched = true 241 | * setList(Array.from({ length: 1e4 }, (_, i) => i)) 242 | * }, 1000) 243 | * 244 | * return ( 245 | *
246 | * {list.map((v) => ( 247 | *
{v}
248 | * ))} 249 | *
250 | * ) 251 | * }) 252 | * 253 | * const App = () => { 254 | * const [count, setCount] = useState(0) 255 | * 256 | * useEffect(() => { 257 | * setTimeout(() => { 258 | * const dispatcher = document.getElementById('dispatcher') 259 | * dispatcher?.click() 260 | * }, 1030) 261 | * }, []) 262 | * return ( 263 | *
264 | * 272 | * 273 | *
274 | * ) 275 | * } 276 | * 当进行Foo组件的渲染时,它会被App组件内产生的更高的优先级的更新打断, 277 | * 所以会先开始以renderLanes为1开始一轮更新,而此时Foo组件的UpdateLanes为 278 | * 16,如果没有memo组件的情况下他不会提前bailout,而会继续render过程 279 | * 在执行updateReducer时处理他updateQueue上因优先级不足而被跳过的update而被打上相应的lanes 280 | * 而现在他被包在memo里面所以他会进入这里的逻辑, 281 | * 而我们在这里提前进行bailout就得手动设置他workInProgress上的lanes 282 | * 如果我们运行上面的代码,并且没有下面这行的代码的话,Foo组件内产生的更新就会好像消失了一样 283 | */ 284 | workInProgress.lanes = current.lanes 285 | return bailoutOnAlreadyFinishedWork( 286 | current, 287 | workInProgress, 288 | renderLanes 289 | ) 290 | } 291 | } 292 | } 293 | 294 | return updateFunctionComponent( 295 | current, 296 | workInProgress, 297 | Component, 298 | nextProps, 299 | renderLanes 300 | ) 301 | } 302 | 303 | const updateMemoComponent = ( 304 | current: Fiber | null, 305 | workInProgress: Fiber, 306 | Component: any, 307 | nextProps: any, 308 | updateLanes: Lanes, 309 | renderLanes: Lanes 310 | ): null | Fiber => { 311 | if (current === null) { 312 | const type = Component.type 313 | if ( 314 | isSimpleFunctionComponent(type) && 315 | Component.compare === null && 316 | Component.defaultProps === undefined 317 | ) { 318 | let resolvedType = type 319 | 320 | workInProgress.tag = SimpleMemoComponent 321 | workInProgress.type = resolvedType 322 | 323 | return updateSimpleMemoComponent( 324 | current, 325 | workInProgress, 326 | resolvedType, 327 | nextProps, 328 | updateLanes, 329 | renderLanes 330 | ) 331 | } 332 | 333 | const child = createFiberFromTypeAndProps( 334 | Component.type, 335 | null, 336 | nextProps, 337 | workInProgress.mode, 338 | renderLanes 339 | ) 340 | 341 | child.return = workInProgress 342 | workInProgress.child = child 343 | return child 344 | } 345 | 346 | const currentChild = current.child as Fiber 347 | 348 | if (!includesSomeLane(updateLanes, renderLanes)) { 349 | const prevProps = currentChild.memoizedProps 350 | let compare = Component.compare 351 | compare = compare !== null ? compare : shallowEqual 352 | if (compare(prevProps, nextProps)) { 353 | return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) 354 | } 355 | } 356 | 357 | const newChild = createWorkInProgress(currentChild, nextProps) 358 | newChild.return = workInProgress 359 | workInProgress.child = newChild 360 | 361 | return newChild 362 | } 363 | 364 | /** 365 | * 传入当前Fiber节点,创建子Fiber节点 366 | * @param current 当前节点 367 | * @param workInProgress workInProgress节点 368 | * @returns 下一个要进行beginWork的节点 369 | */ 370 | export const beginWork = ( 371 | current: Fiber | null, 372 | workInProgress: Fiber, 373 | renderLanes: Lanes 374 | ): Fiber | null => { 375 | const updateLanes = workInProgress.lanes 376 | 377 | //当页面第一次渲染时current fiber树除了HostRoot(也就是FiberRoot.current)节点其他都还未创建, 378 | //workInPgress树中的HostRoot(FiberRoot.current.alternate)也在prepareFreshStack函数中被创建 379 | if (current !== null) { 380 | const oldProps = current.memoizedProps 381 | const newProps = workInProgress.pendingProps 382 | 383 | if (oldProps !== newProps) { 384 | //如果props改变了标记这个fiber需要进行工作 385 | didReceiveUpdate = true 386 | } else if (!includesSomeLane(renderLanes, updateLanes)) { 387 | didReceiveUpdate = false 388 | //这个fiber没有要进行的工作,执行其bailout逻辑,而不用继续 389 | //begin他的阶段 390 | switch (workInProgress.tag) { 391 | case HostRoot: 392 | break 393 | case HostComponent: 394 | break 395 | case HostText: 396 | break 397 | case FunctionComponent: 398 | break 399 | case SimpleMemoComponent: 400 | break 401 | default: { 402 | throw new Error('Not Implement') 403 | } 404 | } 405 | 406 | return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) 407 | } 408 | } else { 409 | //current不存在 410 | didReceiveUpdate = false 411 | } 412 | 413 | //在进入begin流程前,先清除workInProgress pending中的lanes,否则会导致HostRoot不能进入bailout逻辑, 414 | //导致后续的更新不会触发,还会导致root上的pendingLanes一直不为空 415 | //会让performConcurrentWorkOnRoot一直被schedule下去 416 | workInProgress.lanes = NoLanes 417 | 418 | switch (workInProgress.tag) { 419 | case IndeterminateComponent: { 420 | //在mount时FunctionComponent是按indeterminate处理的 421 | return mountIndeterminateComponent( 422 | current, 423 | workInProgress, 424 | workInProgress.type, 425 | renderLanes 426 | ) 427 | } 428 | case FunctionComponent: { 429 | const Component = workInProgress.type 430 | const unresolvedProps = workInProgress.pendingProps 431 | const resolvedProps = unresolvedProps 432 | return updateFunctionComponent( 433 | current!, 434 | workInProgress, 435 | Component, 436 | resolvedProps, 437 | renderLanes 438 | ) 439 | } 440 | case HostRoot: { 441 | //HostRoot类型current,workInProgress一定会同时存在 442 | return updateHostRoot(current!, workInProgress, renderLanes) 443 | } 444 | case HostComponent: 445 | return updateHostComponent(current, workInProgress, renderLanes) 446 | case HostText: 447 | return null 448 | case MemoComponent: { 449 | const type = workInProgress.type 450 | const unresolvedProps = workInProgress.pendingProps 451 | 452 | return updateMemoComponent( 453 | current, 454 | workInProgress, 455 | type, 456 | unresolvedProps, 457 | updateLanes, 458 | renderLanes 459 | ) 460 | } 461 | case SimpleMemoComponent: { 462 | return updateSimpleMemoComponent( 463 | current, 464 | workInProgress, 465 | workInProgress.type, 466 | workInProgress.pendingProps, 467 | updateLanes, 468 | renderLanes 469 | ) 470 | } 471 | } 472 | 473 | throw new Error('Not Implement') 474 | } 475 | 476 | export const markWorkInProgressReceivedUpdate = () => { 477 | didReceiveUpdate = true 478 | } 479 | --------------------------------------------------------------------------------