博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Vue.js异步更新及nextTick
阅读量:4087 次
发布时间:2019-05-25

本文共 10407 字,大约阅读时间需要 34 分钟。

写在前面

前段时间在写项目时对nextTick的使用有一些疑惑。在查阅各种资料之后,在这里总结一下Vue.js异步更新的策略以及nextTick的用途和原理。如有总结错误的地方,欢迎指出!

本文将从以下3点进行总结:

  1. 为什么Vue.js要异步更新视图?
  2. JavaScript异步运行的机制是怎样的?
  3. 什么情况下要使用nextTick?

先看一个例子

复制代码
export default {    data () {        return {            message: 'begin'        };    },    methods () {        handleClick () {            this.message = 'end';            console.log(this.$refs.message.innerText); //打印“begin”        }    }}复制代码

打印出来的结果是“begin”,我们在点击事件里明明将message赋值为“end”,而获取真实DOM节点的innerHTML却没有得到预期中的“begin”,为什么?

再看一个例子

复制代码
export default {    data () {        return {            number: 0        };    },    methods: {        handleClick () {            for(let i = 0; i < 10000; i++) {                this.number++;            }        }    }}复制代码

在点击click事件之后,number会被遍历增加10000次。在Vue.js响应式系统中,可以看一下我的前一篇文章。我们知道Vue.js会经历“setter->Dep->Watcher->patch->视图”这几个流程。。

根据以往的理解,每次number被+1的时候,都会触发number的setter按照上边的流程最后来修改真实的DOM,然后DOM被更新了10000次,想想都刺激!看一下官网的描述:Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要显然

JavaScript的运行机制

为了方便理解Vue.js异步更新策略和nextTick,先介绍以下JS的运行机制,参考阮一峰老师的。摘取的关键部分如下:JS是单线程的,意思就是同一时间只能做一件事情。它是基于事件轮询的,具体可以分为以下几个步骤:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

 

 

上图就是主线程和任务队列的示意图。只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。主线程的执行过程是一个tick。所有的异步结果通过“任务队列”来被调度。任务队列中主要有两大类,“macrotask”和“microtask”,这两类task会进入任务队列。常见的 macrotask 有 setTimeout、MessageChannel、postMessage、setImmediate;常见的 microtask 有 MutationObsever 和 Promise.then。

 

事件轮询

Vue.js在修改数据的时候,不会立马修改数据,而是要等同一事件轮询的数据都更新完之后,再统一进行视图更新。 上的例子:

//改变数据vm.message = 'changed'//想要立即使用更新后的DOM。这样不行,因为设置message后DOM还没有更新console.log(vm.$el.textContent) // 并不会得到'changed'//这样可以,nextTick里面的代码会在DOM更新后执行Vue.nextTick(function(){    console.log(vm.$el.textContent) //可以得到'changed'})复制代码

图解:

 

 

 

模拟nextTick

nextTick在官网当中的定义:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

以下用setTimeout来模拟nextTick,先定义一个callbacks来存储nextTick,在下一个tick处理回调函数之前,所有的cb都会存储到这个callbacks数组当中。pending是一个标记位,代表等待的状态。接着setTimeout 会在 task 中创建一个事件 flushCallbacks ,flushCallbacks 则会在执行时将 callbacks 中的所有 cb 依次执行。

// 存储nextTicklet callbacks = [];let pending = false;function nextTick (cb) {    callbacks.push(cb);    if (!pending) {        // 代表等待状态的标志位        pending = true;        setTimeout(flushCallbacks, 0);    }}function flushCallbacks () {    pending = false;    const copies = callbacks.slice(0);    callbacks.length = 0;    for (let i = 0; i < copies.length; i++) {        copies[i]();    }}复制代码

真实的代码比这儿复杂的多,在Vue.js源码当中,nextTick定义在一个单独的文件中来维护,在src/core/util/next-tick.js中:

/* @flow *//* globals MessageChannel */import { noop } from 'shared/util'import { handleError } from './error'import { isIOS, isNative } from './env'const callbacks = []let pending = falsefunction flushCallbacks () {  pending = false  const copies = callbacks.slice(0)  callbacks.length = 0  for (let i = 0; i < copies.length; i++) {    copies[i]()  }}// Here we have async deferring wrappers using both microtasks and (macro) tasks.// In < 2.4 we used microtasks everywhere, but there are some scenarios where// microtasks have too high a priority and fire in between supposedly// sequential events (e.g. #4521, #6690) or even between bubbling of the same// event (#6566). However, using (macro) tasks everywhere also has subtle problems// when state is changed right before repaint (e.g. #6813, out-in transitions).// Here we use microtask by default, but expose a way to force (macro) task when// needed (e.g. in event handlers attached by v-on).let microTimerFunclet macroTimerFunclet useMacroTask = false// Determine (macro) task defer implementation.// Technically setImmediate should be the ideal choice, but it's only available// in IE. The only polyfill that consistently queues the callback after all DOM// events triggered in the same loop is by using MessageChannel./* istanbul ignore if */if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {  macroTimerFunc = () => {    setImmediate(flushCallbacks)  }} else if (typeof MessageChannel !== 'undefined' && (  isNative(MessageChannel) ||  // PhantomJS  MessageChannel.toString() === '[object MessageChannelConstructor]')) {  const channel = new MessageChannel()  const port = channel.port2  channel.port1.onmessage = flushCallbacks  macroTimerFunc = () => {    port.postMessage(1)  }} else {  /* istanbul ignore next */  macroTimerFunc = () => {    setTimeout(flushCallbacks, 0)  }}// Determine microtask defer implementation./* istanbul ignore next, $flow-disable-line */if (typeof Promise !== 'undefined' && isNative(Promise)) {  const p = Promise.resolve()  microTimerFunc = () => {    p.then(flushCallbacks)    // in problematic UIWebViews, Promise.then doesn't completely break, but    // it can get stuck in a weird state where callbacks are pushed into the    // microtask queue but the queue isn't being flushed, until the browser    // needs to do some other work, e.g. handle a timer. Therefore we can    // "force" the microtask queue to be flushed by adding an empty timer.    if (isIOS) setTimeout(noop)  }} else {  // fallback to macro  microTimerFunc = macroTimerFunc}/** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */export function withMacroTask (fn: Function): Function {  return fn._withTask || (fn._withTask = function () {    useMacroTask = true    const res = fn.apply(null, arguments)    useMacroTask = false    return res  })}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    if (useMacroTask) {      macroTimerFunc()    } else {      microTimerFunc()    }  }  // $flow-disable-line  if (!cb && typeof Promise !== 'undefined') {    return new Promise(resolve => {      _resolve = resolve    })  }}复制代码

加上注释之后:

/** * Defer a task to execute it asynchronously. */ /*    延迟一个任务使其异步执行,在下一个tick时执行,一个立即执行函数,返回一个function    这个函数的作用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完以后以此执行直到执行到timerFunc    目的是延迟到当前调用栈执行完以后执行*/export const nextTick = (function () {  /*存放异步执行的回调*/  const callbacks = []  /*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/  let pending = false  /*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/  let timerFunc  /*下一个tick时的回调*/  function nextTickHandler () {    /*一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/    pending = false    /*执行所有callback*/    const copies = callbacks.slice(0)    callbacks.length = 0    for (let i = 0; i < copies.length; i++) {      copies[i]()    }  }  // the nextTick behavior leverages the microtask queue, which can be accessed  // via either native Promise.then or MutationObserver.  // MutationObserver has wider support, however it is seriously bugged in  // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It  // completely stops working after triggering a few times... so, if native  // Promise is available, we will use it:  /* istanbul ignore if */  /*    这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法    优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法都会在microtask中执行,会比setTimeout更早执行,所以优先使用。    如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。    参考:https://www.zhihu.com/question/55364497  */  if (typeof Promise !== 'undefined' && isNative(Promise)) {    /*使用Promise*/    var p = Promise.resolve()    var logError = err => { console.error(err) }    timerFunc = () => {      p.then(nextTickHandler).catch(logError)      // in problematic UIWebViews, Promise.then doesn't completely break, but      // it can get stuck in a weird state where callbacks are pushed into the      // microtask queue but the queue isn't being flushed, until the browser      // needs to do some other work, e.g. handle a timer. Therefore we can      // "force" the microtask queue to be flushed by adding an empty timer.      if (isIOS) setTimeout(noop)    }  } else if (typeof MutationObserver !== 'undefined' && (    isNative(MutationObserver) ||    // PhantomJS and iOS 7.x    MutationObserver.toString() === '[object MutationObserverConstructor]'  )) {    // use MutationObserver where native Promise is not available,    // e.g. PhantomJS IE11, iOS7, Android 4.4    /*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/    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 {    // fallback to setTimeout    /* istanbul ignore next */    /*使用setTimeout将回调推入任务队列尾部*/    timerFunc = () => {      setTimeout(nextTickHandler, 0)    }  }  /*    推送到队列中下一个tick时执行    cb 回调函数    ctx 上下文  */  return function queueNextTick (cb?: Function, ctx?: Object) {    let _resolve    /*cb存到callbacks中*/    callbacks.push(() => {      if (cb) {        try {          cb.call(ctx)        } catch (e) {          handleError(e, ctx, 'nextTick')        }      } else if (_resolve) {        _resolve(ctx)      }    })    if (!pending) {      pending = true      timerFunc()    }    if (!cb && typeof Promise !== 'undefined') {      return new Promise((resolve, reject) => {        _resolve = resolve      })    }  }})()复制代码

关键在于timeFunc(),该函数起到延迟执行的作用。 从上面的介绍,可以得知timeFunc()一共有三种实现方式。

  • Promise
  • MutationObserver
  • setTimeout

用途

nextTick的用途

应用场景:需要在视图更新之后,基于新的视图进行操作。

看一个例子: 点击show按钮使得原来v-show:false的input输入框显示,并获取焦点:

复制代码
new Vue({  el: "#app",  data() {   return {     inputShow: false   }  },  methods: {    show() {      this.inputShow = true      this.$nextTick(() => {        this.$refs.input.focus()      })    }  }})

作者:Jee
链接:https://juejin.im/post/5b85b3326fb9a019fc76ecee
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的文章
gazebo似乎就是在装ROS的时候一起装了,装ROS的时候选择的是ros-melodic-desktop-full的话。
查看>>
React + TypeScript 实现泛型组件
查看>>
TypeScript 完全手册
查看>>
React Native之原理浅析
查看>>
Git操作清单
查看>>
基础算法
查看>>
前端面试
查看>>
React Hooks 异步操作踩坑记
查看>>
聊聊编码那些事,顺带实现base64
查看>>
TypeScript for React (Native) 进阶
查看>>
React 和 ReactNative 的渲染机制/ ReactNative 与原生之间的通信 / 如何自定义封装原生组件/RN中的多线程
查看>>
JavaScript实现DOM树的深度优先遍历和广度优先遍历
查看>>
webpack4 中的 React 全家桶配置指南,实战!
查看>>
react 设置代理(proxy) 实现跨域请求
查看>>
通过试题理解JavaScript
查看>>
webpack的面试题总结
查看>>
实践这一次,彻底搞懂浏览器缓存机制
查看>>
Koa2教程(常用中间件篇)
查看>>
React Hooks 完全指南
查看>>
React16常用api解析以及原理剖析
查看>>