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 的数据绑定和事件监听方式实现的

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

相关推荐
辻戋1 小时前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保1 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun2 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp2 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.3 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl5 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫6 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友6 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理8 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻8 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js