vue(五):Vue 响应式原理

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

1 Object.defineProperty

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

1.1 语法

Object.defineProperty(obj, prop, descriptor)

参数

  • 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 主要 mount、render 方法,mount 调用 Watcher ,render 触发 Object.defineProperty 中 get 方法;

  • Observer 对 data 中的属性,使用 Object.defineProperty 定义 set 和 get;

  • Watcher 主要 addDep、update 方法,第一次触发 fn(),调用 render 方法,然后触发 get 方法;

  • Dep 主要 depend、addSub、notify 方法,depend 添加依赖,notify 被 set 调用,触发 Watcher update 方法。

实例思路导图:

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

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 前言 Vue.js 的核心包括一套“响应式系统”。 “响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码...
    world_7735阅读 950评论 0 2
  • 前言 Vue.js 的核心包括一套“响应式系统”。 “响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码...
    NARUTO_86阅读 37,384评论 8 86
  • 回忆 Observe观察者(建立响应式对象) 概括:它给数据通过defineProperty进行响应式化。...
    LoveBugs_King阅读 5,762评论 1 1
  • vue理解浅谈 一 理解vue的核心理念 使用vue会让人感到身心愉悦,它同时具备angular和react的优点...
    ambeer阅读 24,102评论 2 18
  • 这方面的文章很多,但是我感觉很多写的比较抽象,本文会通过举例更详细的解释。(此文面向的Vue新手们,如果你是个大牛...
    Ivy_2016阅读 15,367评论 8 64