美文网首页
再谈vue的响应式

再谈vue的响应式

作者: web前端_潘哥哥 | 来源:发表于2022-08-15 18:11 被阅读0次

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

    相关文章

      网友评论

          本文标题:再谈vue的响应式

          本文链接:https://www.haomeiwen.com/subject/upkdgrtx.html