コード断片86オブジェクト向けJavaScript-第2部第3部(ステップ31)


MVVMの適用を試みる

  • ViewModel
  • binder
  • scanner
  • client
  • 1. ViewModel

    const ViewModel = class {
      static #private = Symbol();	// #을 통해 private로 외부에서 접근불가
    
      static get(data) {
        return new ViewModel(this.#private, data);	//this는 클래스를 나타냄
      }
    
      styles = {};
      attributes = {};
      properties = {};
      events = {};
    
      constructor(checker, data) {
        if (checker != ViewModel.#private) throw 'useViewModel.get()!';
        Object.entries(data).forEach(([k, v]) => {
          switch (k) {
            case'styles':
              this.styles = v;
              break;
            case'attributes':
              this.attributes = v;
              break;
            case'properties':
              this.properties = v;
              break;
            case'events':
              this.events = v;
              break;
            default:
              this[k] = v;
          }
        }); 
        Object.seal(this);
      }
    };

    2. Binder

    const BinderItem = class {
      el;
      viewmodel;
    
      constructor(el, viewmodel, _0 = type(el, HTMLElement), _1 = type(viewmodel, 'string')) {
        this.el = el;
        this.viewmodel = viewmodel;
        Object.freeze(this);  // 불변객체화
      }
    };
    const Binder = class {
      #items = new Set;
    
      add(v, _ = type(v, BinderItem)) {
        this.#items.add(v);
      }
    
      render(viewmodel, _ = type(viewmodel, ViewModel)) {
        this.#items.forEach(item => {
          const vm = type(viewmodel[item.viewmodel], ViewModel), el = item.el;
          Object.entries(vm.styles).forEach(([k, v]) => el.style[k] = v);
          Object.entries(vm.attributes).forEach(([k, v]) => el.setAttribute(k, v));
          Object.entries(vm.properties).forEach(([k, v]) => el[k] = v);
          Object.entries(vm.events).forEach(([k, v]) => el['on' + k] = e => v.call(el, e, viewmodel)); // this 를 el 로 바인딩
        });
      }
    };
    対応するBinderで必要な機能を追加または処理する機能.(制御局)

    3. Scanner

    const Scanner = class {
      scan(el, _ = type(el, HTMLElement)) {
        const binder = newBinder;
        this.checkItem(binder, el);
    
        const stack = [el.firstElementChild];
        let target;   // 임시변수
        while (target = stack.pop()) {
          this.checkItem(binder, target);
          if (target.firstElementChild) stack.push(target.firstElementChild);
          if (target.nextElementSibling) stack.push(target.nextElementSibling);
        }
        return binder;
      }
    
      checkItem(binder, el) {
        const vm = el.getAttribute('data-viewmodel');
        if (vm) binder.add(new BinderItem(el, vm));
      }
    };

    4. Client

    const viewmodel = ViewModel.get({
      wrapper: ViewModel.get({styles: {width: '50%', background: '#ffa', cursor: 'pointer'}}),
      title: ViewModel.get({properties: {innerHTML: 'Title'}}),
      contents: ViewModel.get({properties: {innerHTML: 'Contents'}}),
    });
    
    const scanner = new Scanner;
    const binder = scanner.scan(document.querySelector('#target'));
    binder.render(viewmodel);

    クライアント変換

    const viewmodel = ViewModel.get({
      isStop: false,
      changeContents() {
        this.wrapper.styles.background = `rgb(${parseInt(Math.random() * 150) + 100},${...},${...})`;
        this.contents.properties.innerHTML = Math.random().toString(16).replace('.', '');
      }, wrapper: ViewModel.get({
        styles: {width: '50%', background: '#ffa', cursor: 'pointer'}, events: {
          click(e, vm) {
            vm.isStop = true;
          },
        },
      }),
      // ...
    });
    
    const f = _ => {
      viewmodel.changeContents();
      binder.render(viewmodel);
      if (!viewmodel.isStop) requestAnimationFrame(f);
    };
    requestAnimationFrame(f);

    完全なコード

    <!DOCTYPE html>
    <html lang="en">
    <head>
    	<meta charset="UTF-8">
    	<title>MVVM</title>
    </head>
    <body>
    <section id="target" data-viewmodel="wrapper">
      <h2 data-viewmodel="title"></h2>
      <section data-viewmodel="contents"></section>
    </section>
    <script>
    
    const type = (target, type) => {
      if (typeof type == "string") {
        if (typeof target != type) throw `invalid type ${target} : ${type}`
      } else if (!(target instanceof type)) {
        throw `invalid type ${target} : ${type}`
      }
      return target;
    }
    
    const ViewModel = class {
      static #private = Symbol()
      static get (data) {
        return new ViewModel(this.#private, data)
      } 
      styles = {}; attributes = {}; properties = {}; events = {};
      constructor(checker, data) {
        if (checker != ViewModel.#private) throw 'use ViewModel.get()!'
        Object.entries(data).forEach(([k, v]) => {
          switch (k) {
            case 'styles': this.styles = v; break;
            case 'attributes': this.attributes = v; break;
            case 'properties': this.properties = v; break;
            case 'events': this.events = v; break;
            default: this[k] = v;
          }
        });
        Object.seal(this); // Value를 바꿀 순 있지만 Key를 추가할 순 없다.
      }
    }
    
    const BinderItem = class {
      el; viewmodel;
      constructor (el, viewmodel, _0 = type(el, HTMLElement), _1 = type(viewmodel, 'string')) {
        this.el = el
        this.viewmodel = viewmodel
        Object.freeze(this)
      }
    }
    
    const Binder = class {
      #items = new Set()
      add (v, _ = type(v, BinderItem)) { this.#items.add(v) }
      render (viewmodel, _ = type(viewmodel, ViewModel)) {
        this.#items.forEach(item => {
          const vm = type(viewmodel[item.viewmodel], ViewModel), el = item.el
          Object.entries(vm.styles).forEach(([k, v]) => el.style[k] = v)
          Object.entries(vm.attributes).forEach(([k, v]) => el.attribute[k] = v)
          Object.entries(vm.properties).forEach(([k, v]) => el[k] = v)
          Object.entries(vm.events).forEach(([k, v]) => el[`on${k}`] = e => v.call(el, e, viewmodel))
        })
      }
    }
    
    const Scanner = class {
      scan (el, _ = type(el, HTMLElement)) {
        const binder = new Binder();
        this.checkItem(binder, el)
        const stack = [el.firstElementChild]
    
        // HTML 전체에 대한 순회
        let target
        while (target = stack.pop()) {
          this.checkItem(binder, target)
          if (target.firstElementChild) stack.push(target.firstElementChild)
          if (target.nextElementSibling) stack.push(target.nextElementSibling)
        }
        return binder;
      }
      checkItem (binder, el) {
        const vm = el.getAttribute('data-viewmodel')
        if (vm) binder.add(new BinderItem(el, vm))
      }
    }
    
    const scanner = new Scanner()
    const binder = scanner.scan(document.querySelector('#target'))
    
    const getRandom = () => parseInt(Math.random() * 150) + 100
    const viewmodel2 = ViewModel.get({
      isStop: false,
      changeContents () {
        this.wrapper.styles.background = `rgb(${getRandom()},${getRandom()},${getRandom()})`
        this.contents.properties.innerHTML = Math.random().toString(16).replace('.', '')
        binder.render(viewmodel2)
        // viewmodel을 갱신하면, binder가 viewmodel을 view에 rendering 한다.
        // 즉, '인메모리 객체'만 수정하면 된다.
      },
      wrapper: ViewModel.get({
        styles: { width: '50%', background: '#fff', cursor: 'pointer' },
        events: { click(e, vm) { vm.isStop = true } }
      }),
      title: ViewModel.get({
        properties: { innerHTML: 'Title' }
      }),
      contents: ViewModel.get({
        properties: { innerHTML: 'Contents' }
      })
    })
    const f = () => {
      viewmodel2.changeContents()
      binder.render(viewmodel2)
      if (!viewmodel2.isStop) requestAnimationFrame(f)
    }
    requestAnimationFrame(f)
    </script>
    </body>
    </html>
    簡単に言えば、必要なViewModelを変更するだけで、スタイル、イベント、プロパティに関連するコンテンツを理解して追加し、プロセッサを登録してスタイルを適用することができます.(Binderでこれらのキャラクタを処理する制御反転が発生)

    整理する

  • new Scannerスキャナインスタンスの作成
  • scanner.スキャンメソッドの実行
  • 対応する方法でバインディングを作成します.add実行後に接続を適用するBinnerItemdata-viewmodelプロパティを検証し、戻ってきたconst binder
  • にbinderを保存します.
  • viewmodel.get実行後return new ViewModel()でインスタンスを作成
  • コンストラクション関数のswitch文を使用してインスタンスを作成するために必要なデータ値を適用し、オブジェクトキーに基づいて値を適用し、ViewModel 2
  • を生成します.
  • requestAnimationFrame(f)は、引き続きViewModel 2の内容を変更し、binder.renderで適用します.

  • Binderは#itemsで構成され、その配列内容はそれぞれBinderItemを含む.(最初のBinderItemの内容はBinderの0番目の配列BinderItemの属性の内容と同じです.)

    に感銘を与える


    無理に理解できそうですが、コードで書かせてもらうと、本当に難しいと思い、今はできません.=>最后にもっとやれば分かるよ.頑張りましょう.

    ソース


    せんじく

    再整理方法

  • ScannerでHTML Elementを含むコンテンツをインポートします.
  • Scanner.スキャンによるバインディングの作成
  • checkItem実行=>scan関数のwhile文タグ内のタグをチェックし、checkItem
  • を実行
  • checkitemでdata-viewmodel属性が含まれている場合、binderのコンテンツ
  • にBinderItemとして追加されます.
  • return binder.(binder値にBinder Item形状のタグを作成)
  • 実行requestAnimationFrame
  • viewModel2.ChangeContentsの実行
  • binder.実行render()
  • viewModel 2は引き続き純化
  • viewModel 2ブラウザでクリックしたときに停止します.
  • これにより、HTML Elementはスキャナの影響で認識され、Binderに関連付けられ、必要に応じてViewModelで置き換え処理される.