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