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




相关文章
|
17天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
32 9
|
19天前
|
API
vue3知识点:响应式数据的判断
vue3知识点:响应式数据的判断
25 3
|
2月前
|
缓存 JavaScript 前端开发
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
该文章全面覆盖了Vue.js从基础知识到进阶原理的48个核心知识点,包括Vue CLI项目结构、组件生命周期、响应式原理、Composition API的使用等内容,并针对Vue 2与Vue 3的不同特性进行了详细对比与讲解。
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
|
23天前
|
缓存 JavaScript UED
优化Vue的响应式性能
【10月更文挑战第13天】优化 Vue 的响应式性能是一个持续的过程,需要不断地探索和实践,以适应不断变化的应用需求和性能挑战。
29 2
|
27天前
|
JavaScript 前端开发
Vue 2 和 Vue 3 之间响应式区别
10月更文挑战第7天
32 2
|
1月前
|
JavaScript 前端开发 网络架构
如何使用Vue.js构建响应式Web应用
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用
|
1月前
|
JavaScript 前端开发
如何使用Vue.js构建响应式Web应用程序
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用程序
|
1月前
|
JavaScript UED
Vue双向数据绑定的原理
【10月更文挑战第7天】
|
1月前
|
JavaScript 前端开发 数据安全/隐私保护
前端技术分享:使用Vue.js构建响应式表单
【10月更文挑战第1天】前端技术分享:使用Vue.js构建响应式表单
|
19天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
23 0