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 的区别及实现,后面我们将继续分析继续嗨。




相关文章
|
1月前
|
JavaScript 前端开发 Serverless
Vue.js的介绍、原理、用法、经典案例代码以及注意事项
Vue.js的介绍、原理、用法、经典案例代码以及注意事项
58 2
|
11天前
|
JavaScript
Vue中ref创建_基本类型的响应式数据,在Vue2的年代,数据配在data里,Vue3的区别是不把响应数据写在data里,那个数据是响应式的用ref包一下,let name = ref(“张三“)
Vue中ref创建_基本类型的响应式数据,在Vue2的年代,数据配在data里,Vue3的区别是不把响应数据写在data里,那个数据是响应式的用ref包一下,let name = ref(“张三“)
|
17天前
|
JavaScript 前端开发 程序员
探索Vue.js宝库:解锁基础知识与实用技能之门(1. 数据绑定与响应式 2. 条件与循环 3. 组件化开发;1. 路由管理与导航 2. 状态管理与Vuex 3. Vue.js的生命周期)
探索Vue.js宝库:解锁基础知识与实用技能之门(1. 数据绑定与响应式 2. 条件与循环 3. 组件化开发;1. 路由管理与导航 2. 状态管理与Vuex 3. Vue.js的生命周期)
22 1
|
28天前
|
JavaScript 前端开发 API
什么是响应式❓Vue2/Vue3中响应式的原理
什么是响应式❓Vue2/Vue3中响应式的原理
28 2
|
28天前
|
存储 JavaScript API
Vue状态管理深度剖析:Vuex vs Pinia —— 从原理到实践的全面对比
Vue状态管理深度剖析:Vuex vs Pinia —— 从原理到实践的全面对比
44 2
|
9天前
|
API
vue3 原理【详解】Proxy 实现响应式
vue3 原理【详解】Proxy 实现响应式
17 0
|
9天前
|
存储 JavaScript 前端开发
vue3【实用教程】声明响应式状态(含ref,reactive,toRef(),toRefs() 等)
vue3【实用教程】声明响应式状态(含ref,reactive,toRef(),toRefs() 等)
21 0
|
10天前
|
JavaScript API
Vue数据动态代理机制的实现以及响应式与数据劫持
Vue数据动态代理机制的实现以及响应式与数据劫持
11 0
|
11天前
|
JavaScript
Vue3的使用,ref定义基本类型的响应式数据,如何创建对象类型的响应式数据,let car = {brand: ‘奔驰‘,price: 100},{{car.brand}},reactive的使用
Vue3的使用,ref定义基本类型的响应式数据,如何创建对象类型的响应式数据,let car = {brand: ‘奔驰‘,price: 100},{{car.brand}},reactive的使用
|
12天前
|
JavaScript 前端开发 API