当前位置: 首页 > Web前端 > HTML5

从零开始实现一个React(四):异步setState

时间:2023-04-06 00:02:52 HTML5

前言上一篇我们实现了diff算法,性能有了很大的提升。但是文末也指出了一个问题:按照目前的实现,每次调用setState都会触发更新,如果在组件中执行这样一段代码:for(leti=0;i<100;i++){this.setState({num:this.state.num+1});}然后执行这段代码会导致这个组件重新渲染100次,对性能的负担非常重。真正的React是如何工作的?React显然也遇到了这样的问题,所以它对setState做了一些特殊的优化:React会把多个setState调用组合成一个执行,也就是说调用setState时,state和它不会立即更新,例如:classApp扩展组件{constructor(){super();this.state={num:0}}componentDidMount(){for(leti=0;i<100;i++){this.setState({num:this.state.num+1});console.log(this.state.num);//会输出什么?}}render(){return(

{this.state.num}

);}}我们定义了一个App组件,组件挂载后会循环100次,每次this.state.num加1,我们用reactReact渲染这个组件,看结果:组件渲染结果为1,在控制台输出100次0,说明每循环一次,得到的状态还是更新前的状态。这是React的优化方式,但显然也会造成一些不直观的问题(比如上面的例子),所以针对这种情况,React给出了一个解决方案:setState接收的参数也可以是一个函数,其中前一个状态可以是taken和下一个状态可以通过这个函数的返回值得到。我们可以这样修复App组件:.nu??m+1}});}}这种用法是不是很像数组的reduce方法?下面我们来看看App组件的渲染结果:现在终于可以得到我们想要的结果了。因此,本文的目标也很明确。我们需要实现以下两个功能:异步更新状态,短时间内将多个setState合并为一个。为了解决异步更新带来的问题,增加setState的另一种形式:接受一个函数作为参数,在这个函数中可以获取上一个状态,返回下一个状态合并setState回顾第二篇中setState的实现:setState(stateChange){Object.assign(this.state,stateChange);renderComponent(this);}在这个实现中,每次调用setState都会更新状态并立即渲染它。setStatequeue为了合并setState,我们需要一个队列来保存每一个setState的数据,然后在一段时间后清空队列,渲染组件。队列是一种数据结构,其特点是“先进先出”,可以通过js数组的push和shift方法来模拟constqueue=[];functionenqueueSetState(stateChange,component){queue.push({stateChange,component});}然后修改组件的setState方法setState(stateChange){enqueueSetState(stateChange,this);}现在有了队列,如何清空队列渲染组件呢?清空队列我们定义了一个flush方法,它的作用是清空队列functionflush(){letitem;//遍历while(item=setStateQueue.shift()){const{stateChange,component}=item;//如果没有prevState,则使用当前状态作为初始prevStateif(!component.prevState){component.prevState=Object.assign({},component.state);}//如果stateChange是一个方法,就是setStateForm的第二个方法}else{//如果stateChange是一个对象,则直接合并到setState中Object.assign(component.state,stateChange);}component.prevState=组件.state;}}这只是实现状态的更新,我们还没有渲染组件渲染组件不能在遍历队列的时候完成,因为同一个组件可能会被多次加入队列,我们??需要另一个队列来保存所有组件,不同的是这个队列中不会有重复的组件。当我们处于enqueueSetStateconstqueue=[]时,我们可以这样做;const渲染队列=[];functionenqueueSetState(stateChange,component){queue.push({stateChange,component});//如果renderQueue中没有当前组件,则将其添加到队列中}}在flush方法中,我们还需要遍历renderQueue来渲染各个组件functionflush(){letitem,component;while(item=queue.shift()){//...}//渲染每个组件while(component=renderQueue.shift()){renderComponent(component);}}延迟执行还有一个更重要的事情:什么时候执行flush方法。我们需要将一段时间内所有的setStates合并,也就是一段时间后执行flush方法清空队列。关键是如何决定这个“时间段”。更好的做法是利用js的事件队列机制。我们先看这段代码:setTimeout(()=>{console.log(2);},0);Promise.resolve().then(()=>console.log(1));console.日志(3);你可以打开浏览器的调试工具运行。他们打印出来的结果是:312,具体原理可以参考阮一峰这篇文章,这里不再赘述。我们可以使用事件队列让flush执行functionenqueueSetState(stateChange,component){//如果队列长度为0,则第一次加入if(queue.length===0){defer(flush);}queue.push({stateChange,component});if(!renderQueue.some(item=>item===component)){renderQueue.push(component);}}定义defer方法,使用刚才标题中出现的Promise.resolvefunctiondefer(fn){returnPromise.resolve().then(fn);},这样在一个“事件循环”中,最多一个冲洗将被执行。在这个“事件循环”中,所有的setState都会被合并,组件只会被渲染一次。除了使用Promise.resolve().then(fn),我们还可以使用上面提到的setTimeout(fn,0),setTimeout的时间也可以是其他值,比如16毫秒。16毫秒的间隔,一秒大概可以执行60次,也就是60帧。人眼每秒只能捕捉60帧。也可以使用requestAnimationFrame或requestIdleCallbackfunctiondefer(fn){returnrequestAnimationFrame(fn);}try效果,尝试渲染上面用React渲染的两个例子:classAppextendsComponent{constructor(){super();this.state={num:0}}componentDidMount(){for(leti=0;i<100;i++){this.setState({num:this.state.num+1});console.log(this.state.num);}}render(){return(

{this.state.num}

);}}效果和React完全一样。另外,以第二种方式调用setState:componentDidMount(){for(leti=0;i<100;i++){this.setState(prevState=>{console.log(prevState.num);return{num:prevState.数字+1}});}}结果完全一样:在本文后面,我们实现了另一个非常重要的优化:短时间内合并多个setState,并异步更新至此,我们已经实现了React的大部分核心功能和优化方法,所以本文也是本系列的最后一篇。本文所有代码都在这里:https://github.com/hujiulong/...从零开始实现React系列React是最流行的前端框架之一,讲解其源码的文章很多,但我想换个角度来解读React:从头实现一个React,从API层面实现React的大部分功能,探究为什么会有虚拟DOM,diff,setState为什么要这样设计。整个系列大概会有四篇文章。我每周会更新一两篇文章。我会尽快在github上更新它们。如果大家有什么问题想讨论,欢迎在github上回复我~博客地址:https://github。com/hujiulong/...关注star,订阅收看上一篇从零开始实现一个React(三):diff算法