日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
手寫簡(jiǎn)易前端框架:Patch更新(1.0完結(jié)篇)

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

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

測(cè)試代碼

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

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

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

function Item(props) {
return
  • {props.children} X
  • ;
    }

    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


      {this.state.list.map((item, index) => {
      return this.handleItemRemove(index)}>{item.text}
      })}


    {this.ref = ele}}/>


    ;
    }
    }

    render(, document.getElementById('root'));

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

    實(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)了更新功能!

    開個(gè)玩笑。前端框架不會(huì)用這樣的方式更新的,多了很多沒必要的 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 來控制,如果 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 怎么實(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è)組件渲染出來的,不是的話,直接替換,是的話更新子元素:

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

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

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

    instance.dom.__instance = instance;

    然后更新的時(shí)候就可以對(duì)比下 constructor 是否一樣,如果一樣說明是同一個(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 是元素的話,要看下是否是同一類型的:

    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é)點(diǎn)和屬性

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

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

    更新的時(shí)候如果找到 key 就重用,沒找到就 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 它的子元素。如果沒找到,就 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);
    }

    也就是這樣的功能:

     {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);
    }
    }

    文本、組件、元素的更新邏輯都寫完了,我們來測(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 樹太大,會(huì)計(jì)算量很大,性能不會(huì)很好,后面的 Dong 2.0 我們?cè)侔?vdom 改造成 fiber,然后實(shí)現(xiàn)下 hooks 的功能。


    當(dāng)前標(biāo)題:手寫簡(jiǎn)易前端框架:Patch更新(1.0完結(jié)篇)
    分享網(wǎng)址:http://www.dlmjj.cn/article/cogises.html