乐趣区

关于javascript:探索-Vuejs-响应式原理

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


从字面意思能够看出,具备“响应式”特色的事物会依据条件变动,使得指标主动作出对应变动。比方在“响应式布局”中,页面依据不同设施尺寸主动显示不同款式。

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

接下来我依据集体了解,和大家一起摸索下 Vue.js 中的响应式原理,如有谬误,欢送指导????~~

一、Vue.js 响应式的应用

当初有个很简略的需要,点击页面中“leo”文本后,文本内容批改为“你好,前端自习课”。

咱们能够间接操作 DOM,来实现这个需要:

<span id="name">leo</span>
const node = document.querySelector('#name')
node.innerText = '你好,前端自习课';

实现起来比较简单,当咱们须要批改的数据有很多时(比方雷同数据被多处援用),这样的操作将变得复杂。

既然说到 Vue.js,咱们就来看看 Vue.js 怎么实现下面需要:

<template>
  <div id="app">
    <span @click="setName">{{name}}</span>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {name: "leo",};
  },
  methods: {setName() {this.name = "你好,前端自习课";},
  },
};
</script>

察看下面代码,咱们 通过扭转数据,来自动更新视图。当咱们有多个中央援用这个 name 时,视图都会自动更新。

<template>
  <div id="app">
    <span @click="setName">{{name}}</span>
    <span>{{name}}</span>
    <span>{{name}}</span>
    <span>{{name}}</span>
  </div>
</template>

当咱们应用目前支流的前端框架 Vue.js 和 React 开发业务时,只需关注页面数据如何变动,因为数据变动后,视图也会自动更新,这让咱们从繁冗的 DOM 操作中解脱进去,进步开发效率。

二、回顾观察者模式

后面重复提到“通过扭转数据,来自动更新视图 ”,换个说法就是“ 数据扭转后,应用该数据的中央被动产生响应,更新视图”。

是不是有种相熟的感觉?数据无需关注本身被多少对象援用,只需在数据变动时,告诉到援用的对象即可,援用的对象作出响应。恩,有种观察者模式的滋味?

对于观察者模式,可浏览我之前写的《图解设计模式之观察者模式(TypeScript)》。

1. 观察者模式流程

观察者模式示意一种“一对多”的关系,n 个观察者关注 1 个被观察者,被观察者能够被动告诉所有观察者。接下图:


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

2. 观察者模式外围

观察者模式外围组成包含:n 个观察者和 1 个被观察者。这里实现一个简略观察者模式:

2.1 定义接口

// 察看指标接口
interface ISubject {addObserver: (observer: Observer) => void; // 增加观察者
    removeObserver: (observer: Observer) => void; // 移除观察者
    notify: () => void; // 告诉观察者}

// 观察者接口
interface IObserver {update: () => void;
}

2.2 实现被观察者类

// 实现被观察者类
class Subject implements ISubject {private observers: IObserver[] = [];

    public addObserver(observer: IObserver): void {this.observers.push(observer);
    }

    public removeObserver(observer: IObserver): void {const idx: number = this.observers.indexOf(observer);
        ~idx && this.observers.splice(idx, 1);
    }

    public notify(): void {
        this.observers.forEach(observer => {observer.update();
        });
    }
}

2.3 实现观察者类

// 实现观察者类
class Observer implements IObserver {constructor(private name: string) { }

    update(): void {console.log(`${this.name} has been notified.`);
    }
}

2.4 测试代码

function useObserver(){const subject: ISubject = new Subject();
    const Leo = new Observer("Leo");
    const Robin = new Observer("Robin");
    const Pual = new Observer("Pual");

    subject.addObserver(Leo);
    subject.addObserver(Robin);
    subject.addObserver(Pual);
    subject.notify();

    subject.removeObserver(Pual);
    subject.notify();}

useObserver();
// [LOG]: "Leo has been notified." 
// [LOG]: "Robin has been notified." 
// [LOG]: "Pual has been notified." 
// [LOG]: "Leo has been notified." 
// [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() 办法会间接在一个对象上定义一个新属性,或者批改一个对象的现有属性,并返回此对象。
语法如下:

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

obj:要定义属性的 源对象
prop:要定义或批改的 属性名称 Symbol
descriptor:要定义或批改的 属性描述符,包含 configurableenumerablevaluewritablegetset,具体的能够去参阅文档;

  • 出参阐明:

批改后的源对象。

举个简略???? 例子:

const leo = {};
Object.defineProperty(leo, 'age', { 
    value: 18,
    writable: false
})
console.log(leo.age); // 18
leo.age = 22;
console.log(leo.age); // 22

2. 实现 getter/setter

咱们晓得 Object.defineProperty() 办法第三个参数是属性描述符(descriptor),反对设置 getset 描述符:

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

一旦对象领有了 getter/setter 办法,咱们能够简略将该对象称为响应式对象。

这两个操作符为咱们提供拦挡数据进行操作的可能性,批改后面示例,增加 getter/setter 办法:

let leo = {}, age = 18;
Object.defineProperty(leo, 'age', {get(){
        // to do something
          console.log('监听到申请数据');
        return age;
    },
    set(newAge){
        // to do something
          console.log('监听到批改数据');
        age = newAge > age ? age : newAge
    }
})
leo.age = 20;  // 监听到批改数据
console.log(leo.age); // 监听到申请数据  // 18

leo.age = 10;  // 监听到批改数据
console.log(leo.age); // 监听到申请数据  // 10

拜访 leo 对象的 age 属性,会通过 get 描述符解决,而批改 age 属性,则会通过 set 描述符解决。

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

通过后面两个大节,咱们温习了“观察者模式”和“Object.defineProperty()”办法,这两个知识点在 Vue.js 响应式原理中十分重要。

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

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

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

用一张图来形容三者之间关系,当初看不懂没关系,这大节看完能够再回顾这张图:

1. 实现精简观察者模式

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

// 实现被观察者类
class Dep {constructor() {this.subs = [];
    }
    addSub(watcher) {this.subs.push(watcher);
    }
    notify(data) {this.subs.forEach(sub => sub.update(data));
    }
}
// 实现观察者类
class Watcher {constructor(cb) {this.cb = cb;}
    update(data) {this.cb(data);
    }
}

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

  • Dep 被观察者类,提供用来收集观察者(addSub)办法和告诉观察者(notify)办法;
  • Watcher 观察者类,实例化时反对传入回调(cb)办法,并提供更新(update)办法;

2. 实现生成响应式的类

这一步须要实现 Observer 类,外围是通过 Object.defineProperty() 办法为对象的每个属性设置 getter/setter,目标是 将一般数据转换为响应式数据,从而实现响应式对象

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

let initData = {
    text: '你好,前端自习课',
    desc: '每日凌晨,享受一篇前端优良文章。'
};

接下来实现 Observer 类:

// 实现响应式类(最简略单层的对象,暂不思考深层对象)class Observer {constructor (node, data) {this.defineReactive(node, data)
    }

    // 实现数据劫持(外围办法)// 遍历 data 中所有的数据,都增加上 getter 和 setter 办法
    defineReactive(vm, obj) {
        // 每一个属性都从新定义 get、set
        for(let key in obj){let value = obj[key],dep = new Dep();
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get() {
                    // 创立观察者
                    let watcher = new Watcher(v => vm.innerText = v);
                    dep.addSub(watcher);
                    return value;
                },
                set(newValue) {
                    value = newValue;
                    // 告诉所有观察者
                    dep.notify(newValue);
                }
            })
        }
    }
}

下面代码的外围是 defineReactive 办法,它遍历原始对象中每个属性,为每个属性实例化一个被观察者(Dep),而后别离调用 Object.defineProperty() 办法,为每个属性增加 getter/setter。

  • 拜访数据时,getter 执行依赖收集(即增加观察者),通过实例化 Watcher 创立一个观察者,并执行被观察者的 addSub() 办法增加一个观察者;
  • 批改数据时,setter 执行派发更新(即告诉观察者),通过调用被观察者的 notify() 办法告诉所有观察者,执行观察者 update() 办法。

3. 测试代码

为了不便察看数据变动,咱们为“更新数据”按钮绑定点击事件来批改数据:

<div id="app"></div>
<button id="update"> 更新数据 </button>

测试代码如下:

// 初始化测试数据
let initData = {
    text: '你好,前端自习课',
    desc: '每日凌晨,享受一篇前端优良文章。'
};

const app = document.querySelector('#app');

// 步骤 1:为测试数据转换为响应式对象
new Observer(app, initData);

// 步骤 2:初始化页面文本内容
app.innerText = initData.text;

// 步骤 3:绑定按钮事件,点击触发测试
document.querySelector('#update').addEventListener('click', function(){
    initData.text = ` 咱们必须常常放弃旧的记忆和新的心愿。`;
    console.log(` 以后工夫:${new Date().toLocaleString()}`)
})

测试代码中,外围在于通过实例化 Observer,将测试数据转换为响应式数据,而后模仿数据变动,来察看视图变动。
每次点击“更新数据”按钮,在控制台中都能看到“数据发生变化!”的提醒,阐明咱们曾经能通过 setter 察看到数据的变动状况。

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

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

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

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

五、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 响应式,测试代码如下:

// index.js
const vm = new Vue({
    el: '#app',
    data(){
        return {
            text: '你好,前端自习课',
            desc: '每日凌晨,享受一篇前端优良文章。'
        }
    }
});

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

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

晓得每一个文件性能当前,接下来将每一步串联起来。

1. 实现入口文件

咱们首先实现入口文件,包含 index.html / index.js  2 个简略文件,用来不便接下来的测试。

1.1 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <script src="./vue.js"></script>
    <script src="./observer.js"></script>
    <script src="./compile.js"></script>
    <script src="./watcher.js"></script>
</head>
<body>
    <div id="app">{{text}}</div>
    <button id="update"> 更新数据 </button>
    <script src="./index.js"></script>
</body>
</html>

1.2 index.js

"use strict";
const vm = new Vue({
    el: '#app',
    data(){
        return {
            text: '你好,前端自习课',
            desc: '每日凌晨,享受一篇前端优良文章。'
        }
    }
});

console.log(vm.$data.text)
vm.$data.text = '页面数据更新胜利!'; // 模仿数据变动
console.log(vm.$data.text)

2. 实现外围入口 vue.js

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

class Vue {constructor (options = {}) {
        this.$el = options.el;
        this.$data = options.data();
        this.$methods = options.methods;

        // [外围流程]将一般 data 对象转换为响应式对象
        new Observer(this.$data);

        if (this.$el) {// [外围流程]将解析模板的内容
            new Compile(this.$el, this)
        }
    }
}
window.Vue = Vue;

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

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

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

3. 实现 observer.js

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

class Observer {constructor (data) {
        this.data = data;
        this.walk(data);
    }

    // [外围办法]将 data 对象转换为响应式对象,为每个 data 属性设置 getter 和 setter 办法
    walk (data) {if (typeof data !== 'object') return data;
        Object.keys(data).forEach( key => {this.defineReactive(data, key, data[key])
        })
    }

    // [外围办法]实现数据劫持
    defineReactive (obj, key, value) {this.walk(value);  // [外围过程]遍历 walk 办法,解决深层对象。const dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get () {console.log('[getter]办法执行')
                Dep.target &&  dep.addSub(Dep.target);
                return value
            },
            set (newValue) {console.log('[setter]办法执行')
                if (value === newValue) return;
                // [外围过程]当设置的新值 newValue 为对象,则持续通过 walk 办法将其转换为响应式对象
                if (typeof newValue === 'object') this.walk(newValue);
                value = newValue;
                dep.notify(); // [外围过程]执行被观察者告诉办法,告诉所有观察者执行 update 更新
            }
        })
    }
}

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

  • 减少 walk 外围办法,用来遍历对象每个属性,别离调用数据劫持办法(defineReactive());
  • defineReactive() 的 getter 中,判断 Dep.target 存在才增加观察者,下一节会具体介绍 Dep.target
  • defineReactive() 的 setter 中,判断以后新值(newValue)是否为对象,如果是,则间接调用 this.walk() 办法将以后对象再次转为响应式对象,解决深层对象

通过改善后的 Observer 类,咱们就能够实现 将单层或深层嵌套的一般对象转换为响应式对象

4. 实现 watcher.js

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

class Dep {constructor() {this.subs = [];
    }
    addSub(watcher) {this.subs.push(watcher);
    }
    notify(data) {this.subs.forEach(sub => sub.update(data));
    }
}

class Watcher {constructor (vm, key, cb) {
        this.vm = vm;   // vm:示意以后实例
        this.key = key; // key:示意以后操作的数据名称
        this.cb = cb;   // cb:示意数据产生扭转之后的回调

        Dep.target = this; // 全局惟一
        this.oldValue = this.vm.$data[key]; // 保留变动的数据作为旧值,后续作判断是否更新
        Dep.target = null;
    }
    
    update () {console.log(` 数据发生变化!`);
        let oldValue = this.oldValue;
        let newValue = this.vm.$data[this.key];
        if (oldValue != newValue) {  // 比拟新旧值,发生变化才执行回调
            this.cb(newValue, oldValue);
        };
    }
}

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

  • 在构造函数中,减少 Dep.target 值操作;
  • 在构造函数中,减少 oldValue 变量,保留变动的数据作为旧值,后续作为判断是否更新的根据;
  • update() 办法中,减少以后操作对象 key 对应值的新旧值比拟,如果不同,才执行回调。

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 中执行到:

vm.$data.text = '咱们必须常常放弃旧的记忆和新的心愿。';

页面便产生更新,页面显示的文本内容从“你好,前端自习课 ”更新成“ 咱们必须常常放弃旧的记忆和新的心愿。”。

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

六、总结

本文首先通过回顾观察者模式和 Object.defineProperty() 办法,介绍 Vue.js 响应式原理的外围知识点,而后带大家通过一个简略示例实现简略响应式,最初通过革新这个简略响应式的示例,实现一个简略 Vue.js 响应式原理的示例。

置信看完本文的敌人,对 Vue.js 的响应式原理的了解会更粗浅,心愿大家理清思路,再好好回味下~

参考资料

  1. 官网文档 – 深刻响应式原理 
  2. 《浅谈 Vue 响应式原理》
  3. 《Vue 的数据响应式原理》
退出移动版