My Little World

react 简略学习笔记

react.js是一个UI库,按照react规则,可以组建页面里每一小块的功能,这种局部的功能就是一个组件,
各组件之间可以组合、嵌套,从而拼成整个页面
react就是在组建各个尽可能通用的组件

环境搭建

直接使用官方工具 create-react-app
npm install -g create-react-app 安装命令行
create-react-app hello-react 构建工程
cd hello-react 进入工程
npm start 运行
src/App.js 即是编写组件的文件
一个简单的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from 'react' //写组件必须要引入的依赖
import ReactDOM from 'react-dom' //把组件渲染到页面上去,就是把组件的html结构形成页面
import './index.css'

class Header extends Component {
render () { //组件必须要有的函数,且必须要返回JSX
return ( //返回JSX写的组件内容
<div>
<h1>React 小书</h1>
</div>
)
}
}

//ReactDOM.render 功能就是把组件渲染并且构造 DOM 树,然后插入到页面上某个特定的元素上(在这里是 id 为 root 的 div 元素)。

ReactDOM.render(
<Header />,
document.getElementById('root')
)

在编写 React.js 组件的时候,一般都需要继承 React.js 的 Component(也有别的编写组件的方式–>函数式组件)。一个组件类必须要实现一个 render 方法,这个 render 方法必须要返回一个 JSX 元素。但这里要注意的是,

必须要用一个外层的 JSX 元素把所有内容包裹起来。返回并列多个 JSX 元素是不允许的,

1
2
3
4
5
6
7
8
9
10
11
12
//不对的
return (
<div>第一个</div>
<div>第二个</div>
)
//对的
return (
<div>
<div>第一个</div>
<div>第二个</div>
</div>
)

JSX

可以把它看做是js的一种数据类型,长得很像HTML,但不是HTML,可以像html那样写,但有些地方要注意

可应用{}插入任何js代码,
如果是表达式函数,则返回的结果会相应地渲染到页面上,变量则将变量值对应渲染

{}内如果是条件表达式,即可以实现,根据不同条件返回不同jsx内容,
如果在表达式插入里面返回 null ,那么 React.js 会什么都不显示,相当于忽略了该表达式插入。结合条件返回的话,就做到显示或者隐藏某些元素

可以做变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
render () {
const isGoodWord = true
const goodWord = <strong> is good</strong>
const badWord = <span> is not good</span>
const title = <h1 className='title'> ScriptOJ </h1>
const page = <div className= 'content'> {title} </div> //page里面用title的值,即 <h1 className='title'> ScriptOJ </h1>
return (
<div>
<h1>
React 小书
{isGoodWord ? goodWord : badWord}
</h1>
</div>
)
}

给标签添加class时,不能用class,要用className

1
2
3
4
5
6
7
8
render () {
const className = 'header'
return (
<div className={className}>
<h1>React 小书</h1>
</div>
)
}

for属性不能用,要用htmlfor代替,
其他的 HTML 属性例如 style 、data-* 等就可以像普通的 HTML 属性那样直接添加上去。

组件树

通过继承Component可以自定义形成多个类,即形成多个组件,但这些组件名,
或者说类名要作为标签使用,为与普通小写字母开头的HTML标签做区分,一定要以大写字母开头

自定义组件以标签的形式放入另一个自定义组件的JSX中,即可实现包含关系
多个自定义标签组件并列放入同一个JSX中即实现并列关系
组件可以和组件组合在一起,组件内部可以使用别的组件,就像普通的 HTML 标签一样使用就可以
这样的组合嵌套,最后构成一个所谓的组件树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Title extends Component {
render () {
return (
<h1>hello react</h1>
)
}
}

class Header extends Component {
render () {
return (
<div>
<Title /> //header里面包含Title
<h2>This is Header</h2>
</div>
)
}
}

class Main extends Component {
render () {
return (
<div>
<h2>This is main content</h2>
</div>
)
}
}

class Footer extends Component {
render () {
return (
<div>
<h2>This is footer</h2>
</div>
)
}
}

class Index extends Component {
render () {
return (
<div> //多标签并列放置,组成页面
<Header />
<Main />
<Footer />
</div>
)
}
}

ReactDOM.render(
<Index />,
document.getElementById('root')
)

事件监听

React.js 不需要手动调用浏览器原生的 addEventListener 进行事件监听。
React.js 封装好了一系列的 on 的属性,当需要为某个元素监听某个事件的时候,只需要简单地给它加上 on 就可以了。
而且不需要考虑不同浏览器兼容性的问题,React.js 都封装好了这些细节。

但是要注意,没有经过特殊处理的话,这些 on* 的事件监听只能用在普通的 HTML 的标签上,而不能用在组件标签上

React.js 会给每个事件监听传入一个 event 对象,这个对象提供的功能和浏览器提供的功能一致,而且它是兼容所有浏览器的。

React.js 的事件监听方法需要手动 bind 到当前实例,这种模式在 React.js 中非常常用。不然监听事件调用的方法里面的this是undefined
另外可以在bind 的时候传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Title extends Component {
handleClickOnTitle (e) {
console.log(this)//undefined
console.log(e.target.innerHTML)//hello React
}

render () {
return (
<h1 onClick={this.handleClickOnTitle}>hello React</h1>
)
}
}

class Title extends Component {
handleClickOnTitle (word, e) {
console.log(this, word)//Title 对象,“hello”
console.log(e.target.innerHTML)//hello React
}

render () {
return (
<h1 onClick={this.handleClickOnTitle.bind(this, 'Hello')}>hello React </h1>
)
}
}

setState函数和pros

一个组件的显示形态是可以由它数据状态(state)和配置参数(pros)决定的,即两种方式控制组件显示形态

方式一:利用state的值,通过渲染控制显示状态

一个组件可以拥有自己的状态,React.js 的 state 用来存储这种可变化的状态,再让setState函数根据state的值重新渲染组件到页面
setState 方法由父类 Component 所提供。当调用这个函数的时候,React.js 会更新组件的状态 state ,并且重新调用 render 方法,然后再把 render 方法所渲染的最新的内容显示到页面上。
注意,当要改变组件的状态的时候,不能直接用 this.state = xxx 这种方式来修改,如果这样做 React.js 就没办法知道你修改了组件的状态,它也就没有办法更新页面。所以,一定要使用 React.js 提供的 setState 方法,它接受一个对象或者函数作为参数。

传入一个对象的时候,这个对象表示该组件的新状态。但只需要传入需要更新的部分就可以了,而不需要传入整个对象
要注意的是,当调用 setState 的时候,React.js 并不会马上修改 state。而是把这个对象放到一个更新队列里面,稍后才会从队列当中把新的状态提取出来合并到 state 当中,然后再触发组件更新

1
2
3
4
5
6
7
handleClickOnLikeButton () {
console.log(this.state.isLiked)
this.setState({
isLiked: !this.state.isLiked
})
console.log(this.state.isLiked) //第二次打印的就是第一次打印的值
}

如果需要多次setState,而且后续传入参数依赖前一个setState 参数,这时就要用函数做setState参数,
React.js 会把上一个 setState 的结果传入这个函数,你就可以使用该结果进行运算、操作,然后返回一个对象作为更新 state 的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
handleClickOnLikeButton () {

// this.setState({ count: 0 }) // => this.state.count 还是 undefined
// this.setState({ count: this.state.count + 1}) // => undefined + 1 = NaN
// this.setState({ count: this.state.count + 2}) //最终结果为NAN

this.setState((prevState) => {
return { count: 0 }
})
this.setState((prevState) => {
return { count: prevState.count + 1 } // 上一个 setState 的返回是 count 为 0,当前返回 1
})
this.setState((prevState) => {
return { count: prevState.count + 2 } // 上一个 setState 的返回是 count 为 1,当前返回 3
})
// 最后的结果是 this.state.count 为 3,所以如果JSX中如果显示count,则显示3
}

多次调用setState只会返回多次运行后的结果,React.js 内部会把 JavaScript 事件循环中的消息队列的同一个消息中的 setState 都进行合并以后再重新渲染组件。

方式二:在JSX中使用自定义组件时,传入配置属性,根据属性显示状态

在使用一个组件的时候,可以把参数放在标签的属性当中,所有的属性都会作为 props 对象的键值
组件内部就可以通过 this.props 来访问到这些配置参数
可以把任何类型的数据作为组件的参数,包括字符串、数字、对象、数组、甚至是函数等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Index extends Component {
render () {
return (
<div>
<LikeButton wordings={{likedText: '已赞', unlikedText: '赞'}}
onClick={() => console.log('Click on like button!')/> //这里onclick可以被当做属性访问到
</div>
)
}
}
//LikeButton 类
handleClickOnLikeButton () {

this.setState({
isLiked: !this.state.isLiked
})
if (this.props.onClick) { //这里的onclick是使用<LikeButton />时定义的属性
this.props.onClick() //这里的函数通过属性传递进来,不是组件自己实现
}
}
render()return 的JSX的button onclick事件绑定handleClickOnLikeButton函数

JSX 的{}内可以嵌入任何表达式,{ {} }就是在 {} 内部用对象}字面量返回一个对象而已,
this.props.wordings就是对象{ likedText: ‘已赞’, unlikedText: ‘赞’}

可以用defaultProps配置默认属性,这样就不需要判断配置属性是否传进来了,如果没有传进来,会直接使用 defaultProps 中的默认属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class LikeButton extends Component {
static defaultProps = {
likedText: '取消',
unlikedText: '点赞'
}

constructor () {
super()
this.state = { isLiked: false }
}

handleClickOnLikeButton () {
this.setState({
isLiked: !this.state.isLiked
})
}

render () {
//不需要在这里向下面这样判断是否传进来了配置属性,如果配置了wordings就用wordings,否则用后面{}内的默认值
<!-- const wordings = this.props.wordings || {
likedText: '取消',
unlikedText: '点赞'
} -->
return (
<button onClick={this.handleClickOnLikeButton.bind(this)}>
{this.state.isLiked
? this.props.likedText //直接使用
: this.props.unlikedText} 👍
</button>
)
}
}

一个组件在输入确定的 props 的时候,能够输出确定的 UI 显示形态。如果想修改props决定的显示形态,可以主动通过重新渲染的方式,
把新的props传入到组件当中,这样这个组件中由 props 决定的显示形态也会得到相应的改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Index extends Component {
constructor () {
super()
this.state = {
likedText: '已赞',
unlikedText: '赞'
}
}

handleClickOnChange () {
this.setState({
likedText: '取消',
unlikedText: '点赞'
})
}

render () {
return (
<div>
<LikeButton
likedText={this.state.likedText}
unlikedText={this.state.unlikedText} />
<div>
<button onClick={this.handleClickOnChange.bind(this)}>
修改 wordings
</button>
</div>
</div>
)
}
}

即props 一旦传入,你就不可以在组件内部对它进行修改,但是可以通过父组件主动重新渲染的方式来传入新的 props,从而达到更新的效果

小结
state 是让组件控制自己的状态,props 是让外部对组件自己进行配置。
没有 state 的组件叫无状态组件(stateless component),设置了 state 的叫做有状态组件(stateful component)
因为状态会带来管理的复杂性,所以尽量多地写无状态组件,尽量少地写有状态的组件。这样会降低代码维护的难度,也会在一定程度上增强组件的可复用性。

函数式组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//普通写法
class HelloWorld extends Component {
constructor() {
super()
}

sayHi () {
alert('Hello World')
}

render () {
return (
<div onClick={this.sayHi.bind(this)}>Hello World</div>
)
}
}

//函数式组件的编写方式
const HelloWorld = (props) => {
const sayHi = (event) => alert('Hello World')
return (
<div onClick={sayHi}>Hello World</div>
)
}

以前一个组件是通过继承 Component 来构建,一个子类就是一个组件。而用函数式的组件编写方式是一个函数就是一个组件,但使用方法同普通写法
函数式组件只能接受 props 而无法像跟类组件一样可以在 constructor 里面初始化 state。你可以理解函数式组件就是一种只能接受 props 和提供 render 方法的类组件。

渲染列表数据

如果往 {} 放一个数组,React.js 会帮你把数组里面一个个元素罗列并且渲染出来,如果数组每一项是JSX,就可以将数据渲染到html里面了
所以react渲染列表的思路就是,将数组数据利用es6的map方法转换成JSX,再将JSX数组放到render()返回值中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Index extends Component {
render () {
return (
<div>
{users.map((user) => {
return (
<div>
<div>姓名:{user.username}</div>
<div>年龄:{user.age}</div>
<div>性别:{user.gender}</div>
<hr />
</div>
)
})}
</div>
)
}
}

进一步优化,将数组每项JSX抽离成自定义组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const users = [
{ username: 'Jerry', age: 21, gender: 'male' },
{ username: 'Tomy', age: 22, gender: 'male' },
{ username: 'Lily', age: 19, gender: 'female' },
{ username: 'Lucy', age: 20, gender: 'female' }
]

class User extends Component {
render () {
const { user } = this.props
return (
<div>
<div>姓名:{user.username}</div>
<div>年龄:{user.age}</div>
<div>性别:{user.gender}</div>
<hr />
</div>
)
}
}

class Index extends Component {
render () {
return (
<div>
{users.map((user) => <User user={user} />)}
<!-- {users.map((user, i) => <User key={i} user={user} />)} -->
</div>
)
}
}

ReactDOM.render(
<Index />,
document.getElementById('root')
)

但是要注意
对于用表达式套数组罗列到页面上的元素,都要为每个元素加上 key 属性,这个 key 必须是每个元素唯一的标识
这里用循环计数器 i 作为 key对循环标签User添加key,就不会报需要key的错了
{users.map((user) => )} 变成 {users.map((user, i) => )}

受控组件

类似于 <input />、<select />、<textarea> 这些元素的 value 值被 React.js 所控制、渲染的组件,在 React.js 当中被称为受控组件(Controlled Component)。对于用户可输入的控件,一般都可以让它们成为受控组件.
React.js 认为所有的状态都应该由 React.js 的 state 控制,只要类似于 <input />、<textarea />、<select /> 这样的输入控件被设置了 value 值,那么它们的值等于state的一个属性值,就会永远以被设置的值为准。值不变,value 就不会变化。
在 React.js 当中必须要用 setState 才能更新组件的内容,所以可以通过给输入框绑定监听输入框的 onChange 事件,然后获取到用户输入的内容,再通过 setState 的方式更新 state 中的 value属性绑定的值,这样 输入框的内容才会更新。

子组件可以利用props向父组件传递数据,即,父组件在使用子组件时,添加属性A,并将该A属性值赋值为一个父组件的函数B,
子组件在触发数据提交的时候,检查自己的props有没有属性A,如果有的话,将数据以参数的形式,传递给属性A,即
this.props.A({key1:value1,key2”value2….})
父组件函数B通过函数参数就可以获得传递过来的数据

子组件从父组件获取数据,同样使用props,也就是说,父组件在使用子组件的时候把自己的state的一个属性C赋值给子组件的属性D,
子组件通过属性D获取父组件传递过来的数据,在子组件中直接使用props.D的值,但要注意给子组件设置defaultProps,给属性D一个默认值,防止出现不传值的情况
这种行为叫做“状态提升”
如果将属性D的值,再保存到子组件的state中在使用,那么当其他组件也需要这份数据的时候,将无法使用,因为根本没办法访问,因此采用使用props值的办法,将这种组件之间共享的状态交给组件最近的公共父节点保管,然后通过 props 把状态传递给子组件,这样就可以在组件之间共享数据了

当某个状态被多个组件依赖或者影响的时候,就把该状态提升到这些组件的最近公共父组件中去管理,用 props 传递数据或者函数来管理这种依赖或着影响的行为。
对于不会被多个组件依赖和影响的状态(例如某种下拉菜单的展开和收起状态),一般来说只需要保存在组件内部即可,不需要做提升或者特殊的管理。

生命周期

挂载

React.js 将组件渲染,并且构造 DOM 元素然后塞入页面的过程称为组件的挂载
挂载过程:
-> constructor() //自身的状态的初始化工作
-> componentWillMount()//组件还没挂载完成时进行的组件启动工作,例如 Ajax 数据拉取、定时器的启动。
-> render()
// 然后构造 DOM 元素插入页面
-> componentDidMount() //组件挂载完成以后,也就是 DOM 元素已经插入页面后调用。进行依赖DOM的启动工作
// …
// 即将从页面中删除, setState 只能在已经挂载或者正在挂载的组件上调用,
//组件隐藏的时候,组件的回调函数可能还在不停地尝试 setState,因此会报错
-> componentWillUnmount() //组件对应的 DOM 元素从页面中删除之前调用,处理数据清理工作,如定时器的清理
// 从页面中删除

更新阶段

setState 导致 React.js 重新渲染组件并且把组件的变化应用到 DOM 元素上的过程,这是一个组件的变化过程。而 React.js 也提供了一系列的生命周期函数可以让我们在这个组件更新的过程执行一些操作。
关于更新阶段的组件生命周期:
shouldComponentUpdate(nextProps, nextState):你可以通过这个方法控制组件是否重新渲染。如果返回 false 组件就不会重新渲染。这个生命周期在 React.js 性能优化上非常有用。
componentWillReceiveProps(nextProps):组件从父组件接收到新的 props 之前调用。
componentWillUpdate():组件开始重新渲染之前调用。
componentDidUpdate():组件重新渲染并且把更改变更到真实的 DOM 以后调用。
官方文档

ref属性

通过在html标签或自定义的组件标签中添加 ref属性,可以绑定DOM操作

1
2
3
4
5
6
7
8
9
10
11
class AutoFocusInput extends Component {
componentDidMount () { //利用组件声明周期函数
this.input.focus() //这里的this.input就是页面里的DOM元素,因此可以直接使用DOM API
}

render () {
return (
<input ref={(input) => this.input = input} /> //把input标签DOM元素挂到组件的属性input上
)
}
}

input 元素加了一个 ref 属性,这个属性值是一个函数,
当 input 元素在页面上挂载完成以后,React.js 就会调用这个函数,函数的参数,就是挂载以后的DOM结点
在函数中把这个 DOM 元素设置为组件实例的一个属性,这样就可以通过 this.input 获取到这个 DOM 元素。

但注意,能不用 ref 就不用,因为React.js 本来就可以做到的页面自动更新的操作和事件监听,多余DOM操作,不利于理解和维护

dangerouslySetHTML

dangerouslySetInnerHTML 属性可以用于动态渲染HTML结构,即将HTML字符串,在页面中显示时,当做HTML去渲染
给 dangerouslySetInnerHTML 传入一个对象,这个对象的 __html 属性值就相当于元素的 innerHTML,这样就可以动态渲染元素的 innerHTML 结构了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
constructor() {
super()
this.state = {
content: '<h1>React.js cool!</h1>'
}
}

render () {
return (
<div
className='editor-wrapper'
dangerouslySetInnerHTML={{__html: this.state.content}} />
)
}

因为设置 innerHTML 可能会导致跨站脚本攻击(XSS),所以 React.js 团队认为把事情搞复杂可以防止(警示)大家滥用这个属性。这个属性不必要的情况就不要使用

style

在 React.js 中需要把 CSS 属性变成一个对象再传给元素
style 接受一个对象,这个对象里面是这个元素的 CSS 属性键值对,原来 CSS 属性中带 - 的元素都必须要去掉 - 换成驼峰命名,如 font-size 换成 fontSize,text-align 换成 textAlign。
用对象作为 style 方便动态设置元素的样式。可以用 props 或者 state 中的数据生成样式对象再传给元素,然后用 setState 就可以修改样式,非常灵活

<h1 style={ {fontSize: ‘12px’, color: this.state.color} }>React.js color</h1>

PropTypes 组件参数类型验证

给组件的配置参数加上类型验证,就可以验证传进组件的参数是否符合预定的数据类型,报错也能定位问题
PropTypes就是react第三方库,用于验证参数类型,即验证传入组件的数据的数据类型
npm install –save prop-types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from 'react'
import PropTypes from 'prop-types'

class Comment extends Component {
static propTypes = {
comment: PropTypes.object //指定传入comment的类型必须为Object
}

render () {
const { comment } = this.props
return (
<div className='comment'>
<div className='comment-user'>
<span>{comment.username} </span>:
</div>
<p>{comment.content}</p>
</div>
)
}
}

propTypes 指定了参数类型,但是并没有说这个参数一定要传入,事实上,这些参数默认都是可选的。可选参数可以通过配置 defaultProps,让它在不传入的时候有默认值。
但是这里并没有配置 defaultProps,所以如果直接用<Comment /> 而不传入任何参数的话,comment 就会是 undefined,
可以通过 isRequired 关键字来强制组件某个参数必须传入

1
2
3
static propTypes = {
comment: PropTypes.object.isRequired
}

React.js 提供的 PropTypes 提供了一系列的数据类型可以用来配置组件的参数:
PropTypes.array
PropTypes.bool
PropTypes.func
PropTypes.number
PropTypes.object
PropTypes.string
PropTypes.node
PropTypes.element

官方文档

建议命名和定义顺序

组件的私有方法都用 _ 开头
所有事件监听的方法都用 handle 开头
把事件监听方法传给组件的时候,属性名用 on 开头

组件的内容编写顺序如下:

static 开头的类属性,如 defaultProps、propTypes。
构造函数,constructor。
getter/setter。
组件生命周期。
_ 开头的私有方法。
事件监听方法,handle
render
开头的方法,有时候 render() 方法里面的内容会分开到不同函数里面进行,这些函数都以 render* 开头。
render() 方法。

高阶组件

高阶组件就是一个函数,传给它一个组件,它返回一个新的组件,返回的这个新的组件使用传入的组件作为子组件。

高阶组件内部的包装组件和被包装组件之间通过 props 传递数据。

多层高阶组件使用时,this.props是从外向里传递的,即A组件先后被B,C,D组件组装,则this.props到达A组件的顺序是D,C,B,A

高阶组件的作用是用于代码复用,可以把组件之间可复用的代码、逻辑抽离到高阶组件当中。

高阶组件有助于提高我们代码的灵活性,逻辑的复用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//高阶组件wrapWithLoadData ,
import React, { Component } from 'react'

export default (WrappedComponent, name) => {
class NewComponent extends Component {
constructor () {
super()
this.state = { data: null }
}

componentWillMount () {
let data = localStorage.getItem(name) //如果改变数据获取方式,可以修改这里
this.setState({ data })
}
//在此之前,可以做很多自定义逻辑
render () {
return <WrappedComponent data={this.state.data} />
}
}
return NewComponent
}

//使用
import wrapWithLoadData from './wrapWithLoadData'

class InputWithUserName extends Component {
render () {
return <input value={this.props.data} />
}
}

InputWithUserName = wrapWithLoadData(InputWithUserName, 'username') //组件InputWithUserName以参数传入wrapWithLoadData,进行包装
export default InputWithUserName

context

一个组件可以通过 getChildContext 方法返回一个对象,这个对象就是子树的 context,提供 context 的组件必须提供 childContextTypes 作为 context 的声明和验证。

如果一个组件设置了 context,那么它的子组件都可以直接访问到里面的内容,它就像这个组件为根的子树的全局变量。任意深度的子组件都可以通过 contextTypes 来声明你想要的 context 里面的哪些状态,然后可以通过 this.context 访问到那些状态。

context 打破了组件和组件之间通过 props 传递数据的规范,极大地增强了组件之间的耦合性。而且,就如全局变量一样,context 里面的数据能被随意接触就能被随意修改,每个组件都能够改 context 里面的内容会导致程序的运行不可预料。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//组件树的根组件
class Index extends Component {
static childContextTypes = { //验证getChildContext 返回的对象 必写!!!
themeColor: PropTypes.string
}

constructor () {
super()
this.state = { themeColor: 'red' }
}

getChildContext () { //设置 context,返回的对象就是子组件的this.context
return { themeColor: this.state.themeColor }
}

render () {
return (
<div>
<Header />
<Main />
</div>
)
}
}
//其中一层的子组件
class Title extends Component {
static contextTypes = { //来声明和验证需要获取的contxt内容的类型 必写!!!
themeColor: PropTypes.string
}

render () {
return (
<h1 style={{ color: this.context.themeColor }}>React.js 小书标题</h1> //直接使用this.context里面的值
)
}
}

redux 模式

代码中发现了共享的状态如果可以被任意修改的话,那么程序的行为将非常不可预料,所以提高了修改数据的门槛:必须通过 dispatch 执行某些允许的修改操作,而且必须大张旗鼓的在 action 里面声明。

这种模式挺好用的,就把它抽象出来一个 createStore,它可以产生 store,里面包含 getState 和 dispatch 函数,方便使用。

后来发现每次修改数据都需要手动重新渲染非常麻烦,因此希望自动重新渲染视图。所以后来加入了订阅者模式,可以通过 store.subscribe 订阅数据修改事件,每次数据更新的时候自动重新渲染视图。

接下来发现了原来的“重新渲染视图”有比较严重的性能问题(没有发生改变的数据也进行了渲染),我们引入了“共享结构的对象”来帮我们解决问题,这样就可以在每个渲染函数的开头进行简单的判断避免没有被修改过的数据重新渲染。

我们优化了 stateChanger 为 reducer,定义了 reducer 只能是纯函数,功能就是负责初始 state,和根据 state 和 action 计算具有共享结构的新的 state。

createStore 现在可以直接拿来用了,套路就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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({}) // 初始化 state
return { getState, dispatch, subscribe }
}
// 定一个 reducer
function reducer (state, action) {
/* 初始化 state 和 switch case */
}

// 生成 store
const store = createStore(reducer)

// 监听数据变化重新渲染页面
store.subscribe(() => renderApp(store.getState()))

// 首次渲染页面
renderApp(store.getState())

// 后面可以随意 dispatch 了,页面自动更新
store.dispatch(...)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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({}) // 初始化 state
return { getState, dispatch, subscribe }
}

function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 没有传入,所以加了默认参数 oldAppState = {}
if (newAppState === oldAppState) return // 数据没有变化就不渲染了
console.log('render app...')
renderTitle(newAppState.title, oldAppState.title)
renderContent(newAppState.content, oldAppState.content)
}

function renderTitle (newTitle, oldTitle = {}) {
if (newTitle === oldTitle) return // 数据没有变化就不渲染了
console.log('render title...')
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = newTitle.text
titleDOM.style.color = newTitle.color
}

function renderContent (newContent, oldContent = {}) {
if (newContent === oldContent) return // 数据没有变化就不渲染了
console.log('render content...')
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = newContent.text
contentDOM.style.color = newContent.color
}

function stateChanger (state, action) {
if (!state) {
return {
title: {
text: 'React.js lalalal',
color: 'red',
},
content: {
text: 'React.js content',
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(stateChanger)
let oldState = store.getState() // 缓存旧的 state
store.subscribe(() => {
const newState = store.getState() // 数据可能变化,获取新的 state
renderApp(newState, oldState) // 把新旧的 state 传进去渲染
oldState = newState // 渲染完以后,新的 newState 变成了旧的 oldState,等待下一次数据变化重新渲染
})

renderApp(store.getState()) // 首次渲染页面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js is so cool!》' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改标题颜色

react-redux

使用Context和redux实现根据共享状态进行子组件渲染的工作

index.js

react-redux.js

ThemeSwitch.js

Content.js

Header.js

使用真正的react-redux

import { connect } from ‘react-redux’
import { createStore } from ‘redux’
import { Provider } from ‘react-redux’

根据是否需要高度的复用性,把组件划分为 Dumb 和 Smart 组件,约定俗成地把它们分别放到 components 和 containers 目录下。

Dumb 基本只做一件事情 —— 根据 props 进行渲染。而 Smart 则是负责应用的逻辑、数据,把所有相关的 Dumb(Smart)组件组合起来,通过 props 控制它们。

Smart 组件可以使用 Smart、Dumb 组件;而 Dumb 组件最好只使用 Dumb 组件,否则它的复用性就会丧失。

要根据应用场景不同划分组件,如果一个组件并不需要太强的复用性,直接让它成为 Smart 即可;否则就让它成为 Dumb 组件。

还有一点要注意,Smart 组件并不意味着完全不能复用,Smart 组件的复用性是依赖场景的,在特定的应用场景下是当然是可以复用 Smart 的。而 Dumb 则是可以跨应用场景复用,Smart 和 Dumb 都可以复用,只是程度、场景不一样。

例如将ThemeSwitch拆分成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* src/components/ThemeSwitch.js */
import React, { Component, PropTypes } from 'react'

export default class ThemeSwitch extends Component {
static propTypes = {
themeColor: PropTypes.string,
onSwitchColor: PropTypes.func
}

handleSwitchColor (color) {
if (this.props.onSwitchColor) {
this.props.onSwitchColor(color)
}
}

render () {
return (
<div>
<button
style={{ color: this.props.themeColor }}
onClick={this.handleSwitchColor.bind(this, 'red')}>Red</button>
<button
style={{ color: this.props.themeColor }}
onClick={this.handleSwitchColor.bind(this, 'blue')}>Blue</button>
</div>
)
}
}

/* src/containers/ThemeSwitch.js */

import { connect } from 'react-redux'
import ThemeSwitch from '../components/ThemeSwitch'

const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
}
}
const mapDispatchToProps = (dispatch) => {
return {
onSwitchColor: (color) => {
dispatch({ type: 'CHANGE_COLOR', themeColor: color })
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch)

学习资料链接