精华内容
下载资源
问答
  • } 这就涉及到Vue底层的异步更新原理,也要说一说nextTick的实现。不过在说nextTick之前,有必要先介绍一下JS的事件运行机制。 JS运行机制 众所周知,JS是基于事件循环的单线程的语言。执行的步骤大致是: 当代码...

    关注公众号 程序员成长指北,回复“加群”

    加入我们一起学习,天天进步

    作者:Liqiuyue

    链接:https://juejin.cn/post/6908264284032073736

    最近面试总是会被问到这么一个问题:在使用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. 主线程不断重复以上的步骤。

    我们把主线程执行一次的过程叫一个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时是根据idflushing做了一些优化的,并不会每次数据改变都触发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 语法去处理事件,非常方便。

    相关文章

    1. 分享8个非常实用的Vue自定义指令

    2. Vue这些修饰符帮我节省20%的开发时间

    3. Vue路由权限控制分析

    最后

    ❤️爱心三连击
    
    1.看到这里了就点个在看支持下吧,你的「点赞,在看」是我创作的动力。
    2.关注公众号程序员成长指北,回复「1」加入高级前端交流群!「在这里有好多 前端 开发者,会讨论 前端 Node 知识,互相学习」!
    3.也可添加微信【ikoala520】,一起成长。
    
    
    
    

    分享和在看就是最大的支持❤️

    展开全文
  • 虽然 Vue 是数据驱动的,但是有时候我们不得不去操作 DOM 去处理一些特殊的场景,而 Vue 更新 DOM 是异步执行的,所以我们不得不去使用 $nextTick 去异步获取 DOM。 <template> <div> <span ref=...

    $nextTick 的使用场景

    虽然 Vue 是数据驱动的,但是有时候我们不得不去操作 DOM 去处理一些特殊的场景,而 Vue 更新 DOM 是异步执行的,所以我们不得不去使用 $nextTick 去异步获取 DOM。

    <template>
      <div>
        <span ref="msg">{{ msg }}</span>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          msg: 'hello nextTick'
        }
      },
      methods: {
        changeMsg() {
          this.msg = 'hello world'
          console.log(this.$refs.msg.innerHTML, '同步获取')
          this.$nextTick(() => {
            console.log(this.$refs.msg.innerHTML, '异步获取')
          })
        }
      },
      mounted() {
        this.changeMsg()
      }
    }
    </script>
    

    我们可以看到,当我我们直接改变数据后,获取 DOM 的话,值是没有改变的,而在 $nextTick 中却可以看到数据发生了变化,为什么呢?下面我们通过源码看一看原因

    Watcher 视图更新

    update () {
      /* istanbul ignore else */
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) {
        /*同步则执行run直接渲染视图*/
        this.run()
      } else {
        /*异步推送到观察者队列中,由调度者调用。*/
        queueWatcher(this)
      }
    }
    

    如果你看过响应式原理的时候,在 Watcher 中会有一个 update 函数用来更新视图的,当 this.sync 为 false 的时候,就标志着是异步更新,所以会执行 queueWatcher 函数

     /*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id
      /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
      if (has[id] == null) {
        has[id] = true
        if (!flushing) {
          /*如果没有flush掉,直接push到队列中即可*/
          queue.push(watcher)
        } else {
          // if already flushing, splice the watcher based on its id
          // if already past its id, it will be run next immediately.
          // 如果刷新了,那就从队列中取出,立即执行即可
          let i = queue.length - 1
          while (i > index && queue[i].id > watcher.id) {
            i--
          }
          queue.splice(i + 1, 0, watcher)
        }
        // queue the flush
        if (!waiting) { // 没有 waiting,则直接执行 nextTick
          waiting = true
    
          if (process.env.NODE_ENV !== 'production' && !config.async) {
            flushSchedulerQueue()
            return
          }
          nextTick(flushSchedulerQueue)
        }
      }
    }
    

    通过 queueWatcher 函数,我们就能看出来了,Watcher 不是立即更新视图的,而是会放在一个队列中,此时是 waiting 等待状态,它会检查 id 是否重复,如果重复的话,就不会放进队列中;如果没有重复才会放入队列,而且当前 Watcher 是不能刷新的,如果刷新的话,就从队列中取出,没有刷新的 Watcher 才会被放入队列中。如果没有 waiting 等待状态了,那么就证明需要进入下一个 tick 了,会执行 nextTick 方法。

    nextTick

    讲了这么多,终于到 nextTick 了

    export let isUsingMicroTask = false // 是否使用了微任务
    
    const callbacks = [] /*存放异步执行的回调*/
    let pending = false /*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
    
    function 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 microtasks.
    // In 2.5 we used (macro) tasks (in combination with microtasks).
    // However, it has subtle problems when state is changed right before repaint
    // (e.g. #6813, out-in transitions).
    // Also, using (macro) tasks in event handler would cause some weird behaviors
    // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
    // So we now use microtasks everywhere, again.
    // A major drawback of this tradeoff is that there are some scenarios
    // where microtasks have too high a priority and fire in between supposedly
    // sequential events (e.g. #4521, #6690, which have workarounds)
    // or even between bubbling of the same event (#6566).
    let timerFunc /*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
    
    // 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 next, $flow-disable-line */
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      timerFunc = () => {
        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) // 如果是 isIOS 环境,则执行 setTimeout
      }
      isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && ( // MutationObserver 在 IE 下的兼容性有问题
      isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
      // Use MutationObserver where native Promise is not available,
      // e.g. PhantomJS, iOS7, Android 4.4
      // (#6466 MutationObserver is unreliable in IE11)
      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)) {
      // Fallback to setImmediate.
      // Technically it leverages the (macro) task queue,
      // but it is still a better choice than setTimeout.
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else {
      // Fallback to setTimeout.
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    
    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      // 把 cb 加上异常处理存入 callbacks 数组中
      callbacks.push(() => {
        if (cb) {
          try {
            // 调用 cb()
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      if (!pending) {
        pending = true
        // 调用
        timerFunc()
      }
      // $flow-disable-line
      if (!cb && typeof Promise !== 'undefined') {
        // 返回 promise 对象
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    

    nextTick 接收两个参数,一个是回调函数,一个是当前环境的上下文,执行 nextTick 会将回调函数放入 callbacks 回调队列中,然后通过 timerFunc 去执行。然后会判断当前执行环境是否有 Promise,如果有的话,通过 Promise.resolve().then 去执行回调函数中的内容,如果是 IOS 环境的话,则执行 setTimeout,因为 IOS 的某些版本对 Promise 的支持不太好;如果当前环境不支持 Promise,则降级使用微任务 MutationObserver,注释中也列举出了很多不支持 Promise 的环境,例如 e.g. PhantomJS, iOS7, Android 4.4;如果 MutationObserver 也不被支持的话,那么就使用宏任务 setImmediate 了;而最坏的情况就是使用 setTimeout 了,至于为什么不直接使用 setTimeout 而多一个 setImmediate,是因为 setImmediate 的执行速度要比 setTimeout,因为 setTimeout 即使将时间参数设为 0 的话,也还是会有 4 ms 的延迟。

    为什么要异步更新视图

    <template>
      <div>
        <div>{{value}}</div>
      </div>
    </template>
    
    export default {
        data () {
            return {
                value: 0
            };
        },
        mounted () {
          for(let i = 0; i < 1000; i++) {
            this.value++;
          }
        }
    }
    

    当我们在 mounted 钩子函数中,循环改变某一个值的时候,如果没有异步更新,那么 value 每一次 ++ 的时候,都会操作 DOM 去更新,但是这种更新又是没有意义的,这样就会非常消耗性能。但是有了异步 DOM 队列,它只会在下一个 tick 执行,这样就能保障 i 从 0 直接到 1000 才执行,这样大大优化了性能。

    展开全文
  • 源码调试地址 ...什么是异步更新 在本轮宏任务内,组件内多个属性更新,或者一个属性更新多次,最终这个组件只会重新渲染一次,即组件里的dom只会做一次重新渲染 如:以下代码只会触发组件的一次更新渲染 ...

    源码调试地址

    https://github.com/KingComedy/vue-debugger

    什么是异步更新

    在本轮宏任务内,组件内多个属性更新,或者一个属性更新多次,最终这个组件只会重新渲染一次,即组件里的dom只会做一次重新渲染 如:以下代码只会触发组件的一次更新渲染

    this.count = 1
    this.key = 'key'
    this.count = 2

    Vue为什么是异步更新

    避免组件多次patch,引起dom多次重新渲染

    nextTick(cb, ctx)方法源码详解(在src\core\util\next-tick.js中定义)

    • 使用:$nextTick(cb)
    • 在next-tick.js里 定义了callbacks回调函数数组,定义pending任务执行状态
    • nextTick(cb), 将传入的cb回调函数push到callbacks,并且判断pending任务执行状态是否为false,即当前没有执行任务
    • 设置pending=true,执行timeFunc()
    • timeFunc函数的任务主要是异步执行 flushCallbacks函数,即Promise.resolve().then(flushCallbacks)
    • flushCallbacks主要是 遍历执行callbacks里的每个回调
    • timeFunc函数这边会做浏览器兼容的降级处理, 即浏览器不支持Promise, 依次降级为 MutationObserver(前提不是IE浏览器) => setImmediate => setTimeout
    • 总结:nextTick会把传入的回调函数存入callbacks,flushCallbacks函数会遍历执行callbacks里的每个回调,nextTick会将flushCallbacks当做一个任务推入微任务队列。

    异步更新过程

    • 当触发属性更新时,属性的dep(在数据做响应式处理时的依赖收集),会执行dep.notify通知属性的观察者watcher更新,执行所有watcher的update方法
    • watcher的update默认异步更新,执行queueWatcher(this), 将watcher推入watcher列表queue。通过watcher.id判断queue里是否已存在watcher,没有则加入到queue数组里
    • 判断!waiting === true, 即目前是否属于空闲状态,是则执行nextTick(flushSchedulerQueue)
    • flushSchedulerQueue: 对queue列表根据watcher.id进行排序,遍历执行执行queue列表里的所有watcher,先执行每一个watcher.before(即watcher所对应的组件的beforeUpdate生命周期函数) => 再执行watcher.run。遍历完成后,在重新遍历queue列表执行每个watcher所对应的组件的updated生命周期
    • 总结:属性更新 => 将渲染watcher推入queue列表(如果当前watcher已经存在就不入列) => 将遍历执行watcher的函数 flushSchedulerQueue 推入微任务队列, nextTick(flushSchedulerQueue)

     

    通过例子加深理解

    例子一

    countClick() {
      this.count = 1 // 触发更新,watcher进入queue列表,queue.push(watcher), 此时更新任务已经进入微任务队列
      this.count = 2 // 触发更新,当前组件的渲染watcher已经在queue列表里,所以不会进入列表
      console.log('count:', this.$refs.count.innerHTML) // count: 0,微任务队列还未执行,dom还未更新
      this.$nextTick(() => {
        console.log('count:', this.$refs.count.innerHTML) // count: 2 推入微任务队列callbacks
      })
    }

    流程解析:

    • 打印的顺序是:count:0 => count:2
    • count 第一次设置值时,触发更新,watcher进入queue列表,即queue.push(watcher), 此时更新任务已经进入微任务队列,即微任务队列为[flushCallbacks],且callbacks = [flushSchedulerQueue],只有flushSchedulerQueue执行完后,dom才会更新
    • count 第二次更新时,因为当前组件的渲染watcher已经在queue列表里,所以不会进入queue列表
    • 因为本轮宏任务还未执行完成,所以微任务队列也还没有执行,即更新任务还未执行,所以此时dom还没更新,dom上的count值仍旧为0
    • 执行nextTick时,会把回调函数(cb1)推入callbacks列表,所以此时微任务队列为[flushCallbacks], 即[[...callbacks]], 且 callbacks = [flushSchedulerQueue, cb1]
    • 当宏任务执行完成,开始遍历执行微任务,此时微任务就一个flushCallbacks,遍历执行callbacks
    • flushSchedulerQueue先执行,执行完成后,此时dom已经更新
    • cb1执行时,dom已经改变,所以此时dom的count值为2

     例子二

    countClick() {
      this.count = 1 // 触发更新,watcher进入queue列表,queue.push(watcher), 此时更新任务已经进入微任务队列
      this.count = 2 // 触发更新,当前组件的渲染watcher已经在queue列表里,所以不会进入queue列表
      Promise.resolve().then(() => {
        console.log('p1 count:', this.$refs.count.innerHTML) // count: 2
      })
      console.log('count:', this.$refs.count.innerHTML) // count: 0 属于宏任务,先执行,dom还未更新,所以count值还未改变为 0
      this.$nextTick(() => {
        console.log('cb1 count:', this.$refs.count.innerHTML) // count: 2
      })
    }

    流程解析:

    • 打印的顺序是:count:0 => cb1 count:2 => p1 count:2
    • count 第一次设置值时,触发更新,watcher进入queue列表,即queue.push(watcher), 此时更新任务已经进入微任务队列,即微任务队列为[flushCallbacks],且callbacks = [flushSchedulerQueue],只有flushSchedulerQueue执行完后,dom才会更新
    • count 第二次更新时,因为当前组件的渲染watcher已经在queue列表里,所以不会进入queue列表
    • Promise会把回调函数p1推入微任务队列,所以此时微任务队列为:[flushSchedulerQueue, p1]
    • 因为本轮宏任务还未执行完成,所以微任务队列也还没有执行,即更新任务还未执行,所以此时dom还没更新,dom上的count值仍旧为0
    • 执行nextTick时,会把回调函数(cb1)推入callbacks列表,所以此时微任务队列为[flushCallbacks, p1],即[[...callbacks], p1] ,且 callbacks = [flushSchedulerQueue, cb1]
    • 当宏任务执行完成,开始遍历执行微任务,此时微任务为[flushCallbacks, p1],会先执行flushCallbacks,即遍历执行callbacks = [flushSchedulerQueue, cb1]
    • flushSchedulerQueue先执行,执行完成后,此时dom已经更新
    • cb1执行时,dom已经改变,所以此时dom的count值为2,所以打印 cb1 count:2
    • 然后执行第二个微任务p1,即 p1 count:2

     例子三

    countClick() {
      Promise.resolve().then(() => {
        console.log("p1 count:", this.$refs.count.innerHTML); // count: 0
      });
      this.count = 1; // 触发更新, flushSchedulerQueue 存入callbacks, flushCallbacks才进入微任务队列
      this.count = 2;
      
      console.log("count:", this.$refs.count.innerHTML); // count: 0 属于宏任务,先执行,dom还未更新,所以count值还未改变为 0
      this.$nextTick(() => {
        console.log("cb2 count:", this.$refs.count.innerHTML); // count: 2
      });
    }

    流程解析:

    • 打印顺序 count:0 => p1 count:0 => cb1 count:2
    • 因为Promise在 count设值之前,已经进入了微任务队列,count设值时,flushSchedulerQueue 存入callbacks, flushCallbacks才进入微任务队列
    • 所以最终的微任务队列为 [p1, flushCallbacks],所以p1 打印在dom更新前

     例子四

    countClick() {
      this.$nextTick(() => {
        this.count = 3 // 触发一次更新,但是没有进入queue队列
        console.log("cb1 count:", this.$refs.count.innerHTML); // count: 2
      });
      Promise.resolve().then(() => {
        console.log("p1 count:", this.$refs.count.innerHTML); // 进入微任务队列
      });
      this.count = 1;
      this.count = 2;
      this.$nextTick(() => {
        console.log("cb2 count:", this.$refs.count.innerHTML); // count: 2
      });
    }

    流程解析:

    • 打印顺序 cb1 count:0 => cb2 count:3 => p1 count: 3
    • 微任务队列为:[flushCallbacks, p1], 且callbacks = [cb1, flushSchedulerQueue, cb2], cb1在执行的时候,flushSchedulerQueue还未执行,所以dom还未更新,所以打印时dom的count的值还是 0。
    • flushSchedulerQueue执行时,count的值已经在cb1更新为3,所以最终count的值为3
    • 所以flushCallbacks执行后打印顺序为: cb1 count:0 => cb2 count:3
    • 最后执行p1,打印p1 count:3

     

    总结

    • 每个组件都有自己对应的渲染watcher,用于执行dom的更新
    • 组件内,在一个宏任务内,多个属性的更新或者同个属性的多次更新,watcher只会渲染一次
    • 异步更新流程:属性更新 => 通知watcher更新 => queueWatcher(this) => nextTick(flushSchedulerQueue)
    • 属性更新 只会将flushSchedulerQueue推入微任务队列,只有在flushSchedulerQueue执行完后,dom才会更新完成

    相关文章

    Vue源码解析——组件更新过程:https://blog.csdn.net/comedyking/article/details/115670343

    Vue-Watcher观察者源码详解:https://blog.csdn.net/comedyking/article/details/117695761

    展开全文
  • 浅析Vue.nextTick()原理 1、为什么用Vue.nextTick() 2、什么是Vue.nextTick() 3、怎么用 4、小结 为什么用Vue.nextTick() 首先来了解一下JS的运行机制 JS运行机制(Event Loop) JS执行是单线程的,它是基于事件...

    浅谈 vue异步更新策略与Vue.nextTick()原理

    1、为什么用Vue.nextTick()
    2、什么是Vue.nextTick()
    3、怎么用
    4、小结

    • 为什么用Vue.nextTick()

    首先来了解一下JS的运行机制

    JS运行机制(Event Loop)

    JS执行是单线程的,它是基于事件循环的。
    
    1. 所有同步任务都在主线程上执行,形成一个执行栈。
    2. 主线程之外,会存在一个任务队列,只要异步任务有了结果,就在任务队列中放置一个事件。
    3. 当执行栈中的所有同步任务执行完后,就会读取任务队列。那些对应的异步任务,会结束等待状态,进入执行栈。
    4. 主线程不断重复第三步。

    这里主线程的执行过程就是一个tick,而所有的异步结果都是通过任务队列来调度。Event Loop 分为宏任务和微任务,无论是执行宏任务还是微任务,完成后都会进入到一下tick,并在两个tick之间进行UI渲染。

    由于Vue DOM更新是异步执行的,即修改数据时,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。为了确保得到更新后的DOM,所以设置了 Vue.nextTick()方法。

    • 什么是Vue.nextTick()

    是Vue的核心方法之一,官方文档解释如下:

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

    MutationObserver

    先简单介绍下MutationObserver:MO是HTML5中的API,是一个用于监视DOM变动的接口,它可以监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等。

    调用过程是要先给它绑定回调,得到MO实例,这个回调会在MO实例监听到变动时触发。这里MO的回调是放在microtask中执行的。

    // 创建MO实例
    const observer = new MutationObserver(callback)
    
    const textNode = '想要监听的Don节点'
    
    observer.observe(textNode, {
        characterData: true // 说明监听文本内容的修改
    })
    

    源码浅析

    nextTick 的实现单独有一个JS文件来维护它,在src/core/util/next-tick.js中。

    nextTick 源码主要分为两块:能力检测和根据能力检测以不同方式执行回调队列。

    能力检测

    由于宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,再使用宏任务。

    // 空函数,可用作函数占位符
    import { noop } from 'shared/util' 
    
     // 错误处理函数
    import { handleError } from './error'
    
     // 是否是IE、IOS、内置函数
    import { isIE, isIOS, isNative } from './env'
    
    // 使用 MicroTask 的标识符,这里是因为火狐在<=53时 无法触发微任务,
    //在modules/events.js文件中引用进行安全排除
    export let isUsingMicroTask = false 
    
     // 用来存储所有需要执行的回调函数
    const callbacks = []
    
    // 用来标志是否正在执行回调函数
    let pending = false 
    
    // 对callbacks进行遍历,然后执行相应的回调函数
    function flushCallbacks () {
        pending = false
        // 这里拷贝的原因是:
        // 有的cb 执行过程中又会往callbacks中加入内容
        // 比如 $nextTick的回调函数里还有$nextTick
        // 后者的应该放到下一轮的nextTick 中执行
        // 所以拷贝一份当前的,遍历执行完当前的即可,避免无休止的执行下去
        const copies = callbcks.slice(0)
        callbacks.length = 0
        for(let i = 0; i < copies.length; i++) {
            copies[i]()
        }
    }
    
    let timerFunc // 异步执行函数 用于异步延迟调用 flushCallbacks 函数
    
    // 在2.5中,我们使用(宏)任务(与微任务结合使用)。
    // 然而,当状态在重新绘制之前发生变化时,就会出现一些微妙的问题
    // (例如#6813,out-in转换)。
    // 同样,在事件处理程序中使用(宏)任务会导致一些奇怪的行为
    // 因此,我们现在再次在任何地方使用微任务。
    // 优先使用 Promise
    if(typeof Promise !== 'undefined' && isNative(Promise)) {
        const p = Promise.resolve()
        timerFunc = () => {
            p.then(flushCallbacks)
            
            // IOS 的UIWebView, Promise.then 回调被推入 microTask 队列,
            //但是队列可能不会如期执行
            // 因此,添加一个空计时器强制执行 microTask
            if(isIOS) setTimeout(noop)
        }
        isUsingMicroTask = true
    } else if(!isIE && typeof MutationObserver !== 'undefined' 
    && (isNative(MutationObserver) || 
    MutationObserver.toString === '[object MutationObserverConstructor]')) {
        // 当 原生Promise 不可用时,使用 原生MutationObserver
        // e.g. PhantomJS, iOS7, Android 4.4
     
        let counter = 1
        // 创建MO实例,监听到DOM变动后会执行回调flushCallbacks
        const observer = new MutationObserver(flushCallbacks)
        const textNode = document.createTextNode(String(counter))
        observer.observe(textNode, {
            characterData: true // 设置true 表示观察目标的改变
        })
        
        // 每次执行timerFunc 都会让文本节点的内容在 0/1之间切换
        // 切换之后将新值复制到 MO 观测的文本节点上
        // 节点内容变化会触发回调
        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)
        }
    }
    

    延迟调用优先级如下:
    Promise > MutationObserver > setImmediate > setTimeout

    export function nextTick(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)
            }
        })
        
        // pending 为false 说明本轮事件循环中没有执行过timerFunc()
        if(!pending) {
            pending = true
            timerFunc()
        }
        
        // 当不传入 cb 参数时,提供一个promise化的调用 
        // 如nextTick().then(() => {})
        // 当_resolve执行时,就会跳转到then逻辑中
        if(!cb && typeof Promise !== 'undefined') {
            return new Promise(resolve => {
                _resolve = resolve
            })
        }
    }
    

    next-tick.js 对外暴露了nextTick这一个参数,所以每次调用Vue.nextTick时会执行:

    • 把传入的回调函数cb压入callbacks数组
    • 执行timerFunc函数,延迟调用 flushCallbacks 函数
    • 遍历执行 callbacks 数组中的所有函数

    这里的 callbacks 没有直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行nextTick,不会开启多个异步任务,而是把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。

    • 怎么用

    **语法:**Vue.nextTick([callback, context])

    参数:

    • {Function} [callback]:回调函数,不传时提供promise调用
    • {Object} [context]:回调函数执行的上下文环境,不传默认是自动绑定到调用它的实例上。
    //改变数据
    vm.message = 'changed'
    
    //想要立即使用更新后的DOM。这样不行,因为设置message后DOM还没有更新
    console.log(vm.$el.textContent) // 并不会得到'changed'
    
    //这样可以,nextTick里面的代码会在DOM更新后执行
    Vue.nextTick(function(){
        // DOM 更新了
        //可以得到'changed'
        console.log(vm.$el.textContent)
    })
    
    // 作为一个 Promise 使用 即不传回调
    Vue.nextTick()
      .then(function () {
        // DOM 更新了
      })
    

    Vue实例方法vm.$nextTick做了进一步封装,把context参数设置成当前Vue实例。

    • 小结

    使用Vue.nextTick()是为了可以获取更新后的DOM 。
    触发时机:在同一事件循环中的数据变化后,DOM完成更新,立即执行Vue.nextTick()的回调。

    同一事件循环中的代码执行完毕 -> DOM 更新 -> nextTick callback触发

    在这里插入图片描述
    应用场景:

    • 在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。

      原因:是created()钩子函数执行时DOM其实并未进行渲染。

    • 在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作应该放在Vue.nextTick()的回调函数中。

      原因:Vue异步执行DOM更新,只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变,如果同一个watcher被多次触发,只会被推入到队列中一次。

    版本分析

    • 2.6 版本优先使用 microtask 作为异步延迟包装器,且写法相对简单。而2.5 版本中,nextTick 的实现是 microTimerFunc、macroTimerFunc 组合实现的,延迟调用优先级是:Promise > setImmediate >MessageChannel > setTimeout,具体见源码。
    • 2.5 版本在重绘之前状态改变时会有小问题(如 #6813)。此外,在事件处理程序中使用 macrotask 会导致一些无法规避的奇怪行为(如 #7109,#7153等)。
    • microtask 在某些情况下也是会有问题的,因为 microtask 优先级比较高,事件会在顺序事件(如#4521,#6690
      有变通方法)之间甚至在
    展开全文
  • 天啦,vue出bug了,DOM又不刷新了?工作中,用vue开发,经常会碰到用...所谓异步更新,就是vue中用数据去驱动dom,数据变化了,DOM却不会立即的更新,而是在下一个Tick中更新dom。当然,vue中手动操作DOM,DOM是立即...
  • 情景 需要生成一棵树,树的...实现一个有异步批量加载功能的树 class treeLoader { constructor(props) { const { treeData, treeProps, treeLoaderFn, //加载树结点的fn treeMultiLoaderFn, //批量加载树结
  • 利用webpack对代码进行分割是懒加载的前提,懒加载就是异步调用组件,需要时候才下载(按需加载)。为什么需要懒加载?在单页应用中,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要...
  • 组件更新的过程核心 新旧...Vue源码 深入响应式原理 (五)组件更新Vue源码 深入响应式原理 (五)组件更新组件更新新旧节点不同新旧节点相同updateChildren总结Vue源码学习目录 Vue源码 深入响应式原理 (五)组件更新
  • 注意:get请求方式的请求参数如果要封装成对象传递过去,那么需要将请求参数对象再次封装,格式要求为:{params:我们的请求参数对象},例如:{params:this.user} 底层原理:axios底层会获取get方法参数二中对象的...
  • 如果不采用异步更新的话,在每次更新数据都会对当前组件进行重新渲染,所以为了提高性能,vue会在本轮数据更新后,再去异步更新视图。 vue是组件级更新,当组件里边的数据变了,它就会去更新这个组件,当数据更改一...
  • vue的框架原理

    2021-03-30 11:48:31
    1、vue数据双向绑定原理 采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript ...
  • 这篇文章主要介绍了深入解读VUE中的异步渲染的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧 接下来在本文里一起看看当数据变化...
  • vue异步函数async和await的用法 一,异步函数async 1.1,async作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行。 ...
  • Vue的批量更新原理

    2021-08-09 00:03:16
    简单的代码开始:var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' ...
  • 先说一下async的用法,它作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行。 写一个async 函数 async function time...
  • vue promise用法 原理

    2021-09-17 14:54:35
    可以获取异步的消息,各种异步操作可以用同样的方式处理 promise有三种状态 不受外界影响 pending 进行中 fulfilled 已成功 rejected 已失败 优点 在异步流程中 把执行代码和处理结果的代码清晰的分离了,将异步操作以...
  • vue3 原理更新梳理

    2021-11-10 15:21:28
    Vue2 是响应式原理基于 Object.defineProperty 方法重定义对象的 getter 与 setter,vue3 则基于 Proxy 代理对象,拦截对象属性的访问与赋值过程。差异在于,前者并不能对诸如数组长度变化、增删元素操作、对象新增...
  • 总体渲染过程 组件初次渲染 模板在双向绑定监听只监听用到的值 更新过程 异步渲染
  • Vue模板编译原理

    2021-01-18 16:09:56
    模板是vue中最常用的部分, 是否了解其使用相关原理 模板编译不是html, 有指令, 插值, JS表达式, 到底是什么 可能会通过 组件渲染和更新过程 考察 从以下几个方面进行知识梳理 前置知识: JS的with语法 vue template...
  • 不论是计算属性,还是异步计算属性,都是依托于Vue3整体的响应式原理实现的。其核心依旧是ReacetEffect类。如果对响应式原理不清楚,建议先看响应式原理章节。计算属性和常规的动态响应区别在于它不会主动的去执行...
  • Vue模板编译原理详解

    2021-08-13 15:02:06
    Vue有自带编译器的版本和不带编译器的版本,即runtime +complier 和 runtime 版本。编译器的主要作用是将 .vue的模板编译为render函数,因为在开发的时候,写render函数不符合我们的开发习惯,所以我们平常开发用的...
  • Vue原理面试题

    2021-09-29 15:27:34
    传统组件,知识静态渲染,更新依赖于操作DOM 数据驱动视图 - Vue MVVM MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model代表数据模型,View代表UI组件,ViewModel是View和Model...
  • 提示下单成功 看一下打印 code 是我想要的效果了 原理就是经过一个promise函数,将每一次请求保价费的请求放到一个数组里,经过promise.all,去处理,而后在这个promise对面的resolve里面去执行批量下单的操做。
  • Vue.js是通过数据劫持以及结合发布者-订阅者来实现双向绑定的,数据劫持是利用ES5的Object.defineProperty(obj, key, val)来劫持各个属性的的setter以及getter,在数据变动时发布消息给订阅者,从而触发相应的回调来...
  • Vue异步执行DOM更新的 $nextTick()的原理是等组件的DOM更新完之后再执行回调函数;从而保证回到函数能操作到最新的数据更新后的DOM元素 第一步:子组件需要拿到父组件的ID,来渲染页面,父传子,自定义属性props:{} ...
  • vue是组件级更新,组件内有数据变化时,该组件就会更新。例:this.a = 1、this.b=2(同一个watcher) 每一个属性都有一个dep,每一个dep都有对应watcher
  • 当你把一个普通的JavaScript对象传给Vue实例的data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 18,619
精华内容 7,447
关键字:

vue异步更新原理

vue 订阅