变化侦测
-
变化侦测 = 数据观测+依赖收集+依赖更新
- 使用Object.defineProperty来使得数据变得可“观测”
- 依赖收集(Observer):是指收集视图里的部分与数据绑定的关系
- 在getter中收集依赖,在setter中通知更新依赖
- 典型的发布-订阅模式,为了解耦,新增了一个管理对象
- dep(收集某个数据相关的所有依赖),watcher(被dep通知,更新依赖)
// observer.js // 收集依赖 const Dep = require('./dep'); export class Observer { constructor(value) { this.value = value; def(value, "__ob__", this); if (Array.isArray(value)) { console.log("array"); } else { this.walk(value); } } walk(obj) { const keys = Object.keys(obj); for (let i = 0; i < keys.length; i++) { defineReactive(obj,keys[i]); } } } function defineReactive(obj, key, val) { if (arguments.length === 2) { val = obj[key]; } if (typeof val === 'object') { new Observer(val); } const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { dep.depend(); return val; }, set(newval) { if (val === newval) return; val = newval; dep.notify(); } }) }// dep.js // 依赖管理器: 1数据 :n依赖 的一对多关系进行依赖管理,收集某个数据相关的所有依赖 export default class Dep { constructor() { this.subs = []; } addSub(sub) { this.subs.push(sub); } removeSub(sub) { remove(this.subs, sub); } depend() { window.target && this.addSub(window.target); } notify() { const subs = this.subs.slice(); for (let i = 0; i < subs.length; i++) { subs[i].update(); } } } export function remove(arr, item) { if (arr.length > 1) { const itemIndex = arr.indexOf(item); if (itemIndex > 1) { return arr.splice(itemIndex, 1); } } }// watcher.js // watcher表示依赖关系,通知视图更新 // window.target是为了拷贝一份 watcher,添加到Dep的依赖数组中 export default class Watcher { constructor(vm, expOrFn, cb) { this.vm = vm; this.cb = cb; this.getter = parsePath(expOrFn); this.value = this.get(); } get() { window.target = this; const vm = this.vm; let value = this.getter.call(vm, vm); window.target = undefined; return value; } update() { const oldValue = this.value; this.value = this.get(); this.cb.call(this.vm, this.value, oldValue); } } /** * 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来 * 例如: * data = {a:{b:{c:2}}} * parsePath('a.b.c')(data) // 2 */ const bailRE = /[^\w.$]/; export function parsePath(path) { if (bailRE.test(path)) return; const segements = path.split('.'); return function (obj) { for (let i = 0; i < segements.length; i++) { if (!obj) return; obj = obj[segements[i]]; } return obj; } }侦测流程
vue这套变化侦测的缺点很明显,因为利用defineProperty来进行收集,只限于读和写已有值,当我们对obj进行新增或者删除属性值时,它是监听不到的。所以在官网文档上的叙述上说明过,对数组或对象的直接增加或者删除会产生不期望的结果, 为了解决这一问题,特地增加了Vue.set和Vue.delete两个全局API 。数组怎么办?
看到这里,对原型熟悉的人可能会问了,这种方法只针对于
Obj类型,那剩下的常用的Arr类型或者其他类型呢?defineProperty数组是不可能使用的,那么我们应该怎么对数组进行依赖收集和通知更新?还是延续上面的思想:拦截,
vue将所有数组的异变方法(能改变原有数组)拦截一波,就能知道arr啥时候被setter了。经常面试被问到原型,原型链的what,why,那么how???? 我觉得这就是个很巧妙的实践~
拦截数组原型上的异变方法(会改变原有宿主的方法)的代码:
//代码位置 vue/src/core/observer/array.js /* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */ import { def } from '../util/index' const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache original method const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) })
数组依赖收集
无论怎样,先得用walk让元素注入observer依赖,使得在getter中实例化Dep收集依赖并将数组方法拦截掉
// 源码位置:/src/core/observer/index.js
const Dep = require("./dep");
const { arrayKeys, arrayMethods } = require("./array");
// 源码位置:src/core/observer/index.js
// 使用 defineProperty 让数据可观测
export class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
def(value, "__ob__", this);
if (Array.isArray(value)) {
const agument = hasProto ? protoAugment : copyAugument;
[agument](value, arrayMethods, arrayKeys);
this.observerArray(value);
} else {
this.walk(value);
}
}
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
observerArray(ietms) {
for (let i = 0; i < ietms.length; i++) {
observe(ietms[i]);
}
}
}
export const hasProto = "__proto__" in {};
/*
复制原型属性,添加拦截
*/
function protoAugment(target, src, keys) {
target.__proto__ = src;
}
function copyAugument(target, src, keys) {
for (let i = 0; i < keys.length; i++) {
const key = key[i];
def(target, key, src[key]);
}
}
/*
* 尝试为value创建一个0bserver实例,如果创建成功,直接返回新创建的Observer实例。
* 如果 Value 已经存在一个Observer实例,则直接返回它
*/
function observe(value) {
if (!isObject(value) || value instanceof VNode) {
return;
}
let ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}
function defineReactive(obj, key, val) {
let childOb = observe(val);
if (arguments.length === 2) {
val = obj[key];
}
if (typeof val === "object") {
new Observer(val);
}
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (childOb) {
childOb.dep.depend();
}
return val;
},
set(newval) {
if (val === newval) return;
val = newval;
dep.notify();
}
});
}
通知更新
主要是还要对数组进行深度监测和新增元素侦测,在拦截的原型上进行依赖更新。
__ob__是在进行初始化observer的时候,在被监听者上面挂载了自己的实例,以便访问后进行依赖更新。
// 源码位置:vue/src/core/observer/array.js
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
-
总结:
vue的变化侦测与React对比Vdom和Angular的脏值检测都不一样。核心是利用defineProperty的能力,拦截所有绑定的响应式数据(data中),在拦截中添加依赖管理器Dep来收集管理依赖,用Watcher表示依赖关系本身,进行通知依赖更新。其中,对于数组的侦测的思路是,覆盖所有数组原型的的异变方法,在覆盖后植入依赖逻辑。这套缺点就是对数组进行下标赋值操作时,vue是侦测不到的,官网文档上多处对此有说明。