When we use Vue, we just need to modify the data, the view will automatically update, this is the data response today to learn Vue implementation of data response principle ~
Source link gitee.com/ykang2020/v…
Object attribute – Attribute Type – Data attribute – Accessor attribute -Object.defineProperty() method -get method -set method
With getter and setter methods you can listen for data, access and Settings are captured by the listener and the getter is triggered when you read the data, and the setter is triggered when you modify the data
1. Define defineReactive
1.1 Why (temporary variable)
When we want to do data hijacking, the first thing to think about is adding getters and setters to properties in Object.defineProperty(), but there are problems with that
The defineProperty() method requires temporary global variable turnaround getters and setters
Let’s look at the following example
let obj = {};
let temp;
Object.defineProperty(obj, "a", {
get() {
console.log("Getter attempts to access a property of OBj.");
return temp;
},
set(newValue) {
console.log("Setter is trying to change the a property of obj.", newValue); temp = newValue; }});console.log(obj.a);
obj.a = 5
console.log(obj.a);
Copy the code
1.2 How (Closure)
So we define a function to encapsulate defineProperty to achieve data hijacking
You don’t need to set temporary variables to use defineReactive, you use closures instead
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
// Enumeration can be for-in
enumerable: true.// Can be configured, such as delete
configurable: true.// getter
get() {
console.log(Getter attempts to access obj's${key}Attribute `);
return value;
},
// setter
set(newValue) {
console.log(The setter is trying to change obj's${key}Attribute `, newValue);
if (value === newValue) return; value = newValue; }}); }let obj = {};
/ / initialization
defineReactive(obj, "a".10);
console.log(obj.a);
obj.a = 5;
console.log(obj.a);
Copy the code
2. Responsive processing of objects — Recursively detect all properties of objects
2.1 Why (nested)
The defineProperty() function defined above cannot listen for nested objects
That is, objects nested objects
function defineReactive(data, key, value) {
if (arguments.length === 2) {
value = data[key];
}
Object.defineProperty(data, key, {
// Enumeration can be for-in
enumerable: true.// Can be configured, such as delete
configurable: true.// getter
get() {
console.log(Getter attempts to access obj's${key}Attribute `);
return value;
},
// setter
set(newValue) {
console.log(The setter is trying to change obj's${key}Attribute `, newValue);
if (value === newValue) return; value = newValue; }}); }let obj = {
b: {
c: {
d: 4,}}};/ / initialization
defineReactive(obj, "b");
console.log(obj.b.c.d);
Copy the code
This shows no listening inside (obj.b.c.D)
2.2 How (Recursion)
So we create an Observer class — > to convert a normal object into an object whose properties at each level are responsive (detectable)
Traverse object
observe.js
If the value is already responsive data, you do not need to create an Observer instance. Instead, you can directly return the created Observer instance to avoid repeated value detection
import Observer from "./Observer";
/** * listen for value *@param {*} value
* @returns * /
export default function observe(value) {
// If value is not an object, do nothing
if (typeofvalue ! ="object") return;
let ob;
if (typeofvalue.__ob__ ! = ="undefined") {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}
Copy the code
Observer.js
Convert a normal object to an object whose properties at each level are responsive (detectable)
The Observer class is attached to each detected object. Once attached, the Observer converts all properties of the object into getters/setters
The function of __ob__ can be used to indicate whether the current value has been converted to responsive data by the Observer. An instance of the Observer can also be accessed via value.__ob__
import def from "./def";
import defineReactive from "./defineReactive";
/** * Convert a normal object to an object whose attributes at each level are responsive (detectable)
export default class Observer {
/ / the constructor
constructor(value) {
// Add an __ob__ attribute to the instance. The value is an instance of the current Observer, which is not enumerable
def(value, "__ob__".this.false);
console.log("Observer constructor", value);
// Convert a normal object to an object whose properties at each level are responsive (detectable)
this.walk(value);
}
// Iterate over each key of value
walk(value) {
for (let key invalue) { defineReactive(value, key); }}}Copy the code
def.js
The utility method defines an object property
/** * defines an object attribute *@param {*} obj
* @param {*} key
* @param {*} value
* @param {*} enumerable
*/
export default function def(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true.configurable: true}); }Copy the code
defineReactive.js
Define a listener for the key property of the object data
import observe from "./observe";
/** * define a listener for the key of the object data@param {*} Data Incoming data *@param {*} Key Listens to the property *@param {*} The turnover variable */ provided by the value closure environment
export default function defineReactive(data, key, value) {
console.log('defineReactive()', data,key,value)
if (arguments.length === 2) {
value = data[key];
}
// The child element must observe to recurse
let childOb = observe(value)
Object.defineProperty(data, key, {
// Enumeration can be for-in
enumerable: true.// Can be configured, such as delete
configurable: true.// getter
get() {
console.log('Getter attempts to access${key}Attribute `);
return value;
},
// setter
set(newValue) {
console.log(The setter is trying to change${key}Attribute `, newValue);
if (value === newValue) return;
value = newValue;
// When a new value is set, the new value must also be observed
childOb = observe(newValue)
},
});
}
Copy the code
Dependency structure between filesThe three functions call each other to form recursion
So, what if we nested an array, and we’re going to look at arrays
2.3 test
index.js
import observe from "./observe";
let obj = {
a: 1.b: {
c: {
d: 4,}}}; observe(obj); obj.a =5;
obj.b.c.d = 10;
Copy the code
3. Reactive processing of arrays
Because we can change the contents of an Array using the methods on the Array prototype, ojbect’s getter/setter approach doesn’t work.
ES6 did not previously provide the ability to intercept prototype methods, so we could override native prototype methods with custom methods.
Vue is responsive processing of an array by overwriting its seven methods (methods that change the contents of the array itself)
These methods are:push
,pop
,shift
,unshift
,splice
,sort
,reverse
These seven methods are all defined in Array.prototype to preserve method functionality while adding data hijacking code
The idea is to prototype array. prototype, create a new object arrayMthods and then define (rewrite) these methods on the new object arrayMthods that define the Array prototype pointing to arrayMthods
This is equivalent to overwriting array. prototype with an interceptor. Whenever a method on the Array prototype is used to manipulate an Array, the method provided in the interceptor is actually executed. Use the prototypical methods of native arrays in interceptors to manipulate arrays.
3.1 Prior knowledge
Object.setPrototypeOf()
Modify object prototype
The object.setProtoTypeof () method sets the Prototype of a specified Object (that is, the internal [[Prototype]] property) to another Object or null
Object.setPrototypeOf(obj, prototype)
Copy the code
Prototype Specifies the new prototype of the object (an object or null).
Object.create()
Create a new object
The object.create () method creates a new Object, using an existing Object to provide the __proto__ of the newly created Object
ObjectThe create (proto, [propertiesObject])Copy the code
Proto The prototype object of the newly created object. PropertiesObject optional. You need to pass in an Object whose property type refers to the second parameter of Object.defineProperties(). If this parameter is specified and not undefined, the incoming object’s own enumerable properties (that is, properties defined by itself, rather than enumerated properties in its stereotype chain) will add the specified property value and corresponding property descriptor to the newly created object.
Returns a new object with the specified prototype object and properties.
3.2 implementation
array.js
import def from "./def";
const arrayPrototype = Array.prototype;
// Create an arrayMethod based on array. prototype
export const arrayMethods = Object.create(arrayPrototype);
// The 7 array methods to be overwritten
const methodsNeedChange = [
"push"."pop"."shift"."unshift"."splice"."sort"."reverse",];// Execute these methods in batches
methodsNeedChange.forEach((methodName) = > {
// Back up the original method
const original = arrayPrototype[methodName];
// Define a new method
def(
arrayMethods,
methodName,
function () {
console.log("Array data has been hijacked.");
// Restore the original function (array method)
const result = original.apply(this.arguments);
// Turn an array-like object into an array
const args = [...arguments];
// Remove __ob__ from this array
// Get the Observer instance in the interceptor
const ob = this.__ob__;
// There are three methods: push, unshift, splice to insert new items, to hijack (detect) these data (insert new items)
let inserted = [];
switch (methodName) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
// Check to see if there is a new inserted item
if (inserted) {
ob.observeArray(inserted);
}
return result;
},
false
);
});
Copy the code
Observer.js
The function of __ob__ can be used to indicate whether the current value has been converted to responsive data by the Observer and to access an Observer instance
import def from "./def";
import defineReactive from "./defineReactive";
import observe from "./observe";
import {arrayMethods} from './array'
/** * An Object * Observer class that converts a normal object to one whose properties at each level are responsive is attached to each detected object. The Observer collects property dependencies by converting all properties of an object into getters/setters * and notifies those dependencies when properties change */
export default class Observer {
/ / the constructor
constructor(value) {
// Add an __ob__ attribute to the instance. The value is an instance of the current Observer, which is not enumerable
def(value, "__ob__".this.false);
// The function of __ob__ can be used to indicate whether the current value has been converted to responsive data by the Observer; An instance of the Observer can also be accessed via value.__ob__
// console.log("Observer constructor ", value);
// Check whether it is an array or an object
if (Array.isArray(value)) {
// arrayMethods is an array, and the prototype of that array points to arrayMethods
Object.setPrototypeOf(value, arrayMethods);
// The early implementation looked like this
// value.__proto__ = arrayMethods;
/ / observe the array
this.observeArray(value);
} else {
this.walk(value); }}// The object traverses each key of the value
walk(value) {
for (let key invalue) { defineReactive(value, key); }}// Array traversal
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
// Observe item by itemobserve(arr[i]); }}}Copy the code
4. Collect dependencies
4.1 Why collect dependencies
The purpose of hijacking data is to notify those where it was used when its properties changed.
Collect dependencies first, collect the data used in the place, such as property changes, in the previous collection of dependencies trigger a cycle again
So the summary is that objects collect dependencies in the getter, trigger dependencies in the setter and arrays collect dependencies in the getter, trigger dependencies in the interceptor
4.2 How Do I Collect Dependencies
Where does the dependency collection go? Dep
The goal is clear, we’re going to collect dependencies in the getter, so where do we collect dependencies?
Encapsulate dependency collection into a class called Dep. This class helps us manage dependencies by collecting dependencies, removing dependencies, and sending notifications to dependencies
What is dependency? Watcher
Where data is needed, called a dependency, is Watcher
Only getters triggered by the Watcher collect dependencies, and whichever Watcher fires the getter is collected into the Dep
Dep uses a publisk-subscribe model, which loops through the list to notify all watchers when data changes.
Watcher is a mediator role that notifies it when data changes, and then notifies the rest of the world
4.3 implementation
The nifty part of the code implementation: Watcher sets itself to a specified location globally and then reads the data. Because the data is read, the getter for that data is triggered. In the getter, you get the Watcher that is currently reading, and you collect that Watcher into the Dep.
The watcher instance needs to subscribe to a dependency (data), that is, get a dependency or collect a dependency on watcher. When a dependency on Watcher occurs, watcher’s callback function is triggered, that is, to distribute updates
Dep.js
Dep uses a publisk-subscribe model, which loops through the list to notify all watchers when data changes.
The Dep class helps us manage dependencies by collecting them, removing them, sending notifications to them, and so on
let uid = 0;
/** * The Dep class helps us manage dependencies by collecting dependencies, deleting dependencies, sending notifications to dependencies, etc. */
export default class Dep {
constructor() {
console.log("Dep");
this.id = uid++;
// Use an array to store its own subscribers, and place an instance of Watcher
this.subs = [];
}
// Add a subscription
addSub(sub) {
this.subs.push(sub);
}
// Delete the subscription
removeSub(sub) {
remove(this.subs, sub);
}
// Add dependencies
depend() {
// dep. target is a global location that we specify. Window. target is ok, as long as it is globally unique and unambiguous
if (Dep.target) {
this.addSub(Dep.target); }}// Notification update
notify() {
console.log("notify");
// Make a shallow copy
const subs = this.subs.slice();
/ / traverse
for (let i = 0, l = subs.length; i < l; i++) { subs[i].update(); }}}/** * Removes item * from arR array@param {*} arr
* @param {*} item
* @returns * /
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1); }}}Copy the code
Watcher.js
import Dep from "./Dep";
let uid = 0;
/** * Watcher is a mediator role that notifies it when data changes and then notifies other places */
export default class Watcher {
constructor(target, expression, callback) {
console.log("Watcher");
this.id = uid++;
this.target = target;
// To read the contents of data.b.c, execute this.getter()
this.getter = parsePath(expression);
this.callback = callback;
this.value = this.get();
}
get() {
// Enter the dependency collection phase.
// Set the global dep. target to Watcher itself
Dep.target = this;
const obj = this.target;
var value;
// Keep looking as long as you can
try {
value = this.getter(obj);
} finally {
Dep.target = null;
}
return value;
}
update() {
this.run();
}
run() {
this.getAndInvoke(this.callback);
}
getAndInvoke(callback) {
const value = this.get();
if(value ! = =this.value || typeof value === "object") {
const oldValue = this.value;
this.value = value;
callback.call(this.target, value, oldValue); }}}/** * use STR. Break it into array segments and loop through the array, reading the data layer by layer until obj is the data that STR wants to read *@param {*} str
* @returns* /
function parsePath(str) {
let segments = str.split(".");
return function (obj) {
for (let key of segments) {
if(! obj)return;
obj = obj[key];
}
return obj;
};
}
Copy the code
defineReactive.js
Objects collect dependencies in getters, arrays collect dependencies in setters, and interceptors fire dependencies
import Dep from "./Dep";
import observe from "./observe";
/** * define a listener for the key of the object data@param {*} Data Incoming data *@param {*} Key Listens to the property *@param {*} The turnover variable */ provided by the value closure environment
export default function defineReactive(data, key, value) {
// Each data item maintains its own array to store its dependent watcher
const dep = new Dep();
// console.log('defineReactive()', data,key,value)
if (arguments.length === 2) {
value = data[key];
}
// The child element must observe to recurse
let childOb = observe(value);
Object.defineProperty(data, key, {
// Enumeration can be for-in
enumerable: true.// Can be configured, such as delete
configurable: true.// Getter collects dependencies
get() {
console.log('Getter attempts to access${key}Attribute `);
// Collect dependencies
if (Dep.target) {
dep.depend();
// Determine if there are any children
if (childOb) {
// Array collection dependencieschildOb.dep.depend(); }}return value;
},
// setters trigger dependencies
set(newValue) {
console.log(The setter is trying to change${key}Attribute `, newValue);
if (value === newValue) return;
value = newValue;
// When a new value is set, the new value must also be observed
childOb = observe(newValue);
// Trigger dependencies
// Publish subscription mode, notify DEPdep.notify(); }}); }Copy the code
array.js
Arrays collect dependencies in getters and trigger dependencies in interceptors
import def from "./def";
const arrayPrototype = Array.prototype;
// Create an arrayMethod based on array. prototype
export const arrayMethods = Object.create(arrayPrototype);
// The 7 array methods to be overwritten
const methodsNeedChange = [
"push"."pop"."shift"."unshift"."splice"."sort"."reverse",];// Execute these methods in batches
methodsNeedChange.forEach((methodName) = > {
// Back up the original method
const original = arrayPrototype[methodName];
// Define a new method
def(
arrayMethods,
methodName,
function () {
console.log("Array data has been hijacked.");
// Restore the original function (array method)
const result = original.apply(this.arguments);
// Turn an array-like object into an array
const args = [...arguments];
// Remove __ob__ from this array
// Get the Observer instance in the interceptor
const ob = this.__ob__;
// There are three methods: push, unshift, splice to insert new items, to hijack (detect) these data (insert new items)
let inserted = [];
switch (methodName) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
// Check to see if there is a new inserted item
if (inserted) {
ob.observeArray(inserted);
}
// Publish subscription mode, notify DEP
// Send messages to dependencies
ob.dep.notify();
return result;
},
false
);
});
Copy the code
5. Test detection
let obj = {
a: 1.b: {
c: {
d: 4,}},e: [22.33.44.55]};Copy the code
console.log(obj);
Copy the code
observe(obj);
Copy the code
observe(obj);
console.log(obj);
Copy the code
observe(obj);
obj.a = 5;
Copy the code
observe(obj);
obj.a++
Copy the code
observe(obj);
console.log(obj.b.c)
Copy the code
observe(obj);
obj.b.c.d = 10;
Copy the code
observe(obj);
obj.e.push(66.77.88);
console.log(obj.e);
Copy the code
observe(obj);
obj.e.splice(2.1[13.14]);
console.log(obj.e);
Copy the code
observe(obj);
new Watcher(obj, "b.c.d".(val) = > {
console.log("Watcher listening", val);
});
Copy the code
Source linkGitee.com/ykang2020/v…