Cet article présente principalement la liaison bidirectionnelle de l'analyse du framework MVVM. Maintenant, je le partage avec vous et le donne comme référence.
MVVM Framework
Une tendance évidente du développement front-end ces dernières années est la migration de l'architecture du modèle MVC traditionnel vers le modèle MVVM. Sous MVC traditionnel, la page entière sera actualisée après l'interaction des données entre le front-end et le back-end actuels, ce qui entraînera une mauvaise expérience utilisateur. Par conséquent, nous communiquons avec l'API REST de la passerelle via Ajax et actualisons une certaine section de la page de manière asynchrone pour optimiser et améliorer l'expérience.
Concepts de base du framework MVVM
Dans le framework MVVM, View (view) et Model (data) ne peuvent pas communiquer directement Oui , il y a ViewModel comme intermédiaire entre eux, qui agit en tant qu'observateur. Lorsque l'utilisateur exploite la vue, le ViewModel détecte le changement, puis informe le modèle du changement correspondant. À l'inverse, lorsque le modèle (données) change, le ViewModel peut également détecter le changement, provoquant la mise à jour de la vue en conséquence. Ce processus de va-et-vient est ce que nous appelons une liaison bidirectionnelle.
Scénarios d'application du framework MVVM
Les avantages du framework MVVM sont évidents : lorsque le front-end opère sur des données, les données peuvent être conservées via des requêtes Ajax , changez simplement La partie du contenu des données dans le dom qui doit être modifiée sans avoir à actualiser la page entière. Surtout sur mobile, rafraîchir la page coûte trop cher. Bien que certaines ressources soient mises en cache, les DOM, CSS et JS de la page seront ré-analysés par le navigateur, de sorte que les pages mobiles sont généralement transformées en applications SPA monopage. Sur cette base, de nombreux frameworks MVVM sont nés, comme React.js, Vue.js, Angular.js, etc.
Une implémentation simple du framework MVVM
simule le flux de liaison bidirectionnel de Vue et implémente un framework MVVM simple. Vous pouvez voir la ligne pointillée depuis le. image ci-dessus Dans le carré se trouve la couche intermédiaire ViewModel mentionnée plus haut, qui joue le rôle d'observateur. De plus, vous pouvez constater que la vue vers le modèle dans le flux de liaison bidirectionnelle est en fait implémentée via la fonction d'écoute d'événements de l'entrée. Si elle est remplacée par React (flux de liaison unidirectionnel), elle sera transmise à. l'outil de gestion d'état (tel que Redux) à cette étape. De plus, le modèle à afficher dans le flux de liaison bidirectionnel est en fait implémenté de la même manière par chaque framework MVVM. La méthode principale utilisée est Object.defineProperty(). Grâce à cette méthode, les données peuvent être détournées et lorsque les données sont modifiées. , il peut être capturé en conséquence pour un traitement ultérieur.
L'implémentation de Mvvm (fichier d'entrée)
appelle généralement le framework Mvvm comme ceci
const vm = new Mvvm({ el: '#app', data: { title: 'mvvm title', name: 'mvvm name' }, })
Mais dans ce cas, si vous souhaitez obtenir l'attribut title, vous devez l'obtenir sous la forme de vm.data.title. Pour que vm.title obtienne l'attribut title, ajoutez une méthode proxy au prototype de. Mvvm. Le code est le suivant :
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 } }) } }
Après avoir implémenté la méthode proxy, nous passons à la mise en œuvre du processus principal
function Mvvm (options) { this.data = options.data // ... observe(this.data) new Compile(options.el, this) }
implémentation de l'observateur (observer)
responsabilités de l'observateur Il s'agit de surveiller les modifications du modèle (objet JS). La partie principale consiste à utiliser les méthodes get et set de Object.defineProperty() lorsque la valeur du modèle (objet JS). ) doit être obtenu, la méthode get sera automatiquement appelée ; lorsque la valeur du modèle (objet JS) est modifiée, la méthode set sera automatiquement appelée, réalisant ainsi le détournement de données, le code est le suivant :
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 } } }) }
Exécutez le code et vous pouvez voir que la valeur de sortie de la console change newValue:1 oldValue:0 Ceci complète la logique de l'observateur.
La relation entre Dep (tableau d'abonnés) et watcher (abonné)
Après avoir observé les changements, nous informons toujours des groupes spécifiques de personnes et les laissons prendre des décisions. par conséquent. Afin de le comprendre plus facilement, nous pouvons considérer l'abonnement comme l'abonnement à un compte officiel WeChat. Lorsque le contenu du compte officiel WeChat est mis à jour, il poussera (mettra à jour) le contenu aux personnes qui s'y abonnent.
Il y a des milliers de personnes qui s'abonnent au même compte officiel WeChat, donc la première chose qui me vient à l'esprit est d'utiliser new Array() pour stocker ces personnes (html nœuds) Barre. Nous avons donc le code suivant :
// 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() } } }) }
À première vue, le code est relativement fluide, mais il peut rester bloqué dans Dep.target et sub.update, nous tournons donc naturellement notre attention vers watcher,
// 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 框架,不足之处在所难免,欢迎指正。
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!