再谈vue的响应式

2022-08-15  本文已影响0人  web前端_潘哥哥

再谈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好像还没讲到,尴尬,咱们先把这一套流程弄明白,下期再会也不迟)

上一篇 下一篇

猜你喜欢

热点阅读