createElement 의 결과인 가상 돔 객체를
실제 DOM 으로 변경하는 함수
// {
// "type": "div",
// "props": {
// "id": "app",
// "children": {
// "type": "h1",
// "props": {
// "children": "Hello, React Clone!"
// }
// }
// }
// }
const render = tree => {
const rootElement = document.createElement(tree.type)
const { children, ...elementProps } = tree.props
Object.keys(elementProps).forEach(key => {
rootElement.setAttribute(key, elementProps[key])
})
if (children) {
if (typeof children[0] === 'string') {
rootElement.appendChild(document.createTextNode(children[0]))
} else {
children.map(child => rootElement.appendChild(render(child)))
}
}
return rootElement
}
export default render
class React {
states = new Map()
stateIndex = 0
listeners = []
constructor() {
this.currentComponent = null
}
setState = (index, newState) => {
console.log(newState, 'newState')
const states = this.states.get(this.currentComponent)
if (typeof newState === 'function') {
states[index] = newState(states[index])
} else {
states[index] = newState
}
this.listeners.forEach(listener => listener())
}
subscribe = listener => {
this.listeners.push(listener)
return () => {
this.listeners = this.listeners.filter(l => l !== listener)
}
}
useState = initialValue => {
if (!this.states.has(this.currentComponent)) {
this.states.set(this.currentComponent, [])
}
const states = this.states.get(this.currentComponent)
if (states.length <= this.stateIndex) {
states[this.stateIndex] = initialValue
}
const index = this.stateIndex
const setState = this.setState.bind(this, index)
this.stateIndex++
return [states[index], setState]
}
render = tree => {
const rootElement = document.createElement(tree.type)
const { children, onClick, onChange, checked, ...elementProps } = tree.props
if (onClick) {
rootElement.addEventListener('click', onClick)
}
if (onChange) {
rootElement.addEventListener('change', e => onChange(e))
}
if (checked) {
rootElement.checked = checked
}
Object.keys(elementProps).forEach(key => {
rootElement.setAttribute(key, elementProps[key])
})
if (children) {
if (Array.isArray(children)) {
children.map(child => {
if (typeof child !== 'object') {
rootElement.appendChild(document.createTextNode(child))
} else {
rootElement.appendChild(this.render(child))
}
})
} else {
if (typeof children !== 'object') {
rootElement.appendChild(document.createTextNode(children))
} else {
rootElement.appendChild(this.render(children))
}
}
}
return rootElement
}
renderComponent = component => {
this.currentComponent = component
this.stateIndex = 0
return this.render(component())
}
}
const react = new React()
export default react
listeners, subscribe() 로 옵저버 패턴 구현
setState() 실행 시 listener() 를 실행시킨다
// src/main.js
import App from './App.jsx'
import React from './utils/react.js'
// 초기 렌더링
const app = document.getElementById('app')
const rootElement = React.renderComponent(App)
app.appendChild(rootElement)
// 상태 변경 시 재렌더링을 위한 리스너 등록
React.subscribe(() => {
const newRootElement = React.renderComponent(App)
app.innerHTML = ''
app.appendChild(newRootElement)
})
main.js 에서는 React.subscribe() 로
리스너에 등록할 콜백 함수를 정의하는데
여기에 renderComponent() 함수를 사용하여
setState() 시 컴포넌트를 재렌더링 할 수 있도록 한다
현재는 app 전체가 리렌더링 되므로 수정이 필요함
const createElement = (type, props, children) => {
if (typeof type === 'function') {
// props와 children을 합쳐서 컴포넌트에 전달
const componentProps = {
...props,
children,
}
// 컴포넌트 함수 실행
return type(componentProps)
}
// 일반 HTML 엘리먼트의 경우
return {
type,
props: {
...props,
children,
},
}
}
export default createElement
자식 컴포넌트를 import 해오면
함수 컴포넌트이기 때문에 함수가 전달된다
이 경우를 위해 type 이 'function' 인 경우 처리 추가