Atom
유틸
Observer
js
export const createObserver = () => {
const map = new Map();
return {
notify: createNotify(map),
observe: createObserve(map)
};
};
const createNotify = (map) => (key, value) => {
const fns = map.get(key);
fns.forEach(fn => fn(value))
};
const createObserve = (map) => (key, fn) => {
map.has(key) || map.set(key, new Set());
const observers = map.get(key);
observers.add(fn);
};Ref
js
export const ref = (value) => ({value});Debounce
js
export const debounce = (callback, ms = 100) => {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
callback(...args)
}, ms)
}
};atom
js
import {createObserver} from './utils/observer.js';
import {ref} from './utils/ref.js';
import {debounce} from './utils/debounce.js';
export const atom = (state) => {
const KEY = 'atom';
const observer = createObserver();
const innerState = ref(state);
return {
get: () => innerState.value,
set: (state) => {
innerState.value = state;
observer.notify(KEY)
},
observe: (subscriber) => {
observer.observe(KEY, subscriber);
}
}
};
export const useAtom = (atom) => [atom.get(), atom.set];
export const observeAtoms = (atomsObj, subscriber) => {
const debouncedSubscriber = debounce(subscriber, 1000 / 60);
Object
.values(atomsObj)
.forEach((atom) => {
atom.observe(debouncedSubscriber)
})
};컴포넌트
forEach
js
export const forEach = (obj, fn) => {
Object
.entries(obj)
.forEach(fn)
};defineComponent
js
import {observeAtoms} from './atom.js';
export const defineComponent = (atomsOrRenderFn, renderFn) => {
const component = parentNode => {
if (typeof atomsOrRenderFn === 'function') {
parentNode.appendChild(atomsOrRenderFn());
return;
}
let dom = renderFn(atomsOrRenderFn);
parentNode.appendChild(dom);
observeAtoms(atomsOrRenderFn, () => {
const newDom = renderFn(atomsOrRenderFn);
parentNode.replaceChild(newDom, dom);
dom = newDom
});
};
return component;
};html
js
import {forEach} from './utils/forEach.js';
export const html = (tagName, children, options) => {
const elem = document.createElement(tagName);
appendChild(elem, children);
appendOptions(elem, options);
return elem;
};
const appendChild = (elem, children) => {
if (Array.isArray(children)) {
children.forEach((child) => {
typeof child === 'function' ?
child(elem) :
elem.appendChild(child)
})
} else {
elem.textContent = children
}
};
const appendOptions = (elem, options) => {
if (options) {
const {attrs = {}, events = {}} = options;
forEach(attrs, ([key, value]) => {
elem.setAttribute(key, value)
});
forEach(events, ([key, fn]) => {
elem.addEventListener(key, fn)
});
}
};mount
js
export const mount = (selector, component) => {
component(document.querySelector(selector));
};컴포넌트 사용 예제
마운트
js
import {MainComponent} from './advanced/MainComponent.js';
import {mount} from '../core/mount.js';
mount('#app', MainComponent);Atom
js
import {atom} from '../../core/atom.js';
export const inputAtom = atom('Test');MainComponent
js
import {html} from '../../core/html.js';
import {defineComponent} from '../../core/defineComponent.js';
import {InputComponent} from './InputComponent.js';
import {TextComponent} from './TextComponent.js';
export const MainComponent = defineComponent(() => {
return html('div', [
InputComponent,
TextComponent,
TextComponent,
TextComponent,
TextComponent,
TextComponent,
])
});TextComponent
js
import {defineComponent} from '../../core/defineComponent.js';
import {useAtom} from '../../core/atom.js';
import {html} from '../../core/html.js';
import {inputAtom} from './inputAtom.js';
export const TextComponent = defineComponent({inputAtom}, ({inputAtom}) => {
const [input] = useAtom(inputAtom);
return html('p', input)
});- Atom 변경 시, 다시 랜더링이 필요한 컴포넌트는 이와 같이 사용한다.
defineComponent({atomA, atomB, ...}, renderFn)
InputComponent
js
import {defineComponent} from '../../core/defineComponent.js';
import {useAtom} from '../../core/atom.js';
import {html} from '../../core/html.js';
import {inputAtom} from './inputAtom.js';
export const InputComponent = defineComponent(() => {
const [input, setInput] = useAtom(inputAtom);
return html('input', [], {
attrs: {
value: input,
},
events: {
input: event => setInput(event.target.value)
}
})
});고민
Atom 변경 시, 다시 랜더링되면 Focus와 입력이 종료된다. 그래서 Atom 변경 시, 다시 랜더링이 되지 않게 defineComponent의 첫번째 인자에 atom을 주입 못했다.
다시 랜더링 됬을 때, form 요소가 다시 랜더링 되지 않게 처리가 필요하다.