React入门基础教程


用于构建用户界面的 JavaScript 库

React 使创建交互式 UI 变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。以声明式编写 UI,可以让你的代码更加可靠,且方便调试。创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI。组件逻辑使用 JavaScript 编写而非模版,因此你可以轻松地在应用中传递数据,并使得状态与 DOM 分离。无论你现在正在使用什么技术栈,你都可以随时引入 React 来开发新特性,而不需要重写现有代码。

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 发明了 JSXJS 支持嵌入 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入门基础教程 - 生命周期函数

  • 更新方式 - 在 react 中触发 render 的有 4 条路径
    • 首次渲染Initial Render
    • 调用this.setState
    • 并不是一次setState会触发一次renderReact可能会合并操作,再一次性进行render
    • 父组件发生更新
    • 一般就是 props 发生改变,但是就算props没有改变或者父子组件之间没有数据交换也会触发render
    • 调用this.forceUpdate

React入门基础教程 - 更新方式


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

我们可以先通过对比 ReduxFlux 的实现来感受一下 Redux 带来的惊艳。

首先是 action creatorsFlux 是直接在 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,
  };
}

这一步把 dispatcheraction 解藕了,很快我们就能看到它带来的好处。接下来是 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 把它简化成了这样。同样把 dispatchStore 里面剥离了,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 基础

React入门基础教程 - 工作流程

  • 三个基本原则

    • 整个应用只有唯一一个可信数据源,也就是只有一个 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,
  };
}

所以现在要触发一个动作只要调用 dispatchdispatch(addTodo(text)),稍后会讲到如何拿到 store.dispatch

  • Reducers

Reducer 用来处理 Action 触发的对状态树的更改。所以一个 reducer 函数会接受 oldStateaction 两个参数,返回一个新的 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),
  };
}

对于 reducerAreducerB 来说,他们依然是形如:(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

现在有了 ActionReducerStore 的作用就是连接这两者,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 来简化 ReactRedux 之间的绑定,不再需要像 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 一个(暂时)隐藏的特性 ContextsContext 用来传递一些父容器的属性对所有子孙组件可见,在某些场景下面避免了用 props 传递多层组件的繁琐,要想更详细了解 Contexts 可以参考这篇文章

Connect

  • connect() 这个方法略微复杂一点,主要是因为它的用法非常灵活:connect([mapStateToProps], mapDispatchToProps], [mergeProps], [options]),它最多接受 4 个参数,都是可选的,并且这个方法调用会返回另一个函数,这个返回的函数来接受一个组件类作为参数,最后才返回一个和 Redux store 关联起来的新组件,类似这样:
class App extends Component { ... }

export default connect()(App);
  • 这样就可以在 App 这个组件里面通过 props 拿到 Storedispatch 方法,但是注意现在的 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 创建 StoreActionReducer 这部分就省略了,这里只看 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 更改的时候就会自动调用 mapStateToPropsmapDispatchProps 从而更新组件的 props。 组件内部也可以通过 props 调用到 action,如果没有省略了 mapDispatchProps,组件要触发 action 就必须手动 dispatch,类似这样:this.props.dispatch(someActionCreator('arg'))

11. 服务器端渲染

React 提供了两个方法 renderToStringrenderToStaticMarkup 用来将组件(Virtual DOM)输出成 HTML 字符串,这是 React 服务器端渲染的基础,它移除了服务器端对于浏览器环境的依赖,所以让服务器端渲染变成了一件有吸引力的事情。

服务器端渲染除了要解决对浏览器环境的依赖,还要解决两个问题:

  • 前后端可以共享代码
  • 前后端路由可以统一处理

React 生态提供了很多选择方案,这里我们选用 Reduxreact-router 来做说明。

  • Redux

Redux 提供了一套类似 Flux 的单向数据流,整个应用只维护一个 Store,以及面向函数式的特性让它对服务器端渲染支持很友好。

2 分钟了解Redux是如何运作的

关于 Store

  • 整个应用只有一个唯一的 Store
  • Store 对应的状态树(State),由调用一个 reducer 函数(root reducer)生成
  • 状态树上的每个字段都可以进一步由不同的 reducer 函数生成
  • Store 包含了几个方法比如 dispatchgetState 来处理数据流
  • 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),
  };
}

rootReducerstate 参数就是整个 Store 的状态树,状态树下的每个字段对应也可以有自己的 reducer,所以这里引入了 listReduceritemReducer,可以看到这两个 reducerstate 参数就只是整个状态树上对应的 listitem 字段。

  • ./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;
  }
}

文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !
  目录