Vue 2.x 和 3.0 實現雙向綁定的原理(Object.defineProperty 以及 Proxy)

說明

vue實現雙向綁定原理,主要是利用Object.defineProperty 來給實例data的屬性添加 setter和getter.
並通過發佈訂閱模式(一對多的依賴關係,當狀態發生改變,它的所有依賴都將被通知)來實現響應。

這個環節中包含了三個部分

  • Observer 用來監聽攔截data的屬性爲監察者。

  • Dep用來添加訂閱者,爲訂閱器

  • Watcher 就是訂閱者

監察者通過 Dep 向 Watcher發佈更新消息

簡單實現

那麼首先

  1. 通過對set和get的攔截,在get階段進行依賴收集,在set階段對通知該屬性上所啊綁定的依賴。

如下我們就已經實現了一個簡單的雙向綁定了。

我們將data的value屬性綁定上set和get,通過 _value 來進行操作。

<!-- HTML部分 -->

<input type="text" id="inp" oninput="inputFn(this.value)">
<div id='div'></div>
<!-- JS部分 -->
var inp = document.getElementById('inp');
var div = document.getElementById('div');
var data = {
    value:''
}
  Object.defineProperty(data, 'value', {
    enumerable: true,
    configurable: true,
    set: function (newValue) {
        this._value = newValue; 
        div.innerText = data._value = value; //watcher
    },
    get: function () {
        return this._value; 
    }
})
function inputFn(value) {
  data._value = value;
}

如果只是實現一個簡單的雙向綁定那麼上面的代碼就已經實現了。

進一步完善模擬vue實現

首先我們將watcher抽出來 備用

  function watcher(params) {
    div.innerText = inp.value = params; // 派發watcher
  }

聲明一個vm來模擬vue的實例,並初始化。

    var vm = {

        //類似vue實例上的data
        data: {
            value: ''
        }, 

        // vue私有, _data的所有屬性爲data中的所有屬性被改造爲 getter/setter 之後的。
        _data: {
            value: ''
        }, 

        // 代理到vm對象上,可以實現vm.value
        value: '', 

        //value的訂閱器用來收集訂閱者 
        valueWatchers:[] 
    }

遍歷其data上的屬性 進行改造 這裏我們還是隻舉一個例子

  // 利用 Object.defineProperty 定義一個屬性 (eg:value) 描述符爲存取描述符的屬性
  Object.defineProperty(vm._data, 'value', {
      enumerable: true, //是否可枚舉
      configurable: true, //是否可配置
      set: function (newValue) { //set 派發watchers
        vm.data.value = newValue; 
        vm.valueWatchers.map(fn => fn(newValue));
      },
      get: function () { 
          
          // 收集wachter vue中會在compile解析器中通過 顯示調用 (this.xxx) 來觸發get進行收集
          vm.valueWatchers.length = 0; 
          vm.valueWatchers.push(watcher); 
          return vm.data.value; 
      }
  })

    <!--直接通過顯示調用來觸發get進行綁定 vue中是在compile解析器中來進行這一步-->
    vm._data.value 

進行到這兒也已經實現了綁定,但是我們平時使用vue ,都是可以直接通過 this.xxx來獲取和定義數據

那麼我們還需要進行一步Proxy 代理


  Object.defineProperty(vm, 'value', {
      enumerable: true,
      configurable: true,
      set: function (newValue) {
          this._data.value = newValue; //藉助
      },
      get: function () {
          return this._data.value; 
      }
  })

這樣我們就把vm._data.value 代理到vm.value上了,可以通過其直接操作了。

那麼按照官方的寫法


  function proxy (target, sourceKey, key) {
      Object.defineProperty(target, key, {
          enumerable: true,
          configurable: true,
          get() {
              return this[sourceKey][key];
          },
          set(val) {
              this[sourceKey][key] = val;
          }
      });
  }
    
  proxy(vm, '_data', 'value');

完善後的完整代碼

以下爲整個頁面,可以直接運行


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>雙向綁定簡單實現</title>
</head>
<body>
<input type="text" id="inp" oninput="inputFn(this.value)">
<br>
<input type="text" id="inp2" oninput="inputFn(this.value)">
<div id='div'></div>
<script>
    var inp = document.getElementById('inp');
    var inp2 = document.getElementById('inp2');
    var div = document.getElementById('div');

    
    function inputFn(value) {
        div.innerText = vm.value = value;
    }

    function watcher(params) {
        console.log(1)
        div.innerText = inp.value = params; // 派發watcher
    }

    function watcher2(params) {
        console.log(2)

        div.innerText = inp2.value = params; // 派發watcher
    }

    function proxy (target, sourceKey, key) {
        Object.defineProperty(target, key, {
            enumerable: true,
            configurable: true,
            get() {
                return this[sourceKey][key];
            },
            set(val) {
                this[sourceKey][key] = val;
            }
        });
    }

	let handler = {
        enumerable: true,
        configurable: true,
        set: function (newValue) {
            vm.data.value = newValue; 
            vm.valueWatchers.map(fn => fn(newValue));
        },
        get: function () {
            vm.valueWatchers = []; //防止重複添加, 
            vm.valueWatchers.push(watcher); 
            vm.valueWatchers.push(watcher2); 
            return vm.data.value; 
        }
    }

    var vm = {
        data: {},
        _data: {},
        value: '', 
        valueWatchers: [] 
    }
    
    Object.defineProperty(vm._data, 'value', handler)

    proxy(vm, '_data', 'value');

    vm.value;  //顯示調用綁定

</script>
</body>
</html>

解釋

再多講一點。實際上vue在初始化的時候是用解析器解析過程中將wathcer進行綁定的。

它會利用一個全局的Dep.target = watcher

然後在get收集中,只收集全局上Dep.target, 添加完畢後會重新初始化全局Dep.target = null;

類似如下操作


    Dep.target = watcher;
    vm.value;    // 觸發get => Dep.target && valueWatchers.push(Dep.target);
    Dep.target = null;


這樣也會防止我們在調用時觸發get重複去添加watcher。

而我們的例子中只是每次都初始化爲[]. 實際訂閱器也不只是一個watcher數組。

此例跟官方實現還是有很多差距,只是簡單模擬。

vue3.0 使用 Proxy

在vue3.0中,使用proxy這個功能更加強大的函數,它可以定義對象的基本操作的自定義行爲。對比defineProperty只能攔截對象的某一屬性,proxy的功能更方便。所提供的可自定義的操作也更多。

上面,我用defineProperty實現了vue的雙向綁定,接下來我們用proxy來實現。

首先我們可以先了解一下proxy的作用和用法

首先 defineProperty 的用法是Object.defineProperty(obj, prop, descriptor)

proxy的用法如下:

const p = new Proxy(target, handler)

我們用proxy來實現一下雙向綁定:

核心代碼就像這樣,在我們這個需求下分析

  1. set函數中
    1. target 爲所攔截的對象
    2. key 爲屬性名
    3. newValue爲所賦予的值
    4. set中需要return true代表設置成功,返回flase在嚴格模式下報TypeError (代表該值與期望值類型不同)
  2. get函數中
    1. target 爲所攔截的對象
    2. key 爲屬性名
    3. get可返回任意值
let data = {value: 0}
const vm = new Proxy({value: 0 }, {
	set: function(target, key, newValue){
		console.log(key + '被賦值爲' + newValue)
		target[key] = newValue
		return true
	}get: function(target, key) {
		console.log(target[key])
        return target[key]
    }
})

vm.value = 1 // 0; value被賦值爲1

proxy雙向綁定具體實現

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1 id="content"></h1>
    <p><input type="text" id="enter" value=""></p>
</body>
<script>
    let content = document.getElementById("content")
    let enter_input = document.getElementById('enter')

    let data = {
        enter_input: '',
        enter_input_watchers: []
    }
    let watcher = function watcherFn(value) {
        content.innerText = value
    }
    let watcher2 = function watcher2Fn(value) {
        enter_input.value = value
    }
    let handler = {
        set: function(target, key, value) {
            if (key === 'enter_input') {
                target[key] = value;
                target[key + "_watchers"].map(function (watcher) {
                    watcher(value)
                })
            }
        },
        get: function(target, key) {
            target[key + "_watchers"] = [watcher, watcher2];
            return target[key]
        }
    }

    let db = new Proxy(data, handler);
    db.enter_input; //收集監聽
    enter_input.addEventListener('input', function(e){
        db.enter_input = e.target.value;
    })
</script>
</html>

雙向綁定總結

  1. vue2.x版本使用了Object.defineProperty來實現雙向綁定,由於其功能的限制,只能綁定對象的某個屬性,vue需要遞歸遍歷對象的所有屬性挨個進行綁定,功能上不是很完美。
  2. vue3.0版本使用proxy進行雙向綁定,proxy提供了可以定義對象基本操作的自定義行爲的功能(如屬性查找、賦值、枚舉、函數調用),可以直接攔截整個對象,不需要再進行遞歸。本例中我們只使用到了proxy提供自定義setget的能力。

錯誤類型擴展

平時我們常見的錯誤類型分爲ReferenceErrorTypeErrorSyntaxError 這三種。

一、 ReferenceError 代表我們的作用域查找錯誤。
let b = 1;
console.log(b)
console.log(a) //ReferenceError
  1. 我們在全局定義了b,所以console.log(b)爲1,但是我們沒有定義a,所以我們在全局作用域下找不到a,就會報ReferenceError

  2. 如果是在函數中定義,則在函數中查找不到時,會去父作用域查找,一直到全局,都找不到,纔會報ReferenceError

二、 TypeError代表數據類型與預期不符。
let b = 1;
b() //TypeError
  1. 我們在全局定義了b,其類型爲Number,但是我們用()來執行它,把它當作了函數用,所以就會報TypeError
三、 SyntaxError代表語法錯誤。
let b > 1;//SyntaxError
//or
let let b//SyntaxError
  1. 很明顯,我們不可以這麼使用let,語法就錯誤了,所以就會報SyntaxError
錯誤類型總結

ReferenceErrorTypeErrorSyntaxError 分別代表作用域,預期類型,語法錯誤。
我們其實經常看到這幾種錯誤,但是平時不會太注意這個錯誤類型,只關注在哪錯誤了。倘若我們知道了這幾種錯誤類型代表的涵義,對我們排除錯誤也是非常有幫助的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章