Vue 2.x 源码阅读记录(三):响应式系统

我心飞翔 分类:javascript

写在前面

文章为阅读笔记向,需 clone 下来 Vue 源码并加以调试服用~~

Vue 的响应式:

new Vue({
  el: '#app',
  data: {
    msg: 'Hello'
  },
  methods: {
    changeMsg() {
      this.msg = 'World';
    }
  }
});
 

响应式对象

initState

在初始化_init方法中调用了initState方法,来初始化propsdata等属性,使其变成 Vue 的响应式对象。initState调用在合并配置之后。它内部集成了对propsmethodsdatacomputedwatch的初始化:

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)
  }
}
 

initProps

它做了两件事情:一是遍历传入的props,调用defineReactive将其属性变成响应式,通过定义的vm._props可以访问props中的属性;二是通过proxy代理方法将其访问路径代理到vm.props中,也就是通常访问的this.props

function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 将 key 转成小写
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
// 如果为保留属性则报出警告
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}

initData

在开头将调用data函数拿到data对象,随后它也做两件事情:一是遍历key调用proxy代理到vm.data;二是observe订阅整个data的变化:

function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
// data 为 function 时调用 data 返回对象
// return data.call(vm, vm)
? 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)) {
// 在 methods 中以及定义了该 data key 值
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
// props 中以及定义该 data 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)) {
// 非保留属性名,代理到 vm.data
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}

在前面两个 init 中将propsdata都变成了响应式对象,接下来看看其中接触到的一些函数。

Proxy

在前面的例子中,是通过this.msg = 'xxx'来改变data中的数据,而this.msg实际上会被代理到this._data.msg,这就是proxy函数做的事,它使用Object.defineProperty代理了实例属性的访问路径:

const sharedPropertyDefinition = {
enumerable: true,  // 可枚举
configurable: true,  // 可遍历
get: noop,
set: noop
}
// proxy(vm, `_props`, key)
// proxy(vm, `_data`, key)
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
// 将 this.key 的访问代理到 this._xxx.key
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
// set 同理
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}

observe

它用来监听数据(data)的变化, 给非 VNode 的对象类型数据添加一个Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个Observer对象实例 :

// observe(vm._data = {}, true /* asRootData */)
// observe(data, true /* asRootData */)
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
}

Observer

它是一个类,在实例化时,它定义了一些为响应式服务的属性,实例化Dep类,调用def方法为value(这里的 value都为data) 定义一个__ob__属性,也就是打印中的可以看到的,如果value是数组则为每个子元素调用observe方法,如果是一个正常的data对象则为每个属性调用defineReactive方法:

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(data) 定义一个不可枚举的 __ob__ 属性,
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// value(data) 为数组,循环调用 observe 方法
this.observeArray(value)
} else {
// 为 value(data) 每个属性调用 defineReactive 方法使其变成响应式
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])
}
}
}

dep函数使用Object.defineProperty为对象添加一个属性:

/**
* 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
})
}

defineReactive

它在一开始实例化Dep拿到实例对象,再拿到obj(此处为 data 对象) 的属性描述符,然后递归为data中每个属性调用observe方法(当属性是一个对象时),这样就确保了每个属性都带有__ob__属性,都变成了响应式的, 这样我们访问或修改obj中一个嵌套较深的属性,也能触发 getter 和 setter。最后使用Object.definePropertyobjkey属性值添加gettersetter,它们做的事情就是依赖收集派发更新

// defineReactive(obj /* data */, keys[i] /* data key */)
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
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) {
// 在 Observer 中 data 只传递了两个参数
// data 中的每个属性的值
val = obj[key]
}
// 递归地为 data 中的每个属性添加 __ob__ 对象
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
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
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
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
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

可以看到,初始化propsdata的过程中就是利用Object.defineProperty为数据添加gettersetter来拦截对象的读写,并递归给与__ob__对象用于追踪数据变化。


依赖收集

在给响应式对象添加完gettersetter后,对对象的读取就会执行gettergetter会进行一波依赖收集的过程,然后将访问的值返回:

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
},

具体流程

mountComponent执行时,会定义一个渲染Watcher,会将updateComponent函数作为Watchergetter传入:

updateComponent = () => {
// _update() 将 vnode 生成为实际 DOM 元素
// _render() 生成 vnode
vm._update(vm._render(), hydrating /* false */)
}
new Watcher(vm, updateComponent, noop /* 空函数 */, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 组件已挂载并未销毁,执行 beforeUpdate 生命周期
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

Watcher类中,会定义一些依赖相关的数组,将getter赋值为传入的updateComponent,最后会执行get方法,get方法中执行了pushTargetgetter方法:

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)
//...
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 () {
debugger
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)
}
// 这里是 getter 执行完之后执行的
popTarget()
this.cleanupDeps()
}
return value
}
//...
}

pushTarget方法会将当前渲染Watcher存储到Dep.target并添加到targetStack栈中:

Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
// 当前渲染 watcher
Dep.target = target
}

然后Watch会执行getter方法,也就是外部传入的updateComponent,然后结合前面可以知道,会先执行_render()走到render方法的执行,在render执行时,也就访问了data中的属性,触发了响应式对象getter开始依赖收集过程:

vnode = render.call(vm._renderProxy, vm.$createElement)

defineReactive中定义了一个Dep类的实例对象,首先会对这个Dep实例进行依赖收集,执行了当前Dep实例的depend方法:

// 这个 dep 是 data 中 __ob__ 中的 dep
const dep = new Dep()
if (Dep.target) {
dep.depend()
if (childOb) {
// dep 在 Observer 中定义
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}

depend方法实际执行了当前渲染WatcheraddDep方法来收集依赖:

depend () {
if (Dep.target) {
// Dep.target 是当前渲染 Watcherdep
// this 是 data 中每个属性 __ob__ 中的 dep
Dep.target.addDep(this)
}
}

addDep方法通过一些判断将当前dep收集到依赖相关的数组中:

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)
}
}
}

最后执行当前depaddSub方法将当前渲染Watcher订阅到depsubs数组中, 这个目的是为后续数据变化时候能通知到哪些subs做准备:

addSub (sub: Watcher) {
this.subs.push(sub)
}

到此,已经完成了一个依赖收集的过程。此时回到Watcherget方法执行中,会接着执行finally块:

finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
// 这里是 getter 执行完之后执行的
popTarget()
this.cleanupDeps()
}

先了解一下popTarget,当前vm的依赖收集完成后,它将当前的渲染Watcher回归到上一个的状态,因为如果存在组件嵌套的关系,会递归地执行mountComponent方法:

export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

暂时跳过cleanupDeps的总结

可以看到,在依赖收集的过程中当前的渲染Watcher会收集当前dep,而当前dep会收集当前渲染Watcher,这样,二者就建立了桥梁。换一种说法,Watcher作为一个观察者,它会将当前的dep收集起来,作为当前Watcher的一个订阅者,而dep则在它的subs中订阅了当前Watcher,这样,当dep对应的数据改变时,会调用depnotify通知订阅的Watcher,执行Watcher上的update方法告诉Watcher可以开始更新数据了。这是一个典型的观察者模式的实现。


派发更新

完成对数据的依赖收集之后,点击按钮进行赋值操作时会进入响应式对象的setter,其中进行了派发更新的流程,对依赖的收集就是为了修改数据时对相关的依赖派发更新:

set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
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
}
childOb = !shallow && observe(newVal)
// notify 之前,以及将 data[val] 更新了
dep.notify()
}

可以看到,setter会先获取当前需要操作的旧值,对新旧值进行对比,然后将当前值变成新值,如果新值是一个对象,则再调用observe方法将其变成响应式对象,最后调用当前depnotify方法通知Watcher开始派发更新过程。notify方法遍历了之前dep.subs订阅的Watcher,调用了它们的update方法:

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()
}
}

在当前流程中,update中执行了queueWatcher方法,对dep.subs下的Watcher做了一个队列操作:

update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

在当前例子中只有一个渲染Watcher,所以执行queue.push分支,将当前Watcherpush 到队列中,然后调用nextTick方法执行flushSchedulerQueue, 这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue ,稍后手动添加watch的时候再来验证这一点:

const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 保证同一个 Watcher 只 push 一次
if (has[id] == null) {
// 存储当前 watcher.id
has[id] = true 
if (!flushing) {
// 向队列中插入当前 Watcher
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.
// 第二个 Watcher 进来的时候
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
}
// 异步执行 flushSchedulerQueue
debugger
nextTick(flushSchedulerQueue)
}
}
}

(计算属性之后过来的补充)当在 changeMsg 中有多个赋值语句时,它们会被合并。因为它们属于同一个渲染 Watcher 。当第一个赋值语句执行时,已经进入了 has[id] == null 的逻辑,第二次赋值时这个渲染 Watcher 已经存在于 has 中了,则不会进入。又因为在这些属性的 setter 中,已经完成了新值的赋值,那么它们就只需要等待执行 nextTick(flushSchedulerQueue) 就可以了:

this.msg = 'World';
this.msg2 = 'World2';

先看看flushSchedulerQueue前面主要的逻辑,它会对队列中的Watcher根据自增的id进行从小到大排序,因为父组件的Watcher先于子组件的Watcher创建,手动写的watch先于渲染Watcher创建,所以要将先创建的Watcher先执行,可以考虑单向数据流的概念来理解这一点:

queue.sort((a, b) => a.id - b.id)

然后将队列遍历,先执行watcher.before方法,也就是在渲染Watcher执行时定义的,它内部调用了beforeUpdate生命周期函数,然后将has[id]置空,并执行watcher.run()

// queue.length 没有缓存是因为可能在 watcher.run() 的过程中插入新的 Watcher
// 参考下面的 `边界情况`
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// mountComponent 中传入了 before
// before 方法执行了 beforeUpdate hook
watcher.before()
}
id = watcher.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
}
}
}

watcher.run方法就是重新渲染 DOM 的方法,它内部重新调用了watcher.get方法,再次执行了一遍watcher.getter,也就是渲染Watcher定义时的updateComponent方法,走一遍getter流程更新 DOM,最后拿到新旧值执行cb回调,回调对应用户手写的watch,在渲染Watcher中,回调是一个noop

run () {
if (this.active) {
// 这里又调用了一次 getter
// set 中将 val 变成了新的值,这里再次调用会进入 updateComponent 的调用
// _render() 会重新读取改变后的 val,从而渲染页面
debugger
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)
}
}
}
}

DOM 更新完成之后,再看看flushSchedulerQueue收尾逻辑:

// 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')
}

先调用resetSchedulerState重置上面的一些全局变量:

function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}

最后调用了两个 call hooks 函数会当前Watchervm实例执行activatedupdated生命周期:

callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)

边界情况

如果在手动添加watch中又对监听的值进行了赋值:

watch: {
msg() {
this.msg = Math.random();
}
},

那么它在queueWatcher函数中 push 到队列时会进入flushing = true的分支,这是因为在执行watcher.cb回调函数时,又进入的响应式对象的setter,并没有执行到watcher.run之后的resetSchedulerState重置变量的流程。此时就会向队列中当前的Watcher后一位插入一个新的Watcher,它可以看作用户手动写的Watcher的副本:

if (!flushing) {
// 向队列中插入当前 Watcher
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)
}

由于每次run的时候又会添加一个新的Watcher,这是就会进入一个添加Watcher的死循环,最后会在watch.run()方法之后报出一个警告提示当前进入了无限更新循环:

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
}
}

注意

在循环执行subs[i].update()方法时,会将循环的当前Watcher push 到队列中,但遇到nextTick函数时会返回循环,接着 push 下一个WatchernextTick就是派发更新操作中的关键步骤。

总结

1、更新新旧数据,调用notify开始队列操作;

2、循环调用watcher.update添加Watcher到队列;

3、添加完队列后nextTick循环队列中的Watcher执行watcher.run更新 DOM;


nextTick

nextTick是一个setter的一个核心实现,它会在 DOM 完成渲染之后执行。在 Vue 2.5 以上的版本中,在目前主流的 web 环境中nextTick函数使用Promise实现微任务(microTask)。它的整个实现都在一个next-tick.js中。

首先它定义了一些全局变量,用于控制Promise队列:

// 回调函数队列
const callbacks = []
// promise pending
let pending = false
// 微任务入口函数
let timerFunc

在当前环境支持Promise的情况下,使用Promise.resove()来获得一个微任务,并将timerFunc赋值为一个入口函数:

if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}

在微任务中,执行了fulshCallbacks函数,它将遍历执行callbacks队列:

function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

微任务已经创建完毕了,接下来看看nextTick函数如何运用微任务去执行。它接收一个cb回调,将cb回调的调用作为一个callback 插入到callbacks回调队列中,然后开始执行timerFunc函数。最后,在没有传入cb的情况下,会返回一个Promise,也就是nextTick支持Promise调用:

export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
// 相当于执行了 `this.nextTick().then(() => {})`
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

timerFunc会使用p.then(flushCallbacks)将所有用户传入的回调都压到一个微任务中执行,等待当前宏任务执行完毕后,会按开始读取微任务中的结果:

timerFunc = () => {
p.then(flushCallbacks)
// ...
}

按照 JS 异步的概念来说,会先执行同步的代码,比如renderpatch等过程,同步代码执行完成之后在读取异步代码执行的结果,也就是调用用户传入的回调。这也就是为什么在派发更新的过程中遇到nextTick(flushSchedulerQueue)时会回到dep.subs[i].update的循环中,flushSchedulerQueue实际是进入了异步队列,在同步代码中,组件会完成patch的过程,这就是为什么nextTick能够拿到渲染之后的 DOM,而flushSchedulerQueue又能在异步队列中按顺序执行(因为flushCallbacks循环调用了它),这也保证了父组件会先于子组件更新完毕。Vue 在nextTick中巧妙的运用了事件循环

另外,在initGlobalAPI和 Vue 初始化的的时候,向 Vue 挂载了nextTick$nextTick函数,它实际就是调用了这里的nextTick,它可以使用Vue.nextTickvm.$nextTick调用:

// initGlobalAPI
Vue.nextTick = nextTick
// renderMixin
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}

检测变化的注意事项

data中没有定义的属性是没有gettersetter的,所以访问不存在于一个对象中的值时它不会是响应式的。通常,能够使用Vue.setvm.$set来将一个属性变成响应式的,set可以给对象和数组添加响应式的属性:

var vm = new Vue({
data:{
a:1,
someObj: {}
}
})
// vm.b 与 vm.somObj.a 是非响应的
vm.b = 2
vm.someObj.a = 1
// 使其变成响应式
Vue.set(vm.someObj, 'a', 1);

set$set分别定义在初始化实例的时候:

// initGlobalAPI
Vue.set = set
// stateMixin
Vue.prototype.$set = set
// set
export function set (target: Array<any> | Object, key: any, val: any): any {
debugger
// target 不能为 undefined, null, 普通类型值
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 数组情况
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
// key 存在 target 中,直接将 val 赋值给 target
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}

对象属性

target中带有这个key的时候,说明已经在初始化响应式对象或之前的set中设置了gettersetter,则直接返回target[value]的值:

if (key in target && !(key in Object.prototype)) {
// key 存在 target 中,直接将 val 赋值给 target
target[key] = val
return val
}

随后,它会调用defineReactiveval设置gettersetter,走一遍响应式对象的设置流程。之后,会手动调用ob.dep.notify手动派发更新。当页面没有用到这个新值的时候,dep.subs就没有订阅任何Watcher,最后直接返回val;当页面中用到了这个新值的时候,就会从notify开始派发更新,最后执行watcher.run方法来重新执行updateComponent,就会成功将新值渲染到页面上去了:

defineReactive(ob.value, key, val)
ob.dep.notify()
return val

数组元素

在数组的情况下,直接调用splice方法就能将val变成响应式的:

if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}

其实这里的splice是 Vue 重写过的。在target被实例Observer的时候,如果遇到当前Observer的值是数组时,会进行一些处理,在大部分现代浏览器环境中,它会走到protoAugment方法,将target__proto__指向arrayMethods

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(data) 定义一个不可枚举的 __ob__ 属性,
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// value(data) 为数组,循环调用 observe 方法
this.observeArray(value)
} else {
// 为 value(data) 每个属性调用 defineReactive 方法使其变成响应式
this.walk(value)
}
}
function protoAugment (target, src: Object, keys: any) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}

arrayMethods实际就是指向了Array.prototype,然后在后面会对数组所有的原生方法进行遍历重写。这里的重写实际就是对原生方法的执行做一层拦截操作:

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
})
})

可以看到它会先执行原生的数组方法,然后将需要插入到数组中的值转为一个数组,调用observeArray循环调用observe方法使其变成响应式对象,最后调用dep.notify通知Watcher更新。

总结

set就是将响应式对象/数组中不存在的属性以对象或数组的方式最终调用defineReactive方法给它添加gettersetter。补充:响应式对象的流程:observe > Observer (Object, Array)> defineReactive,整个流程就是一个闭环。


计算属性 computed

计算属性可用于对数据的复杂计算,它会缓存计算结果:

const vm = new Vue({
el: '#app',
data: {
firstName: 'Foo',
lastName: 'Bar',
},
methods: 
changeName() {
this.firstName = 'Coven';
// this.firstName = 'Foo';
},
},
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
},
});

纠错

经过痛苦的调试与推理过程,我发现了一个之前定义响应式对象时的错误。在 defineReactivedata 中的属性设置 gettersetter 时,有一个 const dep = new Dep() 的操作,这个 dep 并不是我当时理解的是当前目标对象 __ob__ 下的 dep ,而是当前目标对象下每个属性持有的 dep。为每个属性添加一个 dep ,就能让每个属性都订阅当前的 Watcher,在当前属性更新时通知 Watcher 更新:

export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 这个 dep 是为 data 中每个属性创建的 dep
const dep = new Dep()
//...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
/* 依赖收集 */
get: function reactiveGetter () {
if (Dep.target) {
// 为当前属性订阅 Watcher
dep.depend()
// ...
}
return value
},
set: function reactiveSetter (newVal) {
// 当前属性已经改变,通知 Watcher 开始更新
dep.notify()
}
})
}

初始化

首先看看初始化initComputed的过程。首先遍历用户定义的computed对象,检验定义的合法性,然后为每个计算属性方法定义一个computed watcher,存储在vm._computedWatchers中,然后判断计算属性名是否已经存在于vm下,否则为每个计算属性方法执行defineComputed

function initComputed (vm: Component, computed: Object) {
// debugger
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
// computed 可以支持带有 set 和 get 的对象写法
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
// key 已经在当前 vm 实例 data/props 中定义了
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}

defineComputed的核心就是为计算属性函数添加gettersetter,开发中最常用的就是用计算属性进行复杂计算,所以这里重点看getter,它是createComputedGetter

export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()  // true
if (typeof userDef === 'function') {
// 给当前 userDes 函数添加 getter 和 setter
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}

createComputedGetter返回实际的getter,它的核心就是根据计算属性的key取出之前缓存的computed watcher,然后执行Watchergetter,也就是计算属性方法:

function createComputedGetter (key) {
return function computedGetter () {
debugger
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}

访问

前面知道,在_render()的过程中会访问到定义在模板中的属性,当访问到计算属性时,会触发计算属性的getter。在计算属性初始化的时候,提到过为计算属性定义了一个computed watcher

watchers[key] = new Watcher(
vm,
getter || noop,
noop,
// const computedWatcherOptions = { lazy: true }
computedWatcherOptions
)

这里抽取出一些computed watcher定义时与渲染Watcher的不同之处,computed watcher 的关键在于 this.lazythis.dirty

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
// options
if (options) {
// ...
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.dirty = this.lazy // for lazy watchers
// ...
// lazy 表示是一个计算属性,不同于的渲染 Watcher 的是,它不会在 new Watcher() 时进行求值
this.value = this.lazy
? undefined
: this.get()
}

可以看到在最后,不会立即执行get函数求值,而是会在计算属性getter时再进行求值。回到计算属性getter,此时dirtytrue,会执行watcher.evaluate方法,evaluate 则执行了 watcher.get

if (watcher.dirty) {
watcher.evaluate()
}

get 执行之前,此时的 Dep.target 是渲染 Watcherevaluate 方法执行了 Watcherget,也就是执行了计算属性函数,最终返回计算属性的值,在例子中是return ${this.firstName} ${this.lastName}。开始执行 get 时,此时的 Dep.target 是当前的 computed watcher,在执行计算属性函数时,对 this.firstNamethis.lastName 进行了访问,触发了它们的 getter,随后它们所属的 dep 对象对当前 computed watcher 进行依赖收集,订阅到 dep.subs 中:

get () {
// debugger
// computed watcher 接管当前流程
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)
}
// 这里是 getter 执行完之后执行的
// computed watcher 内会访问 this.xxx 属性,因此会触发 this.xxx 的 getter
// 最终 value 拿到 computed 中 userDef 的返回值
// 将当前 computed watcher 出栈,Dep.target 交还给上一个 Watcher
// targetStack 是后进先出顺序
popTarget()
this.cleanupDeps()
}
return value
}

到这里,已经拿到计算属性的计算结果了,当渲染流程完成时,结果会显示到页面上。接下来,看看对计算属性依赖的值进行更改,它又是如何进行计算的:

changeName() {
this.firstName = 'Coven';
},

它触发了 firstNamesetter,然后执行 dep.notify,此时的 dep.subs 中订阅了渲染 Watchercomputed watcher ,然后将它们循环执行 update。渲染 Watcher 会走正常的 queueWatcher 流程,computed watcher 则只会将 dirty 设为 true

update () {
/* istanbul ignore else */
if (this.lazy) {
// 当前为 computed watcher 时,先不进入派发更新流程
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

dep.notify 执行完成后,会开始执行更新,此时又会执行到 _render(),在 _render() 的过程中,又会访问到计算属性,开始执行计算属性的 getter

function createComputedGetter (key) {
return function computedGetter () {
debugger
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}

经过 update 的流程,此时的 dirtytrue,接下来就开始走计算属性访问的流程。在 firstNamesetter 中,已经执行 val = newVal 将它的值更新了,所以这里 computed watchergetter 会拿到最新的 firstName 值,最后渲染到页面上。

结合上面的流程调试到最后,我发现,只有当计算属性中依赖(dep 中订阅了 computed watcher)的值发生变化时,并且 _render() 流程再次访问到计算属性时,才会进行重新计算。


侦听属性 Watch

在响应式系统中,用户可以手动定义一个 watch 来监听数据的变化,它是一个 user watcher,接下来看看它的实现原理:

watch: {
msg() {
// this.msg = Math.random();
console.log('msg changed.');
}
},

初始化

在初始化的 initState 中,如果用户定义了 watch,则会执行 initWatch 来初始化 user watcher。它会遍历 watch 属性,拿到 watch 中每一个函数/对象,对函数或对象中的 handler 执行 createWatcher 方法:

function initWatch (vm: Component, watch: Object) {
debugger
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}

createWatcher 对参数进行了一些规范处理,最终调用初始化 stateMixin 中定义的 $watch 方法,实际上用户定义在选项中的 watch 也是调用了 $watch API:

function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}

$watch 中实例化了一个 Watcher,注意 options.user = true,表示这是一个用户定义的 watcher,它先于渲染 Watcher 被订阅:

Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}

那么这时在 Watcher 被实例化的时候,expOrFn 是用户传入的 watch 函数的函数名,cbwatch 函数本身,usertrue,注意这里的 expOrFn 是一个字符串,会调用 parsePath(expOrFn) 将其转换成函数赋值给 getter,最后调用 get 方法求值:

get () {
// debugger
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)
}
// 这里是 getter 执行完之后执行的
// computed watcher 内会访问 this.xxx 属性,因此会触发 this.xxx 的 getter
// 最终 value 拿到 computed 中 userDef 的返回值
// 将当前 computed watcher 出栈,Dep.target 交还给上一个 Watcher
// targetStack 是后进先出顺序
popTarget()
this.cleanupDeps()
}
return value
}

实例化过程中,this.getter = parsePath(expOrFn) 是最重要的一步。parsePath 会先校验是不是一个合法的函数名,然后将函数名用 . 分割为一个字符串数组,因为用户可以采用 someObj.foo 这种方式定义 watch 的函数名,以监听对象内的某个属性,最后它返回一个函数,这个函数就是 Watchergetter

在上面 getter 调用时传入的是当前的 vm 实例,就是下面的 obj,然后循环 segments,在循环中将 obj[segments[i]] 的值赋值给 obj,适配了两种 watch 函数名的定义:一是 msg(n, o) {};二是 'someObj.msg'(n, o) {}。这是一个很巧妙的方式:

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
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
}
}

回到 get 函数,当执行 getter 的时候实际是对 data 中的数据做了访问,触发了数据的 getter,然后数据的 dep 对象就会订阅当前的 user watcher,当更新时就会通知它进行派发更新过程。

执行 watch

在看派发更新的时候,知道数据更新会触发数据的 setter,进行派发更新过程。当 watch 监听了一个属性的时候,这个属性的 dep.subs 中就会在初始化 watch 时订阅这个 user watcher,那么到这个 user watch 执行 run 的时候,它就会进入 this.user = true 的逻辑,执行 cb,也就是用户定义的 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)
}

deep watch

在定义一个 watch 时候,可以设置一个 deep: true 属性来监听一个对象或数组中的所有属性变化:

watch: {
someObj: {
deep: true,
handler() {}
}
}

user watcher 初始化调用 getter 最后的 finally 块中,会执行这一段逻辑,也就是 deep: true 的实现:

if (this.deep) {
traverse(value)
}

traverse 实际调用了 _traverse,首先判断当前 user watch 监听的不是对象或数组,不是则直接返回。它遍历 value 中所有的属性,并递归地调用自身,调用时实际又触发了属性的 getter,给每个属性的 dep.subs 订阅当前的 user watcher,这样,当 value 内部的属性发生改变时也能触发 watch 的回调。 这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。

function _traverse (val: any, seen: SimpleSet) {
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)
}
}

总结 computed 和 watch

computedwatch 实际上都是 Watcher 类的实例对象。目前为止,已经接触过三个 Watcher 的不同使用方式:渲染 Watchercomputed watcheruser watcher它们的变化实质都是 _render() 方法触发数据的 getter 依赖收集,赋值操作触发数据的 setter 派发更新。 就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

回复

我来回复
  • 暂无回复内容