近些年来,一股 MVVM 之风刮遍全球,你们无不为之称赞。关于 MVVM 架构模式的实现,你们讨论的最多的算是 Vue.js 了吧!Vue.js 很好的利用 MVVM 中的 VM 声明式的实现了与数据模型 Model 和视图 View 的联通,使得用户只需对数据模型进行操做,就能响应到对应的视图上,这其中核心的实现就是「响应式系统」了。「响应式系统」在整个系统中起到了举足轻重的做用,为咱们大大减轻了生产力,了解其原理与实现就成为了咱们技术人无尽的追求,一样也有助于咱们在实际的生产开发中更好的解决相关的问题。javascript
从上面的分析,不难发现视图 View 和数据模型 Model 算是咱们最熟悉的了,不须要作过多的说明,可是 VM 视图模型就是咱们须要深挖的了,它的原理实现对整个 MVVM 模型系统很是重要。接下来,大部份内容就是对这个核心的探讨了。html
在详细介绍这个 Object.defineProperty 以前,咱们先抛出几个问题:vue
ECMAS-262 第5版在定义只有内部采用的特性时,提供了描述属性特征的几种属性。ECMAScript 对象中目前存在的属性描述符主要有两种:数据描述符(数据属性)和存取描述符(访问器属性)。数据描述符是一个拥有可写或不可写值的属性,存取描述符是由一对 getter-setter 函数功能来描述的属性。java
Object.defineProperty 就是用来定义对象属性的属性描述符方法,它会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。api
数据描述符可包含下列的属性:数组
configurable:默认为 false。当且仅当该属性值为 true 时,该属性描述符才可以被改变,同时该属性也能从对应的对象上被删除。数据结构
enumerable:默认为 false。当且仅当该属性值为 true 时,该属性才可以出如今对象的枚举属性中,好比 for-in循环或 Object.keys() 等。架构
writable:默认为 false。当且仅当该属性值为 true 时,value 才能被赋值运算符改变。ide
value:该属性对应的值。能够是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。函数
示例代码:
var obj = {}
Object.defineProperty(obj, 'a', {
value: 1
})
// 获取属性 a 的值:obj.a => 1
// 获取 obj 上可遍历的属性:Object.keys(obj) => []
// 删除 obj 上的属性 a:delete obj.a => false
// 从新定义 obj 上的属性 a:Object.defineProperty(obj, 'a', { value: 1, configurable: true }) => Cannot redefine property 直接报错
// 从新赋值 obj 上的属性 a 的值为 2:obj.a = 2
// 从新获取属性 a 的值:obj.a => 1 ⚠️注意:从新赋值并无成功
复制代码
上面的示例代码能够说明:当经过 Object.defineProperty 为对象定义或修改属性时,默认不指定属性描述符,全部的属性描述符的值都是 false,这会致使该属性不会存在于对象可遍历的属性列表中,不能对属性从新配置(包括删除和从新定义属性),还有对该属性的从新赋值也不会成功。若是显式的将这些描述符设置为 true,那么以上描述的全部不可行的操做都会可行(能够被从新赋值,能够被删除,能够被从新定义,能够被遍历等),这里我就不演示了,你们感兴趣能够实操一下。
存取描述符可包含下列的属性:
configurable:默认为 false。当且仅当该属性值为 true 时,该属性描述符才可以被改变,同时该属性也能从对应的对象上被删除。
enumerable:默认为 false。当且仅当该属性值为 true 时,该属性才可以出如今对象的枚举属性中,好比 for-in循环或 Object.keys() 等。
get:一个给属性提供 getter 的方法,若是没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,可是会传入 this 对象(因为继承关系,这里的 this 并不必定是定义该属性的对象)。默认为 undefined。
set:一个给属性提供 setter 的方法,若是没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受惟一参数,即该属性新的参数值。默认为 undefined。
示例代码:
var obj = {}
var val = ''
Object.defineProperty(obj, 'a', {
get() {
return val
},
set(newVal) {
val = newVal
}
})
复制代码
当你运行上面的示例代码,你会发现 obj 对象变成了上面这样,是否是有种很熟悉的感受?对,在 Vue.js 项目中,咱们无时不刻不见到这样的数据结构。这就是 Object.defineProperty 存取描述符的魅力了:它会给定义过的属性添加 set 和 get 方法。当咱们经过 . 符号或者 [] 给对象的属性赋值时,就会触发 set 方法了;当咱们经过 . 符号或者 [] 获取对象的属性的对应值时,就会触发 get 方法了。正由于 Object.defineProperty 有这样一个能力,因此咱们能够经过它实现响应式系统,完成 MVVM 模式中 VM 这重要的一环。
Object.defineProperty 的存取描述符中依然能够包含 configurable 和 enumerable,这两个属性的做用咱们在数据描述符中已经提到过了,这里就再也不赘述了。
上面咱们已经分析过了,咱们能够经过 Object.defineProperty 存取描述符将对象定义成可观察的,而后在 set 和 get 方法中加上对应的逻辑。
为了便于看到效果,这里先定义一个方法,该方法在调用时会输出「视图更新啦~~」
function cb() {
console.log('视图更新啦~~')
}
复制代码
为了实现更好的复用和便于递归,咱们将定义一个名为 defineReactive 的方法,该方法就是对 Object.defineProperty 逻辑的封装,它会接受对象、须要定义的属性 key 值和属性的值,而后根据这些参数定义属性的 getter、setter 方法,实现响应式。
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true, // 属性可枚举
configurable: true, // 属性可被修改或删除
get() {
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
observer(val)
cb(newVal);
}
})
}
复制代码
要想将数据变成深度可观察的,咱们还须要封装一层。封装的逻辑主要是对类型进行判断,而后就是对深层的属性进行遍历并调用 defineReactive 实现彻底数据响应式。
function observer(value) {
if (!value || (typeof value !== 'object')) {
return
}
Object.keys(value).forEach((key) => {
const val = value[key]
observer(val)
defineReactive(value, key, val);
})
}
复制代码
到了这一步,就来测试一下咱们的成果吧:
// 定义一个 obj 多级嵌套对象
var obj = {a: 1, b: { c: 2 }}
// 将 obj 变成可观察的
observer(obj)
obj.b = 'lane' => 视图更新啦~~
复制代码
成果还不错,咱们就来趁热打铁封装一个简单的 Vue 响应式系统吧!先来看一个最简单的 Vue 使用示例:
const vm = new Vue({
data: {
message: 'I am lane.'
}
})
复制代码
Vue 会做为构造函数进行调用并接受一个对象做为函数。目前在最简单的状况下,参数对象只包含一个 data 属性,咱们的目的就是将这个 data 属性值变成可观察的。
// Vue构造类
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
}
}
复制代码
测试简易封装的 Vue 示例代码:
vm._data.message = 'hello, world.' // 视图更新啦~
复制代码
固然这还只是 Vue.js 中响应式系统的第一步,为了更好的进行数据更新处理,系统还须要进行依赖收集,以确保数据更新性能达到更优。
经过 Object.defineProperty 实现的数据响应式逻辑对于数组的许多方法都不能触发 set 方法(包括 push、pop、shift、unshift、splice、sort、reverse),Vue.js 为了解决这个问题,从新包装了这些函数,同时当这些方法被调用的时候,手动去触发更新操做;还有另外一个问题,官网也有特别的指出:
因为 JavaScript 的限制,Vue 不能检测如下变更的数组:
- 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:vm.items.length = newLength
这个最根本的缘由是由于这两种状况下,受制于js自己没法实现监听,因此官方建议用他们本身提供的内置 api 来实现,咱们也能够理解到这里既不是 defineProperty 能够处理的,也不是包一层函数就能解决的,这就是 2.x 版本如今的一个问题。
咱们能够利用咱们以前的定义来实验一把:
const vm = new Vue({
data: {
userIds: ['01', '02', '03', '04', '05']
}
})
// 都没有输出 视图更新啦~,说明没有触发 set
vm._data.userIds.push('06')
vm._data.userIds.length = 2
复制代码
今天关于数据响应式的初探就到这里吧,说到的东西也挺多的,首先是 MVVM 模式的架构,而后对 MVVM 的每一个组成都进行详细的说明,接着说到了目前 Vue.js 经过 Object.defineProperty 实现响应式数据的方式,并对 Object.defineProperty 的用法和数据描述符与存取描述符进行了详细的讲解,最后利用 Object.defineProperty 封装了一个简单的 Vue 响应式系统,最后的最后提到了关于 Object.defineProperty 的一些缺陷。固然这还只是走出了第一步,Vue.js 的响应式系统还包括数据劫持、依赖收集等,固然后面咱们也会慢慢的提到。