├── .gitignore ├── README.md ├── config ├── constant.js ├── react-dom.development.js ├── runDemo.js └── runTest.js ├── jest.config.js ├── package.json ├── src ├── helper │ ├── homeworkHook.ts │ ├── renderTip │ │ ├── index.ts │ │ └── style.css │ ├── types.ts │ └── utils.ts ├── lib.dom.d.ts ├── vite-env.d.ts ├── 课程_1_工作原理概览 │ ├── demo │ │ └── App.tsx │ ├── homework │ │ ├── homework1.test.ts │ │ └── index.ts │ ├── index.html │ └── main.tsx ├── 课程_2_render阶段概览 │ ├── demo │ │ └── App.tsx │ ├── homework │ │ ├── homework2.test.tsx │ │ └── index.ts │ ├── index.html │ └── main.tsx └── 课程_3_状态更新原理 │ ├── demo │ └── App.tsx │ ├── homework │ ├── homework3.test.ts │ └── index.ts │ ├── index.html │ └── main.tsx ├── test.ts ├── tsconfig.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .history -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | 3 | ```sh 4 | npx react53 5 | ``` 6 | 7 | ## 使用方式 8 | 9 | 1. 执行命令,选择课程 10 | 11 | ```sh 12 | npm start 13 | # 或 14 | yarn start 15 | ``` 16 | 17 | 2. 在课程页面点击**顶部Tab**查看本节示例对应课程地址 18 | 19 | 3. 打开控制台查看日志信息 20 | 21 | 4. 学完后完成课程对应课后作业 22 | 23 | 5. 进群讨论,共同进步 24 | 25 | ## 课程更新 26 | 27 | 如果发现课程内容不是最新的,执行`git pull`拉取最新代码。 28 | 29 | ## 课程大纲 30 | 31 | ### 基础篇 32 | 33 | 1. 工作原理概览 34 | 2. render阶段概览 35 | 3. 状态更新原理 36 | 37 | ### 性能优化 38 | 39 | 4. 性能优化原理 40 | 5. 深入性能优化API 41 | 6. 性能优化实践 42 | 43 | ### Hooks 44 | 45 | 还在奋笔疾书... -------------------------------------------------------------------------------- /config/constant.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | LESSION_PREFFIX: '课程', 3 | LESSION_ENV: 'lession_env', 4 | LESSION_TEST_ARGV: 'lession', 5 | LESSION_TEST_ARGV_SHORT: 'l' 6 | } -------------------------------------------------------------------------------- /config/runDemo.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const {spawn} = require('child_process'); 4 | const util = require('util'); 5 | const rimraf = util.promisify(require('rimraf')); 6 | const inquirer = require('inquirer'); 7 | 8 | const constant = require('./constant'); 9 | 10 | async function hackReactDOM() { 11 | const pkgName = 'react-dom.development.js'; 12 | const pkgOriginPath = path.resolve(__dirname, `../node_modules/react-dom/cjs/${pkgName}`); 13 | const pkgTargetPath = path.resolve(__dirname, `./${pkgName}`); 14 | const viteCachePath = path.resolve(__dirname, '../node_modules/.vite'); 15 | // 清除vite缓存 16 | await rimraf(viteCachePath); 17 | // 替换ReactDOM 18 | const writeStream = await fs.createReadStream(pkgTargetPath).pipe(fs.createWriteStream(pkgOriginPath)); 19 | return new Promise(resolve => { 20 | writeStream.on('finish', resolve) 21 | }) 22 | } 23 | 24 | function parseLessionList() { 25 | const pkgOriginPath = path.resolve(__dirname, `../src`); 26 | return fs.readdirSync(pkgOriginPath).filter(name => name.startsWith(constant.LESSION_PREFFIX)); 27 | } 28 | 29 | inquirer.prompt([ 30 | { 31 | type: 'list', 32 | name: 'lession', 33 | message: '你想看哪个课程的Demo?', 34 | choices: parseLessionList() 35 | }, 36 | ]).then(async ({lession}) => { 37 | process.env[constant.LESSION_ENV] = lession; 38 | await hackReactDOM(); 39 | spawn('vite', [], {stdio: 'inherit', shell: true}); 40 | }) -------------------------------------------------------------------------------- /config/runTest.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const {spawn} = require('child_process'); 5 | const constant = require('./constant'); 6 | 7 | function parseLessionList() { 8 | const pkgOriginPath = path.resolve(__dirname, `../src`); 9 | return fs.readdirSync(pkgOriginPath) 10 | .filter(name => name.startsWith(constant.LESSION_PREFFIX)) 11 | .map(name => +name.split('_')[1]); 12 | } 13 | 14 | function checkLessionArg(num) { 15 | const lessionList = parseLessionList(); 16 | if (!num) { 17 | console.log(chalk.red('请输入课程序号,如:npm test 2 代表运行作业2的测试用例'), `所有课程序号包括:`, chalk.green(lessionList)); 18 | return false; 19 | } 20 | 21 | if (!lessionList.includes(num)) { 22 | console.log(chalk.red(`没有序号为${num}的课程`), `所有课程序号包括:`, chalk.green(lessionList)); 23 | return false; 24 | } 25 | return true; 26 | } 27 | 28 | const lessionNum = +process.argv.slice(2)[0]; 29 | const isValidNum = checkLessionArg(lessionNum); 30 | isValidNum && spawn(`jest`, [`${constant.LESSION_PREFFIX}_${lessionNum}`], {stdio: "inherit", shell: true}); 31 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const constant = require('./config/constant'); 3 | 4 | module.exports = { 5 | rootDir: path.resolve(__dirname), 6 | clearMocks: true, 7 | coverageDirectory: 'coverage', 8 | coverageProvider: 'v8', 9 | moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'], 10 | preset: 'ts-jest', 11 | testEnvironment: 'jsdom', 12 | // 测试文件 13 | testMatch: [`/src/${constant.LESSION_PREFFIX}*/**/*.test.ts?(x)`], 14 | 15 | transform: { 16 | '^.+\\js$': 'babel-jest', 17 | '^.+\\.(t|j)sx?$': 'ts-jest' 18 | } 19 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "react53-template", 4 | "scripts": { 5 | "start": "node ./config/runDemo.js", 6 | "test": "node ./config/runTest.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/BetaSu/react53.git" 11 | }, 12 | "author": "KaSong", 13 | "license": "ISC", 14 | "bugs": { 15 | "url": "https://github.com/BetaSu/react53/issues" 16 | }, 17 | "homepage": "https://github.com/BetaSu/react53#readme", 18 | "dependencies": { 19 | "react": "^17.0.0", 20 | "react-dom": "17.0.2" 21 | }, 22 | "devDependencies": { 23 | "@types/jest": "^26.0.23", 24 | "@types/react": "^17.0.0", 25 | "@types/react-dom": "17.0.2", 26 | "@vitejs/plugin-react-refresh": "^1.3.1", 27 | "arg": "^5.0.0", 28 | "babel-jest": "27.0.6", 29 | "chalk": "^4.1.1", 30 | "fast-glob": "^3.2.6", 31 | "inquirer": "^8.1.1", 32 | "jest": "27.0.6", 33 | "jest-cli": "27.0.6", 34 | "rimraf": "^3.0.2", 35 | "ts-jest": "^27.0.3", 36 | "typescript": "^4.3.2", 37 | "vite": "2.3.8" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/helper/homeworkHook.ts: -------------------------------------------------------------------------------- 1 | import {log, getType2Use, exist, createCounter} from './utils'; 2 | let hackErrorEmit = false; 3 | 4 | type THomeworkCB = (...args: any) => void; 5 | 6 | function checkHack() { 7 | if (!window.hackReactDOM && !hackErrorEmit) { 8 | hackErrorEmit = true; 9 | console.warn('[错误]请重新执行 yarn start(或 npm start)'); 10 | } 11 | } 12 | 13 | function bindHook(lession: number, hookType: string, callback: THomeworkCB) { 14 | type2Hook[lession] = type2Hook[lession] || {}; 15 | type2Hook[lession][hookType] = callback; 16 | } 17 | 18 | const type2Hook: {[lession: string]: {[lessionType: string]: THomeworkCB}} = {}; 19 | 20 | window.homeworkHook = function(type, ...args) { 21 | checkHack(); 22 | const lessionMap = type2Hook[window.lession]; 23 | const cb = lessionMap?.[type]; 24 | cb?.(...args); 25 | } 26 | 27 | bindHook(1, 'workLoopSync', () => { 28 | log('#727205', '开始render阶段,计算本次更新带来的副作用'); 29 | }) 30 | bindHook(1, 'commitRoot', () => { 31 | log('#04048a', '开始commit阶段,执行副作用'); 32 | }) 33 | bindHook(1, 'commitPlacement', (fiber) => { 34 | log('brown', `执行副作用:${getType2Use(fiber)}插入`, fiber); 35 | }) 36 | bindHook(1, 'commitWork', (fiber) => { 37 | log('brown', `执行副作用:${getType2Use(fiber)}更新属性`, fiber); 38 | }) 39 | bindHook(1, 'commitDeletion', (fiber) => { 40 | log('brown', `执行副作用:${getType2Use(fiber)}删除DOM`, fiber); 41 | }) 42 | 43 | bindHook(2, 'workLoopSync', () => { 44 | log('#727205', '开始深度优先遍历(render阶段开始)'); 45 | }) 46 | bindHook(2, 'commitRoot', () => { 47 | log('#04048a', 'commit阶段,执行副作用...'); 48 | }) 49 | // bindHook(2, 'beginWork', (_, wip) => { 50 | // log('green', `递 ${getType2Use(wip)}`); 51 | // }) 52 | // bindHook(2, 'completeWork', (_, wip) => { 53 | // log('red', `归 ${getType2Use(wip)}`); 54 | // }) 55 | // bindHook(2, 'reconcileChildren', (wip) => { 56 | // log('green', `创建${getType2Use(wip)}对应fiberNode:`, wip); 57 | // }) 58 | bindHook(2, 'beginWork', (_, wip) => { 59 | log('green', `创建${getType2Use(wip)}对应fiberNode:`, wip); 60 | }) 61 | // bindHook(2, 'appendAllChildren', (wip, ins) => { 62 | // log('purple', '生成DOM节点', ins); 63 | // }) 64 | 65 | bindHook(3, 'workLoopSync', () => { 66 | log('#727205', '开始深度优先遍历(render阶段开始)'); 67 | }) 68 | bindHook(3, 'commitRoot', () => { 69 | log('#04048a', 'commit阶段,执行副作用...'); 70 | }) 71 | bindHook(3, 'dispatchAction', (wip) => { 72 | const who = getType2Use(wip); 73 | log('blue', `${who}触发了更新,创建Update:`, wip.memoizedState.queue.pending); 74 | }) 75 | bindHook(3, 'mountState', (hook, wip) => { 76 | const who = getType2Use(wip); 77 | log('blue', `${who}的useState根据初始state计算出新state:`, hook.memoizedState); 78 | }) 79 | bindHook(3, 'updateReducer', (hook, wip) => { 80 | const who = getType2Use(wip); 81 | log('blue', `${who}的useState根据Update计算出新state:`, hook.memoizedState); 82 | }) 83 | bindHook(3, 'beginWork', (_, wip) => { 84 | log('green', `创建${getType2Use(wip)}对应fiberNode`); 85 | 86 | const update = wip.memoizedState?.queue?.pending; 87 | exist(update) && log('#04048a', `在${getType2Use(wip)}中发现Update:`, update); 88 | 89 | const pendingProps = wip.pendingProps; 90 | exist(pendingProps) && Object.keys(pendingProps).length && log('brown', 'props', pendingProps); 91 | }) 92 | bindHook(3, 'commitPlacement', (fiber) => { 93 | log('brown', `执行副作用:${getType2Use(fiber)}插入`, fiber); 94 | }) 95 | bindHook(3, 'commitWork', (fiber) => { 96 | log('brown', `执行副作用:${getType2Use(fiber)}更新属性`, fiber); 97 | }) 98 | bindHook(3, 'commitDeletion', (fiber) => { 99 | log('brown', `执行副作用:${getType2Use(fiber)}删除DOM`, fiber); 100 | }) 101 | 102 | 103 | 104 | // bindHook(4, 'renderWithHooks', (wip) => { 105 | // const cpn = getType2Use(wip); 106 | // log('red', `${cpn}开始render`, wip); 107 | // }) 108 | // bindHook(4, 'mountState', (hook) => { 109 | // log('green', `执行useState,数据为:`, hook); 110 | // }) 111 | // bindHook(4, 'updateState', (hook) => { 112 | // log('green', `执行useState,数据为:`, hook); 113 | // }) 114 | // bindHook(4, 'updateReducer', (hook) => { 115 | // log('green', `执行useReducer,数据为:`, hook); 116 | // }) 117 | // bindHook(4, 'mountReducer', (hook) => { 118 | // log('green', `执行useReducer,数据为:`, hook); 119 | // }) 120 | // bindHook(4, 'mountEffect', (hook) => { 121 | // log('green', `执行useEffect,数据为:`, hook); 122 | // }) 123 | // bindHook(4, 'updateEffect', (hook) => { 124 | // log('green', `执行useEffect,数据为:`, hook); 125 | // }) 126 | // bindHook(4, 'mountRef', (hook) => { 127 | // log('green', `执行useRef,数据为:`, hook); 128 | // }) 129 | // bindHook(4, 'updateRef', (hook) => { 130 | // log('green', `执行useRef,数据为:`, hook); 131 | // }) 132 | // bindHook(4, 'mountCallback', (hook) => { 133 | // log('green', `执行useCallback,数据为:`, hook); 134 | // }) 135 | // bindHook(4, 'updateCallback', (hook) => { 136 | // log('green', `执行useCallback,数据为:`, hook); 137 | // }) 138 | // bindHook(4, 'mountMemo', (hook) => { 139 | // log('green', `执行useMemo,数据为:`, hook); 140 | // }) 141 | // bindHook(4, 'updateMemo', (hook) => { 142 | // log('green', `执行useMemo,数据为:`, hook); 143 | // }) 144 | 145 | export {}; -------------------------------------------------------------------------------- /src/helper/renderTip/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import './style.css'; 3 | 4 | interface TTipData { 5 | lession: number; 6 | title: string; 7 | mainUrl: string; 8 | } 9 | 10 | export default ({title, lession, mainUrl}: TTipData): void => { 11 | document.title = title; 12 | window.lession = lession; 13 | const ele = document.createElement('section'); 14 | ele.className = 'tip-zone'; 15 | ele.innerHTML = ` 16 | 33 | `; 34 | const target = document.querySelector('#root'); 35 | target?.parentNode?.insertBefore(ele, target); 36 | } -------------------------------------------------------------------------------- /src/helper/renderTip/style.css: -------------------------------------------------------------------------------- 1 | #root { 2 | background: rgb(0, 0, 225, .2); 3 | padding: 20px; 4 | border-radius: 0 0 20px 20px; 5 | } 6 | #root::before { 7 | display: block; 8 | content: '示例区域'; 9 | color: white; 10 | font-weight: bolder; 11 | } 12 | 13 | .tip-zone { 14 | padding:10px 10px 10px 20px; 15 | border-radius: 20px 20px 0 0; 16 | background: skyblue; 17 | } 18 | 19 | #nav-drawer { 20 | position: relative; 21 | } 22 | 23 | .nav-unshown { 24 | display:none; 25 | } 26 | 27 | #nav-open { 28 | display: inline-block; 29 | cursor: pointer; 30 | width: 30px; 31 | height: 22px; 32 | vertical-align: middle; 33 | } 34 | 35 | #nav-open span, #nav-open span:before, #nav-open span:after { 36 | position: absolute; 37 | height: 3px; 38 | width: 25px; 39 | border-radius: 3px; 40 | background: #fff; 41 | display: block; 42 | content: ''; 43 | cursor: pointer; 44 | } 45 | #nav-open span:before { 46 | bottom: -8px; 47 | } 48 | #nav-open span:after { 49 | bottom: -16px; 50 | } 51 | 52 | #nav-close { 53 | display: none; 54 | position: fixed; 55 | z-index: 99; 56 | top: 0; 57 | left: 0; 58 | width: 100%; 59 | height: 100%; 60 | background: black; 61 | opacity: 0; 62 | transition: .3s ease-in-out; 63 | } 64 | 65 | #nav-content { 66 | overflow: auto; 67 | position: fixed; 68 | top: 0; 69 | left: 0; 70 | z-index: 9999; 71 | width: 90%; 72 | max-width: 330px; 73 | height: 100%; 74 | background: #fff; 75 | transition: .3s ease-in-out; 76 | -webkit-transform: translateX(-105%); 77 | transform: translateX(-105%); 78 | } 79 | 80 | #nav-title { 81 | color: #fff; 82 | font-weight: bolder; 83 | cursor: pointer; 84 | } 85 | 86 | #nav-input:checked ~ #nav-close { 87 | display: block; 88 | opacity: .5; 89 | } 90 | 91 | #nav-input:checked ~ #nav-content { 92 | -webkit-transform: translateX(0%); 93 | transform: translateX(0%); 94 | box-shadow: 6px 0 25px rgba(0,0,0,.15); 95 | } 96 | 97 | #nav-content ol { 98 | padding-right: 20px; 99 | } 100 | 101 | .highlight { 102 | color: #ea4335; 103 | } 104 | 105 | .qrcode-magician { 106 | width: 160px; 107 | height: 160px; 108 | background-size: contain; 109 | background-image: url(data:image/png;base64,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); 110 | } -------------------------------------------------------------------------------- /src/helper/types.ts: -------------------------------------------------------------------------------- 1 | export type TChoice = () => number; 2 | -------------------------------------------------------------------------------- /src/helper/utils.ts: -------------------------------------------------------------------------------- 1 | interface TWIP { 2 | type: any; 3 | tag: number; 4 | memoizedProps: any; 5 | } 6 | 7 | export const getType2Use = (wip: TWIP): string => { 8 | const {type, tag, memoizedProps} = wip; 9 | switch (tag) { 10 | case 3: 11 | return '根组件'; 12 | case 2: 13 | return `${type.name}(函数组件或类组件)`; 14 | case 1: 15 | return `${type.name}(类组件)`; 16 | case 0: 17 | return `${type.name}(函数组件)`; 18 | case 6: 19 | return `${memoizedProps}(文本组件)` 20 | default: 21 | return type; 22 | } 23 | } 24 | 25 | export const log = (color: string, label: string, message?: any) => { 26 | console.log( 27 | `%c ${label} %c`, 28 | `background-color: ${color}; color: #FFFFFF`, 29 | `background-color: inherit; color: inherit` 30 | , exist(message) ? message : ''); 31 | } 32 | 33 | export const exist = (data: any) => data !== undefined && data !== null; 34 | 35 | export const getReadableAnswer = (count: number, answer: number) => { 36 | return `第${count + 1}题,答案:${answer}`; 37 | } 38 | 39 | export const createCounter = (initialCount = 0) => { 40 | let count = initialCount; 41 | return { 42 | add() { 43 | count++; 44 | }, 45 | reset() { 46 | count = initialCount; 47 | }, 48 | get() { 49 | return count; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/lib.dom.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | hackReactDOM: boolean; 3 | homeworkHook: (type: string, ...args: any) => void; 4 | lession: number; 5 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/课程_1_工作原理概览/demo/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | 3 | function App() { 4 | const [count, update] = useState(0); 5 | const add = () => { 6 | if (count > 0) { 7 | update(count - 1); 8 | } 9 | }; 10 | 11 | const del = () => update(count + 1); 12 | 13 | return ( 14 |
15 |

提莫队长正在送命

16 | 17 | 18 |
    19 | {Array(count).fill('🍄').map((what, i) =>
  • {what}
  • )} 20 |
21 |
22 | ) 23 | } 24 | 25 | export default App 26 | -------------------------------------------------------------------------------- /src/课程_1_工作原理概览/homework/homework1.test.ts: -------------------------------------------------------------------------------- 1 | import homework from '.'; 2 | import {getReadableAnswer} from '../../helper/utils'; 3 | 4 | 5 | test('课程一选择题批改作业', () => { 6 | const answer = [1, 1, 1, 1, 1, 2].map((rightAnswer, i) => getReadableAnswer(i, rightAnswer)); 7 | homework.forEach((cur, i) => { 8 | const curAnswer = getReadableAnswer(i, cur()); 9 | const rightAnswer = answer[i]; 10 | expect(curAnswer).toBe(rightAnswer); 11 | }) 12 | }); -------------------------------------------------------------------------------- /src/课程_1_工作原理概览/homework/index.ts: -------------------------------------------------------------------------------- 1 | import {TChoice} from '../../helper/types'; 2 | 3 | /** 4 | * 今天的课后作业是选择题 5 | * 做完了执行 npm test 1 批改作业 6 | */ 7 | 8 | export default [ 9 | // 这是个选择题使用例子 10 | () => { 11 | // 问题:卡颂帅么? 12 | // 1. 帅 13 | // 2. 不帅 14 | return 1; /** return你的答案 */ 15 | }, 16 | () => { 17 | // 问题:React的更新流程分为render阶段和commit阶段 18 | // 1. 对 19 | // 2. 错 20 | return /** 你的答案 */; 21 | }, 22 | () => { 23 | // 问题:更新流程是如何触发的? 24 | // 1. ReactDOM.render、this.setState、useState的setter... 25 | // 2. 通过定时器定时触发 26 | return /** 你的答案 */; 27 | }, 28 | () => { 29 | // 问题:什么是副作用? 30 | // 1. DOM的增/删/改(属性更新)等... 31 | // 2. 吃了感冒药下午瞌睡 32 | return /** 你的答案 */; 33 | }, 34 | () => { 35 | // 问题:render阶段的作用? 36 | // 1. 找出本次更新产生的副作用 37 | // 2. 执行本次更新的副作用 38 | return /** 你的答案 */; 39 | }, 40 | () => { 41 | // 问题:commit阶段的作用? 42 | // 1. 找出本次更新产生的副作用 43 | // 2. 执行本次更新的副作用 44 | return /** 你的答案 */; 45 | } 46 | ] as TChoice[]; 47 | -------------------------------------------------------------------------------- /src/课程_1_工作原理概览/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React53 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/课程_1_工作原理概览/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './demo/App' 4 | import '../helper/homeworkHook'; 5 | import renderTip from '../helper/renderTip'; 6 | 7 | renderTip({ 8 | lession: 1, 9 | title: 'React53', 10 | mainUrl: 'https://react53.iamkasong.com/chart1.html' 11 | }); 12 | 13 | ReactDOM.render( 14 | , 15 | document.getElementById('root') 16 | ) -------------------------------------------------------------------------------- /src/课程_2_render阶段概览/demo/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | function App() { 5 | 6 | return ( 7 |
8 |

卡颂喜欢的水果:

9 |
    10 |
  • 🍎
  • 11 |
  • 🍌
  • 12 |
  • 🍉
  • 13 |
14 |
15 | ) 16 | } 17 | 18 | export default App 19 | -------------------------------------------------------------------------------- /src/课程_2_render阶段概览/homework/homework2.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {reconciler, choiceList} from '.'; 3 | import {getReadableAnswer} from '../../helper/utils'; 4 | 5 | test('课程二选择题批改作业', () => { 6 | const answer = [2, 1, 2].map((rightAnswer, i) => getReadableAnswer(i, rightAnswer)); 7 | choiceList.forEach((cur, i) => { 8 | const curAnswer = getReadableAnswer(i, cur()); 9 | const rightAnswer = answer[i]; 10 | expect(curAnswer).toBe(rightAnswer); 11 | }) 12 | }); 13 | 14 | test('深度优先遍历', () => { 15 | 16 | const jsx = ( 17 |
18 |

19 | 20 |
    21 |
  • 22 |
  • 23 |
24 |
25 | ) 26 | 27 | const result = reconciler(jsx); 28 | const answer = ['div', 'p', 'span', 'ul', 'li', 'p', 'li', 'p']; 29 | expect(result).toEqual(answer); 30 | }); 31 | 32 | -------------------------------------------------------------------------------- /src/课程_2_render阶段概览/homework/index.ts: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode } from 'react'; 2 | import {TChoice} from '../../helper/types'; 3 | 4 | 5 | /** 6 | * 今天的课后作业是选择题和应用题 7 | * 做完了执行 npm test 2 批改作业 8 | */ 9 | 10 | export const choiceList = [ 11 | () => { 12 | // 问题:render阶段的遍历顺序是? 13 | // 1. 广度优先遍历 14 | // 2. 深度优先遍历 15 | return /** 你的答案 */; 16 | }, 17 | () => { 18 | // 问题:对于如下JSX结构: 19 | /** 20 | *
21 | *

hello

22 | *
    23 | *
  • 24 | *
25 | *
26 | */ 27 | // p生成对应fiberNode后,下一个要生成fiberNode的节点是? 28 | // 1. span 29 | // 2. ul 30 | return /** 你的答案 */; 31 | }, 32 | () => { 33 | // 问题:对于如下JSX结构: 34 | /** 35 | *
36 | *

hello

37 | *
    38 | *
  • 39 | *
40 | *
41 | */ 42 | // hello生成对应fiberNode后,下一个要生成fiberNode的节点是? 43 | // 1. div 44 | // 2. ul 45 | return /** 你的答案 */; 46 | } 47 | ] as TChoice[]; 48 | 49 | 50 | /** 51 | * 应用题 使用深度优先遍历实现一个遍历器 52 | * 接收一段JSX结构作为参数,按与React同样的“递”与“归”流程,将遍历到的element.type push到数组中,并返回数组 53 | * 如: 传入: 54 |
55 | 56 |
    57 |
  • 58 |
  • 59 |
60 |
61 | 返回: ['div', 'span', 'ul', 'li', 'li'] 62 | 注:你可以在这个链接看到JSX编译执行后的数据结构:https://codesandbox.io/s/jsx-playground-forked-tzpqj 63 | * 编写完成后执行 npm test 2 跑用例 64 | */ 65 | export const reconciler = (jsx: React.ReactElement): string[] => { 66 | // 请开始你的表演 67 | return []; 68 | } 69 | 70 | 71 | 72 | 73 | // 参考答案 74 | // export const reconciler = (jsx: React.ReactElement) => { 75 | // const result: string[] = []; 76 | 77 | // if (!jsx) { 78 | // return result; 79 | // } 80 | 81 | // const stack = [jsx]; 82 | 83 | // while (stack.length) { 84 | // const node = stack.pop(); 85 | // if (typeof node?.type === 'string') { 86 | // result.push(node.type); 87 | // } 88 | 89 | // const children = node?.props.children; 90 | // if (children) { 91 | // if (Array.isArray(children)) { 92 | // for (let i = children.length - 1; i >= 0; i--) { 93 | // stack.push(children[i]); 94 | // } 95 | // } else { 96 | // stack.push(children); 97 | // } 98 | // } 99 | // } 100 | 101 | // return result; 102 | // } 103 | -------------------------------------------------------------------------------- /src/课程_2_render阶段概览/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React53 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/课程_2_render阶段概览/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './demo/App' 4 | import '../helper/homeworkHook'; 5 | import renderTip from '../helper/renderTip'; 6 | 7 | renderTip({ 8 | lession: 2, 9 | title: 'React53', 10 | mainUrl: 'https://react53.iamkasong.com/chart2.html' 11 | }); 12 | 13 | ReactDOM.render( 14 | , 15 | document.getElementById('root') 16 | ) 17 | -------------------------------------------------------------------------------- /src/课程_3_状态更新原理/demo/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | function App() { 4 | return ( 5 | 6 | ) 7 | } 8 | 9 | function GrandParent() { 10 | return ( 11 | 12 | ) 13 | } 14 | 15 | function Parent() { 16 | const [num, updateNum] = useState(0); 17 | return ; 18 | } 19 | 20 | function Child({num, onChange}: {num: number; onChange: (num: number) => void;}) { 21 | return
onChange(num + 1)}>{num}
; 22 | } 23 | 24 | export default App 25 | -------------------------------------------------------------------------------- /src/课程_3_状态更新原理/homework/homework3.test.ts: -------------------------------------------------------------------------------- 1 | import {homework, insertUpdate, TUpdate, TFiberNode} from '.'; 2 | import {getReadableAnswer} from '../../helper/utils'; 3 | 4 | test('课程3选择题批改作业', () => { 5 | const answer = [2, 2, 2, 1, 2].map((rightAnswer, i) => getReadableAnswer(i, rightAnswer)); 6 | homework.forEach((cur, i) => { 7 | const curAnswer = getReadableAnswer(i, cur()); 8 | const rightAnswer = answer[i]; 9 | expect(curAnswer).toBe(rightAnswer); 10 | }) 11 | }); 12 | 13 | test('插入update成功且数据结构正确且形成环状链表', () => { 14 | const fiberNode: TFiberNode = { 15 | updateQueue: undefined 16 | } 17 | const firstAction = 0; 18 | insertUpdate(fiberNode, firstAction); 19 | const {updateQueue} = fiberNode; 20 | let curUpdate = updateQueue as TUpdate; 21 | expect(curUpdate).toBeDefined(); 22 | expect(curUpdate.action).toBe(firstAction); 23 | expect(curUpdate.next).toBe(curUpdate); 24 | }) 25 | 26 | test('插入2个update形成正确的环状链表', () => { 27 | const fiberNode: TFiberNode = { 28 | updateQueue: undefined 29 | } 30 | for (let i = 0; i < 2; i++) { 31 | insertUpdate(fiberNode, i); 32 | } 33 | const {updateQueue} = fiberNode; 34 | let curUpdate = (updateQueue as TUpdate).next as TUpdate; 35 | let curRightAction = 0; 36 | const firstAction = 0; 37 | do { 38 | expect(curUpdate.action).toBe(curRightAction++); 39 | curUpdate = curUpdate.next as TUpdate; 40 | } while (curUpdate.action !== firstAction) 41 | }) 42 | 43 | test('插入多个update形成正确的环状链表', () => { 44 | const fiberNode: TFiberNode = { 45 | updateQueue: undefined 46 | } 47 | for (let i = 0; i < 10; i++) { 48 | insertUpdate(fiberNode, i); 49 | } 50 | const {updateQueue} = fiberNode; 51 | let curUpdate = (updateQueue as TUpdate).next as TUpdate; 52 | let curRightAction = 0; 53 | const firstAction = 0; 54 | do { 55 | expect(curUpdate.action).toBe(curRightAction++); 56 | curUpdate = curUpdate.next as TUpdate; 57 | } while (curUpdate.action !== firstAction) 58 | }) -------------------------------------------------------------------------------- /src/课程_3_状态更新原理/homework/index.ts: -------------------------------------------------------------------------------- 1 | import {TChoice} from '../../helper/types'; 2 | /** 3 | * 今天的课后作业是选择题和应用题 4 | * 做完了执行 npm test 3 批改作业 5 | */ 6 | 7 | export const homework = [ 8 | () => { 9 | // 问题:调用this.setState触发更新后,只有被更新组件及其子孙组件会执行遍历流程? 10 | // 1. 对 11 | // 2. 错 12 | return /** 你的答案 */; 13 | }, 14 | () => { 15 | // 问题:state的最新值需要计算么? 16 | // 1. 不需要,this.setState(x)中x就是最新值,直接使用就行 17 | // 2. 需要,state需要根据Update计算得出 18 | return /** 你的答案 */; 19 | }, 20 | () => { 21 | // 问题:被更新的state是在什么时候被计算的? 22 | // 1. 触发更新那一刻 23 | // 2. 遍历流程进行到state所在组件对应fiber时 24 | return /** 你的答案 */; 25 | }, 26 | () => { 27 | // 问题:为什么在触发更新后,有些组件没有render? 28 | // 1. 虽然更新是全局概念,但是React内置了遍历时的优化策略,以及提供了性能优化API(如:PureComponent),命中这些策略的组件不会render 29 | // 2. 因为更新是局部概念,只有触发更新的组件及其子孙组件才会render 30 | return /** 你的答案 */; 31 | }, 32 | () => { 33 | // 问题:一次更新,只有触发更新的组件对应DOM会变化? 34 | // 1. 是,因为这次更新只在该组件对应fiber上创建了Update 35 | // 2. 不一定,如果其他组件对应fiber上也存在Update并计算出新的state,可能也会产生对应DOM变化 36 | return /** 你的答案 */; 37 | } 38 | ] as TChoice[]; 39 | 40 | 41 | /** 42 | * 应用题,请实现向fiberNode插入update的方法insertUpdate 43 | * 其中update是一条环状链表,满足: 44 | 新插入的update是链表中最后一个update 45 | fiberNode.updateQueue指向最后一个update(即新插入这个) 46 | 最后一个update下一个(即.next)指向第一个update 47 | 插入update后函数返回fiberNode 48 | 如: 插入第一个update:u1 49 | 此时: fiberNode.updateQueue = u1 50 | u1.next = u1 51 | 插入第二个update:u2 52 | fiberNode.updateQueue = u2 53 | u2.next = u1; u1.next = u2; 54 | 插入第三个update:u3 55 | fiberNode.updateQueue = u3 56 | u3.next = u1; u1.next = u2; u2.next = u3; 57 | */ 58 | export interface TUpdate { 59 | action: any; 60 | next?: TUpdate; 61 | } 62 | export interface TFiberNode { 63 | updateQueue?: TUpdate; 64 | } 65 | 66 | export function insertUpdate(fiberNode: TFiberNode, action: any): TFiberNode { 67 | const update: TUpdate = { 68 | action, 69 | next: undefined 70 | }; 71 | 72 | // 请开始你的表演 73 | 74 | return fiberNode; 75 | } 76 | 77 | 78 | 79 | 80 | 81 | // 参考答案 82 | // export function insertUpdate(fiberNode: TFiberNode, action: any): TFiberNode { 83 | // const update: TUpdate = { 84 | // action, 85 | // next: undefined 86 | // }; 87 | 88 | // const lastUpdate = fiberNode.updateQueue; 89 | 90 | // if (!lastUpdate) { 91 | // // fiberNode上还不存在update,插入的update与自己形成环状链表 92 | // update.next = update; 93 | // } else { 94 | // // fiberNode上存在update 95 | // // 新插入的作为最后一个,指向第一个 96 | // update.next = lastUpdate.next; 97 | // // 之前的最后一个指向新插入这个 98 | // lastUpdate.next = update; 99 | // } 100 | // // updateQueue指向新插入这个 101 | // fiberNode.updateQueue = update; 102 | 103 | // return fiberNode; 104 | // } -------------------------------------------------------------------------------- /src/课程_3_状态更新原理/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React53 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/课程_3_状态更新原理/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './demo/App' 4 | import '../helper/homeworkHook'; 5 | import renderTip from '../helper/renderTip'; 6 | 7 | renderTip({ 8 | lession: 3, 9 | title: 'React53', 10 | mainUrl: 'https://react53.iamkasong.com/chart3.html' 11 | }); 12 | 13 | ReactDOM.render( 14 | , 15 | document.getElementById('root') 16 | ) 17 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetaSu/react53/9f4256294171c1c740847fe02810ffc183b53cf0/test.ts -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "allowJs": false, 6 | "skipLibCheck": false, 7 | "esModuleInterop": false, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "ESNext", 12 | "moduleResolution": "Node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["./src"] 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import reactRefresh from '@vitejs/plugin-react-refresh' 3 | import constant from './config/constant'; 4 | 5 | const curLession = process.env[constant.LESSION_ENV]; 6 | if (!curLession) { 7 | console.error('错误的课程目录:', curLession); 8 | } 9 | 10 | // https://vitejs.dev/config/ 11 | export default defineConfig({ 12 | root: `./src/${curLession}`, 13 | plugins: [reactRefresh()] 14 | }) 15 | --------------------------------------------------------------------------------