前言
这周阅读了《深入浅出Vue.js》的第一篇,梳理了一下Vue的响应式。
响应式总览
我们知道响应式是发布订阅模式的实现,基于ES5的Object.defineProperty追踪对象变化。每当从data的key中读取数据时,get函数被触发,进行依赖收集;每当往data的key中设置数据时,set函数被触发,通知依赖更新。 响应式通过Observer、Watcher、Dep三兄弟来实现的。我们来分析清理三者是什么,他们之间的关系。
对象响应式
如何收集依赖
如果只是把Object.defineProperty进行封装,那其实并没什么实际用处,真正有用的是收集依赖,依赖其实它是一个Watcher实例,保存在Dep.target上。
对象的每个key都有一个dep,dep就是管理依赖的对象,可以进行依赖收集和通知依赖数据变化了;dep.depend用来存储当前key的依赖,dep.notify来通知所有依赖,数据变化了。
function defineReactive(
obj: Object | any,
key: string,
val: any
) {
const dep = new Dep()
let childOb: any = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = val
if (Dep.target) {
// 收集依赖
dep.depend()
// 为了触发数组更新,做依赖收集
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
// 逐个触发数组每个元素的依赖收集
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
const value = val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
childOb = observe(newVal)
// 通知所有依赖,执行对应的方法
dep.notify()
}
})
}
复制代码
什么是Dep
封装的Dep类,它专门帮助我们管理依赖,进行依赖的收集、删除或者发送通知
/* @flow */
import type Watcher from './watcher'
import { remove } from './utils';
let uid = 0
export default class Dep {
static target?: Watcher | null;
id: number;
subs: Array<Watcher>;
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: Watcher) {
this.subs.push(sub)
}
removeSub(sub: Watcher) {
remove(this.subs, sub)
}
depend() {
// 收集dep
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
const targetStack: Array<any> = []
export function pushTarget(target?: Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
复制代码
什么是Watcher
收集谁,也就是属性变化了,通知谁;通知到对象可能一个用户写的一个watch,也可能是模板,这时抽象出一个Watcher类
Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。watcher的经典使用vm.$watch("a.b.c", function(newVal, oldVal) {})
这段代码表示data.a.b.c属性发生变化时,触发第二个参数中的函数。
这个功能的实现只要把watcher实例添加到data.a.b.c属性的Dep中,当data.a.b.c属性变化时,通知Watcher执行参数中的回调函数。
class Watcher {
constructor(
vm: any,
expOrFn: string | Function,
cb: Function,
options: any
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
if (options) {
this.deep = !!options.deep
} else {
this.deep = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.deps = [] // 订阅了哪些dep
this.newDeps = []
this.depIds = new Set() // 记录当前watcher已经订阅的dep,防止重复订阅
this.newDepIds = new Set()
this.expression = ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = Function.prototype
}
}
this.value = this.get()
}
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
throw e
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
}
return value
}
/**
* Add a dependency to this directive.
*/
addDep(dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
update() {
const oldValue = this.value
const value = this.get()
this.cb.call(this.vm, value, oldValue)
}
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
复制代码
Observer
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor(value: any) {
this.value = value
// 为什么创建dep
// object里面有新增删除,或者数组有变更方法,通过dep通知变更
this.dep = new Dep()
this.vmCount = 0
// 设置一个__ob__属性引用当前Observer实例
def(value, '__ob__', this)
// 判断类型
if (Array.isArray(value)) {
// 替换数组对象原型
value.__proto__ = arrayMethods
// 如果数组里面的元素还是对象
this.observeArray(value)
} else {
this.walk(value)
}
}
walk(obj: any) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
复制代码
数组响应式
拦截器覆盖Array.prototype,拦截数组的7种常用方法,push、pop、shift、unshift、splice、sort和reverse
const arrayProto = Array.prototype
export const arrayMethod = Object.create(arrayProto)
;['push','pop','shift','unshift','splice','sort','reverse'].forEach(method=>{
const original = arrayProto[method]
Object.defineProperty(arrayMethod, method, {
value: 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
},
enumerable: false,
writable: true,
configurable: true
})
})
复制代码
Vue
首先,初始化的时候,将数据进行设置get和set方法。new Watcher的时候,传入回调函数和对应的用到的data的key,首先触发一个get方法,dep将Watcher实例收集到dep.subs数组中,等待set触发,执行Watcher实例的回调函数。
class Vue {
private $options: any
constructor(options: any) {
this.$options = options;
this._init()
// 测试代码
this.$options.mounted && this.$options.mounted.call(this)
}
// 初始化
_init() {
this.initState(this)
}
initState(vm: any) {
let data = vm.$options.data;
vm._data = data
// data代理
const keys = Object.keys(data);
let i = keys.length;
while (i--) {
const key = keys[i];
proxy(vm, `_data`, key);
}
observe(data)
}
// 用户watcher
$watch(expOrFn: string | Function, cb: any, options?: any) {
const vm = this
options = options || {}
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn() {
watcher.teardown()
}
}
}
// 测试代码
const options = {
data: {
a: {
b: 3,
c: [1, 2, { a: 3 }]
}
},
mounted() {
this.$watch('a.c', (val: any) => {
document.getElementById('root').textContent = val
}, {
immediate: false,
deep: true
})
this.$watch(() => {
return this.a.c[2].a
}, (val: any) => {
console.log(val, '监听到了---zzzz');
})
}
}
const vm: any = new Vue(options)
vm.a.b++
复制代码
总结
1. 对象响应式 Object可以通过Object.definProperty将属性转换成getter/setter的形式来追踪变化。读取属性触发getter,修改数据时触发setter。
我们在getter中收集哪些依赖使用了数据,在setter被触发时吗,通知收集的依赖数据发生变化了。
所谓的依赖就是Watcher,只有Watcher触发了getter才会收集依赖,哪个watcher触发了,就把哪个收集到dep中。全局设置一个唯一的watcher,当前watcher正在读取数据时,把这个watcher收集到dep中。
数据发生了变化是,触发setter,dep通知所有的watcher,触发更新。
2.数组响应式
Array追踪变化的方式不一样。因为它通过方法来改变内容,所以通过创建拦截器去覆盖数组原型方式来追踪变化。
由于数组要在拦截器中向依赖发消息,所以依赖不能保存在defineProperty中,我们将依赖保存在Observer实例上。
每个侦测了变化的数据都标上__ob__,并把this(Observer实例)保存在__ob__上。起到2个作用:1.标记被侦测了变化的数据。2.在拦截器中可以通知更新。
数组新增的项,我们需要对新增的数据做侦测变化。