vue 版本2.5.17
Object.defineProperty
Object.defineProperty(obj,prop, descriptor)
// obj 是定义属性的对象,prop 是属性名称,descriptor 是属性描述符,核心点就是 descriptor,vue 主要就是利用 descriptor 的 get 和 set 做响应式处理
依赖收集
const dep = new Dep()
dep.depend()
在 get 方法中创建一个 dep 对象,通过 depend()做依赖收集,Dep 是一个 class,有一个静态属性 target,是一个全局唯一 Watcher,保证同一时间只能有一个全局的 Watcher 被计算,Dep 可以理解为是用来管理 Watcher 的
Watcher
Watcher 是一个 class,定义了一些与 Dep 相关的属性,保存 Wathcer 实例持有的 Dep 实例的数组
过程
知道了在访问数据对象的时候会触发 get 方法收集依赖,那 get 方法是在什么时候触发的呢?这一切需要从 组件mount 开始
在 mountComponent中有一段主要逻辑代码
updateComponent = () => {
vm._update(vm._render(), htdrating
}
new Watcher(vm, updateComponent, noop, null, true)
在 mount 的时候会实例化一个 渲染watcher,先执行 Watcher 的构造函数,然后调用this.get()方法,进入get 函数,首先会执行:
pushTarget(this)
pushTarget的定义在 dep.js 中,定义如下:
export function pushTarget (_target: Wathcer) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
实际上就是把 Dep.target复制为当前的渲染 watcher 并压栈(恢复的时候用),接着又执行了:
value = this.getter.call(vm, vm)
this.getter 对应的就是 updateComponent函数,实际上就是执行
vm._update(vm._render(), hydrating)
会执行 vm._render()方法,这个方法会生成渲染 VNode,并且在这个过程中会访问 vm 上的数据,这个时候就触发了数据对象的 getter,每个对象的 getter 都有一个 dep,在触发 getter 的时候调用 dep.depend()方法,接着调用 Dep.target.addDep(this),即渲染 watcher 的 addDep 方法,定义如下:
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)
}
}
}
派发更新
在 defineProperty的 set中有两个关键点,一个是 childOb = !shaoolw && observe(newVal), 将新值变成一个响应式对象;另一个是 dep.notify(),通知所有的订阅者。
过程分析
当对组件响应式数据做了修改时,就会触发 setter,调用 dep.notify(),notify()方法在 dep.js 中,定义如下
nodify () {
const subs = this.subs.slice()
for(let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
变量所有的 subs,即 Watcher 的实例数组,然后调用每一个 watcher 实例的 update 方法,update 方法定义在 watcher.js 中:
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
这里针对 Watcher的不同状态,会执行不同的逻辑,在渲染中会执行到 queueWatcher,定义在 scheduler.js:
const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
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
nextTick(flushSchedulerQueue)
}
}
}
同一个 watcher 只会触发一次,不是每次数据改变都会触发 watcher 的回调,而是将 watcher 添加到一个队列,在 nextTick 中执行 flushSchedulerQueue,定义如下:
function flushSchedulerQueue () {
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
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]
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')
}
}
首先会依据 id 从小到大排序,主要是确保:1.组件的更新由父到子,2.用户自定义的 watcher 优先于渲染 watcher 执行 3.如果一个组件在父组件的 watcher 执行期间被销毁,那对应的 watcher 不需要执行
然后遍历队列,执行 watcher.run()。有一个细节就是在遍历的时候会堆 queue.length 求值,因为在执行 watcher.run 的时候,可能会有新的 watcher 添加进来,新增的 watcher 会依据 id 添加到队列,在下一次循环中立即执行,状态恢复,情况队列。
接下来看 watcher.run 的逻辑
/**
* 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)
}
}
}
}
首先调用 get 方法,拿到当前的值,然后判读是否满足新旧值不等,新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调,对于渲染 watcher 而言,在执行 this.get()方法的时候会调用 getter 方法,即:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
所以这就是当我们修改响应式数据触发组件重新渲染的原因。
- 问题:数组是怎么收集依赖的?
回答:在defineProperty 中有一段代码 如下
let childOb = !shallow && observe(val)
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
在 Observer的构造函数中有如下代码
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
/**
* Collect dependencies on array elements when the array is touched, since
* we cannot intercept array element access like property getters.
*/
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
如果 val 是数组的话,调用 observe(val)方法将数组变成响应式的,实际上就是给数组添加一个'ob'属性,把该属性值设置为该 Observe的实例,用于标识该数组是一个响应式的数组,并通过数组遍历,将数组里面的每一个元素都变成响应式的,调用 childOb.depend.depend()收集该数组依赖,并且调用 dependArray()遍历收集该数组中每个元素的依赖。在 array.js 中,将改变数组的7个方法做了劫持重写,在调用这些修改数组的方法时,会触发 ob.dep.notify()方法,在之前收集了数组的依赖,所以会在 notify 的时候触发更新,另外对于新增的元素,可以通过ob属性,调用 Observe 的 observeArray 方法,将新增的属性变成响应式的。
网友评论