BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

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

| 作者 张耀春等 关注 0 他的粉丝 发布于 2016年10月16日. 估计阅读时间: 24 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

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

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

1 如何追踪变化

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

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

运行后,我们可以从页面中看到,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方法的源码定义如下:

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

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

<!--源码目录:src/instance/internal/state.js-->
Vue.prototype._proxy = function (key) {
    if (!isReserved(key)) {
      // need to store ref to self here
      // because these getter/setters might
      // be called by child scopes via
      // prototype inheritance.
      var self = this
      Object.defineProperty(self, key, {
        configurable: true,
        enumerable: true,
        get: function proxyGetter () {
          return self._data[key]
        },
        set: function proxySetter (val) {
          self._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 (value, vm) {
  if (!value || typeof value !== 'object') {
    return
  }
  var ob
  if (
    hasOwn(value, '__ob__') &&
    value.__ob__ instanceof Observer
  ) {
    ob = value.__ob__
  } else if (
    shouldConvert &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (ob && vm) {
    ob.addVm(vm)
  }
  return ob
}

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

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

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

<!--源码目录:src/observer/index.js-->
Observer.prototype.walk = function (obj) {
  var keys = Object.keys(obj)
  for (var i = 0, l = keys.length; i < l; i++) {
    this.convert(keys[i], obj[keys[i]])
  }
}

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

<!--源码目录:src/observer/index.js-->
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 function defineReactive (obj, key, val) {
  var dep = new Dep()
  var property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  var getter = property && property.get
  var setter = property && property.set
  var childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (isArray(value)) {
          for (var e, i = 0, l = value.length; i < l; i++) {
            e = value[i]
            e && e.__ob__ && e.__ob__.dep.depend()
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val
      if (newVal === value) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        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 default function Dep () {
  this.id = uid++
  this.subs = []
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null

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

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

<!--源码目录:src/observer/dep.js-->
Dep.prototype.depend = function () {
  Dep.target.addDep(this)
}

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

<!--源码目录:src/observer/dep.js-->
Dep.prototype.notify = function () {
  // stablize the subscriber list first
  var subs = toArray(this.subs)
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

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、测试开发和调试、源码解析及主流打包构建工具等。该书内容全面,讲解细致,示例丰富,适用于各层次的开发者。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT