详解Vue的异步更新实现原理


Posted in Vue.js onDecember 22, 2020

最近面试总是会被问到这么一个问题:在使用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是基于事件循环的单线程的语言。 执行的步骤大致是:

  1. 当代码执行时,所有同步的任务都在主线程上执行,形成一个执行栈;
  2. 在主线程之外还有一个任务队列(task queue),只要异步任务有了运行结果就在任务队列中放置一个事件;
  3. 一旦执行栈中所有同步任务执行完毕(主线程代码执行完毕),此时主线程不会空闲而是去读取任务队列。此时,异步的任务就结束等待的状态被执行。
  4. 主线程不断重复以上的步骤。

详解Vue的异步更新实现原理

我们把主线程执行一次的过程叫一个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 语法去处理事件,非常方便。

以上就是详解Vue的异步更新实现原理的详细内容,更多关于vue 异步更新的资料请关注三水点靠木其它相关文章!

Vue.js 相关文章推荐
如何实现vue的tree组件
Dec 03 Vue.js
vue中如何自定义右键菜单详解
Dec 08 Vue.js
vue中父子组件的参数传递和应用示例
Jan 04 Vue.js
vue使用过滤器格式化日期
Jan 20 Vue.js
vue3 watch和watchEffect的使用以及有哪些区别
Jan 26 Vue.js
vue使用lodop打印控件实现浏览器兼容打印的方法
Feb 07 Vue.js
vue引入Excel表格插件的方法
Apr 28 Vue.js
详解vue身份认证管理和租户管理
May 25 Vue.js
vue基于Teleport实现Modal组件
May 31 Vue.js
vue Element-ui表格实现树形结构表格
Jun 07 Vue.js
Vue 打包后相对路径的引用问题
Jun 05 Vue.js
el-table-column 内容不自动换行的解决方法
Aug 14 Vue.js
Vue组件简易模拟实现购物车
Dec 21 #Vue.js
vue实现购物车的小练习
Dec 21 #Vue.js
Vue实现小购物车功能
Dec 21 #Vue.js
vue监听滚动事件的方法
Dec 21 #Vue.js
vue el-upload上传文件的示例代码
Dec 21 #Vue.js
vue 在单页面应用里使用二级套嵌路由
Dec 19 #Vue.js
vue中如何添加百度统计代码
Dec 19 #Vue.js
You might like
php中取得URL的根域名的代码
2011/03/23 PHP
thinkphp autoload 命名空间自定义 namespace
2015/07/17 PHP
jQuery 打造动态下滑菜单实现说明
2010/04/15 Javascript
JSuggest自动匹配下拉框使用方法(示例代码)
2013/12/27 Javascript
jQuery+ajax实现鼠标单击修改内容的思路
2014/06/29 Javascript
javascript几个易错点记录
2014/11/26 Javascript
jquery控制背景音乐开关与自动播放提示音的方法
2015/02/06 Javascript
Bootstrap树形控件使用方法详解
2016/01/27 Javascript
JS平滑无缝滚动效果的实现代码
2016/05/06 Javascript
用js读写cookie的简单方法(推荐)
2016/08/08 Javascript
VUE饿了么树形控件添加增删改功能的示例代码
2017/10/17 Javascript
对angular4子路由&amp;辅助路由详解
2018/10/09 Javascript
electron制作仿制qq聊天界面的示例代码
2018/11/26 Javascript
Vue编写可显示周和月模式的日历 Vue自定义日历内容的显示
2019/06/26 Javascript
vue仿ios列表左划删除
2019/09/26 Javascript
Python使用chardet判断字符编码
2015/05/09 Python
python cx_Oracle模块的安装和使用详细介绍
2017/02/13 Python
python 画三维图像 曲面图和散点图的示例
2018/12/29 Python
python感知机实现代码
2019/01/18 Python
自定义django admin model表单提交的例子
2019/08/23 Python
python中web框架的自定义创建
2019/09/08 Python
在Python中使用MongoEngine操作数据库教程实例
2019/12/03 Python
python将音频进行变速的操作方法
2020/04/08 Python
python获取响应某个字段值的3种实现方法
2020/04/30 Python
解决keras,val_categorical_accuracy:,0.0000e+00问题
2020/07/02 Python
基于OpenCV的路面质量检测的实现
2020/11/04 Python
html5中audio支持音频格式的解决方法
2018/08/24 HTML / CSS
网络技术支持面试题
2013/04/22 面试题
化工机械应届生求职信
2013/11/04 职场文书
大学军训感言
2014/01/10 职场文书
五年级科学教学反思
2014/02/05 职场文书
捐款活动总结
2014/08/27 职场文书
领导班子三严三实对照检查材料
2014/09/25 职场文书
党员群众路线剖析材料
2014/10/08 职场文书
财务人员入职担保书
2015/09/22 职场文书
为了顺利买到演唱会的票用Python制作了自动抢票的脚本
2021/10/16 Python