精华内容
下载资源
问答
  • Vue响应式原理

    2021-03-10 11:36:00
    Vue响应式原理 1、目标 模拟一个最小版本的Vue 响应式原理在面试的常问问题 实际项目中出现问题的原理层面的解决 ​ 给Vue实例新增一个成员是否是响应式的? ​ 给属性重新赋值成对象,是否是响应式的? 为学习Vue...

    Vue响应式原理

    1、目标

    模拟一个最小版本的Vue

    响应式原理在面试的常问问题

    实际项目中出现问题的原理层面的解决

    ​ 给Vue实例新增一个成员是否是响应式的?

    ​ 给属性重新赋值成对象,是否是响应式的?

    为学习Vue源码做铺垫。

    2、数据驱动

    在实现整个Vue响应式代码之前,我们先来了解几个概念。

    第一个:数据驱动

    第二个:响应式的核心原理

    第三个:发布订阅模式和观察这模式

    我们先来看一下数据驱动的内容:

    数据响应式,双向绑定,数据驱动(我们经常看到这几个词)

    数据响应式:数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了频繁的DOM操作,提高开发效率,这与Jquery不一样,Jquery是频繁的操作Dom

    双向绑定:

    数据改变,视图改变,视图改变,数据也随之改变( 通过这句话,我们可以看到在双向绑定中是包含了数据响应式的内容)

    ​ 我们可以使用v-model 在表单元素上创建双向数据绑定

    数据驱动是Vue最独特的特性之一

    ​ 开发过程中仅仅需要关注数据本身,不需要关心数据是如何渲染到视图中的。主流的MVVM框架都已经实现了数据响应式与双向绑定,所以可以将数据绑定到DOM上。

    3、响应式的核心原理

    3.1 Vue2.x响应式原理

    关于Vue2.x的响应式原理在官方文档中也有介绍。

    https://cn.vuejs.org/v2/guide/reactivity.html

    在该文档中,我们注意如下一段内容:

    当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
    

    通过以上的文字,我们可以看到,在Vue2.x中响应式的实现是通过Object.defineProperty来完成的,注意该属性无法降级(shim)处理,所以Vue不支持IE8以及更低版本的浏览器的原因。

    下面我们来看一下Object.defineProperty基本使用

    修改data对象中的msg属性的值,实现视图的更新.(这也就是我们所说的响应式)

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>defineProperty</title>
      </head>
      <body>
        <div id="app">hello</div>
        <script>
          //模拟Vue中的data选项(当)
          let data = {
            msg: "hello",
          };
          //模拟Vue的实例
          let vm = {};
          //数据劫持,当访问或者设置vm中的成员的时候,做一些干预操作
          Object.defineProperty(vm, "msg", {
            //可枚举(可遍历)
            enumerable: true,
            //可配置(可以使用delete删除,可以通过defineProperty重新定义)
            configurable: true,
            //当获取值的时候执行
            get() {
              console.log("get:", data.msg);
              return data.msg;
            },
            // 当设置值的时候执行
            set(newValue) {
              console.log("set:", newValue);
              //设置的值与原有的值相同,则没有更改,所以不做任何操作
              if (newValue === data.msg) {
                return;
              }
              data.msg = newValue;
              //数据更改,更新DOM的值
              document.querySelector("#app").textContent = data.msg;
            },
          });
          //测试
          //执行set操作
          vm.msg = "abc";
          //执行get操作
          console.log(vm.msg);
        </script>
      </body>
    </html>
    
    

    在进行测试的时候,可以在浏览器的控制台中,输入vm.msg进行测试。

    在上面的代码中,我们是将一个对象中的属性转换成了getter/setter的形式,那么这里我们还有一个问题:

    如果有一个对象中多个属性需要转换getter/setter,那么应该如何处理?

    我们可以通过循环遍历的方式,将对象中的多个属性转换成getter/setter

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>defineProperty多个属性</title>
      </head>
      <body>
        <div id="app">hello</div>
        <script>
          //模拟Vue中的data选项
          let data = {
            msg: "hello",
            count: 10,
          };
          //模拟Vue实例
          let vm = {};
          proxyData(data);
          function proxyData(data) {
            //遍历data对象中的所有属性
            Object.keys(data).forEach((key) => {
              // 把data中的属性,转换成vm的setter/getter
              Object.defineProperty(vm, key, {
                enumerable: true,
                configurable: true,
                get() {
                  console.log("get", key, data[key]);
                  return data[key];
                },
                set(newValue) {
                  console.log("set:", key, newValue);
                  if (newValue === data[key]) {
                    return;
                  }
                  data[key] = newValue;
                  document.querySelector("#app").textContent = data[key];
                },
              });
            });
          }
          vm.msg = "hello world";
          console.log(vm.msg);
        </script>
      </body>
    </html>
    

    在上面的代码中,我们通过循环的方式给data对象中的每个属性添加了getter/setter.

    这里我们只是在视图中展示了msg属性的值,如果想展示count属性的值,可以在浏览器的控制台中,通过vm.count=20这种形式来展示,当然,在后期我们会分别展示出msgcount属性的值,

    3.2 Vue3响应式原理

    Vue3的响应式原理是通过Proxy来完成的。

    Proxy直接监听对象,而非属性,所以将多个属性转换成getter/setter的时候,不需要使用循环。

    ProxyES6中新增的,IE不支持

    Proxy实现响应式的基本代码如下(该代码的功能与上面所讲解的是一样的):

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Proxy</title>
      </head>
      <body>
        <div id="app">hello</div>
        <script>
          //模拟Vue中的data选项
          let data = {
            msg: "hello",
            count: 0,
          };
          //模拟Vue实例
          //为data创建一个代理对象vm,这样就可以通过vm.msg来获取data中的msg属性的值,而这时候会执行get方法
          let vm = new Proxy(data, {
            // 当访问vm的成员时会执行
            //target表示代理的对象(这里为data对象),key表示所代理的对象中的属性
            get(target, key) {
              console.log("get key:", key, target[key]);
              return target[key];
            },
            //当设置vm的成员时会执行
            set(target, key, newValue) {
              console.log("set key:", key, newValue);
              if (target[key] === newValue) {
                return;
              }
              target[key] = newValue;
              document.querySelector("#app").textContent = target[key];
            },
          });
          //测试
          vm.msg = "aaaa";
          console.log(vm.msg);
        </script>
      </body>
    </html>
    
    

    通过以上的代码我们发现使用Proxy的代码是给对象中所有属性添加getter/setter,而不需要通过循环的方式来实现,所以代码更加的简洁。

    4、发布订阅模式

    发布订阅模式:订阅者,发布者,信号中心

    我们假定,存在一个“信号中心”,某个任务执行完成,就向信号中心"发布"(publish)一个信号,其它任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern)
    

    家长向学生所在的班级订阅了获取学生考试成绩的事件,当老师公布学生的成绩后,就会自动通知学生的家长。

    在整个案例中,学生所在的班级为信号中心,老师为发布者,家长为订阅者

    Vue 的自定义事件就是基于发布订阅模式来实现的。

    下面通过Vue中兄弟组件通信过程,来理解发布订阅模式

    // eventBus.js
    // 事件中心
    let eventHub=new Vue()
    //ComponentA.vue
    addTodo:function(){
        //发布消息(事件)
        eventHub.$emit('add-todo',{text:this.newTodoText})
        this.newTodoText=''
    }
    //ComponentB.vue
    //订阅者
    created:function(){
        //订阅消息(事件)
        eventHub.$on('add-todo',this.addTodo)
    }
    

    通过以上代码,我们可以理解发布订阅模式中的核心概念。

    下面我们模拟Vue中的自定义事件的实现

    下面我们先来做一个基本的分析:

    先来看如下代码:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Vue 自定义事件</title>
      </head>
      <body>
        <script src="./js/vue.js"></script>
        <script>
          //Vue自定义事件
          let vm = new Vue();
          //注册事件(订阅消息)
          vm.$on("dataChange", () => {
            console.log("dataChange");
          });
          vm.$on("dataChange", () => {
            console.log("dataChange");
          });
          //触发事件(发布消息)
          vm.$emit("dataChange");
        </script>
      </body>
    </html>
    

    通过上面的代码,我们可以看到$on实现事件的注册,而且可以注册多个事件,那么我们可以推测在其内部有一个对象来存储注册的事件,对象的格式为:

    {'click':[fn1,fn2],'change':[fn]}
    

    以上格式说明了,我们注册了两个事件,分别为clickchange.

    下面我们根据以上的分析过程,来模拟实现自定义事件。

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>发布订阅模式</title>
      </head>
      <body>
        <script>
          class EventEmitter {
            constructor() {
              // {'click':[fn1,fn2],'change':[fn]}
              // 存储事件与处理函数的对应关系
              this.subs = {};
            }
            //注册事件
            //第一个参数为事件名称
            // 第二个参数为处理函数
            // 将对应的处理函数添加到subs对象中
            $on(eventType, fn) {
              //判断对应的eventType是否有相应的处理函数,如果有,直接添加到数组中,如果没有返回一个空数组。
              if (!this.subs[eventType]) {
                this.subs[eventType] = [];
              }
              this.subs[eventType].push(fn);
            }
            //触发事件
            $emit(eventType) {
              if (this.subs[eventType]) {
                this.subs[eventType].forEach((handler) => {
                  handler();
                });
              }
            }
          }
          //测试代码
          let em = new EventEmitter();
          em.$on("click", () => {
            console.log("click1");
          });
          em.$on("click", () => {
            console.log("click2");
          });
          em.$emit("click");
        </script>
      </body>
    </html>
    
    

    5、观察者模式

    Vue的响应式机制使用了观察者模式,所以我们首先要先了解一下观察者模式

    观察者模式与发布订阅模式的区别是,观察者模式中没有事件中心,只有发布者与订阅者,并且发布者需要知道订阅者的存在。

    观察者(订阅者)—Watcher

    update(): 当事件发生时,具体要做的事情。

    目标(发布者)–Dep

    subs 数组:存储所有的观察者

    addSub() 添加观察者,将其保存到subs数组中

    notify(): 当事件发生后,调用所有观察者的update() 方法。

    没事事件中心

    具体代码实现如下:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>观察者模式</title>
      </head>
      <body>
        <script>
          //发布者
          class Dep {
            constructor() {
              //记录所有的订阅者
              this.subs = [];
            }
            //添加订阅者
            addSub(sub) {
              //订阅者中必须有update方法
              if (sub && sub.update) {
                this.subs.push(sub);
              }
            }
            //发布通知
            notify() {
              //遍历subs数组,调用每个订阅者中的update方法
              this.subs.forEach((sub) => {
                sub.update();
              });
            }
          }
          // 订阅者--观察者
          class Watcher {
            //必须有一update方法,当事件发生后,具体要做的事情
            update() {
              console.log("update something");
            }
          }
          //测试
          let dep = new Dep();
          let watcher = new Watcher();
          dep.addSub(watcher);
          dep.notify();
        </script>
      </body>
    </html>
    
    

    下面我们看一下观察者模式与发布订阅模式的区别。

    观察者模式:是由具体目标调度的,比如当事件触发,Dep就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。

    发布订阅模式:由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

    6、模拟Vue响应式原理–Vue

    当我们在使用Vue的时候,首先会根据Vue类来创建Vue的实例。

    那么Vue类主要的功能如下:

    • 负责接收初始化的参数(选项)
    • 负责把data中的属性注入到Vue实例,转换成getter/setter(可以通过this来访问data中的属性)
    • 负责调用observer监听data中所有属性的变化(当属性值发生变化后更新视图)
    • 负责调用compiler解析指令/差值表达式

    结构

    Vue中包含了_proxyData这个私有方法,该方法的作用就是将data中的属性转换成getter/setter并且注入到Vue的实例中。

    模拟Vue/js/vue.js

    基本代码实现如下:

    class Vue {
      constructor(options) {
        // 1、通过属性保存选项的数据
        // options:表示在创建Vue实例的时候传递过来的参数,将其保存到$options中。
        this.$options = options || {};
        //获取参数中的data属性保存到$data中.
        this.$data = options.data || {};
        this.$el =
          typeof options.el === "string"
            ? document.querySelector(options.el)
            : options.el;
        // 2、把data中的成员转换成getter和setter,注入到vue实例中.
            //通过proxy函数后,在控制台上,可以通过vm.msg直接获取数据,而不用输入vm.$data.msg
        this._proxyData(this.$data);
        //3.调用observer对象,监听数据的变化
        //4.调用compiler对象,解析指令和差值表达式
      }
      _proxyData(data) {
        //遍历data中的所有属性
        Object.keys(data).forEach((key) => {
          // 把data中的属性输入注入到Value实例中,注意,这里使用的是箭头函数,this表示的就是Vue的实例。
          //后期我们可以通过this的形式来访问data中的属性。
          Object.defineProperty(this, key, {
            enumerable: true,
            configurable: true,
            get() {
              return data[key];
            },
            set(newValue) {
              if (newValue === data[key]) {
                return;
              }
              data[key] = newValue;
            },
          });
        });
      }
    }
    

    Vue类中,我们主要实现四项内容:

    1、通过属性保存选项的数据

    2、把data中的成员转换成gettersetter,注入到vue实例中.

    3、调用observer对象,监听数据的变化

    4、调用compiler对象,解析指令和差值表达式

    在上面的代码中,我们首先实现了前两项内容。

    下面进行测试。

    index.html的代码如下:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>模拟Vue</title>
      </head>
      <body>
        <div id="app">
          <h1>差值表达式</h1>
          <h3>{{msg}}</h3>
          <h3>{{count}}</h3>
          <h1>v-text</h1>
          <div v-text="msg"></div>
          <h1>v-model</h1>
          <input type="text" v-model="msg" />
          <input type="text" v-model="count" />
        </div>
        <script src="./js/vue.js"></script>
        <script>
          let vm = new Vue({
            el: "#app",
            data: {
              msg: "Hello World",
              count: 12,
            },
          });
        </script>
      </body>
    </html>
    
    

    在模板中添加了差值表达式v-text,v-model内容,同时导入了我们自己创建的vue,并且创建了Vue的实例。

    在浏览器的控制台中查看对应效果

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OfpdZv1r-1615347299040)(images/vue实例.png)]

    7、Observer

    Observer的功能

    • 负责把data选项中的属性转换成响应式数据
    • data中的某个属性也是对象,把该属性转换成响应式数据(例如data中的某个属性为Student对象,也要将Student对象中的属性转换成响应式)
    • 数据变化发送通知

    observer.js文件中的基本代码如下:

    class Observer {
      constructor(data) {
        this.walk(data);
      }
      walk(data) {
        //1、判断data是否是对象,以及data是否为空
        if (!data || typeof data !== "object") {
          return;
        }
        // 2、遍历data对象中的所有属性
        Object.keys(data).forEach((key) => {
          this.defineReactive(data, key, data[key]);
        });
      }
    
      defineReactive(obj, key, val) {
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            return val;
          },
          set(newVal) {
            if (newVal === val) {
              return;
            }
            val = newVal;
            //发送通知,更新视图
          },
        });
      }
    }
    
    

    下面对以上代码进行测试。

    class Vue {
      constructor(options) {
        // 1、通过属性保存选项的数据
        // options:表示在创建Vue实例的时候传递过来的参数,将其保存到$options中。
        this.$options = options || {};
        //获取参数中的data属性保存到$data中.
        this.$data = options.data || {};
        //如果是字符串,转成dom对象
        this.$el =
          typeof options.el === "string"
            ? document.querySelector(options.el)
            : options.el;
        // 2、把data中的成员转换成getter和setter,注入到vue实例中.
        this._proxyData(this.$data);
        //3.调用observer对象,监听数据的变化
        new Observer(this.$data);
        //4.调用compiler对象,解析指令和差值表达式
      }
     }
    

    Vue类的构造方法中的第三部,创建Observer的实例,同时传递data数据。

    index.html文件中,导入observer.js文件

    <script src="./js/observer.js"></script>
    <script src="./js/vue.js"></script>
    

    注意:由于在vue.js文件中使用了Observer对象,所以这里先导入observer.js文件。

    下面我们修改一下代码,看一下效果:

     <script>
          let vm = new Vue({
            el: "#app",
            data: {
              msg: "Hello World",
              count: 12,
            },
          });
          console.log(vm.msg);
        </script>
    

    index.html中,我们打印输出了vm中的msg的值,

    这时候,会执行vue.js文件中的get方法,也会执行observer.js 文件中的get方法。

    如果将observer.js文件中的get方法修改成如下形式

      get() {
            return obj[key];
           
          },
    

    会出现如下错误:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kfA0hMch-1615347299050)(images/异常.png)]

    以上错误信息的含义为:堆栈溢出

    为什么会出现以上错误呢?

    因为obj就是data对象,而通过obj[key]的方式来获取值,还是会执行get方法,所以这里形成了死循环。

    8、完善defineReactive方法

    如果,我们在data中添加一个对象,那么对象中的属性是否为响应式的呢?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ttW8T5qu-1615347299052)(images/响应式.png)]

    在浏览器的控制台中,输出的person对象是响应式的,但是其内部属性并不是响应式的,下面处理一下这块内容。

    Vue中的对象是响应式的,对象中的属性也是响应式的。

    关于这个问题的解决,非常的简单。

    observer.js文件中的defineReactive方法中,调用一次walk方法就可以了。如下代码所示:

     defineReactive(obj, key, val) {
        this.walk(val);
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            // return obj[key];
            return val;
          },
          set(newVal) {
            if (newVal === val) {
              return;
            }
            val = newVal;
            //发送通知,更新视图
          },
        });
      }
    

    在上面的代码中,首先调用了this.walk(val)方法,同时传递了val这个参数。

    这样在所调用walk方法的内部,会先判断传递过来的参数的类型,如果不是对象,就停止执行walk方法总的循环,而这时候会Object.defineProperty,但是

    如果传递过来的参数就是一个对象,那么会进行循环遍历,取出每一个属性,为其添加getter/setter

    下面,我们在看另外一个问题,现在对index.htmlvue对象中data中的msg属性重新赋值,并且赋值为一个对象,那么新赋值的这个对象的成员是否为响应式的呢?下面我们来测试一下:

    <script>
          let vm = new Vue({
            el: "#app",
            data: {
              msg: "Hello World",
              count: 12,
              person: {
                name: "zs",
              },
            },
          });
          console.log(vm.$data.msg);
      	  vm.msg={text:'abc'}//重新给msg属性赋值
        </script>
    

    在浏览器的控制台中,打印vm,看一下对应的效果

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ChXjI0yO-1615347299055)(images/1.png)]

    通过上图,可以发现新赋值给msg属性的对象中的属性并不是响应式的,所以接下来,我们需要为其改造成响应式的。

    当我们给msg属性赋值的时候,就会执行observer.js文件中的defineReactive方法中的set操作,在这里我们可以将传递过来的值再次调用walk方法,

    这样又会对传递过来的值,进行判断是否为对象,然后进行遍历,同时为其属性添加getter/setter

    defineReactive(obj, key, val) {
         // console.log("this==", this);//这里this指向的是Observer
        let that = this;
        this.walk(val);
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            // return obj[key];
            return val;
          },
          set(newVal) {
                // console.log("this==", this);//这里this指向的是data对象。
            if (newVal === val) {
              return;
            }
            val = newVal;
            that.walk(newVal);//注意this指向的问题
            //发送通知,更新视图
          },
        });
    

    通过上面的代码可以看到,在defineReactive方法中的set操作中,又调用了walk方法,但是要注意的就是,这里需要处理this指向的问题。

    9、Compiler

    功能

    • 负责编译模板,解析指令/差值表达式
    • 负责页面的首次渲染
    • 当数据变化后重新渲染视图

    通过以上功能的描述,可以总结出Compiler主要就是对Dom进行操作。

    js目录下面创建compiler.js文件,实现代码如下:

    class Compiler {
      constructor(vm) {
        this.el = vm.$el;
        this.vm = vm;
      }
      //编译模板,处理文本节点和元素节点.
      compile(el) {}
      // 编译元素节点,处理指令
      compileElement(node) {}
      // 编译文本节点,处理差值表达式
      compileText(node) {}
      //判断元素属性是否为指令
      isDirective(attrName) {
        //指令都是以v-开头
        return attrName.startsWith("v-");
      }
      // 判断节点是否是元素节点
      isElementNode(node) {
        //nodeType: 节点的类型  1:元素节点  3:文本节点
        return node.nodeType === 1;
      }
      //判断节点是否是文本节点
      isTextNode(node) {
        return node.nodeType === 3;
      }
    }
    
    

    9.1 compile方法实现

    在调用compile方法的时候传递过来的参数el就是模板,也就是index.html中的<div id="app"></div>

    中的内容。

    所以我们在compile方法中要遍历模板中的所有节点。

    //编译模板,处理文本节点和元素节点.
      compile(el) {
        //获取子节点.
        let childNodes = el.childNodes;
        //childNodes是一个伪数组,需要转换成真正的数组,然后可以执行forEach来进行遍历,每遍历一次获取一个节点,然后判断节点的类型.
        Array.from(childNodes).forEach((node) => {
          //处理文本节点
          if (this.isTextNode(node)) {
            this.compileText(node);
          } else if (this.isElementNode(node)) {
            // 处理元素节点
            this.compileElement(node);
          }
          //判断node节点,是有还有子节点,如果有子节点,需要递归调用compile方法
          if (node.childNodes && node.childNodes.length) {
            this.compile(node);
          }
        });
      }
    

    以上就是compile方法的基本实现.

    9.2 compileText方法实现

    compileText方法的作用就是对对插值表达式进行解析.

    在编写compileText方法之前,我们先测试一下前面写的代码。

    首先在compiler.js文件中的构造方法中,调用compile方法。

    class Compiler {
      constructor(vm) {
        this.el = vm.$el;
        this.vm = vm;
          //调用compile方法
        this.compile(this.el);
      }
    }
    

    vue.js文件中创建Compiler类的实例,传递的是Vue的实例。

    class Vue {
      constructor(options) {
        // 1、通过属性保存选项的数据
        // options:表示在创建Vue实例的时候传递过来的参数,将其保存到$options中。
        this.$options = options || {};
        //获取参数中的data属性保存到$data中.
        this.$data = options.data || {};
        //如果是字符串,转成dom对象
        this.$el =
          typeof options.el === "string"
            ? document.querySelector(options.el)
            : options.el;
        // 2、把data中的成员转换成getter和setter,注入到vue实例中.
        this._proxyData(this.$data);
        //3.调用observer对象,监听数据的变化
        new Observer(this.$data);
        //4.调用compiler对象,解析指令和差值表达式
        new Compiler(this);
      }
    }
    

    在第四步中,创建了Comiler类的实例。

    同时需要在index.html文件中引入comiler.js文件。

     <script src="./js/compiler.js"></script>
        <script src="./js/observer.js"></script>
        <script src="./js/vue.js"></script>
    

    注意导入的顺序。

    compiler.js文件中的comileText方法中可以先打印一下文本节点,看一下具体的文本节点。

     // 编译文本节点,处理差值表达式
      compileText(node) {
        console.dir(node);
      }
    

    下面完善一下compileText方法的实现如下:

      // 编译文本节点,处理差值表达式
      compileText(node) {
        // console.dir(node);
        // {{ msg }}
        //我们是用data中的属性值替换掉大括号中的内容
        let reg = /\{\{(.+)\}\}/;
        //获取文本节点的内容
        let value = node.textContent;
    
        //判断文本节点的内容是否能够匹配正则表达式
        if (reg.test(value)) {
          //获取插值表达式中的变量名,去掉空格($1 表示获取第一个分组的内容。)
          let key = RegExp.$1.trim();
          //根据变量名,获取data中的具体值,然后替换掉差值表达式中的变量名.
          node.textContent = value.replace(reg, this.vm[key]);
        }
      }
    

    这时刷新浏览器,就可以看到对应效果。

    9.3 compileElement方法实现

    compileElement方法,就是完成指令的解析。

    在这里我们重点解析的指令为v-textv-model

     <div v-text="msg"></div>
       <input type="text" v-model="msg" />
    

    这些指令本身就是html标签的属性。

    // 编译元素节点,处理指令
      compileElement(node) {
        // 1、获取当前节点下的所有的属性,然后通过循环的方式,取出每个属性,判断其是否为指令
        // 2、 如果是指令,获取指令的名称与指令对应的值.
        // 3、 分别对v-text指令与v-model指令的情况进行处理.
        //通过node.attributes获取当前节点下所有属性,node.attributes是一个伪数组
        Array.from(node.attributes).forEach((attr) => {
          //获取属性的名称
          let attrName = attr.name;
          //判断是否为指令
          if (this.isDirective(attrName)) {
            //如果是指令,需要分别进行处理,也就是分别对v-text与v-model指令
            //进行处理。
            //为了避免在这里书写大量的if判断语句,这里做一个简单的处理.
            //对属性名字进行截取,只获取v-text/v-model中的text/model
            attrName = attrName.substr(2);
            //获取指令对应的值 v-text指令对应的值为msg,v-model指令对应的值为msg,cout
            let key = attr.value;
            this.update(node, key, attrName);
          }
        });
      }
      update(node, key, attrName) {
        //根据传递过来的属性名字拼接Updater后缀获取方法。
        let updateFn = this[attrName + "Updater"];
        updateFn && updateFn(node, this.vm[key]); //注意:传递的是根据指令的值获取到的是data中对应属性的值。
      }
      //处理v-text指令
      textUpdater(node, value) {
        node.textContent = value;
      }
      //处理v-model
      modelUpdater(node, value) {
        //v-model是文本框的属性,给文本框赋值需要通过value属性
        node.value = value;
      }
    

    通过以上的代码,我们可以看到,如果想以后在处理其它的指令,只需要添加方法就可以了,方法的名字后缀一定要有Updater.

    这比写很多的判断语句方便多了。

    compiler.js文件完整代码

    class Compiler {
      constructor(vm) {
        this.el = vm.$el;
        this.vm = vm;
        this.compile(this.el);
      }
      //编译模板,处理文本节点和元素节点.
      compile(el) {
        //获取子节点.
        let childNodes = el.childNodes;
        //childNodes是一个伪数组,需要转换成真正的数组,然后可以执行forEach来进行遍历,每遍历一次获取一个节点,然后判断节点的类型.
        Array.from(childNodes).forEach((node) => {
          //处理文本节点
          if (this.isTextNode(node)) {
            this.compileText(node);
          } else if (this.isElementNode(node)) {
            // 处理元素节点
            this.compileElement(node);
          }
          //判断node节点,是有还有子节点,如果有子节点,需要递归调用compile方法
          if (node.childNodes && node.childNodes.length) {
            this.compile(node);
          }
        });
      }
      // 编译元素节点,处理指令
      compileElement(node) {
        // 1、获取当前节点下的所有的属性,然后通过循环的方式,取出每个属性,判断其是否为指令
        // 2、 如果是指令,获取指令的名称与指令对应的值.
        // 3、 分别对v-text指令与v-model指令的情况进行处理.
        //通过node.attributes获取当前节点下所有属性,node.attributes是一个伪数组
        Array.from(node.attributes).forEach((attr) => {
          //获取属性的名称
          let attrName = attr.name;
          //判断是否为指令
          if (this.isDirective(attrName)) {
            //如果是指令,需要分别进行处理,也就是分别对v-text与v-model指令
            //进行处理。
            //为了避免在这里书写大量的if判断语句,这里做一个简单的处理.
            //对属性名字进行截取,只获取v-text/v-model中的text/model
            attrName = attrName.substr(2);
            //获取指令对应的值 v-text指令对应的值为msg,v-model指令对应的值为msg,cout
            let key = attr.value;
            this.update(node, key, attrName);
          }
        });
      }
      update(node, key, attrName) {
        //根据传递过来的属性名字拼接Updater后缀获取方法。
        let updateFn = this[attrName + "Updater"];
        updateFn && updateFn(node, this.vm[key]); //注意:传递的是根据指令的值获取到的是data中对应属性的值。
      }
      //处理v-text指令
      textUpdater(node, value) {
        node.textContent = value;
      }
      //处理v-model
      modelUpdater(node, value) {
        //v-model是文本框的属性,给文本框赋值需要通过value属性
        node.value = value;
      }
      // 编译文本节点,处理差值表达式
      compileText(node) {
        // console.dir(node);
        // {{ msg }}
        //我们是用data中的属性值替换掉大括号中的内容
        let reg = /\{\{(.+)\}\}/;
        //获取文本节点的内容
        let value = node.textContent;
    
        //判断文本节点的内容是否能够匹配正则表达式
        if (reg.test(value)) {
          //获取插值表达式中的变量名,去掉空格($1 表示获取第一个分组的内容。)
          let key = RegExp.$1.trim();
          //根据变量名,获取data中的具体值,然后替换掉差值表达式中的变量名.
          node.textContent = value.replace(reg, this.vm[key]);
        }
      }
      //判断元素属性是否为指令
      isDirective(attrName) {
        //指令都是以v-开头
        return attrName.startsWith("v-");
      }
      // 判断节点是否是元素节点
      isElementNode(node) {
        //nodeType: 节点的类型  1:元素节点  3:文本节点
        return node.nodeType === 1;
      }
      //判断节点是否是文本节点
      isTextNode(node) {
        return node.nodeType === 3;
      }
    }
    
    

    当页面首次渲染的时候,把数据更新到视图的功能,我们已经完成了,但是还没有实现对应的响应式,也就是数据更改后,视图也要进行更新。

    下面我们就来实现对应的响应式机制。

    10、Dep

    下面我们先来实现Dep这个类。

    该类的功能:

    	收集依赖,添加观察者(`watcher`)
    
    	通知所有观察值
    

    什么时候收集依赖呢?

    也就是在getter中收集依赖,添加观察者

    什么时候通知观察者呢?

    setter中通知依赖,通知观察者

    dep.js文件中编写如下代码:

    class Dep {
      constructor() {
        //存储所有的观察者
        this.subs = [];
      }
      //添加观察者
      addSub(sub) {
        //判断传递过来的内容必须有值同时还必须是一个观察者,观察者中会有一个update方法
        if (sub && sub.update) {
          this.subs.push(sub);
        }
      }
      //发送通知
      notify() {
        this.subs.forEach((sub) => {
          sub.update();
        });
      }
    }
    

    修改Observer类中的代码

    defineReactive(obj, key, val) {
        // console.log("this==", this);//这里this指向的是Observer
        let that = this;
        //负责收集依赖,并发送通知
        let dep = new Dep();
        this.walk(val);
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            //收集依赖,就是将watcher观察者添加到subs数组中。
            //这里可以通过Dep中的target来获取观察者(watcher对象),当然target属性还没有创建
            //后期在创建Watcher观察者的时候,来确定target属性
            Dep.target && dep.addSub(Dep.target);
            // return obj[key];
            return val;
          },
          set(newVal) {
            // console.log("this==", this);//这里this指向的是data对象。
            if (newVal === val) {
              return;
            }
            val = newVal;
            that.walk(newVal);
            //发送通知,更新视图
            dep.notify();
          },
        });
      }
    

    首先针对每一个响应式数据添加了一个Dep对象(发布者),然后在set方法中,当数据发生了变化后,会调用dep中的notify方法,完成更新视图的操作。

    set方法中添加依赖,也就是将watcher观察者添加到了Dep中的subs数组中。

    以上代码无法进行测试,完成Watcher类可以进行测试

    11、Watcher

    11.1 Watcher类创建

    在编写Watcher类之前,我们先来看一张图,理解一下DepWatcher的关系

    通过前面的学习,我们知道在Observer类中为每一个响应式的数据创建了Dep对象,而且在getter 中会收集依赖,所谓收集依赖就是将watcher观察者添加到subs数组中.

    而在setter中会触发依赖,其实就是调用Dep对象中notify方法,该方法会获取subs数组中的所有的watcher,然后执行watcher中的update方法来更新对应的视图。

    Watcher 类的代码如下:

    class Watcher {
      constructor(vm, key, cb) {
        this.vm = vm;
        //data中的属性名称
        this.key = key;
        //回调函数负责更新视图
        this.cb = cb;
        //获取更新前的旧值
        this.oldValue = vm[key];
      }
      // 当数据发生变化的时候更新视图
      update() {
        //只要update方法调用,获取到的值就是新值,因为当数据发生了变化,才会调用该方法
        let newValue = this.vm[this.key];
        if (newValue === this.oldValue) {
          return;
        }
        //调用cb回调函数更新视图,将新值传递到该回调函数中
        this.cb(newValue);
      }
    }
    

    接下来还有一件事情需要处理一下:

    当创建了·Watcher对象后,需要将当前创建的Watcher对象添加到Dep中的subs数组中。

    我们可以查看Observer类,在get方法中已经写过将Watcher对象添加到Dep中的subs数组中了(Dep.target && dep.addSub(Dep.target);),但是

    问题是,我们并没有创建target属性,所以下面我们创建一下target属性。

    下面在Watcher类的构造方法中,添加给Dep添加target属性,用来保存Watcher的实例。

    class Watcher {
      constructor(vm, key, cb) {
        this.vm = vm;
        //data中的属性名称
        this.key = key;
        //回调函数负责更新视图
        this.cb = cb;
        // 把watcher对象记录添加到Dep类的静态属性target上.
        Dep.target = this;
        //触发get方法,因为在get方法中会调用addSub方法(下面我们通过vm来获取key对应的值的时候,就执行了get方法,因为我们已经将data属性编程了响应式,为其添加了`getter/setter`).
        //获取更新前的旧值
        this.oldValue = vm[key];
        Dep.target = null; //防止以后重复性的添加
      }
    }
    

    以上内容需要重点去体会.

    11.2 创建Watcher对象

    下面来看一下关于Watcher对象的创建。

    // 编译文本节点,处理差值表达式
      compileText(node) {
        // console.dir(node);
        // {{ msg }}
        //我们是用data中的属性值替换掉大括号中的内容
        let reg = /\{\{(.+)\}\}/;
        //获取文本节点的内容
        let value = node.textContent;
    
        //判断文本节点的内容是否能够匹配正则表达式
        if (reg.test(value)) {
          //获取插值表达式中的变量名,去掉空格($1 表示获取第一个分组的内容。)
          let key = RegExp.$1.trim();
          //根据变量名,获取data中的具体值,然后替换掉差值表达式中的变量名.
          node.textContent = value.replace(reg, this.vm[key]);
          //创建Watcher对象,当数据发生变化后,更新视图
          new Watcher(this.vm, key, (newValue) => {
              //newValue是更新后的值
            node.textContent = newValue;
          });
        }
      }
    

    下面要在index.html文件中导入相关的js文件。

    <script src="./js/dep.js"></script>
        <script src="./js/watcher.js"></script>
        <script src="./js/compiler.js"></script>
        <script src="./js/observer.js"></script>
        <script src="./js/vue.js"></script>
    

    注意:以上导入文件的顺序,由于在watcher.js文件中使用了dep.js文件中的内容,所以先导入dep,同样在compiler.js文件中使用了watcher.js文件中内容,所以先导入了watcher.js.

    下面可以进行测试了。

    先将index.html文件中的,如下语句注释掉:

     vm.msg = { text: "abc" };
    

    然后,打开浏览器的控制台,输入如下内容

    vm.msg="abc"
    

    对应的页面视图中的内容也发生了变化。这也就实现了响应式机制,所谓响应式就是当数据变化了,对应的视图也会进行更新。

    所以需要在textUpdatermodelUpdater方法中完成Watcher对象的创建。

     //处理v-text指令
      textUpdater(node, value, key) {
        node.textContent = value;
        new Watcher(this.vm, key, (newValue) => {
          node.textContent = newValue;
        });
      }
      //处理v-model
      modelUpdater(node, value,key) {
        //v-model是文本框的属性,给文本框赋值需要通过value属性
        node.value = value;
        new Watcher(this.vm, key, (newValue) => {
          node.value = newValue;
        });
      }
    
      update(node, key, attrName) {
        //根据传递过来的属性名字拼接Updater后缀获取方法。
        let updateFn = this[attrName + "Updater"];
        updateFn && updateFn.call(this, node, this.vm[key], key); //注意:传递的是根据指令的值获取到的是data中对应属性的值。
      }
    

    12、双向数据绑定

    这一小节,我们看一下Vue的双向数据绑定。

    双向数据绑定包含两部分内容,数据变化更新视图,视图变化更新数据。

    怎样实现双向绑定呢?

    基本的思路就是,我们可以给文本框(第一个文本框)添加一个input事件,在输入完数据后触发该事件,同时将用户在文本框中输入的数据赋值给data中的属性(视图变化,更新数据,而当数据变化后,会执行行observer.js中的set方法,更新视图,也就是触发了响应式的机制)。

    那么我们应该在哪实现数据的双向绑定呢?

    我们知道,这里是对文本框的操作,所以需要compiler.js文件中的modelUpdater方法中,实现双向绑定。因为modelUpdater方法就是处理v-model.

    //处理v-model
      modelUpdater(node, value, key) {
        //v-model是文本框的属性,给文本框赋值需要通过value属性
        node.value = value;
        new Watcher(this.vm, key, (newValue) => {
          node.value = newValue;
        });
        //实现双向绑定
        node.addEventListener("input", () => {
          this.vm[key] = node.value;
        });
      }
    

    在上面的代码中,我们为当前的文本框节点添加了input事件,当在文本框中输入内容的时候会触发该事件,同时,将用户在文本框节点中输入的值重新赋值给了data中对应的属性。

    下面我们可以进行测试,在文本框中输入值,对应的差值表达式和v-text中的内容都会发生改变。同时在控制台中输出vm.msg的值会发现数据也发生了变化。

    而我们知道,当给data中的属性赋值后,会执行observer.js中的set方法,更新视图,也就是触发了响应式的机制。

    现在整个Vue的模拟实现,我们就完成了。

    当然,我们这里只是模拟了最核心的内容也就是数据响应式与双向绑定。

    13、总结

    首先我们先来看一下最开始提出的问题。

    第一个:给属性重新赋值成对象,是否是响应式的?答案:是响应式的。

    应当我们给data中的属性进行重新赋值的时候,会执行Observer类中的defineReactive方法的set方法

    set方法中,调用了walk方法,该方法中判断重新给data属性中赋的值是否为对象,如果是对象,会将对象中的每个属性都修改成响应式的。

    第二个问题:给Vue实例新增一个成员是否是响应式的?

    例如如下代码:

      <script>
          let vm = new Vue({
            el: "#app",
            data: {
              msg: "Hello World",
              count: 12,
              person: {
                name: "zs",
              },
            },
          });
          console.log(vm.$data.msg);
          // vm.msg = { text: "abc" };
    //给Vue实例新增加了一个属性test属性
          vm.test = "abc";
        </script>
    

    index.html文件中,创建了Vue的实例后,给Vue实例后新增了test的属性,那么这个test属性是否为

    响应式的呢?

    答案:不是响应式的。

    因为,我们所有的操作都是在创建Vue的实例的时候完成的,也就是在Vue类的构造函数中完成的。

    Vue类的构造函数中,创建了Observer的实例,完成了监听数据的变化。

    所以当Vue的实例创建完成后,在为其添加属性,该属性并不是一个响应式的。

    当然,为了解决这个问题,Vue中也给出了相应的解决方案,可以查看官方的文档:

    展开全文
  • 本文使用 Zhihu On VSCode 创作并发布前言:Starting from this, there will always be a long way to go不知不觉在拉勾大前端训练...Vue响应式原理面试的常问问题学习别人优秀的经验,转换成自己的经验实际项目中...

    本文使用 Zhihu On VSCode 创作并发布

    前言:Starting from this, there will always be a long way to go

    不知不觉在拉勾大前端训练营学习已经两月有余,始终相信每一分坚持和努力都不会被辜负

    模拟VUE响应式原理学习笔记

    学习目标与准备工作

    1. 目标
    • 模拟一个最小版本的 Vue
    • 响应式原理在面试的常问问题
    • 学习别人优秀的经验,转换成自己的经验
    • 实际项目中出问题的原理层面的解决
    1. 给 Vue 实例新增一个成员是否是响应式的?
    2. 给属性重新赋值成对象,是否是响应式的?
      • 为学习 Vue 源码做铺垫
    1. 准备工作
    • 数据驱动
    • 响应式的核心原理
    • 发布订阅模式和观察者模式

    数据驱动

    数据响应式、双向绑定、数据驱动

    • 数据响应式
      • 数据模型仅仅是普通的 JavaScript 对象,而当我们修改数据时,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率
    • 双向绑定
      • 数据改变,视图改变;视图改变,数据也随之改变
      • 我们可以使用 v-model 在表单元素上创建双向数据绑定
    • 数据驱动是 Vue 最独特的特性之一
      • 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图

    响应式核心原理

    1. Vue 2.x
    • 查看官网说明,可以了解到vue2.x实现响应式原理的核心是Object.defineProperty()数据劫持

    35fed0b6285bac28a7dbfe98b1bc4993.png
    Image
      • MDN中关于Object.defineProperty()的定义
      • 模拟vue的data选项
        <!DOCTYPE html>
        <html lang="en">
        <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>单个属性</title>
        </head>
        <body>
        <div id="app" >hello</div>
        </body>
        <script>
        let data={msg:'hello'}
        let vm={}
        Object.defineProperty(vm,'msg',{
        //可枚举(可遍历)
        enumerable: true,
        //可配置(可以delete,可以使用defineProperty重新定义)
        configurable: true,
        //获取值得时候执行
        get: function() {
        console.log('get:',data.msg)
        return data.msg
        },
        //设置值得时候执行
        set: function(newValue) {
        console.log('set:',newValue)
        if(data.msg === newValue){
        return
        }
        data.msg = newValue
        //更新dom
        document.querySelector("#app").textContent=data.msg
        }
        })
        vm.msg='Hello World'
        console.log(vm.msg)
        </script>
        </html>

    54b922e4e1988923c611f9a0ff4c3259.png
    Image
      • 如果有一个对象中多个属性需要转换 getter/setter 如何处理?
        遍历对象属性,使用Object.defineProperty将对象的属性挂载到vm上,并转换成getter和setter
        <!DOCTYPE html>
        <html lang="en">
        <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>多个属性</title>
        </head>
        <body>
        <div id="app" >hello</div>
        </body>
        <script>
        let data={
        msg:'hello',
        count:10
        }
        let vm={}
        proxyData(data)
        function proxyData(data) {
        //遍历属性
        Object.keys(data).forEach(key=>{
        //将data的属性挂载到vm上,并设置getter和setter
        Object.defineProperty(vm, key, {
        //可枚举(可遍历)
        enumerable: true,
        //可配置(可以delete,可以使用defineProperty重新定义)
        configurable: true,
        //获取值得时候执行
        get: function () {
        console.log('get:', data.key)
        return data.key
        },
        //设置值得时候执行
        set: function (newValue) {
        console.log('set:', newValue)
        if (data.key === newValue) {
        return
        }
        data.key = newValue
        //更新dom
        document.querySelector("#app").textContent = data.key
        }
        })
        })
        }
        vm.msg='Hello World'
        console.log(vm.msg)
        </script>
        </html>

    7f92a461366577fff41e753633ce9435.png
    Image
    1. Vue 3.x
    • MDN中的Proxy数据劫持
    • Proxy直接监听对象,而非属性,因此在把多个属性转换成getter和setter时不需循环
    • Proxy是ES6中新增的,IE不支持,性能由浏览器优化,更优于defineProperty
    • 模拟代码
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>vue3数据劫持</title>
      </head>
      <body>
      <div id="app" >hello</div>
      </body>
      <script>
      let data={
      msg:'hello',
      count:10
      }
      let vm= new Proxy(data,{
      //当访问vm的成员会被执行
      get(target,key){
      console.log('get,key:',key,target[key]);
      return target[key];
      },
      //当设置vm的成员会被执行
      set(target,key,newValue){
      console.log('set,key:',key,newValue);
      if(target[key]===newValue){
      return
      }
      target[key]=newValue
      document.querySelector('#app').textContent=target[key]
      }
      })
      vm.msg='Hello World'
      console.log(vm.msg)
      </script>
      </html>

    ded9d5947cfcf7ddd786b0d613430e54.png
    Image

    发布订阅模式

    • 发布/订阅模式
      • 订阅者
      • 发布者
      • 信号中心

    我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信
    号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执
    行。这就叫做"发布/订阅模式"(publish-subscribe pattern)

    • Vue 的自定义事件以及node中的事件机制都是基于发布订阅模式
    • 兄弟组件通信过程
      // eventBus.js
      // 事件中心
      let eventHub = new Vue()
      // ComponentA.vue
      // 发布者
      addTodo: function () {
      // 发布消息(事件)
      eventHub.$emit('add-todo', { text: this.newTodoText })
      this.newTodoText = ''
      }
      // ComponentB.vue
      // 订阅者
      created: function () {
      // 订阅消息(事件)
      eventHub.$on('add-todo', this.addTodo)
      }
    • 模拟 Vue 自定义事件的实现
      class EventEmitter {
      constructor() {
      // { eventType: [ handler1, handler2 ] }
      this.subs = {}
      }
      // 订阅通知
      $on(eventType, handler) {
      this.subs[eventType] = this.subs[eventType] || []
      this.subs[eventType].push(handler)
      }
      // 发布通知
      $emit(eventType) {
      if (this.subs[eventType]) {
      this.subs[eventType].forEach((handler) => {
      handler()
      })
      }
      }
      }
      // 测试
      var bus = new EventEmitter()
      // 注册事件
      bus.$on('click', function () {
      console.log('click')
      })
      bus.$on('click', function () {
      console.log('click1')
      })
      // 触发事件
      bus.$emit('click')

    观察者模式

    • 观察者(订阅者) -- Watcher
      • update():当事件发生时,具体要做的事情
    • 目标(发布者) -- Dep
      • subs 数组:存储所有的观察者
      • addSub():添加观察者
      • notify():当事件发生,调用所有观察者的 update() 方法
    • 没有事件中心
      // 目标(发布者)
      // Dependency
      class Dep {
      constructor() {
      // 存储所有的观察者
      this.subs = []
      }
      // 添加观察者
      addSub(sub) {
      if (sub && sub.update) {
      this.subs.push(sub)
      }
      }
      // 通知所有观察者
      notify() {
      this.subs.forEach((sub) => {
      sub.update()
      })
      }
      }
      // 观察者(订阅者)
      class Watcher {
      update() {
      console.log('update')
      }
      }
      // 测试
      let dep = new Dep()
      let watcher = new Watcher()
      dep.addSub(watcher)
      dep.notify()

    总结两种模式区别

    • 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
    • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

    57d00e17f97a612f69a962ca0fe0d452.png
    Image

    模拟响应式源码

    class Compiler {
        constructor(vm) {
            this.el = vm.$el
            this.vm = vm
            this.compile(this.el)
        }
        // 编译模板,处理文本节点和元素节点
        compile(el) {
            let childNodes = el.childNodes
            Array.from(childNodes).forEach((node) => {
                // 处理文本节点
                if (this.isTextNode(node)) {
                    this.compileText(node)
                } else if (this.isElementNode(node)) {
                    // 处理元素节点
                    this.compileElement(node)
                }
    
                // 判断node节点,是否有子节点,如果有子节点,要递归调用compile
                if (node.childNodes && node.childNodes.length) {
                    this.compile(node)
                }
            })
        }
        // 编译元素节点,处理指令
        compileElement(node) {
            // console.log(node.attributes)
            // 遍历所有的属性节点
            Array.from(node.attributes).forEach((attr) => {
                // 判断是否是指令
                let attrName = attr.name
                if (this.isDirective(attrName)) {
                    // v-text --> text
                    attrName = attrName.substr(2)
                    let key = attr.value
                    if (attrName.startsWith('on')) {
                        const event = attrName.replace('on:', '') // 获取事件名
                        // 事件更新
                        return this.eventUpdate(node, key, event)
                    }
                    this.update(node, key, attrName)
                }
            })
        }
    
        update(node, key, attrName) {
            let updateFn = this[attrName + 'Updater']
            updateFn && updateFn.call(this, node, this.vm[key], key)
        }
        eventUpdate(node, key, event) {
            this.onUpdater(node, key, event)
        }
    
        // 处理 v-text 指令
        textUpdater(node, value, key) {
            node.textContent = value
            new Watcher(this.vm, key, (newValue) => {
                node.textContent = newValue
            })
        }
        // v-model
        modelUpdater(node, value, key) {
            node.value = value
            new Watcher(this.vm, key, (newValue) => {
                node.value = newValue
            })
            // 双向绑定
            node.addEventListener('input', () => {
                this.vm[key] = node.value
            })
        }
        // 处理 v-html 指令
        htmlUpdater(node, value, key) {
            node.innerHTML = value
            new Watcher(this.vm, key, (newValue) => {
                node.innerHTML = newValue
            })
        }
        // 处理 v-on 指令
        onUpdater(node, key, event) {
            node.addEventListener(event, (e) => this.vm[key](e))
        }
        // 编译文本节点,处理差值表达式
        compileText(node) {
            // console.dir(node)
            // {{  msg }}
            let reg = /{{(.+?)}}/
            let value = node.textContent
            if (reg.test(value)) {
                let key = RegExp.$1.trim()
                node.textContent = value.replace(reg, this.vm[key])
    
                // 创建watcher对象,当数据改变更新视图
                new Watcher(this.vm, key, (newValue) => {
                    node.textContent = newValue
                })
            }
        }
        // 判断元素属性是否是指令
        isDirective(attrName) {
            return attrName.startsWith('v-')
        }
        // 判断节点是否是文本节点
        isTextNode(node) {
            return node.nodeType === 3
        }
        // 判断节点是否是元素节点
        isElementNode(node) {
            return node.nodeType === 1
        }
    }
    
    展开全文
  • 收集依赖5 观察者 Watcher前言:现在前端面试Vue中都会问到响应式原理以及如何实现的,如果你还只是简单回答通过Object.defineProperty()来劫持属性可能已经不够了vue响应式原理学习(1)一句话总结:通过Obje...
    1349e48ed9d37d4952bd1217811a3ac8.gif点击上方蓝字  关注前端知识
    • vue响应式原理学习(1)

      • 1.什么是响应式

      • 2.实现响应式,我们需要做些什么

      • 3.如何侦测数据的变化

      • 3.2  Proxy实现

      • 4.收集依赖

      • 5 观察者 Watcher

    • 前言:现在前端面试Vue中都会问到响应式原理以及如何实现的,如果你还只是简单回答通过Object.defineProperty()来劫持属性可能已经不够了

    vue响应式原理学习(1)

    • 一句话总结:通过Object.defineProperty去劫持data里的属性,将data全部属性替换成getter和setter,配合发布者和订阅者模式,每一个组件都有一个watcher实例,当我们对data属性赋值和改变,就会触发setter,setter会通知watcher,从而使它关联的组件进行重新渲染。

    1.什么是响应式

    数据发生变化后,会重新对页面渲染,这就是Vue响应式

    2.实现响应式,我们需要做些什么

    • 侦测数据的变化
    • 收集视图依赖了哪些数据
    • 数据变化时,自动“通知”需要更新的视图部分,并进行更新

    专业术语

    • 数据劫持 / 数据代理
    • 依赖收集
    • 发布订阅模式

    3.如何侦测数据的变化

    • Vue.js 实现响应式的核心是利用了 ES5 的 Object.defineProperty及ES6的Proxy,进行数据劫持或数据代理。这也是为什么 Vue.js 不能兼容 IE8 及以下浏览器的原因
    6ed9fee598ff7ce011bf82fa653b42e0.png
    在这里插入图片描述

    3.1 Object.defineProperty实现

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

    • 语法:Object.defineProperty(obj, prop, descriptor)
      • obj要定义属性的对象。
      • prop要定义或修改的属性的名称或 Symbol 。
      • descriptor要定义或修改的属性描述符
    html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    head>

    <body>
        <script>let obj = { text'111' };Object.defineProperty(obj, 'text', {// writable:true,//value值可被改变
                configurable: true,//属性可删除
                enumerable: true,//可枚举(遍历)get () {console.log("text被访问 get方法")
                },set(){console.log("text被赋值 set方法")
                }
            });console.log(obj.text)//触发get()console.log(obj.text='222')//触发set()
    script>
    body>

    html>
    eee13b3a6222baa7810a32809423b2c0.png
    在这里插入图片描述
    • 当属性text被访问时,触发了get方法,当属性被赋值时触发了set方法(但是访问到的是undefined的,因为get()中没有设置返回值)
    • Object.defineProperty中get()return 的值是什么,该属性的值就是什么
    • Object.defineProperty中定义的数据不会储存,我们需要一个第三者变量来动态定义数据
    html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    head>
    <body>
        <script>let obj = { text'' };let _text =''Object.defineProperty(obj, 'text', {// writable:true,//value值可被改变
                configurable: true,//属性可删除
                enumerable: true,//可枚举(遍历)get () {console.log("text被访问 get方法")return _text
                },set(value){console.log("text被赋值 set方法",value)
                    _text = value
                }
            });console.log(obj.text='222')console.log(obj.text)
    script>
    body>

    html>

    b24d97b80904fa10d13346ec996692aa.png
    在这里插入图片描述
    • 我们首先要先了解Object.defineProperty基本的用法

    3.1.1 Observer实现一个对象对所有成员的代理

    html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    head>
    <body>
        <script>function render({console.log('模拟视图渲染')
            }let data = {name'浪里行舟',location: { x100y100 }
            }
            observe(data)function observe(obj// 我们来用它使对象变成可观察的// 判断类型if (!obj || typeof obj !== 'object') {return
                }Object.keys(obj).forEach(key => {
                    defineReactive(obj, key, obj[key])
                })function defineReactive(obj, key, value{// 递归子属性
                    observe(value)Object.defineProperty(obj, key, {enumerabletrue//可枚举(可以遍历)
                        configurable: true//可配置(比如可以删除)get: function reactiveGetter() {console.log('get', value) // 监听return value
                        },setfunction reactiveSetter(newVal{
                            observe(newVal) //如果赋值是一个对象,也要递归子属性if (newVal !== value) {console.log('set', newVal) // 监听
                                render()
                                value = newVal
                            }
                        }
                    })
                }
            }
            data.location = {x1000,y1000
            } //set {x: 1000,y: 1000} 模拟视图渲染
            data.name // get 浪里行舟
    script>
    body>
    html>
    • observe这个函数传入一个 obj(需要被追踪变化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理,给每个属性加上 set和get方法,以此来达到实现侦测对象变化。值得注意的是, observe 会进行递归调用。

    那我们如何侦测Vue中data 中的数据,其实也很简单:

    class Vue {
        /* Vue构造类 */
        constructor(options) {
            this._data = options.data;
            observer(this._data);
        }
    }

    这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行追踪变化。「但是我们发现一个问题,上面的代码无法检测到对象属性的添加或删除(如data.location.a=1,增加一个a属性)。」

    • 这是因为 Vue 通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。如果是删除属性,我们可以用vm.$delete实现,那如果是新增属性,该怎么办呢?
    1. 可以使用 Vue.set(location, a, 1) 方法向嵌套对象添加响应式属性;
    2. 也可以给这个对象重新赋值,比如data.location = {...data.location,a:1} Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写

    3.2  Proxy实现

    • Proxy 是 JavaScript 2015 的一个新特性。Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外Proxy支持代理数组的变化。
    • vue3中Api,ES6的新语法.proxy比Object.defineProperty()性能要好
    html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    head>

    <body>
        <script>function render({console.log('模拟视图的更新')
            }let obj = {name'前端工匠',age: { age100 },arr: [123]
            }let handler = {/**
                * target: 真正的目标数据对象 {a:1,b:2,age:10}
                * key:要访问的属性名
                */
    get(target, key) {// 如果取的值是对象就再对这个对象进行数据劫持if (typeof target[key] == 'object' && target[key] !== null) {return new Proxy(target[key], handler)
                    }return Reflect.get(target, key)
                },/**
             * target 原始的数据对象
             * key 要赋值的属性
             * value  = 后面的值
             */
    set(target, key, value) {//key为length时,表示遍历完了最后一个属性if (key === 'length'return true
                    render()return Reflect.set(target, key, value)
                }
            }/**
             * new Proxy 用来创建一个代理对象
             * 第1个参数:要代理的数据对象
             * 第2个参数:handler 配置代理方法的一个对象
             *    get 属性的访问器
             *    set 属性的修改器
             *    一共有 13 个代理器
             * Proxy 返回一个把原始数据对象代理过的数据对象,我们称之为代理对象
             * 注意:必须操作代理对象才会走代理,原始数据对象还是原来的
             */
    let proxy = new Proxy(obj, handler)
            proxy.age.name = '浪里行舟' // 支持新增属性console.log(proxy.age.name) // 模拟视图的更新 浪里行舟
            proxy.arr[0] = '浪里行舟' //支持数组的内容发生变化console.log(proxy.arr) // 模拟视图的更新 ['浪里行舟', 2, 3 ]
            proxy.arr.length-- // 无效
    script>
    body>

    html>

    4.收集依赖

    4.1 为什么要收集依赖

    我们之所以要观察数据,其目的在于当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。比如例子中,模板中使用了location 数据,当它发生变化时,要向使用了它的地方发送通知。

    let globalData = {
        text'浪里行舟'
    };
    let test1 = new Vue({
        template:
            `
    {{text}} 
    `,data: globalData
    });let test2 = new Vue({template:`
    {{text}} 
    `,data: globalData
    });
    如果我们执行下面这条语句:
    globalData.text = '前端工匠';
    此时我们需要通知 test1 以及 test2 这两个Vue实例进行视图的更新,我们只有通过收集依赖。才能知道哪些地方依赖我的数据,以及数据更新时派发更新。。那依赖收集是如何实现的?其中的核心思想就是“事件发布订阅模式”。接下来我们先介绍两个重要角色-- 订阅者 Dep和观察者 Watcher 。,然后阐述收集依赖的如何实现的。

    4.2订阅者 Dep

    「为什么引入 Dep:」

    收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖删除依赖向依赖发送消息等。于是我们先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,「说得具体点」:它的主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。「Dep的简单实现:」*
    class Dep {
        constructor () {
            /* 用来存放Watcher对象的数组 */
            this.subs = [];
        }
        /* 在subs中添加一个Watcher对象 */
        addSub (sub) {
            this.subs.push(sub);
        }
        /* 通知所有Watcher对象更新视图 */
        notify () {
            this.subs.forEach((sub) => {
                sub.update();
            })
        }
    }


    以上代码主要做两件事情addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。所以当需要依赖收集的时候调用 addSub 方当需要派发更新的时候调用  notify - 调用也很简单:
     let dp = new Dep()
    dp.addSub(() => {//依赖收集的时候
       console.log('emit here')
    })
    dp.notify()//派发更新的时候

    5 观察者 Watcher

    5.1 为什么引入Watcher

    Vue 中定义一个 Watcher 类来表示观察订阅依赖。至于为啥引入Watcher,《深入浅出vue.js》给出了很好的解释: 当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。 「依赖收集的目的是:」将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。形成如下所示的这样一个关系(图参考《剖析 Vue.js 内部运行机制》)。f4e42c8061e56149c3dc07ab9cda8113.png

    5.2 Watcher的简单实现

    class Watcher {
      constructor(obj, key, cb) {
        // 将 Dep.target 指向自己
        // 然后触发属性的 getter 添加监听
        // 最后将 Dep.target 置空
        Dep.target = this
        this.cb = cb
        this.obj = obj
        this.key = key
        this.value = obj[key]
        Dep.target = null
      }
      update() {
        // 获得新值
        this.value = this.obj[this.key]
       // 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
        this.cb(this.value)
      }
    }


    以上就是 Watcher 的简单实现,在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,然后执行 update 函数。

    「依赖的本质:」

    所谓的依赖,其实就是Watcher。至于如何收集依赖,总结起来就一句话, 在getter中收集依赖,在setter中触发依赖。先收集依赖,即把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行了。

    具体来说,当外界通过Watcher读取数据时,便会触发getter从而将Watcher添加到依赖中,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。

    最后我们对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码,实现了一个简易的数据响应式。

    function observe (obj{
      // 判断类型
      if (!obj || typeof obj !== 'object') {
        return
      }
      Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
      })
      function defineReactive (obj, key, value{
        observe(value)  // 递归子属性
        let dp = new Dep() //新增
        Object.defineProperty(obj, key, {
          enumerabletrue//可枚举(可以遍历)
          configurable: true//可配置(比如可以删除)
          get: function reactiveGetter () {
            console.log('get', value) // 监听
         // 将 Watcher 添加到订阅
           if (Dep.target) {
             dp.addSub(Dep.target) // 新增
           }
            return value
          },
          setfunction reactiveSetter (newVal{
            observe(newVal) //如果赋值是一个对象,也要递归子属性
            if (newVal !== value) {
              console.log('set', newVal) // 监听
              render()
              value = newVal
         // 执行 watcher 的 update 方法
              dp.notify() //新增
            }
          }
        })
      }
    }

    class Vue {
        constructor(options) {
            this._data = options.data;
            observer(this._data);
            /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
            new Watcher();
            console.log('模拟视图渲染');
        }
    }

    当 render function 被渲染的时候,读取所需对象的值,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果修改对象的值,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

    「完整流程图:」411c3bc585079c66fc974fbeb26d67b5.png

    在 new Vue() 后, Vue 会调用_init 函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter 函数,而在当被赋值的时候会执行 setter函数。当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。在修改对象的值的时候,会触发对应的setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。

    「最后完整的响应式代码:」

    大概结构
      //defineReactive是对Observer的抽离
      const defineReactive = function(obj, key{
        // 以下代码省略
      }
      
      const Vue = function(options{
        console.log("Vue",this)
        //打印1  Vue {
                      _data:{
                          text"123"
                          get text: ƒ get()
                          set text: ƒ set(newVal)
                        },
                      mount: ƒ (),
                      render: ƒ ()
                    }
        // 以下代码省略
      }
      
      const Watcher = function(vm, fn) {
        console.log("Watcher",this)
        //打印3 Watcher  this是下面的Dep中subs的对象
        // 以下代码省略
      }
      
      const Dep = function({
        console.log("Dep",this)
        //打印2  Dep   { 
                        target: null,
                        subs: [
                          {        //是一个Watcher实例
                            subs: Array(1)
                            0: Watcher
                            vm: {    //是一个Vue实例
                                _data:{
                                  text"123",//该属性有了get和set方法
                                  get text: ƒ get(),
                                  set text: ƒ set(newVal)
                                },
                                mount: ƒ (),
                                render: ƒ ()
                              },

                            addDep: ƒ (dep),
                            update: ƒ (),
                            value: undefined
                          }
                        ],
                        depend: ƒ (),
                        addSub: ƒ (watcher),
                        notify: ƒ ()
                      }

        // 以下代码省略
      }
      
      const vue = new Vue({
        data() {
          return {
            text'hello world'
          };
        }
      })
      
      vue.mount(); 
      vue._data.text = '123';

    详细代码
    const Observer = function(data{
        console.log(1)   //开始4 new Vue的时候就会执行
      // 循环修改为每个属性添加get set
      for (let key in data) {
        defineReactive(data, key);
      }
    }

    const defineReactive = function(obj, key{
        console.log(2)    //开始5 new Vue的时候就会执行
      // 局部变量dep,用于get set内部调用
      const dep = new Dep();
      // 获取当前值
      let val = obj[key];
      Object.defineProperty(obj, key, {
          
        // 设置当前描述属性为可被循环
        enumerable: true,
        // 设置当前描述属性可被修改
        configurable: true,
        get() {
            console.log(3)//开始10  开始19
          console.log('in get');
          // 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
          dep.depend();
          return val;
        },
        set(newVal) {
            console.log(4)//开始15
          if (newVal === val) {
            return;
          }
          val = newVal;
          // 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
          // 这里每个需要更新通过什么断定?dep.subs
          dep.notify();
        }
      });
    }

    const observe = function(data{
        console.log(5)  //开始3 new Vue的时候就会执行
      return new Observer(data);
    }

    const Vue = function(options{
        console.log(6)//开始1 new Vue的时候就会执行
      const self = this;
      // 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
      if (options && typeof options.data === 'function') {
        console.log(7)//开始2   new Vue的时候就会执行
        this._data = options.data.apply(this);
      }
      // 挂载函数
      this.mount = function({
        console.log(8)  //开始7  new Vue以后,执行vue.mount()
        new Watcher(self, self.render);
      }
      // 渲染函数
      this.render = function({
        console.log(9//开始9 开始18  render函数执行后走到这里
        with(self) {
          _data.text;  //这里取data值的时候,就会走get方法
        }
      }
      // 监听this._data
      observe(this._data);  //new Vue的时候就会执行,这里执行完,就表示new Vue的过程执行完了
    }

    const Watcher = function(vm, fn{
        console.log(10)  //开始8  执行vue.mount()以后会走到这里
      const self = this;
      this.vm = vm;
      // 将当前Dep.target指向自己
      Dep.target = this;
      // 向Dep方法添加当前Wathcer
      this.addDep = function(dep{
        console.log(11//开始13  
        dep.addSub(self);
      }
      // 更新方法,用于触发vm._render
      this.update = function({
        console.log(12)//开始17
        console.log('in watcher update');
        fn();
      }
      // 这里会首次调用vm._render,从而触发text的get
      // 从而将当前的Wathcer与Dep关联起来
      this.value = fn();   //开始9  fn是render函数,这里fn()就会赋值的时候执行
      // 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
      // 造成代码死循环
      Dep.target = null;
    }

    const Dep = function({
        console.log(13)  //开始6  new Vue的时候就会执行到new Dep,然后执行到这里
      const self = this;
      // 收集目标
      this.target = null;
      // 存储收集器中需要通知的Watcher
      this.subs = [];
      // 当有目标时,绑定Dep与Wathcer的关系

      this.depend = function({
        console.log(14)  //开始11   开始20 走了get获取属性后,就要进行依赖收集 
        if (Dep.target) {
            console.log(15)//开始12  
          // 这里其实可以直接写self.addSub(Dep.target),
          // 没有这么写因为想还原源码的过程。
          Dep.target.addDep(self);
        }
      }
      // 为当前收集器添加Watcher
      this.addSub = function(watcher{
        console.log(16)//开始14
        self.subs.push(watcher);
      }
      // 通知收集器中所的所有Wathcer,调用其update方法
      this.notify = function({
        console.log(17//开始16
        for (let i = 0; i 1) {
          self.subs[i].update();
        }
      }
    }

    const vue = new Vue({
      data() {
        return {
          text'hello world'
        };
      }
    })

    vue.mount(); // in get
    vue._data.text = '123'// in watcher update /n in get

    「解析:」

    一开始new Vue ,会走到46行执行Vue构造函数,打印6

    然后46行Vue的入参options实际上是127行的入参{data(){}},是一个包含了data函数的对象,所以options.data是一个data函数,打印7。将vue中的data函数返回的数据赋值给_data。

    然后走到67行的observe,会继续往上走到41行定义它的地方。

    然后43行 new Observer 的时候会走到第一行Observer(关键函数),打印1。我们发现Observer实际就是给data数据都添加上get和set方法,只不过不添加的方法defineReactive给抽离出去了。

    然后走到第9行,执行defineReactive,打印2,然后15行给每个属性加上get和set方法。

    然后走到12行,new Dep的时候,会走到95行执行Dep,打印13。Dep函数剩下的代码都只是定义函数,都不会执行,会跳出Dep函数。然后会到defineReactive函数第13行,defineReactive剩下的代码中的函数也不会执行,所以会回到Observer,再回到67行,即new Vue的过程走完了。

    然后走到135行的vue.mount(),走到56行,打印8。

    然后执行new Watcher走到70行,打印10,然后「Dep.target = this」,这一步将watch实例挂载到了Dep的target属性上,从而关联起来。

    72行到88行只是定义,没有执行。89行this.value = fn()中:fn实际是传进来的「render」函数(看57行),然后后面又加了()就会立即执行。然后走到60行的render函数,打印9。「Watcher就执行完了」,然后,「关键的来了」:打印完9它会继续往下走,「读取_data.text」。那么,这一步就会触发get方法(这一步的目的就只是为了触发get,所以获取值就行了,并不需要做其他操作)。

    然后走到21行的get,打印3。

    然后走到25行,执行dep.depend(),再走到104行,打印14。

    这时候判断Dep.target,由于第8步将watch挂载到了Dep.target,这时候为true,所以打印15。

    然后走到110行,再跳到77行,打印11。

    79行执行后会跳到114行,打印16,完成了依赖收集,然后会回到Watch,执行最后一行,Dep.target = null,避免陷入死循环,然后Watch执行完了,「vue.mount()也执行完了」。。

    然后就是136行赋值操作了,这时候会走到28行的set,打印4。

    继续向下走,到36行,dep.notify(),然后走到119行,打印17。

    然后会走到122行,触发update,走到82行,打印12。

    然后执行fn(),即render函数,走到60行,打印9。

    然后走到63行,取data值,会走get,走到21,打印3。

    然后25行,会跳到104行,打印14。Dep.target为null,15不会打印

    参考链接:

    Object.defineProperty()[1]

    VUE响应式原理[2]

    vue.js源码GitHub[3]

    Vue-Object.defineProperty[4]

    Vue.js 技术揭秘

    Vue高级指南-01 Vue源码解析之手写Vue源码[5]

    vue响应式详解(重学前端-vue篇1)[6]详解Vue响应式原理

    参考资料

    [1]

    Object.defineProperty(): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

    [2]

    VUE响应式原理: https://juejin.im/post/6872992692268990478

    [3]

    vue.js源码GitHub: https://github.com/vuejs/vue

    [4]

    Vue-Object.defineProperty: https://github.com/zhihuifanqiechaodan/Vue-Object.defineProperty

    [5]

    Vue高级指南-01 Vue源码解析之手写Vue源码: https://juejin.im/post/6844904047921594382

    [6]

    vue响应式详解(重学前端-vue篇1): https://juejin.im/post/6850418111985352711

    e90651c9807cbf1ce77c1d5a1e6bcc0e.png扫码关注04c46f206f0e04a66f5e4782be47f040.pngWEB前端知识分享专业的分享 只为专业的你
    展开全文
  • 图解vueVue响应式原理

    2019-04-10 09:42:49
    本文是受当面试官问你Vue响应式原理,你可以这么回答他 启发自行绘制的vue动态响应原理图解 画风诡异,仅代表个人理解如果有不当的地方,望请斧正。 官方大图 我的见解 JserWang的原理代码,为了看懂我自行添加了...

    本文是受当面试官问你Vue响应式原理,你可以这么回答他
    启发自行绘制的vue动态响应原理图解 画风诡异,仅代表个人理解如果有不当的地方,望请斧正。

    官方大图


    我的见解

    JserWang的原理代码,为了看懂我自行添加了好多注释与console。

    //3.
    const Observer = function(data) {
      // 循环修改为每个属性添加get set
      for (let key in data) {
        console.log("给value的没一个key设置一个收集器");
        defineReactive(data, key);
      }
    }
    //4.
    const defineReactive = function(obj, key) {
      // 局部变量dep,用于get set内部调用
      console.log("创建收集器***********");
      const dep = new Dep();
      // 获取当前值
      let val = obj[key];
      console.log("observe  开始重写data每个属性的get/set");
      Object.defineProperty(obj, key, {
        // 设置当前描述属性为可被循环
        enumerable: true,
        // 设置当前描述属性可被修改
        configurable: true,
        get() {
          console.log('observe get触发');
          // 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
          console.log("observe dep.depend收集当前属性和watcher的依赖关系");
          console.log("当^^^^^^^^^^^^^^^^^^^^^^^^前属性:", key);
          dep.depend();
          return val;
        },
        set(newVal) {
          if (newVal === val) {
            console.log("observe set 设置属性值未变化");
            return;
          }
          val = newVal;
          // 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
          // 这里每个需要更新通过什么断定?dep.subs
          console.log("observe 设置属性值变化通知依赖收集器更新watcher");
          dep.notify();
        }
      });
    }
    const observe = function(data) {
      return new Observer(data);
    }
    
    //1.
    const Vue = function(options) {
      const self = this;
      self.a = 1;
      self.b = 1;
      // 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
      if (options && typeof options.data === 'function') {
        console.log("Vue 将传入配绑定给新建的vue对象:", this);
        this._data = options.data.apply(this);
      }
      // 挂载函数
      this.mount = function() {
        let tmp = self.a;
        self.a += 1;
        console.log("Vue 挂载watcher实例到当前vue对象",  tmp);
        new Watcher(self, self.render);
      }
      // 渲染函数
      this.render = function() {
        let tmp =  self.b;
        self.b += 1;
        console.log("Vue 触发渲染vue对象", tmp);
        console.log('Vue 看该属性是否被组件引用,引用则重新渲染');
        // with(self) {
        //   _data.text;//获取当前的data中的属性
        //   _data.a;
        //   _data.b;
        // }
        console.log(self._data.text);
        console.log(self._data.text1);
        console.log(self._data.text2)
      }
      // 监听this._data
      //2.
      console.log("Vue 设置观察对象, 重写set、get");
      observe(this._data);  
    }
    const Watcher = function(vm, fn) {
      const self = this;
      //保存传入的data对象
      this.vm = vm;
      // 将当前Dep.target指向当前watcher
      console.log("Watcher 将当前Dep.target指向当前watcher");
      console.log("-----------------------临时关联watcher----------------------------");
      Dep.target = this;
      // 向Dep方法添加当前Wathcer
      this.addDep = function(dep) {
        console.log("watcher向当前收集器dep添加当前Wathcer");
        dep.addSub(self);
      }
      // 更新方法,用于触发vm._render
      this.update = function() {
        console.log("watcher  触发render");
        fn();//vue.render
      }
      // 这里会首次调用vm._render,从而触发text的get
      // 从而将当前的Wathcer与Dep关联起来
    
      //vue.render  首次渲染
      console.log("-----------------------首次渲染----------------------------");
      this.value = fn();
      // 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
      // 造成代码死循环
      console.log("————————————————————清空Dep.target");
      Dep.target = null;
    }
    //5.
    const Dep = function() {
      console.log("Dep新建收集器");
      const self = this;
      // 收集目标
      this.target = null;
      // 存储收集器中需要通知的Watcher
      this.subs = [];
      // 当有目标时,绑定Dep与Wathcer的关系
      this.depend = function() {
        //确认实例是否绑定watcher
        if (Dep.target) {
          // 这里其实可以直接写self.addSub(Dep.target),
          // 没有这么写因为想还原源码的过程。
          console.log("Dep向当前收集器dep添加当前Wathcer");
          Dep.target.addDep(self);
        }
      }
      // 为当前收集器添加Watcher
      this.addSub = function(watcher) {
        console.log("Dep addSub为当前收集器添加watcher");
        self.subs.push(watcher);
      }
      // 通知收集器中所的所有Wathcer,调用其update方法
      this.notify = function() {
        console.log("Dep 通知所有的watcher 触发更新");
        for (let i = 0; i < self.subs.length; i += 1) {
          self.subs[i].update();
          console.log(`第${i}个watcher`, self.subs[i]);
        }
      }
    }
    const vue = new Vue({
      data() {
        return {
          text: 'hello world',
          text1: "aaa",
          text2: "333"
        };
      }
    })
    console.log("触发挂载-----------------------");
    vue.mount(); // in get
    console.log(vue._data.text);
    console.log(vue._data.text1);
    console.log(vue._data.text2);
    console.log('data set调用------------------------------------------------------');
    vue._data.text = '123'; // in watcher update /n in get
    console.log('data set调用-----------------------------------------------------------------');
    vue._data.text = 'aaaaa';
    console.log(vue._data.text);
    console.log(vue._data.text1);
    console.log(vue._data.text2);
    复制代码
    展开全文
  • Vue 响应式原理

    2019-07-25 15:53:30
    经常有小伙伴向师傅反映: ...说起来也是,平时小伙伴们估计只是使用 vue 的情况更多一些,对于 vue响应式原理,编程又用不到,所以自然也没怎么去关心过,结果面试时一被问到,自然也就会懵逼了。 ...
  • 响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图。在面试中是经常考查的知识点,也是面试加分项。 本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行: 分析主要成员,了解它们有助于理解...
  • 416Vue响应式原理

    2021-04-20 11:22:09
    响应式原理面试的常见问题 学习别人优秀的经验,转换成自己的经验 给Vue实例细则一个成员是否是响应式的 给属性重新赋值成对象,是否是响应式的 为血虚vue源码做准备 2.数据驱动 数据驱动 数据响应式 数据模型...
  • 收集依赖5 观察者 Watcher前言:现在前端面试Vue中都会问到响应式原理以及如何实现的,如果你还只是简单回答通过Object.defineProperty()来劫持属性可能已经不够了vue响应式原理学习(1)一句话总结:通过Obje...
  • VUE响应式原理

    2019-09-21 17:50:48
    前段时间面试小米被问到vue响应式原理。然后粗略的回答了发布者订阅者模式,Object.defineProperty(),感觉面试官不是很满意,我自己其实当时答的也没底 记录 js中Object.defineProperty()用来在一个对象上定义一...
  • Vue响应式原理的简单模型

    千次阅读 2021-05-01 23:28:40
    其实之前也尝试过了解vue响应式原理,毕竟现在面试看你用的是vue的话,基本上都会问你几句vue响应式原理。以往学习这块就是看看别人写的文章,或者翻翻源码。这个过程中发现相当一部分文章看完之后一句话总结...
  • vue响应式原理,也算是面试中再常见不过的题目了,之前遇见这道题目只会说:利用的是Object.defineProperty进行的数据劫持,监听数据的变化,通知watcher进行的数据更新。总的来说这是没错的,但是只要面试官...
  • 文章目录vue响应式原理学习(1)1.什么是响应式2.实现响应式,我们需要做些什么3.如何侦测数据的变化3.1 Object.defineProperty实现3.1.1 Observer实现一个对象对所有成员的代理3.2 Proxy实现4.收集依赖4.1 为什么要...
  • 学习Vue响应式原理

    2018-04-27 03:39:47
    看了热门文章 当面试官问你Vue响应式原理,你可以这么回答他, 被 Watcher 和 Dep搞晕了, 所以画了张草图, 梳理一下执行顺序, 做个记录. 不规则椭圆 是对象, new Vue(), Dep1, Dep2, new Warcher 长方体是函数, ...
  • 深度解析 Vue 响应式原理 该文章内容节选自团队的开源项目InterviewMap。项目目前内容包含了 JS、网络、浏览器相关、性能优化、安全、框架、Git、数据结构、算法等内容,无论是基础还是进阶,亦或是源码解读,...
  • 响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图。在面试中是经常考查的知识点,也是面试加分项。 本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行: 分析主要成员,了解它们有助于理解...
  • (1)vue-router实现原理 vue-router提供三种路由模式 1.hash模式 默认模式,通过路径中的hash值来控制路由跳转,不存在兼容问题 hash模式实现原理 在正常路径后跟一个 # 号,匹配 # 后边的路径为前端路由,通过window...
  • 来源 |https://www.cnblogs.com/chanwahfung/p/13175515.html前言响应式原理作为Vue的核心,使用数据劫持实现数据驱动视图。在面试中...
  • Vue作为一款现代前端三驾...今天这篇文章将从Vue2版本开始简单讲解Vue响应式原理,和即将发布正式版的Vue3的实现差别。在Vue中,我们开发一个页面的时候,需要分别开发html模板和js数据部分。而数据将直接渲染到...
  • Vue响应式原理之Object.defineProperty,通俗易懂好兄弟快学起来 首先是对对象的监听,话不多说先看代码 let obj = { a: { b: 1, c: 2 }, d: 4 } observe(obj) obj.a.b = 3 //直接...
  • 响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图。在面试中是经常考查的知识点,也是面试加分项。 本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行: 1.分析主要成员,了解它们有助于...
  • vue响应式是如何实现的?听过太多回答,通过Object.defineProperty,可是再详细的问时,对方浑然不知。先撸为敬const Observer = function(data) { for (let key in data) { defineReactive(data, key); } } ...
  • vue-router实现原理 vue-router提供三种路由模式 1.hash模式 默认模式,通过路径中的hash值来控制路由跳转,不存在兼容问题 hash模式实现原理 在正常路径后跟一个 # 号,匹配 # 后边的路径为前端路由,通过window....

空空如也

空空如也

1 2 3 4 5 ... 15
收藏数 284
精华内容 113
关键字:

vue响应式原理面试

vue 订阅