vue2响应式原理-结合源码分析

你通过本文能了解到什么?

  • 数据劫持的原理
  • this 是如何访问到data中的属性的
  • v-model 原理

众所周知,vue2的响应式原理是通过 Object.defineProperty 来实现的,那么他具体的实现方式是什么样的呢?

首先从流程的方面讲解

1. 通过Object.keys 获取到data返回对象的属性,遍历data[key] 执行observe

2. 执行observe 主要功能是对data[key]的值 实例化 Observer

【observer函数源码】(点击展开) ```js /* * * observe 源码 */ export function observe( value: any, // 是对象 shallow?: boolean, // false ssrMockReactivity?: boolean // false ): Observer | void { if (!isObject(value) || isRef(value) || value instanceof VNode) { // 不是对象 不是nul 不是ref(v3) 并且不再vnode实例 return } let ob: Observer | void if (hasOwn(value, 'ob') && value.ob instanceof Observer) { ob = value.ob } else if ( shouldObserve && (ssrMockReactivity || !isServerRendering()) && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && // isExtensible 是否可以扩展 !value.__v_skip /* ReactiveFlags.SKIP */ // 不跳过 ) { // 没有被Observe 实例的属性 ob = new Observer(value, shallow, ssrMockReactivity) } return ob } ```

3. new Observe 的主要功能及源码

  • 添加 dep 属性,并实例化 Dep

  • 为对象属性添加 __ob__属性,并指向实例后的对象

  • 判断 value 是否是数组

    • 是数组的情况下
    • 首先会给这个数组 重写他的 数组方法:'push','pop','shift','unshift','splice','sort','reverse'.
    • 调用 observeArray 遍历数组的值, 递归执行 observe方法
  • 是对象的情况下对value 的 key 遍历执行defineReactive, 此时就要执行数据劫持了

4. defineReactive

只讲 最主要的功能 且目前只考虑到渲染watcher 和 computedWatcher 访问的情况

  • 首先会在此方法里面 创建一个单独的 dep 实例, 用来做依赖收集和通知更新的,注意:在get时会用到此 dep 实例 。形成了 闭包

  • 获取到 data对象属性的每一个值val,并递归 执行 observe(val) 这里只有对象或者数组类型的会递归下去

  • 对 data 的属性进行 Object.defineProperty 的数据劫持

  • 劫持之前 是初始化data的行为, 在访问data属性时,会触发get

  • 在访问data属性时, 可能是渲染watcher(组件是一个渲染wathcer) 也可能是computedWatcher(每一个 computed 属性都会创建一个 computedWatcher ),这里先讲渲染wathcer,

  • 在初始化 new Vue 时会执行$mount(...),mount 就会创建一个渲染watcher,并且会执行watcher 的run 方法

  • wathcer.run 又会执行watcher.get,此时就会访问到data属性,

  • 在watcher.get 时, 会把 全局的一个 target 标记为当前的渲染watcher

  • 所以在访问data属性时,Dep.target 是渲染watcher

  • 此时通过闭包的原理,之前创建的 dep 来进行收集依赖,执行 dep.depend(),并且判断当前val 是否是对象,如果是访问的对象,也要让对象的 dep 也收集依赖,就是在 new Observe 时创建的属于对象的 dep 实例

  • dep.depend 执行后,分为两种, 一种是渲染 watcher 收集依赖, 一种是 computedWatcher 收集依赖

    • 先只讲 watcher 收集依赖的情况
    • dep.depend 执行时,因为当前的 Dep.target 是渲染watcher 所以,会执行 watcher.addDep ,然后 addDep 又会执行 dep的addSub 方法
    • 所以 data属性的值,通过闭包,Dep 实例收集到wathcer在dep.subs 数组中
    • watcher 通过 Dep.target 来收集当前需要收集到的依赖(也就是 Dep 实例),收集到 watcher.newDep数组中, 他俩相互依赖到对方
    js 复制代码
     // dep 的 depend 方法
    function depend() {
      if (Dep.target) { // 渲染wathcer 
        Dep.target.addDep(this)
      }
    }
    // watcher 的 addDep 方法
    function addDep(dep: Dep) {
      const id = dep.id
      if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep) // 收集dep
        if (!this.depIds.has(id)) {
          dep.addSub(this) // 让dep 收集watcher
        }
      }
    }
  • getter 就完成了收集依赖

  • setter 通知更新

    • 首先会判断新值和旧值是否相同,相同则不通知 watcher 进行更新
    • 不同的话,会对新值进行数据劫持
    • 之后也是通过闭包中的 dep 来通知更新 dep.notify()
    • notify 会遍历当前 dep 所收集到的 watcher 来进行update
    • update 方法也是把更新的动作放到了一个队列中去,按照浏览器的 EventLoop 事件处理流程来执行
    js 复制代码
      // 首先subs 中存放的都是这个 watcher 依赖的data 数据
      // 所以便利 watcher 进行让所有的watcher 都进行更新
      // dep.notify 函数
      function notify() {
          const subs = this.subs.slice() // 每一个data的key都有一份订阅者列表 
          for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update() // 更新视图
          }
        }
      }
      // watcher update 函数
      function update() {
         queueWatcher(this)
            // 如果Watcher既不是lazy也不是sync,则会执行queueWatcher(this)。这是最常见的更新情况。queueWatcher()函数将当前Watcher放入一个队列(异步更新队列)。Vue会按照一定的策略(如nextTick、微任务等)批量处理队列中的Watcher,依次调用它们的run()方法,实现异步批量更新。这样可以避免短时间内大量数据变化导致的频繁DOM操作,提高整体性能。
       }

【defineReactive函数源码 和 Dep class】(点击展开)

js 复制代码
function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,// 浅的
  mock?: boolean
) {
  const dep = new Dep() 

  const property = Object.getOwnPropertyDescriptor(obj, key)// 获取对象自身属性上的描述符
  if (property && property.configurable === false) { //  configurable 不可改变和删除的
    return
  }

  // cater for pre-defined getter/setters
  // 满足预定义的 getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if (
    (!getter || setter) &&
    (val === NO_INIITIAL_VALUE || arguments.length === 2)
  ) {
    val = obj[key]
    /**
     * obj[key] 的真实内容
     * 1. vm[$attrs]
     * 2. vm[$listeners]
     */
  }
  // shallow 意思是 浅的 所以反是深 子级的观察
  let childOb = !shallow && observe(val, false, mock) // 递归value
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {// 活性 应该是灵活的意思
      const value = getter ? getter.call(obj) : val // 如果 值的描述属性有get 就用userdef 的
      if (Dep.target) { 
          dep.depend() // 用到了闭包
        if (childOb) {
          // 只有对象才有
          childOb.dep.depend()
          // 当访问的对象的时候 如果对象的值是数组 就走这里
          if (isArray(value)) {
            dependArray(value)
          }
        }
      }
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {  // 定制了setter
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // #7981: for accessor properties without setter
        // 对于没有setter的访问器属性
        return
      } else if (isRef(value) && !isRef(newVal)) { // v3
        value.value = newVal
        return
      } else {
        val = newVal
      }
      // 子级的观察者
      childOb = !shallow && observe(newVal, false, mock)
      
      dep.notify() // 通知
    }
  })

  return dep // Dep 实例
}
js 复制代码
class Dep {
  static target?: DepTarget | null
  id: number
  subs: Array<DepTarget>

  constructor() {
    this.id = uid++
    this.subs = []
  }

  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }

  removeSub(sub: DepTarget) {
    remove(this.subs, sub)
  }

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify(info?: DebuggerEventExtraInfo) {
    const subs = this.subs.slice() // 每一个data的key都有一份订阅者列表 
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // 更新视图
    }
  }
}

数据依赖收集总结

每个属性通过 defineReactive 创建一个 dep 实例, getter 时 通过闭包的原理,让每一个 属性中的dep 收集 wathcer,在 dep 收集 watcher 的同时,watcher 也收集 dep,两个实例相互收集对方。

data 中的属性,this 是如何访问和设置的?

此原理比较简单易懂,原理就是代理每个属性到this上,请阅读步骤

  1. 在 new Vue 时,会执行init方法,此方法会对数据进行初始化,
  2. 在初始化 data 属性时,会先获取到组件 data 返回的对象, 并赋值给 this._data
  3. 之后遍历 data 对象的值, 通过 Object.defineProperty 将 this 作为target(目标对象),key作为访问的key,
  4. 重写get 方法,每次get时,都get this._data[key]
  5. 重写set 方法。每次set是,都set this._data[key]
js 复制代码
    function Vue(options) {
      console.log('开始实例化vue')
      this._init(options)
    }
    function proxy(target: Object, sourceKey: string, key: string) {
      sharedPropertyDefinition.get = function proxyGetter() {
        return this[sourceKey][key]
      }
      sharedPropertyDefinition.set = function proxySetter(val) {
        this[sourceKey][key] = val
      }
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    
    proxy(vm, '_data', key)

v-model 指令的原理

  1. 数据绑定 : 当在表单元素上使用 v-model 时,Vue 会在内部将其转化为对元素 value 属性(或其他特定属性,如 <select>value<checkbox>/<radio>checked)的绑定。例如,<input v-model="message"> 相当于 Vue 自动做了 :value="message"(即 v-bind:value="message")的绑定,确保表单元素的显示值与 Vue 实例中 message 数据属性的当前值相匹配。

  2. 事件监听与数据更新v-model 不仅绑定了数据到表单元素的属性,还监听了相应表单元素的特定输入事件(通常是 input 事件,对于某些类型可能为 change 事件),并在事件触发时更新数据属性。当用户在表单元素上进行输入或选择时,会触发相应的事件,Vue 会捕获该事件并提取新的值,然后将这个值赋给绑定的数据属性。例如,对于 <input v-model="message">,每当用户在输入框内输入字符,就会触发 input 事件,Vue 会更新 message 的值。

  3. 数据变化驱动视图更新 : 由于 Vue 实现了数据响应式,当 v-model 绑定的数据属性值发生变化时,Vue 会检测到这一变化并通过其内部的虚拟DOM(VNode)和Diff算法来确定需要对实际DOM进行哪些最小化的更新操作,确保视图与最新数据状态保持一致。这意味着一旦 message 的值在事件处理中被更新,Vue 会自动刷新相关表单元素的显示值。

  4. 修饰符v-model 支持一些修饰符以改变其默认行为。例如,lazy 修饰符会让 Vue 监听 change 而非 input 事件,只在用户完成输入并离开表单元素时才更新数据;number 修饰符会自动将用户的输入值转化为数字;trim 修饰符则会自动去除输入值的首尾空格。

  5. 在自定义组件中的应用 : 当 v-model 用于自定义组件时,Vue 会期望该组件遵循特定的约定(接收名为 value 的属性并触发名为 input 的自定义事件来传递新值)。开发者可以通过组件的 model 选项自定义这些属性和事件的名称,以适应不同的组件设计。

    总结来说,v-model 的原理在于它巧妙地结合了 Vue 的数据绑定(v-bind)和事件监听(v-on)机制,实现了表单元素与数据属性之间的高效、自动化的双向数据流。通过这种简化的语法,开发者可以轻松地在 Vue 应用中创建交互式的表单,无需手动处理繁琐的事件监听和数据同步逻辑。

总结

  • 双向绑定原理就是通过 Observe、Dep、 Watcher 三大类来进行数据劫持和依赖收集
  • this 访问和设置数据也是通过 Object.definePropety 来进行属性操作的
  • v-model 是通过 vue 的数据绑定和事件监听方式实现的

如有错误可以帮忙指出并帮忙解答一下~~~

相关推荐
我爱学习_zwj8 分钟前
ArkTS的进阶语法-4(函数补充,正则表达式)
前端·华为·正则表达式·harmonyos
咔咔库奇41 分钟前
【CSS问题】margin塌陷
前端·javascript·css
无敌最俊朗@1 小时前
c#————委托Action使用例子
java·前端·c#
见过夏天1 小时前
CSS 中渐变色的使用
前端·css
764331 小时前
JavaScript ES6 继承 class
前端·javascript
袁代码1 小时前
SwiftUI开发教程系列 - 第十二章:本地化与多语言支持
开发语言·前端·ios·swiftui·swift·ios开发
软件聚导航2 小时前
在uniapp中使用canvas封装组件遇到的坑,数据被后面设备覆盖,导致数据和前面的设备一样
java·前端·uni-app
好开心332 小时前
javaScript交互补充2(动画函数封装)
开发语言·前端·javascript·html·ecmascript
将登太行雪满山_2 小时前
自存 关于RestController请求传参数 前端和后端相关
java·前端·spring boot
kali-Myon2 小时前
ctfshow-web入门-SSTI(web369-web372)下
前端·python·学习·web安全·flask·web·ssti