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