vue响应式原理
思路:遍历data中的属性,将属性传入denfineReactive方法中,在defineReactive方法中通过Object.defineProperty重写get与set方法。
观察者模式(Watcher, Dep)
订阅:在defineReactive中创建了Dep实例,在getter方法中会调用dep.depend(),将当前的Dep.target保存的watcher实例push到dep.subs当中。
通知:在setter方法中,当数据更新时会调用dep.notfiy(),遍历subs中的watcher,调用watcher.update()触发相应操作。
dep类:
subs:用于保存订阅了该dep的watcher实例数组(subs);
addSub:用于向subs数组中添加watcher实例;
notify:用于遍历subs,调用每个watcher的update方法进行更新操作;
class Dep { constructor () { this.subs = [] } notify () { const subs = this.subs.slice() for (let i = 0; i < subs.length; i++) { subs[i].update() } } addSub (sub) { this.subs.push(sub) } } class Watcher { constructor () { } update () { } } let dep = new Dep() dep.addSub(new Watcher()) // Watcher订阅了依赖
watcher:
watcher类不太好理解,它有uptate方法,在其中调用了run方法,进行更新操作。
class Watcher () { constructor (vm, expOrFn, cb, options) { this.cb = cb this.value = this.get() } get () { pushTarget(this) // 标记全局变量Dep.target let value = this.getter.call(vm, vm) // 触发getter if (this.deep) { traverse(value) } popTarget() // 标记全局变量Dep.target return value } update () { this.run() } run () { const value = this.get() // new Value // re-collect dep if (value !== this.value || isObject(value)) { const oldValue = this.value this.value = value this.cb.call(this.vm, value, oldValue) } } }
defineReactive :
function defineReactive (obj, key, val) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) const getter = property && property.get const setter = property && property.set let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() // 等价执行dep.addSub(Dep.target),在这里收集 } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } dep.notify() } })
由于defineReactive是在创建vue实例时进行调用,在实例创建后,修改data中的应用类型,比如给对象新增属性,新增的属性没有重写get和set所以不是响应式的,数组也一样,所以vue提供了$set方法来为对象新增属性,同时为劫持数组的prototype重写了push、pop、splice等数组的操作方法,以实现数组操作的响应式操作。
重写Array.prototype中的方法:
// 函数拦截 /* 思路: 1.使用一个临时函数存储原函数(指向原函数) 2.重新定义原函数(将原函数指向一个新的函数) 3.在新的函数中扩展新功能,最后调用临时函数 */ const ARR_METHODS = ['push', 'pop', 'splice'] let arr_methods = Object.create(Array.prototype) // 创建我们自己的原型__proto__ ARR_METHODS.forEach(way => { arr_methods[way] = function() { console.log(`拦截数组操作方法:${way}`) for(let i = 0; i < arguments.length; i++) { reactify(arguments[i]) } Array.prototype[way].apply(this, arguments) // 调用原方法 } })