探索 Vue.js 响应式原理 上

简介: 探索 Vue.js 响应式原理 上


网络异常,图片无法展示
|

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

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

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 :要定义或修改的属性名称Symboldescriptor :要定义或修改的属性描述符,包括 configurableenumerablevaluewritablegetset,具体的可以去参阅文档

  • 出参说明:

修改后的源对象。

举个简单🌰例子:

const leo = {};
Object.defineProperty(leo, 'age', { 
    value: 18,
    writable: true
})
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,地址:github.com/pingan8787/…

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

网络异常,图片无法展示
|


目录
相关文章
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
20天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
33 9
|
23天前
|
API
vue3知识点:响应式数据的判断
vue3知识点:响应式数据的判断
27 3
|
27天前
|
缓存 JavaScript UED
优化Vue的响应式性能
【10月更文挑战第13天】优化 Vue 的响应式性能是一个持续的过程,需要不断地探索和实践,以适应不断变化的应用需求和性能挑战。
31 2
|
1月前
|
前端开发 JavaScript
深入理解JavaScript中的事件循环(Event Loop):从原理到实践
【10月更文挑战第12天】 深入理解JavaScript中的事件循环(Event Loop):从原理到实践
36 1
|
1月前
|
JavaScript 前端开发 网络架构
如何使用Vue.js构建响应式Web应用
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用
|
1月前
|
JavaScript 前端开发
如何使用Vue.js构建响应式Web应用程序
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用程序
|
23天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
24 0
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理、应用与代码演示
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理、应用与代码演示
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript闭包:原理与应用
【10月更文挑战第11天】深入理解JavaScript闭包:原理与应用
19 0