共计 6500 个字符,预计需要花费 17 分钟才能阅读完成。
Vue2.X 官网文档中曾经论述了深刻响应式原理,简略来讲就是数据批改之后,被 es5 里边 Object .defineProperty,setter 拦挡到了,告诉 watcher,watcher 对函数进行渲染,这个过程种要创立新的虚构 dom 节点,比照旧的虚构 dom 节点,比照完之后做成一个补丁,把补丁打在实在 dom 构造中,实在 dom 再更新,视图产生扭转。
Object.defineProperty()数据劫持 / 数据代理
利用 javascript 引擎赋予的性能,检测对象属性变动
Object.defineProperty() 办法会间接在一个对象上定义一个新属性,或者批改一个对象的现有属性,并返回此对象。
var obj = {};
Object.defineProperty(obj, 'a', {value: 3})
Object.defineProperty(obj, 'b', {value: 5})
console.log(obj) // {a:3,b:5}
console.log(obj.a, obj.b) // 3 5
Object.defineProperty()能够设置额定暗藏的属性
Object.defineProperty(obj, 'a', {
// value: 3,
get(){},
// 是否可写
writable: true
})
Object.defineProperty()真正对数据的操作是他它本身的 getter 函数 (读取) 和 setter 函数 (设置) 来进行的:
Object.defineProperty(obj, 'a', {
// getter 函数
get(){console.log(ole.log('拜访 a 属性');
return 7;
},
// setter 函数
set(nVal) {console.log('批改 a 属性为'+nVal)
}
})
console.log(obj.a); // 7
obj.a = 10;
console.log(obj.a); // 7
由以上示例可知,当拜访 obj 的 a 属性时,值为 7,当批改 a 属性的值之后,从打印后果看出,setter 函数的确执行了,然而新值并没有赋给 getter 的返回值,此时的 getter 和 setter 短少了一个连贯的桥梁:变量,所以下面的代码稍作改变:
var temp = '';
Object.defineProperty(obj, 'a', {
// getter 函数
get(){console.log(ole.log('拜访 a 属性');
return temp;
},
// setter 函数
set(nVal) {console.log('批改 a 属性为'+nVal);
temp = nVal;
}
})
console.log(obj.a); // 7
obj.a = 10;
console.log(obj.a); // 10
到这里 Object.defineProperty()的用法就曾经很分明了,接下来要做的就是怎么让它更好看优雅~
defineReactive 函数
var obj = {};
function defineReactive (data, key, val) {
// val 在 defineReactive 函数里给 getter,setter 函数营造了一个闭包环境,这样就不必再申明一个长期变量了
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 能够被配置,比方能够被 delete
configurable: true,
// getter 函数
get(){console.log(ole.log('拜访 a 属性');
return val;
},
// setter 函数
set(nVal) {console.log('批改 a 属性为'+nVal);
if (nVal === val) return;
val = nVal;
}
})
}
defineReactive(obj, 'a', 10);
conso.log(obj.a); // 10
obj.a++; // 赋值时调用了 setter 函数
console.log(obj.a); // 11
这个代码里边解决变量的问题,不过这个例子只满足单层的对象,那么像 obj:{a:{m:{n:5}}}
这种简单构造的数据,就须要进行逐层遍历,递归地调用 Object.defineProperty()去解决~
var obj = {
a:{
m:{n:5}
}
}
拜访 obj.a.m.n 属性,不能每次都设置 a 属性,所以当有简单构造的对象时,defineReactive 函数须要作参数判断:
function defineReactive (data, key, val) {if (arguments.length == 2) {val = data[key]
}
...
}
defineReactive(obj, 'a')
console.log(obj.a.m.n); // 当拜访 obj.a.m.n 属性,只拜访到了 a 这一层
递归侦测对象全副属性
对于 obj 的 a.m.n 属性,须要循环递归地实现 Object.defineProperty(),此时创立一个类 Observer,它次要是将一个一般对象的任何属性都能被侦测到的工具类:
stateDiagram-v2
Observer(观察者) --> 将一个失常的 object 转换为每个层级的属性都是响应式(能够被侦测的)的 object
Observer.js
export default class Observer {constructor(value) {
// 每一个 Observer 的实例身上,都有一个 dep
this.dep = new Dep()
// 给实例(this,构造函数中的 this 不是示意类自身,而是示意实例)增加了__ob__属性,值就是 value(这次 new 的实例)def(value, '__ob__', this, false)
// console.log('我是 Observer 结构器', value)
// Observer 类的目标:将一个失常的 object 转换为每个层级的属性都是响应式(能够被侦测的)的 object
// 查看它是数组还是对象
if (Array.isArray(value)) {
// 如果是数组,要十分强行,将这个数组的原型,指向 arrayMethods
// setPrototypeOf 强制地定义 value 的原型
Object.setPrototypeOf(value, arrayMethods)
// 让这个数组变得 observe
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
observe(arr[i])
}
}
}
这个类被创立进去被实例化为对象才有意义,所以如何被实例化值得思考 – 创立一个 observe 函数
observe.js
import Observer from "./Observer";
// 这个函数只为对象服务
export const observe = function (value) {
// 如果 value 不是对象,什么都不做
if (typeof value !== object) return
// 定义 ob,ob 就是要存储 Observer 的实例
var ob;
if (typeof value.__ob__ !== 'undefined') {
// __ob__ 就是存储 Observer 类的实例的,区别于其余常见的属性
ob = value.__ob__; // value 就是要侦测的对象,就是 defineReactive 中传入的 data,然而实用于 val = data[key]
} else {ob = new Observer(value);
}
return ob;
}
obj 必然是先调用 observe 触发,再看这个对象有没有__ob__,如果没有,调用 New Observer(),将产生的实例增加到__ob__上,此时 obj 的 a 属性 active 的闭包环境了,因为 a 属性的值是 m:{n:5}}, 在 setter 函数中被设置的时候,m:{n:5}}作为新的对象又触发了 observe,也就是图上的遍历下一层属性。
总结起来就是对象 obj 先触发 observe,在 observe 实例化 Observer 作为 obj 的__ob__属性,而在 Observer 类的构造函数中,对对象进行了遍历,每一次遍历又调用了 defineReactive 设置属性,在设置属性时对子元素进行了 observe,至此造成了递归。
数组的响应式解决
Vue2 以 Array.prototype 为原型,创立了一个 arrayMethods 对象,而后用 ES6 中的 setPrototypeOf 强制地使 arr 的__proto__指向了 arrayMethods 对象,这样就能够调用 arrayMethods 对象中被重写的七个数组办法了,它们别离是 push、pop、shift、unshift、splice、sort、reverse。
array.js
import {def} from './utils.js';
const arrayPrototype = Array.prototype;
// 以 Array.prototype 为原型创立 arrayMethods 对象
// 裸露 arrayMethods
export const arrayMethods = Object.create(arrayPrototype)
// console.log(arrayMethods)
// 要被改写的七个数组办法
const methodsNeedChange = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
methodsNeedChange.forEach((methodName) => {// console.log('methodName', methodName)
// 备份原来的办法, 因为 push,pop 等 7 个函数的性能不能被剥夺
const original = arrayPrototype[methodName];
// 把这个数组身上的__ob__取出来,__ob__曾经被增加了,因为数组必定不是最高层,比方 obj.g 属性是数组,obj 不能是数组,第一次遍历 obj 这个对象的第一层的时候,曾经给 g 属性(就是这个数组)增加了__ob__属性
// 定义新的办法
def(arrayMethods, methodName, function(){
// 复原原来的性能
const result = original.apply(this, arguments);
// 把类数组对象变成数组
const args = [...arguments];
// console.log(arguments);
const ob = this.__ob__;
// 有三种办法 push/unshift/splice 可能插入新项,当初要把插入的新项也要变为 observe 的
let inserted = [];
switch (methodName) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
// splice 格局是 splice(下标,数量,插入的新项)inserted = args.slice(2)
break;
}
// 判断有没有要插入的新项,让新项也变成响应式的
if (inserted) {ob.observeArray(inserted);
}
console.log('lalala')
ob.dep.notify()
return result;
}, false)
})
依赖收集
在 getter 中收集依赖,在 setter 中触发依赖
- 把依赖收集的代码封装成一个 Dep 类,它专门用来治理依赖,每个 Observer 的实例,成员中都有一个 Dep 的实例;
- Watcher 是一个中介,数据发生变化时通过 Watcher 直达,告诉组件;
- 依赖就是 Watcher。只有 Watcher 触发的 getter 才会收集依赖,哪个 Watcher 触发了 getter,就把哪个 Watcher 收集到 Dep 中;
-
Dep 应用公布订阅模式,当数据发生变化时,会循环依赖列表,把所有的 Watcher 都告诉一遍。
Dep 类就是用来收集依赖,Watcher 就是依赖。Dep.js var uid = 0; export default class Dep {constructor() {// console.log('我是 dep 类的结构器') this.id = uid++; // 用数组存储本人的订阅者。subs 是英语 subscribes 订阅者的意思。// 这个数组外面放的是 Watcher 的实例 this.subs = [];} // 增加订阅 addSub (sub) {this.subs.push(sub) } // 增加依赖 depend () { // Dep.target 就是一个咱们本人指定的全局的地位,你用 window.target 也行,只有全局惟一,没有歧义就行 if (Dep.target) { // getter 函数就会从全局惟一的这个中央读取正在读取数据的 Watcher,并把这个 Watcher 收集到 Dep 当中 this.addSub(Dep.target) } } // 告诉更新 notify () {console.log('我是 notify') // 浅克隆一份 const subs = this.subs.slice(); // 遍历 for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()} } }
import Dep from "./Dep"; var uid = 0; export default class Watcher {constructor(target, expression, callback) {// console.log('我是 Watcher 类的结构器') this.id = uid++; this.target = target; this.getter = parsePath(expression) this.callback = callback; this.value = this.get()} update () {this.run() } get() { // 进入依赖收集阶段。让全局的 Dep.target 设置为 Watcher 自身,那么就是进入依赖收集阶段 Dep.target = this; const obj = this.target; var value; // 只有能找,就始终找,try{}避免找不到 try {value = this.getter(obj) } finally { // 退出依赖收集阶段,此 Watcher 把依赖收集阶段的资格让给别的 Watcher // 所有 Watcher 都在竞争,以后哪个 Watcher 正在读 getter,哪个 Watcher 就是 Dep 的 target Dep.target = null; } return value } run() {this.getAndInvoke(this.callback) } getAndInvoke(cb) {const value = this.get() if (value !== this.value || typeof value === 'object') { const oldValue = this.value; this.value = value; // this.callback() cb.call(this.target, value, oldValue) } } } function parsePath(str) {var segments = str.split('.'); console.log('segments:', segments) return (obj) => {for (let i =0; i < segments.length; i++) {if (!obj) return; obj = obj[segments[i]] } return obj } }
残缺代码:vue2 数据响应式原理