结合后台管理系统案例理解Vue数据响应式原理

前言

如果对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中无法根据数组下标修改值来让视图响应。

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的更新类推...

以上是 结合后台管理系统案例理解Vue数据响应式原理 的全部内容, 来源链接: utcz.com/a/14272.html

回到顶部