老板,vue又双叒叕出bug了,dom老是获取不到。 详解Vue异步更新和nextTick

天啦,vue出bug了,DOM又不刷新了?

  工做中,用vue开发,常常会碰到用数据驱动dom,而后操做dom却没有效果的状况。若是有用到tab切换加上echarts展现,确定是气的想砸桌子。下面来谈谈vue中dom的刷新。javascript

什么是DOM异步更新?

   所谓异步更新,就是vue中用数据去驱动dom,数据变化了,DOM却不会当即的更新,而是在下一个Tick中更新dom。固然,vue中手动操做DOM,DOM是当即刷新的。   什么是数据驱动DOM?其实就是声名一个响应式的数据,当数据改变时,不用手动的操做dom,vue会自动的更新视图。 先来简单的看下代码 vue

异步dom源码

在浏览器中看vue的dom异步刷新 java

异步dom源码
.    从图中能够看出,当showDom发生变化的时候,‘有梦想的api搬运工’ 本应该隐藏的,而却没有隐藏,他会等到下一个队列中去刷新,这就是所谓的dom异步刷新。一样的,若是工做中经过数据去驱动dom,而后当即的去操做这个dom,有很大几率很报错哦。   看看手动的操做dom是什么结果呢。
同步dom源码
从上图能够看出,当手动操做dom时候,当修改dom后,颜色是马上变化的。这样就不会碰到使用异步dom出现的问题了。

  若是在vue中使用了数据去驱动dom,碰到了问题该怎么办呢?固然是今天的主角$nextTick去解决了。什么是nextTick?套用官网的话来讲 将回调延迟到下次 DOM 更新循环以后执行。在修改数据以后当即使用它,而后等待 DOM 更新。它跟全局方法 Vue.nextTick 同样,不一样的是回调的 this 自动绑定到调用它的实例上。。 通俗的说,当nextTick的里的方法会等到当前dom更新完之后触发。 面试

我很菜
.

那下面一段代码,固然就是很轻易的解释了。 算法

我很菜
. 界面初始化进来,hellow 的初始化值就是 'hello', 改变他hellow的值,而后当即去获取,因为异步更新,dom未发生变化。在nextTick中获取到了真正的值。

vue的异步刷新,用到了promise,MutationObserver,和setTimeout这三个api,真正理解这个三个api,是须要熟悉浏览器的,若是对浏览器eventloop 和microtask,macrotask还不熟悉的,请左拐,等会儿再来这儿看。从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理. 别忘记回来哦,后面的更精彩。下面的vue源码须要用到此部分的知识。api

使用异步刷新有什么优势?

  谈到vue的异步更新,不得不谈到vdom(虚拟dom)。什么是vdom?虚拟dom就是一个javascript对象,这个对象内部有许多dom的属性,以此来模拟一个真正的dom对象。而vue中所操做的dom,就是这个vdom,等到全部dom完成,把vdom挂载到真实的dom下,减小对真实dom的操做。这里就不展开多讲了。数组

  到底异步刷新有什么优势呢?看下面两端代码。 promise

我很菜
.

若是经过vue提供的数据驱动的方式异步刷新dom,add()方法,numbershu改变后,dom并不会当即的刷新,等到for循环结束后,获得最终值后,更新一次dom。dom的重绘与回流只发生一次。 而经过computedNum()方法,每一次的for循环,都会触发一次视图的更新,引起屡次的dom的重绘与回流,这种代码质量不敢恭维哪!! 而若是把computedNum()方法改为这样浏览器

computedNum() {
     let m = 0;
       for(let i = 0; i < 100 ;i ++) {
           m = i;
       }
        this.$refs.computedNum = m;
    }

复制代码

若是改写成这样,dom只会刷新一次。反而比vue使用vdom进行diff算法进行计算,而后更新dom,性能更加的好。 关于vue的vdom设计理念,其实就是在用户在不考虑性能优化的状况下,替用户进行看得过去的优化,并不能保证vue中的dom操做都是最优选择,只是让用户开发的更爽。性能优化

vue异步更新dom得策略,以及nextTick

说到vue异步更新dom得策略,得先看一下nextTick的实现原理。

nextTick实现原理

let callbacks = [];
let pending = false;
let timerFunc



if(typeof Promise !== 'undefined) { //判断当前浏览器是否支持promise,如支持,用promise实现异步刷新dom const p = Promise.resolve(); timerFunc = () => { p.then(flushCallbacks) } } else if(typeof MutationObserver !== 'undefined') { let counter = 1; const observe = new MutationObserver(flushCallbacks); const textNode = document.createTextNode(String(counter)); observe.observe(textNode,{ characterData: true }) timerFunc = () => { conter = (conter + 1) % 2; textNode.data = String(counter) } } else { timerFunc = () => { 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]() } } function nextTick(cb,ctx) { //ctx是vue实例 let _resolve; callbacks.push(() => { if(cb) { cb.call(ctx) //官网对nextTick的解释 自动绑定调用他的实例,就是vue的实例 } else if (_resolve) { _resolve(ctx); } }) if(!pending) { pending = true; timerFunc(); } if(!cb && type Promise !== 'undefined) {
       return new Promise(resolve => {
           _resolve = resolve
       })
   }
}

复制代码

这里把实现nextTick最重要的三部分扣了出来,而且简化了一下,若是有感兴趣的同窗,能够去源码里面看完整版的。   1.首先 nextTick须要传入一个回调函数,在当前执行栈内,当第一次调用nextTick方法的时候,callbacks里push回调函数,此时,pending的值是默认的false,而后改变pending,执行timerFun();p.then异步执行flushCallbacks函数,其实就是执行callbacks数组里的函数。在当前执行栈内,若是有第二次调用nextTick函数,继续向callbacks里推入回调函数。可是pending已是true了,不会在重复调用timerFunc,因为flushCallbacks是异步执行的,callbacks内的回调还未执行,又向callbacks推入一个新的回调函数,此时callbacks数组里有两个回调,以此类推。等到当前栈任务执行完,开始执行flushCallbacks,改变pending的状态,为下一个队列作铺垫。此时callbacks数组内已经有n个回调,而后执行这些函数。   2. 到底采用哪一个方法去异步执行?根据上面的判断,若是有promise,就使用promise,若是没有,就使用MutationObserver,若是尚未,就使用setTimeout。(IE:大家都看我干啥?我长得漂亮?)。简单说一下MutationObserve,h5新出来的api,当dom变化时,会触发回调。和promise同样,都是microtask任务。点我看MutationObserver的相关api。   3.根据源码我知道了,nextTick还能看成一个promise使用。this.nextTick().then();固然,nexTick()不能传函数哟。

DOM的异步更新

谈到vue,确定张口就来,经过defineProperty重写get与set方法,实现数据的双向绑定。毕竟面试必备的一句话。 下面来谈一谈dom是如何异步更新的。 当vue中的的响应式数据发生变化,经过set方法,会调用Watch类的update()方法,而update方法会调用queueWatcher方法来更新视图,看一下queueWatcher的定义

let waiting = false;
let index = 0;
let has = {};
...
...
export function queueWatcher (watcher) { // watch的实例
  const id = watcher.id //每一个响应式数据中都有一个独立watch.id
  if (has[id] == null) { // 若是一个响应式数据屡次改变,只会向queue数组中推入一次,视图刷新只更新最终的值。(还记得上面的add()方法吗,numbershu这个变量只更新一次)
    has[id] = true
    queue.push(watcher)
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) { // 若是是设置的不是异步更新,就执行更新dom。(好像历来没有用到过)
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
}
复制代码

上面的代码也都是简化的代码,便于理解,若是对原版感兴趣的,能够去看vue源码。

  1.当第一次响应式数据变化后,queueWatcher方法会向queue队列中添加一个更新视图的回调函数,若是queue中已经有了这个实例,就不添加。而flushSchedulerQueue这个方法就是遍历执行queue这个数组内的函数,queueWatcher这个方法最后调用了nextTick(flushSchedulerQueue),上面讲到过,nextTick会把回调放入到callbacks的数组内,这里是异步执行。因此calbacks里会有一个flushSchedulerQueue的函数。而flushSchedulerQueue内又有一个queue队列,当前执行栈内,若是有第二个响应式数据发生变化,又会向还未遍历的queue队列中添加watch实例,以此类推。当前执行栈任务结束后,调用任务队列中的callbacks内的回调函数,调用到flushSchedulerQueue函数时,此函数又会遍历调用queue的回调函数,最终调用watch.run()方法去更新视图。

说白了,vue的异步更新,最终的calbacks数组结构就相似[fn,fn,flushSchedulerQueue,fn]。

flushSchedulerQueue函数内部又有一个queue数组等待去遍历,其结构相似[fn,fn,fn],固然纯属猜想。

❤️ 若是各位看官看的还进行,请给一个赞,顺手点个关注,就是对个人最大支持

相关文章
相关标签/搜索