2019.08 Vue2 컴포넌트 구현
컴포넌트 사용 예제
컴포넌트 마운트
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<script type="module" src="./core/fp.js"></script>
<script type="module" src="./core/store.js"></script>
<script type="module" src="./core/helper.js"></script>
<script type="module" src="./core/component.js"></script>
<script type="module" src="./guide/ButtonComponent.js"></script>
<script type="module" src="./guide/CounterComponent.js"></script>
<script type="module" src="./guide/ReactiveComponent/NowComponent.js"></script>
<script type="module" src="./guide/ReactiveComponent/NowControllerComponent.js"></script>
<script type="module" src="./guide/ReactiveComponent/ParentComponent.js"></script>
<script type="module" src="./guide/ReactiveComponent/ChildComponent.js"></script>
<script type="module" src="./guide/ReactiveComponent.js"></script>
<script type="module" src="./guide/mount.js"></script>
</body>
</html>
js
import {ButtonComponent} from './ButtonComponent.js';
import {CounterComponent} from './CounterComponent.js';
import {ReactiveComponent} from './ReactiveComponent.js';
import {InputComponent} from './InputComponent.js';
document
.querySelector('#app')
.appendChild(ButtonComponent())
.appendChild(CounterComponent())
.appendChild(ReactiveComponent())
.appendChild(InputComponent());
ButtonComponent
js
import {component} from '../core/component.js'
export const ButtonComponent = component({
template() {
return `<div>
<button>Child Button</button>
</div>`
}
});
CounterComponent
js
import {component} from '../core/component.js'
import {createStore} from '../core/store.js';
const store = createStore({
count: 0
});
export const CounterComponent = component({
template() {
const count = store.get('count');
return `<div>
<button class="up">Up</button>
${count}
<button class="down">Down</button>
</div>`
},
events() {
return [
['.up', 'onclick', 'upCount'],
['.down', 'onclick', 'downCount'],
]
},
methods() {
return {
upCount() {
this.calcCount(1)
},
downCount() {
this.calcCount(-1)
},
calcCount(num) {
this.setCount(this.getCount() + num)
},
getCount() {
return store.get('count')
},
setCount(count) {
store.set('count', count)
}
}
},
created({render}) {
store.watch('count', render)
}
});
ReactiveComponent
js
import {component} from '../core/component.js';
import {NowComponent} from './ReactiveComponent/NowComponent.js';
import {NowControllerComponent} from './ReactiveComponent/NowControllerComponent.js';
import {ParentComponent} from './ReactiveComponent/ParentComponent.js';
export const ReactiveComponent = component({
data() {
return {
now: Date.now()
}
},
template() {
return `<div>
<now bind-props="now"></now>
<now-controller on="changeNow"></now-controller>
<parent-component></parent-component>
</div>`
},
components() {
return [
['now', NowComponent],
['now-controller', NowControllerComponent],
['parent-component', ParentComponent],
]
},
methods({store}) {
return {
changeNow(data, message) {
console.log(message)
store.set('now', data)
},
}
}
})
NowComponent
js
import {component} from '../../core/component.js';
export const NowComponent = component({
template({props}) {
return `<h1>Now: ${props}</h1>`
}
});
NowControllerComponent
js
import {component} from '../../core/component.js';
export const NowControllerComponent = component({
template() {
return `<div>
<button class="now-btn">Change Now</button>
</div>`
},
events() {
return [
['button.now-btn', 'onclick', 'changeNow'],
]
},
methods({emit}) {
return {
changeNow() {
emit(Date.now(), 'from Child')
},
}
}
});
ParentComponent
js
import {component} from '../../core/component.js';
import {ChildComponent} from './ChildComponent.js';
export const ParentComponent = component({
data() {
return {
message: 'From Parent'
}
},
template() {
return `<div>
<h1>Parent</h1>
<child-component
bind-props="message"
on="onEmit"></child-component>
</div>`
},
components() {
return [
['child-component', ChildComponent]
]
},
methods({store}) {
return {
onEmit(message) {
store.set('message', message)
}
}
}
});
ChildComponent
js
import {component} from '../../core/component.js';
export const ChildComponent = component({
template({props}) {
return `<div>
<h2>Child</h2>
<p>message: ${props}</p>
<button>Send Message</button>
</div>`
},
events() {
return [
['button', 'onclick', 'sendMessage']
]
},
methods({emit}) {
return {
sendMessage() {
emit('Message From Child')
}
}
}
});
컴포넌트 코어
fp.js
js
export const noop = () => {};
export const always = v => () => v;
store.js
js
export const ALL = '*';
export const createStore = (state = {}) => {
const store = new Map();
const subscriber = new Map();
const set = mutation({store, subscriber});
setDefaultState({state, set});
return {
set,
delete: remove({store, subscriber}),
get: getter({store}),
watch: watch({subscriber}),
watchAll: watchAll({subscriber}),
}
};
const setDefaultState = ({state, set}) => {
Object.entries(state).forEach(([key, value]) => set(key, value))
};
const mutation = ({store, subscriber}) => (key, value, noNotify = true) => {
store.set(key, value);
noNotify && notify({subscriber, store, key})
};
const remove = ({store, subscriber}) => key => {
store.delete(key);
notify({subscriber, store, key})
};
const notify = ({subscriber, key, store}) => {
const data = store.get(key);
if (subscriber.has(key)) {
for (const listener of subscriber.get(key)) {
listener(data)
}
}
broadCast({subscriber, key, data})
};
const broadCast = ({subscriber, key, data}) => {
if (subscriber.has(ALL)) {
for (const listener of subscriber.get(ALL)) {
listener({key, data})
}
}
};
const getter = ({store}) => key => store.get(key);
const watch = ({subscriber}) => (key, listener) => {
let listeners;
if (subscriber.has(key)) {
listeners = subscriber.get(key)
} else {
listeners = []
}
listeners.push(listener);
subscriber.set(key, listeners)
};
const watchAll = ({subscriber}) => {
const watcher = watch({subscriber});
return listener => {
watcher(ALL, listener)
}
};
helper.js
js
export const addEvent = (methods, methodName) => event => {
methods[methodName].call(methods, event)
};
export const getElem = (selector, parent = document) => {
return parent.querySelectorAll(selector)
};
export const getAttr = (elem, attr) => elem.getAttribute(attr);
component.js
js
import * as _ from './fp.js'
import {createStore} from './store.js';
import {addEvent, getAttr, getElem} from './helper.js';
/**
* @param options
{
data = _.always({}),
template = _.noop,
template({data: state, props})
components = _.always([]),
methods = _.always([]),
events = _.always([]),
beforeCreate = _.noop,
created = _.noop
}
*/
export const component = (options) => ({props, emit} = {}) => {
const {
data = _.always({}),
template = _.noop,
components = _.always([]),
methods = _.always([]),
events = _.always([]),
beforeCreate = _.noop,
created = _.noop
} = options
const state = data({props})
const store = createStore(state)
beforeCreate({props, data: state})
const render = create({
state,
template,
components,
methods,
events,
store,
})
const dom = render({props, emit})
const renderFn = replaceWith({dom, render, props})
created({
dom,
props,
data: state,
render: renderFn,
})
reactive({store, state, renderFn})
return dom
}
const create = ({
state,
template,
components,
methods,
events,
store,
}) => ({props, emit}) => {
const dom = parseDOM(template({data: state, props}))
const methodResult = methods({dom, data: state, props, store, emit})
bindEvent(events(), methodResult, dom)
bindComponent({
dom,
state,
components: components(),
methodResult
})
return dom
}
const replaceWith = (params) => () => {
// render는 create와 component 함수의 반환 함수, 즉 2가지다.
const {dom, render, props, on} = params
const newDom = render({props, emit: on})
dom.replaceWith(newDom)
Object.assign(params, {dom: newDom})
}
const reactive = ({store, state, renderFn}) => {
store.watchAll(({key, data}) => {
state[key] = data
renderFn()
})
}
export const parseDOM = (template) => {
var tmp = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
tmp.innerHTML = template
return tmp.children[0]
}
export const bindEvent = (events, methods, dom) => {
for (const [selector, event, methodName] of events) {
if (selector === 'document') {
document[event] = addEvent(methods, methodName)
} else {
getElem(selector, dom).forEach(elem => {
elem[event] = addEvent(methods, methodName)
})
}
}
}
export const bindComponent = ({components, dom, state, methodResult}) => {
for (const [selector, component] of components) {
getElem(selector, dom).forEach(elem => {
replaceWith({
dom: elem,
render: component,
props: getProps(elem, state),
on: getOn(elem, methodResult)
})()
})
}
}
// props는 자식에게 전달할 데이터
const getProps = (elem, state) => {
const bindProps = getAttr(elem, 'bind-props')
const props = getAttr(elem, 'props')
if (bindProps) {
return state[bindProps]
} else {
if (props) {
return props
}
return {}
}
}
// on은 자식으로부터 전달받은 콜백
const getOn = (elem, methodResult) => {
const attr = getAttr(elem, 'on')
if (attr && methodResult) {
return methodResult[attr]
}
}