从需求出发阅读 Vue 源码之响应式基础(Vue 2)
今天这篇笔记以响应式基础为主题继续从需求出发阅读 Vue 源码,有了之前两篇笔记从需求出发阅读 Vue 源码之声明式渲染(Vue 2)和从需求出发阅读 Vue 源码之声明式渲染(Vue 3)
的经验,我们在比较 Vue 2 和 Vue 3 在响应式基础上的不同就变得比较容易了。
初始数据被渲染到了页面上,那么接下来自然能想到如何在修改数据的时候自动更新页面渲染呢,这就产生了响应式的需求。
首先,我们先把之前的测试代码更改一下让其在完成挂载后延迟两秒更改 data 选项属性值,打开浏览器执行以下经过更改的代码,页面会在两秒之后渲染新的属性值。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue Reactivity</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>{{ message }}</div>
</div>
</body>
<script>
new Vue({
el: '#app',
data: {
message: 'hello vue!'
},
mounted() {
setTimeout(() => {
this.message += ' hello world!'
}, 2000)
}
})
</script>
</html>
打开 src/core/instance/state.js,在第 113 行 initData
函数中拿到 data 选项值,在函数最后一行执行了 observe
函数。
......
function initData (vm: Component) {
......
observe(data, true /* asRootData */)
}
......
打开 src/core/observer/index.js,第 110 行定义了函数 observe
,第 124 行函数内新建了一个 Observer
实例。
export function observe (value: any, asRootData: ?boolean): Observer | void {
......
let ob: Observer | void
......
ob = new Observer(value)
......
return ob
}
src/core/observer/index.js 第 37 行定义了 Observer
类,在类构造方法中,创建了 Dep
实例并赋值给了类属性 dep
,执行了类方法 walk
去遍历 data
对象的每个属性,将 data
对象和对象中的每个属性名(key)作为参数传入 defineReactive
方法并执行。
......
export class Observer {
value: any;
dep: Dep;
......
constructor (value: any) {
this.value = value
this.dep = new Dep()
......
def(value, '__ob__', this)
......
this.walk(value)
......
}
......
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
src/core/observer/index.js 第 135 行定义了 defineReactive
函数,第 142 行创建了 Dep
实例并赋值给函数内部变量 dep
,第 156 行会继续执行 observe
方法进行一个递归操作对深层嵌套的对象值设置响应式,第 157 行使用 Object.defineProperty
方法为对象属性修改描述符,修改完成后当访问该属性时会执行 160 行的 reactiveGetter 取值函数 get
,当为属性设置新值时会执行 173 行的 reactiveSetter 存值函数 set
。
export function defineReactive (
obj: Object,
key: string,
val: any,
......
) {
const dep = new Dep()
......
let childOb = ...... observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = ...... val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
......
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = ...... val
// 此处用自身进行比较是为了排除值为 NaN 的情况
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
......
val = newVal
......
childOb = ...... observe(newVal)
dep.notify()
}
})
}
Observer
类构造函数和 defineReactive
函数中都创建了 Dep
类的实例,Dep
类定义在 src/core/observer/dep.js 第 13 行,其中类静态属性 target
标识了当前目标(当前目标为 Watcher
实例),并有对应的方法 pushTarget
和 popTarget
在目标栈 targetStack
中添加或移除目标同时设置当前目标,类属性 id
为 Dep
类实例的唯一标识,类属性 subs
是订阅数组并有相对应的 addSub
和 removeSub
方法为数组添加和删除订阅(sub
订阅为 Watcher
实例),另外还有 depend
方法为当前目标添加 Dep
实例,notify
方法执行 subs
数组中每项订阅的 update
方法。
import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'
let uid = 0
/**
* 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)
}
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()
}
}
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
通过上述可以发现 data
对象每个属性对应一个 Dep
实例,包括 data
对象自身也对应一个 Dep
实例,但是没有被 Watcher
实例收集。
src/core/instance/lifecycle.js 中第 197 行创建了 Watcher
实例,传入参数主要是 Vue
实例和 updateComponent
函数,从这里也可以发现一个 Vue
实例对应一个 Watcher
实例,updateComponent
函数就是在从需求出发阅读 Vue 源码之声明式渲染(Vue 2)提到的生成虚拟节点并完成挂载的函数。第 167 行调用了 beforeMount
生命周期函数,第 210 行调用了 mounted
生命周期函数。
export function mountComponent (
vm: Component,
el: ?Element,
......
): Component {
......
callHook(vm, 'beforeMount')
let updateComponent
......
updateComponent = () => {
vm._update(vm._render(), ......)
}
......
new Watcher(vm, updateComponent, noop, {
......
}, true /* isRenderWatcher */)
......
callHook(vm, 'mounted')
......
return vm
}
src/core/observer/watcher.js 第 27 行定义了 Watcher
类,在从需求出发阅读 Vue 源码之声明式渲染(Vue 2)中曾提过在类中将构造函数入参 updateComponent
赋值给 this.getter
并通过 this.get
方法让函数得以执行以此完成页面渲染,现在我们还需关注 this.getter
方法执行之前的 pushTarget
操作。
......
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
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 for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for 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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
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
}
......
}
......
pushTarget
方法会将当前的 Watcher
实例添加到 targetStack
数组中,并将当前目标设置为此 Watcher
实例,随后在执行 this.getter
函数时会读取 data
对象中已经设置了取值函数的属性,触发 dep.depend
收集依赖,执行 Watcher
实例中的 addDep
方法并将当前 Dep
实例添加到 Watcher
实例的 newDeps
数组中,同时在 newDepIds
集合中添加 Dep
实例的 id
属性,最后在 Dep
实例中添加订阅也就是将当前 Watcher
实例添加到 Dep
实例的 subs
数组中,当数据发生变更时发布通知 dep.notify
执行订阅,这里就用到了经典的设计模式发布订阅模式。
// 1. src/core/observer/watcher.js
pushTarget(this)
// 2. src/core/observer/dep.js
pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
// 3. src/core/observer/watcher.js
this.getter()
// 4. reactiveGetter
// 5. src/core/observer/index.js
dep.depend()
// 6. src/core/observer/dep.js
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 7. src/core/observer/watcher.js
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)
}
}
}
// 8. src/core/observer/dep.js
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 9. reactiveSetter
// 10. src/core/observer/dep.js
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
......
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
src/core/observer/dep.js 第 47 行实际上执行的就是 Watcher
实例的 update
方法去更新页面显示,到这里就完成了响应式,至于在更新过程中的异步更新队列、diff 算法、nextTick 等我们在之后的笔记中再去记录。
刚才只是在 data 选项对象中设置了一个字符串类型的 message
属性,接下来继续设置一些复杂类型来试试。将测试代码改成如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>{{ message }}</div>
<div>{{ this.car.name }}</div>
</div>
</body>
<script>
new Vue({
el: '#app',
data: {
message: 'hello vue!',
car: {
name: 'xiaomi'
}
},
mounted() {
setTimeout(() => {
this.message += ' hello world!'
this.car.name = 'tesla'
}, 2000)
}
})
</script>
</html>
执行上述代码能观察到 2 秒后更改 data 选项对象中复杂类型 car
对象的 name
属性值,页面上也能反应出变化,这是由于在 src/core/observer/index.js 第 156 行执行函数 observe
递归处理对象的深层次嵌套属性,因此即使我们把测试代码改成上述那样也依然能保持响应性。
但是当把测试代码改成如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>{{ message }}</div>
<!-- <div>{{ this.car.name }}</div> -->
<div>{{ this.car.color }}</div>
</div>
</body>
<script>
new Vue({
el: '#app',
data: {
message: 'hello vue!',
car: {
name: 'xiaomi'
}
},
mounted() {
setTimeout(() => {
// this.message += ' hello world!'
// this.car.name = 'tesla'
this.car.color = 'red'
}, 2000)
}
})
</script>
</html>
这样的话 2 秒后更改 data 选项对象中复杂类型 car
对象的 color
属性值,页面上就不能反应出变化了,这是因为由于没有在 data 选项对象中定义复杂类型 car
对象的 color
属性,而响应性是依赖于 Object.defineProperty
所定义的存取值函数的,因此其不具备响应性的。这也是我们常常碰到的数据更新了,但是页面没有更新的情况之一,常用的解决办法是使用全局 Vue.set
方法为新属性设置响应性。
但是如果把代码改成如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>{{ message }}</div>
<!-- <div>{{ this.car.name }}</div> -->
<div>{{ this.car.color }}</div>
</div>
</body>
<script>
new Vue({
el: '#app',
data: {
message: 'hello vue!',
car: {
name: 'xiaomi'
}
},
mounted() {
setTimeout(() => {
this.message += ' hello world!'
// this.car.name = 'tesla'
this.car.color = 'red'
}, 2000)
}
})
</script>
</html>
在 2 秒后同时更新 message
和 car.color
的值,页面上可以反应出 car.color
的更改,这是因为 message
是响应性的,当 message
变化出发页面更新时,渲染函数执行时会读取新的 car.color
值,渲染到页面时也就体现了变更。
接下来我们测试当复杂类型属性为数组时的处理,在 src/core/observer/index.js 第 37 行定义的类 Observer
的构造函数中,并没有把数组的每个成员变成响应式,也就是说通过数组下标或者改变数组 length
属性是不能触发页面更新的,而是对数组做了特殊处理,这个原因 Vue 2 的作者尤雨溪亲自回应过就是性能代价和获得的用户体验收益不成正比。
import { ...... def ...... hasProto ......} from '../util/index'
// export const hasProto = '__proto__' in {}
// export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
// Object.defineProperty(obj, key, {
// value: val,
// enumerable: !!enumerable,
// writable: true,
// configurable: true
// })
// }
import { arrayMethods } from './array'
// const arrayProto = Array.prototype
// export const arrayMethods = Object.create(arrayProto)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object) {
target.__proto__ = src
}
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
......
export class Observer {
......
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
......
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
......
}
......
根据上述代码可以看出程序对数组原型链做了变动,改变后不会直接访问数组原型链上的方法,而是访问经过处理的原型链方法。同时,程序执行了 observeArray
函数遍历数组,递归地调用 observe
函数为数组成员设置响应性。
原型链方法在 src/core/observer/array.js 中做出的具体变动如下,针对 methodsToPatch
中这几种方法,会先执行原始方法拿到结果,对会产生新数据的几种方法比如 push
,程序拿到传入的新数据后执行 observeArray
,和上面一样为可能新增的对象属性设置响应性,最后发布通知执行订阅返回结果。也就是说执行这几种数组的执行方法会触发页面更新。
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
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)
// notify change
ob.dep.notify()
return result
})
})
至此 Vue 2 的响应式基础完成。