├── 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 |
--------------------------------------------------------------------------------