vue原理剖析
基礎結構
模闆 :界面展示代碼
邏輯業務功能:
美化布局:
生命周期

文法和概念
- 內插補點表達式
- 指令
- 計算屬性和偵聽器
- class和style綁定
- 條件渲染/清單渲染
- 表單輸入綁定
- 元件:可複用的vue執行個體
- 插槽
- 插件
- 混入mixin
- 深入響應式原理
模拟vue-router hash模式的實作
// 具體代碼在目錄code/homework_test檔案夾下,
// src/router/index.js路由配置檔案中修改配置模式
const router = new VueRouter({
mode: 'hash',
routes,
})
export default router;
- 具體的hash模式相關的知識點梳理: vue-router預設使用hash模式。 使用URL的hash來模拟一個完整的URL,當URL改變時,頁面不會重新加載。hash(#)是URL的錨點,代表的是網頁中的一個位置,值改版#後的部分,浏覽器隻會滾動到相應位置,不會重新加載網頁,也就是說hash出現在URL中,但不會被包含在http請求中,對後端完全沒有影響,是以改變hash不會重新加載頁面;同時,每一次改變#後面的部分,都會在浏覽器的通路曆史中增加一個記錄,使用後退按鈕,就可以回到上一個位置;是以說Hash模式通過錨點值的改變,根據不同的值,渲染指定DOM位置的不同資料。
-
問題:如何監聽hash變化 - hashchange()事件。
1.用class關鍵字初始化一個路由
class Routers {
constructor() {
// 以鍵值對的形式儲存路由
this.routes = {};
// 目前路由的url
this.currentUrl = '';
}
}
2.實作路由hash存儲與執行。在初始化完畢後,需要考慮兩個問題
- 将路由的hash以及對應的callback函數儲存
- 觸發路由hash變化後,執行對應的callback函數
class Routers {
constructor() {
this.routes = {};
this.currentUrl = '';
}
// 将path路徑與對應的callback函數儲存
route (path, callback) {
this.routes[path] = callback || function () {};
}
// 重新整理
refresh () {
// 擷取目前URL中的hash路徑
this.currentUrl = location.hash.slice(1) || '/';
// 執行目前hash路徑的callback函數
this.routes[this.currentUrl]();
}
}
3.監聽對應事件,隻需要在執行個體化class的時候監聽上邊的事件即可。
class Routers {
constructor () {
this.routes = {};
this.currentUrl = '';
this.refresh = this.refresh.bind(this);
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route (path, callback) {
this.routes[path] = callback || function () {};
}
refresh () {
//location.hash = '#/about', currentUrl = '/about';
this.currentUrl = location.hash.slice(1) || '/';
this.routes[this.currentUrl]();
}
}
// 應用一下
window.Router = new Routers();
var content = document.querySelector('body');
function changeColor (color) {
content.style.backgroundColor = color;
}
Router.route('/', function () {
changeColor('red');
});
Router.route('/yellow', function () {
changeColor('yellow');
});
Router.route('/green', function () {
changeColor('green');
});
Hash 模式和 History 模式的差別
原理的差別:
- hash模式是基于錨點,以及onhashchange事件,當路由發生變化時觸發該事件,根據目前路由位址找到對應元件重新渲染
- history模式是基于HTML5中的history API
- history.pushState():調用時路徑發生變化,監聽popstate事件,根據目前路由位址找到對應元件重新渲染,IE10以後才支援,存在相容性問題
- history.replaceState():替換位址欄中的位址,并且把位址記錄到曆史記錄中
history模式
- 需要伺服器支援
- 單頁面中,伺服器不存在"http://www.someurl/login"這樣的位址會傳回找不到該頁面,傳回一個預設的404頁面
- 在伺服器端應該除了靜态資源都傳回單頁應用的index.html
注:@vue/cli中已經配置好了對路由history模式的支援,是以本地開發的時候路由不會傳回404,在生産環境需要上傳到node伺服器或者nginx服務中才會出現此類問題
模拟Vue.js響應式原理
準備工作
- 資料驅動
- 響應式的核心原理
- 釋出訂閱模式和觀察者模式
資料驅動
1.資料響應式
- 資料模型僅僅是普通的JavaScript對象,而當我們修改資料時,視圖會進行更新,避免了繁瑣的DOM操作,提高開發效率
2.雙向綁定
- 資料改變,視圖改變;視圖改變,資料也随之改變
- 我們可以使用v-model在表單元素上建立雙向資料綁定
3.資料驅動
- 是Vue最獨特的特性之一
- 開發過程中僅需要關注資料本身,不需要關心資料是如何渲染到視圖
釋出訂閱模式
// 事件觸發器
class EventEmitter {
constructor() {
// 結構為:{事件名:[事件處理函數1,事件處理函數2],事件2:[事件處理函數]}
this.subs = {}
}
// 注冊事件
$on(eventType, handler) {
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
// 觸發事件
$emit(eventType) {
if (this.subs[eventType]) {
this.subs[eventType].forEach(handler => {
handler()
})
}
}
}
// 測試
let em = new EventEmitter()
em.$on('click', () => {
console.log(1)
})
em.$on('click', () => {
console.log(2)
})
em.$emit('click')
觀察者模式
// 釋出者-目标
class Dep {
constructor() {
// 記錄所有的訂閱者
this.subs = []
}
// 添加訂閱者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 當事件發生的時候,通知所有的訂閱者,調用所有訂閱者的update方法
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 訂閱者-觀察者
class Watcher {
// 當事件發生的時候,由釋出者來調用update,update内可以去更新視圖或者做其他一些操作
update() {
console.log('update')
}
}
// 測試
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
- 原理:Vue 的 data 中的成員實作響應式資料,是在建立 Vue 執行個體,将傳入構造函數的 data 存放在執行個體的 data中,然後周遊data 的成員,利用 Object.defineProperty 它們轉換成 getter/setter 并定義在 Vue 執行個體上(這是友善于在執行個體中使用 this.<成員名> 的操作來觸發真正的響應式變化)
- 然後調用 observer 實作對 data資料劫持。observer對每個成員通過Object.defineProperty将該成員轉化為getter/setter并定義在該成員上,真正的響應式就發生在這裡。又因為Vue使用的是觀察者模式,是以在data 的成員的 getter 中會收集該成員的所有觀察者(收集依賴),在 setter 中發發送通知以觸發觀察者的 update 方法
Diff 算法的執行過程
- 判斷Vnode和oldVnode是否相同,如果是,那麼直接return;
- 如果他們都有文本節點并且不相等,那麼将更新為Vnode的文本節點。
- 如果oldVnode有子節點而Vnode沒有,則删除el的子節點
- 如果oldVnode沒有子節點而Vnode有,則将Vnode的子節點真實化之後添加到el
- 如果兩者都有子節點,則執行updateChildren函數比較子節點
- 當新舊節點的頭部值得對比,進入patchNode方法,同時各自的頭部指針+1;
- 當新舊節點的尾部值得對比,進入patchNode方法,同時各自的尾部指針-1;
- 當oldStartVnode,newEndVnode值得對比,說明oldStartVnode已經跑到了後面,那麼就将oldStartVnode.el移到oldEndVnode.el的後邊。oldStartIdx+1,newEndIdx-1;
- 當oldEndVnode,newStartVnode值得對比,說明oldEndVnode已經跑到了前面,那麼就将oldEndVnode.el移到oldStartVnode.el的前邊。oldEndIdx-1,newStartIdx+1;
- 當以上4種對比都不成立時,通過newStartVnode.key 看是否能在oldVnode中找到,如果沒有則建立節點,如果有則對比新舊節點中相同key的Node,newStartIdx+1
- 當循環結束時
- oldStartIdx > oldEndIdx,可以認為oldVnode對比完畢,當然也有可能 newVnode也剛好對比完,一樣歸為此類。此時newStartIdx和newEndIdx之間的vnode是新增的,調 用addVnodes,把他們全部插進before的後邊。
- newStartIdx > newEndIdx,可以認為newVnode先周遊完,oldVnode還有節點。此時oldStartIdx和oldEndIdx之間的vnode在新的子節點裡已經不存在了,調用removeVnodes将它們從dom裡删除。