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)每次都会计算一次,但是可以不用。