双十一,打包半价理解Vue的nextTick与watcher以及Dep的蓝色生死恋?

Vue之nextTick理解

前言

一开始就只想搞清楚nextTick的一个原理,谁知道,跟吃了辣条一下,停不下来,从nextTick的源码到Watcher源码再到Dep源码,震惊,而后再结合本身以前看掘金小册理解的双向绑定-响应式系统,感受有一种顿悟的感受,总之,这是我我的的理解,请大佬们指教,若有转载,请附上原文连接,毕竟我copy源码也挺累的~javascript

多说一句话

由于这篇文章,有挺多源代码的,通常来讲,换做是我,我也会一扫而过,一目十行,可是笔者我!真心!但愿!大家可以耐住性子!去看!源码中,会有一丢丢注释必定要看尤大大做者给的注释html

若是有什么地方写错了,恳请大佬们指教,互相进步~前端

请开始你的表演

那么怎么说nextTick呢?该从何提及,怪难为情的,仍是让咱们先来看个例子吧vue

<template>
    <div>
      <div ref="username">{{ username }}</div>
      <button @click="handleChangeName">click</button>
    </div>
  </template>
复制代码
export default {
    data () {
      return {
        username: 'PDK'
      }
    },
    methods: {
      handleChangeName () {
        this.username = '彭道宽'
        console.log(this.$refs.username.innerText) // PDK
      }
    }
  }
复制代码

震惊!!!,打印出来的竟然的 "PDK",怎么回事,我明明修改了username,将值赋为"彭道宽",为何仍是打印以前的值,而真实获取到DOM结点的innerText并无获得预期中的“彭道宽”, 为啥子 ?java

不方,咱们再看一个例子,请看:react

export default {
    data () {
      return {
        username: 'PDK',
        age: 18
      }
    },
    mounted() {
      this.age = 19
      this.age = 20
      this.age = 21
    },
    watch: {
      age() {
        console.log(this.age)
      }
    }
  }
复制代码

这段脚本执行咱们猜想会依次打印:19,20,21。可是实际效果中,只会输出一次:21。为何会出现这样的状况?ios

事不过三,因此咱们再来看一个例子git

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

在点击click触发handleClick()事件以后,number会被遍历增长10000次,在vue的双向绑定-响应式系统中,会通过 “setter -> Dep -> Watcher -> patch -> 视图” 这个流水线。那么是否是能够这么理解,每次number++,都会通过这个“流水线”来修改真实的DOM,而后DOM被更新了10000次。github

可是身为一位“资深”的前端小白来讲,都知道,前端对性能的看中,而频繁的操做DOM,那但是一大“忌讳”啊。Vue.js 确定不会以如此低效的方法来处理。Vue.js在默认状况下,每次触发某个数据的 setter 方法后,对应的 Watcher 对象其实会被 push 进一个队列 queue 中,在下一个 tick 的时候将这个队列 queue 所有拿出来 run一遍。这里咱们看看Vue官网的描述 : Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的全部数据改变。若是同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做上很是重要。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工做。express

Vue在修改数据的时候,不会立马就去修改数据,例如,当你设置 vm.someData = 'new value' ,该组件不会当即从新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个 tick 更新, 为了在数据变化以后等待 Vue 完成更新 DOM ,能够在数据变化以后当即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用,下边来自Vue官网中的例子 :

<div id="example">{{message}}</div>
复制代码
var vm = new Vue({
    el: '#example',
    data: {
      message: '123'
    }
  })
  vm.message = 'new message' // 更改数据
  console.log(vm.$el.textContent === 'new message') // false, message还未更新

  Vue.nextTick(function () {
    console.log(vm.$el.textContent === 'new message') // true, nextTick里面的代码会在DOM更新后执行
  })
复制代码

下一个tick是什么鬼玩意 ?

上面一直扯扯扯,那么到底什么是 下一个tick

nextTick函数其实作了两件事情,一是生成一个timerFunc,把回调做为microTask或macroTask参与到事件循环中来。二是把回调函数放入一个callbacks队列,等待适当的时机执行

nextTick在官网当中的定义:

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

在 Vue 2.4 以前都是使用的 microtasks(微任务),可是 microtasks 的优先级太高,在某些状况下可能会出现比事件冒泡更快的状况,但若是都使用 macrotasks(宏任务) 又可能会出现渲染的性能问题。因此在新版本中,会默认使用 microtasks,但在特殊状况下会使用 macrotasks。好比 v-on。对于不知道JavaScript运行机制的,能够去看看阮一峰老师的JavaScript 运行机制详解:再谈Event Loop、又或者看看个人Event Loop

哎呀妈,又扯远了,回到正题,咱们先去看看vue中的源码 :

/* @flow */
  /* globals MessageChannel */

  import { noop } from 'shared/util'
  import { handleError } from './error'
  import { isIOS, isNative } from './env'

  const callbacks = []  // 定义一个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 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 microTimerFunc
  let macroTimerFunc
  let 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
      })
    }
  }
复制代码

来来来,咱们仔细的扯一扯~

首先由于目前浏览器平台并无实现 nextTick 方法,因此 Vue.js 源码中分别用 PromisesetTimeoutsetImmediate 等方式在 microtask(或是macrotasks)中建立一个事件,目的是在当前调用栈执行完毕之后(不必定当即)才会去执行这个事件

对于实现 macrotasks ,会先判断是否能使用 setImmediate ,不能的话降级为 MessageChannel ,以上都不行的话就使用 setTimeout 注意,是对实现宏任务的判断

问题来了?为何要优先定义 setImmediateMessageChannel 建立,macroTasks而不是 setTimeout 呢?

HTML5中规定setTimeout的最小时间延迟是4ms,也就是说理想环境下异步回调最快也是4ms才能触发。Vue使用这么多函数来模拟异步任务,其目的只有一个,就是让回调异步且尽早调用。而 MessageChannelsetImmediate 的延迟明显是小于 setTimeout

// 是否可使用 setImmediate
  if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    macroTimerFunc = () => {
      setImmediate(flushCallbacks)
    }
  } else if (typeof MessageChannel !== 'undefined' && (
    isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]'
  )) { // 是否可使用 MessageChannel
    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = flushCallbacks
    macroTimerFunc = () => {
      port.postMessage(1) // 利用消息管道,经过postMessage方法把1传递给channel.port2
    }
  } else {
    /* istanbul ignore next */
    macroTimerFunc = () => {
      setTimeout(flushCallbacks, 0)  // 利用setTimeout来实现
    }
  }
复制代码

setImmediateMessageChannel 都不行的状况下,使用 setTimeout,delay = 0 以后,执行flushCallbacks(),下边是flushCallbacks的代码

// setTimeout 会在 macrotasks 中建立一个事件 flushCallbacks ,flushCallbacks 则会在执行时将 callbacks 中的全部 cb 依次执行。
  function flushCallbacks () {
    pending = false
    const copies = callbacks.slice(0)
  
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }
复制代码

前面说了,nextTick 同时也支持 Promise 的使用,会判断是否实现了 Promise

export function nextTick (cb?: Function, ctx?: Object) {
    let _resolve
    // 将回调函数整合至一个数组,推送到队列中下一个tick时执行
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')
        }
      } else if (_resolve) {
        _resolve(ctx)
      }
    })
    if (!pending) { // pengding = false的话,说明不须要不存在,尚未timerFunc被推送到任务队列中
      pending = true
      if (useMacroTask) {
        macroTimerFunc() // 执行宏任务
      } else {
        microTimerFunc()  // 执行微任务
      }
    }

    // 判断是否可使用 promise
    // 能够的话给 _resolve 赋值
    // 回调函数以 promise 的方式调用
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(resolve => {
        _resolve = resolve
      })
    }
  }
复制代码

你觉得这就结束了?

ok,上边nextTick的源码比较少,看得大概大概的了,可是呢,仍是很懵,因此我又去github看了一下watcher.js的源码,回到开头的第三个例子,就是那个循环10000次的那个小坑逼,来,咱们看下源码再说,源码里的代码太多,我挑着copy,嗯,凑合看吧

import {
    warn,
    remove,
    isObject,
    parsePath,
    _Set as Set,
    handleError, 
    noop
  } from '../util/index'

  import { traverse } from './traverse'
  import { queueWatcher } from './scheduler'                // 这个很也重要,眼熟它
  import Dep, { pushTarget, popTarget } from './dep'  // 眼熟这个,这个是将 watcher 添加到 Dep 中,去看看源码

  import type { SimpleSet } from '../util/index'

  let uid = 0   // 这个也很重要,眼熟它

  /** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */
  export default class Watcher {
    // 其中的一些我也不知道,我只能从字面上理解,若有大佬,请告知一声
    vm: Component;
    expression: string;
    cb: Function;
    id: number;
    deep: boolean;
    user: boolean;
    lazy: boolean; 
    sync: boolean;
    dirty: boolean;
    active: boolean;
    deps: Array<Dep>;
    newDeps: Array<Dep>;
    depIds: SimpleSet;
    newDepIds: SimpleSet;
    before: ?Function;
    getter: Function;
    value: any;
    ...
    constructor (
      vm: Component,
      expOrFn: string | Function,
      cb: Function,
      options?: ?Object,                // 咱们的options
      isRenderWatcher?: boolean
    ) { 
      this.vm = vm
      if (isRenderWatcher) {
        vm._watch = this
      }
      vm._watchers.push(this)
      // options
      if (options) {
        this.deep = !!options.deep
        this.user = !!options.user
        this.lazy = !!options.lazy
        this.sync = !!options.sync
        this.before = options.before
      } else {
        this.deep = this.user = this.lazy = this.sync = false
      }
      this.cb = cb
      this.id = ++uid       // 看到没有,咱们相似于给每一个 Watcher对象起个名字,用id来标记每个Watcher对象
      this.active = true
      this.dirty = this.lazy 
      this.deps = []
      this.newDeps = []
      this.depIds = new Set()
      this.newDepIds = new Set()
      this.expression = process.env.NODE_ENV !== 'production'
        ? expOrFn.toString()
        : ''
      // parse expression for getter
      if (typeof expOrFn === 'function') {
        this.getter = expOrFn
      } else {
        this.getter = parsePath(expOrFn)
        if (!this.getter) {
          this.getter = noop
          process.env.NODE_ENV !== 'production' && warn(
            `Failed watching path: "${expOrFn}" ` +
            'Watcher only accepts simple dot-delimited paths. ' +
            'For full control, use a function instead.',
            vm
          )
        }
      }
      this.value = this.lazy
        ? undefined
        : this.get()  // 执行get()方法
    }

    get () {
      pushTarget(this) // 调用Dep中的pushTarget()方法,具体源码下边贴出
      let value
      const vm = this.vm
      try {
        value = this.getter.call(vm, vm) 
      } catch (e) {
        if (this.user) {
          handleError(e, vm, `getter for watcher "${this.expression}"`)
        } else {
          throw e
        }
      } finally {
        if (this.deep) {
          traverse(value)
        }
        popTarget() // 调用Dep中的popTarget()方法,具体源码下边贴出
        this.cleanupDeps()
      }
      return value
    }

    // 添加到dep中
    addDep(dep: Dep) {
      const id = dep.id // Dep 中,存在一个id和subs数组(用来存放全部的watcher)
      if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
          dep.addSub(this) // 调用dep.addSub方法,将这个watcher对象添加到数组中
        }
      }
    }

    ...

    update () {
      if (this.lazy) {
        this.dirty = true 
      } else if (this.sync) {
        this.run()
      } else {
        queueWatcher(this) // queueWatcher()方法,下边会给出源代码
      }
    }

    run () {
      if (this.active) {
        const value = this.get()
        if (
          value !== this.value ||
          // 看英文注释啊!!!很清楚了!!!
          // Deep watchers and watchers on Object/Arrays should fire even
          // when the value is the same, because the value may
          isObject(value) ||
          this.deep
        ) {
          // set new value
          const oldValue = this.value
          this.value = value
          if (this.user) {
            try {
              this.cb.call(this.vm, value, oldValue)
            } catch (e) {
              handleError(e, this.vm, `callback for watcher "${this.expression}"`)
            }
          } else {
            this.cb.call(this.vm, value, oldValue) // 回调函数
          }
        }
      }
    }

    ...

  }
复制代码

太长了?染陌大佬的《剖析 Vue.js 内部运行机制》中给出了一个简单而有利于理解的代码(群主,我不是打广告的,别踢我)

let uid = 0;

  class Watcher {
    constructor () {
      this.id = ++uid;
    }

    update () {
      console.log('watch' + this.id + ' update');
      queueWatcher(this);
    }

    run () {
      console.log('watch' + this.id + '视图更新啦~');
    }
  }
复制代码

queueWatcher 是个什么鬼

够抽象吧!再看看这个代码,比较一看,你会发现,都出现了一个 queueWatcher的玩意,因而我去把源码也看了一下。下边是它的源代码(选择copy)

import {
    warn,
    nextTick,                       // 看到没有,咱们一开始要讲的老大哥出现了!!!!
    devtools
  } from '../util/index'

  export const MAX_UPDATE_COUNT = 100

  /** * Flush both queues and run the watchers. */
  function flushSchedulerQueue () {
    flushing = true
    let watcher, id

    // Sort queue before flush.
    // This ensures that:
    // 1. Components are updated from parent to child. (because parent is always
    // created before the child)
    // 2. A component's user watchers are run before its render watcher (because
    // user watchers are created before the render watcher)
    // 3. If a component is destroyed during a parent component's watcher run,
    // its watchers can be skipped.
    queue.sort((a, b) => a.id - b.id)

    // do not cache length because more watchers might be pushed
    // as we run existing watchers
    for (index = 0; index < queue.length; index++) {
      watcher = queue[index]
      if (watcher.before) {
        watcher.before()
      }
      id = watcher.id
      has[id] = null
      watcher.run()     // watcher对象调用run方法执行
      // in dev build, check and stop circular updates.
      if (process.env.NODE_ENV !== 'production' && has[id] != null) {
        circular[id] = (circular[id] || 0) + 1
        if (circular[id] > MAX_UPDATE_COUNT) {
          warn(
            'You may have an infinite update loop ' + (
              watcher.user
                ? `in watcher with expression "${watcher.expression}"`
                : `in a component render function.`
            ),
            watcher.vm
          )
          break
        }
      }
    }
    
    ... 
  }  
  /** * 看注释看注释!!!!!! * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the queue is being flushed. */
  export function queueWatcher (watcher: Watcher) {
    const id = watcher.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 = true  // 标志位,它保证flushSchedulerQueue回调只容许被置入callbacks一次。

        if (process.env.NODE_ENV !== 'production' && !config.async) {
          flushSchedulerQueue()
          return
        }
        nextTick(flushSchedulerQueue)  // 看到没有,调用了nextTick
        // 这里面的nextTick(flushSchedulerQueue)中的flushSchedulerQueue函数其实就是watcher的视图更新。
        // 每次调用的时候会把它push到callbacks中来异步执行。
      }
    }
  }
复制代码

Dep

哎呀妈,咱们再来看看Dep中的源码

import type Watcher from './watcher'          // 眼熟它
  import { remove } from '../util/index'
  import config from '../config'

  let uid = 0

  /** * A dep is an observable that can have multiple * directives subscribing to it. */
  export default class Dep {
    static target: ?Watcher;
    id: number;
    subs: Array<Watcher>;

    constructor () {
      this.id = uid++
      this.subs = []
    }

    // 将全部的watcher对象添加到数组中
    addSub (sub: Watcher) {
      this.subs.push(sub)
    }

    removeSub (sub: Watcher) {
      remove(this.subs, sub)
    }

    depend () {
      if (Dep.target) {
        Dep.target.addDep(this)
      }
    }

    notify () {
      // stabilize the subscriber list first
      const subs = this.subs.slice()
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        // subs aren't sorted in scheduler if not running async
        // we need to sort them now to make sure they fire in correct
        // order
        subs.sort((a, b) => a.id - b.id)
      }

      // 经过循环,来调用每个watcher,而且 每一个watcher都有一个update()方法,通知视图更新
      for (let i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
      }
    }
  }

  // the current target watcher being evaluated.
  // this is globally unique because there could be only one
  // watcher being evaluated at any time.
  Dep.target = null
  const targetStack = []

  export function pushTarget (_target: ?Watcher) {
    if (Dep.target) targetStack.push(Dep.target)
    Dep.target = _target
  }

  export function popTarget () {
    Dep.target = targetStack.pop()
  }

  // 说白了,在数据【依赖收集】过程就是把 Watcher 实例存放到对应的 Dep 对象中去
  // 这时候 Dep.target 已经指向了这个 new 出来的 Watcher 对象
  // get 方法可让当前的 Watcher 对象(Dep.target)存放到它的 subs 数组中
  // 在数据变化时,set 会调用 Dep 对象的 notify 方法通知它内部全部的 Watcher 对象进行视图更新。
复制代码

最后在扯两句

真的是写这篇文章,花了一下午,也在掘金找了一些文章,可是都不够详细,而且不少时候,感受不少文章都是千篇一概,借鉴了别人的理解,而后本身同时看染陌大佬的讲解,以及本身去看了源码,才大概看懂,果真,看的文章再多,还不如去看源码来的实在!!!

友情连接

《个人博客》: github.com/PDKSophia/b…

《剖析 Vue.js 内部运行机制》: juejin.im/book/5a3666…

《Vue官网之异步更新队列》: cn.vuejs.org/v2/guide/re…

《MessageChannel API》: developer.mozilla.org/zh-CN/docs/…

《Vue中DOM的异步更新策略以及nextTick机制》: funteas.com/topic/5a8dc…

《Vue.js 源码之nextTick》: github.com/vuejs/vue/b…

《Vue.js 源码之Watcher》: github.com/vuejs/vue/b…

《Vue.js 源码之Dep》: github.com/vuejs/vue/b…

相关文章
相关标签/搜索