文章目录
- MVVM框架
- 1 理解ViewModel
- 2 MVVM的优点
- vue2.0 双向数据绑定原理
- 1 实现双向数据绑定
- 2 实现
- 3 Vue2.0 缺点和解决办法
- vue3.0 双向数据绑定原理
- vue2.0和vue3.0 的差异
- Vue2.0
- Vue3.0
- Object.defineProperty和Proxy的对比
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 实现
- 先创建一个构造函数,执行初始化,对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); } }
- 对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]); }); } }
- 编译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); } }
- 依赖收集
视图中会用到data中某key,成为依赖。同一个key可能出现多次,每次都需要收集出来用一个watcher来维护,依赖收集多个watcher需要一个Dep来管理,需要更新时由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]) } }
创建watcher是触发getterclass Dep { constructor() { this.deps = []; // } addDep(dep) { this.deps.push(dep); } notify() { this.deps.forEach((dep) => dep.update()); } }
依赖收集,创建Dep实例class Watcher { constructor(vm, key, updateFn) { Dep.target = this; this.vm[this.key]; Dep.target = null; } }
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 不能检测数组和对象的变化。
-
Vue 无法检测 property 的添加或移除——通过Vue.set(vm.someObject, ‘b’, 2),或者 vm.$set
由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。
-
无法检测数组更改例如 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); }); }
-
不能在data里面增加项——初始时写入data,值为空
-
需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题。
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是一个对象