响应式原理
1、vue2中是使用Object.defindProperty来做的,vue遍历data中的所有属性,并使用Object.defindProperty把这些属性转换为getter/setter
2、当用户访问或者设置某个属性时,会触发对应的getter/setter方法,然后会通知每个组件实例对应的watch方法,实现视图更新
3、Object.defindProperty有以下三个缺点:a、对于复杂对象需要深度监听,计算量非常大,性能不太好;b、对象的新增删除属性,这些操作无法实现监听,要使用Vue.$set和delete来做为辅助;c、需要重写数组的原生方法来实现数组的监听
4、vue3中使用proxy代替Object.defindProperty
优势:a、直接监听整个对象而不需要遍历监听属性,性能会有所提升;b、可以直接监听数组的变化而不需要重写数组原生方法;c、proxy多达13中拦截方法,功能会更强大
依赖收集
1、在源码目录src->core->observer目录
vue双向绑定原理
vue最独特的特征就是其响应式系统,当修改数据时,视图会相应更新
当把一个js对象传入vue实例作为data选项,vue会遍历该对象的所有property,并使用Object.defindProperty把这些property转换为getter/setter
这些getter/setter对用户不可见,但在内部它们能让vue追踪依赖,在属性被访问和修改时通知变更
每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把接触过的数据property记录为依赖,之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染
在解读源码时先看一些概念:
data:vue实例中的数据项
observer:数据的观察者,监控数据的读写操作
dep:消息订阅器,拥有收集订阅者、发布更新的功能
watcher:消息的订阅者,可以订阅dep,之后接受dep发布的更新并执行对应视图或者表达式的更新
dep和watcher的关系:dep是报纸,watcher是订阅报纸的人,如果它们之间建立了订阅关系,那么每当数据有更新的时候,就会通知对应的订阅者们
收集依赖:watcher在自身属性中添加dep的行为
收集订阅者:dep在自身属性中添加watcher的行为
下图是根据源码,自己的一些理解
源码版本2.6.11
首先找到vue进行数据处理的方法initData:
// 源码目录:src/core/instance/state.js
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
)
}
// proxy data on instance
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)) {
//<1>data属性代理
proxy(vm, `_data`, key)
}
}
//<2>observe数据绑定
// observe data
observe(data, true /* asRootData */)
}
<1>处将data上面的属性代理到vm实例上,完成之后可以直接通过vm.key访问data.key。
// 我们的数据对象
var data = { a: 1 }
// 该对象被加入到一个 Vue 实例中
var vm = new Vue({
data: data
})
// 获得这个实例上的 property
// 返回源数据中对应的字段
vm.a == data.a // => true
如官方文档对vue实例的解释,一个数据对象被加入vue实例中后,vm.a == data.a //true 并且vm ==this //true
此外,只有当实例被创建时就已经存在于 data 中的 property 才是响应式的。也就是说如果你添加一个新的 property,如vm.b = 1,那么b的改动是不会触发更新的。解决办法是设置初始值。
<2>处开始进行数据绑定,asRootData,此处为根数据,接下来会递归进行深层对象的绑定
// 源码目录:src/core/observer/index.js
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) { //检测当前数据是否被observe过
ob = value.__ob__
} else if ( //检测当前数据是否是普通对象,而不是函数或者是Regexp等情况
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return 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) //给value增加_ob_属性,作为数据已被observer观察的标志
if (Array.isArray(value)) { //value为数组
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
* 遍历对象,并调用对应函数defineReactive给属性添加getter和setter
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
* 遍历数组,并调用对应函数observe给item添加getter和setter
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
observer类有三个属性两个方法,value为传入该类的值,dep: Dep实例,用来收集订阅和发布更新,vmCount: 该实例被调用的次数。walk: 对对象的操作方法,observeArray: 对数组的操作方法
在observer类中,首先给value增加_ob_属性,作为数据已被observer观察的标志
如下
可以看到该数据已有_ob_标志,表示已经被观察。接下来,对于对象和数组有不同的操作方法,对于对象,会调用walk方法遍历对对象的属性进行defineReactive方法绑定。对于数组,是对它的七个方法进行重写
以下是defineReactive方法具体分析
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
//<1>/*在闭包中定义一个dep对象*/
const dep = new Dep()
//返回指定对象上一个自有属性对应的属性描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
//如果之前已经定义了getter和setter函数,则将其取出
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
//<2>对象的子对象递归进行observe,并返回子节点的Observe对象
let childOb = !shallow && observe(val)
//<3>
Object.defineProperty(obj, key, {
enumerable: true, //枚举
configurable: true, //可改属性和可删除属性
get: function reactiveGetter () {
//如果原本有getter方法则执行
const value = getter ? getter.call(obj) : val
if (Dep.target) { //<4>
// 进行依赖收集
dep.depend()
if (childOb) {
//子对象进行依赖收集
childOb.dep.depend()
if (Array.isArray(value)) {
// 如果是数组则需要对每个成员进行依赖收集,如果数组成员还是数组则递归
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
//通过getter方法获取当前值,与新值进行比较,一致则不进行下面的操作
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对象通知所有观察者
dep.notify()
}
})
}
在代码<1>处,new了一个dep实例;<2>处如果有嵌套对象,则进行递归调用observe,这样能保证每一层都进行observe
代码<3>处是defineReactive的核心内容,通过Object.defineProperty()的get和set方法对属性进行相应操作
对于Object.defineProperty()的get和set方法
get:
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为 undefined。
set:
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
默认为 undefined。
当属性被访问时,即会进入get方法,在get中,核心内容是进行当前的watcher(也就是Dap.target)和dep之间的绑定dep.depend(),如果当前数据有子对象,则对子对象也进行绑定;如果是数组则需要对每个成员进行依赖收集,如果数组成员还是数组则递归
当属性被修改时,即会进入set方法,在set中,核心内容是通过dep.notify()来通知当前dep所绑定的订阅者们数据有更新
数组的处理
在Observer类中,对数组有这样的处理
if (Array.isArray(value)) { //value为数组
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
}
如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。
如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。
arrayMethods是对数组方法的重写,具体源代码:
import { def } from '../util/index'
/*取得原生数组的原型*/
const arrayProto = Array.prototype
/*创建一个新的数组对象,修改该对象上的数组的七个方法,防止污染原生数组方法*/
export const arrayMethods = Object.create(arrayProto)
/**
* Intercept mutating methods and emit events
*/
/*这里重写了数组的这些方法,在保证不污染原生数组原型的情况下重写数组的这些方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理*/
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
/*将数组的原生方法缓存起来,后面要调用*/
const original = arrayProto[method]
def(arrayMethods, method, function mutator () {
// avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
let i = arguments.length
const args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
/*调用原生的数组方法*/
const result = original.apply(this, args)
/*数组新插入的元素需要重新进行observe才能响应式*/
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
/*dep通知所有注册的观察者进行响应式处理*/
ob.dep.notify()
return result
})
})
可以看到,对于push,unshift,splice这三个方法,需要observe,其他方法的变更会在当前的索引上进行更新,所以不需要再执行ob.observeArray