21天 react
class Component {
render() {
throw new Error('must implement render method');
}
}
Component.prototype.isReactComponent = true;
function isClass(type) {
return (
Boolean(type.prototype) &&
Boolean(type.prototype.isReactComponent)
);
}
function createElement(type, props, ...args) {
props = Object.assign({}, props);
let children = [].concat(...args);
props.children = children;
return { type, props };
}
function hooks(obj, name, ...args) {
obj && obj[name] && obj[name].apply(obj, args);
}
class DomComponent {
constructor(element) {
this.currentElement = element;
this.renderedChildren = [];
this.node = null;
}
getPublicInstance() {
return this.node;
}
getHostNode() {
return this.node;
}
unmount() {
this.renderedChildren.forEach(child => child.unmount());
}
updateDomProperties(prevProps, nextProps) {
const node = this.node;
// 删除旧的attribute
Object.keys(prevProps).forEach(propName => {
if(propName !== 'children' && !nextProps.hasOwnProperty(propName)) {
node.removeAttribute(propName);
}
})
// 更新新的attribute
Object.keys(nextProps).forEach(propName => {
if(propName !== 'children') {
node.setAttribute(propName, nextProps[propName])
}
})
}
updateChildren(prevProps, nextProps) {
const prevChildren = prevProps.children;
const nextChildren = nextProps.children;
const prevRenderedChildren = this.renderedChildren;
const nextRenderedChildren = [];
const operationQueue = [];
for(let i=0;i< nextChildren.length;i++){
const prevChild = prevRenderedChildren[i];
// insert
if(!prevChild){
const nextChild = instantiateComponent(nextChildren[i]);
const node = nextChild.mount();
operationQueue.push({
type: 'ADD',
node
})
nextRenderedChildren.push(nextChild);
continue;
}
const canUpdate = prevChildren[i].type === nextChildren[i].type;
// replace
if(!canUpdate){
const prevNode = prevChild.getHostNode();
prevChild.unmount();
const nextChild = instantiateComponent(nextChildren[i]);
const nextNode = nextChild.mount();
console.log('prevNode:', prevNode);
console.log('nextNode:', nextNode);
operationQueue.push({
type: 'REPLACE',
prevNode,
nextNode
});
nextRenderedChildren.push(nextChild);
continue;
}
prevChild.receive(nextChildren[i]);
nextRenderedChildren.push(prevChild);
}
// delete
for(let j=nextChildren.length;j<prevChildren.length;j++){
const prevChild = prevRenderedChildren[j];
const node = prevChild.node;
prevChild.unmount();
operationQueue.push({
type: 'REMOVE', node
})
}
this.renderedChildren = nextRenderedChildren;
// batch update DOM
while(operationQueue.length > 0){
const operation = operationQueue.shift();
switch(operation.type){
case 'ADD':
this.node.appendChild(operation.node);
break;
case 'REPLACE':
this.node.replaceChild(operation.nextNode, operation.prevNode);
break;
case 'REMOVE':
this.node.removeChild(operation.node);
break;
}
}
}
receive(nextElement) {
const node = this.node;
const preveElement = this.currentElement;
const prevProps = preveElement.props;
const nextProps = nextElement.props;
this.currentElement = nextElement;
this.updateDomProperties(prevProps, nextProps);
this.updateChildren(prevProps, nextProps);
}
mount() {
const { type, props } = this.currentElement;
let children = props.children;
children = children.filter(Boolean);
const node = document.createElement(type);
Object.keys(props).forEach(propName => {
if(propName !== 'children') {
node.setAttribute(propName, props[propName])
}
})
const renderedChildren = children.map(instantiateComponent);
this.renderedChildren = renderedChildren;
const childNodes = renderedChildren.map(child => child.mount());
for(let child of childNodes) {
node.appendChild(child);
}
this.node = node;
return node;
}
}
class TextComponent {
constructor(element) {
this.currentElement = element;
this.node = null;
}
getPublicInstance() {
return this.node;
}
getHostNode() {
return this.node;
}
receive(element){
this.currentElement = element;
this.node.textContent = element;
}
unmount() {
this.node = null;
}
mount() {
const node = document.createTextNode(this.currentElement);
this.node = node;
return node;
}
}
class CompositeComponent {
constructor(element) {
this.currentElement = element;
this.publicInstance = null;
this.renderedComponent = null;
}
getPublicInstance() {
return this.publicInstance;
}
getHostNode() {
return this.renderedComponent.getHostNode();
}
receive(nextElement) {
const prevProps = this.currentElement.props;
const publicInstance = this.publicInstance;
const prevRenderedComponent = this.renderedComponent;
const prevRenderedElement = prevRenderedComponent.currentElement;
this.currentElement = nextElement;
const type = nextElement.type;
const nextProps = nextElement.props;
let nextRenderedElement;
if(isClass(type)) {
hooks(publicInstance, 'componentWillUpdate', nextProps);
publicInstance.props = nextProps;
nextRenderedElement = publicInstance.render();
} else if(typeof type === 'function') {
nextRenderedElement = type(nextProps);
}
if(prevRenderedElement.type === nextRenderedElement.type) {
prevRenderedComponent.receive(nextRenderedElement);
hooks(publicInstance,'componentDidUpdate',prevProps);
return
}
const prevNode = prevRenderedComponent.getHostNode();
prevRenderedComponent.unmount();
const nextRenderedComponent = instantiateComponent(nextRenderedElement);
const nextNode = nextRenderedComponent.mount();
this.renderedComponent = nextRenderedComponent;
prevNode.parentNode.replaceChild(nextNode, prevNode);
}
unmount() {
hooks(this.publicInstance, 'componentWillUnmount');
this.renderedComponent.unmount();
}
mount() {
const { type, props } = this.currentElement;
const children = props.children;
let instance, renderedElement;
// delegate to mount
if(isClass(type)) {
instance = new type(props);
instance.props = props;
hooks(instance, 'componentWillMount');
renderedElement = instance.render();
this.publicInstance = instance;
} else {
renderedElement = type(props);
}
const renderedComponent = instantiateComponent(renderedElement);
this.renderedComponent = renderedComponent;
return renderedComponent.mount();
}
}
function instantiateComponent(element) {
if(typeof element === 'string') return new TextComponent(element);
if(typeof element.type === 'string') return new DomComponent(element);
if(typeof element.type === 'function') return new CompositeComponent(element);
throw new Error('wrong element type');
}
function mount(element) {
const rootComponent = instantiateComponent(element);
return rootComponent.mount();
}
function findDOMNode(instance){
return instance.getHostNode();
}
function render(element, mountNode) {
if(mountNode.firstChild) {
const prevNode = mountNode.firstChild;
const prevComponent = prevNode._internalInstance;
const prevElement = prevComponent.currentElement;
if(prevElement.type === element.type) {
prevComponent.receive(element);
return;
}
unmountComponentAtNode(mountNode);
}
var rootComponent = instantiateComponent(element); // top-level internal instance
var node = rootComponent.mount(); // top-level node
mountNode.appendChild(node);
node._internalInstance = rootComponent;
var publicInstance = rootComponent.getPublicInstance(); // top-level public instance
return publicInstance;
}
function unmountComponentAtNode(mountNode) {
var node = mountNode.firstChild;
var rootComponent = node._internalInstance; // 读取 internal instance
rootComponent.unmount();
mountNode.innerHTML = '';
}
const React = {
render,
unmountComponentAtNode
}
// test example
class Link extends Component {
componentWillMount() {
console.log('Link will Mount');
}
componentWillUnmount() {
console.log('Link will Unmount');
}
componentWillUpdate() {
console.log('Link will update')
}
componentDidUpdate() {
console.log('Link Did update')
}
render() {
const { children } = this.props
return (
<a href="http://www.baidu.com">{children}</a>
)
}
}
function Button(props) {
return (
<button class="btn">{props.text}</button>
)
}
class App extends Component {
componentWillMount() {
console.log('App will Mount');
}
componentWillUnmount() {
console.log('App will Unmount');
}
componentWillUpdate() {
console.log('App will update')
}
componentDidUpdate() {
console.log('App Did update')
}
render() {
const { content } = this.props;
if(content === 'toutiao'){
return (
<div class="container">
<Link> { content}</Link>
<Link> { content}</Link>
</div>
)
}
return (
<div class="container">
<Button text="this is a button" />
<Link>{content}</Link>
</div>
)
}
}
const mountNode = document.querySelector('#root');
React.render(<App content="baidu"/>, mountNode);
setTimeout(() => {
React.render(<App content="toutiao"/>, mountNode);
}, 1000)