详解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 相关文章推荐
ESLint 是如何检查 .vue 文件的
Nov 30 Vue.js
Vue3+elementui plus创建项目的方法
Dec 01 Vue.js
vuex页面刷新导致数据丢失的解决方案
Dec 10 Vue.js
vue实现桌面向网页拖动文件的示例代码(可显示图片/音频/视频)
Mar 01 Vue.js
vue中axios封装使用的完整教程
Mar 03 Vue.js
详解Vue的列表渲染
Nov 20 Vue.js
vue使用echarts实现折线图
Mar 21 Vue.js
vue如何使用模拟的json数据查看效果
Mar 31 Vue.js
vue-cli3.0修改打包后的文件名和文件地址,打包后本地运行报错解决
Apr 06 Vue.js
vue配置型表格基于el-table拓展之table-plus组件
Apr 12 Vue.js
关于vue-router-link选择样式设置
Apr 30 Vue.js
vue router 动态路由清除方式
May 25 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无限分类(树形类)的深入分析
2013/06/02 PHP
PHP制作用户注册系统
2015/10/23 PHP
PHP弱类型语言中类型判断操作实例详解
2017/08/10 PHP
php封装的pdo数据库操作工具类与用法示例
2019/05/08 PHP
Firefox+FireBug使JQuery的学习更加轻松愉快
2010/01/01 Javascript
IE6下出现JavaScript未结束的字符串常量错误的解决方法
2010/11/21 Javascript
基于jquery的复制网页内容到WORD的实现代码
2011/02/16 Javascript
单击按钮显示隐藏子菜单经典案例
2013/01/04 Javascript
Javascript表单验证要注意的事项
2014/09/29 Javascript
jQuery实现的鼠标滑过弹出放大图片特效
2016/01/08 Javascript
JavaScript每天必学之基础知识
2016/09/17 Javascript
微信小程序 开发经验整理
2017/02/15 Javascript
微信小程序 Buffer缓冲区的详解
2017/07/06 Javascript
js编写简单的聊天室功能
2017/08/17 Javascript
JS实现十分钟倒计时代码实例
2018/10/18 Javascript
vue路由--网站导航功能详解
2019/03/29 Javascript
小程序实现自定义导航栏适配完美版
2019/04/02 Javascript
JavaScript实现的3D旋转魔方动画效果实例代码
2019/07/31 Javascript
js实现列表向上无限滚动
2020/01/13 Javascript
如何使用原生Js实现随机点名详解
2021/01/06 Javascript
vue集成一个支持图片缩放拖拽的富文本编辑器
2021/01/29 Vue.js
跟老齐学Python之再深点,更懂list
2014/09/20 Python
Python实现控制台输入密码的方法
2015/05/29 Python
Python实现破解12306图片验证码的方法分析
2017/12/29 Python
django使用admin站点上传图片的实例
2019/07/28 Python
Django 项目布局方法(值得推荐)
2020/03/22 Python
pytorch cuda上tensor的定义 以及减少cpu的操作详解
2020/06/23 Python
python爬虫搭配起Bilibili唧唧的流程分析
2020/12/01 Python
python中reload重载实例用法
2020/12/15 Python
YSL Beauty加拿大官方商城:圣罗兰美妆加拿大
2017/05/15 全球购物
乌克兰的第一家手表店:Deka
2020/03/05 全球购物
写给女生的道歉信
2014/01/14 职场文书
社区十八大感言
2014/01/19 职场文书
《宋庆龄故居的樟树》教学反思
2014/04/07 职场文书
班训口号大全
2014/06/18 职场文书
使用Redis实现点赞取消点赞的详细代码
2022/03/20 Redis