什么是响应式
在改变数据的时候,视图会跟着更新;React 是通过 this.setState
去改变数据,然后根据新的数据重新渲染出虚拟DOM,最后通过对比虚拟DOM找到需要更新的节点进行更新;而 Vue 则是利用了 Object.defineProperty
的方法里面的 setter
与 getter
方法的观察者模式来实现。
Object.defineProperty
Vue2.x 实现响应式的核心是利用的 ES5 的 Object.defineProperty
,这也是 Vue 不兼容 IE8 及其以下浏览器的原因;Object.defineProperty
的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回该对象;
Object.defineProperty(obj, prop, descriptor)
obj :需要定义属性的当前对象
prop :当前需要定义的属性名
desc: 要定义或修改的属性描述符
let obj = {};
Object.defineProperty( obj, 'name', {
//value:'1',
//writable:true,
enumerable: true,
configurable: true,
get: function () {
return temp
},
set: function (val) {
temp = val
}
})
描述符分为:数据描述符、存取描述符;
数据描述符
1、value:属性的默认值,可以是任何有效的 JavaScript 值(数值,对象,函数等),默认为 undefined;
2、writable:只有当值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变,默认为 false;
3、configurable:只有当值为 true 时,属性才能被重新定义(注意这里不是重新赋值),同时该属性也能从对应的对象上被删除,默认为 false;
4、enumerable:只有当值为 true 时,属性才会出现在对象的枚举属性(for in 或者 Object.keys())中,默认为 false;
存取描述符
1、get:属性的 getter 函数,如果没有 getter,则为 undefined;当访问该属性时会调用此函数,执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象);该函数的返回值会被用作属性的值,默认为 undefined;
2、set:属性的 setter 函数,如果没有 setter,则为 undefined;当属性值被修改时会调用此函数;该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象,默认为 undefined;
注意:
1、get 返回的值才是最终属性的值,修改属性值的时候会先触发 set 去设置这个修改的值,然后才会触发 get 去获取最新的值并返回;
2、如果一个描述符同时拥有 value || writable 和 get || set 键,则会产生一个异常,在使用的时候需要注意;
响应式的实现
先看一下 Vue 官网提供的流程图:
过程梳理:
1、init:在初始化(init)的时候会调用
observe
方法为 data 绑定getter 、setter
方法,当数据被读取时会触发getter
方法,被赋值时会触发setter
方法;每一个 data 的属性都有一个 dep 对象,调用getter
的时候会去 dep 中注册函数,调用setter
的时候会去通知 dep 中对应的函数执行;这个绑定的过程也叫数据劫持;
2、mount:在 mount 阶段(mountComponent
)会创建一个Watcher
对象,Watcher
实际上是连接 Vue 组件与 Dep 的桥梁;创建Watcher
的时候Watcher
构造函数中的this.getter.call(vm, vm)
函数会被执行;Watcher
构造函数里面的getter
就是updateComponent
此时Watcher
会立即调用组件的 render 函数去生成虚拟 DOM;在调用 render 的时候,就会需要用到 data 的属性值,此时会触发上一步的getter
函数,将当前的Watcher
函数注册进 Dep里;这个过程也叫依赖收集;
3、update:当 data 属性发生改变之后,由setter
触发调用 Dep 的notify
函数去通知 Dep 里对应的Watcher
对象,通知它们去重新渲染组件;
下面详细说一下这个过程:
数据劫持(observe)
在初始化环节,会为 data / props 通过 observe
方法绑定 getter / setter
方法;在 src/core/observer/index.js
文件里:
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 不为对象或者是VNode实例时不进行任何操作
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
//判断是否有__ob__属性且__ob__为Observer实例
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
) {
//是数组或者普通对象,非服务端渲染,是可拓展对象,非 vue 实例对象
ob = new Observer(value)
}
// 根 data,并且是响应式
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
1、该方法首先判断传递的 value 不是对象或者是 VNode实例时则直接返回不做处理;
2、然后判断是不是有 _ob_
属性且_ob__
为Observer
实例,是则直接返回避免重复定义响应式;否则在满足条件的情况下重新创建 Observer
实例;
下面看看 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
this.dep = new Dep()
this.vmCount = 0
// 把自身实例添加到数据对象 value 的 __ob__ 属性上
def(value, '__ob__', this)
// value 是否为数组的不同调用
if (Array.isArray(value)) {
if (hasProto) {
//通过使用__proto__截取原型链来增加目标对象或数组
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])
}
}
}
1、⾸先实例化 Dep 对象;
2、接着通过执⾏ def
函数把⾃⾝实例添加到数据对象 value 的 __ob__
属性上,def
函数是⼀个⾮常简单的 Object.defineProperty
的封装,使用def
定义__ob__
的目的是让__ob__
在对象属性遍历的时候不可被枚举出来;
3、接下来会对 value 做判断,对于数组会调⽤ observeArray
⽅法,这里会将自定义的一些数组处理方法arrayMethods
集合绑定到原型上; 否则对纯对象调⽤ walk
⽅法;
4、 observeArray
方法是遍历数组再次调⽤ observe
⽅法,⽽ walk
⽅法是遍历对象的 key 调⽤ defineReactive
⽅法;
插入一下 def
的作用:
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
def 函数接收四个参数,分别是 源对象,要在对象上定义的键名,对应的值,以及是否可枚举,如果不传递 enumerable 参数则代表定义的属性是不可枚举的。
下面看看defineReactive
⽅法:
defineReactive
的功能就是定义⼀个响应式对象,给对象动态添加 getter 和 setter;
// 定义对象上的响应性属性
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
//初始化 Dep
const dep = new Dep()
//获取了当前obj.key的属性描述
const property = Object.getOwnPropertyDescriptor(obj, key)
//如果当前key不可被重新定义则返回
if (property && property.configurable === false) {
return
}
// 没有getter有setter,而且只传了2个参数,那就给val赋值一下
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
//深度检测shallow 为true不会深度检测
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
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 对新的值进行监听
childOb = !shallow && observe(newVal)
// 通知所有订阅者,内部调用 watcher 的 update 方法
dep.notify()
}
})
}
1、初始化 Dep 实例,然后属性的 configurable
为 false 时直接返回,没有 getter
则给 val 赋值;
2、这里对 val 调用observe
,有个 shallow
是用来控制不深度监测的对象,默认深度监测,$attrs 、$ listeners
不会深度监测;
3、调用 Object.defineProperty
的设置 get 和 set ,同时对 Dep 进行操作;
reactiveGetter
1、先判断此属性是否有getter
,若存在就直接执行获取值,否则就返回之前的 val 赋值给 value;
2、target 存在,调用 dep 进行依赖收集;
3、childOb 存在说明是深度监测,则收集 childOb 的依赖;
4、value 如果是数组,调用dependArray
将每一个是对象的子项的依赖收集起来;
5、dependArray
循环数组,判断是子项是否有__ob__
,有说明是对象则将子项的__ob__
加入子项的 dep ,如果子项是数组则递归调用dependArray
方法;
reactiveSetter
1、先判断此属性是否有getter
,若存在就直接执行获取值,否则就返回之前的 val 赋值给 value;
2、如果值没有改变或者 新值和旧值都为 NaN 的情况就直接 return;
3、开发环境,如果 customSetter参数存在,就调用此函数;
4、有getter
没有setter
直接返回;
5、如果有setter
就调用setter
处理新的值,否则直接复制给 val;
6、深度监测情况下(默认),对新值调用observe
进行监听,因为旧值被覆盖,旧值的;
7、最后调用dep.notify()
通知更新;
每一个 observe 都有一个对应的 Dep,它内部维护一个数组,保存与该observe 相关的Watcher;所以在依赖收集的时候才会出现这种 dep.depend()
和childOb.dep.depend()
不同层级的依赖收集;
依赖收集(Dep)
依赖的收集主要发生在 defineReactive
触发 getter
的时候,通过 dep.depend
来对当前 Observer
上的依赖进行收集,在上面已经有很详细的介绍,但是依赖收集的主要位置 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 = []
}
//新增sub
addSub (sub: Watcher) {
this.subs.push(sub)
}
//删除sub
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
//依赖收集
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
//触发更新
notify () {
//获取一个一样的新数组
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 如果没有运行异步,那么在调度程序中sub是不会排序的,我们现在需要对它们排序,以确保它们以正确的顺序触发
//这里通过watcher 的id来排序
subs.sort((a, b) => a.id - b.id)
}
//村换当前observer下的watcher,触发更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
1、Dep 类首先定义了一个静态属性 target,它就是各种Watcher
的实例;然后又定义了两个实例属性,id 是 Dep 的主键,会在实例化的时候自增,subs 是一个存储各种 Watcher
的数组。例如 render watcher、user watcher 和 computed watcher 等;
2、addSub
和removeSub
对应的就是往 subs 数组中添加和移除各种Watcher
;
3、notify
当这个响应式数据发生变化的时候,通知 subs 里面的各种watcher
,然后执行其watcher
的update()
方法。这属于派发更新的过程,后面会将;
可以看到 Dep 主要就是提供一个数组来存储 各种 Watcher
,提供对 sub 数组新增和删除的方法,以及触发对应 watcher
更新的方法;Dep 完全可以看成是对 Watcher
的管理者;
订阅者 (Watcher)
在src/core/observer/watcher.js
文件里:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
//render watch 将当前watcher赋值给 vm上的_watcher
if (isRenderWatcher) {
vm._watcher = this
}
//将watcher放到vm上的_watchers数组里面
vm._watchers.push(this)
// 参数
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // f缓存
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// 获取getter函数
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
//lazy为true则延迟get方法执行
this.value = this.lazy
? undefined
: this.get()
}
//getter
get () {
//把 Dep.target 赋值为当前的渲染 watcher
pushTarget(this)
let value
const vm = this.vm
try {
//获取value值
value = this.getter.call(vm, vm)
} catch (e) {
//watch的watcher
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
//新增
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)
}
}
}
//清除
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
//更新
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
1、Watcher
是⼀个 Class,在它的构造函数中,定义了⼀些和 Dep 相关的属性和⼀些原型的⽅法;
2、当我们去实例化一个 watcher
的时候会执行内部的this.get
方法:
1、get 方法首先会把当前Watcher实例压栈到target栈数组中,然后把Dep.target设置为当前的Watcher实例;
2、执行this.getter 获取 value,this.getter 实际上就是 updateComponent 函数,实际上就是在执⾏vm._update(vm._render(), hydrating)
,在执行 vm.render 的时候会去 vm 上访问数据,这个时候就会触发数据对象的 getter;
3、如果 deep 为true 则把每一个属性都跟踪作为深度监视的依赖,一般是 watch 使用;
4、然后把当前target栈数组的最后一个移除,然后把Dep.target设置为倒数第二个;
5、清空依赖;
3、在依赖收集的时候执行 dep.depend 会触发 addDep 方法;
1、当前dep是否已经在新dep id集合中,不在则更新新dep id集合以及新dep数组;
2、当前dep是否在旧dep id集合中,不在则调用dep.addSub(this)方法,把当前Watcher实例添加到dep实例的subs数组中
4、清空依赖 cleanupDeps:
1、首先遍历旧依赖列表deps,如果发现其中某个dep不在新依赖id集合newDepIds中,则调用dep.removeSub(this)移除依赖;
2、在遍历完deps数组后,会把deps和newDeps、depIds和newDepIds的值进行交换,然后清空newDeps和newDepIds(老的删掉,新的赋值给老的);
5、派发更新 update:
1、lazy 是computed 的标志,这里会设置缓存;
2、是同步更新则走 run 否则走 queueWatcher;
6、同步run:官网没有对于 options 的 sync 配置,所以这里只说一下他的作用—当dep通知某个watcher实例需要更新的时候,这个watcher实例直接调用callback方法进行更新;
7、异步queueWatcher:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
//这个if表示,如果这个watcher尚未被flush则return
if (has[id] == null) {
//再次把watcher置为true
has[id] = true
if (!flushing) {
//如果当前不是正在更新watcher数组的话,那watcher会被直接添加到队列末尾
queue.push(watcher)
} else {
//在watcher队列更新过程中,用户再次更新了队列中的某个watcher
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
queue:各种 Watcher 执行队列;
has:防止重复添加Watcher的标志对象:
1、先通过获取当前Watcher的自增id,判断在标志对象has中是否已经存在,如果不存在,则对这个id进行标记,赋值为true;
2、判断是否为flushing状态,如果不是,则代表我们可以正常的把当前Watcher推入到queue队列数组中
3、是否为waiting状态,如果不是,则代表可以执行queue队列数组,然后设置waiting为true,最后调用nextTick(flushSchedulerQueue);
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
它主要做几件事情:还原flushing状态、排序queue队列、遍历queue、还原状态、触发组件钩子函数
1、首先对 flushing 进行了还原,这样做的目的是为了不影响在执行 queue 队列的时候,有 Watcher 推入到 queue 队列中;
2、使用数组的 sort 方法,把 queue 队列中的 Watcher 按照自增 id 的值从小到大进行了排序,这样做是为了保证以下三种场景:
1、组件从父组件更新到子组件更新,因为父类是在子类之前创建;
2、组用户自定义 Watcher 在组件渲染之前创建,因为用户自定义 Watcher 是在组件渲染之前创建的;
3、如果组件在父组件的监视程序运行期间被销毁,则可以跳过其监视程序;
3、遍历 queue,释放当前 Watcher 在 has 标志对象中的状态,然后调用watcher.run()
方法;
4、当 queue 队列都执行完毕时,把所有相关状态还原为初始状态,这其中包括 queue、has 和 index 等;
5、调用 callActivatedHooks
和 callUpdatedHooks
分别是为了触发组件activated
和updated
钩子函数,其中activated
是与 keep-alive 相关的钩子函数;
派发更新(update)
当响应式数据发生变动的时候,通知所有订阅了这个数据变化的Watcher
(既 Dep 依赖)执行update
。
1、对于render watcher 渲染Watcher
而言,update
就是触发组件重新进行渲染;
2、对于computed watcher 计算属性Watcher
而言,update
就是对计算属性重新求值;
3、对于user watcher用户自定义Watcher
而言,update
就是调用用户提供的回调函数;
以上就是响应式的整个流程!
注意
1、Vue2.x 不能监听数组下标和 length 长度的变化,无法监听到对象属性的动态添加和删除;这个是硬伤,不过 2.x 的 API 中专门提供了 $get $set $delete
来解决这个问题;
this.$set(this.obj, key, value)
Vue3.x 直接正面解决着个问题,使用 ES6 提供的 Proxy
来替代 Object.defineProperty
;解决了 Vue2.x 不能监听数组改变的缺点,并且还支持劫持整个对象,并返回一个新对象;
需要说明的是:对于数组下标(push)和长度变化 Object.defineProperty
是可以监测的到的,Vue2.x 无法监听完全是 Vue2.x 自己做的限制;在 Observer
里面,当数据是数组时调用 observeArray
遍历数组的每一项重新调用 observe
,这里只会对对象属性进行监听,并没有对数组的属性进行监听;
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
按照尤大大的说法就是:“性能代价和获得的用户体验收益不成正比”;换句话说就是:性能消耗大,不划算;
总结
1、Vue 的数据更新是异步的;
2、响应式主要是使用 Object.definedProperty 的 get 和 set 方法来收集依赖和触发更新;