Vue2源码解读三(数据变化侦测-数据响应式原理)
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
【Vue2源码解读三(数据变化侦测-数据响应式原理)】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 = https://www.it610.com/article/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 = https://www.it610.com/article/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数据响应式原理
推荐阅读
- 解读《摩根集团》(1)
- 赋能|赋能|学习关关公益天赋解读~
- Android事件传递源码分析
- Quartz|Quartz 源码解析(四) —— QuartzScheduler和Listener事件监听
- [源码解析]|[源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3)
- ffmpeg源码分析01(结构体)
- Java程序员阅读源码的小技巧,原来大牛都是这样读的,赶紧看看!
- 活的教导7:两个阶段
- Vue源码分析—响应式原理(二)
- cocosbcx白皮书解读(一)|cocosbcx白皮书解读(一)| 区块链如何统一开发者、发行商、用户之间的利益