Vue2.0源码分析
如果觉得写得不错,请到GitHub
我一个Star
深入响应式原理
在介绍完props
、data
、watch
以及computed
后,我们对响应式原理有了一定的初步认识,在这一章节中我们再次回顾响应式,来探究其实现原理。
在之前的章节中,我们以及介绍过:Vue.js
通过Object.defineProperty(obj, key, descriptor)
方法来定义响应式对象,我们可以在Can I Use网站上搜索到,IE8
浏览器并不支持这个方法,这就是Vue.js
不支持IE8
及其以下版本浏览器的真正原因。
在MDN网站上,我们可以发现这个方法支持很多个参数,其中descriptor
支持许多个可选的属性,对于Vue.js
实现响应式对象来说,最重要的是get
和set
属性。
let val = 'msg' const reactiveObj = {} Object.defineProperty(reactiveObj, msg, { get: function () { // 当访问reactiveObj.msg时被调用 return val }, set: function (newVal) { // 当设置reactiveObj.msg时被调用 val = newVal } })
在Vue
的响应式对象中,它会在getter
中收集依赖、在setter
中派发更新,我们会在之后的章节中分别对getter
的收集依赖,setter
的派发更新做单独的讲解。
在介绍完Object.defineProperty
,我们来回答一个问题,什么是响应式对象?在Vue.js
中对于什么是响应式对象,我们可以简单的理解成:用Object.defineProperty()
方法定义时同时提供了get
和set
选项,我们就可以将其称之为响应式对象。
在Vue.js
实例化时,会把props
、data
和computed
等变成响应式对象,在介绍响应式对象时,我们会重点介绍props
和data
的处理过程,这个过程发生在this._init()
方法中的initState(vm)
中。
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
相关的逻辑的:
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') { 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) }
在之前分析initProps
整体流程的过程中,我们知道initProps
主要做三件事情:props校验和求值、props响应式和props代理。对于props
代理而言它很简单,主要作用是方便我们取值。
proxy代理
proxy()
方法是定义在src/core/instance/state.js
文件中:
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } 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 } Object.defineProperty(target, key, sharedPropertyDefinition) }
代码分析:
noop
:它代表空函数,空函数代表什么都不做。target
:它是目标代理对象,在Vue.js
中就是Vue
实例。sourceKey
:它是源属性,在props
代理中传递的是_props
内部私有属性。key
:它是要代理的属性,在props
中就是我们撰写的各种props
属性。sharedPropertyDefinition
:它就是Object.defineProperty(obj, key, descriptor)
方法的descriptor
参数,可以从上面代码中看到,在props
代理中它提供了enumerable
、configurable
、get
和set
这几个选项。
假设我们有如下Vue
实例:
export default { props: ['msg', 'age'] }
在proxy
代理后,我们就能通过this.msg
和this.age
代替this._props.msg
和this._props.age
的形式直接访问或者设置值:
// 代理前 const msg = this._props.msg console.log(msg) // 单项数据流,只要演示,实际不能修改props的值 this._props.msg = 'new msg // 代理后 const msg = this.msg console.log(msg) // 单项数据流,只要演示,实际不能修改props的值 this.msg = 'new msg'
以上就是props
的代理过程分析,对于data
代理而言是相同的道理,这里就不再累述。
defineReactive
在介绍完proxy
代理后,我们紧接着要分析defineReactive
的实现逻辑,关于响应式的代码实现,绝大多数是在src/core/observer
目录下,其中defineReactive
方法是定义在其目录的index.js
入口文件中
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() 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] } 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() } }) }
代码分析:
defineReactive
实际上就是对Object.defineProperty()
方法的一层包裹,主要是处理getter
和setter
相关的逻辑。defineReactive
首先通过Object.getOwnPropertyDescriptor()
方法获取了当前obj.key
的属性描述,如果其属性configurable
为false
,则不能被定义为响应式对象,因此对于obj.key
任何赋值都不会触发组件更新,例如:
export default { data () { return { obj: {} } }, created () { const obj = {} Object.defineProperty(obj, 'msg', { configurable: false, value: 'msg' }) this.obj = obj setTimeout(() => { // this.obj.msg不是响应式对象,修改后不会触发组件更新 this.obj.msg = 'new msg' }, 3000) } }
observe和Observer
我们可以在defineReactive
中看到observe(val)
这段代码,接下来让我们介绍observe()
方法以及Observer
类。observe()
方法定义与defineReactive()
方法定义在同一个文件中,其代码如下:
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 }
代码分析:
- 首先对传递的
value
进行了类型判断,不为对象或者是VNode
实例时不进行任何操作,其中VNode
是一个类,它会在生成虚拟DOM的时候使用到,我们会在后面进行介绍,isObject
是一个定义在src/shared/utils.js
文件中的工具方法。
export function isObject (obj: mixed): boolean { return obj !== null && typeof obj === 'object' }
- 然后对
value
使用hasOwn
判断是否有__ob__
属性且__ob__
为Observer
实例,添加这个属性是为了防止重复观察(避免重复定义响应式),既:如果已经是响应式对象了,直接返回,否则才会进行下一步操作。hasOwn
是一个定义在src/shared/utils.js
文件中的工具方法:
const hasOwnProperty = Object.prototype.hasOwnProperty export function hasOwn (obj: Object | Array<*>, key: string): boolean { return hasOwnProperty.call(obj, key) }
- 最后
value
又进行了一些条件判断,其中最重要的两个条件为Array.isArray
和isPlainObject
,它们分别判断value
是否为数组,是否为普通对象,其它几个边界条件暂时不做介绍。其中isPlainObject
是一个定义在src/shared/utils.js
文件中的工具方法:
export function isPlainObject (obj: any): boolean { return _toString.call(obj) === '[object Object]' }
接下来,我们需要看一下Observer
类的实现:
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 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } 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]) } } }
代码分析:
def
为定义在src/core/utils/lang.js
文件中的一个工具方法,def
本质上也是对Object.defineProperty()
方法的一层包裹封装,使用def
定义__ob__
的目的是让__ob__
在对象属性遍历的时候不可被枚举出来。
export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
- 在
Vue.js
中对于纯对象和数组的响应式的处理方式是不同的,代码首先判断了value
是否为数组。如果不是数组,则调用walk()
方法。walk()
方法实际上就是递归遍历对象属性,然后调用defineReactive()
的过程,例如:
const nestedObj = { a: { b: { c: 'c' } } } // 递归调用 defineReactive(nestedObj) defineReactive(a) defineReactive(b) defineReactive(c)
如果是数组,则调用observeArray()
方法,observeArray
也是一个遍历递归调用的过程,只不过这里遍历的是数组,而不是对象的属性键。然后我们还发现,在observeArray()
方法调用之前,还进行了hasProto
判断,然后根据判断结果进行不同的操作。其中,hasProto
是定义在src/core/util/env.js
文件中的一个常量,它的目的就是为了判断当前浏览器是否支持__proto__
属性:
export const hasProto = '__proto__' in {}
我们都知道因为原生API
某些限制因素,Vue.js
对数组七种可以改变自身数组的方法提供了变异方法支持,这七种方位分别为:
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
对这七种方法的变异处理逻辑在src/core/ovserver/array.js
文件中:
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 }) })
代码分析:
- 首先以
Array.prototype
原型创建一个新的变量,这个变量会在protoAugment
或者copyAugment
方法的时候使用到。 - 然后遍历七种方法,使用
def
来重新定义一个包裹方法。也就是说:当我们调用这七种任意一种方法的时候,首先调用我们的包裹方法,在包裹方法里面再调用原生对应的数组方法,这样做的目的是让我们可以在这个包裹方法中做我们自己的事情,例如notify
,这个过程可以使用以下伪代码实例描述:
// Array.prototype.push方法为例 function mutatorFunc (value) { const result = Array.prototype.push(value) // do something return result } export default { data () { return { arr: [] } }, created () { this.arr.push('123') // 相当于 mutatorFunc(123) } }
然后我们接下来看一下protoAugment
和copyAugment
的实现,首先是最简单的protoAugment
:
// 定义 const arr = [] export const arrayMethods = Object.create(arrayProto) function protoAugment (target, src: Object) { target.__proto__ = src } // 调用 protoAugment(arr, arrayMethods) // 调用后 arr.__proto__ = { // 省略其它 push: function () {}, pop: function () {}, shift: function () {}, unshift: function () {}, splice: function () {}, sort: function () {}, reverse: function () {} } arr.push() arr.pop() arr.shift() arr.unshift() arr.splice() arr.sort() arr.reverse()
代码分析:当浏览器支持__proto__
属性的时候,直接把__proto__
指向我们创建的arrayMethods
变量,这个包含我们在上面定义的七种变异方法。
当浏览器不支持__proto__
属性的时候,我们就调用copyAugment
方法:
// 定义 const arr = [] const arrayKeys = Object.getOwnPropertyNames(arrayMethods) export const arrayMethods = Object.create(arrayProto) 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]) } } // 调用 copyAugment(value, arrayMethods, arrayKeys) // 调用后 arr = { // 省略其它 push: function () {}, pop: function () {}, shift: function () {}, unshift: function () {}, splice: function () {}, sort: function () {}, reverse: function () {} } arr.push() arr.pop() arr.shift() arr.unshift() arr.splice() arr.sort() arr.reverse()
代码分析:我们可以从代码中看到,当浏览器不支持__proto__
的时候,会把我们创建的arrayMethods
变量上所有的key
,遍历赋值到value
数组上。
依赖收集
在这一节中,我们来介绍依赖收集,在介绍之前我们需要知道什么是依赖收集,以及依赖收集的目的。
问:什么是依赖收集?依赖收集的目的是什么?
答:依赖收集就是对订阅数据变化的Watcher
收集的过程。其目的是当响应式数据发生变化,触发它们的setter
时,能够知道应该通知哪些订阅者去做相应的逻辑处理。例如,当在template
模板中使用到了某个响应式变量,在组件初次渲染的时候,对这个响应式变量而言,应该收集render watcher
依赖,当其数据发生变化触发setter
时,要通知render watcher
进行组件的重新渲染。
在之前我们提到过,依赖收集发生在Object.defineProperty()
的getter
中,我们回顾一下defineReactive()
代码:
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 省略代码 const dep = new Dep() // 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] } 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 } }) }
我们可以从代码中看到,当触发getter
的时候,首先判断了Dep.target
是否存在,如果存在则调用dep.depend()
,dep.depend()
函数就是依赖真正收集的地方。在阅读完以上代码后,我们可能会有这样几个疑问:
Dep
是什么?Dep.target
是什么?dep.depend
是如何进行依赖收集的?又是如何进行依赖移除的?
Dep
让我们首先来回答第一个问题,介绍一下Dep
类,Dep
类是定义在observer
目录下dep.js
文件中的一个类,observer
目录结构如下:
|-- observer | |-- array.js | |-- dep.js | |-- index.js | |-- scheduler.js | |-- traverse.js | |-- watcher.js
然后,我们来看一下Dep
类的具体定义:
let uid = 0 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
类首先定义了一个静态属性target
,它就是Dep.target
,我们会在之后介绍它。然后又定义了两个实例属性,id
是Dep
的主键,会在实例化的时候自增,subs
是一个存储各种Watcher
的数组。例如render watcher
、user watcher
和computed watcher
等。addSub
和removeSub
对应的就是往subs
数组中添加和移除各种Watcher
。depend
为依赖收集过程。notify
当数据发生变化触发setter
的时候,有一段这样的代码:dep.notify()
,它的目的就是当这个响应式数据发生变化的时候,通知subs
里面的各种watcher
,然后执行其update()
方法。这属于派发更新的过程,我们会在之后的章节介绍。
在介绍完以上几个属性和方法后,我们就对Dep
是什么以及它做哪些事情有了一个具体的认识。
Dep.target和Watcher
我们接下来回答第二个问题,Dep.target
是什么?Dep.target
就是各种Watcher
的实例,以下面代码举例说明:
<tempalte> <div>{{msg}}</div> </template> <script> export default { data () { return { msg: 'Hello, Vue.js' } } } </script>
当组件初次渲染的时候,会获取msg
的值,然后执行pushTarget(this)
,其中this
代表当前Watcher
实例,pushTarget()
函数是定义在dep.js
文件中的一个方法,与之对应的还有一个叫做popTarget
方法,它们的代码如下:
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] }
在pushTarget
中,我们传递的target
参数就是Watcher
实例,然后在pushTarget
执行的时候,它会动态设置Dep
的静态属性Dep.target
的值。在分析完pushTarget
函数的代码后,我们就能明白为什么说Dep.target
就是各种Watcher
的实例了。
然后,我们会存在一个新的问题:Watcher
类是如何定义的?它其实是定义在watcher.js
文件中一个类,其关键代码如下:
let uid = 0 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() } 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 } 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) } } } 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 } depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } }
从依赖收集的角度去看Watcher
类的时候,我们在其构造函数中需要关注以下四个属性:
this.deps = [] // 旧dep列表 this.newDeps = [] // 新dep列表 this.depIds = new Set() // 旧dep id集合 this.newDepIds = new Set() // 新dep id集合
我们会在之后的addDep
和cleanupDeps
环节详细介绍以上四个属性的作用,在这一小节,我们主要关注Watcher
的构造函数以及get()
方法的实现。
在Watcher
类的构造函数中,当实例化时,deps
和newDeps
数组以及depIds
和newDepIds
集合分别被初始化为空数组以及空集合,在构造函数的最后,判断了如果不是computed watcher
(注:只有computed watcher
其lazy
属性才为true
),则会马上调用this.get()
函数进行求值。
接下来,我们来分析一下this.get()
方法的实现,以及pushTarget
和popTarget
方法配合使用的场景介绍。
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 }
我们可以看到,get()
方法的代码不是很复杂,在方法的最前面首先调用pushTarget(this)
,通过pushTarget()
方法首先把当前Watcher
实例压栈到target
栈数组中,然后把Dep.target
设置为当前的Watcher
实例。
Dep.target = null const targetStack = [] export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target }
然后调用this.getter
进行求值,拿以下计算属性示例来说:
export default { data () { return { age: 23 } }, computed: { newAge () { return this.age + 1 } } } value = this.getter.call(vm, vm) // 相当于 value = newAge()
对于computed watcher
而言,它的getter
属性就是我们撰写的计算属性方法,调用this.getter
的过程,就是执行我们撰写的计算属性方法进行求值的过程。
在this.get()
方法的最后,调用了popTarget()
,它会把当前target
栈数组的最后一个移除,然后把Dep.target
设置为倒数第二个。
Dep.target = null const targetStack = [] export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] }
在分析了pushTarget
和popTarget
后,我们可能会有一个疑问,就是为什么会存在这样的压栈/出栈的操作,这样做的目的是什么?
这样做的目的是因为组件是可以嵌套的,使用栈数组进行压栈/出栈的操作是为了在组件渲染的过程中,保持正确的依赖,以下面代码为例:
// child component export default { name: 'ChildComponent', template: '<div>{{childMsg}}</div>', data () { return { childMsg: 'child msg' } } } export default { name: 'ParentComponent', template: `<div> {{parentMsg}} <child-component /> </div>`, components: { ChildComponent } data () { return { parentMsg: 'parent msg' } } }
我们都知道,组件渲染的时候,当父组件中有子组件时,会先渲染子组件,子组件全部渲染完毕后,父组件才算渲染完毕,因此组件渲染钩子函数的执行顺序为:
parent beforeMount() child beforeMount() child mounted() parent mounted()
根据以上渲染步骤,当parent beforeMount()
开始执行时,会进行parent render watcher
实例化,然后调用this.get()
,此时的Dep.target
依赖为parent render watcher
,target
栈数组为:
// 演示使用,实际为Watcher实例 const targetStack = ['parent render watcher']
当child beforeMount
开始执行的时候,会进行child render watcher
实例化,然后调用this.get()
,此时的Dep.target
依赖为child render watcher
,target
栈数组为:
// 演示使用,实际为Watcher实例 const targetStack = ['parent render watcher', 'child render watcher']
当child mounted()
执行时,代表子组件的this.getter()
调用完毕,进而会调用popTarget()
进行出栈操作,此时的栈数组和Dep.target
会发生变化:
// 演示使用,实际为Watcher实例 const targetStack = ['parent render watcher'] Dep.target = 'parent render watcher'
当parent mounted()
执行时,代表父组件的this.getter()
调用完毕,进而会调用popTarget()
进行出栈操作,此时的栈数组和Dep.target
会发生变化:
// 演示使用,实际为Watcher实例 const targetStack = [] Dep.target = undefined
通过以上示例分析,我们就弄明白了为什么会有依赖压栈/出栈这样的步骤以及这样做的目的了。接下来,让我们来分析依赖收集的过程中,addDep
和cleanupDeps
的逻辑。
addDep和cleanupDeps
addDep
在之前Dep
类的depend()
方法中,我们介绍过其代码实现,它会调用addDep(dep)
:
export default Dep { // 省略其它代码 depend () { if (Dep.target) { Dep.target.addDep(this) } } }
根据前面的分析内容,我们知道Dep.target
其实就是各种Watcher
实例,因此Dep.target.addDep(this)
相当于:
const watcher = new Watcher() watcher.addDep(this)
接下来,让我们来看一下Watcher
类中,addDep
方法的实现逻辑:
export default Watcher { // 精简代码 constructor () { this.deps = [] // 旧dep列表 this.newDeps = [] // 新dep列表 this.depIds = new Set() // 旧dep id集合 this.newDepIds = new Set() // 新dep id集合 } 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
是否已经在新dep id
集合中,不在则更新新dep id
集合以及新dep
数组,随后又判断了当前dep
是否在旧dep id
集合中,不在则调用dep.addSub(this)
方法,把当前Watcher
实例添加到dep
实例的subs
数组中。
生硬的分析源码不是很方便我们理解addDep
的代码逻辑,我们以下面代码示例说明:
<template> <p>位置一:{{msg}}</p> <p>位置二:{{msg}}</p> </template> <script> export default { name: 'App', data () { return { msg: 'msg' } } } </script>
过程分析:
- 当组件初次渲染的时候,会实例化
render watcher
,此时的Dep.target
为render watcher
:
const updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
- 第一次编译读取
msg
响应式变量时,触发getter
进行dep.depend()
依赖收集,然后调用addDep()
方法,因为deps
、newDeps
、depIds
和newDepIds
初始化为空数组或者空集合,所以此时的dep
被添加到newDepIds
、newDeps
中并且会执行dep.addSub(this)
,此时可以用下面代码表示:
// 实例化Dep const dep = { id: 1, subs: [] } // 添加到newDepIds,newDeps this.newDepIds.push(1) this.newDeps.push(dep) // 调用addSub dep.addSub(this) console.log(dep) // { id: 1, subs: [new Watcher()] }
- 当第二次编译读取
msg
响应式变量时,触发getter
进行dep.depend
依赖收集,因为dep
是defineReactive
函数中的闭包变量,因此两次触发的getter
是同一个dep
实例。当调用addDep
判断此时的newDepIds
集合中dep.id
为1
已经存在,因此直接跳过。
你可能会发现,在分析getter
中代码的时候,我们故意忽略了下面这段代码:
if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } }
你可能会有这样的疑问:这点代码是干什么的?有什么作用?那么现在,我们举例说明:
<template> <p>{{obj.msg}}</p> </template> <script> export default { name: 'App', data () { return { obj: { msg: 'msg' } } } } </script>
过程分析:
- 当第一次调用
defineReactive
时,此时defineReactive
第一个参数obj
和key
分别为:
obj = { obj: { msg: 'msg' } } key = 'obj'
在defineReactive
在最开始,实例化了一个闭包dep
实例,我们假设实例化后的dep
如下:
const dep = new Dep() console.log(dep) // { id: 1, subs: [] }
当代码执行到observe(val)
的时候,根据之前我们分析过observe
代码的逻辑,因为参数obj[key]
的值是一个普通对象,因此会执行new Observer()
实例化,而在Observer
构造函数中,有这样一段代码:
this.dep = new Dep()
它又实例化了一个dep
并且把实例化后的dep
赋值给this.dep
,我们假设此时实例化后的dep
如下所示:
const dep = new Dep() console.log(dep) // { id: 2, subs: [] }
因为obj = { msg: 'msg' }
是一个对象,因此执行this.walk()
遍历obj
对象的属性,然后再次调用defineReactive
又实例化了一个闭包dep
实例,我们假设实例后的dep
如下所示:
const dep = new Dep() console.log(dep) // { id: 3, subs: [] }
现在,我们已经有了三个dep
实例了,其中两个是defineReactive
函数中的闭包实例dep
,另外一个是childOb
(Observer
实例)的属性dep
。
- 在组件开始渲染的时候,根据响应式原理加上我们在
template
中读取了obj.msg
变量,因此会先触发obj
对象的getter
,此时dep
为id=1
的那个闭包变量dep
。此时的Dep.target
为render watcher
,然后进行dep.depend()
依赖收集,当走到addDep
方法的时候,因为我们关注的四个属性全部为空数组或者空集合,因此会把此时的dep
添加进去,此时的dep
表示如下:
const dep = { id: 1, subs: [new Watcher()] }
- 在
dep.depend()
依赖收集完毕后,会判断childOb
,因为childOb
为Observer
的实例,因此条件判断为真,调用childOb.dep.depend()
。当执行到addDep()
时,此时的dep
为id=2
的那个Observer
实例属性dep
,不在newDepIds
和depIds
中,因此会把其添加进去,此时的dep
表示如下:
const dep = { id: 2, subs: [new Watcher()] }
- 当响应式变量
obj
的getter
触发完毕后,会触发obj.msg
的getter
,此时的dep
为id=3
的那个闭包变量dep
。此时的Dep.target
依然为render watcher
,然后进行dep.depend()
依赖收集,这个过程与
obj的
getter进行依赖收集的过程基本是一样的,当
addDep()方法执行后,此时的
dep`表示如下:
const dep = { id: 3, subs: [new Watcher()] }
唯一的区别时,此时的childOb
为undefined
,不会调用childOb.dep.depend()
进行子属性的依赖收集。
在分析完以上代码后,我们很容易回答一下问题:
问:childOb.dep.depend()
是干什么的?有什么作用?
答:childOb.dep.depend()
这段代码是进行子属性的依赖收集,这样做的目的是为了当对象或者对象属性任意一个发生变化时,都可以通知其依赖进行相应的处理。
<template> <p>{{obj.msg}}</p> <button @click="change">修改属性</button> <button @click="add">添加属性</button> </template> <script> import Vue from 'vue' export default { name: 'App', data () { return { obj: { msg: 'msg' } } }, methods: { change () { this.obj.msg = 'new msg' }, add () { this.$set(this.obj, 'age', 23) } }, watch: { obj: { handler () { console.log(this.obj) }, deep: true } } } </script>
拿以上例子说明:
- 当存在
childOb.dep.depend()
收集子属性依赖时,我们无论是修改msg
的值还是添加age
新属性,都会触发user watcher
,也就是打印this.obj
的值。 - 当不存在
childOb.dep.depend()
收集子属性依赖时,我们修改msg
的值,虽然会通知render watcher
进行组件重新渲染,但不会通知user watcher
打印this.obj
的值。
cleanupDeps
在这一小节,我们的目标是弄清楚为什么要进行依赖清除以及如何进行依赖清除。
先来看Watcher
类中对于cleanupDeps
的实现:
export default Watcher { // 精简代码 constructor () { this.deps = [] // 旧dep列表 this.newDeps = [] // 新dep列表 this.depIds = new Set() // 旧dep id集合 this.newDepIds = new Set() // 新dep id集合 } 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 } }
我们还是举例说明,假如有如下组件:
<template> <p v-if="count < 1">{{msg}}</p> <p v-else>{{age}}</p> <button @click="change">Add</button> </template> <script> import Vue from 'vue' export default { name: 'App', data () { return { count: 0, msg: 'msg', age: 23 } }, methods: { change () { this.count++ } } } </script>
过程分析:
- 当组件初次渲染完毕后,
render watcher
实例的newDeps
数组有两个dep
实例,其中一个是在count
响应式变量getter
被触发时收集的,另外一个是在msg
响应式变量getter
被触发时收集的(age
因为v-if/v-else
指令的原因,在组件初次渲染的时候不会触发age
的getter
),我们使用如下代码进行表示:
this.deps = [] this.newDeps = [ { id: 1, subs: [new Watcher()] }, { id: 2, subs: [new Watcher()] } ]
- 当我们点击按钮进行
this.count++
的时候,会触发组件重新更新,因为count < 1
条件为假,因此在组件重新渲染的过程中,也会触发age
响应式变量的getter
进行依赖收集。当执行完addDep
后,此时newDeps
发生了变化:
this.deps = [ { id: 1, subs: [new Watcher()] }, { id: 2, subs: [new Watcher()] } ] this.newDeps = [ { id: 1, subs: [new Watcher()] }, { id: 3, subs: [new Watcher()] } ] this.depIds = new Set([1, 2]) this.newDepIds = new Set([1, 3])
在最后一次调用this.get()
的时候,会调用this.cleanupDeps()
方法,在这个方法中首先遍历旧依赖列表deps
,如果发现其中某个dep
不在新依赖id
集合newDepIds
中,则调用dep.removeSub(this)
移除依赖。在组件渲染的过程中,this
代表render watcher
,调用这个方法后当我们再修改msg
变量值的时候,就不会触发组件重新渲染了。在遍历完deps
数组后,会把deps
和newDeps
、depIds
和newDepIds
的值进行交换,然后清空newDeps
和newDepIds
。
在分析完以上示例后,我们就能明白为什么要进行依赖清除了:避免无关的依赖进行组件的重复渲染。
派发更新
在介绍完依赖收集后,我们紧接着来分析一下派发更新。在这一小节,我们的目标是弄清楚派发更新主要做什么事情以及派发更新的具体过程实现。
我们先来回答第一个问题:
问:派发更新主要做什么事情?
答:派发更新就是当响应式数据发生变动的时候,通知所有订阅了这个数据变化的Watcher
(既Dep
依赖)执行update
。对于render watcher
渲染Watcher
而言,update
就是触发组件重新进行渲染;对于computed watcher
计算属性Watcher
而言,update
就是对计算属性重新求值;对于user watcher
用户自定义Watcher
而言,update
就是调用用户提供的回调函数。
场景
大多数人分析派发更新的场景,只说明了Object.defineProperty()
方法中setter
被触发的时候会进行派发更新,其实一共有四处派发更新的地方,其它三处分别为:
Vue.js
中七种数组变异方法被调用时,会进行派发更新。
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'] methodsToPatch.forEach(function (method) { def(arrayMethods, method, function mutator (...args) { // 精简代码 ob.dep.notify() return result }) })
Vue.set
或者this.$set
的时候,会进行派发更新。
export function set (target: Array<any> | Object, key: any, val: any): any { // 精简代码 defineReactive(ob.value, key, val) ob.dep.notify() return val }
Vue.delete
或者this.$delete
的时候,会进行派发更新。
export function del (target: Array<any> | Object, key: any) { // 精简代码 delete target[key] if (!ob) { return } ob.dep.notify() }
其中,以上三种派发更新与Object.defineProperty()
方法中的setter
被触发时的派发更新有一点不一样,setter
中的派发更新,它的dep
是一个在defineReactive
方法中定义的闭包变量,意味着其只能服务于defineReactive
方法。前者的dep
是从this.__ob__
对象中取的,this.__ob__
属性是在Observer
被实例化的时候被定义的,它指向Observer
实例,我们在之前已经介绍过。这种独特的处理方式,方便了我们在以上三种场景下,能方便的读取到dep
依赖,进而进行依赖的派发更新。
过程
在以上代码中,我们以及了解到了dep.notify()
被调用的各种时机,在这一个小节中我们需要来看一下派发更新的过程。
当dep.notify()
被调用时,它会执行notify()
方法中的代码,我们来看一下Dep
类中关于这个方法的实现:
notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }
可以发现,notify
主要做的就是遍历subs
数组,然后调用update
方法。下一步,我们来看一下Watcher
类中关于update
方法的代码实现:
import { queueWatcher } from './scheduler' update () { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
在update
方法被执行的时候,首先判断了this.lazy
和this.sync
两个属性,其中this.lazy
为computed watcher
计算属性的标志,因为计算属性会延后进行求值,因此这里只是把this.dirty
赋值为true
,this.sync
不属于派发更新这一章节的重点,因此不做过多的介绍。
我们来重点分析queueWatcher
,它是撰写在observer/scheduler.js
文件中的一个方法:
const queue: Array<Watcher> = [] let has: { [key: number]: ?true } = {} let waiting = false let flushing = false let index = 0 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) } } }
我们可以在以上代码最顶部发现定义了几个变量,其中有几个比较重要的变量,它们的作用如下:
queue
:各种Watcher
执行队列,无论是render watcher
、user watcher
还是computed watcher
,只要不是重复的Watcher
,最终都会被推入到queue
队列数组中。has
:用来防止重复添加Watcher
的标志对象:
// 表示id为1,2的Watcher实例已经被添加到了queue // 后续遇到同样的Watcher实例,不会重复添加到队列中 const has = { 1: true, 2: true }
index
:当前遍历的Watcher
实例索引,它就是flushSchedulerQueue
方法中使用for
循环遍历queue
队列数组的index
。
介绍完以上几个重要变量后,我们来分析一下queueWatcher
的过程:
- 代码首先通过获取当前
Watcher
的自增id
,判断在标志对象has
中是否已经存在,如果不存在,则对这个id
进行标记,赋值为true
。 - 随后判断是否为
flushing
状态,如果不是,则代表我们可以正常的把当前Watcher
推入到queue
队列数组中。 - 接着判断了是否为
waiting
状态,如果不是,则代表可以执行queue
队列数组,然后设置waiting
为true
,最后调用nextTick(flushSchedulerQueue)
,nextTick
方法是Vue.js
自己封装的一个处理异步逻辑的工具函数,我们现在只要知道:nextTick
中的函数参数,会在下一个tick
执行。
接着,我们来看flushSchedulerQueue
函数是如何实现的:
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') } }
我们粗略观看flushSchedulerQueue
函数代码,可以发现它主要做几件事情:还原flushing状态、排序queue队列、遍历queue、还原状态、触发组件钩子函数。我们按照这几个步骤,分别说明:
- 还原flushing状态:在
flushSchedulerQueue
首先对flushing
进行了还原,这样做的目的是为了不影响在执行queue
队列的时候,有Watcher
推入到queue
队列中。 - 排序queue队列:使用数组的
sort
方法,把queue
队列中的Watcher
按照自增id
的值从小到大进行了排序,这样做是为了保证以下三种场景:
- 我们都知道,组件的更新是从父组件开始,然后到子组件。在组件渲染的时候,会从父组件开始渲染,这时候会创建父组件的
render watcher
,假设此时的parent render watcher
自增id
为1
,接着渲染子组件,实例化子组件的render watcher
,假设此时的child render watcher
自增id
为2
。进行queue.sort()
排序后,id
值小的排序到数组前面,这样在queue
进行遍历的时候,就能保证首先处理parent render watcher
,然后再处理child render watcher
- 因为用户自定义
Watcher
可以在组件渲染之前创建,因此对于用户自定义Watcher
而言,需要优先于render watcher
执行。
<template> <p>{{msg}}</p> <button @click="change">Add</button> </template> <script> export default { data () { return { count: 0, msg: 'msg', age: 23 } }, created () { this.$watch('msg', () => { // 先执行回调函数,再组件渲染 console.log(this.msg) }) }, methods: { change () { this.msg = Math.random() } } } </script>
- 如果一个子组件在父组件执行
queueWatcher
的过程中被销毁了,那么子组件所有的Watcher
执行都应该跳过。
- 遍历queue:在使用
for
循环遍历的时候,我们需要注意遍历条件,它先对queue
的长度进行了求值,然后再判断循环条件,这样做是因为在遍历queue
数组的过程中,queue
数组中的元素有可能会发生变动。在遍历的过程中,首先会释放当前Watcher
在has
标志对象中的状态,然后调用watcher.run()
方法。run
是定义在Watcher
类中的一个方法:
export default class Watcher { // 精简代码 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) } } } } }
run
方法的代码不是很复杂,就是对不同的Watcher
进行不同的处理,如果是render watcher
,它在执行this.get()
的过程中会执行this.getter
,this.getter
对应以下方法:
updateComponent = () => { // 组件渲染方法 vm._update(vm._render(), hydrating) }
如果是user watcher
,其this.user
值为true
,会调用this.cb.call()
,此时的this.cb
就是用户写的user callback
:
export default { data () { return { msg: 'msg' } }, created () { // user callback // this.cb = userCallback const userCallback = () => { console.log(this.msg) } this.$watch(this.msg, userCallback) } }
如果是computed watcher
,其this.user
值为false
,会调用this.cb.call()
,此时的this.cb
就是我们提供的计算属性方法:
export default { data () { return { msg: 'msg' } }, computed: { // this.cb = newMsg () {} newMsg () { return this.msg + '!!!' } } }
- 还原状态:调用
resetSchedulerState
函数的目的是,当queue
队列都执行完毕时,把所有相关状态还原为初始状态,这其中包括queue
、has
和index
等:
function resetSchedulerState () { index = queue.length = activatedChildren.length = 0 has = {} if (process.env.NODE_ENV !== 'production') { circular = {} } waiting = flushing = false }
- 触发组件钩子函数:调用
callActivatedHooks
和callUpdatedHooks
分别是为了触发组件activated
和updated
钩子函数,其中activated
是与keep-alive
相关的钩子函数。
死循环
在使用Vue.js
进行开发的时候,有时候我们会不小心写出死循环的代码,例如:
<template> <p>{{msg}}</p> <button @click="change">Add</button> </template> <script> export default { data () { return { msg: 'msg' } }, methods: { change () { this.msg = Math.random() } }, watch: { msg () { this.msg = Math.random() } } } </script>
当我们点击按钮调用change
方法修改this.msg
的值的时候,因为我们使用watch
监听了msg
的值更新,所以会执行watch
监听函数,但是在watch
监听函数中我们又修改了this.msg
的值,这样会导致一直调用我们写的监听函数,存在一个死循环。在Vue.js
中,为了避免死循环导致浏览器崩溃,它做了特殊处理。
在queueWatcher
的时候,我们并没有分析以下else
这段代码:
export const MAX_UPDATE_COUNT = 100 let circular: { [key: number]: number } = {} 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) }
我们以上面的例子来分析以下这段代码:
- 当我们点击按钮修改
this.msg
的值的时候,触发msg
的setter
,然后进行dep.notify
派发更新,接着调用queueWatcher
,此时msg
存在两个Dep
依赖,一个是render watcher
,另外一个是user watcher
,因此this.subs
是一个长度为2
的Watcher
数组。当初次queueWatcher
的时候,flushing
状态为false
,因为user watcher
比render watcher
先创建,因此这个时候user watcher
会先推入到queue
队列,接着是render watcher
:
// 展示使用,实际为Watcher实例 const queue = ['user watcher', 'render watcher']
- 接着会执行
watch
监听函数,再次执行queueWatcher
的时候,此时的flushing
为false
,走else
分支逻辑,while
循环的作用主要是为了查找应该在queue
数组什么位置插入新的watcher
,例如:
const queue = [ { id: 1, type: 'user watcher' }, { id: 2, type: 'render watcher' }, ] // 当执行watch监听函数的时候,此时的watcher应该插入到数组第二项 const queue = [ { id: 1, type: 'user watcher' }, { id: 1, type: 'user watcher' }, { id: 2, type: 'render watcher' }, ]
因为我们撰写的特殊例子,queue
数组会不断的推入user watcher
,当queue
中的数量超过限制的时候,Vue.js
提前终止这种行为(某些Watcher
被遍历超过100次时),Vue.js
使用circular
标记对象来进行计数,它标记了每一个Watcher
被遍历的次数,例如:
// id为1的Watcher被遍历了101次 // id为2的Watcher被遍历了1次 const circular = { 1: 101, 2: 1 }
circular
计数更新和终止的代码在flushSchedulerQueue
函数中:
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 } } }
因此对于以上例子,Vue.js
会在控制台输出这样一个错误信息:
// You may have an infinite update loop in watcher with expression "msg"
整体流程图
在分析完以上派发更新的过程后,我们可以得到如下流程图。
nextTick实现原理
在使用Vue.js
开发的时候,如果我们要根据数据状态操作正确的DOM
,那么我们一定和nextTick()
方法打过交道,它是Vue.js
中一个比较核心的一个方法,在这一章节中我们来介绍Vue.js
中nextTick
是如何实现的。
异步知识
由于nextTick
涉及到许多与异步相关联的知识,因此为了降低学习难度,我们先来介绍这些异步知识。
Event Loop
我们都知道JavaScript
是单线程的,它是基于Event Loop
事件循环来执行的,Event Loop
在执行的时候遵循一定的规则:所有同步任务都在主线程中执行,形成一个执行栈,所有异步任务,都会被暂时放入一个任务队列中,当所有同步任务执行完毕时,会读取这个任务队列让其进入执行栈,开始执行。以上介绍属于一次执行机制,主线程不断重复这个过程就形成了Event Loop
事件循环。
以上是对Event Loop
的大体介绍,但在Event Loop
执行的时候,还有一些细节需要我们去掌握。
我们在派发更新章节提到过tick
,那么什么是tick
?tick
就是主线程的一次执行过程。所有异步任务都是通过任务队列来调度的,任务队列中存放的是一个个任务(task
),而这一个个task
按照规范,又分为macro task
宏任务和micro task
微任务。macro task
和micro task
在执行的时候存在一个微妙的关系:每个macro task
执行结束后,会清空所有的micro task
。
在浏览器环境下,macro task
和micro task
对应如下:
macro task
宏任务:MessageChannel
、postMessage
、setImmediate
和setTimeout
。micro task
微任务:Promise.then
和MutationObsever
。
MutationObserver
在MDN文档中,我们可以看到MutationObserver
的详细用法,它不是很复杂,它的作用是:创建并返回一个新的 MutationObserver
实例,它会在指定的DOM发生变化时被调用。
我们按照文档介绍,来撰写一个例子:
const callback = () => { console.log('text node data change') } const observer = new MutationObserver(callback) let count = 1 const textNode = document.createTextNode(count) observer.observe(textNode, { characterData: true }) function func () { count++ textNode.data = count } func() // text node data change
代码分析:
- 首先定义了
callback
回调函数和MutationObserver
的实例对象,其中构造函数传递的参数是我们的callback
。 - 然后创建一个文本节点并传入文本节点的初始文本,接着调用
MutationObserver
实例的observe
方法,传入我们创建的文本节点和一个config
观察配置对象,其中characterData:true
的意思是:我们要观察textNode
节点的文本变动。config
还有其他选项属性,你可以在MDN
文档中查看到。 - 接着,我们定义一个
func
函数,这个函数主要做的事情就是修改textNode
文本节点中的文本内容,当文本内容变动后,callback
会自动被调用,因此输出text node data change
。
在了解了MutationObserver
的用法后,我们来看一下nextTick
方法中,是如何使用MutationObserver
的:
import { isIE, isNative } from './env' // 省略代码 else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // Use MutationObserver where native Promise is not available, // e.g. PhantomJS, iOS7, Android 4.4 // (#6466 MutationObserver is unreliable in IE11) let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true }
我们可以看到,nextTick
中首先判断了非IE
浏览器并且MutationObserver
可用且为原生MutationObserver
时才会使用MutationObserver
。对于判断非IE
浏览器的情况,你可以看Vue.js
的issue
#6466来查看原因。
setImmediate和setTimeout
setTimeout
对于大部分人来说是非常常见的一个定时器方法,因此我们不做过多的介绍。
在nextTick
方法实现中,它使用到了setImmediate
,我们在Can I Use网站上可以发现,这个API
方法只存在于高版本IE
浏览器和低版本Edge
浏览器中,其它浏览器不支持。
那么为什么会使用这个方法呢,这是因为我们之前提到的一个issue
:MutationObserver
在IE
浏览器中并不可靠,因此在IE
浏览器下降级到使用setImmediate
,我们可以把setImmediate
看做和setTimeout
一样。
setImmediate(() => { console.log('setImmediate') }, 0) // 约等于 setTimeout(() => { console.log('setTimeout') }, 0)
nextTick实现
介绍完nextTick
与异步相关的知识后,我们来的分析一下nextTick
方法的实现,首先要说的是:异步降级。
let timerFunc // The nextTick behavior leverages the microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, however it is seriously bugged in // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It // completely stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore next, $flow-disable-line */ 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 } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // Use MutationObserver where native Promise is not available, // e.g. PhantomJS, iOS7, Android 4.4 // (#6466 MutationObserver is unreliable in IE11) let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // Fallback to setImmediate. // Technically it leverages the (macro) task queue, // but it is still a better choice than setTimeout. timerFunc = () => { setImmediate(flushCallbacks) } } else { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) } }
我们在前面介绍过Event Loop
事件循环,由于macro task
和micro task
特殊的执行机制,我们首先判断当前浏览器是否支持Promise
,如果不支持,则降级到判断是否支持MutationObserver
,如果还不支持,则继续降级到判断是否支持setImmediate
,最后降级使用setTimeout
。
在介绍完异步降级之后,我们来看nextTick
的实现代码:
const callbacks = [] let pending = false 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) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
nextTick
真正的代码并不复杂,首先会把传入的cb
收集起来,然后判断pending
为false
的时候开始执行timerFunc
方法,其中timeFunc
在异步降级的过程中被定义的。nextTick
方法在最后还进行了判断,如果没有传入cb
并且支持Promise
的话,它会返回一个promise
,因此我们在使用nextTick
的时候可以有两种使用方式:
const callback = () => { console.log('nextTick callback') } // 方式一 this.$nextTick(callback) // 方式二 this.$nextTick().then(() => { callback() })
在最后,我们来看一个在之前没有提到的flushCallbacks
方法实现:
const callbacks = [] let pending = false function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
flushCallbacks
方法的主要作用就是:将pending
状态还原为false
并且遍历callbacks
数组中的方法并执行。
变化侦测注意事项
虽然Object.defineProperty()
方法很好用,但也会存在一些例外情况,这些例外情况的变动不能触发setter
。这种情况,我们分为对象和数组两类来分析。
对象
假设我们有如下例子:
export default { data () { return { obj: { a: 'a' } } }, created () { // 1.新增属性b,属性b不是响应式的,不会触发obj的setter this.obj.b = 'b' // 2.delete删除已有属性,无法触发obj的setter delete this.obj.a } }
从以上例子我们可以看到:
- 当为一个响应式对象新增一个属性的时候,新增的属性不是响应式的,后续对于这个新增属性的任何修改,都无法触发其
setter
。为了解决这种问题,Vue.js
提供了一个全局的Vue.set()
方法和实例vm.$set()
方法,它们实际上都是同一个set
方法,我们会在后续的章节中介绍与响应式相关的全局API
的实现。 - 当一个响应式对象删除一个已有属性的时候,不会触发
setter
。为了解决这个问题,Vue.js
提供了一个全局的vue.delete()
方法和实例vm.$delete()
方法,它们实际上都是同一个del
方法,我们会在后续的章节中介绍与响应式相关的全局API
的实现。
数组
假设我们有如下例子:
export default { data () { return { arr: [1, 2, 3] } }, created () { // 1.通过索引进行修改,无法捕获到数组的变动。 this.arr[0] = 11 // 2.通过修改数组长度,无法捕获到数组的变动。 this.arr.length = 0 } }
从以上例子我们可以看到:
- 通过索引直接修改数组,无法捕捉到数组的变动。
- 通过修改数组长度,无法捕获到数组的变动。
对于第一种情况,我们可以使用前面提到过的Vue.set
或者vm.$set
来解决,对于第二种方法,我们可以使用数组的splice()
方法解决。
在最新版Vue3.0
中,使用到了Proxy
来代替Object.defineProperty()
实现响应式,使用Proxy
后以上问题全部可以解决,然而Proxy
属于ES6
的内容,因此对于浏览器兼容性方面有一定的要求。
变化侦测API实现
在上一节中,我们分析了变化侦测一些问题,在这一节中我们来分析一下为了解决这些问题,Vue.js
是如何实现相关API
的。
Vue.set实现
Vue.set
和vm.$set
引用的是用一个set
方法,其中set
方法被定义在observer/index.js
文件中:
export function set (target: Array<any> | Object, key: any, val: any): any { 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)) { 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 }
在代码分析之前,我们来回顾一下Vue.set
或者vm.$set
的用法:
export default { data () { return { obj: { a: 'a' }, arr: [] } }, created () { // 添加对象新属性 this.$set(this.obj, 'b', 'b') console.log(this.obj.b) // b // 往数组中添加新元素 this.$set(this.arr, 0, 'AAA') console.log(this.arr[0]) // AAA // 通过索引修改数组元素 this.$set(this.arr, 0, 'BBB') console.log(this.arr[0]) // BBB } }
代码分析:
set
方法首先对传入的target
参数进行了校验,其中isUndef
判断是否为undefined
,isPrimitive
判断是否为JavaScript
原始值,如果满足其中一个条件则在开发环境下提示错误信息。
export default { created () { // 提示错误 this.$set(undefined, 'a', 'a') this.$set(1, 'a', 'a') this.$set('1', 'a', 'a') this.$set(true, 'a', 'a') } }
-
随后通过
Array.isArray()
方法判断了target
是否为数组,如果是再通过isValidArrayIndex
判断是否为合法的数组索引,如果都满足则会使用变异splice
方法往数组中指定位置设置值。其中,还重新设置了数组的length
属性,这样做是因为我们传入的索引可能比现有数组的length
还要大。 -
接着判断是否为对象,并且当前
key
是否已经在这个对象上,如果已经存在,则我们只需要进行重新复制即可。 -
最后,通过
defineReactive
方法在响应式对象上面新增一个属性,defineReactive
方法已经在之前介绍过,这里不再累述。在defineReactive
执行完毕后,马上进行派发更新,通知响应式数据的依赖立即更新,可以说以下两段代码是set
方法核心中的核心:
defineReactive(ob.value, key, val) ob.dep.notify()
Vue.delete实现
解决完新增属性的问题后,我们来解决以下删除属性的情况,Vue.delete
和vm.$delete
使用的是同一个delete
方法,它被定义在observer/index.js
文件中:
export function del (target: Array<any> | Object, key: any) { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`) } if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1) return } const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ) return } if (!hasOwn(target, key)) { return } delete target[key] if (!ob) { return } ob.dep.notify() }
在代码分析之前,我们来回顾以下Vue.delete
或者vm.$delete
的用法:
export default { data () { return { obj: { a: 'a' }, arr: [1, 2, 3] } }, created () { // 删除对象属性 this.$delete(this.obj, 'a') console.log(this.obj.a) // undefined // 删除数组元素 this.$delete(this.arr, 1) console.log(this.arr) // [1, 3] } }
代码分析:
- 首先判断了待删除的
target
不能为undefined
或者原始值,如果是则在开发环境下提示错误。
export default { created () { // 提示错误 this.$delete(undefined, 'a') this.$delete(1, 'a') this.$delete('1', 'a') this.$delete(true, 'a') } }
- 随后通过
Array.isArray()
方法判断了target
是否为数组,如果是再通过isValidArrayIndex
判断是否为合法的数组索引,如果都满足则会使用变异splice
方法删除指定位置的元素。 - 接着判断当前要删除的属性是否在
target
对象中,如果不在则直接返回,什么都不做。 - 最后,通过
delete
操作符删除对象上的属性,然后ob.dep.notify()
进行派发更新,通知响应式对象上的依赖进行更新。
Vue.observable实现
Vue.observable
是在Vue2.6+
版本才会有的一个全局方法,它的作用是让一个对象变成响应式:
const obj = { a: 1, b: 2 } const observeObj = Vue.observable(obj) console.log(observeObj.a) // 触发getter observeObj.b = 22 // 触发setter
这个全局方法是在initGlobalAPI
的过程中被定义的,initGlobalAPI
我们在之前已经介绍过,这里不在累述:
export default function initGlobalAPI (Vue) { // ... // 2.6 explicit observable API Vue.observable = <T>(obj: T): T => { observe(obj) return obj } // ... }
我们可以看到observable
的实现很简单,在方法内部仅仅只是调用了observe
方法,然后返回这个obj
。关于observe
的代码实现,我们在之前的章节中已经介绍过了,这里不再过多的进行说明:
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 }