用于构建用户界面的 JavaScript 库
React
使创建交互式 UI
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据改变时 React
能有效地更新并正确地渲染组件。以声明式编写 UI
,可以让你的代码更加可靠,且方便调试。创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI
。组件逻辑使用 JavaScript
编写而非模版,因此你可以轻松地在应用中传递数据,并使得状态与 DOM
分离。无论你现在正在使用什么技术栈,你都可以随时引入 React
来开发新特性,而不需要重写现有代码。
1. React 简介
React
的核心思想是:封装组件。各个组件维护自己的状态和UI
,当状态变更,自动重新渲染整个组件。基于这种方式的一个直观感受就是我们不再需要不厌其烦地来回查找某个DOM
元素,然后操作DOM
去更改UI
。
- React 核心概念
- 组件化
JSX
语法Virtual DOM
虚拟节点Data Flow
数据流
import React, { Component } from "react";
import { render } from "react-dom";
class HelloMessage extends Component {
render() {
return <div>Hello {this.props.name}</div>;
}
}
render(<HelloMessage name="John" />, mountNode);
- 组件
React
应用都是构建在组件之上。上面的 HelloMessage
就是一个 React
构建的组件,最后一句 render
会把这个组件显示到页面上的某个元素 mountNode
里面,显示的内容就是 <div>Hello John</div>
。
props
是组件包含的两个核心概念之一,另一个是 state
(这个组件没用到)。可以把 props
看作是组件的配置属性,在组件内部是不变的,只是在调用这个组件的时候传入不同的属性(比如这里的 name
)来定制显示这个组件。
JSX
从上面的代码可以看到将 HTML
直接嵌入了 JS
代码里面,这个就是 React
提出的一种叫 JSX
的语法,这应该是最开始接触 React
最不能接受的设定之一,因为前端被“表现和逻辑层分离”这种思想“洗脑”太久了。但实际上组件的 HTML
是组成一个组件不可分割的一部分,能够将 HTML
封装起来才是组件的完全体,React
发明了 JSX
让 JS
支持嵌入 HTML
不得不说是一种非常聪明的做法,让前端实现真正意义上的组件化成为了可能。
好消息是你可以不一定使用这种语法,后面会进一步介绍 JSX
,到时候你可能就会喜欢上了。现在要知道的是,要使用包含 JSX
的组件,是需要“编译”输出 JS
代码才能使用的,之后就会讲到开发环境。
Virtual DOM
当组件状态 state
有更改的时候,React
会自动调用组件的 render
方法重新渲染整个组件的 UI
。
当然如果真的这样大面积的操作 DOM
,性能会是一个很大的问题,所以 React
实现了一个Virtual DOM
,组件 DOM
结构就是映射到这个 Virtual DOM
上,React
在这个 Virtual DOM
上实现了一个 diff
算法,当要重新渲染组件的时候,会通过 diff
寻找到要变更的 DOM
节点,再把这个修改更新到浏览器实际的 DOM
节点上,所以实际上不是真的渲染整个 DOM
树。这个 Virtual DOM
是一个纯粹的 JS
数据结构,所以性能会比原生 DOM
快很多。
Data Flow
“单向数据绑定”是 React
推崇的一种应用架构的方式。当应用足够复杂时才能体会到它的好处,虽然在一般应用场景下你可能不会意识到它的存在,也不会影响你开始使用 React
,你只要先知道有这么个概念。
2. JSX 语法
React.createElement
- 第一个参数是标签名
- 第二个参数是属性对象
- 第三个参数是子元素
// 创建方法
React.createElement("a", { href: "http://facebook.github.io/react/" }, "Hello!");
// 标签的嵌套
var child = React.createElement("li", null, "Text Content");
var root = React.createElement("ul", { className: "my-list" }, child);
React.render(root, document.body);
// 对于常见的HTML标签,React已经内置了工厂方法
var root = React.DOM.ul({ className: "my-list" }, React.DOM.li(null, "Text Content"));
- 使用 JSX
// 使用HTML标签
// class在JSX里要写成className,因为class在JS里是保留关键字
import React from "react";
import { render } from "react-dom";
var myDivElement = <div className="foo" />;
render(myDivElement, document.getElementById("mountNode"));
import React from "react";
import { render } from "react-dom";
import MyComponent from "./MyComponet";
var myElement = <MyComponent someProperty={true} />;
render(myElement, document.body);
- 属性扩散
- 有时候你需要给组件设置多个属性,你不想一个个写下这些属性,或者有时候你甚至不知道这些属性的名称,这时候
spread attributes
的功能就很有用了。
- 有时候你需要给组件设置多个属性,你不想一个个写下这些属性,或者有时候你甚至不知道这些属性的名称,这时候
var props = {};
props.foo = x;
props.bar = y;
var component = <Component {...props} />;
// 写在后面的属性值会覆盖前面的属性
var props = { foo: "default" };
var component = <Component {...props} foo={"override"} />;
console.log(component.props.foo); // 'override'
3. 单向传递
一般来说,一个组件类由
extends Component
创建,并且提供一个render
方法以及其他可选的生命周期函数、组件相关的事件或方法来定义。
JSX
语法className
代替class
属性,使用驼峰命名render
只能返回一个div
标签- 使用
props
进行父子信息的传递 - 使用函数的方式进行子父信息的传递
// 定义一个TodoList的React组件,通过继承React.Component来实现
class TestComponent extends React.Component {
render() {
return (
<div>
<h3>{this.props.name}</h3>
<ul>
{this.props.addresses.map((it) => (
<li key={it}>{it}</li>
))}
</ul>
<input
type="text"
className="form-control"
onChange={this.props.onChange}
placeholder="搜索"
/>
</div>
);
}
}
ReactDOM.render(
<TestComponent
name="Escape"
addresses={[1, 2, 3]}
onChange={(e) => console.log(e.target.value)}
/>,
document.getElementById("main")
);
- 参数传递
render: function() {
return <p onClick={this.handleClick.bind(this, 'extra param')}>;
},
handleClick: function(param, event) {
// handle click
}
4. 双向绑定
state
是只读的,使用setState
来改变值props
只能从父组件传递给子组件,单向传递state
是组件的内部状态,相当于私有属性,而props
相当于对象的公有属性- 兄弟组件需要通信,需要上溯到共同的父组件
class InputComponent extends React.Component {
render() {
return <input type="text" onChange={(e) => this.props.onChange(e.target.value)} />;
}
}
class LabelComponent extends React.Component {
render() {
return <div>output: {this.props.value}</div>;
}
}
class RootComponent extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
}
render() {
return (
<div>
<InputComponent onChange={(v) => this.setState({ value: v })} />
<LabelComponent value={this.state.value} />
</div>
);
}
}
ReactDOM.render(<RootComponent />, document.getElementById("main"));
5. 组件生命周期
- 生命周期函数
- 更新方式 - 在 react 中触发 render 的有 4 条路径
- 首次渲染
Initial Render
- 调用
this.setState
- 并不是一次
setState
会触发一次render
,React
可能会合并操作,再一次性进行render
- 父组件发生更新
- 一般就是 props 发生改变,但是就算
props
没有改变或者父子组件之间没有数据交换也会触发render
- 调用
this.forceUpdate
- 首次渲染
6. DOM 操作
大部分情况下你不需要通过查询
DOM
元素去更新组件的UI
,你只要关注设置组件的状态(setState
)。但是可能在某些情况下你确实需要直接操作DOM
。
findDOMNode()
findDOMNode()
不能用在无状态组件上- 当组件加载到页面上之后(
mounted
),通过react-dom
提供的findDOMNode()
方法拿到组件对应的DOM
元素
import { findDOMNode } from 'react-dom';
// Inside Component class
componentDidMound() {
const el = findDOMNode(this);
}
Refs
- 通过在要引用的
DOM
元素上面设置一个ref
属性指定一个名称,然后通过this.refs.name
来访问对应的DOM
元素
- 通过在要引用的
class App extends Component {
constructor() {
return { userInput: "" };
}
handleChange(e) {
this.setState({ userInput: e.target.value });
}
clearAndFocusInput() {
this.setState({ userInput: "" }, () => {
this.refs.theInput.focus();
});
}
render() {
return (
<div>
<div onClick={this.clearAndFocusInput.bind(this)}>Click to Focus and Reset</div>
<input
ref="theInput"
value={this.state.userInput}
onChange={this.handleChange.bind(this)}
/>
</div>
);
}
}
- 总结
- 你可以使用
ref
到的组件定义的任何公共方法,比如this.refs.myTypeahead.reset()
Refs
是访问到组件内部DOM
节点唯一可靠的方法Refs
会自动销毁对子组件的引用(当子组件删除时)- 不要在
render
或者render
之前访问refs
- 不要滥用
refs
,比如只是用它来按照传统的方式操作界面UI
:找到DOM
-> 更新DOM
- 你可以使用
7. 进化 Flux
我们可以先通过对比
Redux
和Flux
的实现来感受一下Redux
带来的惊艳。
首先是 action creators
,Flux
是直接在 action
里面调用 dispatch
。
export function addTodo(text) {
AppDispatcher.dispatch({
type: ActionTypes.ADD_TODO,
text: text,
});
}
Redux
把它简化成了这样。
export function addTodo(text) {
return {
type: ActionTypes.ADD_TODO,
text: text,
};
}
这一步把 dispatcher
和 action
解藕了,很快我们就能看到它带来的好处。接下来是 Store
,这是 Flux
里面的 Store
。
let _todos = [];
const TodoStore = Object.assign(new EventEmitter(), {
getTodos() {
return _todos;
},
});
AppDispatcher.register(function (action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
_todos = _todos.concat([action.text]);
TodoStore.emitChange();
break;
}
});
export default TodoStore;
Redux
把它简化成了这样。同样把 dispatch
从 Store
里面剥离了,Store
变成了一个 pure function
(纯函数):(state, action) => state
。
const initialState = { todos: [] };
export default function TodoStore(state = initialState, action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
return { todos: state.todos.concat([action.text]) };
default:
return state;
}
8. Redux 基础
三个基本原则
- 整个应用只有唯一一个可信数据源,也就是只有一个
Store
State
只能通过触发Action
来更改State
的更改必须写成纯函数,也就是每次更改总是返回一个新的State
,在Redux
里这种函数称为Reducer
- 整个应用只有唯一一个可信数据源,也就是只有一个
Actions
Action
很简单,就是一个单纯的包含 { type, payload }
的对象,type
是一个常量用来标示动作类型,payload
是这个动作携带的数据。Action 需要通过 store.dispatch()
方法来发送。比如一个最简单的 action
:
{
type: 'ADD_TODO',
text: 'Build my first Redux app'
}
一般来说,会使用函数(Action Creators
)来生成 action
,这样会有更大的灵活性,Action Creators
是一个 **pure function
**,它最后会返回一个 action
对象:
function addTodo(text) {
return {
type: "ADD_TODO",
text,
};
}
所以现在要触发一个动作只要调用 dispatch
: dispatch(addTodo(text))
,稍后会讲到如何拿到 store.dispatch
。
- Reducers
Reducer
用来处理 Action
触发的对状态树的更改。所以一个 reducer
函数会接受 oldState
和 action
两个参数,返回一个新的 state:(oldState, action) => newState
。一个简单的 reducer
可能类似这样:
const initialState = {
a: "a",
b: "b",
};
function someApp(state = initialState, action) {
switch (action.type) {
case "CHANGE_A":
return { ...state, a: "Modified a" };
case "CHANGE_B":
return { ...state, b: action.payload };
default:
return state;
}
}
值得注意的有两点:
- 我们用到了
object spread
语法 确保不会更改到oldState
而是返回一个newState
- 对于不需要处理的
action
,直接返回oldState
Reducer
也是 pure function
,这点非常重要,所以绝对不要在 reducer
里面做一些引入 side-effects
的事情,比如:
- 直接修改
state
参数对象 - 请求
API
- 调用不纯的函数,比如
Data.now()
Math.random()
因为 Redux
里面只有一个 Store
,对应一个 State
状态,所以整个 State
对象就是由一个 reducer
函数管理,但是如果所有的状态更改逻辑都放在这一个 reducer
里面,显然会变得越来越巨大,越来越难以维护。得益于纯函数的实现,我们只需要稍微变通一下,让状态树上的每个字段都有一个 reducer
函数来管理就可以拆分成很小的 reducer
了:
function someApp(state = {}, action) {
return {
a: reducerA(state.a, action),
b: reducerB(state.b, action),
};
}
对于 reducerA
和 reducerB
来说,他们依然是形如:(oldState, action) => newState
的函数,只是这时候的 state
不是整个状态树,而是树上的特定字段,每个 reducer
只需要判断 action
,管理自己关心的状态字段数据就好了。Redux
提供了一个工具函数 combineReducers
来简化这种 reducer
合并:
import { combineReducers } from "redux";
const someApp = combineReducers({
a: reducerA,
b: reducerB,
});
如果 reducer 函数名字和字段名字相同,利用 ES6 的 Destructuring 可以进一步简化成:combineReducers({ a, b }),象 someApp 这种管理整个 State 的 reducer,可以称为 root reducer。
- Store
现在有了 Action
和 Reducer
,Store
的作用就是连接这两者,Store
的作用有这么几个:
Hold
住整个应用的State
状态树- 提供一个
getState()
方法获取State
- 提供一个
dispatch()
方法发送action
更改State
- 提供一个
subscribe()
方法注册回调函数监听State
的更改
创建一个 Store
很容易,将 root reducer
函数传递给 createStore
方法即可:
import { createStore } from "redux";
import someApp from "./reducers";
let store = createStore(someApp);
// 你也可以额外指定一个初始 State(initialState),这对于服务端渲染很有用
// let store = createStore(someApp, window.STATE_FROM_SERVER);
现在我们就拿到了 store.dispatch
,可以用来分发 action
了:
let unsubscribe = store.subscribe(() => console.log(store.getState()));
// Dispatch
store.dispatch({ type: "CHANGE_A" });
store.dispatch({ type: "CHANGE_B", payload: "Modified b" });
// Stop listening to state updates
unsubscribe();
9. Data Flow
Data Flow
只是一种应用架构的方式,比如数据如何存放,如何更改数据,如何通知数据更改等等,所以它不是React
提供的额外的什么新功能,可以看成是使用React
构建大型应用的一种最佳实践。
- 官方的
Flux
- 更优雅的
Redux
(Redux(oldState) => newState
)
以上提到的 store.dispatch(action) -> reducer(state, action) -> store.getState()
其实就构成了一个“单向数据流”,我们再来总结一下。
1. 调用 store.dispatch(action)
- Action 是一个包含
{ type, payload }
的对象,它描述了“发生了什么”,比如:
{ type: 'LIKE_ARTICLE', articleID: 42 }
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
{ type: 'ADD_TODO', text: 'Read the Redux docs.' }
- 你可以在任何地方调用
store.dispatch(action)
,比如组件内部,Ajax
回调函数里面等等。
2. Action
会触发给 Store
指定的 root reducer
root reducer
会返回一个完整的状态树,State
对象上的各个字段值可以由各自的reducer
函数处理并返回新的值。reducer
函数接受(state, action)
两个参数reducer
函数判断action.type
然后处理对应的action.payload
数据来更新并返回一个新的state
3. Store
会保存 root reducer
返回的状态树
- 新的
State
会替代旧的State
,然后所有store.subscribe(listener)
注册的回调函数会被调用,在回调函数里面可以通过store.getState()
拿到新的State
。 - 这就是
Redux
的运作流程,接下来看如何在React
里面使用Redux
。
10. React 中使用 Redux
和 Flux
类似,Redux
也是需要注册一个回调函数 store.subscribe(listener)
来获取 State
的更新,然后我们要在 listener
里面调用 setState()
来更新 React
组件。
Redux
官方提供了 react-redux
来简化 React
和 Redux
之间的绑定,不再需要像 Flux
那样手动注册/解绑回调函数。
接下来看一下是怎么做到的,react-redux 只有两个 API
<Provider>
<Provider>
作为一个容器组件,用来接受Store
,并且让Store
对子组件可用,用法如下:
import { render } from "react-dom";
import { Provider } from "react-redux";
import App from "./app";
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
- 这时候
<Provider>
里面的子组件<App />
才可以使用connect
方法关联store
。 <Provider>
的实现很简单,他利用了React
一个(暂时)隐藏的特性Contexts
,Context
用来传递一些父容器的属性对所有子孙组件可见,在某些场景下面避免了用props
传递多层组件的繁琐,要想更详细了解Contexts
可以参考这篇文章。
Connect
connect()
这个方法略微复杂一点,主要是因为它的用法非常灵活:connect([mapStateToProps], mapDispatchToProps], [mergeProps], [options])
,它最多接受 4 个参数,都是可选的,并且这个方法调用会返回另一个函数,这个返回的函数来接受一个组件类作为参数,最后才返回一个和Redux store
关联起来的新组件,类似这样:
class App extends Component { ... }
export default connect()(App);
- 这样就可以在
App
这个组件里面通过props
拿到Store
的dispatch
方法,但是注意现在的App
没有监听Store
的状态更改,如果要监听 Store 的状态更改,必须要指定mapStateToProps
参数。 - 先来看它的参数:
[mapStateToProps(state, [ownProps]): stateProps]
: 第一个可选参数是一个函数,只有指定了这个参数,这个关联(connected)组件才会监听 Redux Store 的更新,每次更新都会调用mapStateToProps
这个函数,返回一个字面量对象将会合并到组件的props
属性。ownProps
是可选的第二个参数,它是传递给组件的props
,当组件获取到新的props
时,ownProps
都会拿到这个值并且执行mapStateToProps
这个函数。[mapDispatchProps(dispatch, [ownProps]): dispatchProps]
: 这个函数用来指定如何传递dispatch
给组件,在这个函数里面直接 dispatch action creator,返回一个字面量对象将会合并到组件的props
属性,这样关联组件可以直接通过props
调用到action
, Redux 提供了一个bindActionCreators()
辅助函数来简化这种写法。 如果省略这个参数,默认直接把dispatch
作为props
传入。ownProps
作用同上。- 剩下的两个参数比较少用到,更详细的说明参看官方文档,其中提供了很多简单清晰的用法示例来说明这些参数。
具体例子
Redux
创建Store
,Action
,Reducer
这部分就省略了,这里只看react-redux
的部分。
import React, { Component } from "react";
import someActionCreator from "./actions/someAction";
import * as actionCreators from "./actions/otherAction";
function mapStateToProps(state) {
return {
propName: state.propName,
};
}
function mapDispatchProps(dispatch) {
return {
someAction: (arg) => dispatch(someActionCreator(arg)),
otherActions: bindActionCreators(actionCreators, dispatch),
};
}
class App extends Component {
render() {
// `mapStateToProps` 和 `mapDispatchProps` 返回的字段都是 `props`
const { propName, someAction, otherActions } = this.props;
return <div onClick={someAction.bind(this, "arg")}>{propName}</div>;
}
}
export default connect(mapStateToProps, mapDispatchProps)(App);
- 如前所述,这个
connected
的组件必须放到<Provider>
的容器里面,当State
更改的时候就会自动调用mapStateToProps
和mapDispatchProps
从而更新组件的props
。 组件内部也可以通过props
调用到action
,如果没有省略了mapDispatchProps
,组件要触发action
就必须手动dispatch
,类似这样:this.props.dispatch(someActionCreator('arg'))
。
11. 服务器端渲染
React
提供了两个方法 renderToString
和 renderToStaticMarkup
用来将组件(Virtual DOM
)输出成 HTML
字符串,这是 React
服务器端渲染的基础,它移除了服务器端对于浏览器环境的依赖,所以让服务器端渲染变成了一件有吸引力的事情。
服务器端渲染除了要解决对浏览器环境的依赖,还要解决两个问题:
- 前后端可以共享代码
- 前后端路由可以统一处理
React
生态提供了很多选择方案,这里我们选用 Redux
和 react-router
来做说明。
- Redux
Redux
提供了一套类似 Flux
的单向数据流,整个应用只维护一个 Store
,以及面向函数式的特性让它对服务器端渲染支持很友好。
2 分钟了解Redux
是如何运作的
关于 Store
:
- 整个应用只有一个唯一的 Store
- Store 对应的状态树(State),由调用一个 reducer 函数(root reducer)生成
- 状态树上的每个字段都可以进一步由不同的 reducer 函数生成
- Store 包含了几个方法比如
dispatch
,getState
来处理数据流 - Store 的状态树只能由
dispatch(action)
来触发更改
Redux
的数据流:
- action 是一个包含
{ type, payload }
的对象 - reducer 函数通过
store.dispatch(action)
触发 - reducer 函数接受
(state, action)
两个参数,返回一个新的 state - reducer 函数判断
action.type
然后处理对应的action.payload
数据来更新状态树
所以对于整个应用来说,一个 Store 就对应一个 UI 快照,服务器端渲染就简化成了在服务器端初始化 Store,将 Store 传入应用的根组件,针对根组件调用 renderToString
就将整个应用输出成包含了初始化数据的 HTML。
- react-router
react-router
通过一种声明式的方式匹配不同路由决定在页面上展示不同的组件,并且通过 props
将路由信息传递给组件使用,所以只要路由变更,props
就会变化,触发组件 re-render
。
假设有一个很简单的应用,只有两个页面,一个列表页 /list
和一个详情页 /item/:id
,点击列表上的条目进入详情页。
可以这样定义路由,./routes.js
import React from "react";
import { Route } from "react-router";
import { List, Item } from "./components";
// 无状态(stateless)组件,一个简单的容器,react-router 会根据 route
// 规则匹配到的组件作为 `props.children` 传入
const Container = (props) => {
return <div>{props.children}</div>;
};
// route 规则:
// - `/list` 显示 `List` 组件
// - `/item/:id` 显示 `Item` 组件
const routes = (
<Route path="/" component={Container}>
<Route path="list" component={List} />
<Route path="item/:id" component={Item} />
</Route>
);
export default routes;
- Reducer
Store
是由 reducer
产生的,所以 reducer
实际上反映了 Store
的状态树结构
./reducers/index.js
import listReducer from "./list";
import itemReducer from "./item";
export default function rootReducer(state = {}, action) {
return {
list: listReducer(state.list, action),
item: itemReducer(state.item, action),
};
}
rootReducer
的 state
参数就是整个 Store
的状态树,状态树下的每个字段对应也可以有自己的 reducer
,所以这里引入了 listReducer
和 itemReducer
,可以看到这两个 reducer
的 state
参数就只是整个状态树上对应的 list
和 item
字段。
./reducers/list.js
const initialState = [];
export default function listReducer(state = initialState, action) {
switch (action.type) {
case "FETCH_LIST_SUCCESS":
return [...action.payload];
default:
return state;
}
}
list
就是一个包含 items
的简单数组,可能类似这种结构:[{ id: 0, name: 'first item'}, {id: 1, name: 'second item'}]
,从 'FETCH_LIST_SUCCESS'
的 action.payload
获得。然后是 ./reducers/item.js
,处理获取到的 item
数据。
const initialState = {};
export default function listReducer(state = initialState, action) {
switch (action.type) {
case "FETCH_ITEM_SUCCESS":
return [...action.payload];
default:
return state;
}
}