Vue2.6.0源码阅读(四):响应性原理
这一篇我们来看一下Vue
核心的响应性原理,在上一篇我们知道了初始化时Vue
会把data
选项的数据递归的转换成响应性的数据,具体来说就是给数组和对象创建一个关联的Observer
实例,然后对于数组,会拦截它的所有方法,以此来监听数组的变化,对于普通对象,会遍历它自身所有可枚举的属性,将其转换成setter
和getter
形式,以此来监听某个属性的变化,为什么要这么做呢,我们来看一下。
首先简单介绍一下挂载的逻辑,如果我们传递了模板字符串,那么会进行编译,编译模板这个过程非常复杂,反正最后会将其转换为渲染函数,如果直接是渲染函数了那么就没有这个过程,像我们平时使用Vue
单文件的形式开发的话,在打包阶段其实就已经编译好了,渲染函数就是返回虚拟DOM
(也就是VNode
)的函数,VNode
怎么转换成实际的DOM
呢,其中涉及到虚拟DOM
的patch
算法,后面会介绍,只要记得这个过程会获取模板里使用到的数据。接着还会给每个Vue
实例创建一个Watcher
实例,这部分的代码简化如下:
// 更新组件的方法
let updateComponent = () => {
// 执行渲染函数产出VNode,然后调用_update方法进行打补丁
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
定义了一个更新方法,然后传给Watcher
,看Watcher
类的实现之前我们先看一下Dep
类的实现。
Dep类
dep
是一个可观察对象,可以有多个指令订阅它:
let uid = 0
export default class Dep {
static target;
id;
subs;
constructor () {
this.id = uid++
this.subs = []
}
}
定义了两个实例属性,一个静态属性,subs
数组就是用来存放订阅它的对象,有四个实例方法:
1.添加订阅者
addSub (sub) {
this.subs.push(sub)
}
2.删除订阅者
removeSub (sub) {
remove(this.subs, sub)
}
3.依赖收集
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
4.通知订阅者更新
notify () {
// 只通知此刻存在的订阅者,调用其update方法
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
Watcher类
dep
的订阅者其实就是Watcher
实例:
export default class Watcher {
constructor (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm
// 如果是渲染watcher,那么会将该Watcher实例添加到Vue实例的_watcher属性中
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
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
this.active = true
this.dirty = this.lazy // lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = ''
// 从表达式中解析出getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
// 执行取值方法,计算当前的值
this.value = this.lazy
? undefined
: this.get()
}
}
watcher
做的事情主要是解析表达式、依赖收集、并在表达式的值发生改变时触发回调。Vue
把给组件创建的Watcher
实例称为渲染watcher
。
构造函数里先定义了一堆变量,然后判断表达式的类型,是函数的话那么直接使用该函数作为取值函数,否则调用parsePath
方法来解析路径,并返回一个取值函数,这是一个简单的解析方法,只支持对.
分隔的字符串进行解析:
const bailRE = new RegExp(`[^${unicodeLetters}.$_\\d]`)
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
}
}
比如实例的data
结构如下:
let vm = new Vue({
data: {
a: {
b: {
c: 1
}
}
}
})
那么当我们使用表达式a.b.c
来创建Watcher
实例的话:
new Watcher(vm, 'a.b.c', () => {})
那么以vm
为上下文执行parsePath
方法返回的函数时就能成功获取到值1
。
构造函数的最后,如果lazy
不为true
的话会立即执行取值函数get
,计算表达式的当前值。
依赖收集
具体到开头的挂载过程,表达式expOrFn
就是updateComponent
函数,也就是Watcher
实例的this.getter
,它会在get
函数里调用:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {} finally {
// "touch"每个属性,以便它们都作为依赖项进行跟踪,以便进行深入观察
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
首先执行了pushTarget
方法:
// 当前正在计算执行中的目标watcher
// 这是全局唯一的,因为一次只能同时执行一个观察者。
Dep.target = null
const targetStack = []
export function pushTarget (target) {
targetStack.push(target)
Dep.target = target
}
Dep.target
是一个全局变量,这一步相当于把当前的这个Watcher
实例赋值给了Dep.target
属性。
随后执行了getter
函数,也就是执行updateComponent
函数,在vm._render
函数中会获取我们定义在data
中的数据(如果模板里使用到了的话),此时,就会触发对应数据的getter
函数。
比如:
<div>{{a}}</div>
new Vue({
data: {
a: 1
}
})
在执行渲染函数的时候会执行vm.a
来获取a
的值,属性a
已经被转换成了getter
和setter
的形式:
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// Dep.target就是当前的Watcher实例
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}
})
所以会触发属性a
的get
函数执行,此时Dep.target
就是当前的Watcher
实例,那么会执行dep.depend
方法,dep
是属性a
的依赖收集对象,depend
方法内会执行Dep.target
也就是当前的Watcher
实例的addDep
方法:
export default class Watcher {
addDep (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)
}
}
}
}
可以看到这里把属性a
的dep
添加到了newDeps
集合中,为什么不直接添加到deps
中呢?而要区分新旧呢?这其实要结合cleanupDeps
方法一起看,这个方法在get
方法的最后调用了,用来清理依赖项:
export default class Watcher {
cleanupDeps () {
let i = this.deps.length
// 如果之前依赖某个dep,此时不依赖了,那么要从对应dep里删除该watcher
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
// newDepIds转为depIds,并清空旧的depIds
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
// newDeps转为deps,并清空旧的deps
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
}
现在应该很清楚为什么要区分新旧了,因为当前watcher
之前的依赖项,现在可能已经不依赖了,那么要找出不再依赖的dep
,并从中删除当前watcher
,所以需要一个新旧对比的过程。
addDep
方法执行完后,属性a
的dep
里会收集到当前的渲染watcher
,渲染watcher
里也会保存属性a
的dep
。
回到a
的getter
函数,depend
函数执行完后,接着:
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
如果属性a
的值是数组或对象的话,那么也会创建一个关联的Observer
实例:
let childOb = !shallow && observe(val)
这里相当于当前的渲染watcher
也会同时订阅a
的值对应的dep
对象,比如下面这种情况:
new Vue({
data: {
a: {
b: 1
}
}
})
渲染watcher
的deps
数组一共会收集到两个dep
实例,一个是属性a
的,一个是a
的值的。
最后对于a
的值为数组的情况还调用了dependArray
方法:
function dependArray(value ) {
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)
}
}
}
递归遍历数组,对所有对象都进行依赖收集。
总结一下,当我们创建一个Vue
实例时,内部会创建一个渲染watcher
,watcher
实例化时会把自身赋值给全局的Dep.target
,然后执行取值函数,也就是会执行渲染函数,如果模板里引用了data
的数据,那么就会触发对data
数据的读取,相应的会触发对应属性的getter
,在getter
里会进行依赖收集,也就是会把Dep.target
属性指向的watcher
收集为依赖添加到它的dep
实例中,同样该watcher
也会保存该dep
,如果该属性存在子observer
,那么也会进行依赖收集,如果该属性的值是数组类型,还会递归遍历数组,给每个对象类型的数组项都进行依赖收集。
接下来回到Watcher
实例的get
方法,执行完取值方法把依赖收集完毕后:
if (this.deep) {
traverse(value)
}
如果deep
选项配置为true
的话,那么会对值调用traverse
方法,对于实例化Vue
时,该选项是false
,所以并不会走这里,我们以一个其他例子来看:
new Vue({
data: {
obj: {
a: {
b: 1
}
}
},
created() {
this.$watch('obj', () => {
console.log('变了')
})
setTimeout(() => {
this.obj.a.b = 2
}, 2000);
}
})
这个例子中,两秒后我们修改了obj.a.b
的属性值,它并不会触发回调,当我们deep
设为true
时,就会触发:
this.$watch('obj', () => {
console.log('变了')
}, {
deep: true
})
这就是上面的traverse
函数做的事情,这里的取值函数返回的value
就是obj
的值。
const seenObjects = new Set()
export function traverse (val) {
_traverse(val, seenObjects)
seenObjects.clear()
}
调用了_traverse
方法:
export function traverse (val) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val, seen) {
let i, keys
const isA = Array.isArray(val)
// 非数组和对象就返回
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
// 这里我猜测是避免循环引用
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
// 递归
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
其实就是一个简单的递归函数,递归遍历了所有的数组项和对象的key
,为什么这样就能实现监听深层值的变化呢,很简单,因为读取了所有的属性,也就是触发了它们的getter
函数,所以所有属性的dep
都收集了当前的watcher
实例,那么当然如何一层里的任何一个属性修改了都会触发该watcher
实例的更新。
回到get
函数,接下来的逻辑:
popTarget()
this.cleanupDeps()
get
函数的开始调用了pushTarget
,依赖收集完毕当然要撤销这个操作:
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
cleanupDeps
方法前面已经说过,不再赘述。
到这里,Watcher
实例化的过程就已经结束了,现在我们的依赖都已经收集完毕,那么它们有什么用呢?
触发依赖更新
例子如下:
new Vue({
el: '#app',
template: `
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
`,
data: {
list: [1, 2, 3]
},
created() {
setTimeout(() => {
this.list = [4, 5, 6]
}, 5000);
}
})
先来考考大家,这个例子里面的渲染watcher
会收集到几个dep
呢?
2个,list属性一个,list的属性值数组一个
五秒后我们修改了list
的值,显然会触发它的setter
:
Object.defineProperty(obj, key, {
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
// 值没有变化则直接返回
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// #7981: 对于不带setter的访问器属性
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 观察新的值
childOb = !shallow && observe(newVal)
// 触发更新
dep.notify()
}
})
首先更新属性值,然后如果新的值是数组或对象的话那么会调用observe
方法来将它转成响应式的,这样当我们修改这个新数组或新对象本身时才能触发更新。
最后通知依赖更新,这里的依赖就是渲染watcher
,notify
方法里会调用watcher
的update
方法:
export default class Watcher {
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
可以看到有三个分支,如果lazy
为true
的话那么只设置一下dirty
属性,这个属性具体有什么作用后面我们如果遇到了再说,如果是同步的,那么会直接执行run
函数:
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// 即使值相同,深度的watcher和对象/数组上的watcher也应该触发,因为值可能已经发生了变化。
isObject(value) ||
this.deep
) {
// 设置为新值
const oldValue = this.value
this.value = value
// 用户的watcher,即通过$watch方法或watch选项设置的
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
方法更新当前表达式的值,然后当值有变化则会调用回调函数。
当也不是同步的watcher
,那么会执行queueWatcher
方法,这个涉及到Vue
的调度功能,也就是异步批量执行的功能,我们会单独开一篇文章来介绍。
总结一下如何触发依赖更新,当依赖收集完毕后,如果我们更新了某个属性的值,那么该属性值的getter
函数会被执行,就会通知保存在该属性值的dep
里的所有依赖,调用它们的update
方法进行更新。
对于我们开头的例子,也就是会调用渲染watcher
的update
方法,重新执行取值函数,也就是会执行updateComponent
方法来重新执行渲染函数,并进行打补丁更新组件,达到数据更新页面自动更新的效果。
- 点赞
- 收藏
- 关注作者
评论(0)