Vue內(nèi)部渲染視圖的方法
1.什么是虛擬DOM
- 以前M的命令式操作DOM即使用jQuery操作DOM節(jié)點(diǎn),隨著狀態(tài)的增多,DOM的操作就會(huì)越來(lái)越頻繁,程序的狀態(tài)也越難維護(hù),現(xiàn)在主流的框架都是采用聲明式操作DOM,將操作DOM的方法封裝起來(lái),我們只要更改數(shù)據(jù)的狀態(tài),框架本身會(huì)幫我們操作DOM。
- 虛擬DOM根據(jù)狀態(tài)建立一顆虛擬節(jié)點(diǎn)樹(shù),新的虛擬節(jié)點(diǎn)樹(shù)會(huì)與舊的虛擬節(jié)點(diǎn)樹(shù)進(jìn)行對(duì)比,只渲染發(fā)生改變的部分,如下圖:

2.引入虛擬DOM的目的
- 把渲染過(guò)程抽象化,從而使得組件的抽象能力也得到提升,并且可以適配DOM以外的渲染目標(biāo);
- 可以更好地支持SSR、同構(gòu)渲染等;
- 不再依賴HTML解析器進(jìn)行模板解析,可以進(jìn)行更多的AOT(預(yù)編譯)工作提高運(yùn)行時(shí)效率,還能將Vue運(yùn)行時(shí)體積進(jìn)一步壓縮。
VNode的定義 Vue中定義了VNode的構(gòu)造函數(shù),這樣我們可以實(shí)例化不同的vnode 實(shí)例如:文本節(jié)點(diǎn)、元素節(jié)點(diǎn)以及注釋節(jié)點(diǎn)等。
var VNode = function VNode (
tag,
data,
children,
text,
elm,
context,
componentOptions,
asyncFactory
) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.ns = undefined;
this.context = context;
this.fnContext = undefined;
this.fnOptions = undefined;
this.fnScopeId = undefined;
this.key = data && data.key;
this.componentOptions = componentOptions;
this.componentInstance = undefined;
this.parent = undefined;
this.raw = false;
this.isStatic = false;
this.isRootInsert = true;
this.isComment = false;
this.isCloned = false;
this.isOnce = false;
this.asyncFactory = asyncFactory;
this.asyncMeta = undefined;
this.isAsyncPlaceholder = false;
};
vnode其實(shí)就是一個(gè)描述節(jié)點(diǎn)的對(duì)象,描述如何創(chuàng)建真實(shí)的DOM節(jié)點(diǎn);vnode的作用就是新舊vnode進(jìn)行對(duì)比,只更新發(fā)生變化的節(jié)點(diǎn)。 VNode有注釋節(jié)點(diǎn)、文本節(jié)點(diǎn)、元素節(jié)點(diǎn)、組件節(jié)點(diǎn)、函數(shù)式組件、克隆節(jié)點(diǎn):
注釋節(jié)點(diǎn)
var createEmptyVNode = function (text) {
if ( text === void 0 ) text = '';
var node = new VNode();
node.text = text;
node.isComment = true;
return node
};
只有isComment和text屬性有效,其余的默認(rèn)為false或者null
文本節(jié)點(diǎn)
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val))
}
只有一個(gè)text屬性
克隆節(jié)點(diǎn)
function cloneVNode (vnode) {
var cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// a child.
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
);
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
cloned.isComment = vnode.isComment;
cloned.fnContext = vnode.fnContext;
cloned.fnOptions = vnode.fnOptions;
cloned.fnScopeId = vnode.fnScopeId;
cloned.asyncMeta = vnode.asyncMeta;
cloned.isCloned = true;
return cloned
}
克隆節(jié)點(diǎn)將vnode的所有屬性賦值到clone節(jié)點(diǎn),并且設(shè)置isCloned = true,它的作用是優(yōu)化靜態(tài)節(jié)點(diǎn)和插槽節(jié)點(diǎn)。以靜態(tài)節(jié)點(diǎn)為例,因?yàn)殪o態(tài)節(jié)點(diǎn)的內(nèi)容是不會(huì)改變的,當(dāng)它首次生成虛擬DOM節(jié)點(diǎn)后,再次更新時(shí)是不需要再次生成vnode,而是將原vnode克隆一份進(jìn)行渲染,這樣在一定程度上提升了性能。
元素節(jié)點(diǎn) 元素節(jié)點(diǎn)一般會(huì)存在tag、data、children、context四種有效屬性,形如:
{
children: [VNode, VNode],
context: {...},
tag: 'div',
data: {attr: {id: app}}
}
組件節(jié)點(diǎn) 組件節(jié)點(diǎn)有兩個(gè)特有屬性 (1) componentOptions,組件節(jié)點(diǎn)的選項(xiàng)參數(shù),包含如下內(nèi)容:
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }
(2) componentInstance: 組件的實(shí)例,也是Vue的實(shí)例 對(duì)應(yīng)的vnode
new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
)
即
{
componentOptions: {},
componentInstance: {},
tag: 'vue-component-1-child',
data: {...},
...
}
函數(shù)式組件 函數(shù)組件通過(guò)createFunctionalComponent函數(shù)創(chuàng)建, 跟組件節(jié)點(diǎn)類(lèi)似,暫時(shí)沒(méi)看到特殊屬性,有的話后續(xù)再補(bǔ)上。
patch
虛擬DOM最重要的功能是patch,將VNode渲染為真實(shí)的DOM。
patch簡(jiǎn)介
patch中文意思是打補(bǔ)丁,也就是在原有的基礎(chǔ)上修改DOM節(jié)點(diǎn),也可以說(shuō)是渲染視圖。DOM節(jié)點(diǎn)的修改有三種:
- 創(chuàng)建新增節(jié)點(diǎn)
- 刪除廢棄的節(jié)點(diǎn)
- 修改需要更新的節(jié)點(diǎn)。
當(dāng)緩存上一次的oldvnode與最新的vnode不一致的時(shí)候,渲染視圖以vnode為準(zhǔn)。
初次渲染過(guò)程
當(dāng)oldvnode中不存在,而vnode中存在時(shí),就需要使用vnode新生成真實(shí)的DOM節(jié)點(diǎn)并插入到視圖中。首先如果vnode具有tag屬性,則認(rèn)為它是元素屬性,再根據(jù)當(dāng)前環(huán)境創(chuàng)建真實(shí)的元素節(jié)點(diǎn),元素創(chuàng)建后將它插入到指定的父節(jié)點(diǎn)。以上節(jié)生成的VNode為例,首次執(zhí)行
vm._update(vm._render(), hydrating);
vm._render()為上篇生成的VNode,_update函數(shù)具體為
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
// 緩存vnode
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// 第一次渲染,preVnode是不存在的
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};
因第一次渲染,執(zhí)行 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); ,注意第一個(gè)參數(shù)是oldVnode為 vm.$el 為元素節(jié)點(diǎn),__patch__函數(shù)具體過(guò)程為:
(1) 先判斷oldVnode是否存在,不存在就創(chuàng)建vnode
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
}
(2) 存在進(jìn)入else,判斷oldVnode是否是元素節(jié)點(diǎn),如果oldVnode是元素節(jié)點(diǎn),則
if (isRealElement) {
...
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode);
}
創(chuàng)建一個(gè)oldVnode節(jié)點(diǎn),其形式為
{
asyncFactory: undefined,
asyncMeta: undefined,
children: [],
componentInstance: undefined,
componentOptions: undefined,
context: undefined,
data: {},
elm: div#app,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "div",
text: undefined,
child: undefined
}
然后獲取oldVnode的元素節(jié)點(diǎn)以及其父節(jié)點(diǎn),并創(chuàng)建新的節(jié)點(diǎn)
// replacing existing element var oldElm = oldVnode.elm; var parentElm = nodeOps.parentNode(oldElm); // create new node createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) );
創(chuàng)建新節(jié)點(diǎn)的過(guò)程
// 標(biāo)記是否是根節(jié)點(diǎn)
vnode.isRootInsert = !nested; // for transition enter check
// 這個(gè)函數(shù)如果vnode有componentInstance屬性,會(huì)創(chuàng)建子組件,后續(xù)具體介紹,否則不做處理
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
接著在對(duì)子節(jié)點(diǎn)處理
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
...
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
if (data && data.pre) {
creatingElmInVPre--;
}
}
}
將vnode的屬性設(shè)置為創(chuàng)建元素節(jié)點(diǎn)elem,創(chuàng)建子節(jié)點(diǎn) createChildren(vnode, children, insertedVnodeQueue); 該函數(shù)遍歷子節(jié)點(diǎn)children數(shù)組
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
} else if (isPrimitive(vnode.text)) {
// 如果vnode是文本直接掛載
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
}
}
遍歷children,遞歸createElm方法創(chuàng)建子元素節(jié)點(diǎn)
else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
如果是評(píng)論節(jié)點(diǎn),直接創(chuàng)建評(píng)論節(jié)點(diǎn),并將其插入到父節(jié)點(diǎn)上,其他的創(chuàng)建文本節(jié)點(diǎn),并將其插入到父節(jié)點(diǎn)parentElm(剛創(chuàng)建的div)上去。 觸發(fā)鉤子,更新節(jié)點(diǎn)屬性,將其插入到parentElm('#app'元素節(jié)點(diǎn))上
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
最后將老的節(jié)點(diǎn)刪掉
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
function removeAndInvokeRemoveHook (vnode, rm) {
if (isDef(rm) || isDef(vnode.data)) {
var i;
var listeners = cbs.remove.length + 1;
...
// recursively invoke hooks on child component root node
if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
removeAndInvokeRemoveHook(i, rm);
}
for (i = 0; i < cbs.remove.length; ++i) {
cbs.remove[i](vnode, rm);
}
if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
i(vnode, rm);
} else {
// 刪除id為app的老節(jié)點(diǎn)
rm();
}
} else {
removeNode(vnode.elm);
}
}
初次渲染結(jié)束。
更新節(jié)點(diǎn)過(guò)程
為了更好地測(cè)試,模板選用
<div id="app">{{ message }}<button @click="update">更新</button></div>
點(diǎn)擊按鈕,會(huì)更新message,重新渲染視圖,生成的VNode為
{
asyncFactory: undefined,
asyncMeta: undefined,
children: [VNode, VNode],
componentInstance: undefined,
componentOptions: undefined,
context: Vue實(shí)例,
data: {attrs: {id: "app"}},
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "div",
text: undefined,
child: undefined
}
在組件更新的時(shí)候,preVnode和vnode都是存在的,執(zhí)行
vm.$el = vm.__patch__(prevVnode, vnode);
實(shí)際上是運(yùn)行以下函數(shù)
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
該函數(shù)首先判斷oldVnode和vnode是否相等,相等則立即返回
if (oldVnode === vnode) {
return
}
如果兩者均為靜態(tài)節(jié)點(diǎn)且key值相等,且vnode是被克隆或者具有isOnce屬性時(shí),vnode的組件實(shí)例componentInstance直接賦值
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return
}
接著對(duì)兩者的屬性值作對(duì)比,并更新
var oldCh = oldVnode.children;
var ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) { // 以vnode為準(zhǔn)更新oldVnode的不同屬性
cbs.update[i](oldVnode, vnode);
}
if (isDef(i = data.hook) && isDef(i = i.update)) {
i(oldVnode, vnode);
}
}
vnode和oldVnode的對(duì)比以及相應(yīng)的DOM操作具體如下:
// vnode不存在text屬性的情況
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 子節(jié)點(diǎn)不相等時(shí),更新
if (oldCh !== ch) {
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
} else if (isDef(ch)) {
{
checkDuplicateKeys(ch);
}
// 只存在vnode的子節(jié)點(diǎn),如果oldVnode存在text屬性,則將元素的文本內(nèi)容清空,并新增elm節(jié)點(diǎn)
if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '');
}
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 如果只存在oldVnode的子節(jié)點(diǎn),則刪除DOM的子節(jié)點(diǎn)
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 只存在oldVnode有text屬性,將元素的文本清空
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// node和oldVnode的text屬性都存在且不一致時(shí),元素節(jié)點(diǎn)內(nèi)容設(shè)置為vnode.text
nodeOps.setTextContent(elm, vnode.text);
}
對(duì)于子節(jié)點(diǎn)的對(duì)比,先分別定義oldVnode和vnode兩數(shù)組的前后兩個(gè)指針?biāo)饕?/p>
var oldStartIdx = 0; var newStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
如下圖:

接下來(lái)是一個(gè)while循環(huán),在這過(guò)程中,oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 會(huì)逐漸向中間靠攏
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
當(dāng)oldStartVnode或者oldEndVnode為空時(shí),兩中間移動(dòng)
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
}
接下來(lái)這一塊,是將 oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 兩兩比對(duì)的過(guò)程,共四種:
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
第一種: 前前相等比較

如果相等,則oldStartVnode.elm和newStartVnode.elm均向后移一位,繼續(xù)比較。 第二種: 后后相等比較

如果相等,則oldEndVnode.elm和newEndVnode.elm均向前移一位,繼續(xù)比較。 第三種: 前后相等比較

將oldStartVnode.elm節(jié)點(diǎn)直接移動(dòng)到oldEndVnode.elm節(jié)點(diǎn)后面,然后將oldStartIdx向后移一位,newEndIdx向前移動(dòng)一位。 第四種: 后前相等比較

將oldEndVnode.elm節(jié)點(diǎn)直接移動(dòng)到oldStartVnode.elm節(jié)點(diǎn)后面,然后將oldEndIdx向前移一位,newStartIdx向后移動(dòng)一位。 如果以上均不滿足,則
else {
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
createkeyToOldIdx函數(shù)的作用是建立key和index索引對(duì)應(yīng)的map表,如果還是沒(méi)有找到節(jié)點(diǎn),則新創(chuàng)建節(jié)點(diǎn)
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
插入到oldStartVnode.elm節(jié)點(diǎn)前面,否則,如果找到了節(jié)點(diǎn),并符合sameVnode,將兩個(gè)節(jié)點(diǎn)patchVnode,并將該位置的老節(jié)點(diǎn)置為undefined,同時(shí)將vnodeToMove.elm移到oldStartVnode.elm的前面,以及newStartIdx往后移一位,示意圖如下:

如果不符合sameVnode,只能創(chuàng)建一個(gè)新節(jié)點(diǎn)插入到 parentElm 的子節(jié)點(diǎn)中,newStartIdx 往后移動(dòng)一位。 最后如果,oldStartIdx > oldEndIdx,說(shuō)明老節(jié)點(diǎn)比對(duì)完了,但是新節(jié)點(diǎn)還有多的,需要將新節(jié)點(diǎn)插入到真實(shí) DOM 中去,調(diào)用 addVnodes 將這些節(jié)點(diǎn)插入即可;如果滿足 newStartIdx > newEndIdx 條件,說(shuō)明新節(jié)點(diǎn)比對(duì)完了,老節(jié)點(diǎn)還有多,將這些無(wú)用的老節(jié)點(diǎn)通過(guò) removeVnodes 批量刪除即可。到這里這個(gè)過(guò)程基本結(jié)束。
總結(jié)
以上所述是小編給大家介紹的Vue內(nèi)部渲染視圖的方法,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺(jué)得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
相關(guān)文章
Vue?transition組件簡(jiǎn)單實(shí)現(xiàn)數(shù)字滾動(dòng)
這篇文章主要為大家介紹了Vue?transition組件簡(jiǎn)單實(shí)現(xiàn)數(shù)字滾動(dòng)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
詳解Vue項(xiàng)目引入CreateJS的方法(親測(cè)可用)
CreateJS是基于HTML5開(kāi)發(fā)的一套模塊化的庫(kù)和工具。這篇文章主要介紹了Vue項(xiàng)目引入CreateJS的方法(親測(cè)),需要的朋友可以參考下2019-05-05
使用yarn?build?打包vue項(xiàng)目時(shí)靜態(tài)文件或圖片未打包成功的問(wèn)題及解決方法
這篇文章主要介紹了使用yarn?build?打包vue項(xiàng)目時(shí)靜態(tài)文件或圖片未打包成功的問(wèn)題及解決方法,解決方法不復(fù)雜通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08

