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

  • 2016-10-14
  • 本文字数:6095 字

    阅读完需:约 20 分钟

编者按:InfoQ 开设新栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自张耀春等著的《Vue.js 权威指南》一书。

Vue.js 最显著的功能就是响应式系统,它是一个典型的 MVVM 框架,模型(Model)只是普通的 JavaScript 对象,修改它则视图(View)会自动更新。这种设计让状态管理变得非常简单而直观,不过理解它的原理也很重要,可以避免一些常见问题。下面让我们深挖 Vue.js 响应式系统的细节,来看一看 Vue.js 是如何把模型和视图建立起关联关系的。

1 如何追踪变化

我们先来看一个简单的例子。代码示例如下:

<span><span><<span>div</span> <span>id</span>=<span>"main"</span>></span>
  <span><<span>h1</span>></span>count: </span><span>{{<span>times</span>}}</span><span><span></<span>h1</span>></span> 
<span></<span>div</span>></span>
<span><<span>script</span> <span>src</span>=<span>"vue.js"</span>></span><span></<span>script</span>></span>
<span><<span>script</span>></span><span>
  <span>var</span> vm = <span>new</span> Vue({
    el: <span>'#main'</span>,
    data: <span><span>function</span> <span>()</span> {</span>
      <span>return</span> {
        times: <span>1</span>
      };
    },
    created: <span><span>function</span> <span>()</span> {</span>
      <span>var</span> me = <span>this</span>;
      setInterval(<span><span>function</span> <span>()</span> {</span>
        me.times++;
      }, <span>1000</span>);
      }
  });
</span><span></<span>script</span>></span></span>

运行后,我们可以从页面中看到,count 后面的 times 每隔 1s 递增 1,视图一直在更新。在代码中仅仅是通过 setInterval 方法每隔 1s 来修改 vm.times 的值,并没有任何 DOM 操作。那么 Vue.js 是如何实现这个过程的呢?我们可以通过一张图来看一下,如图 20-1 所示。

图 20-1 模型和视图关联关系图

图中的模型(Model)就是 data 方法返回的{times:1},视图(View)是最终在浏览器中显示的 DOM。模型通过 Observer、Dep、Watcher、Directive 等一系列对象的关联,最终和视图建立起关系。归纳起来,Vue.js 在这里主要做了三件事:

  • 通过 Observer 对 data 做监听,并且提供了订阅某个数据项变化的能力。
  • 把 template 编译成一段 document fragment,然后解析其中的 Directive,得到每一个 Directive 所依赖的数据项和 update 方法。
  • 通过 Watcher 把上述两部分结合起来,即把 Directive 中的数据依赖通过 Watcher 订阅在对应数据的 Observer 的 Dep 上。当数据变化时,就会触发 Observer 的 Dep 上的 notify 方法通知对应的 Watcher 的 update,进而触发 Directive 的 update 方法来更新 DOM 视图,最后达到模型和视图关联起来。

接下来我们就结合 Vue.js 的源码来详细介绍这三个过程。

20.1.1 Observer

首先来看一下 Vue.js 是如何给 data 对象添加 Observer 的。我们知道,Vue 实例创建的过程会有一个生命周期,其中有一个过程就是调用 vm._initData 方法处理 data 选项。_initData 方法的源码定义如下:

<!
<span>Vue</span>.prototype._initData = function () {
    var dataFn = this.$options.<span><span>data</span></span>
    var <span><span>data</span> = this._data = dataFn ? dataFn<span>()</span> : <span>{}</span></span>
    <span>if</span> (!isPlainObject(<span><span>data</span>)) <span>{
      <span>data</span> = {}</span></span>
      process.env.<span>NODE_ENV</span> !== 'production' && warn(
        '<span><span>data</span> functions should return an object.',</span>
        this
      )
    }
    var props = this._props
    // proxy <span><span>data</span> on instance</span>
    var keys = <span>Object</span>.keys(<span><span>data</span>)</span>
    var i, key
    i = keys.length
    while (i
      key = keys[i]
      // there are two scenarios <span>where</span> we can proxy a <span><span>data</span> key:</span>
      // <span>1.</span> it's not already defined <span>as</span> a prop
      // <span>2.</span> it's provided via a instantiation option <span>AND</span> there are no
      // template prop present
      <span>if</span> (!props || !hasOwn(props, key)) {
        this._proxy(key)
      } <span>else</span> <span>if</span> (process.env.<span>NODE_ENV</span> !== 'production') {
        warn(
          '<span>Data</span> field <span>"' + key + '"</span> is already defined ' +
          '<span>as</span> a prop. <span>To</span> provide <span><span>default</span> value for a prop, use the "<span>default</span>" ' +</span>
          'prop option; <span>if</span> you want to pass prop values to an instantiation ' +
          'call, use the <span>"propsData"</span> option.',
          this
        )
      }
    }
// observe <span><span>data</span></span>
    observe(<span><span>data</span>, this)</span>
}

在 _initData 中我们要特别注意 _proxy 方法,它的功能就是遍历 data 的 key,把 data 上的属性代理到 vm 实例上。_proxy 方法的源码定义如下:

<!-- 源码目录:src/instance/internal/state.js-->
Vue.prototype._proxy = <span><span>function</span> <span>(key)</span> {</span>
    <span>if</span> (!isReserved(key)) {
      
      
      
      
      <span>var</span> <span>self</span> = this
      Object.defineProperty(<span>self</span>, key, {
        configurable: <span>true</span>,
        enumerable: <span>true</span>,
        get: <span><span>function</span> <span>proxyGetter</span> <span>()</span> {</span>
          <span>return</span> <span>self</span>._data[key]
        },
        set: <span><span>function</span> <span>proxySetter</span> <span>(val)</span> {</span>
          <span>self</span>._data[key] = val
        }
      })
    }
  }

_proxy 方法主要通过 Object.defineProperty 的 getter 和 setter 方法实现了代理。在前面的例子中,我们调用 vm.times 就相当于访问了 vm._data.times。

在 _initData 方法的最后,我们调用了 observe(data, this) 方法来对 data 做监听。observe 方法的源码定义如下:

<!-- 源码目录:src/observer/index.js-->
export function observe (<span>value</span>, vm) {
  <span>if</span> (!<span>value</span> || <span>typeof</span> <span>value</span> !== <span>'object'</span>) {
    <span>return</span>
  }
  <span>var</span> ob
  <span>if</span> (
    hasOwn(<span>value</span>, <span>'__ob__'</span>) &&
    <span>value</span>.__ob__ instanceof Observer
  ) {
    ob = <span>value</span>.__ob__
  } <span>else</span> <span>if</span> (
    shouldConvert &&
    (isArray(<span>value</span>) || isPlainObject(<span>value</span>)) &&
    Object.isExtensible(<span>value</span>) &&
    !<span>value</span>._isVue
  ) {
    ob = <span>new</span> Observer(<span>value</span>)
  }
  <span>if</span> (ob && vm) {
    ob.addVm(vm)
  }
  <span>return</span> ob
}

observe 方法首先判断 value 是否已经添加了ob属性,它是一个 Observer 对象的实例。如果是就直接用,否则在 value 满足一些条件(数组或对象、可扩展、非 vue 组件等)的情况下创建一个 Observer 对象。接下来我们看一下 Observer 这个类,它的源码定义如下:

<!-- 源码目录:src/observer/index.js-->
export function Observer (<span>value</span>) {
  <span>this</span>.<span>value</span> = <span>value</span>
  <span>this</span>.dep = <span>new</span> Dep()
  def(<span>value</span>, <span>'__ob__'</span>, <span>this</span>)
  <span>if</span> (isArray(<span>value</span>)) {
    <span>var</span> augment = hasProto
      ? protoAugment
      : copyAugment
    augment(<span>value</span>, arrayMethods, arrayKeys)
    <span>this</span>.observeArray(<span>value</span>)
  } <span>else</span> {
    <span>this</span>.walk(<span>value</span>)
  }
}

Observer 类的构造函数主要做了这么几件事:首先创建了一个 Dep 对象实例(关于 Dep 对象我们稍后作介绍);然后把自身 this 添加到 value 的ob属性上;最后对 value 的类型进行判断,如果是数组则观察数组,否则观察单个元素。其实 observeArray 方法就是对数组进行遍历,递归调用 observe 方法,最终都会调用 walk 方法观察单个元素。接下来我们看一下 walk 方法,它的源码定义如下:

<!
Observer.prototype.walk = <span><span>function</span> (<span>obj</span>) {</span>
  var <span>keys</span> = Object.<span>keys</span>(obj)
  <span>for</span> (var i = <span>0</span>, l = <span>keys</span>.<span>length</span>; i < l; i++) {
    this.<span>convert</span>(<span>keys</span>[i], obj[<span>keys</span>[i]])
  }
}

walk 方法是对 obj 的 key 进行遍历,依次调用 convert 方法,对 obj 的每一个属性进行转换,让它们拥有 getter、setter 方法。只有当 obj 是一个对象时,这个方法才能被调用。接下来我们看一下 convert 方法,它的源码定义如下:


Observer.prototype.convert = function (key, val) {
  defineReactive(this.value, key, val)
}

convert 方法很简单,它调用了 defineReactive 方法。这里 this.value 就是要观察的 data 对象,key 是 data 对象的某个属性,val 则是这个属性的值。defineReactive 的功能是把要观察的 data 对象的每个属性都赋予 getter 和 setter 方法。这样一旦属性被访问或者更新,我们就可以追踪到这些变化。接下来我们看一下 defineReactive 方法,它的源码定义如下:

<!-- 源码目录:src/observer/index.js-->
export <span><span>function</span> <span>defineReactive</span> <span>(obj, key, val)</span> {</span>
  <span>var</span> dep = <span>new</span> Dep()
  <span>var</span> property = Object.getOwnPropertyDescriptor(obj, key)
  <span>if</span> (property && property.configurable === <span>false</span>) {
    <span>return</span>
  }
  
  <span>var</span> getter = property && property.<span>get</span>
  <span>var</span> setter = property && property.<span>set</span>
  <span>var</span> childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: <span>true</span>,
    configurable: <span>true</span>,
    <span>get</span>: <span><span>function</span> <span>reactiveGetter</span> <span>()</span> {</span>
      <span>var</span> value = getter ? getter.call(obj) : val
      <span>if</span> (Dep.target) {
        dep.depend()
        <span>if</span> (childOb) {
          childOb.dep.depend()
        }
        <span>if</span> (isArray(value)) {
          <span>for</span> (<span>var</span> e, i = <span>0</span>, l = value.length; i < l; i++) {
            e = value[i]
            e && e.__ob__ && e.__ob__.dep.depend()
          }
        }
      }
      <span>return</span> value
    },
    <span>set</span>: <span><span>function</span> <span>reactiveSetter</span> <span>(newVal)</span> {</span>
      <span>var</span> value = getter ? getter.call(obj) : val
      <span>if</span> (newVal === value) {
        <span>return</span>
      }
      <span>if</span> (setter) {
        setter.call(obj, newVal)
      } <span>else</span> {
        val = newVal
      }
      childOb = observe(newVal)
      dep.notify()
    }
  })
}

defineReactive 方法最核心的部分就是通过调用 Object.defineProperty 给 data 的每个属性添加 getter 和 setter 方法。当 data 的某个属性被访问时,则会调用 getter 方法,判断当 Dep.target 不为空时调用 dep.depend 和 childObj.dep.depend 方法做依赖收集。如果访问的属性是一个数组,则会遍历这个数组收集数组元素的依赖。当改变 data 的属性时,则会调用 setter 方法,这时调用 dep.notify 方法进行通知。这里我们提到了 dep,它是 Dep 对象的实例。接下来我们看一下 Dep 这个类,它的源码定义如下:

<!-- 源码目录:src/observer/dep.js-->
export <span>default</span> <span><span>function</span> <span>Dep</span> <span>()</span> {</span>
  <span>this</span>.id = uid++
  <span>this</span>.subs = []
}



Dep.target = <span>null</span>

Dep 类是一个简单的观察者模式的实现。它的构造函数非常简单,初始化了 id 和 subs。其中 subs 用来存储所有订阅它的 Watcher,Watcher 的实现稍后我们会介绍。Dep.target 表示当前正在计算的 Watcher,它是全局唯一的,因为在同一时间只能有一个 Watcher 被计算。

前面提到了在 getter 和 setter 方法调用时会分别调用 dep.depend 方法和 dep.notify 方法,接下来依次介绍这两个方法。depend 方法的源码定义如下:


Dep.prototype.depend = function () {
  Dep.target.addDep(this)
}

depend 方法很简单,它通过 Dep.target.addDep(this) 方法把当前 Dep 的实例添加到当前正在计算的 Watcher 的依赖中。接下来我们看一下 notify 方法,它的源码定义如下:


Dep.prototype.notify = function () {
  // stablize the subscriber list first
  var subs = toArray(this.subs)
  for (var i = 0, l = subs.length; i <span>< <span>l</span>; <span>i</span>++) {
    <span>subs</span>[<span>i</span>]<span>.update</span>()
  }
}</span>

notify 方法也很简单,它遍历了所有的订阅 Watcher,调用它们的 update 方法。

至此,vm 实例中给 data 对象添加 Observer 的过程就结束了。接下来我们看一下 Vue.js 是如何进行指令解析的。

书籍介绍

Vue.js 是一个用来开发 Web 界面的前端库。本书致力于普及国内 Vue.js 技术体系,让更多喜欢前端的人员了解和学习 Vue.js。如果你对 Vue.js 基础知识感兴趣,如果你对源码解析感兴趣,如果你对 Vue.js 2.0 感兴趣,如果你对主流打包工具感兴趣,如果你对如何实践感兴趣,本书都是一本不容错过的以示例代码为引导、知识涵盖全面的最佳选择。全书一共 30 章,由浅入深地讲解了 Vue.js 基本语法及源码解析。主要内容包括数据绑定、指令、表单控件绑定、过滤器、组件、表单验证、服务通信、路由和视图、vue-cli、测试开发和调试、源码解析及主流打包构建工具等。该书内容全面,讲解细致,示例丰富,适用于各层次的开发者。