vue created 调用方法_深入解析 Vue 的热更新原理,偷学尤大的秘籍?
大家都用過 Vue-CLI 創建 vue 應用,在開發的時候我們修改了 vue 文件,保存了文件,瀏覽器上就自動更新出我們寫的組件內容,非常的順滑流暢,大大提高了開發效率。想知道這背后是怎么實現的嗎,其實代碼并不復雜。
這個功能的實現底層用了vue-hot-load-api[1]這個庫,得益于 vue 的良好設計,熱更新的實現總共就一個 js 文件,200 行代碼,綽綽有余。
而在這個庫里涉及到的技巧又非常適合我們去深入了解 vue 內部的一些機制,所以趕快來和我一起學習吧。
提要
本文單純的從vue-hot-load-api這個庫出發,在瀏覽器的環境運行 Vue 的熱更新示例,主要測試的組件是普通的 vue 組件而不是 functional 等特殊組件,以最簡單的流程搞懂熱更新的原理。
在源碼解析中貼出的代碼會省略掉一些不太相關的流程,更便于理解。
解析
從 github 倉庫示例入手
進入了這個 github 倉庫以后,最先開始看的肯定是 Readme 的里的示例,在看示例的時候作者給出的注釋就非常重要了,他會標注出每一個重要的環節。并且我們要結合自己的一些經驗排除掉和這個庫無關的代碼。(在這個示例中,webpack 的相關代碼就可以先不去過多關注)
第一步需要調用install方法,傳入 Vue 構造函數,根據注釋來看,這一步是要知道這個庫與 Vue 版本之間是否兼容。
//?make?the?API?aware?of?the?Vue?that?you?are?using.//?also?checks?compatibility.
api.install(Vue);
接下來的這段注釋告訴我們,每個需要熱更新的組件選項對象,我們都需要為它建立一個獨一無二的 id,并且這段代碼需要在初始化的時候完成。
if?(初始化)?{??//?for?each?component?option?object?to?be?hot-reloaded,
??//?you?need?to?create?a?record?for?it?with?a?unique?id.
??//?do?this?once?on?startup.
??api.createRecord('very-unique-id',?myComponentOptions);
}
最后就是激動人心的熱更新時間了,
根據注釋來看,這個庫的使用分為兩種情況。
- rerender 只有 template 或者 render 函改變的情況下使用。
- reload 如果 template 或者 render 未改變,則這個函數需要調用 reload 方法先銷毀然后重新創建(包括它的子組件)。
//?you?can?force?a?re-render?for?all?its?active?instances?without
//?destroying/re-creating?them.?This?keeps?all?current?app?state?intact.
api.rerender('very-unique-id',?myComponentOptions);
//?---?OR?---
//?if?a?component?has?non-template/render?options?changed,
//?it?needs?to?be?fully?reloaded.?This?will?destroy?and?re-create?all?its
//?active?instances?(and?their?children).
api.reload('very-unique-id',?myComponentOptions);
從這個簡單的示例里面可以看出,這個庫的核心流程就是:
什么,Readme 的示例到此就結束了?這個 very-unique-id 到底是個什么東西,myComponentOptions 又是什么樣的。
因為這個倉庫可能并不是面向廣大開發者的,所以它的文檔寫的非常的簡略。其實看完了這個簡短的示例,大家肯定還是一臉懵逼的。
在看一個你沒有熟練使用的庫的源碼的時候,其實還有一個很關鍵的步驟,那就是看測試用例。
探索測試用例
測試用例[2]
上面我們總結出兩個關鍵 api rerender 和 reload 之后,就帶著目的性的去看測試用例。
const?Vue?=?require('vue');const?api?=?require('../src');
//?初始化
api.install(Vue);
//?這個方法接受id和組件選項對象,
//?通過createRecord去記錄組件
//?然后返回一個vue組件實例。
function?prepare(id,?Comp)?{
??api.createRecord(id,?Comp);
??return?new?Vue({
????render:?h?=>?h(Comp),
??});
}
rerender 用例
const?id0?=?'rerender:?mounted';test(id0,?done?=>?{
??//?用'rerender:?mounted'作為這個組件對象的id,
??//?這個組件的內容應該是?foo
??//?調用?$mount生成dom節點
??const?app?=?prepare(id0,?{
????render:?h?=>?h('div',?'foo'),
??}).$mount();
??//?$el就是組件生成的dom元素,期望textContent文字內容為foo
??expect(app.$el.textContent).toBe('foo');
??//?rerender?后dom節點變成?bar
??api.rerender(id0,?{
????render:?h?=>?h('div',?'bar'),
??});
??//?通過nextTick保證dom節點已經更新
??//?期望textContent文字內容為bar
??Vue.nextTick(()?=>?{
????expect(app.$el.textContent).toBe('bar');
????done();
??});
});
reload 用例
const?id1?=?'reload:?mounted';test(id1,?done?=>?{
??//?通過一個count來計數
??let?count?=?0;
??//?app組件會在created的時候讓count?+?1
??//?destroyed的時候讓count?-?1
??const?app?=?prepare(id1,?{
????created()?{
??????count++;
????},
????destroyed()?{
??????count--;
????},
????data:?()?=>?({?msg:?'foo'?}),
????render(h)?{
??????return?h('div',?this.msg);
????},
??}).$mount();
??//?確保內容正確
??expect(app.$el.textContent).toBe('foo');
??//?確保created周期執行?此時的count是1
??expect(count).toBe(1);
??//?調用created?傳入新組件的created時?count會-1
??api.reload(id1,?{
????created()?{
??????count--;
????},
????data:?()?=>?({?msg:?'bar'?}),
????render(h)?{
??????return?h('div',?this.msg);
????},
??});
??Vue.nextTick(()?=>?{
????//?確保內容正確
????expect(app.$el.textContent).toBe('bar');
????//?在reload之前?count是1
????//?調用reload之后?會先調用前一個組件的destory生命周期?此時count是0
????//?接下來調用新組建的created生命周期?此時count是-1
????expect(count).toBe(-1);
????done();
??});
});
具體流程已經在注釋里分析了,果然和示例代碼的注釋里寫的一樣,而且現在我們也更清楚這個 api 到底該怎么用了。
總結一個最簡單的可用 demo
import?api?from?'vue-hot-reload-api';import?Vue?from?'vue';
//?初始化
api.install(Vue,?true);
const?appOptions?=?{
??render:?h?=>?h('div',?'foo'),
};
api.createRecord('my-app',?appOptions);
new?Vue(appOptions).$mount('#app');
setTimeout(()?=>?{
??api.rerender('my-app',?{
????render:?h?=>?h('div',?'bar'),
??});
},?2000);
這個 demo(源碼[3])是直接在瀏覽器可用的,效果如下:
源碼分析
源碼地址[4]
全局變量
進入 js 文件的入口,首先定義了一些變量
//?Vue構造函數let?Vue;?//?late?bind
//?Vue版本
let?version;
//?createRecord方法保存id?->?組件映射關系的對象
const?map?=?Object.create(null);
if?(typeof?window?!==?'undefined')?{
??//?將map對象存儲在window上
??window.__VUE_HOT_MAP__?=?map;
}
//?是否已經安裝過
let?installed?=?false;
//?這個變量暫時沒用
let?isBrowserify?=?false;
//?初始化生命周期的名字?默認是Vue的beforeCreate生命周期
let?initHookName?=?'beforeCreate';
其實看到 window 對象的出現,我們就已經可以確定這個 api 可以在瀏覽器端調用。
install
exports.install?=?function(vue,?browserify)?{??//?如果安裝過了就不再重復安裝
??if?(installed)?{
????return;
??}
??installed?=?true;
??//?兼容es?modules模塊
??Vue?=?vue.__esModule???vue.default?:?vue;
??//?把vue的版本如2.6.3分隔成[2,?6,?3]?這樣的數組
??version?=?Vue.version.split('.').map(Number);
??isBrowserify?=?browserify;
??//?compat?with?//?兼容2.0.0-alpha.7以下版本if?(Vue.config._lifecycleHooks.indexOf('init')?>?-1)?{
????initHookName?=?'init';
??}//?只有Vue在2.0以上的版本才支持這個庫。
??exports.compatible?=?version[0]?>=?2;if?(!exports.compatible)?{console.warn('[HMR]?You?are?using?a?version?of?vue-hot-reload-api?that?is?'?+'only?compatible?with?Vue.js?core?^2.0.0.'
????);return;
??}
};
可以看出 install 方法很簡單,就是幫你看一下 Vue 的版本是否在 2.0 以上,確認一下兼容性,關于初始化生命周期,在這篇文章里我們就不考慮 2.0.0-alpha.7 以下版本,可以認為這個庫的初始化工作就是在 beforeCreate 這個生命周期進行。
createRecord
/**?*?Create?a?record?for?a?hot?module,?which?keeps?track?of?its?constructor
?*?and?instances
?*
?*?@param?{String}?id
?*?@param?{Object}?options
?*/
exports.createRecord?=?function(id,?options)?{
??//?如果已經存儲過了就return
??if?(map[id])?{
????return;
??}
??//?關鍵流程?下一步解析
??makeOptionsHot(id,?options);
??//?將記錄存儲在map中
??//?instances變量應該不難猜出是vue的實例對象。
??map[id]?=?{
????options:?options,
????instances:?[],
??};
};
這一步在把 id 和對應的 options 對象存進 map 后,就沒做啥了,關鍵步驟肯定在于makeOptionsHot這個方法。
/**?*?Make?a?Component?options?object?hot.
?*?讓一個組件對象變得性感...哦不,是支持熱更新。
?*
?*?@param?{String}?id
?*?@param?{Object}?options
?*/
function?makeOptionsHot(id,?options)?{
??//?options?就是我們傳入的組件對象
??//?initHookName?就是'beforeCreate'
??injectHook(options,?initHookName,?function()?{
????//?這個函數會在beforeCreate聲明周期執行
????const?record?=?map[id];
????if?(!record.Ctor)?{
??????//?此時this已經是vue的實例對象了
??????//?把組件實例的構造函數賦值給record的Ctor屬性。
??????record.Ctor?=?this.constructor;
????}
????//?在instances里存儲這個實例。
????record.instances.push(this);
??});
??//?在組件銷毀的時候把上面存儲的instance刪除掉。
??injectHook(options,?'beforeDestroy',?function()?{
????const?instances?=?map[id].instances;
????instances.splice(instances.indexOf(this),?1);
??});
}
//?往生命周期里注入某個方法
function?injectHook(options,?name,?hook)?{
??const?existing?=?options[name];
??options[name]?=?existing
??????Array.isArray(existing)
????????existing.concat(hook)
??????:?[existing,?hook]
????:?[hook];
}
看完了這幾個函數以后,我們對 createRecord 應該有個清晰的認識了。
比如上面我們的例子中這段代碼
??render:?h?=>?h('div',?'foo'),
};
api.createRecord('my-app',?appOptions);
{
????my-app:?{
????????options:?appOptions,
????????instances:?[],
????????Ctor:?null
????}
}
接下來進入關鍵的 rerender 函數。
rerender
exports.rerender?=?(id,?options)?=>?{??const?record?=?map[id];
??if?(!options)?{
????//?如果沒傳第二個參數?就把所有實例調用?$forceUpdate
????record.instances.slice().forEach(instance?=>?{
??????instance.$forceUpdate();
????});
????return;
??}
??record.instances.slice().forEach(instance?=>?{
????//?將實例上的?$options上的render直接替換為新傳入的render函數
????instance.$options.render?=?options.render;
????//?執行?$forceUpdate更新視圖
????instance.$forceUpdate();
??});
};
其實這個原函數很長,但是簡化以后核心的更改視圖的方法就是這些,平常我們在寫 vue 單文件組件的時候都會像下面這樣寫:
{{?msg?}}這樣的.vue 文件,會被 vue-loader 編譯成單個的組件選項對象,template 中的部分會被編譯成 render 函數掛到組件上,最終生成的對象是類似于:
export?default?{??data()?{
????return?{
??????msg:?'Hello?World',
????};
??},
??render(h)?{
????return?h('span',?this.msg);
??},
};
而在運行時,組件實例(也就是生命周期或者 methods 中訪問的 this 對象)會通過option.render 去實現的。我們可以去 vue 的源碼里驗證一下我們的猜想。
_render而在options.render 給替換成新的 render 方法了,這個時候再調用$forceUpdate,不就渲染新傳入的 render 了嗎?這個運行時的偷天換日我不得不佩服~
reload
reload 的講解我們基于這樣一個示例:
一開始會顯示 foo 的文本,一秒以后會顯示成 bar。
??api.createRecord(id,?Comp);
??return?new?Vue({
????render:?h?=>?h(Comp),
??});
}
const?id1?=?'reload:?mounted';
const?app?=?prepare(id1,?{
??data:?()?=>?({?msg:?'foo'?}),
??render(h)?{
????return?h('div',?this.msg);
??},
}).$mount('#app');
//?reload
setTimeout(()?=>?{
??api.reload(id1,?{
????data:?()?=>?({?msg:?'bar'?}),
????render(h)?{
??????return?h('div',?this.msg);
????},
??});
},?1000);
reload 的情況會更加復雜,涉及到很多 Vue 內部的運行原理,這里只能簡單的描述一下。
exports.reload?=?function(id,?options)?{??const?record?=?map[id];
??if?(options)?{
????//?reload的情況下?傳入的options會當做一個新的組件
????//?所以要用makeOptionsHot重新做一下記錄
????makeOptionsHot(id,?options);
????const?newCtor?=?record.Ctor.super.extend(options);
????newCtor.options._Ctor?=?record.options._Ctor;
????record.Ctor.options?=?newCtor.options;
????record.Ctor.cid?=?newCtor.cid;
????record.Ctor.prototype?=?newCtor.prototype;
??}
??record.instances.slice().forEach(function(instance)?{
????instance.$vnode.context.$forceUpdate();
??});
};
這段代碼關鍵的點開始于
const?newCtor?=?record.Ctor.super.extend(options);利用新傳入的配置生成了一個新的組件構造函數 然后對 record 上的 Ctor 進行了一系列的賦值
newCtor.options._Ctor?=?record.options._Ctor;record.Ctor.options?=?newCtor.options;
record.Ctor.cid?=?newCtor.cid;
record.Ctor.prototype?=?newCtor.prototype;
注意第一次調用 reload 時,這里的 record.Ctor 還是最初傳入的 Ctor,是由
const?app?=?prepare(id1,?{??data:?()?=>?({?msg:?'foo'?}),
??render(h)?{
????return?h('div',?this.msg);
??},
}).$mount('#app');
這個配置對象所生成的構造函數,但是構造函數的 options、cid 和 prototype 被替換成了由
api.reload(id1,?{??data:?()?=>?({?msg:?'bar'?}),
??render(h)?{
????return?h('div',?this.msg);
??},
});
這個配置對象所生成的構造函數上的 options、cid 和 prototype,此時的 cid 肯定是不同的。
也就是說,構造函數的 cid 變了!,這個點記住后面要考!
繼續看源碼
record.instances.slice().forEach(function(instance)?{??instance.$vnode.context.$forceUpdate();
});
此時的 instance 只有一個,就是在 reload 之前運行的那個 msg 為 foo 的實例,它的$vnode.context 是什么呢?
直接在放上控制臺打印出來的截圖,這個 context 是一個 vue 實例,注意這個 options 里的 render 函數,是不是非常眼熟,沒錯,這個 vue 實例其實就是我們的 prepare 函數中
??render:?h?=>?h(Comp),
});
返回的 vm 實例。
那么這個函數的 $forceUpdate必然會觸發 render: h => h(Comp) 這個函數,看到此時我們似乎還是沒看出來這些操作為什么會銷毀舊組件,創建新組件。那么此時只能探究一下這個h到底做了什么,這個h就是對應著 $createElement 方法。
$createElement方法$createElement 在創建 vnode 的時候,最底層會調用一個 createComponent 方法,
這個方法把 Comp 對象當做 Ctor,然后調用 Vue.extend 這個 api 創造出構造函數,
默認情況下第一次 h(Comp) 會生成類似于 vue-component-${cid}作為組件的 tag,
在本例中最開始渲染 msg 為 foo 的組件時,tag 為 vue-component-1,
并且會把這個構造函數緩存在_Ctor 這個變量上,這樣下次渲染再執行到 createComponent 的時候就不需要重新生成一次構造函數了,
Vue 在選擇更新策略時調用一個sameVnode方法
來決定是要進行打補丁,還是徹底銷毀重建,這個sameVnode如下:
function?sameVnode(a,?b)?{??return?(
????//?省略其他...
????a.tag?===?b.tag
??);
}
其中很關鍵的一個對比就是a.tag === b.tag
但是 reload 方法偷梁換柱把 Ctor 的 cid 換成了 2,
生成的 vnode 的 tag 是就 vue-component-2
后續再調用 context.$forceUpdate 的時候,會發現兩個組件的 tag 不一樣,所以就走了銷毀 -> 重新創建的流程。
總結
這個庫里面還是能看出很多尤大的編程風格,很適合進行學習,只是 reload 方法必須要深入了解 Vue 源碼才有可能搞懂生效的原理。
可以看出來 Vue 的很多第三方庫是利用 Vue 內部提供的一些機制,甚至是只有了解源碼細節才能想到的一些 hack 的方式去實現的,所以如果想更加深入的玩好 Vue,源碼是有必要去學習的,在學習 Vue 源碼的過程中會被尤大的代碼規范,還有一些精妙的設計所折服,肯定會有很大的收獲。
rerender這個方法相對來說還是比較好理解的,但是reload方法是怎么生效的就非常讓人難以理解了,我一步一步斷點調試了大概六七個小時,才漸漸得出結論,只能說好用的 api 后面潛藏著作者用心良苦的設計啊!
參考資料
[1]vue-hot-load-api: https://github.com/vuejs/vue-hot-reload-api
[2]測試用例: https://github.com/vuejs/vue-hot-reload-api/blob/master/test/test.js
[3]源碼: https://github.com/sl1673495/vue-hot-reload-demo
[4]源碼地址: https://github.com/vuejs/vue-hot-reload-api/blob/master/src/index.js
[5]Vue.js 源碼全方位深入解析 (含 Vue3.0 源碼分析): https://coding.imooc.com/class/228.html
相關推薦用Js實現人臉識別功能
JavaScript 啟動性能瓶頸分析與解決方案
從零看清Node源碼createServer和負載均衡整個過程
【項目實戰】sass使用進階篇(下)
【項目實戰】sass使用基礎篇(上)
最詳細的從零開始配置 TypeScript 項目的教程
5 款非常好用的開源 Docker 工具
WebSocket 全面知識補全
7個處理JavaScript值為undefined的技巧
immutablejs 是如何優化我們的代碼的?
Chrome 新功能嘗鮮!— CSS Overview
又一個布局利器, CSS 偽類 :placeholder-shown
封裝一個vue視頻播放器組件
對于組件的可重用性,大佬給出來6個建議
學習 TS 不要錯過的八個工具
Node 中的全鏈路式日志標記及處理
使用 Node 開發服務器項目時如何高效地打日志?
用TypeScript學設計模式(享元模式)
用TypeScript學設計模式(模板方法模式)
TypeScript 設計模式之適配器模式
用TypeScript學設計模式(觀察者模式)
用TypeScript學設計模式(單例模式)
點在看的人特別帥/美 《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的vue created 调用方法_深入解析 Vue 的热更新原理,偷学尤大的秘籍?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux网络报文接收发送浅析_Dock
- 下一篇: 想问问枣庄哪里可以买宝马车?