端一杯 82 年的加浓美式 ☕️,瞅这
reactive
,咋地?
第一杯美式 ☕️ ,我们了解真个 Vue 的执行过程;
第二杯美式 ☕️,我们深究从 template 到 render 函数的过程;
这第三杯加浓美式 ☕️,我们来聊聊这 reactive
。上栗子 ?:
<div id="app">
<div>{{ message }}</div>
<div v-for="item of relationship" :key="item.name">
<span>name:{{ item.name }}</span>
<span>relation:{{ item.relation }}</span>
</div>
<div>
<button @click="addRelatives">添加亲戚</button>
</div>
<p>
Province: {{ address.children.name }}
</p>
</div>
复制代码
new Vue({
el: '#app',
data () {
return {
// 基础类型
message: 'this is a message',
// array
relationship: [{
name: 'myFatherName',
relation: 'Father and son'
}, {
name: 'myBrotherName',
relation: 'brothers'
}],
// object
address: {
type: 'country',
name: 'China',
children: {
type: 'province',
name: 'GuangDong',
children: {
type: 'city',
name: 'ShenZhen',
}
}
}
};
},
methods: {
addRelatives () {
this.userInfo.relationship.push({
name: 'myMomName',
relation: 'Mom and son'
});
},
}
})
复制代码
栗子 ? 中构造了基础类型、数组、对象的响应式情况。
observe
提几个问题:
observe
是对全部数据做观察吗?- 响应式数据有什么特征?可以让我们快速地判断一个数据是不是响应式的。?
带着问题把代码溜起来,先看到入口:
export function initState (vm: Component) {
// 初始化一个 _wathcers 数组,作用我们后面看
vm._watchers = []
const opts = vm.$options
// 初始化props
if (opts.props) initProps(vm, opts.props)
// 初始化函数
if (opts.methods) initMethods(vm, opts.methods)
// 初始化data
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 初始化computed
if (opts.computed) initComputed(vm, opts.computed)
// 初始化watch
// 这个 nativeWatch 定义在 src/core/util/env.js 中
// nativeWatch = ({}).watch,注释说的是 Firefox 中对象原型有这么一个方法,但是我没有打印出来,囧
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
复制代码
props
、computed
、watch
那些我们放到后面再深究,先从主流程看起。聚焦到 data
部分:
function initData (vm: Component) {
// 获取合并 data 的处理函数,在mergeField时会合并,栗子 ? 中的 parentVal 是 undefined,所以这里的 data 就是我们看到的 data
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// ... 省略一大段报警信息 ?
// 判断key的首字符是不是 $ 或 _
// 不是将属性代理到 vue 实例中
} else if (!isReserved(key)) {
// 代理到实例下面
proxy(vm, `_data`, key)
}
}
// observe data
// 调用 observe 方法观测整个 data 的变化,把 data 变成响应式
observe(data, true /* asRootData */)
}
复制代码
定义数据的首字符不是 $
和 _
作为第一个字符的话,会调用 proxy
代理到 Vue
实例上。这个过程在 [咖聊] Vue 执行过程 有举例说明,忘记的童鞋可以回去看看。最后调用 observe
把 data
数据变成响应式:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 如果有 __ob__ 属性并且是 Observer 的实例,说明已经是个响应式对象,就直接返回
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
// 没有的话实例一个ob对象
} else if (
shouldObserve && // 定义的变量,这里是true,在initProps会改变该值,后面分析 props 的过程再看
!isServerRendering() && // 服务端渲染
(Array.isArray(value) || isPlainObject(value)) && // 数组或纯对象
Object.isExtensible(value) && // 可扩展
!value._isVue // 不是 vue 实例
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
复制代码
observe
先判断数据是不是响应式的,如果是直接返回;不是的话判断几个条件(栗子 ? 中都满足),然后把数据对象当作参数传到 Observer
构造函数,实例化一个 ob
:
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
// 实例化一个 Dep 对象
this.dep = new Dep()
this.vmCount = 0
// 把自身实例添加到数据对象 value 的 __ob__ 属性上
// 定义在 src/core/util/lang.js
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
// 将每个属性转换成 getter/setter,注意函数参数是对象
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>) {
// 遍历数组再次调用 observe
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
复制代码
Observer
构造函数中把参数赋值给 value
私有属性,然后调用 def
函数在对象上挂上 __ob__
属性,最后判断值如果是数组,循环调用 observe
,如果是对象,那依次对每个 key
执行 defineReactive
。回到栗子 ? 中,执行完 def
之后,我们的 value
变成下图所示:
我们的 value
是对象,会执行到 walk
函数,我们先来看下 observe
的重点——defineReactive
:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
/*在闭包内存储一个Dep对象*/
const dep = new Dep()
// 拿到 obj 的属性描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
// 判断,如果是不能配置的属性
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 如果你之前就定义了 getter 和 setter,这里获取定义值
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ...
},
set: function reactiveSetter (newVal) {
// ...
}
})
}
复制代码
第一个重点,获取属性的描述符,如果对象是不可配置的,那么我们就不会设置 getter
和 setter
。这也是为什么我们在写代码的时候使用 Object.freeze
包装常量的原因,再举个栗子 ? 补充说明一下:
回到本文的栗子 ? 中
第一个属性 { message: 'this is a message' }
。直接给 message
挂上 get
和 set
,over
!
第二个属性,数组的情况:
relationship: [{
name: 'myFatherName',
relation: 'Father and son'
}, {
name: 'myBrotherName',
relation: 'brothers'
}],
复制代码
数组会对原型上的部分函数做拦截,然后重新定义方法:
// 后面代码多次访问该原型,所以提前缓存起来,避免多次访问属性。编码小技巧,你get 到了吗?
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 缓存原来的方法
const original = arrayProto[method]
// 重新定义数组相关的API
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
}
// 调用 observe 把插入的项也变成响应式数据
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
复制代码
飘远了,回到 defineReactive
中来,数组值执行到下面这行代码时:
let childOb = !shallow && observe(val)
复制代码
执行到 observe
,然后会进入下面这部分逻辑:
if (Array.isArray(value)) {
// hasProto 是判断是否存在 __proto__ 属性
// 存在直接 value.__proto__ = arrayMethods
// 否则通过 def 函数重新定义数组原型上的方法
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
}
复制代码
然后通过 observeArray
给数组每一项都调用 observe
,栗子 ? 中的数组项是对象,处理过程跟第三个属性一毛一样,所以我们看第三个属性的处理方式:
address: {
type: 'country',
name: 'China',
children: {
type: 'province',
name: 'GuangDong',
children: {
type: 'city',
name: 'ShenZhen',
}
}
}
复制代码
很简单,既然是对象,就回到了一开始 data
的逻辑循环中,defineReactive
-> observe
-> walk
-> defineReactive
,就这样给每个属性都标记上了 __ob__
属性。最终处理完的对象如下图所示:
小结
响应式第一步工作给 data
里的每一个属性都通过 Object.defineProperty
挂上 get
和 set
,小节开头的两个问题也就迎刃而解了。上面 defineReactive
代码逻辑中,将 get
和 set
的逻辑去掉了,接下来我们就来补充这部分的逻辑。
依赖收集
再提几个问题:
- 什么时候会访问到上面这部分数据,触发数据
get
呢? - 依赖收集具体含义是什么?依赖是指什么?收集到哪里?
observe
好了数据之后,程序主流程会进入到挂载阶段——$mount
。这时栗子 ? 就会经过上一节我们分析过的生成 render
函数过程,不了解的童鞋可以前往 [咖聊] “模板编译”真经 了解模板过程。
有了 render
函数,就会进入 mountComponent
:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// ...
} else {
updateComponent = () => {
// vm._render()生成虚拟节点
// 调用 vm._update 更新 DOM
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 实例化 watcher
new Watcher(
vm,
updateComponent,
noop,
{
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
},
true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
// vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
复制代码
逻辑一大段,我们先不管,直接进到 Watcher
类里面看看:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
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
// 第5个参数标志是不是渲染watcher,整个Vue有3种watcher:renderWatcher、computedWatcher、userWatcher,在执行mountComponent时,这里是true的
if (isRenderWatcher) {
// 把渲染 watcher 挂在实例的 _watcher 上
vm._watcher = this
}
// vm._watchers 在前面 initState 时初始化了
vm._watchers.push(this)
// 这里的 options 是上面说到的 3 种 watcher 对应的一些属性
// 对于渲染watcher过程,这里有 before 函数,其他都是false
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.computed = !!options.computed
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.computed = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers
// 跟dep相关的一些属性
// 表示 Watcher 实例持有的 Dep 实例的数组
this.deps = []
this.newDeps = []
// 代表 this.deps 和 this.newDeps 的 id Set
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') {
// 这里的 getter 是 updateComponent 函数
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
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
)
}
}
// computed watcher并不会立刻求值
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
// 执行watch的get函数
this.value = this.get()
}
}
// ...
}
复制代码
Watcher
类定义很长,我们先只看构造函数,就是给实例上挂各种属性。作用后面遇到了再看。这里最终会执行到:
this.value = this.get();
复制代码
看下 get
函数定义:
get () {
// 把当前的 watcher 对象 push 到 targetStack 中
// 然后赋值给 Dep.target,这个作用后面会讲到
pushTarget(this)
let value
const vm = this.vm
try {
// this.getter 对应就是 updateComponent 函数
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
// deep watcher
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
复制代码
pushTarget
和 popTarget
的定义在 src\core\observer\dep.js
:
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []
export function pushTarget (_target: ?Watcher) {
// Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复用)
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
复制代码
get
函数对应的 value = this.getter.call(vm, vm)
最终会执行到 updateComponent
,回到 mountComponent
中查看这部分逻辑:
updateComponent = () => {
// vm._render()生成虚拟节点
// 调用 vm._update 更新 DOM
vm._update(vm._render(), hydrating)
}
复制代码
vm._render()
会执行 render
函数,生成 VNode
:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// ...
let vnode
try {
// 写 render 函数的第一个参数 h,就是 vm.$createElement
// vm._renderProxy 就是 vm,在 init 阶段定义了
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...
}
// ...
// set parent
vnode.parent = _parentVnode
return vnode
}
复制代码
栗子 ? 生成的 render
函数:
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('div', [_v(_s(message))]), _v(" "), _l((relationship), function(item) {
return _c('div', {
key: item.name
}, [_c('span', [_v("name:" + _s(item.name))]), _v(" "), _c('span', [_v("relation:" + _s(item.relation))])])
}), _v(" "), _c('div', [_c('button', {
on: {
"click": addRelatives
}
}, [_v("添加亲戚")])]), _v(" "), _c('p', [_v("\n Province: " + _s(address.children.name) + "\n ")])], 2)
}
}
复制代码
生成的 render 函数有一堆小矮人,_v
、_l
、_s
,这些小矮人定义在 src\core\instance\render-helpers\index.js
:
import { toNumber, toString, looseEqual, looseIndexOf } from 'shared/util'
import { createTextVNode, createEmptyVNode } from 'core/vdom/vnode'
import { renderList } from './render-list'
import { renderSlot } from './render-slot'
import { resolveFilter } from './resolve-filter'
import { checkKeyCodes } from './check-keycodes'
import { bindObjectProps } from './bind-object-props'
import { renderStatic, markOnce } from './render-static'
import { bindObjectListeners } from './bind-object-listeners'
import { resolveScopedSlots } from './resolve-slots'
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}
复制代码
这部分函数都是封装的 VNode
工具函数。具体函数的执行这里不讨论,放在后面 VNode
的生成章节。但是我们可以明确的是,在生成 VNode
的过程中,一定是会访问到我们的数据(this.message
、this.relationship
等等),这时就会触发数据 getter
,此时就把上一小节省掉的 get
函数拉回来:
get: function reactiveGetter () {
// 当前值,比如 message 就是 this is a message
const value = getter ? getter.call(obj) : val
// 还记得大明河畔的夏雨荷吗?
// 在执行 Watcher 的 get 时,执行了 pushTarget,当时给 Dep.target 赋值了渲染 Watcher
if (Dep.target) {
// 依赖收集
dep.depend()
// 如果有子ob,就一起收集了
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
复制代码
万水千山,终于到本节的重点了。在 observe
过程中,实例化了一个依赖实例:
var dep = new Dep();
复制代码
调用 dep.depend
收集依赖,这会我们看到上一小节忽略掉的 Dep
类:
export default class Dep {
// 静态属性 target,这是一个在任何时间都是全局唯一 Watcher
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 指渲染 watcher
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
复制代码
Dep.target.addDep(this)
回到 Watcher
中执行 addDep
逻辑:
addDep (dep: Dep) {
const id = dep.id
// 防止重复添加,去重
if (!this.newDepIds.has(id)) {
// 把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中
// 这个目的是为后续数据变化时候能通知到哪些 subs 做准备
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 把当前渲染 watcher 添加到依赖实例的 subs 数组中
dep.addSub(this)
}
}
}
复制代码
然后再把 渲染watcher
加到 dep
的 subs
数组中,作为订阅项收集。
render
函数执行完了之后,还有一小部分逻辑:
// 处理 deep 的 user watcher
if (this.deep) {
traverse(value)
}
// 恢复成上一个状态,当前vm的依赖收集完了,对应的渲染也要更新
// 在栗子 ? 中把 Dep.target 变成 undefined
popTarget()
this.cleanupDeps()
复制代码
最后这个 cleanupDeps
做了哪些动作呢?来??
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
// 移除对Watcher的订阅
dep.removeSub(this)
}
}
// 交换 newDepIds 和 depIds
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
}
复制代码
因为每次修改数据都会重新触发 render
函数的执行,也就是会重新做依赖收集的动作。因此设计新老 depIds
是为了做最小化的更新。
小结
依赖收集会发生在 render
函数生成 VNode
的过程中。因为会访问到具体的数据,从而触发数据的 get
,然后当前的渲染 watcher
会把数据当作依赖添加到实例相关属性上,dep
也会把渲染 watcher
存到订阅者(subs
)数组中。当我们修改数据时,就会用收集的结果去触发渲染 watcher
更新,下面我们就来看下派发更新的详细过程。
派发更新
栗子 ? 中点击“添加亲戚”按钮,就会 push
一项数据到 relationship
中。我们来看一下这个过程。前面我们讲过,就是数组的操作会重写原型方法来附加派发更新的逻辑。当我们执行代码:
this.relationship.push({
name: 'myMomName',
relation: 'Mom and son'
});
复制代码
就会触发拦截之后的数组方法:
// 重新定义数组相关的API
def(arrayMethods, method, function mutator (...args) {
// 先按照原来定义执行一遍
const result = original.apply(this, args)
// 获取当前实例 this.relationship 的 ob 对象
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 调用 observe 把插入的项也变成响应式数据
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
复制代码
栗子 ? 中的是 push
一个数组项,拿到当前属性的 __ob__
对象,对插入的项做响应式处理(ob.observeArray
)。最终触发 ob.dep.notify()
:
notify () {
const subs = this.subs.slice()
// 依次触发渲染 watcher 的 update
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
复制代码
上面的 update
会触发位于 src\core\observer\watcher.js
的 update
方法:
update () {
// ...
// 这里省略掉 computed watcher 和同步 user watcher 的逻辑,直接看栗子 ?会执行到的 queueWatcher
queueWatcher(this)
}
复制代码
queueWatcher
在 src\core\observer\scheduler.js
中定义:
const queue: Array<Watcher> = []
let waiting = false
let flushing = false
let index = 0
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 确保一个 watcher 只添加一次
if (has[id] == null) {
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.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
// 保证只执行一次nextTick
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
复制代码
queueWatcher
可以看到是把渲染 watcher
加入到队列中,并不是每次更新都实时执行。然后执行到 nextTick(flushSchedulerQueue)
:
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 把传入的函数压入callbacks数组,需要callbacks而不是直接执行cb是因为多次执行nextTick时,能用同步顺序在下一个tick中执行,而不需要开启多个宏/微任务
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 对于手动触发的一些数据更新,比如栗子 ? 中的 click 事件,强行走宏任务
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// $flow-disable-line
// 当nextTick不传函数时,提供一个promise化的调用
// 不传cb直接返回一个promise的调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
复制代码
看 nextTick
之前,我们先看宏任务 macroTimerFunc
和微任务 microTimerFunc
的实现:
// 宏任务的实现:先看是否支持 setImmediate,然后判断是否支持 MessageChannel,最终 setTimeout 兜底
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
// 微任务实现:先判断是否支持Promise,否则直接指向 macroTimerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
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)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
复制代码
回到 nextTick
, 把回调函数存到 callbacks
,通过 macroTimerFunc
触发 flushCallbacks
,然后依次调用 callbacks
中的函数——flushSchedulerQueue
:
function flushSchedulerQueue () {
flushing = true
let watcher, id
/**
* 1.组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。
* 2.用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。
* 3.如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。
*/
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
// 这里不缓存队列长度的原因是在 watcher.run() 的时候,很可能用户会再次添加新的 watcher
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
// 执行 before 函数,也就是 beforeUpdated 钩子
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
// 执行一些钩子函数,比如keep-alive的actived和组件的updated钩子
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
复制代码
先对 watcher
做了排序,然后依次执行组件的 beforeUpated
的钩子函数。依次调用 wathcer.run
触发更新:
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}
getAndInvoke (cb: Function) {
// 对于渲染 watcher 而言,调用 get 会重新触发 updateComponent,就又会重新执行依赖收集,不一样的是更新时是有老的VNode,会做对比再重新patch
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
this.dirty = false
if (this.user) {
try {
// 这就是为什么我们写自定义watcher时能拿到新值和旧值
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
复制代码
然后就会执行 getAndInvoke
函数,其实也就是执行 this.get()
获取当前的值,如果满足新老值不相等、新值是对象、deep
模式下的任一条件,就执行回调 cb
。回调中的参数是新老值,这也是为什么我们写自定义 watcher
有新老值参数的原因。对于栗子 ? 而言,这里 this.get
重新触发 updateComponent
。
小结
在数据更新时,会触发订阅数组中 watcher
的 update
,但并不是每次改变都会实时更新,而是通过队列维护更新列表。最后经过队列排序、队列遍历分别触发 watcher
的 run
,从而触发 VNode
的更新,页面的刷新。flushSchedulerQueue
最后还做了数据重置的工作。
总结
最后再看到官方文档响应式原理图:
根据上图做一个总结:在 render
阶段,会读取到响应式数据,触发数据的 getter
,然后会通过 Dep
做 render watcher
的收集。当修改响应式数据时,会触发数据的 setter
,从而触发 Dep
的 notify
。让所有的 render watcher
异步地更新,从而重新渲染页面!我们知道,重新渲染页面会做 VNode
的 diff
,然后高效地渲染页面。这部分内容我们留在下一小节分析。