간단한 가상 DOM 구현 - ChoDragon9/posts GitHub Wiki
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="main">
<ul data-component="todos"></ul>
<span data-component="counter">0 Item Left</span>
<ul data-component="filters">
<li>
<a href="#">All</a>
</li>
<li>
<a href="#">Active</a>
</li>
<li>
<a href="#">Completed</a>
</li>
</ul>
</div>
<script type="module" src="index.js"></script>
</body>
</html>
렌더링 엔진은 requestAnimationFrame
을 기반으로 한다. 모든 DOM 조작이나 애니메이션은 이 DOM API를 기반으로 해야 한다. 이 콜백 내에서 DOM 작업을 수행하면 더 효율적이 된다. 이 API는 메인 스레드를 차단하지 않으며 다음 Repaint가 이벤트 루프에서 스케줄링되기 직전에 실행된다.
import fetchMockTodos from './mockup/fetchMockTodos.js';
import createApp from './core/createApp.js'
const state = {
todos: [],
currentFilter: 'All'
};
const main = document.querySelector('#main');
const render = () => {
window.requestAnimationFrame(() => {
createApp(main, state);
});
};
window.setInterval(() => {
state.todos = fetchMockTodos();
render();
}, 1000);
import {from} from '../helper.js';
export default () => {
return from({length: Math.max(Math.random() * 10, 5)})
.map((value, index) => ({
text: index,
completed: index > 4
}))
}
export const clone = node => node.cloneNode(true);
export const from = iterable => Array.from(iterable);
export const assign = (...args) => Object.assign(...args);
import registry from './registry.js';
import applyDiff from './virtual-dom/applyDiff.js';
import registerComponents from './registerComponents.js';
const renderRoot = (rootElement, state) => {
const newRootElement = registry.renderRoot(rootElement, state);
const {parentNode} = rootElement;
applyDiff(parentNode, rootElement, newRootElement);
};
export default (rootElement, state) => {
registerComponents();
renderRoot(rootElement, state);
}
import {clone, from} from '../helper.js';
const COMPONENT_KEY = '[data-component]';
const registry = new Map();
const renderWrapper = componentFn => {
return (targetElement, state) => {
const element = componentFn(targetElement, state);
const childComponents = element.querySelectorAll(COMPONENT_KEY);
from(childComponents)
.forEach(child => {
const componentName = child.dataset.component;
if (registry.has(componentName)) {
const childComponentFn = registry.get(componentName);
child.replaceWith(childComponentFn(child, state));
}
});
return element;
}
};
const add = (componentName, componentFn) => {
registry.set(componentName, renderWrapper(componentFn));
};
const renderRoot = (root, state) => {
return renderWrapper(clone)(root, state)
};
export default {
add,
renderRoot
}
import isNodeChanged from './isNodeChanged.js';
import {from} from '../../helper.js';
const applyDiff = (parentNode, realNode, virtualNode) => {
if (realNode && !virtualNode) {
realNode.remove();
return
}
if (!realNode && virtualNode) {
parentNode.appendChild(virtualNode);
return
}
if (isNodeChanged(realNode, virtualNode)) {
realNode.replaceWith(virtualNode);
return
}
const realChildren = from(realNode.children);
const virtualChildren = from(virtualNode.children);
const length = Math.max(realChildren.length, virtualChildren.length);
from({length})
.map((v, i) => [
realChildren[i],
virtualChildren[i]
])
.forEach(([realChild, virtualChild]) => {
applyDiff(
realNode,
realChild,
virtualChild
)
});
};
export default applyDiff;
import {from} from '../../helper.js';
const isDiffAttrLength = (node1, node2) => {
const n1Attrs = node1.attributes;
const n2Attrs = node2.attributes;
return n1Attrs.length !== n2Attrs.length;
};
const isDiffAttrValue = (node1, node2) => {
return from(node1.attributes)
.find(attr => {
const {name} = attr;
const attr1 = node1.getAttribute(name);
const attr2 = node2.getAttribute(name);
return attr1 !== attr2
});
};
const isDiffTextContent = (node1, node2) => {
return node1.children.length === 0 &&
node2.children.length === 0 &&
node1.textContent !== node2.textContent;
};
export default (node1, node2) => {
return isDiffAttrLength(node1, node2)
|| isDiffAttrValue(node1, node2)
|| isDiffTextContent(node1, node2);
};
import registry from './registry.js';
import todos from '../components/todos.js';
import counter from '../components/counter.js';
import filters from '../components/filters.js';
export default () => {
registry.add('todos', todos);
registry.add('counter', counter);
registry.add('filters', filters);
};
import {assign, clone} from '../helper.js';
const getTodoElement = todo => {
const {
text,
completed
} = todo;
return `<li>
<input
type="checkbox"
${completed ? 'checked' : ''}>
<input value="${text}">
</li>`
};
export default (targetElement, state) => {
const newTodoList = clone(targetElement);
const {todos} = state;
const todosElements = todos.map(getTodoElement).join('');
return assign(newTodoList, {
innerHTML: todosElements
});
}
import {assign, clone} from '../helper.js';
const getTodoCount = todos => {
const notCompleted = todos
.filter(todo => !todo.completed);
const { length } = notCompleted;
return `${length} Items left`
};
export default (targetElement, state) => {
return assign(clone(targetElement), {
textContent: getTodoCount(state.todos)
})
};
import {assign, clone, from} from '../helper.js';
export default (targetElement, state) => {
const newFilter = clone(targetElement);
const {currentFilter} = state;
from(newFilter.querySelectorAll('li a'))
.forEach(a => {
const fontWeight =
a.textContent === currentFilter
? 'bold'
: 'normal';
assign(a.style, {fontWeight});
});
return newFilter;
};