1. Preface
When asked about the two-way data binding principle of Vue, you may blurt out:Object.defineProperty
Method property interception methoddata
Read/write of each data in the object is convertedgetter
/setter
To notify the view update when the data changes. Although the general principle is summarized in one sentence, the internal implementation method is worth further research. This article analyzes the implementation process of Vue's internal bidirectional binding principle in a simple and easy-to-understand way.
2. Train of Thought Analysis
The so-called mvvm data two-way binding mainly involves updating views of data changes and updating data of views. For example:
To implement these two processes, the key point is how to update the view based on data changes. We can use event monitoring to update data based on view changes. Therefore, we will focus on how to update the view based on data changes.
The key point of Data Change update view is how we know that the data has changed. As long as we know when the data has changed, the problem will be solved, we only need to notify the view update when the data changes.
3. Make the data object "observability"
Each read and write of data can be seen by us, that is, we can know when the data is read or when it is rewritten, we call it "observability" of data changes '.
To change the data to 'observability ', we need to useObject.defineProperty
Method. MDN introduces this method as follows:
The object. defineproperty () method defines a new property directly on an object, or modifies an existing property of an object and returns this object.
In this article, we use this method to make the data "observability ".
First, we define a Data Objectcar
:
let car = { 'brand':'BMW', 'price':3000 }
We have defined thiscar
Brandbrand
YesBMW
, Priceprice
It is 3000. Now we can usecar.brand
Andcar.price
Directly read and write thiscar
Attribute Value. However, whencar
When the attribute is read or modified, we do not know. So how can we makecar
Let us know that its attributes have been modified?
Next, we useObject.defineProperty()
Rewrite the example above:
Let Car = {} let val = 3000 object. defineproperty (CAR, 'price', {Get () {console. log ('price attribute read ') return Val}, set (newval) {console. log ('price Attribute Modified ') val = newval }})
PassObject.defineProperty()
Methodcar
Definesprice
And use the Read and Write attributes respectively.get()
Andset()
Intercept. Every time this attribute is read or written, it will startget()
Andset()
. For example:
As you can see,car
We can actively tell us the Read and Write status of its attributes, which also means that thiscar
The data object is "observability.
Tocar
All attributes of are become observability. We can write the following two functions:
/*** Converts each item of an object into an observed object * @ Param {object} OBJ object */function observable (OBJ) {If (! OBJ | typeof OBJ! = 'Object') {return;} Let keys = object. keys (OBJ); keys. foreach (key) =>{ definereactive (OBJ, key, OBJ [Key])}) return OBJ ;} /*** convert an object to an object that can be observed * @ Param {object} OBJ object * @ Param {string} key of the object * @ Param {Any} Val a key of the object */function definereactive (OBJ, key, Val) {object. defineproperty (OBJ, key, {Get () {console. log ('$ {key} attribute read'); Return Val ;}, set (newval) {console. log ('$ {key} Attribute Modified'); val = newval ;}})}
Now, we can definecar
:
let car = observable({ 'brand':'BMW', 'price':3000 })
car
The two attributes of are become observability.
4. Dependency collection
After the data is 'observability ', we know when the data is read or written, we can notify those views that depend on the data when the data is read or written. For convenience, we need to collect all dependencies first. Once the data changes, unified notification updates. In fact, this is a typical "Publish subscriber" mode. The data changes to "publisher" and the dependent object is "subscriber ".
Now, we need to create a dependency collection container, that is, the message subscriber DEP, to accommodate all the "subscribers ". The DEP subscriber is mainly responsible for collecting subscribers. When data changes, the subscriber executes the UPDATE function.
Create the message subscriber Dep:
Class Dep {Constructor () {This. subs = []}, // Add subscriber addsub (sub) {This. subs. push (sub) ;}, // determine whether to add the subscriber depend () {If (dep.tar get) {this.addsub(dep.tar get) }}, // notify the subscriber to update notify () {This. subs. foreach (sub) => {sub. update ()} dep.tar get = NULL;
With the subscriberdefineReactive
Transform the function to implant the subscriber to it:
Function definereactive (OBJ, key, Val) {Let Dep = new Dep (); object. defineproperty (OBJ, key, {Get () {dep. depend (); console. log ('$ {key} attribute read'); Return Val ;}, set (newval) {val = newval; console. log ('$ {key} Attribute Modified'); dep. notify () // notify all subscribers of data changes }})}
From the code point of view, we have designed a Dep class for the subscriber, which defines some attributes and Methods. Here, we need to note that it has a static attribute.target
, Which is globally uniqueWatcher
This is a very clever design, because at the same time there can only be one globalWatcher
Is calculated, and its own attributessubs
YesWatcher
.
The DEP subscriber Operation is designed ingetter
This is to makeWatcher
It is triggered during initialization, so you need to determine whether to add a subscriber. Insetter
In the function, if the data changes, all subscribers will be notified, and the subscribers will execute the corresponding updated function.
At this point, the DEP design of the subscriber is complete. Next, we will design the subscriber watcher.
5. subscriber watcher
SubscriberWatcher
You need to add yourself to the subscriber during initialization.Dep
, How to add it? We already know the listenerObserver
Yesget
The function is executed to add a subscriber.Wather
So we only needWatcher
The correspondingget
Function to add a subscriber, how to triggerget
The function is no longer simple, as long as the corresponding attribute value is obtained, it can be triggered, the core reason is that we useObject.defineProperty( )
For data monitoring. Here is another detail that needs to be processed. We only needWatcher
The subscriber needs to be added only during initialization, so a judgment operation is required. Therefore, you can perform the following operations on the subscriber:Dep.target
Subscriber in the cache. After adding the subscriber, remove it. SubscriberWatcher
The implementation is as follows:
Class watcher {Constructor (Vm, exp, CB) {This. vm = VM; this. exp = exp; this. CB = CB; this. value = This. get (); // Add yourself to the subscription operator}, update () {Let value = This. VM. data [this. exp]; let oldval = This. value; If (value! = Oldval) {This. value = value; this. CB. call (this. VM, value, oldval) ;}, get () {dep.tar get = This; // cache your own LET value = This. VM. data [this. exp] // force execute the get function dep.tar get = NULL in the listener; // release your own return value ;}}
Process Analysis:
SubscriberWatcher
Is a class. In its constructor, some attributes are defined:
- VM:A vue instance object;
- Exp:Yes
node
Nodev-model
Orv-on:click
And other command attribute values. For examplev-model="name"
,exp
Yesname
;
- CB:Yes
Watcher
Bound update functions;
When we instantiate a renderingwatcher
Firstwatcher
The constructor logic will execute itsthis.get()
Method, enterget
The function will first execute:
Dep.tar get = This; // cache yourself
In factDep.target
Assign a value to the current Renderingwatcher
And then execute:
Let value = This. VM. Data [This. Exp] // force the get function in the listener
In this processvm
To trigger Data Objectgetter
.
For each object Valuegetter
Both hold onedep
, In the triggergetter
Will calldep.depend()
Method.this.addSub(Dep.target)
, That is, the currentwatcher
Subscribed to this datadep
Ofsubs
The purpose is to notify future data changes.subs
Prepare.
In this way, a dependency collection process has been completed. Is it over now? Actually not. After dependency collection is completed, you needDep.target
Restore to the previous status, that is:
Dep.tar get = NULL; // release yourself
Because the currentvm
The data dependency collection has been completed, then the corresponding RenderingDep.target
It also needs to be changed.
Whileupdate()
A function is called when data changes.Watcher
Update functions. First passlet value = this.vm.data[this.exp];
Obtain the latest data andget()
Compare the obtained old data. If they are different, call the UPDATE function.cb
.
Now, a simple subscriberWatcher
The design is complete.
6. Test
After the above work is completed, we can perform a real test.
Index.html
<! Doctype HTML> <HTML lang = "en">
Observer. js
/*** Converts each item of an object into an observed object * @ Param {object} OBJ object */function observable (OBJ) {If (! OBJ | typeof OBJ! = 'Object') {return;} Let keys = object. keys (OBJ); keys. foreach (key) =>{ definereactive (OBJ, key, OBJ [Key])}) return OBJ ;} /*** convert an object to an object that can be observed * @ Param {object} OBJ object * @ Param {string} key of the object * @ Param {Any} Val a key of the object */function definereactive (OBJ, key, Val) {Let Dep = new Dep (); object. defineproperty (OBJ, key, {Get () {dep. depend (); console. log ('$ {key} attribute read'); Return Val ;}, set (newval) {val = newval; console. log ('$ {key} Attribute Modified'); dep. notify () // notify all subscribers of data changes})} class Dep {Constructor () {This. subs = []} // Add subscriber addsub (sub) {This. subs. push (sub) ;}// determine whether to add the subscriber depend () {If (dep.tar get) {this.addsub(dep.tar get) }}// notify the subscriber to update notify () {This. subs. foreach (sub) => {sub. update ()} dep.tar get = NULL;
Watcher. js
Class watcher {Constructor (Vm, exp, CB) {This. vm = VM; this. exp = exp; this. CB = CB; this. value = This. get (); // Add yourself to the subscription operator} Get () {dep.tar get = This; // cache your own LET value = This. VM. data [this. exp] // force execute the get function dep.tar get = NULL in the listener; // release your own return value;} Update () {Let value = This. VM. data [this. exp]; let oldval = This. value; If (value! = Oldval) {This. value = value; this. CB. Call (this. VM, value, oldval );}}}
Effect:
7. Summary Summary:
To bind data in two directions, we must first hijack and listen to the data, so we need to set a listener.Observer
To listen to all attributes. If the attributes change, you need to tell the subscriberWatcher
Check whether updates are required. Because there are many subscribers, we need a message subscriber.Dep
To specifically collect these subscribers, and then in the listenerObserver
And subscriberWatcher
.
(End)
Easy to understand vue two-way binding principle and implementation