• MVVM与Vue响应式的实现


    Vue的响应式实现原理

    MVVM

    M:模型 ==》data中的数据

    V:视图 ==》模板

    VM:视图模型 ==》Vue实例对象

    在这里插入图片描述

    ViewModel是一个中间的桥梁将视图View与模型Model连接起来,ViewModel内部通过数据绑定,实现数据变化,视图发生更新变化,通过数据劫持实现的数据绑定;通过dom监听,实现事件触发,调用对应的回调函数,比如更新数据(数据变化了,视图就会更新–数据绑定)

    Vue 的设计也受到了MVVM的启发,View对应的是dom,它的ViewModel对应的式Vue实例,,Model对应的是data对象;通过数据劫持来实现数据绑定;编译(事件指令)的时候添加事件监听

    Vue的响应式原理

    主要是:数据代理、数据绑定、模板编译

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iKZWJxqy-1670162086242)(C:\Users\lucas\Desktop\学习\图片\reactive.jpg)]

    绿线是初始化时执行的,红线是数据更新时触发的

    一、初始化的时候:

    1》数据代理

    数据代理就是通过一个对象代理对另一个对象中属性的操作(读/写);vue中通过vm代理data对象(vm代理vm._data)中所有属性的操作,更方便的操作data中的数据

    vue中数据代理:将vue文件中的data保存一份到vm._data; 然后对将vue文件中data中的每个属性添加到vm实例上,通过Object.defineProperty实现数据的代理;当我们读取vm上的属性时,他会到vm._data中找到对应的属性返回当,我们修改vue实例对象的属性后,对应的setter就会监听到变化,然后去修改实例对象上vm._data对应的属性

        Object.defineProperty(me, key, {
          configurable: false, // 不可以重新定义
          enumerable: true, // 可枚举遍历
          // 当执行vm.name获取属性值时自动调用返回属性值
          get: function proxyGetter() {
            // 读取data中对应的属性值返回
            return me._data[key];
          },
          // 当执行vm.name = "xxx"时自动调用
          set: function proxySetter(newVal) {
            // 将最新的值保存给data对应的属性上
            me._data[key] = newVal;
          }
        });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2》数据绑定:

    数据绑定

    初始化显示:页面(表达式、指令)能从data读取数据希纳是(编译、解析)

    更新显示:更新data中的属性数据,能够更新页面

    数据劫持

    1》数据劫持是vue实现数据绑定的一种技术

    2》基本思想:通过defineProperty()来监视data中所有属性(任意层次)数据的变化,一旦变化就更新界面

    具体实现:

    采用递归的方式为data中的每个层级的属性创建dep(实例对象),并通过defineProperty对data进行重新定义,实现数据劫持;在set中判断数据是否发生了变化,如果发生改变,一方面他会更新值,新的值是需要重新劫持监听;另一方面会通知所有相关订阅者watcher去更新界面;在get中不仅返回值,还需要建立watcher与dep的关系(这个get会在模板解析大括号表达式和指令时触发);给Watcher添加Dep,给watch的subs中push对应dep

    dep对象:

    {
    id;0,
    subs:[]
    }
    
    • 1
    • 2
    • 3
    • 4

    dep的id从0开始递增,每个属性对应一个dep,劫持

    watcher对象:

    {
      vm:MVVM
      exp:"name",
      depIds:{depId:dep}  // depId就是上面dep的id,是个数字
      cb: textUpdater,
      value:"luca"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    watcher与dep的关系

    多对多的关系

    一个dep可能对应多个watcher; eg:一个属性在多个标签中使用

    一个watcher可能的对应多个dep; eg: {{a.b.c}}

    什么时候建立关系:编译、解析模板(大括号表达式和非事件指令)时建立

    怎么建立关系:创建watcher时会读取data中的值,defineProperty中get会建立双方关系;在dep的subs中push了watcher,且在watcher的depIds中添加了对应dep

    劫持的部分代码:

      defineReactive: function (data, key, val) {
        // 创建对应的dep对象
        var dep = new Dep();
        // 通过隐式递归调用, 实现对所有层次属性的劫持
        var childObj = observe(val);
        // 给data中指定属性进行重新定义
        Object.defineProperty(data, key, {
          enumerable: true, // 可枚举
          configurable: false, // 不能再define
          // 返回属性值, 同建立dep与watcher之间的关系
          get: function () {
            if (Dep.target) {
              dep.depend();
            }
            return val;
          },
          // 监视属性值的变化, 一旦变化去更新对应的界面
          set: function (newVal) {
            if (newVal === val) {
              return;
            }
            val = newVal;
            // 是object的新的值话,进行监听
            childObj = observe(newVal);
            // 通知订阅者
            dep.notify();
          },
       });
      },
    
    • 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

    3》编译模板

    1、将el所有的子节点去除,添加到加到一个新建的文档 fragment 对象中

    2、对 fragment中所有层次子节点递归进行编译解析处理

    编译大括号表达式和一般指令时创建watcher;创建watcher时指定了更新函数(因为要更新的可能是a.b.c这种,所以要遍历,取值是从_data中取的);这是初始化需要读取_data中的数据,就会走对应属性的get,从而建立dep与watcher的关系,给dep的subs中push了对应的watcher,并且在watcher中的depIds添加了对应的dep

    1》对表达式文本进行解析

    ​ a、根据正则对象匹配的表达式字符串( 匹配eg:{{obj.age}} ):子匹配

    ​ b、将属性值设置为文本节点的 textContent

    ​ c、从data中去除表达式对应的属性(通过.来分割成数组后遍历,因为可能由多层 a.b.c)

    2》对元素节点的指令属性进行解析

    a》 事件指令的解析

    ​ a.1、从指令中取出事件名

    ​ a2、根据指令的值从methods中得到对应的事件处理函数对象

    ​ a3、给当前元素节点绑定事件名和事件回调函数的事件监听

    ​ a4、指令解析完成后,移除次指令属性

    b》一般指令的解析

    ​ b.1、从表达式中取出指令名和指令值

    ​ b.2、从data中根据表达式得到对应的值

    ​ b.3、根据指令名确定需要操作的元素节点的什么属性

    ​ b.4 、将表达式的值设置到对应的属性上(v-text:textContent;;v-html:innerHtml; v-class :className)

    ​ b5、移除元素的指令属性

    3、将解析后的 fragment 添加到el中显示

    fragment修改并不会引起页面的更新

    二、数据变化的时候

    监听到_data的变化,set首先判断值有没有变化,发生变化而且是个对象的话会重新调用observe函数进行监听;通知所有相关的订阅者更新界面(通过遍历subs,调用watcher中的回调函数函数来更新界面)

    双向数据绑定的实现

    双向绑定是建立在单项数据绑定的基础上;只是在解析v-model指令时,给当前元素添加了input监听;当input的value发生改变时,修改对应的_data中的属性

    v-model 是动态属性绑定 v-bind 与 input事件的语法糖

    关的订阅者更新界面(通过遍历subs,调用watcher中的回调函数函数来更新界面)

    双向数据绑定的实现

    双向绑定是建立在单项数据绑定的基础上;只是在解析v-model指令时,给当前元素添加了input监听;当input的value发生改变时,修改对应的_data中的属性

    v-model 是动态属性绑定 v-bind 与 input事件的语法糖

  • 相关阅读:
    运行检测Java版本的代码出现Failed to resolve SDK
    再谈配置maven镜像
    找x。。。
    【无标题】
    669. 修剪二叉搜索树
    微信群发工具,纯Python编写~
    1. 开篇:SpringBoot与SpringCloud的那些事
    P1208 [USACO1.3] 混合牛奶 Mixing Milk
    通过宽高自适应设计两栏布局和三栏布局
    扫描线例题——850. Rectangle Area II
  • 原文地址:https://blog.csdn.net/weixin_50576800/article/details/128178229