Vue响应式原理初探——数据响应式

183 阅读6分钟

Vue数据响应式是一个比较概念的含义,感觉很高科技的样子,可以自动响应呀!!

在工程领域中,感觉没有任何一个概念是先出来的,其背后一定有什么逻辑或待改善的问题!!!所以对于任何一个知识,都应该先问为什么,再看是什么,怎么做。

所以针对这个问题,需要细化成一下几个小问题:

  • 为什么要有数据响应式?
  • 什么是数据响应式?
  • 数据响应式的主体或者针对的对象是哪些?
  • 通过什么方法实现数据响应的
  • 数据响应式具体是做什么的

1. 数据响应式的由来

web前端的发展和人类的发展一样,经历过从原始社会、农业社会、工业社会和知识社会[1]等多个社会的演化。
原始社会:静态网页,这个时代就是我们学习web技术必经的一个阶段,就是静态页面;网页中的数据不是从数据库和后端程序中获取的,基本上是一个个单独的文件。
农业社会:动态网页,这个时代的代表有ASP、JSP等,这时候是后端人员兼顾网页的开发的;其中最核心的就是模板引擎,这导致每一次数据的变化都需要后端重新渲染后返回一个新的html网页文件,不仅增加了后端服务器的压力,还不太灵活。
工业社会:JQuery和Ajax以及SPA的出现,这个时代将数据变化直接在浏览器中进行改变,而不需要后端返回一个新的网页文件了;这个阶段涌现了一大批的前端框架,比如AngularJS、EmberJS、Backbone等,大大节约了开发人员大量的精力、提升了开发效率和迭代速度。
知识社会:百花齐放的时代,这是一个很夸张的时代,你会发现前端生态的选择有点难得过分,这个时代更加注重选择避免迷失
32a6f430-3ac6-11eb-85f6-6fac77c0c9b3.png
其实说这么多,数据响应式是出现在知识社会的产物,它是对工业社会的优化,其优化的重点就是将操作网页页面元素的过程隐藏起来,将这个时代最大的主角“数据”推到了舞台中央:

  • 数据的改变直接可以导致网页视图的更新
  • 网页视图的更新同样会更新数据

直接将数据和视图关联起来。

2. 数据响应式是什么

这是一个数据为王的时代,掌握数据,就是掌握未来!!!
JavaScript中的数据(数据模型)仅仅是普通的JavaScript对象, 而当我们修改数据时,数据响应式要求视图会进行更新,避免繁琐的DOM操作,提高开发效率。
进一步的针对表单元素,需要实现以下的要求:

  • 数据改变,视图改变;(数据响应)
  • 视图改变,数据随之改变;

即提出了数据响应式的进阶版——双向绑定。比如vue中的v-model指令!!

数据驱动是Vue最独特的特性之一,它使得开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图上的!!

3. 数据响应式主体分析

根据数据响应式的概念分析,数据响应式涉及数据模型和DOM视图

  • 数据模型改变后通知DOM视图更新--> dom操作
  • DOM表单操作后通知数据模型改变 --> 事件系统

4. 数据响应式实现方式

数据响应式实现的核心是重写setter方法,再其中进行通知dom视图更新。

4.1 Object.defineProperty()

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

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

  • 不支持 IE8 以及更低版本浏览器
let data = {
	msg:"hello",
};
Object.defineProperty(data, "msg", {
	get(){
		// 获取值进行的额外操作
		return data.msg;
	},
	set(newValue){
		if(newValue === msg.data) return;
		data.msg = newValue;
		// 进行通知更新DOM视图,进行dom操作
	}
})
  • 如果对象中有多个属性,该如何处理;如果对象中嵌套了对象, 又该如何处理——(循环递归处理

4.2 Proxy

在 Vue 3 中则使用了 Proxy 来创建响应式对象,仅将 getter/setter 用于 ref

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。 const p = new Proxy(target, handler)

  • 直接监听对象, 而不是属性——无需循环递归处理, 提高了性能
  • ES6中新增,IE浏览器不支持,性能由浏览器优化
let data = {
	msg:"hello",
};
let vm = new Proxy(data, {
	get(target, key){
		// 获取值进行的额外操作
		return target[key];
	},
	set(target, key, newValue){
		if(target[key] === newValue) return;
		target[key] = newValue;
		// 进行通知更新DOM视图,进行dom操作
	}
});

5. 数据响应式案例分析

5.1 vue2实现原理

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>vue2响应式原理入门版</title>
  </head>
  <body>
    <div id="app"></div>

    <script>
      // 模拟Vue的实例
      let vm = {};

      function notify() {
        document.querySelector("#app").textContent = vm.msg;
      }

      function defineReactive(key) {
        // 进行数据劫持,当访问或设置vm中的成员的时候,做一些干预操作  vue内部完成
        Object.defineProperty(vm, key, {
          // 可枚举(可以被遍历到)
          enumerable: true,
          // 可配置(可以使用delete删除,可以通过defineProperty重新定义)
          configurable: true,
          // 当获取值时执行该函数
          get() {
            console.log("当前是获取msg值的操作:", data[key]);
            return data[key];
          },
          // 当设置值时执行该函数
          set(newValue) {
            if (newValue === data[key]) return;
            console.log("当前是设置msg值的操作:", newValue);
            data[key] = newValue;
            // 此时数据改变了, 需要通知页面dom变化
            notify();
          },
        });
      }

      function observe(prop) {
        defineReactive(prop);
      }
    </script>

    <script>
      // data数据用来模拟vue中的data选项
      let data = {
        msg: "hello",
        age: 12,
      };

      // 劫持数据
      observe("msg");

      // 挂载数据
      (function mounted() {
        notify(data);
      })();

      // 改变数据,测试响应式
      setTimeout(() => {
        vm.msg = "hello world";
      }, 1000);
      setTimeout(() => {
        vm.age = 22;
      }, 2000);
    </script>
  </body>
</html>

5.2 vue3实现原理

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>vue3响应式原理入门版</title>
  </head>
  <body>
    <div id="app"></div>

    <script>
      function observe() {
        // 模拟Vue的实例
        let vm = new Proxy(data, {
          // 进行数据劫持,当访问或设置vm中的成员的时候,做一些干预操作  vue内部完成
          // 当获取值时执行该函数
          get(target, key) {
            console.log("get, key: ", key, "==>", target[key]);
            return target[key];
          },
          // 当设置值时执行该函数
          set(target, key, newValue) {
            if (newValue === target[key]) return;
            console.log("set, key, newValue:", key, "==>", newValue);
            target[key] = newValue;
            // 此时数据改变了, 需要通知页面dom变化
            notify();
          },
        });
        return vm;
      }

      function notify() {
        document.querySelector("#app").textContent = vm.msg;
      }
    </script>

    <script>
      // data数据用来模拟vue中的data选项
      let data = {
        msg: "hello",
      };

      // 劫持数据
      let vm = observe(data);

      // 挂载数据
      (function mounted() {
        notify(data);
      })();

      // 改变数据,测试响应式
      setTimeout(() => {
        vm.msg = "hello world";
      }, 1000);
    </script>
  </body>
</html>

5.3 vue2对象多属性及对象嵌套处理

// 模拟Vue的实例
let vm = {};

function notify() {
	document.querySelector("#app").textContent = vm.msg;
}

function defineReactive(key) {
	observe(data[key]);
	// 进行数据劫持,当访问或设置vm中的成员的时候,做一些干预操作  vue内部完成
	Object.defineProperty(vm, key, {
		// 可枚举(可以被遍历到)
		enumerable: true,
		// 可配置(可以使用delete删除,可以通过defineProperty重新定义)
		configurable: true,
		// 当获取值时执行该函数
		get() {
			console.log("当前是获取msg值的操作:", data[key]);
			return data[key];
		},
		// 当设置值时执行该函数
		set(newValue) {
			if (newValue === data[key]) return;
			console.log("当前是设置msg值的操作:", newValue);
			data[key] = newValue;
			// 此时数据改变了, 需要通知页面dom变化
			notify();
		},
	});
}

function observe(data) {
	if (data != null && typeof data === "object") {
		Object.keys(data).forEach((key) => {
			defineReactive(key);
		});
	}
}

6. 总结

通过对vue2.x和vue3.x响应式原理的分析和初步认识,了解数据响应式的相关概念和前端发展历程,同时对JavaScript对象会有更加深入的理解,为后续阅读vue源码做准备,完成对响应式原理最基础的认识;但还有一个问题重要问题需要处理:数据更新,哪些视图需要更新;透露下这实际上与Vue中第一个设计模式观察者模式有关了

参考:

[1] 如何成为一个现代化国家:中国现代化报告概要(2001-2016)
[2] vue2.x官方文档
[3] vue3.x官方文档
[4] MDN文档——前端程序猿必备网站