2017.09 Vue2 파트별 구현
파트별 구현
옵져버
js
const obj = {};
Observer(obj, 'counter', (newValue, oldValue) => {
console.log('LOG:', newValue, oldValue)
});
obj.counter = 0;
obj.counter = 1;
// LOG: 0 undefined
// LOG: 1 0
js
const Observer = (obj, property, callback) => {
let value = obj[property];
Object.defineProperty(
obj,
property,
{
configurable: true,
enumerable: true,
set: (newValue) => {
callback(newValue, value);
value = newValue;
},
get: () => value
}
);
};
탬플릿 바인딩
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="one-way-bind">
{{language}}
</div>
<script src="./template.js"></script>
<script>
const template = useTemplate('#one-way-bind');
template.bindData({
language: '한국어'
});
</script>
</body>
</html>
js
const useTemplate = (selector) => {
const TEMPLATE_PROPERTY = '$template';
const elem = document.querySelector(selector);
const delimiters = ['{{', '}}'];
const cacheData = {};
const changeTextNode = elem => {
let startPosition = 0;
let endPosition = 0;
let val = '';
let copiedTemplate = '';
//템플릿 캐싱
if (!elem.hasOwnProperty(TEMPLATE_PROPERTY)) {
elem[TEMPLATE_PROPERTY] = elem.textContent;
}
copiedTemplate = elem[TEMPLATE_PROPERTY];
while (startPosition > -1) {
val = '';
//시작 위치를 찾는 다
startPosition = copiedTemplate.indexOf(
delimiters[0],
startPosition
);
//데이터가 모두 변경되어 탬플릿 키워드가 없을 때
if (startPosition === -1) break;
//종료 위치를 찾는 다
endPosition = copiedTemplate.indexOf(
delimiters[1],
startPosition + delimiters[0].length - 1
);
//변수 key값을 찾는 다.
const variableName = copiedTemplate.substring(
startPosition + delimiters[0].length,
endPosition
);
//데이터에 변수값이 있을 경우 반영한다.
if (typeof cacheData[variableName] !== 'undefined') {
val = cacheData[variableName];
}
copiedTemplate = copiedTemplate.replace(
delimiters[0] + variableName + delimiters[1],
val,
);
}
if (elem.textContent !== copiedTemplate) {
elem.textContent = copiedTemplate;
}
};
const cacheObj = data => {
for (const key in data) {
cacheData[key] = data[key];
}
};
const traversal = elem => {
elem.childNodes.forEach(node => {
if (node.childNodes.length > 0) {
traversal(node);
} else {
changeTextNode(node);
}
});
};
const bindData = data => {
cacheObj(data);
traversal(elem);
};
return {bindData}
};
디렉티브
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<ul>
<li helloworld></li>
<li helloworld></li>
<li helloworld></li>
</ul>
<button
type="button"
on-click="changeButtonName(event)">
Button
</button>
<script src="./directive.js"></script>
<script>
Directive.define({
name: 'on-click',
bind: (elem, [callbackName, hasEvent]) => {
elem.onclick = (event) => {
window[callbackName].call(elem, hasEvent ? event : undefined);
};
}
});
Directive.define({
name: 'helloworld',
bind: (elem) => {
elem.innerHTML = 'Hello World';
}
});
Directive.render('body');
function changeButtonName(event) {
this.innerHTML = 'Changed Button';
}
</script>
</body>
</html>
js
const Directive = (() => {
const store = new Set();
const define = ({name, bind}) => {
store.add({name, bind});
};
const parseAttr = (_attr) => {
let attr = _attr;
if (attr.indexOf('(') > -1) {
attr = attr
.replace('(', ',')
.replace(')', '');
}
return attr.split(',');
};
const render = (parentSelector) => {
const parent = document.querySelector(parentSelector);
store.forEach((directive) => {
const finedElems = parent.querySelectorAll(`[${directive.name}]`);
finedElems.forEach((elem) => {
const directiveValue = parseAttr(elem.getAttribute(directive.name));
directive.bind(elem, directiveValue);
});
});
};
return {define, parseAttr, render}
})();
컴포넌트
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<helloworld></helloworld>
<ul>
<list></list>
<list></list>
<list></list>
<list></list>
<list></list>
</ul>
<script src="./component.js"></script>
<script>
Component.define({
name: 'helloworld',
template: '<div>Hello World</div>'
});
Component.define({
name: 'list',
template: '<li>Item</li>'
});
Component.render('body');
</script>
</body>
</html>
js
const Component = (() => {
const store = new Set();
const parseHTML = (template) => {
const shallowElement = document.createElement('div');
shallowElement.innerHTML = template;
return shallowElement.childNodes
};
const define = ({name, template}) => {
store.add({
name,
template: parseHTML(template)[0]
});
};
const render = (parentSelector) => {
const parent = document.querySelector(parentSelector);
store.forEach((component) => {
const finedElems = parent.querySelectorAll(component.name);
finedElems.forEach((elem) => {
//Element 참조 방지
const clonedTemplate = component.template.cloneNode(true);
elem.parentNode.replaceChild(
clonedTemplate,
elem
);
});
});
};
return {define, render}
})();
파트 통합
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app">
<h3>Data Binding</h3>
<p>{{message}}</p>
<input type="text" v-model="message">
<h3>Default Directive(v-onclick)</h3>
<button type="button" v-onclick="reverseMessage">
{{btnName}}
</button>
<h3>Components</h3>
<hello-component></hello-component>
<ul>
<list></list>
<list></list>
<list></list>
<list></list>
</ul>
<h3>Directives</h3>
<div helloworld></div>
</div>
<script src="./component.js"></script>
<script src="./directive.js"></script>
<script src="./observer.js"></script>
<script src="./template.js"></script>
<script src="./vanillajs-vue.js"></script>
<script>
vanillaJsVue({
el: '#app',
data: {
message: 'Hello World',
btnName: 'Reverse Message'
},
watch: {
message: val => {
console.log(val);
}
},
methods: {
reverseMessage() {
this.message = this.message.split('').reverse().join('');
}
},
directives: {
'helloworld': {
bind: el => {
el.innerHTML = 'Hello World';
}
}
},
components: {
'hello-component': {
template: '<p>Hello Component!!</p>'
},
'list': {
template: '<li>list</li>'
}
}
});
</script>
</body>
</html>
js
function vanillaJsVue(options){
let template = null;
const watcher = {};
const vModel = {
name: 'v-model',
bind: (elem, variableName) => {
elem.value = options.data[variableName];
elem.oninput = ((variableName => () => {
options.data[variableName] = elem.value;
})(variableName));
}
};
const vOnClick = {
name: 'v-onclick',
bind: (elem, callbackName) => {
elem.onclick = ((callbackName => event => {
options.methods[callbackName].call(options.data, event);
})(callbackName));
}
};
const platformDirectives = [
vOnClick,
vModel
];
const initTemplate = () => {
template = useTemplate(options.el);
template.bindData(options.data);
};
const runWatcher = (keyName, newVal) => {
let callbacks = [];
if(watcher.hasOwnProperty(keyName)){
callbacks = watcher[keyName];
callbacks.forEach(callback => {
callback.call(options.data, newVal);
});
}
};
const initWatch = () => {
let keyName;
for(keyName in options.watch){
watcher[keyName] = watcher[keyName] || [];
watcher[keyName].push(options.watch[keyName]);
}
for(keyName in options.data){
(((dataObj, keyName, template) => {
Observer(
dataObj,
keyName,
(newValue) => {
template.bindData({
[keyName]: newValue
});
runWatcher(keyName, newValue);
}
);
})(options.data, keyName, template));
}
};
const initDirective = () => {
let directiveName = '';
platformDirectives.forEach(directive => {
Directive.define(directive);
});
for(directiveName in options.directives){
Directive.define({
name: directiveName,
bind: ((bindCallback => (elem, args) => {
bindCallback(elem, ...args);
})(options.directives[directiveName].bind))
});
}
Directive.render(options.el);
};
const initComponents = () => {
let componentName = '';
for(componentName in options.components){
Component.define({
name: componentName,
template: options.components[componentName].template
});
}
Component.render(options.el);
};
const init = () => {
initTemplate();
initWatch();
initDirective();
initComponents();
};
init(options);
}