从 javascript 事件循环看 Vue.nextTick 的原理和执行机制

更新日期: 2020-01-07阅读: 1.8k标签: 机制

抛砖引玉

vue 的特点之一就是响应式,但是有些时候数据更新了,我们看到页面上的 dom 并没有立刻更新。如果我们需要在 DOM 更新之后再执行一段代码时,可以借助 nextTick 实现。

我们先来看一个例子

export default {
  data() {
    return {
      msg: 0
    }
  },
  mounted() {
    this.msg = 1
    this.msg = 2
    this.msg = 3
  },
  watch: {
    msg() {
      console.log(this.msg)
    }
  }
}

这里的结果是只输出一个 3,而非依次输出 1,2,3。这是为什么呢?
vue 的官方文档是这样解释的

Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的Promise.then和 MessageChannel,如果执行环境不支持,会采用setTimeout(fn, 0)代替。

假如有这样一种情况,mounted钩子函数下一个变量 a 的值会被++循环执行 1000 次。 每次++时,都会根据响应式触发setter->Dep->Watcher->update->run。 如果这时候没有异步更新视图,那么每次++都会直接操作 DOM 一次,这是非常消耗性能的。 所以 Vue 实现了一个queue队列,在下一个 Tick(或者是当前 Tick 的微任务阶段)的时候会统一执行queue中Watcher的run。同时,拥有相同 id 的Watcher不会被重复加入到该queue中去,所以不会执行 1000 次Watcher的run。最终的结果是直接把 a 的值从 1 变成 1000,大大提升了性能。

在 vue 中,数据监测都是通过Object.defineProperty来重写里面的 set 和 get 方法实现的,vue 更新 DOM 是异步的,每当观察到数据变化时,vue 就开始一个队列,将同一事件循环内所有的数据变化缓存起来,等到下一次 eventLoop,将会把队列清空,进行 DOM 更新。

想要了解 vue.nextTick 的执行机制,我们先来了解一下 javascript 的事件循环。


js 事件循环

js 的任务队列分为同步任务和异步任务,所有的同步任务都是在主线程里执行的。异步任务可能会在 macrotask 或者 microtask 里面,异步任务进入 Event Table 并注册函数。当指定的事情完成时,Event Table 会将这个函数移入 Event Queue。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。上述过程会不断重复,也就是常说的 Event Loop(事件循环)。

macro-task(宏任务):

每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。浏览器为了能够使得 js 内部(macro)task与 DOM 任务能够有序执行,会在一个(macro)task执行结束后,在下一个(macro)task执行开始前,对页面进行重新渲染。宏任务主要包含:

  • script(整体代码)
  • setTimeout / setInterval
  • setImmediate(Node.js 环境)
  • I/O
  • UI render
  • postMessage
  • MessageChannel

micro-task(微任务):

可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前。所以它的响应速度相比 setTimeout(setTimeout 是 task)会更快,因为无需等渲染。也就是说,在某一个 macrotask 执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)。microtask 主要包含:

  • process.nextTick(Node.js 环境)
  • Promise
  • Async/Await
  • MutationObserver(html5 新特性)

小结

  1. 先执行主线程
  2. 遇到宏队列(macrotask)放到宏队列(macrotask)
  3. 遇到微队列(microtask)放到微队列(microtask)
  4. 主线程执行完毕
  5. 执行微队列(microtask),微队列(microtask)执行完毕
  6. 执行一次宏队列(macrotask)中的一个任务,执行完毕
  7. 执行微队列(microtask),执行完毕
  8. 依次循环。。。


Vue.nextTick 源码

vue 是采用双向数据绑定的方法驱动数据更新的,虽然这样能避免直接操作 DOM,提高了性能,但有时我们也不可避免需要操作 DOM,这时就该 Vue.nextTick(callback)出场了,它接受一个回调函数,在 DOM 更新完成后,这个回调函数就会被调用。不管是 vue.nextTick 还是vue.prototype.\$nextTick 都是直接用的nextTick这个闭包函数。

export const nextTick = (function () {
  const callbacks = []
  let pending = false
  let timerFunc

  function nextTickHandler () {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }
 ...
})()

使用数组callbacks保存回调函数,pending表示当前状态,使用函数nextTickHandler 来执行回调队列。在该方法内,先通过slice(0)保存了回调队列的一个副本,通过设置 callbacks.length = 0清空回调队列,最后使用循环执行在副本里的所有函数。

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve()
  var logError = err => {
    console.error(err)
  }
  timerFunc = () => {
    p.then(nextTickHandler).catch(logError)
    if (isIOS) setTimeout(noop)
  }
} else if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) {
  var counter = 1
  var observer = new MutationObserver(nextTickHandler)
  var textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
} else {
  timeFunc = () => {
    setTimeout(nextTickHandle, 0)
  }
}

队列控制的最佳选择是microtask,而microtask的最佳选择是Promise。但如果当前环境不支持 Promise,就检测到浏览器是否支持 MO,是则创建一个文本节点,监听这个文本节点的改动事件,以此来触发nextTickHandler(也就是 DOM 更新完毕回调)的执行。此外因为兼容性问题,vue 不得不做了microtask向macrotask 的降级方案。

为让这个回调函数延迟执行,vue 优先用promise来实现,其次是 html5 的 MutationObserver,然后是setTimeout。前两者属于microtask,后一个属于 macrotask。下面来看最后一部分。

return function queueNextTick(cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) cb.call(ctx)
    if (_resolve) _resolve(ctx)
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

这就是我们真正调用的nextTick函数,在一个event loop内它会将调用 nextTick的cb 回调函数都放入 callbacks 中,pending 用于判断是否有队列正在执行回调,例如有可能在 nextTick 中还有一个 nextTick,此时就应该属于下一个循环了。最后几行代码是 promise 化,可以将 nextTick 按照 promise 方式去书写(暂且用的较少)。


应用场景

场景一、点击按钮显示原本以 v-show = false 隐藏起来的输入框,并获取焦点。

<input id="keywords" v-if="showit">

showInput(){
  this.showit = true
  document.getElementById("keywords").focus()
}

以上的写法在第一个 tick 里,因为获取不到输入框,自然也获取不到焦点。如果我们改成以下的写法,在 DOM 更新后就可以获取到输入框焦点了。

showsou(){
  this.showit = true
  this.$nextTick(function () {
    // DOM 更新了
    document.getElementById("keywords").focus()
  })
}

场景二、获取元素属,点击获取元素宽度。

<div id="app">
  <p ref="myWidth" v-if="showMe">{{ message }}</p>
  <button @click="getMyWidth">获取p元素宽度</button>
</div>

getMyWidth() {
  this.showMe = true;
  this.message = this.$refs.myWidth.offsetWidth;
  //报错 TypeError: this.$refs.myWidth is undefined
  this.$nextTick(()=>{
      //dom元素更新后执行,此时能拿到p元素的属性
    this.message = this.$refs.myWidth.offsetWidth;
  })
}

来自:https://segmentfault.com/a/1190000022301747

链接: https://www.fly63.com/article/detial/8534

浅析前端页面渲染机制

作为一个前端开发,最常见的运行环境应该是浏览器吧,为了更好的通过浏览器把优秀的产品带给用户,也为了更好的发展自己的前端职业之路,有必要了解从我们在浏览器地址栏输入网址到看到页面这期间浏览器是如何进行工作的

这一次,彻底弄懂 JavaScript 执行机制

javascript是一门单线程语言,Event Loop是javascript的执行机制.牢牢把握两个基本点,以认真学习javascript为中心,早日实现成为前端高手的伟大梦想!

创建js hook钩子_js中的钩子机制与实现

钩子机制也叫hook机制,或者你可以把它理解成一种匹配机制,就是我们在代码中设置一些钩子,然后程序执行时自动去匹配这些钩子;这样做的好处就是提高了程序的执行效率,减少了if else 的使用同事优化代码结构

小程序的更新机制_如何实现强制更新?

在讲小程序的更新机制之前,我们需要先了解小程序的2种启动模式,分别为:冷启动和热启动。小程序不同的启动方式,对应的更新情况不不一样的。无论冷启动,还是热启动。小程序都不会马上更新的,如果我们需要强制更新,需要如何实现呢?

基于JWT的Token认证机制实现及安全问题

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。其JWT的组成:一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

web前端-JavaScript的运行机制

本文介绍JavaScript运行机制,JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。

轮询机制解决后端任务回调问题

现在有一个需求,前端有一个按钮,点击以后会调用后端一个接口,这个接口会根据用户的筛选条件去hadoop上跑任务,将图片的base64转为img然后打包成zip,生成一个下载连接返回给前端,弹出下载框。hadoop上的这个任务耗时比较久

JavaScript预解释是一种毫无节操的机制

js代码执行之前,浏览器首先会默认的把所有带var和function的进行提前的声明或者定义:1.理解声明和定义、2.对于带var和function关键字的在预解释的时候操作不一样的、3.预解释只发生在当前的作用域下

js对代码解析机制

脚本执行js引擎都做了什么呢?1.语法分析 2.预编译 3.解释执行。在执行代码前,还有两个步骤;语法分析很简单,就是引擎检查你的代码有没有什么低级的语法错误 ,查找全局变量声明(包括隐式全局变量声明,省略var声明),变量名作全局对象的属性,值为undefined

web认证机制

以前对认证这方面的认识一直不太深刻,不清楚为什么需要token这种认证,为什么不简单使用session存储用户登录信息等。最近读了几篇大牛的博客才对认证机制方面有了进一步了解。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!