├── README.md ├── src ├── assets │ └── favicon.ico ├── app.jsx ├── Dedoo │ ├── index.ts │ ├── utils.ts │ ├── createElement.ts │ ├── dedoo.d.ts │ ├── render.ts │ ├── hooks.ts │ ├── reconciliation.ts │ ├── commit.ts │ └── concurrent.ts ├── index.jsx └── rest.jsx ├── .gitignore ├── index.html ├── babel.config.js ├── tsconfig.json ├── config ├── webpack.dev.js ├── webpack.prod.js └── webpack.common.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # build-your-own-react 2 | https://pomb.us/build-your-own-react/ 3 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFmiss/build-your-own-react/master/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const App = () => { 4 | return ( 5 |
hello
6 | ) 7 | } 8 | 9 | export default App 10 | -------------------------------------------------------------------------------- /src/Dedoo/index.ts: -------------------------------------------------------------------------------- 1 | import createElement from './createElement'; 2 | import render from './render'; 3 | import { 4 | useState 5 | } from './hooks'; 6 | 7 | export default { 8 | createElement, 9 | render, 10 | useState 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | yarn.lock 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(false) 3 | 4 | const presets = [ 5 | [ 6 | "@babel/preset-env", { 7 | modules: false 8 | } 9 | ], 10 | '@babel/preset-react', 11 | '@babel/preset-typescript' 12 | ] 13 | const plugins = [ 14 | '@babel/plugin-syntax-dynamic-import' 15 | ] 16 | 17 | return { 18 | presets, 19 | plugins 20 | } 21 | } -------------------------------------------------------------------------------- /src/Dedoo/utils.ts: -------------------------------------------------------------------------------- 1 | // 是否是正常属性 (移除children & event prop) 2 | export const isProperty = (key: string) => key !== 'children' && !isEvent(key); 3 | 4 | // 是否新增 或者 是需要更新的属性 5 | export const isNew = (prev, next) => (key: string) => prev[key] !== next[key]; 6 | 7 | // 是否是已被移除的属性 8 | export const isGone = (prev, next) => (key: string) => !(key in next); 9 | 10 | // 是否是时间 11 | export const isEvent = (key: string) => key.startsWith('on'); 12 | 13 | -------------------------------------------------------------------------------- /src/Dedoo/createElement.ts: -------------------------------------------------------------------------------- 1 | export default function createElement( 2 | type: DedooElementType, 3 | props: object, 4 | ...children 5 | ): DedooElement { 6 | return { 7 | type, 8 | props: { 9 | ...props, 10 | children: children?.map( 11 | child => 12 | typeof child === 'object' 13 | ? child 14 | : createTextElement(child) 15 | ) 16 | }, 17 | } 18 | } 19 | 20 | // 文本节点 21 | function createTextElement(child: string): DedooElement { 22 | return { 23 | type: 'TEXT_ELEMENT', 24 | props: { 25 | nodeValue: child, 26 | children: [] 27 | } 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/Dedoo/dedoo.d.ts: -------------------------------------------------------------------------------- 1 | type DedooNode = (props: DedooElementProps) => unknown; 2 | 3 | type FiberDom = HTMLElement | Text; 4 | 5 | type SelfElementType = 'TEXT_ELEMENT'; 6 | type DedooElementType = keyof HTMLElementTagNameMap | DedooNode | SelfElementType; 7 | 8 | type FiberEffectTag = 'UPDATE' | 'DELETE' | 'PLACEMENT'; 9 | 10 | interface DedooElementProps { 11 | [props: string]: any; 12 | children?: DedooElement[]; 13 | }; 14 | interface DedooElement { 15 | type: DedooElementType 16 | props: DedooElementProps | null; 17 | } 18 | 19 | interface DedooFiber extends DedooElement { 20 | dom: FiberDom | null; 21 | parent: DedooFiber; 22 | sibling?: DedooFiber | null; 23 | child?: DedooFiber | null; 24 | alternate: DedooFiber | null; 25 | effectTag: FiberEffectTag; 26 | hooks?: Array; 27 | } 28 | 29 | type FiberNextWork = DedooFiber | null; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "target": "es5", 5 | "module": "ESNext", 6 | "removeComments": false, 7 | "declaration": true, 8 | "outDir": "./dist", 9 | "declarationDir": "./dist", 10 | "baseUrl": ".", 11 | "moduleResolution": "Node", 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "jsx": "react", 15 | "experimentalDecorators": true, 16 | "downlevelIteration": true, 17 | "allowJs": false, 18 | "skipLibCheck": true, 19 | "typeRoots": [ 20 | "node_modules/@types", 21 | "global.d.ts", 22 | "typings" 23 | ], 24 | "lib": [ 25 | "dom", 26 | "esnext" 27 | ], 28 | "sourceMap": false, 29 | "noImplicitAny": false 30 | }, 31 | "include": [ 32 | "./src/**/*", 33 | "global.d.ts" 34 | ], 35 | "exclude": [ 36 | "node_modules" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | const path = require('path'); 4 | const webpackPromptPlugin = require('webpack-prompt-plugin') 5 | 6 | const resolve = function (dir) { 7 | return path.resolve(__dirname, dir); 8 | } 9 | 10 | module.exports = merge(common, { 11 | mode: 'development', 12 | entry: { 13 | app: './src/index.jsx' 14 | }, 15 | output: { 16 | path: resolve('dist'), 17 | publicPath: '/', 18 | filename: 'js/[name]-[hash].js' 19 | }, 20 | 21 | module: { 22 | rules: [ 23 | ] 24 | }, 25 | 26 | plugins: [ 27 | new webpackPromptPlugin() 28 | ], 29 | 30 | devServer: { 31 | // 当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html。通过传入以下启用: 32 | // contentBase: "./", 33 | host: '0.0.0.0', 34 | // 端口号 35 | port: 1994, 36 | //当有编译器错误或警告时,在浏览器中显示全屏覆盖。默认禁用。如果您只想显示编译器错误: 37 | noInfo: true, 38 | // 配置端口号 39 | overlay: true, 40 | historyApiFallback: true 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | // import './module/0_Review'; 2 | 3 | // import './module/1_createElementFn'; 4 | 5 | // import './module/2_render'; 6 | 7 | // import './module/3_concurrentMode'; 8 | 9 | // import './module/4_fibers'; 10 | 11 | // import './module/5_renderCommitPhases'; 12 | 13 | // import './module/6_reconciliation'; 14 | 15 | // import './module/7_functionComponents'; 16 | import Dedoo from './Dedoo'; 17 | 18 | /**@jsx Dedoo.createElement */ 19 | const Element = () => { 20 | const [state, setState] = Dedoo.useState(1); 21 | return ( 22 |
23 | bar 24 |

你好

25 |

{state}

26 |

123123

27 | 30 | 33 | 34 |
35 | ) 36 | } 37 | 38 | const element = 39 | 40 | const container = document.getElementById('root'); 41 | Dedoo.render(element, container); 42 | 43 | -------------------------------------------------------------------------------- /src/Dedoo/render.ts: -------------------------------------------------------------------------------- 1 | import { isProperty } from "./utils"; 2 | import { 3 | setNextUnitOfWork, 4 | } from './concurrent'; 5 | import { setWipRoot, updateDom } from "./commit"; 6 | import { getCurrentRoot, setDeletions } from "./reconciliation"; 7 | 8 | export function createDom(fiber: DedooFiber) { 9 | // * create dom nodes 10 | // 创建元素 11 | // 判断类型。文本直接 12 | const dom = 13 | fiber.type === 'TEXT_ELEMENT' 14 | ? document.createTextNode('') 15 | : document.createElement(fiber.type as keyof HTMLElementTagNameMap); 16 | 17 | // 设置属性 18 | // 过滤children 19 | if (fiber.props) { 20 | // console.info('Object.keys(fiber.props)', Object.keys(fiber.props).filter(isProperty)) 21 | } 22 | updateDom(dom, {}, fiber.props || {}) 23 | return dom; 24 | } 25 | 26 | 27 | function render(element: DedooElement, container: Element | DocumentFragment | null) { 28 | // TODO set next unit of work 29 | // 我们将创建DOM节点的部分保留在其自身的功能中,稍后将使用它 30 | // ! 我们将跟踪纤维树的根。我们称其为进行中的工作根或wipRoot 31 | let wipRoot = { 32 | dom: container, 33 | props: { 34 | children: [element] 35 | }, 36 | // 我们还将替代属性添加到每根光纤。 37 | // ! 该属性是旧光纤的链接,旧光纤是我们在上一个提交阶段提交给DOM的光纤。 38 | alternate: getCurrentRoot() 39 | } 40 | setDeletions([]); 41 | setWipRoot(wipRoot as FiberNextWork); 42 | setNextUnitOfWork(wipRoot); 43 | }; 44 | 45 | export default render; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build-your-own-react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "author": "", 6 | "main": "index.js", 7 | "license": "ISC", 8 | "scripts": { 9 | "dev": "webpack-dev-server --config ./config/webpack.dev.js --progress", 10 | "build": "webpack --progress --config ./config/webpack.prod.js" 11 | }, 12 | "dependencies": { 13 | "react": "16.8", 14 | "react-dom": "16.8" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.8.3", 18 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 19 | "@babel/preset-env": "^7.8.3", 20 | "@babel/preset-react": "^7.8.3", 21 | "@babel/preset-typescript": "^7.12.7", 22 | "autoprefixer": "^9.7.4", 23 | "babel-loader": "^8.0.6", 24 | "classnames": "^2.2.6", 25 | "clean-webpack-plugin": "^3.0.0", 26 | "css-loader": "^3.4.2", 27 | "html-webpack-plugin": "^3.2.0", 28 | "less": "^3.10.3", 29 | "less-loader": "^5.0.0", 30 | "mini-css-extract-plugin": "^0.9.0", 31 | "optimize-css-assets-webpack-plugin": "^5.0.3", 32 | "postcss-loader": "^3.0.0", 33 | "style-loader": "^1.1.2", 34 | "style-resources-loader": "^1.3.3", 35 | "terser-webpack-plugin": "^2.3.2", 36 | "uglifyjs-webpack-plugin": "^2.2.0", 37 | "url-loader": "^3.0.0", 38 | "webpack": "^4.41.5", 39 | "webpack-bundle-analyzer": "^3.6.0", 40 | "webpack-cli": "^3.3.10", 41 | "webpack-dev-server": "^3.10.1", 42 | "webpack-merge": "^4.2.2", 43 | "webpack-prompt-plugin": "^1.1.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Dedoo/hooks.ts: -------------------------------------------------------------------------------- 1 | import { wipRoot } from './commit'; 2 | import { 3 | getWipFiber, 4 | getHookIndex, 5 | setHookIndex, 6 | setNextUnitOfWork 7 | } from './concurrent'; 8 | 9 | import { 10 | getWipRoot, 11 | setWipRoot 12 | } from './commit'; 13 | import { getCurrentRoot, setDeletions } from './reconciliation'; 14 | 15 | export function useState(initial:T): [ 16 | T, 17 | (action: T | ((prevState: T) => T)) => void 18 | ] { 19 | let wipFiber = getWipFiber(); 20 | let hookIndex = getHookIndex(); 21 | console.info(hookIndex) 22 | const oldHook = wipFiber && wipFiber.alternate && 23 | wipFiber.alternate.hooks && 24 | wipFiber.alternate.hooks[hookIndex] 25 | console.info('oldHook', oldHook) 26 | 27 | // 如果有一个旧钩子,则将状态从旧钩子复制到新钩子,如果没有,则初始化状态。 28 | const hook: { 29 | state: T, 30 | queue: any[] 31 | } = { 32 | state: oldHook ? oldHook.state : initial, 33 | queue: [] 34 | } 35 | 36 | console.info('hook', hook); 37 | 38 | // let wipRoot = getWipRoot(); 39 | // let currentRoot = getCurrentRoot(); 40 | 41 | const actions = oldHook ? oldHook.queue : []; 42 | console.info('actions', actions); 43 | actions?.forEach(action => { 44 | hook.state = action(hook.state); 45 | }); 46 | 47 | console.info('hook.state', hook.state); 48 | 49 | const setState = (action: T | ((prevState: T) => T)) => { 50 | let act = action; 51 | if (typeof action !== 'function') { 52 | act = () => action; 53 | } 54 | console.info(act) 55 | hook.queue.push(act); 56 | console.info(hook) 57 | 58 | let currentRoot = getCurrentRoot(); 59 | 60 | console.info('getCurrentRoot()', getCurrentRoot(), currentRoot); 61 | setWipRoot({ 62 | dom: currentRoot?.dom || null, 63 | props: currentRoot?.props || null, 64 | alternate: currentRoot, 65 | } as any) 66 | console.info('getWipRoot()', getWipRoot()); 67 | setNextUnitOfWork(getWipRoot()); 68 | setDeletions([]); 69 | } 70 | 71 | // 然后我们向纤维添加新的钩子,钩子索引增加1,并返回状态 72 | getWipFiber()?.hooks?.push(hook); 73 | setHookIndex((hookIndex as number)++); 74 | return [hook.state, setState]; 75 | } 76 | -------------------------------------------------------------------------------- /config/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const path = require('path'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const common = require('./webpack.common.js'); 5 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 6 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 7 | 8 | // css压缩打包相关 9 | var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 10 | 11 | // 打包清除dist目录 12 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 13 | 14 | module.exports = merge(common, { 15 | mode: 'production', 16 | entry: { 17 | app: './src/index.jsx', 18 | }, 19 | externals: { 20 | // 使用 externals 需要在 index.html 中配置需要的 库 的cdn地址 21 | react: 'React', 22 | }, 23 | output: { 24 | path: path.resolve(__dirname, './../dist'), 25 | publicPath: './', 26 | filename: 'js/[name]-[hash].js', 27 | libraryTarget: 'umd' 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.js$/, 33 | use: { 34 | loader: 'babel-loader', 35 | } 36 | }, 37 | ] 38 | }, 39 | plugins: [ 40 | // 清除 41 | new CleanWebpackPlugin({ 42 | cleanOnceBeforeBuildPatterns: path.resolve(__dirname, 'dist') 43 | }), 44 | 45 | // css 压缩 46 | new OptimizeCssAssetsPlugin({}), 47 | 48 | new BundleAnalyzerPlugin() 49 | ], 50 | optimization: { 51 | namedModules: true, 52 | minimizer: [ 53 | new TerserPlugin({ 54 | cache: true, 55 | parallel: true, 56 | sourceMap: true, // Must be set to true if using source-maps in production 57 | terserOptions: { 58 | // https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions 59 | } 60 | }), 61 | ], 62 | splitChunks: { 63 | chunks: "all", 64 | minSize: 30000, 65 | minChunks: 3, 66 | maxAsyncRequests: 5, 67 | maxInitialRequests: 3, 68 | name: true, 69 | cacheGroups: { 70 | default: { 71 | minChunks: 2, 72 | priority: -20, 73 | reuseExistingChunk: true, 74 | }, 75 | vendors: { 76 | test: /[\\/]node_modules[\\/]/, 77 | chunks: "initial", 78 | name: "vendor", 79 | priority: 10, 80 | enforce: true, 81 | }, 82 | commons: { 83 | name: 'vendors', 84 | chunks: 'all', 85 | minChunks: 2, 86 | maxInitialRequests: 5, // The default limit is too small to showcase the effect 87 | minSize: 0 // This is example is too small to create commons chunks 88 | } 89 | } 90 | }, 91 | runtimeChunk: "single", 92 | minimizer: [ 93 | new UglifyJsPlugin({ 94 | test: /\.js(\?.*)?$/i 95 | }), 96 | ] 97 | } 98 | }); -------------------------------------------------------------------------------- /src/Dedoo/reconciliation.ts: -------------------------------------------------------------------------------- 1 | // 这就是我们现在要做的,我们需要将在render函数上收到的元素与我 2 | // 们提交给DOM的最后一棵纤维树进行比较 3 | // currentRoot 与 alternate 对比 4 | 5 | export let currentRoot: FiberNextWork = null; 6 | export let deletions: null | DedooFiber[] = null; 7 | 8 | export function getCurrentRoot() { 9 | return currentRoot; 10 | } 11 | 12 | export function setCurrentRoot(val: FiberNextWork) { 13 | currentRoot = val; 14 | } 15 | 16 | export function setDeletions (val: null | DedooFiber[] ) { 17 | deletions = val; 18 | } 19 | 20 | // 调和子元素 (增删改)的数据更新 21 | // ! 在这里,我们将旧纤维与新元素进行协调。 22 | // wipFiber 当前父元素的 fiber 23 | // elements 子元素的dom数据 24 | export function reconcileChildren(wipFiber: DedooFiber, elements: DedooElement[]) { 25 | let index = 0; 26 | let olderFiber: DedooFiber | null = wipFiber?.alternate?.child || null; 27 | 28 | let prevSibling: DedooFiber | null = null; 29 | 30 | // 循环生成一个 fiber 链表 31 | while ( 32 | index < elements.length || 33 | olderFiber != null 34 | ) { 35 | const element = elements[index]; 36 | let newFiber: DedooFiber | null = null 37 | 38 | // TODO compare oldFiber to element 39 | 40 | const sameType = 41 | olderFiber && 42 | element && 43 | element.type === olderFiber.type; 44 | 45 | if (sameType) { 46 | // TODO update the node 47 | newFiber = { 48 | type: (olderFiber as DedooFiber)?.type, 49 | props: (element as DedooFiber)?.props, 50 | dom: (olderFiber as DedooFiber)?.dom, 51 | parent: wipFiber, 52 | alternate: olderFiber, 53 | effectTag: 'UPDATE' 54 | } 55 | } 56 | 57 | if (element && !sameType) { 58 | // TODO add node 59 | newFiber = { 60 | type: element.type, 61 | props: element.props, 62 | dom: null, 63 | parent: wipFiber, 64 | alternate: null, 65 | effectTag: 'PLACEMENT' 66 | } 67 | } 68 | 69 | if (olderFiber && !sameType) { 70 | // TODO remove oldFiber node 71 | olderFiber.effectTag = 'DELETE', 72 | deletions?.push(olderFiber); 73 | } 74 | 75 | if (olderFiber) { 76 | olderFiber = olderFiber.sibling || null; 77 | } 78 | 79 | // 对于 newFiber 的处理 80 | // 如果 index 是第一个,则 newFiber 可以理解为是 fiber 的 子元素 81 | // 除了第一个以外的,则属于前一个 fiber 的兄弟节点关系 sibling 82 | // ! 将其添加到纤维树中,将其设置为孩子还是兄弟姐妹,具体取决于它是否是第一个孩子。 83 | if (index === 0) { 84 | wipFiber.child = newFiber; 85 | } else if (element) { 86 | // 此时 prevSibling 已经有值了 87 | (prevSibling as DedooFiber).sibling = newFiber; 88 | } 89 | 90 | // 基于 newFiber 设置新的 prevSibling,用于设置 fiber 的 sibling 91 | // linkList 操作 92 | prevSibling = newFiber; 93 | console.info('newFiber', index, newFiber); 94 | index ++; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /config/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | 6 | const resolve = (dir) => { 7 | return path.resolve(__dirname, dir) 8 | } 9 | 10 | const devMode = process.env.NODE_ENV === "development" 11 | 12 | module.exports = { 13 | plugins: [ 14 | new HtmlWebpackPlugin({ 15 | filename: 'index.html', 16 | template: 'index.html', 17 | inject: true, 18 | title: "build-your-own-react", 19 | favicon: 'src/assets/favicon.ico', 20 | minify: { 21 | removeComments: true 22 | } 23 | }), 24 | new MiniCssExtractPlugin ({ 25 | filename: "css/[name]-[hash].css", 26 | chunkFilename: "css/[name]-[hash].css" 27 | }), 28 | ], 29 | 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.(js|jsx|ts|tsx)$/, 34 | loader: 'babel-loader', 35 | query: { 36 | compact: false 37 | } 38 | }, 39 | { 40 | test: /\.(c)ss$/, 41 | use: [ 42 | devMode ? 'style-loader' : MiniCssExtractPlugin.loader, 43 | { 44 | loader: 'css-loader', 45 | options: { 46 | modules:true 47 | } 48 | }, 49 | { 50 | loader:"postcss-loader", 51 | options: { 52 | plugins: () => [ 53 | require('autoprefixer')() 54 | ] 55 | } 56 | }, 57 | ] 58 | }, 59 | { 60 | test: /\.less$/, 61 | use: [ 62 | devMode ? 'style-loader' : MiniCssExtractPlugin.loader, 63 | "css-loader", 64 | "less-loader", 65 | { 66 | loader: 'style-resources-loader', 67 | options: { 68 | patterns: path.resolve(__dirname, './../src/styles/val.less') 69 | } 70 | } 71 | ], 72 | }, 73 | { 74 | test: /\.(ttf|eot|woff|woff2)$/, 75 | use: [ 76 | { 77 | loader: 'url-loader' 78 | } 79 | ] 80 | }, 81 | { 82 | test: /\.(png|jpg|gif)$/, 83 | use: [ 84 | { 85 | loader: 'url-loader', 86 | options: { 87 | limit: 8192 88 | } 89 | } 90 | ] 91 | } 92 | ] 93 | }, 94 | 95 | resolve: { 96 | alias: { 97 | '@api': resolve('./../src/api'), 98 | '@assets': resolve('./../src/assets'), 99 | '@components': resolve('./../src/components'), 100 | '@constance': resolve('./../src/constance'), 101 | '@store': resolve('./../src/store'), 102 | '@styles': resolve('./../src/styles'), 103 | '@pages': resolve('./../src/pages'), 104 | '@utils': resolve('./../src/utils'), 105 | '@router': resolve('./../src/router') 106 | }, 107 | extensions: ['.tsx', '.ts', '.jsx', '.js'] 108 | }, 109 | } 110 | -------------------------------------------------------------------------------- /src/Dedoo/commit.ts: -------------------------------------------------------------------------------- 1 | import { deletions, setCurrentRoot } from "./reconciliation"; 2 | import { isEvent, isGone, isNew, isProperty } from "./utils"; 3 | 4 | // 当前 渲染到的 fiber信息 5 | export let wipRoot: FiberNextWork = null; 6 | export let currentRoot: FiberNextWork = null; 7 | 8 | export function setWipRoot(val: FiberNextWork) { 9 | console.info('val', val) 10 | wipRoot = val; 11 | } 12 | 13 | export function getWipRoot() { 14 | return wipRoot; 15 | } 16 | 17 | export function commitRoot() { 18 | // 移除需要删除的元素 19 | deletions?.forEach(commitWork); 20 | 21 | // TODO add nodes to dom 22 | // 从根root开始commit 一步步往下 23 | if (wipRoot?.child) { 24 | commitWork(wipRoot?.child); 25 | } 26 | 27 | // 存储当前的 fiber 信息 默认是 // !根元素 28 | 29 | // 因此,在完成提交之后,我们需要 30 | // ! 保存对“我们提交给DOM的最后一棵纤维树”的引用。 31 | // 我们称它为currentRoot 32 | console.info('wipRootwipRootwipRoot', wipRoot) 33 | setCurrentRoot(wipRoot); 34 | 35 | wipRoot = null 36 | } 37 | 38 | // 递归注入到 wipRoot 的子元素上 39 | export function commitWork(fiber: FiberNextWork) { 40 | // 不存在则结束 41 | if (!fiber) { 42 | return; 43 | } 44 | 45 | // 由于可能传递的是 函数 模版, 现在我们有了没有DOM节点的光纤,我们需要更改两件事。 46 | // 首先,要找到DOM节点的父节点,我们需要沿着光纤树向上移动,直到找到带有DOM节点的光纤。 47 | let domParentFiber = fiber.parent; 48 | while(!domParentFiber.dom) { 49 | domParentFiber = domParentFiber.parent; 50 | } 51 | 52 | // 拿到 通用的父元素信息 53 | const domParent = domParentFiber.dom; 54 | // 将当前 fiber的 dom 注入到父元素 55 | // fiber.dom && domParent?.appendChild(fiber.dom); 56 | if ( 57 | // ! 新增 58 | fiber.effectTag === 'PLACEMENT' && 59 | fiber.dom !== null 60 | ) { 61 | domParent?.appendChild(fiber.dom); 62 | } else if ( 63 | // ! 更新 64 | fiber.effectTag === 'UPDATE' && 65 | fiber.dom !== null 66 | ) { 67 | updateDom( 68 | fiber.dom, 69 | fiber.alternate?.props || {}, 70 | fiber.props || {} 71 | ) 72 | } else if ( 73 | // ! 删除 74 | fiber.effectTag === 'DELETE' && 75 | fiber.dom 76 | ) { 77 | commitDeletion(fiber, domParent); 78 | } 79 | 80 | // 继续其子元素 的重复动作 81 | fiber.child && commitWork(fiber.child); 82 | // 继续其兄弟元素的重复动作 83 | fiber.sibling && commitWork(fiber.sibling); 84 | } 85 | 86 | function commitDeletion (fiber: DedooFiber, domParent: FiberDom) { 87 | // 由于可能传递的是 函数 模版 88 | // 在删除节点时,我们还需要继续操作,直到找到带有DOM节点的子节点为止 89 | if (fiber.dom) { 90 | // 如果存在fiber dom 直接 删除 91 | domParent?.removeChild(fiber.dom); 92 | } else { 93 | // 向下寻找 直到找到带有DOM节点的子节点为止 94 | fiber.child && commitDeletion(fiber.child, domParent); 95 | } 96 | } 97 | 98 | // 更新 dom 信息 99 | // event 100 | // props 101 | export function updateDom ( 102 | dom: FiberDom, 103 | prevProps: DedooElementProps, 104 | nextProps: DedooElementProps 105 | ) { 106 | // TODO remove or change eventLister 107 | Object.keys(prevProps) 108 | .filter(isEvent) 109 | .filter(key => ( 110 | // 如果新的props 不存在这个key 或者 111 | !(key in nextProps) || 112 | isNew(prevProps, nextProps)(key) 113 | )) 114 | .forEach(name => { 115 | const eventName = name.toLocaleLowerCase().substring(2); 116 | dom.removeEventListener( 117 | eventName, 118 | prevProps[name] 119 | ); 120 | }) 121 | 122 | // remove old prop 123 | Object.keys(prevProps) 124 | .filter(isProperty) 125 | .filter(isGone(prevProps, nextProps)) 126 | .forEach(name => { 127 | dom[name] = ''; 128 | }) 129 | 130 | // set new changed prop 131 | Object.keys(nextProps) 132 | .filter(isProperty) 133 | .filter(isNew(prevProps, nextProps)) 134 | .forEach(name => { 135 | dom[name] = nextProps[name]; 136 | }) 137 | // TODO add event listeners 138 | Object.keys(nextProps) 139 | .filter(isEvent) 140 | .filter(isNew(prevProps, nextProps)) 141 | .forEach(name => { 142 | const eventName = name 143 | .toLocaleLowerCase() 144 | .substring(2); 145 | dom.addEventListener( 146 | eventName, 147 | nextProps[name] 148 | ); 149 | }) 150 | } 151 | -------------------------------------------------------------------------------- /src/Dedoo/concurrent.ts: -------------------------------------------------------------------------------- 1 | type RequestIdleCallbackHandle = any; 2 | type RequestIdleCallbackOptions = { 3 | timeout: number; 4 | }; 5 | type RequestIdleCallbackDeadline = { 6 | readonly didTimeout: boolean; 7 | timeRemaining: (() => number); 8 | }; 9 | 10 | declare global { 11 | interface Window { 12 | requestIdleCallback: (( 13 | callback: ((deadline: RequestIdleCallbackDeadline) => void), 14 | opts?: RequestIdleCallbackOptions, 15 | ) => RequestIdleCallbackHandle); 16 | cancelIdleCallback: ((handle: RequestIdleCallbackHandle) => void); 17 | } 18 | } 19 | 20 | import { commitRoot, getWipRoot } from "./commit"; 21 | import { reconcileChildren } from "./reconciliation"; 22 | /** 23 | * ! render 函数需要创建 root fiber 且 设置为 nextUnitOfWork 24 | * ! 剩下的函数在 performUnitOfWork 中执行, 每一个fiber 会做三件事 25 | * ! - 将元素添加至 dom 26 | * ! - 为元素的子元素创建 fiber 27 | * ! - 选择下一个工作单元 28 | */ 29 | 30 | /** 31 | * ================== 32 | * HTML 33 | * ================== 34 | *
35 | *

36 | * this is H1 37 | *

38 | * this is p 39 | *

40 | * 41 | * this is a 42 | * 43 | *

44 | *

45 | * this is H2 46 | *

47 | *
48 | * ================== 49 | * 50 | * ================== 51 | * TREE 52 | * ================== 53 | * root 54 | * | 55 | * div 56 | * | \ 57 | * h1 -- h2 58 | * | \ 59 | * p -- a 60 | * 61 | * 62 | * 过程 63 | * 先 root 64 | * 然后到 root 的子元素 div 65 | * 再到 div 的子元素 p 66 | * 此时 p 已经没有子元素的,这时候找到 p 的同级兄弟元素 p.sibling --> a 67 | * 如果 a 也没有子元素此时则去找到父元素的兄弟节点,如果父元素没有兄弟节点,则查找父元素的父元素的兄弟元素 68 | * 一直如此,直到 root 69 | * 结束 70 | * 71 | * ! 当我们完成对光纤的工作时,如果有子级,则光纤将成为下一个工作单元 72 | * ! 如果光纤没有孩子,我们将兄弟姐妹作为下一个工作单位 73 | * ! 如果光纤既没有孩子也没有兄弟姐妹,那么我们去“叔叔”:父母的兄弟姐妹。就像示例中的a和h2光纤一样 74 | * ! 另外,如果父母没有兄弟姐妹,我们会不断检查父母,直到找到有兄弟姐妹的父母,或者直到找到根。如果到达根目录,则意味着我们已经完成了此渲染的所有工作。 75 | */ 76 | 77 | import { createDom } from "./render"; 78 | 79 | // 下一个需要执行的任务 80 | let nextUnitOfWork: FiberNextWork = null; 81 | 82 | export function setNextUnitOfWork (val: any) { 83 | nextUnitOfWork = val; 84 | }; 85 | 86 | export function getNextUnitOfWork () { 87 | return nextUnitOfWork; 88 | } 89 | 90 | // 循环渲染函数 91 | // 在渲染函数中,将nextUnitOfWork设置为纤维树的根。 92 | // 然后,当浏览器准备就绪时,它将调用我们的workLoop,我们将开始在根目录上工作 93 | export function workLoop(deadLine: RequestIdleCallbackDeadline) { 94 | let shouldYield = false; // 是否要挂起 95 | 96 | // 如果存在任务 且 未暂停 97 | while (nextUnitOfWork && !shouldYield) { 98 | // todo 99 | // 执行任务 100 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork); 101 | } 102 | 103 | shouldYield = deadLine.timeRemaining() < 1; 104 | 105 | // 如果没有后续任务,且wipRoot存在,则执行 commit动作; 106 | if (!nextUnitOfWork && getWipRoot()) { 107 | commitRoot() 108 | } 109 | 110 | window.requestIdleCallback(workLoop); 111 | }; 112 | 113 | window.requestIdleCallback(workLoop); 114 | 115 | // 创建工作单元 116 | // 执行工作 117 | // 并返回下一个工作单元 118 | export function performUnitOfWork(fiber: DedooFiber): FiberNextWork { 119 | // 判断是否函数组件 120 | const isFunctionComponent = fiber.type instanceof Function; 121 | 122 | if (isFunctionComponent) { 123 | // 函数组件操作 124 | updateFunctionComponent(fiber); 125 | } else { 126 | // 常规组件操作 127 | updateHostComponent(fiber); 128 | } 129 | 130 | // ! return next unit of work 131 | // 如果任务操作完成之后,则进行下一个任务的 fiber 信息 132 | // 如果有子节点,则返回子元素 133 | // ! 最后,我们搜索下一个工作单元。我们首先尝试与孩子,然后与兄弟姐妹,然后与叔叔,依此类推 134 | if (fiber.child) { 135 | return fiber.child; 136 | } 137 | 138 | let nextFiber = fiber; 139 | // 这个循环用于 sibling,或者parent 查找以及 140 | while(nextFiber) { 141 | // 如果有同级fiber,选择兄弟fiber 用于下一个工作单元 142 | if (nextFiber.sibling) { 143 | return nextFiber.sibling; 144 | } 145 | // 如果没有这一层的则直接返回父元素 146 | nextFiber = nextFiber.parent; 147 | } 148 | return null; 149 | } 150 | 151 | // ! hooks 152 | // 我们需要在调用函数组件之前初始化一些全局变量 153 | // 以便可以在useState函数中使用它们 154 | let wipFiber: DedooFiber | null = null 155 | export function getWipFiber() { return wipFiber }; 156 | let hookIndex: number | null = null 157 | export function getHookIndex() { return hookIndex || 0 }; 158 | export function setHookIndex(val: number | null) { hookIndex = val }; 159 | 160 | function updateFunctionComponent(fiber: DedooFiber) { 161 | // 首先,我们设置正在进行的工作纤维 162 | wipFiber = fiber; 163 | // 我们还向光纤添加了一个hooks数组,以支持在同一组件中多次调用useState 164 | // 并且我们跟踪当前的钩子索引。 165 | hookIndex = 0; 166 | wipFiber.hooks = []; 167 | 168 | const children = [(fiber.type as DedooNode)(fiber.props || {})] as DedooElement[]; 169 | reconcileChildren(fiber, children); 170 | } 171 | 172 | function updateHostComponent(fiber: DedooFiber) { 173 | // ? add dom node 174 | // ? create new fibers 175 | // ? return next unit of work 176 | // 首先,我们创建一个新节点并将其附加到DOM 177 | // 我们在fiber.dom属性中跟踪DOM节点 178 | // ! add dom node 179 | if (!fiber.dom) { 180 | // console.info('fiber add ', fiber) 181 | fiber.dom = createDom(fiber); 182 | } 183 | 184 | // 元素加载到父元素中 185 | // ! 每次处理元素时,我们都会向DOM添加一个新节点。而且,请记住,在完成渲染整个树之前, 186 | // ! 浏览器可能会中断我们的工作。在这种情况下,用户将看到不完整的UI。而且我们不想要那样。 187 | // ! 所以这段代码被注释 188 | // if (fiber.parent) { 189 | // fiber?.parent?.dom?.appendChild(fiber.dom); 190 | // } 191 | 192 | // 元素的子元素 193 | // 为每个孩子创建一个新的纤维 194 | // ! create new fibers 195 | const elements = fiber.props?.children || []; 196 | 197 | // reconcileChildren 协调 198 | reconcileChildren(fiber, elements); 199 | } 200 | -------------------------------------------------------------------------------- /src/rest.jsx: -------------------------------------------------------------------------------- 1 | function createElement(type, props, ...children) { 2 | return { 3 | type, 4 | props: { 5 | ...props, 6 | children: children.map(child => 7 | typeof child === "object" 8 | ? child 9 | : createTextElement(child) 10 | ), 11 | }, 12 | } 13 | } 14 | 15 | function createTextElement(text) { 16 | return { 17 | type: "TEXT_ELEMENT", 18 | props: { 19 | nodeValue: text, 20 | children: [], 21 | }, 22 | } 23 | } 24 | 25 | function createDom(fiber) { 26 | const dom = 27 | fiber.type == "TEXT_ELEMENT" 28 | ? document.createTextNode("") 29 | : document.createElement(fiber.type) 30 | updateDom(dom, {}, fiber.props) 31 | return dom 32 | } 33 | 34 | const isEvent = key => key.startsWith("on") 35 | const isProperty = key => 36 | key !== "children" && !isEvent(key) 37 | const isNew = (prev, next) => key => 38 | prev[key] !== next[key] 39 | const isGone = (prev, next) => key => !(key in next) 40 | function updateDom(dom, prevProps, nextProps) { 41 | //Remove old or changed event listeners 42 | Object.keys(prevProps) 43 | .filter(isEvent) 44 | .filter( 45 | key => 46 | !(key in nextProps) || 47 | isNew(prevProps, nextProps)(key) 48 | ) 49 | .forEach(name => { 50 | const eventType = name 51 | .toLowerCase() 52 | .substring(2) 53 | dom.removeEventListener( 54 | eventType, 55 | prevProps[name] 56 | ) 57 | }) 58 | // Remove old properties 59 | Object.keys(prevProps) 60 | .filter(isProperty) 61 | .filter(isGone(prevProps, nextProps)) 62 | .forEach(name => { 63 | dom[name] = "" 64 | }) 65 | // Set new or changed properties 66 | Object.keys(nextProps) 67 | .filter(isProperty) 68 | .filter(isNew(prevProps, nextProps)) 69 | .forEach(name => { 70 | dom[name] = nextProps[name] 71 | }) 72 | // Add event listeners 73 | Object.keys(nextProps) 74 | .filter(isEvent) 75 | .filter(isNew(prevProps, nextProps)) 76 | .forEach(name => { 77 | const eventType = name 78 | .toLowerCase() 79 | .substring(2) 80 | dom.addEventListener( 81 | eventType, 82 | nextProps[name] 83 | ) 84 | }) 85 | } 86 | function commitRoot() { 87 | deletions.forEach(commitWork) 88 | commitWork(wipRoot.child) 89 | console.info('wipRoot', wipRoot) 90 | currentRoot = wipRoot 91 | wipRoot = null 92 | } 93 | function commitWork(fiber) { 94 | if (!fiber) { 95 | return 96 | } 97 | let domParentFiber = fiber.parent 98 | while (!domParentFiber.dom) { 99 | domParentFiber = domParentFiber.parent 100 | } 101 | const domParent = domParentFiber.dom 102 | if ( 103 | fiber.effectTag === "PLACEMENT" && 104 | fiber.dom != null 105 | ) { 106 | domParent.appendChild(fiber.dom) 107 | } else if ( 108 | fiber.effectTag === "UPDATE" && 109 | fiber.dom != null 110 | ) { 111 | updateDom( 112 | fiber.dom, 113 | fiber.alternate.props, 114 | fiber.props 115 | ) 116 | } else if (fiber.effectTag === "DELETION") { 117 | commitDeletion(fiber, domParent) 118 | } 119 | commitWork(fiber.child) 120 | commitWork(fiber.sibling) 121 | } 122 | 123 | function commitDeletion(fiber, domParent) { 124 | if (fiber.dom) { 125 | domParent.removeChild(fiber.dom) 126 | } else { 127 | commitDeletion(fiber.child, domParent) 128 | } 129 | } 130 | 131 | function render(element, container) { 132 | wipRoot = { 133 | dom: container, 134 | props: { 135 | children: [element], 136 | }, 137 | alternate: currentRoot, 138 | } 139 | deletions = [] 140 | nextUnitOfWork = wipRoot 141 | } 142 | 143 | let nextUnitOfWork = null 144 | let currentRoot = null 145 | let wipRoot = null 146 | let deletions = null 147 | 148 | function workLoop(deadline) { 149 | let shouldYield = false 150 | while (nextUnitOfWork && !shouldYield) { 151 | nextUnitOfWork = performUnitOfWork( 152 | nextUnitOfWork 153 | ) 154 | shouldYield = deadline.timeRemaining() < 1 155 | } 156 | 157 | if (!nextUnitOfWork && wipRoot) { 158 | commitRoot() 159 | } 160 | requestIdleCallback(workLoop) 161 | } 162 | 163 | requestIdleCallback(workLoop) 164 | function performUnitOfWork(fiber) { 165 | const isFunctionComponent = 166 | fiber.type instanceof Function 167 | if (isFunctionComponent) { 168 | updateFunctionComponent(fiber) 169 | } else { 170 | updateHostComponent(fiber) 171 | } 172 | if (fiber.child) { 173 | return fiber.child 174 | } 175 | let nextFiber = fiber 176 | while (nextFiber) { 177 | if (nextFiber.sibling) { 178 | return nextFiber.sibling 179 | } 180 | nextFiber = nextFiber.parent 181 | } 182 | } 183 | let wipFiber = null 184 | let hookIndex = null 185 | function updateFunctionComponent(fiber) { 186 | wipFiber = fiber 187 | hookIndex = 0 188 | wipFiber.hooks = [] 189 | const children = [fiber.type(fiber.props)] 190 | reconcileChildren(fiber, children) 191 | } 192 | function useState(initial) { 193 | const oldHook = 194 | wipFiber.alternate && 195 | wipFiber.alternate.hooks && 196 | wipFiber.alternate.hooks[hookIndex] 197 | console.info('oldHook', oldHook) 198 | const hook = { 199 | state: oldHook ? oldHook.state : initial, 200 | queue: [], 201 | } 202 | const actions = oldHook ? oldHook.queue : [] 203 | actions.forEach(action => { 204 | hook.state = action(hook.state) 205 | }) 206 | const setState = action => { 207 | console.info('hook.queue', hook.queue); 208 | hook.queue.push(action) 209 | console.info('getCurrentRoot()', currentRoot); 210 | wipRoot = { 211 | dom: currentRoot.dom, 212 | props: currentRoot.props, 213 | alternate: currentRoot, 214 | } 215 | nextUnitOfWork = wipRoot 216 | console.info('getCurrentRoot()', wipRoot); 217 | deletions = [] 218 | } 219 | wipFiber.hooks.push(hook) 220 | hookIndex++ 221 | return [hook.state, setState] 222 | } 223 | function updateHostComponent(fiber) { 224 | if (!fiber.dom) { 225 | fiber.dom = createDom(fiber) 226 | } 227 | reconcileChildren(fiber, fiber.props.children) 228 | } 229 | function reconcileChildren(wipFiber, elements) { 230 | let index = 0 231 | let oldFiber = 232 | wipFiber.alternate && wipFiber.alternate.child 233 | let prevSibling = null 234 | while ( 235 | index < elements.length || 236 | oldFiber != null 237 | ) { 238 | const element = elements[index] 239 | let newFiber = null 240 | const sameType = 241 | oldFiber && 242 | element && 243 | element.type == oldFiber.type 244 | if (sameType) { 245 | newFiber = { 246 | type: oldFiber.type, 247 | props: element.props, 248 | dom: oldFiber.dom, 249 | parent: wipFiber, 250 | alternate: oldFiber, 251 | effectTag: "UPDATE", 252 | } 253 | } 254 | if (element && !sameType) { 255 | newFiber = { 256 | type: element.type, 257 | props: element.props, 258 | dom: null, 259 | parent: wipFiber, 260 | alternate: null, 261 | effectTag: "PLACEMENT", 262 | } 263 | } 264 | if (oldFiber && !sameType) { 265 | oldFiber.effectTag = "DELETION" 266 | deletions.push(oldFiber) 267 | } 268 | if (oldFiber) { 269 | oldFiber = oldFiber.sibling 270 | } 271 | if (index === 0) { 272 | wipFiber.child = newFiber 273 | } else if (element) { 274 | prevSibling.sibling = newFiber 275 | } 276 | prevSibling = newFiber 277 | console.info('newFiber', index, newFiber); 278 | index++ 279 | } 280 | } 281 | const Didact = { 282 | createElement, 283 | render, 284 | useState, 285 | } 286 | /** @jsx Didact.createElement */ 287 | function Counter() { 288 | const [state, setState] = useState(1); 289 | return ( 290 |
291 | bar 292 |

你好

293 |

{state}

294 |

123123

295 | {/* */} 298 | 301 | 302 |
303 | ) 304 | } 305 | const element = 306 | const container = document.getElementById("root") 307 | Didact.render(element, container) 308 | --------------------------------------------------------------------------------