资讯专栏INFORMATION COLUMN

浅析Vue响应式原理(一)

lookSomeone / 2478人阅读

摘要:浅析响应式原理一的特点之一是响应式,视图随着数据的更新而更新,在视图中修改数据后实例中的数据也会同步更新。对于每个响应式数据,会有两个实例,第一个是在中的闭包遍历,用途显而易见。接收一个回调函数,会在重新求值且值更新后执行。

浅析Vue响应式原理(一)

Vue的特点之一是响应式,视图随着数据的更新而更新,在视图中修改数据后Vue实例中的数据也会同步更新。内部借助依赖(下文中的Dep类)来实现,数据的获取(即get操作)会触发收集依赖,而对数据赋值(即set操作)会通知依赖数据更新,重新渲染视图。对数据的get/set操作的拦截借助的是ES5的Object.defineProperty

总体架构简介

在Vue源码内,Dep类作为依赖,Watcher类则用来收集依赖和通知依赖重新求值。对于在实例化时传入的数据,使用工厂函数defineReactive令其响应式。而在实例后再通过Vue.set/vm.$set添加的响应式数据,则需要借助Observer类来使其成为响应式数据,最后也是通过defineReactive实现响应式。

对于每个响应式数据,会有两个Dep实例,第一个是在defineReactive中的闭包遍历,用途显而易见。而第二个Dep则在响应式数组的__ob__属性值中,这个值是Observer实例,其实例属性dep是Dep实例,在执行Vue.set/vm.$set添加响应式数据后,会通知依赖更新。

在讲defineReactive之前,先讲一下这些辅助类的实现和用处。

Dep

我们都知道,Vue响应式的实现,会在getter中收集响应式数据的依赖,在setter中通知依赖数据更新,重新计算数据然后来更新视图。在Vue内部,使用Dep实例表示依赖,让我们看一下Dep类是怎么定义的。

Dep有两个实例属性,一个静态属性。静态属性targetWatcher实例,功能是重新求值和通知视图更新,下文我们会讲到。实例属性id是Dep实例的唯一标识,无需多说;属性subs是Watcher实例数组,用于收集Watcher实例,当依赖更新时,这些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 () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

方法addSub用于添加Watcher实例到subs中,方法removeSub用于从subs移除Watcher实例。

方法depond会在收集依赖的时候调用,实际上执行了Watcher的实例方法addDep,在addDep内除了调用dep实例的addSup方法外,还做了避免重复收集Watcher实例的工作。这个方法会在Vue为响应式数据设置的自定义getter中执行。

notify方法则遍历subs,执行Watcher实例方法update来重新求值。这个方法会在Vue为响应式数据设置的自定义setter中执行。

有人可能有疑问,target是静态属性,那不是每个实例的target都一样的?实际上,重新求值的操作在Watcher实例方法get内实现。在get方法内,会先调用pushTarget来更新Dep.target,使其指向当前Watcher实例,之前的`Dep.target会被保存targetStack末尾(相当于入栈操作),完成操作后会执行popTarget函数,从targetStack取出最后一个元素来还原Dep.target(相当于出栈操作)。

Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}
Watcher

当依赖更新时,Watcher类会重新求值,并可能触发重渲染。

constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // 与渲染相关的watcher
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed 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 = function () {}
        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
        )
      }
    }
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
  }

构造函数接受五个参数,vm是挂载的Component实例;expOrFn是观察的属性,当是字符串时表示属性名,是函数时会被当成属性的get方法;cb是属性更新后执行的回调函数;options是配置项;isRenderWatcher表示当前实例是否与渲染相关。

在构造函数内,先将实例属性vm指向传入的Component实例vm,如果当前Watcher实例与渲染相关,会将其保存在vm._watcher中。接着将当前实例添加到vm._watchers中,同时根据传入的配置项options初始化实例属性。实例属性getter是监听属性的getter函数,如果expOrFn是函数,直接赋值,否则会调用parsePath来获取属性的getter。

parsePath内部会先使用正则来判断属性名,如果有除数字、字母、.$以外的字符时视为非法属性名,直接返回,所以属性只能是以.分隔的属性。如果属性名合法,则parsePath返回一个闭包函数,调用时会传入vm,即objvm的引用,这个闭包函数最终的目的是从vm实例里获取属性。

const bailRE = /[^w.$]/
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split(".")
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

初始化完成之后,如果不是计算属性相关的Watcher实例,会调用实例方法get求值。

get方法

执行getter方法求值,完成依赖收集的过程。

方法开始时,执行pushTarget(this),将Dep.target指向当前Watcher实例。然后执行getter收集依赖,最后将Dep.target复原,并执行cleanDeps遍历deps。在每次求值之后,都会调用cleanupDeps方法重置依赖,具体如何重置,稍后再讲。

实际上,Dep.target指向的实例是即将要收集的目标。

getter的执行,除了会获取值外,还会触发在defineReactive中为属性设置的getter,完成依赖的收集。

  get () {
    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
  }
addDep

addDep的功能是将当前Watcher实例添加到传入的Dep实例属性subs数组里去。

addDep接受一个Dep实例作为参数,如果 dep.id 没有在集合 newDepIds 之中,则添加。如果不在集合 depIds 中,则将当前实例添加到 dep.subs 中。 简单来说,这里的操作会避免重复收集依赖,这也是不直接调用dep.addSub(Dep.target)的原因。

  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)
      }
    }
  }

从这里可以看出来Dep实例和Watcher实例会相互引用。Dep实例将Watcher实例保存在实例属性subs中,在响应式属性调用setter时,执行notify方法,通知Watcher实例重新求值。

Watcher实例将Dep实例保存在集合newDeps,目的是避免重复收集依赖,同时会执行Dep实例方法addDep,将当前Watcher实例添加到Dep实例属性subs中。

cleanupDeps

对于Watcher来说,每次求值的依赖并不一定与上一次的相同,在每次执行get之后,都会调用cleanupDeps来重置收集的依赖。Watcher有四个实例属性用于记录依赖,分别是newDeps/newDepIdsdeps/depIdsnewDepsdeps是保存依赖的数组,newDepIdsdepIds是保存依赖Id的集合。记录上一次求值依赖的属性是deps/depIds,记录下一次求值依赖的属性是newDeps/newDepIds(执行cleanupDeps时已经调用过getter重新求值了,所以说是上一次求值,下一次指的是下一次调用get的时候)。

  cleanupDeps () {
    let i = this.deps.length
    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()
    // 交换deps和newDeps
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

首先遍历deps,如果此次求值的依赖在下一次求值中并不存在,则需要调用removeSub方法,从subs数组中移除当前Watcher实例。

接着交换newDeps/newDepIdsdeps/depIds,并清空交换后的newDeps/newDepIds

update

Dep类的notify方法用于通知观察者重新求值,该方法内部实际是遍历subs数组,执行Watcher的update方法。

update 方法定义如下。当实例与计算属性相关时,xxx。如果不是计算属性相关时,判断是否需要同步触发,同步触发时调用run,否则执行queueWatcher(this),交由调度模块统一调度。

  update () {
    if (this.computed) {
      if (this.dep.subs.length === 0) {
        this.dirty = true
      } else {
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
teardown

销毁当前Watcher实例。$watch方法返回一个函数,函数内部就是Watcher实例调用teardown方法。

先判断Watcher实例是否在活跃状态。首先要从Vue实例的观察者队列_watchers中移除当前实例,如果vm正在销毁,因为性能的问题会跳过这一操作。接着遍历deps,取消这些Dep实例对当前Watcher实例的订阅。最后令this.active = false,表示当前Watcher实例已被销毁。

  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
    }
  }
getAndInvoke

不论是同步或异步更新,或者是计算属性相关的Wathcer实例,最终求值都是通过getAndInvoke方法。

getAndInvoke接收一个回调函数,会在重新求值且值更新后执行。

当新值与当前值不同时会被判定为值已更新。当值是对象时且this.deep为真时也判定为值已更新,尽管引用不发生改变,但其属性却可能发生变化,为避免属性发生改变而Watcher判断未更新的情况出现。

  getAndInvoke (cb: Function) {
    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
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }
run

run方法内部只是对getAndInvoke的封装,传入的回调函数是实例化时传入的函数。执行之前会先判断Watcher实例是否已弃用。

  run () {
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }
小结

由于篇幅的原因,本文只简单分析了辅助类和工厂函数的源码和功能。干巴巴地讲了这么多,现在来稍微捋一下。

Watcher类会保存响应式数据的getter函数,这个getter函数可能是实例化参数expOrFn(当其是函数类型时),也可能是执行parsePath(expOrFn)获取到的getter函数。实例方法update对外暴露,用于重新求值,实际上执行真正求值操作的get方法。方法addDep接受一个Dep实例参数,在执行订阅操作前还会执行两个if判断,避免重复订阅。

Dep类代表依赖,实例属性subs是Watcher数组,代表订阅了当前Dep实例的观察者实例,depond方法收集依赖,notify方法通知观察者实例重新求值。订阅列表中可能会有与渲染相关的观察者,所以可能会触发重渲染。

Observer类与Vue.set/vm.$set的联系比较大,所以分析放在后面。

参考链接

Vue技术内幕|揭开数据响应系统的面纱

Vue.js源码

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99405.html

相关文章

  • 浅析Vue响应原理(三)

    摘要:响应式原理之不论如何,最终响应式数据都要通过来实现,实际要借助新增的。在函数内,首先实例化一个实例,会在稍后添加为响应式数据自定义的中发挥作用。只有数组和对象才可能是响应式,才能返回实例。参考链接技术内幕揭开数据响应系统的面纱源码 Vue响应式原理之defineReactive defineReactive 不论如何,最终响应式数据都要通过defineReactive来实现,实际要借助...

    tomener 评论0 收藏0
  • 从数组入手浅析Vue响应原理

    摘要:响应式原理为了探究这一切的原因,我再次点开了的官网。在官网很下面的位置,找到了关于响应式原理的说明。因此,新添加到数组中的对象中的属性,就成了非响应式的属性了,改变它自然不会让组件重新渲染。响应式属性的对象,有这个对象就代表是响应式的。   最近在用Vue开发一个后台管理的demo,有一个非常常规的需求。然而这个常规的需求中,包含了大量的知识点。有一个产品表格,用来显示不同产品的信息。...

    dkzwm 评论0 收藏0
  • 浅析Vue响应原理(二)

    摘要:响应式原理之之前简单介绍了和类的代码和作用,现在来介绍一下类和。对于数组,响应式的实现稍有不同。不存在时,说明不是响应式数据,直接更新。如果对象是响应式的,确保删除能触发更新视图。 Vue响应式原理之Observer 之前简单介绍了Dep和Watcher类的代码和作用,现在来介绍一下Observer类和set/get。在Vue实例后再添加响应式数据时需要借助Vue.set/vm.$se...

    rockswang 评论0 收藏0
  • 前方来报,八月最新资讯--关于vue2&3的最佳文章推荐

    摘要:哪吒别人的看法都是狗屁,你是谁只有你自己说了才算,这是爹教我的道理。哪吒去他个鸟命我命由我,不由天是魔是仙,我自己决定哪吒白白搭上一条人命,你傻不傻敖丙不傻谁和你做朋友太乙真人人是否能够改变命运,我不晓得。我只晓得,不认命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出处 查看github最新的Vue...

    izhuhaodev 评论0 收藏0
  • Vue.nextTick浅析

    摘要:浅析的特点之一就是响应式,但数据更新时,并不会立即更新。尽管已经更新,但新增的元素并不立即插入到中。实际在中,执行了,这也是自动绑定到执行上下文的原因。在内,使用数组保存回调函数,表示当前状态,使用函数来执行回调队列。 Vue.nextTick 浅析 Vue 的特点之一就是响应式,但数据更新时,DOM 并不会立即更新。当我们有一个业务场景,需要在 DOM 更新之后再执行一段代码时,可以...

    MartinDai 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<