目录
Object.defineProperty() 方法
- Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
const object1 = {};
// 在object1对象上定义property1属性的值value为42
Object.defineProperty(object1, 'property1', {
value: 42// value代表属性的值
});
object1.property1 = 77;// 默认不能改变属性值
console.log(object1.property1);// 42
- 为什么要用Object.defineProperty() 方法 你用
object1.property1=42
不是更简洁吗:我们可以使用Object.defineProperty() 方法来定义一些隐藏的属性
(1)writable:当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符 改变。默认为 false。
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: true
});
object1.property1 = 77;
console.log(object1.property1);// 77
(2)enumerable:当且仅当该属性的 值为 true 时,该属性才会出现在对象的枚举属性中 才可以用for…in…遍历。默认为 false。
- get:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为 undefined。
set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined。
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42// value代表属性的值
});
Object.defineProperty(object1, 'property2', {
get(){
console.log('你在访问object1的property2属性')
},
set(){
console.log('你在设置object1的property2属性')
}
});
console.log(object1);// property1: 42因为你没给property2赋值
object1.property2 = 10;
// 你在访问object1的property2属性
// 你在设置object1的property2属性
console.log(object1.property2)// undefined
相当于我们自定义了object1.property2取值和赋值的行为,使用自定义的getter和setter来重写了原有的行为,这也就是数据劫持的含义。
defineReactive函数
getter函数的返回值会被用作属性的值 setter函数接受一个参数(也就是被赋予的新值)
const object1 = {};
Object.defineProperty(object1, 'property2', {
get(){
console.log('你在访问object1的property2属性')
return 7;
},
set(newValue){
console.log('你在设置object1的property2属性', newValue)
}
});
console.log(object1.property2)// 7
object1.property2 = 10;
// 你在访问object1的property2属性
// 你在设置object1的property2属性 10
console.log(object1.property2)// 7
为什么返回7而不是10 我明明给object1.property2赋新值了 因为getter中并不能返回你刚修改的值 你只要一访问object1.property2 就会调用get() 返回7 为了解决这个问题 我们需要用一个变量来周转一下
const object1 = {};
let temp;
Object.defineProperty(object1, 'property2', {
get(){
console.log('你在访问object1的property2属性')
return temp;// 用到闭包
},
set(newValue){
console.log('你在设置object1的property2属性', newValue)
temp = newValue;// 用到闭包
}
});
console.log(object1.property2)// undefined
object1.property2 = 10;
// 你在访问object1的property2属性
// 你在设置object1的property2属性 10
console.log(object1.property2)// 10
Object.defineProperty需要变量周转才能使用setter getter 我们把变量和Object.defineProperty封装一下
const object1 = {};
// data:对象 key:对象的属性 val:变量
function defineReactive(data, key, val){
Object.defineProperty(data, key, {
get(){
console.log('你在访问object1的property2属性')
return val;// 用到闭包
},
set(newValue){
console.log('你在设置object1的property2属性', newValue)
if(val === newValue) return;
val = newValue;// 用到闭包
},
// 你也可以在里面配置一些属性
enumerable: true;
configurable: true// 是否可以被删除
});
}
defineReactive(object1, property2, 7)
console.log(object1.property2)// 7
object1.property2 = 10;
console.log(object1.property2)// 10
defineReactive函数提供了闭包 你可以不另外使用临时变量来使用setter getter
递归侦测对象全部属性
上面只能让property2中的值实现响应式 如果property2是个对象 property2中的对象的值其实不能实现响应式的 不能进行get set 下面我们来实现一个类Observer 它可以将一个正常的object转换为每个层级的属性都是响应式的(可以被侦测到)的object 思路如下图
- 主文件index.js中的代码
import observe from './observe.js';
let obj = {
a: {
m: {
n: 5;
}
},
b: 10
}
observe(obj);
obj.a.m = 10;
- observe.js中的代码
import Observer from './Observer.js';
export const observe = function(value) {
// 如果value不是对象 不操作
if(typeof value != 'object') return;
let ob;
// 如果对象上有__ob__这个属性 说明他属于Observer这个类 已经被监控了
// __ob__属性其实就是来标识这个对象是否被劫持 对象上有这个属性说明该对象及其属性都被劫持了
if(typeof value.__ob__ !== 'undefined') ob = value.__ob__;
else ob = new Observer(value);
return ob;
}
- 创建一个Observer 类 你要想的第一件事:该类如何被实例化
import defineReactive from './defineReactive.js';
import {def} from './utils.js';
export default class Observer{
constructor(value){
// 给实例添加__ob__属性 值是这次new的实例 注意:构造函数中的this指向实例
def(value,'__ob__',this,false);
// Observer:将一个正常的object转换为每个层级的属性都是响应式的(可以被侦测到)的object
this.walk(value);
}
// 遍历value对象 给value对象的每个属性实现数据劫持
walk(value){
for(let k in value){
defineReactive(value,k)
}
}
}
- 定义一个utils.js文件 用以给某个对象obj添加__ob__属性key __ob__属性的值是value __ob__属性不可遍历 所以定义enumerable
export const def = function(obj,key,value,enumerable){
Object.defineProperty(obj,key{
value,
enumerable,
writable: true,
configurable: true
})
}
- defineReactive中的代码
export default function defineReactive(data, key, val){
if(arguments.length === 2) val = data[key];
// 子元素要进行observe 至此形成递归 这个递归不是自己调用自己 而是多个函数、类循环调用
let childOb = observe(val);
Object.defineProperty(data, key, {
get(){
return val;// 用到闭包
},
set(newValue){
if(val === newValue) return;
val = newValue;// 用到闭包
childOb = observe(val);// 当设置了新值 新值也要被observe
},
// 你也可以在里面配置一些属性
enumerable: true;
configurable: true// 是否可以被删除
});
}
数组的响应式处理
- 上面的代码只能劫持对象和值 不能劫持数组 数组中的数据变了是检测不到的 vue通过改写数组的pop push sort reverse splice shift unshift解决了这个问题 但是其他的数组方法以及数组操作依旧检测不到
- 这七个方法都是在Array.prototype上的 任何
Array.
调用的方法都在Array.prototype上 - 如何改写 希望保持这7个方法的功能 又希望能拓展 让这七个方法能触发响应式:备份原来的方法 在新方法中调用原来的方法 然后再写一些其他功能
- 思路:以Array.prototype为原型 创建arrayMethods对象 使用
Object.setPrototypeOf(数组, arrayMethods)
让数组的__proto__指向arrayMethods对象 这样就能触发重写的函数
- array.js文件中就封装了7个方法 重写了push、unshift、splice;改写的原因是因为这三个方法可能会向数组添加对象,而现有的监听方法是没有办法对这些新增的对象进行监听 代码如下
import {def} from './utils.js';// 我们会用到def来给对象添加属性
const arrayPrototype = Array.prototype;// 得到Array.prototype
// 以Array.prototype为原型创建arrayMethods对象并暴露
export const arrayMethods = Object.create(arrayPrototype);
const methodsNeedChange = [// 要被改写的7个数组方法
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsNeedChange.forEach(methodName => {
const original = arrayPrototype[methodName];// 备份原来的方法
def(arrayMethods, methodName, function(){// 定义新的方法
// 让上下文指向当前this(调用方法的数组) 执行原来的方法 恢复原来的功能
const result = original.apply(this,arguments);
// 为什么不用original()执行:因为original()的上下文指向window original()相当于window.original() 但是original()不是window的方法 所以要用apply调用
// 把类数组变成数组
let arg = [...arguments];
// 把这个数组身上的__ob__取出来 __ob__已经被添加了 因为数组肯定不是最高层 比如obj.g属性是数组 obj不能是数组 第一次遍历obj这个对象的第一层的时候 已经给g属性(就是这个数组)添加了__ob__属性
const ob = this.__ob__;
// push splice unshift可以插入新元素 现在要把插入的新元素也变成observe的
let inserted = [];// 用来存放添加的新元素
switch(nethodName){
case 'push':
case 'unshift':
inserted = arg;
break;
case 'splice':
// splice(下标, 数量, 插入的新元素)
inserted = arg.slice(2);
break;
}
// 判断有没有要插入的新元素 让新元素也变为响应式的
if(inserted.length !== 0){
ob.observeArray(inserted);
}
return result;// 有些方法会返回被删除得值(如pop会返回弹出的值)所以这里得return一下
}, false);
});
- Observer中的代码
import defineReactive from './defineReactive.js';
import {def} from './utils.js';
import {arrayMethods} from './array.js';
import observe from './observe.js';
export default class Observer{
constructor(value){
// 给实例添加__ob__属性 值是这次new的实例 注意:构造函数中的this指向实例
def(value,'__ob__',this,false);
// Observer:将一个正常的object转换为每个层级的属性都是响应式的(可以被侦测到)的object
// 判断value是对象还是数组
if(Array.isArray(value)){// 如果是数组 将数组的原型__proto__指向arrayMethods
Object.setPrototypeOf(value, arrayMethods);
this.observeArray(value);// 劫持数组
}else{
this.walk(value);
}
}
// 遍历对象 给对象的每个属性实现数据劫持
walk(value){
for(let k in value){
defineReactive(value,k)
}
}
// 遍历数组 数组中的每个值都要被observe
observeArray(arr){
for(let i = 0, l = arr.length;i < l;i++){
observe(arr[i));
}
}
}
收集依赖
- 需要用到数据得地方 称为依赖
- vuew1.x中 细粒度依赖 用到数据的DOM都是依赖
vue2.x中 中等粒度依赖 用到数据得组件是依赖 当数据发生变化时 只用通知组件就行 组件使用diff算法 更新视图 - 在getter中收集依赖 在setter中触发依赖 依赖要收集起来 以备在数据变化时 通过循环遍历来通知所有的依赖
如何收集依赖:用到数据的地方会触发getter 谁触发getter了 谁就是我的依赖
- Dep类和Watcher类
touch代表用到了 模板引擎中用到了某数据 在该数据的getter中会使用dep.depend()收集依赖 在setter中使用dep.notify()触发依赖 实际上不管触发依赖还是收集依赖 触发的时watcher 收集的也是watcher watcher触发视图更新 组件使用虚拟节点和diff算法更写页面
代码实现的巧妙之处:watcher把自己设置到全局的一个指定位置 然后读取数据 因为读取了数据 所以会触发这个数据的getter 在getter中就能得到当前正在读取数据的watcher 并把这个watcher收集到Dep中 - (1)把依赖收集的代码封装成一个Dep类 他专门用来管理依赖 每个Observer实例的成员中都有一个Dep的实例
(2)Dep使用发布订阅模式 当数据发生变化时 会循环依赖列表 把所有的watcher都通知一遍
(3)Dep.js中的代码:
// 什么时候把watcher放入Dep中
let uid = 0;
export default class Dep{// 发布订阅模式
constructor(){
this.id = uid++;
this.sub = [];// 存储自己的订阅者 数组中存放的时watcher的实例
}
addSub(){// 添加订阅
this.subs.push(sub);
}
removeSub(sub) {// 删除订阅
remove(this.subs, sub);
}
depend() {// 添加依赖
// Dep.target是一个我们指定的全局的位置 用window.target也行 只要是全局唯一 没有歧义就行
if (Dep.target) {
this.addSub(Dep.target);
}
}
notify(){// 通知更新
const subs = this.subs.slice();// 浅拷贝
for(let i = 0, l = subs.length;i < l;i++){
subs[i].update();
}
}
}
function remove(arr, item) {// 从arr数组中删除元素item
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}
- (1)watcher是一个中介 数据发生变化时 通过watcher中转 通知组件
(2)watcher实例需要订阅(依赖)数据 也就是获取依赖或者收集依赖 只有watcher触发的getter才会收集依赖 哪个watcher触发了getter 就把哪个watcher收集到Dep中
(3)watcher的依赖发生变化时触发watcher的回调函数 也就是派发更新。
(4)Watcher.js中的代码
import Dep from "./Dep";
let uid = 0;
export default class Watcher {
constructor(target, expression, callback) {
this.id = uid++;// 用来区分Watcher Watcher的唯一标识
this.target = target;
// 按点拆分 执行this.getter()就可以读取data.a.b.c的内容
this.getter = parsePath(expression);
this.callback = callback;
this.value = this.get();
}
get() {
// 进入依赖收集阶段 让全局的Dep.target设置为Watcher本身
Dep.target = this;
const obj = this.target;
var value;
// 只要能找就一直找
try {
value = this.getter(obj);
} finally {
Dep.target = null;
}
return value;
}
update() {
this.run();
}
run() {
this.getAndInvoke(this.callback);
}
getAndInvoke(callback) {
const value = this.get();
if (value !== this.value || typeof value === "object") {
const oldValue = this.value;
this.value = value;
callback.call(this.target, value, oldValue);
}
}
}
// 将str用.分割成数组segments 然后循环遍历数组 一层一层去读取数据 最后拿到的obj就是str中想要读的数据
function parsePath(str) {
let segments = str.split(".");
return function (obj) {
for (let key of segments) {
if (!obj) return;
obj = obj[key];
}
return obj;
};
}
- 每个Observer实例的成员中都有一个Dep的实例 Observer中的代码
import defineReactive from './defineReactive.js';
import {def} from './utils.js';
import {arrayMethods} from './array.js';
import observe from './observe.js';
import Dep from './Dep.js';
export default class Observer{
constructor(value){
this.dep = new Dep();// 每个Observer实例身上都有一个dep
// 给实例添加__ob__属性 值是这次new的实例 注意:构造函数中的this指向实例
def(value,'__ob__',this,false);
// Observer:将一个正常的object转换为每个层级的属性都是响应式的(可以被侦测到)的object
// 判断value是对象还是数组
if(Array.isArray(value)){// 如果是数组 将数组的原型__proto__指向arrayMethods
Object.setPrototypeOf(value, arrayMethods);
this.observeArray(value);// 劫持数组
}else{
this.walk(value);
}
}
// 遍历对象 给对象的每个属性实现数据劫持
walk(value){
for(let k in value){
defineReactive(value,k)
}
}
// 遍历数组 数组中的每个值都要被observe
observeArray(arr){
for(let i = 0, l = arr.length;i < l;i++){
observe(arr[i));
}
}
}
- defineReactive中的代码
import observe from './observe.js';
import Dep from './Dep.js';
export default function defineReactive(data, key, val){
const dep = new Dep();
if(arguments.length === 2) val = data[key];
// 子元素要进行observe 至此形成递归 这个递归不是自己调用自己 而是多个函数、类循环调用
let childOb = observe(val);
Object.defineProperty(data, key, {
get(){
if(Dep.target){// 如果现在处于依赖收集阶段
dep.depend();
if(childOb){// 如果有子元素 子元素收集依赖
childOb.dep.depend();
}
}
return val;// 用到闭包
},
set(newValue){
if(val === newValue) return;
val = newValue;// 用到闭包
childOb = observe(val);// 当设置了新值 新值也要被observe
dep.notify();// 发布订阅模式 通知dep
}
});
}
- Array.js中的代码
import {def} from './utils.js';// 我们会用到def来给对象添加属性
const arrayPrototype = Array.prototype;// 得到Array.prototype
// 以Array.prototype为原型创建arrayMethods对象并暴露
export const arrayMethods = Object.create(arrayPrototype);
const methodsNeedChange = [// 要被改写的7个数组方法
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsNeedChange.forEach(methodName => {
const original = arrayPrototype[methodName];// 备份原来的方法
def(arrayMethods, methodName, function(){// 定义新的方法
// 让上下文指向当前this(调用方法的数组) 执行原来的方法 恢复原来的功能
const result = original.apply(this,arguments);
// 为什么不用original()执行:因为original()的上下文指向window original()相当于window.original() 但是original()不是window的方法 所以要用apply调用
// 把类数组变成数组
let arg = [...arguments];
// 把这个数组身上的__ob__取出来 __ob__已经被添加了 因为数组肯定不是最高层 比如obj.g属性是数组 obj不能是数组 第一次遍历obj这个对象的第一层的时候 已经给g属性(就是这个数组)添加了__ob__属性
const ob = this.__ob__;
// push splice unshift可以插入新元素 现在要把插入的新元素也变成observe的
let inserted = [];// 用来存放添加的新元素
switch(nethodName){
case 'push':
case 'unshift':
inserted = arg;
break;
case 'splice':
// splice(下标, 数量, 插入的新元素)
inserted = arg.slice(2);
break;
}
// 判断有没有要插入的新元素 让新元素也变为响应式的
if(inserted.length !== 0){
ob.observeArray(inserted);
}
ob.dep.notify();
return result;// 有些方法会返回被删除得值(如pop会返回弹出的值)所以这里得return一下
}, false);
});
------------------------------------------------------
- 我们在app.js中一开始写的代码:
const app = new Vue({
el: '#app';
data: {
age: 18,
name: 'Cara'
}
})
- 上面的代码在整个响应式流程中 data会被传入Observer对象中 Observer对象中使用Object.defineProperty监听数据的getter setter 为每个属性创建一个Dep对象 在vue解析HTML时 发现页面上有两个地方用到该属性了 当属性改变时 通知Dep对象中的subs subs是一个数组 里面存放着该属性的观察者[watcher1, watcher2]
- el会被compile解析 发现属性在哪里使用了 假如页面上使用了
{{name}}
{{name}}
{{age}}
{{age}}
compile在解析el时 你使用了一个属性就为该属性创建一个watcher 该watcher被加到相应属性的Dep对象的subs数组中
- 根据el模板初始化页面view 上述代码显示为
Cara Cara 18 18
- 假设现在 我把name改成kobe 由于在Observer对象中监听了属性 所以他立马就知道属性发生了改变 然后vue调用相应属性的Dep对象的notify函数 一调用notify函数就会遍历subs 调用每个watcher的update方法
- 所以我们一定要写Watcher对象的update方法 该方法会更改页面上用到该属性的值 于是页面就变成了
kobe kobe 18 18
这篇文章写的不错 可以借鉴下:0年前端的Vue响应式原理学习总结1:基本原理