主頁 > 軟體設計 > Vue 原始碼實作: Data Binding 雙向資料系結(使用 Object.defineProperty 實作)

Vue 原始碼實作: Data Binding 雙向資料系結(使用 Object.defineProperty 實作)

2020-12-14 10:28:37 軟體設計

Vue 原始碼實作: Data Binding 雙向資料系結(使用 Object.defineProperty 實作)

文章目錄

  • Vue 原始碼實作: Data Binding 雙向資料系結(使用 Object.defineProperty 實作)
    • 簡介
    • 參考
  • 正文
    • 實作目標
    • 實作架構
    • 具體實作
      • 專案結構 & 靜態內容初始化
      • 核心代碼
      • 物件間互動順序 & 圖
        • input 輸入
        • 修改 `$data` 屬性
  • 結語

簡介

本篇要來干大事了(bushi,大名鼎鼎的 Vue 作為一個最潮的 MVVM 框架,實作了雙向資料系結和虛擬 dom 等多項復雜技術,本篇將要嘗試實作 Vue2 所使用的方法(借助 Object.defineProperty)來實作雙向資料系結,走起

參考

vue 雙向資料系結的實作學習(二)- 監聽器的實作https://www.cnblogs.com/adouwt/p/10039900.html

正文

實作目標

首先我們先來明確我們的實作目標:v-model 實作雙向資料系結,也就是修改系結變數時同時更新輸入框的值;或是輸入框輸入更新系結變數的值,圖示如下

我們的目標就是將左邊的 input 輸入框與右側 data 中的 name 變數系結起來(透過形如 <input v-model="name"> 的形式)

那么接下來我們要做的就是創建一個管理中心(MVVM)來幫我們完成這件事情:

實作架構

在開始上代碼之前,我們先來談談抽象的實作架構,前面我們提到我們的實作目標就是創建一個 MVVM 來幫我們完成雙向系結的作業,而這個 MVVM 內部又可以細分成三種成員:

  • Compiler 模版渲染器:通過編譯特定模版代碼之后渲染成實際 dom
  • Watcher 觀察者:觀察系結變數是否被修改,并通知 Compiler 重新渲染
  • Dispatcher 調度中心:負責決定通知哪個觀察者更新,系結變數修改時通知調度中心進而通知觀察者更新,如下圖所示

具體實作

接下來我們就來著手實作模擬 Vue2 中的雙向資料系結技術

注意:這里的實作代碼有非常多的漏洞,如:

  • 未實作模版渲染 Compiler的決議,而是直接修改目標 dom 的內容
  • 實作中只系結了一個 name 屬性
    • 所以從頭到尾只存在一個 Watcher,所以整個程序 Dispatcher.target 并未更動也未做區分
    • 系結到實體的 $prop 直接設為 name,當存在多個系結屬性的時候就必須加以區分

專案的構成參考了前一篇Express 實戰: 使用 express-static 處理靜態資源的方式創建的專案

專案結構 & 靜態內容初始化

首先給出專案結構:

/data-binding-defineProperty
.
├── package.json
├── server.js       // 靜態資源服務器入口
├── src/
│   ├── compiler.js     // Compiler(模版渲染)
│   ├── dispatcher.js   // Dispatcher(調度中心)
│   ├── favicon32.ico
│   ├── index.css       // 模版樣式
│   ├── index.html      // 模版代碼
│   ├── main.js         // 主入口
│   ├── observe.js      // 觀察方法(observe、reactive)
│   ├── vue.js          // Vue (MVVM)
│   └── watcher.js      // Watcher(觀察者)
└── yarn.lock

接下來給出幾個靜態檔案的初始化內容

package.json:加入 start 作為啟動命令

{
  "name": "js_data_binding_vue2",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "express-static": "^1.2.6"
  }
}

server.js

const express = require('express')
const expressStatic = require('express-static')

const app = express()

app.use(expressStatic('./src'))

const port = 3000

app.listen(port, () => {
  console.log(`server listen at: http://localhost:${port}`)
})

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>data-binding Vue2</title>
    <link rel="icon" href="favicon32.ico">
    <link rel="stylesheet" href="index.css">
</head>
<body>
    <div id="app">
        <input type="text" class="input" v-model="name">
        <h1 class="content">{{ name }}</h1>
    </div>

    <script src="main.js" type="module"></script>
</body>
</html>

index.css

#app {
    width: 500px;
    margin: 100px auto;
    text-align: center;
}

核心代碼

接下來給出具體的核心代碼實作,相關說明請看代碼注釋:

main.js:主入口

import Vue from './vue.js'

// 創建 MVVM 類,并暴露成全域變數 vm 供訪問
window.vm = new Vue({
  el: '#app',             // 模版選擇器,即替換目標
  data: {                 // 資料項
    name: 'default name'
  }  
})

vue.js:MVVM 類實作

import observe from './observe.js'
import Compiler from './compiler.js'

// MVVM 主類
export default function Vue (options) {
  // 初始化各屬性
  this.$options = options // 傳入引數備份
  this.$el = document.querySelector(options.el) // 選到實際 dom 元素
  this.$data = options.data // 資料項備份
  Object.keys(this.$data).forEach(key => {
    this.$prop = key // 目前只有一個系結屬性 name
  })
  this.init() // 初始化
}

Vue.prototype.init = function () {
  // 初始化時遞回觀察 this.$data 資料項
  observe(this.$data)
  // 創建模版渲染物件,于自身系結(傳入 vm)
  new Compiler(this)
}

observe.js:觀察函式實作

import Dispatcher from './dispatcher.js'

// 觀察(系結)資料項
export default function observe (data) {
  // 只對 object 作用
  if (!data || typeof data !== 'object') {
    return
  }
  // 激活 reactive 物件的每個鍵
  Object.keys(data).forEach(key => {
    reactive(data, key, data[key])
  })
}

// 激活函式
function reactive (data, key, value) {
  // 對每個鍵創建獨有的調度中心 Dispatcher
  const dp = new Dispatcher()
  // 使用 Object.defineProperty 設定成訪問器屬性(getter/setter)
  Object.defineProperty(data, key, {
    get () {
      // 訪問屬性時檢查當前訪問者是否已經訂閱該屬性
      if (Dispatcher.target && !dp.subs.includes(Dispatcher.target)) {
        dp.addSub(Dispatcher.target)
      }
      return value
    },
    set (newValue) {
      if (value !== newValue) {
        // 實際的值透過閉包系結到區域變數 value 上
        value = newValue
        // 每次更新就透過 Dispatcher 更新(notify 將通知所有 subs)
        dp.notify()
      }
    }
  })
  // 遞回觀察
  observe(value)
}

dispatcher.js:調度中心實作

export default function Dispatcher () {
  // 訂閱者串列,是一個 Watcher 串列
  this.subs = []
}

Dispatcher.target = null

Dispatcher.prototype.notify = function () {
  // 通知更新時提醒所有觀察者更新(呼叫 sub.update())
  this.subs.forEach(sub => {
    sub.update()
  })
}

Dispatcher.prototype.addSub = function (sub) {
  // 添加訂閱
  this.subs.push(sub)
}

watcher.js:觀察者實作

import Dispatcher from './dispatcher.js'

// 觀察者(訂閱者)
export default function Watcher (vm, prop, callback) {
  this.vm = vm
  this.$prop = prop
  this.value = this.get()
  this.callback = callback
}

Watcher.prototype.get = function () {
  Dispatcher.target = this
  const value = this.vm.$data[this.$prop]
  return value
}

Watcher.prototype.update = function () {
  const value = this.vm.$data[this.$prop]
  const oldValue = this.value
  // 觀察者更新時檢查當前保留資料(this.value 將與實際 dom 展示資料同步)
  // 與 新資料(data.name 為實際系結資料)
  if (oldValue !== value) {
    // 不相同時則更新 this.value 并通知 Compiler 更新 dom(callback 為 Compiler 傳入的更新 dom 函式)
    this.value = value
    this.callback(this.value)
  }
}

compiler.js:模版渲染實作(非常簡陋的實作)

import Watcher from './watcher.js'

// 模版渲染器
export default function Compiler (vm) {
  this.vm = vm
  this.$el = vm.$el
  this.fragment = null
  this.init()
}

Compiler.prototype.init = function () {
  // 初始化時使用 data 的值替換 dom 展示的內容
  // 這邊沒有實作模版決議,而是直接指定 data.name 并替換標簽內容(textContent)
  let value = this.vm.$data.name
  document.querySelector('.input').value = value
  document.querySelector('.content').textContent = value

  // 為觀察屬性(prop)創建相應的觀察者,并傳入能夠更新模版內容的回呼函式(callback)
  // 這邊只有一個 data.name 屬性,并且回呼函式直接修改指定標簽內容
  // 正常實作是需要遇上方模版決議語法配合,在虛擬 dom 上修改相應標簽
  new Watcher(this.vm, this.vm.$prop, value => {
    document.querySelector('.input').value = value
    document.querySelector('.content').textContent = value
  })

  // 為輸入框添加監聽函式
  document.querySelector('.input').addEventListener('input', e => {
    const targetValue = e.target.value
    if (value !== targetValue) {
      // 輸入框的值修改時直接修改系結變數的值
      this.vm.$data.name = targetValue
      // 并直接更新模版內容
      document.querySelector('.input').value = targetValue
      document.querySelector('.content').textContent = targetValue
    }
  }, false) // 默認 false 為冒泡事件
}

物件間互動順序 & 圖

所謂雙向系結就是不管我們是(1)修改系結變數(2)輸入框輸入哪種操作另一種都會同步,所以我們分別從兩個路線來看物件間是如何互動的:

input 輸入

當我們在輸入框進行輸入時會產生 input 事件,并進入到 compiler.js

// 為輸入框添加監聽函式
document.querySelector('.input').addEventListener('input', e => {
  const targetValue = e.target.value
  if (value !== targetValue) {
    // 輸入框的值修改時直接修改系結變數的值
    this.vm.$data.name = targetValue
    // 并直接更新模版內容
    document.querySelector('.input').value = targetValue
    document.querySelector('.content').textContent = targetValue
  }
}, false) // 默認 false 為冒泡事件

這段處理函式,他會直接修改 $data.name 的值并更新模版,而修改 $data.name 則會觸發 observe.js 中定義的訪問器屬性(set 方法)

// 使用 Object.defineProperty 設定成訪問器屬性(getter/setter)
Object.defineProperty(data, key, {
  get () {
    // 訪問屬性時檢查當前訪問者是否已經訂閱該屬性
    if (Dispatcher.target && !dp.subs.includes(Dispatcher.target)) {
      dp.addSub(Dispatcher.target)
    }
    return value
  },
  set (newValue) {
    if (value !== newValue) {
      // 實際的值透過閉包系結到區域變數 value 上
      value = newValue
      // 每次更新就透過 Dispatcher 更新(notify 將通知所有 subs)
      dp.notify()
    }
  }
})

這時候就會更新閉包中的 value 并通知相應的 Dispatcher 進行更新 notifynotify 函式則會通知所有觀察者進行更新(watcher.js

Watcher.prototype.update = function () {
  const value = this.vm.$data[this.$prop]
  const oldValue = this.value
  // 觀察者更新時檢查當前保留資料(this.value 將與實際 dom 展示資料同步)
  // 與 新資料(data.name 為實際系結資料)
  if (oldValue !== value) {
    // 不相同時則更新 this.value 并通知 Compiler 更新 dom(callback 為 Compiler 傳入的更新 dom 函式)
    this.value = value
    this.callback(this.value)
  }
}

觀察者 Watcher 就會根據 Compiler 提供的回呼函式更新模版內容

修改 $data 屬性

第二種可能是直接修改 $data.name 的值,就會經過 set -> dp.notify() -> sub.update() -> callback() 的路徑更新系結標簽的值啦,

結語

本篇模仿 Vue2 使用 Object.defineProperty 實作雙向資料系結,不過最重要的是缺少了模版決議和渲染的部分(這很重要,應該只修改 {{}} 所覆寫的范圍而不是修改整個標簽內容,同時應該為每個 {{}} 參考創建相應的觀察者 Watcher 才是正確的實作),供大家參考,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/234237.html

標籤:其他

上一篇:CTF中干擾逆向分析的幾種技術的介紹和解題思路

下一篇:MyBatis學習總結

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more