最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • [Vue源码]学习一下Vue常用API的源码(二)

    正文概述 掘金(村上小树)   2021-03-08   621

    前言

    继续上一次的Vue源码阅读,学到更多的一些API的内部源码,这里再次分享一下自己的理解。

    Vue.set

    参数

    • {Object | Array} target
    • {string | number} propertyName/index
    • {any} value

    用法

    向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property。如下:

    // 假设this.myObject是响应式数据
    // 下面的操作能给数据新增属性,但不能达到响应式变化
    this.myObject.newProperty = 'hi'
    // 下面的操作即能给数据新增属性,又能达到响应式变化
    this.$set(this.myObject,'newProperty','hi')
    

    其中,vm.$setVue.set的作用和用法一致。

    源码:

    export function set (target: Array<any> | Object, key: any, val: any): any {
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target)) //判断target是否为undefined或者为原始数据类型
      ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      // 判断target是否为对象,
      // key是否是合法的索引
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 判断是否要修改target的长度
        target.length = Math.max(target.length, key)
        // 此时target中的splice方法已被增强,即设置为响应式
        target.splice(key, 1, val)
        return val
      }
      if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
      }
      const ob = (target: any).__ob__
      // 判断target是否为vue实例或者是否为$data
      if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.'
        )
        return val
      }
      // 如果ob不存在,即代表target不是响应式对象,则直接赋值
      if (!ob) {
        target[key] = val
        return val
      }
      // 通过defineReactive把ob.value(即target)的key属性设置为响应式属性
      defineReactive(ob.value, key, val)
      // 发送通知触发响应式更新
      ob.dep.notify()
      return val
    }
    

    流程总结: [Vue源码]学习一下Vue常用API的源码(二)

    Vue.delete

    参数

    • {Object | Array} target
    • {string | number} propertyName/index

    用法

    删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到 property 被删除的限制,但是你应该很少会使用它。

    // 假设this.myObject是响应式数据
    // 下面的操作能给数据删除属性,但不能达到响应式变化
    delete this.myObject.newProperty
    // 下面的操作即能给数据删除属性,又能达到响应式变化
    this.$delete(this.myObject,'newProperty','hi')
    

    其中,vm.$deleteVue.delete的作用和用法一致。

    源码

    export function del (target: Array<any> | Object, key: any) {
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      // 判断target是否是对象,key是否是合法的索引
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 此时target中的splice方法已被增强,即设置为响应式
        target.splice(key, 1)
        return
      }
      const ob = (target: any).__ob__
      // 判断target是否为vue实例或者是否为$data
      if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid deleting properties on a Vue instance or its root $data ' +
          '- just set it to null.'
        )
        return
      }
      // 如果target中不存在key属性
      if (!hasOwn(target, key)) {
        return
      }
      delete target[key]
      // 如果ob不存在,即代表target不是响应式对象,则直接return
      if (!ob) {
        return
      }
      // 发送通知
      ob.dep.notify()
    }
    

    流程总结: [Vue源码]学习一下Vue常用API的源码(二)

    Vue.nextTick

    参数

    • {Function} [callback]

    用法

    将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。

    new Vue({
      // ...
      methods: {
        // ...
        example: function () {
          // 修改数据
          this.message = 'changed'
          // DOM 还没有更新
          this.$nextTick(function () {
            // DOM 现在更新了
            // `this` 绑定到当前实例
            this.doSomethingElse()
          })
        }
      }
    })
    

    这里针对以上代码做一下解释,Vue中针对next-tick方法创建了一个异步队列callbacks,这个异步队列的处理可能放在当前微任务列表中遍历执行,也可能放在下一个宏任务中遍历执行,这取决于浏览器对PromiseMutationObserver的支持程度。

    [Vue源码]学习一下Vue常用API的源码(二)

    以上图为例做解释:

    1. 当前代码可能是在微任务中运行,也可能是在宏任务中运行。

    2. 当代码执行到this.message = 'changed'时,由于定义在data中的数据发生变化,且该数据在template中被引用到,则会触发页面响应式更新,而负责处理页面响应式更新的函数flushQueue会被nextTick方法推送到callbacks任务队列中。与此同时,处理callbacks队列中的任务的函数flushCallbacks会被推送到微任务队列或者事件队列中,这这取决于浏览器对PromiseMutationObserver的支持程度。

    3. 当代码执行到this.$nexiTick(function(){...})时,vm.$nextTick(callback)中的callback会被添加到callbacks的末尾。此时因为flushCallbacks已在上一步中被推送到微任务队列或者事件队列中,故不在重复操作。

    4. 当前任务执行完成后,如果当前微任务列表不为空,则逐个取出微任务执行直至微任务队列为空。然后继续取出事件队列中的宏任务执行直至事件队列被清空。

    5. 当执行到含flushCallbacks的微任务或宏任务时,flushCallbacks会遍历callbacks把里面的方法逐个取出执行。第一个执行的是flushQueue以更新视图,视图更新完毕后。继续取出第二个方法执行,当执行到doSomething时,视图已被更新,故可以通过操作DOM方法操作获取最新的数据。

    源码

    vue2-study\src\core\util\next-tick.js

    因为next-tick代码较多,我们分开展示,先看nextTick的源码:

    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      // 推送处理方法到callbacks中
      callbacks.push(() => {
        // 如果cb存在,则在try-catch的块作用域中执行cb函数
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        /**
         * 当cb不存在时,例如:
         * this.$nextTick().then(function(){...})
         * 此时会执行生成的_resolve(),
         * this.$nextTick在传入的cb为空时会初始化且返回一个Promise实例
         * 该_resolve对应Promise实例中的resolve,当_resolve被执行时,
         * 则会执行this.$nextTick().then中传入的函数
         *
         * 注意:该用法需要在支持Promise的浏览器下或者带polyfill的程序中使用
         */
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      // 每次调用nextTick方法时,都会调用timerFunc,
      // timeFunc作用在于调用flushCallkacks遍历执行callbacks队列中的方法
      // 这里设置了pending状态位保证callbacks未被清空前,timerFunc只会被执行一遍
      if (!pending) {
        pending = true
        timerFunc()
      }
      // $flow-disable-line
      // cb为空时,代表nextTick的调用方式为:this.$nextTick().then(function(){...})
      // 此时,返回一个Promise实例
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    

    由上已知,nextTick通过timerFunc调用flushCallkacks执行队列,接下来看timerFunc的定义:

    let timerFunc
    
    // 1.如果当前浏览器支持Promise且Promise为原生API,
    //    timerFunc则会通过Promise.then调用flushCallbacks
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      timerFunc = () => {
        p.then(flushCallbacks)
        // 在部分有问题的浏览器中,Promise.then不会完全崩溃,但它可能会陷入一种奇怪的状态,
        // 回调函数被推入微任务队列,但队列没有被刷新,直到浏览器需要做一些其他的工作,
        // 例如处理一个计时器。因此,我们可以通过添加一个空计时器来“强制”刷新微任务队列。
        if (isIOS) setTimeout(noop)
      }
      isUsingMicroTask = true
    // 2.如果当前浏览器并非IE且支持MutationObserver且MutationObserver为原生API,
    //    则通过MutationObserver实例监听新生成的textNode的变化,当timerFunc改变textNode的内容时,
    //    MutationObserver实例会调用flushCallbacks
    } 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
    // 如果当前浏览器支持setImmediate时(仅IE10以上和node.js环境支持),则通过setImmediate调用flushCallbacks
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    // 如果上面三种判断都不成立,则通过setTimeout调用flushCallbacks
    } else {
      // Fallback to setTimeout.
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    

    timerFunc的设计重点在于用哪种回调方式执行flushCallbacks,代码中依次优先使用PromiseMutationObserver以把flushCallbacks放到微任务列表中。因为宏任务中产生的微任务的执行顺序会优先于下一个宏任务的执行顺序。但如果浏览器(万恶之源IE)不支持PromiseMutationObserver,则只能依次优先通过setImmediatesetTimeout以把flushCallbacks放到事件列表中。为什么优先用setImmediate,因为setImmediate会直接把传入的回调函数放到事件队列开头。

    最后再看flushCallbacks代码:

    // 用于遍历执行callbacks中的方法
    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    

    flushCallbacks逻辑比较简单,就是遍历执行callbacks且清空它。

    后记

    之后会继续更新Vue的源码分析笔记。


    起源地下载网 » [Vue源码]学习一下Vue常用API的源码(二)

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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