vue(五):Vue 响应式原理

2019-07-07  本文已影响0人  林ze宏

理解原理之前,可以先查看 Object.defineProperty 相关知识点。

1 Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

1.1 语法

Object.defineProperty(obj, prop, descriptor)

参数

1.2 示例

添加多个属性和默认值

考虑特性被赋予的默认特性值非常重要,通常,使用点运算符和 Object.defineProperty() 为对象的属性赋值时,数据描述符中的属性默认值是不同的,如下例所示。

var o = {};

o.a = 1;
// 等同于 :
Object.defineProperty(o, "a", {
  value : 1,
  writable : true,
  configurable : true,
  enumerable : true
});


// 另一方面,
Object.defineProperty(o, "a", { value : 1 });
// 等同于 :
Object.defineProperty(o, "a", {
  value : 1,
  writable : false,
  configurable : false,
  enumerable : false
});

一般的 Setters 和 Getters

下面的例子展示了如何实现一个自存档对象。 当设置temperature 属性时,archive 数组会获取日志条目。

function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get: function() {
      console.log('get!');
      return temperature;
    },
    set: function(value) {
      temperature = value;
      archive.push({ val: temperature });
    }
  });

  this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.temperature; // 输出 'get!',获取值的时候,调用 get 方法
arc.temperature = 11; // 设置值的时候,调用 set 方法
arc.temperature = 13; // 设置值的时候,调用 set 方法
arc.getArchive(); // [{ val: 11 }, { val: 13 }]

var pattern = {
    get: function () {
        return 'I alway return this string,whatever you have assigned';
    },
    set: function () {
        this.myname = 'this is my name string';
    }
};


function TestDefineSetAndGet() {
    Object.defineProperty(this, 'myproperty', pattern);
}


var instance = new TestDefineSetAndGet();
instance.myproperty = 'test';

// 'I alway return this string,whatever you have assigned'
console.log(instance.myproperty);

// 'this is my name string'
console.log(instance.myname); // 继承属性

参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

2 深入响应式原理

如何追踪变化

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

检测变化的注意事项

由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如:

var vm = new Vue({
  data:{
    a:1
  }
})

// `vm.a` 是响应式的

vm.b = 2
// `vm.b` 是非响应式的

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。例如,对于:

Vue.set(vm.someObject, 'b', 2)

您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

this.$set(this.someObject,'b',2)

有时你可能需要为已有对象赋值多个新属性,比如使用 Object.assign()_.extend()。但是,这样添加到对象上的新属性不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的属性一起创建一个新的对象。

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

声明响应式属性

由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值:

var vm = new Vue({
  data: {
    // 声明 message 为一个空值字符串
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'

异步更新队列

可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。

例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

3 实例分析

自己实现一个响应式实例,完整代码为:

const Observer = function(data) {
  for (let key in data) {
    defineReactive(data, key);
  }
};

const defineReactive = function(obj, key) {
  const dep = new Dep();
  let val = obj[key];
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log('in get');
      dep.depend();
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      dep.notify();
    }
  });
};

const observe = function(data) {
  return new Observer(data);
};

const Vue = function(options) {
  const self = this;
  if (options && typeof options.data === 'function') {
    this._data = options.data.apply(this);
  }

  this.mount = function() {
    return new Watcher(self, self.render);
  };

  this.render = function() {
    // 在 Vue 的 render 函数中,我们调用了本次渲染相关的值,self._data.text
    // 所以,与渲染无关的值,并不会触发 get,
    // 也就不会在依赖收集器中添加到监听(addSub 方法不会触发),
    // 即使调用 set 赋值,notify 中的 subs 也是空的。
    // 例如:下文的 vue._data.text2 = '123'; 因为 render 函数中没有获取该属性,
    //       也就是没有触发 get 方法,则没有对该属性进行监听

    // self._data.text2; 只要有调用就会进行监听,并不需要返回
    return {
      text: self._data.text // 触发 get 方法,则会在依赖收集器 Dep 中添加到监听
      // text3: 'ssds' // 不会触发 get 方法
    };
  };

  observe(this._data);
};

const Watcher = function(vm, fn) {
  const self = this;
  this.vm = vm;
  Dep.target = this; // 将收集器的目标指向了当前 Watcher

  this.addDep = function(dep) {
    dep.addSub(self);
  };

  this.update = function() {
    console.log('in watcher update');
    fn();
  };

  this.value = fn();
  Dep.target = null;
};

const Dep = function() {
  const self = this;
  this.target = null;
  this.subs = [];
  this.depend = function() {
    if (Dep.target) {
      console.log('Dep.target');
      console.log(Dep.target);
      Dep.target.addDep(self);
    }
  };

  this.addSub = function(watcher) {
    self.subs.push(watcher);
  };

  this.notify = function() {
    for (let i = 0; i < self.subs.length; i += 1) {
      self.subs[i].update();
    }
  };
};

const vue = new Vue({
  data() {
    return {
      text2: 'hello worlds',
      text: 'hello world'
    };
  }
});

vue.mount(); // in get
vue._data.text = '123'; // in watcher update /n in get

vue._data.text2 = '123';

说明:主要对象有 Vue、Observer、Watcher、Dep;

实例思路导图:

vue 响应式原理--实例思路导图

参考

上一篇 下一篇

猜你喜欢

热点阅读