Vue2. X

First, theoretical knowledge

Vue responsive data principle:

Vue internally converts every data read and write in the target Object into a getter/setter by intercepting the Object. DefineProperty method attribute, notifying the view of updates when data changes.

Two, the basic process

The following is a simple flowchart for implementing a responsive system similar to VUE responsive data binding

From the diagram above, the relationship between the Observer, subscriber and Watcher may seem a little abstract to those who have not studied design patterns. Let’s use a simple example to describe their relationship

Xiao Ming to buy A house, so he added A sales department salesman Xiao Hong’s wechat, Xiao Hong has A lot of information about housing, Xiao Ming wants A flat just no, please xiao Hong pay attention to the latest news, once this flat and immediately notify Xiao Ming, Xiao Ming will immediately pay to buy this flat. Many of Xiao Hong’s customers actually want to buy this apartment type A. In order to conveniently notify these prospective buyers, Xiao Hong records their wechat accounts in A small book and associate them with apartment type A.

In the above information, we remove The person Xiao Ming as A subscriber instance, the apartment type A information in Xiao Hong’s hands is the data, Xiao Hong is the listener instance, and the small book that records the information of the intended buyers of apartment type A is the Dep instance. When the apartment type A began to sell, red quickly notify the people on the small book.

Here is a small example, which may not be appropriate, but look at the publish-subscribe model for details

Next, we implement a responsive system bit by bit

1. Implement a listener Observer

Here we will implement data hijacking with the Object.defineProperty method, which means we can sense the read and write of the data Object and make the data Object observable.

  • About ObjectdefineProperty

I will not expand into the details of how to use MDN. Note the use of property descriptors and access descriptors

Next is coding time:

Basic implementation
  • The defineReactive() method implements data interception for attributes
function defineReactive(obj, key, value) { Object.defineProperty(obj, key, { get: Function getter() {console.log(" get ") return value; }, set: function setter(newValue) { if (newValue === value) return; Value = newValue console.log(" Settings ")}})}Copy the code

The above functions implement data listening by setting setter and getter methods

  • The Observer class implements traversal interception of object attributes
class Observer {
  constructor(obj) {
    this.value = obj
    this.trave()
  }
  trave() {
    Object.keys(this.value).forEach((key) => defineReactive(this.value, key,this.value[key]))
  }
}

Copy the code

The above method traverses all attributes of the Object through object. keys, so as to achieve data interception for each attribute

Optimization of ascension

Through the above code we can basically achieve the data hijacking of all attributes of an object OBJ, but there is a small problem, if the object property is an object, that is, obJ has nested attributes, so we can iterate, so the code is optimized as

class Observer { constructor(obj) { this.value = obj this.trave() } trave() { Object.keys(this.value).forEach((key) => defineReactive(this.value, key,this.value[key])) } } function observe(obj){ if(! obj||typeof obj! =='object'){ return; } // Instantiate new Observer(obj); } function defineReactive(obj, key, value) {// Observe (value) object.defineProperty (obj, key, {get: Function getter() {console.log(" get ") return value; }, set: function setter(newValue) { if (newValue === value) return; Observe (newValue) console.log(" set ")}})}Copy the code

The observe function is used to determine whether the object has nested attributes. If so, the object is recursively traversed to achieve data hijacking for each attribute

2. Realize Watcher as a subscriber

What does Watcher need to do first?

  1. When you instantiate a subscriber, it means you need to get the properties to which it subscribed
  2. When the subscriber receives a change in the data, it needs to invoke the corresponding callback function

Next coding time:

Basic implementation
class Watcher{ constructor(obj,exp,cb){ this.obj = obj; //exp is a string, like 'person.name'. This. exp = exp; this.cb = cb; Get (){var value = getValue(this.obj,this.exp); return value; } update(){var newVal = getValue(this.obj,this.exp); var oldVal = this.value; if(newVal! ==oldVal){this.value = newVal; // Execute the callback this.cb(); } } } function getValue(obj,exp) { var keys= exp.split("."); var value; for(let i of keys){ if(! obj) return; value = obj[i]; } return value; }Copy the code

There is a lot to be done to optimize the Watcher instance. After Dep the subscriber, we will talk about optimizing Watcher

3. Implement subscriber Dep

Let’s start by looking at what it needs to do.

  1. Each attribute corresponds to a DEP instance that stores every Watcher subscribed to that attribute
  2. When data changes, the corresponding DEP instance goes to notify each watcher in the store
Basic implementation
class Dep{ constructor(){ this.subs = []; } // Add Watcher instance to subs array addSub(sub){this.subs.push(sub); } // Send a message to notify(){this.subs.foreach (function (sub) {sub.update(); }}})Copy the code

The above just implements a Dep class with methods for adding and notifying Watcher instances

Optimization of ascension

Through the above coding, we have only implemented the basic classes, but the connection between them has not been implemented, specifically the following functions:

  1. When a subscriber is added to a property, and how is the corresponding DEP instance notified when the property changes

By changing the data hijacking function, the corresponding DEP instance is added as each attribute is traversed.

When setter methods change property values, they need to notify the DEP instance

Function defineReactive(obj, key, value) {var dep = new dep (); observe(value) Object.defineProperty(obj, key, { get: function getter() { return value; }, set: function setter(newValue) { if (newValue === value) return; value = newValue; observe(newValue); // Send message dep.notify(); }})}Copy the code
  1. How to instantiate watcher instance and add it to the deP instance of the corresponding property?

When instantiating a Watcher to get the corresponding property value, the getter method of defineReactive also fires, so add the Watcher instance to the corresponding DEP array in the getter method.

  1. How to get watcher instance from getter method?

Obviously, just put the watcher instance where the getter can access it — in this case, we put it on the dep.target property.

  • Modify the Watcher
Get (){dep. target = this var value = getValue(this.obj,this.exp); Target dep. target = null; return value; }Copy the code
  • Modify the defineReactive function
If (dep.target){dep.addSub(dep.target); if(dep.target){dep.addSub(dep.target); return value; }}Copy the code

Complete code

class Observer { constructor(obj) { this.value = obj this.trave() } trave() { Object.keys(this.value).forEach((key) => defineReactive(this.value, key,this.value[key])) } } function observe(obj){ if(! obj||typeof obj! =='object'){ return; } new Observer(obj); } function defineReactive(obj, key, value) {observe(value) // instantiate dep var dep = new dep (); Object.defineProperty(obj, key, { get: function getter() { if(Dep.target){ dep.addSub(Dep.target); } return value; }, set: function setter(newValue) { if (newValue === value) return; value = newValue observe(newValue); dep.notify() } }) } //watcher class Watcher{ constructor(obj,exp,cb){ this.obj = obj; this.exp = exp; this.cb = cb; This.value = this.get()} // Subscribe attribute get(){dep.target = this; var value = getValue(this.obj,this.exp); Dep.target = null return value; } update(){var newVal = getValue(this.obj,this.exp); var oldVal = this.value; if(newVal! ==oldVal){this.value = newVal; this.cb(newVal,oldVal); } } } function getValue(obj,exp) { var keys= exp.split("."); console.log(keys) var value; for(let i of keys){ if(! obj) return; value = obj[i]; } return value; } class Dep{ constructor(){ this.subs = []; } // Add Watcher instance to subs array addSub(sub){this.subs.push(sub); } // Send a message to notify(){this.subs.foreach (function (sub) {sub.update(); }}})Copy the code

The above is a simple version of the data response, the specific two-way data binding requires the compile parser

Vue3. X

First, theoretical knowledge

Vue3. X uses Proxy instead of Object.defineProperty. Because proxies can listen directly for changes in objects and arrays, there are as many as 13 interception methods. And as a new standard, browser vendors will continue to focus on performance optimization.

I won’t go into details about Proxy, but look directly at MDN or Teacher Ruan Yifeng’s documents

Reactive processing in VUe3 is in the Reactive class, so the next step is to emulate vue3 for reactive processing of data

Two, the basic process

Based on the power of Proxy, the following code mainly reflects the encapsulation of the code

import {reactive} from './vue3/reactivity';
const state = reactive({
  name:'beatrix',
  age:'21',
  hobby:{
    "a":1,
    "b":2
  }
})
Copy the code

Obviously, the next step is to implement a reactive method so that objects we pass in can be listened to

1. Implement reactive

import {mutableHandler} from './mutableHandler' function reactive(target) { return createReactiveObject(target,mutableHandler); } function createReactiveObject(target,baseHandler) { if(! isObject(target)){ return target } const observer = new Proxy(target,baseHandler); // Return observer; } function isObject(obj){ return typeof obj ==='object' && obj! == null } export{ reactive }Copy the code

The main thing to note about this code is

  • mutableHandler

It is the behavior of a newly created object that we delegate to perform various operations, with many functions as properties

2. Implement mutableHandler. Js

import { reactive } from "./reactive" function hasOwnProperty(target,key){ return Object.prototype.hasOwnProperty.call(target,key) } function isEqual(newVal,oldVal) { return newVal === oldVal } const get = createGetter(), set = createSetter(); function createGetter () { return function get (target, key, receiver) { const res = Reflect.get(target, key, receiver); // todo ...... Console. log(' reactive fetch: '+ target[key]); If (isObject(res)) {return reactive(res); } return res; } } function createSetter () { return function set (target, key, value, receiver) { const isKeyExist = hasOwnProperty(target, key), oldValue = target[key], res = Reflect.set(target, key, value, receiver); // todo ...... if (! IsKeyExist) {console.log(' responsive new: '+ value); } else if (! IsEqual (value, oldValue)) {console.log(' responsive: '+ key + '=' + value); } return res; } } const mutableHandler = { get, set } export { mutableHandler }Copy the code

The main focus of the above is to use Reflect to implement the operation on the property.

  • Reflect

For Reflect, look directly at MDN

conclusion

The principle of vue2 2/3 responsive data is only briefly implemented here.

For example, Vue3 reactive is not that simple. It is only implemented a little bit. If you are interested, you can check the source code.