接下來主要優化一下組件的更新
從之前文章可以知道,我們組件是通過createElement然后遍歷children屬性,完成整個dom的創建,其實是相當耗費性能的,react之所以能實現高性能,是因為它實現了局部dom的更新,而不是像我們_render方法里面那么簡單粗暴。
function _render(vnode) {
if (vnode === undefined || vnode === null || typeof vnode === 'boolean') vnode = '';
if (typeof vnode === 'number') vnode = String(vnode);
if (typeof vnode === 'string') {
let textNode = document.createTextNode(vnode);
return textNode;
}
if (typeof vnode.tag === 'function') {
const component = createComponent(vnode.tag, vnode.attrs);
setComponentProps(component, vnode.attrs);
return component.base;
}
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
Object.keys(vnode.attrs).forEach(key => {
const value = vnode.attrs[key];
setAttribute(dom, key, value);
});
}
vnode.children.forEach(child => render(child, dom)); // 遞歸渲染子節點
return dom;
}
先定義一個diff的算法,傳入原來通過createElement創建的dom和新的node(JSX解析出來的返還),diff主要通過新的對象與已有的dom對比,決定是否需要創建或者更新dom,如下面:
{"tag":"div","attrs":null,"children":["Text",{"tag":"div","attrs":null,"children":["Count: ",0]},{"tag":"button","attrs":{},"children":["Add"]}]}
diff函數
function diff( dom, vnode, container ) {
const ret = diffNode( dom, vnode );
if ( container && ret.parentNode !== container ) {
container.appendChild( ret );
}
return ret;
}
diffNode做同級節點對比,同時遞歸便利字節點
function diffNode( dom, vnode ) {
let out = dom;
if ( vnode === undefined || vnode === null || typeof vnode === 'boolean' )
{
vnode = '';
}
if ( typeof vnode === 'number' ) vnode = String( vnode );
// diff text node
if ( typeof vnode === 'string' ) {
// 如果當前的DOM就是文本節點,則直接更新內容
if ( dom && dom.nodeType === 3 ) { // nodeType: https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType
if ( dom.textContent !== vnode ) {
dom.textContent = vnode;
}
// 如果DOM不是文本節點,則新建一個文本節點DOM,并移除掉原來的
} else {
out = document.createTextNode( vnode );
if ( dom && dom.parentNode ) {
dom.parentNode.replaceChild( out, dom );
}
}
return out;
}
if ( typeof vnode.tag === 'function' ) {
return diffComponent( dom, vnode );
}
//
if ( !dom || !isSameNodeType( dom, vnode ) ) {
out = document.createElement( vnode.tag );
if ( dom ) {
[ ...dom.childNodes ].map( out.appendChild ); // 將原來的子節點移到新節點下
if ( dom.parentNode ) {
dom.parentNode.replaceChild( out, dom ); // 移除掉原來的DOM對象
}
}
}
if ( vnode.children && vnode.children.length > 0 || ( out.childNodes && out.childNodes.length > 0 ) ) {
diffChildren( out, vnode.children );
}
diffAttributes( out, vnode );
return out;
}
diffChildren相對麻煩,因為要針對key進行優化,具體原因可以參考:https://jdc.jd.com/archives/212685
function diffChildren( dom, vchildren ) {
const domChildren = dom.childNodes;
const children = [];
const keyed = {};
if ( domChildren.length > 0 ) {
for ( let i = 0; i < domChildren.length; i++ ) {
const child = domChildren[ i ];
const key = child.key;
if ( key ) {
keyed[ key ] = child;
} else {
children.push( child );
}
}
}
if ( vchildren && vchildren.length > 0 ) {
let min = 0;
let childrenLen = children.length;
for ( let i = 0; i < vchildren.length; i++ ) {
const vchild = vchildren[ i ];
const key = vchild.key;
let child;
if ( key ) {
if ( keyed[ key ] ) {
child = keyed[ key ];
keyed[ key ] = undefined;
}
} else if ( min < childrenLen ) {
for ( let j = min; j < childrenLen; j++ ) {
let c = children[ j ];
if ( c && isSameNodeType( c, vchild ) ) {
child = c;
children[ j ] = undefined;
if ( j === childrenLen - 1 ) childrenLen--;
if ( j === min ) min++;
break;
}
}
}
child = diffNode( child, vchild );
const f = domChildren[ i ];
if ( child && child !== dom && child !== f ) {
if ( !f ) {
dom.appendChild(child);
} else if ( child === f.nextSibling ) {
removeNode( f );
} else {
dom.insertBefore( child, f );
}
}
}
}
}
最后還有diffcomponent的函數,主要是component有unmount的生命周期需要實現。注意一下,vnode返回的的tag就是component的構建函數,這一步是babel幫我們轉換的,加入vnode的tag跟原本_component的constructor不同,可以認為原來的組件已經卸載了
function diffComponent(dom, vnode) {
let c = dom && dom._component;
let oldDom = dom;
// 如果組件類型沒有變化,則重新set props
if (c && c.constructor === vnode.tag) {
setComponentProps(c, vnode.attrs);
dom = c.base;
// 如果組件類型變化,則移除掉原來組件,并渲染新的組件
} else {
if (c) {
unmountComponent(c);
oldDom = null;
}
c = createComponent(vnode.tag, vnode.attrs);
setComponentProps(c, vnode.attrs);
dom = c.base;
if (oldDom && dom !== oldDom) {
oldDom._component = null;
removeNode(oldDom);
}
}
return dom;
}
使用以下代碼進行測試
import React from './react';
// end with react frame work
class Count extends React.Component {
constructor() {
super();
this.state = {
count: 0,
}
setInterval(()=>{
this.setState({
count: this.state.count+1,
})
}, 1000)
}
render() {
return <div>
Text
<div>Count: {this.state.count}</div>
<button
onClick={()=>{
this.setState({
count: this.state.count+1,
})
}}
>
Add
</button>
</div>;
}
}
const ReactDOM = {
render: (vnode, container) => {
container.innerHTML = '';
return React.render(vnode, container);
},
};
const element = <Count name="counter"/>;
console.log(element);
ReactDOM.render(
element,
document.getElementById('root'),
);
原本的加載方式,不使用diff,而是每次更新state觸發renderComponent時都是重新創建整個dom,base = _render(renderer),打開chrome可以看到,整個root都是重新掛載的:
我們把renderComponent里面base = _render(renderer)替換為上面的函數base = diff( component.base, renderer ):
function renderComponent(component) {
let base;
const renderer = component.render();
if (component.base && component.componentWillUpdate) {
component.componentWillUpdate();
}
console.log('component.base');
console.log(component.base);
console.log('renderer');
console.log(JSON.stringify(renderer));
//base = _render(renderer);
base = diff(component.base, renderer);
if (component.base) {
if (component.componentDidUpdate) component.componentDidUpdate();
} else if (component.componentDidMount) {
component.componentDidMount();
}
//if (component.base && component.base.parentNode) {
// component.base.parentNode.replaceChild(base, component.base);
//}
component.base = base;
base._component = component;
}
打開chrome可以看到接下來,只有text部分是重新更新的。
diff算法的核心就是同級比較,同index比較,并增加key進行優化,根據每次新的state,決定需要更新的那部分節點,實質上瀏覽器渲染的原理也是類似的。
最后一篇會說一下setState的優化,React中一次同步生命周期中其實setState只進行了一次,目的是減少人為的錯誤導致無效的渲染。
比如有人在onClick里面設置setState({count: this.state.count + 1}),之后在componentWillUpdate里面又使用一次setState({count: this.state.count + 1}),最終只會觸發一次render更新。