vue响应式原理

  vue是一个轻量级、数据驱动的渐进式框架,其核心就是数据驱动。一直以来,对vue响应式原理的理解还是停留在利用Object.defineProperty中的get和set进行数据劫持,至于内部是如何运转的,并没有一个全面的认知。本文就简要概述下,vue响应式原理具体的实现。

  就一般而言,props、data、computed、watch四个模块构建成vue数据处理系统的骨架,绝大部分的数据声明、转换、传递,都是通过这几个模块进行。盗用vue.js官网的一张图,vue的数据流如下图所示:

  在说数据流之前,我们先明确一点,Object.defineProperty(obj, prop, descriptor)主要作用是声明对象的属性,obj是对象名称,prop是对象的键值,descriptor是属性的数据描述符和存取描述符,其中属性的存取描述符就是上图中的getter/setter,getter触发条件是取,setter的触发条件是存。如下述代码,

let o ={}
let bValue;
Object.defineProperty(o, "b", {
  get : function(){
    console.log('执行了取操作')
    return bValue;
  },
  set : function(newValue){
    console.log('执行了存操作')
    bValue = newValue;
  },
  enumerable : true,
  configurable : true
});

o.b = 1; //

console.log(o.b) //
console.log(bValue) // o.b === bValue

  在vue中,声明响应式属性用的方法和上述大致类似,只是中间加了许多业务处理逻辑。先简要概述下vue数据流,主要分为以下两步:

  1、通过getter收集依赖

     什么是依赖?依赖可以理解为一个观察者对象,里面包含数据必须的属性(比如属性作用域对象)、方法(比如watch的回调)、字符串表达式(比如渲染函数)等,简单来说,观察者对象Watcher的主要作用就是数据与视图的粘合剂,通过发布-订阅模式,响应式的进行视图渲染。

    为什么在getter中能够收集到依赖信息呢?首先,视图要想渲染数据,肯定要有对数据的取操作,取值时,会触发对数据的取操作,此时,可以将依赖的数据信息添加到属性的订阅者列表中,这样后续数据如果有变化,就可以从数据的订阅者列表中,依次执行对应的处理逻辑。

  2、通过setter派发更新

      setter是承接getter的作用的。在第一次数据渲染时,会通过getter,收集到属性的所有订阅者信息,在给属性赋值时,会触发属性的setter方法,在setter里,会遍历订阅者列表,根据订阅者的表达式,回调,处理对应的逻辑方法,实现数据的响应式更新。setter在响应式系统中,更多的可能是承接一个启后的角色。

  在vue.js中,进行响应式数据声明的代码如下所示:

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
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  // 处理obj的值
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 如果val值存在Object,则需要侦听val值的变化
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 每次取值时,如果存在依赖的target对象,将watcher对象添加到订阅列表中,当数据发生改变时,通知watcher进行更新
      if (Dep.target) {
        // 将watcher加入到订阅列表
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 派发更新
      // 获取到value数据
      const value = getter ? getter.call(obj) : val
      /* 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) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 重新更新下数据依赖
      childOb = !shallow && observe(newVal)
      // 通知数据更新
      dep.notify()
    }
  })
}

    上述代码量比较多,以defineReactive方法响应式声明属性为中心,往前往后,分析下整个依赖收集与派发更新的过程。在开始之前,可以参考【vue实例化过程】,来熟悉下vue实例化的大致流程。

  1、依赖收集

    依赖收集中,需要重点关注的是dep.depend方法,在说dep.depend之前,需要关注上面有个判断条件,Dep.target,只有存在Dep.target时,才能将观察者对象添加到订阅列表中。以实例化Vue为例,在实例化组件时,会实例化一个watcher,watcher中包含组件挂载的相关信息。其中比较重要的一环就是会调用Watcher的get方法,get方法具体执行逻辑如下:

get () {
    // 设置当前处理的Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      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
  }

  其中,this.getter是指Watcher的一个表达式,在组件挂载中是指方法mountComponent,此方法主要作用是根据render函数生成vnode,根据vnode转换成真实的DOM节点;在watch中,this.getter是指根据属性名获取属性值的一个方法。在代码的第一句,有个pushTarget操作,这一步就是为后面get的依赖收集服务的,是将当前的Watcher实例,挂载到Dep上,表明当前正在处理的Watcher,在this.getter中,往往会伴随着对数据的取操作。在getter调用完毕后,会有一个popTarget,执行完当前的Watcher后,会清除对当前Watcher的依赖,pushTarget/popTarget的实现过程如下所示,在一个时间点,只能有一个被执行的Watcher:

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
// 进栈
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
// 出栈
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

   2、派发更新

    在我们对响应式数据更改时,会执行属性声明时的set方法,在set方法里,有一个dep.notify操作,主要就是遍历执行属性的订阅者列表Watcher的update方法,dep.notify执行的方法如下:

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++) {
      // 执行订阅对象批量更新???@todo
      // 每个subs都有update方法
      subs[i].update()
    }
  }

  更新执行操作比较多,但是需要注意的点是,视图和响应式回调更新的时机,当我们改变数据时,直接获取DOM中绑定的数据,可能还是原先的数据,视图并没有随之更新。如果想要在数据渲染完成后再进行回调处理,通常使用this.$nextTick。在派发更新时,会调用nextTick方法,传入更新数据的回调,nextTick在一般情况下是一个微任务,在支持promise的浏览器中是在promise中处理回调。我们都知道,在浏览器中,代码一般的执行顺序是 同步任务>微任务>宏任务。微任务常用的就是promise.then,宏任务常用的setTimeout。所以,捕获视图更新的回调,一般在setTimeout里也是可行的。微任务、宏任务与同步任务的处理流程可以参考下面代码:

console.log('开始执行测试代码')
let p = Promise.resolve()
p.then(function () {
  console.log('执行了微任务')
})

setTimeout(function () {
  console.log('执行了宏任务')
})

console.log('执行结束')

  代码的执行顺序如下图所示:

  派发更新操作,到最后都会执行到Watcher的run方法,最终都会执行到Watcher的getter,继续走页面的渲染流程。其实vue响应式原理核心就是利用Object.defineProperty进行数据劫持,对引用的数据进行依赖收集,数据改变时在进行派发更新消息。Watcher中存储的是数据依赖更新的消息。

  附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
  ) {
    debugger
    this.vm = vm
    // 渲染组件的watcher,通常computed 和 watch 也会实例化watcher
    if (isRenderWatcher) {
      vm._watcher = this
    }
    // 组件上的watcher队列
    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()
    // updateComponents, watch, computed callback function
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    // expression是function的情况:computed,mountComponent
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // watch走这个分支,watch传入的expression是字符串
      // 将字符串按照.进行字符串分割解析,exp: 'a.b.c' 获取到具体对象的值
      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()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   * 执行更新组件的方法,先将当前要执行的watcher推入到执行队列
   */
  get () {
    // 设置当前处理的Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      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
  }

  /**
   * 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)) {
        dep.addSub(this)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  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
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   * 依赖改变,会触发watcher的update方法
   */
  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) {
      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) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * 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.
   * 移除事件订阅
   */
  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
    }
  }
}
posted @ 2020-01-10 11:10  Gerryli  阅读(895)  评论(0编辑  收藏  举报