코드스피츠 86 객체지향 자바스크립트 - 4회차 part2 (step 33)

KHW·2021년 4월 2일
1

js-study

목록 보기
21/39
post-custom-banner

설계목표

설계내용

viewModel

순수한 view모델과 관련된 내용만 존재

ViewModelSubject

subject와 관련된 기능만 모음

ViewModelListener

ViewModelValue

위의 3개 모두 ViewModelValue를 의존한다.
(선이 많이 몰린 대상은 수정하기 어려운 객체이다)

A->B일경우 A는 B를 알지만 B는 A를 모른다.

Scanner

visit메소드 위임, scan 추상메소드 제공

DomScanner

진짜 scanning 로직 구현

Visitor

DomVisitor

Binder

scan의 결과인 binder, ViewModel을 알고 ViewModelListener를 상속받고 updated를 알기 위해 ViewModelValue를 알아야한다.

전체코드

<!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 ViewModelListener = class {
  viewmodelUpdated(updated){throw 'override'}
}

const ViewModelSubject = class extends ViewModelListener {
  static #subjects = new Set
  static #inited = false
  static notify () {
    const f = () => {
      this.#subjects.forEach(v => {
        if (v.#info.size) {
          v.notify()
          v.clear()
        }
      })
      if (this.#inited) requestAnimationFrame(f)
    }
    requestAnimationFrame(f)
  }
  static watch (vm, _ = type(vm, ViewModelListener)) {
    this.#subjects.add(vm)
    if (!this.#inited) {
      this.#inited = true
      this.notify()
    }
  }
  static unwatch (vm, _ = type(vm, ViewModelListener)) {
    this.#subjects.delete(vm)
    if (!this.#subjects.size) this.#inited = false
  }
  #info = new Set
  #listeners = new Set
  add (v, _ = type(v, ViewModelValue)) { this.#info.add(v) }
  clear () { this.#info.clear() }
  addListener (v, _ = type(v, ViewModelListener)) {
    this.#listeners.add(v)
    ViewModelSubject.watch(this)
  }
  removeListener (v, _ = type(v, ViewModelListener)) {
    this.#listeners.delete(v)
    if (!this.#listeners.size) ViewModelSubject.unwatch(this)
  }
  notify () { this.#listeners.forEach(v => v.viewmodelUpdated(this.#info)) }
}


const ViewModel = class extends ViewModelSubject {
  static get (data) { return new ViewModel(data) }
  styles = {}; attributes = {}; properties = {}; events = {};
  #subKey = ''
  #parent = null
  get subKey () { return this.#subKey } // read only
  get parent () { return this.#parent }
  setParent (parent, subKey) {
    this.#parent = type(parent, ViewModel)
    this.#subKey = subKey
    this.addListener(parent)
  }
  static descriptor = (vm, category, k, v) => ({
    enumerable: true,
    get: () => v,
    set (newV) {
      v = newV
      vm.add(new ViewModelValue(vm.subKey, category, k, v))
    }
  })
  static define = (vm, category, obj) => (
    Object.defineProperties(
      obj,
      Object.entries(obj)
            .reduce((r, [k, v]) => (r[k] = ViewModel.descriptor(vm, category, k, v), r), {})
    )
  )
  constructor(data, _ = type(data, 'object')) {
    super();
    Object.entries(data).forEach(([k, v]) => {
      if('styles,attributes,properties'.includes(k)) {
        if(!v || typeof v != 'object') throw `invalid object k: ${k}, v:${v}`
        this[k] = ViewModel.define(this, k, v)
      } else {
        Object.defineProperty(this, k, ViewModel.descriptor(this, '', k, v))
        if (v instanceof ViewModel) {
          v.setParent(this, k)
        }
      }
    })
    Object.seal(this)
  }
  viewmodelUpdated (updated) { updated.forEach(v => this.add(v)) }
}

const ViewModelValue = class {
  subKey; category; k; v;
  constructor (subKey, category, k, v) {
    Object.assign(this, { subKey, category, k, v })
    Object.freeze(this)
  }
}

const BinderItem = class {
  el; vmName;
  constructor (el, vmName, _0 = type(el, HTMLElement), _1 = type(vmName, 'string')) {
    this.el = el
    this.vmName = vmName
    Object.freeze(this)
  }
}

const Binder = class extends ViewModelListener {
  #items = new Set;
  #processors = {};
  add (v, _ = type(v, BinderItem)) { this.#items.add(v) }
  addProcessor (v, _ = type(v, Processor)) { this.#processors[v.category] = v }
  render (viewmodel, _ = type(viewmodel, ViewModel)) {
    const processores = Object.entries(this.#processors)
    this.#items.forEach(({ vmName, el }) => {
      const vm = type(viewmodel[vmName], ViewModel)
      processores.forEach(([pk, processor]) => {
        Object.entries(vm[pk]).forEach(([k, v]) => {
          processor.process(vm, el, k, v)
        })
      })
    })
  }

  watch(viewmodel, _ = type(viewmodel, ViewModel)){
    viewmodel.addListener(this)
    this.render(viewmodel)
  }

  unwatch(viewmodel, _ = type(viewmodel, ViewModel)){
    viewmodel.removeListener(this)
  }

  viewmodelUpdated (updated) {
    const items = {}
    this.#items.forEach(({ vmName, el }) => {
      items[vmName] = [type(rootViewModel[vmName], ViewModel), el]
    })
    updated.forEach(({ subKey, category, k, v }) => {
      if (!items[subKey]) return
      const [vm, el] = items[subKey], processor = this.#processors[category]
      if (!el || !processor) return
      processor.process(vm, el, k, v)
    })
  }
}

const Processor = class {
  category;
  constructor (category) {
    this.category = category
    Object.freeze(this)
  }
  process (vm, el, k, v, _0 = type(vm, ViewModel),
                         _1 = type(el, HTMLElement),
                         _2 = type(k, "string")) {
    this._process(vm, el, k, v)
  }
  _process (vm, el, k, v) { throw 'override' }
}

const Scanner = class {
  #visitor
  constructor (visitor, _ = type(visitor, Visitor)) {
    this.#visitor = visitor
  }
  visit (f, target) { this.#visitor.visit(f, target) }
  scan (target) { throw `override` }
}

const DomScanner = class extends Scanner {
  constructor (visitor, _ = type(visitor, DomVisitor)) {
    super(visitor)
  }
  scan (target, _ = type(target, HTMLElement)) {
    const binder = new Binder
    const f = el => {
      const vm = el.getAttribute('data-viewmodel')
      if (vm) binder.add(new BinderItem(el, vm))
    }
    f(target)
    this.visit(f, target)
    return binder
  }
}

const Visitor = class {
  visit (action, target, _ = type(action, 'function')) {
    throw 'override'
  }
}
const DomVisitor = class extends Visitor {
  visit (action, target , _0 = type(action, 'function'), _1 = type(target, HTMLElement)) {
    const stack = []
    let curr = target.firstElementChild
    do {
      action(curr)
      if (curr.firstElementChild) stack.push(curr.firstElementChild)
      if (curr.nextElementSibling) stack.push(curr.nextElementSibling)
    } while (curr = stack.pop())
  }
}

const scanner = new DomScanner(new DomVisitor)
const binder = scanner.scan(document.querySelector('#target'))
binder.addProcessor(new class extends Processor {
  _process (vm, el, k, v) { el.style[k] = v }
}('styles'))
binder.addProcessor(new class extends Processor {
  _process (vm, el, k, v) { el.setAttribute(k, v) }
}('attributes'))
binder.addProcessor(new class extends Processor {
  _process (vm, el, k, v) { el[k] = v }
}('properties'))
binder.addProcessor(new class extends Processor {
  _process (vm, el, k, v) { el[`on${k}`] = e => v.call(el, e, vm) }
}('events'))

const getRandom = () => parseInt(Math.random() * 150) + 100
const wrapper = ViewModel.get({
  styles: { width: '50%', background: '#ffa', cursor: 'pointer' },
  events: {
    click(e, vm) {
      vm.parent.isStop = true
    }
  }
})
const title = ViewModel.get({ properties: { innerHTML: 'Title' } })
const contents = ViewModel.get({ properties: { innerHTML: 'Contents' } })
const rootViewModel = ViewModel.get({
  isStop: false,
  changeContents () {
    this.wrapper.styles.background = `rgb(${getRandom()},${getRandom()},${getRandom()})`
    this.contents.properties.innerHTML = Math.random().toString(16).replace('.', '')
  },
  wrapper, title, contents
})
binder.watch(rootViewModel)
const f = () => {
  rootViewModel.changeContents()
  if (!rootViewModel.isStop) requestAnimationFrame(f)
}
requestAnimationFrame(f)
</script>
</body>
</html>

느낀점

진짜 어렵다

profile
나의 하루를 가능한 기억하고 즐기고 후회하지말자
post-custom-banner

1개의 댓글

comment-user-thumbnail
2021년 4월 14일

진짜 어렵다에 공감합니다ㅠ 처음 보는 개념들에 익숙해지자라는 마인드로 공부하며 다시 잘 이해해봐야겠습니다. 공부내용들을 깔끔하고 꾸준하게 기록하시는 것 같아요 배워갑니다!

답글 달기