通俗易懂了解Vue双向绑定原理及实现

1. 前言

每当被问到Vue数据双向绑定原理的时候,大家可能都会脱口而出:Vue内部通过Object.defineProperty方法属性拦截的方式,把data对象里每个数据的读写转化成getter/setter,当数据变化时通知视图更新。虽然一句话把大概原理概括了,但是其内部的实现方式还是值得深究的,本文就以通俗易懂的方式剖析Vue内部双向绑定原理的实现过程。

2. 思路分析

所谓MVVM数据双向绑定,即主要是:数据变化更新视图,视图变化更新数据。如下图:
在这里插入图片描述
也就是说:

  1. 输入框内容变化时,data 中的数据同步变化。即 view => model 的变化。
  2. data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化。

要实现这两个过程,关键点在于数据变化如何更新视图,因为视图变化更新数据我们可以通过事件监听的方式来实现。所以我们着重讨论数据变化如何更新视图。

数据变化更新视图的关键点则在于我们如何知道数据发生了变化,只要知道数据在什么时候变了,那么问题就变得迎刃而解,我们只需在数据变化的时候去通知视图更新即可。

3. 使数据对象变得“可观测”

数据的每次读和写能够被我们看的见,即我们能够知道数据什么时候被读取了或数据什么时候被改写了,我们将其称为数据变的‘可观测’。

要将数据变的‘可观测’,我们就要借助前言中提到的Object.defineProperty方法了,关于该方法,MDN上是这么介绍的:

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
在本文中,我们就使用这个方法使数据变得“可观测”。

首先,我们定义一个数据对象car:

let car = {
        'brand':'BMW',
        'price':3000
    }

我们定义了这个car的品牌brandBMW,价格price是3000。现在我们可以通过car.brandcar.price直接读写这个car对应的属性值。但是,当这个car的属性被读取或修改时,我们并不知情。那么应该如何做才能够让car主动告诉我们,它的属性被修改了呢?

接下来,我们使用Object.defineProperty()改写上面的例子:

let car = {}
    let val = 3000
    Object.defineProperty(car, 'price', {
        get(){
            console.log('price属性被读取了')
            return val
        },
        set(newVal){
            console.log('price属性被修改了')
            val = newVal
        }
    })

通过Object.defineProperty()方法给car定义了一个price属性,并把这个属性的读和写分别使用get()set()进行拦截,每当该属性进行读或写操作的时候就会出发get()set()。如下图:
在这里插入图片描述
可以看到,car已经可以主动告诉我们它的属性的读写情况了,这也意味着,这个car的数据对象已经是“可观测”的了。

为了把car的所有属性都变得可观测,我们可以编写如下两个函数:

/**
     * 把一个对象的每一项都转化成可观测对象
     * @param { Object } obj 对象
     */
    function observable (obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) =>{
            defineReactive(obj,key,obj[key])
        })
        return obj;
    }
    /**
     * 使一个对象转化成可观测对象
     * @param { Object } obj 对象
     * @param { String } key 对象的key
     * @param { Any } val 对象的某个key的值
     */
    function defineReactive (obj,key,val) {
        Object.defineProperty(obj, key, {
            get(){
                console.log(`${key}属性被读取了`);
                return val;
            },
            set(newVal){
                console.log(`${key}属性被修改了`);
                val = newVal;
            }
        })
    }

现在,我们就可以这样定义car:

let car = observable({
        'brand':'BMW',
        'price':3000
    })

car的两个属性都变得可观测了。

4. 依赖收集

完成了数据的’可观测’,即我们知道了数据在什么时候被读或写了,那么,我们就可以在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是典型的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。

现在,我们需要创建一个依赖收集容器,也就是消息订阅器Dep,用来容纳所有的“订阅者”。订阅器Dep主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。

创建消息订阅器Dep:

class Dep {
        constructor(){
            this.subs = []
        },
        //增加订阅者
        addSub(sub){
            this.subs.push(sub);
        },
        //判断是否增加订阅者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        },

        //通知订阅者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
    }
Dep.target = null;

有了订阅器,再将defineReactive函数进行改造一下,向其植入订阅器:

function defineReactive (obj,key,val) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get(){
                dep.depend();
                console.log(`${key}属性被读取了`);
                return val;
            },
            set(newVal){
                val = newVal;
                console.log(`${key}属性被修改了`);
                dep.notify()                    //数据变化通知所有订阅者
            }
        })
    }

从代码上看,我们设计了一个订阅器Dep类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 target,这是一个全局唯一 的Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组。

我们将订阅器Dep添加订阅者的操作设计在getter里面,这是为了让Watcher初始化时进行触发,因此需要判断是否要添加订阅者。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。

到此,订阅器Dep设计完毕,接下来,我们设计订阅者Watcher.

5. 订阅者Watcher

订阅者Watcher在初始化的时候需要将自己添加进订阅器Dep中,那该如何添加呢?我们已经知道监听器Observer是在get函数执行了添加订阅者Wather的操作的,所以我们只要在订阅者Watcher初始化的时候出发对应的get函数去执行添加订阅者操作即可,那要如何触发get的函数,再简单不过了,只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了Object.defineProperty( )进行数据监听。这里还有一个细节点需要处理,我们只要在订阅者Watcher初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者Watcher的实现如下:

class Watcher {
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            this.value = this.get();  // 将自己添加到订阅器的操作
        },

        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            },
        get(){
            Dep.target = this;  // 缓存自己
            let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
            Dep.target = null;  // 释放自己
            return value;
        }
    }

过程分析:

订阅者Watcher 是一个 类,在它的构造函数中,定义了一些属性:

  • vm:一个Vue的实例对象;
  • exp:是node节点的v-modelv-on:click等指令的属性值。如v-model="name"exp就是name;
  • cb:是Watcher绑定的更新函数

当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,就会执行它的 this.get() 方法,进入 get 函数,首先会执行:

Dep.target = this;  // 缓存自己

实际上就是把 Dep.target 赋值为当前的渲染 watcher ,接着又执行了:

let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数

在这个过程中会对 vm 上的数据访问,其实就是为了触发数据对象的getter

每个对象值的 getter都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行this.addSub(Dep.target),即把当前的 watcher 订阅到这个数据持有的 depsubs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。

这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把 Dep.target 恢复成上一个状态,即:

Dep.target = null;  // 释放自己

因为当前vm的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变。

update()函数是用来当数据发生变化时调用Watcher自身的更新函数进行更新的操作。先通过let value = this.vm.data[this.exp];获取到最新的数据,然后将其与之前get()获得的旧数据进行比较,如果不一样,则调用更新函数cb进行更新。

至此,简单的订阅者Watcher设计完毕。

6. 测试

完成以上工作后,我们就可以来真正的测试了。
index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1 id="name"></h1>
    <input type="text">
    <input type="button" value="改变data内容" onclick="changeInput()">
    
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
    function myVue (data, el, exp) {
        this.data = data;
        observable(data);                      //将数据变的可观测
        el.innerHTML = this.data[exp];           // 初始化模板数据的值
        new Watcher(this, exp, function (value) {
            el.innerHTML = value;
        });
        return this;
    }

    var ele = document.querySelector('#name');
    var input = document.querySelector('input');
    
    var myVue = new myVue({
        name: 'hello world'
    }, ele, 'name');
    
    //改变输入框内容
    input.oninput = function (e) {
        myVue.data.name = e.target.value
    }
    //改变data内容
    function changeInput(){
        myVue.data.name = "难凉热血"
    
    }
</script>
</body>
</html>

observer.js

/**
     * 把一个对象的每一项都转化成可观测对象
     * @param { Object } obj 对象
     */
    function observable (obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) =>{
            defineReactive(obj,key,obj[key])
        })
        return obj;
    }
    /**
     * 使一个对象转化成可观测对象
     * @param { Object } obj 对象
     * @param { String } key 对象的key
     * @param { Any } val 对象的某个key的值
     */
    function defineReactive (obj,key,val) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get(){
                dep.depend();
                console.log(`${key}属性被读取了`);
                return val;
            },
            set(newVal){
                val = newVal;
                console.log(`${key}属性被修改了`);
                dep.notify()                    //数据变化通知所有订阅者
            }
        })
    }
    class Dep {
        
        constructor(){
            this.subs = []
        }
        //增加订阅者
        addSub(sub){
            this.subs.push(sub);
        }
        //判断是否增加订阅者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        }

        //通知订阅者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
        
    }
    Dep.target = null;

watcher.js

class Watcher {
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            this.value = this.get();  // 将自己添加到订阅器的操作
        }
        get(){
            Dep.target = this;  // 缓存自己
            let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
            Dep.target = null;  // 释放自己
            return value;
        }
        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            }
    }
}

效果:
在这里插入图片描述

7. 总结

总结一下:

实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。

在这里插入图片描述

本文载抄来自 :https://www.cnblogs.com/wangjiachen666/p/9883916.html#_label0

<think>好的,我现在需要回答用户关于基于SSM框架和Vue的系统原理的问题。首先,我需要确认用户提到的SSM框架是指Spring、Spring MVC和MyBatis这三个技术的组合,而Vue是前端框架。接下来,我要解释这两个部分如何协同工作,以及整个系统的工作原理。 首先,我应该从系统架构的角度出发,分为后端和前端两部分。后端使用SSM框架,前端使用Vue,这样前后端分离,通过API进行通信。用户可能想知道这两个部分各自的职责以及它们如何交互。 对于SSM框架,我需要分别介绍Spring、Spring MVC和MyBatis的作用。Spring负责控制反转(IoC)和依赖注入(DI),管理Bean的生命周期;Spring MVC处理Web层的请求分发,控制器接收请求并调用服务层;MyBatis作为ORM框架,负责数据库操作,将Java对象映射到数据库表。 然后是Vue的部分,作为前端框架,Vue负责构建用户界面,通过组件化开发,实现数据双向绑定,响应式更新视图。前端通过Axios或其他HTTP客户端与后端RESTful API交互,获取数据并展示。 接下来,要描述整个系统的工作流程。用户在前端进行操作,比如点击按钮,触发Vue的事件处理函数,发送HTTP请求到后端。Spring MVC的DispatcherServlet接收到请求,根据路由找到对应的Controller,Controller调用Service层处理业务逻辑,Service层再通过MyBatis访问数据库。数据库返回数据后,Service处理结果返回给Controller,Controller生成JSON响应,前端Vue接收数据并更新页面。 还需要考虑前后端分离的优势,比如职责分明、开发效率高、易于维护和扩展。可能用户对技术选型的原因感兴趣,比如为什么选择SSM和Vue组合,可以提到Spring的生态成熟,MyBatis灵活性高,Vue的轻量级和易上手。 此外,可能需要提到一些关键概念,比如RESTful API设计、跨域问题解决(CORS)、数据格式(JSON)的使用,以及可能用到的前端路由(Vue Router)和状态管理(Vuex)。对于后端,可以提Spring的事务管理、AOP面向切面编程,MyBatis的映射文件和动态SQL。 最后,要确保回答结构清晰,分点说明,结合用户可能关心的实际开发流程和原理,避免过于技术化的术语,用通俗易懂的语言解释。同时检查是否符合之前的用户指令,比如使用中文,数学公式的格式是否正确,但在这个问题中可能不需要数学公式,所以主要关注代码块的正确使用,如SQL示例是否正确用反引号包裹等。</think>基于**SSM框架(Spring + Spring MVC + MyBatis)**和**Vue**的系统是一种典型的前后端分离架构,核心原理是通过分层设计和模块化开发实现高效协作。以下是其工作原理的分步说明: --- ### **1. 系统分层架构** #### **后端(SSM框架)** - **Spring** - **作用**:管理对象(Bean)和依赖注入(IoC),整合各模块(如事务管理、AOP)。 - **示例**:通过`@Service`标注业务层,`@Autowired`自动注入依赖。 - **Spring MVC** - **作用**:处理HTTP请求,实现控制器(Controller)层,负责请求分发和响应。 - **流程**: 1. 用户请求 → `DispatcherServlet`(前端控制器) 2. 根据URL映射到对应的`Controller` 3. `Controller`调用`Service`层处理业务逻辑 4. 返回JSON数据给前端(通过`@ResponseBody`) - **MyBatis** - **作用**:操作数据库,通过XML或注解实现SQL映射,避免手动处理JDBC。 - **示例**:`UserMapper.xml`中定义SQL语句,通过`@Mapper`接口调用。 #### **前端(Vue)** - **Vue框架** - **作用**:构建用户界面,通过组件化开发实现动态数据绑定。 - **核心机制**: - **响应式数据**:`data`属性变化自动更新视图。 - **组件通信**:`props`(父传子)、`$emit`(子传父)、Vuex(全局状态管理)。 - **路由**:`Vue Router`管理单页面应用(SPA)的路由跳转。 - **API交互** - 使用`Axios`发送HTTP请求到后端接口(如`GET /api/users`),接收JSON数据并渲染页面。 --- ### **2. 系统交互流程** 1. **用户触发前端操作**(如点击按钮) → Vue组件调用方法,通过`Axios`发送请求到后端API。 2. **后端接收请求** → Spring MVC的`DispatcherServlet`解析请求路径。 → 匹配对应的`Controller`方法(如`UserController.getUser()`)。 3. **业务逻辑处理** → `Controller`调用`Service`层(如`UserService`)处理业务(如权限校验、计算)。 → `Service`层调用`MyBatis`的`Mapper`接口执行SQL操作数据库。 4. **数据库响应** → MyBatis将SQL执行结果映射为Java对象,逐层返回给`Controller`。 → `Controller`将数据封装为JSON格式(如`{"name": "Alice", "age": 25}`)。 5. **前端渲染数据** → Vue接收JSON数据,更新组件的`data`属性。 → 页面自动重新渲染(如表格展示新数据)。 --- ### **3. 关键技术点** #### **前后端分离优势** - **职责清晰**:后端专注数据逻辑,前端专注交互体验。 - **并行开发**:通过API文档约定接口,前后端可独立开发。 - **性能优化**:前端通过Vue的虚拟DOM减少渲染开销,后端通过MyBatis缓存提升查询效率。 #### **数据交互格式** - 使用**RESTful API**设计,请求方法(GET/POST/PUT/DELETE)对应操作类型。 - 数据格式为**JSON**(轻量且易解析),如: ```json { "id": 1, "name": "iPhone", "price": 6000 } ``` #### **跨域问题解决** - 后端通过`@CrossOrigin`注解或配置过滤器(Filter)允许前端域名访问。 - 前端开发时可配置代理(vue.config.js)绕过浏览器限制。 --- ### **4. 典型代码示例** #### 后端(Spring MVC Controller) ```java @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public ResponseEntity<User> getUser(@PathVariable int id) { User user = userService.findUserById(id); return ResponseEntity.ok(user); } } ``` #### 前端(Vue组件请求数据) ```javascript export default { data() { return { user: null }; }, methods: { async fetchUser(id) { try { const response = await axios.get(`/api/users/${id}`); this.user = response.data; } catch (error) { console.error("请求失败:", error); } }, }, }; ``` --- ### **5. 扩展问题方向** 1. **如何实现SSM框架的整合?** - 需配置`Spring`的XML或Java Config,整合MyBatis(`SqlSessionFactoryBean`)和Spring MVC。 2. **Vue如何实现权限控制?** - 结合路由守卫(`beforeEach`)和动态菜单,根据用户角色过滤可访问页面。 3. **如何优化前后端通信效率?** - 使用JWT Token验证身份,减少会话状态依赖;启用HTTP缓存(如ETag)。 如果需要进一步深入某个环节,可以继续提问!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值