目录
- 前言
- 一、入口查找
- 二、初始化
- initState()
- initProps()
- initData()
- observe() - Observer的守护
- Observer
- defineReactive()
- 三、依赖收集
- Watcher 和 Dep 通过 例子 对概念有个了解
- 1)什么是Watcher呢?
- 2)Watcher的种类有哪些呢?
- 3)什么是Dep呢?
- Dep源码
- Watcher源码
- 依赖收集过程
- 四、派发更新
- notify()
- update()
- queueWatcher()
- flushSchedulerQueue()
- updated()
- 五、Object.defineProperty 的缺陷和处理方法
- 重写数组方法
- Vue2.x中后面添加的属性没有响应式的原因
- 六、vue2.X数据响应式做个总结
- 1)首先
- 2)其次
- 2.1) 对象
- 2.2) 数组
- 3)dep的作用
- 4)Vue2.X 的数据响应式流程总结
- 补充:
前言
Vue.Js的核心包括一套 “响应式系统”。“响应式”,是指当数据改变后,Vue会通知到使用该数据的代码。例如,视图渲染中使用了数据,数据改变后,视图也会自动更新。
一、入口查找
vue2.X的数据响应式是利用 Object.defineProperty()
实现的,通过定义对象属性 getter/setter
拦截对属性的获取和设置。
具体如何实现的呢?首先需要考虑一下从哪里入手去看源码。
在上一篇文章 Vue源码学习 - new Vue初始化都做了什么?说到了一个方法叫:initState()
,当时给它的解释是:对props,methods,data,computed,watch进行初始化,包括响应式处理。
// src/core/instance/init.tsexport function initMixin(Vue: typeof Component) {
// 在原型上添加 _init 方法Vue.prototype._init = function (options?: Record<string, any>) {const vm: Component = this//...vm._self = vminitLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等initEvents(vm) // 初始化事件:$on, $off, $emit, $onceinitRender(vm) // 初始化渲染: render, mixincallHook(vm, 'beforeCreate', undefined, false) // // 调用生命周期的钩子函数,在这里就能看出一个组件在创建之前和之后分别做了哪些初始化initInjections(vm) // 初始化 injectinitState(vm) // 对props,methods,data,computed,watch进行初始化,包括响应式的处理initProvide(vm) // 初始化 providecallHook(vm, 'created') // created 初始化完成,可以执行挂载了//...}
}
初始化这里调用了很多方法,每个方法都做着不同的事,而关于响应式主要就是组件内的数据 props、data
。这一块的内容就是在 initState()
这个方法里,所以进入这个方法源码看一下。
二、初始化
initState()
// src/core/instance/state.ts// 数据响应式的入口
export function initState(vm: Component) {const opts = vm.$options// 初始化 propsif (opts.props) initProps(vm, opts.props)// 初始化 methodsif (opts.methods) initMethods(vm, opts.methods)// 初始化 dataif (opts.data) {initData(vm)} else {// 没有 data 的话就默认赋值为空对象,并监听const ob = observe((vm._data = {}))ob && ob.vmCount++}// 初始化 computedif (opts.computed) initComputed(vm, opts.computed)// 初始化 watch if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)}
}
这也是一堆初始化的东西,我们还是直奔主题,取响应式数据相关的,也就是 initProps()
、initData()
、observe()
,然后挨个看里面的源码。
initProps()
这里主要做的事情:
- 遍历父组件传进来的
props
列表。 - 校验每个属性的命名、类型、default属性等,都没有问题的话调用
defineReactive
设置成响应式。 - 然后用
proxy()
把属性代理到当前实例上,如把vm._props.xx
变成vm.xx
,就可以访问。
// src/core/instance/state.tsfunction initProps(vm: Component, propsOptions: Object) {// 父组件传入子组件的 propsconst propsData = vm.$options.propsData || {}// 经过转换后最终的 propsconst props = (vm._props = shallowReactive({}))// 存放 props 的数组const keys: string[] = (vm.$options._propKeys = [])const isRoot = !vm.$parent// 转换非根实例的 propsif (!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
initData()
这里主要做的事情:
- 初始化一个 data,并拿到 keys 集合。
- 遍历 keys 集合,来判断有没有和 props 里的属性名或者 methods 里的方法名重名的。
- 没有问题就通过 proxy() 把 data 里的每一个属性都代理到当前实例上,就可以通过
this.xx
访问了。 - 最后再调用
observe
监听整个 data。
// src/core/instance/state.tsfunction 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.methodslet i = keys.lengthwhile (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 监听 dataconst ob = observe(data)ob && ob.vmCount++
}
observe() - Observer的守护
这个方法主要就是用来给数据加上监听器的。
严格的说,observe() 方法应该算是Observer的守护,为Observer即将开启前做的一些合规检测。
这里主要做的事情:
- 如果值已经做过了响应式处理(已经被观察过了),则返回现有的观察者。
- 否则就给没有添加 Observer 的值添加一个新的 Observer 实例。
// src/core/observer/index.tsexport 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)}
}
Observer
这是一个类,作用是把一个正常的数据成可观测的数据。
这里主要做的事情:
- 给当前 value 打上已经是响应式属性的标记,避免重复操作。
- 然后判断数据类型
1)如果是数组
,遍历数组,调用observe()
对每一个元素进行监听。
2)如果是对象
,遍历对象,调用defineReactive()
创建响应式对象。
// src/core/observer/index.tsexport class Observer {dep: DepvmCount: 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)}}
defineReactive()
作用是定义响应式对象。
这里主要做的事情:
- 先给每个属性都创建一个
dep 实例
。 - 如果是
对象
就调用observe()
,递归监听,保证不管结构嵌套多深,里面的属性都能变成响应式对象。 - 然后调用
Object.defineProperty()
劫持对象的 getter 和 setter。 - 如果获取时,触发 getter 会调用
dep.depend()
把watcher(观察者)
push 到依赖的数组subs
里面。 - 如果更新时,触发 setter 会做以下操作:
1)新值没有变化或者没有 setter 属性的直接跳出。
2)如果新值是对象就调用observe()
递归监听。
3)然后调用dep.notify()
派发更新。
// src/core/observer/index.tsexport 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 和 setterconst getter = property && property.getconst setter = property && property.setif ((!getter || setter) &&(val === NO_INITIAL_VALUE || arguments.length === 2)) {val = obj[key]}// 如果 val 是对象的话就递归监听// 递归调用 observe 就可以保证不管对象结构嵌套有多深,都能变成响应式对象let childOb = !shallow && observe(val, false, mock)// 截持对象属性的 getter 和 setterObject.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 = newValreturn} 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
也是密不可分的。下面通过 例子 和 源码 讲解就会有体会了。
Watcher 和 Dep 通过 例子 对概念有个了解
1)什么是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
了。
2)Watcher的种类有哪些呢?
上面所说的三处地方就刚刚好代表了三种 Watcher
,分别是:
- 渲染Watcher :变量修改时,负责通知HTML里的重新渲染。
- computed Watcher :变量修改时,负责通知 computed 里依赖此变量的 computed 属性变量的更改。
- user Watcher :变量修改时,负责通知 watch 属性里所对应的变量函数的执行。
3)什么是Dep呢?
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 的源码 。
Dep源码
这是一个类,实际上就是对 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 | nullid: numbersubs: Array<DepTarget | null>_pending = falseconstructor() {this.id = uid++// 用来存储 Watcher 的数组this.subs = []}// 在 dep 中添加 观察者addSub(sub: DepTarget) {this.subs.push(sub)}// 移除观察者removeSub(sub: DepTarget) {this.subs[this.subs.indexOf(sub)] = nullif (!this._pending) {this._pending = truependingCleanupDeps.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
实例。当属性发生变化时,对应的 Watcher
会被添加到 调度器队列
(也叫渲染队列)中。
// src/core/observer/watcher.tsexport 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.deepthis.user = !!options.userthis.lazy = !!options.lazythis.sync = !!options.syncthis.before = options.beforeif (__DEV__) {this.onTrack = options.onTrackthis.onTrigger = options.onTrigger}} else {this.deep = this.user = this.lazy = this.sync = false}this.cb = cbthis.id = ++uid this.active = truethis.post = falsethis.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// 因为在组件含有嵌套组件的情况下,需要恢复父组件的 WatcherpushTarget(this)let valueconst vm = this.vmtry {// 调用回调函数,也就是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)}// 恢复WatcherpopTarget()// 清理不需要了的依赖this.cleanupDeps()}return value}// 添加依赖addDep(dep: Dep) {const id = dep.idif (!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() {// 如果是懒执行走这里,比如:computedif (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.valuethis.value = valueif (this.user) {const info = `callback for watcher "${this.expression}"`invokeWithErrorHandling(this.cb,this.vm,[value, oldValue],this.vm,info)} else {// 渲染watcherthis.cb.call(this.vm, value, oldValue)}}}}// 懒执行的watcher会调用该方法 比如:computedevaluate() {this.value = this.get()// computed的缓存原理// this.dirty设置为false 则页面渲染时只会执行一次computed的回调// 数据更新以后 会在update中重新设置为truethis.dirty = false}depend() {let i = this.deps.lengthwhile (i--) {this.deps[i].depend()}}
}
补充:
- 我们自己组件里写的watch监听,为什么自动就能拿到新值和老值两个参数呢?其实就是就在
Watcher.run()
函数里面会执行会掉,并且把新值和老值传过去。
依赖收集过程
在 首次渲染挂载 的时候,还会有这样一段代码逻辑。
// src/core/instance/lifecycle.tsexport 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
- 每一个对象值的 getter 都有一个
dep
,在触发 getter 的时候就会调用dep.depend()
方法,也就会执行Dep.target.addDep(this)
- 然后这里会做一些判断,以确保同一数据不会被多次添加,接着把符合条件的数据
push
到subs
里,到这就已经完成了依赖的收集
,不过到这里还没执行完,如果是对象还会递归对象
触发所有子项的getter,还要恢复 Dep.target 状态
四、派发更新
当定义的 响应式数据 被改变时,会触发 Object.defineProperty
的set方法,直接改变数据层的的数据,但是问题来了,数据是修改了,那视图该怎么更新呢?这时候 dep
就排上用场了,dep
会触发 notify
方法,通知渲染Watcher
去更新视图。
notify()
触发 setter 的时候会调用 dep.notify()
通知所有订阅者进行派发更新。
// src/core/observer/dep.tsnotify(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()}}
update()
派发更新时调用。
// src/core/observer/watcher.tsupdate() {if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {// 将 Watcher 对象添加到调度器(scheduler)队列中,以便在适当的时机执行其更新操作。queueWatcher(this)}}
queueWatcher()
这是一个队列,也是Vue在做 派发更新
时的一个优化点。就是在每次数据改变的时候不会都触发 watcher
回调,而是把这些 watcher
都添加在一个队列里,并对 watcher
进行 去重
,然后在 nextTick
异步任务中执行。
这里主要做的事情:
- 先用 has 对象查找 id,保证 同一个 watcher 只会被 push 一次。
- else 如果在执行 watcher 期间又有新的 watcher 插入进来就会到这里,然后从后往前找,找到第一个待插入的 id 比当前队列中的 id 大的位置,插入到队列中,这样队列的长度就发生了变化。
- 最后通过 waiting 保证
nextTick
只会调用一次。
// src/core/observer/scheduler.tsexport function queueWatcher(watcher: Watcher) {// 获得 watcher 的 idconst id = watcher.id// 判断当前 id 的 watcher,是否在观察者队列中,已经存在的话return出去if (has[id] != null) {return}if (watcher === Dep.target && watcher.noRecurse) {return}has[id] = trueif (!flushing) {// 最开始会进入这里queue.push(watcher)} else {// 如果在执行 watcher 期间又有新的 watcher 插入进来就会到这里,插入新的 watcher let i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}queue.splice(i + 1, 0, watcher)}// 最开始会进入这里if (!waiting) {waiting = trueif (__DEV__ && !config.async) {flushSchedulerQueue()return}// 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用nextTick(flushSchedulerQueue)}
}
flushSchedulerQueue()
flushSchedulerQueue() 函数通常在下一个 Event Loop 中异步执行,以确保在 Vue.js 内部更新操作完成后再进行批量处理。这样可以避免过早地触发 DOM 更新,保证在相同的异步任务中只触发一次 DOM 更新。
这里主要做的事情:
- 先sort排序 watcher 队列,排序条件有三点,看注释。
- 然后
遍历
watcher 队列,执行对应的watcher.run()
;遍历的时候每次都会对队列长度进行求值,因为在run之后,很可能又会有新的 watcher 添加进来,这时就会再次执行上面的queueWatcher()
方法。
// src/core/observer/scheduler.tsfunction flushSchedulerQueue() {currentFlushTimestamp = getNow()flushing = truelet watcher, id// 根据 id 排序,有如下条件// 1.组件更新需要按从父到子的顺序,因为创建过程中也是先父后子// 2.组件内我们自己写的 watcher 优先于渲染 watcher// 3.如果某组件在父组件的 watcher 运行期间销毁了,就跳过这个 watcherqueue.sort(sortCompareFn)// 不要缓存队列长度,因为遍历过程中可能队列的长度发生变化for (index = 0; index < queue.length; index++) {watcher = queue[index]// 执行 beforeUpdate 生命周期钩子函数if (watcher.before) {watcher.before()}id = watcher.idhas[id] = null// 执行组件内我们自己写的 watch 的回调函数并渲染组件watcher.run()// 检查并停止循环更新,比如在 watcher 的过程中又重新给对象赋值了,就会进入无限循环if (__DEV__ && has[id] != null) {circular[id] = (circular[id] || 0) + 1if (circular[id] > MAX_UPDATE_COUNT) {warn('无限循环了', watcher.vm)break}}}// 重置状态之前,先保留一份队列备份const activatedQueue = activatedChildren.slice()const updatedQueue = queue.slice()resetSchedulerState()// 调用组件激活的钩子 activatedcallActivatedHooks(activatedQueue)// 调用组件更新的钩子 updatedcallUpdatedHooks(updatedQueue)cleanupDeps()
}
updated()
终于可以更新了,updated 大家都熟悉了,就是生命周期钩子函数。
// src/core/observer/scheduler.tsfunction callUpdatedHooks(queue: Watcher[]) {let i = queue.lengthwhile (i--) {const watcher = queue[i]const vm = watcher.vmif (vm && vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {callHook(vm, 'updated')}}
}
到此 Vue2.X 数据响应式原理流程的源码基本就分析完毕了,接下来就介绍一下Object.defineProperty 的缺陷和处理方法。
五、Object.defineProperty 的缺陷和处理方法
由上可知,Object.defineProperty
在劫持对象和数组时的缺陷:
- 无法检测到
对象属性
的添加
或删除
。 - 监听对象的多个属性,需要
遍历
该对象。 - 无法检测
数组
元素的变化,需要进行数组方法的重写。
而这些问题,Vue2.X 里也有相应的解决文案。
- 需要使用
this.$set
和this.$delete
触发对象属性添加、删除的响应式。 - 重写了会改变原数组的7个方法,再通过
ob.dep.notify()
手动派发更新。
重写数组方法
这里主要做的事情:
- 保存会改变数组的方法列表( ‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’)。
- 然后重写了数组中的那些原生方法,首先获取到这个数组的
__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 insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase '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})
})
Vue2.x中后面添加的属性没有响应式的原因
...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,这也是为什么后面添加的属性没有响应式的原因。
六、vue2.X数据响应式做个总结
1)首先
Vue的数据响应式原理核心就是通过 Object.defineProperty
来拦截对数据的获取和设置。
2)其次
Vue的响应式数据分为两类:对象
和 数组
。
2.1) 对象
遍历对象的所有属性,并为每个属性设置 getter
和 setter
,以便将来获取和设置,如果属性的值也是对象的话会多次调用 observe() 递归遍历
,为属性值上的每个key设置 getter
和 setter
。
获取数据时 :在 dep
中添加相关的 watcher
。
设置数据时:再由 dep
去通知相关的 watcher
去更新。
2.2) 数组
重写了数组中的那些原生方法,首先获取到这个数组(那7个改变数组元素的方法列表)的 __ob__
,也就是它的 Observer
对象,如果有新的值,就调用 observeArray
继续对新的值观察变化(也就是通过 target.__proto__= arrayMethods
来改变了数组实例的型),然后手动调用 notify
,通知渲染 watcher
,执行 update
。
添加新数据时:需要进行 数据响应式
的处理,再调用 ob.dep.notify()
通知 watcher
去更新
删除数据时:也要由 ob.dep.notify()
通知 watcher
去更新
3)dep的作用
类似于管家,当数据更改的时候, dep 会去调用 notify()
方法,然后去通知 watcher 更新,执行 update
。
Dep在数据响应式中扮演的角色就是 数据的依赖收集
和 变更通知
。
在获取数据的时候知道自己(Dep)依赖的 watcher 都有谁,同时在数据变更的时候通知自己(Dep)依赖的这些 watcher 去执行他们(watcher)的 update。
4)Vue2.X 的数据响应式流程总结
- 初始化数据
在 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) 一开始我分不清观察者队列和调度器队列,以为是一个东西,这里也记录下吧。
- 观察者队列 与 调度器队列在 Vue 中是不同的概念。
- 观察者队列 是 Vue 在数据更新时使用的一种策略,用于管理需要进行更新的观察者。当响应式数据发生变化时,与该数据相关的观察者会被添加到观察者队列中,等待进一步的更新操作。观察者队列负责管理和调度这些观察者对象的更新。
- 调度器队列 是 Vue 中用于调度 Watcher 对象的队列。当需要对多个 Watcher 进行更新时,Vue 会通过调度器队列来批量处理这些 Watcher 的更新操作。调度器队列可以避免在同一事件循环中立即执行大量 Watcher 的更新方法,从而提高性能并避免不必要的重绘和渲染。
- 简而言之,
观察者队列
是为了管理观察者对象的更新顺序
,而调度器队列
则是为了批量处理 Watcher 对象的更新操作
。它们在实现上有所区别,但都是为了保证数据的响应式更新和视图的同步。
2)一个组件中有多个属性,如果每个属性都更新数据都是进入一个调度器队列吗?
- 在 Vue 中,一个组件中的多个属性的更新操作都会进入同一个调度器队列。这个调度器队列称为「渲染队列」或「异步更新队列」。
- 当某个属性的数据发生变化时,与该属性相关的 Watcher 对象会被添加到调度器队列中。这个过程会
不断重复
,直到所有需要更新的属性都被添加到队列中。- 在适当的时机,Vue 会执行调度器的更新操作,遍历渲染队列中的 Watcher 对象,并依次执行它们的更新逻辑。这样可以保证多个属性的更新操作按照正确的顺序进行,并且能够高效地进行批量更新。
- 所以,无论一个组件中有多少个属性发生更新,它们都会进入同一个调度器队列,由调度器统一管理和触发更新操作。这种机制确保了数据更新的有序性和性能优化。
可参考:
Vue3.2 响应式原理源码剖析,及与 Vue2 .x响应式的区别
深入浅出 Vue 响应式原理源码剖析
Vue源码系列(三):数据响应式原理