天天看點

手寫 Vue Router、手寫響應式實作、虛拟 DOM 和 Diff 算法vue原理剖析

vue原理剖析

基礎結構

模闆 :界面展示代碼

邏輯業務功能:

美化布局:

生命周期

手寫 Vue Router、手寫響應式實作、虛拟 DOM 和 Diff 算法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裡删除。