[关闭]
@hanting003 2016-10-31T10:37:57.000000Z 字数 6172 阅读 889

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

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

1 如何追踪变化

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

  1. <div id="main">
  2. <h1>count: {{times}}</h1>
  3. </div>
  4. <script src="vue.js"></script>
  5. <script>
  6. var vm = new Vue({
  7. el: '#main',
  8. data: function () {
  9. return {
  10. times: 1
  11. };
  12. },
  13. created: function () {
  14. var me = this;
  15. setInterval(function () {
  16. me.times++;
  17. }, 1000);
  18. }
  19. });
  20. </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在这里主要做了三件事:

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

20.1.1 Observer

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

  1. <!--源码目录:src/instance/internal/state.js-->
  2. Vue.prototype._initData = function () {
  3. var dataFn = this.$options.data
  4. var data = this._data = dataFn ? dataFn() : {}
  5. if (!isPlainObject(data)) {
  6. data = {}
  7. process.env.NODE_ENV !== 'production' && warn(
  8. 'data functions should return an object.',
  9. this
  10. )
  11. }
  12. var props = this._props
  13. // proxy data on instance
  14. var keys = Object.keys(data)
  15. var i, key
  16. i = keys.length
  17. while (i--) {
  18. key = keys[i]
  19. // there are two scenarios where we can proxy a data key:
  20. // 1. it's not already defined as a prop
  21. // 2. it's provided via a instantiation option AND there are no
  22. // template prop present
  23. if (!props || !hasOwn(props, key)) {
  24. this._proxy(key)
  25. } else if (process.env.NODE_ENV !== 'production') {
  26. warn(
  27. 'Data field "' + key + '" is already defined ' +
  28. 'as a prop. To provide default value for a prop, use the "default" ' +
  29. 'prop option; if you want to pass prop values to an instantiation ' +
  30. 'call, use the "propsData" option.',
  31. this
  32. )
  33. }
  34. }
  35. // observe data
  36. observe(data, this)
  37. }

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

  1. <!--源码目录:src/instance/internal/state.js-->
  2. Vue.prototype._proxy = function (key) {
  3. if (!isReserved(key)) {
  4. // need to store ref to self here
  5. // because these getter/setters might
  6. // be called by child scopes via
  7. // prototype inheritance.
  8. var self = this
  9. Object.defineProperty(self, key, {
  10. configurable: true,
  11. enumerable: true,
  12. get: function proxyGetter () {
  13. return self._data[key]
  14. },
  15. set: function proxySetter (val) {
  16. self._data[key] = val
  17. }
  18. })
  19. }
  20. }

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

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

  1. <!--源码目录:src/observer/index.js-->
  2. export function observe (value, vm) {
  3. if (!value || typeof value !== 'object') {
  4. return
  5. }
  6. var ob
  7. if (
  8. hasOwn(value, '__ob__') &&
  9. value.__ob__ instanceof Observer
  10. ) {
  11. ob = value.__ob__
  12. } else if (
  13. shouldConvert &&
  14. (isArray(value) || isPlainObject(value)) &&
  15. Object.isExtensible(value) &&
  16. !value._isVue
  17. ) {
  18. ob = new Observer(value)
  19. }
  20. if (ob && vm) {
  21. ob.addVm(vm)
  22. }
  23. return ob
  24. }

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

  1. <!--源码目录:src/observer/index.js-->
  2. export function Observer (value) {
  3. this.value = value
  4. this.dep = new Dep()
  5. def(value, '__ob__', this)
  6. if (isArray(value)) {
  7. var augment = hasProto
  8. ? protoAugment
  9. : copyAugment
  10. augment(value, arrayMethods, arrayKeys)
  11. this.observeArray(value)
  12. } else {
  13. this.walk(value)
  14. }
  15. }

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

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

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

  1. <!--源码目录:src/observer/index.js-->
  2. Observer.prototype.convert = function (key, val) {
  3. defineReactive(this.value, key, val)
  4. }

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

  1. <!--源码目录:src/observer/index.js-->
  2. export function defineReactive (obj, key, val) {
  3. var dep = new Dep()
  4. var property = Object.getOwnPropertyDescriptor(obj, key)
  5. if (property && property.configurable === false) {
  6. return
  7. }
  8. // cater for pre-defined getter/setters
  9. var getter = property && property.get
  10. var setter = property && property.set
  11. var childOb = observe(val)
  12. Object.defineProperty(obj, key, {
  13. enumerable: true,
  14. configurable: true,
  15. get: function reactiveGetter () {
  16. var value = getter ? getter.call(obj) : val
  17. if (Dep.target) {
  18. dep.depend()
  19. if (childOb) {
  20. childOb.dep.depend()
  21. }
  22. if (isArray(value)) {
  23. for (var e, i = 0, l = value.length; i < l; i++) {
  24. e = value[i]
  25. e && e.__ob__ && e.__ob__.dep.depend()
  26. }
  27. }
  28. }
  29. return value
  30. },
  31. set: function reactiveSetter (newVal) {
  32. var value = getter ? getter.call(obj) : val
  33. if (newVal === value) {
  34. return
  35. }
  36. if (setter) {
  37. setter.call(obj, newVal)
  38. } else {
  39. val = newVal
  40. }
  41. childOb = observe(newVal)
  42. dep.notify()
  43. }
  44. })
  45. }

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

  1. <!--源码目录:src/observer/dep.js-->
  2. export default function Dep () {
  3. this.id = uid++
  4. this.subs = []
  5. }
  6. // the current target watcher being evaluated.
  7. // this is globally unique because there could be only one
  8. // watcher being evaluated at any time.
  9. Dep.target = null

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

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

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

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

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

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

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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注