Vue2响应式框架
2.Object.defineProperty
- 实现数据的代理
class Vue {
constructor(options) {
this.$options = options
// TODO:data可能是函数
this._data = options.data
this.initData();
}
initData() {
let data = this._data;
let keys = Object.keys(data);
for(let i = 0; i < keys.length; i++) {
// 数据代理,实现vm.data
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
console.log('数据代理get')
return data[keys[i]]
},
set: function proxySetter(value) {
console.log('数据代理set')
data[keys[i]] = value;
}
})
}
}
}
- 实现数据的劫持,变成响应式
class Vue {
constructor(options) {
this.$options = options
// TODO:data可能是函数
this._data = options.data
this.initData();
}
initData() {
let data = this._data;
let keys = Object.keys(data);
for(let i = 0; i < keys.length; i++) {
// 数据代理,实现vm.data
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
console.log('数据代理get')
return data[keys[i]]
},
set: function proxySetter(value) {
console.log('数据代理set')
data[keys[i]] = value;
}
})
}
observe(data); // 数据劫持
}
function observe(data) {
// data是基本类型则返回
let type = Object.prototype.toString.call(data);
if(type !== '[object Object]' && type !== '[object Array]') {
return
}
if(data.__ob__) {
return data.__ob__ // $set
}
return new Observer(data);
}
}
// Observer: 变成响应式
class Observer {
constructor(data) {
this.dep = new Dep() // $set
if(Array.isArray(data)) {
data.__proto__ = ArrayMethods;
this.observeArray(data);
} else {
this.walk(data)
}
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false,
configurable: true,
writable: true
})
}
walk(data) {
let keys = Object.keys(data);
for(let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
observeArray(arr) {
for(let i = 0; i <arr.length; i++) {
observe(arr[i])
}
}
}
function defineReactive(obj, key, value) {
let childOb = observe(obj[key])
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
dep.depend();
if(childOb) {
childOb.dep.depend(); // $set
}
console.log(`获取data的${key}值`)
return value
},
set: function reactiveSetter(val) {
console.log(`data的${key}发生了改变`)
if(val === value) {
return
}
dep.notice();
value = val;
}
})
}
3. 收集依赖
class Dep {
constructor() {
this.subs = []
}
depend() {
if(Dep.target) {
this.subs.push(Dep.target)
}
}
notice() {
this.subs.forEach((watcher) => {
// 依次执行回调函数
watcher.run();
})
}
}
4. Watcher
我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个。接着,它再负责通知通知其他地方。所以,我们要抽象的这个东西--Watcher
class Vue {
constructor(options) {
this.$options = options
// TODO:data可能是函数
this._data = options.data
this.initData();
}
initData() {
let data = this._data;
let keys = Object.keys(data);
for(let i = 0; i < keys.length; i++) {
// 数据代理,实现vm.data
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
console.log('数据代理get')
return data[keys[i]]
},
set: function proxySetter(value) {
console.log('数据代理set')
data[keys[i]] = value;
}
})
}
observe(data);
this.initWatch(); // 初始化watcher
}
}
initWatch() {
let watch = this.$options.watch;
if(watch) {
let keys = Object.keys(watch);
for(let i = 0; i < keys.length; i++) {
new Watcher(this, keys[i], watch[keys[i]]); // 收集watcher实例依赖
}
}
}
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.id = ++watcherId;
this.get();
}
// 求值
get() {
Dep.target = this;
this.vm[this.exp]; //触发getter,然后就收集依赖
Dep.target = undefined;
}
run() {
if(watcherQueue.indexOf(this.id) !== -1) { // 已经存在于队列中
return
}
watcherQueue.push(this.id);
let index = watcherQueue.length -1;
Promise.resolve().then(() => {
this.cb.call(this.vm); // callback执行一下
watcherQueue.splice(index, 1);
})
}
}
5. 解决新增属性问题---$set
- 在创建observer实例时,也创建一个新的dep,挂在observer实例上,然后把observer实例挂载到对象的__ob__属性上。
- 触发getter时,不光把Watcher收集一份到之前的dep,也收集一份在这个新的dep上。
- 用户调用$set时,手动地触发__ob__.dep.notice()。
- 最后别忘了在notice()之前调用defineReactive把新的属性也定义成响应式。
class Observer {
constructor(data) {
this.dep = new Dep() // $set
if(Array.isArray(data)) {
data.__proto__ = ArrayMethods;
this.observeArray(data);
} else {
this.walk(data)
}
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false,
configurable: true,
writable: true
})
}
walk(data) {
let keys = Object.keys(data);
for(let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
observeArray(arr) {
for(let i = 0; i <arr.length; i++) {
observe(arr[i])
}
}
}
function observe(data) {
// data是基本类型则返回
let type = Object.prototype.toString.call(data);
if(type !== '[object Object]' && type !== '[object Array]') {
return
}
if(data.__ob__) {
return data.__ob__ // $set
}
return new Observer(data);
}
function defineReactive(obj, key, value) {
let childOb = observe(obj[key])
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
dep.depend();
if(childOb) {
childOb.dep.depend(); // $set
}
console.log(`获取data的${key}值`)
return value
},
set: function reactiveSetter(val) {
console.log(`data的${key}发生了改变`)
if(val === value) {
return
}
dep.notice();
value = val;
}
})
}
class Vue {
constructor(options) {
this.$options = options
// TODO:data可能是函数
this._data = options.data
this.initData();
}
initData() {
let data = this._data;
let keys = Object.keys(data);
for(let i = 0; i < keys.length; i++) {
// 数据代理,实现vm.data
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
console.log('数据代理get')
return data[keys[i]]
},
set: function proxySetter(value) {
console.log('数据代理set')
data[keys[i]] = value;
}
})
}
observe(data);
this.initWatch();
}
initWatch() {
let watch = this.$options.watch;
if(watch) {
let keys = Object.keys(watch);
console.log(this)
for(let i = 0; i < keys.length; i++) {
new Watcher(this, keys[i], watch[keys[i]]);
}
}
}
$watch(key, cb) {
new Watcher(this, key, cb);
}
$set(target, key, value) { // $set
defineReactive(target, key, value); // 新增的属性也变成响应式。
target.__ob__.dep.notice();
}
}
6. 侦测数组的变化
- 因为前面已经说了,使用object.defineProperty的办法劫持数组,会存在问题(使用key索引)。所以在实现数据劫持的时候,数组本身不用管,而是去循环劫持数组的元素,因为元素也有可能是对象。 实现方法:数组的回调也通过__ob__.dep来收集,在数组调用push,pop等方法时手动去触发__ob__.dep.notice。
- 原型对象Array.prototype上的方法不能直接修改,因为这样会破坏其他用到这些方法的代码的功能。 实现方法:在数组和Array.prototype的原型链上插入一个自定义的对象,拦截原来的push等方法,在自定义对象中的同名方法中先执行原本对的方法,再去人为的调用__ob__.dep.notice()去执行之前收集的回调。
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype;
const methods = [
'push',
'pop'
// 其他需要拦截的方法
]
methods.forEach(method => {
ArrayMethods[method] = function (...args) {
// args [1,2,3]
if(method === 'push') {
this.__ob__.observeArray(args);
}
const result = Array.prototype[method].apply(this, args)
this.__ob__.dep.notice()
return result
}
})