天天看点

事件循环&nextTick原理&异步渲染1.事件循环机制2.nextTick3.异步渲染

1.事件循环机制

众所周知,js是单线程的,即任务是串行的,后一个任务需要等待前一个任务的执行,这就可能出现长时间的等待。但由于类似ajax网络请求、setTimeout时间延迟、DOM事件的用户交互等,这些任务并不消耗 CPU,是一种空等,资源浪费,因此出现了异步。通过将任务交给相应的异步模块去处理,主线程的效率大大提升,可以并行的去处理其他的操作。当异步处理完成,主线程空闲时,主线程读取相应的callback任务队列,进行后续的操作,最大程度的利用CPU。

事件循环&nextTick原理&异步渲染1.事件循环机制2.nextTick3.异步渲染

任务队列

一. 分类

1.microtask queue:唯一,整个事件循环当中,仅存在一个;执行为同步,同一个事件循环中的microtask会按队列顺序,串行执行完毕;

典型:Promise、Object.observe、MutationObserver

2.macrotask queue:不唯一,存在一定的优先级(用户I/O部分优先级更高);异步执行,同一事件循环中,只执行一个。

典型:整体代码script,setTimeout,setInterval、I/O、UI render

二. 流程

如下图,初始化运行script是一个宏任务,此过程中出现的新的宏任务setTimeout被放到macrotask队列,微任务Promise.then放置到microtask队列,并且将比setTimeout优先执行,如果Promise.then执行时又产生了微任务,微任务将在插入当前微任务队列下,直到所有微任务队列执行完毕才会开始执行setTimeout(在Promise.then加入死循环页面将卡住,一直停留在微任务)

参考视频:https://www.bilibili.com/video/BV1VE411u7Xx?t=602

事件循环&nextTick原理&异步渲染1.事件循环机制2.nextTick3.异步渲染

2.nextTick

原理

简化来说,nextTick的作用就是将一堆任务放到一个异步函数中,当主线程代码全部执行完就将这些任务按照执行,根据浏览器兼容性的不同,nextTick选用了四种异步api,优先级(Promise > MutationObserver > setImmediate > setTimeout)前两种是微任务,后两种是宏任务,根据以上任务队列的知识可知,nextTick为了更快执行,首先选用微任务,只有当浏览器不兼容才会采取宏任务方式,这是Vue2.6.11版本的源码,之前的版本对dom操作事件强行使用宏任务api。

/* 执行回调队列的任务 */
function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
           
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) {/* 上一个callback数组已经清空,任务已经作为同步代码在执行了 */
    pending = true
    timerFunc()/* 将flushCallbacks放到到任务队列中 */
  }
  if (!cb && typeof Promise !== 'undefined') {/* 没有cb则返回Promise实例*/
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
           

先举个例子,看下面输出:

const $name = this.$refs.name
  this.$nextTick(() => console.log('setter前:' + $name.innerHTML))
  this.name = ' name改喽 '
  console.log('数据1:' + this.name);
  console.log(' 同步方式setter后:' + this.$refs.name.innerHTML)
  setTimeout(() => console.log("setTimeout方式:" + this.$refs.name.innerHTML))
  this.$nextTick(() => console.log('setter后:' + $name.innerHTML))
  /* 结果:
  数据1: name改喽                            
  同步方式setter后:SHERlocked93                
  setter前:SHERlocked93                         
  setter后: name改喽                    
  setTimeout方式: name改喽                      
   */
           

原因:

第1-2行:

数据更改是同步的,dom更改操作异步且此时还没有在主线程执行,所以输出为更改后的数据和更改前的dom。

setter前:

nextTick的第一个任务,dom更改任务在它之后。

setter后:

在此之前,name进行了更新,然后会有一个触发页面渲染的回调被加入到nextTick的callbacks中,然后setter后的输出函数也将加入,当主线程任务未执行完时,callback任务数组=[setter前输出、页面更新函数、setter后输出]。

setTimeout

由于setTimeout是宏任务并且在代码顺序后面,不管nextTick使用的是微任务api还是宏任务api,都将在前面执行。

3.异步渲染

在Vue中异步渲染实际在数据每次变化时,将其所要引起页面变化的部分都放到一个异步API的回调函数里(Promise、MutationObserver、setImmidiate、setTimeout),直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有的需要渲染变化的部分合并起来,执行一次渲染操作,具体步骤如下图

事件循环&amp;nextTick原理&amp;异步渲染1.事件循环机制2.nextTick3.异步渲染

上面的例子可以用异步渲染进行深入,name进行了更新(数据是同步更新),会触发name对应的Object.defineProperty里面的set()函数,然后通过dep.notify通知name的所有订阅者watcher执行update,watcher会根据设置的sync属性确定是否直接执行更新,默认sync=false

update() {
   if (this.lazy) {
   this.dirty = true
   } else if (this.sync) {
   this.run()
   } else {
   queueWatcher(this)
   }
}
           

然后调用queueWatcher对这个watcher添加到全局数组queue里并且进行处理,当不是waiting态的时候nextTick传递flushSchedulerQueue(更新页面函数)任务,将此任务存入callback数组,利用异步API执行这个函数。

export function queueWatcher(watcher: Watcher) {
  const id = watcher.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
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
           

轮到flushSchedulerQueue执行时对之前存入的queue进行排序,然后逐个调用渲染的run函数,run函数调用get函数,get函数将实例watcher对象push到全局数组中,开始调用实例的getter方法,执行完毕后,将watcher对象从全局数组弹出,并且清除已经渲染过的依赖实例。

function flushSchedulerQueue () 
  var watcher, id;
  // 安装id从小到大开始排序,越小的越前触发的update
  queue.sort(function (a, b) { return a.id - b.id; });
  // queue是全局数组,它在queueWatcher函数里,每次update触发的时候将当时的watcher,push进去
  for (index = 0; index < queue.length; index++) {
    ...
    watcher.run();  // 渲染
    ...
  }
}
           
Watcher.prototype.get = function get () {
  pushTarget(this); // 将实例push到全局数组targetStack
  var vm = this.vm;
  value = this.getter.call(vm, vm);
  ...
}
           

getter方法实际是在实例化的时候传入的函数,也就是下面vm的真正更新函数_update,_update函数执行后,将会把两次的虚拟节点传入vm的patch方法执行渲染操作。

function () {
  vm._update(vm._render(), hydrating);
};
           
Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    ...
    var prevVnode = vm._vnode;
    vm._vnode = vnode;
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
    ...
  };
           

参考:https://blog.csdn.net/qq_27053493/article/details/105213003

继续阅读