React笔记

React笔记

元素

定义元素

const element = <h1>哈哈哈</h1>;

react中的元素类似Java的对象.react DOM 可以确保浏览器 DOM 的数据内容与 React 元素保持一致.

元素渲染

定义一个div块,id对应const的变量,这个div块的所有内容都由react来管理,称为’根’DOM节点.

<div id="example"></div>

使用react开发只会定义一个根节点.

ReactDOM.render

使用ReactDOM.render()方法.将react元素渲染到根节点的DOM中.

1
2
3
4
5
6
7
8
9
10
//定义块元素
<div id="example"></div>
//根据元素id,渲染页面
<script type="text/babel">
const element =<h1>Hello, world!</h1>;
ReactDOM.render(
element,
document.getElementById('example')
);
</script>
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
<div id="example"></div>
<script type="text/babel">
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>现在是 {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('example')
);
}

//通过setInterval方法,每秒钟调用一次tick方法重新渲染,达到页面更新的目的
setInterval(tick, 1000);
</script>

<div id="example"></div>
<script type="text/babel">
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>现在是 {props.date.toLocaleTimeString()}.</h2>
</div>
);
}

function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('example')
);
}

setInterval(tick, 1000);
</script>

//创建一个 React.Component 的 ES6 类
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
//this.props 替换 props
//使用 ES6 类写法,用 this.props.属性名 来取值
<h2>现在是 {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('example')
);
}

setInterval(tick, 1000);

JSX

JSX 就是用来声明 React 当中的元素.

React元素是普通的对象.React DOM 可以确保 浏览器 DOM 的数据内容与 React 元素保持一致.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<style>
.foo{
color:red;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
//JSX语法
const element = <h1 className="foo">Hello, world</h1>;
//div定义一个React DOM根节点,通过id选择器.
//通过render把元素element渲染到页面.
ReactDOM.render(element, document.getElementById('root'));
</script>
</body>
1
2
//添加自定义属性需要使用 data- 前缀
<p data-myattribute = "somevalue">这是一个很不错的 JavaScript 库!</p>

在JSX中使用js表达式.使用花括号{}定义表达式

1
<h1>{1+1}<h1>

JSX不能使用if else语句

1
2
//使用三元运算符代替
<h1>{i == 1 ? 'True!' : 'False'}</h1>

我们知道在 React 组件render() 返回的是 JSX,而 JSX 将会被 babel 转换。JSX 将被转换为 React.createElement(type, config, children)的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// App.js
// 转换前
Class App extends Component {
render() {
return <h1 id='title'>Hello World<h1>
}
}

// 转换后
var App = React.createClass({
render() {
return React.createElement('h1', {
id: 'title'
}, 'hello world')
}
})

React.createElement() 的实现位于 /src/isomorphic/classic/element/ReactElement.js

这里的 React.createElement()是用来生成虚拟 DOM 元素,该函数对组件的属性,事件,子组件等进行了处理,并返回值为一个 ReactElement 对象(单纯的 JavaScript 对象,仅包括 type, props, key, ref 等属性)。

这恰好说明了 JSX 中的 <h1 id='title'>hello world</h1>实际上是 JavaScript 对象,而不是我们通常写的 HTML 标签。

样式

1
2
3
4
5
6
7
8
9
10
//定义内联样式
var myStyle = {
fontSize: 100,
color: '#FF0000'
};
//渲染样式
ReactDOM.render(
<h1 style = {myStyle}>哈哈哈</h1>,
document.getElementById('example')
);

注释

1
2
3
4
5
6
7
ReactDOM.render(
<div>
<h1>哈哈哈</h1>
{/*注释...*/}
</div>,
document.getElementById('example')
);

数组

1
2
3
4
5
6
7
8
var arr = [
<h1>哈哈哈</h1>,
<h2>呵呵呵</h2>,
];
ReactDOM.render(
<div>{arr}</div>,
document.getElementById('example')
);

组件

原生HTML 元素名以小写字母开头,而自定义的React类名以大写字母开头.

1
2
3
4
5
6
7
8
9
10
11
function HelloMessage(props) {
return <h1>Hello World!</h1>;
}

//为用户自定义的组件
const element = <HelloMessage />;

ReactDOM.render(
element,
document.getElementById('example')
);
1
2
3
4
5
6
7
8
9
10
11
//使用函数定义组件
function HelloMessage(props) {
return <h1>Hello World!</h1>;
}

//使用ES6 class定义组件
class Welcome extends React.Component {
render() {
return <h1>Hello World</h1>;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="example"></div>
<script type="text/babel">
function HelloMessage(props) {
//使用props.属性取数据.
return <h1>Hello {props.name}!</h1>;
}

const element = <HelloMessage name="哈哈"/>;

ReactDOM.render(
element,
document.getElementById('example')
);
</script>

复合组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Name(props) {
return <h1>姓名:{props.name}</h1>;
}
function Url(props) {
return <h1>地址:{props.url}</h1>;
}
function Nickname(props) {
return <h1>昵称:{props.nickname}</h1>;
}
function App() {
return (
<div>
<Name name="哈哈哈" />
<Url url="c@cscar.me" />
<Nickname nickname="芜湖起飞" />
</div>
);
}

ReactDOM.render(
<App />,
document.getElementById('example')
);

渲染到页面

渲染到页面

单单声明了组件而没有渲染到页面上我们是看不见的(废话),所以我们需要使用 ReactDOM.render()将其渲染到页面指定位置上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// index.html
<html>
// ...
<body>
<div id='root'></div>
</body>
</html>


// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.js'

ReactDOM.render(<App />, document.getElementById('root'))

ReactDOM.render() 的实现位于 /src/renderers/dom/client/ReactMount.js

ReactDOM.render() 函数将会根据 ReactElement 的类型生成相对应的ReactComponent 实例,并调用其 mountComponent()函数进行组件加载(返回 HTML片段),递归加载所有组件后,通过 setInnerHTML 将 HTML 渲染到页面上。

判断需要生成那种 ReactComponent实例根据 ReactElement 对象的 type 属性来决定。对应 HTML 标签的 type 一般为字符串,而自定义的组件则是大写字母开头的组件函数(自定义组件需要 import,而 HTML 标签不需要)。

生成 ReactComponent

React 中生成对应的 ReactComponent实例由 instantiateReactComponent()完成,其实现位于 /src/renderers/shared/stack/reconciler/instantiateReactComponent.js

ReactComponent 分为 3 种:

  • ReactEmptyComponent: 空组件(ReactElement 的 type 属性为 null 或 false 的组件),在浏览器中返回 ReactDOMEmptyComponent
  • ReactHostComponent: 原生组件(ReactElement 为string,number 或 ReactElement 的 type 属性为 string 的组件)。
    • createInternalComponent():该函数用于创建原生组件,在浏览器中返回 ReactDOMComponent
    • createInstanceForText() : 该函数用于创建纯文本组件,在浏览器中返回 ReactDOMTextComponent
  • ReactCompositeComponent: 自定义组件(ReactElement 的 type 属性为 function)

可以发现 React 与平台解耦,使用 ReactEmptyComponentReactHostComponent。而这两种组件会根据平台的不同生成不同的组件对象,在浏览器中则为 ReactDOMEmptyComponentReactDOMComponentReactDOMTextComponent

它们通过 /src/renderers/dom/stack/client/ReactDOMStackInjection.js 进行注入。

/src/renderers 路径下包含各个平台上不同的 ReactComponent 实现,包括 react-art/react-dom/react-native。)

State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
<div id="example"></div>
<script type="text/babel">
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>现在是 {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

ReactDOM.render(
<Clock />,
document.getElementById('example')
);
</script>

从零开始:实现初始化渲染

设置 babel

首先我们需要了解 babel 如何转换 JSX:React JSX transform

babel 可以通过transform-react-jsx插件来设置解析 JSX 之后调用的函数,默认解析为调用 React.createElement()。所以这就是为什么虽然在 JSX 代码中没有使用到 React,却仍然需要导入它。

通过配置 transform-react-jsx插件的 pragma选项可以修改解析后调用的函数。

1
2
3
4
5
6
7
8
// 修改解析为调用 dom() 函数
{
"plugins": [
["transform-react-jsx", {
"pragma": "dom" // 默认 pragma 为 React.createElement
}]
]
}

babel 将会把 JSX 中的标签名作为第一个参数,把 JSX 中的标签属性作为第二个参数,将标签内容作为剩余的参数。传递这些参数给 pragma 选项设置的函数。

PS: 为了方便起见,我们使用默认的解析为 React.createElement()

实现 createElement

createElement()接受至少 2 个参数:元素类型 type(字符串表示原生元素,函数表示自定义元素),元素设置 config。其他参数视为元素的子元素 children。并且该函数返回的是一个 ReactElement 对象,属性包括 type, props, key, ref。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// element.js
class ReactElement {
constructor(type, props, key, ref) {
this.type = type
this.props = props
this.key = key
this.ref = ref
}
}

export function createElement(type, config, ...children){
// ...
return new ReactElement(type, props, key, ref)

然后需要导出 createElement,才能够通过 React.createElement() 的方式调用。

1
2
3
4
5
6
7
8
// index.js
import { createElement } from './element'

const React = {
createElement,
}

export default React

ReactElement需要 props, key 与 ref 参数,这三个参数将通过处理 config 与 children 得到。

我们将从 config 中获取 key 与 ref(若它们存在的话),并且根据 config 得到 props (去除一些不必要的属性),同时将 children 添加到 props 当中。

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
export function createElement(type, config, ...children) {
let props = {}
let key = null
let ref = null

if (config != null) {
ref = config.ref === undefined ? null : config.ref
// 当 key 为数字时,将 key 转换为字符串
key = config.key === undefined ? null : '' + config.key

for (let propsName in config) {
// 剔除一些不需要的属性(key, ref, __self, __source)
if (RESERVED_PROPS.hasOwnProperty(propsName)) {
continue
}

if (config.hasOwnProperty(propsName)) {
props[propsName] = config[propsName]
}
}

props.children = children
}

return new ReactElement(type, props, key, ref)
}

除此之外,添加对 defaultProps 的支持。defaultProps 的使用方式如下:

1
2
3
4
5
6
7
// App.js
class App extends Component {
}

App.defaultProps = {
name: "ahonn"
}

当传入 App 组件的 props 中不包含 name 时,设置默认的 name 为 “ahonn”。具体实现:当 ReactElement 的 type 属性为组件函数且包含 defaultProps 时遍历 props,若 props 中不包含 defaultProps 中的属性时,设置默认的 props。

1
2
3
4
5
6
7
8
9
10
11
export function createElement(type, config, ...children) {
// ...
if (type && type.defaultProps) {
let defaultProps = type.defaultProps
for (let propsName in defaultProps) {
if (props[propsName] === undefined) {
props[propsName] = defaultProps[propsName]
}
}
}
}

目前为止完成了将 JSX 解析为函数调用(这部分由 babel 完成),调用 React.createElement() 生成 ReactElement 对象。

接下来将实现 instantiateReactComponent(),通过 ReactELemnt 生成相对应的 ReactComponent 实例。

实现工厂方法 instantiateReactComponent

instantiateReactComponent(element)接受一个参数 element,该参数可以是 ReactElement 对象,string,number,false 或者 null。

我们将只考虑 Web 端,而不像 React 一样使用适配器模式进行解耦。

ReactElement 生成相应 ReactComponent 实例的规则:

  • element 为 null 或 false 时,生成 ReactDOMEmptyComponent 对象实例
  • element 为 string 或者 number 时,生成 ReactDOMTextComponent 对象实例
  • element 为 object
    • element.type 为 string 时,生成 ReactDOMComponent 对象实例
    • element.type 为 function(组件函数)时,生成 ReactCompositeComponent 对象实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// virtual-dom.js
export function instantiateReactComponent(element) {
let instance = null
if (element === null || element === false) {
instance = new ReactDOMEmptyComponent()
}

if (typeof element === 'string' || typeof element === 'number') {
instance = new ReactDOMTextComponent(element)
}

if (typeof element === 'object') {
let type = element.type
if (typeof type === 'string') {
instance = new ReactDomComponent(element)
} else if (typeof type === 'function'){
instance = new ReactCompositeComponent(element)
}
}
return instance
}

实现 ReactComponent

现在,我们需要有不同的 ReactComponent 类以供 instantiateReactComponent()使用。同时需要实现每个类的 mountComponent() 方法来返回对应的 HTML 片段。

ReactDOMEmptyComponent

ReactDOMEmptyComponent 表示空组件, mountComponent() 方法返回空字符串。

1
2
3
4
5
6
7
8
9
class ReactDOMEmptyComponent {
constructor() {
this._element = null
}

mountComponent() {
return ''
}
}

ReactDOMTextComponent

ReactDOMTextComponent 表示 DOM 文本组件,mountComponent()方法返回对应的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
class ReactDOMTextComponent {
constructor(text) {
this._element = text
this._stringText = '' + text
this._rootID = 0
}

mountComponent(rootID) {
this._rootID = rootID
return this._stringText
}
}

ReactDOMComponent

ReactDOMComponent 表示原生组件,即浏览器支持的标签(div, p, h1, etc.)。mountConponent() 方法返回对应的 HTML 字符串。

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
class ReactDomComponent {
constructor(element) {
let tag = element.type

this._element = element
this._tag = tag.toLowerCase()
this._rootID = 0
}

mountComponent(rootID) {
this._rootID = rootID
if (typeof this._element.type !== 'string') {
throw new Error('DOMComponent\'s Element.type must be string')
}

let ret = `<${this._tag} `
let props = this._element.props
for (var propsName in props) {
if (propsName === 'children') {
continue
}
let propsValue = props[propsName]
ret += `${propsName}=${propsValue}`
}
ret += '>'

let tagContent = ''
if (props.children) {
tagContent = this._mountChildren(props.children)
}
ret += tagContent
ret += `</${this._tag}>`
return ret
}
}

ReactDOMComponentmountComponent()方法会相对复杂一点。具体实现思路是,通过 ReactElement 的 type 与 props 属性拼接对应的 HTML 标签。处理 props 的时候需要跳过 children 属性,因为需要将子组件放在当前组件中。

当存在子组件(children)时,调用 _mountChildren(children)将组件转换为对应的 HTML 片段。具体过程是遍历 children,转换为 ReactComponent 并调用其 mountComponent() 方法。

1
2
3
4
5
6
7
8
9
_mountChildren(children) {
let result = ''
for (let index in children) {
const child = children[index]
const childrenComponent = instantiateReactComponent(child)
result += childrenComponent.mountComponent(index)
}
return result
}

ReactCompositeComponent

ReactCompositeComponent 表示自定义的组件,mountComponent()方法将根据提供的组件函数(element.type)实例化,并调用该组件的 render()方法返回 ReactElement 对象。再通过instantiateReactComponent() 生成对应的 ReactComponent,最后执行该 ReactComponentmountComponent()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ReactCompositeComponent {
constructor(element) {
this._element = element
this._rootId = 0
}

mountComponent(rootID) {
this._rootId = rootID
if (typeof this._element.type !== 'function') {
throw new Error('CompositeComponent\'s Element.type must be function')
}

const Component = this._element.type
const props = this._element.props
const instance = new Component(props)

const renderedElement = instance.render()
const renderedComponent = instantiateReactComponent(renderedElement)
const renderedResult = renderedComponent.mountComponent(rootID)
return renderedResult
}
}

通过 ReactCompositeComponent 将之前的 ReactComponent 联系起来,并递归调用 mountComponent方法得到一段 HTML。最后 render()通过 node.innerHTML 将 HTML 字符串填到页面上对应的容器中

实现 render

最后将之前的实现串起来,利用 innerHTML 将组件渲染到页面上。

1
2
3
4
5
6
7
export function render(element, container) {
const rootID = 0
const mainComponent = instantiateReactComponent(element)
const containerContent = mainComponent.mountComponent(rootID)

container.innerHTML = containerContent
}

到这里就基本上简单的实现了 React 中将组件渲染到页面上的部分。可以通过一个简单的例子验证一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// index.js
import React from './tiny-react'
import ReactDOM from './tiny-react'
import App from './App'

ReactDOM.render(<App />, document.getElementById('root'))

// App.js

import React, { Component } from './tiny-react'

class App extends Component {
render() {
return (
<div>
<span>Hello Work!</span>
</div>
)
}
}

export default App

页面上将显示Hello Work!