ホームページ  >  記事  >  ウェブフロントエンド  >  MVVM フレームワークが双方向バインディングを解決する方法

MVVM フレームワークが双方向バインディングを解決する方法

亚连
亚连オリジナル
2018-06-09 16:10:402093ブラウズ

この記事では主に MVVM フレームワーク分析の双方向バインディングについて紹介しますので、参考にしてください。

MVVM フレームワーク

近年のフロントエンド開発の明らかなトレンドは、従来の MVC モデルから MVVM モデルへのアーキテクチャの移行です。従来の MVC では、現在のフロントエンドとバックエンドの間でデータがやり取りされた後にページ全体が更新されるため、ユーザー エクスペリエンスが低下します。したがって、Ajax を介してゲートウェイ REST API と通信し、ページの特定のセクションを非同期的に更新して、エクスペリエンスを最適化および強化します。

MVVM フレームワークの基本概念

MVVM フレームワークでは、View (ビュー) と Model (データ) は直接通信できません。ViewModel はそれらの間の仲介者であり、オブザーバーとして機能します。ユーザーがビューを操作すると、ビューモデルは変更を感知し、対応する変更をモデルに通知します。逆に、モデル (データ) が変更されると、ビューモデルもその変更を感知し、それに応じてビューを更新します。この前後のプロセスは、双方向バインディングとして知られています。

MVVM フレームワークのアプリケーション シナリオ

MVVM フレームワークの利点は明白です。フロントエンドがデータを操作するとき、データは Ajax リクエストを通じて保持され、必要なデータ コンテンツの部分のみが保持されます。 dom で変更するには、ページ全体を更新する必要があります。特にモバイルでは、ページの更新にコストがかかりすぎます。一部のリソースはキャッシュされますが、ページの DOM、CSS、および JS はブラウザーによって再解析されるため、通常、モバイル ページは SPA シングルページ アプリケーションになります。これに基づいて、React.js、Vue.js、Angular.js などの多くの MVVM フレームワークが誕生しました。

MVVM フレームワークの単純な実装

は、Vue の双方向バインディング フローをシミュレートし、単純な MVVM フレームワークを実装します。上の図からわかるように、点線の四角形は前述の ViewModel 中間層です。観察者の役割を果たします。さらに、双方向バインディング フローの View to Model は、実際には入力のイベント リスニング機能を通じて実装されていることがわかります。React (一方向バインディング フロー) に切り替えると、それが に引き継がれます。このステップでは、状態管理ツール (Redux など) を使用します。さらに、双方向バインディング フローのビューへのモデルは、実際には各 MVVM フレームワークによって同じ方法で実装され、使用されるコア メソッドは Object.defineProperty() によって、データが変更されたときにデータをハイジャックすることができます。に応じて、後続の処理のために変更することができます。

Mvvm(エントリーファイル)の実装

は、通常このようにMvvmフレームワークを呼び出します

const vm = new Mvvm({
      el: '#app',
      data: {
       title: 'mvvm title',
       name: 'mvvm name'
      },
     })

しかし、この場合、title属性を取得したい場合は、次の形式で取得する必要がありますvm.data.title。vm .title が title 属性を取得できるようにするため、プロキシ メソッドを Mvvm プロトタイプに追加します。コードは次のとおりです。

function Mvvm (options) {
 this.data = options.data
 const self = this
 Object.keys(this.data).forEach(key =>
  self.proxyKeys(key)
 )
}
Mvvm.prototype = {
 proxyKeys: function(key) {
  const self = this
  Object.defineProperty(this, key, {
   get: function () { // 这里的 get 和 set 实现了 vm.data.title 和 vm.title 的值同步
    return self.data[key]
   },
   set: function (newValue) {
    self.data[key] = newValue
   }
  })
 }
}

プロキシ メソッドを実装した後、次の実装を入力します。メインプロセス

function Mvvm (options) {
 this.data = options.data
 // ...
 observe(this.data)
 new Compile(options.el, this)
}

observer (オブザーバー) の実装

オブザーバーの役割は、Model (JS オブジェクト) の変更を監視することです。中心となる部分は、Object.defineProperty() の get メソッドと set メソッドを使用することです。 Model (JS オブジェクト) の値を取得する場合は get メソッドが自動的に呼び出され、Model (JS オブジェクト) の値が変更されると自動的に set メソッドが呼び出されます。 、コードは次のとおりです。

let data = {
 number: 0
}
observe(data)
data.number = 1 // 值发生变化
function observe(data) {
 if (!data || typeof(data) !== 'object') {
  return
 }
 const self = this
 Object.keys(data).forEach(key =>
  self.defineReactive(data, key, data[key])
 )
}
function defineReactive(data, key, value) {
 observe(value) // 遍历嵌套对象
 Object.defineProperty(data, key, {
  get: function() {
   return value
  },
  set: function(newValue) {
   if (value !== newValue) {
    console.log('值发生变化', 'newValue:' + newValue + ' ' + 'oldValue:' + value)
    value = newValue
   }
  }
 })
}

コードを実行すると、コンソールの出力値が newValue:1 oldValue:0 に変化することがわかります。この時点で、オブザーバー ロジックは完了しています。

Dep (加入者配列) と監視者 (加入者) の関係

変化を観察した後は、常に特定の人々のグループに通知し、それに応じて処理してもらう必要があります。わかりやすくするために、サブスクリプションとは、WeChat 公式アカウントのコンテンツが更新されると、そのコンテンツを購読した人にプッシュ (更新) することと考えることができます。

同じ WeChat 公式アカウントに登録している人は何千人もいます。そのため、最初に思いつくのは、new Array() を使用してこれらの人々 (HTML ノード) を保存することです。したがって、次のコードがあります:

// observer.js
function Dep() {
 this.subs = [] // 存放订阅者
}
Dep.prototype = {
 addSub: function(sub) { // 添加订阅者
  this.subs.push(sub)
 },
 notify: function() { // 通知订阅者更新
  this.subs.forEach(function(sub) {
   sub.update()
  })
 }
}
function observe(data) {...}
function defineReactive(data, key, value) {
 var dep = new Dep()
 observe(value) // 遍历嵌套对象
 Object.defineProperty(data, key, {
  get: function() {
   if (Dep.target) { // 往订阅器添加订阅者
    dep.addSub(Dep.target)
   }
   return value
  },
  set: function(newValue) {
   if (value !== newValue) {
    console.log('值发生变化', 'newValue:' + newValue + ' ' + 'oldValue:' + value)
    value = newValue
    dep.notify()
   }
  }
 })
}

コードは一見すると比較的スムーズですが、Dep.target と sub.update でスタックする可能性があるため、自然にウォッチャーに注意を向けます

// watcher.js
function Watcher(vm, exp, cb) {
 this.vm = vm
 this.exp = exp
 this.cb = cb
 this.value = this.get()
}
Watcher.prototype = {
 update: function() {
  this.run()
 },
 run: function() {
  // ...
  if (value !== oldVal) {
   this.cb.call(this.vm, value) // 触发 compile 中的回调
  }
 },
 get: function() {
  Dep.target = this // 缓存自己
  const value = this.vm.data[this.exp] // 强制执行监听器里的 get 函数
  Dep.target = null // 释放自己
  return value
 }
}

从代码中可以看到当构造 Watcher 实例时,会调用 get() 方法,接着重点关注 const value = this.vm.data[this.exp] 这句,前面说了当要获取 Model(JS 对象) 的值时,会自动调用 Object.defineProperty 的 get 方法,也就是当执行完这句的时候,Dep.target 的值传进了 observer.js 中的 Object.defineProperty 的 get 方法中。同时也一目了然地在 Watcher.prototype 中发现了 update 方法,其作用即触发 compile 中绑定的回调来更新界面。至此解释了 Observer 中 Dep.target 和 sub.update 的由来。

来归纳下 Watcher 的作用,其充当了 observer 和 compile 的桥梁。

1 在自身实例化的过程中,往订阅器(dep) 中添加自己

2 当 model 发生变动,dep.notify() 通知时,其能调用自身的 update 函数,并触发 compile 绑定的回调函数实现视图更新

最后再来看下生成 Watcher 实例的 compile.js 文件。

compile(编译) 的实现

首先遍历解析的过程有多次操作 dom 节点,为提高性能和效率,会先将跟节点 el 转换成 fragment(文档碎片) 进行解析编译,解析完成,再将 fragment 添加回原来的真实 dom 节点中。代码如下:

function Compile(el, vm) {
 this.vm = vm
 this.el = document.querySelector(el)
 this.fragment = null
 this.init()
}
Compile.prototype = {
 init: function() {
  if (this.el) {
   this.fragment = this.nodeToFragment(this.el) // 将节点转为 fragment 文档碎片
   this.compileElement(this.fragment) // 对 fragment 进行编译解析
   this.el.appendChild(this.fragment)
  }
 },
 nodeToFragment: function(el) {
  const fragment = document.createDocumentFragment()
  let child = el.firstChild // △ 第一个 firstChild 是 text
  while(child) {
   fragment.appendChild(child)
   child = el.firstChild
  }
  return fragment
 },
 compileElement: function(el) {...},
}

这个简单的 mvvm 框架在对 fragment 编译解析的过程中对 {{}} 文本元素、v-on:click 事件指令、v-model 指令三种类型进行了相应的处理。

Compile.prototype = {
 init: function() {
  if (this.el) {
   this.fragment = this.nodeToFragment(this.el) // 将节点转为 fragment 文档碎片
   this.compileElement(this.fragment) // 对 fragment 进行编译解析
   this.el.appendChild(this.fragment)
  }
 },
 nodeToFragment: function(el) {...},
 compileElement: function(el) {...},
 compileText: function (node, exp) { // 对文本类型进行处理,将 {{abc}} 替换掉
  const self = this
  const initText = this.vm[exp]
  this.updateText(node, initText) // 初始化
  new Watcher(this.vm, exp, function(value) { // 实例化订阅者
   self.updateText(node, value)
  })
 },
 compileEvent: function (node, vm, exp, dir) { // 对事件指令进行处理
  const eventType = dir.split(':')[1]
  const cb = vm.methods && vm.methods[exp]
  if (eventType && cb) {
   node.addEventListener(eventType, cb.bind(vm), false)
  }
 },
 compileModel: function (node, vm, exp) { // 对 v-model 进行处理
  let val = vm[exp]
  const self = this
  this.modelUpdater(node, val)
  node.addEventListener('input', function (e) {
   const newValue = e.target.value
   self.vm[exp] = newValue // 实现 view 到 model 的绑定
  })
 },
}

在上述代码的 compileTest 函数中看到了期盼已久的 Watcher 实例化,对 Watcher 作用模糊的朋友可以往上回顾下 Watcher 的作用。另外在 compileModel 函数中看到了本文最开始提到的双向绑定流中的 View 到 Model 是借助 input 监听事件变化实现的。

项目地址

本文记录了些阅读 mvvm 框架源码关于双向绑定的心得,并动手实践了一个简版的 mvvm 框架,不足之处在所难免,欢迎指正。

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

通过微信小程序如何实现验证码获取倒计时效果

ES6 迭代器和 for.of循环(详细教程)

在vue中使用better-scroll滚动插件

在VUE + UEditor中如何实现单图片跨域上传功能

以上がMVVM フレームワークが双方向バインディングを解決する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。