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