앞서 'prototype' 이라는 개념을 숙지해야 한다.
JS에서 모든 객체는 자신의 부모 객체를 가리키는 'proto' 속성을 가지는데 이렇게 연결된 객체들의 체인을
프로토타입 체인이라고 부른다.
'prototype'은 함수 객체에만 존재하는 속성으로 함수로 생성된 객체들이 자신의 프로토타입 체인의 맨 위에 위치할 객체를 가리키게된다.
당연히도 이 프로토타입의 객체에는 이 객체로 생성된 모든 객체에서 공유해야 하는 메서드나 속성이 포함된다.
import ReactNoopUpdateQueue from './ReactNoopUpdateQueue';
import assign from 'shared/assign';
/**
* 환경 변수 '__DEV__'를 사용하여 개발환경에서만 해당 오브젝트 객체를 불변하게 만들어준다.
* 이렇게 하면서, 해당 객체의 프로퍼티가 재할당 되거나 삭제되는 것을 방지하고,
* 읽기 전용으로 만들고자 할 때 사용한다
*/
const emptyObject = {};
if (__DEV__) {
Object.freeze(emptyObject);
}
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.isReactComponent = {};
컴포넌트를 정의하는 함수이다.
파라미터로는 props, context(체크), updater가 존재하고
updater는 업데이트 큐를 말한다.
컴포넌트가 업데이트가 되면 React는 새로운 props와 state를 이용하여 가상 DOM을 재생성한다.
그리고 이전 가상 DOM과 비교하여 변경된 부분을 실제 DOM에 적용하게 된다.
업데이트 큐는 이러한 업데이트 작업을 추적하고, 작업을 순서대로 처리하는 역할을 수행한다.
context는 상위 계층에서 하위 계층으로 데이터를 전달하기 위한 것이다.
ex) 사용자 인증 정보와 같은 전역 데이터 처리, UI에 영향을 미치는 데이터를 처리
반면 props는 부모 컴포넌트로부터 자식 컴포넌트로 데이터를 전달할 때 사용한다.
일반적으로 props를 사용하는 것이 좋다.
리액트에서는 컴포넌트를 렌더링할 때 해당 컴포넌트의 업데이트를 처리할 수 있는 업데이트 큐를 컴포넌트에게 주입한다.
물론 실제 사용되는 업데이트 큐는 'renderer(체크)'인 'ReactDOM.render'에 의해 결정된다.
'renderer'는 각 플랫폼마다 다르게 존재하며 컴포넌트를 생성할 때 필요한 기능을 제공한다.
'this.refs'는 컴포넌트 내에서 'ref' 속성이 문자열로 지정된 경우에만 나중에 다른 객체로 할당된다.
리액트에서 ref는 DOM 요소나 컴포넌트 인스턴스에 접근하는 방법을 제공한다.
ref는 문자열 형태와 콜백 함수 형태 두 가지로 사용할 수 있다. 문자열 형태 ref는 이전 버전에서 사용되었고
권장하는 방식은 콜백 함수 형태이다.
만일 문자열 형태로 사용하는 경우 해당 문자열은 ref를 참조할 수 있는 유일한 값으로 사용된다.
따라서 ref를 업데이트 하거나 다른 객체로 변경할 수 없다.
'this.updater'와 같은 경우에는 updater 객체가 존재하는 경우 해당 객체를 사용하고 없는 경우 'ReactNoopUpdateQueue'를 사용한다. ('ReactNoopUpdateQueue' 실제 DOM에서 동작하지 않고 컴포넌트의 업데이트를 추적하는 테스트용이다.)
'Component'클래스가 React 컴포넌트임을 나타내기 위해서 'isReactComponent' 속성을 빈 객체로 할당한다.
이는 React에서 컴포넌트 유형을 확인하기 위해 사용한다.
Component.prototype.setState = function (partialState, callback) {
if (
typeof partialState !== 'object' &&
typeof partialState !== 'function' &&
partialState != null
) {
throw new Error(
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
}
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
React 컴포넌트 클래스의 메서드인 'setState'를 정의하는 부분이다.
파라미터로는 'partialState', 'callback'이 있다.
부분적 상태 객체를 가리키는 'partialState' 인자가 객체나 함수인지를 먼저 파악하고 아니면 에러를 발생시킨다.
그리고 'updater' 객체('renderer' 에서 제공)에 있는 'enqueueSetState' 메서드를 호출하여 상태 업데이트를 예약한다.
enqueueSetState' 메서드는 업데이트 작업을 큐에 추가하고, 이후에 상태를 변경하고 렌더링을 다시 수행한다.
두 번째 인자인 'callback'은 컴포넌트의 상태 업데이트가 완료된 후 호출되는 함수를 말한다.
Component.prototype.forceUpdate = function (callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
말그대로 강제 렌더링이다. 이 메서드는 'shouldComponentUpdate()' 메서드를 호출하지 않고, 컴포넌트의 'state'와 'props'가 변경되었을 때도 렌더링된다.
실질적으로 호출되는 것은 updater 객체의 'equenceForceUpdate()' 메서드로 세 번째 파라미터는 해당 메서드의 이름을 말한다.
이 메서드를 호출하게 되면 컴포넌트의 업데이트가 대기열에 추가되고
리액트 엔진은 이후에 컴포넌트를 업데이트할때 해당 업데이트가 대기열에서 처리된다.
'setState()' 메서드와 달리 상태를 업데이트하지 않고 컴포넌트를 그저 다시 렌더링한다.
따라서 성능상의 이유로 사용을 자제하는 것이 좋다.
아래 코드는 더 이상 사용하지 않는 API들에 대한 경고 기능을 담당한다.
/**
* Deprecated APIs. These APIs used to exist on classic React classes but since
* we would like to deprecate them, we're not going to move them over to this
* modern base class. Instead, we define a getter that warns if it's accessed.
*/
if (__DEV__) {
const deprecatedAPIs = {
isMounted: [
'isMounted',
'Instead, make sure to clean up subscriptions and pending requests in ' +
'componentWillUnmount to prevent memory leaks.',
],
replaceState: [
'replaceState',
'Refactor your code to use setState instead (see ' +
'https://github.com/facebook/react/issues/3236).',
],
};
const defineDeprecationWarning = function (methodName, info) {
Object.defineProperty(Component.prototype, methodName, {
get: function () {
console.warn(
'%s(...) is deprecated in plain JavaScript React classes. %s',
info[0],
info[1],
);
return undefined;
},
});
};
for (const fnName in deprecatedAPIs) {
if (deprecatedAPIs.hasOwnProperty(fnName)) {
defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);
}
}
}
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
컴포넌트 클래스를 상속하여 새로운 컴포넌트 클래스를 정의할 때, 'prototype'을 직접 수정하게 되면 모든 컴포넌트에 영향을 미치기 때문에 컴포넌트 클래스를 복제하여 새로운 클래스를 만들고 그 클래스를 상속하는 것이 일반적이다.
'CompoenetDummy()' 메서드는 이를 위한 메서드로 'prototype'을 상속하게 되면 이 인스턴스는 'Component'클래스와 동일한 메서드와 속성을 가진다. 이를 이용하여 'prototype'을 직접 수정하지 않고도 같은 기능을 이용하여 상속을 할 수 있다.
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
export {Component, PureComponent};
'PureComponent'는 컴포넌트 클래스와 달리 'shouldComponentUpdate' 메소드를 오버라이딩해서 자체적으로 얕은 비교를 수행하여 성능을 최적화한다.
'shouldComponentUpdate' 메소드는 다시 렌더링할 필요가 있는지 여부를 결정하는데 사용되며
props와 state 값을 이전 값과 비교하여 boolean으로 반환한다.
그러면 얕은 비교(shallow comparison)은 무엇인가? 이는 참조가 아닌 값을 비교하는 것이다.
예를 들어서 객체가 다른 객체를 참조하고 있으면 객체의 값이 변경되더라도
두 객체는 다른 객체이므로 얕은 비교에서는 두 객체를 같은것으로 처리하지 않는다.
이것이 왜 성능을 올려주는 것이냐면 update를 위해서 프로퍼티의 값이 변경되었는지만
체크함으로써 객체가 복잡하거나 깊이있는 경우에도 불필요한 렌더링을 방지할 수 있게 된다.
이 클래스 또한 'prototype'을 설정해야 한다.
위에서 선언한 'ComponentDummy' 클래스의 인스턴스를 프로토타입으로 지정하고
'Component'의 프로토타입의 메소드, 속성들을 복사하고
해당 인스턴스를 구분하기 위한 'isPureReactComponent'의 값을 true로 지정한다.