——慢慢来,一切会比较快
前言
继上篇《Vue2.0源码阅读计划(一)——工具函数》之后,我们来探究响应式原理,这是vue
的核心所在。本篇采取模块化的阅读方式link,一定要对照着源码来阅读。对于源码,重在理解思想,不必一行行死抠代码,要学会学习。思想是境界上的提升,有了思想的高度,就不怕不会做。
这块的文章真的不好写,一定要自己结合代码多思考,欢迎指教???。
引子
源码中文件第一行的注释/* @flow */
,是因为Vue2.0
源码采用的flow
做的静态类型检查,并没有用typescript
,所以行首会有注释,目的就是启用静态类型检查,flow
的写法与typescript
大体差不多,所以源码阅读起来不要有太大的困惑。
var vm = new Vue({
data: {
a: 1
}
})
我们是这样初始化一个Vue
实例的,当然在单文件组件中我们的data
会定义为一个函数,这是因为单文件组件是组件,组件是会复用的。
扩展
vue
单文件组件通过vue-loader
解析,而vue-loader
做的事只是把.vue
文件中的template
与style
编译到.js
(编译到render
函数),并混合到你在.vue
中export
出来的Object
中。产出的js
只是导出了一个符合component
定义的Object
。
在 Vue 2.0
版本中,所有 Vue
的组件的渲染最终都需要 render
方法,无论我们是用单文件 .vue
方式开发组件,还是写了 el
或者 template
属性,最终都会转换成 render
方法,这个过程就是 Vue
的模板编译
过程。
模板编译
后续篇章会讲,继续正文。
初始化完成后vm.a
就是响应式的了。我们要探究这中间发生了什么?所以我们需要看Vue
这构造函数到底做了什么。我们先找到Vue
的构造函数link:
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
其中options
就是我们传入的包含data、computed、methods...
的对象,上面构造函数的重点在this._init(options)
这一行,this
指代Vue
实例,所以我们需要去Vue
的原型上找到定义所在。
_init
_init
定义在init.js
文件,我们一起看一下:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
Vue 的初始化逻辑写的非常清楚:合并配置,初始化生命周期,初始化事件中心,初始化渲染,调用beforeCreate
钩子函数,初始化注入、初始化状态等等。
initState()
因为本篇探究响应式原理,所以我们重点关注initState(vm)
函数,具体如下:
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)
}
}
initState
中初始化了props、methods、data、computed、watch
五项,不一一分析。
initData()
我们重点分析initData
:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
initData
首先对data
的类型进行判断,如果是函数就调用getData
函数拿到返回值,然后调用isPlainObject
函数判断data
是否为一个朴素的对象,isPlainObject
内部采用Object.prototype.toString.call()
进行的判断。下面紧接着遍历data
的属性,不能与methods
和props
的属性重名,然后通过proxy
将vm._data
代理到vm
上,这也就是我们属性明明是在data
上定义的却能在this
上拿到的原因。注意这里的proxy
函数并不是ES6
中的Proxy
,是个自定义函数,大小写开头区别开,只是名字类似。proxy
函数内部通过对象的访问器属性代理过去,自行查阅。
函数的最后执行了observe
函数,该函数就是对数据添加变化侦测响应式的关键所在,因为ES5
的Object.defineProperty()
只能针对对象,无法作用于数组,所以Object
与Array
的变化侦测是有区别的,下面分开解析。
对象的变化侦测
在observer
文件夹下,一共有这么6
个文件,这就是响应式的核心代码。
三个关键角色:
Observer
: 它的作用是给对象的属性添加getter
和setter
,用于依赖收集和派发更新Dep
: 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个Dep
实例(里面subs
是Watcher
实例数组),当数据有变更时,会通过dep.notify()
通知各个watcher
。Watcher
: 观察者对象 , 实例分为渲染watcher
(render watcher
),计算属性watcher
(computed watcher
),侦听器watcher
(user watcher
)三种
observe()
在index.js
文件中,我们找到observe
函数,我们看看这个函数:
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
}
首先对传入的值进行判断,如果不是一个对象或者是VNode
类型(准确说应该是VNode
的prototype
是否出现在value
的原型链上)就直接返回。然后通过__ob__
属性去判断是否已经添加过响应式了,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer
对象实例并返回。
问题1:value
为什么不允许是VNode
类型?
我的理解是,VNode
不允许是响应式的,我们通过产生VNode
实例的createElement
函数来看(定义render
函数时的第一参数),源码中判断了第二参数data
不能为响应式的,这是因为data
在vnode
的渲染过程中可能会被改变,如果是一个监听属性,就会触发监控,从而产生不可预估的问题,data
就是VNode
类中的一个属性,在new VNode()
时会作为第二参数传入,所以不允许为VNode
添加响应式。大致看下createElement
的实现:
export function _createElement(
context: Component,
tag ? : string | Class < Component > | Function | Object,
data ? : VNodeData,
children ? : any,
normalizationType ? : number
): VNode | Array < VNode > {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}n` +
'Always create fresh vnode data objects in each render!',
context)
return createEmptyVNode()
}
...
}
注意:Object.isExtensible(value)
这个判断很有用
一定要利用好这个特性哦,上篇文章我放过个很有用的例子:
new Vue({
data: {
// vue不会对list里的object做getter、setter绑定
list: Object.freeze([
{ value: 1 },
{ value: 2 }
])
},
mounted () {
// 界面不会有响应
this.list[0].value = 100;
// 下面两种做法,界面都会响应
this.list = [
{ value: 100 },
{ value: 200 }
];
this.list = Object.freeze([
{ value: 100 },
{ value: 200 }
]);
}
})
当你需要定义一个比较复杂的对象但又不依赖它的响应式时就很有用,比如在绘制echarts
时,我就经常见有人把那么复杂的一个options
定义在data
中。因为vue
会给data
数据的所有子对象递归绑定getter
和setter
,数据量大的时候影响性能是必然的。
Observer()
继续正文,我们接着来看Observer
的实现:
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])
}
}
......
}
在new Observer()
时执行constructor()
函数,注意这里有个dep
属性赋值为new Dep()
,然后在value
上注入属性__ob__
值为当前Observer
的实例,表明已经添加过响应式了。接着if
判断了值为数组的情况,本节我们只看对象的,走入else
分支,执行walk
函数,walk()
函数很简单就是遍历value
的所有可枚举属性并执行defineReactive()
函数。于是重点落在了defineReactive()
函数。
defineReactive()
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
的功能从名字就能看出来:定义一个响应式对象,给对象动态添加 getter
和 setter
。注意这里一开始也执行了new Dep()
,然后拿到传入键的属性描述符,判断configurable
是否为false
,因为false
了我们就无法再在下面使用Object.defineProperty
方法重新定义访问器属性了。接着获取访问器属性中的get
与set
,如果get
不存在,就直接通过obj[key]
的形式拿到val
。let childOb = !shallow && observe(val)
递归子属性,将子属性也变为响应式的,这就是为什么我们访问或修改 obj
中一个嵌套较深的属性,也能触发 getter
和 setter
。
重点来了,定义getter
,先看首尾,拿到value
,return
出去,保持了获取数据默认行为,中间的部分为收集依赖。我们先将依赖暂且认为是保存在Dep.target
上的一个东西,判断依赖存在就调用depend
方法进行收集,里面还包含了子属性的依赖收集。 定义setter
,同样先拿到value
,比较新旧值,不同再继续往下,略过自定义setter
,判断如果getter
存在且setter
不存在(也就是说你定义访问器属性的时候只定义了get
却没有定义set
),直接结束,否则向下递归新值的子属性将其也变为响应式,最后触发当前属性的依赖。
注意,vue
源码中基本没写过匿名函数,从定义getter、setter
就能看出来,这是一个很好的习惯。
至此,我们已经大概了解了vue
的响应式原理,但目前我们只知道是通过dep.depend()
去收集依赖,通过dep.notify()
去触发依赖,所以留下最大的疑问是:
Dep
是做什么的?- 依赖到底是什么?
- 依赖具体怎么去收集或触发的?
下面一一解析。
Dep
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()
}
}
}
我们需要注意的是target
这个静态属性,它规定为Watcher
类型,这是一个全局唯一 Watcher
,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher
被计算,另外它的自身属性 subs
也是 Watcher
的数组,subs
为存放依赖的地方。
问题2: Watcher
就是依赖吗?
Watcher
并不是依赖,你可以理解Watcher
为依赖的代理执行者,每一个依赖都对应一个Watcher
,收集依赖时收集Watcher
就可以,触发依赖时通过Watcher
去代理执行。
问题3:依赖到底是什么?
很简单一句话:谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher
实例。vue2.0
版本依赖的粒度大小为中等组件级,即一个状态所绑定的依赖为一个组件。所以可以理解依赖为使用了某个状态的当前组件,当状态改变时,通过Watcher
代理通知到组件,组件内部再使用虚拟dom
进行patch
,然后触发界面组件展示的地方重新渲染。
说了这么多Watcher
,它是什么,我们一起看看:
Watcher
class Watcher {
......
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
......
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
......
}
文中省略了部分代码,我们简单分析下实例化Watcher
的过程中都发生了什么。首先判断是否为渲染watcher
(初始化Vue
实例时mounted
阶段就是渲染watcher
),所以this._watcher
存放的是renderWatcher
,this._watchers
会存放上面说的全部三种类型watcher
。下面我们结合实例化renderWatcher
的那部分源码来继续看Watcher
的实例化过程会好懂些(在new Vue()
的过程中会实例化一个renderWatcher
):
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
没看过模板编译
与patch
部分源码的同学可以简单认为updateComponent
是一个更新组件的函数,准确点来说updateComponent
函数就是生成组件的vnode
并进行patch
然后渲染的那个函数,当然首次渲染并不会进行patch
,因为没有oldVNode
与之对比。上面dep.notify
最后就是通知它执行。
上面Watcher
实例化走到对expOrFn
的判断,此时expOrFn
等于updateComponent
,是函数类型,赋值给this.getter
,最后调用this.get()
,先看一下pushTarget
的定义:
function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
执行pushTarget
将Dep.target
置为当前Watcher
的实例,然后执行this.getter
也就是updateComponent
,在进行模板编译
的过程中会对data
上的属性访问,触发getter
完成依赖收集。其实在这一步走完的时候,当前组件就已经渲染完成了,最后下面的就是执行popTarget
将Dep.target
置空。
派发更新
依赖收集完了,我们再回来看派发更新。我们更新状态,触发setter
,执行dep.notify
,源码中notify
的定义如下:
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
主要做了一件事,就是遍历subs
(收集到的依赖)调用update
方法,update
方法定义如下:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
这理判断了lazy
属性与sync
属性:lazy
属性对应的是computed watcher
,会从缓存中去拿;sync
对应user watcher
,不把更新watcher
放到nextTick
队列 而是立即执行更新。因为我们分析的是render watcher
,所以逻辑走进else
里面,关于三种类型的watcher
之后我会单独开一篇。解析queueWatcher
:
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
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)
}
}
}
这里引入了一个队列的概念,这也是 Vue
在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher
的回调,而是把这些 watcher
先添加到一个队列里,然后在 nextTick
后执行 flushSchedulerQueue
。
这理首先用 has
对象保证同一个 Watcher
只添加一次;接着走 flushing
的判断;最后通过 waiting
保证对 nextTick(flushSchedulerQueue)
的调用逻辑只有一次。
flushSchedulerQueue
接下来我们来看 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()
......
}
// 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)
}
这里首先对队列进行了排序,这么做主要有以下要确保以下几点(代码块中注释的翻译):
- 组件的更新由父到子;因为父组件的创建过程是先于子的,所以
watcher
的创建也是先父后子,执行顺序也应该保持先父后子。 - 用户的自定义
watcher
要优先于渲染watcher
执行;因为用户自定义watcher
是在渲染watcher
之前创建的(initComputed() > initWatch() > render watch
)。 - 如果一个组件在父组件的
watcher
执行期间被销毁,那么它对应的watcher
执行都可以被跳过,所以父组件的watcher
应该先执行。
接着遍历队列,拿到对应的 watcher
,对watcher.before
进行判断,这里就是执行beforeUpdate
钩子函数的地方,然后执行watcher.run()
。这里需要注意一个细节,在遍历的时候每次都会对 queue.length
求值,因为在 watcher.run()
的时候,很可能用户会再次添加新的 watcher
,这样会再次执行到 queueWatcher
,走进上面if (!flushing)
的else
分支中:
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)
}
这里会从后往前找,找到第一个待插入 watcher
的 id
比当前队列中 watcher
的 id
大的位置,把 watcher
按照 id
的插入到队列中,因此 queue
的长度发生了变化。
然后我们来看watcher.run()
:
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
方法很简单,就是执行this.get()
(我们之前存的updateComponent
函数),进行patch
触发界面更新。下面的逻辑主要判断了新旧值是否相等、新值是否为对象、deep
是否为true
,然后传入新旧值作为第一第二参数执行相应的回调函数,对于user watcher
特别添加了try...catch
处理。
最后执行resetSchedulerState()
将控制流程状态的一些变量恢复到初始值,把 watcher
队列清空。
nextTick的实现
我们分析完了flushSchedulerQueue
,再回过头来看nextTick
的实现,我们毕竟是在nextTick
里面调用执行的flushSchedulerQueue
:
let isUsingMicroTask = false
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]()
}
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
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)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
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
})
}
}
懂Event Loop
机制,理解起来就很简单。为了保证浏览器和移动端兼容,vue
不得不做了microtask
向macrotask
的兼容(降级)方案,优先支持哪个就用哪个,所以timerFunc
赋值得看浏览器看设备。将flushSchedulerQueue
再搞个箭头函数压入callbacks
中,对pending
的判断保证timerFunc
只执行一次,timerFunc
无论是使用宏任务还是微任务都会在下一个 tick
执行 flushCallbacks
,flushCallbacks
的逻辑非常简单,对 callbacks
遍历,然后执行相应的回调函数。
这里使用
callbacks
而不是直接在nextTick
中执行回调函数的原因是保证在同一个tick
内多次执行nextTick
,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个tick
依次执行完毕。
上面的话可能有些绕,举个例子分析一下:
methods: {
todo () {
this.a = 1
this.b = 2
}
}
假设我点击一个按钮触发了todo
函数,更新a、b
的值,为a
赋值的时候,触发一次setter
,
调用一次nextTick
,为b
赋值的时候,触发一次setter
,再调用一次nextTick
,假设支持promise
,第一次调用nextTick
后,就将pending
就设为了true
,导致第二次再调用nextTick
时只会往callbacks
中push
,不会再走后面从而产生新的微任务,所以在这一轮宏任务执行完毕时,上面callbacks
中此时应该存放了两个值,但只产生了一个微任务。这也就理解了上面说的避免了开启多个异步任务
。
因为存在异步队列的机制,所以我们此时直接访问dom
就还是之前未更新的,为了确保我们想要访问到的是更新后的dom
,我们一般需要采用this.$nextTick()
的形式(当然你也可以写setTimeout
或者Promise.resolve().then()
,但是this.$nextTick()
就好在做了平台兼容,我不用就是我傻),通过this.$nextTick()
会在之前的callbacks
中再push
一次,等timerFunc
执行起来就走的是同步代码,前面的a、b
两个依赖触发updateComponent
函数后已经重新渲染界面了,我们this.$nextTick()
中的回调最后执行,就保证了拿到的是更新后的dom
。
至此派发更新完毕。
数组的变化侦测
上面已经说了,数组与对象的变化侦测不同是因为Object.defineProperty()
引起的。我们平时都是如下定义数组的:
data(){
return {
arr: [1, 2, 3]
}
}
我们按照之前的逻辑为arr
添加了响应式,但[1, 2, 3]
作为arr
的子属性会递归添加响应式,当再次走到new Observer()
的时候,如下:
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)
}
}
}
走进了Array
的判断中,我们只看protoAugment(value, arrayMethods)
这个分支:
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
很简单就是改变了数组原型的指向,所以再来看arrayMethods
:
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
})
})
arrayProto
保留了原来Array
的原型,arrayMethods
则是以arrayProto
为原型创建出来的对象,接下来列出了Array
原型中可以改变数组自身内容的7
个方法,分别是:push、pop、shift、unshift、splice、sort、reverse
。然后遍历它们,目的就是添加拦截器,采用重写操作数组的方法,以达到在不改变原有功能的前提下,为其新增一些其他功能。因为其他方法并没有操作Array
基本都是遍历查找之类的最后生成新的数组并返回,所以做拦截没有任何意义。
拦截器的第一步就执行了数组原有的方法,首先保证了原有的功能,还用最开始举的例子,那这里面的this
此时是指向arr
的,arr
已经是响应式的了,先用它拿到Observer
的实例ob
,然后针对push、unshift、splice
三个可以新增元素的方法,拿到新增的元素inserted
,调用ob.observeArray
方法,该方法如下:
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
很简单,就是遍历为其添加响应式,因为新加入进来的元素可能是对象类型的。ob.observeArray()
之后再执行ob.dep.notify()
,触发依赖更新。
至此数组的变化侦测分析完毕,此时你应该可以理解我下面这个例子了:
export default {
data() {
return {
arr: [1, 2, 3, { x: 1 }]
}
},
methods: {
todo() {
/* 不要一起测试,因为下面的依赖更新会触发整个组件的重新渲染 */
this.arr[0] = 4 // 单独测试:界面不会变化
this.arr[3].x = 5 // 单独测试:界面会进行响应式变化
this.arr = [1, 2, 3] // 单独测试:界面会进行响应式变化
}
}
}
问题4:很多人难以理解在defineReactive
里面已经有一个Dep
实例了,为什么在Observer
里面最开始还要创建一个Dep
实例?
其实他们的着重点是不同的。Observer
实例的dep
是一个对象有一个,可以从$set
和$delete
方法里看到,这个dep
只有在对象新加属性或者删除属性时才会触发更新,而defineReactive
中的dep
是对象的每个键上都有一个,在这个键被重新赋值之后会触发更新。
补充:观察者模式
相信大家在面试的时候,经常被问到vue的响应式原理
,其实耐住性子读完上面就已经难不住你了。但在这之前,大家的回答可能大致是这样的:采用了观察者模式,所以我这里还是做个简单的补充介绍吧。观察者模式
是一种设计模式。
设计模式(
Design pattern
)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。
使用场景:当对象间存在一对多关系时,则使用观察者模式。
意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
vue
采用了观察者模式,很适合。观察者模式分被观察者与观察者,毫无疑问我们在data
中定义的对象就是被观察者,观察者则是在模板中需要响应的数据。观察者又名发布-订阅者模式,我们以报社为例,报社每天需要发布新的新闻杂志并给到订阅者,那报社怎么知道要将报纸派发给哪些人?所以报社会有注册机构,你先注册成为我们的用户,然后我们才会给你派发报纸。对应到vue
,就指依赖收集
与派发更新
。
结语
文中有不对的地方欢迎指正,大家一起努力呀!!!
参考:
Vue源码系列-Vue中文社区
Vue.js 技术揭秘