vue3.0(十)双向数据绑定原理和v2.0对比


MVVM框架

MVVM(Model-View-ViewModel)是一种软件架构模式,它将软件系统分为三个部分:模型(Model)、视图(View)和视图模型(ViewModel)。MVVM 模式的目标是将业务逻辑和用户界面分离,从而提高代码的可维护性和可测试性。
MVVM 中的三个缩写分别是 Model(模型)、View(视图)和 ViewModel(视图模型)。

  • Model:模型是指应用程序的数据模型,它包含了应用程序的业务逻辑和数据。
  • View:视图是指应用程序的用户界面,它负责显示模型中的数据。
  • ViewModel:视图模型是 MVVM 模式中的核心部分,它是一个中间层,负责将模型中的数据转换为视图可以显示的数据,并将用户在视图上的操作转换为对模型的操作。MVVM 模式的目标是将应用程序的业务逻辑和用户界面分离,从而提高应用程序的可维护性和可测试性。通过数据绑定机制将视图和模型进行关联。当模型发生变化时,视图会自动更新,从而实现了数据的双向绑定。
    在这里插入图片描述

1 理解ViewModel

主要职责:
- 数据变化后更新视图
- 视图变化后更新数据
有主要部分组成:
- 家庭起(Observer): 对所有数据的属性进行监听
- 解析器(Compiler):对每个元素节点的指令进扫描跟解析,根据指令模版一环数据,以及绑定相应的更新函数。

2 MVVM的优点

MVVM模式,主要目的是分离视图(View)和模型(Model),有几大优点
1. 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xaml代码。
4. 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。

vue2.0 双向数据绑定原理

Vue2.0实现MVVM(双向数据绑定)的原理是通过 Object.defineProperty 来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

1 实现双向数据绑定

  • new Vue()首先执行初始化,对 data执行响应化处理,这个过程发生 Observe中。
  • 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
  • 同时定义一个更新函数和W啊提车人,将来对应数据变化时Watcher会调用更新函数
  • 由于data的某个可以再一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
  • 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
    流程图如下:
    在这里插入图片描述

2 实现

  1. 先创建一个构造函数,执行初始化,对data执行响应化处理
    class Vue { 
     constructor(options) { 
     this.$options = options; 
     this.$data = options.data; 
     
     // data 
     observe(this.$data); 
     
     // data vm 
     proxy(this); 
     
     // 
     new Compile(options.el, this); 
     } 
    }
    
  2. 对data选项执行响应化具体操作
    function observe(obj) { 
    	if (typeof obj !== "object" || obj == null) { 
    		 return; 
    	 } 
    	 new Observer(obj); 
    } 
     
    class Observer { 
    	 constructor(value) { 
    	 this.value = value; 
    	 this.walk(value); 
     } 
     walk(obj) { 
    	 Object.keys(obj).forEach((key) => { 
    	 defineReactive(obj, key, obj[key]); 
     }); 
     } 
    }
    
  3. 编译compile
    对每个元素节点的指令进行扫描跟解析,根据指令模版替换数据,以及绑定相应的更新函数
    在这里插入图片描述
    class Compile { 
    	 constructor(el, vm) { 
    		 this.$vm = vm; 
    		 this.$el = document.querySelector(el); // dom 
    		 if (this.$el) { 
    		 this.compile(this.$el); 
    	 } 
     } 
     compile(el) { 
    	 const childNodes = el.childNodes; 
     	Array.from(childNodes).forEach((node) => { // 
    		 if (this.isElement(node)) { // 
    		 	console.log(" " + node.nodeName); 
    		 } else if (this.isInterpolation(node)) { 
    		 	console.log(" " + node.textContent); // 
    		{{}} 
     	} 
    	 if (node.childNodes && node.childNodes.length > 0) { // 
    	 	this.compile(node); // 
    	 } 
     }); 
    } 
     isElement(node) { 
     	return node.nodeType == 1; 
     } 
     isInterpolation(node) { 
     	return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent); 
     } 
    }
    
  4. 依赖收集
    视图中会用到data中某key,成为依赖。同一个key可能出现多次,每次都需要收集出来用一个watcher来维护,依赖收集多个watcher需要一个Dep来管理,需要更新时由Dep统一通知
    // 
    class Watcher { 
     constructor(vm, key, updater) { 
     this.vm = vm 
     this.key = key 
     this.updaterFn = updater 
     
     // Dep.target 
     Dep.target = this 
     // key get 
     vm[key] 
     // 
     Dep.target = null 
     } 
     
     // dom dep 
     update() { 
     this.updaterFn.call(this.vm, this.vm[this.key]) 
     } 
    }
    
    声明Dep
    class Dep { 
    	constructor() { 
    		 this.deps = []; // 
     	} 
    	addDep(dep) { 
    		 this.deps.push(dep); 
    	} 
    	notify() { 
    	 this.deps.forEach((dep) => dep.update()); 
    	} 
    }
    
    创建watcher是触发getter
    class Watcher { 
     constructor(vm, key, updateFn) { 
    	 Dep.target = this; 
    	 this.vm[this.key]; 
    	 Dep.target = null; 
     } 
    }
    
    依赖收集,创建Dep实例
    function defineReactive(obj, key, val) { 
     this.observe(val); 
     const dep = new Dep(); 
     Object.defineProperty(obj, key, { 
    	 get() { 
    		 Dep.target && dep.addDep(Dep.target);// Dep.target Watcher 
    		 return val; 
    	 }, 
    	 set(newVal) { 
    		 if (newVal === val) return; 
    		 dep.notify(); // dep 
    	 }, 
     	}); 
    }
    

3 Vue2.0 缺点和解决办法

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。

  1. Vue 无法检测 property 的添加或移除——通过Vue.set(vm.someObject, ‘b’, 2),或者 vm.$set

    由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。

  2. 无法检测数组更改例如 a[1]=0;——同上方法Vue.set(vm.items, indexOfItem, newValue)

    当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
    当你修改数组的长度时,例如:vm.items.length = newLength

    数组的[‘push’, ‘unshift’, ‘splice’, ‘reverse’, ‘sort’, ‘shift’, ‘pop’]这些方法中splice、‘push’、unshift是劫持不到的,在vue2.0源码中,是对数组的这三个方法进行了重写。具体如下:

    let arrayProto = Array.prototype;
    let newProto = Object.create(arrayProto); 
    const arrMethods=['push', 'unshift', 'splice', 'reverse', 'sort', 'shift', 'pop'];
    arrMethods.forEach(method => {
        newProto[method] = function(...args) {
            let inserted = null;
            switch (method){
                // args 是一个数组
                case 'push':
                    inserted = args;
                    break;
                case 'unshift':
                    inserted = args;
                    break;
                case 'splice': //splice方法有三个参数,第三个参数才是被新增的元素
                    inserted = args.slice(2);//slice返回一个新的数组
                    break;
            }
            if (inserted) ArrayObserver(inserted);      //因为inserted是一个新的数组项,所以要对数组的新增项重新进行劫持
            arrayProto[method].call(this, ...args);     //调用数组本身对应的方法
        }
    })
    function observer(obj){
    ...
        //通过Object.defineProperty进行循环递归绑定
    }
    function ArrayObserver(obj){ //对数组的新增项进行监控
        obj.forEach(item => {
            observer(item);  
        });
    }
    
  3. 不能在data里面增加项——初始时写入data,值为空

  4. 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题。

vue3.0 双向数据绑定原理

Vue3.0基于Proxy来做数据大劫持代理,可以原生支持到数组的响应式,不需要重写数组的原型,还可以直接支持新增和删除属性, 比Vue2.x的Object.defineProperty更加的清晰明了。
Proxy的监听是针对一个对象的,那么对这个对象的所有操作会进行监听操作,完全可以代理所有属性。

const proxyData = new Proxy(data, {
   get(target,key,receive){ 
     // 只处理本身(非原型)的属性
     const ownKeys = Reflect.ownKeys(target)
     if(ownKeys.includes(key)){
       console.log('get',key) // 监听
     }
     const result = Reflect.get(target,key,receive)
     return result
   },
   set(target, key, val, reveive){
     // 重复的数据,不处理
     const oldVal = target[key]
     if(val == oldVal){
       return true
     }
     const result = Reflect.set(target, key, val,reveive)
     return result
   },
   // 删除属性
   deleteProperty(target, key){
     const result = Reflect.deleteProperty(target,key)
     return result
   }
 })

Proxy直接会代理监听data的内容,非常的简单方便,唯一的不足就是部分浏览器无法兼容Proxy,也不能hack,所以目前只能兼容到IE11。
后期会有专门的博文解释说明es6中的Proxy

vue2.0和vue3.0 的差异

Vue2.0

  • 基于Object.defineProperty,不具备监听数组的能力,需要重新定义数组的原型来达到响应式。
  • Object.defineProperty 无法检测到对象属性的添加和删除 。
  • 由于Vue会在初始化实例时对属性执行getter/setter转化,所有属性必须在data对象上存在才能让Vue将它转换为响应式。
  • 深度监听需要一次性递归,对性能影响比较大。

Vue3.0

  • 基于Proxy和Reflect,可以原生监听数组,可以监听对象属性的添加和删除。
  • 不需要一次性遍历data的属性,可以显著提高性能。
  • 因为Proxy是ES6新增的属性,有些浏览器还不支持,只能兼容到IE11 。

Object.defineProperty和Proxy的对比

  • 参数不同

    使用Object.defineProperty对象以及对象属性的劫持+发布订阅模式。
    语法:
    Object.defineproperty( object,‘ propName ’ ,descriptor);
    object:要监听的目标对象
    propName :要定义或修改的属性的名称。
    descriptor:要定义或修改的属性描述符,操作详情。

    let pObj = new Proxy(target, handler);
    target 代表需要添加代理的对象
    handler 用来自定义对象中的操作

  • 返回值不同

    Object.defineProperty返回值
    被传递给函数的对象,就是要定义或修改属性的对象

    Proxy 返回值
    一个Proxy代理的对象,操作这个对象会触发handler对应操作。改变原始对象不会触发。

  • 数据类型不同

    Object.defineProperty是函数
    Proxy是一个对象

  • 15
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值