• 事件循环&nextTick原理&异步渲染


    1.事件循环机制

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

    任务队列

    一. 分类
    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
    js事件循环

    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),直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有的需要渲染变化的部分合并起来,执行一次渲染操作,具体步骤如下图
    异步渲染

    上面的例子可以用异步渲染进行深入,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

  • 相关阅读:
    Java中的HashMap
    单机百万连接调优和Netty应用级别调优
    简单排序(冒泡排序,插入排序,选择排序)
    使用AC自动机解决文章匹配多个候选词问题
    树状数组解决数组单点更新后快速查询区间和的问题
    LeetCode 763. Partition Labels
    LeetCode 435. Non-overlapping Intervals
    线段树
    无序数组求第K大的数
    KMP算法解决字符串匹配问题
  • 原文地址:https://www.cnblogs.com/aeipyuan/p/12638597.html
Copyright © 2020-2023  润新知