Vue2的数据响应式是基于Object.defineProperty开实现的,因此我们要先学习这个方法。
Object.defineProperty()方法
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
兼容性
先来看看兼容性,Object.defineProperty是ECMAScript 5 新增的语法,IE9 开始完全兼容,基本上2011年后主流的浏览器都支持该语法。
使用方法
let obj = {};
Object.defineProperty(obj, "a",{
value: "123"
})
console.log(obj) // { a : 123 }
可以看出Object.defineProperty接受三个参数,第一个参数为要定义的对象,第二个参数为要定义或者修改的属性,第三个参数为属性的描述。
描述对象支持以下几个参数:
- writable 表示属性是否可以被修改
- enumerable 表示属性是否可以被枚举
- configurable 表示属性的描述符是否能够被改变
- value 表示属性的值
- get 函数,访问该属性时,会调用此函数
- set 函数,属性值被修改时,会调用此函数
writable
表示是否可写,默认为true。
let obj = {};
Object.defineProperty(obj, "a", {
value: 123,
writable: false
})
Object.defineProperty(obj, "b", {
value: 123,
writable: true
})
obj.a++;
obj.b++;
console.log(obj) // {a: 123, b: 124}
enumerable
表示属性是否可以被枚举,默认为false。设置为false之后将无法被for ... in ...
进行遍历到。
let obj = {};
Object.defineProperty(obj, "a", {
value: 1,
enumerable: true
})
Object.defineProperty(obj, "b", {
value: 2,
enumerable: true
})
Object.defineProperty(obj, "c", {
value: 3,
enumerable: false
})
for (key in obj) {
console.log(key)
}
// 这里只会输出a和b, c不会被遍历到
// a
// b
console.log(obj) // {a: 1, b: 2, c: 3}
configurable
表示属性的描述符是否能够被改变,例如被删除。
get与set函数
重点来了!!!get 和 set 是实现响应式的核心函数。
- get函数,访问该属性时,会调用此函数,该函数的返回值会被用做属性的值。
- set函数,属性值被修改时,会调用此函数,该函数接受被赋予的新值作为参数。
let obj = {};
Object.defineProperty(obj, "a", {
enumerable: true,
configurable: true,
get() {
console.log("访问a");
return 1;
},
set(a) {
console.log("设置a", a);
}
})
obj.a = 10;
console.log(obj.a)
// 会输出以下内容
/*
设置a 10
访问a
1
*/
我们会发现,无论我们给obj.a赋予任何值,obj.a会永远返回1,这是因为get()的返回值为1。
封装defineReactive()函数
如果我们想去实现一个数据劫持,会发现不太好用,因为我们需要在外部定义一个变量并使用get和set来访问和修改它。所以我们可以去定义一个defineReactive函数,创建出一个闭包环境来存放这个变量。
let obj = {};
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
console.log("访问a", value);
return value;
},
set(newValue) {
console.log("设置a为", newValue);
value = newValue;
}
})
}
defineReactive(obj, "a", 1);
console.log(obj.a)
obj.a = 2;
console.log(obj.a)
//结果是
/*
访问a 1
1
设置a为 2
访问a 2
2
*/
递归侦测对象所有属性
我们将defineReactive函数封装在一个js文件以方便调用:
defineReactive.js
import observe from "./observe.js"
export default function defineReactive(data, key, value) {
if (arguments.length == 2) {
value = data[key]
}
// 子元素要进行observe,这里有递归
let childObj = observe(value);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log(`正在读取${key}属性`);
return value;
},
set(newValue) {
console.log(`正在设置${key}属性`);
value = newValue;
// 当设置了新值,这个新值也要被observe
childObj = observe(newValue);
}
})
}
定义observe方法来让一个对象变成响应式。
observe.js
import Observer from "./Observer.js"
// 这个函数本质上是在给这个对象身上加上__ob__属性
export default function observe(value) {
if (typeof value !== "object") return;
let ob;
if (typeof value.__ob__ !== "undefined") {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob;
}
观察者类
Observer.js
import { def } from './utils'
import defineReactive from "./defineReactive.js"
import observe from './observe.js';
export default class Observer {
constructor(value) {
// 给value添加一个属性,值为创建出来的Observer对象
def(value, "__ob__", this, false); // false 不希望这个属性能被枚举出来
this.walk(value);
}
walk(value) {
for (let key in value) {
defineReactive(value, key)
}
}
}
工具方法,用于定义一个属性
utils.js
function def(data, key, value, enumerable) {
Object.defineProperty(data, key, {
value,
enumerable,
configurable: true,
writable: true
})
}
export {
def
}
数组的响应式处理
数组响应式是通过改写7个会修改数组的方法来实现的。核心语句是 Object.setPrototypeOf(value, arrayMethods),用于替换Array实例身上的__proto__。
array.js
import { def } from "./utils.js"
const arrayPrototype = Array.prototype;
// 以arrayPrototype为原型,创建对象
const arrayMethods = Object.create(arrayPrototype);
const arrayMethodNames = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
]
arrayMethodNames.forEach(methodName => {
const originalMethod = arrayPrototype[methodName];
def(arrayMethods, methodName, function() {
console.log("数组操作中...")
let ob = this.__ob__;
// 有三种方法能插入新项目,要把新的项目也变成响应式
let insertedArr;
const args = [...arguments]
switch (methodName) {
case "push":
case "unshift":
insertedArr = args;
break;
case "splice":
insertedArr = args.splice(2);
break;
}
if (insertedArr.length > 0) {
ob.observeArray(insertedArr)
}
return originalMethod.apply(this, arguments)
}, false)
})
export { arrayMethods }
Observer类要新增相应的处理
observer.js
import { def } from './utils'
import defineReactive from "./defineReactive.js"
import { arrayMethods } from "./array.js"
import observe from './observe.js';
export default class Observer {
constructor(value) {
// 给value添加一个属性,值为创建出来的Observer对象
def(value, "__ob__", this, false); // false 不希望这个属性能被枚举出来
if (Array.isArray(value)) {
Object.setPrototypeOf(value, arrayMethods);
// 如果这个值为数组,那么要遍历它的每一项使之成为响应式
this.observeArray(value)
} else {
this.walk(value);
}
console.log(value)
}
walk(value) {
for (let key in value) {
defineReactive(value, key)
}
}
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}
依赖收集
核心概念:在getter
中收集依赖,在setter
中触发依赖.
每一个Observer实例身上都应该有个一个dep。