npm i create-react-app -g
全局安装脚手架create-react-app project_name
新建项目cd project_name
进入项目目录npm start
运行项目第一个 React 应用诞生!
所谓 JSX
其实是JS的语法扩展,从代码中也可以看到,这种语法让我们可以在JS中编写像 HTML
一样的代码。但是我们需要明白 JSX
和 HTML
到底有什么不同:
JSX
中使用的“元素”不局限于 HTML
中的元素,可以是任何一个React组件(React判断一个元素是 HTML
元素还是React组件的原则就是看第一个字母是否大写)JSX
中可以通过 onClick
这样的方式给一个元素添加一个事件处理函数(注意是 onClick
不是 onclick
)看到上面第二条是不是感觉到一个困惑,长期以来不是一直不提倡在 HTML
中使用事件直接绑定元素(onclick
)嘛,那为什么在React JSX
中我们却要使用 onClick
这样的方式来绑定事件处理函数呢?
首先说一下 HTML
中直接使用 onclick
绑定事件处理函数不专业的原因:
onclick
添加的事件处理函数是全局环境下执行的,污染了全局环境很容易产生bugonclick
事件可能会影响网页在重绘时候的性能onclick
的DOM元素,如果要动态从DOM树中删除的话,需要把对应的事件处理注销,否则可能造成内存泄漏产生bug而这些问题,JSX
中都不存在:
onClick
挂载的每个函数都可以控制在组件范围内,不会污染全局空间onClick
都挂载一个事件处理函数要高要了解一个新产品的特点和必要,最好的方法就是拿这个新产品和旧产品作比较,这里拿 jQuery
来作比较。
web1.0 时,所有和状态数据相关的操作,都已经由服务器完成了,前端开发只需要根据state(状态数据)来决定view(页面)。这时候的前端开发思维是一个从state到view的“单向流”(当state变化时只要简单粗暴的刷新页面即可,服务器会把最新数据的页面渲染完返回到浏览器)。
这种方式的显著缺陷是:
于是,出现了ajax技术,web2.0时代到来,出现了大量的交互细腻内容丰富的应用,同时 jQuery
库也得到了很大的应用。
jQuery
的解决方案是:根据 CSS
规则找到对应 id
值的元素,挂上一个匿名事件处理函数,在事件处理函数中选中需要被修改的DOM元素,读取其中的文本值加以修改,然后修改这个DOM元素。
这是一种最容易理解的开发模式(找到它,然后修改它),但是当项目越来越庞大,这种模式会造成代码结构复杂,难以维护,特别是当各种交互操作耦合起来以后,这种局部修改就会消耗大量的脑细胞,很容易变得顾此失彼,相信每个 jQuery
使用者都会是这种体会。
还是改变state,让view自动更新这种“单向流”更符合程序员的开发思维,所有各种新型框架应运而生。
React
开发应用组件并没有像 jQuery
那样“找到它,然后做一些事”。而是像一个函数(render),用户看到的界面(UI)就是这个函数的执行结果,只接受数据(data)作为参数,就像这样:
U I = r e n d e r ( s t a t e ) UI=render(state) UI=render(state)
根据确定的交互状态(state)一股脑的决定页面的呈现(view),这种“单向流”的开发状态对程序员来说是思维清晰、比较轻松的。
React定义组件的生命周期可能会经历以下三个过程:
Mounting
),当组件实例被创建并插入 DOM 中时。Updating
),当组件的 props
或 state
发生变化时会触发更新。Unmounting
),当组件从 DOM 中移除时。三种不同的过程会依次调用组件的一些成员函数,这些函数称为生命周期函数,定制一个React组件,实际上就是定制这些生命周期函数,生命周期图谱 如下,详细的关于每个生命周期的介绍可以查看相应生命周期API:
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
componentWillUnmount()
static getDerivedStateFromError()
componentDidCatch()
React组件数据分为两种:props
和 state
。设计组件的时候关于数据的一个原则就是,props
是组件的对外接口,state
是组件的内部状态,对外用 props
,内部用 state
。
props
是从外部传递给组件的数据,因为每个组件都是独立存在的模块,组件之外的一切都是外部世界,外部世界就是通过 props
来和组件对话。
具体使用呢,就像组件的 HTML
属性一样写在组件标签上的就是它的 props
<SampleButton onClick={onButtonClick} msg='Click me' />
一个组件需要定义自己的构造函数一定要在构造函数的第一行通过 super
调用父类(也就是 React.Component
)的构造函数,否则类实例的所有成员函数都无法通过 this.props
访问到父组件传递过来的 props
值,props
的读取也是很简单:
class SampleButton extents Component {
constructor(props){
super(props)
console.log(props.msg) // Click me
}
}
state
代表组件的内部状态,也就是组件内部使用的变量放在 state
中,且 state
必须是对象,即使这个组件只有一个属性。需要注意的是,修改 state
中的值的时候需要使用 setState
方法,而不能直接重新赋值state
的值:
this.setState({count: this.state.count + 1})
// 不能这样赋值
// this.state.count = this.state.count + 1
组件的 state
存在的意义就是被修改,每一次通过 this.setState
函数修改 state
就改变了组件的状态然后通过渲染过程把这种变化体现出来,但是组件绝不应该去修改 props
的值,这样可能让程序陷入一团混乱中,完全违背React设计的初衷。
注意:避免将
props
的值复制给state
!如此做毫无必要,同时还产生了 bug。
在下面的案例中,每个 Counter
组件都有自己的状态记录当前计数,而父组件 ControlPanel
也有一个状态来存储所有 Counter
计数之和,也就是数据发生了重复。数据如果出现了重复,带来的问题就是如何保证重复的数据始终一致,如果数据存多份而且不一致,那就很难决定到底使用哪个数据作为正确结果了。
还有一个问题就是如果在一个应用中包含三级或者三级以上的组件结构,顶层的祖父组件想要传递一个数据给最低层的子组件,如果用 props
的方式,就只能通过父组件中转,即使这个父组件根本不需要这个值,那也要搬运这个值,暂且不说是不是违反了低耦合的设计要求了,想想都恶心不是么… …
所以,全局状态就是唯一可靠的数据源,这就是 Flux
和 Redux
中 Store
的概念。
The Flux project is in maintenance mode and there are many more sophisticated alternatives available (e.g.
Redux
,MobX
) and we would recommend using them instead.
官方建议使用Redux
,MobX
等其他方式,不建议使用Flux
了,所以这里仅仅当做一个理解即可。
Flux 和 React 没有直接的关系,二者是完全独立的概念。Flux 是一种前端代码的组织思想,比如说 Redux 库就可以认为是一种 Flux 思想的实现。
对于服务器端开发,Model 指的是和处理业务数据相关的代码,例如实现数据库的增删改查等;View 指的是和页面组装相关的代码,例如各种后端模板引擎等;Controller 指的是和用户交互行为的代码,指的就是各种 http 请求的 handler,并且和后端的 router 紧密相关,根据不同 url 和 http 请求参数将数据和模板绑定在一起,最终形成页面呈现给用户。
对于前端而言,Model 相当于后台数据;View 对应页面的内容,html、css等;Controller 主要是用户和网页之间的交互事件的handler,例如 click、enter事件等。
当项目中的“单向流”被破坏,修改 Model 的 Controller 代码像一把黄豆一样散落在各个 View 组件中时,此时就需要一个特定的方式将这些散落的黄豆(行为)单独聚拢在一起。
参考 http 请求,我们将要定义的 action,需要一个 typeName 用来表示对 Model 操作的意图(类似于http 请求的 url 路径),还可能需要其他字段,用来描述怎样具体操作 Model(类似于 http 请求的参数)。也就是说,当用户在 View 上的交互行为(例如点击事件)应当引起 Model 发生变化时,我们不直接修改 Model,而是简单的 dispatch 一个 action 来表达我们修改的意图,这些 action 被集中起来转移到数据端来管理。
所以,从代码层面来看,Flux 相当于一个event dispatcher,目的就是要将以往 MVC 中分散在各个不同 View 组件内的 Controller 代码片段提取出来放到更恰当的地方集中管理,形成一个更加容易驾驭的“单向流”模式。Flux 可以说是对前端 MVC 思想的补充和优化吧。
Counter 组件和 Summary 组件需要共用一套数据,所以这里采用 Flux 的方式来统一管理这个共用的数据,下面是从 Model 到 View 的数据流方式:
那从 View 到 Model 的改动呢?比如 Counter 中对数据进行增减操作时,如何将数据的改变再映射到各个组件中去?
AppDispatcher.js
文件Counter
和 Summary
)都有一个自己的 store
用来注册这个模块中的动作(注册到 AppDispatcher
上)Actions.js
文件用来存储所有的动作(也就是 Controller),并通过 AppDispatcher
来派发这些动作总结就是:不同模块中的动作都通过在对应模块的 xxxStore.js
文件中将动作注册到 AppDispatcher
上,同时所有动作都集中管理在 Actions
中并通过 AppDispatcher
来派发注册在上面的具体动作的 handler。
// AppDispatcher.js
import { Dispatcher } from 'flux'
export default new Dispatcher()
// Actions.js
import AppDispatcher from './AppDispatcher'
export const increment = counterCaption => {
AppDispatcher.dispatch({
type: 'INCREMENT', // 相当于http请求的url路径,表示对model操作的意图
counterCaption // 相当于http请求的参数,描述怎样具体操作model
})
}
export const decrement = counterCaption => {
AppDispatcher.dispatch({
type: 'DECREMENT',
counterCaption
})
}
// Counter.js
import { Component } from 'react'
import CounterStore from './store/CounterStore'
import * as Actions from './store/Actions'
class Counter extends Component {
constructor(props) {
super(props)
this.state = {
count: CounterStore.getCounterValues()[props.caption]
}
}
onClickIncrementButton = () => {
Actions.increment(this.props.caption)
}
onClickDecrementButton = () => {
Actions.decrement(this.props.caption)
}
componentDidMount() {
CounterStore.on('changed', () => {
const new_count = CounterStore.getCounterValues()[this.props.caption]
this.setState({count: new_count})
})
}
render() {
return (
<div>
<button onClick={this.onClickIncrementButton}>+</button>
<button onClick={this.onClickDecrementButton}>-</button>
<span>{this.props.caption} count: {this.state.count}</span>
</div>
)
}
}
export default Counter
// CounterStore.js
import { EventEmitter } from 'events'
import AppDispatcher from './AppDispatcher'
let counterValues = {
'First': 0,
'Second': 10,
'Third': 30
}
const CounterStore = Object.assign({}, EventEmitter.prototype, {
getCounterValues: () => counterValues
})
CounterStore.dispatchToken = AppDispatcher.register(action => {
if(action.type === 'INCREMENT') {
counterValues[action.counterCaption]++
CounterStore.emit('changed')
} else if(action.type === 'DECREMENT') {
counterValues[action.counterCaption]--
CounterStore.emit('changed')
}
})
export default CounterStore
Counter
组件中点击按钮动作,通过 Actions
在 AppDispatcher
上派发CounterStore
中检测到派发过来的动作,对数据进行修改后通过 CounterStore.emit('changed')
广播出去Counter
中通过对 CounterStore.on('changed', cb)
对刚广播的事件进行触发,此时同一数据源的数据已经进行了修改,直接取出赋值给当前组件即可// Summary.js
import { Component } from 'react'
import SummaryStore from './store/SummaryStore'
class Summary extends Component {
constructor() {
super()
this.state = {
sum: SummaryStore.getSum()
}
}
componentDidMount() {
SummaryStore.on('changed', () => {
this.setState({sum: SummaryStore.getSum()})
})
}
render() {
return (
<div>
<span>Total Count: {this.state.sum}</span>
</div>
)
}
}
export default Summary
// SummaryStore.js
import { EventEmitter } from 'events'
import CounterStore from './CounterStore'
import AppDispatcher from './AppDispatcher'
const SummaryStore = Object.assign({}, EventEmitter.prototype, {
getSum: () => {
let _sum = 0
let _counter_values = CounterStore.getCounterValues()
for(let key in _counter_values) {
_sum += _counter_values[key]
}
return _sum
}
})
SummaryStore.dispatchToken = AppDispatcher.register(action => {
if(action.type === 'INCREMENT' || action.type === 'DECREMENT') {
AppDispatcher.waitFor([CounterStore.dispatchToken])
SummaryStore.emit('changed')
}
})
export default SummaryStore
流程和上面的 Counter
组件流程类似,唯一不同的是 waitFor
函数的应用,它表示需要等 CounterStore 中的派发事件处理完之后再处理这里后面的内容。
到此可以看出,在 Flux 的理念里,如果要改变界面必须改变 Store
中的状态数据,要改变状态数据就必须派发一个 action
给 dispatcher
。在这种规则之下驱动界面改变始于一个动作的派发,别无他法: 我们定义的 action
确实可以参考 http 请求,其中 action.type
用来表示对 Model 操作的意图(类似于http 请求的 url 路径),其他字段,用来描述怎样具体操作 Model(类似于 http 请求的参数)。
Store
之间有逻辑关系就必须用上 waitFor
函数Store
混杂了很多的逻辑和状态数据所以,Redux 出现了…
应用的状态数据应该只存储在唯一的一个 Store
上
在 Flux 中,应用可以拥有多个 Store
,根据功能把应用状态数据进行划分存储给若干个 Store
中,这样容易造成数据冗余,虽然利用 waitFor
可以保证多个 Store
之间的更新顺序,但是产生了不同 Store
之间的依赖关系,说好的不依赖呢?相互独立呢?所以,Redux 整个应用只保持一个 Store
,那如何涉及这个 Store
结构就是 Redux 的核心问题了。
不能直接修改状态数据,要修改必须通过派发的方式,而修改状态数据的方法不是去修改状态数据的值,而是创建一个新的状态对象返回给 Redux,由 Redux 自己去完成新状态数据的组装
这里所说的纯函数就是 Reducer
r e d u c e r ( p r e v i o u s S t a t e , a c t i o n ) = > n e w S t a t e reducer(previousState, action) => newState reducer(previousState,action)=>newState
第一个参数 previousState
是当前状态,第二个参数 action
是接收到的 action
对象(想象一下 http 请求),reducer
根据这两个参数产生一个新的对象返回,只负责计算状态数据,不负责存储状态数据。
// store/index.js
import { createStore } from 'redux'
import Reducer from './Reducer'
const initValues = {
'First': 0,
'Second': 10,
'Third': 30
}
const store = createStore(Reducer, initValues)
export default store
// Reducer.js
const Reducer = (state, action) => {
let _caption = action.counterCaption
// 不会修改 state 本身的值,因为 reducer 是一个纯函数不产生副作用,而是返回一个新值
switch (action.type) {
case 'INCREMENT':
return {...state, [_caption]: state[_caption] + 1}
case 'DECREMENT':
return {...state, [_caption]: state[_caption] - 1}
default:
return state
}
}
export default Reducer
// Actions.js
export const increment = counterCaption => {
return {
type: 'INCREMENT',
counterCaption
}
}
export const decrement = counterCaption => {
return {
type: 'DECREMENT',
counterCaption
}
}
import { Component } from 'react'
import Store from './store'
import * as Actions from './store/Actions'
class Counter extends Component {
constructor(props) {
super(props)
this.state = {
count: Store.getState()[props.caption]
}
}
onClickIncrementButton = () => {
Store.dispatch(Actions.increment(this.props.caption))
}
onClickDecrementButton = () => {
Store.dispatch(Actions.decrement(this.props.caption))
}
componentDidMount() {
Store.subscribe(() => {
this.setState({count: Store.getState()[this.props.caption]})
})
}
render() {
return (
<div>
<button onClick={this.onClickIncrementButton}>+</button>
<button onClick={this.onClickDecrementButton}>-</button>
<span>{this.props.caption} count: {this.state.count}</span>
</div>
)
}
}
export default Counter
Store.getState()
可以获取到当前的 state
的值Store.dispatch(action)
时, Reducer
会接收到 prevState
和这里的 action
进行数据处理生成 newState
Store.subscribe()
可以监听到 state
发生变化(即得到了 newState
),好进行下一步操作同理 Summary
组件流程
import { Component } from 'react'
import Store from './store'
class Summary extends Component {
constructor() {
super()
this.state = {
sum: this.getSum()
}
}
getSum() {
const _state = Store.getState()
let _sum = 0
for(let key in _state) {
if(_state.hasOwnProperty(key)) {
_sum+=_state[key]
}
}
return _sum
}
componentDidMount() {
Store.subscribe(() => {
this.setState({sum: this.getSum()})
})
}
render() {
return (
<div>
<span>Total Count: {this.state.sum}</span>
</div>
)
}
}
export default Summary
Redux 推荐使用 @reduxjs/toolkit
这个库,它其实原理跟 Redux 是一样的,只是更加方便我们的书写方式。看如何改写上面的实例
// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import CounterSlice from './CounterSlice'
const store = configureStore({
reducer: {
counter: CounterSlice.reducer
}
})
export default store
// CounterSlice.js
import { createSlice } from '@reduxjs/toolkit'
const CounterSlice = createSlice({
name: 'counter',
initialState: {
'First': 0,
'Second': 10,
'Third': 30
},
reducers: {
incremented: (state, {payload}) => {
state[payload] += 1
},
decremented: (state, {payload}) => {
state[payload] -= 1
}
}
})
export default CounterSlice
// Counter.js
...
constructor(props) {
super(props)
this.state = {
count: Store.getState().counter[props.caption]
}
}
onClickIncrementButton = () => {
Store.dispatch(CounterSlice.actions.incremented(this.props.caption))
}
onClickDecrementButton = () => {
Store.dispatch(CounterSlice.actions.decremented(this.props.caption))
}
componentDidMount() {
Store.subscribe(() => {
this.setState({count: Store.getState().counter[this.props.caption]})
})
}
...
// Summary.js
...
constructor() {
super()
this.state = {
sum: this.getSum()
}
}
getSum() {
const _state = Store.getState().counter
let _sum = 0
for(let key in _state) {
if(_state.hasOwnProperty(key)) {
_sum+=_state[key]
}
}
return _sum
}
componentDidMount() {
Store.subscribe(() => {
this.setState({sum: this.getSum()})
})
}
...
configureStore
会使用默认设置自动设置好 store
createSlice
允许将 reducer
、state
、action
集中成模块形式书写,这样可以将关联的数据动作统一在一个文件中进行createSlice
的 name
是唯一标识reducers
方式通过使用 immer
包会把修改属性值得方式按照不可变方式修改 state
,不需要手动做副本Store.dispatch(CounterSlice.actions.incremented(this.props.caption))
可以用来触发 action
,是因为 reducers
方法中的每一个 case 都会生成一个 action
。