工程化配置 momorepo 简介 由于我们React
库中有React-dom
库,React-native
库等等等,为了管理这些工程文件库可以使用momorepo
来进行版本控制和管理
momorepo: 一种项目库的管理方式 可以很方便的协同管理不同独立的库的生命周期
一些简单的工具:npm-workspace、Yarn-workspace、pnpm-workspace 专业工具:nx、bit、turborepo、rush、lerna 包管理器 选择 pnpm 作为我们项目的包管理器
pnpm 初始化
1 2 npm install -g pnpm pnpm init
pnpm 实验 momorepo:建立 pnpm-workspace.yaml
文件去书写配置
1 2 3 4 5 6 7 packages: - 'packages/*' - 'components/**' - '!**/test/**'
定义开发规范 代码规范检查和修复 Commit 规范 安装husky
用于拦截 commit 命令
初始化 husky
将刚才执行的pnpm lint
纳入 commit 时将 husky 将执行的脚本
1 npx husky add .husky/pre-commit "pnpm lint"
通过commitlint
对 git 提交信息进行检查 首先安装必要的库
1 pnpm i -D -w @commitlint/cli @commitlint/config-conventional -D -w
新建配置文件.commitlintrc.js
1 2 3 module .exports = { extends : ['@commitlint/config-conventional' ] }
集成到 husky 中
1 npx husky add .husky/commit-msg "npx --no-install commitlint -e $HUSKY_GIT_PARAMS "
conventional 规规集意义
1 2 // 提交的类型:摘要信息 <type >: <subject>
常见的type值
feat
: 添加新功能fix
: 修复 bugchore
: 一些不影响功能的更改docs
: 文档的修改perf
:性能方面的优化refactor
: 代码重构test
: 添加一些测试代码配置 tsconfig.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "compileOnSave" : true , "compilerOptions" : { "target" : "ESNext" , "useDefineForClassFields" : true , "module" : "ESNext" , "lib" : [ "ESNext" , "DOM" ] , "moduleResolution" : "Node" , "strict" : true , "sourceMap" : true , "resolveJsonModule" : true , "isolatedModules" : true , "esModuleInterop" : true , "noEmit" : true , "noUnusedLocals" : true , "noUnusedParameters" : true , "noImplicitReturns" : false , "skipLibCheck" : true , "baseUrl" : "./packages" } , "exclude" : [ "node_modules" , "packages/dist/**/*" ] }
选择构建工具 rollup
我们项目是库 而不是业务代码 希望工具尽可能简洁 打包产物可读性更高 安装 rollup
下面我们先写一段代码在看如何配置打包工具
JSX 转换 React 中项目结构:
react (宿主环境无关的公用方法) react-reconciler (协调器的实现 宿主环境无关) 各种宿主环境的包 shared (公用辅助方法 宿主环境无关) JSX 转换是什么 包括两部分:
编译时:(babel 有专门的 parser 去解析 jsx 语言拓展) 运行时:jsx 方法或 React.createElement 方法的实现(包括 dev、prod) 编译时由 babel 编译实现 我们只用实现运行时 工作量包括
实现 jsx 方法 实现打包流程 React.createElement 方法 React 中的 JSX 初步实现
首先将 packages/* 目录下面分成两个工程 react
和shared
并对应初始化,shared 下面是一些共享的方法和函数 react 中需要引入 shared 则要在 package.json 中存在安装
1 2 3 4 5 { "dependencies" : { "shared" : "workspace:*" } }
在react/index.ts
1 2 3 4 5 import { jsxDEV } from './src/jsx' export default { version : '0.0.0' , createElement : jsxDEV }
在react/src/jsx.ts
中实现 jsx 的具体方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols' import type { Type , Key , Ref , Props , ElementType , ReactElementType } from 'shared/ReactTypes' export const ReactElement = function ( type : Type, key: Key, ref: Ref, props: Props ): ReactElementType { const element = { $$typeof : REACT_ELEMENT_TYPE , type , key, ref, props, __mark : 'yueyun' } return element } export const jsxDEV = function ( type : ElementType, config: Props ) { let key : Key = null let ref : Ref = null const props : Props = {} for (const propName in config) { const val = config[propName] if (propName === 'key' ) { if (val !== undefined ) { key = '' + val } continue } if (propName === 'ref' ) { if (val !== undefined ) { ref = val } continue } if ({}.hasOwnProperty .call (config, propName)) { props[propName] = val } } return ReactElement (type , key, ref, props) }
在shared/ReactTypes.ts
中定义
1 2 3 4 5 6 7 8 9 10 11 12 13 export type Type = any export type Props = any export type Key = any export type Ref = any export type ElementType = any export interface ReactElementType { $$typeof : symbol | number type : ElementType key : Key | null ref : Ref | null props : Props __mark : string }
在ReactSybols.ts
中定义
1 2 3 4 5 const supporSymbol = typeof Symbol === 'function' && Symbol .for export const REACT_ELEMENT_TYPE = supporSymbol ? Symbol .for ('react.element' ) : 0xeac7
实现打包流程 实现 jsx 方法
jsxDEV 方法(dev 环境) jsx 方法 (prod 环境) React.createElement 方法 对应上述方法打包文件
react/jsx-dev-runtime.js (dev 环境) react/jsx-runtime.js (prod 环境) React 现在项目根目录下面新建scripts
文件夹
新建工具scripts/utils.js
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import path from 'path' import fs from 'fs' import ts from 'rollup-plugin-typescritp2' import cjs from '@rollup/plugin-commonjs' const pkgPath = path.resolve (__dirname, '../../packages' )const distPath = path.resole (__dirname, '../../dist/node_modules' )export function resolvePkgPath (pakName, isDist ) { return isDist ? `${distPath} /${pakName} ` : `${pkgPath} /${pakName} ` } export function getPackageJSON (pkgName ) { const path = `${resolvePkgPath(pkgName)} /package.json` const str = fs.readFileSync (path, { encoding : 'utf-8' }) return JSON .parse (str) } export function getBaseRollupPlugins ({ typescript = {} } = {} ) { return [ts (typescript), cjs ()] }
新建scripts/react.config.js
去处理 react 库的打包情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import { getPackageJSON, resolvePkgPath, getBaseRollupPlugins } from './utils' import generatePackageJson from 'rollup-plugin-generate-package-json' const { name, module } = getPackageJSON ('react' )const pkgPath = resolvePkgPath (name)const pkgDistPath = resolvePkgPath (name, true )export default [ { input : `${pkgPath} /${module } ` , output : { file : `${pkgDistPath} /index.js` , name : 'index.js' , format : 'umd' }, plugins : [ ...getBaseRollupPlugins ({}), generatePackageJSON ({ inputFolder : pkgPath, outputFolder : pkgDistPath, baseContents : ({ name, description, version } ) => ({ name, description, version, main : 'index.js' }) }) ] }, { input : `${pkgPath} /src/jsx.ts` , output : [ { file : `${pkgDistPath} /jsx-runtime.js` , name : 'jsx-runtime.js' , format : 'umd' }, { file : `${pkgDistPath} /jsx-dev-runtime.js` , name : 'jsx-dev-runtime.js' , format : 'umd' } ], plugins : getBaseRollupPlugins ({}) } ]
在根目录的package.json
下面添加打包的命令
1 2 3 4 5 "scripts" : { "clean" : "rm -rf ./dist" , "lint" : "eslint --ext .js,.jsx,.ts,.tsx --fix --quiet ./packages" , "build:dev" : "pnpm run clean && rollup --bundleConfigAsCjs --config scripts/rollup/react.config.js" } ,
打包后的文件目录如下所示
调式打包结果 Pnpm link 文档
Reconciler reconciler 是 React 核心逻辑所在的模块、意思是协调器、协调
Reconciler 有什么用 jQuery
工作原理(过程驱动):
前端框架结构与工作原理(状态驱动):
消费 JSX 没有编译优化 开放通用 API 供不同的宿主环境使用 核心模块消费 JSX 的过程 核心模块操作的数据结构 当前已知的数据结构:ReactElement
React Element 如果作为核心模块操作的数据结构存在的问题:
无法表达节点之间的关系 字段有限不能更容易的拓展(比如无法表达状态) 所以需要一种新的数据结构 它的特点如下:
介于 React Element 与真实 UI 节点之间 能够表达节点之间的关系 方便拓展 (不仅能作为数据存储单元,也能作为工作单元) 这就是FiberNode (虚拟DOM 在 React 中的实现)
当前了解的节点类型:
JSX React Element FiberNode DOM Element reconciler 的工作方式 对于同一个节点,比较其ReactElement
与fiberNode
,生成子fiberNode
并根据比较的结果生成不同的标记(插入、删除、移动…..),对应不同宿主环境 API 的执行
比如 挂载<div></div>
:
1 2 3 4 5 6 7 jsx ('div' )null Placement
将<div></div>
更新为<p></p>
:
1 2 3 4 5 6 7 jsx ("p" )FiberNode (type : "div" )Deletion Placement
当所有的 React Element 比较完全后 会生成一个 fiberNode 树 一共会存在两颗 fiberNode 树:
current:与视图中真实 UI 对应的 fiberNode workInProgress:触发更新后、正在 reconciler 中计算的 fiberNode 树 JSX 消费顺序 DFS 深度优先遍历与 BFS 广度优先遍历详解
以 DFS(深度优先遍历)的顺序遍历React Element
这意味着
如果有子节点,遍历子节点
如果没有子节点,遍历兄弟节点
1 2 3 4 <Card > <h3 > 你好</h3 > <p > MyReact</p > </Card >
这是一个递归的过程、存在递、归两个阶段:
递:对应 beginWork 归:对应 completeWork 我们新建一个packages/react-reconciler
包去实现协调器
建立packages/react-reconciler/filber.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import type { Props , Key , Ref } from 'shared/ReactTypes' import { WorkTag } from './workTags' import { Flags , NoFlags } from './fiberFlags' export class FiberNode { type : any tag : WorkTag pendingProps : Props key : Key stateNode : any return : FiberNode | null sibling : FiberNode | null child : FiberNode | null index : number ref : Ref memoizedProps : Props | null alternate : FiberNode | null flags : Flags constructor (tag: WorkTag, pendingProps: Props, key: Key ) { this .tag = tag this .key = key this .stateNode = null this .type = null this .return = null this .sibling = null this .child = null this .index = null this .ref = null this .pendingProps = pendingProps this .memoizedProps = null this .alternate = null this .flags = NoFlags } }
建立fiberFlags.ts
1 2 3 4 5 export type Flags = number export const NoFlags = 0b0000001 export const Placement = 0b0000010 export const Update = 0b0000100 export const ChildDeletion = 0b0001000
建立workTags.ts
1 2 3 4 5 6 7 8 9 export type WorkTag = | typeof FunctionComponent | typeof HostRoot | typeof HostComponent | typeof HostText export const FunctionComponent = 0 export const HostRoot = 3 export const HostComponent = 5 export const HostText = 6
现在定义工作函数
建立src/beginWork.ts
1 2 3 4 5 import { FiberNode } from './fiber' export const beginWork = (fiber: FiberNode ) => { console .log ('beginWork' , fiber) }
建立src/completeWork.ts
1 2 3 4 import { FiberNode } from './fiber' export const completeWork = (fiber: FiberNode ) => { console .log ('completeWork' , fiber) }
建立src/workLoop.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 import { FiberNode } from './fiber' import { completeWork } from './completeWork' import { beginWork } from './beginWork' let workInProgress : FiberNode | null = null function prepareFreshStatck (fiber: FiberNode ) { workInProgress = fiber } function renderRoot (root: FiberNode ) { prepareFreshStatck (root) do { try { workLoop () break } catch (err) { console .warn ('workLoop发生错误' , err) workInProgress = null } } while (true ) } function workLoop ( ) { while (workInProgress !== null ) { performUnitOfWork (workInProgress) } } function performUnitOfWork (fiber: FiberNode ) { const next = beginWork (fiber) fiber.memoizedProps = fiber.pendingProps if (next == null ) { completeUnitOfWork (fiber) } else { workInProgress = next } } function completeUnitOfWork (fiber: FiberNode ) { let node : FiberNode | null = fiber do { completeWork (node) const sibling = node.sibling if (sibling !== null ) { workInProgress = sibling return } node = node.return workInProgress = node } while (node !== null ) }
如何触发更新 常见的触发更新的方式:
ReactDom.createRoot().render
(或者老版的ReactDom.render
)this.setState
useState
中的dispatch
方法我们希望实现一套同一的更新机制 他们的特点是
兼容上述触发更新的方式 方便后续扩展(优先级机制…) 更新机制的组成部分 代表更新的数据结构 – Update
消费 Update 的数据结构 – UpdateQueue
现在先简单的实现一个 update 的机制
新建src/updateQueue.ts
Action
的类型定义
1 export type Action <State > = State | ((prevState: State ) => State )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 import { Action } from 'shared/ReactTypes' export interface Update <State > { action : Action <State > } export interface UpdateQueue <State > { shared : { pending : Update <State > | null } } export const createUpdate = <State >(action : Action <State >): Update <State > => { return { action } } export const createUpdateQueue = <Action >() => { return { shared : { pending : null } } as UpdateQueue <Action > } export const enqueueUpdate = <Action >( updateQueue: UpdateQueue<Action>, update: Update<Action> ) => { updateQueue.shared .pending = update } export const processUpdateQueue = <State >( baseState : State , pendingUpdate : Update <State > | null ): { memoizedState : State } => { const result : ReturnType <typeof processUpdateQueue<State >> = { memorizedState : baseState } if (pendingUpdate !== null ) { const action = pendingUpdate.action if (action instanceof Function ) { result.memoizedState = action (baseState) } else { result.memoizedState = action } } return result }
实现mount
时调用的 API 将该 API 接入上述更新机制中 需要考虑的事情:
更新可能发生于任意的组件,而更新流程是从根节点递归的 需要一个统一的根节点保存通用信息 1 ReactDom .createRoot (rootElement).render (<App /> )
首先实现FiberRootNode
的数据结构 在fiber.ts
中继续添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import type { Container } from 'hostConfig' export class FiberRootNode { container : Container current : FiberNode finishiedWork : FiberNode | null constructor (container: Container, hostRootFiber: FiberNode ) { this .current = hostRootFiber this .container = container hostRootFiber.stateNode = this this .finishiedWork = null } }
新建src/fiberReconciler.ts
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import { HostRoot } from './workTags' import { Container } from 'hostConfig' import { FiberNode , FiberRootNode } from './fiber' import { UpdateQueue , createUpdate, createUpdateQueue, enqueueUpdate } from './updateQueue' import { ReactElementType } from 'shared/ReactTypes' import { scheduleUpdateOnFiber } from './workLoop' export function createContainer (container: Container ) { const hostRoot = new FiberNode (HostRoot , null , null ) const root = new FiberRootNode (container, hostRoot) hostRoot.updateQueue = createUpdateQueue return root } export function updateContainer ( element: ReactElementType | null , root: FiberRootNode ) { const hostRootFiber = root.current const update = createUpdate<ReactElementType | null >(element) enqueueUpdate ( hostRootFiber.updateQueue as UpdateQueue <ReactElementType | null >, update ) scheduleUpdateOnFiber (hostRootFiber) return element }
修改workLoop.ts
中的部分内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { createWorkInProgress } from './fiber' function prepareFreshStack (root: FiberRootNode ) { workInProgress = createWorkInProgress (root.current , {}) } export function scheduleUpdateOnFiber (fiber: FiberNode ) { console .log ('scheduleUpdateOnFiber' , fiber) const root = makeUpdateFromFiberToRoot (fiber) renderRoot (root) } export function makeUpdateFromFiberToRoot (fiber: FiberNode ) { console .log ('makeUpdateFromFiberToRoot' , fiber) let node = fiber let parent = fiber.return if (parent !== null ) { node = parent parent = node.return } if (node.tag === HostRoot ) { return node.stateNode } return null }
在fiber.ts
中新增加createWorkInProgress函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 export const createWorkInProgress = ( current : FiberNode , pendingProps : Props ):FiberNode => { let wip = current.alternate if (wip === null ) { wip = new FiberNode (current.tag , pendingProps, current.key ) wip.type = current.type wip.stateNode = current.stateNode wip.alternate = current current.alternate = wip } esle { wip.pendingProps = pendingProps wip.flags = NoFlags wip.updateQueue = current.updateQueue wip.child = current.child wip.memoizedProps = current.memoizedProps wip.memoizedState = current.memoizedState } return wip }