In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-04-10 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/03 Report--
This article mainly introduces "how to understand the design principle from observer mode to responsive mode". In daily operation, I believe many people have doubts about how to understand the design principle from observer mode to responsive design principle. The editor consulted all kinds of materials and sorted out simple and useful operation methods. I hope it will be helpful for you to answer the doubts about "how to understand design principles from observer mode to responsive mode". Next, please follow the editor to study!
1. Observer model
Observer pattern, which defines an one-to-many relationship, allows multiple observer objects to listen to a topic object at the same time, and notifies all observer objects when the state of the subject object changes, so that they can update themselves automatically. There are two main roles in observer mode: Subject (subject) and Observer (observer).
Because the observer mode supports simple broadcast communication, all observers are automatically notified when the message is updated. Let's take a look at how to use TypeScript to implement the observer pattern:
1.1 define ConcreteObserver
Interface Observer {notify: Function;} class ConcreteObserver implements Observer {constructor (private name: string) {} notify () {console.log (`${this.name} has been notified.`);}
1.2Defining Subject classes
Class Subject {private observers: Observer [] = []; public addObserver (observer: Observer): void {this.observers.push (observer);} public notifyObservers (): void {console.log ("notify all the observers"); this.observers.forEach (observer = > observer.notify ());}}
1.3 examples of use
/ / ① create subject object const subject: Subject = new Subject (); / / ② add observer const observerA = new ConcreteObserver ("ObserverA"); const observerC = new ConcreteObserver ("ObserverC"); subject.addObserver (observerA); subject.addObserver (observerC); / / ③ notifies all observers subject.notifyObservers ()
For the above example, there are three main steps: ① creates the subject object, ② adds the observer, and ③ notifies the observer. After the above code runs successfully, the console outputs the following results:
Notify all the observers ObserverA has been notified. ObserverC has been notified.
In most scenarios at the front end, the target we observe is the data. When the data changes, the page can be updated automatically, and the corresponding effect is shown below:
To achieve automatic update, we need to meet two conditions: one is to be able to update accurately, and the other is to be able to detect changes in the data. In order to achieve accurate update, it is necessary to collect the update function (observer) that is interested in the abnormal movement of the data. after the collection is completed, when the abnormal movement of the data is detected, the corresponding update function can be notified.
The above description seems to be circuitous, but in order to achieve automatic update, we want to automate the three steps of creating a topic object by ①, adding an observer to ②, and notifying an observer by ③. This is the core idea of achieving responsiveness. Next, let's give a concrete example:
I believe that friends who are familiar with the principle of Vue2 response will be familiar with the code in the above figure, in which the second step is also known as collection dependency. By using Object.defineProperty API, we can intercept read and modify operations on the data.
If a data is read in the body of a function, it indicates that the function is interested in the abnormal movement of the data. When the data is read, the defined getter function is triggered, and the observer of the data can be stored. When the data changes, we can notify all the observers in the observer list and perform the corresponding update operation.
Vue3 uses Proxy API to implement responsiveness. What are the advantages of Proxy API over Object.defineProperty API? Brother Po is not going to introduce here, but is going to write a special article to introduce Proxy API. Next, Brother Po will begin to introduce the protagonist of this article-observer-util:
Transparent reactivity with 100% language coverage. Made with ❤️ and ES6 Proxies.
Https://github.com/nx-js/observer-util
The library also uses ES6's Proxy API to implement responsiveness, so before we introduce how it works, let's take a look at how to use it.
II. Brief introduction of observer-util
The library observer-util is also very easy to use. Using the observable and observe functions provided by the library, we can easily implement the responsive data. Let's start with a simple example:
2.1 known attributes
Import {observable, observe} from'@ nx-js/observer-util'; const counter = observable ({num: 0}); const countLogger = observe (() = > console.log (counter.num)); / / output 0 counter.num++; / / output 1
In the above code, we import the observable and observe functions from the @ nx-js/observer-util module, respectively. The observable function is used to create observable objects, and the observe function is used to register the observer function. After the above code is executed successfully, the console outputs 0 and 1 in turn. In addition to known attributes, observer-util also supports dynamic attributes.
2.2 dynamic attributes
Import {observable, observe} from'@ nx-js/observer-util'; const profile = observable (); observe (() = > console.log (profile.name)); profile.name = 'abao'; / / output' abao'
After the above code is executed successfully, the console outputs undefined and abao in turn. In addition to supporting ordinary objects, observer-util also supports collections in arrays and ES6, such as Map, Set, and so on. Here we take a commonly used array as an example to see how to turn an array object into a responsive object.
2.3 array
Import {observable, observe} from'@ nx-js/observer-util'; const users = observable ([]); observe () = > console.log (users.join (',')); users.push ('abao'); / / output' abao' users.push ('kakuqo'); / / output' abao, kakuqo' users.pop (); / / output 'abao,'
Here, Brother Po only introduces a few simple examples. Other partners who are interested in using the examples of observer-util can read the README.md documentation of the project. Next, Brother Po will take the simplest example to analyze the principle of the responsive implementation of the library observer-util.
If you want to run the above example locally, you can modify the index.js file in the debug/index.js directory, and then execute the npm run debug command in the root directory.
III. Observer-util 's original understanding
First of all, let's review the earliest example:
Import {observable, observe} from'@ nx-js/observer-util'; const counter = observable ({num: 0}); / / A const countLogger = observe (() = > console.log (counter.num)); / / B counter.num++; / / C
In line A, we create an observable counter object through the observable function, which has the following internal structure:
By looking at the figure above, you can see that the counter variable points to a Proxy object that contains three Internal slots. So how does the observable function convert our {num: 0} object into a Proxy object? In the project's src/observable.js file, we found the definition of the function:
/ / src/observable.js export function observable (obj = {}) {/ / if obj is already an observable object or should not be wrapped, return it directly if (proxyToRaw.has (obj) | |! builtIns.shouldInstrument (obj)) {return obj} / / if obj already has a corresponding observable object, return it. Otherwise, create a new observable object return rawToProxy.get (obj) | | createObservable (obj)}
In the above code, two objects, proxyToRaw and rawToProxy, are defined in the src/internals.js file:
/ / src/internals.js export const proxyToRaw = new WeakMap () export const rawToProxy = new WeakMap ()
These two objects store the mapping between proxy = > raw and raw = > proxy, where raw represents the original object and proxy represents the wrapped Proxy object. It is obvious that when executed for the first time, proxyToRaw.has (obj) and rawToProxy.get (obj) return false and undefined, respectively, so the logic to the right of the | | operator is executed.
Let's analyze the shouldInstrument function, which is defined as follows:
/ / src/builtIns/index.js export function shouldInstrument ({constructor}) {const isBuiltIn = typeof constructor = = 'function' & & constructor.name in globalObj & & globalObj [constructor.name] = constructor return! isBuiltIn | | handlers.has (constructor)}
Inside the shouldInstrument function, the constructor of the parameter obj is used to determine whether it is a built-in object. For the {num: 0} object, its constructor is Object () {[native code]}, so the value of isBuiltIn is true, so the logic to the right of the | | operator continues. Where the handlers object is a Map object:
/ src/builtIns/index.js const handlers = new Map ([[Map, collectionHandlers], [Set, collectionHandlers], [WeakMap, collectionHandlers], [WeakSet, collectionHandlers], [Object, false], [Array, false], [Int8Array, false], [Uint8Array, false], / / omit some codes [Float64Array, false])
After looking at the structure of handlers, it is obvious that the result of the builtIns.shouldInstrument (obj) expression is false. So next, our focus is on the createObservable function:
Function createObservable (obj) {const handlers = builtIns.getHandlers (obj) | | baseHandlers const observable = new Proxy (obj, handlers) / / Save raw = > proxy,proxy = > raw mapping relationship rawToProxy.set (obj, observable) proxyToRaw.set (observable, obj) storeObservable (obj) return observable}
By looking at the above code, we can see why a Proxy object is returned after calling the observable ({num: 0}) function. For the constructor of Proxy, it supports two parameters:
Const p = new Proxy (target, handler)
Target: the target object to be wrapped with Proxy (can be any type of object, including native arrays, functions, or even another proxy)
Handler: an object that usually takes functions as attributes, and the functions in each property define the behavior of proxy p when performing various operations.
The target in the example points to the {num: 0} object, and the value of handlers returns a different handlers depending on the type of obj:
/ / src/builtIns/index.js export function getHandlers (obj) {return handlers.get (obj.constructor) / / [Object, false],}
BaseHandlers is an object that contains "traps" such as get, has, and set:
Export default {get, has, ownKeys, set, deleteProperty}
After the observable object is created, the mapping between raw = > proxy,proxy = > raw is saved, and then the storeObservable function is called to perform the storage operation, and the storeObservable function is defined in the src/store.js file:
/ / src/store.js const connectionStore = new WeakMap () export function storeObservable (obj) {/ / used to save the mapping relationship between obj.key-> reaction later connectionStore.set (obj, new Map ())}
With so many introductions, Brother Po summed up the previous content with a picture:
What is the use of proxyToRaw and rawToProxy objects? I believe that after reading the following code, you will know the answer.
/ / src/observable.js export function observable (obj = {}) {/ / if obj is already an observable object or should not be wrapped, return it directly if (proxyToRaw.has (obj) | |! builtIns.shouldInstrument (obj)) {return obj} / / if obj already has a corresponding observable object, return it. Otherwise, create a new observable object return rawToProxy.get (obj) | | createObservable (obj)}
Let's start with line B:
Const countLogger = observe (() = > console.log (counter.num)); / / B
The observe function is defined in the src/observer.js file as follows:
/ / src/observer.js export function observe (fn, options = {}) {/ / const IS_REACTION = Symbol ('is reaction') const reaction = FN [is _ REACTION]? Fn: function reaction () {return runAsReaction (reaction, fn, this, arguments)} / / omit part of the code reaction [is _ REACTION] = true / / if it is not lazy, run if (! options.lazy) {reaction ()} return reaction} directly
In the above code, you first determine whether the incoming fn is a reaction function, and if so, use it directly. If not, the incoming fn is wrapped as a reaction function before the function is called. Inside the reaction function, another function, runAsReaction, is called, which, as its name implies, is used to run the reaction function.
The runAsReaction function is defined in the src/reactionRunner.js file:
/ / src/reactionRunner.js const reactionStack = [] export function runAsReaction (reaction, fn, context, args) {/ / omit part of the code if (reactionStack.indexOf (reaction) =-1) {/ / release (obj-> key-> reactions) link and reset the cleaner link releaseReaction (reaction) try {/ / push into the reactionStack stack So that the connection between (observable.prop-> reaction) can be established in the get trap reactionStack.push (reaction) return Reflect.apply (fn, context, args)} finally {/ / remove the executed reaction function reactionStack.pop ()}} from the reactionStack stack
In the body of the runAsReaction function, the currently executing reaction function is pushed into the reactionStack stack, and the incoming fn function is called using Reflect.apply API. When the fn function executes, it executes the console.log (counter.num) statement, within which the num property of the counter object is accessed. The counter object is a Proxy object, and the get trap in baseHandlers is triggered when the properties of the object are accessed:
/ src/handlers.js function get (target, key, receiver) {const result = Reflect.get (target, key, receiver) / / Register and save (observable.prop-> runningReaction) registerRunningReactionForOperation ({target, key, receiver, type: 'get'}) const observableResult = rawToProxy.get (result) if (hasRunningReaction () & typeof result =' object' & & result! = = null) {/ omit some code} return observableResult | | result}
In the above functions, the registerRunningReactionForOperation function is used to save the mapping between observable.prop-> runningReaction. In fact, it is a crucial step to specify the properties of the object and add the corresponding observer. So let's focus on the registerRunningReactionForOperation function:
/ / src/reactionRunner.js export function registerRunningReactionForOperation (operation) {/ / get the currently executing reaction const runningReaction = reactionStack [reactionStack.length-1] if (runningReaction) {debugOperation (runningReaction, operation) registerReactionForOperation (runningReaction, operation)}} from the top of the stack
In the registerRunningReactionForOperation function, you first get the running reaction function from the reactionStack stack, and then call the registerReactionForOperation function again to register the reaction function for the current operation. The specific processing logic is as follows:
/ / src/store.js export function registerReactionForOperation (reaction, {target, key, type}) {/ / omit some codes const reactionsForObj = connectionStore.get (target) / / A let reactionsForKey = reactionsForObj.get (key) / / B if (! reactionsForKey) {/ / C reactionsForKey = new Set () reactionsForObj.set (key) ReactionsForKey)} if (! reactionsForKey.has (reaction)) {/ / D reactionsForKey.add (reaction) reaction.cleaners.push (reactionsForKey)}
When the observable (obj) function is called to create an observable object, the obj object is key and saved in the connectionStore (connectionStore.set (obj, new Map () object.
Brother Po divides the processing logic inside the registerReactionForOperation function into four parts:
(a): if you get the value of target from the connectionStore (WeakMap) object, you will return a reactionsForObj (Map) object
(B): gets the value corresponding to key (object property) from the reactionsForKey (Map) object, and returns undefined if it does not exist
(C): if reactionsForKey is undefined, a Set object is created and saved as a value in the reactionsForObj (Map) object
(d): determine whether the reactionsForKey (Set) set contains the current reaction function, and if not, add the current reaction function to the reactionsForKey (Set) set.
In order to better understand the content of this part, Brother Po continues to summarize the above content by drawing:
Because each attribute in the object can be associated with multiple reaction functions, to avoid duplication, we use the Set object to store the reaction function associated with each property. An object can contain multiple attributes, so the Map object is used internally in observer-util to store the relationship between each attribute and the reaction function.
In addition, in order to support the ability to turn multiple objects into observable objects and reclaim memory in time when the original objects are destroyed, observer-util defines connectionStore objects of type WeakMap to store the link relationships of objects. For the current example, the internal structure of the connectionStore object is as follows:
Finally, let's analyze the line of counter.num++;. For simplicity, Brother Po only analyzes the core processing logic, and partners who are interested in the complete code can read the source code of the project. When the line of counter.num++; is executed, the set set trap is triggered:
/ src/handlers.js function set (target, key, value, receiver) {/ / omit some codes const hadKey = hasOwnProperty.call (target, key) const oldValue = target [key] const result = Reflect.set (target, key, value, receiver) if (! hadKey) {queueReactionsForOperation ({target, key, value, receiver, type: 'add'})} else if (value! = = oldValue) {queueReactionsForOperation ({target, key, value) OldValue, receiver, type: 'set'})} return result}
For our example, the queueReactionsForOperation function will be called:
/ / src/reactionRunner.js export function queueReactionsForOperation (operation) {/ / iterate and queue every reaction, which is triggered by obj.key mutation getReactionsForOperation (operation) .forEach (queueReaction, operation)}
Inside the queueReactionsForOperation function, the getReactionsForOperation function will be called to obtain the reactions corresponding to the current key:
/ src/store.js export function getReactionsForOperation ({target, key, type}) {const reactionsForTarget = connectionStore.get (target) const reactionsForKey = new Set () if (type = 'clear') {reactionsForTarget.forEach ((_, key) = > {addReactionsForKey (reactionsForKey, reactionsForTarget, key)})} else {addReactionsForKey (reactionsForKey, reactionsForTarget, key)} / / omit part of the code return reactionsForKey}
After successfully getting the reactions object corresponding to the current key, each reaction is executed by traversing the object. The specific processing logic is defined in the queueReaction function:
/ / src/reactionRunner.js function queueReaction (reaction) {debugOperation (reaction, this) / / queue the reaction for later execution or run it immediately if (typeof reaction.scheduler = 'function') {reaction.scheduler (reaction)} else if (typeof reaction.scheduler =' object') {reaction.scheduler.add (reaction)} else {reaction ()}}
Because our example does not configure the scheduler parameter, we will directly execute the code of the else branch, that is, execute the statement reaction ().
Well, the core logic of how to convert ordinary objects into observable objects within the observer-util library has been analyzed. For ordinary objects, observer-util internally provides get and set traps through Proxy API, which implements the processing logic of automatically adding observers (adding reaction functions) and notifying observers (executing reaction functions).
If you finish reading this article, you should be able to understand the definition of targetMap in the reactivity module in Vue3:
/ / vue-next/packages/reactivity/src/effect.ts type Dep = Set type KeyToDepMap = Map const targetMap = new WeakMap ()
In addition to ordinary objects and arrays, observer-util also supports collections in ES6, such as Map, Set, and WeakMap. When working with these objects, the collectionHandlers object is used instead of the baseHandlers object when creating the Proxy object.
At this point, the study on "how to understand the design principles from observer mode to responsive mode" is over. I hope to be able to solve everyone's doubts. The collocation of theory and practice can better help you learn, go and try it! If you want to continue to learn more related knowledge, please continue to follow the website, the editor will continue to work hard to bring you more practical articles!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.