Open hujiulong opened 6 years ago
终于等到这篇~ 板凳一下 setState 之所以是异步 最大原因是为了性能提升吗~
@Sunshine168 是的,不能每次setState都触发更新,所以是异步的。
顺便一提,上面的那个例子react和preact跑的结果不一样,preact在每次setState都会立即更新state(但不会立即渲染),而react的行为就和这篇文章的实现一样
剛好看到這系列文章,寫得很不錯,持續關注!
順便分享一下 React Issue 的這篇,在探討為什麼 setState 是異步的:https://github.com/facebook/react/issues/11527
@aszx87410 感谢分享,对于为什么state更新要是异步的我首先也没想太清楚,因为仅从效率考虑,完全可以更新state是同步,渲染是异步的,preact就是这样做的。看完以后感觉清楚了很多。
请教一下,react路由可以讲讲吗?
@hujiulong 程墨 有个专栏讲解了state更新为什么要是异步的,可以去看看,讲的非常清楚。
这里面关键还是用了Promise进行异步呢,跟react里面的实现还是不一样吧,有尝试过像react那样用变量锁(isBatchingUpdates)进行异步更新吗
@p2227 没有说和React的实现一样呀,只是从使用的角度去模拟React,细节上会有很大的区别。
如果setState一直被调用(理想情况),那么是不是flush就没有机会被执行?我看code的感觉是 这里的实现中setState是同步的。
666
相同组件的一直setState没有被处理。我看preact里的操作是将未更新的state,全部放到_nextState中,然后清空队列的时候,一口气更新
相同组件的一直setState没有被处理。我看preact里的操作是将未更新的state,全部放到_nextState中,然后清空队列的时候,一口气更新
我看错了,你用两个不同的队列问题,一个负责更新state,一个负责渲染组件
花了两天时间通过模仿代码的方式研读完了四篇文章,收获很大,react的基本原理有了一个大概的认识,谢谢博主!
赞一个
谢谢博主。
谢谢博主!赞
@hujiulong 期待再加一篇 Fiber 的实现
期待博主总结 Fiber!
@hujiulong 非常感谢博主的文章,写的非常好,
但是我有一个疑问, renderQueue 这个变量仅仅是用来保存 当前组件component 的,在自定义组件中调用setState()的时候,始终传入的都是当前组件吧,
setState(stateChange, callback) { enqueueSetState(stateChange, this); //this始终都是 component }
并且这个数组的长度始终都是1吧,我看代码中,每次都是进行了过滤,
if (!renderQueue.some(item => item === component)) { renderQueue.push(component); }
要想要在flush中使用的话,在enqueueSetState调用flush的时候,直接传入flush不就可以了,为啥非要使用一个数组来保存呢?
export function enqueueSetState(stateChange, component) { if (setStateQueue.length === 0) { defer(flush(component)); } ... } function flush(component) { return function() { ... renderComponent(component); } }
希望博主能够帮我解答下,非常感谢
@Qingchundejiaobu 并不是每个组件都会有一个renderQueue,只有一个全局的renderQueue
@Qingchundejiaobu 并不是每个组件都会有一个renderQueue,只有一个全局的renderQueue
哦,明白了,我理解错了;非常感谢; 不知道博主会不会写react源码系列
您好,有个小小的疑问,您在自定义的setState中enqueueSetState之后写了一次renderComponent,但是在后来的flush中又写了一次renderComponent,并且在enqueueSetState中defer去执行flush,那是不是进行了重复两次的renderComponent了呢?是不是可以去掉setState中的renderComponent?也就是说setState=(stateChange)=>{enqueueSetState(stateChange,this)}; @hujiulong
给大佬点赞,很早之前看过一次,这次再次重温还是收获良多啊!
您好,您的邮件我已收到。
前言
在上一篇文章中,我们实现了diff算法,性能有非常大的改进。但是文章末尾也指出了一个问题:按照目前的实现,每次调用setState都会触发更新,如果组件内执行这样一段代码:
那么执行这段代码会导致这个组件被重新渲染100次,这对性能是一个非常大的负担。
真正的React是怎么做的
React显然也遇到了这样的问题,所以针对setState做了一些特别的优化:React会将多个setState的调用合并成一个来执行,这意味着当调用setState时,state并不会立即更新,举个栗子:
我们定义了一个
App
组件,在组件挂载后,会循环100次,每次让this.state.num
增加1,我们用真正的React来渲染这个组件,看看结果:组件渲染的结果是1,并且在控制台中输出了100次0,说明每个循环中,拿到的state仍然是更新之前的。
这是React的优化手段,但是显然它也会在导致一些不符合直觉的问题(就如上面这个例子),所以针对这种情况,React给出了一种解决方案:setState接收的参数还可以是一个函数,在这个函数中可以拿先前的状态,并通过这个函数的返回值得到下一个状态。
我们可以通过这种方式来修正App组件:
现在来看看App组件的渲染结果: 现在终于能得到我们想要的结果了。
所以,这篇文章的目标也明确了,我们要实现以下两个功能:
合并setState
回顾一下第二篇文章中对setState的实现:
这种实现,每次调用setState都会更新state并马上渲染一次。
setState队列
为了合并setState,我们需要一个队列来保存每次setState的数据,然后在一段时间后,清空这个队列并渲染组件。
清空队列
我们定义一个flush方法,它的作用就是清空队列
这只是实现了state的更新,我们还没有渲染组件。渲染组件不能在遍历队列时进行,因为同一个组件可能会多次添加到队列中,我们需要另一个队列保存所有组件,不同之处是,这个队列内不会有重复的组件。
我们在enqueueSetState时,就可以做这件事
在flush方法中,我们还需要遍历renderQueue,来渲染每一个组件
延迟执行
现在还有一件最重要的事情:什么时候执行flush方法。 我们需要合并一段时间内所有的setState,也就是在一段时间后才执行flush方法来清空队列,关键是这个“一段时间“怎么决定。
一个比较好的做法是利用js的事件队列机制。
先来看这样一段代码:
你可以打开浏览器的调试工具运行一下,它们打印的结果是:
具体的原理可以看阮一峰的这篇文章,这里就不再赘述了。
我们可以利用事件队列,让flush在所有同步任务后执行
定义defer方法,利用刚才题目中出现的Promise.resolve
这样在一次“事件循环“中,最多只会执行一次flush了,在这个“事件循环”中,所有的setState都会被合并,并只渲染一次组件。
别的延迟执行方法
除了用
Promise.resolve().then( fn )
,我们也可以用上文中提到的setTimeout( fn, 0 )
,setTimeout的时间也可以是别的值,例如16毫秒。另外也可以用
requestAnimationFrame
或者requestIdleCallback
试试效果
就试试渲染上文中用React渲染的那两个例子:
效果和React完全一样 同样,用第二种方式调用setState:
结果也完全一样:
后话
在这篇文章中,我们又实现了一个很重要的优化:合并短时间内的多次setState,异步更新state。 到这里我们已经实现了React的大部分核心功能和优化手段了,所以这篇文章也是这个系列的最后一篇了。
这篇文章的所有代码都在这里:https://github.com/hujiulong/simple-react/tree/chapter-4
从零开始实现React系列
React是前端最受欢迎的框架之一,解读其源码的文章非常多,但是我想从另一个角度去解读React:从零开始实现一个React,从API层面实现React的大部分功能,在这个过程中去探索为什么有虚拟DOM、diff、为什么setState这样设计等问题。
整个系列大概会有四篇左右,我每周会更新一到两篇,我会第一时间在github上更新,有问题需要探讨也请在github上回复我~
上一篇文章
从零开始实现一个React(三):diff算法