現代框架的本質其實還是Dom
操作,今天看到一句話特別喜歡,不要給自己設限,到最後,大多數的技術本質是相同的。
例如後端用到的Kafka , redis , sql事務寫入 ,Nginx
負載均衡演算法,diff
演算法,GRPC
,Pb 協議
的序列化和反序列化,鎖等等,都可以在前端被類似的大量複用邏輯,即便js
和Node.js
都是單執行緒的
認真看完本文與原始碼,你會收穫不少東西
框架誰優誰劣,就像Web
技術的開發效率與Native
開發的使用者體驗一樣誰也不好一言而論誰高誰低,不過可以確定的是,web
技術已經越來越接近Native
端體驗了
作者是一位跨平臺桌面端開發的前端工程師,由於是即時通訊應用,專案效能要求很高。於是苦尋名醫,為了達到想要的效能,最終選定了非常冷門的幾種優化方案拼湊在一起
過程雖然非常曲折,但是市面上能用的方案都用到了,嘗試過了,但是後面發現,極致的優化,並不是1+1=2
,要考慮業務的場景,因為一旦優化方案多了,他們之間的技術出發點,考慮的點可能會衝突。
這也是前端需要架構師的原因,開發重型應用如果前端有了一位架構師,那麼會少走很多彎路。
後端也是如此
Vue.js
中的keep-alive
使用:
在Vue.js
中,尤大大是這樣定義的:
keep-alive
主要用於保留元件狀態或避免重新渲染
基礎使用:
<keep-alive>
<component :is="view"></component>
</keep-alive>
大概思路:
這裡本來做了gif
圖,不知道為什存後切換也是非常平滑,沒有任何的閃屏
特別提示: 這裡每個元件,下面還有一個1000行的列表哦~ 切換也是秒級
圖看完了,開始梳理原始碼
第一步,初次渲染快取
import {Provider , KeepAlive} from 'react-component-keepalive';
將需要快取渲染的元件包裹,並且給一個name
屬性即可
例如:
import Content from './Content.jsx'
export default App extends React.PureComponent{
render(){
return(
<div>
<Provider>
<KeepAlive name="Content">
<Content/>
</KeepAlive>
</Provider>
</div>
)
}
}
這樣這個元件你就可以在第二次需要渲染他的時候直接取快取渲染了
下面是一組被快取的一個元件,
仔細看上面的註釋內容,再看當前body
中多出來的div
那麼他們是不是對應上了呢? 會是怎樣快取渲染的呢?
到底怎麼快取的
找到庫的原始碼入口:
import Provider from './components/Provider';
import KeepAlive from './components/KeepAlive';
import bindLifecycle from './utils/bindLifecycle';
import useKeepAliveEffect from './utils/useKeepAliveEffect';
export {
Provider,
KeepAlive,
bindLifecycle,
useKeepAliveEffect,
};
最主要先看 Provider,KeepAlive
這兩個元件:
快取元件這個功能是通過 React.createPortal API
實現了這個效果。
react-component-keepalive
有兩個主要的元件 <Provider>
和 <KeepAlive>
;<Provider>
負責儲存元件的快取,並在處理之前通過 React.createPortal API
將快取的元件渲染在應用程式的外面。快取的元件必須放在 <KeepAlive>
中,<KeepAlive>
會把在應用程式外面渲染的元件掛載到真正需要顯示的位置。
這樣很明瞭了,原來如此
開始原始碼:
Provider
元件生命週期
public componentDidMount() {
//建立`body`的div標籤
this.storeElement = createStoreElement();
this.forceUpdate();
}
createStoreElement
函式其實就是建立一個類似UUID
的附帶註釋內容的div
標籤在body
中
import {prefix} from './createUniqueIdentification';
export default function createStoreElement(): HTMLElement {
const keepAliveDOM = document.createElement('div');
keepAliveDOM.dataset.type = prefix;
keepAliveDOM.style.display = 'none';
document.body.appendChild(keepAliveDOM);
return keepAliveDOM;
}
呼叫createStoreElement
的結果:
然後呼叫forceUpdate
強制更新一次元件
這個元件內部有大量變數鎖:
export interface ICacheItem {
children: React.ReactNode; //自元素節點
keepAlive: boolean; //是否快取
lifecycle: LIFECYCLE; //列舉的生命週期名稱
renderElement?: HTMLElement; //渲染的dom節點
activated?: boolean; // 已啟用嗎
ifStillActivate?: boolean; //是否一直保持啟用
reactivate?: () => void; //重新啟用的函式
}
export interface ICache {
[key: string]: ICacheItem;
}
export interface IKeepAliveProviderImpl {
storeElement: HTMLElement; //剛才渲染在body中的div節點
cache: ICache; //快取遵循介面 ICache 一個物件 key-value格式
keys: string[]; //快取佇列是一個陣列,裡面每一個key是字串,一個標識
eventEmitter: any; //這是自己寫的自定義事件觸發模組
existed: boolean; //是否退出狀態
providerIdentification: string; //提供的識別
setCache: (identification: string, value: ICacheItem) => void; 。//設定快取
unactivate: (identification: string) => void; //設定不活躍狀態
isExisted: () => boolean; //是否退出,會返回當前元件的Existed的值
}
上面看不懂 別急,看下面:
接著是Provider
元件真正渲染的內容程式碼:
<React.Fragment>
{innerChildren}
{
keys.map(identification => {
const currentCache = cache[identification];
const {
keepAlive,
children,
lifecycle,
} = currentCache;
let cacheChildren = children;
//中間省略若干細節判斷
return ReactDOM.createPortal(
(
cacheChildren
? (
<React.Fragment>
<Comment>{identification}</Comment>
{cacheChildren}
<Comment
onLoaded={() => this.startMountingDOM(identification)}
>{identification}</Comment>
</React.Fragment>
)
: null
),
storeElement,
);
})
}
</React.Fragment>
innerChildren
即是傳入給Provider
的children
一開始我們看見的快取元件內容顯示的都是一個註釋內容 那為什麼可以渲染出東西來呢
Comment
元件是重點
Comment
元件
public render() {
return <div />;
}
初始返回是一個空的div
標籤
但是看他的生命週期ComponentDidmount
public componentDidMount() {
const node = ReactDOM.findDOMNode(this) as Element;
const commentNode = this.createComment();
this.commentNode = commentNode;
this.currentNode = node;
this.parentNode = node.parentNode as Node;
this.parentNode.replaceChild(commentNode, node);
ReactDOM.unmountComponentAtNode(node);
this.props.onLoaded();
}
這個邏輯到這裡並沒有完,我們需要進一步檢視KeepAlive
元件原始碼
KeepAlive
原始碼:
元件componentDidMount
生命週期鉤子:
public componentDidMount() {
const {
_container,
} = this.props;
const {
notNeedActivate,
identification,
eventEmitter,
keepAlive,
} = _container;
notNeedActivate();
const cb = () => {
this.mount();
this.listen();
eventEmitter.off([identification, START_MOUNTING_DOM], cb);
};
eventEmitter.on([identification, START_MOUNTING_DOM], cb);
if (keepAlive) {
this.componentDidActivate();
}
}
其他邏輯先不管,重點看:
const cb = () => {
this.mount();
this.listen();
eventEmitter.off([identification, START_MOUNTING_DOM], cb);
};
eventEmitter.on([identification, START_MOUNTING_DOM], cb);
當接收到事件被觸發後,呼叫`mout和listen`方法,然後取消監聽這個事件
private mount() {
const {
_container: {
cache,
identification,
storeElement,
setLifecycle,
},
} = this.props;
this.setMounted(true);
const {renderElement} = cache[identification];
setLifecycle(LIFECYCLE.UPDATING);
changePositionByComment(identification, renderElement, storeElement);
}
changePositionByComment
這個函式是整個呼叫的重點,下面會解析
private listen() {
const {
_container: {
identification,
eventEmitter,
},
} = this.props;
eventEmitter.on(
[identification, COMMAND.CURRENT_UNMOUNT],
this.bindUnmount = this.componentWillUnmount.bind(this),
);
eventEmitter.on(
[identification, COMMAND.CURRENT_UNACTIVATE],
this.bindUnactivate = this.componentWillUnactivate.bind(this),
);
}
listen
函式監聽的自定義事件為了觸發componentWillUnmount
和componentWillUnactivate
COMMAND.CURRENT_UNMOUNT
這些都是列舉而已
changePositionByComment
函式:
export default function changePositionByComment(identification: string, presentParentNode: Node, originalParentNode: Node) {
if (!presentParentNode || !originalParentNode) {
return;
}
const elementNodes = findElementsBetweenComments(originalParentNode, identification);
const commentNode = findComment(presentParentNode, identification);
if (!elementNodes.length || !commentNode) {
return;
}
elementNodes.push(elementNodes[elementNodes.length - 1].nextSibling as Node);
elementNodes.unshift(elementNodes[0].previousSibling as Node);
// Deleting comment elements when using commet components will result in component uninstallation errors
for (let i = elementNodes.length - 1; i >= 0; i--) {
presentParentNode.insertBefore(elementNodes[i], commentNode);
}
originalParentNode.appendChild(commentNode);
}
老規矩,上圖解析原始碼:
很多人看起來雲裡霧裡,其實最終的實質就是通過了Coment
元件的註釋,來查詢到對應的需要渲染真實節點再進行替換,而這些節點都是快取在記憶體中,DOM
操作速度遠比框架對比後渲染快。這裡再次得到體現
這個庫,無論是否路由元件都可以使用,虛擬列表+快取KeepAlive元件的Demo體驗地址
庫原連結地址為了專案安全,我自己重建了倉庫自己定製開發這個庫
感謝原先作者的貢獻 在我出現問題時候也第一時間給了我技術支援 謝謝!
新的庫名叫react-component-keepalive
直接可以在npm
中找到
npm i react-component-keepalive
就可以正常使用了
如果你對React
並不瞭解,可以看一些我之前的文章:
歡迎關注我的前端公眾號: 前端巔峰
本人專注前端最前沿技術,跨平臺重型應用開發,即時通訊等技術。
版本的後續計劃: