• Vue响应式系统原理并实现一个双向绑定


    这一章就着重讲两个点:

    • 响应式系统如何收集依赖
    • 响应式系统如何更新视图 我们知道通过Object.defineProperty做了数据劫持,当数据改变的时候,get方法收集依赖,进而set方法调用dep.notify方法去通知Watcher调用本身update方法去更新视图。那么我们抛开其他问题,就讨论getnotifyupdate等方法,直接上代码:

    get( )

      get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们知道Dep.target在创建Watcher的时候是null,并且它只是起到一个标记的作用,当我们创建Watcher实例的时候,我们的Dep.target就会被赋值到Watcher实例,进而放入target栈中,我们这里调用的是pushTarget函数:

    // 将watcher实例赋值给Dep.target,用于依赖收集。同时将该实例存入target栈中
    export function pushTarget (_target: ?Watcher) {
      if (Dep.target) targetStack.push(Dep.target)
      Dep.target = _target
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那我们继续执行到if (Dep.target)语句的时候就会调用Dep.depend函数:

     // 将自身加入到全局的watcher中
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那下面的childOb是啥东西呢?

      let childOb = !shallow && observe(val)
    
    • 1

    我们通过这个变量判断当前属性下面是否还有ob属性,如果有的话继续调用Dep.depend函数,没有的话则不处理。
    我们还需要处理当前传入的value类型,是数组属性的话则会调用dependArray收集数组依赖

    // 收集数组依赖
    function dependArray (value: Array<any>) {
    for (let e, i = 0, l = value.length; i < l; i++) {
      e = value[i]
      e && e.__ob__ && e.__ob__.dep.depend()
      if (Array.isArray(e)) {
        dependArray(e)
      }
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    那么收集依赖部分到这里就完了现在进行下一步触发更新

    set( )

    参考vue实战视频讲解:进入学习

       set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          // 判断NaN的情况
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          }
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          dep.notify()
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    我们看到了下面的 set函数触发了dep.notify()方法

    notify( )

      // 通知所有订阅者
      notify () {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    notify里面我们就做了一件事情,遍历subs数组里面的所有Watcher,逐一调用update方法,也就是我们说的通知所有的订阅者Watcher调用自身update方法 update( )

      update () {
        if (this.lazy) {
          // 计算属性会进来这段代码块
          // 这里将dirty赋值为true
          // 也不会马上去读取值
          // 当render-watcher的update被触发时
          // 重新渲染页面,计算属性会重新读值
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    那么update方法实现了什么呢?lazydirtysync又是啥?

       if (options) {
          this.deep = !!options.deep
          this.user = !!options.user
          this.lazy = !!options.lazy
          this.sync = !!options.sync
        } else {
          this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
         // 这里将lazy的值赋值给了dirty
        // 就是说实例化的时候dirty = lazy = true
        this.dirty = this.lazy // for lazy watchers
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    那是控制计算属性的,当render—watcher的方法update被调用的时候,this.dirty会变为true会重新计算computed值,渲染视图,我们这里不叙述。
    那么我们直接看queueWatcher()函数:

    export function queueWatcher (watcher: Watcher) {
    const id = watcher.id
    if (has[id] == null) {
     has[id] = true
     if (!flushing) {
       queue.push(watcher)
     } else {
       // if already flushing, splice the watcher based on its id
       // if already past its id, it will be run next immediately.
       let i = queue.length - 1
       while (i > index && queue[i].id > watcher.id) {
         i--
       }
       queue.splice(i + 1, 0, watcher)
     }
     // queue the flush
     if (!waiting) {
       waiting = true
       nextTick(flushSchedulerQueue)
     }
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    我们可以看到一个更新队列,更新队列指向:

    function callUpdatedHooks (queue) {
      let i = queue.length
      while (i--) {
        const watcher = queue[i]
        const vm = watcher.vm
        if (vm._watcher === watcher && vm._isMounted) {
          callHook(vm, 'updated')
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们的callback调用updated钩子
    讲到这里就有点超纲了,咱们初始化渲染会调用一个initRender函数创建dom,还有上面所述的nextTick,后期都会讲,那么了解了更新机制,下一章我们就来实现一个让面试官都惊呆了双向绑定

    我们对Vue响应式系统有一定的了解,并且知道它是如何实现数据更新视图视图改变数据的,那么有这样的基础,我们来手写一个MVVM,以便面试的时候,吊打面试官(此为笑谈,不足论,嘿嘿)。
    那么先抛出一张在座的各位再也熟悉不过的图:

    在这里插入图片描述

    1、当我们new MVVM之后有两步操作,Observer,Compile,我们知道Observer是做数据劫持,Compile是解析指令,那么问题来了:

    • Observer为什么要做数据劫持?
    • Compile为什么要做解析指令?
      带着这两个问题,我们回顾一下往期内容:
    • 什么是数据响应式
    • 数据响应式原理是什么?
    • 数据响应式是如何实现的?

    数据响应式就是数据双向绑定,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新;如果用户更新了View,那么Model数据也被自动更新了,这种情况就是双向绑定。

    数据响应式原理

    • Vue实现数据响应式原理就是通过Object.defineProperty()这个方法重新定义了对象获取属性值get设置属性值set的操作来实现的
    • Vue3.0中是通过ECMAScript6中的proxy对象代理来实现的。
      那么本章节就是来实现数据响应式的。

    那么回答前面的两个问题,为什么要劫持数据?为什么要解析指令

    • 只有劫持到数据,才能对数据做到监听,以便于数据更改能够及时做到更新视图。
    • Vue中自定义了N多指令,只有解析它,我们JavaScript才能认识它,并运行它。
      诸如此类问题我们不再复述,下面开始实现数据响应式。

    写一个demo之前,我们应当整理好思路:

    1. 首先实现整体的一个架构(包括MVVM类或者VUE类、Watcher类),   /这里用到一个订阅发布者设计模式。
    2. 然后实现MVVM中的由M到V,把模型里面的数据绑定到视图。
    3. 最后实现V-M, 当文本框输入文本的时候,由文本事件触发更新模型中的数据
    4. 同时也更新相对应的视图。
    
    • 1
    • 2
    • 3
    • 4
    //html代码
    <div id="app">
          <h1>MVVM双向绑定</h1>
          <div>
            <div v-text="myText"></div>
            <div v-text="myBox"></div>
            <input type="text" v-model="myText" />
            <input type="text" v-model="myBox" />
          </div>
    </div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们创建了两个divinput实现input框数据关联,说白了也就是相同的数据源,那我们的数据源在哪呢?

    //数据源data
    const app = new Vue({
            el: "#app",
            data: {
              myText: "大吉大利!今晚吃鸡!",
              myBox: "我是一个盒子!",
            },
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可见我们需要一个Vue类,也就是一个发布者,那么直接上代码:

    //Vue类(发布者)
    class Vue{
    
    }
    
    • 1
    • 2
    • 3
    • 4

    发布者有了,我们还需要有订阅者:

    //Watcher类(订阅者)
    class Watcher{
    
    }
    
    • 1
    • 2
    • 3
    • 4

    可见两者都有了,那么我们该怎么实现呢?

    • 获取data数据
    • 获取元素对象
    • 构造一个存放订阅者的对象
     class Vue {
            constructor(optios) {
              this.$data = optios.data; //获取数据
              this.$el = document.querySelector(optios.el); //获取元素对象
              this._directive = {}; // 存放订阅者
            }
     }       
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么我们说了,我们需要劫持数据解析指令,那么我们得构造两个方法。

       class Vue {
           constructor(optios) {
             this.$data = optios.data; //获取数据
             this.$el = document.querySelector(optios.el); //获取元素对象
             this._directive = {}; // 存放订阅者
             this.Observer(this.$data);
             this.Compile(this.$el);
           }
           //劫持数据
           Observer(data) {
               Object.defineProperty(this.$data, key, {
                 get: function(){},
                 set: function(){}
                 },
               });
           }
           //解析指令   //视图 --- >对象 -- >指令
           Compile(el) {
    
           }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    一个是劫持数据,一个是解析元素指令,劫持到的属性要根据属性分配容器,当当前容器不存在该属性的时候,我们便需要把他添加到订阅器对象里面,等待通知更新。

      for (let key in data) {
              this._directive[key] = [];
              let val =data[key];
              let watch = this._directive[key];
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那么解析指令,首先必须要递归当前节点,是否还有子节点,是否有v-text指令,v-model指令。

           let nodes = el.children;
              for (let i = 0; i < nodes.length; i++) {
                let node = nodes[i];
                //递归 查询所有当前对象子类是否再含有子类
                if (node.children.length) {
                  this.Compile(nodes[i]);
                }
                //判断是否含有V-text指令
                if (node.hasAttribute("v-text")) {
                  let attrVal = node.getAttribute("v-text");
    
                  this._directive[attrVal].push(
                    new Watcher(node, this, "innerHTML", attrVal)
                  );
                }
    
                //判断是否含有V-model指令
                if (node.hasAttribute("v-model")) {
                  let attrVal = node.getAttribute("v-model");
    
                  this._directive[attrVal].push(
                    new Watcher(node, this, "value", attrVal)
                  );
                  node.addEventListener("input", () => {
                    //赋值到模型
                    this.$data[attrVal] = node.value;
                    // console.log(this.$data);
                  });
                }
              }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    那么我们触发更新时候需要收集依赖,我们直接吧收集到的依赖return出去

     Object.defineProperty(this.$data, key, {
                  get: function(){
                        return val;
                  }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那么我们订阅者长什么样呢?我们订阅者,接收当前元素信息,MVVM对象,标识,属性。并且需要构造一个更新方法update

        class Watcher {
            constructor(el, vm, exp, attr) {
              this.el = el;
              this.vm = vm;
              this.exp = exp;
              this.attr = attr;
              this.update();
            }
            //更新视图
            update() {
              this.el[this.exp] = this.vm.$data[this.attr];
              //div.innerHTML/value = this.Vue.$data["myText/myBox"]
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    到这里已经快完成了,那么我们收集了依赖就要去,通知watcher去更新视图啊,那么来了:

        Object.defineProperty(this.$data, key, {
                  get: function(){
                        return val;
                  },
                  set: function(newVal){
                      if(newVal !== val){
                        val = newVal;
                        watch.forEach(element => {
                            element.update();  
                        });
                      }
                  },
        });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    做到这里,你就可以实现一个数据响应式了。

    在这里插入图片描述
    在这里插入图片描述

    我们已经掌握了响应式原理,那我们开始着手Vue的另一个核心概念组件系统

  • 相关阅读:
    【瑞吉外卖】day01:整体介绍以及开发环境搭建
    vue封装一个查询URL参数方法
    1336_FreeRTOS中一组队列辅助接口函数的实现分析
    本地部署Confluence遇到的问题:MySQL数据库编码、隔离级别、验证码不显示
    什么是深拷贝;深拷贝和浅拷贝有什么区别;深拷贝和浅拷贝有哪些方法(详解)
    Vue3 生命周期新写法
    还在用命令行看日志?快用Kibana吧,可视化日志分析YYDS
    基于Python实现的遗传算法求TSP问题
    Spring全家桶相关注解总结
    C++ - 完美语义(右值引用的中篇) - lambda表达式
  • 原文地址:https://blog.csdn.net/yyds2026/article/details/127752138