简介
响应式系统是vue框架核心的部分,数据对象仅仅是普通的 js对象。当我们改变数据时,视图也会被改变,本文就来探究一下vue 的响应式原理。
Object.defineProperty
vue响应式的核心是使用了es5 新增的API Object.defineProperty(因此vue不支持ie8) 。Object.defineProperty的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 它允许我们为对象的实行设置get和set方法,在对象取值和赋值会调用这两个方法,所以我们可以在赋值和取值时劫持这两个方法,触发更新试视图的操作。
const obj = {
attr: '测试',
}
let val = '劫持'
Object.defineProperty(obj, 'attr', {
enumberable: true,
configurable: true,
get() {
return val
},
set(newVal) {
val = newVal
console.log('更新视图的操作')
}
})
console.log(obj.attr)
obj.attr = '赋值'
console.log(obj.attr)
发布订阅模式
vue的依赖收集使用了发布订阅模式。
class Dep {
constructor() {
// 存放订阅者的数组
this.subs = []
}
// 增加订阅者
addSub(sub) {
this.subs.push(sub)
}
// 通知订阅者
notify() {
this.subs.forEach((sub) => {
sub.update()
})
}
}
const dep = new Dep()
const sub_fn1 = {
update() {
console.log('fn1 update')
}
}
const sub_fn2 = {
update() {
console.log('fn2 update');
}
}
dep.addSub(sub_fn1)
dep.addSub(sub_fn2)
dep.notify()
init
vue在初始化时会条用一系列方法,其中initState对数据data进行了初始化
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
这段代码对props methods data进行了初始化,我们选择initData作为切入点来探究vue的响应式系统。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
// 期待纯对象
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
// 添加代理对象的属性
proxy(vm, `_data`, key)
}
}
observe(data, true /* asRootData */)
}
这段代码获取了data,先判断了data的类型是不是纯对象,然后判断了data的属性与props和methods是否冲突,接着在vue 实例对象上添加了访问数据代理对象_data的同名属性,最后调用了observe 函数开启了数据响应式系统。
observe
因为observe代码相对比较复杂,有很多边界条件的判断,如避免收集重复的依赖,如何深度观测,如何处理数组与对象等,为了简化理解,我们只关注最核心的逻辑。
export function observe (value: any, asRootData: ?boolean): Observer | void {
// data是对象且是Vnode实例时,继续
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__ // ob保存 Observer 实例,避免重复观测
} 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
}
这段代码是observe的全部内容,observe对data进行了一个判断, 如果不是对象或者不是Vnode的实例,不进行监测。然后判断data是否有__ob__ 属性,如果有直接赋值,避免重复监测,因为对象被监测后会添加__ob__ 属性。 紧接着又有一些是否可以监测的条件判断,当满足时,就会创建一个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
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
上面是Observer类。this.value = value 是实例化对象时传进来数据的引用,可以看到def(value, ‘ob’, this)这句代码为监测对象添加了__ob__属性,然后判断时纯对象还是数组,分别走不同的监测逻辑。因为对象相对简单,我们选择对象理一下逻辑。当数据是纯对象时,会调用walk函数,遍历data所有可枚举的属性,然后循环将这些属性作为参数调用defineReactive 函数。
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()
}
})
}
这段代码相对比较复杂,有许多边界条件的判断,我们不必关心实现实践的细节,只关注Object.defineProperty即可。defineReactive 的核心是将数据对象的属性用Object.defineProperty进行数据劫持 const dep = new Dep()定义了依赖收集器。Object.defineProperty定义了get和set方法,当执行赋值时,会调用get函数执行dep.depend()进行依赖收集,当值改变时调用set触发dep.notify(),进行视图更新。
总结
vue响应式系统的核心原理是数据劫持结合发布订阅模式,在vue初始化过程中,vue会对templae编译生成指令对象与订阅者关联,通过Object.defineProperty为数据对象添加getter和setter,并执行gettter,添加订阅者。当数据改变时,触发setter,订阅者执行指令更新,指令是对原生dom对象的封装,从而导致视图的更新。