关于vue.js:每日一题之Vue的异步更新实现原理是怎样的

32次阅读

共计 4013 个字符,预计需要花费 11 分钟才能阅读完成。

最近面试总是会被问到这么一个问题:在应用 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

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 函数一次性解决。参考 前端 vue 面试题具体解答

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 语法去处理事件,十分不便。

正文完
 0