[TOC]
数据驱动与响应式原理
Vue 的一个核心思想就是 数据驱动,即数据会驱动界面,如果想修改界面,直接对相应的数据进行修改即可, DOM 元素会自动更新。
数据驱动 的思想,其实质是将界面 DOM 元素映射到数据上,解耦了业务与视图,这样可以让我们只专注于对数据的操作,而无须与视图进行交互,简化了业务逻辑,代码更加清晰。
注:前端开发中,视图的展示本身就是由数据进行驱动的。传统前端开发过程中,一般都是先从后端获取数据,然后手动将数据渲染到前端 DOM 元素上,由于一个页面上可能存在很多 DOM 元素需要进行渲染,这样我们的业务代码中就充斥着许多与业务无关的 DOM 操作,代码会显得臃肿和混乱。而 数据驱动 思想可以自动完成渲染到 DOM 元素这个操作,对于我们的代码来说,只需进行数据获取即可,这才是更加纯粹的数据驱动模型。
举个栗子:一个最简单的数据驱动例子如下所示:
<div id="app">
<h1>{{message}}</h1>
</div>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
});
</script>
h1
元素的内容由Vue
实例的数据data.message
进行驱动,并且,当我们手动更改message
的值时,可以看到,视图同时也随之更新了。
由上,我们可以知道,数据驱动 其实包含两部分内容:组件挂载 与 响应式。
- 组件挂载:是将初始数据渲染到真实 DOM 上的过程
- 响应式:是数据的更新驱动视图的过程
组件挂载 内容请参考:Vue 源码解析 - 组件挂载
以下我们主要对 Vue 的 响应式原理 进行解析。
在 Vue 源码解析 - 主线流程 中,我们知道,当new Vue(Options)
的时候,实际上调用的是_init
函数:
// src/core/instance/index.js
function Vue(options) {
...
this._init(options)
}
而_init
函数中, 会执行一系列的初始化,其中就包含有initState(vm)
:
// src/core/instance/init.js
export function initMixin(Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
...
// 初始化 props、methods、data、computed 与 watch
initState(vm)
...
}
}
initState
函数主要对Vue
组件的props
,methods
,data
,computed
和watch
等状态进行初始化:
// src/core/instance/state.js
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
// 初始化 options.props
if (opts.props) initProps(vm, opts.props)
// 初始化 options.methods
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 初始化 options.data
initData(vm)
} else {
// 没有 options.data 时,绑定为一个空对象
observe(vm._data = {}, true /* asRootData */)
}
// 初始化 options.computed
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
// 初始化 options.watcher
initWatch(vm, opts.watch)
}
}
我们主要来看下initState
函数中的initData(vm)
操作:
// core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm) // getData:解析出原本的 options.data
: data || {} // data 不是函数,直接使用
if (!isPlainObject(data)) { // data 不是对象,开发环境会给出警告
data = {}
...
}
// proxy data on instance
const keys = Object.keys(data)
...
let i = keys.length
while (i--) {
const key = keys[i]
...
else if (!isReserved(key)) {
// 为 vm 组件对象设置与 key 同名的访问器属性,作为 key 的代理,真实值存储于 vm._data 对象中
// 这步操作过后,vm 就具备了与 options.data 所有的同名 key 的访问器属性,因此,使用 this.xxx 操作
// data 中的数据就是操作组件对象 vm 的访问器属性,相当于 options.data 的数据被组件对象拦截了。
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
export function getData (data: Function, vm: Component): any {
...
return data.call(vm, vm)
...
}
// src/shared/util.js
/**
* Strict object type check. Only returns true
* for plain JavaScript objects.
*/
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
// src/core/util/lang.js
/**
* Check if a string starts with $ or _
*/
export function isReserved (str: string): boolean {
const c = (str + '').charCodeAt(0)
return c === 0x24 || c === 0x5F
}
initData
函数会获取我们在new Vue(Options)
时传递进去的Options.data
数据,然后进行遍历,对其进行proxy
操作,最后会为Options.data
进行observe
操作。
下面我们先对proxy
进行分析,其源码如下:
// core/instance/state.js
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
// 为 target 添加访问器属性 key
Object.defineProperty(target, key, sharedPropertyDefinition)
}
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
proxy
函数的功能就是通过Object.defineProperty
为target
对象添加字段为key
的访问器属性,并设置其get
/set
的具体操作。
当我们调用proxy(vm, '_data', key)
时,就是为vm
添加与Options.data
相同键值的访问器属性,这些属性的get
/set
方法内部实现为:this['_data']
,也即:当调用this.xxx
(this
为Vue
实例,xxx
为Options.data
中的某个key
)时,会被代理到sharedPropertyDefinition.get
/ sharedPropertyDefinition.set
,其结果为:this._data.xxx
。
简而言之,proxy
函数的作用就是让 Vue 实例创建与Options.data
相同键值的访问器属性,使得在源码中可以使用this
(即Vue
实例)访问Options.data
中的同名key
的值,相当于代理了Options.data
。
initData
的最后,还为Options.data
做了observe
操作,我们接下来查看下observe
函数源码:
// src/core/observer/index.js
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
export function observe(value: any, asRootData: ?boolean): Observer | void {
// value 必须为对象(且非 VNode),否则就无须观察
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 有 __ob__ 属性表示 value 已经被 Observer 了
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
}
从observe
函数的注释可以知道,observe
函数会为要被观察的数据对象(即Options.data
等)创建一个观察者实例Observer
,该函数主要做了如下几件事:
-
判断数据是否需要被观察:如果传递进来的
value
不是对象,或者是VNode
对象,则无须进行观察。
这里进行判断主要是因为Observer
会对被观察对象的每一个key
都进行监控,代码编写上涉及一个递归过程(见后文),停止的条件就是非对象类型或是VNode
类型。
-
判断数据是否有
__ob__
属性:如果数据对象拥有__ob__
属性,且该属性是一个Observer
,说明这个数据对象已经被监控了,故无须再次进行监控。 -
监控数据对象:如果数据未被监控,则创建一个
Observer
实例监控该数据。
我们主要来看下Observer
实例的创建过程,即:new Observer(value)
:
// src/core/observer/index.js
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor(value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 为 value 对象添加 __ob__ 属性,其值指向当前 Observer
def(value, '__ob__', this)
if (Array.isArray(value)) {
...
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* 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])
}
}
/**
* Observe a list of Array items.
*/
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
// src/core/util/lang.js
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
从Observer
的注释可以看到,每个需要被观察的数据对象都会被Observer
实例监控,Observer
实例会把被观察数据对象的所有键值属性转换为getter
/ setter
函数(即将被观察数据对象的所有键值属性转换为访问器属性),从而可以进行 依赖收集 和 派发更新 操作。其具体的操作细节如下:
-
创建
Dep
对象:每一个Observer
对象内部维护一个Dep
实例,Dep
的源码内容如下:// src/core/observer/dep.js 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() } } }
可以看到,
Dep
内部维护了一个Watcher
数组subs
,具备添加Watcher
和删除Watcher
的能力,且其具备通知功能notify
,因此,当需要更新数据时,Dep
可以通知到相应的Watcher
,让它们重新进行更新过程。 -
为数据对象添加
__ob__
属性:此处相当于一个标记作用,表明该数据对象已经被进行观察。def(value, '__ob__', this)
-
进行响应式设置:其代码如下:
if (Array.isArray(value)) { ... this.observeArray(value) } else { this.walk(value) }
响应式设置对于数据对象的类型做了区分,共有两种形式:
-
数组类型:如果数据对象是数组,那么就调用
observeArray
方法,查看下其源码:
// src/core/observer/index.js /** * Observe a list of Array items. */ observeArray(items: Array<any>) { for(let i = 0, l = items.length; i<l; i++) { observe(items[i]) } }
其实逻辑很简单,如果是数组,那么遍历每一个数据元素,依次进行
observe
设置。因此,数组类型的数据对象每个元素都会有各自一个Observer
对其进行监控。注:在 Vue 中,
data
必须为一个对象,因此是不会进行observeArray
这个流程的,但是如果数据对象的某个key
的值为对象/数组,Vue 则会对该值进行observe
操作,因此该值是数组的话,则会进入observeArray
流程,从而具备响应式,比如下面的例子:const vm = new Vue({ el: "#app", data: { message: [{msg: 'Hello Vue'}] }, });
data.message
为数组,则其会进入observeArray
流程,从而让data.message[0].msg
也具备响应式功能。 -
数组类型:如果数据对象是数组,那么就调用
-
对象类型:如果数据对象不是数组,直接调用
walk
函数,其源码如下:// src/core/observer/index.js /** * 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]) } }
walk
函数主要就是遍历数据对象的所有key
,然后对每个key
都进行响应式设置,具体设置如下:// src/core/observer/index.js /** * Define a reactive property on an Object. */ export function defineReactive( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 每个 Dep 对应一个 key const dep = new Dep() // 获取对象 obj 的 key 的属性描述符 const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get // 如果是访问器属性,直接获取 getter,否则为空 const setter = property && property.set // 如果是访问器属性,直接获取 setter,否则为空 // 如果不是访问器属性,并且当前方法传入的参数个数为 2,则直接获取当前 key 的值 if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // 如果当前 key 的值为对象,则递归进行观察 let childOb = !shallow && observe(val) // 将 obj 的 key 设置为访问器属性,从而具备拦截功能 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { ... }, set: function reactiveSetter(newVal) { ... } }) }
从
defineReactive
函数源码可以看到,其为数据对象的每个key
又创建了一个Dep
实例,前面说过,Dep
内部维护了一系列Watcher
实例,其具备通知功能,而在这里,可以看出,Dep
也具备依赖收集功能。当创建完成一个
Dep
实例后,会判断一下数据对象当前key
是否是一个访问器属性(即带有gettter
/setter
),如果是一个普通属性且defineReactive
参数为 2 的话,那就先获取其值。如果当前
key
的值是一个对象的话,那么会通过observe
函数对其值进行观测。这里可以看出,如果数据对象某个key
的值是一个对象,那么无论对象的嵌套多深,Vue 都能进行监控(即对key
的值进行响应式设置)。最后通过
Object.defineProperty
将数据对象的当前key
设置为访问器属性,从而具备动态拦截功能,其具体设置如下:-
首先看下访问器属性的
getter
函数:get: function reactiveGetter() { 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 },
getter
函数首先会获取当前key
的值,然后会进行依赖收集dep.depend()
,但是在进行依赖收集之前,会先判断下Dep.target
,看下源码:// src/core/observer/dep.js export default class Dep { static target: ?Watcher; ... }
Dep.target
是一个Watcher
实例,这个Watcher
实例的创建过程我们在 Vue 源码解析 - 组件挂载 已经讲过,大致过程为:在进行组件挂载时,
mount
函数内部通过mountComponent
进行挂载,而mountComponent
内部有如下一段代码:// src/core/instance/lifecycle.js export function mountComponent(...): Component { ... updateComponent = () => { vm._update(vm._render(), hydrating); }; new Watcher( vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, "beforeUpdate"); } }, }, true /* isRenderWatcher */ ); return vm; }
所以,在组件进行挂载的时候,就会创建一个
Watcher
实例,查看下Watcher
源码:// src/core/observer/watcher.js export default class Watcher { ... constructor(...) { ... this.value = this.lazy ? undefined : this.get() } /** * Evaluate the getter, and re-collect dependencies. */ get() { pushTarget(this) ... value = this.getter.call(vm, vm) ... return value } ... }
当
new Watcher
的时候,其构造函数内会调用this.get()
,而this.get()
内第一个操作就是pushTarget
,其源码如下:// src/core/observer/dep.js export function pushTarget(target: ?Watcher) { ... Dep.target = target }
这里就对
Dep.target
进行了设置。然后我们再回到
Watcher
构造函数,在pushTarget
后,会调用this.getter.call
函数,其实就是调用updateComponent
函数:updateComponent = () => { vm._update(vm._render(), hydrating); };
这个过程就会触发
vm._render
函数,我们知道,这个函数最终会渲染出一个VNode
,生成VNode
的过程会涉及到对数据对象(比如vm.data
)的获取,此时就会触发数据对象相应key
的getter
函数。到这里,我们就理清了
Dep.target
的设置位置以及数据对象获取拦截的过程。下面就继续回到
getter
函数,接着看下具体的依赖收集步骤:get: function reactiveGetter() { 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 },
首先调用
dep.depend
方法,查看下其源码:// src/core/observer/dep.js depend() { if (Dep.target) { Dep.target.addDep(this) } } addSub(sub: Watcher) { this.subs.push(sub) } // 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) } } }
这里最终调用的是
dep.addSub(this)
,于是这里就成功地将关注某个key
的Watcher
实例添加到Dep
实例中了。收集完成当前
key
的依赖后,还对childOb
进行了判断:// src/core/observer/index.js export function defineReactive(...) { // 如果当前 key 的值为对象,则递归进行观察 let childOb = !shallow && observe(val) // 将 obj 的 key 设置为访问器属性,从而具备拦截功能 Object.defineProperty(obj, key, { ... get: function reactiveGetter() { ... if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, ... }) } 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) } } }
childOb
是对数据对象当前key
的值观测的对象Observer
,如果当前key
的值为对象,那么也同样会进行依赖收集childOb.dep.depend()
,依赖收集的Watcher
放置到该值数据对象的Observer.dep
上。
如果当前key
的值为数组,那么还会对数组的每个元素进行依赖收集dependArray(value)
。注:
getter
函数中,dep.depend()
主要是对当前key
进行依赖收集,监控的是当前key
的更改;而childOb.dep.depend()
是对当前key
的值进行依赖收集,这样当key
的值的某个数据更改时,才能监控得到。简而言之,每个数据对象都对应一个
Observer
,数据对象的每个key
都对应一个Dep
(Dep
负责依赖收集和派发更新),Dep
对应一系列对该key
感兴趣的Watcher
。如果数据对象某个
key
的值为对象,则该值对象也对应一个Observer
,由该Observer
负责对该值对象进行响应式设置,此时依赖收集和派发更新由该值对象的Observer.dep
负责。如果数据对象某个
key
的值为数组对象,则会为该数组对象创建一个Observer
,同时也会为每个数组元素各自创建一个Observer
,让每个数组元素具备响应式功能。此时的依赖收集和派发更新交由该数组对象的Observer.dep
负责。数据对象与
Observer
与Dep
大致关系如下图所示:综上所述,整个响应式数据拦截与依赖收集可以简单理解为:
每个数据对象都对应一个Observer
,Observer
会遍历数据对象的每个key
,将其设置为访问器属性,使该key
具备动态拦截功能。同时,Observer
还会为每个key
设置一个Dep
,用于 依赖收集 和 派发更新。 下面来看下
setter
函数:
// src/core/observer/index.js set: function reactiveSetter(newVal) { // 获取旧值 const value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return; } ... // 设置新值 if (setter) { setter.call(obj, newVal); } else { val = newVal; } // 对新值进行响应式设置 childOb = !shallow && observe(newVal); // 派发更新 dep.notify(); }
setter
主要就做了两件事:-
响应式设置:如果设置的新值是
object
类型,那么就进行响应式设置。 - 派发更新:其源码如下:
// 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() } }
其实就是遍历
Dep
实例的subs
数组,依次调用Watcher.update
方法,通知其数据更新。我们来看下
Watcher
的update
方法源码:// src/core/observer/watcher.js export default class Watcher { ... update() { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } ... }
update
会根据自身携带的一些标识进行不同的处理,对于一般的数据更新场景,比如我们最上面的例子,当我们调用vm.message='others'
时,这里会进入queueWatcher
流程,我们来看下该函数源码:// src/core/observer/scheduler.js const queue: Array<Watcher> = [] let has: {[key: number]: ?true} = {} let waiting = false let flushing = false /** * 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) } } }
queueWatcher
内部首先对watcher.id
进行获取,判断一些缓存中是否存在该id
:has[id]
,确保队列queue
不重复添加相同的Watcher
。如果
Watcher
是首次添加,那么根据当前是否处于flushing
状态,分别进行不同的处理:- 如果不处于
flushing
,则直接将Watcher
入对列 - 如果处于
flushing
状态,则将当前Watcher
的id
将其添加进队列queue
的相应位置。
最后在
nextTick
触发flushSchedulerQueue
函数。在 Vue 中,
nextTick
相当于开启了一个异步任务,可以确保视图更新完成后再执行相应任务,因此,nextTick(flushSchedulerQueue)
会在视图更新完成后,执行flushSchedulerQueue
,我们来看下该函数源码:// src/core/observer/scheduler.js /** * Flush both queues and run the watchers. */ function flushSchedulerQueue() { currentFlushTimestamp = getNow() // 表示正处于 flushing 状态 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 watcher.run() ... } // 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) // callHook(vm, "activated"); callUpdatedHooks(updatedQueue) // callHook(vm, 'updated') // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit('flush') } }
flushSchedulerQueue
函数内部做了以下几件事:设置当前为
flushing
状态:flushing = true
-
对队列
queue
依据watcher.id
由小到大进行排序:queue.sort((a, b) => a.id - b.id)
,这是为了确保以下问题:- 组件更新是由父到子(因为父组件会比子组件先创建)
- 用户
Watcher
会比渲染Watcher
先执行(因为用户Watcher
会比渲染Watcher
先创建) - 如果父组件的
Watcher
在执行过程中,其中一个子组件被销毁了,那么可以跳过该子组件的Watcher
运行。
-
遍历
queue
,依次执行每个Watcher
的run
方法。注:在遍历
queue
的时候,每次都要重新计算queue
的大小,原因是每次在执行Watcher.run
的时候,可能还会创建新的Watcher
,此时这些新的Watcher
就会走到我们前面刚分析过的queueWatcher
:
// src/core/observer/scheduler.js export function queueWatcher(watcher: Watcher) { ... if (!flushing) { ... } 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) } ... }
当
flushing
为true
时,这时就会走else
分支,从而会动态添加新的Watcher
到队列queue
中。我们主要来看下
watcher.run
方法:// src/core/observer/watcher.js export default class Watcher { ... /** * 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 // 用户 Watcher 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
方法内部首先会通过this.get()
获取新值,this.get()
前面我们分析过,这里再次看下其源码:// src/core/observer/watcher.js export default class Watcher { ... /** * 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) { ... } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value } ... }
this.get()
内部主要通过this.getter.call(vm,vm)
获取新值,Vue 中存在两种Watcher
:- 用户
Watcher
:比如我们自定义的watch
函数就是一个用户Watcher
,此时,this.getter.call(vm,vm)
获取得到的是用户Watcher
的值,那么对于Watcher.run
方法来说,获取到新值后,就会对新值进行判断,然后将新值与旧值传递给回调函数this.cb
,这样,我们自定义的watch
函数就能在回调中获取新值与旧值:
// src/core/observer/watcher.js export default class Watcher { ... run() { if (this.active) { const value = this.get() if ( value !== this.value || isObject(value) || this.deep ) { ... this.cb.call(this.vm, value, oldValue) ... } } ... }
- 渲染
Watcher
:对于渲染Watcher
来说,this.getter
函数是updateComponent
函数:
// src/core/instance/lifecycle.js updateComponent = () => { vm._update(vm._render(), hydrating) }
所以
this.get()
的用途不是获取返回值,而是间接触发重新渲染出一个VNode
,然后在update
内部最终执行patch
流程。我们在 Vue 源码解析 - 组件挂载 分析过,Vue 中总共存在两种数据渲染:
- 首次渲染:首次将虚拟节点渲染到一个真实的 DOM 中。
- 数据更新:对虚拟节点绑定的真实 DOM 节点上的数据进行更新。
// src/core/instance/lifecycle.js export function lifecycleMixin(Vue: Class<Component>) { Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { ... if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } ... } ... }
此处对应的就是 数据更新 部分内容:
vm.__patch__(prevVnode, vnode)
,这部分最终还是会走到patch
函数:// src/core/vdom/patch.js return function patch (oldVnode, vnode, hydrating, removeOnly) { ... const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { if (isRealElement) { ... } // replacing existing element const oldElm = oldVnode.elm // 获取挂载 DOM 的父节点 const parentElm = nodeOps.parentNode(oldElm) // create new node createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // update parent placeholder node element, recursively if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }
这里通过判断
oldVnode
和vnode
是否是相同虚拟节点,会进入不同的分支处理,判断相同节点的依据如下:// src/core/vdom/patch.js function sameVnode(a, b) { return ( a.key === b.key && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error) ) ) ) }
判断相同
VNode
的主要依据是key
要相同,同时以下条件满足其一即可:-
同步组件:需满足
tag
,isComment
相同,都定义了data
,都拥有相同的input
类型 -
异步组件:需满足
asyncFactory
相同
patch
函数对新旧VNode
的比较,其实就是VNode
之间的 diff 算法,这部分内容网上已经有很详细的讲解,这里直接引用网上的文章:「一起搞明白令人头疼的diff算法」。- 当队列
queue
遍历完成后,就会重新状态resetSchedulerState
:
// src/core/observer/scheduler.js /** * Reset the scheduler's state. */ function resetSchedulerState() { index = queue.length = activatedChildren.length = 0 has = {} ... waiting = flushing = false }
- 最后触发生命周期钩子函数:
// call component updated and activated hooks callActivatedHooks(activatedQueue) // callHook(vm, "activated"); callUpdatedHooks(updatedQueue) // callHook(vm, 'updated')
-
到这里,数据驱动与响应式原理的分析过程就大致结束了。