Skip to content
On this page

双向数据绑定与mvvm

本文是基于vue的理解

目录

  • 双向绑定的概念
  • 双向绑定实现的原理
    • 发布者-订阅者模式
    • 数据劫持模式
  • vue中的mvvm的理解

双向绑定的概念

直接点说,就是初始数据(接口拉取或本地定义的data)变化时视图要更新;在视图上进行了某种操作(如input),初始数据也需要被同步更新。

双向绑定是在单向绑定的基础上扩展的,单向绑定就是初始数据变化,触发视图更新。然后在视图上做一些监听(如input)后,去触发更新初始数据,就形成了双向绑定,例如vue的v-model,我们也知道v-model的本质是:value@input的组合。

观察者模式

发布者-订阅者模式

三要素:

  • 订阅
  • 发布
  • 取消订阅

那么一个观察者看起来是这样的

const pubSub = {
  pub(topic, data) {},
  sub(top, func) {},
  unSub(func) {}
}

const toEat = () => {}

// 订阅
pubSub.sub('eat', toEat)
// 取消订阅
pubSub.unSub(toEat)
// 发布
pubSub.pub('eat', 'eat big')

看起来很熟悉有木有,像极了addEventListener。此处参考深入理解JavaScript系列(32):设计模式之观察者模式

数据劫持模式

通过Object.defineProperty实现

当前版本Vue的响应式核心便是通过Object.defineProperty改写每个属性的set从而监听属性,然后在get中添加订阅实现的。

如果你看过vue的源码,你应该知道在vue的双向绑定中,有几个概念是需要理解的

  • 监听器Observer
  • 订阅者Watcher
  • 用来统一管理订阅与发布操作的Dep
  • 编译器Compile,编译模版(属性、事件与指令)

vue双向绑定的工作流程
初始化时,监听(Observer)data里的属性,Dep收集属性的订阅器(Watcher),然后解析模版,得到属性、事件与指令后,初始化数据到视图并添加该属性到订阅器。随后,属性赋值操作,会触发监听器Observer去Dep里执行与该属性有关的更新函数update。

我们可以实现一个很简单的demo,源码请戳,文件结构如下:

.
├── README.md
├── pubSub.html
└── src
    ├── compile.js  // 初始化时模版编译,并添加订阅者,即new Watcher
    ├── dep.js  // 收集订阅与集中发布
    ├── index.html  // 首页
    ├── mvvm.js  // 构造函数
    ├── observer.js  // 初始化时,最先监听data,当属性变化时,触发订阅者更新
    └── watcher.js  // 订阅者的更新与添加

我们的使用方法如下

<div id="app">
    {{ name }}
    <div>
        <input type="text" :value="name" @input="changeName">
    </div>
</div>

<script>
    const root = new Mvvm({
      el: '#app',
      data: {
        name: '123'
      },
      methods: {
        changeName(e) {
          this.name = e.target.value
        }
      }
    })
</script>

demo如下

demo

在源码的compile中,我只对当前例子的text和Element属性中的:和@做解析处理,以便我们能知道mvvm双向绑定的原理,源码部分如下

compile(node) {
  const attrs = node.attributes
  Array.from(attrs).forEach(attr => {
    let attrName = attr.name
    let fn = this.$vm.$options.methods[attr.value]

    // 这里只处理:和@
    if (/:|@/.test(attrName)) {
      if (/@/.test(attrName)) {
        // 事件绑定的简写
        node.addEventListener(attrName.substring(1), fn.bind(this.$vm), false)
      } else {
        console.log('bind value', attrName)
        this.compileModel(node, attr.value)
      }
      // 执行完后 移除掉
      node.removeAttribute(attrName)
    }
  })
}

最后再强化一遍理解:我们其实要明白,观察者模式,只有在订阅者存在的情况下才有意义,所以,我们必须在初始化的时候,收集完所有的订阅者,也就是说,在compile的时候,Dep到所有的Watcher实例,后续的数据变化, 才会触发该实例的更新操作。

参考:

vue中的mvvm的理解

今天又在网上查mvcmvvm的相关概念,答案太多,看不太清,以至于我到此刻都还不是很明白每一个部分应该干什么事情。因为,貌似在前端领域,mvc的理解与后端是有很大差异的吧。

我们还是直接用vue这个框架来理解mvvm,再去看mvc。

我们都知道

  • M是model,即数据结构模型,用来存储视图需要的属性的
  • V是view,做属性,事件的绑定与监听
  • VM是ViewModel,即view与model之间的桥梁,用来处理视图与数据之间的逻辑

在vue中,拿一个单文件组件示例,template就是view层,data就是model层,Vue抽象的双向绑定及组件script中的其它方法(业务逻辑)就是viewModel了。

我们可以参考wiki中对mvvm的定义来论证我的观点:

MVVM的视图模型是一个值转换器,这意味着视图模型负责从模型中暴露(转换)数据对象,以便轻松管理和呈现对象。在这方面,视图模型比视图做得更多,并且处理大部分视图的显示逻辑。

在查资料的过程中又看到一句话似乎点通了我对网上关于mvc的看法不一致的理解,其大意是后端和前端中的mvc是不一样的,后端中的mvc中的c即代表了前端整个mvc。原文请查看这篇文章的评论,其中@刘宇清的说法我比较认同。

其实,我们只需要搞清楚mvc中它们自己该干什么事情就好了,在前端里:model是原始数据,view就是展示这些数据的,至于怎么展示,数据需要怎样转换,以及视图与原始数据之间的交互(表单提交),是controller该干的事情。

在mvvm中,vm很显然充当了controller的职责。

mvvm的核心便是数据绑定,依旧是参考wiki:

声明性数据和命令绑定隐含在MVVM模式中。在Microsoft解决方案堆中,绑定器是一种名为XAML的标记语言。绑定器使开发人员免于被迫编写样板式逻辑来同步视图模型和视图。在微软的堆之外实现时,声明性数据绑定技术的出现是实现该模式的一个关键因素。

很显然,理解数据绑定对于理解mvvm一类的框架是很有帮助的。

最后,欢迎一起交流。