43 |
44 | {
46 | this.setState({
47 | hasLeadingEle: !hasLeadingEle,
48 | })
49 | }}
50 | >
51 | toggle leading ele
52 |
53 | change order
54 |
55 |
56 | {hasLeadingEle &&
leading ele
}
57 |
(this.flag = ref)} />
58 |
this.updateClicks(clicks - 1)}>-
59 | {clicks}
60 |
this.updateClicks(clicks + 1)}>+
61 | {_.times(3, n => {
62 | return
{n}
63 | })}
64 | {orderFlag ? itemA : itemB}
65 | {!orderFlag ? itemA : itemB}
66 |
67 |
68 | )
69 | }
70 | }
71 | return ReconcileTest
72 | }
73 |
--------------------------------------------------------------------------------
/packages/example-simple/src/Controller/Controller.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import testGroups from '../allTests'
3 | import './Controller.css'
4 | import parseQuery from '../parseQuery'
5 |
6 | const withAll = arr => {
7 | return [{ desc: 'all' }, ...arr].map((item, idx) => ({
8 | ...item,
9 | index: idx === 0 ? '' : idx - 1,
10 | }))
11 | }
12 |
13 | class Controller extends React.Component {
14 | state = {
15 | activeGroup: '',
16 | activeTest: '',
17 | }
18 |
19 | componentDidMount() {
20 | const queryObj = parseQuery(window.location.search.substr(1))
21 | this.setState({
22 | activeGroup: queryObj.group,
23 | activeTest: queryObj.test,
24 | })
25 | }
26 |
27 | goto = () => {
28 | window.location.href = `/?group=${this.state.activeGroup}&test=${
29 | this.state.activeTest
30 | }`
31 | }
32 |
33 | render() {
34 | const { activeGroup, activeTest } = this.state
35 | const tests = testGroups[activeGroup]
36 | ? testGroups[activeGroup].children
37 | : []
38 | return (
39 |
40 | group
41 | {
44 | const val = e.target.value
45 | this.setState(
46 | {
47 | activeGroup: val,
48 | activeTest: '',
49 | },
50 | this.goto,
51 | )
52 | }}
53 | >
54 | {withAll(testGroups).map((group, idx) => {
55 | return (
56 |
57 | {group.desc}
58 |
59 | )
60 | })}
61 |
62 | {activeGroup !== '' && (
63 |
64 | test
65 | {
68 | this.setState(
69 | {
70 | activeTest: e.target.value,
71 | },
72 | this.goto,
73 | )
74 | }}
75 | >
76 | {withAll(tests).map((test, idx) => {
77 | return (
78 |
79 | {test.desc}
80 |
81 | )
82 | })}
83 |
84 |
85 | )}
86 |
87 | )
88 | }
89 | }
90 |
91 | export default Controller
92 |
--------------------------------------------------------------------------------
/packages/my-react/src/dom/mount.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import invariant from 'invariant'
3 | import { FLAGS } from '../vnode'
4 | import { updateAttrs } from './utils'
5 | import { addListeners } from './event'
6 | import { reactState, funcStates } from '../hooks';
7 |
8 | const createDOMElement = vNode => {
9 | const { ref } = vNode
10 | const dom = document.createElement(vNode.type)
11 | updateAttrs(dom, vNode.attributes)
12 | addListeners(dom, vNode.listeners)
13 | vNode.children.forEach(child => {
14 | mount(child, dom)
15 | })
16 | return dom
17 | }
18 |
19 | const createDOMTextElement = vNode => {
20 | const type = typeof vNode.textContent
21 | const textContent =
22 | type === 'number' || type === 'string' ? vNode.textContent.toString() : ''
23 | const dom = document.createTextNode(textContent)
24 | return dom
25 | }
26 |
27 | const mountChild = (child, parent) => {
28 | parent.appendChild(child)
29 | }
30 |
31 | // mount
32 | const mountClassComponent = (vNode, parentDOM) => {
33 | const instance = new vNode.type(vNode.props)
34 | vNode.instance = instance
35 | instance._vNode = vNode
36 | const rendered = instance.render()
37 | vNode.rendered = rendered
38 | const dom = mount(rendered, parentDOM)
39 | vNode.dom = dom
40 | if (instance.componentDidMount) {
41 | instance.componentDidMount()
42 | }
43 | // @todo: 这里处理 ref 的时机和 react 并不一样,不知道 react 是出于什么考虑
44 | if (vNode.ref) {
45 | vNode.ref(instance)
46 | }
47 | return dom
48 | }
49 |
50 | const mountFunctionComponent = (vNode, parentDOM) => {
51 | reactState.currentVNode = vNode
52 | reactState.isCreatingState = true
53 | const rendered = vNode.type(vNode.props)
54 | // reset cursor if function component uses hooks
55 | if (funcStates.get(vNode)) {
56 | funcStates.get(vNode).cursor = -1
57 | }
58 | vNode.rendered = rendered
59 | const dom = mount(rendered, parentDOM)
60 | vNode.dom = dom
61 | return dom
62 | }
63 |
64 | const mountText = (vNode, parentDOM) => {
65 | const textNode = createDOMTextElement(vNode)
66 | mountChild(textNode, parentDOM)
67 | vNode.dom = textNode
68 | return textNode
69 | }
70 |
71 | const mountElement = (vNode, parentDOM) => {
72 | const dom = createDOMElement(vNode)
73 | mountChild(dom, parentDOM)
74 | if (vNode.ref) {
75 | vNode.ref(dom)
76 | }
77 | vNode.dom = dom
78 | return dom
79 | }
80 |
81 | const mount = (vNode, parentDOM) => {
82 | const { flag } = vNode
83 | let dom
84 |
85 | switch (flag) {
86 | case FLAGS.CLASS:
87 | dom = mountClassComponent(vNode, parentDOM)
88 | break
89 | case FLAGS.FUNC:
90 | dom = mountFunctionComponent(vNode, parentDOM)
91 | break
92 | case FLAGS.ELEMENT:
93 | dom = mountElement(vNode, parentDOM)
94 | break
95 | case FLAGS.TEXT:
96 | dom = mountText(vNode, parentDOM)
97 | break
98 | default:
99 | throw new Error(`Unknown flag ${flag}`)
100 | }
101 |
102 | invariant(dom, 'mount dom null')
103 | return dom
104 | }
105 |
106 | export default mount
107 |
--------------------------------------------------------------------------------
/packages/my-react/src/patch.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import _ from 'lodash'
3 | import invariant from 'invariant'
4 | import { FLAGS } from './vnode'
5 | import mount from './dom/mount'
6 | import unmount from './dom/unmount'
7 | import { updateAttrs, getNodeIndex } from './dom/utils'
8 | import { removeListeners, addListeners } from './dom/event'
9 | import { reactState, funcStates } from './hooks'
10 |
11 | const patchClassComponent = (vNode, prevVNode) => {
12 | invariant(prevVNode.dom !== null, 'patchClassComponent dom null')
13 | vNode.instance = prevVNode.instance
14 | const oldProps = vNode.instance.props
15 | vNode.instance.props = vNode.props
16 | // 因为这个问题排查了很久,一开始表现为 dom 为 null,事件触发两次
17 | vNode.instance._vNode = vNode
18 | const newRendered = prevVNode.instance.render()
19 | vNode.rendered = newRendered
20 | vNode.dom = prevVNode.dom
21 |
22 | const patchResult = patch(newRendered, prevVNode.rendered)
23 | if (vNode.instance.componentDidUpdate) {
24 | vNode.instance.componentDidUpdate(oldProps, vNode.instance.state)
25 | }
26 | // @todo: react 里面,在 update 阶段,一开始 ref 会是 null
27 | if (vNode.ref) {
28 | vNode.ref(vNode.instance)
29 | }
30 | return patchResult
31 | }
32 |
33 | const patchFunctionComponent = (vNode, prevVNode) => {
34 | const prevStore = funcStates.get(prevVNode)
35 | if (prevStore) {
36 | // reset cursor before rendering
37 | prevStore.cursor = -1
38 | funcStates.set(vNode, prevStore)
39 | funcStates.delete(prevVNode)
40 | }
41 | reactState.currentVNode = vNode
42 | reactState.isCreatingState = false
43 | const newRendered = vNode.type(vNode.props)
44 | // 需要手动更新 rendered
45 | // 其实可以写成 vNode.render() 然后自己更新内部状态,但那样太 OO 了
46 | vNode.rendered = newRendered
47 | vNode.dom = prevVNode.dom
48 | invariant(prevVNode.dom !== null, 'patchFunctionComponent dom null')
49 | return patch(newRendered, prevVNode.rendered)
50 | }
51 |
52 | const patchElement = (vNode, prevVNode) => {
53 | vNode.dom = prevVNode.dom
54 | if (!_.isEqual(vNode.attributes, prevVNode.attributes)) {
55 | updateAttrs(vNode.dom, vNode.attributes)
56 | }
57 | invariant(prevVNode.dom !== null, 'patchElement dom null')
58 | removeListeners(prevVNode.dom, prevVNode.listeners)
59 | addListeners(vNode.dom, vNode.listeners)
60 | patchChildren(vNode.children, prevVNode.children, prevVNode.dom)
61 | if (vNode.ref) {
62 | vNode.ref(vNode.dom)
63 | }
64 | }
65 |
66 | const patchTextElement = (vNode, prevVNode) => {
67 | // 这个之前居然放到判断里了,导致之前未更新的 text 节点在之后的更新里找不到 dom
68 | vNode.dom = prevVNode.dom
69 | if (vNode.textContent !== prevVNode.textContent) {
70 | const type = typeof vNode.textContent
71 | const textContent =
72 | type === 'number' || type === 'string' ? vNode.textContent.toString() : ''
73 | invariant(prevVNode.dom !== null, 'patchTextElement dom null')
74 |
75 | // 没有真正采用和 react 一样的实现,担心会引入各种 dom 为 null 的问题
76 | // 现在在 chrome 里面通过检查器至少看不到 ""
77 | if (textContent) {
78 | prevVNode.dom.textContent = textContent
79 | vNode.dom = prevVNode.dom
80 | } else {
81 | mount(vNode, prevVNode.dom.parentNode)
82 | unmount(prevVNode)
83 | }
84 | }
85 | }
86 |
87 | const patch = (vNode, prevVNode) => {
88 | if (vNode === prevVNode) {
89 | return
90 | }
91 | const { flag, type } = vNode
92 |
93 | if (prevVNode.flag !== flag || type !== prevVNode.type) {
94 | const parentDOM = prevVNode.dom.parentNode
95 | unmount(prevVNode)
96 | mount(vNode, parentDOM)
97 | return
98 | }
99 |
100 | switch (flag) {
101 | case FLAGS.CLASS:
102 | patchClassComponent(vNode, prevVNode)
103 | break
104 | case FLAGS.FUNC:
105 | patchFunctionComponent(vNode, prevVNode)
106 | break
107 | case FLAGS.ELEMENT:
108 | patchElement(vNode, prevVNode)
109 | case FLAGS.TEXT:
110 | patchTextElement(vNode, prevVNode)
111 | default:
112 | break
113 | }
114 | }
115 |
116 | export const patchChildren = (currentChildren, lastChildren, parentDOM) => {
117 | const lastChildInUse = []
118 | let results = []
119 | currentChildren.forEach((currentVNode, idx) => {
120 | const { key } = currentVNode
121 | if (lastChildren[idx] && key === lastChildren[idx].key) {
122 | patch(currentVNode, lastChildren[idx])
123 | lastChildInUse.push(lastChildren[idx])
124 | } else {
125 | const match = lastChildren.find(child => child.key === key)
126 | if (match) {
127 | lastChildInUse.push(match)
128 | patch(currentVNode, match)
129 | } else {
130 | mount(currentVNode, parentDOM)
131 | }
132 | }
133 | })
134 |
135 | const lastChildNotInUse = lastChildren.filter(
136 | child => !lastChildInUse.includes(child),
137 | )
138 |
139 | // reorder
140 | lastChildNotInUse.forEach(unmount)
141 | currentChildren.forEach((currentVNode, idx) => {
142 | const domIdx = getNodeIndex(currentVNode.dom)
143 | if (domIdx !== idx) {
144 | parentDOM.insertBefore(currentVNode.dom, parentDOM.childNodes[idx])
145 | }
146 | })
147 |
148 | return results
149 | }
150 |
151 | export default patch
152 |
--------------------------------------------------------------------------------
/packages/my-react/src/dom/event.js:
--------------------------------------------------------------------------------
1 | export const addListeners = (dom, listeners = {}) => {
2 | _.forEach(listeners, ({ handler, useCapture }, key) => {
3 | dom.addEventListener(key, handler, useCapture)
4 | })
5 | }
6 |
7 | export const removeListeners = (dom, listeners = {}) => {
8 | _.forEach(listeners, ({ handler, useCapture }, key) => {
9 | dom.removeEventListener(key, handler, useCapture)
10 | })
11 | }
12 |
13 | export const REACT_EVENT_KEYS = [
14 | // Clipboard Events
15 | 'onCopy',
16 | 'onCopyCapture',
17 | 'onCut',
18 | 'onCutCapture',
19 | 'onPaste',
20 | 'onPasteCapture',
21 |
22 | // Composition Events
23 | 'onCompositionEnd',
24 | 'onCompositionEndCapture',
25 | 'onCompositionStart',
26 | 'onCompositionStartCapture',
27 | 'onCompositionUpdate',
28 | 'onCompositionUpdateCapture',
29 |
30 | // Focus Events
31 | 'onFocus',
32 | 'onFocusCapture',
33 | 'onBlur',
34 | 'onBlurCapture',
35 |
36 | // Form Events
37 | 'onChange',
38 | 'onChangeCapture',
39 | 'onInput',
40 | 'onInputCapture',
41 | 'onReset',
42 | 'onResetCapture',
43 | 'onSubmit',
44 | 'onSubmitCapture',
45 | 'onInvalid',
46 | 'onInvalidCapture',
47 |
48 | // Image Events
49 | 'onLoad',
50 | 'onLoadCapture',
51 | 'onError',
52 | 'onErrorCapture',
53 |
54 | // Keyboard Events
55 | 'onKeyDown',
56 | 'onKeyDownCapture',
57 | 'onKeyPress',
58 | 'onKeyPressCapture',
59 | 'onKeyUp',
60 | 'onKeyUpCapture',
61 |
62 | // Media Events
63 | 'onAbort',
64 | 'onAbortCapture',
65 | 'onCanPlay',
66 | 'onCanPlayCapture',
67 | 'onCanPlayThrough',
68 | 'onCanPlayThroughCapture',
69 | 'onDurationChange',
70 | 'onDurationChangeCapture',
71 | 'onEmptied',
72 | 'onEmptiedCapture',
73 | 'onEncrypted',
74 | 'onEncryptedCapture',
75 | 'onEnded',
76 | 'onEndedCapture',
77 | 'onLoadedData',
78 | 'onLoadedDataCapture',
79 | 'onLoadedMetadata',
80 | 'onLoadedMetadataCapture',
81 | 'onLoadStart',
82 | 'onLoadStartCapture',
83 | 'onPause',
84 | 'onPauseCapture',
85 | 'onPlay',
86 | 'onPlayCapture',
87 | 'onPlaying',
88 | 'onPlayingCapture',
89 | 'onProgress',
90 | 'onProgressCapture',
91 | 'onRateChange',
92 | 'onRateChangeCapture',
93 | 'onSeeked',
94 | 'onSeekedCapture',
95 | 'onSeeking',
96 | 'onSeekingCapture',
97 | 'onStalled',
98 | 'onStalledCapture',
99 | 'onSuspend',
100 | 'onSuspendCapture',
101 | 'onTimeUpdate',
102 | 'onTimeUpdateCapture',
103 | 'onVolumeChange',
104 | 'onVolumeChangeCapture',
105 | 'onWaiting',
106 | 'onWaitingCapture',
107 |
108 | // MouseEvents
109 | 'onClick',
110 | 'onClickCapture',
111 | 'onContextMenu',
112 | 'onContextMenuCapture',
113 | 'onDoubleClick',
114 | 'onDoubleClickCapture',
115 | 'onDrag',
116 | 'onDragCapture',
117 | 'onDragEnd',
118 | 'onDragEndCapture',
119 | 'onDragEnter',
120 | 'onDragEnterCapture',
121 | 'onDragExit',
122 | 'onDragExitCapture',
123 | 'onDragLeave',
124 | 'onDragLeaveCapture',
125 | 'onDragOver',
126 | 'onDragOverCapture',
127 | 'onDragStart',
128 | 'onDragStartCapture',
129 | 'onDrop',
130 | 'onDropCapture',
131 | 'onMouseDown',
132 | 'onMouseDownCapture',
133 | 'onMouseEnter',
134 | 'onMouseLeave',
135 | 'onMouseMove',
136 | 'onMouseMoveCapture',
137 | 'onMouseOut',
138 | 'onMouseOutCapture',
139 | 'onMouseOver',
140 | 'onMouseOverCapture',
141 | 'onMouseUp',
142 | 'onMouseUpCapture',
143 |
144 | // Selection Events
145 | 'onSelect',
146 | 'onSelectCapture',
147 |
148 | // Touch Events
149 | 'onTouchCancel',
150 | 'onTouchCancelCapture',
151 | 'onTouchEnd',
152 | 'onTouchEndCapture',
153 | 'onTouchMove',
154 | 'onTouchMoveCapture',
155 | 'onTouchStart',
156 | 'onTouchStartCapture',
157 |
158 | // Pointer Events
159 | 'onPointerDown',
160 | 'onPointerDownCapture',
161 | 'onPointerMove',
162 | 'onPointerMoveCapture',
163 | 'onPointerUp',
164 | 'onPointerUpCapture',
165 | 'onPointerCancel',
166 | 'onPointerCancelCapture',
167 | 'onPointerEnter',
168 | 'onPointerEnterCapture',
169 | 'onPointerLeave',
170 | 'onPointerLeaveCapture',
171 | 'onPointerOver',
172 | 'onPointerOverCapture',
173 | 'onPointerOut',
174 | 'onPointerOutCapture',
175 | 'onGotPointerCapture',
176 | 'onGotPointerCaptureCapture',
177 | 'onLostPointerCapture',
178 | 'onLostPointerCaptureCapture',
179 |
180 | // UI Events
181 | 'onScroll',
182 | 'onScrollCapture',
183 |
184 | // Wheel Events
185 | 'onWheel',
186 | 'onWheelCapture',
187 |
188 | // Animation Events
189 | 'onAnimationStart',
190 | 'onAnimationStartCapture',
191 | 'onAnimationEnd',
192 | 'onAnimationEndCapture',
193 | 'onAnimationIteration',
194 | 'onAnimationIterationCapture',
195 |
196 | // Transition Events
197 | 'onTransitionEnd',
198 | 'onTransitionEndCapture',
199 | ]
200 |
201 | // react 并没有严格遵从 dom 的事件命名
202 | const reactEventMap = {
203 | doubleclick: 'dblclick',
204 | }
205 |
206 | export const REACT_EVENT_MAP = REACT_EVENT_KEYS.reduce((map, key) => {
207 | const useCapture = key.endsWith('Capture')
208 | const domEventKey = key
209 | .substring(2, !useCapture ? key.length : key.length - 7)
210 | .toLocaleLowerCase()
211 | return {
212 | ...map,
213 | [key]: {
214 | key: reactEventMap[domEventKey] || domEventKey,
215 | useCapture,
216 | },
217 | }
218 | }, {})
219 |
--------------------------------------------------------------------------------
/packages/example-simple/src/allTests.js:
--------------------------------------------------------------------------------
1 | import renderPrimitiveTest from './tests/renderPrimitiveTest'
2 | import setStateTest from './tests/setStateTests/setStateTest'
3 | import childrenTest from './tests/childrenTest'
4 | import reconcileTest from './tests/reconcileTest'
5 | import reduxTest from './tests/reduxTest'
6 | import lifecycleTest from './tests/lifecycleTest'
7 | import keyedTest from './tests/diffTests/keyedTest'
8 | import manuallyKeyedTest from './tests/diffTests/manuallyKeyedTest'
9 | import reorderTextNodes from './tests/diffTests/reorderTextNodes'
10 | import unkeyedTest from './tests/diffTests/unkeyedTest'
11 | import reorderTextNodes2 from './tests/diffTests/reorderTextNodes2'
12 | import nestedKeyedTest from './tests/diffTests/nestedKeyedTest'
13 | import propTest from './tests/setStateTests/propTest'
14 | import setStateWithChildren from './tests/setStateTests/setStateWithChildren'
15 | import multipleSetStateCalls from './tests/setStateTests/multipleSetStateCalls'
16 | import callbackAndSetState from './tests/setStateTests/callbackAndSetState'
17 | import mutateState from './tests/setStateTests/mutateState'
18 | import reorderTest from './tests/diffTests/reorderTest'
19 | import updateAttrs from './tests/diffTests/updateAttrs'
20 | import unmountTest from './tests/unmountTest'
21 | import didUpdateTest from './tests/didUpdateTest'
22 | import inputTest from './tests/form/inputTest'
23 | import keyTest from './tests/keyTest'
24 | import refTest from './tests/refTests/refTest'
25 | import useStateTest from './tests/hooksTests/useStateTest'
26 | import useStateTest2 from './tests/hooksTests/useStateTest2'
27 | import useStateTest3 from './tests/hooksTests/useStateTest3'
28 | // import reactReduxTest from './tests/reactReduxTest'
29 |
30 | const testGroups = [
31 | {
32 | desc: 'form tests',
33 | children: [
34 | {
35 | test: inputTest,
36 | desc: 'input test',
37 | },
38 | ],
39 | },
40 | {
41 | desc: 'render tests',
42 | children: [
43 | {
44 | test: renderPrimitiveTest,
45 | desc: 'render primitive',
46 | },
47 | {
48 | test: childrenTest,
49 | desc: 'children test',
50 | },
51 | {
52 | test: keyTest,
53 | desc: 'key test',
54 | },
55 | ],
56 | },
57 | {
58 | desc: 'hook tests',
59 | children: [
60 | {
61 | test: useStateTest,
62 | desc: 'useState test',
63 | },
64 | {
65 | test: useStateTest2,
66 | desc: 'useState test2',
67 | },
68 | {
69 | test: useStateTest3,
70 | desc: 'useState test3',
71 | },
72 | ],
73 | },
74 | {
75 | desc: 'ref tests',
76 | children: [
77 | {
78 | test: refTest,
79 | desc: 'ref test',
80 | },
81 | ],
82 | },
83 | {
84 | desc: 'setState tests',
85 | children: [
86 | {
87 | test: setStateTest,
88 | desc: 'setStateTest basic',
89 | },
90 | {
91 | test: propTest,
92 | desc: 'prop test',
93 | },
94 | {
95 | test: setStateWithChildren,
96 | desc: 'setStateWithChildren',
97 | },
98 | {
99 | test: multipleSetStateCalls,
100 | desc: 'multipleSetStateCalls',
101 | },
102 | {
103 | test: callbackAndSetState,
104 | desc: 'callbackAndSetState',
105 | },
106 | {
107 | test: mutateState,
108 | desc: 'mutateState',
109 | },
110 | ],
111 | },
112 | {
113 | desc: 'diff tests',
114 | children: [
115 | {
116 | test: manuallyKeyedTest,
117 | desc: 'manuallyKeyedTest',
118 | },
119 | {
120 | test: keyedTest,
121 | desc: 'keyedTest',
122 | },
123 | {
124 | test: updateAttrs,
125 | desc: 'update attrs',
126 | },
127 | {
128 | test: reorderTest,
129 | desc: 'reorder test',
130 | },
131 | {
132 | test: nestedKeyedTest,
133 | desc: 'nestedKeyedTest',
134 | },
135 | {
136 | test: unkeyedTest,
137 | desc: 'unkeyedTest',
138 | },
139 | {
140 | test: reorderTextNodes,
141 | desc: 'reorderTextNodes',
142 | },
143 | {
144 | test: reorderTextNodes2,
145 | desc: 'reorderTextNodes2',
146 | },
147 | ],
148 | },
149 | {
150 | desc: 'lifecycle tests',
151 | children: [
152 | {
153 | test: lifecycleTest,
154 | desc: 'lifecycleTest',
155 | },
156 | {
157 | test: unmountTest,
158 | desc: 'unmount test',
159 | },
160 | {
161 | test: didUpdateTest,
162 | desc: 'didUpdateTest',
163 | },
164 | ],
165 | },
166 | {
167 | desc: 'misc',
168 | children: [
169 | {
170 | test: reconcileTest,
171 | desc: 'reconcileTest',
172 | },
173 | ],
174 | },
175 | {
176 | desc: '3rd party integration tests',
177 | children: [
178 | {
179 | test: reduxTest,
180 | desc: 'reduxTest',
181 | },
182 | // Provider 需要 node_modules 里面的 react 依赖,这里采用的
183 | // 依赖注入的方式变得不可行
184 | // {
185 | // test: reactReduxTest,
186 | // desc: 'reactReduxTest',
187 | // },
188 | ],
189 | },
190 | ]
191 |
192 | export default testGroups
193 |
--------------------------------------------------------------------------------
/packages/my-react/src/vnode.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import _ from 'lodash'
3 | import { getAttrs } from './dom/utils'
4 | import { REACT_EVENT_MAP, REACT_EVENT_KEYS } from './dom/event'
5 |
6 | export const FLAGS = {
7 | TEXT: 'text',
8 | CLASS: 'class',
9 | FUNC: 'func',
10 | ELEMENT: 'element',
11 | }
12 |
13 | const TEXT_VNODE_TYPE = Symbol('text_vnode_type')
14 |
15 | const isTextElement = element => {
16 | return !(element instanceof VNode)
17 | }
18 |
19 | let debugId = 0
20 |
21 | class VNode {
22 | /**
23 | * @param {{ type?, props?, textContent?, key?, flag }} config
24 | */
25 | constructor({ type, props, key, textContent, flag }) {
26 | this._debugId = debugId
27 | this.type = type
28 | this.flag = flag
29 | this.props = props
30 | this.textContent = textContent
31 | this.key = key
32 | this.children = []
33 | this.ref = null
34 | this.validated = false
35 | this.parent = null
36 | this.dom = null
37 | this.listeners = null
38 | this.attributes = null
39 | this.rendered = null
40 | this.instance = null
41 | this.state = null
42 | }
43 | }
44 |
45 | // generate unique keys & flatten children
46 | const processChildren = (children, keyPrefix = '__root_', parent) => {
47 | return (Array.isArray(children) ? children : [children]).reduce(
48 | (output, childElement, idx) => {
49 | if (Array.isArray(childElement)) {
50 | return output.concat(
51 | processChildren(childElement, `${keyPrefix}${idx}_>`, parent),
52 | )
53 | }
54 |
55 | const generatedKey = '__gen_' + keyPrefix + idx
56 |
57 | if (isTextElement(childElement)) {
58 | const textVNode = createVNode(TEXT_VNODE_TYPE, {
59 | textContent: childElement,
60 | key: generatedKey,
61 | })
62 | textVNode.parent = parent
63 | return [...output, textVNode]
64 | }
65 |
66 | childElement.parent = parent
67 | if (childElement.key === null) {
68 | childElement.key = generatedKey
69 | } else {
70 | childElement.key = keyPrefix + childElement.key
71 | }
72 | return [...output, childElement]
73 | },
74 | [],
75 | )
76 | }
77 |
78 | export const createVNode = (type, allProps = {}) => {
79 | let vNode
80 | const props = _.omit(allProps, ['key', 'ref', '__source', '__self'])
81 | if (typeof type === 'string') {
82 | vNode = createElementVNode(type, props)
83 | } else if (type.$IS_CLASS) {
84 | vNode = createClassVNode(type, props)
85 | } else if (typeof type === 'function') {
86 | vNode = createFunctionVNode(type, props)
87 | if (allProps.ref) {
88 | console.error(
89 | 'Function components cannot be given refs. Attempts to access this ref will fail.',
90 | )
91 | }
92 | } else if (type === TEXT_VNODE_TYPE) {
93 | vNode = createTextVNode(props.textContent)
94 | } else {
95 | throw new Error(`Unknown type: ${type}`)
96 | }
97 | vNode.key = allProps.key === undefined ? null : allProps.key
98 | vNode.ref = allProps.ref || null
99 |
100 | if (vNode.key !== null) {
101 | Object.defineProperty(props, 'key', {
102 | configurable: false,
103 | enumerable: false,
104 | get() {
105 | console.error(
106 | '`key` is not a prop. Trying to access it will result in `undefined` being returned',
107 | )
108 | },
109 | })
110 | }
111 |
112 | if (vNode.ref !== null) {
113 | Object.defineProperty(props, 'ref', {
114 | configurable: false,
115 | enumerable: false,
116 | get() {
117 | console.error(
118 | '`ref` is not a prop. Trying to access it will result in `undefined` being returned',
119 | )
120 | },
121 | })
122 | }
123 |
124 | vNode.children =
125 | props.children === undefined
126 | ? []
127 | : processChildren(props.children, undefined, vNode)
128 |
129 | // validate keys
130 | const keys = vNode.children.map(child => child.key)
131 | const uniqueKeys = _.union(keys)
132 | const print = arr => console.error(arr.sort().join(', '))
133 | if (keys.length !== uniqueKeys.length) {
134 | console.error('key should be unique!')
135 | print(uniqueKeys)
136 | print(keys)
137 | }
138 |
139 | debugId++
140 | return vNode
141 | }
142 |
143 | const createTextVNode = text => {
144 | const vNode = new VNode({
145 | textContent: text,
146 | flag: FLAGS.TEXT,
147 | })
148 | return vNode
149 | }
150 |
151 | const createFunctionVNode = (type, props) => {
152 | const vNode = new VNode({
153 | type,
154 | props,
155 | flag: FLAGS.FUNC,
156 | })
157 | return vNode
158 | }
159 |
160 | const createClassVNode = (type, props) => {
161 | const vNode = new VNode({
162 | type,
163 | props,
164 | flag: FLAGS.CLASS,
165 | })
166 | return vNode
167 | }
168 |
169 | const createElementVNode = (type, props) => {
170 | let finalProps = props
171 | let listeners = null
172 | let attributes = null
173 | const eventProps = _.pick(finalProps, REACT_EVENT_KEYS)
174 | if (!_.isEmpty(eventProps)) {
175 | listeners = _.reduce(
176 | eventProps,
177 | (listeners, handler, key) => {
178 | const match = REACT_EVENT_MAP[key]
179 | return {
180 | ...listeners,
181 | [match.key]: {
182 | handler,
183 | useCapture: match.useCapture,
184 | },
185 | }
186 | },
187 | {},
188 | )
189 | finalProps = _.omit(finalProps, _.keys(eventProps))
190 | }
191 | attributes = getAttrs(finalProps)
192 | const vNode = new VNode({
193 | type,
194 | props: finalProps,
195 | flag: FLAGS.ELEMENT,
196 | })
197 | vNode.listeners = listeners
198 | vNode.attributes = attributes
199 | return vNode
200 | }
201 |
--------------------------------------------------------------------------------