变化侦测
Object 变化侦测
对象变化追踪
function defineReactive(data, key, val) { Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { return val; }, set: function(newVal) { if (val === newVal) { return; } val = newVal; } }); }
- 如何收集依赖
在 getter 中收集依赖,在 setter 中触发依赖 依赖收集在哪
假设依赖是一个函数,保存在 window.target 上,用 dep 数组收集依赖,在 set 被触发时,循环 dep 以触发收集到的依赖function defineReactive(data, key, val) { let dep = []; Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { dep.push(window.target); // 收集 return val; }, set: function(newVal) { if (val === newVal) { return; } // 触发 for (let i = 0; i < dep.length; i++) { dep[i](newVal, val); } val = newVal; } }); }
功能解耦
export default class Dep { constructor() { this.subs = []; } addSub(sub) { this.subs.push(sub); } removeSub(sub) { remove(this.subs, sub); } depend() { if (window.target) { this.addSub(window.target); } } notify() { const subs = this.subs.slice(); for (let i = 0, l = subs.length; i < l; i++) { subs[i].update(); } } } function remove (arr, item) { if (arr.length) { const index = arr.indexOf(item); if (index > -1) { return arr.splice(index, 1); } } } function defineReactive (data, key, val) { let dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { dep.depend(); return val; }, set: function(newVal) { if (val === newVal) { return; } val = newVal; dep.notify(); } }) }
依赖是谁
收集依赖收集的是数据发生变换时需要通知用到数据的地方,而使用这个数据的地方有很多,且类型不一样,既有可能是模板,也有可能是用户写的一个 watch,需要抽象出一个能集中处理这些情况的类。在收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个。接着,它再通知其他地方,将这个类叫 Watcher。// keypath 当属性变化时做点什么 // vm.$watch('a.b.c', function(newVal, oldVal) { // // do something // }) export default class Watcher { constructor (vm, expOrFn, cb) { this.vm = vm; this.getter = parsePath(expOrFn); this.cb = cb; this.value = this.get(); } get() { window.target = this; let value = this.getter.call(this.vm, this.vm); window.target = undefined; return value; } update() { const oldValue = this.value; this.value = this.get(); this.cb.call(this.vm, this.value, oldValue); } }
/** * 解析简单路径 */ const bailRE = /[^\w.$]/; export function parsePath(path) { if (bailRE.test(path)) { return } const segments = path.split('.'); return function(obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return; obj = obj[segments[i]]; } return obj; }; }
递归侦测所有 key
/** * Observer 类会附加到每一个被侦测的 object * 一旦被附加上,Observer 会将 object 的所有属性转换为 getter/setter 的形式 * 来收集属性的依赖,并且当属性变化时会通知这些依赖 */ export class Observer { constructor (value) { this.value = value; if (!Array.isArray(value)) { this.walk(value); } } /** * walk 会将每一个属性都转换成 getter/setter 的形式来侦测变化 * 该方法只有在数据类型是 Object 时被调用 */ walk (obj) { const keys = Object.keys(obj); for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]); } } } // 支持递归子属性 function defineReactive(data, key, val) { if (typeof val === 'object') { new Observer(val); } let dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.depend(); return val; }, set: function(newVal) { if (val === newVal) { return } val = newVal; dep.notify(); } }) }
将一个 object 传到 Observer 中,该 object 会变成响应式的 object。
- 关于 Object 的问题
由于数据的变化是通过 getter/setter 来追踪的,有些语法中数据发生变化,Vue.js 追踪不到,如向 obj 添加或删除属性。
为了解决该问题,Vue.js 提供了两个 API——vm.$set 与 vm.$delete
Array 变化侦测
在 ES6 之前,JavaScript 没有提供元编程的能力,也就是没有提供可以拦截原型方法的能力。但是可以使用自定义方法覆盖原生的原型方法来实现目的。
拦截器
Array 原型中可以改变数组自身内容的方法:push, pop, shift, unshift, splice, sort 和 reverseconst arrayProto = Array.prototype; export const arrayMethods = Object.create(arrayProto); [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'] .forEach(function (method) { // 缓存原始方法 const original = arrayProto[method]; Object.defineProperty(arrayMethods, method, { value: function mutator (...args) { // do something return original.apply(this, args); }, enumerable: false, writable: true, configurable: true, }); });
使用拦截器覆盖 Array 原型
拦截器支只覆盖那些响应式数组的原型。而 Observer 将数据转换成响应式,所以在 Observer 中使用拦截器覆盖。export class Observer { constructor (value) { this.value = value; if (Array.isArray(value)) { // __proto__ 是 Object.getPrototypeOf 和 Object.setPrototypeOf 的早期实现 value.__proto__ = arrayMethods; } else { this.walk(value); } } }
将拦截器的方法挂载到数组的属性上
处理不能使用__proto__
的情况import { arrayMethods } from './array'; // __proto__ 是否可用 const hasProto = '__proto__' in {}; const arrayKeys = Object.getOwnPropertyNames(arrayMethods); export class Observer { constructor (value) { this.value = value; if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); } else { this.walk(value); } } // ... } function protoAugment (target, src, keys) { target.__proto__ = src; } function copyAugment (target, src, keys) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i]; def(target, key, src[key]); } }
如何收集依赖
Array 的依赖和 Object 一样,也在 defineReactive 中收集。
Array 在 getter 中收集依赖,在拦截器中触发依赖。function defineReactive(data, key, val) { if (typeof val === 'object') new Observer(val); let dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { dep.depend(); // 收集 Array 的依赖 return val; }, set: function(newVal) { if (val === newVal) { return; } dep.notify(); val = newVal; } }); }
依赖列表存在哪
Vue.js 把 Array 的依赖存放在 Observer 中。因为数组在 getter 中收集依赖,在 拦截器 中触发依赖,所以依赖保存的位置,必须在 getter 和 拦截器中都可以访问到。export class Observer { constructor (value) { this.value = value; this.dep = new Dep(); if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); } else { this.walk(value); } } // ... }
收集依赖
function defineReactive(data, key, val) { let childOb = observe(val); let dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { dep.depend(); if (childOb) { childOb.dep.depend(); } return val; }, set: function(newVal) { if (val === newVal) { return; } dep.notify(); val = newVal; } }); } /** * 尝试为 value 创建一个 Observer 实例 * 如果创建成功,直接返回新创建的 Observer 实例 * 如果 value 已经存在一个 Observer 实例,直接返回它 */ export function observe (value, asRootData) { if (!isObject(value)) { return; } let ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else { ob = new Observer(value); } return ob; }
在拦截器中获取 Observer 实例
因为 Array 拦截器是对原型的一种封装,所以可以在拦截器中访问到 this (当前正在被操作的数组)。dep 保存在 Observer 中,所以需要在 this 上读到 Observer 的实例。// 工具函数 function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true, }); } export class Observer { constructor (value) { this.value = value; this.dep = new Dep(); // 通过 __ob__ 属性可以拿到 Observer,进而拿到 dep // 同时 __ob__ 还可以用来标记当前的 value 是否被转换成响应式数据 def(value, '__ob__', this); if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); } else { this.walk(value); } } // ... }
使用
__ob__
, 向数组依赖发送通知[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { const original = arrayProto[method]; Object.defineProperty(arrayMethods, method, { value: function mutator(...args) { const result = original.apply(this, args); const ob = this.__ob__; ob.dep.notify(); return result; }, enumerable: false, writable: true, configurable: true, }); });
侦测数组中元素的变化
响应式数据的子数据也要进行侦测,不管是 Object 还是 Arrayexport class Observer { constructor (value) { this.value = value; def(value, '__ob__', this); if (Array.isArray(value)) { this.observeArray(value); } else { this.walk(value); } } /** * 侦测 Array 中的每一项 */ observeArray (items) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]); // 对每个元素执行一遍 new Observer,递归 } } // ... }
侦测新增元素的变化
[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (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); ob.dep.notify(); return result; }) })
Array 的问题
对 Array 的变化侦测是通过拦截原型的方式实现的,因此有些数组操作拦截不到。后续可能会用 Proxy 解决。this.list[0] = 2; this.list.length = 0;