React Hooks --- useState 和 useEffect

  React Hooks都是函数,当React渲染函数组件(调用函数)时,组件里的每一行代码都会被执行,一个个的Hooks就会被执行。useState()可以接受一个参数,返回一个数组,数组的第一项是值,第二项是更新值的函数。

const App= () => {
  const [message, setMessage]= useState('');
}

  message获取到useState()返回的值,setMessage更新值,导致React重新渲染组件,再次调用useState,message变量获取到更改后的值,组件就有了状态。使用create-react-app 创建项目,修改App.js

const App = () => {
  const [message, setMessage]= useState('');

  function handleChange(e) { setMessage(e.target.value)}
  return ( <input value={message} onChange={handleChange}></input> )
}

  组件首次渲染,第一行调用useState(),返回了['', 更新函数],分别赋值给了message和setMessage。继续向下执行,创建函数handleChange,再继续,返回一个jsx,message赋值给value, value的值为'',组件执行完成,页面上显示一个input 输入框,值为空。此时,输入1,触发了onChange事件,调用setMessage,触发了React 的更新机制。React不会立刻更新组件,而是把setMessage得到的新状态(1)放到更新队列中,React的渲染是异步的。当真正重新渲染时,React又会调用App函数组件,还是从上到下,一行一行执行代码。先调用useState(),此时useState返回的是[新的状态('1'), 更新函数],分别赋值给了message和setMessage,初始值(函数的参数)被忽略了。接着函数handleChange创建,然后jsx,value是message, 是'1'。渲染完成,页面上input中显示1,组件状态改变了。当再输入2时,setMessage再次调用,新的状态(e.target.value('12'))再次存储。App组件再次被调用,还是先执行useSate(),返回最新的状态'12',赋值给message, 然后创建一个handleChange函数,最后jsx中message取12, 组件渲染完成,页面中的input显示12. 整个过程如下

// 初始渲染。
const message = '';  // useState() 的调用
function handleChange(e) { setMessage(e.target.value)}
return (<input value='' onChange={handleChange}></input>)

// 输入1 更新渲染
const message = '1';  // useState() 的调用
function handleChange(e) { setMessage(e.target.value)}
return (<input value='1' onChange={handleChange}></input>)

// 再次输入2,更新渲染
const message = '12';  // useState() 的调用
function handleChange(e) { setMessage(e.target.value)}
return (<input value='12' onChange={handleChange}></input>)

  组件每一次渲染,都会形成它自己独有的一个版本,在每次渲染中,都拥有着属于它本次渲染的状态和事件处理函数,每一次的渲染都是相互隔离,互不影响的,也就是说,在每一次的渲染中,如果jsx和事件处理函数中引用状态变量,它们仅仅能够获取到本次渲染中的状态变量的值,状态变量的值,在渲染开始时,调用useState就已经决定了。状态变量,只不过是每一次调用useState 的返回值。React 负责状态的管理,而我们只是声明变量,使用状态。状态的更新,也不过是组件的重新渲染,React重新调用了组件函数,重新获取useState 返回的值。useState() 返回的永远都是最新的状态值。Each Render Has Its Own Props and State,Each Render Has Its Own Event Handlers。

  useState的参数只在初次渲染时起作用,提供组件的初始状态。在以后的渲染中,不管是调用更新函数导致的组件渲染,还是父组件渲染导致子组件的渲染,参数都不会再用了,直接被忽略了,组件中的state状态变量,获取的都是最新值。useState可以接受函数初始化状态,毕竟只需初次渲染时执行一次。

const Message= (props) => {
const messageState = useState(() => {return localstorage.getItem('name')}); // localStorage中去取数据作为初始状态
/* ... */
}

   更新函数的参数还可以是函数,函数参数是当前状态值。如果想使用当前状态,生成一个新状态,就可以使用函数。

function handleChange(e){
   const val = e.target.value;
   setMessage(prev => prev + val);
}

  当组件的状态是引用类型,比如数组和对象时,更新函数的参数,如果是值,必须接受一个全新的对象和数组,如果是回调函数,回调函数必须返回一个全新的对象和数组,因为React更新状态时,会用Object.js()对新旧状态进行比较,如果它俩相等,就不会重新渲染组件。对象的比较是引用(地址)的比较,改变对象的属性,地址不会变,新旧状态一样,React 不会重新渲染。两个完全不同的对象,地址当然不一样,新旧状态也就不一样。这也是状态的整体替换原则,使用新的状态去替换掉老的状态,而不是setState的合并原则。

const App = () => {
  const [messageObj, setMessage] = useState({ message: '', id: 1 });

  return (
    <div>
      <input value={messageObj.message}
        onChange={e => {
          setMessage(prevState => {
            return { ...prevState, message: e.target.value } // 返回一个全新的对象
          });
        }}
      />
      <p>{messageObj.id} : {messageObj.message}</p>
    </div>
  );
};

  也正因为如此,React 建议把复杂的状态进行拆分,拆成一个一个单一的变量,更新的时候,只更新其中的某个或某些变量。就是使用多个useState(), 生成多个状态变量和更新函数。

const App = () => {
    const [message, setMessage] = useState('');
    const [id, setId] = useState(1);

    return (
        <div>
            <input value={message} onChange={e => { setMessage(e.target.value); }} />
            <p>{id} : {message}</p>
        </div>
    );
};

  当然,复杂状态变量可以拆分,主要是每一个属性所代表的状态之间的关联不大。如果状态关联性特别强,就必须是一个复杂对象,建议使用useReducer.

  useEffect()

  React的世界里,不是只有状态和改变状态,渲染组件,它还要和外界进行交互,比如,发送ajax请求,修改DOM,这些统称为副作用,因为React作用就是把state转化为UI,偏离主要功能的都是副作用。怎么理解副作用呢?可以把副作用想像成,组件渲染完成后,稍带要做的事情,主要任务做完了,稍带做点其他事情。怎么管理副作用?使用useEffect()。useEffect的第一个参数是回调函数,函数里面写副作用。组件渲染完成了,要改变dom,那就把改变dom的实现写到useEffect的回调函数中。

import React, { useEffect, useState } from 'react';

export default function App() {
    const [message, setMessage]= useState('');

    function handleChange(e) { setMessage(e.target.value)}
    useEffect(() => { document.title = `${message}`;})

    return <input value={message} onChange={handleChange}></input>
}

  effect中是怎么获取到message的最新状态值的?和事件处理函数一样,每一次的渲染都有属于这一次渲染的effect函数,在这次渲染中,它能够获取到属于本次渲染状态值。每一次渲染,effect函数都不相同。React记住了effect函数,组件渲染完成后,就会调用它

// 初始渲染。
const message = '';  // useState() 的调用
useEffect(() => { document.title = `${''}`;}) // 初始渲染的effect函数

// 输入1 更新渲染
const message = '1';  // useState() 的调用
useEffect(() => { document.title = `${1}`;}) // 第二次渲染的effect函数

// 再次输入2,更新渲染
const message = '12';  // useState() 的调用
useEffect(() => { document.title = `${12}`;}) // 第三次渲染的effect函数  

  总结一下React的渲染过程,React调用组件函数,state是'',组件返回<input value='' onChange={handleChange}></input>,同时告诉React,渲染完成后,执行() => { document.title = `${''}`;},React更新DOM,浏览器在屏幕上画出输入框,时机已到,React执行effect函数(() => { document.title = `${''}`;})。当在输入框输入1,调用setMessage, 告诉React把状态更新为'1',React重新调用组件,state为'1',组件返回<input value='1' onChange={handleChange}></input>,同时告诉React,渲染完成后,执行() => { document.title = `${'1'}`;},React更新DOM,浏览器在屏幕上画出输入框,时机已到,React执行effect函数(() => { document.title = `${'1'}`;})。

  当useEffect的回调函数中返回一个函数时,这个函数称为清理函数

useEffect(() => {
  function log() {console.log(message);}
  window.addEventListener('resize', log);

  return () => {
    window.removeEventListener('resize', log);
  }
})

  初次渲染,useEffect 就是告诉React,渲染完成后,执行   

() => {
  function log() {console.log('');} // 初次渲染时,message 为空
  window.addEventListener('resize', log);

  return () => {
    window.removeEventListener('resize', log);
  }

  等到组件渲染到屏幕上时,effect函数执行,并注册了一个清理函数。组件再次渲染屏幕时,还是需要执行上面effect函数,不过上一次渲染完成后,注册了清理函数,所以先执行上一次渲染完成后注册的清理函数,再执行这一次渲染的effect函数,并再次注册一个清理函数。清理函数是在再次渲染完成后,需要执行effect函数前执行。需要注意的是,突然关闭页面,清理函数是不会执行的。但是在某些场景下,组件每次渲染后,都执行effect的函数,会带来问题,比如请求数据,组件渲染完成后,只请求一次就可以了。如下代码

import React, { useEffect, useState } from 'react';

export default function App() {
    const [message, setMessage]= useState('');

    useEffect(() => {
        fetch('https://jsonplaceholder.typicode.com/todos/1')
            .then(response => response.json())
            .then(json => {
                setMessage(json.title);
            })
    })
    return <div>{message}</div>
}

  打开控制台,发现不停地调用接口,状态更新会导致组件重新渲染,渲染完成,useEffect又会重新调用。请求数据-> 更新状态->重新请求数据->更新状态,死循环了。这要用到useEffect的第二个参数,一个数组,用来告诉React ,再次渲染完成后,要不要调用useEffect中的函数。把useEffect回调函数中的要用到的外部变量,依次写到数组中,React就知道回调函数的执行是依赖这些变量,那么它就会时时地监听这些变量的变化,只要有更新,它就会重新调用回调函数。这个数组也称为依赖数组,回调函数要再次执行的依赖。函数的依赖发生变化,才会重新调用函数,函数的依赖没有变化,就不用调用函数。现在的回调函数fetch,  没有任何外部变量依赖,那就写一个空数组。空数组表示回调函数不依赖任何变量,没有依赖,永远不会变化,初次渲染完成后执行一次,以后更新就不用管了。

    useEffect(() => {
        fetch('https://jsonplaceholder.typicode.com/todos/1')
        //...
    }, []) // 空数组,回调函数没有依赖作何外部的变量

  如果组件属性或状态发生变化,都要执行useEffect,useEffect的回调函数就有依赖了,比如id变化,获取某个id的todos。

export default function App() {
  const [todoTitle, setTodoTitle]= useState('');
  const [id, setId] = useState(1);

  function handleId(e: any) { setId(e.target.value);}

  useEffect(() => {
      fetch('https://jsonplaceholder.typicode.com/todos/' + id)
          .then(response => response.json())
          .then(json => { setTodoTitle(json.title); })
  }, [id]) // 回调函数依赖了一个外部变量id

  return( 
      <p>id: <input type='number' value={id} onChange={handleId}></input>
          item title: {todoTitle}
      </p>
  )
}

  useEffect的回调函数中的变量、依赖数组中的变量和状态变量都是id,一一对应。只要在effect函数中用到了react数据流中的值,无论是props,state和函数,都要放到effect依赖数组中。其实effect回调函数还依赖setTotoTitle,但React 保证 set* 函数,在整个组件的渲染过程是都是一致的,可以不写。以下情况,可以把依赖去掉,而不会影响effect函数的执行。

useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [count]);

  转化成

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // c => c + 1,函数每次都会获取最新组件的状态给c
    }, 1000);
    return () => clearInterval(id);
  }, []);

   函数要不要定义到组件中,就看函数中有没有用到组件的属性和状态,如果没有,就和React无关,就可以把它放到组件外面,比如下面的函数

function getFetchUrl(query) {
  return 'https://hn.algolia.com/api/v1/search?query=' + query;
}

   useEffect的执行时机是渲染结果画到页面上后才执行,如果effect中操作dom,dom元素先出现到页面上,再修改,可能会造成闪动。如果不想要这种闪动,就要使用useLayoutEffect,useLayoutEffect则是DOM更新完成,还没有渲染到页面上时执行。

   useState的简单理解:每一个组件都是一个fiber,它有一个memoizedState ,指向第一个useState,每一个useState都用链表链接起来

   hook的调用顺序至关重要。

const Title = ({ flag }) => {

  const a = flag ? _useHook('a') : ' '

  const b = _useHook('b')

  return <h1>Hello World+{a}{b}</h1>

}

   那怎么实现上面的条件逻辑?

const Title = ({ flag }) => {

  const _a = _useHook('a')

  const b = _useHook('b')

  const a = flag ? _a : ' '

  return <h1>Hello World+{a}{b}</h1>

}

  useHook(_a)每次都会计算一次,但是可以不用。

 

posted @ 2019-11-28 20:42  SamWeb  阅读(6451)  评论(0编辑  收藏  举报