vue源码之批量异步更新策略的深入解析

vue异步更新源码中会有涉及事件循环、宏任务、微任务的概念,所以先了解一下这几个概念。

一、事件循环、宏任务、微任务

1.事件循环Event Loop:浏览器为了协调事件处理、脚本执行、网络请求和渲染等任务而定制的工作机制。

2.宏任务Task: 代表一个个离散的、独立的工作单位。浏览器完成一个宏任务,在下一个宏任务开始执行之前,会对页面重新渲染。主要包括创建文档对象、解析HTML、执行主线JS代码以及各种事件如页面加载、输入、网络事件和定时器等。

3.微任务:微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务。如果存在微任务,浏览器会在完成微任务之后再重新渲染。微任务的例子有Promise回调函数、DOM变化等。

执行过程:执行完宏任务 => 执行微任务 => 页面重新渲染 => 再执行新一轮宏任务

任务执行顺序例子:

//第一个宏任务进入主线程
console.log('1');
//丢到宏事件队列中
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
//微事件1
process.nextTick(function() {
    console.log('6');
})
//主线程直接执行
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    //微事件2
    console.log('8')
})
//丢到宏事件队列中
setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

// 1,7,6,8,2,4,3,5,9,11,10,12

解析:

第一个宏任务

  1. 第一个宏任务进入主线程,打印1
  2. setTimeout丢到宏任务队列
  3. process.nextTick丢到微任务队列
  4. new Promise直接执行,打印7
  5. Promise then事件丢到微任务队列
  6. setTimeout丢到宏任务队列

第一个宏任务执行完,开始执行微任务

  1. 执行process.nextTick,打印6
  2. 执行Promise then事件,打印8

微任务执行完,清空微任务队列,页面渲染,进入下一个宏任务setTimeout

  1. 执行打印2
  2. process.nextTick丢到微任务队列
  3. new Promise直接执行,打印4
  4. Promise then事件丢到微任务队列

第二个宏任务执行完,开始执行微任务

  1. 执行process.nextTick,打印3
  2. 执行Promise then事件,打印5

微任务执行完,清空微任务队列,页面渲染,进入下一个宏任务setTimeout,重复上述类似流程,打印出9,11,10,12

二、Vue异步批量更新过程

1.解析:当侦测到数据变化,vue会开启一个队列,将相关的watcher存入队列,将回调函数存入callbacks队列,异步执行回调函数,遍历watcher队列进行渲染。

异步:Vue 在更新 DOM 时是异步执行的,只要侦听到数据变化,vue将开启一个队列,并缓冲  在同一事件循环中发生的所有数据  的变更。

批量:如果同一个watcher被多次触发,只会被推入到队列中一次。去重可以避免不必要的计算和DOM操作。然后在下一个的事件循环“tick”中,vue刷新队列执行实际工作。

异步策略:Vue的内部对异步队列尝试使用原生的Promise.then、MutationObserver和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。即会先尝试使用微任务方式,不行再用宏任务方式。

异步批量更新流程图:

三、vue批量异步更新源码

异步更新:整个过程相当于将臭袜子放到盆子里,最后一起洗。

1.当一个Data更新时,会依次执行以下代码:

(1)触发Data.set()

(2)调用dep.notify():遍历所有相关的Watcher,调用watcher.update()。

core/oberver/index.js:

notify () {
    const subs = this.subs.slice()
    // 如果未运行异步,则不会在调度程序中对sub进行排序
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 排序,确保它们按正确的顺序执行
      subs.sort((a, b) => a.id - b.id)
    }
    // 遍历相关watcher,并调用watcher更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
}

(3)执行watcher.update(): 判断是立即更新还是异步更新。若为异步更新,调用queueWatcher(this),将watcher入队,放到后面一起更新。

core/oberver/watcher.js:

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      //立即执行渲染
      this.run()
    } else {
      // watcher入队操作,后面一起执行渲染
      queueWatcher(this)
    }
}

(4)执行queueWatcher(this): watcher进行去重等操作后,添加到队列中,调用nextTick(flushSchedulerQueue)执行异步队列,传入回调函数flushSchedulerQueue。

core/oberver/scheduler.js:

function queueWatcher (watcher: Watcher) {
  // has 标识,判断该watcher是否已在,避免在一个队列中添加相同的 Watcher
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    // flushing 标识,处理 Watcher 渲染时,可能产生的新 Watcher。
    if (!flushing) {
      // 将当前 Watcher 添加到异步队列
      queue.push(watcher)
    } else {
      // 产生新的watcher就添加到排序的位置
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    // waiting 标识,让所有的 Watcher 都在一个 tick 内进行更新。
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      // 执行异步队列,并传入回调
      nextTick(flushSchedulerQueue)
    }
  }
}

(5)执行nextTick(cb): 将传进去的 flushSchedulerQueue 函数处理后添加到callbacks队列中,调用timerFunc启动异步执行任务。

core/util/next-tick.js:

function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 此处的callbacks就是队列(回调数组),将传入的 flushSchedulerQueue 方法处理后添加到回调数组
  callbacks.push(() => {
    if (cb) {
      try {
        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') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

(6)timerFunc():根据浏览器兼容性,选用不同的异步方式去执行flushCallbacks。由于宏任务耗费的时间是大于微任务的,所以先选用微任务的方式,都不行时再使用宏任务的方式,

core/util/next-tick.js:

let timerFunc

// 支持Promise则使用Promise异步的方式执行flushCallbacks
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 {
  // 实在不行再使用setTimeout的异步方式
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

(7)flushCallbacks:异步执行callbacks队列中所有函数

core/util/next-tick.js:

// 循环callbacks队列,执行里面所有函数flushSchedulerQueue,并清空队列
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

(8)flushSchedulerQueue():遍历watcher队列,执行watcher.run()

watcher.run():真正的渲染

function flushSchedulerQueue() {
  currentFlushTimestamp = getNow();
  flushing = true;
  let watcher, id;

  // 排序,先渲染父节点,再渲染子节点
  // 这样可以避免不必要的子节点渲染,如:父节点中 v -if 为 false 的子节点,就不用渲染了
  queue.sort((a, b) => a.id - b.id);

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  // 遍历所有 Watcher 进行批量更新。
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    // 真正的更新函数
    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;
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice();
  const updatedQueue = queue.slice();

  resetSchedulerState();

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit("flush");
  }
}

(9)updateComponent():watcher.run()经过一系列的转圈,执行updateComponent,updateComponent中执行render(),让组件重新渲染, 再执行_update(vnode) ,再执行 patch()更新界面。

(10)_update():根据是否有vnode分别执行不同的patch。

四、Vue.nextTick(callback)

1.Vue.nextTick(callback)作用:获取更新后的真正的 DOM 元素。

由于Vue 在更新 DOM 时是异步执行的,所以在修改data之后,并不能立刻获取到修改后的DOM元素。为了获取到修改后的 DOM元素,可以在数据变化之后立即使用 Vue.nextTick(callback)。

2.为什么 Vue.$nextTick 能够获取更新后的 DOM?

因为Vue.$nextTick其实就是调用 nextTick 方法,在异步队列中执行回调函数。

Vue.prototype.$nextTick = function (fn: Function) {
  return nextTick(fn, this);
};

3.使用 Vue.$nextTick

例子1:

<template>
  <p id="test">{{foo}}</p>
</template>
<script>

export default{
  data(){
    return {
      foo: 'foo'
    }
  },
  mounted() {
    let test  = document.querySelector('#test');
    this.foo = 'foo1';
    // vue在更新DOM时是异步进行的,所以此处DOM并未更新
    console.log('test.innerHTML:' + test.innerHTML);

    this.$nextTick(() => {
      // nextTick回调是在DOM更新后调用的,所以此处DOM已经更新
      console.log('nextTick:test.innerHTML:' + test.innerHTML);
    })
  }
}
</script>
执行结果:
test.innerHTML:foo
nextTick:test.innerHTML:foo1

例子2:

<template>
  <p id="test">{{foo}}</p>
</template>
<script>

export default{
  data(){
    return {
      foo: 'foo'
    }
  },
  mounted() {
    let test  = document.querySelector('#test');
    this.foo = 'foo1';
    // vue在更新DOM时是异步进行的,所以此处DOM并未更新
    console.log('1.test.innerHTML:' + test.innerHTML); 

    this.$nextTick(() => {
      // nextTick回调是在DOM更新后调用的,所以此处DOM已经更新
      console.log('nextTick:test.innerHTML:' + test.innerHTML);
    })

    this.foo = 'foo2';
    // 此处DOM并未更新,且先于异步回调函数前执行
    console.log('2.test.innerHTML:' + test.innerHTML);
  }
}
</script>
执行结果:
1.test.innerHTML:foo
2.test.innerHTML:foo
nextTick:test.innerHTML:foo2
 

例子3:

<template>
  <p id="test">{{foo}}</p>
</template>
<script>

export default{
  data(){
    return {
      foo: 'foo'
    }
  },
  mounted() {
    let test  = document.querySelector('#test');
    this.$nextTick(() => {
      // nextTick回调是在触发更新之前就放入callbacks队列,
      // 压根没有触发watcher.update以及以后的一系列操作,所以也就没有执行到最后的watcher.run()实行渲染
      // 所以此处DOM并未更新
      console.log('nextTick:test.innerHTML:' + test.innerHTML);
    })
    this.foo = 'foo1';
    // vue在更新DOM时是异步进行的,所以此处DOM并未更新
    console.log('1.test.innerHTML:' + test.innerHTML);
    this.foo = 'foo2';
    // 此处DOM并未更新,且先于异步回调函数前执行
    console.log('2.test.innerHTML:' + test.innerHTML);
  }
}
</script>
执行结果:
1.test.innerHTML:foo
2.test.innerHTML:foo
nextTick:test.innerHTML:foo

4、 nextTick与其他异步方法

nextTick是模拟的异步任务,所以可以用 Promise 和 setTimeout 来实现和 this.$nextTick 相似的效果。

例子1:

<template>
  <p id="test">{{foo}}</p>
</template>
<script>

export default{
  data(){
    return {
      foo: 'foo'
    }
  },
  mounted() {
    let test  = document.querySelector('#test');
    this.$nextTick(() => {
      // nextTick回调是在触发更新之前就放入callbacks队列,
      // 压根没有触发watcher.update以及以后的一系列操作,所以也就没有执行到最后的watcher.run()实行渲染
      // 所以此处DOM并未更新
      console.log('nextTick:test.innerHTML:' + test.innerHTML);
    })
    this.foo = 'foo1';
    // vue在更新DOM时是异步进行的,所以此处DOM并未更新
    console.log('1.test.innerHTML:' + test.innerHTML);
    this.foo = 'foo2';
    // 此处DOM并未更新,且先于异步回调函数前执行
    console.log('2.test.innerHTML:' + test.innerHTML); 

    Promise.resolve().then(() => {
      console.log('Promise:test.innerHTML:' + test.innerHTML);
    });
    setTimeout(() => {
        console.log('setTimeout:test.innerHTML:' + test.innerHTML);
    });
  }
}
</script>
执行结果:
1.test.innerHTML:foo
2.test.innerHTML:foo
nextTick:test.innerHTML:foo
Promise:test.innerHTML:foo2
setTimeout:test.innerHTML:foo2

例子2:

​
<template>
  <p id="test">{{foo}}</p>
</template>
<script>

export default{
  data(){
    return {
      foo: 'foo'
    }
  },
  mounted() {
    let test  = document.querySelector('#test');
    // Promise 和 setTimeout 依旧是等到DOM更新后再执行
    Promise.resolve().then(() => {
      console.log('Promise:test.innerHTML:' + test.innerHTML);
    });
    setTimeout(() => {
        console.log('setTimeout:test.innerHTML:' + test.innerHTML);
    });
    this.$nextTick(() => {
      // nextTick回调是在触发更新之前就放入callbacks队列,
      // 压根没有触发watcher.update以及以后的一系列操作,所以也就没有执行到最后的watcher.run()实行渲染
      // 所以此处DOM并未更新
      console.log('nextTick:test.innerHTML:' + test.innerHTML);
    })
    this.foo = 'foo1';
    // vue在更新DOM时是异步进行的,所以此处DOM并未更新
    console.log('1.test.innerHTML:' + test.innerHTML);
    this.foo = 'foo2';
    // 此处DOM并未更新,且先于异步回调函数前执行
    console.log('2.test.innerHTML:' + test.innerHTML);
  }
}
</script>
执行结果:
1.test.innerHTML:foo
2.test.innerHTML:foo
nextTick:test.innerHTML:foo
Promise:test.innerHTML:foo2
setTimeout:test.innerHTML:foo2

总结

到此这篇关于vue源码之批量异步更新策略的文章就介绍到这了,更多相关vue批量异步更新策略内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • vue 解决异步数据更新问题

    问题 记录一下出现的问题, 数据翻倍 这是复现问题的代码 data() { return { space: "", allresult: [] }; }, methods: { getmessage() { this.allresult = []; axios .get( "https://gist.githubusercontent.com/xiaolannuoyi/9b0defe4959e71fa97e6096cc4f82ba4/raw/4be939123d488cee7

  • 详解Vue的异步更新实现原理

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

  • 详解从Vue.js源码看异步更新DOM策略及nextTick

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:https://github.com/answershuto/learnVue. 在学习过程中,为Vue加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习Vue源码的小伙伴有所帮助. 可能会有理解存在偏差的地方,欢迎提issue指出,

  • vue源码之批量异步更新策略的深入解析

    vue异步更新源码中会有涉及事件循环.宏任务.微任务的概念,所以先了解一下这几个概念. 一.事件循环.宏任务.微任务 1.事件循环Event Loop:浏览器为了协调事件处理.脚本执行.网络请求和渲染等任务而定制的工作机制. 2.宏任务Task: 代表一个个离散的.独立的工作单位.浏览器完成一个宏任务,在下一个宏任务开始执行之前,会对页面重新渲染.主要包括创建文档对象.解析HTML.执行主线JS代码以及各种事件如页面加载.输入.网络事件和定时器等. 3.微任务:微任务是更小的任务,是在当前宏任务

  • vue中的任务队列和异步更新策略(任务队列,微任务,宏任务)

    目录 事件循环 任务队列 如何理解微任务和宏任务? 深究Vue异步更新策略原理 事件循环 JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事. 为了协调事件.用户交互.脚本.UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生. Event Loop 包含两类: 一类是基于 Browsing Context 一种是基于 Worker 二者的运行是独立的,也就是说,每一个 JavaScript 运行的"线程环境"都有一个独立的

  • vue源码解读子节点优化更新

    目录 前言 优化前存在的问题 优化策略分析 源码解析 小结 前言 Vue中更新节点,当新 VNode 和旧 VNode 都是元素节点且都有子节点时,Vue会循环对比新旧 VNode 的子节点数组,然后根据不同情况做不同处理. 虽然这种方法能解决问题,但是当更新子节点特别多时,循环算法的时间复杂度就会很高,所以Vue对此进行了优化. 优化前存在的问题 现在有新的 newChildren 数组和旧的 oldChildren 数组: newChildren = ['a','b','c','d']; o

  • vue 源码解析之虚拟Dom-render

    vue 源码解析 --虚拟Dom-render instance/index.js function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } renderMixin

  • Vue源码学习之响应式是如何实现的

    目录 前言 一.一个响应式系统的关键要素 1.如何监听数据变化 2.如何进行依赖收集--实现 Dep 类 3.数据变化时如何更新--实现 Watcher 类 二.虚拟 DOM 和 diff 1.虚拟 DOM 是什么? 2.diff 算法--新旧节点对比 三.nextTick 四.总结 前言 作为前端开发,我们的日常工作就是将数据渲染到页面➕处理用户交互.在 Vue 中,数据变化时页面会重新渲染,比如我们在页面上显示一个数字,旁边有一个点击按钮,每次点击一下按钮,页面上所显示的数字会加一,这要怎么

  • Vue源码学习记录之手写vm.$mount方法

    目录 一.概述 二.使用方式 三.完整版vm.$mount的实现原理 四.只包含运行时版本的vm.$mount的实现原理 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.概述 在我们开发中,经常要用到Vue.extend创建出Vue的子类来构造函数,通过new 得到子类的实例,然后通过$mount挂载到节点,如代码: <div id="mount-point"></div> <!-- 创建构造器 --> var Profile =

  • 浅析从vue源码看观察者模式

    观察者模式 首先话题下来,我们得反问一下自己,什么是观察者模式? 概念 观察者模式(Observer):通常又被称作为发布-订阅者模式.它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合. 讲个故事 上面对于观察者模式的概念可能会比较官方化,所以我们讲个故事来理解它. A:是共产党派往国民党密探,代号 001(发布者) B:是共产党的通信人员,负责与 A 进行秘密交接(订阅者) A 日常工作就是在明面采集

  • Vue 源码分析之 Observer实现过程

    导语: 本文是对 Vue 官方文档深入响应式原理(https://cn.vuejs.org/v2/guide/reactivity.html)的理解,并通过源码还原实现过程. 响应式原理可分为两步,依赖收集的过程与触发-重新渲染的过程.依赖收集的过程,有三个很重要的类,分别是 Watcher.Dep.Observer.本文主要解读 Observer . 这篇文章讲解上篇文章没有覆盖到的 Observer 部分的内容,还是先看官网这张图: Observer 最主要的作用就是实现了上图中touch

  • Vue源码解析之Template转化为AST的实现方法

    什么是AST 在Vue的mount过程中,template会被编译成AST语法树,AST是指抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式. Virtual Dom Vue的一个厉害之处就是利用Virtual DOM模拟DOM对象树来优化DOM操作的一种技术或思路. Vue源码中虚拟DOM构建经历 template编译成AST语法树 -> 再转换为render函数 最终返回一个VNode(VNod

  • 深入解析Vue源码实例挂载与编译流程实现思路详解

    在正文开始之前,先了解vue基于源码构建的两个版本,一个是 runtime only ,另一个是 runtime加compiler 的版本,两个版本的主要区别在于后者的源码包括了一个编译器. 什么是编译器,百度百科上面的解释是 简单讲,编译器就是将"一种语言(通常为高级语言)"翻译为"另一种语言(通常为低级语言)"的程序.一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) →

随机推荐