vue2之響應式雙向綁定,在對象或數組新增屬性頁面無響應的情況

vue2響應式雙向綁定,在對象或數組新增屬性頁面無響應

問題描述

vue2 中可以將數據與視圖進行綁定,修改 data 對象的屬性值將引起對應視圖的改變。

Vue2的數據視圖綁定是通過JS特性這一語法實現,其使用中存在數據屬性丟失的這 一 bug,主要針對 對象或數組 屬性丟失。

使用 antv a-select (下拉框)組件,使用 v-mode 綁定對象 的值,但是 對象之前是沒有賦值的,是一個 空對象 ,這就導致瞭 頁面視圖不刷新 ,但是 屬性值有變化 ,這可以說是 vue2 的一個缺陷。

// 空對象

queryParam: {

      },

解決方法

方法一 (設置初始值)

給 綁定的對象 賦初始值 null 或者 ' ' 

  queryParam: {
        approveStatus : null ,    
      },

 如果是普通 輸入框 input ,這樣的方法沒什麼問題,但是 我的頁面使用 a-select 下拉框,默認有提示消息( placeholder),如果 賦初始值 為空 ,下拉框會填充空白內容 ,覆蓋之前的提示消息 ,這樣的頁面 會比較不美觀且不友好

  <a-select  v-model="queryParam.approveStatus" placeholder="審核狀態" :allowClear="true">
        <a-select-option v-for="status of videoApproveStatus" :key="status.id">
                 {{  status.text }}
        </a-select-option>
 </a-select>
                    
  <a-select  v-model="queryParam.videotypeid" placeholder="視頻類型" :allowClear="true">
         <a-select-option v-for="d in videotype" :key="d.myid">
                 {{ d.name }}
          </a-select-option>
   </a-select>          

       

雖然這種方法可以解決綁定對象屬性丟失問題,但是如果給 每個屬性設置 初始值 為 null,那麼所有的下拉框都是空白 , 可以看到 下拉框 賦初始值 為 null ,頁面的效果非常不友好 ,沒有提示信息 ,所有這種方法不推薦

方法二 (創建一個新的對象,替換原對象)

這種方法可以用於需要添加多個新屬性,再把原對象與新屬性合並到新對象中

Object.assign(目標對象,原對象, 新屬性)

this.queryParam = Object.assign({}, this.queryParam, obj)

我這裡是 利用 a-select 下拉框 自帶的 下拉框 改變方法 ,@change ,該方法有兩個參數 value 和 option ,value 代表你改變的值 ,option (我也不太明白 ,反正裡面東西很多),然後 我利用 這個 @change 方法 和  Object.assign(目標對象,原對象, 新屬性), 解決瞭狀態丟失問題 ,大傢可以 參考下面的代碼 ,根據自己的實際情況進行調整 (每個人的情況都不一樣)

 <a-select @change="handleChange"  v-model="queryParam.approveStatus" placeholder="審核狀態" :allowClear="true">
       <a-select-option v-for="status of videoApproveStatus" :key="status.id">
               {{  status.text }}
       </a-select-option>
 </a-select>
 handleChange(value,option) { 
      if(option) {
        //  解決雙向綁定狀態丟失
          this.queryParam = Object.assign({}, this.queryParam, option.context.queryParam)
      } else {
        return 
      } 
    },

vue2實現響應式數據

JS中的對象屬性

JS的對象有兩種屬性進行描述 分別是數據屬性和訪問器屬性。

數據屬性有四個值:

  • [[Configurable]] 表示是否可以刪除定義,是否可以修改屬性,默認為true
  • [[Enumberable]] 是否可以迭代
  • [[Writable]] 是否可以被修改
  • [[Value]] 對象具體的值

而訪問器屬性也有四個值:

  • [[Configurable]] 表示是否可以刪除定義,是否可以修改屬性,默認為true
  • [[Enumberable]] 是否可以迭代
  • [[Get]] 獲取函數,讀取屬性值時使用
  • [[Set]] 設置函數,寫入屬性時調用

那麼如何實現數據響應呢?

利用Object.defineProperty()進行數據劫持

實現響應式的前提是可以捕捉到到數據的更改,獲取數據同理,這就需要利用JS對象的訪問器屬性,而更改這些屬性 就要用到JS中的一個方法 Object.defineProperty() 上述的屬性在更改時,哪怕更改一個屬性,所有屬性都會變為默認值。

具體使用方法如下:

let obj = {name:'Ton', age: 19}
_value = obj.name
Object.defineProperty(obj, name, {
    get(){
        return _value
    },
    set(newValue){
       _value = newValue
    }
})

方法的參數分別是 Object.defineProperty(對象名, 屬性名, {執行器})

而get 函數在讀取該對象屬性時調用,返回的值為讀取的值; set 函數會在設置新值時調用 傳入的newValue為新值。

在Vue中 會用data一個對象包裹所有的值,因此可以用遍歷的方法給每個屬性加上該方法。

將該邏輯封裝到一個函數中:

let data = {
    name: 'Ton',
    age: 19,
    salary: '10k'
}

Object.keys(data).forEach( key => { 
    observe(data, key, data[key])
})
//形成閉包 內部的變量不會消失
function observe(obj, key, value){
    Object.defineProperty(obj, key, {
        get(){
            return value
        },
        set(newValue){
            value = newValue
        }
    })
}

這樣data中的所有變量都會被綁定,但如果嵌套對象或數組,內部的對象不會被檢測到。(可以用vue提供的 $set和 $delate 處理或 使用內部重寫的數組方法)

與標簽聯動

與標簽聯動的方法有許多,最簡單的是使用id 來綁定,而Vue中是使用指令的方式。

過程主要分兩步:

  • 獲取dom
  • 將數據放上去
...
<div id="app">
    <div v-test="name" class="box"></div>
    <div v-test="age" class="box"></div>
</div>
...
function compile(){
    let app = document.getElementById('app')
    //獲取所有的子節點 值為3的是text節點 1為子標簽節點
    app.childNodes.forEach( node => {
        if(node.nodeType === 1){
            //遍歷該節點的屬性 找出 v-text 
            //node.attributes是個類數組對象, 先轉化為數組
            Array.from(node.attributes).forEach( key => {
                //解構對象 nodeName 找到屬性
                let { nodeName, nodeValue } = key 
                 //如果存在 則修改值(關鍵步驟)
                if(nodeName === 'v-test'){
                    node.innerText = data[nodeValue]
                   }
            })
        }
    })
}
compile()
...

此時可以獲取節點,並賦值數據,與數據劫持聯動,最終結果如下:

let data = { name: 'Ton', age: 19}
//劫持數據
Object.keys(data).forEach(key => {
    observe(data, key, data[key])
})
//形成閉包 內部的變量不會消失
function observe(obj, key, value) {
    Object.defineProperty(obj, key, {
        get() {
            return value 
        },
        set(newValue) {
            value = newValue
            compile()
        }
    })
}
//獲取元素 將數據放入
function compile(){
    let app = document.getElementById('app')
    //獲取所有的子節點 包括很多節點 3 為text 節點 1為子標簽節點
    app.childNodes.forEach( node => {
        if(node.nodeType === 1){
            //遍歷該節點的屬性 找出 v-text 
            //node.attributes是個類數組對象, 先轉化為數組
            let result = Array.from(node.attributes).filter( key => {
                //結構屬性的 nodeName 找到屬性
                let { nodeName } = key 
                return nodeName === 'v-test'
            })
            if(result){
                node.innerText = data[result[0].nodeValue]
            }
        }
    })

}
compile()

每次修改也會引起DOM元素的修改,實現響應式。

v-model的實現

同v-test(v-on) 的不同,v-model要實現雙向綁定,即 input框輸入的值也需要傳回data。實現的邏輯前面是相同的,都需要獲取元素,但需要新增將input輸入框的內容,傳回。

let data = { name: 'Ton', age: 19}
//劫持數據
Object.keys(data).forEach(key => {
    observe(data, key, data[key])
})
//形成閉包 內部的變量不會消失
function observe(obj, key, value) {
    Object.defineProperty(obj, key, {
        get() {
            return value 
        },
        set(newValue) {
            value = newValue
            compile()
        }
    })
}
//獲取元素 將數據放入
function compile(){
    let app = document.getElementById('app')
    //獲取所有的子節點 包括很多節點 3 為text 節點 1為子標簽節點
    app.childNodes.forEach( node => {
        if(node.nodeType === 1){
            //遍歷該節點的屬性 找出 v-text 
            //node.attributes是個類數組對象, 先轉化為數組
            let result = Array.from(node.attributes).filter( key => {
                //結構屬性的 nodeName 找到屬性
                let { nodeName } = key 
                return nodeName === 'v-model'
            })
            if(result){
                node.value = data[result[0].nodeValue]
                addEventLisener('input', e => {
                        data[result[0].nodevalue] = e.target.value 
                    }
                )
            }
        }
    })

}
compile()

但目前的方法並不完美,需要添加一個防抖函數

let data = { name: 'Ton', age: 19}
//劫持數據
Object.keys(data).forEach(key => {
    observe(data, key, data[key])
})
//形成閉包 內部的變量不會消失
function observe(obj, key, value) {
    Object.defineProperty(obj, key, {
        get() {
            return value 
        },
        set(newValue) {
            value = newValue
            compile()
        }
    })
}
//獲取元素 將數據放入
function compile(){
    let app = document.getElementById('app')
    //獲取所有的子節點 包括很多節點 3 為text 節點 1為子標簽節點
    app.childNodes.forEach( node => {
        if(node.nodeType === 1){
            //遍歷該節點的屬性 找出 v-text 
            //node.attributes是個類數組對象, 先轉化為數組
            let result = Array.from(node.attributes).filter( key => {
                //結構屬性的 nodeName 找到屬性
                let { nodeName } = key 
                return nodeName === 'v-model'
            })
            if(result){
                node.value = data[result[0].nodeValue]
                addEventLisener('input', debounce(handel, result[0].nodeValue)
            }
        }
    })
    function debounce(fn, key, timer = 1000){
        let t = null
        return function(){
            if(t) { clearTimeout(t) }
            t= setTimeOut( _ => {
                t = null
                fn.call(this, key, arguments)
            },timer)
        }
    }
    function handel(key, event){
        data[key] = event.target.value
    }
}
compile()

總結

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: