什么是响应式
我们先来看个例子:
Price :¥{{ price }}
Total:¥{{ price * quantity }}
Taxes: ¥{{ totalPriceWithTax }}
var app = new Vue({
el: '#app',
data() {
return {
price: 5.0,
quantity: 2
};
},
computed: {
totalPriceWithTax() {
return this.price * this.quantity * 1.03;
}
},
methods: {
changePrice() {
this.price = 10;
}
}
})
上例中当price 发生变化的时候,Vue就知道自己需要做三件事情:
数据发生变化后,会重新对页面渲染,这就是Vue响应式,那么这一切是怎么做到的呢?
想完成这个过程,我们需要:
如何侦测数据的变化
方法1. Object.defineProperty实现
Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
function render () {
console.log('模拟视图渲染')
}
let data = {
name: '浪里行舟',
location: { x: 100, y: 100 }
}
observe(data)
function observe (obj) { // 我们来用它使对象变成可观察的
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
function defineReactive (obj, key, value) {
// 递归子属性
observe(value)
Object.defineProperty(obj, key, {
enumerable: true, //可枚举(可以遍历)
configurable: true, //可配置(比如可以删除)
get: function reactiveGetter () {
console.log('get', value) // 监听
return value
},
set: function reactiveSetter (newVal) {
observe(newVal) //如果赋值是一个对象,也要递归子属性
if (newVal !== value) {
console.log('set', newVal) // 监听
render()
value = newVal
}
}
})
}
}
data.location = {
x: 1000,
y: 1000
} //set {x: 1000,y: 1000} 模拟视图渲染
data.name // get 浪里行舟
上面这段代码的主要作用在于:observe这个函数传入一个 obj(需要被追踪变化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理,以此来达到实现侦测对象变化。值得注意的是,observe 会进行递归调用。
那我们如何侦测Vue中data 中的数据,其实也很简单:
class Vue {
/* Vue构造类 */
constructor(options) {
this._data = options.data;
observer(this._data);
}
}
这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行追踪变化。 不过这种方式有几个注意点需补充说明:
无法检测到对象属性的添加或删除(如data.location.a=1)。
这是因为 Vue 通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。如果是删除属性,我们可以用vm.$delete实现,那如果是新增属性,该怎么办呢? 1)可以使用 Vue.set(location, a, 1) 方法向嵌套对象添加响应式属性; 2)也可以给这个对象重新赋值,比如data.location = {…data.location,a:1}
Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写,具体代码如下:
function render() {
console.log('模拟视图渲染')
}
let obj = [1, 2, 3]
let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']
// 先获取到原来的原型上的方法
let arrayProto = Array.prototype
// 创建一个自己的原型 并且重写methods这些方法
let proto = Object.create(arrayProto)
methods.forEach(method => {
proto[method] = function() {
// AOP
arrayProto[method].call(this, ...arguments)
render()
}
})
function observer(obj) {
// 把所有的属性定义成set/get的方式
if (Array.isArray(obj)) {
obj.__proto__ = proto
return
}
if (typeof obj == 'object') {
for (let key in obj) {
defineReactive(obj, key, obj[key])
}
}
}
function defineReactive(data, key, value) {
observer(value)
Object.defineProperty(data, key, {
get() {
return value
},
set(newValue) {
observer(newValue)
if (newValue !== value) {
render()
value = newValue
}
}
})
}
observer(obj)
function $set(data, key, value) {
defineReactive(data, key, value)
}
obj.push(123, 55)
console.log(obj) //[1, 2, 3, 123, 55]
方法2. Proxy实现
function render() {
console.log('模拟视图的更新')
}
let obj = {
name: '前端工匠',
age: { age: 100 },
arr: [1, 2, 3]
}
let handler = {
get(target, key) {
// 如果取的值是对象就在对这个对象进行数据劫持
if (typeof target[key] == 'object' && target[key] !== null) {
return new Proxy(target[key], handler)
}
return Reflect.get(target, key)
},
set(target, key, value) {
if (key === 'length') return true
render()
return Reflect.set(target, key, value)
}
}
let proxy = new Proxy(obj, handler)
proxy.age.name = '浪里行舟' // 支持新增属性
console.log(proxy.age.name) // 模拟视图的更新 浪里行舟
proxy.arr[0] = '浪里行舟' //支持数组的内容发生变化
console.log(proxy.arr) // 模拟视图的更新 ['浪里行舟', 2, 3 ]
proxy.arr.length-- // 无效
订阅者 Dep
class Dep {
constructor () {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
addSub (sub) {
this.subs.push(sub);
}
/* 通知所有Watcher对象更新视图 */
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
以上代码主要做两件事情:
用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。
观察者 Watcher
依赖收集的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。
.Watcher的简单实现
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
this.cb(this.value)
}
}
收集依赖
function observe (obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
function defineReactive (obj, key, value) {
observe(value) // 递归子属性
let dp = new Dep() //新增
Object.defineProperty(obj, key, {
enumerable: true, //可枚举(可以遍历)
configurable: true, //可配置(比如可以删除)
get: function reactiveGetter () {
console.log('get', value) // 监听
// 将 Watcher 添加到订阅
if (Dep.target) {
dp.addSub(Dep.target) // 新增
}
return value
},
set: function reactiveSetter (newVal) {
observe(newVal) //如果赋值是一个对象,也要递归子属性
if (newVal !== value) {
console.log('set', newVal) // 监听
render()
value = newVal
// 执行 watcher 的 update 方法
dp.notify() //新增
}
}
})
}
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
new Watcher();
console.log('模拟视图渲染');
}
}