数据变化:
1、Object.defineProperty数据劫持:
每次访问或修改对象某个属性时,都能够被get或set捕捉到,从而能够进行响应式的处理
Object.defineProperty(obj, 'a', {
// value: 3, 不能同时指定get当有value/writable属性,set同理
get() { 当访问obj对象的a属性时,会进入到getter中,相当于被劫持了
return 4;
},
set(newValue){
}
})
弊端:
虽然能数据劫持,但是修改属性时,set并不能完成对数据的修改,调用this.a来修改会因为循环进入set而死循环,最终返回的值依旧是get中设置的值,相当于修改无效
解决方式:在外部定义一个变量,set中将新值赋给变量,get中返回变量
2、包装原始数据劫持:
使用闭包,将上述的变量保存起来
function defineReactive(obj,key,val) {
if(arguments.length==2){ 未传入第三个参数,赋值属性本身的值
val=obj[key];
}
Object.defineProperty(obj, key, {
get() {
return val;
},
set(newVal) {
if (newVal == val) {
return;
}
val = newVal;
},
enumerable: true,
configurable:true
})
}
defineReactive(obj, 'a', 0);
console.log(obj.a);
obj.a = 5;
console.log(obj.a);
弊端:无法劫持更深层次的属性,如obj.a.b
3、相互引用来实现递归遍历对象每层属性,将所有属性转换成响应式(调用能够被劫持):
循环调用顺序observe->Observer->defineReactive->observe...
observe方法:将属性对象交给Observer,属性不为对象则返回空,为对象则返回对应的Observer实例
Observer类:将属性对象添加__ob__属性并在__ob__上挂载一个Observer实例,并调用实例的walk方法遍历对象属性并调用defineReactive传递属性
def:Observer类内部调用,给属性添加__ob__属性并赋值当前Observer实例,并将__ob__变成不可枚举的一个方法
defineReactive方法:若属性不为对象,将属性变为响应式,否则继续交给observe递归调用
observe.js:
import Observer from './observer';
//辅助判别函数,将传入的对象添加__ob__属性,并将对象每个层级属性转换成响应式的
export default function observe(value) {
if (typeof value != 'object') { //属性值是基础类型直接返回
return;
}
var ob;
if (typeof value.__ob__!=='undefined') { //vue中, __ob__会指向一个Observer对象,每个被双向绑定的对象元素(数组也是对象)都会有一个__ob__ ,而且是单例的
ob = value.__ob__;
} else {
ob = new Observer(value); //将传入的对象转换为每个层级都是响应式的(可以被数据劫持侦测的)的对象
}
return ob;
}
Observer类:
import { def } from './util'
import defineReactive from './defineReactive'
export default class Observer{ //将传入的对象转换为每个层级都是响应式的(可以被数据劫持侦测的)的对象
constructor(value) {
console.log('observer');
//给传入的对象添加不可枚举属性__ob__,避免遍历时获取到,并将将__ob__属性值设置为当前实例this
def(value, '__ob__', this, false);
this.walk(value);
}
walk(value) { //遍历
for (let k in value) {
defineReactive(value, k); //将对象属性转换成可被数据劫持的对象
}
}
}
def方法:
export const def = function (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable,
writable: true,
configurable:true
})
}
defineReactive:
import observe from './observe'; //辅助判别函数,将传入的对象添加__ob__属性,并将对象每个层级属性转换成响应式的
export default function defineReactive(obj, key, value) {
if (arguments.length == 2) {
value = obj[key];
}
//会产生递归的效果,会将当前属性值中的内层属性遍历,内层属性为了变成响应式又会调用defineReactive方法,直到当前属性内层的属性不是个对象为止,才会走后续的操作,然后递归进行返回走之后的操作
//循环调用顺序observe->Observer->defineReactive->observe...
//observe:将属性对象交给Observer,属性不为对象返回
//Observer:将属性对象添加__ob__属性并在__ob__上挂载一个Observer实例,并调用实例的walk方法遍历对象属性调用defineReactive
//defineReactive:若属性不为对象,将属性变为响应式,否则继续交给observe递归调用
let childOb=observe(value); //返回属性对象对应的Observer实例
Object.defineProperty(obj, key, {
get() {
console.log('访问obj的'+key+'属性')
return value;
},
set(newValue) {
console.log('修改obj的' +key+'属性为'+newValue)
if (newValue == value) {
return;
}
value = newValue;
//如果赋的值是一个对象,则赋值后对象内部属性也应该是响应式的
childOb=observe(newValue);
},
enumerable: true,
configurable:true
})
}
将传入的对象每层属性都变成响应式,index.js:
import observe from './observe';
var obj = {
a: {
m: {
n: 5
}
},
b:4
};
observe(obj);
console.log(obj);
obj.a.m.n++;
将会打印:
实现数组的响应式:
1、通过上一部分,能够将数组变成响应式,即访问修改能够侦测到
2、但通过.push方法添加元素,因为并没有进行赋值的操作,所以没有触发set,不能响应式侦测
解决方法:
修改数组的方法调用,使得调用时能够被侦测
(1)定义一个方法,若属性是一个数组,则调用该方法,将修改后的'push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice'等方法挂载到数组原型上,使得数组调用对应方法时,调用这些方法
1、通过Object.create可以创造一个原型指向原生数组原型的对象,即新对象具有原生数组的所有方法
2、为新对象添加'push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice'对应的属性,并重写方法以及设置成不可枚举
使用上一部分中的def方法
3、因为新对象会被挂载到数组原型上,所以this会指向数组,数组调用上述方法时会优先调用新对象中重写的方法
因此可通过arguments获取到调用时的参数,意味着可通过调用原生的对应方法实现和原生方法相同效果,且因为为自定义方法,使得调用时能被我们知道,即被侦测
4、因为数组对象上绑定了__ob__属性,即Observer实例,就可调用实例上的observeArray方法,并将添加的参数传入,使得参数中的数组和对象变成响应式的
observeArray方法:遍历数组的元素,将元素传入observe中,使得对象元素被侦测,数组元素的原型方法修改为自定义的七种方法,普通元素不处理,实现响应式
(2)修改Observer类,添加如果传入的是数组判断,如果是数组则修改数组的原型为修改后的对象,并调用observeArray方法,遍历数组元素,将元素传入observe中,使得对象元素被侦测,数组元素的原型方法修改为自定义的七种方法,普通元素不处理,实现响应式
Object.setPrototypeOf能够修改对象原型的指向
解释了为什么vue中,数组[下标]=值视图不能刷新,但若数组[下标]为对象,数组[下标]=值/或数组[下标].属性=值能够被响应,数组.push等七种指定方法能够被追踪刷新视图
array.js,暴露修改了七种方法的对象,将被挂载到数组对象的原型上:
import {def} from './util'
//被改写的七个数组方法
const methods = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']
//获取原生数组原型链方法
const arrayPrototype = Array.prototype;
//创造一个原型链为原生数组原型链的对象,不能直接使用原型对象赋值,否则将直接修改原生原型链上的方法
//这样的目的是,如果使用改写的方法,则直接调用对象
//并将其暴露,在Obsever类中,修改数组的原型指向
export const arrayMethods = Object.create(arrayPrototype);
methods.forEach(method => {
//备份原来的方法
const original = arrayPrototype[method]; //直接执行原函数,会因为无上下文而将内部的this指向window而报错
//取出数组身上的在observe方法调用后添加的__ob__属性,即Observer实例
// console.log(this);
//定义新的方法
def(arrayMethods, method, function () {
//因为this指向arrayMethods对象,而arrayMethods是数组对象的原型,在调用时是数组在调用该方法,所以this又指向了数组本身
//取出数组身上的在observe方法调用后添加的__ob__属性,即Observer实例
const ob = this.__ob__;
//将类数组变成数组,使其具有数组的方法
let transArguments = Array.from(arguments);
//能够添加新元素的三种方法push、spilce、unshift,添加的元素都应该是响应式的
let inserted = [];
switch (method) {
case "push": //push、unshift参数都相同
case "unshift":
inserted = transArguments;
break;
case "splice":
inserted = transArguments.slice(2); //splice从第二参数之后才是添加的元素
break;
}
//将新元素变成响应式
if (inserted.length) {
ob.observeArray(inserted);
}
//arguments为调用该方法传入的参数
let res=original.apply(this, arguments); //调用原数组方法,使得功能未改变
console.log('new me')
return res;
}, false);
})
Observer类,添加了判断属性是数组的清空:
添加了能够将数组原型指向修改后的对象上,和遍历素组元素,递归检测的方法
import { def } from './util'
import defineReactive from './defineReactive'
import {arrayMethods} from './array'
import observe from './observe';
export default class Observer{ //将传入的对象转换为每个层级都是响应式的(可以被数据劫持侦测的)的对象
constructor(value) {
//给传入的对象添加不可枚举属性__ob__,避免遍历时获取到,并将将__ob__属性值设置为当前实例this
def(value, '__ob__', this, false);
//检查属性是否是数组
if (Array.isArray(value)) {
//是数组,修改数组的原型为修改后的对象
Object.setPrototypeOf(value, arrayMethods); //使得调用数组的七种指定方法能够被侦测
//遍历数组元素调用observe方法,只将数组中的对象变成响应式的,数组中的数组调用七种指定方法能够侦测,普通值不响应
//解释了为什么vue中,数组[下标]=值视图不能刷新,但若数组[下标]为对象,数组[下标]=值/或数组[下标].属性=值能够被响应,数组.push等七种指定方法能够被追踪刷新视图
this.observeArray(value);
} else { //如果是对象
this.walk(value);
}
}
walk(value) { //遍历
for (let k in value) {
defineReactive(value, k); //将对象属性转换成可被数据劫持的对象
}
}
observeArray(arr) {
for (let i = 0,l=arr.length; i < l; i++){ //循环检测每一项是否是数组或对象,普通值会退出
observe(arr[i]);
}
}
}
在setter外部实现依赖改变监听(依赖:状态数据),即Vue中的watch函数/计算属性等依赖监听实现
收集监听器需要当监听器开启时(即 new Watcher后)再进行,所以需要一个全局标识,这里往Dep类上添加一个target属性,Dep.target默认为null,当new Watcher后赋值为Watcher实例作为标识
1、创建一个Dep类,用来保存watcher监听器实例,并提供一个方法来添加监听器,一个方法来通知监听器再次获取值
当非数组的普通值元素(包括数组本身)进行修改和访问时,都会进入set和get内,所以在get中收集监听器依赖,在set中通知收集的监听器再次获取要监听的属性值
当数组调用如.push方法时,因为不会进入get,而是进入之前修改后的自定义push中(在array.js中),所以在自定义的方法中通知收集的监听器再次获取要监听的属性值
而因为自定义修改的方法能通过this.__ob__访问到Observer实例,而在set之前也会调用observe来返回对应的Observer实例,所以在set方法中,对于数组对象,将监听器存放在对应的Observer实例的dep属性上,dep属性也是一个Dep实例,
因此就能通过调用Observer实例上的dep属性来通知存储的监听器来再次获取要监听的属性值
2、创建一个Watcher监听器类,传入要监听的对象、要监听的对象的值的属性链(如:"a.b.c",a为属性)、对象值修改后的回调
当首次实例化监听器时,就会将对象上的值存在监听器实例上,又因为会根据属性链依次访问属性的get,所以每次访问都会将该监听器实例存储进dep中
Watcher还会提供一个update方法,用来再次获取要监听的属性值,会在Dep通知监听器后触发
Dep类:
var uid = 0;
export default class Dep{
constructor() {
this.id = uid++; //唯一id
//订阅的是watcher的实例
this.subs = [];
}
addSub(sub) { //添加订阅
// console.log(sub);
this.subs.push(sub);
}
depend() { //添加依赖
//自己指定的开始收集监听器依赖的全局标识(new Watcher后),这里指定为指定了属性链的Watcher实例
if (Dep.target) {
this.addSub(Dep.target);
}
}
notify() { //通知更新
console.log('notify');
//浅克隆
const subs = this.subs.slice();
console.log(subs);
for (let i = 0, l = subs.length; i < l; i++){
subs[i].update();
}
}
}
Watcher监听器类:
import Dep from "./dep";
var uid = 0;
export default class Watcher{
constructor(target,expression,callback) { //target:要监听的对象,expression:'a.b.c'属性调用链,callback:c属性改变时的回调
this.id = uid++; //id标识
this.target = target;
this.getter = parsePath(expression);//传入"a.b.c",解析返回{a:{b:{c:4}}}中c的值
this.callback = callback;
this.value = this.get();//实例化时,就会获取一次属性调用链指定的属性值,会进入每个属性调用链上的属性的get中,所以如a.b.c,当c之前的任意属性更改,都能触发wather的更新update函数,重新获取值
}
update() { //重新获取指定expression下的对象的属性值
this.run()
}
run() {
this.getAndInvoke(this.callback);
}
getAndInvoke(callback) { //重新调用get方法,获取指定expression下的对象的属性值
const value = this.get();
if (value !== this.value || typeof value == 'object') { //当重新获取的值和上一次值不相同时,更新值,并触发回调
const oldValue = this.value;
this.value = value;
callback.call(this.target, value, oldValue);//调用对象为要监听的对象,value为新值,oldValue为之前的值
}
}
get() {//获取指定expression下的对象的属性值
//进入依赖收集,即全局Dep.target设置为watcher实例,能调用其中方法
Dep.target = this;
const obj = this.target;
let value;
try {
value =this.getter(obj); //会进入每个属性调用链上的属性的get中,所以如a.b.c,当c之前的任意属性更改,都能触发wather的更新update函数,重新获取值
} catch (e) {
} finally {
Dep.target = null; //退出依赖收集,让给别的watcher
}
return value;
}
}
function parsePath(str) { //传入"a.b.c",返回一个函数用来解析返回{a:{b:{c:4}}}中c的值,最后返回的函数的返回值为c的值
let segments = str.split('.');
return (obj) => {
for (let i = 0; i < segments.length; i++){
if (!obj) {
return;
}
obj = obj[segments[i]];
}
return obj;
}
}
修改Observer类,添加dep实例属性,因为每个数组对象都有__ob__属性,即Observer实例,所以将监听器放在添加的dep属性上,dep是个Dep实例,当数组调用.push方法等,会进入修改后的方法,修改后的方法能获取到Observer实例,从而获取到dep上存储的监听器,从而通知监听器再次获取指定属性的值:
import { def } from './util'
import defineReactive from './defineReactive'
import {arrayMethods} from './array'
import observe from './observe';
import Dep from './dep';
export default class Observer{ //将传入的对象转换为每个层级都是响应式的(可以被数据劫持侦测的)的对象
constructor(value) {
//会为每个对象绑定一个对应的__ob__属性,即Observer实例,故在此收集的监听器可以提供给修改后的数组方法中能够访问
this.dep = new Dep();
//给传入的对象添加不可枚举属性__ob__,避免遍历时获取到,并将将__ob__属性值设置为当前实例this
def(value, '__ob__', this, false);
//检查属性是否是数组
if (Array.isArray(value)) {
//是数组,修改数组的原型为修改后的对象
Object.setPrototypeOf(value, arrayMethods); //使得调用数组的七种指定方法能够被侦测
//遍历数组元素调用observe方法,只将数组中的对象变成响应式的,数组中的数组调用七种指定方法能够侦测,普通值不响应
//解释了为什么vue中,数组[下标]=值视图不能刷新,但若数组[下标]为对象,数组[下标]=值/或数组[下标].属性=值能够被响应,数组.push等七种指定方法能够被追踪刷新视图
this.observeArray(value);
} else { //如果是对象
this.walk(value);
}
}
walk(value) { //遍历
for (let k in value) {
defineReactive(value, k); //将对象属性转换成可被数据劫持的对象
}
}
observeArray(arr) {
for (let i = 0,l=arr.length; i < l; i++){ //循环检测每一项是否是数组或对象,普通值会退出
observe(arr[i]);
}
}
}
修改defineReactive,使得非数组的普通值元素每次get/set都能触发收集监听器/触发监听器再次更新,以及将监听器收集在Observer实例的dep上,便于.push等自定义函数中获取到
import observe from './observe'; //辅助判别函数,将传入的对象添加__ob__属性,并将对象每个层级属性转换成响应式的
import Dep from './dep';
export default function defineReactive(obj, key, value) {
const dep = new Dep();
if (arguments.length == 2) {
value = obj[key];
}
//会产生递归的效果,会将当前属性值中的内层属性遍历,内层属性为了变成响应式又会调用defineReactive方法,直到当前属性内层的属性不是个对象为止,才会走后续的操作,然后递归进行返回走之后的操作
//循环调用顺序observe->Observer->defineReactive->observe...
//observe:将属性对象交给Observer,属性不为对象返回
//Observer:将属性对象添加__ob__属性并在__ob__上挂载一个Observer实例,并调用实例的walk方法遍历对象属性调用defineReactive
//defineReactive:若属性不为对象,将属性变为响应式,否则继续交给observe递归调用
let childOb=observe(value); //返回属性对象对应的Observer实例
Object.defineProperty(obj, key, {
get() {
console.log('访问obj的' + key + '属性')
if (Dep.target) { //如果处于依赖收集阶段,即new Watcher了,因为会先调用new Watcher,其中的get方法,会根据属性链(如:"a.b.c"),挨个进入getter,并在各自的闭包中保存watcher实例
//使得"a.b.c",若监听的是.c,则c之前任意一个属性变化,都会触发
dep.depend(); //将对应的watcher实例保存在当前属性的set闭包中
if (childOb) { //如果当前属性是个数组,则不会触发set,为了在.push等方法中触发监听,所以将watcher实例保存在,数组对象的Observer实例的dep属性上,在修改后的数组的方法(array.js),如push,又能通过this.__ob__获取到Observer实例,继而能够通过Observer实例获取到dep实例,然后调用dep实例上的方法通知watcher来更新监听
childOb.dep.depend();//将watcher实例保存在当前对象属性的Observer实例的dep实例属性中
}
}
return value;
},
set(newValue) {
console.log('修改obj的' +key+'属性为'+newValue)
if (newValue == value) {
return;
}
value = newValue;
//如果赋的值是一个对象,则赋值后对象内部属性也应该是响应式的
childOb = observe(newValue);
//发布订阅模式,当对象属性改变时,通知dep改变,这里无法通知到数组.push等调用的改变,需要在array.js中,重写的push等方法中进行通知
dep.notify()
},
enumerable: true,
configurable:true
})
}
修改array.js,因为数组的push等方法会进入这里,所以在这里获取Observer实例上保存的dep属性上保存的监听器,并通知监听器再次获取值
import {def} from './util'
//被改写的七个数组方法
const methods = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']
//获取原生数组原型链方法
const arrayPrototype = Array.prototype;
//创造一个原型链为原生数组原型链的对象,不能直接使用原型对象赋值,否则将直接修改原生原型链上的方法
//这样的目的是,如果使用改写的方法,则直接调用对象
//并将其暴露,在Obsever类中,修改数组的原型指向
export const arrayMethods = Object.create(arrayPrototype);
methods.forEach(method => {
//备份原来的方法
const original = arrayPrototype[method]; //直接执行原函数,会因为无上下文而将内部的this指向window而报错
//取出数组身上的在observe方法调用后添加的__ob__属性,即Observer实例
// console.log(this);
//定义新的方法
def(arrayMethods, method, function () {
//因为this指向arrayMethods对象,而arrayMethods是数组对象的原型,在调用时是数组在调用该方法,所以this又指向了数组本身
//取出数组身上的在observe方法调用后添加的__ob__属性,即Observer实例
const ob = this.__ob__;
//将类数组变成数组,使其具有数组的方法
let transArguments = Array.from(arguments);
//能够添加新元素的三种方法push、spilce、unshift,添加的元素都应该是响应式的
let inserted = [];
switch (method) {
case "push": //push、unshift参数都相同
case "unshift":
inserted = transArguments;
break;
case "splice":
inserted = transArguments.slice(2); //splice从第二参数之后才是添加的元素
break;
}
//将新元素变成响应式
if (inserted.length) {
ob.observeArray(inserted);
}
//当数组调用了修改后的方法时,也通知改变
ob.dep.notify();
console.log('new me')
//arguments为调用该方法传入的参数
let res = original.apply(this, arguments); //调用原数组方法,使得功能未改变
return res;
}, false);
})
index.js调用监听器:
import defineReactive from './defineReactive';
import observe from './observe';
import Watcher from './watcher';
var obj = {
a: {
m: {
n: 5,
g:7
}
},
b: 4,
c:[1,2,3]
};
observe(obj);
new Watcher(obj, 'a.m.n', (val) => {
console.log("***watcher监听***",val);
})
obj.a.m.n= 4;
// obj.a.m.g = 9;
// obj.b = 5;
会打印输出监听修改的结果: