zzzmj / old-blog Goto Github PK
View Code? Open in Web Editor NEW✏️ 写博客的地方~
✏️ 写博客的地方~
Redux三个基本原则
单一数据源的意思是应用的数据状态只存储在一个唯一的Store中
保持状态只读,意思就是不要去修改状态,要修改Store的状态,只能通过派发一个action对象完成
思考:驱动用户界面更改的是状态,状态只读,那怎么能引起用户界面的改变呢?
答:当然要改,只是我们不去修改状态值,而是创建一个新的状态对象给Redux,由Redux完成新状态的组装
这里说的纯函数是Reducer
reducer(state, action)
第一个参数state是当前的状态
第二个参数action是接收到的action对象,而reducer函数要做的事情,就是根据state和action的值产生一个新的对象返回,注意reducer必须是纯函数,也就是说函数的返回结果必须完全由参数state和action决定,而且不产生任何副作用,也不能修改参数state和action对象。
例子在src目录下,与上节相同的例子加减组件,用Redux改写。
通过三张图了解Redux中的重要概念
先用因为react-redux帮我们省去了很多代码,不利于理解,所以先从redux开始
Action是一个对象,用来代表所有会引起状态(state)变化的行为
action.js如下所示。
// ActionTypes.js
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
// Action.js
import * as ActionTypes from './ActionTypes'
export const increment = (counterCaption) => {
return {
type: ActionTypes.INCREMENT,
counterCaption: counterCaption
}
}
export const decrement = (counterCaption) => {
return {
type: ActionTypes.DECREMENT,
counterCaption: counterCaption
}
}
Store是Redux中数据的统一存储,维护着state的所有内容
import { createStore } from 'redux'
import reducer from './Reducer.js'
const initValue = {
'First': 0,
'Second': 10,
'Third': 20
}
const store = createStore(reducer, initValue)
export default store
Reducer决定着如何更新state
``(previousState, action) => newState```
该函数接收两个参数,一个旧的状态previousState和一个Action对象
然后返回一个新的状态newState,去重新渲染View
class Counter extends Component {
constructor(props) {
super(props)
this.state = this.getOwnState()
//...
}
getOwnState() {
const { caption } = this.props
return {
value: store.getState()[caption]
}
}
}
import store from '../Store'
//...
componentDidMount() {
// 通过store.subscribe()监听组件变化,只要组件的状态变化,就会调用onChange方法
store.subscribe(this.onChange)
}
componentWillUnmount() {
// 组件删除后,注销监听
store.unsubscribe(this.onChange)
}
onChange() {
const newState = this.getOwnState()
this.setState(newState)
}
onClickIncrementButton() {
const { caption } = this.props
store.dispatch(Actions.increment(caption))
}
onClickDecrementButton() {
const { caption } = this.props
store.dispatch(Actions.decrement(caption))
}
大功告成。 看似复杂了很多,增加了很多约束,其实对开发项目有好处,利于提高软件质量
注意一个地方,我们在每个组件里都需要引入store,这样才能更新变化。
但这样做很麻烦,那有啥好的办法呢? 后面会讲到使用Context组件
ps: 其实Redux的核心功能实现并不难,自己实现了一下核心的功能,导入环境能正常使用
// 发布订阅模式
function createStore(reducer, initState) {
let state = initState
let listeners = []
function subscribe(listener) {
listeners.push(listener)
}
function dispatch(action) {
// 按照 reducer 修改 state
state = reducer(state, action)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
}
function getState() {
return state
}
return {
subscribe,
dispatch,
getState,
}
}
export default createStore
想学会react-redux,得先清楚几个概念
在Redux框架下,一个React组件基本上就是要承担两个任务
和Redux打交道,读取Store的状态,用于初始化组件状态。
同时还要监听Store的状态改变
当需要更新Store状态时,就要派发Action对象
Store状态发生改变时,就需要更新组件状态
根据当前props和state,渲染出用户界面
如果每个React组件都要包办这两个任务,似乎事情稍微多了一些。
所以考虑拆分成两个组件,分别承担一个任务
承担第一个任务的叫做容器组件,负责与Redux打交道(也叫聪明组件)
承担第二个任务的叫做傻瓜组件,负责渲染界面(也叫展示组件)
傻瓜组件是一个纯函数,根据容器组件传进来的props产生结果,因此它也是一个无状态组件
状态全交给容器组件打理
改写Counter
class Counter extends Component {
render() {
const { caption, onClickDecrementButton, onClickIncrementButton, value } = this.props
return (
<div>
<button style={style} onClick={onClickIncrementButton} >+</button>
<button style={style} onClick={onClickDecrementButton} >-</button>
<span>{caption} count: {value}</span>
</div>
)
}
}
可以把傻瓜组件写成无状态组件,获取props就不能用this.props,而是通过函数的
参数props获得
const Counter = (props) => {
const { caption, onClickDecrementButton, onClickIncrementButton, value } = props
return (
<div>
<button style={style} onClick={onClickIncrementButton} >+</button>
<button style={style} onClick={onClickDecrementButton} >-</button>
<span>{caption} count: {value}</span>
</div>
)
}
容器组件承担了所有的和Store关联的工作,它的Render函数只负责渲染傻瓜组件Counter,传递必要的prop
import store from '../Store.js';
class CounterContainer extends Component {
constructor(props) {
super(props)
this.state = this.getOwnState()
this.onClickIncrementButton = this.onClickIncrementButton.bind(this)
this.onClickDecrementButton = this.onClickDecrementButton.bind(this)
this.onChange = this.onChange.bind(this)
}
shouldComponentUpdate(nextProps, nextState) {
// 避免不必要的渲染
return (nextProps.caption !== this.props.caption) ||
(nextState.value !== this.state.value);
}
componentDidMount() {
// 通过store.subscribe()监听组件变化,只要组件的状态变化,就会调用onChange方法
store.subscribe(this.onChange)
}
componentWillUnmount() {
// 组件删除后,注销监听
store.unsubscribe(this.onChange)
}
getOwnState() {
return {
value: store.getState()[this.props.caption]
}
}
onChange() {
const newState = this.getOwnState()
this.setState(newState)
}
onClickIncrementButton() {
store.dispatch(Actions.increment(this.props.caption))
}
onClickDecrementButton() {
store.dispatch(Actions.decrement(this.props.caption))
}
render() {
return (
<Counter caption={this.props.caption}
onClickDecrementButton={this.onClickDecrementButton}
onClickIncrementButton={this.onClickIncrementButton}
value={this.state.value} />
)
}
}
前面我们注意到,我们必须在每个需要监听组件状态的组件中引入store
这样做不仅在组件很多的时候很麻烦,而且在团队开发的时候很不利于复用。
所以在一个应用中,最好只有一个地方直接导入Store,这个地方当然是在最顶层的React组件中。
那使用什么来向下传递我们的Store呢?
用prop显然是不行的,因为prop需要一级一级往下传,假设最底层的组件需要用store,中间的组件都得帮忙传递,这样无疑非常麻烦
React提供了一个叫Context的功能,实现了跨层级的组件数据传递,不需要通过组件树逐层传递prop
首先上级组件宣称自己支持Context,并提供一个函数来返回Context对象
然后这个上级组件的所有子孙组件都可以通过this.context访问到这个共同的环境对象
使用Context需要用到两种组件
现在我们先实现Context的生产者Provider,作为最顶级的父节点
对于Provider,要做两件事
下面是代码示例
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class Provider extends Component {
// 1. 实现getChildContext,返回代表context的对象
getChildContext() {
return {
store: this.props.store
}
}
render() {
// 简单的将子组件渲染出来
return this.props.children
}
}
// 2. 通过childContextTypes静态属性来声明提供给子组件的属性
Provider.childContextTypes = {
store: PropTypes.object
}
export default Provider
其中render函数中返回了this.props.children
每个React组件的props中都可以一个特殊属性children,代表的是子组
件
例如下面的代码,this.props.children
代表的就是ControlPanel
<Provider>
<ControlPanel />
</Provider>
然后我们使用<Provider>
来包裹顶层组件中
ReactDOM.render(
<Provider store={store}>
<ControlPanel />
</Provider>,
document.getElementById('root')
)
写好了Provider, 我们看子孙组件如何使用context对象
class CounterContainer extends Component {
constructor(props, context) {
// 得带上参数context,
super(props, context)
}
// ...
}
// 子组件需要通过一个静态属性contextTypes声明后,才能访问父组件Context对象的属性
CounterContainer.contextTypes = {
store: PropTypes.object
}
在上面两节中,引入了容器组件,傻瓜组件
和Context
组件来优化Redux
有一个这样的库已经帮我们做好了这些工作,也就是react-redux
react-redux提供了两个最主要的api
connect用法
// 完整写法
const CounterContainer = connect(mapStateToProps, mapDispatchToProps)(Counter)
export default CounterContainer;
connect的作用就是根据我们传入的UI组件和业务逻辑,自动生成容器组件
它具体做了什么工作呢?
换句话说这两个工作一个就是傻瓜组件的输入,一个就是傻瓜组件的输出
mapStateToProps是connect函数的第一个参数
看名字就知道意思是建立state和props的映射,也就是把Store上的状态,转化为props传给傻瓜组件
例子
// 第二个是可选参数,这个参数代表着容器组件的props
function mapStateToProps(state, ownProps) {
const { caption } = ownProps
return {
value: state[caption]
}
}
mapDispatchToProps是connect函数的第二个参数
建立dispathch和props的映射,将内层傻瓜组件的用户动作转化为派发给容器组件的动作
// 第二个是可选参数,这个参数代表着容器组件的props
function mapDispatchToProps(dispatch, ownProps) {
const {caption} = ownProps
return {
onIncrement: () => {
dispatch(Actions.increment(caption))
},
onDecrement: () => {
dispatch(Actions.decrement(caption))
}
}
}
connect函数还是有一些复杂的,脑海中要时刻存在容器组件和傻瓜组件的两个概念
connect函数帮我们做的事情就是创建容器组件,然后连接容器组件和傻瓜组件
记住导出的是connect帮我们创造的容器组件,而并不是我们自己写的傻瓜组件
Provider的用法比较简单,也就是提供包含store的context。
之前也实现过一个完整的Provider,只是没那么严谨
用法没有差别,在APP的顶层包裹,将store传入即可
<Provider store={store}>
<ControlPanel />
</Provider>
最后通过react-redux的流程图,再复习一遍流程
Redux自身提供了很强大的数据流管理功能,当Redux不满足我们要求的时候,开发者可以通过扩展增强Redux的功能
一般有两种方式扩展Redux
在派发一些特殊action的时候,中间件可以拦截,中间件先将这个action进行处理后,再交给下一个中间件,依次最后交付给原生的dispatch处理
中间件的特定
来看一个最简单的中间件例子
function doNothingMiddleware({dispatch, getState}) {
return function(next) {
return function(action) {
return next(action)
}
}
}
看起来很复杂吧,其实不然,现在分析一下这个中间件
接收一个对象为参数,对象参数有两个函数dispatch
,getState
这代表Redux上两个同名函数
doNothingMiddleware
返回的函数接收一个next类型的参数
redux-thunk源码非常的简单,只有十行不到
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
代码很容易看懂,redux-thunk执行时,发现action是函数,于是执行这个action,否则就直接交给下一个中间件处理
写一个Logger中间件
// logger.js
const logger = ({dispatch, getState}) => next => action => {
console.group(action.type)
console.log('dispathching:', action)
const result = next(action)
console.log('next state:', getState())
console.groupEnd()
return result
}
export default logger
// store.js
const store = createStore(rootReducer, applyMiddleware(logger))
中间件可以用来增强Redux Store的dispatch方法,但是也仅限于dispatch方法
Store Enhancer
可以对Redux Store进行更深度的定制,比中间件更强大(事实上中间件applyMiddleware
也是作为一个store enhancer
)
怎么用呢?
createStore(reducer, [preloadedstate], [enhancer])
createStore的第三个参数就是我们要传入的Enhancer
看一个什么都不做的Enhancer的例子
const doNothingEnhancer = (createStore) => (reducerpreloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
return store
}
最里层的函数可以拿到createStore
函数,然后最里层的函数接收三个参数,通过这三个参数,创造出一个store对象,然后定制store对象,最后返回store对象就可以了
自己写一个logEnhancer
const logger = createStore => (...args) => {
const store = createStore(...args)
const dispatch = (action) => {
console.group(action.type)
console.log('dispathching:', action)
const result = store.dispatch(action)
console.log('next state:', getState())
console.groupEnd()
return result
}
return {...store, dispatch}
}
export default logger
enhancer一般都是这样的套路
这个例子中,增强了dispatch方法的功能
使用compose可以组合多个Store Enhancer
import { createStore, applyMiddleware, compose } from 'redux'
import rootReducer from './reducers'
import logger from './middleare/logger';
import logEnhancer from './enhancers/logger'
const store = createStore(rootReducer, compose(applyMiddleware(logger), logEnhancer))
export default store
回顾todoList项目中的FooterContainer
(这是一个todoList的过滤器容器)
import { connect } from 'react-redux'
import * as actions from '../actions'
import Footer from '../components/Footer'
const mapStateToProps = state => {
return {
filter: state.filter
}
}
const mapDispatchToProps = dispatch => {
return {
setVisibleTodos: (filter) => dispatch(actions.setFilter(filter))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Footer)
在mapStateToProps
函数中,我们发现FooterContainer
作为视图层的一个容器组件,直接引用了Redux层的数据,这是一种强耦合的关系。
我们可以通过selector来对这两者进行解耦
改写后
// FooterContainer
import { connect } from 'react-redux'
import * as actions from '../actions'
import Footer from '../components/Footer'
import { getFilter } from '../seletors'
const mapStateToProps = state => {
return {
filter: getFilter(state)
}
}
const mapDispatchToProps = dispatch => {
return {
setVisibleTodos: (filter) => dispatch(actions.setFilter(filter))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Footer)
// selectors
export const getFilter = (state) => state.filter
这样我们就通过selector,实现了两者的解耦
我们来看todoList中的getVisibleTodos
方法
export const getVisibleTodos = (state) => {
const {todos, filter} = state
switch (filter) {
case 'all':
return todos
case 'completed':
return todos.filter(todo => todo.completed)
case 'active':
return todos.filter(todo => !todo.completed)
default:
return new Error('Unknown filter:' + filter)
}
}
我们发现,我们的这个selector函数,需要经过一些计算。
每当state发生改变的时候,即使是一个无关的state,getVisibleTodos
函数都会被重新执行一遍
我们发现只要我们的state
的todos
和filter
字段没有发生改变,我们就不需要再重复进行计算
可以选择引用上次计算好的结果,就没必要再重复计算了,reselect就帮我们做了这件事情,帮我们缓存好结果,避免重新的计算
使用方法:
import { createSelector } from 'reselect'
export const getFilter = (state) => state.filter
const getTodos = (state) => state.todos
export const getVisibleTodos = createSelector(
[ getFilter, getTodos ],
(filter, todos) => {
switch (filter) {
case 'all':
return todos
case 'completed':
return todos.filter(todo => todo.completed)
case 'active':
return todos.filter(todo => !todo.completed)
default:
return new Error('Unknown filter:' + filter)
}
}
)
这一章是介绍构建高质量组件的原则和方法
React组件的数据只分为两种:prop
和state
无论prop
或者state
发生改变,都可能引发组件的重新渲染
使用这两者的原则是:
对外使用prop
,对内使用state
在React中, prop是从外部传递给组件的数据。
每个React组件都是独立存在的模块,组件之外都是外部世界,外部世界就是通过prop跟组件进行对话的
class ControlPanel extends Component {
render() {
return (
<div>
<Counter caption="First" initValue={0} />
<Counter caption="Second" initValue={10} /> <Counter caption="Third" initValue={20} />
</div>
);
}
}
class Counter extends Component {
constructor(props) {
// 如果要用this,必须调用super()
super(props)
this.state = {
count: props.initValue || 0
}
this.onClickIncrementButton = this.onClickIncrementButton.bind(this)
this.onClickDecrementButton = this.onClickDecrementButton.bind(this)
}
onClickIncrementButton() {
var count = this.state.count + 1
this.setState({
count
})
}
onClickDecrementButton() {
var count = this.state.count - 1
this.setState({
count
})
}
render() {
const { caption } = this.props
return (
<div>
<button style={style} onClick={this.onClickIncrementButton} >+</button>
<button style={style} onClick={this.onClickDecrementButton} >-</button>
<span>{ caption } count: {this.state.count}</span>
</div>
)
}
}
ControlPanel组件给Counter组件传递了两个prop属性caption
和initValue
propTypes
因为prop是组件的对外接口,以防传进来的东西混杂,应该制定良好的接口规范
通过propTypes即可实现约束我们的prop
Counter.propTypes = {
caption: PropTypes.string.isRequired, // caption必须是字符串并且必须要填
initValue: PropTypes.number // initValue必须是数字,可不填
}
state代表组件的内部状态,是对内的。
React不能修改传入的prop,记录自身数据变化就需要用到state
上面的例子也说明了
要更新state必须使用setState方法,不能直接修改
React的生命周期分为三个阶段
讲解
constructor()也就是ES6中的构造函数。
注意:并不是每个组件都需要定义构造函数,无状态组件就不需要定义构造函数
一个React组件需要定义构造函数的原因一般有几点:
尽量不要去动它
render函数无疑是React组件中最重要的函数,一个React组件可以忽略
其他所有函数都不实现,但是一定要实现render函数,因为所有React组件的
父类React.Component类对除render之外的生命周期函数都有默认实现.
render()函数并不会做实际上的渲染动作,它只是返回JSX描述的结构,最终由React完成渲染。
需要注意,render函数应该是一个纯函数,完全根据this.state和this.props
来决定返回的结果,而且不要产生任何副作用
componentDidMount在render函数之后被调用
组件渲染完成后调用。
shouldComponentUpdate(nextProps, nextState)
render和shouldComponentUpdate函数,也是React生命周期函数中唯二两
个要求有返回结果的函数
render函数的返回结果将用于构造DOM对象,而
shouldComponent-Update函数返回一个布尔值,告诉React库这个组件在这次
更新过程中是否要继续。
值得一提的是,通过this.setState函数引发更新过程,并不是立刻更新组件的state值,在执行到到函数shouldComponentUpdate的时候,this.state依然是this.setState函数执行之前的值,所以我们要做的实际上就是在nextProps、nextState、this.props和this.state中互相比对。
react-router等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面。路由的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。
基本实现方式有两种
改变url的hash值是不会刷新页面的
使用方法:
// 使用前页面http://localhost:3000
window.location.hash='/discover'
// 使用后页面http://localhost:3000/#/3000
url中多了一个#号的hash值,然后我们可以通过监听url中的hash值变化,来显示不同的页面,从而实现前端路由
优点是兼容性比较好,仅 hash 符号之前的内容会被包含在请求中,如 http://www.abc.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。
缺点是不太美观
history是HTML5提供的一个对象
提供了两个方法history.pushState
和history.replaceState
这两者的区别,前者是push进去当前地址状态,后者是修改当前地址状态,也不会引起浏览器的刷新
它像一个栈,可以让我们将浏览器地址历史记录push到栈中
// 初始值:http://localhost:3000
// http://localhost:3000/test
window.history.pushState(null, null, "test");
// http://localhost:3000/test/book
window.history.pushState(null, null, "test/book");
// http://localhost:3000/test/book#/hello
window.history.pushState(null, null, "#/hello");
// http://localhost:3000/test/book?name=
window.history.pushState(null, null, "?name=");
// http://localhost:3000/test/book#/hello
window.history.back()
// http://localhost:3000/test/book?name=
window.history.forward()
然后我们push进去的状态,可以通过前进,后退来控制
优点是比较美观,不带#
缺点是需要后端配合,不然刷新就会发送http请求,导致404错误
react-router
中有三种组件
对于每个react-router项目来说, 核心就是路由器组件.
react-router-dom
提供了两个路由器
两者的区别
// <BrowserRouter>
http://example.com/about
// <HashRouter>
http://example.com/#/about
如果你想兼容老式浏览器,你应该使用。
一般来说使用第一个.
有两种
Route是react-router
中最重要的组件, 负责路由的匹配, 一般和Link
结合使用
Route的用法.
// 如果路径是/about, 则显示组件About
<Route path="/about" component={About}></Route>
有一点需要注意, 如果有两个路由匹配组件
<Route path="/about" component={About}></Route>
<Route path="/about/resume" component={Resume}></Route>
如路径是/about
, 则两个组件均会被渲染.
如果要完全匹配, 需要加上exact
关键字
<Route exact path="/about" component={About}></Route>
Link
的用法
Link
类似于HTML中的a标签, 使用它可以跳转到指定的URL
// 点击即可跳到about界面
<Link to="/about">about</Link>
<Route exact path="/about" component={About}></Route>
Switch有点类似我们js中的switch语法, 它与Route组合使用
<Switch>
<Route exact path="/" component={Main}></Route>
<Route path="/about" component={About}></Route>
<Route path="/topics" component={Topics}></Route>
</Switch>
Switch只渲染一个与之匹配的地址, 找到一个与之匹配的地址后, 就会结束.
当路由路径和当前路径成功匹配,会生成一个对象,我们叫它match.
match对象包含了很多url和path的信息.
第三个属性需要例子来理解=.=
举个栗子
<Route path="/Home/:name" component={HomePage} />
const HomePage = ({ match }) => (
<div>
<h1> parameter => {match.params.name}
</div>
);
在这个例子中match.params.name
就是 从Route中传来的:name
属性
Route组件特别重要,因此来详细了解一下Route组件
Route组件有三个可以来定义渲染内容的props
<Route path='/page' component={Page} />
const extraProps = { color: 'red' }
<Route path='/page' render={(props) => (
<Page {...props} data={extraProps}/>
)}/>
<Route path='/page' children={(props) => (
props.match
? <Page {...props}/>
: <EmptyPage {...props}/>
)}/>
component参数与render参数的组件是用很大的区别的。使用component参数的组件会使用React.createElement来创建元素,使用render参数的组件则会调用render函数。如果我们定义一个内联函数并将其传给component参数,这将会比使用render参数慢很多。
在之前做的Todo应用中其实是用性能问题的
const todoList = props => {
const { todos, onToggleTodo, onRemoveTodo } = props
return (
<div>
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
text={todo.text}
completed={todo.completed}
onToggleTodo={() => onToggleTodo(todo.id)}
onRemoveTodo={() => onRemoveTodo(todo.id)}
/>
))}
</ul>
</div>
)
}
// ...
export default connect(
mapStateToProps,
mapDispatchToProps
)(todoList)
// TodoItem
const TodoItem = (props) => {
const { text, completed, onToggleTodo, onRemoveTodo } = props
const checked = completed ? 'checked' : ''
return (
<li>
<input type="checkbox" onClick={onToggleTodo} checked={checked} readOnly />
<span>{text}</span>
<button onClick={onRemoveTodo}>删除</button>
</li>
)
}
我们发现我们的TodoItem是一个无状态组件
假设我们列表有一千条数据,只有一条数据发生了变化,但是整个TodoList都被重新渲染了一遍
因为TodoItem是一个无状态函
数,所以使用的是React默认的shouldComponentUpdate函数实现,也就是永
远返回true的实现。
所以整个TodoList都被重新渲染了一遍
所以我们可以改写TodoItem
class TodoItem extends Component {
shouldComponentUpdate(nextProps, nextState) {
// completed或者text发生变化时,才重新渲染
return (nextProps.completed !== this.props.completed) ||
(nextProps.text !== this.props.text)
}
render() {
const { text, completed, onToggleTodo, onRemoveTodo } = this.props
const checked = completed ? 'checked' : ''
return (
<li>
<input type="checkbox" onClick={onToggleTodo} checked={checked} readOnly />
<span>{text}</span>
<button onClick={onRemoveTodo}>删除</button>
</li>
)
}
}
这里要介绍到React的原理了。
首先在装载阶段,没有太多优化的过程,所有的React的组件都要经历一遍装载
至于卸载阶段,只有一个生命周期函数componentWillUnmount,这个函
数做的事情只是清理componentDidMount添加的事件处理监听等收尾工作,
做的事情比装载过程要少很多,所以也没有什么可优化的空间
关键在更新阶段
在装载阶段的时候,React通过render()函数在内存中产生了一个树形结构,树上每一个节点就代表React组件或者原生DOM元素,这个树形结构就是所谓的虚拟DOM(Virtual DOM)
用户操作触发了页面更新,React重新生成虚拟DOM,然后比较两个虚拟DOM的不同,来修改真实的DOM树
这个找不同的过程就叫做调和,React采用了巧妙的diffing算法进行实现
如果根节点类型不相同,就不用费心考虑了,就直接销毁旧树,重新建树。
原有的树会经历componentWillUnmount的生命周期,取而代之的组件componentWillMount、render和componentDidMount方法依次被调用
举个例子
// 原有的结构
<div>
<Todos />
</div>
// 更新
<span>
<Todos />
</span>
我们只是将根节点div改成了span,但是这个算法会废掉div节点以及所有子节点,一切推倒重来
很明显,这是一个巨大的浪费,顶层的元素实际上不做什么实质的功能,但是仅仅因为类
型不同就把本可以重用的Todos组件卸载掉,然后重新再把这个组件装载一
遍。
所以作为开发者一定要尽量避免这种情况
如果两个树形结构的根节点类型相同,React就认为原来的根节点只需
要更新过程,不会将其卸载,也不会引发根节点的重新装载
这里考虑一个情况
// 原状态
<ul>
<TodoItem text="First" completed={false}>
<TodoItem text="Second" completed={false}>
</ul>
// 更新
<ul>
<TodoItem text="Zero" completed={false}>
<TodoItem text="First" completed={false}>
<TodoItem text="Second" completed={false}>
</ul>
按道理说只需要渲染 text=Zero
的组件
但React没有那么聪明,它不会仔细判断两个组件是否相同,只是单纯的按位置比较
于是三个组件都被重新渲染了一遍
React就出了一个key的功能,在渲染列表的时候,指定组件的唯一key值(必须稳定不变)
React就会根据key去比较组件,避免重复渲染
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.