首頁 > 軟體

Vue編譯優化實現流程詳解

2023-01-31 06:01:28

動態節點收集與修補程式標誌

1.傳統diff演演算法的問題

對於一個普通模板檔案,如果只是標籤中的內容發生了變化,那麼最簡單的更新方法很明顯是直接替換標籤中的文字內容。但是diff演演算法很明顯做不到這一點,它會重新生成一棵虛擬DOM樹,然後對兩棵虛擬DOM樹進行比較。很明顯,與直接替換標籤中的內容相比,傳統diff演演算法需要做很多無意義的操作,如果能夠去除這些無意義的操作,將會省下一筆很大的效能開銷。其實,只要在模板編譯時,標記出哪些節點是動態的,哪些是靜態的,然後再通過虛擬DOM傳遞給渲染器,渲染器就能根據這些資訊,直接修改對應節點,從而提高執行時效能。

2.Block和PatchFlags

對於一個傳統的模板:

<div>
    <div>
        foo
    </div>
    <p>
        {{ bar }}
    </p>
</div>

在這個模板中,只用{{ bar }}是動態內容,因此在bar變數發生變化時,只需要修改p標籤內的內容就行了。因此我們在這個模板對於的虛擬DOM中,加入patchFlag屬性,以此來標籤模板中的動態內容。

const vnode = {
    tag: 'div',
    children: [
        { tag: 'div', children: 'foo' },
        { tag: 'p', children: ctx.bar, patchFlag: 1 },
    ]
}

對於不同的數值繫結,我們分別用不同的patch值來表示:

  • 數位1,代表節點有動態的textContent
  • 數位2,代表節點有動態的class繫結
  • 數位3,代表節點有動態的style繫結
  • 數位4,其他…

我們可以新建一個列舉型別來表示這些值:

enum PatchFlags {
    TEXT: 1,
    CLASS,
    STYLE,
    OTHER
}

這樣我們就在虛擬DOM的建立階段,將動態節點提取出來:

const vnode = {
    tag: 'div',
    children: [
        { tag: 'div', children: 'foo' },
        { tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT },
    ],
    dynamicChildren: [
        { tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT },
    ]
}

3.收集動態節點

首先我們建立收集動態節點的邏輯。

const dynamicChildrenStack = []; // 動態節點棧
let currentDynamicChildren = null; // 當前動態節點集合
function openBlock() {
    // 建立一個新的動態節點棧
	dynamicChildrenStack.push((currentDynamicChildren = []));
}
function closeBlock() {
    // openBlock建立的動態節點集合彈出
    currentDynamicChildren = dynamicChildrenStack.pop();
}

然後,我們在建立虛擬節點的時候,對動態節點進行收集。

function createVNode(tag, props, children, flags) {
    const key = props && props.key;
    props && delete props.key;
    const vnode = {
        tag,
        props,
        children,
        key,
        patchFlags: flags
    }
    if(typeof flags !== 'undefined' && currentDynamicChildren) {
        currentDynamicChildren.push(vnode);
    }
    return vnode;
}

然後我們修改元件渲染函數的邏輯。

render() {
    return (openBlock(), createBlock('div', null, [
        createVNode('p', { class: 'foo' }, null, 1),
        createVNode('p', { class: 'bar' }, null)
    ]));
}
function createBlock(tag, props, children) {
    const block = createVNode(tag, props, children);
    block.dynamicChildren = currentDynamicChildren;
    closeBlock();
    return block;
}

4.渲染器執行時支援

function patchElement(n1, n2) {
    const el = n2.el = n1.el;
    const oldProps = n1.props;
    const newProps = n2.props;
    // ...
    if(n2.dynamicChildren) {
        // 如果有動態節點陣列,直接更新動態節點陣列
        patchBlockChildren(n1, n2);
    } else {
        patchChildren(n1, n2, el);
    }
}
function pathcBlockChildren(n1, n2) {
    for(let i = 0; i < n2.dynamicChildren.length; i++) {
        patchElement(n1.dynamicChildren[i], n2.dynamicChildren[i]);
    }
}

由於我們標記了不同的動態節點型別,因此我們可以針對性的完成靶向更新。

function patchElement(n1, n2) {
    const el = n2.el = n1.el;
    const oldProps = n1.props;
    const newProps = n2.props;
    if(n2.patchFlags) {
        if(n2.patchFlags === 1) {
            // 只更新內容
        } else if(n2.patchFlags === 2) {
            // 只更新class
        } else if(n2.patchFlags === 3) {
            // 只更新style
        } else {
            // 更新所有
            for(const k in newProps) {
                if(newProps[key] !== oldProps[key]) {
                	patchProps(el, key, oldProps[k], newProps[k]);
                }
            }
            for(const k in oldProps) {
                if(!key in newProps) {
                    patchProps(el, key, oldProps[k], null);
                }
            }
        }
    }
    patchChildren(n1, n2, el);
}

5.Block樹

元件的根節點必須作為Block角色,這樣,從根節點開始的所有動態子代節點都會被收集到根節點的dynamicChildren陣列中。除了根節點外,帶有v-if、v-for這種結構化指令的節點,也會被作為Block角色,這些Block角色共同構成一棵Block樹。

靜態提升

假設有以下模板

<div>
    <p>
        static text
    </p>
    <p>
        {{ title }}
    </p>
</div>

預設情況下,對應的渲染函數為:

function render() {
    return (openBlock(), createBlock('div', null, [
        createVNode('p', null, 'static text'),
        createVNode('p', null, ctx.title, 1 /* TEXT */)
    ]))
}

在這段程式碼中,當ctx.title屬性變化時,內容為靜態文字的p標籤節點也會跟著渲染一次,這很明顯式不必要的。因此,我們可以使用“靜態提升”,即將靜態節點,提取到渲染函數之外,這樣渲染函數在執行的時候,只是保持了對靜態節點的參照,而不會重新建立虛擬節點。

const hoist1 = createVNode('p', null, 'static text');
function render() {
    return (openBlock(), createBlock('div', null, [
        hoist1,
        createVNode('p', null, ctx.title, 1 /* TEXT */)
    ]))
}

除了靜態節點,對於靜態props我們也可以將其進行靜態提升處理。

const hoistProps = { foo: 'bar', a: '1' };
function render() {
    return (openBlock(), createBlock('div', null, [
        hoist1,
        createVNode('p', hoistProps, ctx.title, 1 /* TEXT */)
    ]))
}

預字元化

除了對節點進行靜態提升外,我們還可以對於純靜態的模板進行預字元化。對於這樣一個模板:

<templete>
	<p></p>
    <p></p>
    <p></p>
    <p></p>
    <p></p>
    ...
    <p></p>
    <p></p>
    <p></p>
    <p></p>
</templete>

我們完全可以將其預處理為:

const hoistStatic = createStaticVNode('<p></p><p></p><p></p><p></p>...<p></p><p></p><p></p><p></p>');
render() {
    return (openBlock(), createBlock('div', null, [
		hoistStatic
    ]));
}

這麼做的優勢:

  • 大塊的靜態內容可以通過innerHTML直接設定,在效能上具有一定優勢
  • 減少建立虛擬節點帶來的額外開銷
  • 減少記憶體佔用

快取內聯事件處理常式

當為元件新增內聯事件時,每次新建一個元件,都會為該元件重新建立並繫結一個新的內聯事件函數,為了避免這方面的無意義開銷,我們可以對內聯事件處理常式進行快取。

function render(ctx, cache) {
    return h(Comp, {
        onChange: cache[0] || cache[0] = ($event) => (ctx.a + ctx.b);
    })
}

v-once

v-once指令可以是元件只渲染一次,並且即使該元件繫結了動態引數,也不會更新。它與內聯事件一樣,也是使用了快取,同時通過setBlockTracking(-1)阻止該VNode被Block收集。

v-once的優點:

  • 避免元件更新時重新建立虛擬DOM帶來的效能開銷
  • 避免無用的Diff開銷

到此這篇關於Vue編譯優化實現流程詳解的文章就介紹到這了,更多相關Vue編譯優化內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com