Skip to content

Latest commit

 

History

History
751 lines (640 loc) · 25.8 KB

Vue中nextTick与watcher以及Dep的原理.md

File metadata and controls

751 lines (640 loc) · 25.8 KB

Vue 之 nextTick 理解

前言

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

多说一句话

因为这篇文章,有挺多源代码的,一般来说,换作是我,我也会一扫而过,一目十行,但是笔者我!真心!希望!你们能够耐住性子!去看!源码中,会有一丢丢注释一定要看尤大大作者给的注释

如果有什么地方写错了,恳请大佬们指教,互相进步~

请开始你的表演

那么怎么说 nextTick 呢?该从何说起,怪难为情的,还是让我们先来看个例子吧

<template>
  <div>
    <div ref="usernmae">{{ 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 并没有得到预期中的“彭道宽”, 为啥子 ?

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

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。为什么会出现这样的情况?

事不过三,所以我们再来看一个例子

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 次。

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

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 对象进行视图更新。

最后在扯两句

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

友情链接

《我的博客》: https://github.com/PDKSophia/blog.io

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

《Vue 官网之异步更新队列》: https://cn.vuejs.org/v2/guide/reactivity.html#%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E9%98%9F%E5%88%97

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

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

《Vue.js 源码之 nextTick》: https://github.com/vuejs/vue/blob/dev/src/core/util/next-tick.js

《Vue.js 源码之 Watcher》: https://github.com/vuejs/vue/blob/dev/src/core/observer/watcher.js#L31

《Vue.js 源码之 Dep》: https://github.com/vuejs/vue/blob/dev/src/core/observer/dep.js