前言
如果对Vue的数据改变如何在视图上响应还没有深入研究过,那不妨看看这篇文章,这篇文章会结合项目中的案例来解读Vue数据响应式原理。
Vue响应式思想
在new Vue时会初始化data中的变量,在Vue2.0+中会通过defineProperty给data中的变量逐一设置getter/setter方法,将data中的变量设置成响应式。Vue中只有通过defineProperty设置了getter/setter方法的对象属性被赋值时才能触发视图更新,Vue对数组中的方法进行了额外处理,思想就是基于Array的原型对象将其上的属性:push/pop、unshift/shift、splice、sort/reserve方法通过definedProperty设置成响应式,当调用数组中的这些方式时就会触发视图更新回调函数,其中对于push、unshift、splice插入的数组属性, 最终会遍历逐一设置getter/setter方法,这样当修改新添的属性的值时,也能让视图响应。
Vue中视图响应数组值的改变
defineProperty的原理
我们知道,Vue 2.0+中基于defineProperty实现的数据响应式,只有定义在data中的变量才是响应式的;而对于对象的属性的新增和删除,视图不能直接响应改变;除此之外对于[]类型,不能通过修改数组下标来修改、新增属性值。当对象属性通过defineProperty定义之后,在给属性赋值时就能触发属性对应的set方法,从而触发视图更新。下面验证一下defineProperty的使用:
1、给{}类型的属性定义getter/setter方法
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可写
get: function () {
console.log('get');
return val;
},
set: function (newVal) {
// 设置时,可以添加相应的操作
console.log('set:', val);
val += newVal;
}
});
}
let obj = {
name: '成龙大哥',
say: ':其实我之前是拒绝拍这个游戏广告的,',
books: [{name: 'hh',price: 233}]
};
Object.keys(obj).forEach(k => {
defineReactive(obj, k, obj[k]);
});
obj.say = '后来我试玩了一下,哇,好热血,蛮好玩的'; //会触发set
obj.age = 20; //不会触发set
obj.books = [{name: 'yyyyyy',price: 23}]; //会触发set
obj.books[0].name = 'ttttt';
obj.books.push({name: 'hhhh',price: 66}); //触发get,push未找到
console.log(obj.name + obj.say, obj);
2、给数组类型的属性值定义getter/setter方法
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可写
get: function() {
console.log('get');
return val;
},
set: function(newVal) {
// 设置时,可以添加相应的操作
console.log('set:', val);
val += newVal;
}
});
}
let arr = [1,2,3,4,5];
arr.forEach((v, i) => {
defineReactive(arr, i, v);
});
arr[0] = 'oh nanana'; // 触发set
arr.push(6) // 不会触发set
console.log(arr)
let arr2 = [{name: '1'},{name: '2'}, {name: '3'}];
arr2.forEach((v, i) => {
defineReactive(arr2, i, v);
});
arr2.forEach((v, i) => {
v.status= true // 不会触发set
});
arr2.forEach((v, i) => {
v.name = true // 不会触发set
});
arr2[0] = {name: 2} // 会触发set
arr2.splice(1,1,{name: 111}) // 会触发set
arr2.push({name: 'tt'}) // 不会触发set
arr2.length = 2 // 不会触发set
我们得出什么结论?
defineProperty不能检测到的
1、对象的属性的新增和删除,新增属性也可以理解为没有通过defineProperty定义setter/getter的新增的对象属性;
2、数组通过push/pop/shift/unshift/sort等方法修改值,length修改长度。
defineProperty能检测到的
1、定义了setter/getter的对象属性的值修改,包括数组中下标定义了setter/getter的情况。
那么问题来了,Vue基于defineProperty实现的数据响应,但为什么通过下标修改的数组值视图不能响应,通过push/pop/shift/unshift/sort修改的数组值可以响应呢?Vue中具体是如何实现的基于数组值的变化,视图随之响应的呢?
Vue中对数组变量的Hack
index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
可以看到在Vue中并没有对数组类型的变量进行遍历,将数组中的属性设置setter/getter方法,所以Vue中无法根据数组下标修改值来让视图响应。(为什么不提供数组下标修改值让视图响应的功能可以参考:https://segmentfault.com/a/1190000015783546)
Vue不能根据数组下标修改值来让视图响应,却对push/pop/shift/unshift/sort等方法做了hack,通过这些方法修改数组可以让视图响应。
Vue中对数组的hack:
对于push、unshift方法新增的数组属性,splice插入的值, 最终会遍历逐一设置getter/setter方法,这样当修改新添的属性的值时,也能让视图响应。最后会调用notify()触发视图更新。
array.js
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.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
})
})
factory.js
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '_ob', this);
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
this.walk(value);
}
};
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i], obj[keys[i]]);
}
};
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
function observe (value, asRootData) {
if (!isObject(value)) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
}
if (Array.isArray(value)) {
dependArray(value);
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/\* eslint-disable no-self-compare \*/
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/\* eslint-enable no-self-compare \*/
if (process.env.NODE\_ENV !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
总结
1、只有定义了setter/getter的对象属性,在修改值时才能触发视图更新;
2、Vue中提供$set(Obj, Key, Value)原理就是将对象中新增的属性设置setter/getter方法,然后赋值;所以才可以做到新增的属性值,视图能响应到;
3、Vue对数组中的方法做了hack,以下方法对数组做改变,视图可以响应到变化:
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
案例分析
先看看下面这个案例,其中的编辑模块应用到的数据结构包含了一个二维数组,逻辑包含了对一维数组、二维数组的属性的值添加、删除、修改功能。利用到的Vue的核心技术点主要是数组值改变如何触发视图更新。
编辑页的数据结构:
data: {
OperatorId: 18,
is_auto: true,
cycle: {
end: `2019-04-04 00:00:00`,
start: `2019-03-27 00:00:00`
},
enroll: {
end: `2019-03-26 21:00:00`,
start: `2019-03-25 23:52:01`
},
switch_type: {
name: "按人数切期or按时间切期",
value: 0
},
assigned: 140,
max_assign: 1246,
rebind_assign: 5,
mode: "daily",
isBeforeEnroll: "true",
operators: [
{
assistants: [{
enable: true,
assigned: 23,
rebind_assign: 2,
max_assign: 300,
id: 45,
name: "酱酱4",
is_abnormal: false,
abnormal_assign: [{
open_time: "05-03 20:15:11",
close_time: "05-03 20:15:11",
count: 5
},
{
open_time: "05-04 20:15:11",
close_time: "现在",
count: 20
}
]
}],
assistants_type: 1,
avatar: "",
id: 18,
name: "骆美姗",
is_leave: true,
leave_day_text: "5天",
leave_date: "05/15(D1)-05-20(D5)"
},
],
state: {
enroll_doing: "false",
enroll_over: "true"
},
term: 153
},
use_memory_usage: `2.39 mb`,
assign_memory_usage: `2.38 mb`,
real_memory_usage: "2 mb"
};
最主要是对operators、assistants进行添加、修改、删除,涉及到数组的嵌套,如果对Vue响应式原理了解不够透彻就很容易遇到修改了数组的值,但是视图却不更新的问题。那需要注意些什么?下面拿这个案例举个例子:
对最外层的数组operators新增一个operator属性时,可以通过this.$set来添加也可以push/unshift,但是最好用在这里就用this.$set,因为添加的operator属性值是个对象,还有对这个对象进行修改的需求, 在要想在修改值时能够正常在视图更新,对应的变量必须是设置了getter/setter方法的响应式变量,在创建的时候operator时就应该将其设置成响应式变量:
addOperator() {
//延缓状态且招生结束
...
const arr = this.getInterTutorIndexArr
let index = 0
if (arr.length != 0) {
index = arr[arr.length - 1] + 1
}
const operator = this.factoryOperator(index)
this.$set(this.operatorsArr, this.operatorsArr.length, operator)
...
},
而不是在修改operator的时候用$set,这里其实就可以直接用数组下标的方式来更新operator的值:
changeOperator(
operator,
operatorIndex,
is_leave,
leave_date,
leave_day_text,
) {
...
let obj = Object.assign({}, operator, {
is_leave: is_leave,
leave_date: leave_date,
leave_day_text: leave_day_text,
})
//this.operatorsAr[operatorIndex] = obj
this.$set(this.operatorsArr, operatorIndex, obj)
...
},
而对于删除一个operator:
delOperator(operator, index) {
//延缓状态且招生结束
...
this.operatorsArr.splice(index, 1)
this.updateOperators(this.operatorsArr)
...
},
updateOperators(newOperators) {
...
//operatorsArr是data中的变量,是响应式的,直接赋值新值可以让视图响应
this.operatorsArr = JSON.parse(JSON.stringify(newOperators))
},
assistants的更新类推...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。