- 篇幅较长,不过都是精华。
- 不过静下心来通篇吃透,保你收获不小。
- 有任何问题欢迎评论区留言。
- 来吧,话不多说,就是干!
响应式原理回顾
- 当你把一个普通的
JavaScript对象传入Vue实例作为 data 选项,Vue将遍历此对象所有的property,并使用 Object.defineProperty 把这些property全部转为 getter/setter。 - 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
- 每个组件实例都对应一个
watcher实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。 【 依赖收集 】 - 之后当依赖项的
setter触发时,通知watcher,从而使它关联的组件重新渲染。 【 派发更新 】
以上原理回顾来自 Vue 官方文档,更多信息可以进官方文档细看。
简介
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。接下来我们通过对部分 Vue 源码(本文主要针对 v2.6.11 的 Vue 源码进行分析)的简单分析和学习来深入了解 Vue 中的数据响应式是怎样实现的。我们将这一部分的代码分析大致分为三部分:怎么让数据变成响应式、依赖收集、派发更新
一、怎么让数据变成响应式?
整体流程图
调试代码调用栈
我自己 debug 了一下,上图流程的调用栈如下图所示:
具体一点的函数调用流程
1、调用 new Vue() 执行构造函数
在 src/core/instance/index 中定义了 Vue 的构造函数,传入的参数是 options。进行 vue 实例化时主要调用了一个 this._init() 的函数。
2、构造函数中执行 initMixin(Vue) 中定义的 _init() 方法
function Vue (options) {
// ...
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
// ...
export default Vue
3、_init() 方法 中 调用 initState(vm)
在 _init() 中对当前传入的 options 进行了一些处理,主要是判断当前实例化的是否为组件,使用 mergeOptions 方法对 options 进行加工,此处不做赘述。然后又调用了一系列方法进行了生命周期、事件、渲染器的初始化等,这里我们主要关注 initState 这个方法:
Vue.prototype._init = function (options?: Object) {
// ...
// 初始化 vm 的 _props/methods/_data/computed/watch
initState(vm)
// ...
}
4、initState(vm) 中调用 initData(vm)
initState(vm) 中对 props、methods、data、computed 和 watch 进行了初始化,这些都是 Vue 实例化方法中传入参数 options 对象的一些属性,这些属性都需要被响应式化。而针对于 data 的初始化分了两种情况,一种是 options 中没有 data 属性的,该方法会给 data 赋值一个空对象并进行 observe,如果有 data 属性,则调用 initData 方法进行初始化。
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe((vm._data = {}), true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
5、initData(vm) 方法中 最后 调用了 observe(data, true /* asRootData */)
initData 方法对 options 中的 data 进行处理,主要是有两个目的
- 1、将
data代理到Vue实例上,同时检查data中是否使用了Vue中的保留字、是否与props、methods中的属性同名。 - 2、使用
observe方法将data中的属性变成响应式的。
接下来会对 data 中的每一个数据进行遍历,遍历过程将会使用 hasOwn(methods, key)、hasOwn(props, key)方法对该数据是否占用保留字、是否与 props 和 methods 中的属性重名进行判断并在浏览器等控制台给出 warn 提示信息。最后再用 !isReserved(key) 判断当前属性名是否是以$和_开头的(即判断是否是 Vue 中的保留字),最后才调用 proxy 方法将其代理到 Vue 实例上。
此处需要注意的是:如果 data 中的属性与 props、methods 中的属性重名,那么在 Vue 实例上调用这个属性时的优先级顺序是 props > methods > data。
最后对 data 中的每一个属性调用 observe 方法,该方法赋予 data 中的属性可被监测的特性。
function initData(vm: Component) {
let data = vm.$options.data
// 初始化 _data,组件中 data 是函数,调用函数返回结果
// 否则直接返回 data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' &&
warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' &&
warn(`The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
// 响应式处理
observe(data, true /* asRootData */)
}
6、observe(value) 中最后实例化了 Observer 类,并且把需要响应式的数据传入
export function observe(value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
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)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
7、Observer 类的构造函数中调用了它的实例方法 this.walk(value)
可以看到该构造函数有以下几个目的
- 针对当前的数据对象新建了一个订阅器
- 为每个数据的
value都添加一个__ob__属性,该属性不可枚举并指向自身 - 针对数组类型的数据进行单独处理
- 调用
this.walk(value)
constructor(value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0 // 初始化实例的 vmCount 为0
def(value, '__ob__', this) // 将实例挂载到观察对象的 __ob__ 属性
// 数组的响应式处理
if (Array.isArray(value)) {
if (hasProto) {//判断当前浏览器是否支持 __proto__ 这个属性
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 为数组中的每一个对象创建一个 observer 实例
this.observeArray(value)
} else {
// 遍历对象中的每一个属性,转换成 setter/getter
this.walk(value)
}
}
8、this.walk(value) 中遍历了 value 中的所有属性,调用 defineReactive(obj, keys[i])
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk(obj: Object) {
// 获取观察对象的每一个属性
const keys = Object.keys(obj)
// 遍历每一个属性,设置为响应式数据
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
9、defineReactive(obj, keys[i])
defineReactive() 方法中最终使用 Object.defineProperty 把这些 property 全部转为 getter/setter。是真正为数据添加 get 和 set 属性方法的方法,它将 data 中的数据定义一个响应式对象,并给该对象设置 get 和 set 属性方法,其中 get 方法是对依赖进行收集, set 方法是当数据改变时通知 Watcher 派发更新。
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean // 浅的,如果为 true 就只监听第一层数据,false 进行深度监听
) {
// 创建依赖对象实例 作用是为当前这个属性收集依赖 也就是收集观察当前这个属性的所有 watcher
const dep = new Dep()
// 获取 obj 的属性描述符对象
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]
}
// 判断是否递归观察子对象,并将子对象属性都转换成 getter/setter,返回子观察对象
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 如果预定义的 getter 存在则 value 等于 getter 调用的返回值
// 否则直接赋予属性值
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend() // 依赖收集
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 如果新值等于旧值或者新值旧值为NaN则不执行
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 判断是否递归观察子对象,观察子对象并返回子对象的 observer 对象
childOb = !shallow && observe(newVal)
// 派发更新(给订阅者发布更新通知)
dep.notify()
}
})
}
到此我们就把创建 Vue 实例时传入的 options 中的 data 通过 Object.defineProperty() 转换为一个响应式对象了
二、依赖收集
依赖收集的简要原理就是:当我们的视图被渲染时,会用到我们定义在 data 中的数据,这样就会触发这些数据的 get 属性方法,Vue 通过 get 方法进行依赖收集。get 方法中的代码如下:
get: function reactiveGetter() {
// 如果预定义的 getter 存在则 value 等于 getter 调用的返回值
// 否则直接赋予属性值
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
可以看到 get 属性方法中调用了一个 dep.depend 方法,该方法正是依赖收集的开始。
Dep 类之订阅器原理
数据对象中的 get 方法主要使用 depend 方法进行依赖收集,而 depend 是 Dep 类中的属性方法,我们来看下 Dep 类的代码是怎样实现的:
export default class Dep {
static target: ?Watcher
id: number
subs: Array<Watcher>
constructor() {
this.id = uid++
this.subs = []
}
// 添加新的订阅者 watcher 对象
addSub(sub: Watcher) {
this.subs.push(sub)
}
// 移除订阅者
removeSub(sub: Watcher) {
remove(this.subs, sub)
}
// 将观察对象和 watcher 建立依赖
depend() {
if (Dep.target) {
// 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
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()
}
}
}
// Dep.target 用来存放目前正在使用的 watcher
// 全局唯一,并且一次也只能有一个watcher被使用
// 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 = []
// 入栈并将当前 watcher 赋值给 Dep.target
// 父子组件嵌套的时候先把父组件对应的 watcher 入栈,
// 再去处理子组件的 watcher,子组件的处理完毕后,再把父组件对应的 watcher 出栈,继续操作
export function pushTarget(target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
Dep 类中定义了三个属性:target、id 和 subs,分别表示全局唯一的静态数据依赖的监听器 Watcher、该属性的 id 以及订阅这个属性数据的订阅者列表 subs。其中 subs 其实就是存放了所有订阅了该数据的订阅者们。另外还提供了将订阅者添加到订阅者列表的 addSub 方法、从订阅者列表删除订阅者的 removeSub 方法。
我们能看到在执行 dep.depend() 方法之前,其实是需要先判断 Dep.target 这个全局唯一的静态数据依赖的监听器 Watcher 是否存在的,只有当它有值的时候才会去执行 dep.depend() 收集依赖。所以我们得先搞清楚这个 Dep.target 是在哪里被赋值的。
Dep.target 是在哪里被赋值的
我们在 Dep 类的代码中能看到这个 Dep.target 是一个 Watcher 的实例,所以我通过查看源码找到了创建 watcher 对象的地方,发现是在 src/core/instance/lifecycle.js中的 mountComponent函数中执行了 new Watcher()
mountComponent()
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)// 是否为渲染watcher的标识
watcher 类的构造函数
通过 Watcher 类的构造函数传参可以看到 mountComponent 中创建的 Watcher 是一个渲染 Watcher,将当前的 getter 设置为了 updateComponent 方法(这也就是重新渲染视图的方法),最后调用了 get 属性方法。我们接下来看看 get 方法中做了什么
this.value = this.lazy //lazy 的含义是-是否延迟加载
? undefined
: this.get()
watcher 类的静态方法 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
}
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
get 方法中首先调用了在 Dep 类文件中定义的全局方法 pushTarget,将 Dep.target 置为当前正在执行的渲染 Watcher,并将这个 watcher 压到了 targetStack。接下来就会调用 wathcer 的 getter 方法
由于此时的 getter 就是 updateComponent 方法,执行 updateComponent 方法,也就是 vm._update(vm._render(), hydrating) 进行渲染页面了,渲染过程中就会 touch 到 data 中相关的依赖属性,这样就会触发各个属性中的 get 方法了,然后就会走到方法里面的 dep.depend()(因为此时 Dep.target已经是被赋值为当前正在执行的渲染 Watcher)
// 将观察对象和 watcher 建立依赖
depend() {
if (Dep.target) {
// 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
Dep.target.addDep(this)
}
}
depend() 方法很简单,只是执行了一下 Dep.target.addDep(this),这里的 Dep.target 是一个 Watcher 实例,里面的参数 this 也就是当前的 Dep 实例。我们看下在 Watcher 中的 addDep() 方法执行了啥操作
/**
* 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)
}
}
}
addDep方法中做了一些优化,对同一个属性只收集一次依赖。dep.addSub(this)就是调用Dep的属性方法addSub将当前Watcher添加到依赖数组中,用于之后的派发更新。
// 添加新的订阅者 watcher 对象
addSub(sub: Watcher) {
this.subs.push(sub)
}
在本次渲染完之后会将当前的渲染 Watcher 从 targetStack 推出,进行下一个 watcher 的任务。最后会进行依赖的清空,每次数据的变化都会引起视图重新渲染,每一次渲染又会进行依赖收集,又会触发 data 中每个属性的 get 方法。有下面这种情况
<template>
<div>
<span v-if="isShow"> {{ jing }} </span>
<span v-else> {{ hao }} </span>
</div>
</template>
第一次当 isShow 为真时,进行渲染,当我们把 isShow 的值改为 false 时,这时候属性 jing 根本不会被使用,所以如果不进行依赖清空,会导致不必要的依赖通知。依赖清空主要是对 newDeps 进行更新,通过对比本次收集的依赖和之前的依赖进行对比,把不需要的依赖清除。
/**
* Clean up for dependency collection.
*/
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
}
三、派发更新
debug 实例
页面代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>observe</title>
</head>
<body>
<div id="app">
<span> {{ arr }}</span>
<child-one :arr="arr"></child-one>
<child-two :arr="arr"></child-two>
<child-three :arr="arr"></child-three>
</div>
<script src="../../dist/vue.js"></script>
<script>
Vue.component('child-one', {
props: ['arr'],
template: '<p>{{ arr }}</p>'
})
Vue.component('child-two', {
props: ['arr'],
template: '<div>{{ arr }}</div>'
})
Vue.component('child-three', {
props: ['arr'],
template: '<p>{{ arr }}</p>'
})
const vm = new Vue({
el: '#app',
data: {
arr: [2, 3, 5]
}
})
</script>
</body>
</html>
调试效果如下:
可以很清楚的看到 arr 这个数据的 dep.subs 数组中收集了 4 个 Watcher 实例。
dep.notify()
// 发布通知
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()
}
}
而当数据变化时,会触发数据的 setter,在 setter 中调用了 dep.notify() 方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖的 update() 方法,也就是 Watcher 类中的update()实例方法。
update()
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
update 方法是在 Watcher 类文件中,里面使用了队列的方式来管理订阅者的更新派发,其中主要调用了 queueWatcher 这个方法来实现该逻辑:
queueWatcher()
/**
* 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
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
首先使用一个名为 has 的 Map 来保证每一个 watcher 仅推入队列一次。flushing 这个标识符表示目前这个队列是否正在进行更新派发,如果是,那么将这个 id 对应的订阅者进行替换,如果已经路过这个 id,那么就立刻将这个 id 对应的 watcher 放在下一个排入队列。接下来根据 waiting 这个标识符来表示当前是否正在对这个队列进行更新派发,如果没有的话,就可以调用 nextTick(flushSchedulerQueue) 进行真正的更新派发了。
flushSchedulerQueue()
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
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]
if (watcher.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')
}
}
这个方法首先对队列中的 watcher 按照其 id 进行了排序,排序的主要目的注释中写的很清楚了。
- 1、
Vue的组件更新是由父到子,因为父组件的创建过程是先于子的,所以watcher的创建也是先父后子,执行顺序也应该保持先父后子。 - 2、用户的自定义
watcher要优先于渲染watcher执行;因为用户自定义watcher(initState) 是在渲染watcher(mountComponent) 之前创建的。 *( *initState 是比 mountComponent 先执行的) - 3、如果一个组件在父组件的
watcher执行期间被销毁,那么它对应的watcher都应该被跳过,所以父组件的watcher应该先执行。
排序之后就遍历这个队列,执行 watcher.run() 方法。
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)
}
}
}
}
watcher.run() 方法中
- 新值与老值不同时会触发回调函数
- 但是如果值是对象或者
deep为true时都会触发回调函数
当回调函数重新渲染视图的时候,又会进行下一轮的依赖收集,之后又派发更新......无限套娃
四、🌈🌈🌈💨💨💨
- 好了,我是金同学,这次的分享就到这里了。
- 要是有下一篇,应该就是关于
Vue虚拟Dom源码的相关分享了。 - 当然了,希望我们会再次重逢。
- 努力生活是头等大事,祝朋友们岁月静好。
- 希望疫情下的次生灾难少一点、悲剧发生的少一点。🙏🏻🙏🏻🙏🏻