溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何實(shí)現(xiàn)一個(gè)vue雙向綁定

發(fā)布時(shí)間:2022-03-04 09:02:58 來源:億速云 閱讀:84 作者:iii 欄目:編程語言

這篇文章主要講解了“如何實(shí)現(xiàn)一個(gè)vue雙向綁定”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“如何實(shí)現(xiàn)一個(gè)vue雙向綁定”吧!

如何實(shí)現(xiàn)一個(gè)vue雙向綁定

開始

開局一張圖

如何實(shí)現(xiàn)一個(gè)vue雙向綁定

從圖上可以看出new Vue()分為了兩步走

  • 代理監(jiān)聽所有數(shù)據(jù),并與Dep進(jìn)行關(guān)聯(lián),通過Dep通知訂閱者進(jìn)行視圖更新。【相關(guān)推薦:vuejs視頻教程】

  • 解析所有模板,并將模板中所用到的數(shù)據(jù)進(jìn)行訂閱,并綁定一個(gè)更新函數(shù),數(shù)據(jù)發(fā)生改變時(shí)Dep通知訂閱者執(zhí)行更新函數(shù)。

接下里就是分析如何去實(shí)現(xiàn),并且都需要寫什么,先看一段vue的基礎(chǔ)代碼,我們從頭開始分析

<div id="app">
  <input v-model="message" />
  <p>{{message}}</p>
</div>
let app = new Vue({
    el:"#app",
    data:{
      message:"測試這是一個(gè)內(nèi)容"
    }
})

從上面代碼我們可以看到new Vue的操作,里面攜帶了eldata屬性,這算是最基礎(chǔ)的屬性,而在html代碼中我們知道<div id="app">是vue渲染的模板根節(jié)點(diǎn),所以vue要渲染頁面就要去實(shí)現(xiàn)一個(gè)模板解析的方法Compile類,解析方法中還需要去處理{{ }}v-model兩個(gè)指令,除了解析模板之后我們還需要去實(shí)現(xiàn)數(shù)據(jù)代理也就是實(shí)現(xiàn)Observer

實(shí)現(xiàn) Vue 類

如下代碼所示,這就寫完了Vue類,夠簡單吧,如果對(duì)class關(guān)鍵字不熟悉的,建議先去學(xué)習(xí)一下,從下面我們可能看到,這里實(shí)例化了兩個(gè)類,一個(gè)是代理數(shù)據(jù)的類,一個(gè)是解析模板的類。

class Vue {
  constructor(options) {
    // 代理數(shù)據(jù)
    new Observer(options.data)
    // 綁定數(shù)據(jù)
    this.data = options.data
    // 解析模板
    new Compile(options.el, this)
  }
}

接著往下我們先寫一個(gè)Compile類用于解析模板,我們?cè)賮矸治鲆徊?,解析模板要做什么?/p>

  • 我們要解析模板不可能直接對(duì)dom繼續(xù)操作,所以我們要?jiǎng)?chuàng)建一個(gè)文檔片段(虛擬dom),然后將模板DOM節(jié)點(diǎn)復(fù)制一份到虛擬DOM節(jié)點(diǎn)中,對(duì)虛擬DOM節(jié)點(diǎn)解析完成之后,再將虛擬DOM節(jié)點(diǎn)替換掉原來的DOM節(jié)點(diǎn)

  • 虛擬節(jié)點(diǎn)復(fù)制出來之后,我們要遍歷整個(gè)節(jié)點(diǎn)樹進(jìn)行解析,解析過程中會(huì)對(duì)DOM的atrr屬性進(jìn)行遍歷找到Vue相關(guān)的指令,除此之外還要對(duì) textContent節(jié)點(diǎn)內(nèi)容進(jìn)行解析,判斷是否存在雙花括號(hào)

  • 將解析出來所用到的屬性進(jìn)行一個(gè)訂閱

實(shí)現(xiàn)模板解析 Compile 類

下面我們將逐步實(shí)現(xiàn)

  • 構(gòu)建Compile類,先把靜態(tài)節(jié)點(diǎn)和Vue實(shí)例獲取出來,再定義一個(gè)虛擬dom的屬性用來存儲(chǔ)虛擬dom

class Compile {
  constructor(el, vm) {
    // 獲取靜態(tài)節(jié)點(diǎn)
    this.el = document.querySelector(el);
    // vue實(shí)例
    this.vm = vm 
    // 虛擬dom
    this.fragment = null 
    // 初始化方法
    this.init()
  }
}
  • 實(shí)現(xiàn)初始化方法init(),該方法主要是用于創(chuàng)建虛擬dom和調(diào)用解析模板的方法,解析完成之后再將DOM節(jié)點(diǎn)替換到頁面中

class Compile { 
  //...省略其他代碼

  init() {
    // 創(chuàng)建一個(gè)新的空白的文檔片段(虛擬dom)
    this.fragment = document.createDocumentFragment()
  	// 遍歷所有子節(jié)點(diǎn)加入到虛擬dom中
    Array.from(this.el.children).forEach(child => {
      this.fragment.appendChild(child)
    })
    // 解析模板
    this.parseTemplate(this.fragment)
    // 解析完成添加到頁面
    this.el.appendChild(this.fragment);
  }
}
  • 實(shí)現(xiàn)解析模板方法parseTemplate,主要是遍歷虛擬DOM中的所有子節(jié)點(diǎn)并進(jìn)行解析,根據(jù)子節(jié)點(diǎn)類型進(jìn)行不同的處理。

class Compile { 
  //...省略其他代碼

  // 解析模板 
  parseTemplate(fragment) {
    // 獲取虛擬DOM的子節(jié)點(diǎn)
    let childNodes = fragment.childNodes || []
    // 遍歷節(jié)點(diǎn)
    childNodes.forEach((node) => {
      // 匹配大括號(hào)正則表達(dá)式 
      var reg = /\{\{(.*)\}\}/;
      // 獲取節(jié)點(diǎn)文本
      var text = node.textContent;
      if (this.isElementNode(node)) { // 判斷是否是html元素
        // 解析html元素
        this.parseHtml(node)
      } else if (this.isTextNode(node) && reg.test(text)) { //判斷是否文本節(jié)點(diǎn)并帶有雙花括號(hào)
        // 解析文本
        this.parseText(node, reg.exec(text)[1])
      }

      // 遞歸解析,如果還有子元素則繼續(xù)解析
      if (node.childNodes && node.childNodes.length != 0) {
        this.parseTemplate(node)
      }
    });
  }
}
  • 根據(jù)上面的代碼我們得出需要實(shí)現(xiàn)兩個(gè)簡單的判斷,也就是判斷是否是html元素和文字元素,這里通過獲取nodeType的值來進(jìn)行區(qū)分,不了解的可以直接看一下 傳送門:Node.nodeType,這里還擴(kuò)展了一個(gè)isVueTag方法,用于后面的代碼中使用

class Compile { 
  //...省略其他代碼

	// 判斷是否攜帶 v-
  isVueTag(attrName) {
    return attrName.indexOf("v-") == 0
  }
  // 判斷是否是html元素
  isElementNode(node) {
    return node.nodeType == 1;
  }
  // 判斷是否是文字元素
  isTextNode(node) {
    return node.nodeType == 3;
  }
}
  • 實(shí)現(xiàn)parseHtml方法,解析html代碼主要是遍歷html元素上的attr屬性

class Compile {
  //...省略其他代碼

  // 解析html
  parseHtml(node) {
    // 獲取元素屬性集合
    let nodeAttrs = node.attributes || []
    // 元素屬性集合不是數(shù)組,所以這里要轉(zhuǎn)成數(shù)組之后再遍歷
    Array.from(nodeAttrs).forEach((attr) => {
      // 獲取屬性名稱
      let arrtName = attr.name;
      // 判斷名稱是否帶有 v- 
      if (this.isVueTag(arrtName)) {
        // 獲取屬性值
        let exp = attr.value;
        //切割 v- 之后的字符串
        let tag = arrtName.substring(2);
        if (tag == "model") {
          // v-model 指令處理方法
          this.modelCommand(node, exp, tag)
        }
      }
    });
  }
}
  • 實(shí)現(xiàn)modelCommand方法,在模板解析階段來說,我們只要把 vue實(shí)例中data的值綁定到元素上,并實(shí)現(xiàn)監(jiān)聽input方法更新數(shù)據(jù)即可。

class Compile {
	//...省略其他代碼
  
   // 處理model指令
  modelCommand(node, exp) {
    // 獲取數(shù)據(jù)
    let val = this.vm.data[exp]
    // 解析時(shí)綁定數(shù)據(jù)
    node.value = val || ""

    // 監(jiān)聽input事件
    node.addEventListener("input", (event) => {
      let newVlaue = event.target.value;
      if (val != newVlaue) {
        // 更新data數(shù)據(jù)
        this.vm.data[exp] = newVlaue
        // 更新閉包數(shù)據(jù),避免雙向綁定失效
        val = newVlaue
      }
    })
  }
}
  • 處理Text元素就相對(duì)簡單了,主要是將元素中的textContent內(nèi)容替換成數(shù)據(jù)即可

class Compile {
	//...省略其他代碼
  
  //解析文本
  parseText(node, exp) {
    let val = this.vm.data[exp]
    // 解析更新文本
    node.textContent = val || ""
  }
}

至此已經(jīng)完成了Compile類的初步編寫,測試結(jié)果如下,已經(jīng)能夠正常解析模板

如何實(shí)現(xiàn)一個(gè)vue雙向綁定

下面就是我們目前所實(shí)現(xiàn)的流程圖部分

如何實(shí)現(xiàn)一個(gè)vue雙向綁定

坑點(diǎn)一:

  • 在第6點(diǎn)modelCommand方法中并沒有實(shí)現(xiàn)雙向綁定,只是單向綁定,后續(xù)要雙向綁定時(shí)還需要繼續(xù)處理

坑點(diǎn)二:

  • 第7點(diǎn)parseText方法上面的代碼中并沒有去訂閱數(shù)據(jù)的改變,所以這里只會(huì)在模板解析時(shí)綁定一次數(shù)據(jù)


實(shí)現(xiàn)數(shù)據(jù)代理 Observer 類

這里主要是用于代理data中的所有數(shù)據(jù),這里會(huì)用到一個(gè)Object.defineProperty方法,如果不了解這個(gè)方法的先去看一下文檔傳送門:

文檔:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Observer類主要是一個(gè)遞歸遍歷所有data中的屬性然后進(jìn)行數(shù)據(jù)代理的的一個(gè)方法

defineReactive中傳入三個(gè)參數(shù)data, key, val

datakey都是Object.defineProperty的參數(shù),而val將其作為一個(gè)閉包變量供Object.defineProperty使用

// 監(jiān)聽者
class Observer {
  constructor(data) {
    this.observe(data)
  }
  // 遞歸方法
  observe(data) {
    //判斷數(shù)據(jù)如果為空并且不是object類型則返回空字符串
    if (!data || typeof data != "object") {
      return ""
    } else {
      //遍歷data進(jìn)行數(shù)據(jù)代理
      Object.keys(data).forEach(key => {
        this.defineReactive(data, key, data[key])
      })
    }
  }

  // 代理方法
  defineReactive(data, key, val) {
    // 遞歸子屬性
    this.observe(data[key])
    Object.defineProperty(data, key, {
      configurable: true,  //可配置的屬性
      enumerable: true, //可遍歷的屬性
      get() {
        return val
      },
      set(newValue) {
        val = newValue
      }
    })
  }
}

下面我們來測試一下是否成功實(shí)現(xiàn)了數(shù)據(jù)代理,在Vue的構(gòu)造函數(shù)輸出一下數(shù)據(jù)

class Vue {
  constructor(options) {
    // 代理數(shù)據(jù)
    new Observer(options.data)
    console.log(options.data)
    // 綁定數(shù)據(jù)
    this.data = options.data
    // 解析模板
    new Compile(options.el, this)
  }
}

結(jié)果如下,我們可以看出已經(jīng)實(shí)現(xiàn)了數(shù)據(jù)代理。

如何實(shí)現(xiàn)一個(gè)vue雙向綁定

對(duì)應(yīng)的流程圖如下所示

如何實(shí)現(xiàn)一個(gè)vue雙向綁定

坑點(diǎn)三:

  • 這里雖然實(shí)現(xiàn)了數(shù)據(jù)代理,但是按照?qǐng)D上來說,還需要引入管理器,在數(shù)據(jù)發(fā)生變化時(shí)通知管理器數(shù)據(jù)發(fā)生了變化,然后管理器再通知訂閱者更新視圖,這個(gè)會(huì)在后續(xù)的填坑過程過講到。


實(shí)現(xiàn)管理器 Dep 類

上面我們已經(jīng)實(shí)現(xiàn)了模板解析到初始化視圖,還有數(shù)據(jù)代理。而下面要實(shí)現(xiàn)的Dep類主要是用于管理訂閱者和通知訂閱者,這里會(huì)用一個(gè)數(shù)組來記錄每個(gè)訂閱者,而類中也會(huì)給出一個(gè)notify方法去調(diào)用訂閱者的update方法,實(shí)現(xiàn)通知訂閱者更新功能。這里還定義了一個(gè)target屬性用來存儲(chǔ)臨時(shí)的訂閱者,用于加入管理器時(shí)使用。

class Dep {
  constructor() {
    // 記錄訂閱者
    this.subList = []
  }
  // 添加訂閱者
  addSub(sub) {
    // 先判斷是否存在,防止重復(fù)添加訂閱者
    if (this.subList.indexOf(sub) == -1) {
      this.subList.push(sub)
    }
  }
  // 通知訂閱者
  notify() {
    this.subList.forEach(item => {
      item.update() //訂閱者執(zhí)行更新,這里的item就是一個(gè)訂閱者,update就是訂閱者提供的方法
    })
  }
}
// Dep全局屬性,用來臨時(shí)存儲(chǔ)訂閱者
Dep.target = null

管理器實(shí)現(xiàn)完成之后我們也就實(shí)現(xiàn)了流程圖中的以下部分。要注意下面幾點(diǎn)

  • Observer通知Dep主要是通過調(diào)用notify方法

  • Dep通知Watcher主要是是調(diào)用了Watcher類中的update方法

如何實(shí)現(xiàn)一個(gè)vue雙向綁定


實(shí)現(xiàn)訂閱者 Watcher 類

訂閱者代碼相對(duì)少,但是理解起來還是有點(diǎn)難度的,在Watcher類中實(shí)現(xiàn)了兩個(gè)方法,一個(gè)是update更新視圖方法,一個(gè)putIn方法(我看了好幾篇文章都是定義成 get 方法,可能是因?yàn)槲依斫獾牟粔蚝冒?。

  • update:主要是調(diào)用傳入的cb方法體,用于更新頁面數(shù)據(jù)

  • putIn:主要是用來手動(dòng)加入到Dep管理器中。

// 訂閱者
class Watcher {
  // vm:vue實(shí)例本身
  // exp:代理數(shù)據(jù)的屬性名稱
  // cb:更新時(shí)需要做的事情
  constructor(vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.putIn()
  }
  update() {
    // 調(diào)用cb方法體,改變this指向并傳入最新的數(shù)據(jù)作為參數(shù)
    this.cb.call(this.vm, this.vm.data[this.exp])
  }
  putIn() {
    // 把訂閱者本身綁定到Dep的target全局屬性上
    Dep.target = this
    // 調(diào)用獲取數(shù)據(jù)的方法將訂閱者加入到管理器中
    let val = this.vm.data[this.exp]
    // 清空全局屬性
    Dep.target = null
  }
}

坑點(diǎn)四:

  • Watcher類中的putIn方法再構(gòu)造函數(shù)調(diào)用后并沒有加入到管理器中,而是將訂閱者本身綁定到target全局屬性上而已

埋坑

通過上面的代碼我們已經(jīng)完成了每一個(gè)類的構(gòu)建,如下圖所示,但是還是有幾個(gè)流程是有問題的,也就是上面的坑點(diǎn)。所以下面要填坑

如何實(shí)現(xiàn)一個(gè)vue雙向綁定

埋坑 1 和 2

完成坑點(diǎn)一和坑點(diǎn)二,在modelCommandparseText方法中增加實(shí)例化訂閱者代碼,并自定義要更新時(shí)執(zhí)行的方法,其實(shí)就是更新時(shí)去更新頁面中的值即可

modelCommand(node, exp) {
  
  // ...省略其他代碼
  
  // 實(shí)例化訂閱者,更新時(shí)直接更新node的值
  new Watcher(this.vm, exp, (value) => {
    node.value = value
  })
}


parseText(node, exp) {
  
  //  ...省略其他代碼
  
  // 實(shí)例化訂閱者,更新時(shí)直接更新文本內(nèi)容
  new Watcher(this.vm, exp, (value) => {
    node.textContent = value
  })
}

埋坑 3

完成坑點(diǎn)三,主要是為了引入管理器,通知管理器發(fā)生改變,主要是在Object.defineProperty set方法中調(diào)用dep.notify()方法

// 監(jiān)聽方法
defineReactive(data, key, val) {
  // 實(shí)例化管理器--------------增加這一行
  let dep = new Dep()
  
  // ...省略其他代碼
  
    set(newValue) {
      val = newValue
      // 通知管理器改變--------------增加這一行
      dep.notify()
    }

}

埋坑 4

完成坑點(diǎn)四,主要四將訂閱者加入到管理器中

defineReactive(data, key, val) {
  // ...省略其他代碼
    get() {
      // 將訂閱者加入到管理器中--------------增加這一段
      if (Dep.target) {
        dep.addSub(Dep.target)
      }
      return val
    },
  // ...省略其他代碼
}

完成了坑點(diǎn)四可能就會(huì)有靚仔疑惑了,這里是怎么加入的呢Dep.target又是什么呢,我們不妨從頭看看代碼并結(jié)合下面這張圖

如何實(shí)現(xiàn)一個(gè)vue雙向綁定

至此我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡單的雙向綁定,下面測試一下

如何實(shí)現(xiàn)一個(gè)vue雙向綁定

感謝各位的閱讀,以上就是“如何實(shí)現(xiàn)一個(gè)vue雙向綁定”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)如何實(shí)現(xiàn)一個(gè)vue雙向綁定這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

vue
AI