Reconciliation
更新实现是React的核心,React v15之前使用stack reconciliation算法进行更新,v16则使用了fiber reconciliation
本章和下章主要讲解stack reconciliation的实现。
因为update操作涉及到旧组件实例的unmount,所以先介绍unmount的实现。
unmount的实现类似于mount,先卸载子组件后卸载父组件。前面我们在internal instance上已经存储了组件的信息,只需后序遍历组件树进行卸载即可,同时执行componentWillUnmount的生命周期。
function unmountComponentAtNode(mountNode){
var node = mountNode.firstChild;
var rootComponent = node._internalInstance; // 读取 internal instance
rootComponent.unmount();
mountNode.innerHTML = '';
}unmountComponentAtNode需要读取_internalInstance且对_internalInstance进行unmount操作,这需要我们在render里对_internalInstance进行存储和Composite Component及Dom Component对unmount操作进行支持。实现如下:
function render(element, mountNode){
if(mountNode.firstChild){ // 若组件已经mount,则卸载以前的组件
unmountComponentAtNode(mountNode);
}
var rootComponent = instantiateComponent(element); // top-level internal instance
var node = rootComponent.mount(); // top-level node
mountNode.appendChild(node);
node._internalInstance = rootComponent; // 存储internal instance供 unmount和update使用
var publicInstance = rootComponent.getPublicInstance(); // top-level public instance
return publicInstance;
}
class DomComponent {
unmount(){
this.renderedChildren.forEach(child => child.unmount()); //递归卸载children组件
}
}
class CompositeComponent {
unmount(){
hooks(this.publicInstance, 'componentWillUnmount'); //执行生命周期
this.renderedComponent.unmount(); // 递归执行renderedComponent
}
}Update
React的更新基于两个关键点
- 不同的组件类型认为会生成完全不同的组件树,React不会尝试去比对他们,而是直接卸载上一个组件树,加载新的组件树。
- 列表的对比是基于key值的,不同的key值表示不同的组件树,React会通过替换、删除、增加、更新的方式将一个列表更新成新的列表。
为了支持更新操作我们扩充internal instance,支持一个新的方法receive(nextElement)以支持更新操作。
class CompositeComponent{
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);
}
}
function findDOMNode(instance){
return instance.getHostNode();
}CompositeComponent 的receive实现如上所示,主要逻辑是
- 判断组件类型是否相同,相同则执行更新操作(由于CompositeComponent并不负责创建DOM节点,所以与mount类似,将其delegate to renderedComponent执行,依次递归直到DOMComposite才负责执行真正的DOM更新。
- 如果组件类型不同则卸载旧组件,加载新组件,同时替换旧组件渲染的DOM节点为新组件的DOM节点,因此我们需要获得旧组件的DOM节点,同理delegate到DOM Composite,ReactDOM.findDOMNode(instance)的执行逻辑即如此
- 执行componentWillUpdate和componentDidUpdate生命周期,Function Component因为不存在public instance也就无法执行所有的生命周期。
我们发现CompositeComponent做的更新工作实际很少,实际的DOM更新都delegate给DomComponent了
class DomComponent{
getHostNode() {
return this.node;
}
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) {
// TODO 粗暴实现
const children = nextProps.children;
const node = this.node;
// 卸载所有旧的组件
this.renderedChildren.forEach(child => child.unmount())
this.node.innerHTML = '';
// 加载新的组件
const renderedChildren = children.map(instantiateComponent);
this.renderedChildren = renderedChildren;
const childNodes = renderedChildren.map(child => child.mount());
for(let child of childNodes) {
node.appendChild(child);
}
}
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);
}
}DomComponent的更新主要包含两部分attribute的更新和children的更新,attribute的更新比较简单,删除不存在的attribute和替换新的attribute,而children的更新则比较复杂,最为粗暴的实现是卸载所有旧的children组件,加载新的children组件。这样使得所有的children组件没有办法复用之前的DOM结构,浪费性能。
较为复杂的实现是依次比对新旧两个list,类型相同的则进行更新,类型不同则进行替换。
列表比对的结果有四种,对应四种操作:
- 旧列表不存在,新列表存在,对应新增(ADD)操作。
- 旧列表存在,新列表不存在,对应删除(DELETE)操作。
- 旧列表存在,新列表存在,但组件类型不同,对应替换(REPLACE)操作。
- 旧列表存在,新列表存在,且组件类型相同,对应更新(UPDATE),更新原组件即可。
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;
}
}
}至此我们已经实现了组件的更新操作,但其仍然存在很大的缺陷。
其对于列表的追加和尾部删除,效率很高如[<A/>,<B/>,<C/>] => [<A/>,<B/>] 或者[<A/>,<B/>] => [<A/>,<B/>,<C/>]只需增加或删除尾部的<C/>即可,但对于头部的插入或者删除效率很差,需要替换所有的组件,所有的DOM都无法复用。
为此需要一种更高效的算法来尽可能的复用原有的组件,React通过key唯一的标记组件来实现原有组件尽可能的复用。
至此所有代码如下
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)