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

508 阅读4分钟

前言

继续上一次的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.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.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的支持程度。

以上图为例做解释:

  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的源码分析笔记。