共计 10465 个字符,预计需要花费 27 分钟才能阅读完成。
要理解 Vue2 响应式零碎原理,咱们要思考两个问题:
- 当咱们扭转组件的状态时,零碎会产生什么变动?
- 零碎是如何晓得哪些局部依赖于这个状态?
实际上,组件的渲染、计算属性、组件 watch
对象和 Vue.&watch()
办法,它们之所以能响应组件 props
和data
的变动,都是围绕着 Watcher
类来实现的。
本文只截取局部外围代码,重在解说响应式原理,尽量减少其它代码的烦扰,但会正文代码起源,联合源码观看风味更佳。另外,本文源码版本:
"version": "2.7.14",
定义响应式属性
首先,看看组件的 props
和data
中的属性是如何定义为响应式的:
// src/core/instance/init.ts
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
initState(vm) // 初始化状态
}
// src/core/instance/state.ts
export function initState(vm: Component) {
const opts = vm.$options
initProps(vm, opts.props) // 初始化 Props
initData(vm) // 初始化 Data
initComputed(vm, opts.computed)
initWatch(vm, opts.watch)
}
function initProps(vm: Component, propsOptions: Object) {const props = (vm._props = shallowReactive({}))
for (const key in propsOptions) {defineReactive(props, key, value) // 定义响应式属性
}
}
function initData(vm: Component) {
let data: any = vm.$options.data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
observe(data)
}
// src/core/observer/index.ts
export function observe(value: any, shallow?: boolean, ssrMockReactivity?: boolean) {return new Observer(value, shallow, ssrMockReactivity)
}
export class Observer {constructor(public value: any, public shallow = false, public mock = false) {const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {const key = keys[i]
// 定义响应式属性
defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
}
}
}
从下面代码能够看出,在组件初始化阶段,无论是 props
还是 data
属性,最终都通过函数 defineReactive
定义为响应式属性。所以咱们要重点关注这个办法:
// src/core/observer/index.ts
export function defineReactive(obj: object, key: string, val?: any, customSetter?: Function | null, shallow?: boolean, mock?: boolean) {const dep = new Dep() // 创立一个 dep 实例
const property = Object.getOwnPropertyDescriptor(obj, key)
const getter = property && property.get
const setter = property && property.set
Object.defineProperty(obj, key, {get: function reactiveGetter() {const value = getter ? getter.call(obj) : val
dep.depend() // 增加依赖关系 Watcher
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {setter.call(obj, newVal)
dep.notify() // 赋值时,公布告诉}
}
}
Object.defineProperty
从新定义了属性的 get
和set
。当读取属性时,会主动触发 get
,当设置属性值时,会主动触发set
,记住这一机制。从下面代码能够发现,每个属性都有一个dep
实例,它的作用就是记录依赖这个属性 watcher
列表,并在属性赋值时,告诉列表中的 watcher
更新,这些更新包含:扭转计算属性值、执行组件 watch 对象中定义的办法、从新渲染等。
收集依赖关系
在进一步理解 dep.depend()
是之前,先看一下 Vue.$watch
如何办法创立watcher
,有利于前面的了解:
Vue.prototype.$watch = function (expOrFn: string | (() => any), // 重点关注这个参数
cb: any,
options?: Record<string, any>
) {
const vm: Component = this
const watcher = new Watcher(vm, expOrFn, cb, options) // 创立 watcher
}
expOrFn
类型是一个字符串或函数,如果是字符串,会转化成函数,赋值给 watcher.getter
。接下来看dep.depend()
是如何收集依赖的,重点关注 Dep
和Watcher
两个类:
// src/core/observer/dep.ts
export default class Dep {
static target?: DepTarget | null // Watcher 正是 DepTarget 类的实现
subs: Array<DepTarget | null> // 依赖列表
addSub(sub: DepTarget) {this.subs.push(sub)
}
depend(info?: DebuggerEventExtraInfo) {if (Dep.target) {Dep.target.addDep(this) // 向 watcher 中增加 dep 实例
}
}
}
const targetStack: Array<DepTarget | null | undefined> = []
// 入栈 watcher,并将 target 指向这个 watcher
export function pushTarget(target?: DepTarget | null) {targetStack.push(target)
Dep.target = target
}
// 出栈 watcher,并将 target 指向最初的 watcher
export function popTarget() {targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
// src/core/observer/watcher.ts
export default class Watcher implements DepTarget {
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
this.cb = cb // 回调函数
if (isFunction(expOrFn)) {this.getter = expOrFn} else {this.getter = parsePath(expOrFn) // 转化为函数
}
this.value = this.get() // 获取值}
// 获取值,并收集依赖关系
get() {pushTarget(this) // 入栈,Dep.target 指向以后 watcher
let value
const vm = this.vm
value = this.getter.call(vm, vm) // 执行 getter 期间只有读取了响应式属性,会触发属性的 get,而后调用 dep.depend(),再调用 Dep.target(以后 watcher)的 addDep 办法,将 watcher 增加到 dep.subs
popTarget() // 出栈
return value
}
addDep(dep: Dep) {dep.addSub(this) // 将 watcher 增加到 dep.subs
}
}
执行 getter
期间只有读取了响应式属性,会触发改属性重写的 get
,而后调用dep.depend()
,再调用Dep.target
(以后watcher
)的addDep
办法,将 watcher
增加到 dep.subs
。于是,属性的dep
就晓得了哪些 watcher
用到了这个属性,它们都保留在了 dep.subs
列表中。
赋值响应式属性
接着,看扭转 props 或 state 后,会产生什么状况:
- 扭转响应式属性值
- 触发重写的
set
,调用dep.notify()
dep.notify()
告诉dep.subs
所有的watcher.update()
watcher.update()
中将watcher
本人退出更新队列nextTick
后执行更新,调用队列中所有watcher.run()
watcher.run()
中调用watcher.get()
取得新值,并从新收集依赖- 调用回调函数
watcher.cb
,传入新旧值
// 1. 扭转响应式属性值 examples/composition/todomvc.html
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="state.allDone"/>
// 2. 触发重写的 set,调用 dep.notify() src/core/observer/index.ts
export function defineReactive() {const dep = new Dep()
Object.defineProperty(obj, key, {set: function reactiveSetter(newVal) {dep.notify()
}
}
}
// 3. dep.notify()告诉 dep.subs 所有的 watcher.update() src/core/observer/dep.ts
notify(info?: DebuggerEventExtraInfo) {const subs = this.subs.filter(s => s) as DepTarget[]
for (let i = 0, l = subs.length; i < l; i++) {const sub = subs[i]
sub.update()}
}
// 4. watcher.update()中将 watcher 本人退出队列 src/core/observer/watcher.ts
update() {queueWatcher(this)
}
// 5. nextTick 后执行更新,调用队列中所有 watcher.run() src/core/observer/seheduler.ts
const queue: Array<Watcher> = []
export function queueWatcher(watcher: Watcher) {queue.push(watcher)
nextTick(flushSchedulerQueue)
}
function flushSchedulerQueue() {for (index = 0; index < queue.length; index++) {watcher = queue[index]
watcher.run()}
}
// 6. watcher.run()中调用 watcher.get()取得新值,并从新收集依赖 src/core/observer/watcher.ts
run() {const value = this.get()
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue) // 7. 调用回调函数 watcher.cb,传入新旧值
}
渲染函数响应式
渲染函数 _render
用于生成虚构 DOM,也就是 VNode
。当组件的props
或data
发生变化时,会触发 _render
从新渲染组件:
// src/types/component.ts
class Component {_render: () => VNode
}
触发重绘机制也是通过 watcher
来实现的,不过这个 watcher
会比拟非凡,它没有回调函数,创立于组件 mount
阶段:
// src/platforms/web/runtime/index.ts
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// src/core/instance/lifecycle.ts
export function mountComponent(vm: Component, el: Element | null | undefined, hydrating?: boolean) {updateComponent = () => {vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, watcherOptions, true /* isRenderWatcher */)
}
updateComponent
作为第二参数,也就成为了 watcher.getter
。和一般的watcher
一样,getter
执行时,也就是 updateComponent
执行期间,或者说 _update
和_render
执行期间,读取响应式属性时,会触发它们的 get
,将渲染watcher
增加到属性对应的 dep.subs
中。当响应式属性发生变化时,触发从新渲染,这个流程与之前略有不同:
- 扭转响应式属性值
- 触发重写的
set
,调用dep.notify()
dep.notify()
告诉dep.subs
所有的watcher.update()
watcher.update()
中将watcher
本人退出更新队列nextTick
后执行更新,调用队列中所有watcher.run()
watcher.run()
中调用watcher.get()
取得新值,并从新收集依赖watcher.get()
中会调用wacher.getter.call()
- 等于调用
updateComponent
,从新渲染组件(渲染watcher
回调函数等于noop
,相当于不执行回调)
以官网例子来看以上流程:
// 1. 扭转响应式属性值 examples/composition/todomvc.html
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="state.allDone"/>
// 2. 触发重写的 set,调用 dep.notify() src/core/observer/index.ts
export function defineReactive() {const dep = new Dep()
Object.defineProperty(obj, key, {set: function reactiveSetter(newVal) {dep.notify()
}
}
}
// 3. dep.notify()告诉 dep.subs 所有的 watcher.update() src/core/observer/dep.ts
notify(info?: DebuggerEventExtraInfo) {const subs = this.subs.filter(s => s) as DepTarget[]
for (let i = 0, l = subs.length; i < l; i++) {const sub = subs[i]
sub.update()}
}
// 4. watcher.update()中将 watcher 本人退出队列 src/core/observer/watcher.ts
update() {queueWatcher(this)
}
// 5. nextTick 后执行更新,调用队列中所有 watcher.run() src/core/observer/seheduler.ts
const queue: Array<Watcher> = []
export function queueWatcher(watcher: Watcher) {queue.push(watcher)
nextTick(flushSchedulerQueue)
}
function flushSchedulerQueue() {for (index = 0; index < queue.length; index++) {watcher = queue[index]
watcher.run()}
}
// 6. watcher.run()中调用 watcher.get()取得新值,并从新收集依赖 src/core/observer/watcher.ts
run() {const value = this.get()
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
// 7. watcher.get()中会调用 wacher.getter.call() src/core/observer/watcher.ts
get() {pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm) // 等于 updateComponent()
popTarget()
return value
}
// 8. 等于调用 updateComponent,从新渲染组件(渲染 watcher 回调函数等于 noop,相当于不执行任何回调)src/core/instance/lifecycle.ts
export function mountComponent(vm: Component, el: Element | null | undefined, hydrating?: boolean) {updateComponent = () => {vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, watcherOptions, true /* isRenderWatcher */)
}
计算属性响应式
计算属性同样是通过 watcher
实现的。在实例初始化阶段 initState
时,调用 initComputed
为每个计算属性创立一个watcher
,它同样没有回调函数:
// src/core/instance/state.ts
export function initState(vm: Component) {
const opts = vm.$options
if (opts.computed) initComputed(vm, opts.computed)
}
const computedWatcherOptions = {lazy: true}
function initComputed(vm: Component, computed: Object) {const watchers = (vm._computedWatchers = Object.create(null))
for (const key in computed) {const userDef = computed[key]
const getter = isFunction(userDef) ? userDef : userDef.get
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
if (!(key in vm)) {defineComputed(vm, key, userDef)
}
}
}
export function defineComputed(target: any, key: string, userDef: Record<string, any> | (() => any)) {sharedPropertyDefinition.get = createComputedGetter(key) // 重写属性的 get
sharedPropertyDefinition.set = noop // 不容许更改属性值
Object.defineProperty(target, key, sharedPropertyDefinition) // 从新定义计算属性的 set 和 get
}
function createComputedGetter(key) {return function computedGetter() {const watcher = this._computedWatchers && this._computedWatchers[key]
return watcher.value // 返回 watcher.value 值
}
}
以上代码能够看出,defineComputed
从新定义了计算属性的 set
和get
,get
永远返回对应watcher.value
。计算属性的值是用户定义的函数,它也是watcher.getter
,原理同上。函数中的响应式属性发生变化时:
- 扭转响应式属性值
- 触发重写的
set
,调用dep.notify()
dep.notify()
告诉dep.subs
所有的watcher.update()
watcher.update()
中将watcher
本人退出更新队列nextTick
后执行更新,调用队列中所有watcher.run()
watcher.run()
中调用watcher.get()
取得新值,并从新收集依赖- 读取计算属性时,触发重写的
get
办法,返回watcher.value
值
组件的 watch 对象
它通过 Vue.$watch
来实现的,看代码即可,原理同上。
// src/core/instance/state.ts
function initWatch(vm: Component, watch: Object) {for (const key in watch) {const handler = watch[key]
createWatcher(vm, key, handler)
}
}
function createWatcher(
vm: Component,
expOrFn: string | (() => any),
handler: any,
options?: Object
) {return vm.$watch(expOrFn, handler, options)
}
异步更新和 Watcher 执行程序
nextTick
中的函数是异步执行的,也就是说随响应式属性变动的 watcher
会顺次退出更新队列中,直到这部分同步代码全副执行结束,之后才会执行异步代码,按顺序调用队列中watch.run
,执行回调函数和从新渲染组件。
watcher.run
执行是考究程序的,为了满足执行程序,必须在 watcher.run
之前从新按 watcher.id
大小排序,因为 watcher.id
是自增的,所以后创立的 wacher.id
要大于先创立的。排序能满足以下要求:
- 组件更新必须从父组件到子组件。(父组件永远先于子组件创立,因而父组件
watcher.id
小于子组件) - 用户
wachers
必须在渲染watcher
之前执行。(用户props
、data
和computed
的wacher
创立于组件初始化阶段,watcher.id
肯定小于mount
阶段创立的渲染watcher
)
function flushSchedulerQueue() {
// 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(sortCompareFn)
for (index = 0; index < queue.length; index++) {watcher = queue[index]
watcher.run()}
}
const sortCompareFn = (a: Watcher, b: Watcher): number => {if (a.post) {if (!b.post) return 1
} else if (b.post) {return -1}
return a.id - b.id
}