a small incremental UI library for the web
javascript web ui
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add Noir.memo()

garrison bc1ad932 8a5b50b3

+88 -11
+8 -1
demos/todo/todo.tsx
··· 142 142 <div class="container"> 143 143 <ItemInput addItem={name => setItems(items => [...items, makeItem(name)])} /> 144 144 <div class="info"> 145 - <div>{completeCount} items completed</div> 145 + <Status completeCount={completeCount} /> 146 146 <button disabled={completeCount === 0} onClick={clearComplete}>Clear completed</button> 147 147 </div> 148 148 <div ··· 163 163 </div> 164 164 ); 165 165 } 166 + 167 + const Status = Noir.memo(({completeCount}) => { 168 + const [bold, setBold] = useState(false); 169 + useStyle('.bold { font-weight: bold; }'); 170 + 171 + return <div class={bold && 'bold'} onClick={() => setBold(b => !b)}>{completeCount} items completed</div>; 172 + }); 166 173 167 174 function ItemInput({addItem}) { 168 175 const [text, setText] = useState('');
+6 -5
js/commit.ts
··· 19 19 } 20 20 21 21 function commit(fiber) { 22 - commitWork(fiber); 22 + fiber.current = true; 23 + if (fiber.alternate) fiber.alternate.current = false; 23 24 24 - if (fiber.child) commit(fiber.child); 25 + if (!fiber.wasMemoized) { 26 + commitWork(fiber); 27 + if (fiber.child) commit(fiber.child); 28 + } 25 29 if (fiber.sibling) commit(fiber.sibling); 26 30 } 27 31 28 32 function commitWork(fiber) { 29 - fiber.current = true; 30 - if (fiber.alternate) fiber.alternate.current = false; 31 - 32 33 if (fiber.tag == HostTextFiber) { 33 34 if (fiber.effect === 'placement') commitTextPlacement(fiber); 34 35 else commitTextUpdate(fiber);
+6
js/fiber.ts
··· 51 51 52 52 deletedChildren: null, 53 53 54 + bustMemo: false, 55 + wasMemoized: false, 56 + 54 57 alternate: null, 55 58 }; 56 59 } ··· 80 83 fiber.sibling = fromFiber.sibling; 81 84 82 85 fiber.deletedChildren = null; 86 + 87 + fiber.bustMemo = false; 88 + fiber.wasMemoized = false; 83 89 84 90 fiber.alternate = fromFiber; 85 91
+35
js/memo.ts
··· 1 + import { getCurrentFiber } from './current.ts'; 2 + 3 + const MEMOIZED = Symbol('MEMOIZED'); 4 + export { MEMOIZED }; 5 + 6 + export function memo(component, arePropsEqual = compareProps) { 7 + return (newProps) => { 8 + const currentFiber = getCurrentFiber(); 9 + 10 + if (currentFiber.bustMemo || !currentFiber.alternate || !arePropsEqual(currentFiber.alternate.props, newProps)) { 11 + return component(newProps); 12 + } 13 + else { 14 + return MEMOIZED; 15 + } 16 + }; 17 + } 18 + 19 + function compareProps(oldProps, newProps) { 20 + if (oldProps === undefined || newProps === undefined) { 21 + return oldProps === newProps; 22 + } 23 + 24 + const oldKeys = Object.keys(oldProps); 25 + const newKeys = Object.keys(newProps); 26 + if (oldKeys.length !== newKeys.length) return false; 27 + 28 + for (const k of oldKeys) { 29 + if (!Object.is(oldProps[k], newProps[k])) return false; 30 + } 31 + for (const k of newKeys) { 32 + if (!Object.is(oldProps[k], newProps[k])) return false; 33 + } 34 + return true; 35 + }
+6 -1
js/noir.ts
··· 1 1 import { FiberTag, makeFiber, reconcileChildren } from './fiber.ts'; 2 2 import { startRenderLoop, renderFiberTree } from './render.ts'; 3 3 import { setCurrentFiber, getCurrentScope } from './current.ts'; 4 + import { memo } from './memo.ts'; 5 + 6 + export { memo }; 4 7 5 8 const { 6 9 RootFiber, ··· 11 14 export function createElement(type, inputProps, ...children) { 12 15 const scope = getCurrentScope(); 13 16 14 - const props = {...inputProps, children}; 17 + // TODO: maybe don't clone input? 18 + const props = {...inputProps}; 19 + if (children.length > 0) props.children = children; 15 20 if (scope) props[scope] = true; 16 21 17 22 return {
+27 -4
js/render.ts
··· 2 2 import { commitTree } from './commit.ts'; 3 3 import { setCurrentFiber, setCurrentScope } from './current.ts'; 4 4 import { commitSideEffects, resetSideEffectQueue } from './effects.ts'; 5 + import { MEMOIZED } from './memo.ts'; 5 6 import { assert } from './utils.ts'; 6 7 7 8 const { ··· 52 53 WIP_FIBER = wipFiber; 53 54 NEXT_WORK_FIBER = wipFiber; 54 55 56 + // Bust memo on the root fiber of this render since it may be a re-render due to setState 57 + // (Note: bustMemo is always reset by cloneFiber on the next render) 58 + wipFiber.bustMemo = true; 59 + 55 60 performWork(); 56 61 commitTree(wipFiber); 57 62 commitSideEffects(); ··· 105 110 renderHostFiber(fiber); 106 111 } 107 112 108 - if (fiber.child) return fiber.child; 113 + if (!fiber.wasMemoized && fiber.child) return fiber.child; 109 114 110 115 let next = fiber; 111 116 while (true) { ··· 122 127 function renderFunctionFiber(fiber) { 123 128 setCurrentFiber(fiber); 124 129 fiber.pointer = 0; 125 - const elements = fiber.type(fiber.props); 130 + const result = fiber.type(fiber.props); 131 + 126 132 setCurrentFiber(null); 127 133 setCurrentScope(null); 128 134 129 - const firstNewChild = reconcileChildren(fiber, elements); 130 - fiber.child = firstNewChild; 135 + if (result === MEMOIZED) { 136 + fiber.wasMemoized = true; 137 + 138 + // A fiber with no alternate cannot have been memoized (no old props) 139 + assert(fiber.alternate); 140 + // Guaranteed by cloneFiber() 141 + assert(fiber.child === fiber.alternate.child); 142 + 143 + let child = fiber.child; 144 + while (child) { 145 + child.parent = fiber; 146 + child = child.sibling; 147 + } 148 + } 149 + else { 150 + const elements = result; 151 + const firstNewChild = reconcileChildren(fiber, elements); 152 + fiber.child = firstNewChild; 153 + } 131 154 } 132 155 133 156 function renderHostFiber(fiber) {