Vue.js最独特的特性之一是看起来并不显眼的响应式系统。数据模型仅仅是普通的JavaScript对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单、直接。不过理解其工作原理同样重要,这样你可以回避一些常见的问题。
1. 背景
我们都知道JavaScript要监听一个对象的变化可以使用Object.defineProperty和Proxy实现。最初考虑到ES6语法在浏览器的支持度不理想,所以Vue2使用的是Object.defineProperty来实现,由于IE8及以下版本不支持该API,这也是Vue不支持IE8及以下的原因。
不熟悉Object.defineProperty的童鞋可以移步MDN学习。
2. 基础版
- 实现一个监听对象属性变化的函数:
function defineReactive(obj, key, value, cb) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return value;
},
set(newVal) {
if(value === newVal) return;
value = newVal;
cb(); // 执行观察者收到更新消息的回调
}
})
}
- 将对象的每个属性都监听起来
function observe(obj, cb) {
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key], cb));
}
- 实现一个
Vue类
class Vue {
constructor(options) {
this._data = options.data;
observe(this._data, options.render);
}
}
使用:
const app = new Vue({
el: '#app',
data: {
name: 'tom',
age: 11
},
render() {
console.log('render');
}
});
app._data.name = 'mike'; // render
可以看到,app这个实例里面的data已经被监听了起来,当我们改变data的时候,会触发相应的回调函数。
但是上面改变data的写法有些累,我们可以写一个代理函数,然后改变数据就可以写成app.name='mike'。
function _proxy(data) {
const that = this;
Object.keys(data).forEach(key => Object.defineProperty(that, key, {
configurable: true,
enumerable: true,
get() {
return that._data[key];
},
set(newVal) {
that._data[key] = newVal;
}
}))
}
改写一下Vue类:
class Vue {
constructor(options) {
this._data = options.data;
_proxy.call(this, this._data); // 新增
observe(this._data, options.render);
}
}
这样就可以直接写成app.name='mike'了。
_proxy函数实际上是把data里面的属性加在app这个对象上,并监听加上的属性。执行app.name='mike'的时候,就会触发_proxy里面的set,set里面实际上是执行app._data.name='mike'。
升级版
基础版实际上只能够监听对象的一级属性,想要监听对象更深级的属性,需要使用递归的方法改写代码:
function observe(obj, cb) {
Object.keys(obj).forEach(key => {
const value = obj[key];
// 新增:属性值是对象,就递归监听
if(Object.prototype.toString.call(value) === '[object Object]') {
observe(value, cb);
}
defineReactive(obj, key, value, cb);
});
}
function defineReactive(obj, key, value, cb) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖。。。
return value;
},
set(newVal) {
if(value === newVal) return;
value = newVal;
cb(); // 执行订阅者收到更新消息的回调
}
})
}
class Vue {
constructor(options) {
this._data = options.data;
_proxy.call(this, this._data);
observe(this._data, options.render);
}
}
const app = new Vue({
el: '#app',
data: {
name: 'tom',
age: 11,
otherInfo: {
sex: 'boy'
}
},
render() {
console.log('render');
}
});
function _proxy(data) {
const that = this;
Object.keys(data).forEach(key => Object.defineProperty(that, key, {
configurable: true,
enumerable: true,
get() {
return that._data[key];
},
set(newVal) {
that._data[key] = newVal;
}
}))
}
app.otherInfo.sex = 'girl'; // render