Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
227 lines (179 sloc) 5.61 KB

2.3.1 响应式原理

前言

在之前的例子中,我们总是通过 vm.setData( { a:1, b:2 /* 需要填写整个完整的 data */} ) 来改变数据,从而引起界面的响应式变化。

为了提高开发效率和可读性,我们更希望使用 vm.a = 3 来修改值,从而更新视图。

所以问题就转化为:怎么监听 vm 对象上的数据变化?

我们分两步走:

  1. 监听 new Vue( { data } ) 里边的 $options.data 对象的数据变化
  2. 把 $options.data 代理到当前 vm 对象上

1. Object.defineProperty

可以利用 Object.defineProperty 定义对象的 get / set 行为,例如:

var val = vm.a // val = 1
Object.defineProperty(vm, 'a', {
  enumerable: true,
  configurable: true,
  get: function () { // 有人调用 vm.a 的读操作,例如 a = vm.a
    return val // val = 1
  },
  set: function (newVal) { // 有人调用 vm.a 的写操作,例如 vm.a = 3
    // newVal = 3
    val = newVal // val = 1
  }
}

接着我们就可以通过 Object.defineProperty 的方式全部重写掉 new Vue( { data } ) 里边的 options.data 对象的读写操作,以达到监听数据变化的目的。

在发生 set 写操作的时候,我们要调用 vm._update() 进行视图更新。

// core/instance/index.js
vm._data = $options.data
observe(vm._data = {}, vm)

// core/observer/index.js
export function observe (obj, vm) {
  if (!isObject(obj)) {
    return
  }

  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i], obj[keys[i]], vm)
  }
}

export function defineReactive (obj, key, val, vm) {
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      return val
    },
    set: function reactiveSetter (newVal) {
      const value = val

      if (newVal === value) {
        return
      }

      // console.log("newVal = ", newVal)
      val = newVal
      observe(newVal)
      
      // 当发生写操作的时候 要更新视图
      vm && vm._update()
    }
  })
}

2. 把 options.data 代理到 vm 对象

Vue 里边把 $options.data 挂在当前 vm 对象的 _data 属性,也就是 vm._data = $options.data

我们现在需要实现代理效果是: vm.a = 1 内部对应 vm._data.a = 1

我们依旧通过 Object.defineProperty 重新定义 vm 对象的 get / set 行为即可:

Object.defineProperty(vm, 'a', {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    return vm._data.a
  },
  set: function reactiveSetter (newVal) {
    vm._data.a = newVal
  }
});

按照这个思路,我们只要把 data 所有的 key 用这个方式全部代理一下即可。

// proxy(vm, '_data', 'a')
const keys = Object.keys(_data)
let i = keys.length
while (i--) {
  proxy(vm, `_data`, keys[i]) // 把 vm.abc 代理到 vm._data.abc
}

function proxy (target, sourceKey, key) {
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: function () { 
      // return vm._data.a
      return this[sourceKey][key]
    },
    set: function reactiveSetter (newVal) {
      // vm._data.a = newVal
      this[sourceKey][key] = newVal;
    }
  });
}

到这里基本的响应式原理就搞清楚了。

下边介绍一个新特性 computed ,也可以利用它更灵活的数据绑定。

computed 计算属性

为了让 todo 案例代码更加简洁,在这个分支,我们加入 Vue 的 computed 特性。

我们常常会有这么一个情况:

<div>
  <div>{{ a }}</div>
  <div>{{ b }}</div>
  <div>{{ a + b }}</div>
</div>

第三个 div 绑定的表达式是 a+b ,往往一些业务逻辑并不是这么简单的表达式,所以我们会在 data 里边定义多一个 c 属性:

<div>
  <div>{{ a }}</div>
  <div>{{ b }}</div>
  <div>{{ c }}</div>
</div>
vm = new Vue ({
  data : {
    a : a,
    b : b,
    c : a + b
  }
})

vm.a = 1
vm.c = vm.a + vm.b

但是这样有个问题,就是每次我们修改 vm.a 或者 vm.b 的时候,都需要再重新修改一次 vm.c 的值。

对于 c 这种通过其他值计算得到的属性,在 Vue 里边成为计算属性 computed (详细文档可以见: computed官方文档)。

我们可以新增 computed 特性,前边的例子改成:

vm = new Vue ({
  data : {
    a : a,
    b : b
  },
  computed: {
    c : () => {
      return this.a + this.b
    }
  }
})

vm.a = 1

接下来我们要改造源码,把 c 这个 getter 函数代理在当前 vm 对象:

// core/instance/index.js
Vue.prototype._init = function (options) {
  const vm = this
  // blabla...

  if (options.computed) initComputed(vm, options.computed)
  // blabla...
}
  
function initComputed(vm, computed) {
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

function defineComputed (target, key, userDef) {
  if (typeof userDef === 'function') { // computed传入function的话,不可写
    sharedPropertyDefinition.get = function () { return userDef.call(target) }
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get ? userDef.get : noop
    sharedPropertyDefinition.set = userDef.set ? userDef.set : noop
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

todo 案例的代码也做了些许更新,引入了 computed 特性,案例代码见: examples/2.3.1/todo/