自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

手寫簡易前端框架:Patch 更新(1.0 完結(jié)篇)

開發(fā) 項目管理
能夠做 vdom 的渲染和更新,支持組件(props、state),這就是一個比較完整的前端框架了。

前面兩篇文章,我們實現(xiàn)了 vdom 的渲染和 jsx 的編譯,實現(xiàn)了 function 和 class 組件,這篇來實現(xiàn) patch 更新。

能夠做 vdom 的渲染和更新,支持組件(props、state),這就是一個比較完整的前端框架了。

首先,我們準備下測試代碼:

測試代碼

在上節(jié)的基礎上做下改造:

添加一個刪除按鈕,一個輸入框和添加按鈕,并且還要添加相應的事件監(jiān)聽器:

這部分代碼大家經(jīng)常寫,就不過多解釋了:

function Item(props) {
return <li className="item" style={props.style}>{props.children} <a href="#" onClick={props.onRemoveItem}>X </a></li>;
}

class List extends Component {
constructor(props) {
super();
this.state = {
list: [
{
text: 'aaa',
color: 'pink'
},
{
text: 'bbb',
color: 'orange'
},
{
text: 'ccc',
color: 'yellow'
}
]
}
}

handleItemRemove(index) {
this.setState({
list: this.state.list.filter((item, i) => i !== index)
});
}

handleAdd() {
this.setState({
list: [
...this.state.list,
{
text: this.ref.value
}
]
});
}

render() {
return <div>
<ul className="list">
{this.state.list.map((item, index) => {
return <Item style={{ background: item.color, color: this.state.textColor}} onRemoveItem={() => this.handleItemRemove(index)}>{item.text}</Item>
})}
</ul>
<div>
<input ref={(ele) => {this.ref = ele}}/>
<button onClick={this.handleAdd.bind(this)}>add</button>
</div>
</div>;
}
}

render(<List textColor={'#000'}/>, document.getElementById('root'));

前面我們已經(jīng)實現(xiàn)了渲染,現(xiàn)在要實現(xiàn)更新,也就是 setState 之后更新頁面的流程。

實現(xiàn) patch

其實最簡單的更新就是 setState 的時候重新渲染一次,整個替換掉之前的 dom:

setState(nextState) {
this.state = Object.assign(this.state, nextState);

const newDom = render(this.render());
this.dom.replaceWith(newDom);
this.dom = newDom;
}

測試下:

我們實現(xiàn)了更新功能!

開個玩笑。前端框架不會用這樣的方式更新的,多了很多沒必要的 dom 操作,性能太差。

所以還是要實現(xiàn) patch,也就是:

setState(nextState) {
this.state = Object.assign(this.state, nextState);
if(this.dom) {
patch(this.dom, this.render());
}
}

「patch 功能是把要渲染的 vdom 和已有的 dom 做下 diff,只更新需要更新的 dom,也就是按需更新」。

是否要走 patch 邏輯,這里可以加一個 shouldComponentUpdate 來控制,如果 props 和 state 都沒變就不用 patch 了。

setState(nextState) {
this.state = Object.assign(this.state, nextState);

if(this.dom && this.shouldComponentUpdate(this.props, nextState)) {
patch(this.dom, this.render());
}
}

shouldComponentUpdate(nextProps, nextState) {
return nextProps != this.props || nextState != this.state;
}

patch 怎么實現(xiàn)呢?

渲染的時候我們是遞歸 vdom,對元素、文本、組件分別做不同的處理,包括創(chuàng)建節(jié)點和設置屬性。patch 更新的時候也是同樣的遞歸,但是對元素、文本、組件做的處理不同:

文本

判斷 dom 節(jié)點是文本的話,要再看 vdom:

  • 如果 vdom 不是文本節(jié)點,直接替換
  • 如果 vdom 也是文本節(jié)點,那就對比下內(nèi)容,內(nèi)容不一樣就替換
if (dom instanceof Text) {
if (typeof vdom === 'object') {
return replace(render(vdom, parent));
} else {
return dom.textContent != vdom ? replace(render(vdom, parent)) : dom;
}
}

這里的 replace 的實現(xiàn)是用 replaceChild:

const replace = parent ? el => {
parent.replaceChild(el, dom);
return el;
} : (el => el);

然后是組件的更新:

組件

如果 vdom 是組件的話,對應的 dom 可能是同一個組件渲染的,也可能不是。

要判斷下 dom 是不是同一個組件渲染出來的,不是的話,直接替換,是的話更新子元素:

怎么知道 dom 是什么組件渲染出來的呢?

我們需要在 render 的時候在 dom 上加個屬性來記錄:

改下 render 部分的代碼,加上 instance 屬性:

instance.dom.__instance = instance;

然后更新的時候就可以對比下 constructor 是否一樣,如果一樣說明是同一個組件,那 dom 是差不多的,再 patch 子元素:

if (dom.__instance && dom.__instance.constructor == vdom.type) {
dom.__instance.componentWillReceiveProps(props);

return patch(dom, dom.__instance.render(), parent);
}

否則,不是同一個組件的話,那就直接替換了:

class 組件的替換:

if (Component.isPrototypeOf(vdom.type)) {
const componentDom = renderComponent(vdom, parent);
if (parent){
parent.replaceChild(componentDom, dom);
return componentDom;
} else {
return componentDom
}
}

function 組件的替換:

if (!Component.isPrototypeOf(vdom.type)) {
return patch(dom, vdom.type(props), parent);
}

所以,組件更新邏輯就是這樣的:

元素如果 dom 是元素的話,要看下是否是同一類型的:

function isComponentVdom(vdom) {
return typeof vdom.type == 'function';
}

if(isComponentVdom(vdom)) {
const props = Object.assign({}, vdom.props, {children: vdom.children});
if (dom.__instance && dom.__instance.constructor == vdom.type) {
dom.__instance.componentWillReceiveProps(props);
return patch(dom, dom.__instance.render(), parent);
} else if (Component.isPrototypeOf(vdom.type)) {
const componentDom = renderComponent(vdom, parent);
if (parent){
parent.replaceChild(componentDom, dom);
return componentDom;
} else {
return componentDom
}
} else if (!Component.isPrototypeOf(vdom.type)) {
return patch(dom, vdom.type(props), parent);
}
}

還有元素的更新:

元素

如果 dom 是元素的話,要看下是否是同一類型的:

不同類型的元素,直接替換

if (dom.nodeName !== vdom.type.toUpperCase() && typeof vdom === 'object') {
return replace(render(vdom, parent));
}

同一類型的元素,更新子節(jié)點和屬性

更新子節(jié)點我們希望能重用的就重用,所以在 render 的時候給每個元素加上一個標識 key:

instance.dom.__key = vdom.props.key;

更新的時候如果找到 key 就重用,沒找到就 render 一個新的。

首先我們把所有的子節(jié)點的 dom 放到一個對象里:

const oldDoms = {};
[].concat(...dom.childNodes).map((child, index) => {
const key = child.__key || `__index_${index}`;
oldDoms[key] = child;
});

[].concat 是為了拍平數(shù)組,因為數(shù)組的元素也是數(shù)組。

默認 key 設置為 index。

然后循環(huán)渲染 vdom 的 children,如果找到對應的 key 就直接復用,然后繼續(xù) patch 它的子元素。如果沒找到,就 render 一個新的:

[].concat(...vdom.children).map((child, index) => {
const key = child.props && child.props.key || `__index_${index}`;
dom.appendChild(oldDoms[key] ? patch(oldDoms[key], child) : render(child, dom));
delete oldDoms[key];
});

把新的 dom 從 oldDoms 里去掉。剩下的就是不再需要的 dom,直接刪掉即可:

for (const key in oldDoms) {
oldDoms[key].remove();
}

刪掉之前還可以執(zhí)行下組件的 willUnmount 的生命周期函數(shù):

for (const key in oldDoms) {
const instance = oldDoms[key].__instance;
if (instance) instance.componentWillUnmount();

oldDoms[key].remove();
}

子節(jié)點處理完了,再處理下屬性:

這個就是把舊的屬性刪掉,設置新的 props 即可:

for (const attr of dom.attributes) dom.removeAttribute(attr.name);
for (const prop in vdom.props) setAttribute(dom, prop, vdom.props[prop]);

setAttribute 之前我們只做了 style、event listener 和普通屬性的處理,還需要再完善下:

每次 event listener 都要 remove 再 add,這樣 render 多次也始終只有一個:

function isEventListenerAttr(key, value) {
return typeof value == 'function' && key.startsWith('on');
}

if (isEventListenerAttr(key, value)) {
const eventType = key.slice(2).toLowerCase();

dom.__handlers = dom.__handlers || {};
dom.removeEventListener(eventType, dom.__handlers[eventType]);

dom.__handlers[eventType] = value;
dom.addEventListener(eventType, dom.__handlers[eventType]);
}

把各種事件的 listener 放到 dom 的 __handlers 屬性上,每次刪掉之前的,換成新的。

然后再支持下 ref 屬性:

function isRefAttr(key, value) {
return key === 'ref' && typeof value === 'function';
}

if(isRefAttr(key, value)) {
value(dom);
}

也就是這樣的功能:

<input ref={(ele) => {this.ref = ele}}/>


再支持下 key 的設置:

if (key == 'key') {
dom.__key = value;
}

還有一些特殊屬性的設置,包括 checked、value、className:

if (key == 'checked' || key == 'value' || key == 'className') {
dom[key] = value;
}

其余的就都是 setAttribute 設置了:

function isPlainAttr(key, value) {
return typeof value != 'object' && typeof value != 'function';
}

if (isPlainAttr(key, value)) {
dom.setAttribute(key, value);
}

所以現(xiàn)在的 setAttribute 是這樣的:

const setAttribute = (dom, key, value) => {
if (isEventListenerAttr(key, value)) {
const eventType = key.slice(2).toLowerCase();
dom.__handlers = dom.__handlers || {};
dom.removeEventListener(eventType, dom.__handlers[eventType]);
dom.__handlers[eventType] = value;
dom.addEventListener(eventType, dom.__handlers[eventType]);
} else if (key == 'checked' || key == 'value' || key == 'className') {
dom[key] = value;
} else if(isRefAttr(key, value)) {
value(dom);
} else if (isStyleAttr(key, value)) {
Object.assign(dom.style, value);
} else if (key == 'key') {
dom.__key = value;
} else if (isPlainAttr(key, value)) {
dom.setAttribute(key, value);
}
}

文本、組件、元素的更新邏輯都寫完了,我們來測試下吧:

大功告成!

我們實現(xiàn)了 patch 的功能,也就是細粒度的按需更新。

代碼上傳到了 github:https://github.com/QuarkGluonPlasma/frontend-framework-exercize

總結(jié)

patch 和 render 一樣,也是遞歸的處理元素、組件、文本。

patch 時要對比下 dom 中的和要渲染的 vdom 的一些信息,然后決定渲染新的 dom,還是復用已有 dom,所以 render 的時候要在 dom 上記錄 instance、key 等信息。

元素的子元素更新要支持 key做標識,這樣可以復用之前的元素,減少 dom 的創(chuàng)建。屬性設置的時候 event listener 要每次刪掉已有的再添加一個新的,保證只會有一個。

實現(xiàn)了 vdom 的渲染和更新,實現(xiàn)了組件和生命周期,這已經(jīng)是一個完整的前端框架了。

這是我們實現(xiàn)的前端框架的第一個版本,叫做 Dong 1.0。

但是,現(xiàn)在的前端框架是遞歸的 render 和 patch 的,如果 vdom 樹太大,會計算量很大,性能不會很好,后面的 Dong 2.0 我們再把 vdom 改造成 fiber,然后實現(xiàn)下 hooks 的功能。


責任編輯:武曉燕 來源: 神光的編程秘籍
相關推薦

2021-04-27 19:20:54

微應用模塊聯(lián)邦

2022-01-25 18:11:55

vdomclassfunction

2022-01-21 08:21:48

前端vdom渲染

2021-04-25 18:42:02

Serverless 文件上傳用戶管理

2018-03-27 13:26:51

教程

2010-06-11 09:01:02

.NET 4并行編程

2014-12-25 10:48:21

程序員代碼

2021-06-04 05:16:33

瀏覽器js源碼

2021-06-07 00:15:26

瀏覽器HtmlParser

2025-02-24 07:39:53

2020-12-16 05:58:43

算法數(shù)據(jù)結(jié)構(gòu)前端

2012-02-06 13:15:37

IP-guard三重保信息防泄漏溢信科技

2013-01-21 13:52:47

2023-05-04 10:43:42

Qwik前端框架

2018-06-01 15:41:21

2022-03-07 14:39:01

前端框架批處理

2021-08-04 10:36:34

git項目開發(fā)

2022-02-11 13:44:56

fiber架構(gòu)React

2013-03-21 13:56:21

JavaScriptBackBone

2022-04-10 10:42:44

CSS前端前端布局
點贊
收藏

51CTO技術棧公眾號