旧React demo

旧 React 简易实现

react-old.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import { updateDomProperties } from "./dom-utils.js"

// 根实例
let rootInstance = null
const TEXT_ELEMENT = "TEXT_ELEMENT"

// 创建 VDOM 元素
function createElement(type, config, ...args) {
const props = Object.assign({}, config)
const hasChildren = args.length > 0
const rawChildren = hasChildren ? [].concat(...args) : []

props.children = rawChildren
.filter((c) => c !== null && c !== false)
.map((c) => (c instanceof Object ? c : createTextElement(c)))

return { type, props }
}

function createTextElement(text) {
return createElement(TEXT_ELEMENT, { nodeValue: text })
}

function render(element, container) {
const prevInstance = rootInstance
const nextInstance = reconcile(container, prevInstance, element)
rootInstance = nextInstance
}

// 递归更新子元素
function reconcile(parentDom, instance, element) {
if (instance === null) {
// 创建新实例
const newInstance = instantiate(element)
parentDom.appendChild(newInstance.dom)
return newInstance
} else if (element === null) {
// 删除实例
parentDom.removeChild(instance.dom)
return null
} else if (instance.element.type !== element.type) {
// 如果类型不同,则替换实例
const newInstance = instantiate(element)
// 替换实例
parentDom.replaceChild(newInstance.dom, instance.dom)
return newInstance
} else if (typeof element.type === "string") {
// 更新 DOM 实例
updateDomProperties(instance.dom, instance.element.props, element.props)

// 更新 instance
instance.childInstances = reconcilerChildren(instance, element)
instance.element = element

return instance
} else {
// 更新组件实例
instance.publicInstance.props = element.props
const childElement = instance.publicInstance.render()
const oldChildInstance = instance.childInstance
const childInstance = reconcile(parentDom, oldChildInstance, childElement)

instance.dom = childInstance.dom
instance.childInstance = childInstance
instance.element = element

return instance
}
}

function reconcilerChildren(instance, element) {
const dom = instance.dom
const childInstances = instance.childInstances
const nextChildElements = element.props.children || []
const newChildInstances = []

const count = Math.max(childInstances.length, nextChildElements.length)

// 本 demo, 顺序不同也算作更新

for (let i = 0; i < count; i++) {
const childInstance = childInstances[i]
const childElement = nextChildElements[i]

const newChildInstance = reconcile(dom, childInstance, childElement)

newChildInstances.push(newChildInstance)
}

return newChildInstances.filter((instance) => instance !== null)
}

// 把元素实例化成真实的DOM节点
function instantiate(element) {
const { type, props } = element
const isDomElement = typeof type === "string"

if (isDomElement) {
// 创建 DOM 元素
const isTextElement = type === TEXT_ELEMENT
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type)

// 更新 dom
updateDomProperties(dom, [], props)

// 实例化子元素
const childElements = props.children || []
const childInstances = childElements.map(instantiate)
const childDoms = childInstances.map((childInstance) => childInstance.dom)
childDoms.forEach((childDom) => dom.appendChild(childDom))

const instance = { dom, element, childInstances }

return instance
} else {
// 初始化组件
const instance = {}
const publicInstance = createPublicInstance(element, instance)
const childElement = publicInstance.render() // 拿到 vdom
const childInstance = instantiate(childElement) // 实例化
const dom = childInstance.dom

Object.assign(instance, { dom, element, publicInstance, childInstance })

return instance
}
}

function createPublicInstance(element, internalInstance) {
const { type, props } = element

// type 此时是一个 Class
const publicInstance = new type(props)

publicInstance.__internalInstance = internalInstance

return publicInstance
}

function updateInstance(internalInstance) {
const parentDom = internalInstance.dom.parentNode
const element = internalInstance.element

reconcile(parentDom, internalInstance, element)
}

class Component {
constructor(props) {
this.props = props
this.state = this.state || {}
}

setState(partialState) {
this.state = Object.assign({}, this.state, partialState)
updateInstance(this.__internalInstance)
}

// render 出来的也是 VDOM
render() {}
}

export default { render, createElement, Component }
dom-utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
const isEvent = (name) => name.startsWith("on")
const isAttribute = (name) =>
!isEvent(name) && name !== "children" && name !== "style"
const isNew = (prev, next) => (key) => prev[key] !== next[key]
const isGone = (prev, next) => (key) => !(key in next)

const TEXT_ELEMENT = "TEXT ELEMENT"

// 更新 DOM 属性
function updateDomProperties(dom, prevProps, nextProps) {
// remove old event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2)
dom.removeEventListener(eventType, prevProps[name])
})

// remove old attributes
Object.keys(prevProps)
.filter(isAttribute)
.filter(isGone(prevProps, nextProps))
.forEach((name) => {
dom[name] = null
})

// add new attributes
Object.keys(nextProps)
.filter(isAttribute)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
dom[name] = nextProps[name]
})

// set style
prevProps.style = prevProps.style || {}
nextProps.style = nextProps.style || {}
Object.keys(nextProps.style)
.filter(isNew(prevProps.style, nextProps.style))
.forEach((key) => {
dom.style[key] = nextProps.style[key]
})
Object.keys(prevProps.style)
.filter(isGone(prevProps.style, nextProps.style))
.forEach((key) => {
dom.style[key] = ""
})

// add new event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2)
dom.addEventListener(eventType, nextProps[name])
})
}

function createDomElement(fiber) {
const isTextElement = fiber.type === TEXT_ELEMENT
const dom = isTextElement
? document.createTextNode("")
: document.createElement(fiber.type)

updateDomProperties(dom, [], fiber.props)

return dom
}

export { createDomElement, updateDomProperties }

usage:

index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import React from "./react-old.js"

const randomLikes = () => Math.ceil(Math.random() * 100)

const stories = [
{
name: "React introduction",
url: "http://baidu.com",
likes: randomLikes(),
},
{
name: "Rendering DOM elements ",
url: "http://baidu.com",
likes: randomLikes(),
},
{
name: "Element creation and JSX",
url: "http://baidu.com",
likes: randomLikes(),
},
{
name: "Instances and reconciliation",
url: "http://baidu.com",
likes: randomLikes(),
},
{
name: "Components and state",
url: "http://baidu.com",
likes: randomLikes(),
},
]

class App extends React.Component {
render() {
return (
<div>
<h1>React Stories</h1>
<ul>
{this.props.stories.map((story) => (
<Story name={story.name} url={story.url} />
))}
</ul>
</div>
)
}
}

class Story extends React.Component {
constructor(props) {
super(props)
this.state = { like: Math.ceil(Math.random() * 100) }
}

like() {
this.setState({ like: this.state.like + 1 })
}

render() {
const { name, url } = this.props
const { like } = this.state

return (
<li>
<button onClick={() => this.like()}>{like}❤️</button>
<a href={url}>{name}</a>
</li>
)
}
}

React.render(<App stories={stories} />, document.getElementById("root"))
index.html
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>