[Vue]实现一个简易的响应式系统

211 阅读4分钟

本文主要通过探究Vue2.x实现数据响应系统的原理。我们都知道Vue 2.x采用的是ES5的Object.defineProperty实现数据响应的。基于该API,手动实现一个简易的响应式系统,以此来进一步理解Vue的响应式设计。

Object.defineProperty

一、方法的使用

查看一下MDN文档,用一句话总结就是:Object.defineProperty会作用一个对象,在该对象上定义一个新的属性或者修改现有的属性,最后返回该对象。先看一段代码,了解该方法是干嘛用的,通过代码概括它有哪些特点。

var obj = {};
Object.defineProperty(obj, 'name', {});

obj.name; // undefined
obj.name = 'xiao';
obj.name; // undefined

for(var k in obj) {
  console.log(k); // undefined
}
Object.keys(obj); // []
JSON.stringify(obj); // "{}"

从上面测试的代码可以看出,如果不设置descriptor对象,默认该对象内的所有为boolean类型的值都为false,非boolean类型的都为undefined。即:

{
  configurable: false,
  enumberable: false,
  writable: false,
  value: undefined,
  set: undefined,
  get: undefined
}
  • configurable:决定该对象的属性描述对象是否可被修改、对象的属性是否可被deleteReflect.deleteProperty等方法删除。
  • enumerable:决定该对象的属性是否可被for..inObject.keysJSON.stringify获取
  • writable:决定该对象的属性是否可被修改。
  • value:对象属性的值。
  • set:属性的setter函数。当属性值被修改时会触发该方法,并传入一个新值作为参数, 同时内部可以获取this对象,函数的返回值会作为属性的新值。
  • get:属性的getter函数。当读取属性值时会触发该方法,同时内部可以获取this对象。

二、使用注意事项

  • 错误示例一:指定了value,又指定了get/set访问器。此时会报错:Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object> at Function.defineProperty。意思是:非法的描述符。不能同时指定访问器和值或可写属性。解决办法:valueget/set访问器只能二选一。
var obj = {};

Object.defineProperty(obj, 'name', {
  // 这里既然指定了value,就表明obj对象的name属性就是xiaoming了,
  // 且这里没有指明writable,默认为false,所以会跟get/set职责冲突了,
  // 即使这里设置`writable: true`也同样报错!这是为什么?
  value: 'xiaoming',
  get() { },
  set(newVal) { }
});
  • 错误示例二:对象属性存取出现死循环。解决办法:用一个新的变量保存存取值。
var obj = {};
// var value = 'xiaoming';

Object.defineProperty(obj, 'name', {
  // 当读取obj.name时,又会触发get方法,
  // get方法内又去读取obj.name,又会触发get方法,循环往复...
  // set方法里的操作同理。
  get() { return obj.name; },
  set(newVal) { obj.name = newVal; }
});

了解了Object.defineProperty的大致用法后,来看看Vue 2.x是如何使用它来完成数据响应式系统的。

简易的响应式系统

核心点:

  • 视图更新函数。更新DOM。
  • 数据响应函数。内部使用Object.defineProperty创建一个响应式对象,检测对象的每个属性的存取操作,然后通知视图更新函数进行DOM更新。
  • Vue类。测试该数据的响应式系统。

视图更新函数

主要职责就是处理视图的更新,更新DOM。

function renderView() {
  console.log('view is udpated');
}

数据响应函数

主要职责就是给对象的属性添加getter函数setter函数,便于知晓和控制对象属性的变化,然后去做一些事情(比如:视图更新依赖收集等)。

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      return val;
    },
    set: function reactiveSetter(newVal) {
      // newVal !== newVal && val !== val: for NaN scenario
      if (newVal === val || (newVal !== newVal && val !== val)) {
        return;
      }
      renderView();
    }
  });
}

对象观察函数

主要职责就是给数据对象的每一个属性添加响应式操作。

function observe(value) {
  if (!value || typeof value !== 'object') {
    return;
  }
  Object.keys(value).forEach((key) => {
    // 这里需要判断对象属性的子属性为对象的情况,
    // 递归给每一个属性添加getter、setter函数
    // ⚠注意:Object.defineProperty对数组的push/pop、length等操作无感
    if (typeof value[key] === 'object') {
      observe(value[key]);
      return;
    }
    defineReactive(vale, key, value[key]);
  });
}

Vue类

Vue类初始化时,就将该配置对象的data对象进行observe,以达到数据变化,视图更新的目的。

class Vue {
  constructor(options) {
    this._data = options.data;
    observe(this._data);
  }
}

const vm = new Vue({
  data: {
    name: 'xiaoming',
    age: 18
  }
});

vm._data.age = 20; // view is udpated

// 剥开vm._data,看看内部是什么!很明显,age和name都分别有了各自的get、set函数
{
  age: 12
  name: "xiaoming"
  get age: ƒ ()
  set age: ƒ (newVal)
  get name: ƒ ()
  set name: ƒ (newVal)
  __proto__: Object
}

总结

Object.defineProperty的特点:

  1. 它的最大威力在于可以知道对象属性的存取行为;
  2. 不能处理数组的更新,比如对数组执行push、pop等操作,视图无感;而在vue源码内部,它是单独对数组的更新进行了处理的;
  3. 当要处理的data数据很多(key-value很多)、层级嵌套很深时,对于性能来说会是一个问题;