成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

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

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

首先,我們準(zhǔn)備下測(cè)試代碼:

測(cè)試代碼

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

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

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

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)實(shí)現(xiàn)了渲染,現(xiàn)在要實(shí)現(xiàn)更新,也就是 setState 之后更新頁(yè)面的流程。

實(shí)現(xiàn) patch

其實(shí)最簡(jiǎn)單的更新就是 setState 的時(shí)候重新渲染一次,整個(gè)替換掉之前的 dom:

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

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

測(cè)試下:

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

開(kāi)個(gè)玩笑。前端框架不會(huì)用這樣的方式更新的,多了很多沒(méi)必要的 dom 操作,性能太差。

所以還是要實(shí)現(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 邏輯,這里可以加一個(gè) shouldComponentUpdate 來(lái)控制,如果 props 和 state 都沒(méi)變就不用 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 怎么實(shí)現(xiàn)呢?

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

文本

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

  • 如果 vdom 不是文本節(jié)點(diǎn),直接替換
  • 如果 vdom 也是文本節(jié)點(diǎn),那就對(duì)比下內(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 的實(shí)現(xiàn)是用 replaceChild:

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

然后是組件的更新:

組件

如果 vdom 是組件的話,對(duì)應(yīng)的 dom 可能是同一個(gè)組件渲染的,也可能不是。

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

怎么知道 dom 是什么組件渲染出來(lái)的呢?

我們需要在 render 的時(shí)候在 dom 上加個(gè)屬性來(lái)記錄:

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

instance.dom.__instance = instance;

然后更新的時(shí)候就可以對(duì)比下 constructor 是否一樣,如果一樣說(shuō)明是同一個(gè)組件,那 dom 是差不多的,再 patch 子元素:

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

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

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

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 是元素的話,要看下是否是同一類(lèi)型的:

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 是元素的話,要看下是否是同一類(lèi)型的:

不同類(lèi)型的元素,直接替換

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

同一類(lèi)型的元素,更新子節(jié)點(diǎn)和屬性

更新子節(jié)點(diǎn)我們希望能重用的就重用,所以在 render 的時(shí)候給每個(gè)元素加上一個(gè)標(biāo)識(shí) key:

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

更新的時(shí)候如果找到 key 就重用,沒(méi)找到就 render 一個(gè)新的。

首先我們把所有的子節(jié)點(diǎn)的 dom 放到一個(gè)對(duì)象里:

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

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

默認(rèn) key 設(shè)置為 index。

然后循環(huán)渲染 vdom 的 children,如果找到對(duì)應(yīng)的 key 就直接復(fù)用,然后繼續(xù) patch 它的子元素。如果沒(méi)找到,就 render 一個(gè)新的:

[].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é)點(diǎn)處理完了,再處理下屬性:

這個(gè)就是把舊的屬性刪掉,設(shè)置新的 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 多次也始終只有一個(gè):

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 的設(shè)置:

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

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

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

其余的就都是 setAttribute 設(shè)置了:

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ě)完了,我們來(lái)測(cè)試下吧:

大功告成!

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

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

總結(jié)

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

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

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

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

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

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


責(zé)任編輯:武曉燕 來(lái)源: 神光的編程秘籍
相關(guān)推薦

2022-01-25 18:11:55

vdomclassfunction

2021-04-27 19:20:54

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

2022-01-21 08:21:48

前端vdom渲染

2018-03-27 13:26:51

教程

2021-04-25 18:42:02

Serverless 文件上傳用戶管理

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

2018-06-01 15:41:21

2023-05-04 10:43:42

Qwik前端框架

2022-03-07 14:39:01

前端框架批處理

2021-08-04 10:36:34

git項(xiàng)目開(kāi)發(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前端前端布局
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 国产一区 | 黑人巨大精品欧美一区二区免费 | 可以看黄的视频 | 日韩在线播放中文字幕 | 日韩视频在线一区 | 国内自拍第一页 | 国产精品免费一区二区三区四区 | av免费电影在线 | 成人av在线播放 | 欧美精品久久久久 | a级毛片免费高清视频 | 国产精品永久免费视频 | a免费视频 | 人人做人人澡人人爽欧美 | 国产一区二区电影 | 黄色一级免费 | 青青99| 国产精品一区二区视频 | 亚洲精品在线看 | 激情av| 男女视频在线免费观看 | 国产日韩欧美在线观看 | 欧美日韩国产不卡 | 久久久123| 曰批视频在线观看 | 国产精品久久久久久久久婷婷 | 免费一区二区三区 | 久久青草av | 香蕉久久av | 免费网站国产 | 性高湖久久久久久久久3小时 | 国产激情视频网址 | 亚洲精品视频一区 | 日韩精品在线一区 | 日日夜夜精品视频 | 日韩一级欧美一级 | 九九伊人sl水蜜桃色推荐 | 欧美一级片在线播放 | 日韩中文字幕在线视频 | 天天天久久久 | 亚洲国产一区二区三区在线观看 |