├── .gitignore ├── .npmrc ├── README.md ├── assets ├── logo.png └── simple-demo.gif ├── example ├── index.html └── index.tsx ├── package.json ├── src ├── index.ts ├── model.ts ├── utils.ts └── vnode.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .rpt2_cache 3 | .idea 4 | .cache/ 5 | .DS_Store 6 | dist/ 7 | package-lock.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | https://registry.npmjs.org/ 2 | scope=waynecz -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |
7 | 8 |

9 | A concise version of React just good enough for building uncomplicated UI
deadly suitable for SDK which cares about the size
10 | 11 |

12 | 13 |
14 |
15 | 16 |
17 |
npm i @waynecz/react
18 |
19 | 20 |
21 |
22 | 23 |

Features

24 | 25 | - Basic JSX and Renderer with VirtualDOM(diff & patch) 26 | - Functional Component 27 | - Hooks like `useState` / `useEffect` 28 | 29 |
30 | 31 |

Todo

32 | 33 | - [ ] `useCallback` / `useMemo` ... 34 | 35 |
36 | 37 |

Demos

38 | 39 | - [Simple usage](https://codesandbox.io/s/2pkyw29ymp) 40 | - [Advanced: A Feedback UI](https://codesandbox.io/s/n7yvzjrkoj) 41 | 42 | ![](./assets/simple-demo.gif) 43 | 44 |
45 |
46 | 47 |

References

48 | 49 | - [《Gooact: React in 160 lines of JavaScript》](https://medium.com/@sweetpalma/gooact-react-in-160-lines-of-javascript-44e0742ad60f) by [Paul Marlow](https://github.com/sweetpalma) 50 | - [《React as a UI Runtime》](https://overreacted.io/react-as-a-ui-runtime/) by [Dan Abramov](https://overreacted.io) 51 | - [《从零开始实现一个 React》](https://github.com/hujiulong/blog/issues/4) by [Jiuling Hu](https://github.com/hujiulong) 52 | - [《React hooks: not magic, just arrays》](https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e) by [Rudi Yardley](https://github.com/ryardley) 53 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waynecz/tiny-react-with-hooks/172b93b85bb88d362f6f43068c01e4934c83a786/assets/logo.png -------------------------------------------------------------------------------- /assets/simple-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waynecz/tiny-react-with-hooks/172b93b85bb88d362f6f43068c01e4934c83a786/assets/simple-demo.gif -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Tiny React 11 | 12 | 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from '../src' 2 | import ReactDOM from '../src/vnode' 3 | 4 | function App() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ) 11 | } 12 | 13 | function Number() { 14 | const [number, setNumber] = useState(~~(Math.random() * 100)) 15 | const [dep, setDep] = useState('initial dep') 16 | 17 | useEffect(() => { 18 | console.log('number effect:', number) 19 | }, [dep]) 20 | 21 | useEffect(() => { 22 | console.log('dependencies effect:', dep) 23 | }, [number]) 24 | 25 | return ( 26 |
27 |
28 | 29 | setNumber(e.target.value)} /> 30 | 31 |
32 |

{dep}

33 | 34 |
35 | ) 36 | } 37 | 38 | ReactDOM.render(, document.getElementById('root')) 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@waynecz/react", 3 | "version": "0.1.2", 4 | "description": "Fundamental implementation of React with hooks & VDOM", 5 | "main": "dist/index", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "lint": "npx tslint src/**/*", 11 | "dev": "npx parcel ./example/index.html", 12 | "prebuild": "npm run lint && rimraf dist", 13 | "build": "npx tsc" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/waynecz/tiny-react-with-hooks.git" 18 | }, 19 | "keywords": [ 20 | "react" 21 | ], 22 | "author": "waynecz <451578533@qq.com>", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/waynecz/tiny-react-with-hooks/issues" 26 | }, 27 | "homepage": "https://github.com/waynecz/tiny-react-with-hooks#readme", 28 | "devDependencies": { 29 | "tslint": "^5.12.0", 30 | "typescript": "^3.2.2", 31 | "rimraf": "^2.6.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { isClass, isArrayEqual, isFunction, isArray } from './utils' 2 | import { patch } from './vnode' 3 | import { ReactElement, FunctionComponent, MemoCursor, Effect } from './model' 4 | 5 | let __ReactCurrentInstance = null 6 | 7 | let __ReactLatestStateCursor = 0 8 | const __ReactMemoizedStates: any[] = [] 9 | const __ReactStateSetters = [] 10 | 11 | let __ReactLatestEffectCursor = 0 12 | const __ReactMemoizedEffects: Effect[] = [] 13 | 14 | /** 15 | * This is the real Component in React, the Functional Component we write is 16 | * just an renderFunc of a component, besides Functional Component return a tree consist of ReactElements 17 | * real Component also store props, memorize those states and effect it used etc. 18 | * 19 | * 这个才是真正的组件,代码里写的 function 其实只是 Component 的 renderFunc(渲染函数), 20 | * renderFunc 返回一个由 ReactElement 组成的 tree 。 21 | * Component 实例会保存 props, 组件的 real DOM node 等必要信息, 22 | * 实例还记录了 renderFunc 所需的 states, effect 指针等。 23 | */ 24 | export class Component { 25 | // record how many states and which states this component used. 26 | // For example the value is [2,3,4,5], it says this Component invoke useState 4 times, generate 4 memorized states in __ReactMemoizedStates 27 | // and the states' index in __ReactMemoizedStates should be 2nd, 3rd, 4th... 28 | // 记录当前实例用了多少个、哪几个在 __ReactMemoizedStates 的状态 29 | // 例如值是 [2,3,4,5], 则说明了该组件内用了四个 useState, 30 | // 并且每个状态的位置对应在 __ReactMemoizedStates 数组中按顺序就是 第二个,第三个... 31 | private memoStatesCursors: MemoCursor[] = [] 32 | // ⚠️ every time useState been executed, this pointer will increase by step 1, the pointer is memoStatesCursors' index! 33 | // ⚠️ 每次组件内执行 useState 时这个指针就会 +1,这个指针的值是 memoStatesCursors 的 index!! 34 | private stateWalkerPointer = -1 35 | 36 | // record how many effects and which effects this component used 37 | // 记录当前实例用了多少个、哪几个在 __ReactMemoizedEffects 的 effect 回调 38 | private memoEffectsCursors: MemoCursor[] = [] 39 | // the same as stateWalkerPointer 40 | // 同 stateWalkerPointer 41 | private effectWalkerPointer = -1 42 | // cursors of active effect, will be invoked after mount 43 | // 记录首次渲染 或者 本次更新 里需要激活执行的 effect 回调,因为可能 effect 有第二个参数决定是否执行 44 | public activeEffectCallbacks: number[] = [] 45 | 46 | private renderFunc = null 47 | 48 | public base = null 49 | public isReactComponent = {} 50 | 51 | constructor(public props = {}) {} 52 | 53 | public _render(func) { 54 | this.renderFunc = func 55 | // reset activeEffectCallbacks array before every time renderFunc invoking 56 | // cuz they maybe different, depend on states' change 57 | // 每次执行渲染、更新渲染前,要重新开始收集需要激活的 effect 58 | this.activeEffectCallbacks.length = 0 59 | 60 | /** 61 | * Hooks like useState and useEffect would be invoked in renderFunc's execution. 62 | * States will take effect immediately. 63 | * But effectCallbacks run after rendered or pathed, 64 | * and only run active effectCallbacks if renderFunc called for patch 65 | * 在 rednderFunc 执行的时候 useState 和 useEffect 才会被真正执行 66 | * state 会立马返回值 67 | * 但是 effect 的回调是要等到组件 首次挂载、更新完后 再执行的,并且只跑那些激活的回调 68 | */ 69 | const vnode = this.renderFunc(this.props) 70 | 71 | // every time after finishing _render 72 | // reset walkers to -1 for next render 73 | // 每次执行完组件函数拨回这两个指针,因为下次渲染又是从头开始 74 | this.stateWalkerPointer = -1 75 | this.effectWalkerPointer = -1 76 | return vnode 77 | } 78 | 79 | public runActiveEffects() { 80 | this.activeEffectCallbacks.forEach(cursor => { 81 | const { callback, cleanup } = __ReactMemoizedEffects[cursor] 82 | if (!isFunction(callback)) return 83 | // try to cleanup last effect before run effecr callback 84 | // 在执行回调前先试着清理之前的 effect 85 | isFunction(cleanup) && cleanup!() 86 | 87 | const newCleanup = callback(this.base) 88 | if (newCleanup) { 89 | __ReactMemoizedEffects[cursor].cleanup = newCleanup 90 | } 91 | }) 92 | } 93 | 94 | public cleanupEffects() { 95 | this.memoEffectsCursors.forEach(({ value }) => { 96 | const effectHook = __ReactMemoizedEffects[value] 97 | 98 | const cleanup = effectHook.cleanup 99 | 100 | isFunction(cleanup) && cleanup() 101 | }) 102 | } 103 | 104 | public useState(initialValue) { 105 | this.stateWalkerPointer++ 106 | 107 | if (!this.memoStatesCursors[this.stateWalkerPointer]) { 108 | const memoState = initialValue 109 | 110 | __ReactMemoizedStates.push(memoState) 111 | __ReactStateSetters.push( 112 | createStateSetter(__ReactLatestStateCursor, this) 113 | ) 114 | 115 | let currentCursor: MemoCursor = { 116 | value: __ReactLatestStateCursor, 117 | __inited: true 118 | } 119 | 120 | this.memoStatesCursors.push(currentCursor) 121 | __ReactLatestStateCursor++ 122 | } 123 | 124 | const memoCursor = this.memoStatesCursors[this.stateWalkerPointer] 125 | 126 | const getter = __ReactMemoizedStates[memoCursor.value] 127 | const setter = __ReactStateSetters[memoCursor.value] 128 | 129 | return [getter, setter] 130 | } 131 | 132 | /** 133 | * Effect hook: https://reactjs.org/docs/hooks-effect.html 134 | * @param effectCallback method be invoked when component did mount, can return a cleanup function 135 | * @param dependiencies effect will only activate if the values in the list change. 136 | */ 137 | public useEffect( 138 | effectCallback: () => any, 139 | dependiencies: any[] | null = null 140 | ) { 141 | this.effectWalkerPointer++ 142 | 143 | if (!this.memoEffectsCursors[this.effectWalkerPointer]) { 144 | // first time render 145 | // 组件第一次执行时 146 | __ReactMemoizedEffects.push({ 147 | callback: effectCallback, 148 | cleanup: null, 149 | lastTimeDeps: isArray(dependiencies) 150 | ? dependiencies.map(dep => dep) 151 | : dependiencies 152 | }) 153 | 154 | let currentCursor: MemoCursor = { 155 | value: __ReactLatestEffectCursor, 156 | __inited: true 157 | } 158 | 159 | this.memoEffectsCursors.push(currentCursor) 160 | 161 | this.activeEffectCallbacks.push(__ReactLatestEffectCursor) 162 | 163 | __ReactLatestEffectCursor++ 164 | } else { 165 | // time trigger state setter 166 | // 组件更新时 167 | const cursor = this.memoEffectsCursors[this.effectWalkerPointer].value 168 | 169 | const memoEffect = __ReactMemoizedEffects[cursor] 170 | // reassign callback for refreshing closure 171 | // 一定要重新将 callback 赋值,因为闭包内的变量可能会更新!!! 172 | memoEffect.callback = effectCallback 173 | 174 | const shouldActive = 175 | memoEffect.lastTimeDeps === null || 176 | !isArrayEqual(memoEffect.lastTimeDeps, dependiencies) 177 | 178 | if (shouldActive) { 179 | this.activeEffectCallbacks.push(cursor) 180 | // update dependiencies 181 | memoEffect.lastTimeDeps = dependiencies 182 | } 183 | } 184 | } 185 | } 186 | 187 | export function setCurrentDispatcher(instance) { 188 | __ReactCurrentInstance = instance 189 | } 190 | 191 | function createStateSetter(__ReactLatestStateCursor, instance) { 192 | return function(newValue) { 193 | Promise.resolve().then(() => { 194 | // update new value into __ReactMemoizedStates 195 | __ReactMemoizedStates[__ReactLatestStateCursor] = newValue 196 | const { base, renderFunc } = instance 197 | setCurrentDispatcher(instance) 198 | const newVnode = instance._render(renderFunc) 199 | // patch current DOM node with newVnode 200 | patch(base, newVnode) 201 | 202 | instance.runActiveEffects() 203 | }) 204 | } 205 | } 206 | 207 | // Actually, it should be called 'create ReactElement' 208 | export function createElement( 209 | type: string | FunctionComponent, 210 | props: { [key: string]: any } | null, 211 | ...children: [] 212 | ): ReactElement { 213 | if (isClass(type)) { 214 | console.warn(`Doesn\'t support class Component: ${type.toString()}`) 215 | return 216 | } else { 217 | const VDOM = { 218 | type: type, 219 | props: props || {}, 220 | children 221 | } 222 | 223 | return VDOM 224 | } 225 | } 226 | 227 | export function useState(initialValue) { 228 | return __ReactCurrentInstance.useState(initialValue) 229 | } 230 | 231 | export function useEffect(effectCallback: any, dependiencies?: any[]) { 232 | return __ReactCurrentInstance.useEffect(effectCallback, dependiencies) 233 | } 234 | 235 | export default { 236 | createElement, 237 | Component, 238 | useState, 239 | useEffect 240 | } 241 | -------------------------------------------------------------------------------- /src/model.ts: -------------------------------------------------------------------------------- 1 | // An ReactElement is an immutable, plain object which represents a DOM node or component 2 | export interface ReactElement { 3 | type: string | ((...args: any[]) => ReactElement) 4 | props: { 5 | [key: string]: any 6 | } 7 | children: ReactElement[] 8 | key?: string | number 9 | } 10 | 11 | export type MemoCursor = { 12 | value: number 13 | __inited: boolean 14 | } 15 | 16 | export type Effect = { 17 | callback: (elm: HTMLElement) => any 18 | cleanup: Function | null 19 | lastTimeDeps: any[] 20 | } 21 | 22 | export interface ReactRealDomNode extends HTMLElement { 23 | __react__?: any 24 | __key__?: any 25 | __listeners__?: Map 26 | } 27 | 28 | export interface FunctionComponent { 29 | (props: object, children: ReactElement[]): ReactElement 30 | } 31 | 32 | export type Primitive = string | number | boolean | undefined | null 33 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function isFunction(anything: any): boolean { 2 | return !isClass(anything) && typeof anything === 'function' 3 | } 4 | 5 | export function isClass(anything: any): boolean { 6 | return ( 7 | typeof anything === 'function' && 8 | Function.prototype.toString.call(anything).startsWith('class') 9 | ) 10 | } 11 | 12 | export function isEventBinding(prop: string): boolean { 13 | return prop.startsWith('on') 14 | } 15 | 16 | export function isStrOrNumber(anything: any): boolean { 17 | return typeof anything === 'string' || typeof anything === 'number' 18 | } 19 | 20 | export function isNegitiveValue(anything: any): boolean { 21 | return anything === false || anything === null || anything === undefined 22 | } 23 | 24 | export function isObject(anything: any): boolean { 25 | return Object.prototype.toString.call(anything) === '[object Object]' 26 | } 27 | 28 | export function isArray(anything: any): boolean { 29 | return Array.isArray(anything) 30 | } 31 | 32 | export function flatten(array: T[]): T[] { 33 | return array.reduce((a, b) => { 34 | if (Array.isArray(b)) { 35 | return [...a, ...flatten(b)] 36 | } else { 37 | return [...a, b] 38 | } 39 | }, []) 40 | } 41 | 42 | export function isArrayEqual(arr: any[], other: any[]): boolean { 43 | if (arr.length !== other.length) return false 44 | 45 | if (arr.length === 0 && other.length === 0) return true 46 | 47 | const allEqual = arr.every((value, index) => { 48 | return _compareTwoValueToSeeIfTheyAreEquall(value, other[index]) 49 | }) 50 | 51 | return allEqual 52 | } 53 | 54 | export function isNaNX(anything: any) { 55 | return typeof anything === 'number' && isNaN(anything) 56 | } 57 | 58 | export function isEqual( 59 | object: Object, 60 | other: Object, 61 | ignoreKeys?: any[] 62 | ): boolean { 63 | const objKeys = _omitKeysIgnored(Object.keys(object), ignoreKeys) 64 | const otherKeys = _omitKeysIgnored(Object.keys(other), ignoreKeys) 65 | 66 | if (objKeys.length !== otherKeys.length) return false 67 | 68 | const allEqual = objKeys.every((key: string) => { 69 | return _compareTwoValueToSeeIfTheyAreEquall(object[key], other[key]) 70 | }) 71 | 72 | return allEqual 73 | } 74 | 75 | function _omitKeysIgnored(originKeys, ignoreKeys) { 76 | return originKeys.reduce((a, b) => { 77 | return a.concat(ignoreKeys.includes(b) ? [] : [b]) 78 | }, []) 79 | } 80 | 81 | function _compareTwoValueToSeeIfTheyAreEquall(v1: any, v2: any): boolean { 82 | if (isObject(v1)) { 83 | if (!isObject(v2)) return false 84 | // only compare their reference 85 | return v1 === v2 86 | } 87 | 88 | if (isArray(v1)) { 89 | if (!isArray(v2)) return false 90 | // if not equal, return true 91 | return isArrayEqual(v1, v2) 92 | } 93 | 94 | if (isNaNX(v1)) { 95 | return isNaNX(v2) 96 | } 97 | 98 | if (v1 !== v2) return false 99 | 100 | return true 101 | } 102 | -------------------------------------------------------------------------------- /src/vnode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isStrOrNumber, 3 | isNegitiveValue, 4 | flatten, 5 | isFunction, 6 | isEventBinding, 7 | isObject, 8 | isEqual 9 | } from './utils' 10 | import { ReactElement, Primitive, ReactRealDomNode } from './model' 11 | import { Component, setCurrentDispatcher } from '.' 12 | 13 | function domWillUnmount(dom: ReactRealDomNode) { 14 | const instance = dom.__react__ 15 | if (instance) { 16 | instance.cleanupEffects() 17 | } 18 | } 19 | 20 | /** 21 | * Turn a vnode tree into real DOM and mount it 22 | * @param vnode 23 | * @param parent 24 | */ 25 | export function render( 26 | vnode: ReactElement | Primitive, 27 | parent: ReactRealDomNode | null = null 28 | ) { 29 | const mount = parent ? el => parent.appendChild(el) : el => el 30 | 31 | if (isStrOrNumber(vnode)) { 32 | // primitive dom render 33 | return mount(document.createTextNode(vnode as string)) 34 | } else if (isNegitiveValue(vnode)) { 35 | // primitive dom render 36 | return mount(document.createTextNode('')) 37 | } else if (typeof vnode === 'object' && typeof vnode.type === 'string') { 38 | // vnode tree render 39 | const dom = mount(document.createElement(vnode.type as string)) 40 | 41 | // vnode.children would be a 2 dimension array if children come from a component's children slot, 42 | flatten(vnode.children).forEach(child => { 43 | render(child, dom) 44 | }) 45 | 46 | Object.entries(vnode.props).forEach(([attr, value]) => { 47 | setAttribute(dom, attr, value) 48 | }) 49 | 50 | return dom 51 | } else if (typeof vnode === 'object' && isFunction(vnode.type)) { 52 | // Render react component 53 | const func = vnode.type 54 | const props = { ...vnode.props, children: vnode.children } 55 | 56 | const instance = new Component(props) 57 | 58 | setCurrentDispatcher(instance) 59 | 60 | const base = render(instance._render(func), parent) 61 | 62 | base.__react__ = instance 63 | base.__key__ = vnode.props.key 64 | 65 | instance.base = base 66 | 67 | instance.runActiveEffects() 68 | 69 | return base 70 | } else { 71 | console.warn(`ERROR ReactElement:`) 72 | console.log(vnode) 73 | } 74 | } 75 | 76 | const __ReactKeyedElmsPool: Map = new Map() 77 | 78 | /** 79 | * Diff old dom and fresh vnode, then patch 80 | * @param dom old DOM node 81 | * @param vnode fresh virtual DOM node 82 | * @param parent parentElement 83 | */ 84 | export function patch( 85 | dom: ReactRealDomNode, 86 | vnode: ReactElement, 87 | parent: HTMLElement = null 88 | ) { 89 | const replace = parent ? el => parent.replaceChild(el, dom) && el : el => el 90 | 91 | if (typeof vnode === 'object' && isFunction(vnode.type)) { 92 | // Function component patch 93 | const instance = dom.__react__ 94 | if (instance && instance.renderFunc === vnode.type) { 95 | const shouldComponentUpdate = !isEqual(instance.props, vnode.props, [ 96 | 'children' 97 | ]) 98 | 99 | if (shouldComponentUpdate) { 100 | instance.props = Object.assign({}, instance.props, vnode.props, { 101 | children: vnode.children 102 | }) 103 | setCurrentDispatcher(instance) 104 | const newVnode = instance._render(instance.renderFunc) 105 | 106 | patch(dom, newVnode) 107 | 108 | instance.runActiveEffects() 109 | } 110 | } 111 | } else if (typeof vnode !== 'object' && dom instanceof Text) { 112 | // only text content change 113 | // 文本节点对比文本节点 114 | return dom.textContent === vnode ? dom : replace(render(vnode, parent)) 115 | } else if (typeof vnode !== 'object' && dom instanceof HTMLElement) { 116 | // only text override element 117 | // 文本节点替代元素 118 | const node = document.createTextNode(vnode) 119 | 120 | domWillUnmount(dom) 121 | 122 | return replace(node) 123 | } else if (typeof vnode === 'object' && dom instanceof Text) { 124 | // element take place of original text 125 | // 元素替代文本节点 126 | return replace(render(vnode, parent)) 127 | } else if ( 128 | typeof vnode === 'object' && 129 | dom.nodeName !== (vnode.type as string).toUpperCase() 130 | ) { 131 | // different type of dom, full rerender and replace 132 | // 不一样的节点直接视为全替换 133 | return replace(render(vnode, parent)) 134 | } else if ( 135 | typeof vnode === 'object' && 136 | dom.nodeName === (vnode.type as string).toUpperCase() 137 | ) { 138 | // the most common scenario, DOM update partially 139 | // 最常见的情况,节点部分更新,开始做详细的 diff 140 | const unkeyedElmsPool: Map = new Map() 141 | 142 | const oldChildrenNodes = Array.from(dom.childNodes) as ReactRealDomNode[] 143 | 144 | oldChildrenNodes.forEach((child, index) => { 145 | const key = child.__key__ 146 | 147 | if (!key) { 148 | const tmpMockKey: string = `INDEX__${index}` 149 | unkeyedElmsPool.set(tmpMockKey, child) 150 | } 151 | }) 152 | 153 | // add or modify DOM node 154 | // 新增和更改 DOM 155 | flatten(vnode.children).forEach((newChildVnode: ReactElement, index) => { 156 | const key = newChildVnode.props && newChildVnode.props.key 157 | const tmpMockKey: string = `INDEX__${index}` 158 | 159 | if (key && __ReactKeyedElmsPool.has(key)) { 160 | const newDom = patch(__ReactKeyedElmsPool.get(key), newChildVnode, dom) 161 | __ReactKeyedElmsPool.set(key, newDom) 162 | } else if (!key && unkeyedElmsPool.has(tmpMockKey)) { 163 | // modify 164 | // 修改 165 | patch(unkeyedElmsPool.get(tmpMockKey), newChildVnode, dom) 166 | unkeyedElmsPool.delete(tmpMockKey) 167 | } else { 168 | // add 169 | // 新增 170 | render(newChildVnode, dom) 171 | } 172 | }) 173 | 174 | // unmount the rest of doms not exist in current vnode.children 175 | // 删除这次更新不存在于新 children 的 176 | unkeyedElmsPool.forEach(restElm => { 177 | domWillUnmount(restElm) 178 | 179 | restElm.remove() 180 | }) 181 | 182 | patchAttributes(dom, vnode) 183 | } 184 | } 185 | 186 | function patchAttributes(dom: ReactRealDomNode, { props: newProps }) { 187 | const oldAttrs = {} 188 | 189 | Array.from(dom.attributes).forEach(attr => { 190 | oldAttrs[attr.name] = attr.value 191 | }) 192 | 193 | for (let attrName in oldAttrs) { 194 | if (attrName === 'class') { 195 | attrName = 'className' 196 | } 197 | if (!(attrName in newProps)) { 198 | setAttribute(dom, attrName, undefined) 199 | } 200 | } 201 | 202 | for (let attrName in newProps) { 203 | if (attrName === 'children') continue 204 | 205 | const value = newProps[attrName] 206 | 207 | if (attrName === 'className') { 208 | attrName = 'class' 209 | } 210 | 211 | if (oldAttrs[attrName] !== value) { 212 | if (attrName.startsWith('on')) { 213 | // 如果是事件绑定,先移除旧的 214 | const event = attrName.slice(2).toLocaleLowerCase() 215 | const oldListener = dom.__listeners__.get(event) 216 | dom.removeEventListener(event, oldListener) 217 | } 218 | setAttribute(dom, attrName, value) 219 | } 220 | } 221 | } 222 | 223 | /** 224 | * Make vnode.props into real attributes 225 | * @param elm target element 226 | * @param attr attribute name 227 | * @param value attribute value 228 | */ 229 | function setAttribute(dom: ReactRealDomNode, attr: string, value: any) { 230 | if (isFunction(value) && isEventBinding(attr)) { 231 | // event binding 232 | const event = attr.slice(2).toLocaleLowerCase() 233 | dom.addEventListener(event, value) 234 | if (!dom.__listeners__) { 235 | dom.__listeners__ = new Map() 236 | } 237 | 238 | dom.__listeners__.set(event, value) 239 | } else if (['checked', 'value', 'className'].includes(attr)) { 240 | // normal attributes 241 | dom[attr] = value 242 | } else if (attr === 'style' && isObject(value)) { 243 | // style assign 244 | Object.assign(dom.style, value) 245 | } else if (attr === 'ref' && isFunction(value)) { 246 | // allow user refer element to their custom variable 247 | // value: `(dom) => { someVar = dom }` alike 248 | value(dom) 249 | } else if (attr === 'key') { 250 | ;(dom as any).__key__ = value 251 | __ReactKeyedElmsPool.set(value, dom) 252 | } else { 253 | // whatever it be, just set it 254 | dom.setAttribute(attr, value) 255 | } 256 | } 257 | 258 | export default { 259 | render, 260 | patch 261 | } 262 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "noLib": false, 7 | "allowSyntheticDefaultImports": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "target": "es6", 11 | "jsx": "react", 12 | "jsxFactory": "React.createElement", 13 | "lib": ["dom", "es2017"], 14 | "sourceMap": false, 15 | "outDir": "./dist", 16 | "baseUrl": "./src" 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "**/*.spec.ts"] 20 | } 21 | --------------------------------------------------------------------------------