set 和 数组方法 其实都是对新属性执行下列操作
- 劫持新属性(Object.defineProperty)
- 手动通知 watcher 更新(
dep.notify()
)
set
vue 在初始化时对 data 中的属性进行了监测,实现了数据驱动视图更新,可查看我上次写的:vue源码 - 利用 Object.defineProperty 进行数据监测
未在 data 中初始化的属性是非响应的,也就是修改这些数据,并不会驱动视图更新,Vue 为了解决这个问题,定义了一个 set 方法
<template>
<div>
{{ obj.b }}
</div>
</template>
var vm = new Vue({
data:{
obj: {
a: 1
// 未初始化 b
}
},
mounted () {
this.obj.b = 2 // 视图不会更新
/*
this.$set(this.obj, 'b', 1) // 视图会更新
*/
}
})
下面看一下 set 源码,定义在 src/core/global-api/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 {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
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__
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
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
可以看到 set 方法先判断了要设置的目标属性是否数组,是则调用 splice 进行设置,此处的 splice 是经过 vue 覆盖的方法,是具有响应处理的,并不是原生的 splice,关于这些方法此处先忽略,详情查下面的数组方法
之后通过 in 判断要设置的属性是否已定义,若已定义,说明本身就是响应式的,直接设置即可
之后判断是否是设置 vue 实例的属性,是则警告
之后判断目标是否存在 __ob__
属性,如果不存在说明目标不是响应式的,直接设置即可
如果之前的判断都没触发,则通过 defineReactive
监测属性,并通过 ob.dep.notify()
手动更新视图,这两个操作的详情可查看:vue源码 - 利用 Object.defineProperty 进行数据监测,经过这两步,设置的属性就变成响应式的了
数组方法
上文说到 set 中对于数组是使用 splice 处理的,而 splice 是经过 vue 重写的,vue 对一些数组常用的方法都进行了重写,增加了响应式处理,下面看下这部分源码
从vue源码 - 利用 Object.defineProperty 进行数据监测 这篇文章中可以看到 vue 在实例化 Observer 时,针对数组类型进行了相应处理
export class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
// ...
}
}
}
这里的 hasProto 是判断对象中是否存在 __proto__
这个指向原型对象的属性,是则调用 protoAugment 方法,否则调用 copyAugment,对于支持 Vue 的浏览器一般都就走到 protoAugment 了,这个方法直接将 vue 处理过的方法 arrayMethods
覆盖原型中的方法
function protoAugment (target, src: Object, keys: any) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
下面看看 arrayMethods
,看看 Vue 是如何重写这写数组方法的,源码在 在 src/core/observer/array.js
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
可以看到,arrayMethos 先继承了 Array,然后循环重写一系列数组方法,这些方法调用原始方法拿到结果并返回
对于 push
unshift
splice
做了特殊处理,因为这三个方法是可以为数组添加子元素的,这些子元素要进行响应式处理
此处比较巧妙的是使用拓展符接收参数,这样参数就是一个数组,对于 push unshift
拿到参数数组,对于 splice
则切割拿到 index 为 2 之后的元素,这些都是新添加的元素,然后传给 ob.observeArray(inserted)
进行处理,observeArray 会循环数组进行响应式处理,这个方法就不详细写了,其实也就是循环数组调用上面说到的 defineReactive
, 可查看vue源码 - 利用 Object.defineProperty 进行数据监测
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
这样处理之后就对新添加的元素进行了监测,这些新元素就是响应式的了