vue.js响应式原理解析与实现

简介:

从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页面更新渲染。之后,再接触了vue.js,当时也一度很好奇vue.js是如何监测数据更新并且重新渲染页面。今天,就我们就来一步步解析vue.js响应式的原理,并且来实现一个简单的demo。

首先,先让我们来了解一些基础知识。

基础知识

Object.defineProperty

es5新增了Object.defineProperty这个api,它可以允许我们为对象的属性来设定getter和setter,从而我们可以劫持用户对对象属性的取值和赋值。比如以下代码:

 
  1. const obj = {

  2. };

  3. let val = 'cjg';

  4. Object.defineProperty(obj, 'name', {

  5. get() {

  6. console.log('劫持了你的取值操作啦');

  7. return val;

  8. },

  9. set(newVal) {

  10. console.log('劫持了你的赋值操作啦');

  11. val = newVal;

  12. }

  13. });

  14. console.log(obj.name);

  15. obj.name = 'cwc';

  16. console.log(obj.name);

我们通过Object.defineProperty劫持了obj[name]的取值和赋值操作,因此我们就可以在这里做一些手脚啦,比如说,我们可以在obj[name]被赋值的时候触发更新页面操作。

发布订阅模式

发布订阅模式是设计模式中比较常见的一种,其中有两个角色:发布者和订阅者。多个订阅者可以向同一发布者订阅一个事件,当事件发生的时候,发布者通知所有订阅该事件的订阅者。我们来看一个例子了解下。

 
  1. class Dep {

  2. constructor() {

  3. this.subs = [];

  4. }

  5. // 增加订阅者

  6. addSub(sub) {

  7. if (this.subs.indexOf(sub) < 0) {

  8. this.subs.push(sub);

  9. }

  10. }

  11. // 通知订阅者

  12. notify() {

  13. this.subs.forEach((sub) => {

  14. sub.update();

  15. })

  16. }

  17. }

  18. const dep = new Dep();

  19. const sub = {

  20. update() {

  21. console.log('sub1 update')

  22. }

  23. }

  24. const sub1 = {

  25. update() {

  26. console.log('sub2 update');

  27. }

  28. }

  29. dep.addSub(sub);

  30. dep.addSub(sub1);

  31. dep.notify(); // 通知订阅者事件发生,触发他们的更新函数

动手实践

我们了解了Object.defineProperty和发布订阅者模式后,我们不难可以想到,vue.js是基于以上两者来实现数据监听的。

vue.js首先通过Object.defineProperty来对要监听的数据进行getter和setter劫持,当数据的属性被赋值/取值的时候,vue.js就可以察觉到并做相应的处理。
通过订阅发布模式,我们可以为对象的每个属性都创建一个发布者,当有其他订阅者依赖于这个属性的时候,则将订阅者加入到发布者的队列中。利用Object.defineProperty的数据劫持,在属性的setter调用的时候,该属性的发布者通知所有订阅者更新内容。

接下来,我们来动手实现(详情可以看注释):

 
  1. class Observer {

  2. constructor(data) {

  3. // 如果不是对象,则返回

  4. if (!data || typeof data !== 'object') {

  5. return;

  6. }

  7. this.data = data;

  8. this.walk();

  9. }

  10. // 对传入的数据进行数据劫持

  11. walk() {

  12. for (let key in this.data) {

  13. this.defineReactive(this.data, key, this.data[key]);

  14. }

  15. }

  16. // 创建当前属性的一个发布实例,使用Object.defineProperty来对当前属性进行数据劫持。

  17. defineReactive(obj, key, val) {

  18. // 创建当前属性的发布者

  19. const dep = new Dep();

  20. /*

  21. * 递归对子属性的值进行数据劫持,比如说对以下数据

  22. * let data = {

  23. * name: 'cjg',

  24. * obj: {

  25. * name: 'zht',

  26. * age: 22,

  27. * obj: {

  28. * name: 'cjg',

  29. * age: 22,

  30. * }

  31. * },

  32. * };

  33. * 我们先对data最外层的name和obj进行数据劫持,之后再对obj对象的子属性obj.name,obj.age, obj.obj进行数据劫持,层层递归下去,直到所有的数据都完成了数据劫持工作。

  34. */

  35. new Observer(val);

  36. Object.defineProperty(obj, key, {

  37. get() {

  38. // 若当前有对该属性的依赖项,则将其加入到发布者的订阅者队列里

  39. if (Dep.target) {

  40. dep.addSub(Dep.target);

  41. }

  42. return val;

  43. },

  44. set(newVal) {

  45. if (val === newVal) {

  46. return;

  47. }

  48. val = newVal;

  49. new Observer(newVal);

  50. dep.notify();

  51. }

  52. })

  53. }

  54. }

  55. // 发布者,将依赖该属性的watcher都加入subs数组,当该属性改变的时候,则调用所有依赖该属性的watcher的更新函数,触发更新。

  56. class Dep {

  57. constructor() {

  58. this.subs = [];

  59. }

  60. addSub(sub) {

  61. if (this.subs.indexOf(sub) < 0) {

  62. this.subs.push(sub);

  63. }

  64. }

  65. notify() {

  66. this.subs.forEach((sub) => {

  67. sub.update();

  68. })

  69. }

  70. }

  71. Dep.target = null;

  72. // 观察者

  73. class Watcher {

  74. /**

  75. *Creates an instance of Watcher.

  76. * @param {*} vm

  77. * @param {*} keys

  78. * @param {*} updateCb

  79. * @memberof Watcher

  80. */

  81. constructor(vm, keys, updateCb) {

  82. this.vm = vm;

  83. this.keys = keys;

  84. this.updateCb = updateCb;

  85. this.value = null;

  86. this.get();

  87. }

  88. // 根据vm和keys获取到最新的观察值

  89. get() {

  90. Dep.target = this;

  91. const keys = this.keys.split('.');

  92. let value = this.vm;

  93. keys.forEach(_key => {

  94. value = value[_key];

  95. });

  96. this.value = value;

  97. Dep.target = null;

  98. return this.value;

  99. }

  100. update() {

  101. const oldValue = this.value;

  102. const newValue = this.get();

  103. if (oldValue !== newValue) {

  104. this.updateCb(oldValue, newValue);

  105. }

  106. }

  107. }

  108. let data = {

  109. name: 'cjg',

  110. obj: {

  111. name: 'zht',

  112. },

  113. };

  114. new Observer(data);

  115. // 监听data对象的name属性,当data.name发现变化的时候,触发cb函数

  116. new Watcher(data, 'name', (oldValue, newValue) => {

  117. console.log(oldValue, newValue);

  118. })

  119. data.name = 'zht';

  120. // 监听data对象的obj.name属性,当data.obj.name发现变化的时候,触发cb函数

  121. new Watcher(data, 'obj.name', (oldValue, newValue) => {

  122. console.log(oldValue, newValue);

  123. })

  124. data.obj.name = 'cwc';

  125. data.obj.name = 'dmh';

结语

这样,一个简单的响应式数据监听就完成了。当然,这个也只是一个简单的demo,来说明vue.js响应式的原理,真实的vue.js源码会更加复杂,因为加了很多其他逻辑。

接下来我可能会将其与html联系起来,实现v-model、computed和{{}}语法。代码地址 有兴趣的欢迎来一起研究探讨下。点击这里查看第二节的内容。如果觉得有收获的话也请点个赞,嘿嘿。


原文发布时间为:2018-08-30
本文作者:陈陈jg
本文来自云栖社区合作伙伴“ 前端大学”,了解相关信息可以关注“ 前端大学”。
相关文章
|
6月前
|
JavaScript 前端开发 IDE
TypeScript vs. JavaScript:技术对比与核心差异解析
TypeScript 作为 JavaScript 的超集,通过静态类型系统、编译时错误检测和强大的工具链支持,显著提升代码质量与可维护性,尤其适用于中大型项目和团队协作。相较之下,JavaScript 更灵活,适合快速原型开发。本文从类型系统、错误检测、工具支持等多维度对比两者差异,并提供技术选型建议,助力开发者合理选择。
1158 1
|
6月前
|
JavaScript 前端开发 开发者
Nest.js控制器深度解析:路由与请求处理的高级特性
以上就是对 NestJS 控制层高级特性深度解析:从基本概念到异步支持再到更复杂场景下拦截其与管道等功能性组件运用都有所涉及,希望能够帮助开发者更好地理解和运用 NestJS 进行高效开发工作。
419 15
|
6月前
|
存储 JavaScript 前端开发
JavaScript 语法全面解析
JavaScript 语法体系丰富且不断更新,从基础的变量声明、数据类型,到复杂的函数、对象、异步语法,每个知识点都需要开发者深入理解并灵活运用。本文梳理的 JS 语法核心内容,可为开发者提供系统的学习框架,后续还需通过大量实践(如编写交互组件、实现业务逻辑)巩固知识,逐步提升 JS 编程能力,应对前端开发中的各类挑战。
|
9月前
|
机器学习/深度学习 JavaScript 前端开发
JS进阶教程:递归函数原理与篇例解析
通过对这些代码示例的学习,我们已经了解了递归的原理以及递归在JS中的应用方法。递归虽然有着理论升华,但弄清它的核心思想并不难。举个随手可见的例子,火影鸣人做的影分身,你看到的都是同一个鸣人,但他们的行为却能在全局产生影响,这不就是递归吗?雾里看花,透过其间你或许已经深入了递归的魅力之中。
354 19
|
10月前
|
JSON 前端开发 Serverless
Mock.js 语法结构全解析
Mock.js 的语法规范介绍,从数据模板定义规范和数据占位符定义规范俩部分介绍, 让你更好的使用 Mock.js 来模拟数据并提高开发效率。
|
12月前
|
数据采集 前端开发 JavaScript
金融数据分析:解析JavaScript渲染的隐藏表格
本文详解了如何使用Python与Selenium结合代理IP技术,从金融网站(如东方财富网)抓取由JavaScript渲染的隐藏表格数据。内容涵盖环境搭建、代理配置、模拟用户行为、数据解析与分析等关键步骤。通过设置Cookie和User-Agent,突破反爬机制;借助Selenium等待页面渲染,精准定位动态数据。同时,提供了常见错误解决方案及延伸练习,帮助读者掌握金融数据采集的核心技能,为投资决策提供支持。注意规避动态加载、代理验证及元素定位等潜在陷阱,确保数据抓取高效稳定。
378 17
|
12月前
|
JavaScript 前端开发 UED
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
797 2
|
12月前
|
监控 JavaScript 前端开发
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例
MutationObserver 是一个非常强大的 API,提供了一种高效、灵活的方式来监听和响应 DOM 变化。它解决了传统 DOM 事件监听器的诸多局限性,通过异步、批量的方式处理 DOM 变化,大大提高了性能和效率。在实际开发中,合理使用 MutationObserver 可以帮助我们更好地控制 DOM 操作,提高代码的健壮性和可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例
|
12月前
|
消息中间件 JavaScript 前端开发
最细最有条理解析:事件循环(消息循环)是什么?为什么JS需要异步
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。 本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。 但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗? 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您
|
12月前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
1127 29

推荐镜像

更多
  • DNS