一、高阶组件
?阶组件是?个函数(?不是组件),它接受?个组件作为 参数,返回?个新的组件。这个新的组件会使?你传给它的组件作为?组件。
一个简单的示范:
import React, { Component } from 'react';
export default (WrappedComponent)=>{
class OuterComponent extends Component{
render(){
return (
<WrappedComponent />
);
}
}
}
以上就是一个简单的高阶组件,传入组件WrappedComponent,然 后把传进?去的 WrappedComponent 渲染出来。
可以增加一些数据启动工作:
wrapWithLoadData.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
export default wrapWithLoadData=(WrappedComponent,name)=>{
class OuterComponent extends Component{
constructor(){
super();
this.state={
data:null
}
}
UNSAFE_componentWillMount(){
let data=localStorage.getItem(name);
this.setState({data});
}
render(){
return (
<WrappedComponent data={this.state.data}/>
);
}
};
return OuterComponent;
}
另外新建一个文件,然后引入上面的高阶组件:
InputWithUsername.js
import React, { Component } from 'react';
import './index.css';
import wrapWithLoadData from './wrapWithLoadData';
class InputWithUsername extends Component{
render(){
return (
<input value={this.props.data}/>
);
}
}
InputWithUsername=wrapWithLoadData(InputWithUsername,'username');
export default InputWithUsername;
首先将InputWithUsername组件和字符串username传给wrapWithLoadData这个高阶组件,然后高阶组件从localStorage中查到用户名信息,然后再以props的形式将从localStorage获取到的data传给InputWithUsername组件,并返回InputWithUsername组件。所以InputWithUsername在经过了高阶组件后,获取到了data。
我们可以根据自己的需求,设计具有不同包装效果的高阶组件,不需要改动被包装的组件,就可以很好地实现代码的复用。可以说,高阶组件具有充分的灵活性。
二、React.js 的 context
React.js规定,某个组件只要往??的 context ??放了 某些状态,这个组件之下的所有?组件都直接访问这个状态?不需要通过中间组件的 传递。?个组件的 context 只有它的?组件能够访问,它的?组件是不能访问到的。
先创建好组件树:
import React, { Component } from 'react';
import './index.css';
import ReactDOM from 'react-dom';
class Index extends Component {
render () {
return (
<div>
<Header />
<Main />
</div>
)
}
}
class Header extends Component {
render () {
return (
<div>
<h2>This is header</h2>
<Title />
</div>
)
}
}
class Main extends Component {
render () {
return (
<div>
<h2>This is main</h2>
<Content />
</div>
)
}
}
class Title extends Component {
render () {
return (
<h1>React.js 标题</h1>
)
}
}
class Content extends Component {
render () {
return (
<div>
<h2>React.js 内容</h2>
</div>
)
}
}
ReactDOM.render( <Index />, document.getElementById('root') )
然后修改Index组件:
class Index extends Component {
static childContextTypes = {
themeColor: PropTypes.string
}
constructor(){
super();
this.setState={
themeColor:'red'
}
}
getChildContext () {
return {
themeColor: this.state.themeColor
}
}
render () {
return (
<div>
<Header />
<Main />
</div>
)
}
}
子组件Title获取context:
class Title extends Component {
static childContextTypes = {
themeColor: PropTypes.string
}
render () {
return (
<h1 style={{color:this.context.themeColor}}>React.js 标题</h1>
)
}
}
context 打破了组件和组件之间通过 props 传递数据的规范,极?地增强了组件之间的耦合性。因此不到必要的时刻不建议使用。
三、动手实现 Redux
Redux 和 React-redux 并不是同?个东西。Redux 是?种架构模式 (Flux 架构的?种变种),它不关注你到底?什么库,你可以把它应?到 React 和Vue,甚?跟 jQuery 结合都没有问题。? React-redux 就是把 Redux 这种架构模式 和 React.js 结合起来的?个库,就是 Redux 架构在 React.js 中的体现。
创建一个新的项目redux-demo。
修改index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<div id='title'></div>
<div id='content'></div>
</body>
</html>
修改index.js:
const appState = {
title: {
text: 'React.js 教程',
color: 'red',
},
content: {
text: 'React.js 教程内容',
color: 'blue'
}
}
function renderApp (appState) {
renderTitle(appState.title)
renderContent(appState.content)
}
function renderTitle (title) {
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = title.text
titleDOM.style.color = title.color
}
function renderContent (content) {
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = content.text
contentDOM.style.color = content.color
}
renderApp(appState);
运行项目:

数据渲染成功。
这个项目存在着?个重?的隐患,我们渲染数据的时候,使?的是?个共享状态 appState ,每个?都可以修改它。?旦数据可以任意修改,所有对共享状态的操作都 是不可预料的,出现问题的时候 debug 起来就?常困难,因此我们应该避免使用全局变量。
我们可以学习 React.js 团队的做法,把事情搞复杂 ?些,提?数据修改的?槛:模块(组件)之间可以共享数据,也可以改数据。但是这个数据并不能直接改,只能执?允许的修改,?且这种修改必须?张旗?地告诉我。
1.dispatch函数
dispatch负责数据的修改,case限制了可以进行哪几种修改:
function dispatch (action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
appState.title.text = action.text
break
case 'UPDATE_TITLE_COLOR':
appState.title.color = action.color
break
default:
break
}
}
传入的参数action是一个JavaScript对象,type属性说明要修改的类型,其他属性则用来存储数据。
所有对数据的操作必须通过 dispatch 函数。它接受?个参数 action ,这个 action 是?个普通的 JavaScript 对象,??必须包含?个 type 字段来声明你到底想?什 么。 dispatch 在 swtich ??会识别这个 type 字段,能够识别出来的操作才会执 ?对 appState 的修改。
有了这个函数之后,我们只需要看dispatch函数在哪些地方被调用了就可以了,而不必担心我们的共享变量被随意地改来改去,一切尽在掌握之中。
示范:
const appState = {
title: {
text: 'React.js 教程',
color: 'red',
},
content: {
text: 'React.js 教程内容',
color: 'blue'
}
}
function dispatch (action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
appState.title.text = action.text
break
case 'UPDATE_TITLE_COLOR':
appState.title.color = action.color
break
default:
break
}
}
function renderApp (appState) {
renderTitle(appState.title)
renderContent(appState.content)
}
function renderTitle (title) {
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = title.text
titleDOM.style.color = title.color
}
function renderContent (content) {
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = content.text
contentDOM.style.color = content.color
}
renderApp(appState)
dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 高阶教程》' })
dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'black' })
renderApp(appState)

2.抽取state和diapatch
在原来的基础上,我们修改一下代码,使得代码更加具有组织性和复用性:
let appState = {
title: {
text: 'React.js 教程',
color: 'red'
},
content: {
text: 'React.js 教程内容',
color: 'blue'
}
}
function stateChanger (action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
appState.title.text = action.text
break
case 'UPDATE_TITLE_COLOR':
appState.title.color = action.color
break
default:
break
}
}
function createStore(state,stateChanger){
const getState=()=>state
const dispatch=(action)=>stateChanger(state,action)
return{getState,dispatch}
}
function renderApp (appState) {
renderTitle(appState.title)
renderContent(appState.content)
}
function renderTitle (title) {
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = title.text
titleDOM.style.color = title.color
}
function renderContent (content) {
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = content.text
contentDOM.style.color = content.color
}
const store=createStore(appState,stateChanger)
renderApp(store.getState())
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 高阶教程》' })
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'black' })
renderApp(store.getState())
3.监控数据变化
上面的代码有点问题,每次修改了数据之后我们都得手动再次渲染页面,所以下面我们来修改一下代码。
我们在 createStore ??定义了?个数组 listeners ,还有?个新的?法 subscribe ,可以通过 store.subscribe(listener) 的?式给 subscribe 传??个监听 函数,这个函数会被 push 到数组当中。
我们修改了 dispatch ,每次当它被调?的时候,除了会调? stateChanger 进?数据 的修改,还会遍历 listeners 数组??的函数,然后?个个地去调?。相当于我们可 以通过 subscribe 传?数据变化的监听函数,每当 dispatch 的时候,监听函数就会 被调?,这样我们就可以在每当数据变化时候进?重新渲染:
let appState = {
title: {
text: 'React.js 教程',
color: 'red'
},
content: {
text: 'React.js 教程内容',
color: 'blue'
}
}
function stateChanger (action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
appState.title.text = action.text
break
case 'UPDATE_TITLE_COLOR':
appState.title.color = action.color
break
default:
break
}
}
function createStore(state,stateChanger){
const listeners=[]
const subscrib=(listener)=>listeners.push(listener)
const getState=()=>state
const dispatch=(action)=>{
stateChanger(state,action)
listeners.forEach((listener)=>listener())
}
return{getState,dispatch,subscrib}
}
function renderApp (appState) {
renderTitle(appState.title)
renderContent(appState.content)
}
function renderTitle (title) {
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = title.text
titleDOM.style.color = title.color
}
function renderContent (content) {
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = content.text
contentDOM.style.color = content.color
}
const store=createStore(appState,stateChanger)
store.subscrib(()=>renderApp(store.getState()))
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 高阶教程》' })
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'black' })
修改一次数据,自动渲染多个页面也是可以的:
const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState()))
store.subscribe(() => renderApp2(store.getState()))
store.subscribe(() => renderApp3(store.getState()))
4.纯函数
纯函数的两个特征:
- 只依赖形参,不依赖外部变量。
- 不对外部产生影响,比如修改外部变量的值。
5.通过共享结构的对象来优化渲染性能
上面的代码虽然能实现我们需要的功能,但是每次一个组件稍有变动就把所有组件都重新渲染,当组件越来越多的时候,将会出现严重的性能问题,所以我们应该想办法只渲染有改动的组件,对于没有改动的组件不重新渲染。
我们很容易想到的就是通过判断newAppState == oldAppState是否为true的方式来决定是否重新渲染父组件,通过判断newAppState.title == oldAppState.title是否为true的方式来决定是否重新渲染Title组件,通过判断newAppState.content == oldAppState.content是否为true的方式来决定是否重新渲染Content组件。但是,如果newAppState是由oldAppState对象复制得来的,虽然其中的某些信息发生了变化,比如title的text变化了,content的color变化了,但事实上newAppState和oldAppState指向的仍然是同一个对象,newAppState.title和oldAppState.title,newAppState.content和oldAppState.content指向的也都是同一个对象。所以哪怕其中的某些数据被改动了,但上面几个相等判断始终为true,无法作为我们判断是否应该重新渲染组件的依据。
这一切都归功于JavaScript语言的特性,当我们试图复制一个对象的时候,只会复制这个变量,事实上这两个变量指向的仍然是同一个对象。
所以我们需要学习一下ES6的浅复制。
下面的两行代码表示将obj的每个属性都复制一份,生成一个新的对象,然后由obj2指向这个新对象。这样就得到了两个完全一样却并不相等的对象:
const obj = { a: 1, b: 2}
const obj2 = { ...obj }
还可以对原来的对象进行部分数据覆盖,以及扩展:
const obj = { a: 1, b: 2}
const obj2 = { ...obj, b: 3, c: 4}
我们可以把上面的语法应用到我们的项目代码中去:
index.js
function createStore(state,stateChanger){
const listeners=[]
const subscribe=(listener)=>listeners.push(listener)
const getState=()=>state
const dispatch=(action)=>{
state=stateChanger(state,action)
listeners.forEach((listener)=>listener())
}
return{getState,dispatch,subscribe}
}
function renderApp (newAppState,oldAppState={}) {
if(newAppState === oldAppState)return;
renderTitle(newAppState.title,oldAppState.title);
renderContent(newAppState.content,oldAppState.content);
console.log("renderApp...")
}
function renderTitle (newTitle,oldTitle={}) {
if(newTitle===oldTitle)return;
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = newTitle.text
titleDOM.style.color = newTitle.color
console.log("renderTitle...")
}
function renderContent (newContent,oldContent={}) {
if(newContent===oldContent)return;
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = newContent.text
contentDOM.style.color = newContent.color
console.log("renderContent...")
}
function stateChanger(state,action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
return{
...state,
title:{
...state.title,
text:action.text
}
}
case 'UPDATE_TITLE_COLOR':
return{
...state,
title:{
...state.title,
color:action.color
}
}
default:
return state
}
}
let appState = {
title: {
text: 'React.js 教程',
color: 'red'
},
content: {
text: 'React.js 教程内容',
color: 'blue'
}
}
const store=createStore(appState,stateChanger)
let oldState=store.getState()
store.subscribe(()=>{
const newState=store.getState()
renderApp(newState,oldState)
oldState=newState
})
renderApp(store.getState())
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 高阶教程》' })
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'black' })
在两次修改数据后,仅仅是重新渲染了修改的组件:

6.reducer
我们把appState和createStore合并到一起,并把形参stateChanger改为叫reducer:
得到了最终版的createStore方法:
function createStore(reducer){
let state=null
const listeners=[]
const subscribe=(listener)=>listeners.push(listener)
const getState=()=>state
const dispatch=(action)=>{
state=reducer(state,action)
listeners.forEach((listener)=>listener())
}
dispatch({});
return{getState,dispatch,subscribe}
}
再来修改下stateChanger:
function titleReducer(state,action) {
if(!state){
return ({
title: {
text: 'React.js 教程',
color: 'red'
},
content: {
text: 'React.js 教程内容',
color: 'blue'
}
})
}
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
return{
...state,
title:{
...state.title,
text:action.text
}
}
case 'UPDATE_TITLE_COLOR':
return{
...state,
title:{
...state.title,
color:action.color
}
}
default:
return state
}
}
createStore 接受?个叫 reducer 的函数作为参数,这个函数规定是?个纯函数,它 接受两个参数,?个是 state ,?个是 action 。 如果没有传? state 或者 state 是 null ,那么它就会返回?个初始化的数据。如 果有传? state 的话,就会根据 action 来“修改“数据,然后产??个新的对象返回。如果它不能识别你的 action ,它就不会产?新的数据,?是(在 default 内部)把 state 原封不动地返回。
下面用代码来示范下用法:
function renderApp (newAppState,oldAppState={}) {
if(newAppState === oldAppState)return;
renderTitle(newAppState.title,oldAppState.title);
renderContent(newAppState.content,oldAppState.content);
console.log("renderApp...")
}
function renderTitle (newTitle,oldTitle={}) {
if(newTitle===oldTitle)return;
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = newTitle.text
titleDOM.style.color = newTitle.color
console.log("renderTitle...")
}
function renderContent (newContent,oldContent={}) {
if(newContent===oldContent)return;
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = newContent.text
contentDOM.style.color = newContent.color
console.log("renderContent...")
}
function createStore(reducer){
let state=null
const listeners=[]
const subscribe=(listener)=>listeners.push(listener)
const getState=()=>state
const dispatch=(action)=>{
state=reducer(state,action)
listeners.forEach((listener)=>listener())
}
dispatch({});
return{getState,dispatch,subscribe}
}
function titleReducer(state,action) {
if(!state){
return ({
title: {
text: 'React.js 教程',
color: 'red'
},
content: {
text: 'React.js 教程内容',
color: 'blue'
}
})
}
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
return{
...state,
title:{
...state.title,
text:action.text
}
}
case 'UPDATE_TITLE_COLOR':
return{
...state,
title:{
...state.title,
color:action.color
}
}
default:
return state
}
}
const store=createStore(titleReducer)
let oldState=store.getState()
store.subscribe(()=>{
const newState=store.getState()
renderApp(newState,oldState)
oldState=newState
})
renderApp(store.getState())
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 高阶教程》' })
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'black' })

四、动手实现 React-redux(未完待续)
这节开始我们来看看如何把 Redux 和 React.js 结合起来。
在学状态提升的时候,我们发现?个状态可能被多个组件依赖或者影响,? React.js 并没有提供好的解决 ?案,我们只能把状态提升到依赖或者影响这个状态的所有组件的公共?组件上,我 们把这种?为叫做状态提升。但是需求不停变化,共享状态没完没了地提升也不是办 法。
后来我们学到了context,我们可?把共享状态放到?组件的context 上,这个?组件下所有的组件都可以从 context 中直接获取到状态?不需要 ?层层地进?传递了。但是直接从 context ??存放、获取数据增强了组件的耦合性;并且所有组件都可以修改 context ??的状态就像谁都可以修改共享状态?样, 导致程序运?的不可预料。
现在,我们不如把context 和 store 结合起来?毕竟 store 的数据不是谁都能 修改,?是约定只能通过 dispatch 来进?修改,这样的话每个组件既可以去context ??获取 store 从?获取状态,?不?担?它们乱改数据了。
1.明确需求,新建项目theme-color

我们这一章做一个小实例,Header 和 Content 的组件的?本内容会随着主题?的变化?变化,? Content 下 的?组件 ThemeSwitch 有两个按钮,可以切换红?和蓝?两种主题,按钮的颜?也会 随着主题?的变化?变化。
我们新建一个项目theme-color,运行之后在VS code中打开它。在src下新增三个文件:Header.js 、 Content.js 、 ThemeSwitch.js。
修改Header.js:
import react,{Component} from 'react'
import PropTypes from 'prop-types'
class Header extends Component{
render(){
return (
<h1>React.js 教程</h1>
)
}
}
export default Header
修改ThemeSwitch.js:
import react,{Component} from 'react'
import PropTypes from 'prop-types'
class ThemeSwitch extends Component {
render () {
return (
<div>
<button>Red</button>
<button>Blue</button>
</div>
)
}
}
export default ThemeSwitch
修改Content.js:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ThemeSwitch from './ThemeSwitch'
class Content extends Component {
render () {
return (
<div>
<p>React.js 教程内容</p>
<ThemeSwitch />
</div>
)
}
}
export default Content
修改index.js:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import Header from './Header'
import Content from './Content'
import './index.css'
class Index extends Component {
render () {
return (
<div>
<Header />
<Content />
</div>
)
}
}
ReactDOM.render( <Index />, document.getElementById('root') )

2.结合store和context
我们先修改index.js,设置好createStore、reducer,并把store放进context里面。
index.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import Header from './Header'
import Content from './Content'
import './index.css'
class Index extends Component {
static childContextTypes={
store:PropTypes.object
}
getChildContext(){
return {store}
}
render () {
return (
<div>
<Header />
<Content />
</div>
)
}
}
function createStore (reducer) {
let state = null
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action)
listeners.forEach((listener) => listener())
}
dispatch({})
return { getState, dispatch, subscribe }
}
const themeReducer=(state,action)=>{
if(!state)return {
themeColor:'red'
}
switch(action.type){
case 'CHANGE_COLOR':
return {...state,themeColor:action.themeColor}
default:
return state
}
}
const store=createStore(themeReducer)
ReactDOM.render( <Index />, document.getElementById('root') )
然后修改几个子组件,需要添加的代码部分是一模一样的:
Header.js
import react,{Component} from 'react'
import PropTypes from 'prop-types'
class Header extends Component{
static contextTypes={
store:PropTypes.object
}
constructor(){
super()
this.state={themeColor:''}
}
componentWillMount(){
this._updateThemeColor()
}
_updateThemeColor(){
const store=this.context.store
const state=store.getState()
this.setState({
themeColor:state.themeColor
})
}
render(){
return (
<h1 style={{color:this.state.themeColor}}>React.js 教程</h1>
)
}
}
export default Header
Content.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ThemeSwitch from './ThemeSwitch'
class Content extends Component {
static contextTypes={
store:PropTypes.object
}
constructor(){
super()
this.state={themeColor:''}
}
componentWillMount(){
this._updateThemeColor()
}
_updateThemeColor(){
const store=this.context.store
const state=store.getState()
this.setState({
themeColor:state.themeColor
})
}
render () {
return (
<div>
<p style={{color:this.state.themeColor}}>React.js 教程内容</p>
<ThemeSwitch />
</div>
)
}
}
export default Content
ThemeSwitch.js
import react,{Component} from 'react'
import PropTypes from 'prop-types'
class ThemeSwitch extends Component {
static contextTypes={
store:PropTypes.object
}
constructor(){
super()
this.state={themeColor:''}
}
componentWillMount(){
this._updateThemeColor()
}
_updateThemeColor(){
const store=this.context.store
const state=store.getState()
this.setState({
themeColor:state.themeColor
})
}
render () {
return (
<div>
<button style={{color:this.state.themeColor}}>Red</button>
<button style={{color:this.state.themeColor}}>Blue</button>
</div>
)
}
}
export default ThemeSwitch
主题色生效了:

下面来实现一键更换主题色
修改ThemeSwitch.js
import react,{Component} from 'react'
import PropTypes from 'prop-types'
class ThemeSwitch extends Component {
static contextTypes={
store:PropTypes.object
}
constructor(){
super()
this.state={themeColor:''}
}
componentWillMount(){
this._updateThemeColor()
}
_updateThemeColor(){
const store=this.context.store
const state=store.getState()
this.setState({
themeColor:state.themeColor
})
}
handleChangeColor(color){
const store=this.context.store
store.dispatch({
type:'CHANGE_COLOR',
themeColor:color
})
}
render () {
return (
<div>
{}
<button style={{color:this.state.themeColor}}
onClick={this.handleChangeColor.bind(this,'red')}>Red</button>
<button style={{color:this.state.themeColor}}
onClick={this.handleChangeColor.bind(this,'blue')}>Blue</button>
</div>
)
}
}
export default ThemeSwitch
然后将Header、Content、ThemeSwitch组件componentWillMount方法都修改了:
componentWillMount(){
const store=this.context.store
this._updateThemeColor()
store.subscribe(()=>this._updateThemeColor())
}
这样每次切换了主题色之后几个组件都会重新渲染组件:


3.connect和mapStateToProps
上面的代码有两个问题:
- 注意到了吗,每个组件从取出context到渲染页面的过程逻辑和代码几乎都是一样的,所以其实我们写了大量重复无用的代码。这个问题可以通过前面我们学过的高阶组件来解决,只需要把每个组件传进去,再返回包装之后的组件就可以了,
- 另一个问题是,每一个组件对context都依赖过强,必须保证实现了context和store才能复用这个组件,所以其实我们的组件复用性很差。因此我们应该写组件的时候应该尽可能地让组件不依赖外界数据,也不对外界产生副作用(这种组件又称为Dumb组件),与context打交道的工作就交给高阶组件来完成。
我们把这里提到的高阶组件称为connect,因为它把 Dumb 组件和 context 连接 (connect)起来了。总之,我们需要?阶组件帮助我们从 context 取数据,我们 也需要写 Dumb 组件帮助我们提?组件的复?性。所以我们尽量多地写 Dumb 组件,然后??阶组件把它们包装?层,?阶组件和 context 打交道,把??数据取出来通 过 props 传给 Dumb 组件。

在src下面新建一个模块react-redux.js,用来存放connect高阶组件:
react-redux.js 业务逻辑简单来说就是:把接收到的context以props的形式传给dumb组件
import React, {Component} from 'react';
import PropTypes from 'prop-types';
export const connect=(mapStateToProps)=>(WrappedComponent)=>{
class Connect extends Component{
static contextType={
store:PropTypes.object
}
render(){
const store=this.context.store
let stateProps=mapStateToProps(store.getState())
return <WrappedComponent {...stateProps}/>
}
}
return Connect
}
关于mapStateToProps函数,不同dumb组件有不同的定义,因为它们需要获取的props很可能各不相同。
Header.js 业务逻辑简单来说就是,从connect中按照需要(由mapStateToProps规定)获取props,并渲染到组件中去。
import react,{Component} from 'react'
import PropTypes from 'prop-types'
import connect from './react-redux'
class Header extends Component{
static propsTypes={
themeColor:PropTypes.string
}
render(){
return (
<h1 style={{color:this.props.themeColor}}>React.js 教程</h1>
)
}
}
const mapStateToProps=(state)=>{
return {
themeColor:state.themeColor
}
}
Header=connect(mapStateToProps)(Header)
export default Header
Content.js修改同上:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ThemeSwitch from './ThemeSwitch'
import { connect } from './react-redux'
class Content extends Component {
static propTypes={
themeColor:PropTypes.string
}
render () {
return (
<div>
<p style={{color:this.props.themeColor}}>React.js 教程内容</p>
<ThemeSwitch />
</div>
)
}
}
const mapStateToProps=(state)=>{
return {
themeColor:state.themeColor
}
}
Content=connect(mapStateToProps)(Content)
export default Content
修改connect 给 connect 的?阶组件增加监听数据变化重新渲染的逻辑。
import React, {Component} from 'react';
import PropTypes from 'prop-types';
export const connect=(mapStateToProps)=>(WrappedComponent)=>{
class Connect extends Component{
static contextType={
store:PropTypes.object
}
constructor(){
super()
this.state={allProps:{}}
}
componentWillMount(){
const store=this.context.store
this._updateProps()
store.subscribe(()=>this._updateProps)
}
_updateProps(){
const store=this.context.store
let stateProps=mapStateToProps(store.getState(),this.props)
this.setState({
allProps:{
...stateProps,
...this.props
}
})
}
render(){
return <WrappedComponent {...this.state.allProps}/>
}
}
return Connect
}
(这一章未完待续)
五、Smart 组件 vs Dumb 组件
只会接受 props 并且渲染确定结果的组件我们把它叫做 Dumb 组 件,这种组件只关??件事情 —— 根据 props 进?渲染。
还有?种组件,它们?常 聪明(smart),城府很深精通算计,我们叫它们 Smart 组件。它们专?做数据相关 的应?逻辑,和各种数据打交道、和 Ajax 打交道,然后把数据通过 props 传递给Dumb,它们带领着 Dumb 组件完成了复杂的应?程序逻辑。
当我们拿到?个需求开始划分组件的时候,要认真考虑每个被划分成组件的单元到底 会不会被复?。如果这个组件可能会在多处被使?到,那么我们就把它做成 Dumb 组件。
Smart 组件不?考虑太多复?性问题,它们就是?来执?特定应?逻辑的。Smart 组 件可能组合了 Smart 组件和 Dumb 组件;但是 Dumb 组件尽量不要依赖 Smart 组 件。因为 Dumb 组件?的之?是为了复?,?旦它引?了 Smart 组件就相当于带?了 ?堆应?逻辑,导致它?法??,所以尽量不要?这种事情。?旦?个可复?的 Dumb组件之下引?了?个 Smart 组件,就相当于污染了这个 Dumb 组件树。如果?个组件 是 Dumb 的,那么它的?组件们都应该是 Dumb 的才对。
我们把 Smart 和 Dumb 组件分开到两个不同的?录,不再在Dumb 组件内部进? connect ,在 src/ ?录下新建两个?件夹 components/ 和 containers/,所有的 Dumb 组件都放在 components/ ?录下,所有的 Smart 的组件都 放在 containers/ ?录下,这是?种约定俗成的规则。
六、实战:评论功能3.0
|