Vue 2.6 源码剖析-响应式原理学习 - 4.其他方法

vm.$set

vm.$set 是全局 Vue.set 的别名,它们是同一个方法。

用法:向响应式对象中添加一个属性,并且确保这个新属性同样是响应式的。当数据发生变化的时候,它会触发视图的更新。

注意:

  • 它必须用于向响应式对象上添加新属性
    • 因为 Vue 无法探测到普通对象上新增的属性。
    • 如果这个属性已存在并且不是响应式的,set方法会更新它的值,但不会把它转化成响应式的。
  • 对象不能是 Vue实例,或者 Vue 实例的根数据对象($data)。
<div id="app">
  {{ obj.title }}
  <hr>
  {{ obj.name }}
  <hr>
  {{ arr }}
</div>
<script src="../../dist/vue.js"></script>
<script>
  const vm = new Vue({
    el: "#app",
    data: {
      obj: {
        title: 'Hello Vue'
      },
      arr: [1, 2, 3],
    },
  });

  // 在控制台执行下面的代码
  // vm.obj.name = 'foo' // 无效:数据变化,视图没变化
  // vm.arr[0] = 100 // 无效:数据变化,视图没变化
  // 刷新页面:测试正常操作
  // vm.$set(vm.obj, 'name', 'foo') // 生效
  // vm.$set(vm.arr, 0, 100) // 生效
  // 刷新页面:测试给非响应式对象添加属性
  // vm.staticObj = {}
  // vm.$set(vm.staticObj, 'name', 'bar') // 打印查看没有 getter/setter
  // 刷新页面:测试修改非响应式的属性
  // vm.obj.count = 0
  // vm.$set(vm.obj, 'count', 100) //打印查看没有 getter/setter
  // 刷新页面
  // vm.$set(vm, 'a', 1) // 报错
  // vm.$set(vm.$data, 'b', 1) // 报错
</script>

定义位置

  • Vue.set() 构造函数中的方法(静态方法)

    • src\core\global-api\index.js
    	// 静态方法 set delete nextTick
      Vue.set = set
      Vue.delete = del
      Vue.nextTick = nextTick
    
  • vm.$set() 实例方法

    • src\core\instance\state.js
    // src\core\instance\index.js
    // 继续混入(注册) vm 的成员:$data/$props/$set/$delete/$watch
    stateMixin(Vue)
    
    // src\core\instance\state.js
    Vue.prototype.$set = set
    Vue.prototype.$delete = del
    

初始化 GlobalApi 时将 Vue.set 指向 set 函数。

set 函数在 src\core\observer\index.js 中定义。

所以 set 函数是和响应式相关的。

实例方法 vm.$set 也指向了同一个 set 函数。

所以 Vue.set 和 vm.$set 是相同的。

set 源码

// src\core\observer\index.js
/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  // 判断目标对象是否是undefined 或 原始值
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 如果 target 是数组,判断 key 是否是合法的数组索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 更新数组的长度
    target.length = Math.max(target.length, key)
    // 通过 splice 对 key 位置的元素进行替换
    // array.js 重定义的 splice 修改数组元素可以触发响应式机制
    target.splice(key, 1, val)
    return val
  }
  // 判断 key 是否是对象本身的属性,而不是Object原型上的成员
  if (key in target && !(key in Object.prototype)) {
    // 如果已经存在,那它应该已经进行了响应式处理
    // 更新属性的值,触发响应式机制
    target[key] = val
    return val
  }
  // 获取 target 中的 observer 对象
  const ob = (target: any).__ob__
  // 判断 target 是 Vue 实例 或 $data 根数据(ob.vmCount=1)
  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 设置对象的响应式属性
  defineReactive(ob.value, key, val)
  // 发送通知
  ob.dep.notify()
  return val
}

vm.$delete

vm.$delete(vm.obj, 'msg')

用法:删除对象的属性(数组的元素也属于对象的属性)。如果对象是响应式的,确保删除能触发更新视图。

这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是一般很少会使用它。

它与 $set 非常相似:

  • 目标对象不能是一个 Vue 实例 或 Vue 实例的根数据对象
  • 和 Vue.delete 相同,都是定义为 del 函数。
  • src\core\observer\index.js 中定义,和响应式相关。
// src\core\observer\index.js
/**
 * Delete a property and trigger change if necessary.
 */
export function del (target: Array<any> | Object, key: any) {
  // 判断目标对象是否是undefined 或 原始值
  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)) {
    // 通过 splice 对 key 位置的元素进行删除
    // array.js 重定义的 splice 修改数组元素可以触发响应式机制
    target.splice(key, 1)
    return
  }
  // 获取 target 中的 observer 对象
  const ob = (target: any).__ob__
  // 判断 target 是 Vue 实例 或 $data 根数据(ob.vmCount=1)
  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]
  // 如果 target 不是响应式对象,不做任何处理
  if (!ob) {
    return
  }
  // 发送通知
  ob.dep.notify()
}

vm.$watch

基本使用

vm.$watch(expOrFn, callback, [options])

用法:监听 expOrFn(Vue 实例上的一个表达式或者一个函数计算结果)的变化,触发callback (回调函数)。

  • expOrFn:要监听的 Vue 实例上的数据,值可以为:
    • 表达式:只接受简单的键路径。
    • 函数:对于更复杂的表达式,用一个函数取代。
      • 例如监听两个属性相加后的结果,结果发生变化时触发回调
  • callback:数据变化后执行的函数,值可以为:
    • 函数:回调函数,callback(newVal, oldVal) 接收 新值 和 旧值。
    • 对象:具有 handler 属性的对象,handler可以为:
      • 字符串:对应methods 中相应的方法
      • 函数
  • options:可选的选项
    • deep:深度监听
    • immediate:是否立即以表达式的当前值触发回调
<div id="app">
  {{ user.fullName }}
    </div>
<script src="../../dist/vue.js"></script>
<script>
  const vm = new Vue({
    el: "#app",
    data: {
      user: {
        firstName: '诸葛',
        lastName: '亮',
        fullName: ''
      }
    },
  });

vm.$watch(
  'user',
  function (newVal, oldVal) {
    this.user.fullName = `${newVal.firstName} ${newVal.lastName}`
  },
  {
    immediate: true,
    deep: true
  }
)

// 在控制台执行下面的代码
// vm.user.firsName = '张'
</script>

$watch 没有对应的静态方法,因为 $watch 方法中要使用 Vue 的实例。

Watcher 三种类型及创建顺序:

  1. 计算属性 Watcher
  2. 用户 Watcher (侦听器)
  3. 渲染 Watcher

$watch 中创建了 Watcher 对象,也就是用户 Watcher (侦听器)。

vm.$watcher 的定义位置:src\core\instance\state.js

调试三种 Watcher 的创建顺序

<div id="app">
  {{ reversedMessage }}
  {{ user.fullName }}
</div>
<script src="../../dist/vue.js"></script>
<script>
  const vm = new Vue({
    el: "#app",
    data: {
      message: 'Hello Vue',
      user: {
        firstName: '诸葛',
        lastName: '亮',
        fullName: ''
      }
    },
    computed: {
      reversedMessage() {
        return this.message.split(''),reverse().join('')
      }
    },
    watch: {
      'user': {
        handler: function(newVal, oldVal) {

        },
        immediate: true,
        deep: true
      }
    }
  });

</script>

在 Watcher 类的构造函数中打断点 src/core/observer/watcher.js

从调用栈(Call Stack)可以看到,第一个 Watcher 对象,是在 initComputed 中创建的。

它是计算属性 Watcher。

initComputed 是在 initState 中调用的。

在这里插入图片描述

在这里插入图片描述

接着进入 Watcher 类中,在更新 id 的位置打断点,查看每个 Watcher 的 id。

因为 Watcher 对象的 id 是自增的,所以通过 id 可以看到它们的创建顺序。

在这里插入图片描述

F8执行,此时 id 为 1。继续 F8 查看创建第二个 Watcher 的调用栈。它是在 $watch 方法中创建的。

它是用户 Watcher(侦听器)。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • initState 中先判断并初始化了 计算属性,然后调用了 initWatch 方法,将 options 的 watch 选项传入进去,初始化 侦听器。
  • 内部调用了 createWatcher 方法,最终调用 vm.$watch 方法。
  • vm.$watch 方法中创建了 Watcher 对象,也就是用户 Watcher。

F8 继续执行,此时 id 更新为 2。

继续执行,就是调用 mountComponent 创建渲染 Watcher 了,并在创建时标识了 isRenderWatcher。

F8继续执行,此时 id 为 3。

三种 Watcher 的执行顺序

调试发现,通过 Watcher 的 id 可以获取它们的创建顺序。

而执行 Watcher 的时候,是通过调用 flushSchedulerQueue 方法。

它会先将队列中的 Watcher 根据 id 排序,在依次执行。

所以 Watcher 的执行顺序也是:计算属性 Watcher -> 用户 Watcher(侦听器) -> 渲染 Watcher

$watch 源码

初始化侦听器 的时候 最终调用了 $watch 方法。

查看调用 $watch 的地方,initState中调用了initWatch方法。

内部调用 createWatcher。

createWatcher 方法:解析 watch 对象属性的值,用解析好的参数调用 vm.$watch

// src\core\instance\state.js
function initWatch (vm: Component, watch: Object) {
  // 遍历选项的 watch 对象
  for (const key in watch) {
    // 获取 watch 对象的值
    const handler = watch[key]
    // 判断值是否是数组:回调数组
    if (Array.isArray(handler)) {
      // 遍历数组,进行处理
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

// 解析参数调用 vm.$watch
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 先判断 属性 的值,是否是原生对象
  if (isPlainObject(handler)) {
    // 将它拷贝给options,用于保留其他选项
    options = handler
    // 重新赋值为 handler
    handler = handler.handler
  }
  // 如果 handler 是字符串,就去 Vue 实例(vm.methods)中找到这个字符串对应的方法
  if (typeof handler === 'string') {
    // 最终重新赋值为 回调函数
    handler = vm[handler]
  }
  // 用解析好的参数调用 vm.$watch 方法
  return vm.$watch(expOrFn, handler, options)
}

$watch 方法也是在当前文件中定义的。

// src\core\instance\state.js
Vue.prototype.$watch = function (
expOrFn: string | Function,
 cb: any,
 options?: Object
): Function {
  // 获取 Vue 实例(因为需要Vue实例,所以它没有对应的静态方法)
  const vm: Component = this
  // 判断 回调函数 是否是一个原始对象
  //   侦听器中解析了这个参数,所以它是一个function
  //   手动调用 vm.$watch 的时候可以传一个包含 handler 函数的对象
  if (isPlainObject(cb)) {
    // 如果传的是对象,就调用createWatcher对其解析
    // 并返回取消监听的方法
    return createWatcher(vm, expOrFn, cb, options)
  }
  // 如果options曾经解析过,直接赋值
  // 否则设置为一个空对象
  options = options || {}
  // options.user 标记当前要创建的 Watcher 对象是 用户 Watcher
  // 这也是为什么将 watch 选项和 vm.$watch 创建的 Watcher 对象称为 用户 Watcher
  options.user = true
  // 创建 用户 Watcher 对象
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    // 立即执行一次 回调函数,并把当前值传入
    // try catch 用于确保用户传入的函数(异常)不影响后面代码的执行
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  // 返回取消监听的方法
  return function unwatchFn () {
    watcher.teardown()
  }
}	

标记的 options.user 在调用 watcher的run方法时用于判断,如果当前是用户 Watcher, 就调用它的回调函数。

// src\core\observer\watcher.js
	run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value
        this.value = value
        // 判断如果当前是用户watcher
        if (this.user) {
          // 就调用它的 cb 回调函数(例如侦听器对应的function)
          // 使用try catch 是避免用户定义的回调发生异常
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

计算属性 Watcher

Watcher 中判断了 lazy 属性,这是计算属性 Watcher 用到的属性,它固定为 true。

initState 中调用了 initComputed 方法,它内部创建了计算属性对应的 Watcher 对象。

// src\core\instance\state.js
function initComputed (vm: Component, computed: Object) {
 // $flow-disable-line
 // _computedWatchers存储所有计算属性创建的 Watcher
 const watchers = vm._computedWatchers = Object.create(null)
 // computed properties are just getters during SSR
 const isSSR = isServerRendering()

 // 遍历计算属性选项
 for (const key in computed) {
   // 获取属性的值
   const userDef = computed[key]
   // 获取属性的getter方法
   const getter = typeof userDef === 'function' ? userDef : userDef.get
   if (process.env.NODE_ENV !== 'production' && getter == null) {
     warn(
       `Getter is missing for computed property "${key}".`,
       vm
     )
   }

   if (!isSSR) {
     // create internal watcher for the computed property.
     
     // 创建 Watcher对象,传入属性的getter方法,get会调用 getter

     // computedWatcherOptions 是一个常量:{lazy: true}
     // Watcher对象在构造函数中判断 lazy 为 true 就不会调用 get 方法
     // 因为计算属性是在渲染模板的时候使用的,所以创建 Watcher 的时候不需要调用

     // render渲染或数据变更时会通知Watcher调用update,内部会调用 get 方法

     // 把对象存入 _computedWatchers
     watchers[key] = new Watcher(
       vm,
       getter || noop,
       noop,
       computedWatcherOptions
     )
   }

   // component-defined computed properties are already defined on the
   // component prototype. We only need to define computed properties defined
   // at instantiation here.
   // 下面是将计算属性注入到 Vue 实例,并收集依赖
   // 内部通过 _computedWatchers 获取计算属性的 Watcher
   if (!(key in vm)) {
     defineComputed(vm, key, userDef)
   } else if (process.env.NODE_ENV !== 'production') {
     if (key in vm.$data) {
       warn(`The computed property "${key}" is already defined in data.`, vm)
     } else if (vm.$options.props && key in vm.$options.props) {
       warn(`The computed property "${key}" is already defined as a prop.`, vm)
     }
   }
 }
}

总结:计算属性 Watcher 和 用户 Watcher

  • 在 initState 中先后初始化(创建)了 计算属性 和 侦听器。
    • 计算属性
      • initComputed 中解析获取属性的getter,创建 Watcher 对象
      • 最后将计算属性注入到 Vue 实例
    • 侦听器
      • 过程
        • 调用 initWatch 遍历 watch 选项,获取属性的值
        • 调用 createWatcher 解析属性的值作为参数调用 vm.$watch
        • vm.$watch 中创建了 Watcher 对象
      • 用户还可以手动调用 vm.$watch 创建侦听器
      • 内部通过 options.user 标记,所以称呼为侦听器为 用户 Watcher

nextTick() 异步更新队列

数据变化时,等待数据更新到DOM后,会执行 nextTick(cb) 中传递的回调函数。

Vue 更新 DOM 是异步执行的,批量的:

  • 更新数据的时候,每更新一个属性值,并不会立即更新视图
    • 测试:此时通过 ref 获取 DOM上展示的内容,结果并不是最新的
    • 方法:在函数内通过 nextTick 的回调获取
  • Vue 会等到函数执行完,将所有变化的结果批量更新到视图。

nextTick 的目的是获取 DOM 上的最新数据。

nextTick 源码

nextTick 也有静态方法 Vue.nextTick 和 实例方法 vm.$nextTick。

它们的区别是实例方法中回调函数的 this 自动绑定到调用它的实例上。

  • 静态方法 Vue.nextTicksrc\core\global-api\index.js中定义

    • 它直接赋值为 nextTick 函数。
    // src\core\global-api\index.js
    // 定义一些外部常用的静态方法
    Vue.set = set
    Vue.delete = del
    Vue.nextTick = nextTick
    
  • 实例方法 vm.$nextTick 是在 src\core\instance\index.js中调用的 renderMixin 方法中定义

    • 它定义为 nextTick 函数,并将Vue实例传入作为上下位(回调函数 this 的指向)
    // src\core\instance\index.js
    // $nextTick/_render
    renderMixin(Vue)
    
    // src\core\instance\render.js
    // 定义$nextTick
    Vue.prototype.$nextTick = function (fn: Function) {
      return nextTick(fn, this)
    }
    
  • nextTick

// src\core\util\next-tick.js
/**
 * nextTick
 * @param {*} cb 回调函数
 * @param {*} ctx 上下文(回调函数的调用者)
 */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 把 cb 加上异常处理,存入 callbacks 数组中
  callbacks.push(() => {
    if (cb) {
      // 所有用户传入的函数,Vue都认为是危险的,所以都要增加 try catch
      try {
        // 调用 cb(),更改this指向
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      // 如果没有传递回调函数,会判断_resolve
      // 如果_resolve有值,就会执行
      // _resolve 是 Promise 对象的 resolve
      _resolve(ctx)
    }
  })
  // pending 表示 callbacks 队列是否正在被处理
  if (!pending) {
    // 标记队列正在被处理
    pending = true
    // 开始处理
    timerFunc()
  }
  // $flow-disable-line
  // 2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise
  if (!cb && typeof Promise !== 'undefined') {
    // 返回一个 promise 对象,内部将 resolve 传递给 _resolve
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
  • timerFunc
// src\core\util\next-tick.js
let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // timerFunc优先以Promise(微任务)的方式执行nextTick
  // 也就是所有同步任务执行完成后,再执行

  // 可是此时视图并没有更新完
  // 这是因为 nextTick中的回调执行之前,数据其实已经发生改变
  // 当数据发生变化,会立即通知 Watcher 重新渲染视图。
  // Watcher 中首先做的是把DOM上的数据更新,也就是更改DOM树
  // 至于DOM什么时候更新到浏览器上,是在这次事件循环结束之后,才会执行DOM的更新操作
  // 而nextTick在微任务中获取数据,其实是在DOM树上获取数据,此时DOM还没有渲染到浏览器中
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    
    // 兼容 IOS 一些不支持 Promise 的版本,改用 setTimeout
    if (isIOS) setTimeout(noop)
  }
  // 标记当前使用的nextTick使用的是微任务
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  // 判断当前不是IE浏览器,并且支持 MutationObserver
  // MutationObserver在IE10 11中才支持,并且还有些小问题
  // 这里主要用于兼容这些环境:PhantomJS, iOS7, Android 4.4

  // MutationObserver 监听DOM对象的改变
  // 如果DOM对象发生改变,会执行回调,这个回调也是以微任务的形式执行的
  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
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 如果不支持上面的两个方式,就降低为使用 setImmediate
  // 它和 setTimeout 的区别是:只有IE和nodejs支持
  // 优于setTimeout使用的原因是:setImmediate的性能更好
  // setTimeout使用时即使延迟设为0,最快也要等4ms才会执行
  // 而setImmediate会立即执行
  // 可以通过同时调用 setImmediate setTimeout 查看打印结果确认执行顺序
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
  • flushCallbacks
// src\core\util\next-tick.js
function flushCallbacks () {
  // 先标记为结束
  pending = false
  // 备份回调函数数组
  const copies = callbacks.slice(0)
  // 清空callbacks,以便允许继续向数组中添加回调
  callbacks.length = 0
  // 遍历执行回调函数
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

总结

  • nextTick在执行回调函数的时候,先会把回到函数放到callbacks数组中。
  • 然后它会优先以微任务的方式进行处理回调函数
  • 如果浏览器不支持微任务,就降低为宏任务。

PS:在执行 Watcher 队列(flushSchedulerQueue)时是包含在 nextTick 中的,所以 Watcher 的执行过程是在视图更新后才会执行,它是异步执行的。

PS:nextTick 用来异步获取 DOM 的最新数据,本身仅仅是用来开启一个微任务或者宏任务,所以它不会造成页面的卡顿,除非回调函数中的代码过于耗时会导致页面卡顿。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值