vue2源码系列-响应式原理

简介: 之前学习了 vue 实例化及渲染函数的实现,其中的数据初始化 initData 中的 observe(data) 我们没有继续深入分析。因为内容比较多,所以今天单独开了篇来介绍 vue 的响应式原理。对数据劫持与订阅发布比较生疏的,可以先阅读 浅谈订阅发布实现vue打打基础。

之前学习了 vue 实例化及渲染函数的实现,其中的数据初始化 initData 中的 observe(data) 我们没有继续深入分析。因为内容比较多,所以今天单独开了篇来介绍 vue 的响应式原理。

对数据劫持与订阅发布比较生疏的,可以先阅读 浅谈订阅发布实现vue打打基础。

从入口开始

initState -> initData -> observe(data)

observe

我们从 src/core/observer/index.jsobserve 开始

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  // 如果已经监听数据则直接返回
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 实例化监听者
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
复制代码

主要就是实例化 Observer

Observer

我们再来看看 Observer 实例化都做了什么

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 定义__ob__属性
    def(value, '__ob__', this)
    // 数组处理
    if (Array.isArray(value)) {
      // 我们知道Object.defineProperty无法监听数组的改变
      // 所以这边其实是重写了数组方法,使其支持监听,例如splice
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  walk (obj: Object) {
    // 遍历属性
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
复制代码

Observer 的实例化也比较简单,判断为数组时遍历数组进行数据监听。再就是为数据对象的属性遍历调用 defineReactive

defineReactive

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 每个属性会有一个dep实例
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // 这里会存着属性自身的 set get 函数
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 递归子属性进行数据监听
  let childOb = !shallow && observe(val)
  // 重点:数据劫持
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 如有开发者自定义的get则执行
      const value = getter ? getter.call(obj) : val
      // Dep.target作为连接dep和watcher的纽带
      if (Dep.target) {
        // dep添加订阅者Watcher实例
        // Watcher实例的来源后面会说到
        dep.depend()
        // 未知作用:暂不讲解
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 如有开发者自定义的get则执行,注意执行的也是get,因为这里是取值
      const value = getter ? getter.call(obj) : val
      // 判断数据更新 (newVal !== newVal && value !== value) 暂不清楚是干嘛的 猜测是处理某些情况导致数据修改的异常
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
      // 如有开发者自定义的set则执行
        setter.call(obj, newVal)
      } else {
        // 设置新值
        val = newVal
      }
      // 对新值也要进行监听
      childOb = !shallow && observe(newVal)
      // dep进行发布通知 这里将在get中收集的watcher逐一进行通知
      dep.notify()
    }
  })
}
复制代码

defineReactive 的实现其实就是数据劫持的实现,利用 Object.getOwnPropertyDescriptor 对数据的具体属性进行 setget 定义。之后便可以在 get 中进行订阅 dep.depend(),当属性值修改时,进行发布通知 dep.notify()

Dep

defineReactive 函数中,我们实例化了订阅发布中心 const dep = new Dep(),并且调用了 dep.notify 现在我们再来看看 Dep 的实现

let uid = 0
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    // 订阅者列表
    this.subs = []
  }
  // 添加订阅者
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  // 将当前watcher实例作为订阅者添加
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    // 通知订阅者更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// 缓存watcher实例
// 同一时间仅有一个正在实例化的watcher
Dep.target = null
复制代码

Dep 的实现就是个简单的订阅发布中心,同时给 Dep 添加了个静态属性 target 用于缓存当前的 watcher 实例。

Watcher

我们接着再来看看 Watcher 实现,会比较复杂一些

let uid = 0
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // 顾名思义渲染watch
    if (isRenderWatcher) {
      vm._watcher = this
    }
    // _watchers添加当前实例
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    // 初始化配置属性 暂时跳过
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      // expOrFn是函数
      this.getter = expOrFn
    } else {
      // expOrFn是表达式则转化为函数
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
      }
    }
    // 执行get
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get () {
    // 将静态属性设置为当前watcher实例Dep.target = this
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行getter也就是expOrFn
      // expOrFn函数将访问到我们的vm数据,所以将触发我们在defineReactive函数定义的get函数
      // 此处便解答了defineReactive中Dep.target的来源
      // new Watcher -> 执行get -> Dep.target = this -> 执行expOrFn -> 触发数据get函数 -> dep.depend() -> 属性的dep添加订阅者(this)
      // 这样就完成了数据属性的订阅
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // 深度watcher 暂时跳过
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  addDep (dep: Dep) {
    // 在newDeps列表中存储dep
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
  // 清除dep
  cleanupDeps () {
    let i = this.deps.length
    // 仅存在deps中的dep清除订阅
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    // 清空depIds
    // 这边使用了两个数组交换 我猜测是为了节约内存 如果单纯设置 newDepIds = [] 的话需要新开辟内存
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
  // 数据更新
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      // 同步watcher
      this.run()
    } else {
      // 异步watcher
      queueWatcher(this)
    }
  }
  // 执行回调
  run () {
    if (this.active) {
      const value = this.get()
      if (
        // 判断新旧值是否有改变
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        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 {
          // 执行回调
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
  // ...
}
复制代码

通过阅读 Watcher 实现,我们可以大致了解其主要功能,在实例化的时候将调用 get 方法,在 get 方法中定义 Dep.target = this 将实例赋值给 Dep.target。之后再调用 getter 函数将访问 vm 数据属性的 get 函数,在 get 中调用 dep.depend() 实现订阅。

同时 watcher 实例有一些其它的方法,当前面我们说的数据改变触发属性 set 函数时,将通知 dep 进行发布通知,此时将会调用到订阅了该 dep 的所有 watcherupdate 函数。通过 update 函数我们可以看到如果时同步 watcher 其主要就是直接调用 run 函数进行回调函数 cb 的执行。

流程梳理

通过前面源码的分析,我们来梳理下 订阅发布模式 的实现

  1. 首先对数据 data 调用 Observer 进行实例化进行数据监听
  • 遍历递归数据属性,调用 defineReactive
  • 每个属性生成唯一 dep 订阅发布中心实例
  • 劫持 get,在 get 中调用 dep.depend() 收集订阅者
  • 劫持 set,在 set 中调用 dep.notify() 发布通知
  1. Dep 订阅发布中心的实现,具备收集订阅者及发布通知的能力
  2. Watcher 实例化生成订阅者,在实例化的同时访问数据 get 属性,通过 Dep.target 纽带完成订阅
  3. 开发者修改数据 Data 触发 set 通知,订阅中心 dep 依次调用 watcherupdate 函数,update 中执行回调函数完成整个流程
  4. 96.png

结语


本文从实际源码出发,研究了 Vue 内部的响应式实现,收获满满。但虽然我们弄清楚了其整个流程,还有很多细节我们是没有去深入研究分析的,例如 Watcher 实例化的各种参数配置,及同步异步 Watcher 的区别及实现,后面我们将继续分析继续嗨。




相关文章
|
2天前
|
JavaScript
Vue3中props的原理与使用
Vue3中props的原理与使用
10 0
|
2天前
|
JavaScript 前端开发 开发者
响应式原理:Vue 如何跟踪数据变化
【4月更文挑战第22天】Vue 的响应式系统是其核心,通过数据双向绑定实现视图与数据同步。依赖收集和观测数据使Vue能跟踪变化,变化通知组件更新视图。高效的更新策略如批量更新和虚拟DOM提升性能。组件化和可组合性支持有效通信和代码复用,强调数据驱动开发。开发者应合理组织数据、谨慎处理变更并充分利用组件化优势,以提高效率和用户体验。
|
2天前
|
JavaScript 前端开发
vue中nextTick使用以及原理
vue中nextTick使用以及原理
7 0
|
2天前
|
JavaScript 前端开发
深入了解前端框架Vue.js的响应式原理
本文将深入探讨Vue.js前端框架的核心特性之一——响应式原理。通过分析Vue.js中的数据绑定、依赖追踪和虚拟DOM等机制,读者将对Vue.js的响应式系统有更深入的理解,从而能够更好地利用Vue.js构建灵活、高效的前端应用。
|
2天前
|
开发框架 JavaScript 算法
了解vue3的基本特性和底层原理
Vue3的底层原理涵盖了响应式系统的Proxy-based实现、组件的模板编译与渲染更新机制、组合式API带来的逻辑组织变革,以及其他关键特性的具体实现。这些原理共同构成了Vue3强大、高效、灵活的现代前端开发框架基础。
28 2
|
2天前
|
JavaScript
Vue3中props的原理与使用
Vue3中props的原理与使用
|
2天前
|
JavaScript 前端开发 开发者
Vue的响应式原理:深入探索Vue的响应式系统与依赖追踪
【4月更文挑战第24天】Vue的响应式原理通过JavaScript getter/setter实现,当数据变化时自动更新视图。它创建Watcher对象收集依赖,并通过依赖追踪机制精确通知更新。当属性改变,setter触发更新相关Watcher,重新执行操作以反映数据最新状态。Vue的响应式系统结合依赖追踪,有效提高性能,简化复杂应用的开发,但对某些复杂数据结构需额外处理。
|
2天前
|
JavaScript 前端开发 开发者
浅谈Vue 3的响应式对象: ref和reactive
浅谈Vue 3的响应式对象: ref和reactive
|
2天前
|
JavaScript 前端开发 API
Vue中v-model的原理
Vue中v-model的原理
|
2天前
Vue3 响应式数据 reactive使用
Vue3 响应式数据 reactive使用