从源码解析vue响应式原理

37

写在前面

(距离上一次写文章已经过去了四个月,羞愧...)这几个月对vue的使用不少,但是自觉地始终停留在比较粗浅的层面,一直无法提升,所以尝试着开始阅读源码。 文中内容仅代表个人理解,如果错误,欢迎指正

Vue中一个显著特性是数据响应式系统:当数据被修改时,视图会相应更新。从而方便的完成状态管理。官方文档中对此进行了简要的描述,本文将结合vuejs的源码,做出进一步的解析。

基本概念

首先简单介绍一些在响应式系统中重要的概念。

data

vue实例中的数据项

observer

数据属性的观察者,监控对象的读写操作。

dep

(dependence的缩写),字面意思是“依赖”,扮演角色是消息订阅器,拥有收集订阅者、发布更新的功能。

watcher

消息订阅者,可以订阅dep,之后接受dep发布的更新并执行对应视图或者表达式的更新。

dep和watcher

depwatcher的关系,可以理解为:dep是报纸,watcher是订阅了报纸的人,如果他们建立了订阅 的关系,那么每当报纸有更新的时候,就会通知对应的订阅者们。

view

暂且认为就是在浏览器中显示的dom(关于virtual dom的内容暂时不在本文讨论)

收集依赖

watcher在自身属性中添加dep的行为,后面会详细介绍

收集订阅者

dep在自身属性中添加watcher的行为,后面会详细介绍

流程简介

首先给出官方文档的流程图
图片描述

在此基础上,我们根据源码更细一步划分出watcher和data之间的部分,即Depobserver
图片描述

总的来说,vue的数据响应式实现主要分成2个部分:

  1. 把数据转化为getter和setter
  2. 建立watcher并收集依赖

第一部分是上图中dataobserverdep之间联系的建立过程,第二部分是watcherdep的关系建立

源码解析

本文中采用的源码是vuejs 2.5.0,Git地址

PS:简单的代码直接添加中文注释,所以在关键代码部分做<数字>标记,在后文详细介绍

首先我们在源码中找到vue进行数据处理的方法initData

 /* 源码目录 src/core/instance/state.js */
function initData (vm: Component) {
  let data = vm.$options.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
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      //<1>data属性代理
      proxy(vm, `_data`, key)
    }
  }
  // observe data
   //对data调用observe
  observe(data, true /* asRootData */)
}

这一段代码主要做2件事:

  • 代码<1>while循环内调用proxy函数把data的属性代理到vue实例上。完成之后可以通过vm.key直接访问data.key
  • 之后对data调用了observe方法,在这里说明一下,如果是在实例化之前添加的数据,因为被observe过,所以会变成响应式数据,而在实例化之后使用vm.newKey = newVal这样设置新属性,是不会自动响应的。解决方法是:

    - 如果你知道你会在晚些时候需要一个属性,但是一开始它为空或不存在,那么你仅需要设置一些初始值
    - 使用`vm.$data`等一些api进行数据操作

接下来来看对应代码:

/* 源码目录 src/core/observer/index.js */
export function observe (value: any, asRootData: ?boolean): Observer | void {
 if (!isObject(value) || value instanceof VNode) {
   return
 }
 let ob: Observer | void
 //检测当前数据是否被observe过,如果是则不必重复绑定
 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
   ob = value.__ob__
 } else if (
   //<1>检测当前的数据是否是对象或者数组,如果是,则生成对应的Observer
   observerState.shouldConvert &&
   !isServerRendering() &&
   (Array.isArray(value) || isPlainObject(value)) &&
   Object.isExtensible(value) &&
   !value._isVue
 ) {
   ob = new Observer(value)
 }
 if (asRootData && ob) {
   ob.vmCount++
 }
 return ob
}
  • 在本段代码中,代码<1>处,对传入的数据对象进行了判断,只对对象和数组类型生成Observer实例,然后看Observer这个类的代码,

Observer

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    // 生成了一个消息订阅器dep实例 关于dep的结构稍后详细介绍 
    this.dep = new Dep()
    this.vmCount = 0
    //def函数给当前数据添加不可枚举的__ob__属性,表示该数据已经被observe过
    def(value, '__ob__', this)
    //<1>对数组类型的数据 调用observeArray方法;对对象类型的数据,调用walk方法
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
   /* 循环遍历数据对象的每个属性,调用defineReactive方法 只对Object类型数据有效 */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }

  /**
   * Observe a list of Array items. 
   */
   /* observe数组类型数据的每个值, */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

/* defineReactive的核心思想改写数据的getter和setter */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  //<2>生成一个dep实例,注意此处的dep和前文Observer类里直接添加的dep的区别
  const dep = new Dep()
    
  //检验该属性是否允许重新定义setter和getter
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 获取原有的 getter/setters
  const getter = property && property.get
  const setter = property && property.set
  
  //<3>此处对val进行了observe
  let childOb = !shallow && observe(val)
  
  //<4>下面的代码利用Object.defineProperty函数把数据转化成getter和setter,并且在getter和setter时,进行了一些操作
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // dep.depend()其实就是dep和watcher进行了互相绑定,而Dep.target表示需要绑定的那个watcher,任何时刻都最多只有一个,后面还会解释
        dep.depend()
        if (childOb) {
          //<5>当前对象的子对象的依赖也要被收集
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      //<6>观察新的val并通知订阅者们属性有更新
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
  • 在Observer类代码中,首先给当前数据添加了一个dep实例,存放于对象或者数组类型数据的_![图片描述][2]ob_属性上,然后把_ob_挂在该数据上,它是该数据项被observe的标志,我们可以在控制台看到这个属性,,例如:
//例子 1
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>vue demo</title>
</head>

<body>
  <script src="https://cdn.jsdelivr.net/npm/vue"></script>
  <div id="app">
    <div>obj:{{ obj}}</div>
  </div>
</body>
<script>
  app = new Vue({
    el: '#app',
    data: {
      str: "a",
      obj: {
        key: "val"
      }
    }
  });
  console.log(app._data)
</script>

</html>

在控制台我们可以看到这样的数据
图片描述

可以看到,首先,data对象上已经有_ob_属性,这是被observe的标志;其次,objarr属性上有_ob_属性,而str没有,这个进一步证明了前文提到的:observe只对对象和数组有效

  • 随后,对于数组和对象类型的数据做不同处理:对于数组类型observe里面的每个值,对于对象,我们执行walk()方法,而walk()就是对于当前数据对象的每个key,执行defineReactive()方法,所以接下来重点来看defineReactive()
  • defineReactive()中,在代码<2>处生成了一个dep实例,并在接下来的代码里,把这个dep对象放在当前数据对象的key(比如上面例子1中的str)的getter里,这个之前Observer中的dep是有区别的:

    • Observer中的dep挂在Object或者Array类型的数据的dep属性上,可以在控制台直接查看;
    • 此处添加的dep挂在属性的getter/setter上,存在于函数闭包中,不可直接查看
为什么会有2种`Dep`呢?因为对于`Object`或者`Array`类型的数据,可能会有**添加

或者删除成员的操作而其他类型的值只有赋值操作,赋值操作可以在getter/setter上中检测到。**,

  • 接下来代码<3>处的是为了处理嵌套的数据对象,比如例子1中,data是最顶层的Object,obj就是data下的Object,而obj里面也可以再继续嵌套数据,有了此处的代码之后,就可以对嵌套的每一层都做observe处理。
  • 代码<4>处是defineReactive()的核心:利用Object.defineProperty()(这个函数建议了解一下mdn地址)

在当前属性的getter和setter中插入操作:

  • 在当前数据被get时,当前的watcher(也就是Dap.target)和dep之间的绑定,这里有个注意点是在代码<5>处,如果当前数据对象存在子对象,那么子对象的dep也要和当前watcher进行绑定,以此类推。
  • 在setter时,我们重新观测当前val,然后通过dep.notify()来通知当前dep所绑定的订阅者们数据有更新。

Dep

接下来介绍一下dep。源码如下:

/* 源码目录 src/core/observer/dep.js */
let uid = 0
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }
  //添加一个watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  //移除一个watcher
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  //让当前watcher收集依赖 同时Dep.target.addDep也会触发当前dep收集watcher
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
 //通知watcher们对应的数据有更新
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

这个类相对简单很多,只有2个属性:第一个是id,在每个vue实例中都从0开始计数;另一个是subs数组,用于存放wacther,根绝前文我们知道,一个数据对应一个Dep,所以subs里存放的也就是依赖该数据需要绑定的wacther

这里有个Dep.target属性是全局共享的,表示当前在收集依赖的那个Watcher,在每个时刻最多只会有一个

watcher

接下里看watcher的源码,比较长,但是我们只关注其中的几个属性和方法:

/* 源码目录 src/core/observer/watcher.js */
/**
 * 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.
 */
 /* watcher用来解析表达式,收集依赖,并且当表达式的值改变时触发回调函数 
 用在$watch() api 和指令中
 */
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: ISet;
  newDepIds: ISet;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    // options
    //这里暂时不用关注 
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } 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
    //deps和newDeps表示现有的依赖和新一轮收集的依赖
    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
    //<1>解析getter的表达式 
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      //<2>获取实际对象的值
      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
        )
      }
    }
    //this.lazy为true是计算属性的watcher,另外处理,其他情况调用get
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  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()
      //<3>清除先前的依赖
      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.
   */
   /* 订阅者的接口 当依赖改变时会触发 */
  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) {
      //<14>重新收集依赖
      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
    }
  }

首先看官方文档的英文注释可知,watcher用于watcher用来解析表达式,收集依赖,并且当表达式的值改变时触发回调函数,用在$watch()api 和指令之中。

watcher函数主要内容是:

  • 初始化属性的值,其中和本文相关的主要是depsnewDepsdepIdsnewDepIds,分别表示现有依赖和新一轮收集的依赖,这里的依赖就是前文介绍的数据对应的dep
  • 设置getter属性。<1>判断传入的表达式类型:可能是函数,也可能是表达式。如果是函数,那么直接设置成getter,如果是表达式,由于代码<2>处的expOrFn只是字符串,比如例子1中的obj.key,在这里仅仅是一个字符串,所以要用parsePath获取到实际的值
  • 执行get()方法,在这里主要做收集依赖,并且获取数据的值,之后要调用代码<3>`cleanupDeps`清除旧的依赖。这是必须要做的,因为数据更新之后可能有新的数据属性添加进来,前一轮的依赖中没有包含这个新数据,所以要重新收集。
  • update方法主要内容是里面的触发更新之后会触发run方法(虽然这里分了三种情况,但是最终都是触发run方法),而run方法调用get()首先重新收集依赖,然后使用this.cb.call更新模板或者表达式的值。

总结

在最后,我们再总结一下这个流程:首先数据从初始化data开始,使用observe监控数据:给每个数据属性添加dep,并且在它的getter过程添加收集依赖操作,在setter过程添加通知更新的操作;在解析指令或者给vue实例设置watch选项或者调用$watch时,生成对应的watcher并收集依赖。之后,如果数据触发更新,会通知watcherwacther在重新收集依赖之后,触发模板视图更新。这就完成了数据响应式的流程。

本文的流程图根据源码的过程画出,而在官方文档的流程图中,没有单独列出depobvserver,因为这个流程最核心的思路就是将data的属性转化成gettersetter然后和watcher绑定

然后依然是惯例:如果这篇文章对你有帮助,希望可以收藏和推荐,以上内容属于个人见解,如果有不同意见,欢迎指出和探讨。请尊重作者的版权,转载请注明出处,如作商用,请与作者联系,感谢!


如果觉得我的文章对你有用,请随意赞赏
已赞赏

你可能感兴趣的

pardon110 · 2017年12月28日

长见识了。不错,大赞。

+1 回复

0

谢谢!

安歌 作者 · 2017年12月28日
weapon · 2017年12月28日

厉害了,大赞

回复

载入中...