【vue2.x原理剖析八】异步更新原理

86 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情

前言

源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大

定义

nextTick定义的有两处地方,所以可以通过Vue.nextTickthis.$nextTick两种方式调用

// src/core/global-api/index.js
Vue.nextTick = nextTick;
// src/core/instance/render.js
Vue.prototype.$nextTick = function (fn: Function) {
  return nextTick(fn, this);
};

nextTick实现

首先是嗅探环境,依次检测Promise.then(微任务)->MutationObserver(微任务)->setImmediate(宏任务IE)->setTimeout(宏任务),依次去找,如果存在就使用它来异步执行

// src/core/util/next-tick.js
export let isUsingMicroTask = false;
// 存放异步调用任务
const callbacks = [];
let pending = false;

function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  // 循环执行队列中的任务
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}
// 嗅探环境
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) ||
    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);
  };
}

export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve;
  callbacks.push(() => {
    if (cb) {
      // 为了保证队列中有出错的任务不会影响到其他任务执行,故采用try...catch的形式
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  // 确保其中的逻辑只执行一次
  if (!pending) {
    pending = true;
    timerFunc();
  }
  // 如果有Promise,则nextTick支持then写法  this.$nextTick().then()
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}

watcher 中的异步更新

在更新时,可能更改了多次,但是多次的更改调用的是同一个watcher,所以先将其去重后缓存起来,统一做更新,如此一来就不用更新多次了

// src/core/observer/watcher.js
update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
  // 异步队列机制
    queueWatcher(this)
  }
}
// src/core/observer/scheduler.js
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // beforeUpdate生命周期
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run() // run更新视图 调用回调等
  }
  ...
}
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // watcher去重,如果没有就加入
  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)
    }
    // 变量防抖,初次执行,之后不执行,在将watcher添加完之后,会异步执行nextTick执行watcher的run方法
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

注意点

  • nextTick有微任务,微任务是在渲染前执行,为什么还可以拿到最新的$el---取得是内存中的$el
  • 如果在赋值之前执行this.$nextTick再赋值,此时拿到的是旧的值,因为nextTick是一个队列

总结

nextTick把要执行的任务推入到一个队列中,首先是嗅探环境,依次检测Promise.then->MutationObserver->setImmediate->setTimeout,依次去找,如果存在就使用它来异步执行。watcher中的更新也是异步更新,会将watcher放入队列中,之后调用nextTick来异步执行。