文章目录
本文旨在帮助 Vue 方向的同学了解 React 用法,所以侧重两者对比。而且是根据我的实践选取了一些我认为需要注意或重点讲解的内容,不涉及深层原理。文中没有提到基本语法和核心 API 建议阅读文档,见下文。
如果没有使用过 Vue 的同学建议直接阅读文档进行学习:
React 中文文档
Redux 英文文档
React-Redux 英文文档
希望对你有帮助,那我们开始吧
一、Vue & React 对比
Vue
template
<template>
<p>{{ greeting }} World!</p>
</template>
<script>
module.exports = {
data: function() {
return {
greeting: "Hello"
};
}
};
</script>
<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>
这是一段 vue 单文件组件的代码,其中 template 模版编译的过程是:vue-template-compiler
将 template 预编译为 render functions 后注入到从 <script>
导出的组件中。
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。
然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
比如使用 iView 的 Table 组件渲染按钮(从 3.2.0 版本开始支持 slot-scope 写法):
{
title: 'Action',
key: 'action',
width: 150,
align: 'center',
render: (h, params) => {
return h('div', [
h('Button', {
props: {
type: 'primary',
size: 'small'
},
style: {
marginRight: '5px'
},
on: {
click: () => {
this.show(params.index)
}
}
}, 'View'),
h('Button', {
props: {
type: 'error',
size: 'small'
},
on: {
click: () => {
this.remove(params.index)
}
}
}, 'Delete')
]);
}
}
渲染函数和JSX可以参考这两篇文章:
终于搞懂了vue 的 render 函数(一) -_-|||
终于搞懂了vue 的 render 函数(二)(๑•̀ㅂ•́)و✧
React
在 React 中,所有的组件的渲染功能都依靠 JSX。Babel 会把 JSX 转译成一个名为 React.createElement()
函数调用。
元素
元素是构成 React 应用的最小砖块。描述了你在屏幕上想看到的内容。
// React 元素
const element = <h1>Hello, world</h1>;
// React 自定义元素
const element = <Welcome name="Sara" />;
组件
从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。
React 元素可以简单理解为是用 <></>
包起来的,而组件返回的是元素。
简单来说组件返回的都是我们即将渲染在页面上的内容,我们的页面就是一个个组件组合起来的,这个概念跟 Vue 是相通的。
props
在组件内部可以直接使用,需要注意:props 是只读的,不能修改。
函数组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
class 组件
- 使用 class 创建组件的时候需要继承
React.Component
- class 内部一定要有一个
render()
函数返回要渲染的元素
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
渲染组件
ReactDOM.render(
element,
document.getElementById('root')
);
State
State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。
state 用于标记当前组件的状态,其他组件不能使用。
- 只有 class 组件可以使用 state
- 构造函数是唯一可以给
this.state
赋值的地方
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
正确使用 State
- 不要直接修改 state,应该使用
this.setState()
// Wrong this.state.comment = 'Hello'; // Correct this.setState({comment: 'Hello'});
- state 的更新可能是异步的,所以你不要依赖他们的值来更新下一个状态。
// Wrong this.setState({ counter: this.state.counter + this.props.increment, }); // Correct this.setState((state, props) => ({ counter: state.counter + props.increment }))
组合
React 有十分强大的组合模式。我们推荐使用组合而非继承来实现组件间的代码重用。
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
<FancyBorder>
JSX 标签中的所有内容都会作为一个children
prop 传递给FancyBorder
组件。因为FancyBorder
将{props.children}
渲染在一个<div>
中,被传递的这些子组件最终都会出现在输出结果中。
少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用
children
,而是自行约定:将所需内容传入 props,并使用相应的 prop。
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
<Contacts />
和<Chat />
之类的 React 元素本质就是对象(object),所以你可以把它们当作 props,像其他数据一样传递。这种方法可能使你想起别的库中“槽”(slot)的概念,但在 React 中没有“槽”这一概念的限制,你可以将任何东西作为 props 进行传递。
事件处理
你必须谨慎对待 JSX 回调函数中的
this
,在 JavaScript 中,class 的方法默认不会绑定this
。如果你忘记绑定this.handleClick
并把它传入了onClick
,当你调用这个函数的时候this
的值为undefined
。
这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理 有关。
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 为了在回调中使用 `this`,这个绑定是必不可少的
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
二、Vuex & Redux 对比
Vuex
一个简单的 store
可以看到 Vuex 的 store 结构是比较清晰的,包含:
- state
- getters
- mutations
- actions
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
double: state => {
return state.count * 2
}
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
工作流程
Redux
这里主要是结合文档梳理下核心概念和 Redux 工作流程。
当用户操作 view 之后发出一个 action,store 会根据这个 action 的 type 来遍历所有的 reducers 找到对应的 reducer 来改变 state。
核心概念
假设你的 state 结构是这样的:
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}
这看起来跟普通对象没有什么不同,不过不能通过直接赋值来改变 state。
Action
Action 是把数据从应用传到 store 的有效载荷。是 store 数据的唯一来源。通过 store.dispatch()
将 action 传到 store。
- action 是改变 state 的唯一方式;
- action 对象必须有
type
属性,用来描述动作类型; type
通常被定义为常量;
Action 创建函数:就是创建 action 的函数。在 Redux 中,action 创建函数只是返回一个 action 对象。这样做使 action 方便使用且易于测试。
actions.js
/*
* action types
*/
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'
/*
* other constants
*/
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}
/*
* action creators
*/
export function addTodo(text) {
return { type: ADD_TODO, text }
}
export function toggleTodo(index) {
return { type: TOGGLE_TODO, index }
}
export function setVisibilityFilter(filter) {
return { type: SET_VISIBILITY_FILTER, filter }
}
也许你会觉得这跟 Vuex 中的 mutation 有点像,但实际上 action 只是描述了执行哪些操作,并没有描述具体怎么执行的。如何改变 state 的代码是在 reducer 中。
Reducer
(previousState, action) => nextState
Reducer 描述了如何更新 state。
- 每个 reducer 都在管理全局 state 中自己负责的部分;
- 每个 reducer 的 state 参数都不同,并且对应于它管理的那部分 state 数据。
比如 visibilityFilter
reducer 的 state 参数对应的是 state.visibilityFilter
,而 todos
reducer 的 state 参数对应的 则是 state.todos
。
reducers.js
import { combineReducers } from 'redux'
import {
ADD_TODO,
TOGGLE_TODO,
SET_VISIBILITY_FILTER,
VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFilters
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
⚠️ reducer 是纯函数,不能有以下操作:
- 修改传入参数;
- 执行有副作用的操作,如 API 请求和路由跳转;
- 调用非纯函数,如
Date.now()
或Math.random()
。
⚠️ 注意
- Redux 首次执行时 state 为
undefined
,可以 reducer 里初始化。 - 不要直接修改 state。而是使用
Object.assign()
新建一个副本,或者使用{ ...state, ...newState }
达到相同的目的。 - 遇到未知的 action 时,一定要返回旧的 state。
combineReducers()
随着应用变得越来越复杂,可以考虑将 reducer 函数拆分成多个单独的函数,拆分后的每个函数负责独立管理 state 的一部分。
combineReducers
辅助函数的作用是,把多个 reducer 合并成一个。
const rootReducer = combineReducers(reducers)
参数:reducers
是一个对象,对象的值是各个 reducer 函数。
返回值:是一个函数。调用参数里所有 reducer 的函数(这个返回的函数也是一个 reducer),并且构造一个与 reducers 对象结构相同的 state 对象,也就是全局 state。
Store
Store 是把 action 和 reducer 联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供
getState()
方法获取 state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器。
createStore
用来创建一个 Redux store 来以存放应用中所有的 state。
const store = createStore(reducer, [preloadedState], enhancer)
index.js
import { createStore } from 'redux'
import todoApp from './reducers'
const store = createStore(todoApp)
组件可以访问 Redux store,通过把它以 props
的形式传入。但这样一层层的传递既繁琐又容易出错。
ReactDOM.render(
<App store={store} />,
document.getElementById('root')
)
三、React-Redux
Redux 本身是一个独立的库,可以与任何UI层或框架一起使用,包括React,Angular,Vue,Ember和vanilla JS。
React-Redux 是 Redux 的官方 React 绑定库。它能够使你的 React 组件从 Redux store 中读取数据,并且向 store 分发 actions 以更新数据。
Provider
<Provider/>
组件是 React-Redux 提供的一个 API,能够使其子组件访问 Redux store。
通常在渲染根组件时使用。
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { App } from './App'
import createStore from './createReduxStore'
const store = createStore()
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
connect
connect()
是 React-Redux 提供的另一个 API,作用是连接 React 组件 和 Redux store。使组件能够访问 state,dispatch actions。
import { login, logout } from './actionCreators'
const mapState = state => state.user
const mapDispatch = { login, logout }
// call connect to generate the wrapper function, and immediately call
// the wrapper function to generate the final wrapper component.
export default connect(
mapState,
mapDispatch
)(Login)
connect()
返回的是一个包装函数。该包装函数接收你的组件,并返回一个被传入了额外 props
的组件。
额外的 props
就是通过 connect()
访问到的 state。
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
这里只讲解前两个参数: mapStateToProps
,mapDispatchToProps
。
mapStateToProps
mapStateToProps
用于从 store 中选择被 connect 的组件所需的数据。
function mapStateToProps(state, ownProps?)
- 参数
- state:Redux store 的完整 state,每次 state 更改时都会调用
mapStateToProps
。 - ownProps:可选参数,被 connect 的组件自身的
props
。
- 返回值
是一个普通对象,包含被 connect 的组件所需要的数据。该对象中的字段将作为props
合并到被 connect 的组件。
mapDispatchToProps
mapDispatchToProps
is used for dispatching actions to the store.
使用 store.dispatch
可以 dispatch 一个 action,这是更改 state 的唯一方法。使用了 React-Redux 之后,我们的组件无需直接访问 store,connect 会帮我们访问。
connect 封装了与 Redux store 对话的逻辑,我们只需要使用它提供的API就行了。
有两种方式可以 dispatch action
第一种是默认方式,第二种要用到我们接下来要说的 mapDispatchToProps
参数。
一、默认(不给 connect 传 mapDispatchToProps
参数)情况下,被 connect 的组件会接收 props.dispatch
来 dispatch action。
connect()(MyComponent)
// which is equivalent with
connect(
null,
null
)(MyComponent)
// or
connect(mapStateToProps /** no second argument */)(MyComponent)
使用:
function Counter({ count, dispatch }) {
return (
<div>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<span>{count}</span>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'RESET' })}>reset</button>
</div>
)
}
二、另外 connect 可以接受 mapDispatchToProps
参数来创建 dispatch 函数并将这些函数作为 props
传递给组件。
⚠️ 如果给 connect 传递了 mapDispatchToProps
参数,则被 connect 的组件将不再接收 props.dispatch
。
mapDispatchToProps 也有两种形式
-
函数形式:允许更多的自定义,能够访问 dispatch,以及可选参数
ownProps
。const mapDispatchToProps = dispatch => { return { // dispatching plain actions increment: () => dispatch({ type: 'INCREMENT' }), decrement: () => dispatch({ type: 'DECREMENT' }), reset: () => dispatch({ type: 'RESET' }) } }
参数:
- dispatch:
store.dispatch
- ownProps:可选参数,组件自身的 props
返回值
- 是一个普通对象。该对象的字段将作为
props
合并到被 connect 的组件的props
。
function Counter({ count, increment, decrement, reset }) { return ( <div> <button onClick={decrement}>-</button> <span>{count}</span> <button onClick={increment}>+</button> <button onClick={reset}>reset</button> </div> ) }
此时的
increment
,decrement
,reset
还可以传递给子组件。 - dispatch:
-
对象形式:更具说明性且易于使用。是函数形式的简写,connect 帮我们做了省略的操作。
import {increment, decrement, reset} from "./counterActions"; const mapDispatchToProps = { increment, decrement, reset } export default connect(mapState, mapDispatchToProps)(Counter); // or export default connect( mapState, { increment, decrement, reset } )(Counter);
异步 Action
Redux Thunk 是处理 Redux 异步逻辑推荐使用的中间件。Redux Thunk 中间件可以让你的 Action 创建函数返回一个函数而不是普通对象。
这里不展开说了,更多内容查看 Redux Thunk 文档 。
启用 Redux-Thunk 需要使用 applyMiddleware()
。
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
// Note: this API requires redux@>=3.1.0
const store = createStore(rootReducer, applyMiddleware(thunk));
// 异步操作网络请求
function fetchSecretSauce() {
return fetch('https://www.google.com/search?q=secret+sauce');
}
// 普通 Action 创建函数:返回普通对象
function makeASandwich(forPerson, secretSauce) {
return {
type: 'MAKE_SANDWICH',
forPerson,
secretSauce,
};
}
// 普通 Action 创建函数:返回普通对象
function apologize(fromPerson, toPerson, error) {
return {
type: 'APOLOGIZE',
fromPerson,
toPerson,
error,
};
}
// 异步 Action 创建函数:返回的是函数。此时的 Action 创建函数就成了 thunk action。
// 当 Action 创建函数返回函数时,这个函数会被 Redux Thunk 中间件执行。
function makeASandwichWithSecretSauce(forPerson) {
return function(dispatch) {
return fetchSecretSauce().then(
(sauce) => dispatch(makeASandwich(forPerson, sauce)),
(error) => dispatch(apologize('The Sandwich Shop', forPerson, error)),
);
};
}
import { connect } from 'react-redux';
import { Component } from 'react';
class SandwichShop extends Component {
componentDidMount() {
this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson));
}
componentDidUpdate(prevProps) {
if (prevProps.forPerson !== this.props.forPerson) {
this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson));
}
}
render() {
return <p>{this.props.sandwiches.join('mustard')}</p>;
}
}
export default connect((state) => ({
sandwiches: state.sandwiches,
}))(SandwichShop);