数据代理

MVVM

原生的Javascript代码Model和View没有分离,如果数据发生任意的改动, 接下来我们需要编写大篇幅的JS代码操作DOM元素更新视图

MVVM是目前前端开发领域当中倡导Model和View进行分离的开发思想或者架构模式,大部分主流框架如Vue和React都借鉴了这个MVVM思想

  • Model和View分离之后出现了一个核心VM负责更新,当Model发生改变之后VM自动去更新View,同理当View发生改动之后VM自动去更新Model

Vue框架中对应MVVM中的角色

  • M(Model): 对应data配置项中的数据

  • V(View): 对应容器中的模板语句, Vue实例的所有属性及Vue原型上的所有属性在模板语句中都可以直接使用

  • VM(ViewModel): 对应Vue的实例对象也是MVVM中核心部分

<div id="app">姓名:<input type="text" v-model="name"></div><script>// ViewModelvm表示Vue实例const vm = new Vue({el : '#app',// ModelMdata : {name : 'zhangsan'}})</script>

代理机制的原理

Vue实例可以访问的属性有三种: 以$或以_开始的属性用来和数据代理的属性区分开, Vue实例对象的原型对象的属性,Vue实例对象代理的目标对象data上的属性

  • 以$开始的属性: 可以看做是公开的属性,这些属性是供程序员使用的
  • 以_开始的属性: 可以看做是私有的属性,这些属性是Vue框架底层使用的, 程序员很少使用

对象新增属性的方法: Object.defineProperty(新增属性的对象, ‘新增的属性名’, {新增属性的相关配置项key:value})

  • 当配置项当中有setter和getter的时候,value和writable配置项存在会报错, enumerable和configurable可以存在
属性配置项作用
value设置新增属性的值
writable设置新增属性的值是否可以被修改, true表示可以修改 , 默认是false表示不能修改
enumerable设置新增属性是否可以遍历,true表示可以遍历的,默认是false表示不可遍历,Object.keys(对象)可以遍历对象的属性
configurable设置新增属性是否可以被删除,true表示可以被删除, ,默认是false表示不可删除,delete 对象.属性可以删除对象的属性
getter方法当读取新增属性值的时候,getter()方法被自动调用, 返回新增属性的值
setter方法当给新增属性赋值的时候,setter(val)方法被自动调用,val参数可以接收修改后的值
<script>// 这是一个普通的对象let phone = {}// 临时变量let temp// 给phone对象新增一个color属性并设置setter和getter方法Object.defineProperty(phone, 'color', {//value : '太空灰',//writable : true,enumerable : false,configurable : false// getter方法配置项get : function(){console.log('getter方法执行');return temp// 以下这种写法会造成死循环,一直读取新增属性值一直调用get方法//return this.color},// setter方法配置项set : function(val){console.log('setter方法执行',val);temp = val// 以下这种写法会造成死循环,一直给新增属性赋值一直调用set方法//this.color = val}// 在ES6对象中的函数/方法:function是可以省略的 get(){console.log('getter方法执行');return temp},// setter方法配置项set(val){console.log('setter方法执行',val);temp = val}})</script>

代理机制的实现

数据代理机制就是通过访问代理对象的属性来间接访问目标对象的属性

<script>// 目标对象let target = {name : 'zhangsan'}// 代理目标对象的name属性加给代理对象新增一个name属性(属性名要一致)let proxy = {}Object.defineProperty(proxy, 'name', {get(){return target.name},set(val){target.name = val}})</script>

在Vue框架中,代理对象是Vue的实例对象vm,目标对象是参数中的data对象,vm新增属性给data对象的属性做数据代理

  • Vue实例不会给以_和$开始的属性名做数据代理,即在Vue当中给data对象的属性名命名的时候不能以这两个符号开始(防止和Vue框架自身的属性名冲突)
<script>const vm = new Vue({el : '#app',data : {msg : 'Hello Vue!',// 以下这两个属性就是data对象的属性,vm不会做数据代理_name : 'zhangsan',$age : 20}})</script>

Vue框架数据代理的实现

// 定义一个Vue类class Vue {// 定义构造函数constructor(options){// options是一个简单的纯粹的JS对象{},有一个data配置项// 获取data对象的所有的属性名Object.keys(options.data).forEach((propertyName, index) => {//console.log(typeof propertyName, propertyName, index)// 如果是以_和$开始的属性名就不做数据代理let firstChar = propertyName.charAt(0)if(firstChar != '_' && firstChar != '$'){// this就是Vue的实例对象Object.defineProperty(this, propertyName, {get(){// propertyName是个字符串,通过对象["属性名"]的方式读取属性值return options.data[propertyName]},set(val){options.data[propertyName] = val}})}})}}

_data和$data

<script>function Vue(options){this._init(options);Vue.prototype._init = function (options){// 调用方法将options合并到$options,即$options含有options的所有属性// 程序执行到这里的时候vm上还没有_data属性var data = vm.$options.data;// 程序执行完这个代码之后,vm对象上多了一个_data这样的属性// 如果data是函数,则调用getData(data, vm)来获取data对象// 如果data不是函数,则直接将data对象返回给data变量, 并且同时将data对象赋值给vm._data属性data = vm._data = isFunction(data) " />getData(data, vm) : data || {};// 对于Vue实例vm来说,_data和$data都直接指向了底层真实的data对象,_data是私有的用于框架内部使用的, $data是公开的是给程序员使用// 如果我们程序员不想走代理的方式读取data(不走getter和setter方法),可以通过_data和$data属性直接读取data当中的数据// 判断字符串是否以_和$开始,true表示是, false表示否function isReserved(str) {var c = (str + '').charCodeAt(0);return c === 0x24 || c === 0x5f;}// 数据代理: 给vm新增属性,代理_data即data对象的所有属性while (i--) {var keys = Object.keys(data);// key是data对象的一个属性名var key = keys[i];// sharedPropertyDefinition是新增属性的配置项var sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop};proxy(vm, "_data", key)function proxy(target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter() {return this[sourceKey][key];};sharedPropertyDefinition.set = function proxySetter(val) {this[sourceKey][key] = val;};Object.defineProperty(vm, key, sharedPropertyDefinition);} } }</script>

数据响应式

当修改data配置项中的数据后,页面实现自动改变/刷新的响应式效果

数据劫持

Vue的响应式是通过数据劫持机制实现的: 底层使用了Object.defineProperty方法给新增的属性都配置了setter方法对数据进行劫持

  • 当给新增属性赋值时setter方法则被自动调用,setter方法的主要作用是修改属性值和重新渲染页面
  • Vue会给创建Vue实例时data中所有的属性(包括属性中的属性)都添加响应式

后期给Vue实例动态追加的属性默认没有添加响应式处理的

  • Vue.set/$set(目标对象,‘属性名’, 值)
  • 追加响应式属性时目标对象不能直接是vm或vm.$data,所以vm和data中的属性只能在声明时提前定义好

通过数组的下标去修改数组中的元素,默认情况下是没有添加响应式处理的(数组本身或者数组元素的内部属性都是有响应式处理的)

  • 第一种方案:vm.$set/set(数组对象, 下标, 值)
  • 第二种方案:push(),pop(),reverse(),splice(), shift(),unshift(),sort(),Vue对这些方法进行了重写增加了响应式处理的功能
<body><div id="app"><h1>{{msg}}</h1><div>姓名:{{name}}</div><div>年龄:{{age}}岁</div><div>数字:{{a.b.c.e}}</div><div>邮箱:{{a.email}}</div> <ul><li v-for="user in users">{{user}}</li></ul><ul><li v-for="vip in vips" :key="vip.id">{{vip.name}}</li></ul></div><script>const vm = new Vue({el : '#app',// Vue会给创建Vue实例时data中所有的属性(包括属性中的属性)都添加响应式data : {msg : '响应式与数据劫持',name : 'jackson',age : 20,a : {b : {c : {e : 1}}}users : ['jack', 'lucy', 'james'],vips : [{id:'111', name:'zhangsan'},{id:'222', name:'lisi'}]}})// vm后期追加的属性并没有添加响应式处理//vm.$data.a.email = 'jack@126.com'// 调用以下的两个方法给后期追加的属性添加响应式处理//Vue.set(vm.a, 'email', 'jack@123.com')vm.$set(vm.a, 'email', 'jack@456.com')// 不能直接给vm/vm.$data追加响应式属性,只能在声明时提前定义好//Vue.set(vm, 'x', '1')//Vue.set(vm.$data, 'x', '1')// 直接通过数组下标修改数组中的没有响应式效果vm.users[0] = "李四"vm.vips[0] = {id:'333',name:'wangwu'}// 数组中元素的属性有响应式效果vm.vips[0].name = "张三"// 操作数组元素并具有响应式效果Vue.$set(vm.users,0,"李四")Vue.$set(vm.vips,2,{id:'333',name:'wangwu'})vm.users.push('王五')// 修改数组从0位置开始的一个元素vm.users.splice(0,1'杰克')</script></body>