Title:浅析 vue2 数据响应式原理
Date:2022-04-28
Source:ITgo
什么是数据响应式?当 对象自身
或对象属性
被读和写的时候,咱们须要晓得该数据被操作了,并在这过程中执行一些函数,例如:render 函数,而这一过程我把它定义为数据响应式。
那么 vue
具体是如何实现数据响应式的呢?接下来咱们通过 vue
的源码探索一下响应式数据的始末。
响应式数据 的源码在 ./src/core/observer
上面
在具体实现下面,vue 用到了 4 个核心部件:
- Observer
- Dep
- Watcher
- Scheduler
Observer
Observer 的目标很简略,它次要就是把一个一般的对象转换成响应式的对象。
那 Observer 到底是如何做到把一个一般对象转换成响应式对象的呢?
为了实现这一点,Observer 通过 object.defineProperty
将一个一般对象包装成一个带有 getter/setter
属性的非凡对象,当拜访属性的时候会调用getter
,批改属性的时候会调用setter
,这样一来,咱们就能够晓得数据什么时候被读写了。
晓得实现逻辑了,那咱们就来实现一个简略的响应式数据吧!
首先咱们先定义一个一般对象
const obj = {
a: 1,
b: 2
}
console.log(obj);
很显然,这个对象它并不具备响应式,从控制台输入就能够看得出来
接下来咱们通过 Object.defineProperty 来改写下面的对象
let obj = {b:2}
let val = 1
Object.defineProperty(obj, 'a', {
enumerable: true,
configurable: true,
get() {console.log('a 被读取了')
return val
},
set(newVal) {console.log('a 被批改了')
obj.a = newVal
}
})
这下就很显著了,a 属性和 b 属性齐全不同了,当对 a 读写对时候会就回登程相应的 getter/setter
办法
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
/*
* 将 Observer 实例绑定到 data 的__ob__属性下面去,* observe 的时候会先检测是否曾经有__ob__对象寄存 Observer 实例了,* def 办法定义能够参考 https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16
*/
def(value, '__ob__', this)
if (Array.isArray(value)) {if (hasProto) {protoAugment(value, arrayMethods) /* 间接笼罩原型的办法来批改指标对象 */
} else {copyAugment(value, arrayMethods, arrayKeys) /* 定义(笼罩)指标对象或数组的某一个办法 */
}
/* 如果是数组则须要遍历数组, 将数组中的所有元素都转化为可被侦测的响应式 */
this.observeArray(value)
} else {
/* 如果是对象则间接 walk 进行绑定 */
this.walk(value)
}
}
从 Observer 的源码能够看出,Observer 对对象和数组的响应式解决有所不同,如果是对象就间接调用 walk,遍历每一个对象并且在它们下面绑定 getter 与 setter,如果是数组则须要遍历数组, 将数组中的所有元素都转化为可被侦测的响应式
1.Object
walk (obj: Object) {const keys = Object.keys(obj)
/*walk 办法会遍历对象的每一个属性进行 defineReactive 绑定 */
for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i])
}
}
function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
//...
/* 对象的子对象递归进行 observe 并返回子节点的 Observer 对象 */
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
/* 如果本来对象领有 getter 办法则执行 */
const value = getter ? getter.call(obj) : val
// ...
return value
},
set: function reactiveSetter(newVal) {
/* 通过 getter 办法获取以后值,与新值进行比拟,统一则不须要执行上面的操作 */
const value = getter ? getter.call(obj) : val
// ...
val = newVal
/* 新的值须要从新进行 observe,保证数据响应式 */
childOb = !shallow && observe(newVal)
}
})
}
总之就是递归遍历对象的所有属性,以实现深度属性转换
2.Array
如果是数组,vue
会重写数组的一些办法,更改 Array 的隐式原型,之所以要这样做,是因为 vue 须要监听哪些办法可能扭转数组数据。别离重写了这些办法:push
, pop
, shift
, unshift
, splice
, sort
, reverse
if (Array.isArray(value)) {if (hasProto) {protoAugment(value, arrayMethods) /* 间接笼罩原型的办法来批改指标对象 */
} else {copyAugment(value, arrayMethods, arrayKeys) /* 定义(笼罩)指标对象或数组的某一个办法 */
}
/* 如果是数组则须要遍历数组, 将数组中的所有元素都转化为可被侦测的响应式 */
this.observeArray(value)
}
/* 地位:./src/core/observer/array.js
* 扭转数组本身内容的 7 个办法
*/
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/*
* 这里重写了数组的这些办法,* 在保障不净化原生数组原型的状况下重写数组的这些办法,* 截获数组的成员产生的变动,*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method] // 缓存原生办法
def(arrayMethods, method, function mutator (...args) {const result = original.apply(this, args) /* 调用原生的数组办法 */
/* 数组新插入的元素须要从新进行 observe 能力响应式 */
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
return result
})
})
总结:Observer 的指标就是,当对象的属性被读写,数组的数据被增删改时都要被 vue 感知到。
Dep
Observer 只是让 vue 感知到数据被读写了,然而接下来到底要干什么就须要 Dep 来解决了。
Dep 的含意是Dependency
,示意依赖的意思,vue 会为对象中的每一个属性,对象自身,数组自身创立一个 Dep 实例,而每个 Dep 实例都会做两件事:
- 收集依赖,即谁在应用该数据,
- 告诉依赖更新,即当数据产生扭转的时候,告诉依赖更新,
总结一句话就是:在 getter 中收集依赖,在 setter 中告诉依赖更新。
// ./src/core/observer/index.js
/* 为对象 defineProperty 上在变动时告诉的属性 */
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
/* 定义一个 dep 对象 */
const dep = new Dep()
//...
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// ...
if (Dep.target) {
/* 进行依赖收集 */
dep.depend()
if (childOb) {
/*
* 子对象进行依赖收集,* 其实就是将同一个 watcher 观察者实例放进了两个 depend 中,* 一个是正在自身闭包中的 depend,另一个是子元素的 depend
*/
childOb.dep.depend()
if (Array.isArray(value)) {
/* 是数组则须要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
// ...
/*dep 对象告诉所有的观察者 */
dep.notify()}
})
}
/**
* ./src/core/observer/dep.js
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
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)
}
/* 依赖收集,当存在 Dep.target 的时候增加观察者对象 */
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
Watcher 又是干什么的呢?
dep 收集依赖后,当数据产生扭转,筹备派发告诉的时候,不晓得该派给谁,或者说不晓得谁用了该数据,于是就须要 watcher 了。
当某个函数在执行的过程中,应用到了响应式数据时,vue 就会为响应式数据创立一个 watcher 实例,当数据产生扭转时,vue 不间接告诉相干依赖更新,而是告诉依赖对应的 watcher 实例去执行。
watcher 会设置一个全局变量window.targe
,让全局变量记录以后负责执行的 watcher 等于本人,而后在执行函数,在执行的过程中,如果产生了依赖记录dep.depenf()
,那么 Dep 会把这个全局变量记录下来,示意有一个 watcher 实例用到了这个响应式数据。
watcher 外围源代码
export default class Watcher {constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn)
this.value = this.get()}
get () {
window.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
/**
* Parse simple path.
* 把一个形如 'data.a.b.c' 的字符串门路所示意的值,从实在的 data 对象中取出来
* 例如:* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
const bailRE = /[^\w.$]/
export function parsePath (path) {if (bailRE.test(path)) {return}
const segments = path.split('.')
return function (obj) {for (let i = 0; i < segments.length; i++) {if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
咱们剖析 Watcher
类的代码实现逻辑:
- 当实例化
Watcher
类时,会先执行其构造函数; - 在构造函数中调用了
this.get()
实例办法; - 在
get()
办法中,首先通过window.target = this
把实例本身赋给了全局的一个惟一对象window.target
上,而后通过let value = this.getter.call(vm, vm)
获取一下被依赖的数据,获取被依赖数据的目标是触发该数据下面的getter
,上文咱们说过,在getter
里会调用dep.depend()
收集依赖,而在dep.depend()
中取到挂载window.target
上的值并将其存入依赖数组中,在get()
办法最初将window.target
开释掉。 - 而当数据变动时,会触发数据的
setter
,在setter
中调用了dep.notify()
办法,在dep.notify()
办法中,遍历所有依赖 (即 watcher 实例),执行依赖的update()
办法,也就是Watcher
类中的update()
实例办法,在update()
办法中调用数据变动的更新回调函数,从而更新视图。
参考文档
Scheduler
当在 setter
中调用了 dep.notify()
办法,在 dep.notify()
办法中,遍历所有依赖 (即 watcher 实例) 时,如果 watcher 执行重运行对应的函数,就会导致函数频繁执行,从而升高了效率,试想一下,如果一个函数,外面用到了 a,b,c,d 等响应式数据,这些数据都会记录依赖,于是当这些数据发生变化时会触发屡次更新,例如:
state.a = "new value";
state.b = "new value";
state.c = "new value";
state.d = "new value";
...
// 每更新一个值触发一次更新
这样显然是不适合的,因而,当 watcher 收到派发的更新告诉后,watcher 不会立刻执行,而是将本人交给一个调度器scheduler
。
调度器 scheduler
保护一个执行队列,同一个 watcher 在该队列中只会存在一次,队列中的 watcher 不会立刻执行,而是通过 nextTick 的工具函数执行,nextTick 是一个微队列,会把须要执行的 watcher 放到事件循环的微队列中执行。
nextTick 的具体做法是通过 Promise
实现的,具体实现办法专利临时不探讨,nextTick 文档
总结
- vue 首先通过
Observer
类,应用Object.defineProperty
办法包装了数据,使 object 变成一个具备getter/setter
属性的数据。 - 读取数据的时候通过
getter
办法读取,并在getter
办法外面调用了Dep
模块的dep.depend()
办法收集依赖,并为该依赖创立一个对应的 watcher 实例。 - 通过
setter
办法扭转数据的时候调用了Dep
模块的dep.notify()
办法来告诉依赖,即依赖对应的 watcher 实例,遍历所有的 watcher 实例。 - watcher 实例不间接更新视图,而是交给
scheduler
调度器,scheduler
保护一个事件队列通过 nextTick 执行事件,从而更新视图。
流程图
补充
Observer
产生在beforeCreate
和created
之间,-
因为遍历时只能遍历到对象的以后属性,因而无奈监测到未来动静减少或删除的属性。
// html <template> <div class="hello"> <h1>a:{{obj.a}}</h1> <h1>b:{{obj.b}}</h1> <button @click="obj.b = 2">Add B</button> </div> </template>
// js <script> export default { name: "HelloWorld", data() { return { obj: {a: 1,}, }; }, };
当点击 Add B 动静给 obj 增加 b 属性时,obj 数据更新了,然而页面没有展现,由此可见之后动静增加和删除的数据不具备响应式个性。
因而 vue
提供了 $set
和$delete
两个实例办法来解决这种状况。
// 新增
this.$set(this.obj, b, 2)
// 删除
this.$delete(this.obj, b)
以上仅集体了解,如有不当之处还请不吝赐教