生命周期&State - 图2

点击跳转来源:

声明周期方法

我们需要在react组件渲染的不同阶段,触发不同的事件逻辑,所以我们需要有生命周期方法.以下列举几个常用的生命周期函数:
componentDidMount(): 组件已经渲染完毕.
componentDidUpdate(): 组件更新后会被立即调用.
componentWillUnmount(): 组件即将销毁.
不常用生命周期函数:
static getDerivedStateFromProps(props, state): render 方法之前调用,并且在初始挂载及后续更新时都会被调用. 文档:
shouldComponentUpdate(): 根据返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响.
getSnapshotBeforeUpdate(prevProps, prevState): 在最近一次渲染输出(提交到 DOM 节点)之前调用.

  1. class HelloWorld extends React.Component {
  2. componentDidMount(){
  3. console.log('组件渲染完毕...');
  4. }
  5. componentWillUnMount(){
  6. console.log('组件即将销毁...');
  7. }
  8. render(){
  9. console.log('render...')
  10. return (
  11. <div className={'danger large'}>
  12. <h2>倒计时</h2>
  13. { /* className和class属性是一样的,而且必须是驼峰 */ }
  14. <span>{ new Date().toString() }</span>
  15. </div>
  16. )
  17. }
  18. }
  19. // 通过调用React自身方法render可以得到当前组件的实例对象,并渲染到页面容器.
  20. ReactDOM.render(<HelloWorld />,document.getElementById('root'));

componentDidMount中声明当前时间:

  1. class HelloWorld extends React.Component {
  2. componentDidMount(){
  3. console.log('this',this);
  4. this.nowTime = new Date().getTime();
  5. }
  6. componentWillUnMount(){
  7. console.log('组件即将销毁...');
  8. }
  9. render(){
  10. console.log('render...')
  11. return (
  12. <div className={'danger large'}>
  13. <h2>倒计时</h2>
  14. { /* className和class属性是一样的,而且必须是驼峰 */ }
  15. <span>{ this.nowTime }</span>
  16. </div>
  17. )
  18. }
  19. }
  20. // 通过调用React自身方法render可以得到当前组件的实例对象,并渲染到页面容器.
  21. ReactDOM.render(<HelloWorld />,document.getElementById('root'));

我们发现,可以打印出实例对象,存在nowTime属性,但是页面不渲染. 为什么?
因为render只在初始化的时候执行了一次,而nowTime是在componentDidMount生命周期函数中才添加的.
也就是说,如果我们想要得到页面渲染的结果:

  1. 在构造函数中优先创建nowTime
  2. componentDidMount 中修改nowTime的值

image.png

构造函数

  1. 在构造函数中优先创建nowTime

我们可以对class添加构造函数 constructor() , 如果对es6 class部分不清楚的同学,需要回顾下相关知识点.
我们发现 constructor()中的 this render() 中的this是一致的,指向 HelloWorld组件的实例对象.
注意: 构造函数中的super() 必须调用 , 文档 .

  1. componentDidMount 中修改nowTime的值

我们尝试在生命周期函数中,修改nowTime, 发现也打印修改后的内容,但页面并没有被渲染. 也就是说直接手动修改实例对象的属性,并不会触发**render() **执行 (这个和vue的双向数据绑定不同,react是没有双向数据绑定的).
那如何才能在数据被修改的时候,重新触发render()呢?

  1. class HelloWorld extends React.Component {
  2. constructor(){
  3. // 如果写了构造函数,那么super必须写
  4. super();
  5. console.log('构造函数...');
  6. this.nowTime = new Date().getTime();
  7. }
  8. componentDidMount(){
  9. console.log('this',this);
  10. setTimeout(()=>{
  11. this.nowTime = '新时间';
  12. console.log(new Date().getTime(),this.nowTime)
  13. },3000)
  14. }
  15. componentWillUnMount(){
  16. console.log('组件即将销毁...');
  17. }
  18. render(){
  19. console.log('render...')
  20. return (
  21. <div className={'danger large'}>
  22. <h2>倒计时</h2>
  23. { /* className和class属性是一样的,而且必须是驼峰 */ }
  24. <span>{ this.nowTime }</span>
  25. </div>
  26. )
  27. }
  28. }
  29. // 通过调用React自身方法render可以得到当前组件的实例对象,并渲染到页面容器.
  30. ReactDOM.render(<HelloWorld />,document.getElementById('root'));

image.png

state和setState()

创建响应式属性

state 是组件的属性 , 组件中的 state 包含了随时可能发生变化的数据。state 由用户自定义,它是一个普通 JavaScript 对象。
我们不能直接修改state本身,可以通过api **setState()** 方法来修改state.
利用setState在componentDidMount 中修改nowTime的值:

  1. class HelloWorld extends React.Component {
  2. constructor(){
  3. // 如果写了构造函数,那么super必须写
  4. super();
  5. console.log('构造函数...');
  6. this.state = {
  7. nowTime: new Date().getTime()
  8. }
  9. }
  10. componentDidMount(){
  11. console.log('this',this);
  12. setTimeout(()=>{
  13. this.setState({
  14. nowTime: '新时间'
  15. })
  16. console.log(new Date().getTime(),this.state.nowTime)
  17. },3000)
  18. }
  19. componentWillUnMount(){
  20. console.log('组件即将销毁...');
  21. }
  22. render(){
  23. console.log('render...')
  24. return (
  25. <div className={'danger large'}>
  26. <h2>倒计时</h2>
  27. { /* className和class属性是一样的,而且必须是驼峰 */ }
  28. <span>{ this.state.nowTime }</span>
  29. </div>
  30. )
  31. }
  32. }
  33. // 通过调用React自身方法render可以得到当前组件的实例对象,并渲染到页面容器.
  34. ReactDOM.render(<HelloWorld />,document.getElementById('root'));

页面得到更新,也就是说,state可以创建 响应式 对象,我们可以通过setState()来实现对数据的修改,同时触发render() 函数更新页面.
image.png

注意1: 不要直接修改 State

  1. // 这样无法触发UI更新
  2. this.state.nowTime = 'xxx';
  3. //而应该使用
  4. this.setState({nowTime='xx'});

注意2: State 的更新可能是异步的

出于性能考虑,React 可能会把多个 [setState()](https://zh-hans.reactjs.org/docs/react-component.html#setstate) 调用合并成一个调用。所以你不要依赖他们的值来更新下一个状态。

  1. // 这种事错误的
  2. this.setState({
  3. counter: this.state.counter + 1,
  4. });
  5. // 可以通过回调方法来获取到上一次更新后的state.
  6. // 回调方法接收两个参数,props下一章节再讨论
  7. this.setState((state,props) => ({
  8. counter: state.counter + 1
  9. }));

回调:

  1. setState(updater, [callback])

setTimeout中调用setState()

  1. componentDidMount(){
  2. console.log('组件挂载完毕...');
  3. // 1. 更新一次count ,检查setState()是否是异步的
  4. // 2. setState()会合并更新,某一次更新如果依赖于上一次的值,可能会有问题
  5. // 3. setState() 如果写到setTimeout/setInterval中,会变成同步函数.
  6. // 尝试修改nowTime
  7. setTimeout(()=>{
  8. this.setState({
  9. count: this.state.count + 1 // 1
  10. })
  11. console.log('this.state.count_1',this.state.count);
  12. this.setState({
  13. count: this.state.count + 1 // 2
  14. })
  15. // 不能直接修改state的值,需要通过setState()
  16. this.setState({
  17. nowTime: new Date().toString()
  18. })
  19. // console.log('this.nowTime',this.state.nowTime);
  20. console.log('this.state.count_2',this.state.count); // 2?
  21. },3000)
  22. }

注意3: State 的更新会被合并

setState是对state进行了浅合并,只会修改相同的属性,保留其他属性.

  1. constructor(props) {
  2. super(props);
  3. this.state = {
  4. posts: [],
  5. comments: []
  6. };
  7. }
  8. componentDidMount() {
  9. fetchPosts().then(response => {
  10. this.setState({
  11. posts: response.posts
  12. });
  13. });
  14. fetchComments().then(response => {
  15. this.setState({
  16. comments: response.comments
  17. });
  18. });
  19. }

完整的声明周期函数

声明: 放到Ref课程之后讲解比较合适.

constructor(): 构造函数.
render(): 渲染/更新.
componentDidMount(): 组件已经渲染完毕.
componentDidUpdate(): 组件更新后会被立即调用.
componentWillUnmount(): 组件即将销毁.
不常用生命周期函数:
static getDerivedStateFromProps(props, state): render 方法之前调用,并且在初始挂载及后续更新时都会被调用. 文档:
shouldComponentUpdate(): 根据返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响.
getSnapshotBeforeUpdate(prevProps, prevState): 在最近一次渲染输出(提交到 DOM 节点)之前调用.

  1. class HelloWorld extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. number: 1,
  6. };
  7. this.ulRef = React.createRef();
  8. console.log('constructor...');
  9. }
  10. static getDerivedStateFromProps(props, state) {
  11. console.log("getDerivedStateFromProps...", this);
  12. return {
  13. age: 10,
  14. };
  15. }
  16. componentDidMount() {
  17. console.log("componentDidMount...");
  18. }
  19. componentDidUpdate(prevProps, prevState, snapshot) {
  20. console.log("componentDidUpdate...", snapshot);
  21. }
  22. shouldComponentUpdate(){
  23. console.log('shouldComponentUpdate...',this.state.number)
  24. // 判断是否渲染UI
  25. // return (this.state.number%2==0)
  26. return true;
  27. }
  28. getSnapshotBeforeUpdate(prevProps, prevState) {
  29. console.log("getSnapshotBeforeUpdate...");
  30. return null;
  31. }
  32. incNumber() {
  33. this.setState((state) => ({
  34. number: ++state.number,
  35. age: ++state.age,
  36. }));
  37. }
  38. render() {
  39. console.log("render...", this);
  40. return (
  41. <div>
  42. <ul ref={this.ulRef} onClick={() => this.incNumber()}>
  43. {Array(this.state.number)
  44. .fill(null)
  45. .map((item, index) => (
  46. <li key={index}>{index}__</li>
  47. ))}
  48. </ul>
  49. <p>age: {this.state.age}</p>
  50. </div>
  51. );
  52. }
  53. }
  54. ReactDOM.render(<HelloWorld />, document.getElementById("root"));

了解:getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate()在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给componentDidUpdate()。
此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。
应返回 snapshot 的值(或null)。
b.gif

  1. <style>
  2. *{
  3. margin: 0;
  4. padding: 0;
  5. }
  6. .ul-chat{
  7. width: 200px;
  8. height: 200px;
  9. border: 1px solid red;
  10. margin: 30px;
  11. overflow: auto;
  12. }
  13. .ul-chat>li{
  14. height: 30px;
  15. font-size: 20px;
  16. line-height: 30px;
  17. text-indent: 2em;
  18. }
  19. </style>
  20. class HelloWorld extends React.Component {
  21. constructor(props){
  22. super(props);
  23. console.log('constructor....');
  24. // 创建ref
  25. this.chatUlRef = React.createRef();
  26. this.state = {
  27. chatItems: [
  28. {
  29. id: 1,
  30. text: '吃了么'
  31. },
  32. {
  33. id: 2,
  34. text: '吃了!'
  35. },
  36. {
  37. id: 3,
  38. text: '吃啥了?'
  39. },
  40. {
  41. id: 4,
  42. text: '吃好吃的!'
  43. }
  44. ]
  45. }
  46. // bind
  47. this.handleKeyUp = this.handleKeyUp.bind(this);
  48. }
  49. componentDidMount(){
  50. }
  51. // 在state被更新后触发,而又没来得及更新到UI的时候
  52. // 这里可以获取到更新之前的UI状态,比如滚轮位置
  53. // prevState 上一次的state
  54. // 此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。
  55. getSnapshotBeforeUpdate(prevProps, prevState){
  56. // 1. 得到之前容器的高度
  57. // 注意: 必须是等内容高度发生变化的时候再处理
  58. if(this.state.chatItems.length > prevState.chatItems.length){
  59. // 这里没来得及更新UI,所以得到的是上一次更新后的DOM
  60. var chatUl = this.chatUlRef.current;
  61. // 之前的容器内容高度
  62. var preContentHeight = chatUl.scrollHeight - chatUl.scrollTop;
  63. return preContentHeight;
  64. }
  65. // 如果聊天列表高度没变化,则返回null
  66. return null;
  67. }
  68. // snapshot 是通过getSnapshotBeforeUpdate声明周期函数返回的
  69. componentDidUpdate(prevProps, prevState, snapshot){
  70. console.log('didUpdate....',this.state);
  71. console.log('snapshot',snapshot);
  72. // 2. 用更新后的最新高度 - 原来的内容高度 得到的就是需要被卷进去的高度
  73. if(snapshot){
  74. var chatUl = this.chatUlRef.current;
  75. // 计算需要被卷入的高度(多余的高度)
  76. var useScrollTop = chatUl.scrollHeight - snapshot;
  77. // 3. 赋值
  78. chatUl.scrollTop = useScrollTop;
  79. }
  80. }
  81. // 如果是输入了enter,需要把最新文本内容 更新到state.chatItems
  82. handleKeyUp(e){
  83. // keyCode 可以获取键盘按键的 Ascii编码值
  84. var keyCode = e.keyCode;
  85. // 如果值== 13 说明是enter键
  86. if(keyCode == 13){
  87. // 获取input的value
  88. // 不能直接修改state的值,必须通过setState才能触发render()!!!
  89. // 所以如果state的某个值是对象类型,需要先克隆
  90. // [...xxx] 数组结构
  91. var chatItems = [...this.state.chatItems];
  92. chatItems.push({
  93. id: new Date().getTime(), // 时间戳肯定不同
  94. text: e.target.value
  95. })
  96. // 用setState更新
  97. this.setState({
  98. chatItems
  99. })
  100. // 更新后还原输入框
  101. e.target.value = '';
  102. }
  103. }
  104. render(){
  105. console.log('render....',this.state);
  106. var liItems = this.state.chatItems.map(item=>(
  107. <li key={item.id}>{item.text}</li>
  108. ))
  109. return (
  110. <div>
  111. <input type="text" onKeyUp={this.handleKeyUp} placeholder="请输入聊天内容" />
  112. <ul className="ul-chat" ref={this.chatUlRef}>
  113. {liItems}
  114. </ul>
  115. </div>
  116. )
  117. }
  118. }
  119. // 使用react 相关api 渲染到容器
  120. ReactDOM.render(<HelloWorld />,document.getElementById('root'));

snapshot.jpg