Vue.js 是一款 MVVM 框架,数据模型仅仅是普通的 JavaScript 对象,但是对这些对象进行操作时,却能影响对应视图,它的核心实现就是「响应式系统」。
首先我们来介绍一下 Object.defineProperty,Vue.js就是基于它实现「响应式系统」的。
首先是使用方法:
/*
obj: 目标对象
prop: 需要操作的目标对象的属性名
descriptor: 描述符
return value 传入对象
*/
Object.defineProperty(obj, prop, descriptor)
descriptor的一些属性,简单介绍几个属性,具体可以参考 MDN 文档。
enumerable,属性是否可枚举,默认 false。
configurable,属性是否可以被修改或者删除,默认 false。
get,获取属性的方法。
set,设置属性的方法。
实现 observer(可观察的)
知道了 Object.defineProperty 以后,我们来用它使对象变成可观察的。
在 init 的阶段会进行初始化,对数据进行「响应式化」。
为了便于理解,我们不考虑数组等复杂的情况,只对对象进行处理。
首先我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图,内部可以是一些更新视图的方法。
function cb (val) {
/* 渲染视图 */
console.log("视图更新啦~");
}
然后我们定义一个 defineReactive
,这个方法通过 Object.defineProperty
来实现对对象的「响应式」化,入参是一个 obj
(需要绑定的对象)、key
(obj的某一个属性),val
(具体的值)。经过 defineReactive
处理以后,我们的 obj
的 key
属性在「读」的时候会触发 reactiveGetter
方法,而在该属性被「写」的时候则会触发 reactiveSetter
方法。
function defineReactive (obj, key, val) {
// 递归
observe(val)
// 对传入对象进行拦截
Object.defineProperty(obj, key, {
enumerable: true, /* 属性可枚举 */
configurable: true, /* 属性可被修改或删除 */
get: function reactiveGetter () {
return val /* 实际上还会依赖收集 */
},
set: function reactiveSetter (newVal) {
if (newVal !== val) {
cb(newVal)
observe(newVal) /* 递归, 进行响应化处理 */
val = newVal
}
}
})
}
当然这是不够的,我们需要在上面再封装一层 observer
。这个函数传入一个 value
(需要「响应式」化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive
处理。
function observe (obj) {
if (typeof obj !== 'object' || obj === null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
最后,让我们用 observer 来封装一个 Vue 吧!
在 Vue 的构造函数中,对 options 的 data 进行处理,这里的 data 想必大家很熟悉,就是平时我们在写 Vue 项目时组件中的 data 属性(实际上是一个函数,这里当作一个对象来简单处理)。
class Vue {
constructor (options) {
// 保存选项
this.$options = options
this.$data = options.data
// 响应化处理
observe(this.$data)
// 代理 $data,代理后可以直接访问 Mvue 中的数据。
proxy(this, '$data')
}
}
为了能类似 Vue
直接访问到 data
中的数据,例如 this.text = 'test'
,我们需要对 Vue
构造函数中的 data
做一层代理(这里看不明白的可以多看几遍)。
function proxy (vm, $data) { /* vm:代理到的对象上,这里是 Vue实例本身;$data 为代理的对象 */
Object.keys(vm[$data]).forEach(key => {
// 给vm设置代理属性,将$data中的key代理到vm的属性中
Object.defineProperty(vm, key, {
get () {
return vm[$data][key]
},
set (newVal) {
vm[$data][key] = newVal
}
})
})
}
这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行「响应式」化。如果我们对 data 的属性进行下面的操作,就会触发 cb 方法更新视图。
let a = new MVue({
data: {
test: "I am test"
}
})
a.test = "hello, world" /* 视图更新啦~ */
至此,响应式原理已经介绍完了。
注:本文所有代码参考 Vue响应式系统基本原理 。