我们都知道,vue最大的一个特点就是数据驱动视图,也就是当数据变化的时候,对应的视图也随之变化。想要实现这一点,其重点在于要知道什么时候数据发生了变化,也就是说要对数据的变化进行侦测。
那我们就从源码出发,来看一看vue是如何对数据的变化进行侦测的。
vue将数据分为了两类进行侦测,分别是Object和Array
1、Object的变化侦测
在vue中实现数据的可侦测,用到的核心API就是:Object.defineProperty()方法
// 首先定义一个对象,我们可以通过obj.name的方式来读取或修改name属性
let obj={
name:'xxx',
age:'26'
}
// 使用Object.defineProperty改写,实现可侦测
Object.defineProperty(obj,'name',{
enumerable: true, // 此属性是否可以被枚举
configurable: true, // 目标属性是否可以被删除或是否可以再次修改特性
get(){
console.log('读取name属性')
return val
},
set(newVal){
console.log('修改name属性')
val = newVal
}
})
上面已经实现了对obj对象中一个属性进行侦测,接下来对整个obj对象进行侦测。在源码中vue中封装了一个Observer类来实现,Observer类会通过递归,将整个对象的属性都转化为可侦测的对象
let obj=new Observer({
name:'xxx',
age:'26'
})
export class Observer{
constructor(value){
this.value = value
// 给value新增一个_ob_属性,相当于打了一个标记,表示已经被转化成响应式了
def(value, '__ob__', this)
if (isArray(value)) {
// 当属性为数组的时候
}else{
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(value, key[i])
}
}
}
}
export function defineReactive(obj,key,val){
if(arguments.length === 2){
val = obj[key]
}
if(typeof val === 'object'){
// 如果属性为对象,递归
new Observer(val)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
console.log('读取属性')
return val
},
set(newVal){
val = newVal
console.log('修改属性')
}
}
}
2、Array的变化侦测
为什么这两种数据要用不同的方式进行侦测呢?
主要是因为Object.defineProperty()这个方法是原型对象上的,所以数组无法使用这个方法。
Object的变化是通过setter来追踪的,但是Array并没有setter。但是换一种角度思考,如果Array数据发生了变化,那一定是进行了操作,而js中提供的操作数组的方法就那么几种,我们可以在不改变原有功能的前提下,新增一些功能进去。也就是对原数组的方法进行“劫持”!
// 数组劫持
let arr = []
arr.push(1)
Array.prototype.pushNew = function(){
// 进行其他操作
this.push(val)
}
arr.pushNew(2)
于是vue实现了一个数组方法拦截器
Array原型中可以改变数组本身的方法共有7个:push,pop,shift,unshift,splice,sort,reverse
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method){
const original = arrayProto[method]
// 源码封装了def()方法,这里直接将def方法提出来了
Object.defineProperty(arrayMethods, method, {
enumerable: false,
configurable: true,
writable: true,
value:function mutator(...args){
const result = original.apply(this, args)
return result
}
})
// 将数组的新增元素也转化为响应式
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 通知更新
ob.dep.notify()
})
// 遍历数组,使数组每一项都转化为响应式
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
不足之处:
拦截器只能拦截到通过调用原型上的方法进行的改变,如果通过下标修改数据则无法侦测。
vue为解决这点,增加了两个全局API:Vue.set和Vue.delete