与变化侦测相关的常用API,主要有vm.$watch、vm.$set、vm.$delete,他们是挂载到Vue原型上的, 下面我们来一一介绍。
vm.$watch
一、介绍
vm.$watch用于观察一个表达式或函数在Vue实例上的变化。回调函数调用时,会从参数得到新数据(new value) 和旧数据(old value).表达式只接受以点分隔的路径,例如a.b.c,如果是一个比较复杂的表达式,可以用函数代替。
二、语法
/**
* 参数
* {String | Function} expOrFn
* {Function | Object} callback
* {Object} [options]
* 返回值
* {Function} unwatch
*/
vm.$watch( expOrFn, callback, [options] )
三、使用
- 当监听Object的某个子属性的变化时
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做点什么
})
- 当监听Object自身及所有子属性的变化时(注意需要加deep选项,实现深度监听)
vm.$watch('someObject', callback, {
deep: true
})
- 当监听一个函数的变化时
vm.$watch(
function () {
return this.a + this.b
},
function (newVal, oldVal) {
// 做点什么
}
)
- 当以表达式的当前值立即触发回调时
vm.$watch('a', callback, {
immediate: true
})
- 当callback与 options合并使用时(第二个参数为一个对象)
vm.$watch(
'a.b.c',
{
handler: function (val, oldVal{},
deep: true
}
)
四、内部原理
了解到vm.$watch的用法后,我们来根据其源码来分析其内部实现原理。
Vue.prototype.$watch = function (expOrFn,cb,options) {
const vm: Component = this
//判断传入的回调是否为一个对象。
if (isPlainObject(cb)) {
// 如果传入的回调是个对象,那就表明用户是把第二参数回调函数cb和第三个参数选项options合起来传入的,此时调用createWatcher。
// createWatcher方法其实就是将用户传入的回调(对象)中的回调函数`cb`和参数`options`剥离出来,然后再以常规的方式$watch方法并将剥离出来的参数传进去。
return createWatcher(vm, expOrFn, cb, options)
}
//如果用户传入的回调不是一个对象,而是一个函数,则获取用户传入的options,如果用户没有传入则将其赋值为一个默认空对象
options = options || {}
//$watch方法内部会创建一个watcher实例,由于该实例是用户手动调用$watch方法创建而来的,所以给options添加user属性并赋值为true,用于区分用户创建的watcher实例和Vue内部创建的watcher实例
options.user = true
//传入参数创建一个watcher实例
const watcher = new Watcher(vm, expOrFn, cb, options)
//判断如果用户在选项参数options中指定的immediate为true,则立即用被观察数据当前的值触发回调
if (options.immediate) {
cb.call(vm, watcher.value)
}
// 返回一个取消观察函数unwatchFn,用来停止触发回调
return function unwatchFn () {
watcher.teardown()
}
}
通过上述源码逐行解读,我们知道了vm.$watch是如何实现的,那么还有最后一个问题,当选项参数options中的deep属性为true时,如何实现深度观察呢?
export default class Watcher {
constructor (/* ... */) {
// ...
this.value = this.get()
}
get () {
// 关键代码
if (this.deep) {
traverse(value)
}
return value
}
}
可以看到,在get方法中,如果传入的deep为true,则会调用traverse函数,把被观察数据的内部值都递归遍历读取一遍,从而实现深度观察。
vm.$set
一、介绍
vm.$set用来向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。
它必须用于向响应式对象上添加新属性,因为对于 Vue 来说,当我们向object数据里添加一对新的key/value,Vue是无法观测到的;而对于Array型数据,当我们通过数组下标修改数组中的数据时,Vue也是是无法观测到的。
二、语法
/**
* 参数
* {Object | Array} target
* {string | number} propertyName/index
* {any} value
* 返回值
* 设置的值
*/
vm.$set( target, propertyName/index, value )
三、使用
- 当需要向对象中新增属性时
vm.$set( obj, a, 'hello' )
- 当需要通过下标的形式修改数组时
vm.$set( Array, 3, 'world' )
四、内部原理
了解到vm.$set的用法后,我们来根据其源码来逐行分析其内部实现原理。
export function set (target, key, val){
//首先判断在非生产环境下如果传入的target是否为undefined、null或是原始类型,如果是,则抛出警告
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是有效索引的话,那么就取当前数组长度与key这两者的最大值作为数组的新长度,然后使用数组的splice方法将传入的索引key对应的val值添加进数组。
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
//如果传入的target不是数组,那就当做对象来处理。
//判断传入的key是否已经存在于target中,如果存在,表明这次操作不是新增属性,而是对已有的属性进行简单的修改值,那么就只修改属性值即可
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
//获取到traget的__ob__属性,该属性是否为true标志着target是否为响应式对象,接着判断如果tragte是 Vue 实例,或者是 Vue 实例的根数据对象,则抛出警告并退出程序,
const ob = (target: any).__ob__
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属性为false,那么表明target不是一个响应式对象,那么我们只需简单给它添加上新的属性,不用将新属性转化成响应式
if (!ob) {
target[key] = val
return val
}
//如果target是对象,并且是响应式,那么就调用defineReactive方法将新属性值添加到target上,defineReactive方会将新属性添加完之后并将其转化成响应式
defineReactive(ob.value, key, val)
//通知依赖更新
ob.dep.notify()
return val
}
以上即是对vm.$set内部原理的分析
vm.$delete
一、介绍
vm.$delete用来删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制
二、语法
/**
* 参数
* {Object | Array} target
* {string | number} propertyName/index
*/
vm.$delete( target, propertyName/index, value )
三、使用
- 当需要删除对象的某个属性时
vm.$delete( obj, a)
- 当需要通过下标的形式删除数组的某个值时
vm.$delete( Array, 3)
四、内部原理
了解到$delete的用法后,我们来根据其源码来逐行分析其内部实现原理。
export function del (target, key) {
//判断在非生产环境下如果传入的target不存在,或者target是原始值,则抛出警告
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是有效索引的话,就使用数组的splice方法将索引key对应的值删掉
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
//如果传入的target不是数组,那就当做对象来处理。
const ob = (target: any).__ob__
// 取到traget的__ob__属性,我们说过,该属性是否为true标志着target是否为响应式对象,接着判断如果tragte是 Vue 实例,或者是 Vue 实例的根数据对象,则抛出警告并退出程序,
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
}
//判断传入的key是否存在于target中,如果key本来就不存在于target中,那就不用删除,直接退出程序即可
if (!hasOwn(target, key)) {
return
}
//如果target是对象,并且传入的key也存在于target中,那么就从target中将该属性删除
delete target[key]
//判断当前的target是否为响应式对象,如果不是,删除完后直接返回不通知更新
if (!ob) {
return
}
//如果是响应式对象,则通知依赖更新
ob.dep.notify()
}
该方法的内部原理与set方法有几分相似,都是根据不同情况作出不同处理。
\