开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情
Data 响应式化
在开始分析 Data 之前,我们先抛出几个问题,这些问题的会在后面的分析中逐一解答
-
Dep作为管理依赖的类,那么Dep是在什么时候进行初始化的呢?
-
Dep收集了哪些类型的依赖?即Watcher作为依赖有哪些分类,分别在什么场景下使用,却别是什么?
-
Observer这个类在重写getter、setter时具体做了什么?
-
- 在
Vue选项中定义的watcher选项,和页面数据渲染监听的watcher同时都监听到数据的变化,优先级时怎样的?
- 在
-
- 有进行依赖收集,那么是否存在解除依赖?解除依赖的意义在哪里?
依赖收集
data 在初始化阶段会实例化一个 observe 对象
function initData (vm: Component) {
// observe data
observe(data, true /* asRootData */)
}
function observe (value: any, asRootData: ?boolean): Observer | void {
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)
}
return ob
}
// Observer 类
class Observe{
constructor(){
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)
}
}
}
function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
可以看到,在 Observer 实例化时,会往对象上添加一个不可枚举的 __ob__ 属性,并将该属性的值设置为 Observer 实例本身,在调用 observe 方法时, 会判断对象上是否存在 __ob__ 属性,如果已经存在,表明该对象已经时响应式对象,则跳过响应式化的过程。另外, Observer 构造函数中还调用了 walk 方法,在该方法中,会遍历对象属性,对每个属性的 getter setter 方法进行重写,也正是在这一步对添加了数据劫持
class Observer{
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
defineReactive 方法时数据响应式化的核心,来看下具体的实现。
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) {
// 将当前的 watcher 添加到依赖管理中
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
}
})
}
通过上面代码看出, defineReactive 方法在执行时,先会实例化一个 Dep 类,也就是说,数据响应式化过程中,会为数据的每一个属性创建一个依赖管理。之后,使用 Object.defineProperty 重写 getter setter 方法。我们知道,当 data 中的属性值被访问时,会触发 getter 函数的执行,也就是会被 getter 函数拦截,在之前的分析得知,在实例挂载前会创建一个实例 Watcher , 而实例挂载时,会经历将模版解析成 render 函数,由 render 函数转化成虚拟 DOM 的过程,在 render 函数转换成虚拟 DOM 时,会访问袋定义的 data 数据, 那么就会触发 getter 进行依赖收集,而此时收集的依赖就是渲染 Watcher 本身。
class Dep{
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
},
addSub (sub: Watcher) {
this.subs.push(sub)
}
}
class Watcher{
/**
* Add a dependency to this directive.
*/
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)
}
}
}
}
派发更新
在数据发生变化时,会执行在 defineReactive 中重写的 setter 方法。来看下 setter 方法的实现
Object.defineProperty(obj, key, {
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// 新值和旧值相等时,不进行更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// 如果新值是一个对象,需要对新值重现进行依赖收集
childOb = !shallow && observe(newVal)
dep.notify()
}
})
派发更新阶段主要完成一下步骤
-
- 判断更改前后的数据是否相等,如果相等则不进行派发更新操作
-
- 新值为对象时,会对该值的属性进行依赖收集的过程
-
- 通知该数据收集的 watcher 依赖,遍历 watcher 进行数据更新
class Dep{
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 上的 update 方法执行时会将 watcher 对象自身添加到队列中,等待下一个 tick 到来时取出每一个 watcher 执行 run 操作
class Watcher{
update(){
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
queueWatcher 方法的调用,会将数据所收集的依赖一次添加到 queue 数组中, 数组会在下一个 tick 中根据缓冲结果进行视图更新。而在执行视图更新操作时,可能会因为数据的改变而在渲染模版上添加新的依赖,这样又会执行 queueWatcher 过程,所以需要一个标志为判断是否处于异步更新的队列中,如果处于异步更新过程中,则将新的 watcher 添加到 queue 队列中。
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
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)
}
}
}
当下一个 tick 到来时,会执行 flushSchedulerQueue 方法,该方法会拿到一个由 watcher 组成的数组,然后会数据进行排序,源码中对排序进行了说明
-
- 父组件在子组件之前创建,因此需要保证父组件的渲染
watcher在子组件的渲染watcher之前执行
- 父组件在子组件之前创建,因此需要保证父组件的渲染
-
- 用户定义的
watch会在渲染watcher之前执行,因此,用户的watch会在渲染watch之前执行
- 用户定义的
-
- 如果一个组件在父组件的
watcher执行阶段被销毁,那么它对应的所有watcher都可以跳过
- 如果一个组件在父组件的
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// 对 队列中的 watcher 进行排序
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// 如果 watcher 中定义了 before 的配置,则先执行 before 方法
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
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
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
flushSchedulerQueue 阶段,重要的过程可以总结为四点
- 对
queue中的watcher进行排序 - 遍历
watcher, 如果watcher有before配置,则执行before配置,对应前面的渲染watcher: 在渲染watcher实例化时,我们传递了before函数,即在下个tick更新视图之前,会先调用beforeUpdate生命周期钩子 - 执行
watcher.run进行修改的操作 - 重置恢复状态,这个阶段会将一些流程控制的状态变量恢复成初始值,并清空记录
watcher的队列。
重点看看 watcher.run 的操作。
class Watcher{
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
) {
// 设置新值
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)
}
}
}
}
}
首先 run 方法会执行 watcher 上的 get 方法,得到变化后的数据值,然后与旧值进行对比,如果满足条件,则执行 cb 回调, cb 为实例化 watcher 时传入的回调。再来看看 get 方法的定义
class Watcher{
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
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
}
}
get 方法会调用 this.getter 进行求值,在渲染 watcher 下, this.getter 会进行视图更新操作,这一阶段会重新渲染页面组件。在执行完 getter 操作之后,最后一步会进行依赖的清除,也就是 cleanupDeps 的过程。
关于清楚依赖的作用,列举一个场景: 在开发过程中经常使用
v-if来进行模版的切换,切换过程中执行不同的模版渲染,不同模版对数据的依赖可能不同,模版切换之后,看你不再对默写数据进行依赖,这时如果数据发生变化,会引起依赖的重新渲染,造成性能的浪费。因此旧依赖的清除在优化阶段是有必要的。
class Watcher{
// 清除依赖的过程
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
}
}
上面派发更新总结成两点:
- 执行
run操作会执行getter方法,也就是重新计算值,针对渲染watcher而言,会执行updateComponent进行视图更新 - 重新计算
getter之后,会进行依赖的清除