🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# Counter计数器组件 通过计数器组件来实现数字增加和减少。 ## 涉及知识点 >绑定事件,this指向解决,生命周期。内容量还是比较多的 ## 新建Counter组件 `src/components/Counter.js` ~~~jsx import React, {Component} from 'react' export default class Counter extends Component { constructor(props) { super(props) this.state = {num: 0} } icrement() { this.setState({ num: this.state.num + 1 }) } render() { return <div> <span id="span">{this.state.num}</span> <br/> <button onClick={this.icrement.bind(this)}>增加</button> </div> } } ~~~ # 问题 1.在这里面要注意数据更新必须是通过**this.setState()**来进行更新数据, 不可以直接对数据进行this.state.num++这种前置++或者后置++来实现效果。因为this.state.num++只会影响数据增加,但不会更新视图。 this.setState()其实和微信小程序的操作方式很相似。 2.绑定事件,绑定事件要注意,这里要通过onClick= {this.increment}这样写会报错,this指向有问题。打印后this是undefined。必须手动修改this指向。通过this.increment.bind(this); 为什么不是call 和applay,而是bind。答案很简单,call和applay调用的话会立即执行。而bind 不会立即执行它是call和applay的衍生版。 下面是bind的简易版实现方式。 ~~~ function bind(fn,context){ return function(){ return fn.apply(context,arguments) } } ~~~ 如果想看复杂版请查看[Bind高级版](https://www.jianshu.com/p/4b293581a03f) # 正式进入生命周期 ## 初始化阶段 ### `constructor`初始化数据 ~~~ console.log('这是constructor') console.log(document.getElementById('box')) this.setState({ num:1 }) ~~~ 打印结果 ``` 这是constructor null ``` 通过打印结果说明网页初始化发布时,默认会先执行constructor生命周期,在这时候还不能获取到真实的dom元素。 ### `componentWillMount`将要被挂载 ~~~ componentWillMount() { console.log('这是componentWillMount',2) console.log(document.getElementById('box')) } ~~~ 打印结果 ``` 这是componentWillMount 2 null ``` 通过打印结果说明网页初始化发布时,第二个执行componentWillMount生命周期,这时候还不能获取到真实的dom元素。 ### render生命周期 进行渲染 因为初始化,它会先将数据和结构进行绑定,然后转换为虚拟dom,此时不需要domdiff算法,直接将数据呈现在网页中即可。 ~~~jsx render() { console.log('这是render',3) console.log(document.getElementById('box')) return <div id="box"> <span id="span">{this.state.num}</span> <br/> <button onClick={this.icrement}>增加</button> </div> } ~~~ 打印结果 ``` 这是render 3 null ``` 通过打印结果说明网页初始化发布时,第三个执行render 生命周期,这时候还不能获取到真实的dom元素。 ### `componentDidMount`挂载完成 ~~~ componentDidMount() { console.log('这是componentDidMount',4) console.log(document.getElementById('box')) } ~~~ ``` 这是componentDidMount 4 <div id=​"box">​…​</div>​ ``` 通过打印结果说明,网页初始化发布是,第四个执行componentDidMount 生命周期,这时候数据和模板已经更新完成,可以获取真正的元素了。 #初始化生命周期小结 通过上面我们已经可以得到什么时候可以去获取真实的dom元素以及它们的执行顺序。 # 运行中生命周期 运行中生命周期分两种情况,可以根据下图来说明,第一种是私有数据发生改变,第二种是属性发生改变。 ![](https://box.kancloud.cn/1c14d8a7a85b626eccf1994765579a5b_1611x909.png) # 我们先研究私有数据即state发生改变的情况。 ## shouldComponentUpdate 组件是否更新生命周期 ~~~ shouldComponentUpdate(nextProps, nextState, nextContext) { console.log('shouldComponentUpdate',5) console.log(document.getElementById('box').innerHTML) } ~~~ 单击增加按钮,我们让state中的num值变为10,控制台会输出下面内容。 ``` shouldComponentUpdate 5 <span id="span">0</span><br><button>增加</button> Warning: Counter.shouldComponentUpdate(): Returned undefined instead of a boolean value. Make sure to return true or false. ``` 控制台输出说明,它默认不会自动触发,当私有数据发生变化时才会更新,同样的也能获取到标签元素,不过注意这个标签元素是老的数据,并不是最新的。 我们也发现浏览器中并未将数据更新为10。控制台还报错了:我们必须在这个生命周期中返回true或者false。返回true表示会更新,如果返回false则不更新。 可以尝试自己写写试试效果 ### 小结 `shouldComponentUpdate`生命周期,一般我们可以用来优化性能,需要人为来处理,一般对于基础数据我们可以来进行性能优化,如果是比较复杂的对象或者数组则不需要,让它在`render`中进行优化。 ## componentWillUpdate ~~~ componentWillUpdate(nextProps, nextState, nextContext) { console.log('componentWillUpdate',6) console.log(document.getElementById('box').innerHTML) } ~~~ 控制台输出 ``` componentWillUpdate 6 <span id="span">0</span><br><button>增加</button> ``` 根据控制台输出结果我们可以看到组件将要被更新,且能获取到元素,且元素的内容还是老旧内容。 ## render生命周期函数 render生命周期函数改造 在此时,这个生命周期函数做的事情较为复杂,内部需要对比老旧的虚拟dom,进行diff算法。 ~~~ console.log('这是render',3) console.log(document.getElementById('box')&&document.getElementById('box').innerHTML) ~~~ 控制台输出 ``` 这是render 3 Counter.js:65 <span id="span">0</span><br><button>增加</button> ``` 根据控制台输出,表明render生命周期还是之前那一个,同样的,它获取的元素内容还是之前老旧的内容。 ## componentDidUpdate生命周期 ~~~ componentDidUpdate(prevProps, prevState, snapshot) { console.log('componentDidUpdate',7) console.log(document.getElementById('box').innerHTML) } ~~~ 控制台输出 ``` componentDidUpdate 7 <span id="span">1</span><br><button>增加</button> ``` 这时我们它是运行中生命周期中最后一个,同时,完成更新后,它获取的元素内容已经成为最新的了。 ## 运行中生命周期小结 运行中生命周期中是组件持续时间最长的。只要触发this.setState这个函数它就会执行。 大家可以猜想一下为什么在初始化render生命周期不可以调用this.setState可以直接告诉大家它是死循环,可以将你的思考总结一下(了解一下)**,这个题需要看完思考题 再来解释。** # 组件销毁生命周期(了解掌握) 组件销毁生命周期这个案例比较复杂,在理解上有点麻烦。大家刚开始时候只需要能跟着写就可以了。在这里我们需要再学习两个ReactDOM的api。 通过ReactDOM的unmountComponentAtNode来卸载组件。不过这里卸载组件只能卸载通过ReactDOM.render加载的组件。 ~~~ ReactDom.unmountComponentAtNode(document.getElementById('box')) 销毁指定容器内的所有React节点。 ~~~ 我们需要结合Counter计数组器组件和Main.js中添加一层组件Life组件。 ## 新建Life.js组件 ~~~ import React, {Component, Fragment} from 'react' import ReactDom from 'react-dom' import Counter from "./Counter"; export default class Life extends Component{ unmont(){ ReactDom.unmountComponentAtNode(document.getElementById('mybox')) } render() { return ( <Fragment> <button onClick={this.unmont.bind(this)}>卸载</button> <div id="mybox"> <Counter/> </div> </Fragment> ) } } ~~~ ### 修改main.js ~~~ ReactDom.render(<Life/>,document.getElementById('root')) ~~~ ![](https://box.kancloud.cn/d8350225367478bda6a33091e05703ec_521x97.png) ##尝试卸载 单击卸载按钮报错 ``` Warning: unmountComponentAtNode(): The node you're attempting to unmount was rendered by React and is not a top-level container. You may have accidentally passed in a React root node instead of its container. ``` 报错信息,说的不通过这种形式卸载组件。 ## 解决问题 我们还记得可以卸载的组件只能通过通过ReactDOM.render()的组件才可以卸载掉。 ~~~ unmont(){ ReactDom.unmountComponentAtNode(document.getElementById('mybox')) } mount(){ ReactDom.render(<Counter/>,document.getElementById('mybox')) } render() { return ( <Fragment> <button onClick={this.mount.bind(this)}>加载</button> <button onClick={this.unmont.bind(this)}>卸载</button> <div id="mybox"> </div> </Fragment> ) } ~~~ 通过上面,我们要通过单击加载按钮才可以让元素显示出来,同样的我们单击卸载,它也可以直接将Counter组件卸载掉。 我们设置Counter.js组件中的卸载生命周期 ~~~ componentWillUnmount() { console.log('componentWillUnmount',8); console.log(document.getElementById('box').innerHTML) } ~~~ 单击卸载按钮,控制台输出 ``` componentWillUnmount 8 Counter.js:111 <span id="span">0</span><br><button>增加</button> ``` 组件卸载生命周期只有一个,但是也比较重要,比如当前页面被卸载掉后我们经常要使用它来清除定时器,以达到节省性能的效果。 # 运行中生命周期第二种情况 运行中生命周期第二种情况就是当父组件给子组件传递数据,引发的改变。 那么LIfe组件中使用了Counter组件,那么在Life组件中我们可以去改变Counter组件的数据。 ## Life组件 ~~~ import React, {Component, Fragment} from 'react' import ReactDom from 'react-dom' import Counter from "./Counter"; export default class Life extends Component{ constructor(props){ super(props); this.state = { number:10 } } unmont(){ ReactDom.unmountComponentAtNode(document.getElementById('mybox')) } mount(){ ReactDom.render(<Counter number={this.state.number}/>,document.getElementById('mybox')) } changeState(){ this.setState({ number:100 },()=>{ this.mount(); }) } render() { return ( <Fragment> <button onClick={this.changeState.bind(this)}>修改number</button> <button onClick={this.mount.bind(this)}>加载</button> <button onClick={this.unmont.bind(this)}>卸载</button> <div id="mybox"> </div> </Fragment> ) } } ~~~ ## Countr组件 ~~~ constructor(props) { super(props) this.state = {num: props.number} console.log('这是constructor,1') console.log(document.getElementById('box')) } componentWillReceiveProps(nextProps, nextContext) { console.log('componentWillReceiveProps',9) console.log(document.getElementById('box').innerHTML) } ~~~ ## 解释 在这里面我们在Life组件中新添加了私有数据,然后级Counter组件的父组件传递number数据,让子组件接收props,然后将props的数据放到Counter组件的state数据的num属性中 ### 单击加载,再单击修改number 控制台输出结果 ``` componentWillReceiveProps 9 <span id="span">10</span><br><button>增加</button> shouldComponentUpdate 5 <span id="span">10</span><br><button>增加</button> componentWillUpdate 6 <span id="span">10</span><br><button>增加</button> 这是render 3 <span id="span">10</span><br><button>增加</button> componentDidUpdate 7 <span id="span">10</span><br><button>增加</button> ``` 我们可以看出只要当父组件的属性的值发生变化 ,那么会引起子组件运行中的所有生命周期会一一执行。不过还可以发现一个问题,我们更新父组件中的number值,子组件并没有更新成功,原因是因为什么呢? ### 解决 componentWillReceiveProps 这个生命周期我们可以接收到父容器的数据,我们来查看一下。 其实我们应该利用最新的属性数据来改变私有数据。让其更新。基本新的私有数据如何获得呢?我们通过控制台发现props并不是最新的数据,而nextProps才是最新的数据 ~~~ componentWillReceiveProps(nextProps, nextContext) { console.log(this.props.number) console.log(nextProps.number) console.log('componentWillReceiveProps',9) console.log(document.getElementById('box').innerHTML) } ~~~ ![](https://box.kancloud.cn/837b6c50ddab4a255d6f4c0318153ab2_1215x337.png) 和最新的props数据来更新state中的数据 ~~~ componentWillReceiveProps(nextProps, nextContext) { console.log(this.props.number) console.log(nextProps.number) this.setState({ num:nextProps.number }) console.log('componentWillReceiveProps',9) console.log(document.getElementById('box').innerHTML) } ~~~ ![](https://box.kancloud.cn/3e97cf36be81b57489ee8ccf91039cc4_1027x314.png) ## 思考题: 但是他们什么时候可以去更新数据呢?也就是this.setState这个函数在生命周期中什么时候可以执行呢?即比如ajax呢? 我们可以在上面的所有生命周期中,都添加`this.setState({num:10})`。 发现如下效果: ### constructor中不能更新数据 ~~~ Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the Counter component ~~~ >警告:无法对尚未挂载的组件调用SetState。这是一个禁止操作,但它可能指示应用程序中的错误。相反,直接分配给“this.state”,或在计数器组件中定义一个具有所需状态的“state=”类属性。 ### render,shouldComponentUpdate,componentWillUpdate,componentDidUpdate不能更新数据错误原因一样 ~~~ Uncaught Invariant Violation: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. ~~~ 超出最大更新深度。当组件在componentWillUpdate或componentDidUpdate中重复调用setState时,可能会发生这种情况。React限制嵌套更新的数量以防止无限循环。 原因是为什么?因为当state发生改变会执行上面的生命周期,自然而然的就导致死循环,最终报错。 ### componentWillMount,componentDidMount,componentWillReceiveProps可以更新数据 这里面可以进行ajax。 ![](https://box.kancloud.cn/a60c788caa3764ca3cf4ef131068743a_898x518.png) 总结:React生命周期在学习上成本上是很大的,在刚工始学习过程中会遇到问题,也会感觉到特别麻烦 ,反而没有Vue的生命周期好用。这些都是正常的,要多练习几次就可以达到熟练的效果。