Vue源码学习 - 数据响应式原理

news/2024/11/28 7:30:51/

目录

  • 前言
  • 一、入口查找
  • 二、初始化
    • 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 去完成自己该做的事。

了解了上面的例子,再去看下面 DepWatcher 的源码 。

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()}}
}

补充:

  1. 我们自己组件里写的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)
  • 然后这里会做一些判断,以确保同一数据不会被多次添加,接着把符合条件的数据 pushsubs 里,到这就已经 完成了依赖的收集 ,不过到这里还没执行完,如果是对象还会 递归对象 触发所有子项的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.$setthis.$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) { ... }
})
...

由参数可以看出,它是需要根据具体的 keykeys 里找 keys [i],来进行拦截处理的,所以就有需要满足一个前置条件,一开始就得知道 key 是啥,所以就需要遍历每一个 key,并定义 getter、setter,这也是为什么后面添加的属性没有响应式的原因。

六、vue2.X数据响应式做个总结

1)首先

Vue的数据响应式原理核心就是通过 Object.defineProperty 来拦截对数据的获取和设置。

2)其次

Vue的响应式数据分为两类:对象数组

2.1) 对象

遍历对象的所有属性,并为每个属性设置 gettersetter,以便将来获取和设置,如果属性的值也是对象的话会多次调用 observe() 递归遍历,为属性值上的每个key设置 gettersetter

获取数据时 :在 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 的数据响应式流程总结

  1. 初始化数据
    在 Vue 组件的初始化过程中,会对数据进行初始化。Vue会 遍历 组件实例的 data 对象,并使用 Object.defineProperty() 方法将数据属性转换为 gettersetter,从而实现响应式。
  2. 数据劫持
    Vue 使用【数据劫持】 的方式来实现数据的观测。具体来说,Vue 在数据对象的每个属性上定义 gettersetter,当属性被访问或修改时,会触发相应的 gettersetter 方法。
  3. 依赖收集
    在进行数据劫持的同时,Vue 会创建一个 Watcher 对象,并将其与当前组件的依赖关系建立起来。当访问响应式数据的 getter 方法时,Watcher会被添加到当前正在处理的依赖关系对应的 Dep 对象中。Dep 对象负责管理这些依赖关系并在需要更新时触发相应的 Watcher。
  4. 模板编译和渲染
    Vue 会进行模板编译,将模板转换为 Virtual DOM。在编译过程中,遇到响应式数据的引用,会生成对应的访问表达式,并在对应位置添加相应的更新逻辑。
  5. 执行更新
    当响应式数据发生变化时,其 setter 方法会被调用。这时 Vue 会通知与该数据相关的 Watcher 对象进行 update 更新操作。
  6. 调度更新
    Vue 使用 调度器队列 来管理需要进行更新的 Watcher 。在数据发生变化时, Watcher 会被添加到 调度器队列 中。将 多个 Watcher 的更新操作合并成 一个 异步的批量更新,可以减少不必要的重绘和渲染,并提高性能。
  7. 触发依赖更新(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源码系列(三):数据响应式原理


http://www.ppmy.cn/news/979231.html

相关文章

【linux】uboot之链接重定向

文章目录 一、uboot中的链接脚本重定向二、具体代码编译器选项uboot中的符号重定向 一、uboot中的链接脚本重定向 在 U-Boot 中&#xff0c;链接脚本&#xff08;Linker Script&#xff09;用于指定可执行文件的内存布局和符号表等信息。通过修改链接脚本&#xff0c;可以对程…

于大模型迁移中学习 Docker

最近在做大模型的昇腾迁移&#xff0c;国产化框架踩坑不少&#xff0c;基本一天的工作量相当于之前做纯视觉算法时一周踩过的坑数了。 现在在modelarts上用八卡昇腾910跑llama&#xff0c;不同于之前自己配环境&#xff0c;昇腾生态创新中心都是用的镜像&#xff0c;虽说打包起…

Shell编程基础(三)环境变量 位置变量 系统内置变量

环境变量 & 环境变量环境变量范围父子进程之间有效指定用户有效所有用户有效 位置变量系统内置变量 环境变量 在脚本种直接定义的变量&#xff0c;只能在当前shell进程中使用 若想要在其他shell进程中使用&#xff0c;可以将变量声明为 环境变量 export 变量名 &#xff…

问题解决Can‘t update table ‘category‘ in store

问题描述: 使用spring boot的时,候访问更新数据库内容接口报错: Error updating database. Cause: java.sql.SQLException: Cant update table category in stored function/trigger because it is already used by statement which invoked this stored function/trigger. 问题…

夯实数字化转型安全地基,华东某农商行开源安全治理经验

华东某农村商业银行是一家全国首批组建的股份制农村金融机构。近年来&#xff0c;该农商行坚持“科技强行”战略&#xff0c;进一步夯实数字化核心基础&#xff0c;积极推动金融科技与产品、服务的深度融合&#xff0c;努力拓展数字金融的包容性&#xff0c;让数字金融更有温度…

Java maven project XPathFctory

java Maven project, 更新了一个java库&#xff0c;项目无法编译了&#xff0c; 一直报错&#xff1a;No XPathFctory implementation found for the object model: Java 中 XPathFactory 只有抽象定义&#xff0c;没有具体实现&#xff0c;需要添加实现类&#xff0c;经过百度…

【100天精通python】Day16:python 模块的搜索目录和导入模块异常时的处理方法

目录 1 搜索模块所在目录 2 模块不在搜索目录中 2.1 添加模块所在的目录到PYTHONPATH环境变量 2.2 修改sys.path 2.3 使用绝对路径导入 2.4将模块复制到Python搜索路径中的任意一个目录 2.5 总结 3 其他导入的模块异常处理 3.1 模块未安装 3.2 模块名称拼写错误 3.3模…

实现基于UDP简易的英汉词典

文章目录 实现目标认识相关接口socketbzerobindrecvfromsendto 实现思路和注意事项完整代码Server.hppServer.ccClient.hppClient.cc 运行效果END 实现目标 实现一个服务端和一个客户端&#xff0c;客户端负责发送一个单词&#xff0c;服务端接收到后将翻译后的结果返回发送到…