Vue.Js的核心包括一套 “响应式系统”。“响应式”,是指当数据改变后,Vue会通知到使用该数据的代码。例如,视图渲染中使用了数据,数据改变后,视图也会自动更新。
vue2.X的数据响应式是利用 Object.defineProperty()
实现的,通过定义对象属性 getter/setter
拦截对属性的获取和设置。
具体如何实现的呢?首先需要考虑一下从哪里入手去看源码。
在上一篇文章 Vue源码学习 - new Vue初始化都做了什么?说到了一个方法叫:initState()
,当时给它的解释是:对props,methods,data,computed,watch进行初始化,包括响应式处理。
// src/core/instance/init.ts
export function initMixin(Vue: typeof Component) {
// 在原型上添加 _init 方法
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
//...
vm._self = vm
initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
initEvents(vm) // 初始化事件:$on, $off, $emit, $once
initRender(vm) // 初始化渲染: render, mixin
callHook(vm, 'beforeCreate', undefined, false) // // 调用生命周期的钩子函数,在这里就能看出一个组件在创建之前和之后分别做了哪些初始化
initInjections(vm) // 初始化 inject
initState(vm) // 对props,methods,data,computed,watch进行初始化,包括响应式的处理
initProvide(vm) // 初始化 provide
callHook(vm, 'created') // created 初始化完成,可以执行挂载了
//...
}
}
初始化这里调用了很多方法,每个方法都做着不同的事,而关于响应式主要就是组件内的数据 props、data
。这一块的内容就是在 initState()
这个方法里,所以进入这个方法源码看一下。
// src/core/instance/state.ts
// 数据响应式的入口
export function initState(vm: Component) {
const opts = vm.$options
// 初始化 props
if (opts.props) initProps(vm, opts.props)
// 初始化 methods
if (opts.methods) initMethods(vm, opts.methods)
// 初始化 data
if (opts.data) {
initData(vm)
} else {
// 没有 data 的话就默认赋值为空对象,并监听
const ob = observe((vm._data = {}))
ob && ob.vmCount++
}
// 初始化 computed
if (opts.computed) initComputed(vm, opts.computed)
// 初始化 watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
这也是一堆初始化的东西,我们还是直奔主题,取响应式数据相关的,也就是 initProps()
、initData()
、observe()
,然后挨个看里面的源码。
这里主要做的事情:
props
列表。defineReactive
设置成响应式。proxy()
把属性代理到当前实例上,如把 vm._props.xx
变成 vm.xx
,就可以访问。// src/core/instance/state.ts
function initProps(vm: Component, propsOptions: Object) {
// 父组件传入子组件的 props
const propsData = vm.$options.propsData || {}
// 经过转换后最终的 props
const props = (vm._props = shallowReactive({}))
// 存放 props 的数组
const keys: string[] = (vm.$options._propKeys = [])
const isRoot = !vm.$parent
// 转换非根实例的 props
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
// 校验 props 类型、default 属性等
const value = validateProp(key, propsOptions, propsData, vm)
// 非生产环境下
if (__DEV__) {
const hyphenatedKey = hyphenate(key)
if (
isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)
) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
// 把 props 设置成响应式的
defineReactive(props, key, value, () => {
// 如果用户修改子组件中的 props 发出警告
if (!isRoot && !isUpdatingChildComponent) {
warn( `xxx警告`,vm)
}
})
} else {
// 把 props 设置成响应式的
defineReactive(props, key, value)
}
// 把不在默认 vm 上的属性,代理到实例上
// 可以让 vm._props.xx 通过 vm.xx 访问
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
- List item
这里主要做的事情:
this.xx
访问了。observe
监听整个 data。// src/core/instance/state.ts
function initData(vm: Component) {
// 获取当前实例的 data
let data: any = vm.$options.data
// 判断 data 类型
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
if (!isPlainObject(data)) {
data = {}
__DEV__ && warn('数据函数应该返回一个对象',vm)
}
// 获取当前实例的 data 属性名集合
const keys = Object.keys(data)
// 获取当前实例的 props
const props = vm.$options.props
// 获取当前实例的 methods 对象
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
// 非生产环境下判断 methods 里的方法是否存在于 props 中
if (__DEV__) {
if (methods && hasOwn(methods, key)) {
warn(`Method方法不能重复声明`, vm)
}
}
// 非生产环境下判断 data 里的属性是否存在于 props 中
if (props && hasOwn(props, key)) {
__DEV__ &&
warn(`属性不能重复声明`,vm)
} else if (!isReserved(key)) {
// 都不重名的情况下,代理到 vm 上
// 可以让 vm._data.xx 通过 vm.xx 访问
proxy(vm, `_data`, key)
}
}
// observe 监听 data
const ob = observe(data)
ob && ob.vmCount++
}
这个方法主要就是用来给数据加上监听器的。
严格的说,observe() 方法应该算是Observer的守护,为Observer即将开启前做的一些合规检测。
这里主要做的事情:
// src/core/observer/index.ts
export function observe(
value: any,
shallow?: boolean,
ssrMockReactivity?: boolean
): Observer | void {
// 首先'__ob__'的值其实就是一个'Observer'实例
// 所以下面的判断其实就是:如果已经做过了响应式处理(已经被观察过了),则直接返回'ob',也就是'Observer'实例
if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
return value.__ob__
}
// 如果是初始化的时候,则没有'Observer'的实例,因此需要创建一个'Observer'实例
if (
shouldObserve &&
(ssrMockReactivity || !isServerRendering()) &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value.__v_skip /* ReactiveFlags.SKIP */ &&
!isRef(value) &&
!(value instanceof VNode)
) {
return new Observer(value, shallow, ssrMockReactivity)
}
}
这是一个类,作用是把一个正常的数据成可观测的数据。
这里主要做的事情:
数组
,遍历数组,调用 observe()
对每一个元素进行监听。对象
,遍历对象,调用 defineReactive()
创建响应式对象。// src/core/observer/index.ts
export class Observer {
dep: Dep
vmCount: number
constructor(public value: any, public shallow = false, public mock = false) {
// 实例化一个dep
// 正常来说,遍历一个对象的属性时,都是一个属性创建一个dep,为什么此处要给当前对象额外创建一个dep?
// 其目的在于如果使用Vue.set/delete添加或删除属性,这个dep负责通知更新。
this.dep = mock ? mockDep : new Dep()
this.vmCount = 0
// 给 value 添加 __ob__ 属性,值为value的 Observe 实例
// 表示已经变成响应式了,目的是对象遍历时就直接跳过,避免重复操作
def(value, '__ob__', this)
// 类型判断
if (isArray(value)) {
if (!mock) {
// 判断数组是否有__proto__
if (hasProto) {
// 如果有就重写数组的方法
;(value as any).__proto__ = arrayMethods
} else {
// 没有就通过 def,也就是Object.defineProperty 去定义属性值
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(value, key, arrayMethods[key])
}
}
}
if (!shallow) {
this.observeArray(value)
}
} else {
// 对象响应式处理方法
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)
}
}
}
observeArray(value: any[]) {
// 遍历数组,为数组的每一项设置观察,处理数组元素为对象的情况
for (let i = 0, l = value.length; i < l; i++) {
observe(value[i], false, this.mock)
}
}
作用是定义响应式对象。
这里主要做的事情:
dep 实例
。对象
就调用 observe()
,递归监听,保证不管结构嵌套多深,里面的属性都能变成响应式对象。Object.defineProperty()
劫持对象的 getter 和 setter。dep.depend()
把 watcher(观察者)
push 到依赖的数组 subs
里面。observe()
递归监听。dep.notify()
派发更新。// src/core/observer/index.ts
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean
) {
// 给每个响应式数据的 属性 都对应着一个 Dep 实例(重要)
const dep = new Dep()
// 拿到对象的属性描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 获取自定义的 getter 和 setter
const getter = property && property.get
const setter = property && property.set
if (
(!getter || setter) &&
(val === NO_INITIAL_VALUE || arguments.length === 2)
) {
val = obj[key]
}
// 如果 val 是对象的话就递归监听
// 递归调用 observe 就可以保证不管对象结构嵌套有多深,都能变成响应式对象
let childOb = !shallow && observe(val, false, mock)
// 截持对象属性的 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 拦截 getter,当取值时会触发该函数
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// 进行依赖收集
// 初始化渲染 watcher 时访问到需要双向绑定的对象,从而触发 get 函数
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
if (childOb) {
childOb.dep.depend()
if (isArray(value)) {
dependArray(value)
}
}
}
return isRef(value) && !shallow ? value.value : value
},
// 拦截 setter,当值改变时会触发该函数
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
// 判断新值是否发生变化
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
val = newVal
}
// 如果新值是对象的话递归监听,也做响应式处理
childOb = !shallow && observe(newVal, false, mock)
if (__DEV__) {
// 派发更新
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
// 派发更新
dep.notify()
}
}
})
return dep
}
上面源码中介绍了通过 dep.depend
来做依赖收集,再通过 dep.notify()
来派发更新。
可以说 Dep 在 数据响应式 中扮演的角色就是 数据的依赖收集
和 变更通知
。
依赖收集的核心是 Dep
,而且它与 Watcher
也是密不可分的。下面通过 例子 和 源码 讲解就会有体会了。
请看下面代码:
// 例子代码,与本章代码无关
<div>{{ name }}</div>
data() {
return {
name: '铁锤妹妹'
}
},
computed: {
info () {
return this.name
}
},
watch: {
name(newVal) {
console.log(newVal)
}
}
上方代码可知,name
变量被三处地方所依赖,分别是 html里
、computed里
、watch里
。只要 name
属性值一改变,html里就会重新渲染,computed里就会重新计算,watch里就会重新执行。那么是谁去通知这三个地方 name
修改了呢?那就是 Watcher
了。
上面所说的三处地方就刚刚好代表了三种 Watcher
,分别是:
Dep是什么呢?还是之前的例子代码:
<div>{{ name }}</div>
data() {
return {
name: '铁锤妹妹'
}
},
computed: {
info () {
return this.name
}
},
watch: {
name(newVal) {
console.log(newVal)
}
}
这里name
变量被三个地方所依赖,三个地方代表了三种Watcher
,那么name
会直接自己管这三个Watcher
吗?答案是不会的,name
会实例一个Dep,来帮自己管这几个Wacther
,类似于管家,当name
更改的时候,会通知dep,而dep则会带着主人的命令去通知这些 Wacther
去完成自己该做的事。
了解了上面的例子,再去看下面 Dep 和 Watcher 的源码 。
这是一个类,实际上就是对 Watcher
的管理。
首先初始化一个 subs
数组,用来存放依赖,也就是观察者;谁依赖这个数据,谁就在这个数组里;然后定义几个方法来对依赖添加、删除、通知更新等。
另外 Dep 还有一个静态属性 target
,这是一个全局的 Watcher
,也表示同一时间只能存在一个全局的 Watcher
。
- 因为涉及到dep部分,这个看看就好,先大致了解下流程,下面会具体讲,跟这有联系。
- 每个被监听的属性都会有一个对应的
Dep
实例。在属性的 getter 中,会将当前正在执行的 Watcher 添加到 Dep 的依赖列表中。当属性发生变化时,会通过 Dep 的notify
方法通知所有依赖的 Watcher 进行更新。- 在更新过程中,首先会触发属性的 setter 方法,然后该属性对应的 Dep 实例会通知所有依赖的 Watcher 对象进行更新。这些 Watcher 对象会被添加到
调度器队列
中,具体是通过调用 Watcher 的update
方法将 Watcher 添加到调度器队列
中。最终,Vue 在适当的时机会执行调度器的更新操作,从 调度器队列 中依次取出 Watcher 并执行其更新逻辑。
// src/core/observer/dep.ts
// Dep在数据响应式中扮演的角色就是数据的 依赖收集 和 变更通知
// 在获取数据的时候知道自己(Dep)依赖的watcher都有谁,同时在数据变更的时候通知自己(Dep)依赖的这些watcher去执行他们(watcher)的update
export default class Dep {
static target?: DepTarget | null
id: number
subs: Array<DepTarget | null>
_pending = false
constructor() {
this.id = uid++
// 用来存储 Watcher 的数组
this.subs = []
}
// 在 dep 中添加 观察者
addSub(sub: DepTarget) {
this.subs.push(sub)
}
// 移除观察者
removeSub(sub: DepTarget) {
this.subs[this.subs.indexOf(sub)] = null
if (!this._pending) {
this._pending = true
pendingCleanupDeps.push(this)
}
}
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
// 调用 watcher 的 addDep 函数
Dep.target.addDep(this)
if (__DEV__ && info && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
...info
})
}
}
}
// 遍历 dep 中所有的 Watcher,通知相关的 Watcher 执行 update 派发更新
notify(info?: DebuggerEventExtraInfo) {
const subs = this.subs.filter(s => s) as DepTarget[]
if (__DEV__ && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
if (__DEV__ && info) {
sub.onTrigger &&
sub.onTrigger({
effect: subs[i],
...info
})
}
sub.update()
}
}
}
// 同一时间只有一个观察者使用,赋值观察者
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []
// 开始收集的时候 设置:Dep.target = watcher
export function pushTarget(target?: DepTarget | null) {
targetStack.push(target)
Dep.target = target
}
// 结束收集的时候 设置:Dep.target = null
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
Watcher 也是一个类,也叫观察者(订阅者),这里干的活还挺复杂的,而且还串连了 模板编译
和 渲染
。
在一个组件中,每个被监听的属性都会对应一个 Watcher
实例。当属性发生变化时,对应的 Watcher
会被添加到 调度器队列
(也叫渲染队列)中。
// 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
) {
// ...
if ((this.vm = vm) && isRenderWatcher) {
vm._watcher = this
}
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
if (__DEV__) {
this.onTrack = options.onTrack
this.onTrigger = options.onTrigger
}
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid
this.active = true
this.post = false
this.dirty = this.lazy
// Watcher 实例持有的 Dep 实例的数组
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = __DEV__ ? expOrFn.toString() : ''
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
__DEV__ &&
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() {
// 该函数用于缓存 Watcher
// 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher
pushTarget(this)
let value
const vm = this.vm
try {
// 调用回调函数,也就是upcateComponent,对需要双向绑定的对象求值,从而触发依赖收集
value = this.getter.call(vm, vm)
} catch (e: any) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// 深度监听
if (this.deep) {
traverse(value)
}
// 恢复Watcher
popTarget()
// 清理不需要了的依赖
this.cleanupDeps()
}
return value
}
// 添加依赖
addDep(dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// watcher添加它和dep的关系
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 和上面的反过来,dep添加它和watcher的关系
// 把当前 Watcher push 进 subs 数组
dep.addSub(this)
}
}
}
// 清理不需要的依赖
cleanupDeps() { }
// 派发更新时调用
update() {
// 如果是懒执行走这里,比如:computed
if (this.lazy) {
this.dirty = true
// 如果是同步执行 则执行run函数
} else if (this.sync) {
this.run()
// 将watcher放到watcher队列中
} else {
queueWatcher(this)
}
}
// 执行 watcher 的回调
run() {
if (this.active) {
// 调用get方法
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 更换旧值为新值
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(
this.cb,
this.vm,
[value, oldValue],
this.vm,
info
)
} else {
// 渲染watcher
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// 懒执行的watcher会调用该方法 比如:computed
evaluate() {
this.value = this.get()
// computed的缓存原理
// this.dirty设置为false 则页面渲染时只会执行一次computed的回调
// 数据更新以后 会在update中重新设置为true
this.dirty = false
}
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
补充:
Watcher.run()
函数里面会执行会掉,并且把新值和老值传过去。在 首次渲染挂载 的时候,还会有这样一段代码逻辑。
// src/core/instance/lifecycle.ts
export function mountComponent(...): Component {
// 调用生命周期钩子函数
callHook(vm, 'beforeMount')
let updateComponent
// 创建一个更新渲染函数; 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Diff )到真实DOM,这里是首次渲染
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 当触发更新的时候,会在更新之前调用
const watcherOptions: WatcherOptions = {
before() {
// 判断 DOM 是否是挂载状态,就是说首次渲染和卸载的时候不会执行
if (vm._isMounted && !vm._isDestroyed) {
// 调用生命周期钩子函数
callHook(vm, 'beforeUpdate')
}
}
}
// 生成一个渲染 watcher 每次页面依赖的数据更新后会调用 updateComponent 进行渲染
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true
)
// 没有老的 vnode,说明是首次渲染
if (vm.$vnode == null) {
vm._isMounted = true
// 渲染真实 dom 结束后调用 mounted 生命周期
callHook(vm, 'mounted')
}
return vm
}
这里就是开始 准备挂载 真实dom
了,创建了渲染 watcher ,渲染 watcher 内部调用了 updateComponent 方法。
依赖收集(过程有个大致了解就行) :
watcher
,进入 watcher
构造函数里就会执行 this.get()
方法pushTarget(this)
,就是把 Dep.target
赋值为当前渲染 watcher
并压入栈(为了恢复用)this.getter.call(vm, vm)
,也就是上面的 updateComponent()
函数,里面就执行了 vm._update(vm._render(), hydrating)
vm._render()
就会生成渲染 vnode
,这个过程中会访问 vm 上的数据
,就触发了数据对象的 getter
dep
,在触发 getter 的时候就会调用 dep.depend()
方法,也就会执行 Dep.target.addDep(this)
push
到 subs
里,到这就已经 完成了依赖的收集
,不过到这里还没执行完,如果是对象还会 递归对象
触发所有子项的getter,还要恢复 Dep.target 状态当定义的 响应式数据 被改变时,会触发 Object.defineProperty
的set方法,直接改变数据层的的数据,但是问题来了,数据是修改了,那视图该怎么更新呢?这时候 dep
就排上用场了,dep
会触发 notify
方法,通知渲染Watcher
去更新视图。
触发 setter 的时候会调用 dep.notify()
通知所有订阅者进行派发更新。
// src/core/observer/dep.ts
notify(info?: DebuggerEventExtraInfo) {
const subs = this.subs.filter(s => s) as DepTarget[]
if (__DEV__ && !config.async) {
// 如果不是异步,需要排序以确保正确触发
subs.sort((a, b) => a.id - b.id)
}
// 遍历dep的所有 watcher 然后执行他们的 update
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
if (__DEV__ && info) {
sub.onTrigger &&
sub.onTrigger({
effect: subs[i],
...info
})
}
// 触发更新
sub.update()
}
}
派发更新时调用。
// src/core/observer/watcher.ts
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 将 Watcher 对象添加到调度器(scheduler)队列中,以便在适当的时机执行其更新操作。
queueWatcher(this)
}
}
这是一个队列,也是Vue在做 派发更新
时的一个优化点。就是在每次数据改变的时候不会都触发 watcher
回调,而是把这些 watcher
都添加在一个队列里,并对 watcher
进行 去重
,然后在 nextTick
异步任务中执行。
这里主要做的事情:
nextTick
只会调用一次。// src/core/observer/scheduler.ts
export function queueWatcher(watcher: Watcher) {
// 获得 watcher 的 id
const id = watcher.id
// 判断当前 id 的 watcher,是否在观察者队列中,已经存在的话return出去
if (has[id] != null) {
return
}
if (watcher === Dep.target && watcher.noRecurse) {
return
}
has[id] = true
if (!flushing) {
// 最开始会进入这里
queue.push(watcher)
} else {
// 如果在执行 watcher 期间又有新的 watcher 插入进来就会到这里,插入新的 watcher
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 最开始会进入这里
if (!waiting) {
waiting = true
if (__DEV__ && !config.async) {
flushSchedulerQueue()
return
}
// 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用
nextTick(flushSchedulerQueue)
}
}
flushSchedulerQueue() 函数通常在下一个 Event Loop 中异步执行,以确保在 Vue.js 内部更新操作完成后再进行批量处理。这样可以避免过早地触发 DOM 更新,保证在相同的异步任务中只触发一次 DOM 更新。
这里主要做的事情:
遍历
watcher 队列,执行对应的 watcher.run()
;遍历的时候每次都会对队列长度进行求值,因为在run之后,很可能又会有新的 watcher 添加进来,这时就会再次执行上面的 queueWatcher()
方法。// src/core/observer/scheduler.ts
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// 根据 id 排序,有如下条件
// 1.组件更新需要按从父到子的顺序,因为创建过程中也是先父后子
// 2.组件内我们自己写的 watcher 优先于渲染 watcher
// 3.如果某组件在父组件的 watcher 运行期间销毁了,就跳过这个 watcher
queue.sort(sortCompareFn)
// 不要缓存队列长度,因为遍历过程中可能队列的长度发生变化
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
// 执行 beforeUpdate 生命周期钩子函数
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
// 执行组件内我们自己写的 watch 的回调函数并渲染组件
watcher.run()
// 检查并停止循环更新,比如在 watcher 的过程中又重新给对象赋值了,就会进入无限循环
if (__DEV__ && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn('无限循环了', watcher.vm)
break
}
}
}
// 重置状态之前,先保留一份队列备份
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// 调用组件激活的钩子 activated
callActivatedHooks(activatedQueue)
// 调用组件更新的钩子 updated
callUpdatedHooks(updatedQueue)
cleanupDeps()
}
终于可以更新了,updated 大家都熟悉了,就是生命周期钩子函数。
// src/core/observer/scheduler.ts
function callUpdatedHooks(queue: Watcher[]) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm && vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
到此 Vue2.X 数据响应式原理流程的源码基本就分析完毕了,接下来就介绍一下Object.defineProperty 的缺陷和处理方法。
由上可知,Object.defineProperty
在劫持对象和数组时的缺陷:
对象属性
的 添加
或 删除
。遍历
该对象。数组
元素的变化,需要进行数组方法的重写。而这些问题,Vue2.X 里也有相应的解决文案。
this.$set
和 this.$delete
触发对象属性添加、删除的响应式。ob.dep.notify()
手动派发更新。这里主要做的事情:
__ob__
,也就是它的 Observer 对象
,如果有新的值,就调用 observeArray
继续对新的值观察变化(也就是通过 target__proto__== arrayMethods 来改变了数组实例的型),然后手动调用 notify
,通知渲染 watcher
,执行 update
。// src/core/observer/array.ts
// 获取数组的原型
const arrayProto = Array.prototype
// 创建一个新对象并继承了数组原型的属性和方法,将其原型指向 Array.prototype
// 为什么要克隆一份呢?因为如果直接更改数组的原型,那么将来所有的数组都会被我改了。
export const arrayMethods = Object.create(arrayProto)
// 会改变原数组的方法列表;为什么只有7个方法呢?因为只有这7个方法改变了原数组
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 重写数组事件
methodsToPatch.forEach(function (method) {
// 保存原本的事件
const original = arrayProto[method]
// 创建响应式对象
def(arrayMethods, method, function mutator(...args) {
// 首先 先执行原始行为,以前咋滴现在就咋滴
const result = original.apply(this, args)
// 然后 再做变更通知,如何变更的呢?
// 1.获取ob实例
const ob = this.__ob__
// 2.如果是新增元素的操作,比如push、unshift或者增加元素的splice操作
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 3.新加入的元素需要做响应式处理
if (inserted) ob.observeArray(inserted)
// 4.让内部的dep派发更新
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ARRAY_MUTATION,
target: this,
key: method
})
} else {
// 派发更新
ob.dep.notify()
}
// 返回原生数组方法的执行结果
return result
})
})
...
const keys = Object.keys(value) //获取data中的所有属性名
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock)
}
...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () { ... },
set: function reactiveSetter (newVal) { ... }
})
...
由参数可以看出,它是需要根据具体的 key
去 keys
里找 keys [i]
,来进行拦截处理的,所以就有需要满足一个前置条件,一开始就得知道 key
是啥,所以就需要遍历每一个 key
,并定义 getter、setter,这也是为什么后面添加的属性没有响应式的原因。
Vue的数据响应式原理核心就是通过 Object.defineProperty
来拦截对数据的获取和设置。
Vue的响应式数据分为两类:对象
和 数组
。
遍历对象的所有属性,并为每个属性设置 getter
和 setter
,以便将来获取和设置,如果属性的值也是对象的话会多次调用 observe() 递归遍历
,为属性值上的每个key设置 getter
和 setter
。
获取数据时 :在 dep
中添加相关的 watcher
。
设置数据时:再由 dep
去通知相关的 watcher
去更新。
重写了数组中的那些原生方法,首先获取到这个数组(那7个改变数组元素的方法列表)的 __ob__
,也就是它的 Observer
对象,如果有新的值,就调用 observeArray
继续对新的值观察变化(也就是通过 target.__proto__= arrayMethods
来改变了数组实例的型),然后手动调用 notify
,通知渲染 watcher
,执行 update
。
添加新数据时:需要进行 数据响应式
的处理,再调用 ob.dep.notify()
通知 watcher
去更新
删除数据时:也要由 ob.dep.notify()
通知 watcher
去更新
类似于管家,当数据更改的时候, dep 会去调用 notify()
方法,然后去通知 watcher 更新,执行 update
。
Dep在数据响应式中扮演的角色就是 数据的依赖收集
和 变更通知
。
在获取数据的时候知道自己(Dep)依赖的 watcher 都有谁,同时在数据变更的时候通知自己(Dep)依赖的这些 watcher 去执行他们(watcher)的 update。
- 初始化数据
在 Vue 组件的初始化过程中,会对数据进行初始化。Vue会遍历
组件实例的data
对象,并使用Object.defineProperty()
方法将数据属性转换为getter
和setter
,从而实现响应式。- 数据劫持
Vue 使用【数据劫持】 的方式来实现数据的观测。具体来说,Vue 在数据对象的每个属性上定义getter
和setter
,当属性被访问或修改时,会触发相应的getter
和setter
方法。- 依赖收集
在进行数据劫持的同时,Vue 会创建一个Watcher
对象,并将其与当前组件的依赖关系建立起来。当访问响应式数据的getter
方法时,Watcher会被添加到当前正在处理的依赖关系对应的Dep
对象中。Dep 对象负责管理这些依赖关系并在需要更新时触发相应的 Watcher。- 模板编译和渲染
Vue 会进行模板编译,将模板转换为 Virtual DOM。在编译过程中,遇到响应式数据的引用,会生成对应的访问表达式,并在对应位置添加相应的更新逻辑。- 执行更新
当响应式数据发生变化时,其setter
方法会被调用。这时 Vue 会通知与该数据相关的Watcher
对象进行update
更新操作。- 调度更新
Vue 使用调度器队列
来管理需要进行更新的Watcher
。在数据发生变化时, Watcher 会被添加到 调度器队列 中。将多个
Watcher 的更新操作合并成一个
异步的批量更新,可以减少不必要的重绘和渲染,并提高性能。- 触发依赖更新(Watcher )
在适当的时机(通常是下一个 tick 或微任务), 会执行调度器的更新操作。将队列中的Watcher
逐个取出并执行其更新方法。这些更新方法会触发组件
的重新渲染
,从而保持视图与数据的同步。
通过以上流程,Vue 实现了数据的响应式更新。当数据发生变化时,相关的 Watcher
会被追踪并进行更新,最终影响到 组件的渲染
结果。这种响应式的机制使得开发者能够方便地操作数据,并自动更新相关的视图,提高开发效率和用户体验。
1) 一开始我分不清观察者队列和调度器队列,以为是一个东西,这里也记录下吧。
观察者队列
是 为了管理观察者对象的更新顺序
,而 调度器队列
则是 为了批量处理 Watcher 对象的更新操作
。它们在实现上有所区别,但都是为了保证数据的响应式更新和视图的同步。2)一个组件中有多个属性,如果每个属性都更新数据都是进入一个调度器队列吗?
- 在 Vue 中,一个组件中的多个属性的更新操作都会进入同一个调度器队列。这个调度器队列称为「渲染队列」或「异步更新队列」。
- 当某个属性的数据发生变化时,与该属性相关的 Watcher 对象会被添加到调度器队列中。这个过程会
不断重复
,直到所有需要更新的属性都被添加到队列中。- 在适当的时机,Vue 会执行调度器的更新操作,遍历渲染队列中的 Watcher 对象,并依次执行它们的更新逻辑。这样可以保证多个属性的更新操作按照正确的顺序进行,并且能够高效地进行批量更新。
- 所以,无论一个组件中有多少个属性发生更新,它们都会进入同一个调度器队列,由调度器统一管理和触发更新操作。这种机制确保了数据更新的有序性和性能优化。
可参考:
Vue3.2 响应式原理源码剖析,及与 Vue2 .x响应式的区别
深入浅出 Vue 响应式原理源码剖析
Vue源码系列(三):数据响应式原理