最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手写 Vue2 系列 之 异步更新队列

    正文概述 掘金(李永宁)   2021-07-15   383

    前言

    上一篇文章 手写 Vue 系列 之 computed 实现了 Vue 的 computed 计算属性。

    目标

    本篇文章是 手写 Vue 系列 的最后一篇,实现 Vue 的异步更新队列。

    读过源码,相信大家都知道 Vue 异步更新的大概流程:依赖收集结束之后,当响应式数据发生变化 -> 触发 setter 执行 dep.notify -> 让 dep 通知 自己收集的所有 watcher 执行 update 方法 -> watch.update 调用 queueWatcher 将自己放到 watcher 队列 -> 接下来调用 nextTick 方法将刷新 watcher 队列的方法放到 callbacks 数组 -> 然后将刷新 callbacks 数组的方法放到浏览器的异步任务队列 -> 待将来执行时最终触发 watcher.run 方法,执行 watcher.get 方法。

    实现

    接下来会完整实现 Vue 的异步更新队列,让你彻底理解 Vue 的异步更新过程都发生了什么。

    Watcher

    // 用来标记 watcher
    let uid = 0
    
    **
     * @param {*} cb 回调函数,负责更新 DOM 的回调函数
     * @param {*} options watcher 的配置项
     */
    export default function Watcher(cb, options = {}, vm = null) {
      // 标识 watcher
      this.uid = uid++
      // ...
    }
    
    

    watcher.update

    /**
     * 响应式数据更新时,dep 通知 watcher 执行 update 方法,
     * 让 update 方法执行 this._cb 函数更新 DOM
     */
    Watcher.prototype.update = function () {
      if (this.options.lazy) { // 懒执行,比如 computed 计算属性
        // 将 dirty 置为 true,当页面重新渲染获取计算属性时就可以执行 evalute 方法获取最新的值了
        this.dirty = true
      } else {
        // 将 watcher 放入异步 watcher 队列
        queueWatcher(this)
      }
    }
    
    

    watcher.run

    /**
     * 由刷新 watcher 队列的函数调用,负责执行 watcher.get 方法
     */
    Watcher.prototype.run = function () {
      this.get()
    }
    
    

    异步更新队列

    /**
     * 异步更新队列
     */
    
    // 存储本次更新的所有 watcher
    const queue = []
    
    // 标识现在是否正在刷新 watcher 队列
    let flushing = false
    // 标识,保证 callbacks 数组中只会有一个刷新 watcher 队列的函数
    let waiting = false
    // 存放刷新 watcher 队列的函数,或者用户调用 Vue.nextTick 方法传递的回调函数
    const callbacks = []
    // 标识浏览器当前任务队列中是否存在刷新 callbacks 数组的函数
    let pending = false
    
    

    queueWatcher

    /**
     * 将 watcher 放入队列
     * @param {*} watcher 待会儿需要被执行的 watcher,包括渲染 watcher、用户 watcher、computed
     */
    export function queueWatcher(watcher) {
      if (!queue.includes(watcher)) { // 防止重复入队
        if (!flushing) { // 现在没有在刷新 watcher 队列
          queue.push(watcher)
        } else { // 正在刷新 watcher 队列,比如用户 watcher 的回调函数中更改了某个响应式数据
          // 标记当前 watcher 在 for 中是否已经完成入队操作
          let flag = false
          // 这时的 watcher 队列时有序的(uid 由小到大),需要保证当前 watcher 插入进去后仍然有序
          for (let i = queue.length - 1; i >= 0; i--) {
            if (queue[i].uid < watcher.uid) { // 找到了刚好比当前 watcher.uid 小的那个 watcher 的位置
              // 将当前 watcher 插入到该位置的后面
              queue.splice(i + 1, 0, watcher)
              flag = true
              break;
            }
          }
          if (!flag) { // 说明上面的 for 循环在队列中没找到比当前 watcher.uid 小的 watcher
            // 将当前 watcher 插入到队首 
            queue.unshift(watcher)
          }
        }
        if (!waiting) { // 表示当前 callbacks 数组中还没有刷新 watcher 队列的函数
          // 保证 callbacks 数组中只会有一个刷新 watcher 队列的函数
          // 因为如果有多个,没有任何意义,第二个执行的时候 watcher 队列已经为空了
          waiting = true
          nextTick(flushSchedulerQueue)
        }
      }
    }
    
    

    flushSchedulerQueue

    /**
     * 负责刷新 watcher 队列的函数,由 flushCallbacks 函数调用
     */
    function flushSchedulerQueue() {
      // 表示正在刷新 watcher 队列
      flushing = true
      // 给 watcher 队列排序,根据 uid 由小到大排序
      queue.sort((a, b) => a.uid - b.uid)
      // 遍历队列,依次执行其中每个 watcher 的 run 方法
      while (queue.length) {
        // 取出队首的 watcher
        const watcher = queue.shift()
        // 执行 run 方法
        watcher.run()
      }
      // 到这里 watcher 队列刷新完毕
      flushing = waiting = false
    }
    
    

    nextTick

    /**
     * 将刷新 watcher 队列的函数或者用户调用 Vue.nextTick 方法传递的回调函数放入 callbacks 数组
     * 如果当前的浏览器任务队列中没有刷新 callbacks 的函数,则将 flushCallbacks 函数放入任务队列
     */
    function nextTick(cb) {
      callbacks.push(cb)
      if (!pending) { // 表明浏览器当前任务队列中没有刷新 callbacks 数组的函数
        // 将 flushCallbacks 函数放入浏览器的微任务队列
        Promise.resolve().then(flushCallbacks)
      }
    }
    
    

    flushCallbacks

    /**
     * 负责刷新 callbacks 数组的函数,执行 callbacks 数组中的所有函数
     */
    function flushCallbacks() {
      // 表示浏览器任务队列中的 flushCallbacks 函数已经被拿到执行栈执行了
      // 新的 flushCallbacks 函数可以进入浏览器的任务队列了
      pending = false
      while(callbacks.length) {
        // 拿出最头上的回调函数
        const cb = callbacks.shift()
        // 执行回调函数
        cb()
      }
    }
    
    

    总结

    到这里 精通 Vue 系列 就要结束了,现在我们再回头看下整个系列:从 Vue 源码解读 开始到现在的 手写 Vue,总共 20 篇文章。如果你是从头到尾跟下来的,相信我们最初定的目标早已实现,这会儿你是否可以在自己的简历上写上:精通 Vue 源码原理。

    关注

    欢迎大家关注我的 掘金账号 和 B站,如果内容有帮到你,欢迎大家点赞、收藏 + 关注

    链接

    • 精通 Vue 技术栈的源码原理

    • 配套视频

    • 学习交流群


    起源地下载网 » 手写 Vue2 系列 之 异步更新队列

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元