探索Vue.js响应式原理

 提到“响应式”三个字,大家立刻想到啥?响应式布局?响应式编程?

创新互联是一家集网站建设,长兴企业网站建设,长兴品牌网站建设,网站定制,长兴网站建设报价,网络营销,网络优化,长兴网站推广为一体的创新建站企业,帮助传统企业提升企业形象加强企业竞争力。可充分满足这一群体相比中小企业更为丰富、高端、多元的互联网需求。同时我们时刻保持专业、时尚、前沿,时刻以成就客户成长自我,坚持不断学习、思考、沉淀、净化自己,让我们为更多的企业打造出实用型网站。

从字面意思可以看出,具有“响应式”特征的事物会根据条件变化,使得目标自动作出对应变化。比如在“响应式布局”中,页面根据不同设备尺寸自动显示不同样式。

Vue.js 中的响应式也是一样,当数据发生变化后,使用到该数据的视图也会相应进行自动更新。

接下来我根据个人理解,和大家一起探索下 Vue.js 中的响应式原理,如有错误,欢迎指点~~

一、Vue.js 响应式的使用

现在有个很简单的需求,点击页面中 “leo” 文本后,文本内容修改为“你好,前端自习课”。

我们可以直接操作 DOM,来完成这个需求:

 
 
 
 
  1. leo  
 
 
 
 
  1. const node = document.querySelector('#name')  
  2. node.innerText = '你好,前端自习课'; 

实现起来比较简单,当我们需要修改的数据有很多时(比如相同数据被多处引用),这样的操作将变得复杂。

既然说到 Vue.js,我们就来看看 Vue.js 怎么实现上面需求: 

 
 
 
 
  1.   
  2.  

观察上面代码,我们通过改变数据,来自动更新视图。当我们有多个地方引用这个 name 时,视图都会自动更新。 

 
 
 
 
  1.  

当我们使用目前主流的前端框架 Vue.js 和 React 开发业务时,只需关注页面数据如何变化,因为数据变化后,视图也会自动更新,这让我们从繁杂的 DOM 操作中解脱出来,提高开发效率。

二、回顾观察者模式

前面反复提到“通过改变数据,来自动更新视图”,换个说法就是“数据改变后,使用该数据的地方被动发生响应,更新视图”。

是不是有种熟悉的感觉?数据无需关注自身被多少对象引用,只需在数据变化时,通知到引用的对象即可,引用的对象作出响应。恩,有种观察者模式的味道?

关于观察者模式,可阅读我之前写的《图解设计模式之观察者模式(TypeScript)》。

1. 观察者模式流程

观察者模式表示一种“一对多”的关系,n 个观察者关注 1 个被观察者,被观察者可以主动通知所有观察者。接下图:

在这张图中,粉丝想及时收到“前端自习课”最新文章,只需关注即可,“前端自习课”有新文章,会主动推送给每个粉丝。该过程中,“前端自习课”是被观察者,每位“粉丝”是观察者。

2. 观察者模式核心

观察者模式核心组成包括:n 个观察者和 1 个被观察者。这里实现一个简单观察者模式:

2.1 定义接口 

 
 
 
 
  1. // 观察目标接口  
  2. interface ISubject {  
  3.     addObserver: (observer: Observer) => void; // 添加观察者  
  4.     removeObserver: (observer: Observer) => void; // 移除观察者  
  5.     notify: () => void; // 通知观察者  
  6. }  
  7. // 观察者接口  
  8. interface IObserver {  
  9.     update: () => void;  

2.2 实现被观察者类 

 
 
 
 
  1. // 实现被观察者类  
  2. class Subject implements ISubject {  
  3.     private observers: IObserver[] = [];  
  4.     public addObserver(observer: IObserver): void {  
  5.         this.observers.push(observer);  
  6.     }  
  7.     public removeObserver(observer: IObserver): void {  
  8.         const idx: number = this.observers.indexOf(observer);  
  9.         ~idx && this.observers.splice(idx, 1);  
  10.     }  
  11.     public notify(): void { 
  12.          this.observers.forEach(observer => {  
  13.             observer.update();  
  14.         });  
  15.     }  

2.3 实现观察者类 

 
 
 
 
  1. // 实现观察者类  
  2. class Observer implements IObserver {  
  3.     constructor(private name: string) { }  
  4.     update(): void {  
  5.         console.log(`${this.name} has been notified.`);  
  6.     }  

2.4 测试代码 

 
 
 
 
  1. function useObserver(){  
  2.     const subject: ISubject = new Subject();  
  3.     const Leo = new Observer("Leo");  
  4.     const Robin = new Observer("Robin");  
  5.     const Pual = new Observer("Pual");  
  6.     subject.addObserver(Leo);  
  7.     subject.addObserver(Robin);  
  8.     subject.addObserver(Pual);  
  9.     subject.notify();  
  10.     subject.removeObserver(Pual);  
  11.     subject.notify();  
  12. }  
  13. useObserver();  
  14. // [LOG]: "Leo has been notified."   
  15. // [LOG]: "Robin has been notified."   
  16. // [LOG]: "Pual has been notified."   
  17. // [LOG]: "Leo has been notified."   
  18. // [LOG]: "Robin has been notified."  

三、回顾 Object.defineProperty()

Vue.js 的数据响应式原理是基于 JS 标准内置对象方法 Object.defineProperty() 方法来实现,该方法不兼容 IE8 和 FF22 及以下版本浏览器,这也是为什么 Vue.js 只能在这些版本之上的浏览器中才能运行的原因。

理解 Object.defineProperty() 对我们理解 Vue.js 响应式原理非常重要。

Vue.js 3 使用 proxy 方法实现响应式,两者类似,我们只需搞懂Object.defineProperty() , proxy 也就差不多理解了。

1. 概念介绍

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

语法如下: 

 
 
 
 
  1. Object.defineProperty(obj, prop, descriptor) 
  •  入参说明:

obj :要定义属性的源对象;

prop :要定义或修改的属性名称或 Symbol;

descriptor :要定义或修改的属性描述符,包括 configurable、enumerable、value、writable、get、set,具体的可以去参阅文档;

  •  出参说明:

修改后的源对象。

举个简单例子: 

 
 
 
 
  1. const leo = {};  
  2. Object.defineProperty(leo, 'age', {  
  3.     value: 18,  
  4.     writable: true  
  5. })  
  6. console.log(leo.age); // 18  
  7. leo.age = 22;  
  8. console.log(leo.age); // 22 

2. 实现 getter/setter

我们知道 Object.defineProperty() 方法第三个参数是属性描述符(descriptor),支持设置 get 和 set 描述符:

  •  get 描述符:当访问该属性时,会调用此函数,默认值为 undefined ;
  •  set 描述符:当修改该属性时,会调用此函数,默认值为 undefined 。

一旦对象拥有了 getter/setter 方法,我们可以简单将该对象称为响应式对象。

这两个操作符为我们提供拦截数据进行操作的可能性,修改前面示例,添加 getter/setter 方法: 

 
 
 
 
  1. let leo = {}, age = 18;  
  2. Object.defineProperty(leo, 'age', {   
  3.     get(){  
  4.         // to do something  
  5.           console.log('监听到请求数据');  
  6.         return age;  
  7.     },  
  8.     set(newAge){  
  9.         // to do something  
  10.           console.log('监听到修改数据');  
  11.         age = newAge > age ? age : newAge  
  12.     }  
  13. })  
  14. leo.age = 20;  // 监听到修改数据  
  15. console.log(leo.age); // 监听到请求数据  // 18  
  16. leo.age = 10;  // 监听到修改数据  
  17. console.log(leo.age); // 监听到请求数据  // 10 

访问 leo 对象的 age 属性,会通过 get 描述符处理,而修改 age 属性,则会通过 set 描述符处理。

四、实现简单的数据响应式

通过前面两个小节,我们复习了“观察者模式”和“Object.defineProperty()” 方法,这两个知识点在 Vue.js 响应式原理中非常重要。

接下来我们来实现一个很简单的数据响应式变化,需求如下:点击“更新数据”按钮,文本更新。

接下来我们将实现三个类:

  •  Dep 被观察者类,用来生成被观察者;
  •  Watcher 观察者类,用来生成观察者;
  •  Observer 类,将普通数据转换为响应式数据,从而实现响应式对象。

用一张图来描述三者之间关系,现在看不懂没关系,这小节看完可以再回顾这张图:

1. 实现精简观察者模式

这里参照前面复习“观察者模式”的示例,做下精简: 

 
 
 
 
  1. // 实现被观察者类  
  2. class Dep {  
  3.     constructor() {  
  4.         this.subs = [];  
  5.     }  
  6.     addSub(watcher) {  
  7.         this.subs.push(watcher);  
  8.     }  
  9.     notify(data) {  
  10.         this.subs.forEach(sub => sub.update(data));  
  11.     }  
  12. }  
  13. // 实现观察者类  
  14. class Watcher {  
  15.     constructor(cb) {  
  16.         this.cb = cb;  
  17.     }  
  18.     update(data) {  
  19.         this.cb(data);  
  20.     }  

Vue.js 响应式原理中,观察者模式起到非常重要的作用。其中:

  •  Dep 被观察者类,提供用来收集观察者( addSub )方法和通知观察者( notify )方法;
  •  Watcher 观察者类,实例化时支持传入回调( cb )方法,并提供更新( update )方法;

2. 实现生成响应式的类

这一步需要实现 Observer 类,核心是通过 Object.defineProperty() 方法为对象的每个属性设置 getter/setter,目的是将普通数据转换为响应式数据,从而实现响应式对象。

这里以最简单的单层对象为例(下一节会介绍深层对象),如: 

 
 
 
 
  1. let initData = {  
  2.     text: '你好,前端自习课',  
  3.     desc: '每日清晨,享受一篇前端优秀文章。'  
  4. }; 

接下来实现 Observer 类: 

 
 
 
 
  1. // 实现响应式类(最简单单层的对象,暂不考虑深层对象)  
  2. class Observer {  
  3.     constructor (node, data) {  
  4.         this.defineReactive(node, data)  
  5.     }  
  6.     // 实现数据劫持(核心方法)  
  7.     // 遍历 data 中所有的数据,都添加上 getter 和 setter 方法  
  8.     defineReactive(vm, obj) {  
  9.         //每一个属性都重新定义get、set  
  10.         for(let key in obj){  
  11.             let value = obj[key], dep = new Dep();  
  12.             Object.defineProperty(obj, key, {  
  13.                 enumerable: true,  
  14.                 configurable: true,  
  15.                 get() {  
  16.                     // 创建观察者  
  17.                     let watcher = new Watcher(v => vvm.innerText = v);  
  18.                     dep.addSub(watcher);  
  19.                     return value;  
  20.                 },  
  21.                 set(newValue) {  
  22.                     value = newValue;  
  23.                     // 通知所有观察者  
  24.                     dep.notify(newValue);  
  25.                 }  
  26.             })  
  27.         }  
  28.     }  

上面代码的核心是 defineReactive 方法,它遍历原始对象中每个属性,为每个属性实例化一个被观察者(Dep),然后分别调用 Object.defineProperty() 方法,为每个属性添加 getter/setter。

  •  访问数据时,getter 执行依赖收集(即添加观察者),通过实例化 Watcher 创建一个观察者,并执行被观察者的 addSub() 方法添加一个观察者;
  •  修改数据时,setter 执行派发更新(即通知观察者),通过调用被观察者的 notify() 方法通知所有观察者,执行观察者 update() 方法。

3. 测试代码

为了方便观察数据变化,我们为“更新数据”按钮绑定点击事件来修改数据: 

 
 
 
 
  
  • 更新数据 
  • 测试代码如下: 

     
     
     
     
    1. // 初始化测试数据  
    2. let initData = {  
    3.     text: '你好,前端自习课', 
    4.     desc: '每日清晨,享受一篇前端优秀文章。'  
    5. };  
    6. const app = document.querySelector('#app'); 
    7.  // 步骤1:为测试数据转换为响应式对象  
    8. new Observer(app, initData);  
    9. // 步骤2:初始化页面文本内容  
    10. app.innerText = initData.text;  
    11. // 步骤3:绑定按钮事件,点击触发测试  
    12. document.querySelector('#update').addEventListener('click', function(){  
    13.     initData.text = `我们必须经常保持旧的记忆和新的希望。`;  
    14.     console.log(`当前时间:${new Date().toLocaleString()}`)  
    15. }) 

    测试代码中,核心在于通过实例化 Observer,将测试数据转换为响应式数据,然后模拟数据变化,来观察视图变化。

    每次点击“更新数据”按钮,在控制台中都能看到“数据发生变化!”的提示,说明我们已经能通过 setter 观察到数据的变化情况。

    当然,你还可以在控制台手动修改 initData 对象中的 text 属性,来体验响应式变化~~

    到这里,我们实现了非常简单的数据响应式变化,当然 Vue.js 肯定没有这么简单,这个先理解,下一节看 Vue.js 响应式原理,思路就会清晰很多。

    这部分代码,我已经放到我的 Github,地址:https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/Basics-Reactive-Demo.js

    可以再回顾下这张图,对整个过程会更清晰:

    observer-watcher-dep.png

    五、Vue.js 响应式实现

    本节代码:https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/leo-vue-reactive/

    这里大家可以再回顾下下面这张官网经典的图,思考下前面讲的示例。

    (图片来自:https://cn.vuejs.org/v2/guide/reactivity.html)

    上一节实现了简单的数据响应式,接下来继续通过完善该示例,实现一个简单的 Vue.js 响应式,测试代码如下: 

     
     
     
     
    1. // index.js  
    2. const vm = new Vue({  
    3.     el: '#app',  
    4.     data(){  
    5.         return {  
    6.             text: '你好,前端自习课',  
    7.             desc: '每日清晨,享受一篇前端优秀文章。' 
    8.         }  
    9.     }  
    10. }); 

    是不是很有内味了,下面是我们最终实现后项目目录: 

     
     
     
     
    1. - mini-reactive  
    2.     / index.html   // 入口 HTML 文件  
    3.   / index.js     // 入口 JS 文件  
    4.   / observer.js  // 实现响应式,将数据转换为响应式对象  
    5.   / watcher.js   // 实现观察者和被观察者(依赖收集者)  
    6.   / vue.js       // 实现 Vue 类作为主入口类  
    7.   / compile.js   // 实现编译模版功能 

    知道每一个文件功能以后,接下来将每一步串联起来。

    1. 实现入口文件

    我们首先实现入口文件,包括 index.html / index.js  2 个简单文件,用来方便接下来的测试。

    1.1 index.html 

     
     
     
     
    1.   
    2.   
    3.   
    4.       
    5.       
    6.       
    7.       
    8.   
    9.   
    10.     {{text}}
      
  •     更新数据  
  •       
  •   
  •  
  • 1.2 index.js 

     
     
     
     
    1. "use strict";  
    2. const vm = new Vue({  
    3.     el: '#app',  
    4.     data(){  
    5.         return {  
    6.             text: '你好,前端自习课',  
    7.             desc: '每日清晨,享受一篇前端优秀文章。'  
    8.         }  
    9.     }  
    10. });  
    11. console.log(vm.$data.text)  
    12. vm.$data.text = '页面数据更新成功!'; // 模拟数据变化  
    13. console.log(vm.$data.text) 

    2. 实现核心入口 vue.js

    vue.js 文件是我们实现的整个响应式的入口文件,暴露一个 Vue 类,并挂载全局。 

     
     
     
     
    1. class Vue {  
    2.     constructor (options = {}) {  
    3.         this.$el = options.el;  
    4.         this.$data = options.data();  
    5.         this.$methods = options.methods;  
    6.         // [核心流程]将普通 data 对象转换为响应式对象  
    7.         new Observer(this.$data);  
    8.         if (this.$el) {  
    9.             // [核心流程]将解析模板的内容  
    10.             new Compile(this.$el, this)  
    11.         }  
    12.     }  
    13. }  
    14. window.Vue = Vue; 

    Vue 类入参为一个配置项 option ,使用起来跟 Vue.js 一样,包括 $el 挂载点、 $data 数据对象和 $methods 方法列表(本文不详细介绍)。

    通过实例化 Oberser 类,将普通 data 对象转换为响应式对象,然后判断是否传入 el 参数,存在时,则实例化 Compile 类,解析模版内容。

    总结下 Vue 这个类工作流程 :

    3. 实现 observer.js

    observer.js 文件实现了 Observer 类,用来将普通对象转换为响应式对象: 

     
     
     
     
    1. class Observer {  
    2.     constructor (data) {  
    3.         this.data = data;  
    4.         this.walk(data);  
    5.     }  
    6.     // [核心方法]将 data 对象转换为响应式对象,为每个 data 属性设置 getter 和 setter 方法  
    7.     walk (data) {  
    8.         if (typeof data !== 'object') return data;  
    9.         Object.keys(data).forEach( key => {  
    10.             this.defineReactive(data, key, data[key])  
    11.         })  
    12.     }  
    13.     // [核心方法]实现数据劫持  
    14.     defineReactive (obj, key, value) {  
    15.         this.walk(value);  // [核心过程]遍历 walk 方法,处理深层对象。  
    16.         const dep = new Dep();  
    17.         Object.defineProperty(obj, key, {  
    18.             enumerable: true, 
    19.              configurable: true,  
    20.             get () {  
    21.                 console.log('[getter]方法执行')  
    22.                 Dep.target &&  dep.addSub(Dep.target);  
    23.                 return value  
    24.             },  
    25.             set (newValue) {  
    26.                 console.log('[setter]方法执行')  
    27.                 if (value === newValue) return;  
    28.                 // [核心过程]当设置的新值 newValue 为对象,则继续通过 walk 方法将其转换为响应式对象  
    29.                 if (typeof newValue === 'object') this.walk(newValue);  
    30.                 value = newValue;  
    31.                 dep.notify(); // [核心过程]执行被观察者通知方法,通知所有观察者执行 update 更新  
    32.             }  
    33.         })  
    34.     }  

    相比较第四节实现的 Observer 类,这里做了调整:

    通过改善后的 Observer 类,我们就可以实现将单层或深层嵌套的普通对象转换为响应式对象。

    4. 实现 watcher.js

    这里实现了 Dep 被观察者类(依赖收集者)和 Watcher 观察者类。 

     
     
     
     
    1. class Dep {  
    2.     constructor() {  
    3.         this.subs = [];  
    4.     }  
    5.     addSub(watcher) {  
    6.         this.subs.push(watcher);  
    7.     }  
    8.     notify(data) {  
    9.         this.subs.forEach(sub => sub.update(data));  
    10.     }  
    11. }  
    12. class Watcher {  
    13.     constructor (vm, key, cb) {  
    14.         this.vm = vm;   // vm:表示当前实例  
    15.         this.key = key; // key:表示当前操作的数据名称  
    16.         this.cb = cb;   // cb:表示数据发生改变之后的回调  
    17.         Dep.target = this; // 全局唯一  
    18.         thisthis.oldValue = this.vm.$data[key]; // 保存变化的数据作为旧值,后续作判断是否更新  
    19.         Dep.target = null;  
    20.     } 
    21.      update () {  
    22.         console.log(`数据发生变化!`);  
    23.         let oldValue = this.oldValue;  
    24.         let newValue = this.vm.$data[this.key];  
    25.         if (oldValue != newValue) {  // 比较新旧值,发生变化才执行回调  
    26.             this.cb(newValue, oldValue);  
    27.         };  
    28.     }  

    相比较第四节实现的 Watcher  类,这里做了调整:

    Dep.target 是当前全局唯一的订阅者,因为同一时间只允许一个订阅者被处理。target 指当前正在处理的目标订阅者,当前订阅者处理完就赋值为 null 。这里 Dep.target 会在 defineReactive() 的 getter 中使用到。

    通过改善后的 Watcher 类,我们操作当前操作对象 key 对应值的时候,可以在数据有变化的情况才执行回调,减少资源浪费。

    4. 实现 compile.js

    compile.js 实现了 Vue.js 的模版编译,如将 HTML 中的 {{text}} 模版转换为具体变量的值。

    compile.js 介绍内容较多,考虑到篇幅问题,并且本文核心介绍响应式原理,所以这里就暂时不介绍 compile.js 的实现,在学习的朋友可以到我 Github 上下载该文件直接下载使用即可,地址:

    https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/leo-vue-reactive/compile.js

    5. 测试代码

    到这里,我们已经将第四节的 demo 改造成简易版 Vue.js 响应式,接下来打开 index.html 看看效果:

    当 index.js 中执行到:

     
     
     
     
    1. vm.$data.text = '我们必须经常保持旧的记忆和新的希望。'; 

    页面便发生更新,页面显示的文本内容从“你好,前端自习课”更新成“我们必须经常保持旧的记忆和新的希望。”。

    到这里,我们的简易版 Vue.js 响应式原理实现好了,能跟着文章看到这里的朋友,给你点个大大的赞

    六、总结

    本文首先通过回顾观察者模式和 Object.defineProperty() 方法,介绍 Vue.js 响应式原理的核心知识点,然后带大家通过一个简单示例实现简单响应式,最后通过改造这个简单响应式的示例,实现一个简单 Vue.js 响应式原理的示例。

    相信看完本文的朋友,对 Vue.js 的响应式原理的理解会更深刻,希望大家理清思路,再好好回味下~

    文章题目:探索Vue.js响应式原理
    网页网址:http://www.shufengxianlan.com/qtweb/news44/511844.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联

    猜你还喜欢下面的内容

    网页设计公司知识

    分类信息网站