探索 Vue.js 响应式原理 下

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

五、Vue.js 响应式实现

本节代码:github.com/pingan8787/…

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

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

(图片来自:cn.vuejs.org/v2/guide/re…

上一节实现了简单的数据响应式,接下来继续通过完善该示例,实现一个简单的 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.vm.$data[key] 读取属性值,触发 getter
        this.oldValue = this.vm.$data[key]; // 保存变化的数据作为旧值,后续作判断是否更新
        // 前面 getter 执行完后,执行下面清空
        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 上下载该文件直接下载使用即可,地址: github.com/pingan8787/…

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的数据响应式原理》
目录
相关文章
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
20天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
33 9
|
22天前
|
API
vue3知识点:响应式数据的判断
vue3知识点:响应式数据的判断
27 3
|
26天前
|
缓存 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应用程序
|
22天前
|
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