Vue源码解析 :深入理解响应式原理

860 阅读9分钟

Vue.js 最核心的功能有两个,一是响应式的数据绑定系统,二是组件系统。

如何理解响应式

可以这样理解:当一个状态改变之后,与这个状态相关的事务也立即随之改变,从前端来看就是数据状态改变后相关 DOM 也随之改变。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

抛个问题

我们先看看我们在 Vue 中常见的写法:

<div id="app" @click="changeNum">
  {{ num }}
</div>

var app = new Vue({
  el: '#app',
  data: {
    num: 1
  },
  methods: {
    changeNum() {
      this.num = 2
    }
  }
})

这种写法很常见,不过你考虑过当为什么执行 this.num = 2 后视图为什么会更新呢?接下来通过源码把这个点讲清楚。

如果不使用 Vue,我们应该怎么实现?

let data = {
  num: 1
};
Object.defineProperty(data, 'num',{
  value: value,
  set: function( newVal ){
    document.getElementById('app').value = newVal;
  }
});
input.addEventListener('input', function(){
  data.num = 2;
});

这样可以粗略的实现点击元素,自动更新视图。这里我们需要通过 Object.defineProperty 来操作对象的访问器属性。监听到数据变化的时候,操作相关 DOM。

而这里用到了一个常见模式 —— 发布/订阅模式。

下面的流程图,用来说明观察者模式和发布/订阅模式。如下:

我们会发现,这个粗略的过程和使用 Vue 的不同的地方就是需要我自己操作 DOM 重新渲染。如果我们使用 Vue 的话,这一步就是 Vue 内部的代码来处理的。这也是我们为什么在使用 Vue 的时候无需手动操作 DOM 的原因。

Vue 是如何实现响应式的

我们知道对象可以通过 Object.defineProperty 操作其访问器属性,即对象拥有了 getter 和 setter 方法。这就是实现响应式的基石。

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

data

vue实例中的数据项

observer

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

dep

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

watcher

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

dep和watcher

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

view

暂且认为就是在浏览器中显示的dom(其实是virtual dom)

收集依赖

watcher在自身属性中添加dep的行为

收集订阅者

dep在自身属性中添加watcher的行为

流程简介

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

在此基础上,我们根据源码更细一步划分出watcher和data之间的部分,即Dep和observer。

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

  • 把数据转化为getter和setter
  • 建立watcher并收集依赖 第一部分是上图中data、observer、dep之间联系的建立过程,第二部分是watcher、dep的关系建立

源码解析

采用的源码是vuejs 2.61

PS:简单的代码直接添加中文注释

initData

首先在源码中找到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实例
  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()
    }
  })
}

随后,对于数组和对象类型的数据做不同处理:对于数组类型observe里面的每个值,调用observeArray方法;对于对象,我们执行walk()方法,而walk()就是对于当前数据对象的每个key,执行defineReactive()方法,所以接下来重点来看defineReactive()。

defineReactive()中,在代码<2>处生成了一个dep实例,并在接下来的代码里,把这个dep对象放在当前数据对象的key(比如上面例子1中的str)的getter里,这个之前Observer中的dep是有区别的:

  • Observer中的dep挂在Object或者Array类型的数据的dep属性上,可以在控制台直接查看;
  • 此处添加的dep挂在属性的getter/setter上,存在于函数闭包中,不可直接查看

接下来代码<3>处的是为了处理嵌套的数据对象

代码<4>处是defineReactive()的核心:利用Object.defineProperty()

在当前属性的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函数主要内容是:

  • 初始化属性的值,其中和本文相关的主要是deps、newDeps、depIds、newDepIds,分别表示现有依赖和新一轮收集的依赖,这里的依赖就是前文介绍的数据对应的dep。

  • 设置getter属性。<1>判断传入的表达式类型:可能是函数,也可能是表达式。如果是函数,那么直接设置成getter,如果是表达式,由于代码<2>处的expOrFn只是字符串,要用parsePath获取到实际的值

  • 执行get()方法,在这里主要做收集依赖,并且获取数据的值,之后要调用代码<3>cleanupDeps清除旧的依赖。这是必须要做的,因为数据更新之后可能有新的数据属性添加进来,前一轮的依赖中没有包含这个新数据,所以要重新收集。

  • update方法主要内容是里面的触发更新之后会触发run方法(虽然这里分了三种情况,但是最终都是触发run方法),而run方法调用get()首先重新收集依赖,然后使用this.cb.call更新模板或者表达式的值。

总结

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

在官方文档的流程图中,没有单独列出dep和obvserver,因为这个流程最核心的思路就是将data的属性转化成getter和setter然后和watcher绑定。

这里的 getter 跟 setter 已经在之前介绍过了,在 init 的时候通过 Object.defineProperty 进行了绑定,它使得当被设置的对象被读取的时候会执行 getter 函数,而在当被赋值的时候会执行 setter 函数。

当 render function 被渲染的时候,因为会读取所需对象的值,所以会触发 getter 函数进行「依赖收集」,「依赖收集」的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。形成如下所示的这样一个关系。

在修改对象的值的时候,会触发对应的 setter, setter 通知之前「依赖收集」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch 的过程以及使用队列来异步更新的策略

几种实现双向绑定的做法

目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。 实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)
脏值检查(angular.js) 
数据劫持(vue.js)
发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value),这种方式现在毕竟太low了,我们更希望通过 vm.property = value 这种方式更新数据,同时自动更新视图,于是有了下面两种方式

脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:
DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
XHR响应事件 ( $http )
浏览器Location变更事件 ( $location )
Timer事件( $timeout , $interval )
执行 $digest() 或 $apply()

数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

思路整理

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一,如果不熟悉defineProperty,猛戳这里

整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:

  • 1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
  • 2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
  • 3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
  • 4、mvvm入口函数,整合以上三者

上述流程如图所示:

简单来说:

把一个普通 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为getter/setter。Object.defineProperty 是仅 ES5 支持,且无法 shim 的特性,这也就是为什么Vue 不支持 IE8以及更低版本浏览器的原因。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>vue</title>
</head>
<body>
  <script>
    var book={
        _year:2019,
        edition:1
    };
    Object.defineProperty(book,"year",{
        get:function(){
            return this._year;
        },
        set:function(newValue){
            if(newValue>2019){
                this._year=newValue;
                this.edition+=newValue-2019;
            }
        }
    });
    console.log(book.year); // 2019  在读取访问器属性时会调用get函数
    book.year=2020;  // 在给访问器属性赋值时会调用set函数
    console.log(book.edition); // 2
  </script>
</body>
</html>

所以,当对象下的访问器属性值发生了改变之后(vue会将属性都转化为访问器属性), 那么就会调用set函数,这时vue就可以通过这个set函数来追踪变化,调用相关函数来实现view视图的更新。