MVVM 原理 和 JavaScript数据劫持——Object.defineProperty()方法和Proxy-对象级别

MVVM 原理

常见的面试问题:

  • Vue 数据绑定的原理?
  • MVVM 数据绑定的原理?
  • Vue 双向数据绑定的原理?
  • Vue 数据响应式原理?
  • 数据响应式原理?

在这里插入图片描述

当前比较流行的前端框架都是采用的 MVVM 的方式:

什么是 MVVM?

简单一句话:数据驱动视图

介绍

感受 MVVM

  • 传统的 DOM 操作方式
  • 模板引擎方式
  • 数据驱动视图方式(MVVM)

什么是 MVVM

简单一句话:数据驱动视图

在这里插入图片描述

<!-- 视图 -->
<template>
  <div>{{ message }}</div>
</template>

<!-- ViewModel -->
把普通的 JavaScript 对象和视图 DOM 之间建立了一种映射关系:
- 数据的改变影响视图
- 视图(表单元素)的改变影响数据

<script>
// Model 普通数据对象
export default {
  data () {
    return {
      message: 'Hello World'
    }
  }
}
</script>

<style>

</style>

  • Model(M):普通的 JavaScript 对象,例如 Vue 实例中的 data
    • 普通数据
  • View(V):视图
    • HTML DOM 模板
  • ViewModel(VM):Vue实例
    • 负责数据和视图的更新
    • 它是 Model数据 和 View 视图通信的一个桥梁

逻辑:

1.把数据绑定到视图,vm解析插值表达式和指令,找到模型中的数据,把数据在视图中呈现出来

2.视图发生变化,在DOM中注册input等事件进行触发事件,更改model中的数据

JavaScript 数据劫持

  • 数据劫持?
  • Observer 数据观察
  • 数据拦截器

如何实现修改一个对象成员就修改了DOM?

const data = {
	message: 'Hello World'
}

// 监视 data.message 的改变
// watch('data.message', () => {
//  dom.xxx = xxx
// })

data.message = 'hello'

// data.message = xxx 不仅仅对数据进行了修改,还操作了 DOM

// 

答案是:JavaScript 数据劫持,或者说是 JavaScript 对象属性拦截器。

什么是数据劫持(属性拦截器)

说白了就是:观察数据的变化。

  • Object.defineProperty
    • ECMAScript 5 中的一个 API
    • Vue 1 和 Vue 2 中使用的都是 Object.defineProperty
  • Proxy
    • ECMAScript 6 中的一个 API
    • 即将升级的 Vue 3 会升级使用 Proxy
    • Proxy 比 Object.defineProperty 性能要更好

深入响应式原理https://cn.vuejs.org/v2/guide/reactivity.html

如何追踪变化:

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setterObject.defineProperty 是 ES5 中一个无法 shim (ES5之前的语法无法解析)的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

在这里插入图片描述

检测变化的注意事项

Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如:

var vm = new Vue({
  data:{
    a:1
  }
})

// `vm.a` 是响应式的

vm.b = 2
// `vm.b` 是非响应式的

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。例如,对于:

Vue.set(vm.someObject, 'b', 2)

您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

this.$set(this.someObject,'b',2)

Object.defineProperty

参考资料:

**Object.defineProperty()** 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

语法
Object.defineProperty(obj, prop, descriptor)

参数:

  • obj 要在其上定义属性的对象。

  • prop 要定义或修改的属性的名称。

  • descriptor 将被定义或修改的属性描述符。

返回值:

被传递给函数的对象。

属性描述符

对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。

数据描述符和存取描述符均具有以下可选键值(默认值是在使用Object.defineProperty()定义属性的情况下):

  • configurable

    当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false

  • enumerable

    当且仅当该属性的enumerabletrue时,该属性才能够出现在对象的枚举属性中。默认为 false

数据描述符同时具有以下可选键值

  • value

    该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined

  • writable

    当且仅当该属性的writabletrue时,value才能被赋值运算符改变。默认为 false

存取描述符同时具有以下可选键值

  • get

    一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。

    默认为 undefined

  • set

    一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。

    默认为 undefined

在这里插入图片描述

打印台显示:属性描述符赋值

在这里插入图片描述

赋值方法对比

在这里插入图片描述

属性的访问器 获取值get 和 修改值set

在这里插入图片描述

get set 和 value可以同时使用

在这里插入图片描述

get set 和 value可以同时使用 打印显示

在这里插入图片描述

1. enumerable 可枚举方法,同get获取数据 set修改数据

在这里插入图片描述

显示结果为:name

enumerable:false时,不可打印,页面不可打印,不显示内容

2. configurable 是否可以配置

在这里插入图片描述

打开页面,控制台打印,仍可以看到 name删除操作无效,但不报错,是静默错误,需要ES5语法开启严格模式优化JS

在这里插入图片描述

放在script最首行,开启严格模式,此时删除属性name会显示报错

在这里插入图片描述

configurable:true时,删除有效,name不显示

严格模式

JavaScript 的严格模式是使用受限制的 JavaScript 的一种方式,从而隐式地退出“草率模式”。严格模式不仅仅是一个子集:这种模式有意地与普通情形下的代码有所区别。

通过在脚本文件/函数开头添加 "use strict"; 声明,即可启用严格模式。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode

严格模式对正常的 JavaScript语义做了一些更改:

  1. 严格模式通过抛出错误来消除了一些原有静默错误
  2. 严格模式修复了一些导致 JavaScript引擎难以执行优化的缺陷:有时候,相同的代码,严格模式可以比非严格模式下运行得更快
  3. 严格模式禁用了在ECMAScript的未来版本中可能会定义的一些语法。

3.writable 该属性是否是可写-只读状态

在这里插入图片描述

writable:false且 去掉严格模式 ,在打印台输入 vm.name ,显示为: xxx ,不是hello,赋值失败;

说明此时是不可写的,writable:false只读状态

开启严格模式'use strict',就会报错

描述符全实例代码:
<body>
  <script>
    // 开启严格模式(es5中新增),需要在当前作用域的最上面
    'use strict'

    function fn () {
      // 严格模式下 全局函数调用,this 指向 undefined
      console.log(this)
    }

    fn()


    // Array.prototype = {}  // 只读
    

    var obj = {
      name: 'zs'
    }
    
    var vm = {}


    Object.defineProperty(vm, 'name', {
      // 是否可以配置(是否可以删除,是否可以重新被defineProperty)
      configurable: false,
      // 可枚举(遍历)
      enumerable: true,
      // 该属性是否是可写
      // 不能和get/set一起使用  (只读的)
      writable: false,
      value: 'xxx'
      // get () {
      //   console.log('get')
      //   return obj.name
      // },
      // set (value) {
      //   console.log('set')
      //   obj.name = value
      // }
    })

    vm.name = 'hello'

    // delete vm.name

    // for (var key in vm) {
    //   console.log(key)
    // }


  </script>
</body>

Object.keys()方法 遍历对象中的所有属性(遍历数组中的所有参数)

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/keys

Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for...in 循环遍历该对象时返回的顺序一致 。

实例1:

在这里插入图片描述

显示结果:

在这里插入图片描述

实例2:

在这里插入图片描述

显示结果:

在这里插入图片描述

实现对一个对象所有成员的代理——Object.defineProperty()方法中的描述符功能展示

需求:

const data = {
  foo: 'bar',
  user: {
    name: '张三',
    age: 18
  }
}

// data.foo 被访问了
data.foo

// data.foo 被改变了
data.foo = xxx

// data.user 被改变了
data.user = xxx

// data.user.name 被访问了
data.user.name

实现:

// 普通数据源
var data = {
  name: 'kindeng',
  user: {
    age: 18,
    foo: 'bar'
  },
  count: 0
};

// 对 data 中所有数据成员进行数据劫持(观察)
observe(data);

// data.name = 'dmq'; // 哈哈哈,监听到值变化了 kindeng --> dmq
// data.user.name = 'zs'

// console.log(data.user.name)

function observe(data) {
  // 如果 data 数据无效或者 data 不是一个对象,就停止处理
  if (!data || typeof data !== 'object') {
    return;
  }

  // 取出所有属性遍历,对属性成员进行代理(拦截、观察)操作
  Object.keys(data).forEach(function (key) {
    defineReactive(data, key, data[key]);
  });
};

/**
 * data 是数据对象
 * key 是属性名
 * val 当前属性名对应的值
 */
function defineReactive(data, key, val) {
  // observe('kindeng');
  // observe(data.user);
  observe(val); // 监听子属性

  // 'name'
  // 'age'
  // 'foo'
  // 'user'
  // 'count'
  console.log(key)

  Object.defineProperty(data, key, {
    enumerable: true, // 可枚举
    configurable: false, // 不能再define
    get: function () {
      return val;
    },
    set: function (newVal) {
      console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
      val = newVal;
    }
  });
}

// function fn (a) {
//   return {
//     getA () {
//       return a
//     },
//     setA () {
//       a++
//     }
//   }
// }

// const { getA, setA } = fn(100)

// console.log(getA())
// setA()
// setA()
// setA()
// console.log(getA())

Proxy-对象级别

Object.defineProperty()方法-属性级别

参考资料:

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

语法
let p = new Proxy(target, handler);

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中:

  • new Proxy()表示生成一个Proxy实例
  • target参数表示所要拦截的目标对象
    • 可以是任意类型的对象,包括原生数组,函数,甚至是另一个 Proxy
  • handler参数也是一个对象,用来定制拦截行为。
    • 其属性是当执行一个操作时定义代理的行为的函数

实例Proxy.html

<body>
  <script>
    var obj = {
      name: 'zs',
      age: 18
    }

//obj-目标对象 , { }-给对象配置的选项
    var p = new Proxy(obj, {
      get (target, key) {  //target-目标对象 , key-obj目标对象中的某个属性
        console.log(key)
          //属性中有name 和 age ,没有变量key, key里是属性数组成员
        return target[key]
      },
        //target-目标对象 , key-obj目标对象中的某个属性 ,value-设置的新的值
      set (target, key, value) {
        console.log(key, value)
        target[key] = value
      }
    })
 
  </script>
</body>

控制台操作打印-Proxy方法操作值

在这里插入图片描述

注:

Object.definePropertyProxy,都可以实现描述符get(获取数据)和set(修改数据),区别是:

  1. Object.defineProperty()方法-属性级别,给属性设值;

Proxy-对象级别,给对象设值;给对象设值更方便,是ES6新增属性

  1. 给对象的多个属性设值,需要循环遍历各个属性值,需要分别调用defineProperty()方法,实现起来麻烦,性能也比Proxy差

  2. 如果有一个定义的实例对象-p代理了数据操作,后期操作目标对象-obj 通过 代理对象 设 属性值 即可

示例

const data = {}

var proxy = new Proxy(data, {
  get: function(target, property) {
    return 35;
    // return property in target ? target[property] : 37;
  }
});

注意:要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是data)进行操作。

示例

如果handler没有设置任何拦截,那就等同于直接通向原对象。

let target = {};
let p = new Proxy(target, {});

p.a = 37;   // 操作转发到目标

console.log(target.a);    // 37. 操作已经被正确地转发

上面代码中,handler是一个空对象,没有任何拦截效果,访问proxy就等同于访问target

set的用途-实现响应式,还有给目标对象 设置值的时候,对值进行合法性的校验

示例1:

<body>
  <script>
    var obj = {
      name: 'zs',
      age: 18
    }


    var p = new Proxy(obj, {
      get (target, key) {
        console.log(key)
        return target[key]
      },
      set (target, key, value) {
        console.log(key, value)
        if (key === 'age') {
          if ( value >= 0 && value <= 200 ) {
          } else {
              //用throw new Error()方法,这样可以停止js执行,但浏览器会显示有错误
            throw new Error('age的值不合法')
          }
        }
        target[key] = value
      }
    })

  
  </script>
</body>

页面显示为:

在这里插入图片描述

示例2:

通过代理,你可以轻松地验证向一个对象的传值。这个例子使用了 set

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // The default behavior to store the value
    obj[prop] = value;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

console.log(person.age); 
// 100

person.age = 'young'; 
// 抛出异常: Uncaught TypeError: The age is not an integer

person.age = 300; 
// 抛出异常: Uncaught RangeError: The age seems invalid
差值表达式的处理

用正则表达式,匹配差值表达式的模式,实现值的修改

<body>
    <script>
        // 实现——用  name的值'zs' ,替换 Name 中的 插值表达式(其内可能存在空格)
        // 方法——用正则表达式,匹配差值表达式的模式
        var data = {
            name: 'zs'
        }

        var value = 'Name: {{ name }}'

        // 正则表达式,匹配差值表达式的模式
        // 正则表达式中的() 分组
        // . 是任意单个字符 , + 是量词,修饰前面的任意字符
        var reg = /\{\{(.+)\}\}/
            // console.log(RegExp.$1) ——>没调test,控制台打印显示 为 空

        if (reg.test(value)) {
            // 1.确保匹配到差值表达式
            // console.log('....') ——>控制台打印显示  ....  ,证明匹配正则成功
            // 2.获取差值表达式中的内容name

            // 当正则匹配后,获取分组匹配的结果  $1 就是匹配第一组(第一个小括号获取的结果)
            // console.log(RegExp.$1)  ——> 控制台打印显示 name ,但name前后有空格
            // .trim() 去空格
            var key = RegExp.$1.trim()

            // replace() 替换
            //释义:用data中的key属性,可以获取到 值,然后 用获取到的值 把正则表达式 匹配到的结果插值表达式{{ name }}  给替换掉
            value = value.replace(reg, data[key])
            console.log(value) // 控制台打印显示 Name:zs
        }
    </script>
</body>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值