Vue源码系列(三):数据响应式原理🔥🔥

6,663 阅读8分钟

一个人的价值和报酬不是和你的劳动成正比,而是和你劳动的不可替代性成正比!
空气很重要,谁离了它也活不了,可为什么没有价值呢?因为它不具备稀缺性到处都是!
所以一个人想体现自己的价值,一定要具备某种稀缺性,比如精通一项技术。

前言

这是一个Vue源码系列文章,建议从第一篇文章 Vue源码系列(一):Vue源码解读的正确姿势 开始阅读。 文章是我个人学习源码的一个历程,这边分享出来希望对大家有所帮助。

文章持续更新中,期望大家点赞加关注 🙏


做响应式原理的初衷

在上一篇文章 Vue源码系列(二):Vue初始化都做了什么? 出完之后,其实我也在想下一步该从那个地方入手源码系列的文章,说来也巧,今早正好有人来面试,面试的是一个妹子,整体感觉反正就很程序媛,腼腆的表情,朴实无华的样子,一看就是我喜...... 咳~ 一看就是个有务实精神且工作踏实认真的人。哎~ 扯远了!
在面试过程中整体都不错,在问道Vue这里的东西的时候,我也的问了一些Vue常规的面试题,比如:一个组件的 data 选项为什么必须是一个函数?对SPA单页面的理解及它的优缺点vue数据响应式的原理 等。
在通过以上问题我发现她应该是没有看过vue的源码的,或者说没有太深入的去看过,基本只停留在一搜一大堆的面试题的程度。尤其在数据响应式这一块只知道是用 Object.defineProperty 来实现的,在深入就不知道了。但最终还是在她整个人给我的一种踏实的感觉和技术还可以的基础上我让她进了三面。
也是因为这次面试吧,我觉得从数据响应式原理开始继续说源码解析。


入口查找

MVVM框架的三要素:数据响应式、模板引擎及其渲染。
因此数据响应式是MVVM框架的一个特性,所有的MVVM框架都需要做数据响应式,只是各自的策略不一样。Angular是它的脏检测机制,React是有明确的一些api可以供开发者调用,Vue则是一种被动检测,其特点就是利用Object.defineProperty(),通过定义对象属性getter/setter拦截对属性的获取和设置。
具体如何实现的呢?首先需要考虑一下从哪里入手去看源码。在上一篇文章Vue源码系列(二):Vue初始化都做了什么?中,说到了一个方法叫:initState(),当时给它的解释是:对props,methods,data,computed,watch进行初始化,包括响应式的处理。

代码块 1

// src/core/instance/init.js

... 
// 初始化方法:_init中的部分代码

initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) 
initState(vm) // 对props,methods,data,computed,watch进行初始化,包括响应式的处理
initProvide(vm) 
callHook(vm, 'created')
...


源码分析

很显然Vue的数据响应式肯定是在这里边,所以咱们就从这里入手。首先找到对应的文件和方法

initState

代码块 2

//数据响应式的入口
export function initState (vm: Component) {
  vm._watchers = []
  // 从Vue实例上拿到配置项,并对配置项做以下处理:
  const opts = vm.$options
  
  // 1、处理props对象,并把props对象的属性设置为响应式,且代理到vm实例上,使我们可以用this.props[key]的方式去获取
  if (opts.props) initProps(vm, opts.props)
  
  // 2、处理methods对象,首先和props对象做重复处理,且优先级props > methods
  // 然后把methods对象的属性赋值到vm实例上,使我们可以用this.methods[key]的方式去获取
  if (opts.methods) initMethods(vm, opts.methods)
   
  // 3、处理data对象,首先和props对象、methods对象做重复处理,且优先级props > methods > data
  // 然后给data对象的属性设置为响应式,且把data对象的属性代理到vm实例上,使我们可以用this.data[key]的方式去获取
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  
  // 4、首先为每个计算属性(computed)创建一个内部观察者(watcher),因为computed都是通过watcher来实现的
  // 然后和props对象、methods对象、data对象做重复处理,且优先级props > methods > data > computed
  // 最后把computed对象的属性代理到vm实例上,使我们可以用this.computed[key]的方式去获取
  if (opts.computed) initComputed(vm, opts.computed)
  
  // 5、和computed对象一样,watch对象的属性也都需要创建watcher实例,也需要对watch对象的属性进行属性处理。
  // 需要注意的是computed对象是懒执行,而watch对象是可以配置的,如果设置了immediate为true,则立即执行
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

以上就是Vue数据响应式的入口介绍,接下来我们以data对象的响应式为例,开始看看Vue的响应式都是如何做的。

代码块 3

  // 给data对象的属性设置为响应式,且把data对象的属性代理到vm实例上,使我们可以用this.data[key]的方式去获取
  if (opts.data) {
    initData(vm)
  } else {
    // 响应式处理
    observe(vm._data = {}, true /* asRootData */)
  }

处理响应式的入口:observe()

接着对Vue响应式处理的入口做一个代码解析

代码块 4

// Vue处理响应式的入口
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 如果是非对象和虚拟dom 则不做响应式的处理 直接返回 
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  
  // 'Observer'是一个真正的观察者,具体做了什么咱们【代码块 5】再说
  let ob: Observer | void
  
  // 首先'__ob__'的值其实就是一个'Observer'实例
  // 所以下面的判断其实就是:如果已经做过了响应式处理(已经被观察过了),则直接返回'ob',也就是'Observer'实例
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  // 如果是初始化的时候,则没有'Observer'的实例,因此需要创建一个'Observer'实例
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 创建一个'Observer'(观察者)实例,初始化传入需要做响应式的对象
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer(观察者)

下面是对 Observer(观察者)的代码解析

代码块 5

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
    // 实例化一个dep
    // 正常来说,遍历一个对象的属性时,都是一个属性创建一个dep,为什么此处要给当前对象额外创建一个dep?
    // 其目的在于如果使用Vue.set/delete添加或删除属性,这个dep负责通知更新。
    this.dep = new Dep()
    this.vmCount = 0
    // 为value对象设置 __ob__
    def(value, '__ob__', this)
    
    // 学习源码之前我相信大部分的小伙伴已经知道了 Vue2的响应式是根据 Object.defineProperty() 来实现的,不能监听数组的改变。所以下面做了一个数组和对象的判断
    if (Array.isArray(value)) {
      // 数组的响应式处理
      // 如果是现代的浏览器
      if (hasProto) {
        // protoAugment方法其实就是做一个原型的覆盖,那怎么去覆盖一个对象的实例的原型呢?通过覆盖'__proto__'就能实现,只会影响当前数组的实例
        // arrayMethods 又是什么?数组具体是怎么做响应式处理的呢?咱们看【代码块 6】
        protoAugment(value, arrayMethods)
      } else {
        // 如果的老的IE浏览器,没有原型,那该怎么办?
        // copyAugment方法 主要数就是做:毫不讲道理的直接给数组上覆盖几个方法,则就会把需要覆盖的方法全部替换掉,就硬换掉。【老IE说:年轻人不讲武德!!】
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else { 
      // 对象的响应式处理,见下面的walk方法
      this.walk(value)
    }
  }

  // 对象的响应式处理方法
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 遍历对象中的key,并对key做响应式处理
      // 具体 defineReactive 方法怎么做对象的响应式,见【代码块 7】
      defineReactive(obj, keys[i])
    }
  }

  // 数组的处理方法
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      // 遍历数组,为数组的每一项设置观察,处理数组元素为对象的情况
      observe(items[i])
    }
  }
}

数组的响应式处理:arrayMethods

数组具体是怎么做响应式处理的呢?咱们来看一下

代码块 6


import { def } from '../util/index'
// 获取数组的原型
const arrayProto = Array.prototype
// 克隆一份,为什么要克隆一份呢?因为如果直接更改数组的原型,那么将来所有的数组都会被我改了。
export const arrayMethods = Object.create(arrayProto)
// 需要覆盖的7个方法,为什么只有7个方法呢?因为只有这7个方法改变了原数组
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

// 如何覆盖这7个方法呢?
// 首先开始循环它们
methodsToPatch.forEach(function (method) {
  // 每次从原型中拿出原始的方法
  const original = arrayProto[method]
  // 将当前的对象重新定义它的属性,如何重新定义呢?
  def(arrayMethods, method, function mutator (...args) {
    // 首先 先执行原始行为,以前咋滴现在就咋滴
    const result = original.apply(this, args)
    
    // 然后 再做变更通知,如何变更的呢?
    // 1、获取ob实例
    const ob = this.__ob__
    // 2、如果是新增元素的操作:比如puah、unshift或者增加元素的splice操作
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 3、新加入的元素依然需要做响应式处理
    if (inserted) ob.observeArray(inserted)
    // 4、让内部的dep去通知更新
    ob.dep.notify()
    
    return result
  })
})




对象的响应式处理:defineReactive

下面看一下具体的 defineReactive 方法怎么做对象的响应式

代码块 7

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 首先创建一个和key一一对应的dep,这里需要注意一下 一个key就对应着一个Dep
  const dep = new Dep()

  // 然后是一些getter/setters的设定,正常也很少设置,所以不用关心这个
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  
  // 递归遍历
  // 如果已经做了响应式就返回出来 '__ob__', 如果是一个新对象就创建一个实例返回出来
  let childOb = !shallow && observe(val)
  //拦截对obj[key]的获取和设置
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 拦截对obj[key]的获取操作
    get: function reactiveGetter () {
      // 拿到obj[key]的值
      const value = getter ? getter.call(obj) : val
      // 依赖收集
      // 如果存在,则说明此次调用触发者是一个Watcher实例
      if (Dep.target) {
        // 依赖关系的创建,建立dep和Dep.target之间的依赖关系(把dep添加到watcher中,也将watcher添加到dep中)
        dep.depend()
        if (childOb) {
          //  也是依赖关系的创建,只是建立是ob内部的dep和Dep.target之间的依赖关系,也就是嵌套对象的依赖收集
          childOb.dep.depend()
          // 如果是数组,数组内部的所有项都需要做以上相同的依赖收集处理
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    
    // 拦截对obj[key]的设置操作
    set: function reactiveSetter (newVal) {
    
      // 获取老的值
      const value = getter ? getter.call(obj) : val
      
      // 如果新值和老值相等则不做处理 直接返回
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      
      // 如果setter不存在,说明只能获取不能设置,也直接返回
      if (getter && !setter) return
      
      // 设置为新的值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      
      // 对新的值也做响应式处理
      childOb = !shallow && observe(newVal)
      
      // 通知更新
      dep.notify()
    }
  })
}

dep

看看dep是如何依赖管理,变更通知的

代码块 8
// src/core/observer/dep.js


// Dep在数据响应式中扮演的角色就是数据的依赖收集和变更通知
// 在获取数据的时候知道自己(Dep)依赖的watcher都有谁,同时在数据变更的时候通知自己(Dep)依赖的这些watcher去执行他们(watcher)的update 
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    //每个Dep都有唯一的ID
    this.id = uid++
    //subs用于存放依赖
    this.subs = []
  }
  
  // 在dep中添加watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  
  // 删除dep中的watcher
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  
  // 反过来 在watcher中添加dep,详细的一会查看 【代码块 9】中的 addDep 方法
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 遍历dep的所有watcher 然后执行他们的update 
  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()
    }
  }
}


Dep.target = null
const targetStack = []

// 开始收集的时候 设置:Dep.target = watcher
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
// 结束收集的时候 设置:Dep.target = null
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

watcher

看看watcher是如何被触发的

代码块 9
// src/core/observer/watcher.js

// 首先回顾一下上面的代码解析咱们可以总结得出:

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
    if (isRenderWatcher) {
      vm._watcher = this
    }
    // 当前Watcher添加到vue实例上
    vm._watchers.push(this)
    
    // 参数配置,options默认false
    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()
      : ''
    
    // 如果exporfn是函数的话,就会把这个函数赋值给getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 如果不是函数是字符串的话,会调用parsePath方法,
      // parsePath方法会把我们传入的path节分为数组,通过patch来访问到我们的对象。
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    // Vue的设计上,Watcher不止会监听Observer,还会直接把值计算出来放在this.value上。
    // 这里lazy没有直接计算,但是取值的时候肯定要计算的,所以我们直接看看get方法
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  
  get () {
    // 这里的pushTarget函数不是清除,而是把this作为一个参数传进去,其结果为:Dep.target = this
    // 将Dep的target添加到targetStack,同时Dep的target赋值为当前watcher对象
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用updateComponent方法
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) {
        traverse(value)
      }
      // update执行完成后,又将Dep.target从targetStack弹出, 其结果为:Dep.target = null
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  // 添加依赖
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // watcher添加它和dep的关系
      this.newDepIds.add(id)
      this.newDeps.push(dep)

      if (!this.depIds.has(id)) {
        // 和上面的反过来,dep添加它和watcher的关系
        dep.addSub(this)
      }
    }
  }

  // 清理依赖项收集
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    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) {
      // 如果是懒执行走这里,比如:computed
      this.dirty = true
    } else if (this.sync) {
      // 如果是同步执行 则执行run函数
      this.run()
    } else {
      // 将watcher放到watcher队列中 具体实现查看 【代码块 10】
      queueWatcher(this)
    }
  }

  // 更新视图
  run () {
    if (this.active) {
      // 调用get方法
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // 更新旧值为新值
        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 {
          // 渲染watcher
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  // 懒执行的watcher会调用该方法 比如:computed
  evaluate () {
    this.value = this.get()
    // computed的缓存原理
    // this.dirty设置为false 则页面渲染时只会执行一次computed的回调
    // 数据更新以后 会在update中重新设置为true
    this.dirty = false
  }

  // 依赖这个观察者收集的所有deps
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  // 从所有依赖项的订阅者列表中把自己删除
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

queueWatcher方法的实现

代码块 10

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  /**
   * 当我们调用某个watcher的callback之前会先将它在has中的标记置为null
   * 
   * 注意 这里是==而不是===
   * 如果has[id]不存在,则has[id]为undefined,undefined==null结果为true
   * 如果has[id]存在且为null,则为true
   * 如果has[id]存在且为true,则为false
   * 这个if表示,如果这个watcher尚未被 flush 则 return
   */
  if (has[id] == null) {
    // 再次把watcher置为true
    has[id] = true
    if (!flushing) {
      // 如果当前不是正在更新watcher数组的话,那watcher会被直接添加到队列末尾
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      // 这个循环其实是在处理边界情况。 即:在watcher队列更新过程中,用户再次更新了队列中的某个watcher
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

总结一下

到此Vue的数据响应式原理结束,简单总结一下

数据响应式原理

首先

Vue的数据响应式原理其核心就是通过Object.defineProperty来拦截对数据的获取和设置

其次

Vue的响应式数据分为两类:对象和数组

对象

遍历对象的所有属性,并为每个属性设置getter和setter,以便将来的获取和设置,如果属性的值也是对象,则递归为属性值上的每个key设置getter和setter
获取数据时:在dep中添加相关的watcher
设置数据时:再由dep通知相关的watcher去更新

数组

覆盖了原有的7个改变了原数组的方法,并克隆了一份,然后在克隆的这一份上更改自身的原型方法,然后拦截对这些方法的操作
添加新数据时:需要进行数据响应式的处理,再由dep通知watcher去更新
删除数据时:也要由dep通知watcher去更新

——END

最后希望看完的各位大佬们点赞👍🏻 、 收藏⭐️ 加关注➕ !
下一篇文章手撸一个简易版的Vue响应式:Vue源码系列(四):手写Vue2.X、Vue3.X数据响应式原理和区别