寫在開頭
學習完了ES 6基礎,推薦閱讀:ECMAScript 6 全套學習目錄 整理 完結
現在開始逐步深入Vue 技術棧,想了想,技術棧專欄的主要內容包括:
1、Vue源碼分析
2、手把手教 保姆級 擼代碼
3、無懼面試,學以致用,繼承創新
4、談談前端發展與學習心得
5、手寫源碼技術棧,附上詳細註釋
6、從源碼中學習設計模式,一舉兩得
7、編程思想的提升及代碼質量的提高
8、通過分析源碼學習架構,看看優秀的框架
9、項目實戰開發
10、面試準備,完善個人簡歷
暫時想到的就這麼多,把這列舉的10點做好了,我覺得也OK了,歡迎一起學習,覺得不錯的話,可以關注博主,專欄會不斷更新,可以關注一下,傳送門~
學習目錄
爲了方便自己查閱與最後整合,還是打算整個目錄,關於Vue技術棧優秀的文章:
Vue 技術棧 教你玩"壞" v8引擎 吃透 js 內存回收機制
文章目錄
正文
Vue路由的工作流程
前端路由和後端路由的區別
自從前後端分離後,說路由不再僅是說後端路由了,我們前端也有了路由。路由簡單來說,就是分發請求,將對應的請求分發到應該到的位置。
後端路由-mvc的時代:
- 輸入url -》 請求發送到服務器 -》 服務器請求解析的路徑 -》 拿取對應頁面 -》 返回出去
前端路由-spa應用:
- 輸入url -》js解析地址 -》 找到對應地址的頁面 -》 執行頁面生成的js -》 生成頁面
前端路由無需發送服務器,通過js進行解析,在瀏覽器上進行導向
vue-router 工作流程
vue插件
請讀者閱讀以下代碼,這就是vue-router的默認配置,最終返回給vue的是一個new VueRouter,也就是說是一個對象,而這個對象裏面就有我們之前圖示流程的current
變量。
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = new VueRouter({
routes
})
export default router
vue 與 vue-router工作過程
再次回到我們的vue路由的工作流程,最下面三個部分是由vue-router
來實現的,而上面是vue
來工作的,vue一直監視着current
變量,而vue-router能改變current
,一旦改變,就會觸發監聽事件,根據current來獲取新的組件,然後vue去渲染
新的組件,客戶就能看到新的界面
了。總的來說就是:(兩個監聽,一個渲染)
上述文字類的表述或許不能讓你恍然大悟,接下來我們就化繁爲簡,將整個路由過程進行實現:
PS:但是在研究深入一點的知識前,爲了照顧小白,還是從基礎開始講起,已經熟悉的讀者可以選擇性閱讀。
hash 與 history
vue-router是怎麼觸發監聽事件的呢?
其實就是用到了hash,這裏對於前端來說就着重介紹hash了,history記得會有一定兼容性問題。
hash
1、#號後的就是hash的內容
2、可以通過location.hash拿到
3、可以通過onhashchange監聽hash的改變
history
1、history即正常的路徑
2、可以通過location.pathname拿到
3、可以通過onpopstate監聽history的改變
對於hash,我們可以在控制檯通過location.hash
獲取值(如果沒有#就獲得空字符串),如下所示:
監聽hash的改變
window.onhashchange=function(){
console.log('hash值已改變!')
}
history模式與上述方法類似
vue插件基礎知識
vue-router、vuex等其實都是屬於vue的插件,這些插件都是我們平常很多次使用的,下文將會循序漸進教你vue插件是如何開發的,我們怎樣開發一個vue插件。
我們不管是使用vue-router還是vuex都會調用Vue.use()這個方法,如下圖所示,但是你有思考過Vue.use()到底是幹什麼用的呢?有什麼作用呢?
進行實踐,在main.js
中我們進行如下操作,定義一個方法a,然後調用Vue.use()
方法
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
function a(){
console.log(6);
}
Vue.use(a);
new Vue({
router,
render: h => h(App)
}).$mount('#app')
執行結果:
從上述結果來看的話,我們給Vue一個方法,它就會執行一遍
那麼,我們給a一個install
屬性,看看會打印什麼:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
function a(){
console.log(6);
}
a.install=function(){
console.log('install!');
}
Vue.use(a);
new Vue({
router,
render: h => h(App)
}).$mount('#app')
執行結果:
Vue.use( ) 作用
從上述兩個例子來看,Vue.use()作用就是把你給的方法執行一遍,但如果有install屬性的話,會執行install屬性。
疑惑:如果只是爲了執行這個方法或者擁有install屬性的某個方法,那乾脆自己調用一下好了,爲啥還要用Vue.use()
執行呢?
解決:其實,在install屬性的可以有一個參數傳進來,我們將上述代碼進行更改:
a.install=function(vue){
console.log(vue);
}
打印結果:
ƒ Vue (options) {
if ( true &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
從打印結果來看的話,其實就是一個Vue的類,與下述代碼類似的一個類:
import Vue from 'vue'
Vue.mixin( )初識
對於Vue.use( )確實只是執行了一遍給的方法,但完成功能方面、起核心作用的還是vue.mixin()
方法,
請看如下代碼,在main.js
文件內,我們在vue.mixin()
中混入data
,裏面寫一個c
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
function a(){
console.log(6);
}
a.install=function(vue){
//console.log(vue);
//全局混入vue實例
vue.mixin({
data(){
return {
c:'歡迎訪問超逸の博客'
}
}
});
}
Vue.use(a);
new Vue({
router,
render: h => h(App)
}).$mount('#app')
然後在HelloWorld.vue
組件內,顯示上文的c
打開界面,查看如下:
由上文可知,在HelloWorld組件裏是沒有c這個變量的,但是可以進行渲染顯示到我們的頁面,那麼mixin是可以混入全局變量,任何組件可以拿到mixin混入的實例
除了混入data外,我們還可以混入方法,舉個栗子:
a.install=function(vue){
//console.log(vue);
//全局混入vue實例
vue.mixin({
data(){
return {
c:'歡迎訪問超逸の博客'
}
},
methods::{
globalMethods:function(){
}
}
});
}
那麼,其它組件都可以調用上述的方法,那麼這樣做有什麼好處呢?
一提及到全局可以使用,應該可以想到可複用性
這個特點,比如我們開發常見的有些組件需要消息彈窗,可能大部分人會在每個組件進行import註冊等等,但是有了mixin()後,我們可以定義一個全局的方法,首先在App.vue寫好我們的消息彈窗的方法,用全局的方法去操作App.vue寫好的方法,那麼就有很高的複用性。
但是data
和methods
並不是我們mixin方法的關鍵,最牛的還是可以進行全局生命週期注入 比如created
、beforecreated
、mounted
等等
Vue插件開發一系列api(開始探索源碼)
console.log(Vue.util);
執行結果:
Vue.util.defineReactive
很重要的一個就是:Vue.util.defineReactive
,它就是Vue監聽current
變量重要執行者
不妨從源碼來學習:
/**
* Define a reactive property on an Object.
*/
//Vue的data監聽,也是通過這個方法
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
//依賴收集者
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
//雙向綁定
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
//進行依賴收集
dep.depend();
/*採用依賴收集的原因:*/
//1.data裏面的數據並不是所有地方都要用到
//2.如果我們直接更新整個視圖,會造成資源浪費
//3.將依賴於某個變量的組件收集起來
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
//觸發依賴的組件產生更新
dep.notify();
}
});
}
雙向綁定
上述關於響應式 雙向綁定,強烈推薦之前寫過的一篇文章:
手寫實現defineReactive
我們可以通過defineReactive
來實現Vue監聽current的監視者,監聽某個第三方的變量
手寫:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
//Vue插件開發一系列api
//console.log(Vue.util.defineReactive);
//test是屬於window的對象
var test={
testa:'計時開始'
}
//設置定時器
setTimeout(function(){
test.testa='計時結束'
},2000)
function a(){
console.log(6);
}
a.install=function(vue){
//console.log(vue);
//監聽testa
Vue.util.defineReactive(test,'testa');
//全局混入vue實例
vue.mixin({
data(){
return {
c:'歡迎訪問超逸の博客'
}
},
methods:{
},
beforeCreate:function(){
this.test=test;
},
//全局生命週期注入
created:function(){
//console.log(this)
}
});
}
Vue.use(a);
new Vue({
router,
render: h => h(App)
}).$mount('#app')
然後我們在HelloWorld組件進行渲染,查看頁面
執行結果:
疑問:爲什麼要寫在beforeCreate
裏面?
解決:因爲create
階段組件已經生成了,this實例已經創建了,而beforeCreate
纔剛開始。這樣HelloWorld組件可以this調用來獲取testa的值
Vue.util.extend 與 Vue.extend 的區別
關於這個問題,我百度了一下,貌似很少有人去探究這個問題,既然查不到,那麼我們就從源碼來學習,這就是一個比較好的方法。源碼能夠給你答案
console.log(Vue.util.extend);
console.log(Vue.extend);
/**
* Mix properties into target object.
*/
//Vue.util.extend
//其實就是拷貝一份,以後可以直接調用即可
function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}
/**
* Class inheritance
*/
//Vue.extend
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
var name = extendOptions.name || Super.options.name;
if (name) {
validateComponentName(name);
}
var Sub = function VueComponent (options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
Sub.options = mergeOptions(
Super.options,
extendOptions
);
Sub['super'] = Super;
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps$1(Sub);
}
if (Sub.options.computed) {
initComputed$1(Sub);
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.use = Super.use;
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type];
});
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub;
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);
// cache constructor
cachedCtors[SuperId] = Sub;
return Sub
};
}
單元測試
關於Vue.extend我們以下面這個單元測試
例子來講解:
由下圖可知,我們獲取到了HelloWorld的構造函數,然後再拿到組件。簡單來說,你可以在任何地方,拿到任何組件,這對於單元測試方面是比較方便的,你可以拿到任何組件裏的方法進行測試。
手寫Vue-router(核心)
開始前準備
-
在src下創建一個新的文件夾
myrouter
,新建一個index.js
的文件
-
將之前寫過的代碼都註釋掉,返回最初的模樣
-
將VueRouter引用改爲我們自己所寫的
myrouter
根據上文的流程圖,手寫vue-router
//記錄路由
class historyRouter{
constructor() {
this.current=null;
}
}
class vueRouter{
constructor(options) {
this.mode=options.mode||'hash';
this.routes=options.routes||[];
this.history=new historyRouter;
//創建routesMap 將數組形式的轉換成key-value形式的路由
this.routesMap=this.createMap(this.routes);
//事件監聽
this.init();
}
init(){
if(this.mode=='hash'){
location.hash? '':location.hash='/';
window.addEventListener('load',()=>{
this.history.current=location.hash.slice(1);
});
window.addEventListener('hashchange',()=>{
this.history.current=location.hash.slice(1);
})
}else{
location.pathname? '':location.pathname='/';
window.addEventListener('load',()=>{
this.history.current=location.pathname;
});
window.addEventListener('popstate',()=>{
this.history.current=location.pathname;
})
}
}
createMap(routes){
return routes.reduce((memo,current)=>{
memo[current.path]=current.component;
return memo;
},{})
}
}
//Vue監視current變量
vueRouter.install=function(Vue){
Vue.mixin({
beforeCreate(){
if(this.$options&&this.$options.router){
this._root=this;
this._router=this.$options.router;
Vue.util.defineReactive(this,'current',this._router.history);
}else{
//嵌套路由,如果沒有路由,去找父組件
this._root=this.$parent._root;
}
}
})
//獲取新組件以及render
Vue.component('router-view',{
//渲染新組件
render(h){
let current=this._self._root._router.history.current;
//console.log(current);
let routesMap=this._self._root._router.routesMap;.
//console.log(routesMap);
return h(routesMap[current]);
}
})
}
//將類暴露出去
export default vueRouter;
總結
對於最後手寫的vue-router讀者只要弄懂它的思想即可,作爲前端開發,我們不能只侷限於寫業務代碼,造輪子等,我們要提高我們的編程思維,弄懂其中的思想與原理,瞭解底層才能不是一個簡單的搬磚工!
附本篇學習源碼
鏈接:https://pan.baidu.com/s/11xAkcdSyMxGTCPyQJafPGg
提取碼:0z9j
(鏈接失效請評論區留言)
結尾
本篇文章是自學而寫,當然還會有很多不足的地方,希望您來指正,感激不盡!
學如逆水行舟,不進則退