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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介:

从很久之前就已经接触过了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
本文来自云栖社区合作伙伴“ 前端大学”,了解相关信息可以关注“ 前端大学”。
相关文章
|
17天前
|
JavaScript 前端开发 算法
vue渲染页面的原理
vue渲染页面的原理
93 56
|
4天前
|
数据采集 前端开发 JavaScript
金融数据分析:解析JavaScript渲染的隐藏表格
本文详解了如何使用Python与Selenium结合代理IP技术,从金融网站(如东方财富网)抓取由JavaScript渲染的隐藏表格数据。内容涵盖环境搭建、代理配置、模拟用户行为、数据解析与分析等关键步骤。通过设置Cookie和User-Agent,突破反爬机制;借助Selenium等待页面渲染,精准定位动态数据。同时,提供了常见错误解决方案及延伸练习,帮助读者掌握金融数据采集的核心技能,为投资决策提供支持。注意规避动态加载、代理验证及元素定位等潜在陷阱,确保数据抓取高效稳定。
35 17
|
4天前
|
JavaScript 前端开发 UED
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
|
7天前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
7天前
|
存储 JavaScript 前端开发
全网最全情景,深入浅出解析JavaScript数组去重:数值与引用类型的全面攻略
如果是基础类型数组,优先选择 Set。 对于引用类型数组,根据需求选择 Map 或 JSON.stringify()。 其余情况根据实际需求进行混合调用,就能更好的实现数组去重。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
7天前
|
消息中间件 JavaScript 前端开发
最细最有条理解析:事件循环(消息循环)是什么?为什么JS需要异步
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。 本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。 但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗? 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您
|
1月前
|
移动开发 JavaScript API
Vue Router 核心原理
Vue Router 是 Vue.js 的官方路由管理器,用于实现单页面应用(SPA)的路由功能。其核心原理包括路由配置、监听浏览器事件和组件渲染等。通过定义路径与组件的映射关系,Vue Router 将用户访问的路径与对应的组件关联,支持哈希和历史模式监听 URL 变化,确保页面导航时正确渲染组件。
|
4月前
|
JavaScript 前端开发 开发者
Vue是如何劫持响应式对象的
Vue是如何劫持响应式对象的
78 18
|
4月前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
139 17
|
4月前
|
JavaScript 前端开发 API
介绍一下Vue中的响应式原理
介绍一下Vue中的响应式原理
74 1

推荐镜像

更多