實現一個簡單的Vue.js

2020-12-13 mySoulCode

原文轉自 https://const_white.gitee.io/gitee-blog/blog/vue/mini-vue/

Vue響應式原理

圖片引自 孟思行 - 圖解 Vue 響應式原理

乞丐版 mini-vue

實現mini-vue之前,先看看官網的描述。在Vue官網,深入響應式原理中,是這樣說明的:

每個組件實例都對應一個 watcher實例,它會在組件渲染的過程中把「接觸」過的數據 property 記錄為依賴。之後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件重新渲染。

起步

技術原因,這裡不做Virtual DOM、render部分,而選擇直接操作DOM

簡單來說,mini vue在創建Vue實例時

Vue類負責把data中的屬性注入到Vue實例,並調用Observer類和Compiler類。Observer類負責數據劫持,把每一個data轉換成getter和setter。其核心原理是通過Object.defineProperty實現。Compiler類負責解析指令和插值表達式(更新視圖的方法)。Dep類負責收集依賴、添加觀察者模式。通知data對應的所有觀察者Watcher來更新視圖。在Observer類把每一個data轉換成getter和setter時,會創建一個Dep實例,用來負責收集依賴並發送通知。在每一個data中在getter中收集依賴。在setter中通知依賴,既通知所有Watcher實例新視圖。Watcher類負責數據更新後,使關聯視圖重新渲染。

實現代碼都添加了詳細的注釋,無毒無害,可放心查看

Vue類

classVue{constructor(options) {// 1. 保存 options的數據this.$options = options || {}this.$data = options.data || {}this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el// 2. 為方便調用(vm.msg),把 data中的成員轉換成 getter和 setter,並注入到 Vue實例中this._proxyData(this.$data)// 3. 調用 Observer類,監聽數據的變化new Observer(this.$data)// 4. 調用 compiler類,解析指令和插值表達式new Compiler(this) } _proxyData(data) {Object.keys(data).forEach(key => {Object.defineProperty(this, key, {enumerable: true,configurable: true,get() {return data[key] },set(newValue) {if (newValue === data[key]) {return } data[key] = newValue } }) }) }}Observer類

classObserver{constructor(data) {this.walk(data) }// 遍歷 data($data)中的屬性,把屬性轉換成響應式數據 walk(data) {if (!data || typeof data !== 'object') {return }Object.keys(data).forEach((key) => {this.defineReactive(data, key, data[key]) }) }// 定義響應式數據 defineReactive(obj, key, value) {const that = this// 負責收集依賴並發送通知let dep = new Dep()// 利用遞歸使深層(內部)屬性轉換成響應式數據this.walk(value)Object.defineProperty(obj, key, {enumerable: true,configurable: true,get() {// 收集依賴 Dep.target && dep.addSub(Dep.target)return value },set(newValue) {if (value === newValue) {return } value = newValue// 如果新設置的值為對象,也轉換成響應式數據 that.walk(newValue)// 發送通知 dep.notify() } }) }}Compiler類

classCompiler{constructor(vm) {this.vm = vmthis.el = vm.$elthis.compiler(this.el) }// 編譯模板,處理文本節點和元素節點 compiler(el) {const childNodes = el.childNodesArray.from(childNodes).forEach(node => {// 處理文本節點if (this.isTextNode(node)) {this.compilerText(node) } elseif (this.isElementNode(node)) {// 處理元素節點this.compilerElement(node) }// 判斷 node節點是否有子節點。如果有,遞歸調用 compileif (node.childNodes.length) {this.compiler(node) } }) }// 編譯元素節點,處理指令 compilerElement(node) {// 遍歷所有屬性節點Array.from(node.attributes).forEach(attr => {// 判斷是否 v-開頭指令let attrName = attr.nameif (this.isDirective(attrName)) {// 為了更優雅的處理不同方法,減去指令中的 v- attrName = attrName.substr(2)const key = attr.valuethis.update(node, key, attrName) } }) }// 執行對應指令的方法 update(node, key, attrName) {let updateFn = this[attrName + 'Updater']// 存在指令才執行對應方法 updateFn && updateFn.call(this, node, this.vm[key], key) }// 處理 v-text指令 textUpdater(node, value, key) { node.textContent = value// 創建 Watcher對象,當數據改變時更新視圖new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) }// 處理 v-model指令 modelUpdater(node, value, key) { node.value = value// 創建 Watcher對象,當數據改變時更新視圖new Watcher(this.vm, key, (newValue) => { node.value = newValue })// 雙向綁定 node.addEventListener('input', () => {this.vm[key] = node.value }) }// 編譯文本節點,處理插值表達式 compilerText(node) {const reg = /\{\{(.+?)\}\}/let value = node.textContentif (reg.test(value)) {// 只考慮一層的對象,如 data.msg = 'hello world',不考慮嵌套的對象。且假設只有一個插值表達式。const key = RegExp.$1.trim() node.textContent = value.replace(reg, this.vm[key])// 創建 Watcher對象,當數據改變時更新視圖new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } }// 判斷元素屬性是否屬於指令 isDirective(attrName) {return attrName.startsWith('v-') }// 判斷節點是否屬於文本節點 isTextNode(node) {return node.nodeType === 3 }// 判斷節點書否屬於元素節點 isElementNode(node) {return node.nodeType === 1 }}Dep類

classDep{constructor() {this.subs = [] }// 添加觀察者 addSub(sub) {if (sub && sub.update) {this.subs.push(sub) } }// 發送通知 notify() {this.subs.forEach(sub => { sub.update() }) }}Watcher類

classWatcher{constructor(vm, key, cb) {this.vm = vm// data中的屬性名this.key = key// 回調函數負責更新視圖this.cb = cb// 把 watcher對象記錄到 Dep類的靜態屬性 target中 Dep.target = this// 觸發 get方法,在 get方法中會調用 addSubthis.oldValue = vm[key] Dep.target = null }// 當數據發生變化的時候更新視圖 update() {const newValue = this.vm[this.key]// 數據沒有發生變化直接返回if (this.oldValue === newValue) {return }// 更新視圖this.cb(newValue) }}完整版思維導圖

對於數組的監聽

這裡直接把數組的每一項都添加上了getter和setter,所以vm.items[1] = 'x'也是響應式的。

Vue中為什麼沒這樣做呢?參考 為什麼vue沒有提供對數組屬性的監聽

相關焦點

  • 「Vue.js開發連載一」Vue.js簡介
    一、簡介Vue.js(讀音 /vju/,類似於view)是一個構建數據驅動的web界面的漸進式框架。Vue。js的目標是通過儘可能簡單的API實現響應的數據綁定和組合的視圖組件。它不僅易於上手,還便於與第三方庫或既有項目整合。
  • 【Vue.js 入門到實戰教程】01-Vue.js 數據綁定的基本實現和代碼分析
    從 MVVM 聊起Vue.js 是一個簡單、小巧的漸進式 JavaScript 框架,提供了現代 Web 開發中常用的高級功能:解耦視圖和數據可復用的組件前端路由狀態管理虛擬 DOM接下來,學院君將圍繞這些功能來給大家介紹 Vue.js 的基本語法和使用。
  • 【項目推薦】Vue.js
    【編者按】vue 是法語中視圖的意思,Vue.js 是一個輕巧、高性能、可組件化的 MVVM 庫,同時擁有非常容易上手的 API。
  • 【Vue.js入門到實戰教程】11-Vue Loader(下)| 編寫一個單文件 Vue 組件
    然後在 src/main.js 中引入 Bootstrap 的腳本和樣式文件:import Vue from 'vue'import App from '.編寫 ModalExample 組件我們將 vue_learning/component/slot.html 中的 modal-example 組件拆分出來,在 vue_learning/demo-project/src/components 目錄下新建一個單文件組件 ModalExample.vue,將 modal-example 組件代碼按照 Vue Loader 指定的格式填充到對應位置
  • Vue.js布局
    動態Vue.js布局組件前言vue.js是漸進增強的視圖庫,可以作為.html頁面部分使用,也可以結合vue-router、vuex、axios用來構建單頁面或多頁面應用。
  • [前端]分別原生JS和Vue實現計數器功能
    題目用vue實現計數器功能,其中vue實現的代碼由黑馬程式設計師vue教程給出,這裡對其CSS代碼進行了注釋
  • 【分享】Vue.js新手入門指南
    Vue.js 的目標是通過儘可能簡單的 API 實現響應的數據綁定和組合的視圖組件。如果你是有經驗的前端開發者,想知道 Vue.js 與其它庫/框架的區別,查看對比其它框架。這是官網的介紹,是不是覺得非常的抽象非常的官方?
  • Vue.js最佳實踐(五招讓你成為Vue.js大師)
    招式解析:我們需要藉助一下神器webpack,使用 require.context() 方法來創建自己的(模塊)上下文,從而實現自動動態require組件。這個方法需要3個參數:要搜索的文件夾目錄,是否還應該搜索它的子目錄,以及一個匹配文件的正則表達式。
  • 揭密vue.js的神秘之處,小程序跟vue有什麼關係
    微容用的微信小程序平臺採用開發思路是前後端分離,前端採用vue.js+h5+jq開發,後端採用MVE的思路,用php開發,用vue.js構建來微信小程序可視化前端之後,微容給大家介紹下vue的來源和特徵。
  • Vue.js以業務為中心的常見面試題
    在vue.js中的MVVM模式:vue.js是通過數據驅動的,vue.js實例化對象將dom和數據進行綁定,一旦綁定,dom和數據將保持同步,每當數據發生變化,dom也會隨著變化;ViewModel是Vue.js的核心,它是Vue.js的一個實例。
  • Vue-Router源碼學習之index.js(vue-router類)
    今天,帶來Vue-Router源碼解析系列的第二篇文章:index.js。正文vue-router類裡面都做了什麼?index.js是vue-router這個類的主構造函數,所以內容上算是比較關鍵的:從圖片中我們可以看出來,這是一個ES6聲明類的方法,vue-router源碼中類的聲明都是使用類ES的語法,constructor (options: RouterOptions
  • 推薦8 個漂亮實用的 vue.js 進度條組件
    為大家精心挑選了 8 個漂亮的 Progress Bars 組件,並附上 GitHub 連結和 vue.js 代碼示例,以及Vue3 快速深入全攻略。1.easy-circular-progress一個簡單的循環進度組件,帶有計數效果。
  • Vue.js 教程:構建一個特斯拉汽車餘電計算器
    但很多人都關心的一個問題是,電車充滿電後究竟可以跑多遠?行駛速度、氣溫和輪轂尺寸會對續航裡程有什麼影響?在本教程中,我們會使用 Vue.js 這個容易理解的 JavaScript 框架製作一個儀錶盤,通過它可以計算特斯拉電動汽車在不同情況下的行駛距離。
  • 初步認識vue.js框架的使用
    vue.js框架是幹什麼的Vue.js 是一個JavaScriptMVVM庫,是一套構建用戶界面的漸進式框架。它是以數據驅動和組件化的思想構建的,採用自底向上增量開發的設計。相比於Angular.js,Vue.js提供了更加簡潔、更易於理解的API,使得我們能夠快速地上手並使用Vue.js。如何使用vue.js1.下載 vue.min.js 並用 <script> 標籤引入。
  • CKEditor 4 編輯器已與 Vue.js 集成
    近日,CKEditor 團隊發布了與 Vue.js 框架原生集成的 CKEditor 4。為了儘可能簡單直觀地安裝使用 CKEditor 4,集成 Vue.js 的 CKEditor 4 已在 npm 和 CDN 中提供,也可通過 zip 包使用。
  • Ultimate Vue.js和Laravel CRUD教程
    著名的JavaScript框架專家Vue.js討論了如何創建一個執行CRUD操作的完整堆棧web應用程式。CRUD(創建,讀取,更新和刪除)是數據存儲的基本操作,也是您作為Laravel開發人員學習的第一件事情之一。但是,當您將Vue.js單頁應用程式添加到此堆棧的前端時會發生什麼?突然之間,你必須處理異步CRUD,因為操作現在不需要刷新頁面。
  • 10個簡單的技巧讓你的 vue.js 代碼更優雅
    DOCTYPE html><html lang="en"><body> <div id="app"> <child :status="status"></child> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist
  • Vue.js 框架作者公布 Vue 3 最新進展
    Vue.js 作者尤雨溪近日介紹了 Vue 3 的最新進展。Vue Router目前存在部分與 vue-router@3.x 相關的路由鉤子 (router hook)行為一致性問題,這也是 Vue Router 沒有被標記為 Beta 的原因。不過在非關鍵項目上可以使用新的路由。
  • JS實現Vue雙向綁定命令-下
    之前我們講到js實現Vue雙向綁定命令,需要將任務分解為三步:1、輸入框以及文本節點與 data 中的數據綁定2、輸入框內容變化時,data 中的數據同步變化。即 view => model 的變化。3、data 中的數據變化時,文本節點的內容同步變化。
  • 教你如何以Vue.js插件的形式實現消息提示框
    我們知道Vue.extend()方法可以創建一個組件的「子類」,我們可以利用這個「子類」來構造一個提示框組件對象,並設置組件的數據屬性的默認值。$el)}})();接下來將上述代碼封裝為Vue的插件,Vue的插件開發很簡單,只需要按照如下形式編寫代碼即可。