聊聊Vue中如何實現資料雙向繫結

2022-11-24 22:00:27
中如何實現資料雙向繫結?下面本篇文章給大家介紹一下Vue.js資料雙向繫結的實現方法,希望對大家有所幫助!

前端(vue)入門到精通課程:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:

在我們使用vue的時候,當資料發生了改變,介面也會跟著更新,但這並不是理所當然的,我們修改資料的時候vue是如何監聽資料的改變以及當資料發生改變的時候vue如何讓介面重新整理的?

當我們修改資料的時候vue是通過es5中的Object.defineProperty方法來監聽資料的改變的,當資料發生了改變通過釋出訂閱模式統計訂閱者介面發生了重新整理,這是一種設計模式。【學習視訊分享:、】

下圖,從new Vue開始建立Vue範例,會傳入el和data,data會被傳入一個觀察者物件,利用Object.definproperty將data裡資料轉化成getter/setter進行資料劫持,data裡的每個屬性都會建立一個Dep範例用來儲存watcher範例

而el則傳入compile,在compile裡進行指令的解析,當解析到el中使用到data裡的資料會觸發我們的getter,從而將我們的watcher新增到依賴當中。當資料發生改變的時候會觸發我們的setter發出依賴通知,通知watcher,watcher接受到通知後去向view發出通知,讓view去更新

image.png

資料劫持

html部分建立一個id為app的div標籤,裡面有span和input標籤,span標籤使用了插值表示式,input標籤使用了v-model

<div class="container" id="app">
    <span>內容:{{content}}</span>
    <input type="text" v-model="content">
</div>
登入後複製

js部分引入了一個vue.js檔案,實現資料雙向繫結的程式碼就寫在這裡面,然後建立Vue範例vm,把資料掛載到div標籤上

const vm=new Vue({
    el:'#app',
        data:{
        content:'請輸入開機密碼'
    }
})
登入後複製

new了一個Vue範例很明顯需要用到建構函式,在vue的原始碼裡定義類是使用了function來定義的,這裡我使用ES6的class來建立這個Vue範例

然後設定constructor,形參設為obj_instance,作為new一個Vue範例的時候傳入的物件,並把傳進來的物件裡的data賦值給範例裡的$data屬性

在javascript裡物件的屬性發生了變化,需要告訴我們,我們就可以把更改後的屬性更新到dom節點裡,因此初始化範例的時候定義一個監聽函數作為呼叫,呼叫的時候傳入需要監聽的資料

class Vue{//建立Vue範例
  constructor(obj_instance){
    this.$data=obj_instance.data
    Observer(this.$data)
  }
}
function Observer(data_instance){//監聽函數
  
}
登入後複製

列印一下這個範例vm

image.png

範例已經建立出來了但是還需要為$data裡的每一個屬性進行監聽,要實現資料監聽就用到了Object.definePropertyObject.defineProperty可以修改物件的現有屬性,語法格式為Object.defineProperty(obj, prop, descriptor)

  • obj:要定義屬性的物件
  • prop:要定義或修改的屬性的名稱
  • descriptor:要定義或修改的屬性描述符

監聽物件裡的每一個屬性,我們使用Object.keys和foreach遍歷物件裡的每一個屬性並且對每一個屬性使用Object.defineProperty進行資料監聽

function Observer(data_instance){
  Object.keys(data_instance).forEach(key=>{
    Object.defineProperty(data_instance,key,{
      enumerable:true,//設定為true表示屬性可以列舉
      configurable:true,//設定為true表示屬性描述符可以被改變
      get(){},//存取該屬性的時候觸發,get和set函數就是資料監聽的核心
      set(){},//修改該屬性的時候觸發
    })
  })
}
登入後複製

Object.defineProperty前需要將屬性對應的值存起來然後在get函數裡面返回出來,不然到了get函數以後屬性的值已經沒了,返回給屬性的值就變成了undefined

let value=data_instance[key]
Object.defineProperty(data_instance,key,{
  enumerable:true,
  configurable:true,
  get(){
    console.log(key,value);
    return value
  },
  set(){}
})
登入後複製

點選一下$data裡的屬性名就會觸發get函數

5.gif

然後設定set函數,為set設定形參,這個形參表示新傳進來的屬性值,然後將這個新的屬性值賦值給變數value,不需要return返回什麼,只做修改,返回是在存取get的時候返回的,修改之後get也會存取最新的value變數值

set(newValue){
    console.log(key,value,newValue);
    value = newValue
}
登入後複製

6.gif

但是當前只為$data的第一層屬性設定了get和set,如果還有更深的一層如

obj:{
    a:'a',
    b:'b'
}
登入後複製

這種的並沒有設定get和set,我們需要一層一層的往屬性裡面進行資料劫持,因此使用遞迴再次監聽自己,並在遍歷之前進行條件判斷,沒有子屬性了或者沒有檢測到物件就終止遞迴

function Observer(data_instance){
  //遞迴出口
  if(!data_instance || typeof data_instance != 'object') return
  Object.keys(data_instance).forEach(key=>{
    let value=data_instance[key]
    Observer(value)//遞迴-子屬性的劫持
    Object.defineProperty(data_instance,key,{
      enumerable:true,
      configurable:true,
      get(){
        console.log(key,value);
        return value
      },
      set(newValue){
        console.log(key,value,newValue);
        value = newValue
      }
    })
  })
}
登入後複製

還有一個細節,如果我們將$data的content屬性從字串改寫成一個物件,這個新的物件並沒有get和set

1669019095973.png

因為我們在修改的時候根本沒有設定get和set,因此在set裡要呼叫監聽函數

set(newValue){
    console.log(key,value,newValue);
    value = newValue
    Observer(newValue)
}
登入後複製

1669019254699.png

模板解析

劫持資料後就要把Vue範例裡的資料應用帶頁面上,得要加一個臨時記憶體區域,將所有資料都更新後再渲染頁面以此減少dom操作

建立一個解析函數,設定2個引數,一個是Vue範例裡掛載的元素,另一個是Vue範例,在函數裡獲取獲取元素儲存在範例了的$el裡,獲取元素後放入臨時記憶體裡,需要用到建立一個新的空白的檔案片段

然後把$el的子節點一個一個加到fragment變數裡,頁面已經沒有內容了,內容都被臨時存在fragment裡了

class Vue{
  constructor(obj_instance){
    this.$data=obj_instance.data
    Observer(this.$data)
    Compile(obj_instance.el,this)
  }
}
function Compile(ele,vm){
  vm.$el=document.querySelector(ele)
  const fragment=document.createDocumentFragment()
  let child;
  while (child=vm.$el.firstChild){
    fragment.append(child)
  }
  console.log(fragment);
  console.log(fragment.childNodes);
}
登入後複製

image.png

現在直接把需要修改的內容應用到檔案碎片裡面,應用後重新渲染,只需修改了fragment的childNodes子節點的文位元組點,文位元組點的型別是3,可以建立一個函數並呼叫來修改fragment裡的內容

節點裡面可能還會有節點,因此判定節點型別是否為3,不是就遞迴呼叫這個解析函數

節點型別為3就進行修改操作,但也不行把整個節點的文字都修改,只需修改插值表示式的內容,因此要使用正規表示式匹配,將匹配的結果儲存到變數裡,匹配的結果是一個陣列,而索引為1的元素才是我們需要提取出來的元素,這個元素就是去除了{{}}和空格得到的字串,然後就可以直接用Vue範例來存取對應屬性的值,修改完後return出去結束遞迴

function Compile(ele,vm){
  vm.$el=document.querySelector(ele) //獲取元素儲存在範例了的$el裡
  const fragment=document.createDocumentFragment() //建立檔案碎片
  let child;
  while (child=vm.$el.firstChild){//迴圈將子節點新增到檔案碎片裡
    fragment.append(child)
  }
  
  fragment_compile(fragment)
  function fragment_compile(node){ //修改文位元組點內容
    const pattern = /\{\{\s*(\S*)\s*\}\}/ //檢索字串中正規表示式的匹配,用於匹配插值表示式
    if(node.nodeType===3){
      const result = pattern.exec(node.nodeValue)
      if(result){
        console.log('result[1]')
        const value=result[1].split('.').reduce(//split將物件裡的屬性分佈在陣列裡,鏈式地進行排列;reduce進行累加,層層遞進獲取$data的值
          (total,current)=>total[current],vm.$data
        )
        node.nodeValue=node.nodeValue.replace(pattern,value) //replace函數將插值表示式替換成$data裡的屬性的值
      }
      return 
    }
    node.childNodes.forEach(child=>fragment_compile(child))
  }
  vm.$el.appendChild(fragment) //將檔案碎片應用到對應的dom元素裡面
}
登入後複製

頁面的內容又出來了,插值表示式替換成了vm範例裡的資料

image.png

image.png

訂閱釋出者模式

雖然進行了資料劫持,和將資料應用到頁面上,但是資料發生變動還不能及時更新,還需要實現訂閱釋出者模式

首先建立一個類用來收集和通知訂閱者,生成範例的時候需要有一個陣列存放訂閱者的資訊,一個將訂閱者新增到這個陣列裡的方法和一個通知訂閱者的方法,呼叫這個方法就回去遍歷訂閱者的陣列,讓訂閱者呼叫自身的update方法進行更新

class Dependency{
  constructor(){
    this.subscribers=[] //存放訂閱者的資訊
  }
  addSub(sub){
    this.subscribers.push(sub) //將訂閱者新增到這個陣列裡
  }
  notify(){
    this.subscribers.forEach(sub=>sub.update()) //遍歷訂閱者的陣列,呼叫自身的update函數進行更新
  }
}
登入後複製

設定訂閱者類,需要用到Vue範例上的屬性,需要Vue範例和Vue範例對應的屬性和一個回撥函數作為引數,將引數都賦值給範例

然後就可以建立訂閱者的update函數,在函數裡呼叫傳進來的回撥函數

class Watcher{
  constructor(vm,key,callback){//將引數都賦值給Watcher範例
    this.vm=vm
    this.key=key
    this.callback=callback
  }
  update(){
    this.callback() 
  }
}
登入後複製

替換檔案碎片內容的時候需要告訴訂閱者如何更新,所以訂閱者範例在模板解析把節點值替換內容的時候建立,傳入vm範例,exec匹配成功後的索引值1和回撥函數,將替換文字的執行語句複製到回撥函數裡,通知訂閱者更新的時候就呼叫這個回撥函數

回撥函數裡的nodeValue要提前儲存,不然替換的內容就不是插值表示式而是替換過的內容

1669085039395.png

然後就要想辦法將訂閱者儲存到Dependency範例的陣列裡,我們可以在構造Watcher範例的時候儲存範例到訂閱者陣列裡

Dependency.temp=this //設定一個臨時屬性temp
登入後複製

將新的訂閱者新增到訂閱者陣列裡且還要將所有的訂閱者都進行同樣的操作,那麼就可以在觸發get的時候將訂閱者新增到訂閱者陣列裡,為了正確觸發對應的屬性get,需要用reduce方法對key進行同樣的操作

32022edb035588799072d6ce8c9cf04.png

b6b1eb7db9110d36607b58ebcef28e4.png

可以看到控制檯列印出了Wathcer範例,每個範例都不同,都對應不同的屬性值

image.png

Dependency類還沒建立範例,裡面的訂閱者陣列是不存在的,所以要先建立範例再將訂閱者新增到訂閱者陣列裡

1669086610689.png

1669086716726.png

修改資料的時候通知訂閱者來進行更新,在set裡呼叫dependency的通知方法,通知方法就會去遍陣列,訂閱者執行自己的update方法進行資料更新

1669087112449.png

但是update呼叫回撥函數缺少設定形參,依舊使用split和reduce方法獲取屬性值

update(){
    const value =this.key.split('.').reduce(
        (total,current)=>total[current],this.vm.$data
    )
    this.callback(value)
}
登入後複製

在控制檯修改屬性值都修改成功了,頁面也自動更新了

6.gif

完成了文字的繫結就可以繫結輸入框了,在vue裡通過v-model進行繫結,因此要判斷哪個節點有v-model,元素節點的型別是1,可以使用nodeName來匹配input元素,直接在判斷文位元組點下面進行新的判斷

if(node.nodeType===1&&node.nodeName==='INPUT'){
    const attr=Array.from(node.attributes)
    console.log(attr);
}
登入後複製

節點名字nodeName為v-model,nodeValue為name,就是資料裡的屬性名

1669087923831.png

因此對這個陣列進行遍歷,匹配到了v-model根據nodeValue找到對應的屬性值,把屬性值賦值到節點上,同時為了在資料更新後訂閱者知道更新自己,也要在INPUT節點裡新增Watcher範例

attr.forEach(i=>{
    if(i.nodeName==='v-model'){
        const value=i.nodeValue.split('.').reduce(
            (total,current)=>total[current],vm.$data
        )
        node.value=value
        new Watcher(vm,i.nodeValue,newValue=>{
            node.value=newValue
        })
    }
})
登入後複製

修改屬性值,頁面也作出修改

7.gif

最後剩下用檢視改變資料,在v-model的節點上使用addEventListener增加input監聽事件就行了

node.addEventListener('input',e=>{
    const arr1=i.nodeValue.split('.')
    const arr2=arr1.slice(0,arr1.length - 1)
    const final=arr2.reduce(
        (total,current)=>total[current],vm.$data
    )
    final[arr1[arr1.length - 1]]=e.target.value
})
登入後複製

7.gif

(學習視訊分享:、)

以上就是聊聊Vue中如何實現資料雙向繫結的詳細內容,更多請關注TW511.COM其它相關文章!