浅析Vue响应式

Vue 响应式是什么

Vue 是一个 MVVM 的框架,即 Model-View-ViewModel,Model 与 View 之间不直接联系,而是由 ViewModel(相当于 Pipe) 去监听 Model 的变化并触发 View 改变以及监听 View 中的事件操作响应的 Model

我们来看一段简单的双向绑定的例子:




在这个例子中,

均为 view,msg 为 model,而 Vue 则是我们的 ViewModel。
Vue 提供了两个工具:

  • DOMListener:监听页面的 DOM 事件,修改 Model 中的数据

  • DataBinding:监听 Js 中的数据变化,修改 View 视图

Vue 如何实现响应式

先来了解几个名词

  1. Observer 观察者(监听器),每个可监听对象都会挂载一个观察者实例,负责订阅数据变化,通知对应的 Dep 实例

  2. Dep 消息订阅器(依赖收集器),负责依赖收集,管理 watcher,依赖收集操作在获取数据时执行(defineReactive > get)

  3. Watcher 订阅者,负责响应,Vue中有三种

    • User Watcher 组件的 watch 中定义的 watcher

      于 initWatch > createWatcher 中初始化

    • Computed Watcher 组件的 computed 中定义的 watcher

      于 initComputed 中初始化

    • Render Watcher 渲染 Watcher,只要有数据变化,最终都会由 Render Watcher 触发页面更新

      于 mountComponent 方法中 beforeMount 与 mounted 钩子之间初始化

再来看一下响应的流程

来自官网的盗图

reactive.png

稍微加工了一下,看下图


my-reactive.png

实线部分为内部实现;虚线部分为响应的流程。

结合上图,简单来说就是在 DOM 上操作数据(如 input )时,会被 Observer 中定义的 setter 函数劫持并调用 notify 函数通知消息订阅器 Dep,Dep 遍历其 subs 数组对所有的订阅者 Watcher 调用 update 函数,update 函数通过一系列操作更新 DOM

这一系列操作包括:

  1. queueWatcher 将当前 Watcher 放入待更新的 Watcher 队列中
  2. flushSchedulerQueue 依次执行队列中 Watcher 的 run 函数
  3. run 函数中const value = this.get()调用 Watcher 的 get 函数
  4. get 函数中执行value = this.getter.call(vm, vm)调用 Watcher 中定义的 expression
    • Render Watcher 中的 expression 是 function () { vm._update(vm._render(), hydrating); }
    • User Watcher 中的 expression 是用户自定义的 watch 中的函数
    • Computed Watcher 中的 expression 是用户自定义的 computed 中回调函数
  5. 执行vm._update(vm._render(), hydrating),vm._render() 返回一个新的 VNode,vm._update 中执行 vm.__patch__(prevVnode, vnode) 将VNode 渲染成真实 DOM 反应在页面上

接下来分别看这三者的实现

  • Observer

    /**
     * Observer class that is attached to each observed
     * object. Once attached, the observer converts the target
     * object's property keys into getter/setters that
     * collect dependencies and dispatch updates.
     */
    export class Observer {
      ...
      constructor(value: any) {
        ...
        def(value, '__ob__', this)
        ...
        this.walk(value);
      }
      walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i])
        }
      }
      ...
    }
    

    撇开具体的逻辑代码不看,Observer 类的构造函数中就做了两件事

    1. 将 Observer 的实例挂载到数据对象上def(value, '__ob__', this)
    2. 循环执行 defineReactive 方法将数据对象的所有属性变成响应式的

DefineReactive

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  ...
  const getter = property && property.get
  const setter = property && property.set
  ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        ...
        dep.depend()
        ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      ...
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      ...
      dep.notify()
    }
  })
}

defineReactive 做了一下几件事

  • 实例化一个消息订阅器 new Dep()
  • 利用 Object.defineProperty 给 data 对象上的每个属性添加 getter 和 setter 以"劫持"数据操作(get / set)
    • 在 get 中调用 dep 的 depend 方法将当前 Watcher 挂载到当前 dep 上
    • 在 set 时调用 dep 的 notify 方法通知所有订阅该 dep 的 Watcher

注意: 这里的 Dep.target 表示当前正在计算的 Watcher,其具有全局唯一性。

  • Dep

    /**
     * A dep is an observable that can have multiple
     * directives subscribing to it. 
     * directives 中的通过 Watcher 订阅数据
     */
    export default class Dep {
      static target: ?Watcher;
      id: number;
      subs: Array;
    
      constructor () {
        this.id = uid++
        this.subs = []
      }
    
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
    
      removeSub (sub: Watcher) {
        remove(this.subs, sub)
      }
    
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
    
      notify () {
      ...
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update() // 调用 Watcher 的 update 方法
        }
      }
    }
    

    Dep 类作为消息订阅器,只负责依赖收集(通过 depend 与 addSub 收集所有与之相关的 Watcher )和管理订阅它的所有 Watcher。

  • Watcher

    /**
     * A watcher parses an expression, collects dependencies,
     * and fires callback when the expression value changes.
     * This is used for both the $watch() api and directives.
     */
    export default class Watcher {
       ...
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        this.vm = vm // 当前Vue实例
        if (isRenderWatcher) {
          vm._watcher = this
        }
        vm._watchers.push(this) // 添加render watcher
        // options
      ...
        // parse expression for getter 下面用到的 getter 函数就是这么来的
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn)
          if (!this.getter) {
            this.getter = noop
          ...
          }
        }
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    
      /**
       * Evaluate the getter, and re-collect dependencies.
       * 调用 this.getter 函数(user、computed、render watcher 的 callback)
       * 然后重新收集依赖
       */
      get () {
        ...
        value = this.getter.call(vm, vm)
      ...
        this.cleanupDeps() 
        ...
        return value
      }
    
      /**
       * Add a dependency to this directive.
       * 把 dep 添加到 Watcher 实例的依赖数组中
       */
      addDep (dep: Dep) {
      ...
        dep.addSub(this)
      }
    
      /**
       * Clean up for dependency collection.
       * 重新整理依赖数组(deps)
       */
      cleanupDeps () {
        ...
        this.deps = this.newDeps
      ...
      }
    
      /**
       * Subscriber interface.
       * Will be called when a dependency changes.
       * 同步 sync 直接执行 run 
       * 异步 async 则先把当前 Watcher 推入队列,在 nextTick 中通过 flushSchedulerQueue 循环执行每个 watch    的 run 方法
       */
      update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
      /**
       * Scheduler job interface.
       * Will be called by the scheduler.
       */
      run () {
        if (this.active) {
         /** 
          * 对 watcher 求值,重新收集依赖
          * watcher 的执行顺序是 user > computed > render
          * get 函数内部做了两件事:
          * 触发 watcher callback;返回当前 value 值(只有 user watcher 有返回值)
          */
          const value = this.get() 
          
          if (
            value !== this.value ||
            isObject(value) ||
            this.deep
          ) {
            // set new value
            const oldValue = this.value
            this.value = value
            ...
            this.cb.call(this.vm, value, oldValue)
          ...
          }
        }
      }
    
      ...
    
      /**
       * Depend on all deps collected by this watcher.
       * 循环收集依赖
       */
      depend () {
        let i = this.deps.length
        while (i--) {
          this.deps[i].depend()
        }
      }
    
    /**
       * Remove self from all dependencies' subscriber list.
     * 把当前 watcher 从其依赖的所有 dep 的 subs 数组中删除
       */
      teardown () {
      ...
      }
    }
    

    Watcher 中的方法不多,主要就以下几个

    • addDep、depend、cleanupDeps、teardown 主要用于管理 watcher 与 dep 的依赖关系
    • get、update、run 用于响应数据更新的操作(如更新视图等)

一些小知识

  • 由于 Vue 响应式的核心 defineReactive 是使用 ES5 的Object.defineProperty 实现的,所以不支持 IE8 以下的浏览器

  • __patch__过程关于 DOM 操作的部分都定义在 platforms > runtime > node-ops.js 中

  • Vue 不能检测到对象属性的添加和删除,需要通过 Vue.$Set(target,key,value) 去实现,set 函数中会重新执行 defineReactive 将对象变为响应式,并且调用 dep.notify 以达到更新视图的效果

    /**
     * Set a property on an object. Adds the new property and
     * triggers change notification if the property doesn't
     * already exist.
     */
    export function set (target: Array | Object, key: any, val: any): any {
      ... 
      // 如果属性已经存在就直接返回
      if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
      }
      // 将新添加的属性变成响应式
      // 触发 notify
      const ob = (target: any).__ob__
      ...
      defineReactive(ob.value, key, val)
      ob.dep.notify()
      return val
    }
    

你可能感兴趣的:(浅析Vue响应式)