instructions

Vue implements the principle of bidirectional binding by adding setters and getters to attributes of instance data using Object.defineProperty. The response is implemented through a publish-subscribe pattern (a one-to-many dependency where all of its dependencies are notified when the state changes).

There are three parts to this process

  • The property the Observer uses to listen for intercepting data is the monitor.

  • Dep is used to add subscribers, which are subscribers

  • Watcher is the subscriber

The watchdog sends updates to Watcher through the Dep

Simple implementation

So, first of all,

  1. Dependencies are collected in the GET phase by intercepting set and GET, and dependencies on the binding on the property are notified in the SET phase.

Here we have a simple bidirectional binding.

We bind the value attribute of data to set and GET, and operate by _value.

<! -- HTML section -->

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

Copy the code

The above code would have been implemented if it had just implemented a simple two-way binding.

Further improve the simulation VUE implementation

First we pull out the Watcher

  function watcher(params) {
    div.innerText = inp.value = params; / / distributing watcher
  }
Copy the code

Declare a VM to simulate an instance of vUE and initialize it.

    var vm = {

        // Similar to data on vue instance
        data: {
            value: ' '
        }, 

        // Vue is private, all attributes of _data are after all attributes in data have been converted to getters/setters.
        _data: {
            value: ' '
        }, 

        // Delegate to a VM object to implement vm.value
        value: ' '.// The subscriber of value is used to collect subscribers
        valueWatchers:[] 
    }

Copy the code

We’re going to go through the properties on their data and we’re just going to do one example here

  DefineProperty defines an attribute (eg: value) descriptor as an attribute of the access descriptor
  Object.defineProperty(vm._data, 'value', {
      enumerable: true.// Whether enumerable
      configurable: true.// Whether it can be configured
      set: function (newValue) { / / set out watchers
        vm.data.value = newValue; 
        vm.valueWatchers.map(fn= > fn(newValue));
      },
      get: function () { 
          
          Wachter Vue collects by displaying a call (this.xxx) in the Compile parser that triggers get
          vm.valueWatchers.length = 0; 
          vm.valueWatchers.push(watcher); 
          returnvm.data.value; }}) <! Vue is used in the compile parser --> vm._data.valueCopy the code

The binding has been implemented up to this point, but when we use VUE, we can get and define data directly through this.xxx

So we need to do another step of Proxy Proxy


  Object.defineProperty(vm, 'value', {
      enumerable: true.configurable: true.set: function (newValue) {
          this._data.value = newValue; / / with the aid of
      },
      get: function () {
          return this._data.value; }})Copy the code

So we can delegate vm._data.value to vm.value and operate directly from it.

So the official way to write it is


  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');

Copy the code

Complete code after improvement

The following is the entire page, which can be run directly


<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Simple implementation of bidirectional binding</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; / / distributing watcher
    }

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

        div.innerText = inp2.value = params; / / distributing 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 = []; // To prevent repeated additions,
            vm.valueWatchers.push(watcher); 
            vm.valueWatchers.push(watcher2); 
            returnvm.data.value; }}var vm = {
        data: {},
        _data: {},
        value: ' '.valueWatchers: []}Object.defineProperty(vm._data, 'value', handler)

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

    vm.value;  // Display call binding

</script>
</body>
</html>
Copy the code

explain

A little bit more. Vue actually binds wathcer to the parser during initialization.

It makes use of a global dep.target = watcher

Target = null; Dep. Target = null;

Similar to the following


    Dep.target = watcher;
    vm.value;    // Trigger get => dep.target && valueWatchers. Push (dep.target);
    Dep.target = null;


Copy the code

This also prevents us from triggering get to add watcher repeatedly on the call.

In our example, it is only initialized to [] each time. The actual subscriber is not just a Watcher array.

This example still has a lot of gap with official implementation, just simple simulation.

Vue3.0 use Proxy

In Vue3.0, you use proxy, a more powerful function that defines custom behavior for basic operations on objects. Proxy is more convenient than defineProperty, which intercepts only one property of an object. There are also more customizable operations available.

Above, I implemented the two-way binding of VUE using defineProperty. Next, we implemented it using proxy.

First of all, we can understand the function and usage of proxy

First defineProperty is object.defineProperty (obj, prop, descriptor)

Proxy can be used as follows:

const p = new Proxy(target, handler)
Copy the code

Let’s use proxy to implement bidirectional binding:

The core code looks like this, analyzed under our requirements

  1. setIn the function
    1. targetIs the object that is intercepted
    2. keyFor the property name
    3. newValueIs the value assigned
    4. The set of needsreturn trueFlase returns TypeError in strict mode (indicating that the value is different from the expected type)
  2. getIn the function
    1. targetIs the object that is intercepted
    2. keyFor the property name
    3. Get can return any value
let data = {value: 0}
const vm = new Proxy({value: 0 }, {
	set: function(target, key, newValue){
		console.log(key + 'assigned to' + newValue)
		target[key] = newValue
		return true},get: function(target, key) {
		console.log(target[key])
        return target[key]
    }
})

vm.value = 1 / / 0; Value is assigned to 1
Copy the code

Proxy bidirectional binding concrete implementation

<! DOCTYPEhtml>
<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; // Collect listener
    enter_input.addEventListener('input'.function(e){
        db.enter_input = e.target.value;
    })
</script>
</html>
Copy the code

Summary of bidirectional binding

  1. The vue2. X version is usedObject.definePropertyTo achieve bidirectional binding, due to its functional limitations, can only bind a certain property of the object, vUE needsRecursively traverses all properties of an objectBinding one by one, functionally, is not perfect.
  2. Vue3.0 is usedproxyWith bidirectional binding, proxies provide the ability to define custom behavior for the basic operations of objects (e.gAttribute lookup and assignment, enumerations, function calls), can be directlyIntercepting the entire objectI don’t need to recurse anymore. In this example, we only use proxy to provide customizationsetandgetAbility.

Error type extension

At ordinary times, common error types are divided into ReferenceError, TypeError and SyntaxError.

A,ReferenceError represents our scoped lookup error.
let b = 1;
console.log(b)
console.log(a) //ReferenceError
Copy the code
  1. Log (b) is 1 because we defined b globally, but we didn’t define a, so we can’t find a in the global scope, so ReferenceError will be reported

  2. If it is defined in a function, if it cannot be found in the function, it will go to the parent scope to search, until the global, can not be found, and then ReferenceError will be reported

Second,TypeError indicates that the data type is not expected.
let b = 1;
b() //TypeError
Copy the code
  1. We defined it globallybIs of type Number, but we use () to execute it as a function, so it returnsTypeError
Three,SyntaxError stands for SyntaxError.
let b > 1;//SyntaxError
//or
let let b//SyntaxError
Copy the code
  1. It is obvious that we cannot use let this way, because the syntax is wrong, so we will declare itSyntaxError
Error type summary

ReferenceError, TypeError, and SyntaxError represent scope, expected type, and syntax errors, respectively. We see these errors all the time, but usually we don’t pay much attention to the type of error, just where the error is. If we know what these types of errors represent, it will be very helpful for us to eliminate errors.