30行のコードのみJavascriptのMVCを実現します.

6728 ワード

09年ごろから、MVCは徐々に先端の領域で異彩を放ってきました.そしてついに過去2015年にReact Nativeの発売によって大爆発を迎えました.アングラーJS、EmbergerJS、Backbone、ReactJS、RiotJS、VueJS….一連の名前の走馬観花式の出現と更に、それらの中のいくつかはだんだんすくすくとして成長しています.いくつかはすでに特定の生態環境において、独自の役割を果たしています.いずれにしても、MVCは、フロントエンドエンジニアの考え方や作業方法に深く影響を与え続けています.
多くの説明MVCの例は具体的な枠組みのある概念から始まります.例えば、BackboneのcollectionやAnglarJSの中のmodelなどです.これはもちろんいい方法です.しかし、フレームワークがフレームワークである理由は、クラス(jQuery)やツールセット(Underscore)ではなく、その背後には多くの優れたデザイン理念とベストプラクティスがあり、これらのデザインのエッセンスが互いに補完されています.
これはこのエッセイの由来です.コンセプトを理解するために生まれたプロトタイプのコードは、簡単であればあるほどいいです.
1.MVCの基礎は観察者モードであり、これはmodelとviewの同期を実現する鍵である.簡単のために、各modelの例には一つのprmitive value値しか含まれていない.

function Model(value) {
  this._value = typeof value === 'undefined' ? '' : value;
  this._listeners = [];
}
Model.prototype.set = function (value) {
  var self = this;
  self._value = value;
  // model      ,           
  //   Javascript         ,           
  //     setTimeout    ,     requestAnimationFrame
  setTimeout(function () {
    self._listeners.forEach(function (listener) {
      listener.call(self, value);
    });
  });
};
Model.prototype.watch = function (listener) {
  //          
  this._listeners.push(listener);
};

// html  :
// : (function () { var model = new Model(); var div1 = document.getElementById('div1'); model.watch(function (value) { div1.innerHTML = value; }); model.set('hello, this is a div'); })();

借助观察者模式,我们已经实现了在调用model的set方法改变其值的时候,模板也同步更新,但这样的实现却很别扭,因为我们需要手动监听model值的改变(通过watch方法)并传入一个回调函数,有没有办法让view(一个或多个dom node)和model更简单的绑定呢?

2. 实现bind方法,绑定model和view


Model.prototype.bind = function (node) {
  //  watch               
  this.watch(function (value) {
    node.innerHTML = value;
  });
};

// html  :
// : (function () { var model = new Model(); model.bind(document.getElementById('div1')); model.bind(document.getElementById('div2')); model.set('this is a div'); })();

通过一个简单的封装,view和model之间的绑定已经初见雏形,即使需要绑定多个view,实现起来也很轻松。注意bind是Function类prototype上的一个原生方法,不过它和MVC的关系并不紧密,笔者又实在太喜欢bind这个单词,一语中的,言简意赅,所以索性在这里把原生方法覆盖了,大家可以忽略。言归正传,虽然绑定的复杂度降低了,这一步依然要依赖我们手动完成,有没有可能把绑定的逻辑从业务代码中彻底解耦呢?

3. 实现controller,将绑定从逻辑代码中解耦

细心的朋友可能已经注意到,虽然讲的是MVC,但是上文中却只出现了Model类,View类不出现可以理解,毕竟HTML就是现成的View(事实上本文中从始至终也只是利用HTML作为View,javascript代码中并没有出现过View类),那Controller类为何也隐身了呢?别急,其实所谓的"逻辑代码"就是一个框架逻辑(姑且将本文的原型玩具称之为框架)和业务逻辑耦合度很高的代码段,现在我们就来将它分解一下。
如果要将绑定的逻辑交给框架完成,那么就需要告诉框架如何来完成绑定。由于JS中较难完成annotation(注解),我们可以在view中做这层标记――使用html的标签属性就是一个简单有效的办法。


function Controller(callback) {
  var models = {};
  //      bind     
  var views = document.querySelectorAll('[bind]');
  //  views       
  views = Array.prototype.slice.call(views, 0);
  views.forEach(function (view) {
    var modelName = view.getAttribute('bind');
    //             model
    models[modelName] = models[modelName] || new Model();
    //         model   
    models[modelName].bind(view);
  });
  //   controller     , models  ,      
  callback.call(this, models);
}





// html:
// : new Controller(function (models) { var model1 = models.model1; model1.set('this is a div'); });

就这么简单吗?就这么简单。MVC的本质就是在controller中完成业务逻辑,并对model进行修改,同时model的改变引起view的自动更新,这些逻辑在上面的代码中都有所体现,并且支持多个view、多个model。虽然不足以用于生产项目,但是希望对大家的MVC学习多少有些帮助。

整理后去掉注释的"框架"代码:


function Model(value) {
  this._value = typeof value === 'undefined' ? '' : value;
  this._listeners = [];
}
Model.prototype.set = function (value) {
  var self = this;
  self._value = value;
  setTimeout(function () {
    self._listeners.forEach(function (listener) {
      listener.call(self, value);
    });
  });
};
Model.prototype.watch = function (listener) {
  this._listeners.push(listener);
};
Model.prototype.bind = function (node) {
  this.watch(function (value) {
    node.innerHTML = value;
  });
};
function Controller(callback) {
  var models = {};
  var views = Array.prototype.slice.call(document.querySelectorAll('[bind]'), 0);
  views.forEach(function (view) {
    var modelName = view.getAttribute('bind');
    models[modelName] = models[modelName] || new Model();
    models[modelName].bind(view);
  });
  callback.call(this, models);
}
後記:
fluxとreduxを勉強しているうちに、ツールの使い方を身につけましたが、それを知っているだけです.だから、ReactJS公式文書で強調されている「Flux eschews MVC in favor of a unidirectional data flow」はよく分かりません.データフローとMVCは衝突しないと思います.なぜReactJSの文書の中でこの両者が対立しているのか分かりません.彼がいても私がいなくても、私がいても彼がいます.やっと决心しました.MVCの定义に戻り、もう一度研究してみます.普段の仕事は大雑把にコピーして贴り付けますが、たまにはわがままにして、かみ砕いてみましょう.MVCとfluxの一方的なデータの流れが似ていると思うのは、MVCと観察者モードの関係がはっきりしていないためかもしれません.MVCは観察者モードに基づいています.fluxも同じです.MVCやflux自体ではありません.このような理解も4つのグループのデザインモデルの原書で実証されました.「The first and perharst-known example of the Observer patternappars in Smalltalk Model/View/Conttrler(MVC)、the user interface framboboork the the Smallatttthe Smatals the the Smaclclclinininininininininininininininincleeemmmclcleeemmclininininininininininininininclattttstststststststststststststststststststststststststservers.」
もし読者がこのような原型のおもちゃに興味があるなら、次の方向を参考にしてください.
  • 1.input類ラベルへの双方向バインディングを実現する
  • .
  • .controllerによって制御されるscopeの正確な制御を実現し、ここで一つのcontrollerはdomツリー全体を制御した
  • .
  • .view層のdom node隠し/表示、作成/廃棄を実現するロジック
  • .virtual domを統合し、dom diffの機能を増加させ、レンダリング効率を向上させる.
  • .注入依存機能を提供し、制御反転
  • を実現する.
  • .innerHTMLの割当内容についてセキュリティチェックを行い、悪意のある注入を防止する
  • .model collectionを実現するロジックであり、ここでは各modelは一つの値
  • だけである.
  • .es 5のsetterを利用してset方法の実現を変更し、modelの修正をより簡単にする.
  • 9.view層に属性とcssの制御を追加する
  • .
  • 10.AnglarJSのような二大括弧の文法をサポートしています.部分だけを結びつける
  • .
  • 完璧な枠组みは无数の试练と修正を経なければならない.ここは最初の一歩で、道はまだ长いです.