Vue 响应式原理解析与实现

从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页面更新渲染。之后,再接触了vue.js,当时也一度很好奇vue.js是如何监测数据更新并且重新渲染页面。今天,就我们就来一步步解析vue.js响应式的原理,并且来实现一个简单的demo。

首先,先让我们来了解一些基础知识。

基础知识

Object.defineProperty

es5新增了Object.defineProperty这个api,它可以允许我们为对象的属性来设定getter和setter,从而我们可以劫持用户对对象属性的取值和赋值。比如以下代码:

const obj = {

};

let val = 'cjg';

Object.defineProperty(obj, 'name', {

 get() {

   console.log('劫持了你的取值操作啦');

   return val;

 },

 set(newVal) {

   console.log('劫持了你的赋值操作啦');

   val = newVal;

 }

});

console.log(obj.name);

obj.name = 'cwc';

console.log(obj.name);

我们通过Object.defineProperty劫持了obj[name]的取值和赋值操作,因此我们就可以在这里做一些手脚啦,比如说,我们可以在obj[name]被赋值的时候触发更新页面操作。

发布订阅模式

发布订阅模式是设计模式中比较常见的一种,其中有两个角色:发布者和订阅者。多个订阅者可以向同一发布者订阅一个事件,当事件发生的时候,发布者通知所有订阅该事件的订阅者。我们来看一个例子了解下。

class Dep {

 constructor() {

   this.subs = [];

 }

 // 增加订阅者

 addSub(sub) {

   if (this.subs.indexOf(sub) < 0) {

     this.subs.push(sub);

   }

 }

 // 通知订阅者

 notify() {

   this.subs.forEach((sub) => {

     sub.update();

   })

 }

}

const dep = new Dep();

const sub = {

 update() {

   console.log('sub1 update')

 }

}

const sub1 = {

 update() {

   console.log('sub2 update');

 }

}

dep.addSub(sub);

dep.addSub(sub1);

dep.notify(); // 通知订阅者事件发生,触发他们的更新函数

动手实践

我们了解了Object.defineProperty和发布订阅者模式后,我们不难可以想到,vue.js是基于以上两者来实现数据监听的。

vue.js首先通过Object.defineProperty来对要监听的数据进行getter和setter劫持,当数据的属性被赋值/取值的时候,vue.js就可以察觉到并做相应的处理。

通过订阅发布模式,我们可以为对象的每个属性都创建一个发布者,当有其他订阅者依赖于这个属性的时候,则将订阅者加入到发布者的队列中。利用Object.defineProperty的数据劫持,在属性的setter调用的时候,该属性的发布者通知所有订阅者更新内容。 

接下来,我们来动手实现(详情可以看注释):

class Observer {

 constructor(data) {

   // 如果不是对象,则返回

   if (!data || typeof data !== 'object') {

     return;

   }

   this.data = data;

   this.walk();

 }

 // 对传入的数据进行数据劫持

 walk() {

   for (let key in this.data) {

     this.defineReactive(this.data, key, this.data[key]);

   }

 }

 // 创建当前属性的一个发布实例,使用Object.defineProperty来对当前属性进行数据劫持。

 defineReactive(obj, key, val) {

   // 创建当前属性的发布者

   const dep = new Dep();

   /*

   * 递归对子属性的值进行数据劫持,比如说对以下数据

   * let data = {

   *   name: 'cjg',

   *   obj: {

   *     name: 'zht',

   *     age: 22,

   *     obj: {

   *       name: 'cjg',

   *       age: 22,

   *     }

   *   },

   * };

   * 我们先对data最外层的name和obj进行数据劫持,之后再对obj对象的子属性obj.name,obj.age, obj.obj进行数据劫持,层层递归下去,直到所有的数据都完成了数据劫持工作。

   */

   new Observer(val);

   Object.defineProperty(obj, key, {

     get() {

       // 若当前有对该属性的依赖项,则将其加入到发布者的订阅者队列里

       if (Dep.target) {

         dep.addSub(Dep.target);

       }

       return val;

     },

     set(newVal) {

       if (val === newVal) {

         return;

       }

       val = newVal;

       new Observer(newVal);

       dep.notify();

     }

   })

 }

}

// 发布者,将依赖该属性的watcher都加入subs数组,当该属性改变的时候,则调用所有依赖该属性的watcher的更新函数,触发更新。

class Dep {

 constructor() {

   this.subs = [];

 }

 addSub(sub) {

   if (this.subs.indexOf(sub) < 0) {

     this.subs.push(sub);

   }

 }

 notify() {

   this.subs.forEach((sub) => {

     sub.update();

   })

 }

}

Dep.target = null;

// 观察者

class Watcher {

 /**

  *Creates an instance of Watcher.

  * @param {*} vm

  * @param {*} keys

  * @param {*} updateCb

  * @memberof Watcher

  */

 constructor(vm, keys, updateCb) {

   this.vm = vm;

   this.keys = keys;

   this.updateCb = updateCb;

   this.value = null;

   this.get();

 }

 // 根据vm和keys获取到最新的观察值

 get() {

   Dep.target = this;

   const keys = this.keys.split('.');

   let value = this.vm;

   keys.forEach(_key => {

     value = value[_key];

   });

   this.value = value;

   Dep.target = null;

   return this.value;

 }

 update() {

   const oldValue = this.value;

   const newValue = this.get();

   if (oldValue !== newValue) {

     this.updateCb(oldValue, newValue);

   }

 }

}

let data = {

 name: 'cjg',

 obj: {

   name: 'zht',

 },

};

new Observer(data);

// 监听data对象的name属性,当data.name发现变化的时候,触发cb函数

new Watcher(data, 'name', (oldValue, newValue) => {

 console.log(oldValue, newValue);

})

data.name = 'zht';

// 监听data对象的obj.name属性,当data.obj.name发现变化的时候,触发cb函数

new Watcher(data, 'obj.name', (oldValue, newValue) => {

 console.log(oldValue, newValue);

})

data.obj.name = 'cwc';

data.obj.name = 'dmh';


结语


这样,一个简单的响应式数据监听就完成了。当然,这个也只是一个简单的demo,来说明vue.js响应式的原理,真实的vue.js源码会更加复杂,因为加了很多其他逻辑。

接下来我可能会将其与html联系起来,实现v-model、computed和{{}}语法。代码地址 有兴趣的欢迎来一起研究探讨下。点击这里查看第二节的内容。如果觉得有收获的话也请点个赞,嘿嘿。


作者:陈陈jg

https://www.cnblogs.com/chenjg/p/9541291.html


感兴趣的小伙伴,可以关注公众号【grain先森】,回复关键词 “Vue”,获取更多资料,更多关键词玩法期待你的探索~

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

推荐阅读更多精彩内容

  • 本文是lhyt本人原创,希望用通俗易懂的方法来理解一些细节和难点。转载时请注明出处。文章最早出现于本人github...
    lhyt阅读 2,193评论 0 4
  • 几种双向绑定的做法目前几种主流的mvc(vm)框架都实现了单向数据绑定,我认为的双向数据绑定其实就是在单向绑定的基...
    Picidae阅读 5,617评论 2 4
  • vue理解浅谈 一 理解vue的核心理念 使用vue会让人感到身心愉悦,它同时具备angular和react的优点...
    ambeer阅读 24,100评论 2 18
  • 网传,发哥减肥靠的是每天最少快走一个半小时,再配合清淡餐单10个月内激减28斤。一时间很多媒体小伙伴开始宣扬,快走...
    surberina阅读 10,674评论 14 229
  • 本以为电脑内存 16 G 已经足够使用了,安装系统时也没有开 swap 分区。但是开了多个 Android Stu...
    FrankFan阅读 3,825评论 0 0