vue系列 -- 响应式原理

276 阅读5分钟

我们都知道:vue 数据双向绑定是通过 数据劫持 结合 发布者-订阅者模式 的方式来实现的

MVC 和 MVVM 的区别

意义解析:

  • M 表示 Modal,模型
  • V 表示 View,视图
  • C 表示 Controller,控制器
  • VM 表示 ViewModal,是 MVVM 相对于 MVC 改进的核心思想

假设有个需求

在输入框输入times的值后,会在输入的times的值是:文本后实时显示输入结果,点击“确定”按钮会提交数据

image.png

MVC 和 MVVM 的处理方式

  1. MVC 的处理方式:
<html>
  <head></head>
  <body>
    <input id="inputAge" type="number" />
    <div id="times">输入的times值是:</div>
    <button id="submit">确定</button>
    <script>
      document.getElementById('confirm').oninput = function(){
        let times = document.getElementById('inputTimes').value;
        document.getElementById('times').innerHTML = "输入的times值是:" + times;
      };
      document.getElementById('submit').addEventListener('click',function(){
        alert('提交数据:' + { times: times });
      })
    </script>
  </body>
</html>

image.png

  1. MVVM 的处理方式:(采用 vue 框架)
<template>
  <input v-modal type="number" />
  <div>输入的times值是:{{times}}</div>
  <button @click="submit">确定</button>
</template>
<script>
  export default {
    data(){
      return {
        times: 1
      }
    },
    methods: {
      submit(){
        alert('提交数据:' + { times: this.times });
      }
    }
  }
</script>

image.png

图中的Model就是data方法返回的{times:1},View是最终在浏览器中显示的DOM,模型通过Observer,Dep,Watcher,Directive等一系列对象的关联,最终和视图建立起关系。总的来说,vue在些做了3件事:

  1. 通过Observerdata做监听,并提供了订阅某个数据项变化的能力。
  2. template编译成一段document fragment,然后解析其中的Directive,得到每一个Directive所依赖的数据项和update方法。
  3. 通过Watcher把上述2部分结合起来,即把Directive中的数据依赖通过Watcher订阅在对应数据的ObserverDep上,当数据变化时,就会触发ObserverDep上的notify方法通知对应的Watcherupdate,进而触发Directiveupdate方法来更新dom视图,最后达到模型和视图关联起来。

实例图解

image.png

Observer 是如何监听到属性的值的变化的?

vue 2.x:Object.defineProperty()

vue 2.x 是通过 Object.defineProperty() 来实现数据劫持的。

那么 Object.defineProperty() 是什么呢?它是 JavaScript 中 Object 的一个原生方法,可以用来控制一个对象属性的一些特有操作,比如读写权、是否可以枚举,这里我们主要先来研究下它对应的两个访问器属性 get 和 set,这块详见文章:JavaScript系列 -- Object 详解

在平常,我们很容易就可以打印出一个对象的属性数据:

var Book = {
  name: 'vue权威指南'
};
console.log(Book.name);  // vue权威指南

如果想要在执行 console.log(book.name) 的同时,如果想给书名加个书名号《》,那要怎么处理呢?或者说要通过什么监听对象 Book 的属性值。这时候 Object.defineProperty() 就派上用场了,代码如下:

var Book = {}
var name = '';
Object.defineProperty(Book, 'name', {
  set: function (value) {
    name = value;
    console.log('你取了一个书名叫做' + value); // 做一个反馈,表示【修改成功】
  },
  get: function () {
    return '《' + name + '》'
  }
})

console.log(Book.name);  // 《》
Book.name = 'vue权威指南';  // 你取了一个书名叫做vue权威指南
console.log(Book.name);  // 《vue权威指南》

我们通过Object.defineProperty()设置了对象 Book 的 name 属性,同时对其 get 和 set 进行重写操作,get 方法是在读取 name 属性这个值触发的函数,set 方法是在设置 name 属性这个值触发的函数,所以当执行 Book.name = 'vue权威指南' 这个语句时,控制台会打印出 "你取了一个书名叫做vue权威指南",紧接着,当读取这个属性时,就会输出 "《vue权威指南》",因为我们在get函数里面对该值做了加工了。如果这个时候我们执行下下面的语句,控制台会输出什么?

console.log(Book);

image.png

是不是跟我们在上面打印 vue 数据长得有点类似,说明 vue 2.x 确实是通过Object.defineProperty()方法来进行 数据劫持 的。

vue 2.x 实现数据双向绑定的两大缺陷

vue 2.x 的第一个缺陷,无法完整地监听数组变化。vue 2.x 只能监听数组的这八种变化方法:

push/pop/unshift/shift/splice/sort/reverse

有两种数组变化方法,vue 2.x 监听不到数组变化:

  • arr[indexofitem] = newValue
  • arr.length = newLength

vue 2.x 的第二个缺陷,只能劫持对象的属性, 因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要 深度遍历,如果对象新增属性也是要走一遍 Object.defineProperty(),显然能劫持一个完整的对象是更好的选择

vue 3.x:Proxy

ES6新方法,Proxy 可以理解成:在访问目标对象之前加一层“代理

Proxy() 语法

const proxy = new Proxy(target, handle)
  • target:要使用 Proxy 包装的目标对象
  • handle:用来定制拦截行为

handle 包含的方法

image.png

使用:给对象架起一层代理

const origin = {}
const obj = new Proxy(origin, {
  get: function (target, propKey, receiver) {
    return '10'
  }
});

obj.a // 10
obj.b // 10
origin.a // undefined
origin.b // undefined

代理只会对proxy代理对象生效,而上方的origin原生对象就没有任何效果

get 方法监听到属性变化

语法:get(target, propKey, ?receiver)

参数:

  • target:目标对象
  • propkey:属性名
  • receiver:Proxy 实例本身

先指定对象,再指定属性

const person = {
  like: "vuejs"
}

const obj = new Proxy(person, {
  get: function(target, propKey) {
    if (propKey in target) {
      return target[propKey];
    } else {
      throw new ReferenceError("Prop name "" + propKey + "" does not exist.");
    }
  }
})

obj.like // vuejs
obj.test // Uncaught ReferenceError: Prop name "test" does not exist.
  • 如果有值则直接返回
  • 没有值就抛出一个自定义的错误

vue 3.x 注意的点

  • Proxy 不兼容 IE,也没有 polyfill, defineProperty 能支持到 IE9
  • 使用 defineProperty 时,我们修改 原来的 obj 对象就可以触发拦截,而使用 proxy,就 必须修改代理对象,即 Proxy 的实例才可以触发拦截
  • Proxy 能观察的 类型 比 defineProperty 更丰富
  • Object.definedProperty 是劫持对象的属性,新增元素 需要再次 definedProperty。而 Proxy 劫持的是整个对象,不需要做特殊处理

参考文章