再谈vue的响应式

再谈vue的响应式这次争取讲的明明白白

<div #app>name is {{this.name}}, age is {{this.age}</div>
const vm = new Vue({
 el: '#app',
 data: {
 name: 'Jason',
 age: 18,
 }
})

整体流程(最核心):

  1. 调用beforeCreate钩子函数

  2. 拿到option中的data,将其交给Observer类变成响应式的数据

  3. 调用created钩子函数

  4. 判断有没有el属性,

    1. 有的话就调用$mount方法,将el传进去,这个函数里面会进行将模板编译为render函数(如果是运行时编译的话),然后调用render函数拿到最新地vnode,根据vnode进行遍历递归渲染页面

    2. 没有就啥也不干,就完事了


      Vue.png
init.png

响应式(vue中通过Observer,Dep,Watcher配合scheduler调度器来实现,接下来会逐步引入这些概念)

什么是响应式数据?

响应式数据就是当我们给响应式数据重新赋值的时候,会自动执行某一些依赖这个响应式数据的函数

具体表现就是:

比如这里有一个对象,还有一个函数,我们把它叫做render函数吧

const obj = {
   name: "Jason",
   age: 18
}

function render () {
   const div = document.querySelector('#app')
   div.innerHTML = `name is ${obj.name}, age is ${obj.age}`
}

通过简单的观察,我们可以发现render函数执行的时候,使用到了obj的两个属性。

思考一下,如果说我们做这么一步操作obj.age = 19, 然后自动地执行了render函数,界面就会更新

是不是就好像跟vue差不多了,数据一改变,视图自当更新!

然后我们再回过头看看上面那句话:

响应式数据就是当我们给响应式数据重新赋值的时候,会自动执行某一些依赖这个响应式数据的函数

是不是感觉好像明白了些什么~~

响应式数据怎么实现呢?

我们通过一个函数专门来做这件事,暂且就叫做Observer吧

这个函数接收一个普通的对象,然后再对这个对象进行一些处理,那么这个对象就变成了响应式对象

function Observer (data) {
   for (const prop in data) {
     let value = data[prop]
     Object.defineProperty(data, prop, {
       get() {
         // 虽然获取data[prop]的时候这里我可以知道,但是这里我要做啥?
         return value
       },
       set(val) {
         value = val
         // 虽然给data[prop]赋值的时候我可以在这里知道,但是这里我要做啥?
       }
     })
   }
}

思考一下我们可以发现,在调用前面的render函数的时候,会用到响应式数据,就会触发getter

那么我们就可以在getter里面做文章了

function Observer (data) {
   for (const prop in data) {
     let value = data[prop]
     const dep = [] // 新增代码
     Object.defineProperty(data, prop, {
       get() {
       // 有函数用到了我,我得将这个函数收集一下,但是收集到哪里去呢?
       // 我们给每一个属性分配一个数组dep容器,就放到这里面去
         dep.push(render)  // 新增代码
         return value
       },
       set(val) {
         value = val
         // 虽然给data[prop]赋值的时候我可以在这里知道,但是这里我要做啥?
       }
     })
   }
}

然后我们再思考,setter里面要干嘛呢?

setter执行,说明什么,说明有人要给这个属性重新赋值,那么我们需要怎么做?是不是把刚刚收集到的那个render函数拿出来执行一下就可以了

于是就有了以下代码:

function Observer (data) {
   for (const prop in data) {
     let value = data[prop]
     const dep = [] // 新增代码
     Object.defineProperty(data, prop, {
       get() {
         // 有函数用到了我,我得将这个函数收集一下,但是收集到哪里去呢?
         // 我们给每一个属性分配一个数组dep容器,就放到这里面去
         dep.push(render)  // 新增代码
         return value
       },
      set(val) {
        value = val
        // 这里把刚刚getter收集到的依赖函数拿出来执行一遍
        dep.forEach(item => {  // 新增代码
          item()
        })
      }
    })
  }
}

现在,我们可以浅浅地模拟一下vue的源码:

function Observer(vm, data) {
   for (const prop in data) {
     let value = data[prop];
     const dep = []; // 新增代码
     Object.defineProperty(data, prop, {
       get() {
         // 有函数用到了我,我得将这个函数收集一下,但是收集到哪里去呢?
         // 我们给每一个属性分配一个数组dep容器,就放到这里面去
         dep.push(vm._render); // 新增代码
         return value;
       },
       set(val) {
         value = val;
         // 这里把刚刚getter收集到的依赖函数拿出来执行一遍
         dep.forEach((item) => {
           // 新增代码
           item.call(vm);
         });
       },
     });
     Object.defineProperty(vm, prop, {
       get() {
         return data[prop];
       },
       set(val) {
         data[prop] = val
       },
     });
   }
}

function Vue(options) {
   // 1\. 调用beforeCreate钩子函数 ...
   // 2\. 拿到option中的data,将其交给Observer类变成响应式的数据
   Observer(this, options.data || {});
   this._render = options.render;
   // 3\. 调用created钩子函数 ...

   // 4\. 判断有没有el属性
   //   if (options.el) {
   //     this.$mount(options.el) // 这个代码就不实现了
   //   }

   // 我们将第四部简化一下
   options.render.call(this);
}
<!DOCTYPE html>
<html lang="en">
 <head>
 <meta charset="UTF-8" />
 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 <title>Document</title>
 </head>
 <body>
 <div id='app'></div>
 <script src="./my-vue.js"></script>
 <script>
   const vm = new Vue({
     data: {
       name: "Jason",
       age: 18,
     },
     render() {
       const div = document.querySelector("#app");
       div.innerHTML = `name is ${this.name}, age is ${this.age}`;
     },
   });


   setTimeout(() => {
     vm.age = 19
   }, 1000)

 </script>
 </body>
</html>

强烈建议大家把上面的代码copy到编辑器中,然后运行看看效果,然后捋一下代码每一行是什么意思

引入Dep概念:

我们可以把依赖收集,派发更新这些操作专门抽离出一个类来处理

class Dep {
 constructor() {
   this.subs = [];
 }
 depend(target) {
   this.subs.push(target);
 }
 notify() {
   this.subs.forEach((sub) => {
     sub();
   });
 }
}

然后把Observer函数里面的代码小改一下,就是下面这个样子:

class Dep {
 constructor() {
   this.subs = [];
 }
 depend(target) {
   this.subs.push(target);
 }
 notify() {
   this.subs.forEach((sub) => {
     sub();
   });
 }
}

function Observer(vm, data) {
   for (const prop in data) {
     let value = data[prop];
     const dep = new Dep();   // 改动点
     Object.defineProperty(data, prop, {
       get() {
         dep.depend(vm._render.bind(vm));  // 改动点
         return value;
       },
       set(val) {
         value = val;
         dep.notify();  // 改动点
       },
     });
     Object.defineProperty(vm, prop, {
       get() {
         return data[prop];
       },
       set(val) {
         data[prop] = val;
       },
     });
   }
}

function Vue(options) {
   // 1\. 调用beforeCreate钩子函数 ...
   // 2\. 拿到option中的data,将其交给Observer类变成响应式的数据
   Observer(this, options.data || {});
   this._render = options.render;
   // 3\. 调用created钩子函数 ...

   // 4\. 判断有没有el属性
   //   if (options.el) {
   //     this.$mount(options.el) // 这个代码就不实现了
   //   }

   // 我们将第四部简化一下
   options.render.call(this);
}

引入Watcher概念:

我们仔细想想会发现一个大问题,我们在依赖收集的时候,是不是把收集到的东西写死了,导致只能收集到render函数,不能收集到别的。

这里大家可能会想,我也不需要收集其他什么东西了啊,不就是render函数嘛。数据更新,视图自动更新,还有什么其他的东西嘛

大家可以看看如下代码:

 const vm = new Vue({
   el: "#app",
   data: {
     lastname: "老",
     firstname: "王",
     no: 1,
   },
   computed: {
     fullname() {
       console.log("fullname");
       return this.lastname + this.firstname;
     }
   },
   methods: {
     console() {
       console.log(this.fullname)
     }
   },
   render(h) {
     return h("p", [h("span", this.no)]);
   },
 });

可以发现,我的视图只依赖no属性,你firstname,lastname变了跟我视图有什么关系,我并不需要更新视图。

虽然大家目前还不知道firstname变了需要干嘛,可以猜想应该是执行跟fullname这个计算属性有关的函数,但肯定不是执行render函数对吧。


总而言之,依赖收集的时候不能写死,而应该跟在获取这个属性的时候,所在的函数有关

那这句话又怎么理解呢?

是这样的,no属性在获取的时候是由于render函数调用,而firstname属性获取的时候,是由于fullname这个计算属性的调用,

那么他们应该分别收集render函数,fullname函数,而不能写死为render函数。


说了这么多,其实就是想引入Watcher这么一个概念

让watcher去管理这些属性到底应该收集什么东西,你在收集的时候,只管去收集一个固定的变量就好了,Wacther会去管理那个变量的值。

那具体应该怎么管理呢?

其实就是把那些要执行的函数不要直接去执行,而是交给Watcher去执行。

上代码:

class Watcher {
 // 新增代码
 constructor(vm, fn) {
   this.vm = vm;
   this.getter = fn;
   this.get();
 }
 get() {
   Dep.target = this;
   this.getter.call(this.vm);
   Dep.target = undefined;
 }
 update() {
   this.get();
 }
}

class Dep {
   static target = undefined; // 改动点
   constructor() {
      this.subs = [];
   }
   depend(target) {
     this.subs.push(target);
   }
   notify() {
     this.subs.forEach((sub) => {
        sub.update(); // 改动点
     });
   }
}

function Observer(vm, data) {
   for (const prop in data) {
   let value = data[prop];
   const dep = new Dep();
   Object.defineProperty(data, prop, {
     get() {
     if (Dep.target) {
        // 改动点
       dep.depend(Dep.target); // 改动点
     }
     return value;
     },
     set(val) {
       value = val;
       dep.notify();
     },
     });
   Object.defineProperty(vm, prop, {
     get() {
       return data[prop];
     },
     set(val) {
       data[prop] = val;
     },
   });
 }
}

function Vue(options) {
 // 1\. 调用beforeCreate钩子函数 ...
 // 2\. 拿到option中的data,将其交给Observer类变成响应式的数据
 Observer(this, options.data || {});
 this._render = options.render;
 // 3\. 调用created钩子函数 ...

 // 4\. 判断有没有el属性
 //   if (options.el) {
 //     this.$mount(options.el) // 这个代码就不实现了
 //   }

 // 我们将第四部简化一下
 // options.render.call(this);
 new Watcher(this, options.render); // 新增代码
}

强烈建议大家把上面的代码copy到编辑器中,然后运行看看效果,然后捋一下代码每一行是什么意思

那么到这里呢,关于vue的响应式的三个类的最最核心功能就讲完了,

虽然这一套流程还是有很多的缺陷,但是肯定能够帮助大家理解vue源码里面的主线。

其实大家好好捋捋,多看几遍,然后打打断点啥的,还是能够理解这一套流程的。

这里面的每一个类中,都有很多实现细节,我这里就不展开了,打算之后专门弄一个系列来讲这些细节部分,每一个细节可能都会用一篇文章来讲解。(先给自己挖个坑)

(调度器相关的nextTick好像还没讲到,尴尬,咱们先把这一套流程弄明白,下期再会也不迟)

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

推荐阅读更多精彩内容