-
2020-12-31 11:19:55
目录
1. 为什么使用setState
在开发中我们并不能直接通过修改
state
的值来让界面发生更新:- 因为修改了
state
之后,希望React根据最新的State
来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化; - React并没有实现类似于Vue2中的
Object.defineProperty
或者Vue3中的Proxy
的方式来监听数据的变化; - 必须通过
setState
来告知React数据已经发生了变化;
如果我们直接修改
state
中的值,会报错:this.state.counter += 1 // Do not mutate state directly. Use setState()
那在组件中并没有实现setState的方法,为什么可以调用呢?原因很简单,
setState
方法是当前组件从Component中继承过来的:
所以,在React中,我们需要使用
setState
来实现数据的更新:this.setState({ counter: this.state.counter + 1 })
2. setState 异步更新
组件除了可以接收外界传递的状态外,还可以拥有自己的状态,并且这个状态也可以通过
setState
来进行更新。setState
用于变更状态,触发组件重新渲染,更新视图 UI。其语法如下:setState(updater, callback)
setState
可以接收两个参数:第一个参数可以是对象或函数,第二个参数是函数。- 第一个参数是对象的写法:
this.setState({ key: newState });
- 第一个参数是函数的写法:
// prevState 是上一次的 state,props 是此次更新被应用时的 props this.setState((prevState, props) => { return { key: prevState.key } })
那么,这两种写法的区别是什么呢?我们来看计数器的例子:
class App extends React.Component { constructor (props) { super (props) this.state = { val: 0 } } handleClick () { this.setState({ val: this.state.val + 1 }) } render () { return ( <div className="App"> <input type="text" value={this.state.val} disabled/> <input type="button" onClick={this.handleClick.bind(this)} /> </div> ) } }
如果在
handleClick
方法内调两次setState
,是不是每次点击就自增2了呢?handleClick () { this.setState({ val: this.state.val + 1 }) this.setState({ val: this.state.val + 1 }) }
结果并非我们想的那样,每次点击按钮,依然是自增1。这是因为调用
setState
其实是异步的,也就是setState
调用之后,this.state
不会立即映射为新的值。上面代码会解析为以下形式:// 后面的数据会覆盖前面的更改,所以最终只加了一次. Object.assign( previousState, {val: state.val + 1}, {val: state.val + 1}, )
在上面我们调用了两次
setState
,但state
的更新会被合并,所以即使多次调用setState
,实际上可能也只是会重新渲染一次。如果想基于当前的
state
来计算出新的值,那么setState
第一个参数不应该像上面一样传递一个对象,而应该传递一个函数。handleClick () { this.setState((prevState, props) => { val: prevState.val + 1 } }) this.setState((prevState, props) => { val: prevState.val + 1 } }) }
此时,在
handleClick
方法内调两次setState
,就能实现每次点击都自增2了。传递一个函数可以让你在函数内访问到当前的
state
值。setState
的调用是分批的,所以可以链式地进行更新,并确保它们是一个建立在另一个之上的,这样才不会发生冲突。setState
的第二个参数是一个可选的回调函数。这个回调函数将在组件重新渲染后执行。等价于在componentDidUpdate
生命周期内执行。通常建议使用componentDidUpdate
来代替此方式。在这个回调函数中你可以拿到更新后state
的值。this.setState({ key1: newState1, key2: newState2, ... }, callback) // 第二个参数是 state 更新完成后的回调函数
通过上面内容,可以知道调用
setState
时,组件的state
并不会立即改变,setState
只是把要修改的state
放入一个队列,React
会优化真正的执行时机,并出于性能原因,会将React
事件处理程序中的多次React
事件处理程序中的多次setState
的状态修改合并成一次状态修改。 最终更新只产生一次组件及其子组件的重新渲染,这对于大型应用程序中的性能提升至关重要。批量更新的流程图如下:
this.setState({ count: this.state.count + 1 ===> 入队,[count+1的任务] }); this.setState({ count: this.state.count + 1 ===> 入队,[count+1的任务,count+1的任务] }); ↓ 合并 state,[count+1的任务] ↓ 执行 count+1的任务
注意: 在React中,不能直接使用
this.state.key = value
方式来更新状态,这种方式 React 内部无法知道我们修改了组件,因此也就没办法更新到界面上。所以一定要使用 React 提供的setState
方法来更新组件的状态。那 为什么 setState 是异步的,React官方团队的解释如下:
- 保持内部一致性。如果改为同步更新的方式,尽管
setState
变成了同步,但是 props 不是。 - 为后续的架构升级启用并发更新。为了完成异步渲染,React 会在
setState
时,根据它们的数据来源分配不同的优先级,这些数据来源有:事件回调句柄、动画效果等,再根据优先级并发处理,提升渲染性能。
简单总结如下:
setState
设计为异步,可以显著的提升性能。如果每次调用setState
都进行一次更新,那么意味着render
函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;- 如果同步更新了
state
,但是还没有执行render
函数,那么state
和props
不能保持同步。state
和props
不能保持一致性,会在开发中产生很多的问题;
3. setState 同步场景
上面的例子使我们建立了这样一个认知:setState 是异步的,但下面这个案例又会颠覆你的认知。如果我们将 setState 放在 setTimeout 事件中,那情况就完全不同了:
class Test extends Component { state = { count: 0 } componentDidMount(){ this.setState({ count: this.state.count + 1 }); console.log(this.state.count); setTimeout(() => { this.setState({ count: this.state.count + 1 }); console.log("setTimeout: " + this.state.count); }, 0); } render(){ ... } }
这时就会输出 0,2。因为 setState 并不是真正的异步函数,它实际上是通过队列延迟执行操作实现的,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新。值为 true 则执行异步操作,false 则直接同步更新。
在 onClick、onFocus 等事件中,由于合成事件封装了一层,所以可以将 isBatchingUpdates 的状态更新为 true;在 React 的生命周期函数中,同样可以将 isBatchingUpdates 的状态更新为 true。那么在 React 自己的生命周期事件和合成事件中,可以拿到 isBatchingUpdates 的控制权,将状态放进队列,控制执行节奏。而在外部的原生事件中,并没有外层的封装与拦截,无法更新 isBatchingUpdates 的状态为 true。这就造成 isBatchingUpdates 的状态只会为 false,且立即执行。所以在 addEventListener 、setTimeout、setInterval 这些原生事件中都会同步更新。
实际上,setState 并不是具备同步这种特性,只是在特定的情境下,它会从 React 的异步管控中“逃脱”掉。
4. 调用 setState 发生了什么
修改 state 方法有两种:
- 构造函数里修改
state
,只需要直接操作this.state
即可, 如果在构造函数里执行了异步操作,就需要调用setState
来触发重新渲染。 - 在其余的地方需要改变state的时候只能使用
setState
,这样 React 才会触发 UI 更新。
所以, setState 时会设置新的
state
并更新 UI。当然,state
的更新可能是异步的,出于性能考虑,React
可能会把多个setState
调用合并成一个调用。那么 state 的更新何时是同步何时又是异步的呢?我们来看一下setState的执行流程图:
(1)setState
下面来看下每一步的源码,首先是 setState 入口函数:
ReactComponent.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); } };
入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去。这里我们以对象形式的入参为例,可以看到它直接调用了 this.updater.enqueueSetState 这个方法。
(2)enqueueSetState
enqueueSetState: function (publicInstance, partialState) { // 根据 this 拿到对应的组件实例 var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState'); // 这个 queue 对应的就是一个组件实例的 state 数组 var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); // enqueueUpdate 用来处理当前的组件实例 enqueueUpdate(internalInstance); }
这里 enqueueSetState 做了两件事:
- 将新的 state 放进组件的状态队列里;
- 用 enqueueUpdate 来处理将要更新的实例对象。
(3)enqueueUpdate
function enqueueUpdate(component) { ensureInjected(); // 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段 if (!batchingStrategy.isBatchingUpdates) { // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件 batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } // 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等” dirtyComponents.push(component); if (component._updateBatchNumber == null) { component._updateBatchNumber = updateBatchNumber + 1; } }
这个 enqueueUpdate 引出了一个关键的对象——batchingStrategy,该对象所具备的isBatchingUpdates属性直接决定了当下是要走更新流程,还是应该排队等待;其中的batchedUpdates 方法更是能够直接发起更新流程。由此可以推测,batchingStrategy 或许正是 React 内部专门用于管控批量更新的对象。
(4)batchingStrategy
var ReactDefaultBatchingStrategy = { // 全局唯一的锁标识 isBatchingUpdates: false, // 发起更新动作的方法 batchedUpdates: function(callback, a, b, c, d, e) { // 缓存锁变量 var alreadyBatchingStrategy = ReactDefaultBatchingStrategy. isBatchingUpdates // 把锁“锁上” ReactDefaultBatchingStrategy. isBatchingUpdates = true if (alreadyBatchingStrategy) { callback(a, b, c, d, e) } else { // 启动事务,将 callback 放进事务里执行 transaction.perform(callback, null, a, b, c, d, e) } } }
batchingStrategy 对象可以理解为它是一个“锁管理器”。
这里的“锁”,是指 React 全局唯一的 isBatchingUpdates 变量,isBatchingUpdates 的初始值是 false,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate 去执行更新动作时,会先把这个锁给“锁上”(置为 true),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队等候下一次的批量更新,而不能随意“插队”。此处体现的“任务锁”的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。**
5. 总结
对于那道常考的面试题:setState 是同步更新还是异步更新? 我们心中或许已经有了答案。
setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同:在 React 钩子函数及合成事件中,它表现为异步;而在 setTimeout、setInterval 等函数中,包括在 DOM 原生事件中,它都表现为同步。这种差异,本质上是由 React 事务机制和批量更新机制的工作方式来决定的。
在源码中,通过 isBatchingUpdates 来判断setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。
那什么情况下 isBatchingUpdates 会为 true 呢?
- 在 React 可以控制的地方,isBatchingUpdates就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。
- 在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。
一般认为,做异步设计是为了性能优化、减少渲染次数,React 团队还补充了两点:
- 保持内部一致性。如果将 state 改为同步更新,那尽管 state 的更新是同步的,但是 props不是。
- 启用并发更新,完成异步渲染。
附一个常考的面试题:
class Test extends React.Component { state = { count: 0 }; componentDidMount() { this.setState({count: this.state.count + 1}); console.log(this.state.count); this.setState({count: this.state.count + 1}); console.log(this.state.count); setTimeout(() => { this.setState({count: this.state.count + 1}); console.log(this.state.count); this.setState({count: this.state.count + 1}); console.log(this.state.count); }, 0); } render() { return null; } };
- 首先第一次和第二次的 console.log,都在 React 的生命周期事件中,所以是异步的处理方式,则输出都为 0;
- 而在 setTimeout 中的 console.log 处于原生事件中,所以会同步的处理再输出结果,但需要注意,虽然 count 在前面经过了两次的 this.state.count + 1,但是每次获取的 this.state.count 都是初始化时的值,也就是 0;
- 所以此时 count 是 1,那么后续在 setTimeout 中的输出则是 2 和 3。
所以答案是 0,0,2,3。
更多相关内容 - 因为修改了
-
c#异步读取数据库与异步更新ui的代码实现
2020-09-04 20:17:51主要介绍了c#从数据库里取得数据并异步更新ui的方法,大家参考使用吧 -
c#中Winform实现多线程异步更新UI(进度及状态信息)
2021-01-20 05:30:32引言 在进行Winform程序开发需要进行大量的数据的读写操作的时候,往往会需要...下面就开始一步步的去实现异步线程更新ui的demo程序吧。 应用背景 写入一定量的数据到文本文件中,同时需要在主界面中反应出写入数据的 -
详解android进行异步更新UI的四种方式
2021-01-04 13:54:50使用AsyncTask异步任务; 使用runOnUiThread(action)方法; 使用Handler的post(Runnabel r)方法; 下面分别使用四种方式来更新一个TextView。 1.使用Handler消息传递机制 package ... -
详解android异步更新UI的几种方法
2021-01-04 23:18:01前言 我们知道在Android开发中不能在非...android中有下列几种异步更新ui的解决办法: Activity.runOnUiThread(Runnable) View.post(Runnable) long) View.postDelayed(Runnable, long) 使用handler(线程间通讯) -
Tensorflow的梯度异步更新示例
2020-12-20 17:45:51先说一下应用吧,一般我们进行网络训练时,都有一个batchsize设置,也就是一个batch一个batch的更新梯度,能有这个batch的前提是这个batch中所有的图片的大小一致,这样才能组成一个placeholder。那么若一个网络对... -
Winform实现多线程异步更新UI(进度及状态信息)
2018-04-03 10:09:43Winform实现多线程异步更新UI(进度及状态信息) 实例代码 -
浅谈Vuejs中nextTick()异步更新队列源码解析
2020-11-29 11:18:00vue2.0里面的深入响应式原理的异步更新队列 官网说明如下: 只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会一次推入到队列中。这种... -
android开发教程之handler异步更新ui
2020-09-04 16:55:02主要介绍了android使用handler异步更新ui的示例,大家参考使用吧 -
详解从Vue.js源码看异步更新DOM策略及nextTick
2020-10-19 03:39:32本篇文章主要介绍了从Vue.js源码看异步更新DOM策略及nextTick,具有一定的参考价值,感兴趣的小伙伴们可以参考一 -
VUE异步更新DOM – 用$nextTick解决DOM视图的问题
2021-01-18 18:25:49VUE异步更新DOM 首先,Vue 在更新 DOM 时是异步执行的! 所以只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在... -
vue在使用ECharts时的异步更新和数据加载详解
2020-08-28 18:34:26主要给大家介绍了关于vue在使用ECharts时的异步更新和数据加载的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。 -
安卓Android源码——动态ListView,支持异步更新列表,异步更新图片.rar
2021-10-11 10:58:59安卓Android源码——动态ListView,支持异步更新列表,异步更新图片.rar -
C# WPF异步更新UI元素
2022-03-28 22:06:37} 正确方式2: 使用异步函数, asyn, await 关键字 async Task UpdateMessage(string msg, Brush brush) { await this.txtMsg.Dispatcher.InvokeAsync(new Action(() => { Run run = new Run(); Paragraph paragraph ...常见的问题"调用线程无法访问此对象,因为另一个线程拥有该对象。"
常见错误测试代码如下:
private void UpdateWrongMethod() { try { Thread.Sleep(1000); txtMsg.AppendText("Wrong Method Demo!"); } catch (Exception ex) { UpdateMessage(ex.Message, System.Windows.Media.Brushes.Red); } } private void btnWrongMethod_Click(object sender, RoutedEventArgs e) { try { Thread th = new Thread(UpdateWrongMethod); th.Start(); } catch (Exception ex) { UpdateMessage(ex.Message, System.Windows.Media.Brushes.Red); } }
正确方式1:
使用 Dispatcher.BeginInvoke 方法
private void UpdateRightMethod() { try { Thread.Sleep(1000); this.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(() => { txtMsg.AppendText("Use dispatcher Begin Invoke is right!"); })); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void btnRightMethod_Click(object sender, RoutedEventArgs e) { Thread th = new Thread(UpdateRightMethod); th.Start(); }
正确方式2:
使用异步函数, asyn, await 关键字
async Task UpdateMessage(string msg, Brush brush) { await this.txtMsg.Dispatcher.InvokeAsync(new Action(() => { Run run = new Run(); Paragraph paragraph = new Paragraph(); run.Foreground = brush; run.Text = msg; paragraph.Inlines.Add(run); paragraph.LineHeight = 1; this.txtMsg.Document.Blocks.Add(paragraph); this.txtMsg.ScrollToEnd(); })); } private async void btnDirectRight\_Click(object sender, RoutedEventArgs e) { for (int i = 1; i < 10; i++) { await UpdateMessage($"Test Point {i}", Brushes.Green); await Task.Delay(500); } }
-
vue 解决异步数据更新问题
2020-10-16 02:38:18今天小编就为大家分享一篇vue 解决异步数据更新问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 -
Jquery工作常用实例 使用AJAX使网页进行异步更新
2021-01-19 17:27:33AJAX 通过在后台与服务器交换少量数据的方式,允许网页进行异步更新。这意味着有可能在不重载整个页面的情况下,对网页的一部分进行更新。 通过 jQuery AJAX,你可以直接把远程数据载入网页被选HTML元素中。 Jquery... -
Android应用源码动态ListView,支持异步更新列表,异步更新图片.zip项目安卓应用源码下载
2022-03-07 14:48:09Android应用源码动态ListView,支持异步更新列表,异步更新图片.zip项目安卓应用源码下载Android应用源码动态ListView,支持异步更新列表,异步更新图片.zip项目安卓应用源码下载 1.适合学生毕业设计研究参考 2.适合... -
redis和mysql的异步更新
2021-12-21 12:27:47#指定 gearman 的服务信息到server2上的4730端口 实现异步更新 server4: vim test.sql #编写 mysql 触发器 注释掉2、3行,取消注释5到9行 mysql < test.sql mysql --> SHOW TRIGGERS FROM test; #查看触发器 真机中...redis和mysql如何同步:
一定要确定好master
-------- (上次实验server3是master)
从真机中把lib_mysqludf_json-master.zip传给server4
scp /home/westos/lib_mysqludf_json-master.zip server4:在server2中开启nginx和php-fpm服务
server2:nginx #开启nginx
systemctl start php-fpm.service #开启php-fpm服务server4:【lib_mysqludf_json】
systemctl start mariadb #开启mariadb服务 ps ax #查看mariadb服务是否开启 mysql --> use test --> select * from test; #查看test表
yum list mariadb-devel #列出mariadb-devel yum install -y unzip #下载unzip解压工具 unzip lib_mysqludf_json-master.zip #解压 cd lib_mysqludf_json-master/ ls ll yum install -y gcc #下载依赖环境 ls gcc $(mysql_config --cflags) -shared -fPIC -o lib_mysqludf_json.so lib_mysqludf_json.c ls ll cd /usr/lib64/mysql/ ls ll cd plugin/ ls pwd mysql > show global variables like 'plugin_dir';
cd cd lib_mysqludf_json-master/ ls cp lib_mysqludf_json.so /usr/lib64/mysql/plugin/ cd /usr/lib64/mysql/plugin/ mysql > CREATE FUNCTION json_object RETURNS STRING SONAME 'lib_mysqludf_json.so'; > select * from mysql.func; #查看函数
安装 gearman-mysql-udf
从真机中把实验需要的包传给server4:
scp libgearman-* libevent-devel-2.0.21-4.el7.x86_64.rpm server4:
scp gearman-mysql-udf-0.6.tar.gz server4:server4:
安装 gearman-mysql-udf【这个插件是用来管理调用 Gearman 的分布式的队列】
cd yum install -y libgearman-* libevent-devel-2.0.21-4.el7.x86_64.rpm tar zxf gearman-mysql-udf-0.6.tar.gz cd gearman-mysql-udf-0.6/ ./configure --libdir=/usr/lib64/mysql/plugin/ --with-mysql make && make install cd /usr/lib64/mysql/plugin/ ls cd mysql --> CREATE FUNCTION gman_do_background RETURNS STRING SONAME 'libgearman_mysql_udf.so'; #注册函数 --> CREATE FUNCTION gman_servers_set RETURNS STRING SONAME 'libgearman_mysql_udf.so'; #注册函数 --> select * from mysql.func; #查看函数
从真机中把实验需要的包传给server2:
scp gearmand-1.1.12-18.el7.x86_64.rpm server2:
scp libgearman-1.1.12-18.el7.x86_64.rpm server2:server2中:【slave端】
yum install gearmand-1.1.12-18.el7.x86_64.rpm libgearman-1.1.12-18.el7.x86_64.rpm systemctl start gearmand.service #开启gearmand服务 netstat -anplt #查看4730端口是否开启
server4:
mysql
--> SELECT gman_servers_set('172.25.70.2:4730');#指定 gearman 的服务信息到server2上的4730端口
实现异步更新
server4:
vim test.sql #编写 mysql 触发器 注释掉2、3行,取消注释5到9行 mysql < test.sql mysql --> SHOW TRIGGERS FROM test; #查看触发器
真机中把需要的文件和包传给server2
server2:
vim worker.php #编写 gearman 的 worker 端 第7行改为:$redis->connect('172.25.70.3', 6379); #指向ip是master php -m | grep redis php -m | grep gearman yum install -y php-pecl-gearman-1.1.2-1.el7.x86_64.rpm php -m | grep gearman systemctl reload php-fpm #平滑更新 mv worker.php /usr/local/ which php php /usr/local/worker.php #先不要打入后台【ctrl+z打入后台】 ps ax #查看进程 #确保worker.php在后台运行,只有一个进程开启
server4:
mysql
> update test set name='hello' where id=1; #更新 mysql 中的数据server3: #一定要在master主机上查看
redis-cli
> get 1 #看该的名字是否同步 -
动态ListView,支持异步更新列表,异步更新图片毕业设计—(包含完整源码可运行).zip
2022-04-23 22:20:45动态ListView,支持异步更新列表,异步更新图片毕业设计—(包含完整源码可运行).zip -
Android应用源码动态ListView,支持异步更新列表,异步更新图片.zip
2021-12-17 18:36:24源码参考,欢迎下载 -
动态ListView,支持异步更新列表,异步更新图片.zip
2021-08-10 18:17:27动态ListView,支持异步更新列表,异步更新图片.zip -
Vue的异步更新实现原理
2020-12-21 15:52:13} 这就涉及到Vue底层的异步更新原理,也要说一说nextTick的实现。不过在说nextTick之前,有必要先介绍一下JS的事件运行机制。 JS运行机制 众所周知,JS是基于事件循环的单线程的语言。执行的步骤大致是: 当代码...关注公众号 前端开发博客,回复“加群”
加入我们一起学习,天天进步
作者:Liqiuyue
链接:https://juejin.cn/post/6908264284032073736
最近面试总是会被问到这么一个问题:在使用vue的时候,将
for
循环中声明的变量i
从1增加到100,然后将i
展示到页面上,页面上的i
是从1跳到100,还是会怎样?答案当然是只会显示100,并不会有跳转的过程。怎么可以让页面上有从1到100显示的过程呢,就是用
setTimeout
或者Promise.then
等方法去模拟。讲道理,如果不在vue里,单独运行这段程序的话,输出一定是从1到100,但是为什么在vue中就不一样了呢?
for(let i=1; i<=100; i++){ console.log(i); }
这就涉及到Vue底层的异步更新原理,也要说一说
nextTick
的实现。不过在说nextTick
之前,有必要先介绍一下JS的事件运行机制。JS运行机制
众所周知,JS是基于事件循环的单线程的语言。执行的步骤大致是:
当代码执行时,所有同步的任务都在主线程上执行,形成一个执行栈;
在主线程之外还有一个任务队列(task queue),只要异步任务有了运行结果就在任务队列中放置一个事件;
一旦执行栈中所有同步任务执行完毕(主线程代码执行完毕),此时主线程不会空闲而是去读取任务队列。此时,异步的任务就结束等待的状态被执行。
主线程不断重复以上的步骤。
我们把主线程执行一次的过程叫一个
tick
,所以nextTick
就是下一个tick
的意思,也就是说用nextTick
的场景就是我们想在下一个tick
做一些事的时候。所有的异步任务结果都是通过任务队列来调度的。而任务分为两类:宏任务(macro task)和微任务(micro task)。它们之间的执行规则就是每个宏任务结束后都要将所有微任务清空。常见的宏任务有
setTimeout/MessageChannel/postMessage/setImmediate
,微任务有MutationObsever/Promise.then
。想要透彻学习事件循环,推荐Jake在JavaScript全球开发者大会的演讲,保证讲懂!
nextTick原理
派发更新
大家都知道vue的响应式的靠依赖收集和派发更新来实现的。在修改数组之后的派发更新过程,会触发
setter
的逻辑,执行dep.notify()
:// src/core/observer/watcher.js class Dep { notify() { //subs是Watcher的实例数组 const subs = this.subs.slice() for(let i=0, l=subs.length; i<l; i++){ subs[i].update() } } }
遍历
subs
里每一个Watcher
实例,然后调用实例的update
方法,下面我们来看看update
是怎么去更新的:class Watcher { update() { ... //各种情况判断之后 else{ queueWatcher(this) } } }
update
执行后又走到了queueWatcher
,那就继续去看看queueWatcher
干啥了(希望不要继续套娃了://queueWatcher 定义在 src/core/observer/scheduler.js const queue: Array<Watcher> = [] let has: { [key: number]: ?true } = {} let waiting = false let flushing = false let index = 0 export function queueWatcher(watcher: Watcher) { const id = watcher.id //根据id是否重复做优化 if(has[id] == null){ has[id] = true if(!flushing){ queue.push(watcher) }else{ let i=queue.length - 1 while(i > index && queue[i].id > watcher.id){ i-- } queue.splice(i + 1, 0, watcher) } if(!waiting){ waiting = true //flushSchedulerQueue函数: Flush both queues and run the watchers nextTick(flushSchedulerQueue) } } }
这里
queue
在pushwatcher
时是根据id
和flushing
做了一些优化的,并不会每次数据改变都触发watcher
的回调,而是把这些watcher
先添加到⼀个队列⾥,然后在nextTick
后执⾏flushSchedulerQueue
。flushSchedulerQueue
函数是保存更新事件的queue
的一些加工,让更新可以满足Vue更新的生命周期。这里也解释了为什么for循环不能导致页面更新,因为
for
是主线程的代码,在一开始执行数据改变就会将它push到queue
里,等到for
里的代码执行完毕后i
的值已经变化为100时,这时vue才走到nextTick(flushSchedulerQueue)
这一步。nextTick源码
接着打开vue2.x的源码,目录
core/util/next-tick.js
,代码量很小,加上注释才110行,是比较好理解的。const callbacks = [] let pending = false export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() }
首先将传入的回调函数
cb
(上节的flushSchedulerQueue
)压入callbacks
数组,最后通过timerFunc
函数一次性解决。let timerFunc if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) } } else { timerFunc = () => { setTimeout(flushCallbacks, 0) } }
timerFunc
下面一大片if else
是在判断不同的设备和不同情况下选用哪种特性去实现异步任务:优先检测是否原生⽀持Promise
,不⽀持的话再去检测是否⽀持MutationObserver
,如果都不行就只能尝试宏任务实现,首先是setImmediate
,这是⼀个⾼版本 IE 和 Edge 才⽀持的特性,如果都不⽀持的话最后就会降级为 setTimeout 0。这⾥使⽤
callbacks
⽽不是直接在nextTick
中执⾏回调函数的原因是保证在同⼀个 tick 内多次执⾏nextTick
,不会开启多个异步任务,⽽把这些异步任务都压成⼀个同步任务,在下⼀个 tick 执⾏完毕。nextTick使用
nextTick
不仅是vue的源码文件,更是vue的一个全局API。下面来看看怎么使用吧。当设置
vm.someData = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环tick中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用数据驱动的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)
。这样回调函数将在 DOM 更新完成后被调用。官网用例:
<div id="example">{{message}}</div>
var vm = new Vue({ el: '#example', data: { message: '123' } }) vm.message = 'new message' // 更改数据 vm.$el.textContent === 'new message' // false Vue.nextTick(function () { vm.$el.textContent === 'new message' // true })
并且因为
$nextTick()
返回一个Promise
对象,所以也可以使用async/await
语法去处理事件,非常方便。相关文章
最后
转发文章并关注公众号:前端开发博客,回复 1024,领取前端进阶资料
回复「电子书」领取27本精选电子书
回复「加群」加入前端大神交流群,一起学习进步
回复「Vue」获取 Vue 精选文章
分享和在看就是最大的支持❤️
-
异步更新缓存的逻辑
2021-03-22 14:45:09消息队列 为完成数据异步更新到缓存,可以采用消息队列方式(主备AMQ)来管理异步任务。 异步更新缓存的核心逻辑是,如何判断缓存过期。上图中引入了一个Router。 举个例子:运营会设置细化一个航班段的缓存有效期... -
Android项目动态ListView,支持异步更新列表,异步更新图片.rar
2021-09-09 16:01:22Android项目动态ListView,支持异步更新列表,异步更新图片.rar