순수한 view모델과 관련된 내용만 존재
subject와 관련된 기능만 모음
위의 3개 모두 ViewModelValue를 의존한다.
(선이 많이 몰린 대상은 수정하기 어려운 객체이다)
A->B일경우 A는 B를 알지만 B는 A를 모른다.
visit메소드 위임, scan 추상메소드 제공
진짜 scanning 로직 구현
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>
진짜 어렵다
진짜 어렵다에 공감합니다ㅠ 처음 보는 개념들에 익숙해지자라는 마인드로 공부하며 다시 잘 이해해봐야겠습니다. 공부내용들을 깔끔하고 꾸준하게 기록하시는 것 같아요 배워갑니다!