詳解Vue中的自定義渲染器和異步渲染
自定義渲染器的原理
渲染器是圍繞 Virtual DOM 而存在的,在 Web 平臺(tái)下它能夠把 Virtual DOM 渲染為瀏覽器中的真實(shí) DOM 對象,通過前面幾章的講解,相信你已經(jīng)能夠認(rèn)識(shí)到渲染器的實(shí)現(xiàn)原理,為了能夠?qū)?Virtual DOM 渲染為真實(shí) DOM,渲染器內(nèi)部需要調(diào)用瀏覽器提供的 DOM 編程接口,下面羅列了在出上一章中我們曾經(jīng)使用到的那些瀏覽器為我們提供的 DOM 編程接口:
- document.createElement / createElementNS:創(chuàng)建標(biāo)簽元素。
- document.createTextNode:創(chuàng)建文本元素。
- el.nodeValue:修改文本元素的內(nèi)容。
- el.removeChild:移除 DOM 元素。
- el.insertBefore:插入 DOM 元素。
- el.appendChild:追加 DOM 元素。
- el.parentNode:獲取父元素。
- el.nextSibling:獲取下一個(gè)兄弟元素。
- document.querySelector:掛載 Portal 類型的 VNode 時(shí),用它查找掛載點(diǎn)。
這些 DOM 編程接口完成了 Web 平臺(tái)(或者說瀏覽器)下對 DOM 的增加、刪除、查找的工作,它是 Web 平臺(tái)獨(dú)有的,所以如果渲染器自身強(qiáng)依賴于這些方法(函數(shù)),那么這個(gè)渲染器也只能夠運(yùn)行在瀏覽器中,它不具備跨平臺(tái)的能力。換句話說,如果想要實(shí)現(xiàn)一個(gè)平臺(tái)無關(guān)的渲染器,那么渲染器自身必須不能強(qiáng)依賴于任何一個(gè)平臺(tái)下特有的接口,而是應(yīng)該提供一個(gè)抽象層,將 “DOM” 的增加、刪除、查找等操作使用抽象接口實(shí)現(xiàn),具體到某個(gè)平臺(tái)下時(shí),由開發(fā)者決定如何使用該平臺(tái)下的接口實(shí)現(xiàn)這個(gè)抽象層,這就是自定義渲染器的本質(zhì)。
tip
在下文中,我們將使用 “元素” 一詞指代所有平臺(tái)中的元素對象,例如在 Web 平臺(tái)下 “元素” 一詞指的就是 DOM 元素。
渲染器除了負(fù)責(zé)對元素的增加、刪除、查找之外,它還負(fù)責(zé)修改某個(gè)特定元素自身的屬性/特性,例如 Web 平臺(tái)中元素具有 id、href 等屬性/特性。在上一章中,我們使用 patchData 函數(shù)來完成元素自身屬性/特性的更新,如下代碼用于修改一個(gè)元素的類名列表(class):
// patchData.js case 'class': el.className = nextValue break
這段代碼同樣也只能運(yùn)行在瀏覽器中,為了渲染器能夠跨平臺(tái),那么修改一個(gè)元素自身的屬性/特性的工作也應(yīng)該作為可自定義的一部分才行,因此,一個(gè)跨平臺(tái)的渲染器應(yīng)該至少包含兩個(gè)可自定義的部分:可自定義元素的增加、刪除、查找等操作、可自定義元素自身屬性/特性的修改操作。這樣對于任何一個(gè)元素來說,它的增刪改查都已經(jīng)變成了可自定義的部分,我們只需要“告知”渲染器在對元素進(jìn)行增刪改查時(shí)應(yīng)該做哪些具體的操作即可。
接下來我們就著手將一個(gè)普通渲染器修改為擁有自定義能力的渲染器,在之前的講解中,我們將渲染器的代碼存放在了 render.js 文件中,如下是整個(gè) render.js 文件的核心代碼:
export default function createRenderer(options) {
function render(vnode, container) { /* ... */ }
// ========== 掛載 ==========
function mount(vnode, container, isSVG, refNode) { /* ... */ }
function mountElement(vnode, container, isSVG, refNode) { /* ... */ }
function mountText(vnode, container) { /* ... */ }
function mountFragment(vnode, container, isSVG) { /* ... */ }
function mountPortal(vnode, container) { /* ... */ }
function mountComponent(vnode, container, isSVG) { /* ... */ }
function mountStatefulComponent(vnode, container, isSVG) { /* ... */ }
function mountFunctionalComponent(vnode, container, isSVG) { /* ... */ }
// ========== patch ==========
function patch(prevVNode, nextVNode, container) { /* ... */ }
function replaceVNode(prevVNode, nextVNode, container) { /* ... */ }
function patchElement(prevVNode, nextVNode, container) { /* ... */ }
function patchChildren(
prevChildFlags,
nextChildFlags,
prevChildren,
nextChildren,
container
) { /* ... */ }
function patchText(prevVNode, nextVNode) { /* ... */ }
function patchFragment(prevVNode, nextVNode, container) { /* ... */ }
function patchPortal(prevVNode, nextVNode) { /* ... */ }
function patchComponent(prevVNode, nextVNode, container) { /* ... */ }
return { render }
}
// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function lis(arr) { /* ... */ }createRenderer 函數(shù)的返回值就是之前的 render 函數(shù),也就是說調(diào)用 createRenderer 函數(shù)可以創(chuàng)建一個(gè)渲染器。createRenderer 函數(shù)接收一個(gè)參數(shù) options,該參數(shù)的作用是為了允許外界有能力將操作元素的具體實(shí)現(xiàn)以選項(xiàng)的方式傳遞進(jìn)來。
那么 options 參數(shù)中應(yīng)該包含哪些選項(xiàng)呢?其實(shí)前面我們已經(jīng)分析過了,只要是需要自定義的部分就應(yīng)該作為選項(xiàng)傳遞進(jìn)來,所以參數(shù) options 中至少要包含兩部分:一部分是元素的增加、刪除、查找;另外一部分是元素的修改,即 patchData 函數(shù)。如下代碼所示:
const { render } = createRenderer({
// nodeOps 是一個(gè)對象,該對象包含了所有用于操作節(jié)點(diǎn)的方法
nodeOps: {
createElement() { /* ... */ },
createText() { /* ... */ }
// more...
},
patchData
})基于此,在 createRenderer 函數(shù)內(nèi)部我們就可以通過解構(gòu)的方式從 options 參數(shù)中得到具體的方法:
export default function createRenderer(options) {
// options.nodeOps 選項(xiàng)中包含了本章開頭羅列的所有操作 DOM 的方法
// options.patchData 選項(xiàng)就是 patchData 函數(shù)
const {
nodeOps: {
createElement: platformCreateElement,
createText: platformCreateText,
setText: platformSetText, // 等價(jià)于 Web 平臺(tái)的 el.nodeValue
appendChild: platformAppendChild,
insertBefore: platformInsertBefore,
removeChild: platformRemoveChild,
parentNode: platformParentNode,
nextSibling: platformNextSibling,
querySelector: platformQuerySelector
},
patchData: platformPatchData
} = options
function render(vnode, container) { /* ... */ }
// ========== 掛載 ==========
// 省略...
// ========== patch ==========
// 省略...
return { render }
}如上代碼所示,options.nodeOps 選項(xiàng)是一個(gè)對象,它包含了所有用于對元素進(jìn)行增、刪、查的操作,options.patchData 選項(xiàng)是一個(gè)函數(shù),用于處理某個(gè)特定元素上的屬性/特性,這些內(nèi)容都是在創(chuàng)建渲染器時(shí)由外界來決定的。
接下來我們要做的就是將渲染器中原本使用了 Web 平臺(tái)進(jìn)行 DOM 操作的地方修改成使用通過解構(gòu)得到的函數(shù)進(jìn)行替代,例如在創(chuàng)建 DOM 元素時(shí),原來的實(shí)現(xiàn)如下:
function mountElement(vnode, container, isSVG, refNode) {
isSVG = isSVG || vnode.flags & VNodeFlags.ELEMENT_SVG
const el = isSVG
? document.createElementNS('http://www.w3.org/2000/svg', vnode.tag)
: document.createElement(vnode.tag)
// 省略...
}現(xiàn)在我們應(yīng)該使用 platformCreateElement 函數(shù)替代 document.createElement(NS):
function mountElement(vnode, container, isSVG, refNode) {
isSVG = isSVG || vnode.flags & VNodeFlags.ELEMENT_SVG
const el = platformCreateElement(vnode.tag, isSVG)
// 省略...
}類似的,其他所有涉及 DOM 操作的地方都應(yīng)該使用這些通過解構(gòu)得到的抽象接口替代。當(dāng)這部分工作完成之后,接下來要做的就是對這些用于操作節(jié)點(diǎn)的抽象方法進(jìn)行實(shí)現(xiàn),如下代碼所示,我們實(shí)現(xiàn)了 Web 平臺(tái)下創(chuàng)建 DOM 節(jié)點(diǎn)的方法:
const { render } = createRenderer({
nodeOps: {
createElement(tag, isSVG) {
return isSVG
? document.createElementNS('http://www.w3.org/2000/svg', tag)
: document.createElement(tag)
}
}
})再舉一個(gè)例子,下面這條語句是我們之前實(shí)現(xiàn)的渲染器中用于移除舊 children 中節(jié)點(diǎn)的代碼:
container.removeChild(prevChildren.el)
現(xiàn)在我們將之替換為 platformRemoveChild 函數(shù):
platformRemoveChild(container, prevVNode.el)
為了讓這段代碼在 Web 平臺(tái)正常工作,我們需要在創(chuàng)建渲染器時(shí)實(shí)現(xiàn) nodeOps.removeChild 函數(shù):
const { render } = createRenderer({
nodeOps: {
createElement(tag, isSVG) {
return isSVG
? document.createElementNS('http://www.w3.org/2000/svg', tag)
: document.createElement(tag)
},
removeChild(parent, child) {
parent.removeChild(child)
}
}
})也許你已經(jīng)想到了,當(dāng)我們實(shí)現(xiàn)了所有 nodeOps 下的規(guī)定的抽象接口之后,實(shí)際上就完成了一個(gè)面向 Web 平臺(tái)的渲染器,如下代碼所示:
const { render } = createRenderer({
nodeOps: {
createElement(tag, isSVG) {
return isSVG
? document.createElementNS('http://www.w3.org/2000/svg', tag)
: document.createElement(tag)
},
removeChild(parent, child) {
parent.removeChild(child)
},
createText(text) {
return document.createTextNode(text)
},
setText(node, text) {
node.nodeValue = text
},
appendChild(parent, child) {
parent.appendChild(child)
},
insertBefore(parent, child, ref) {
parent.insertBefore(child, ref)
},
parentNode(node) {
return node.parentNode
},
nextSibling(node) {
return node.nextSibling
},
querySelector(selector) {
return document.querySelector(selector)
}
}
})當(dāng)然了,如上代碼所創(chuàng)建的渲染器只能夠完成 Web 平臺(tái)中對 DOM 的增加、刪除和查找的功能,為了能夠修改 DOM 元素自身的屬性和特性,我們還需要在創(chuàng)建渲染器時(shí)將 patchData 函數(shù)作為選項(xiàng)傳遞過去,好在我們之前已經(jīng)封裝了 patchData 函數(shù),現(xiàn)在直接拿過來用即可:
import { patchData } from './patchData'
const { render } = createRenderer({
nodeOps: {
// 省略...
},
patchData
})以上我們就完成了對渲染器的抽象,使它成為一個(gè)平臺(tái)無關(guān)的工具。并基于此實(shí)現(xiàn)了一個(gè) Web 平臺(tái)的渲染器,專門用于瀏覽器環(huán)境。
自定義渲染器的應(yīng)用
Vue3 提供了一個(gè)叫做 @vue/runtime-test 的包,其作用是方便開發(fā)者在無 DOM 環(huán)境時(shí)有能力對組件的渲染內(nèi)容進(jìn)行測試,這實(shí)際上就是對自定義渲染器的應(yīng)用。本節(jié)我們嘗試來實(shí)現(xiàn)與 @vue/runtime-test 具有相同功能的渲染器。
原理其實(shí)很簡單,如下代碼所示,這是用于 Web 平臺(tái)下創(chuàng)建真實(shí) DOM 元素的代碼:
const { render } = createRenderer({
nodeOps: {
createElement(tag, isSVG) {
return isSVG
? document.createElementNS('http://www.w3.org/2000/svg', tag)
: document.createElement(tag)
}
}
})其中 nodeOps.createElement 函數(shù)會(huì)返回一個(gè)真實(shí)的 DOM 對象,在其內(nèi)部調(diào)用的是瀏覽器為我們提供的 document.createElement/NS 函數(shù)。實(shí)際上 nodeOps.createElement 函數(shù)的真正意圖是:創(chuàng)建一個(gè)元素,然而并沒有規(guī)定這個(gè)元素應(yīng)該由誰來創(chuàng)建,或這個(gè)元素應(yīng)該具有什么樣的特征,這就是自定義的核心所在。因此,我們完全使 nodeOps.createElement 函數(shù)返回一個(gè)普通對象來代指一個(gè)元素,后續(xù)的所有操作都是基于我們所規(guī)定的元素而進(jìn)行,如下代碼所示:
const { render } = createRenderer({
nodeOps: {
createElement(tag) {
const customElement = {
type: 'ELEMENT',
tag
}
return customElement
}
}
})在這段代碼中,我們自行規(guī)定了 nodeOps.createElement 函數(shù)所返回的元素的格式,即 customElement 對象,它包含兩個(gè)屬性,分別是 用來代表元素類型的 type 屬性以及用來代表元素名稱的 tag 屬性。雖然看上去很奇怪,但這確實(shí)是一個(gè)完全符合要求的實(shí)現(xiàn)。這么做的結(jié)果就是:nodeOps.createElement 函數(shù)所創(chuàng)建的元素不來自于瀏覽器的 DOM 編程接口,更不來自于任何其他平臺(tái)的 API,因此,如上代碼所創(chuàng)建的渲染器也將是一個(gè)平臺(tái)無關(guān)的渲染器。這就是為什么 @vue/runtime-test 可以運(yùn)行在 NodeJs 中的原因。
當(dāng)然了,如上代碼中 customElement 只有兩個(gè)屬性,實(shí)際上這并不能滿足需求,即使元素的格式由我們自行定義,但還是要有一定的限制,例如元素會(huì)有子節(jié)點(diǎn),子節(jié)點(diǎn)也需要保存對父節(jié)點(diǎn)的引用,元素自身也會(huì)有屬性/特性等等。一個(gè)最小且完整的元素定義應(yīng)該包含以下屬性:
const customElement = {
type, // 元素的類型:ELEMENT ---> 標(biāo)簽元素;TEXT ---> 文本
tag, // 當(dāng) type === 'ELEMENT' 時(shí),tag 屬性為標(biāo)簽名字
parentNode, // 對父節(jié)點(diǎn)的引用
children, // 子節(jié)點(diǎn)
props, // 當(dāng) type === 'ELEMENT' 時(shí),props 中存儲(chǔ)著元素的屬性/特性
eventListeners, // 當(dāng) type === 'ELEMENT' 時(shí),eventListeners 中存儲(chǔ)著元素的事件信息
text // 當(dāng) type === 'TEXT' 時(shí),text 存儲(chǔ)著文本內(nèi)容
}現(xiàn)在 customElement 就是一個(gè)能完全代替真實(shí) DOM 對象的模擬實(shí)現(xiàn)了,我們用它修改之前的代碼:
const { render } = createRenderer({
nodeOps: {
createElement(tag) {
const customElement = {
type: 'ELEMENT',
tag,
parentNode: null,
children: [],
props: {},
eventListeners: {},
text: null
}
return customElement
}
}
})如上代碼所示,由于 nodeOps.createElement 函數(shù)用于創(chuàng)建元素節(jié)點(diǎn),因此 type 屬性的值為 'ELEMENT';剛剛創(chuàng)建的元素還不能確定其父節(jié)點(diǎn),因此 parentNode 為 null;用于存儲(chǔ)子節(jié)點(diǎn)的 children 屬性被初始化為一個(gè)數(shù)組,props 屬性和 eventListeners 被初始化為空對象;最后的 text 為 null,因?yàn)樗皇且粋€(gè)文本節(jié)點(diǎn)。
現(xiàn)在創(chuàng)建元素節(jié)點(diǎn)的功能已經(jīng)實(shí)現(xiàn),那么創(chuàng)建文本節(jié)點(diǎn)呢?如下:
const { render } = createRenderer({
nodeOps: {
createElement(tag) {
const customElement = {/* 省略... */}
return customElement
},
createText(text) {
const customElement = {
type: 'TEXT',
parentNode: null,
text: text
}
return customElement
}
}
})文本元素的 type 類型值為 'TEXT',parentNode 同樣被初始化為 null,text 屬性存儲(chǔ)著文本節(jié)點(diǎn)的內(nèi)容。由于文本元素沒有子節(jié)點(diǎn)、屬性/特性、事件等信息,因此不需要其他描述信息。
文本節(jié)點(diǎn)與元素節(jié)點(diǎn)的創(chuàng)建都已經(jīng)實(shí)現(xiàn),接下來我們看看當(dāng)元素被追加時(shí)應(yīng)該如何處理,即 nodeOps.appendChild 函數(shù)的實(shí)現(xiàn):
const { render } = createRenderer({
nodeOps: {
createElement(tag) {
const customElement = {/* 省略... */}
return customElement
},
createText(text) {
const customElement = {/* 省略... */}
return customElement
},
appendChild(parent, child) {
// 簡歷父子關(guān)系
child.parentNode = parent
parent.children.push(child)
}
}
})如上高亮代碼所示,追加節(jié)點(diǎn)時(shí)我們要做的就是建立節(jié)點(diǎn)間正確的父子關(guān)系,在 Web 平臺(tái)下,當(dāng)我們調(diào)用 el.appendChild 函數(shù)時(shí),父子關(guān)系是由瀏覽器負(fù)責(zé)建立的,但在模擬實(shí)現(xiàn)中,這個(gè)關(guān)系需要我們自己來維護(hù)。不過好在這很簡單,讓子元素的 parentNode 指向父元素,同時(shí)將子元素添加到父元素的 children 數(shù)組中即可。
類似的,如下是 nodeOps.removeChild 函數(shù)的實(shí)現(xiàn):
const { render } = createRenderer({
nodeOps: {
createElement(tag) {/* 省略... */},
createText(text) {/* 省略... */},
appendChild(parent, child) {
// 簡歷父子關(guān)系
child.parentNode = parent
parent.children.push(child)
},
removeChild(parent, child) {
// 找到將要移除的元素 child 在父元素的 children 中的位置
const i = parent.children.indexOf(child)
if (i > -1) {
// 如果找到了,則將其刪除
parent.children.splice(i, 1)
} else {
// 沒找到,說明渲染器出了問題,例如沒有在 nodeOps.appendChild 函數(shù)中維護(hù)正確的父子關(guān)系等
// 這時(shí)需要打印錯(cuò)誤信息,以提示開發(fā)者
console.error('target: ', child)
console.error('parent: ', parent)
throw Error('target 不是 parent 的子節(jié)點(diǎn)')
}
// 清空父子鏈
child.parentNode = null
}
}
})如上高亮代碼所示,在移除節(jié)點(diǎn)時(shí),思路也很簡單,首先需要在父節(jié)點(diǎn)的 children 屬性中查找即將要被移除的節(jié)點(diǎn)的位置索引,如果找到了,那么就直接將其從父節(jié)點(diǎn)的 children 數(shù)組中移除即可。如果沒有找到則說明渲染器出問題了,例如在你實(shí)現(xiàn)自定義渲染器時(shí)沒有在 nodeOps.appendChild 函數(shù)或 nodeOps.insertBefore 函數(shù)中維護(hù)正確的父子關(guān)系,這時(shí)我們需要打印錯(cuò)誤信息以提示開發(fā)者。最后不要忘記清空父子鏈。
通過如上的講解,你可能已經(jīng)領(lǐng)會(huì)到了,我們所做的其實(shí)就是在模擬 Web 平臺(tái)在操作元素時(shí)的行為,并且這個(gè)模擬的思路也及其簡單。實(shí)際上,當(dāng)我們實(shí)現(xiàn)了所有 nodeOps 下的抽象函數(shù)之后,那么這個(gè)類似于 @vue/runtime-test 的自定義渲染器就基本完成了。當(dāng)然,不要忘記的是我們還需要實(shí)現(xiàn) patchData 函數(shù),這可能比你想象的要簡單的多,如下高亮代碼所示:
const { render } = createRenderer({
nodeOps: {
createElement(tag) {/* 省略... */},
createText(text) {/* 省略... */},
appendChild(parent, child) {/* 省略... */},
removeChild(parent, child) {/* 省略... */}
// 其他 nodeOps 函數(shù)的實(shí)現(xiàn)
},
patchData(
el,
key,
prevValue,
nextValue
) {
// 將屬性添加到元素的 props 對象下
el.props[key] = nextValue
// 我們將屬性名字中前兩個(gè)字符是 'o' 和 'n' 的屬性認(rèn)為是事件綁定
if (key[0] === 'o' && key[1] === 'n') {
// 如果是事件,則將事件添加到元素的 eventListeners 對象下
const event = key.slice(2).toLowerCase()
;(el.eventListeners || (el.eventListeners = {}))[event] = nextValue
}
}
})在創(chuàng)建渲染器時(shí)我們需要實(shí)現(xiàn) patchData 函數(shù)的功能,它的功能是用來更新元素自身的屬性/特性的,在之前的講解中我們實(shí)現(xiàn)了 Web 平臺(tái)中 patchData 函數(shù),然而在這個(gè)模擬實(shí)現(xiàn)中,我們要做的事情就少了很多。只需要把元素的屬性添加到元素的 props 對象中即可,同時(shí)如果是事件的話,我們也只需要將其添加到元素的 eventListeners 對象中就可以了。
實(shí)際上,本節(jié)我們所實(shí)現(xiàn)的自定義渲染器,就能夠滿足我們對組件測試的需求,我們可以利用它來測試組件所渲染內(nèi)容的正確性。如果你想要進(jìn)一步提升該自定義渲染器的能力,例如希望該渲染器有能力在控制臺(tái)中打印出操作元素的信息,也很簡單,我們以創(chuàng)建元素為例,如下代碼所示:
const { render } = createRenderer({
nodeOps: {
createElement(tag) {
const customElement = {
type: 'ELEMENT',
tag,
parentNode: null,
children: [],
props: {},
eventListeners: {},
text: null
}
console.table({
type: 'CREATE ELEMENT',
targetNode: customElement
})
??????? return customElement
}
}
})只需要在 nodeOps.createElement 函數(shù)中調(diào)用 console.table 進(jìn)行打印你想要的信息即可,例如我們打印了一個(gè)對象,該對象包含 type 屬性用于指示當(dāng)前操作元素的類型,所以對于創(chuàng)建元素來說,我們?yōu)?type 屬性賦值了字符串 'CREATE ELEMENT',同時(shí)將目標(biāo)節(jié)點(diǎn)也打印了出來(即 targetNode)。類似的,追加節(jié)點(diǎn)可以打印如下信息:
const { render } = createRenderer({
nodeOps: {
createElement(tag) {/* 省略... */},
appendChild(parent, child) {
// 建立父子關(guān)系
child.parentNode = parent
parent.children.push(child)
??????? console.table({
type: 'APPEND',
targetNode: child,
parentNode: parent
})
}
}
})怎么樣,是不是很簡單。當(dāng)然了這只是自定義渲染器的應(yīng)用之一,對于自定義渲染器來說,它可發(fā)揮的空間還是非常大的,舉幾個(gè)例子:
- 渲染到 PDF,我們可以實(shí)現(xiàn)一個(gè)自定義渲染器如 vue-pdf-renderer,它能夠?qū)?Vue 組件渲染為 PDF 文件。
- 渲染到文件系統(tǒng),我們可以實(shí)現(xiàn)一個(gè) vue-file-renderer,它可以根據(jù) VNode 的結(jié)構(gòu)在本地渲染與該結(jié)構(gòu)相同的文件目錄。
- canvas 渲染器,我們可以實(shí)現(xiàn)一個(gè) vue-canvas-renderer,它可以從渲染器的層面渲染 canvas,而非組件層面。
這僅僅是簡單的列了幾個(gè)小想法,實(shí)際上由于自定義渲染器本身就是平臺(tái)無關(guān)的,很多事情需要看特定平臺(tái)的能力,渲染器為你提供的就是在組件層面的抽象能力以及虛擬 DOM 的更新算法,剩下的就靠社區(qū)的想象力和實(shí)現(xiàn)能力了。
以上就是詳解Vue中的自定義渲染器和異步渲染的詳細(xì)內(nèi)容,更多關(guān)于Vue自定義渲染器和異步渲染的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue.js學(xué)習(xí)筆記之綁定style樣式和class列表
數(shù)據(jù)綁定一個(gè)常見需求是操作元素的 class 列表和它的內(nèi)聯(lián)樣式。這篇文章主要介紹了vue.js綁定style和class的相關(guān)資料,需要的朋友可以參考下2016-10-10
element表格el-table實(shí)現(xiàn)虛擬滾動(dòng)解決卡頓問題
當(dāng)頁面數(shù)據(jù)過多,前端渲染大量的DOM時(shí),會(huì)造成頁面卡死問題,本文主要介紹了element表格el-table實(shí)現(xiàn)虛擬滾動(dòng)解決卡頓問題,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10
vue計(jì)算屬性想要傳入?yún)?shù)如何解決
這篇文章主要介紹了vue計(jì)算屬性想要傳入?yún)?shù)如何解決問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01
vue-cli3.x配置全局的scss的時(shí)候報(bào)錯(cuò)問題及解決
這篇文章主要介紹了vue-cli3.x配置全局的scss的時(shí)候報(bào)錯(cuò)問題及解決,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04

