上次总结的知识已经入门的差不多了,今天继续总结第二阶段的东西,如果发觉有错请及时帮我纠正一下谢谢!
前端应用状态管理 —— 状态提升
React组件中父组件可以通过
<Children state={this.state} />
这种方式传递状态,然后在子组件里面可以通过this.props
拿到这个状态,但是两个子组件如果需要传递状态的话,子组件之间是无法直接访问得到的。
我们可以将这种组件之间共享的状态交给组件最近的公共父节点保管,然后再通过
props
传递给另一个子组件就行了,这样就可以在子组件之间共享数据了,把状态交给公共父组件的行为就叫状态提升,当某个状态被多个组件依赖或者影响的时候,就把该状态提升到这些组件的最近公共父组件中去管理,用 props 传递数据或者函数来管理这种依赖或行为。
在父组件中传递一个回调函数然后子组件调用把东西传过来,然后父组件再传递给其他子组件,我们就可以实现这种效果了。
<!-- index -->
class Index extends Component {
constructor(props) {
super(props)
this.state = {
message: ''
}
}
<!-- 回调函数 -->
onSubString(options) {
<!-- 回调结束设置参数 -->
console.log(options)
this.setState({
message: options
})
}
render() {
return(
<div>
<!-- 向组件一传入一个回调函数 -->
<Template1 onSubString={this.onSubString.bind(this)} />
<!-- 向组件二传入组件一的参数 -->
<Template2 message={this.state.message} />
</div>
)
}
}
<!-- template1 -->
class Template1 extends Component {
static defaultProps = {
onSubString() {
console.log('默认值')
}
}
constructor(props) {
super(props)
this.state = {
message: 'is template1'
}
}
<!-- 组件挂载前调用回调函数传入参数 -->
componentWillMount() {
this.props.onSubString(this.state.message)
}
render() {
return (
<div className="template1">
<h1>template1</h1>
</div>
)
}
}
<!-- template2 -->
class Template2 extends Component {
constructor(options) {
super(options)
}
render() {
return(
<div className="template2">
<!-- 接受组件一的参数 -->
<h1>这里是template2 收到 {this.props.message}</h1>
</div>
)
}
}
<!-- 渲染 -->
ReactDOM.render(
<Index />,
document.getElementById('root')
)
复制代码
上面是打开浏览器展示的效果,状态提升项对于复杂的项目来说很不好维护和管理,对于子组件之间传递参数一般都是利用
Reudx
来操作,也就是把一组数据共享出来所有组件都可以拿到。
React 生命周期
对于各种具有架构能力的框架来说生命周期是少不了的,
React
的生命周期跟其他框架的也差不多,首先得看看React
的运行过程。
ReactDOM.render(
<Index />,
document.getElementById('root')
)
编译为:
ReactDOM.render(
React.createElement(Index, null),
document.getElementById('root')
)
复制代码
执行上面步骤时
React
也会执行几个步骤:
// React.createElement 中实例化一个 Index
const Index = new Index(props, children)
// React.createElement 中调用 index.render 方法渲染组件的内容
const indexJsxObject = index.render()
// ReactDOM 用渲染后的 JavaScript 对象来来构建真正的 DOM 元素
const indexDOM = createDOMFromObject(indexJsxObject)
// ReactDOM 把 DOM 元素塞到页面上
document.getElementById('root').appendChild(indexDOM)
复制代码
我们把 React.js 将组件渲染,并且构造 DOM 元素然后塞入页面的过程称为 组件的挂载
-> constructor() // 初始化
-> componentWillMount() // 挂载前
-> render() // 渲染
// 然后构造 DOM 元素插入页面
-> componentDidMount() // 挂载后
// ...
// 即将从页面中删除
-> componentWillUnmount() // 删除
// 从页面中删除
复制代码
除了上面挂载和删除的生命周期还有更新的生命周期,更新的生命周期。我们知道
setData
和组件的props
会给组件和另一个组件带来重新渲染,从而会触发更新生命周期
1. componentWillUpdate() // 更新前
2. componentDidUpdate() // 更新后
3. componentWillReceiveProps() // 组件从父组件接收到新的 props 之前调用
复制代码
除了上面三个以外还有一个极为关键的
shouldComponentUpdate()
,这个方法可以控制组件是否重新渲染。如果返回false
组件就不会重新渲染。这个生命周期在 React.js 性能优化上非常有用。
class Index extends Component {
constructor(props) {
super(props)
this.state = {
message: '组件更新'
}
}
updateMessage() {
this.setState({
message: this.state.message
})
}
componentWillUpdate() {
console.log('更新前')
}
componentDidUpdate() {
console.log('更新后')
}
render() {
return(
<div className="header">
<button onClick={this.updateMessage.bind(this)}>更新数据</button>
</div>
)
}
}
复制代码
上面的代码我点击三次并且我只是调用了
setState
并没有更新message
的数据,他也会每次都重新渲染,为了节约性能,我们可以使用shouldComponentUpdate()
方法,shouldComponentUpdate()
是重渲染时render()
函数调用前被调用的函数,它接受两个参数:nextProps
和nextState
,分别表示下一个props
和下一个state
的值。并且,当函数返回false
时候,阻止接下来的render()
函数的调用,阻止组件重渲染,而返回true
时,组件照常重渲染。
class Index extends Component {
constructor(props) {
super(props)
this.state = {
message: '组件更新'
}
}
updateMessage() {
this.setState({
message: this.state.message
})
}
<!-- 调用 setState 时控制是否需要重新渲染 -->
shouldComponentUpdate(nextProps, nextState) {
console.log(nextState.message) // 打印的是更新前的 message
console.log(nextProps) // 打印的是更新前的 props (这里没有 props 所以是空的对象)
<!-- 利用上一次的 message 跟更新后的对比 如果相同则返回 false -->
if (nextState.message == this.state.message) {
return false
}
}
componentWillUpdate() {
console.log('更新前')
}
componentDidUpdate() {
console.log('更新后')
}
render() {
return (
<div className="header">
<button onClick={this.updateMessage.bind(this)}>更新数据</button>
</div>
)
}
}
复制代码
我这里点击了4次,但是没有打印其他两个更新的生命周期,只打印了我在
shouldComponentUpdate()
方法里打印的东西,这说明页面并没有重新渲染。
我们再继续看
props
的重新渲染。组件的state
没有变化,并且从父组件接受的props
也没有变化,也有可能重新渲染。
<!-- Son -->
class Son extends Component {
<!-- 点击时候把父级传过来的参数原封不动传回去 -->
updateMessage() {
this.props.handleClick(this.props.message)
}
render() {
<!-- 看看是否重新渲染 -->
console.log(this.props.message, 'son')
return(
<div>
<button onClick={this.updateMessage.bind(this)}>{this.props.message}</button>
</div>
)
}
}
<!-- Index -->
class Index extends Component {
constructor(props) {
super(props)
this.state = {
message: '组件更新',
}
}
updateMessage() {
this.setState({
message: this.state.message
})
}
componentWillUpdate() {
console.log('更新前')
}
componentDidUpdate() {
console.log('更新后')
}
<!-- 把子级的参数重新设置 -->
handleClick(message) {
this.setState({
message: message
})
}
render() {
return (
<div className="header">
<Son handleClick={this.handleClick.bind(this)} message={this.state.message} />
</div>
)
}
}
复制代码
我这里点击三次,不仅父组件会重新渲染,连子组件都会重新渲染。我们再来处理一下这个情况。
<!-- Son -->
shouldComponentUpdate(nextProps, nextState) {
console.log(nextProps.message)
if (nextProps.message === this.props.message) {
return false
}
}
复制代码
如果在子组件加上这个生命周期,监听父级上次传过来的
props
是否和这次穿过来的props
是否一样,然后我们再点击时子组件就不会重新渲染了。
这里进来时渲染打印一次,然后之后就没有打印了,之后点的两次只打印父级的更新生命周期还有子级
shouldComponentUpdate()
里面打印的props
。
React 中的 DOM 操作
一般来说类似
Vue
和React
这种具有MVVM
思想的框架一般能做到像setData
一样的重新渲染,还有父子组件传参来更新状态,这些都不需要通过手动操作DOM
来实现,虽然这些重新渲染机制帮助我们免除大部分的DOM
操作,但是有部分还是不能满足,最常见的就是获取元素的宽度和高度来进行操作,或者是控制输入框的状态等等,这些都是需要依靠DOM
操作来实现的。
React
提供了ref
属性来帮助我们获取 已经挂载 的元素的DOM
节点
class Index extends Component {
componentDidMount() {
console.log(this.indexBox)
}
render() {
return(
<div className="index" ref={(indexBox) => this.indexBox = indexBox} style={{width: '500px', height: '500px', backgroundColor: 'green'}}></div>
)
}
}
复制代码
获取到元素之后你可以调用他原有的
api
,比如输入框的禁止输入以及自动聚焦等等操作。
能不用 ref 就不用。特别是要避免用 ref 来做 React.js 本来就可以帮助你做到的页面自动更新的操作和事件监听。多余的 DOM 操作其实是代码里面的“噪音”,不利于我们理解和维护。
除此之外我们还可以给组件加上
ref
class Header extends Component {
render(){
return(
<div>
<h1>组件组件组件</h1>
</div>
)
}
}
class Index extends Component {
componentDidMount() {
console.log(this.header)
}
render() {
return(
<div>
<Header ref={(header)=> this.header = header } />
</div>
)
}
}
复制代码
这种用法并不常用,也不推荐这样做。
props.children 和容器类组件
组件本身是一个不带任何内容的方形的容器,我可以在用这个组件的时候给它传入任意内容。
class Index extends Component {
render() {
console.log(this.props.children)
return(
<div className="index">
{this.props.children}
</div>
)
}
}
ReactDOM.render(
<Index>
<h1>我洗渣渣辉</h1>
<h2>我洗古天乐</h2>
</Index>,
document.getElementById('root')
)
复制代码
这个属性打印出来的是一个数组,把我们嵌套的
jsx
元素一个个都放到数组当中,然后通过props.children
传给Index
,我们可以把它们分别存在不同的地方。
class Index extends Component {
render() {
console.log(this.props.children)
return(
<div className="index">
<div className="header">
<!-- 第0条 -->
{this.props.children[0]}
</div>
<div className="main">
<!-- 第1条 -->
{this.props.children[1]}
</div>
</div>
)
}
}
复制代码
使用自定义组件的时候,可以在其中嵌套
JSX
结构。嵌套的结构在组件内部都可以通过props.children
获取到,这种组件编写方式在编写容器类型的组件当中非常有用。
dangerouslySetHTML 和 style 属性
出于安全考虑的原因(XSS 攻击),在
React.js
当中所有的表达式插入的内容都会被自动转义,就相当于jQuery
里面的text()
函数一样,任何的HTML
格式都会被转义掉。
class Index extends Component {
constructor(props) {
super(props)
this.state = {
content: '<h1>富文本转义</h1>'
}
}
render() {
return(
<div className="index">
{this.state.content}
</div>
)
}
}
复制代码
如果要把
<h1>
转为标签,可以使用dangerouslySetHTML
属性动态设置元素的innerHTML
。
class Index extends Component {
constructor(props) {
super(props)
this.state = {
content: '<h1>富文本转义</h1>'
}
}
render() {
console.log(this.props.children)
return(
<div className="index" dangerouslySetInnerHTML={{__html: this.state.content}}></div>
)
}
}
复制代码
在添加了
dangerouslySetHTML
属性的元素里面不能再嵌套任何东西( 包括文本 ),否则将会报错。设置 innerHTML 可能会导致跨站脚本攻击(XSS),React.js 团队认为把事情搞复杂可以防止(警示)大家滥用这个属性。这个属性不必要的情况就不要使用。
在
React
的style
里面的css属性需要转化为对象才能传给元素,并且所有元素需要带-
的都要转为驼峰命名,比如font-size
、background-color
,需要写为fontSize
、backgroundColor
才有效果。
<div className="index">
<h1 style={{color: 'red', fontSize: 14}}>文本文本</h1>
</div>
复制代码
PropTypes 和组件参数验证
React.js 的组件其实是为了构建大型应用程序而生。但是因为 JavaScript 这样的特性,你在编写了一个组件以后,根本不知道别人会怎么使用你的组件,往里传什么乱七八糟的参数,我们可以利用
propTypes
这个库来指定传递过来参数的类型,而这个库在后面的context
也要用到。
class Template1 extends Component {
render() {
return(
<h1>{ this.props.userData.name }</h1>
)
}
}
class Index extends Component {
constructor(props) {
super(props)
this.state = {
userData: {
name: 'jack',
age: 18
}
}
}
render() {
return(
<div className="index">
<Template1 userData={ this.state.userData } />
</div>
)
}
}
复制代码
上面的组件无论父级传什么东西都是没有限制了,这让其他人共同开发项目很复杂,有时会因为参数问题到处找
Bug
,这时你可以给组件配置参数加上 类型验证 。
import PropTypes from 'prop-types'
class Template1 extends Component {
static propTypes = {
userData: PropTypes.object // 接受对象参数
}
render() {
return(
<h1>{ this.props.userData.name }</h1>
)
}
}
class Index extends Component {
constructor(props) {
super(props)
this.state = {
userData: {
name: 'jack',
age: 18
}
}
}
render() {
return(
<div className="index">
<!-- 这里传递的是数值 -->
<Template1 userData={ 123 } />
</div>
)
}
}
复制代码
现在可以验证参数的数据类型了,如果传的类型与设置的类型不一致,将会报出警告。
propTypes
提供了多种 数据验证 ,还可以限制单个元素传递,并且当参数没有传递时还可以设置默认值,如果在不传递参数下还不设置默认值,则默认为undefined
,这时会出现一个很不友好的报错信息。
这时我们可以使用
isRequired
来强调这个参数必须传入。
static propTypes = {
userData: PropTypes.object.isRequired
}
复制代码
这时报错信息里面会再添加一条提示,这样的话就更容易找到问题所在。