├── .github
└── funding.yml
├── .prettierrc
├── package.json
├── readme.md
└── didact.js
/.github/funding.yml:
--------------------------------------------------------------------------------
1 | patreon: pomber
2 | custom: https://www.paypal.me/pomber
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "printWidth": 52,
4 | "trailingComma": "es5"
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "didact",
3 | "version": "3.0.0",
4 | "description": "A didactic alternative to React",
5 | "license": "MIT",
6 | "repository": "pomber/didact",
7 | "author": "pomber",
8 | "main": "didact.js",
9 | "keywords": [
10 | "react"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | # Didact
4 |
5 | #### A DIY guide to build your own React
6 |
7 | This repository goes together with a [series of posts](https://engineering.hexacta.com/didact-learning-how-react-works-by-building-it-from-scratch-51007984e5c5) that explains how to build React from scratch step by step. **You can jump straight to [the last post](https://pomb.us/build-your-own-react) which is self-contained and includes everything.**
8 |
9 | | Blog Post | Code sample | Commits | Other languages |
10 | | ----------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------: |
11 | | [Introduction](https://engineering.hexacta.com/didact-learning-how-react-works-by-building-it-from-scratch-51007984e5c5) | | | |
12 | | [Rendering DOM elements](https://engineering.hexacta.com/didact-rendering-dom-elements-91c9aa08323b) | [codepen](https://codepen.io/pomber/pen/eWbwBq?editors=0010) | [diff](https://github.com/hexacta/didact/commit/fc4d360d91a1e68f0442d39dbce5b9cca5a08f24) | [中文](https://github.com/chinanf-boy/didact-explain#1-%E6%B8%B2%E6%9F%93dom%E5%85%83%E7%B4%A0) |
13 | | [Element creation and JSX](https://engineering.hexacta.com/didact-element-creation-and-jsx-d05171c55c56) | [codepen](https://codepen.io/pomber/pen/xdmoWE?editors=0010) | [diff](https://github.com/hexacta/didact/commit/15010f8e7b8b54841d1e2dd9eacf7b3c06b1a24b) | [中文](https://github.com/chinanf-boy/didact-explain#2-%E5%85%83%E7%B4%A0%E5%88%9B%E5%BB%BA%E5%92%8Cjsx) |
14 | | [Virtual DOM and reconciliation](https://engineering.hexacta.com/didact-instances-reconciliation-and-virtual-dom-9316d650f1d0) | [codepen](https://codepen.io/pomber/pen/WjLqYW?editors=0010) | [diff](https://github.com/hexacta/didact/commit/8eb7ffd6f5e210526fb4c274c4f60d609fe2f810) [diff](https://github.com/hexacta/didact/commit/6f5fdb7331ed77ba497fa5917d920eafe1f4c8dc) [diff](https://github.com/hexacta/didact/commit/35619a039d48171a6e6c53bd433ed049f2d718cb) | [中文](https://github.com/chinanf-boy/didact-explain#3-%E5%AE%9E%E4%BE%8B-%E5%AF%B9%E6%AF%94%E5%92%8C%E8%99%9A%E6%8B%9Fdom) |
15 | | [Components and State](https://engineering.hexacta.com/didact-components-and-state-53ab4c900e37) | [codepen](https://codepen.io/pomber/pen/RVqBrx) | [diff](https://github.com/hexacta/didact/commit/2e290ff5c486b8a3f361abcbc6e36e2c21db30b8) | [中文](https://github.com/chinanf-boy/didact-explain#4-%E7%BB%84%E4%BB%B6%E5%92%8C%E7%8A%B6%E6%80%81) |
16 | | [Fiber: Incremental reconciliation](https://engineering.hexacta.com/didact-fiber-incremental-reconciliation-b2fe028dcaec) (self-contained post) | [codepen](https://codepen.io/pomber/pen/veVOdd) | [diff](https://github.com/hexacta/didact/commit/6174a2289e69895acd8fc85abdc3aaff1ded9011) [diff](https://github.com/hexacta/didact/commit/accafb81e116a0569f8b7d70e5b233e14af999ad) | [中文](https://github.com/chinanf-boy/didact-explain#5-fibre-%E9%80%92%E5%A2%9E%E5%AF%B9%E6%AF%94) |
17 | | [The one with Hooks](https://pomb.us/build-your-own-react) (self-contained post) | [codesandbox](https://codesandbox.io/s/didact-8-21ost) | | |
18 |
19 | > Follow [@pomber](https://twitter.com/pomber) on twitter for updates.
20 |
21 | ## License
22 |
23 | The MIT License (MIT)
24 |
--------------------------------------------------------------------------------
/didact.js:
--------------------------------------------------------------------------------
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 |
31 | updateDom(dom, {}, fiber.props)
32 |
33 | return dom
34 | }
35 |
36 | const isEvent = key => key.startsWith("on")
37 | const isProperty = key =>
38 | key !== "children" && !isEvent(key)
39 | const isNew = (prev, next) => key =>
40 | prev[key] !== next[key]
41 | const isGone = (prev, next) => key => !(key in next)
42 | function updateDom(dom, prevProps, nextProps) {
43 | //Remove old or changed event listeners
44 | Object.keys(prevProps)
45 | .filter(isEvent)
46 | .filter(
47 | key =>
48 | !(key in nextProps) ||
49 | isNew(prevProps, nextProps)(key)
50 | )
51 | .forEach(name => {
52 | const eventType = name
53 | .toLowerCase()
54 | .substring(2)
55 | dom.removeEventListener(
56 | eventType,
57 | prevProps[name]
58 | )
59 | })
60 |
61 | // Remove old properties
62 | Object.keys(prevProps)
63 | .filter(isProperty)
64 | .filter(isGone(prevProps, nextProps))
65 | .forEach(name => {
66 | dom[name] = ""
67 | })
68 |
69 | // Set new or changed properties
70 | Object.keys(nextProps)
71 | .filter(isProperty)
72 | .filter(isNew(prevProps, nextProps))
73 | .forEach(name => {
74 | dom[name] = nextProps[name]
75 | })
76 |
77 | // Add event listeners
78 | Object.keys(nextProps)
79 | .filter(isEvent)
80 | .filter(isNew(prevProps, nextProps))
81 | .forEach(name => {
82 | const eventType = name
83 | .toLowerCase()
84 | .substring(2)
85 | dom.addEventListener(
86 | eventType,
87 | nextProps[name]
88 | )
89 | })
90 | }
91 |
92 | function commitRoot() {
93 | deletions.forEach(commitWork)
94 | commitWork(wipRoot.child)
95 | currentRoot = wipRoot
96 | wipRoot = null
97 | }
98 |
99 | function commitWork(fiber) {
100 | if (!fiber) {
101 | return
102 | }
103 |
104 | let domParentFiber = fiber.parent
105 | while (!domParentFiber.dom) {
106 | domParentFiber = domParentFiber.parent
107 | }
108 | const domParent = domParentFiber.dom
109 |
110 | if (
111 | fiber.effectTag === "PLACEMENT" &&
112 | fiber.dom != null
113 | ) {
114 | domParent.appendChild(fiber.dom)
115 | } else if (
116 | fiber.effectTag === "UPDATE" &&
117 | fiber.dom != null
118 | ) {
119 | updateDom(
120 | fiber.dom,
121 | fiber.alternate.props,
122 | fiber.props
123 | )
124 | } else if (fiber.effectTag === "DELETION") {
125 | commitDeletion(fiber, domParent)
126 | }
127 |
128 | commitWork(fiber.child)
129 | commitWork(fiber.sibling)
130 | }
131 |
132 | function commitDeletion(fiber, domParent) {
133 | if (fiber.dom) {
134 | domParent.removeChild(fiber.dom)
135 | } else {
136 | commitDeletion(fiber.child, domParent)
137 | }
138 | }
139 |
140 | function render(element, container) {
141 | wipRoot = {
142 | dom: container,
143 | props: {
144 | children: [element],
145 | },
146 | alternate: currentRoot,
147 | }
148 | deletions = []
149 | nextUnitOfWork = wipRoot
150 | }
151 |
152 | let nextUnitOfWork = null
153 | let currentRoot = null
154 | let wipRoot = null
155 | let deletions = null
156 |
157 | function workLoop(deadline) {
158 | let shouldYield = false
159 | while (nextUnitOfWork && !shouldYield) {
160 | nextUnitOfWork = performUnitOfWork(
161 | nextUnitOfWork
162 | )
163 | shouldYield = deadline.timeRemaining() < 1
164 | }
165 |
166 | if (!nextUnitOfWork && wipRoot) {
167 | commitRoot()
168 | }
169 |
170 | requestIdleCallback(workLoop)
171 | }
172 |
173 | requestIdleCallback(workLoop)
174 |
175 | function performUnitOfWork(fiber) {
176 | const isFunctionComponent =
177 | fiber.type instanceof Function
178 | if (isFunctionComponent) {
179 | updateFunctionComponent(fiber)
180 | } else {
181 | updateHostComponent(fiber)
182 | }
183 | if (fiber.child) {
184 | return fiber.child
185 | }
186 | let nextFiber = fiber
187 | while (nextFiber) {
188 | if (nextFiber.sibling) {
189 | return nextFiber.sibling
190 | }
191 | nextFiber = nextFiber.parent
192 | }
193 | }
194 |
195 | let wipFiber = null
196 | let hookIndex = null
197 |
198 | function updateFunctionComponent(fiber) {
199 | wipFiber = fiber
200 | hookIndex = 0
201 | wipFiber.hooks = []
202 | const children = [fiber.type(fiber.props)]
203 | reconcileChildren(fiber, children)
204 | }
205 |
206 | function useState(initial) {
207 | const oldHook =
208 | wipFiber.alternate &&
209 | wipFiber.alternate.hooks &&
210 | wipFiber.alternate.hooks[hookIndex]
211 | const hook = {
212 | state: oldHook ? oldHook.state : initial,
213 | queue: [],
214 | }
215 |
216 | const actions = oldHook ? oldHook.queue : []
217 | actions.forEach(action => {
218 | hook.state = action(hook.state)
219 | })
220 |
221 | const setState = action => {
222 | hook.queue.push(action)
223 | wipRoot = {
224 | dom: currentRoot.dom,
225 | props: currentRoot.props,
226 | alternate: currentRoot,
227 | }
228 | nextUnitOfWork = wipRoot
229 | deletions = []
230 | }
231 |
232 | wipFiber.hooks.push(hook)
233 | hookIndex++
234 | return [hook.state, setState]
235 | }
236 |
237 | function updateHostComponent(fiber) {
238 | if (!fiber.dom) {
239 | fiber.dom = createDom(fiber)
240 | }
241 | reconcileChildren(fiber, fiber.props.children)
242 | }
243 |
244 | function reconcileChildren(wipFiber, elements) {
245 | let index = 0
246 | let oldFiber =
247 | wipFiber.alternate && wipFiber.alternate.child
248 | let prevSibling = null
249 |
250 | while (
251 | index < elements.length ||
252 | oldFiber != null
253 | ) {
254 | const element = elements[index]
255 | let newFiber = null
256 |
257 | const sameType =
258 | oldFiber &&
259 | element &&
260 | element.type == oldFiber.type
261 |
262 | if (sameType) {
263 | newFiber = {
264 | type: oldFiber.type,
265 | props: element.props,
266 | dom: oldFiber.dom,
267 | parent: wipFiber,
268 | alternate: oldFiber,
269 | effectTag: "UPDATE",
270 | }
271 | }
272 | if (element && !sameType) {
273 | newFiber = {
274 | type: element.type,
275 | props: element.props,
276 | dom: null,
277 | parent: wipFiber,
278 | alternate: null,
279 | effectTag: "PLACEMENT",
280 | }
281 | }
282 | if (oldFiber && !sameType) {
283 | oldFiber.effectTag = "DELETION"
284 | deletions.push(oldFiber)
285 | }
286 |
287 | if (oldFiber) {
288 | oldFiber = oldFiber.sibling
289 | }
290 |
291 | if (index === 0) {
292 | wipFiber.child = newFiber
293 | } else if (element) {
294 | prevSibling.sibling = newFiber
295 | }
296 |
297 | prevSibling = newFiber
298 | index++
299 | }
300 | }
301 |
302 | const Didact = {
303 | createElement,
304 | render,
305 | useState,
306 | }
307 |
308 | /** @jsx Didact.createElement */
309 | function Counter() {
310 | const [state, setState] = Didact.useState(1)
311 | return (
312 | setState(c => c + 1)}>
313 | Count: {state}
314 |
315 | )
316 | }
317 | const element =
318 | const container = document.getElementById("root")
319 | Didact.render(element, container)
320 |
--------------------------------------------------------------------------------