vue源码分析-13-数据响应式原理

223 阅读7分钟

数据响应式原理

在之前的章节中,我们讲解了Vue的实例化,从模版编译-> ast -> render函数 -> vnode -> 初次渲染。

但是界面初次渲染之后,我们需要使用系统,在使用系统的过程中,我们会修改掉许多数据,在修改的同时,界面会随机发生变化,这就是所谓的数据响应式,也就是界面随着数据的变化而变化,我们只需修改数据,界面就会跟着响应,而不需要手动写代码再修改界面。

数据响应式最典型的就是当我们改变options.data中定义的值的时候,绑定在界面上的值就会跟着发生变化。那么Vue框架是如何处理data这个函数返回的数据(一般是返回一个对象)呢?

initState

Vue的_init方法中,initState(vm)方法中初始化了props,methods,data,computed,watch

export function initState (vm: Component) {
  // 定义watcher
  vm._watchers = []
  const opts = vm.$options
  // 初始化props
  if (opts.props) initProps(vm, opts.props)
  // 初始化methods
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // 初始化数据
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化计算属性
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化监听方法
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initData

接下来我们优先着重分析Vue是如何处理data,实现数据响应式的,initData(data)方法主要是调用了observe()方法

function initData (vm: Component) {
  // 获得data数据
  let data = vm.$options.data
  // 如果是函数,通过getData拿到data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
    // 如果不是对象,报错
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  /*
  省略非重要代码
  */
  // observe data
  // 调用observe方法
  observe(data, true /* asRootData */)
}

observe

接下来我们看一下observe(data, true)方法,此方法主要调用了new Observer(value)方法,实例化之后赋值给data._ _ ob _ _

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 如果data不是对象/数组 或者 是一个Vnode,直接返回,不做处理
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 如果value有__ob__属性,并且是由Observer构造出来的,就拿到这个属性
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    // 如果可监听,因为有时候可以关闭将data处理成响应式对象
    shouldObserve &&
    // 不是服务端渲染
    !isServerRendering() &&
    // 如果是数组或者对象
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    // 不是vue的实例
    !value._isVue
  ) {
    // new一个Observer对象
    ob = new Observer(value)
  }
  // 如果已经存在Observer对象实例了,一般情况下初始化是没有的
  if (asRootData && ob) {
    ob.vmCount++
  }
  // 将Observer对象实例返回
  return ob
}

new Observer(data)

接下来我们分析一下new Observer(data)发生了什么 ,Observer的构造方法主要是实例化了一个Dep对象,如果是数据是数组,递归调用observe()方法,如果不是数组,就遍历对象的key,调用defineReactive方法,使之成为响应式的数据

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属性
    this.dep = new Dep()
    // 一个data对象,作为vue实例根数据的次数,也就是多个vue实例可以使用同一份data数据
    this.vmCount = 0
    // 将Observer实例,赋值给value.__ob__
    def(value, '__ob__', this)
    // 如果value 是数组的话,这里主要是重写了数组的原型方法,使数组也能实现数据响应式,
    // 即push(),shift(),pop()等方法调用后,界面可以跟着改变
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 如果是数组,又会递归调用observe(value)方法,将数组的每一项作为参数
      this.observeArray(value)
    } else {
      // 如果不是数组,遍历,并调用defineReactive方法,使之成为响应式的数据
      this.walk(value)
    }
  }
  
   /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }

defineReactive

接下来我们着重分析一下defineReactive方法

defineReactive方法作用就是给属性设置get和set方法,每个属性值都会创建一个依赖收集对象dep,每当调用属性的get方法,就会调用dep.depend()方法进行依赖收集的过程。

每个组件会创建一个Watcher对象,如果某个组件使用了一个数据(也就是调用了该属性的get方法),那么这个数据的dep.subs就会保存这个组件的Watcher对象,这就是依赖收集的过程。

当我们给属性设置值的时候,会调用该属性的set方法,就会调用该数据的dep对象的notify()方法,notify()方法会触发所有保存在dep.subs中的Watcher对象(即观察者)的update方法,触发更新操作。

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 定义依赖收集对象
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果属性不可配置,直接返回
  if (property && property.configurable === false) {
    return
  }

  // 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]
  }

  // 递归调用observe(val)方法将 val是数组或者对象变为响应式数据
  let childOb = !shallow && observe(val)
  // 定义属性的getter方法
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // Dep.target指向当前正在渲染的组件的Watcher实例子
      if (Dep.target) {
        // 如果在组件渲染过程中用到了data中的响应式数据,也就会调用改属性的get方法,
        // 那么就会进行依赖收集的过程,
        // 其实也就是将当前正在渲染的组件的watcher对象,添加到dep.subs中
        // 注意这里的dep对象因为闭包不会被销毁
        dep.depend()
        // 如果该数据是个数组或对象,那么同样会递归进行依赖收集的过程
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      /*省略非重要代码*/
      // 通知watcher更新
      dep.notify()
    }
  })
}

依赖收集Dep

上述分析中出现了Dep对象,和Watchaer对象,接下来我们先大致了解一下这两个类

其实依赖收集的过程就是以一个发布订阅者为模型而实现的,dep扮演的角色就是订阅中心,每个数据都有一个dep(订阅中心),Watcher就是订阅者(观察者),如果订阅者订阅了某一份数据(发布者),那么这份数据的dep就会记录下该订阅者,当这份数据(发布者)有修改的时候,他会告诉订阅中心dep,通知所有的观察者数据已经修改了。

export default class Dep {
  // 静态的实例,类似全局变量,当前正在计算的Watcher
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    // 依赖收集对象id
    this.id = uid++
    // 保存依赖于该数据的观察者Watcher
    this.subs = []
  }

  // 添加依赖该数据的观察者对象
  addSub (sub: Watcher) {
    this.subs.push(sub)

  }

  // 移除观察者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  // 依赖收集
  depend () {
    // Dep.target指向当前正在渲染的组件的Watcher实例子
    if (Dep.target) {
      // this指向dep的实例
      Dep.target.addDep(this)
    }
  }

  // 通知观察者触发更新
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // 调用Watcher.update方法
      subs[i].update()
    }
  }
}

观察者Watcher

我们知道每一个数据都会生成一个dep对象(订阅中心),订阅中心会收集并记录下所有的数据订阅者Watcher,那么我们看看Watcher这个类吧。

每个组件在实例化的时候,调用$mount方法,此方法会经过 模版编译-> ast -> Vnode -> 生成真实dom的过程,在mountComponent方法的最后,会实例化一个Watcher对象,我们称之为渲染Watcher

/* istanbul ignore if */
 
    updateComponent = () => {
      // 先调用_render()方法生成vnode 然后调用_update方法,更新真实dom
      vm._update(vm._render(), hydrating)
    }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

我们看一下Watcher类的构造函数,其实就是定义了一些属性,最后调用了this.get方法

constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    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') {
      this.getter = expOrFn
    } else {
      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
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

get()方法中主要逻辑就是执行this.getter,this.getter是在构造方法中做了处理并设置,如果是渲染时$mount方法中创建的渲染Watcher,this.getter就是传入的_update的一个渲染真实dom的方法,总之get()方法就是先执行完整的首次渲染操作。

/**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    // 设置当前正在渲染的组件的渲染Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // getter是在构造方法中设置的
      // 执行getter方法,其实就是传入的_update方法,执行其可以生成vnode,首次渲染时创建真实dom
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

接下来我们看一些Watcher类的一些重要方法

addDep方法就是将当前的Watcher实例,push到dep.subs这个数组中,记录下订阅了dep的观察者,此方法会在响应式对象属性的getter方法中被调用,以记录下订阅了该数据的订阅者们

/**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // this指向Watcher实例
        // Watcher实例添加至Dep的subs中
        dep.addSub(this)
      }
    }
  }

update方法,如果是同步渲染,那么会执行this.run方法立即重新渲染,如果是异步渲染,那么会将渲染的Watcher推到异步队列中,延迟重新计算并渲染,一般情况下我们都是使用的异步渲染,也就是说频繁修改数据,只会触发一次 执行render函数 -> 生成新的vnode -> 更新dom 的过程,异步更新的实现将在专门的章节讲解,这里我们只要知道Watcher的update方法能响应数据的变化,重新渲染界面就可以了。

/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 通常情况会走异步队列更新
      queueWatcher(this)
    }
  }

示例

接下来我们以一个示例来说明Vue的数据响应式的过程

首先我们的代码是这样的,为了方便演示效果,我们将子组件和根实例使用同一份data。当我们点击p标签的时候,message的值会发生变化,界面也会随着发生变化

<body>
    <div id="app">
        <p @click="click">{{message}}</p>
        <comp1></comp1>
    </div>
</body>
<script>
     let data = {
         array: [1,2,3],
         message: "hello vue",
     }
     var app = new Vue({
         el: "#app",
         data: data,
         methods: {
             click () {
                 this.message = "Hello World"
             }
         },
         components: {
             comp1: {
                 template: '<h1>{{message}}</h1>',
                 props: {
                     prop1: String
                 },
                 data: function () {
                     return data
                 },
             }
         },
     })

以上代码有两个组件,一个是根组件,一个是comp1组件,这两个组件都会使用同一份数据,那就是data.message。

在初始化数据的过程中,observe方法会将data中的属性都变为响应式属性,也就是给data中的数据设置getter和setter方法。在渲染界面的时候,会用到data.message这个值,那么就会调用message这个属性的getter方法,getter方法中会进行依赖收集的过程,我们在getter方法中打印出该属性和创建的dep对象

Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          // 打印
          console.log(dep, value)
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },

image

由上图我们可以看到,打印出了两个Dep对象,由于我们是使用的同一份数据,其实这两个dep对象是同一个,因为根组件和子组件Comp1都使用的data.message这个值,所以会调用两次getter方法。

我们再观察Dep中的subs中保存了两个Watcher对象,这两个Watcher对象就是根组件和Comp1子组件在实例化的时候创建的,因为这两个组件都使用了data.message这个数据,所以dep就会收集依赖,将Watcher 观察者们保存起来。

至此依赖收集的过程就算结束了。

那么当数据发生变化的时候,是如何通知到所有的数据订阅者呢?

当数据发生变化时(示例中是点击p标签会改变data.mesage的值),会调用该属性值的setter方法, setter方法中会调用dep实例的notify方法

set: function reactiveSetter (newVal) {
       // 代码省略
        dep.notify();
      }

dep.notify方法中其实就是遍历dep.subs中的Watcher,调用每一个Watcher的update方法,这样就可以通知到根组件和Comp1子组件重新渲染界面了。

Dep.prototype.notify = function notify () {
    // 代码省略
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  };

其实依赖收集(订阅)和派发更新(发布)的过程很简单。重点在于理解发布订阅者模式。

总结

Vue的响应式原理其实就是界面可以响应数据的变化随即跟着从新渲染,而不需要我们手动再更新界面。

image

实现数据响应式的关键就在于 对数据的处理,给对象的属性设置了getter和setter方法,在使用到该数据的地方会调用getter方法,每个数据会维护一个dep对象(类比订阅中心),该对象中保存了使用该数据的所有观察者(订阅者),当该数据(发布者)更新(发布更新)的时候,会通知到所有dep(订阅中心)中保存的观察者们,告诉他们数据发生了变化, 观察者们接收到数据发生变化的消息,就会调用update方法进行界面的重新渲染。