vue双向绑定
vue数据双向绑定是通过数据劫持结合发布者-订阅者模式来实现的。首先要对数据进行劫持监听,所以需要设置一个监听器observer
,用来监听所有的属性。如果属性发生了变化,就要告诉订阅者watcher
看是否需要更新,因为订阅者很多,所以我们需要有一个消息订阅器容器dep
专门收集订阅者,然后在监听器observe
和订阅者watcher
之间进行统一管理。接着需要一个指令解析器compile
,对每一个节点元素进行扫描分析,将相关指令对应初始化成一个订阅者watcher
,并替换掉模板数据或者绑定相应的函数。当订阅者watcher
接收到相应属性发生变化就会执行对应的更新函数,从而更新视图。
1.实现一个监听器observer
,用来劫持并监听所有属性,如果有变动的,通知订阅者。
2.实现一个订阅者watcher
,可以接收到属性的变化,通知并执行相应的函数,进行更新视图。
3.实现一个解析器compile,可以扫描和解析每个节点的相关指令,并根据初始模板去初始化相对应的订阅器
4.核心是Object.defineProperty
,通过defineProperty
的set
方法来提醒订阅者watcher
属性发生了变化,需要去更新视图,通过defineProperty
的get
方法去替换相应模板数据。
Object.defineProperty
js 提供的 Object.defineProperty
可以轻松通过 getter
和 setter
方法得知数据何时发生了改变。
vue2源码 Observer
类,提供了将数据全部转化为可观测的、响应式的 object。
谁用到了这个数据"称为"谁依赖了这个数据",我们给每个数据都建一个依赖数组。
在创建响应式 object 时,使用了 defineReactive 方法在 getter 里使用了 Dep 添加依赖,setter 里使用了Dep通知依赖更新。总结就是在 getter 中收集依赖,在setter 中通知依赖更新。
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && 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()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
array 的数据检测实现
在组件data中数组一般这么写:
data(){
return {
arr:[1,2,3]
}
}
Array 也还是在 getter 中收集依赖的,比如 arr 这个数据始终都是在一个 object 里面。而从 object数据对象中获取 arr 数据自然就会触发 arr 的 getter,所以我们就可以在 getter 中收集依赖。
Array 想要检测数据的变化,必然是操作了 Array,而 js 中提供的方法就那几种,只要把这些方法重写一遍,在不改变功能的前提下,增加其他的功能就可以。
在Vue中创建了一个数组方法拦截器,它拦截在数组实例与Array.prototype之间,在拦截器内重写了操作数组的一些方法,当数组实例使用操作数组方法时,其实使用的是拦截器中重写的方法,而不再使用Array.prototype上的原生方法。源码重的拦截器如下:
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method] // 缓存原生方法
Object.defineProperty(arrayMethods, method, {
enumerable: false,
configurable: true,
writable: true,
value:function mutator(...args){
const result = original.apply(this, args)
return result
}
})
})
在上面的代码中,首先创建了继承自Array原型的空对象arrayMethods,接着在arrayMethods上使用object.defineProperty方法将那些可以改变数组自身的7个方法遍历逐个进行封装。最后,当我们使用push方法的时候,其实用的是arrayMethods.push,而arrayMethods.push就是封装的新函数mutator,也就后说,实标上执行的是函数mutator,而mutator函数内部执行了original函数,这个original函数就是Array.prototype上对应的原生方法。 那么,接下来我们就可以在mutator函数中做一些其他的事,比如说发送变化通知。
我们把拦截器做好还不够,还要把它挂载到数组实例与Array.prototype之间,这样拦截器才能够生效。
其实挂载不难,我们只需把数据的__proto__属性设置为拦截器arrayMethods即可。
深度监听: 对于Array型数据,调用了observeArray()方法,该方法内部会遍历数组中的每一个元素,然后通过调用observe函数将每一个元素都转化成可侦测的响应式数据。
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
}
function protoAugment (target, src: Object) {
target.__proto__ = src
}
依赖管理器dep
为每一个数据都建立一个依赖管理器,把这个数据所有的依赖都管理起来。依赖管理器 dep
类应运而生 src/core/observer/dep.js,里面包含了,添加,删除依赖,通知依赖更新等方法。
export default class Dep {
static target: ?Watcher;
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 () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Watcher 就是依赖
Vue 的Watcher类,其实就是上文说的依赖。在之后数据变化时,我们不直接去通知依赖更新,而是通知依赖对应的Watch实例,由Watcher实例去通知真正的视图。
分析Watcher类的代码实现逻辑:
1、当实例化Watcher类时,会先执行其构造函数;
2、在构造函数中调用了this.get()实例方法;
3、在get()方法中,通过let value = this.getter.call(vm, vm)获取被依赖的数据,获取被依赖数据的目的是触发该数据上面的getter,上文我们说过,在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中,在get()方法最后将window.target释放掉。
4、而当数据变化时,会触发数据的setter,在setter中调用了dep.notify()方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖的update()方法,也就是Watcher类中的update()实例方法,在update()方法中调用数据变化的更新回调函数,从而更新视图。
简单总结一下就是:Watcher先把自己设置到一个指定位置,然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个watcher收集到Dep中去。收集好之后,当数据发生变化时,会向Dep中的每个Watcher发送通知。通过这样的方式,Watcher可以主动去订阅任意一个数据的变化。
不足
虽然我们通过Object.defineProperty
方法实现了对object数据的可观测,但是这个方法仅仅只能观测到object数据的取值及设置值,当我们向object数据里添加一对新的key/value或删除一对已有的key/value时,它是无法观测到的,导致当我们对object数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。
当然,Vue也注意到了这一点,为了解决这一问题,Vue增加了两个全局API:Vue.set和Vue.delete