探索 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的数据响应式原理》
目录
相关文章
|
3月前
|
机器学习/深度学习 JavaScript 前端开发
JS进阶教程:递归函数原理与篇例解析
通过对这些代码示例的学习,我们已经了解了递归的原理以及递归在JS中的应用方法。递归虽然有着理论升华,但弄清它的核心思想并不难。举个随手可见的例子,火影鸣人做的影分身,你看到的都是同一个鸣人,但他们的行为却能在全局产生影响,这不就是递归吗?雾里看花,透过其间你或许已经深入了递归的魅力之中。
128 19
|
6月前
|
JavaScript 前端开发 算法
vue渲染页面的原理
vue渲染页面的原理
214 56
|
6月前
|
JavaScript 前端开发 UED
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
388 2
|
6月前
|
JavaScript 前端开发 Java
深入理解 JavaScript 中的 Array.find() 方法:原理、性能优势与实用案例详解
Array.find() 是 JavaScript 数组方法中一个非常实用和强大的工具。它不仅提供了简洁的查找操作,还具有性能上的独特优势:返回的引用能够直接影响原数组的数据内容,使得数据更新更加高效。通过各种场景的展示,我们可以看到 Array.find() 在更新、条件查找和嵌套结构查找等场景中的广泛应用。 在实际开发中,掌握 Array.find() 的特性和使用技巧,可以让代码更加简洁高效,特别是在需要直接修改原数据内容的情形。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一
|
6月前
|
监控 JavaScript 前端开发
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例
MutationObserver 是一个非常强大的 API,提供了一种高效、灵活的方式来监听和响应 DOM 变化。它解决了传统 DOM 事件监听器的诸多局限性,通过异步、批量的方式处理 DOM 变化,大大提高了性能和效率。在实际开发中,合理使用 MutationObserver 可以帮助我们更好地控制 DOM 操作,提高代码的健壮性和可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例
|
7月前
|
移动开发 JavaScript API
Vue Router 核心原理
Vue Router 是 Vue.js 的官方路由管理器,用于实现单页面应用(SPA)的路由功能。其核心原理包括路由配置、监听浏览器事件和组件渲染等。通过定义路径与组件的映射关系,Vue Router 将用户访问的路径与对应的组件关联,支持哈希和历史模式监听 URL 变化,确保页面导航时正确渲染组件。
|
6月前
|
JavaScript 前端开发 Java
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
柯里化是一种强大的函数式编程技术,它通过将函数分解为单参数形式,实现了灵活性与可复用性的统一。无论是参数复用、延迟执行,还是函数组合,柯里化都为现代编程提供了极大的便利。 从 Redux 的选择器优化到复杂的数据流处理,再到深度嵌套的函数优化,柯里化在实际开发中展现出了非凡的价值。如果你希望编写更简洁、更优雅的代码,柯里化无疑是一个值得深入学习和实践的工具。从简单的实现到复杂的应用,希望这篇博客能为你揭开柯里化的奥秘,助力你的开发之旅! 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一
|
10月前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
259 17
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
229 2