The time is ticking away
Let’s keep looking while we’re here
- This article is actually no bird use, but for the current front-end interview, is already a test point was asked tired
- Since it is a test point, so I want to simply to give you a point
As we all know, this is the era of MVVM, from the early Angular to the React and Vue, and from the original three parts of the world to the current two tiger competition.
No doubt not to our development has brought an unprecedented new experience, say goodbye to the operation of THE DOM thinking, changed the idea of data-driven page, as expected the progress of The Times, changed our many many.
It’s not good to talk too much. Let’s get into today’s topic
To highlight
MVVM bidirectional data binding is handled by dirty value detection in angular1.x
Now React, Vue, and the latest Angular implementations are much more similar
That’s through data hijacking + publish/subscribe
The real implementation is also provided in ES5 Object. DefineProperty, of course, this is not compatible so Vue and others only support IE8+
Why it is
Object.defineproperty () is not used much in development. It is mostly used to modify internal properties. Why is it so hard (just a personal thought)
But in the implementation of the framework or library But played a big use, this is not much to say, just a piece of light boat, not to write the strength of the library
You know what it is, you know why it is, so how do you use it
let obj = {};
let song = 'Hair like snow';
obj.singer = 'Jay Chou';
Object.defineProperty(obj, 'music', {
// 1. value: 'Qilixiang',
configurable: true, // 2. Can configure the object, delete the properties // writable:true// 3. You can modify enumerable:true☆ Get, // 4.setYou cannot set writable and value. They replace the two and are mutually exclusiveget() {// 5. The get method is called when obj. Music is obtainedreturn song;
},
set(val) {// 6. Reassign the modified value to song song = val; }}); // Execute console.log(obj); // {singer:'Jay Chou', music: 'Qilixiang'} // 1 delete obj.music; // If you want to delete the properties of obj, the signals must be set totrue2 console.log(obj); // {singer:'Jay Chou'}
obj.music = 'Listen to your mother'; // If you want to modify the properties of obj, set writable totrue 3
console.log(obj); // {singer: 'Jay Chou', music: "Listen to your mother."}
for (let key inObj) {// By default, attributes defined by defineProperty cannot be enumeratedtrue// Otherwise you can't get the music property, you can only get singer console.log(key); // singer, music 4 } console.log(obj.music); //'Hair like snow' 5
obj.music = 'his'; / / callsetSet the new value console.log(obj.music); //'his' 6
Copy the code
That’s how to use object.defineProperty
Let’s write an example. Here we use Vue as a reference to implement how to write MVVM
// index.html
<body>
<div id="app"> < h1 > {{song}} < / h1 > < p > {{album. The name}} is {{singer}} in November 2005 issue of the album < / p > < p > theme song for the {{album. The theme}} < / p > < p > lyrics for {{singer}}. < div style = "box-sizing: border-box! Important; word-wrap: break-word! Important;" MVVM --> <script SRC ="mvvm.js"Word-wrap: break-word! Important; ">< span style =" box-sizing: border-box; color: RGB (74, 74, 74)let mvvm = new Mvvm({
el: '#app',
data: { // Object.defineProperty(obj, 'song'.'Hair like snow');
song: 'Hair like snow',
album: {
name: 'Chopin in November',
theme: 'his'
},
singer: 'Jay Chou'}}); </script> </body>Copy the code
Above is the HTML in the writing method, believe that used Vue students are not unfamiliar
So start implementing your own MVVM
To build the MVVM
/ / create a Mvvm constructor es6 way / / here will be given an initial value options, prevent not preach, equivalent to the options | | {}function Mvvm(options = {}) {
// vm.$optionsOn Vue, we mount all the attributes on it$options
this.$options= options; // this._data is the same as Vuelet data = this._data = this.$options.data; Observe (data); // Observe (data); }Copy the code
The data was hijacked
Why data hijacking?
- Look at the Object and add object.defineProperty to the Object
- The characteristic of VUE is that you cannot add nonexistent attributes. Nonexistent attributes do not have get or set
- Depth response because every time you assign a new object you add a defineProperty to that new object
No more talking. Let’s look at the code
// Create an Observe constructor // write the main logic for data hijackingfunction Observe(data) {
// 所谓数据劫持就是给对象增加get,set// Iterate over the object firstfor (let key inData) {// define the data property with definePropertyletval = data[key]; observe(val); Object.defineproperty (data, key, {6464x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x)true.get() {
return val;
},
set(newVal) {// when the value is changedif(val === newVal) {// Set the same value as before and ignore itreturn; } val = newVal; // Return observe(newVal) to observe(observ); // Return observe(newVal) to observe(observ); // Return observe(newVal) to observe(observ); // When set to a new value, you need to define the new value as a property}}); } // Write a function outside of it. // You don't need to write a new every time you call itfunctionObserve (data) {// Direct if it is not an objectreturn// Prevent recursion overflowif(! data || typeof data ! = ='object') return;
return new Observe(data);
}
Copy the code
The above code implements data hijacking, but there may be some confusing areas such as recursion
Let’s see why recursion is a little bit more detailed. Look at this chestnut
let mvvm = new Mvvm({
el: '#app',
data: {
a: {
b: 1
},
c: 2
}
});
Copy the code
Let’s take a look at the console
recursive
So, observe(newVal), why do we recurse here
Mvvm._data. a = {b:’ OK ‘}
Then continue to look at the picture and talk
Data hijacking is done. Let’s make another data broker
Data brokers
A data broker is a way for us to get the data in data without having to write a long list every time. For example, mVVM._data.b, we can just write mVVM.a.b in an obvious way
So let’s move on. The plus sign is the implementation part
functionMvvm(options = {}) {// observe(data); // this represents this._data +for (let key in data) {
Object.defineProperty(this, key, {
configurable: true.get() {
returnthis._data[key]; A = {b: 1}},set(newVal) { this._data[key] = newVal; }}); +}} console.log(mvvm.a.b); // 1 mvvm.a.b ='ok';
console.log(mvvm.a.b); // 'ok'
Copy the code
With data hijacking and data broker implemented, it’s time to compile and parse the contents of {{}}
Data compilation
functionMvvm(options = {}) { // observe(data); + new Compile(options.el, this); } // Create the Compile constructorfunctionCompile(el, vm) {// mount EL to the instance to facilitate vm invocation.$el= document.querySelector(el); // In the el range to get the content, of course not one by one to take // can choose to move to memory and then put into the document fragment, save overheadlet fragment = document.createDocumentFragment();
while (child = vm.$el.firstChild) { fragment.appendChild(child); // Insert the contents of el into memory} // Replace the contents of ELfunction replace(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent;
letreg = /\{\{(.*?) \}\}/g; // Regular match {{}}if(node.nodetype === = 3 && reg.test(TXT)) {{}} console.log(RegExp.The $1); // The first group to be matched is a.b, clet arr = RegExp.The $1.split('. ');
letval = vm; arr.forEach(key => { val = val[key]; / / such as this. A.}); Node.textcontent = txt.replace(reg, val).trim(); } // If there are children, continue with the replace recursionif(node.childNodes && node.childNodes.length) { replace(node); }}); } replace(fragment); // Replace the content vm.$el.appendChild(fragment); // Put the document fragment in el}Copy the code
See here in the interview has been able to show the edge, it is to keep up one’s spirits, do a full set of things, to a dragon
Now the data is ready to compile, but the data we manually modified does not change on the page
So let’s see what we can do with that, and in fact we use a very common design pattern, publish subscribe, right
Release subscription
Publish subscribe is basically an array relationship, subscribe is putting functions in, publish is getting the functions in the array to execute
// Publish/subscribe mode subscribe and publish such as [fn1, fn2, fn3]function Dep() {// An array (which holds the function's event pool) this.subs = []; } Dep.prototype = { addSub(sub) { this.subs.push(sub); },notifyForEach (sub => sub.update()); this.subs.foreach (sub.update()); }}; Each instance created with the Watcher class has an update methodfunctionWatcher(fn) { this.fn = fn; // Put fn on the instance} watcher.prototype.update =function() {
this.fn();
};
let watcher = new Watcher(() => console.log(111)); //
letdep = new Dep(); dep.addSub(watcher); => [watcher] dep.addSub(watcher); dep.notify(); / / 111, 111Copy the code
Data Update view
- Now we need to subscribe to an event that will refresh the view when the data changes, which needs to be handled in the replace logic
- By subscribing to the data through new Watcher, the data changes and the content changes
functionReplace (frag) {// omit... // Replace logic node.textContent = txt.replace(reg, val).trim(); // Add two more parameters to Watcher(newVal) to the callback function + new Watcher(VM, RegExp).The $1, newVal => { node.textContent = txt.replace(reg, newVal).trim(); +}); } // Override the Watcher constructorfunctionWatcher(vm, exp, fn) { this.fn = fn; + this.vm = vm; + this.exp = exp; // Add an event // here we define a property + dep. target = this; +let arr = exp.split('. ');
+ letval = vm; + arr.forEach(key => {// value + val = val[key]; // If this.a.b is obtained, the default is to call get +}); + Dep.target = null; }Copy the code
The get method is automatically called when the value is retrieved, so let’s go to the data hijack where the get method is
function Observe(data) {
+ letdep = new Dep(); / / to omit... Object.defineProperty(data, key, {get() { + Dep.target && dep.addSub(Dep.target); // Add watcher to the subscribed event [watcher]return val;
},
set(newVal) {
if (val === newVal) {
return; } val = newVal; observe(newVal); + dep.notify(); // Let all watcher's update methods execute}})}Copy the code
When set is changed, the dep.notify method is executed. This method executes watcher’s update method, so let’s change the update again
Watcher.prototype.update = function() {// notify when the value has changed // use vm, exp to get the new value +let arr = this.exp.split('. ');
+ letval = this.vm; + arr.forEach(key => { + val = val[key]; // Get the new value +}); this.fn(val); // Replace {{}} with {{}};Copy the code
Now that our data changes can modify the view, that’s good. One last thing, let’s look at two-way data binding, a common interview test
Bidirectional data binding
// HTML structure <input v-model="c" type="text"> a: {b: 1}, c: 2}functionReplace (frag) {// omit... +if(node.nodetype === = 1) {// Element nodeletnodeAttr = node.attributes; Array.from(nodeAttr).foreach (attr => {let name = attr.name; // v-model type
let exp = attr.value; // c text
if (name.includes('v-')){ node.value = vm[exp]; } new Watcher(vm, exp,function(newVal) { node.value = newVal; // When watcher fires, it will automatically put the content into the input box}); node.addEventListener('input', e => {
letnewVal = e.target.value; // This is equivalent to assigning a new value to this.c and changing the value is calledset.setUpdate vm[exp] = newVal; update vm[exp] = newVal; }); }); +}if(node.childNodes && node.childNodes.length) { replace(node); }}Copy the code
Finally, the interview asked Vue things but this stopped, what two-way data binding how to achieve, asked no intention, bad comment!!
Please stop, I should stop, but I have to write some more functions, such as computed and Mounted
Computed && Mounted
/ / HTML structure < p > the value of the sum is {{sum}} < / p > data: {a: 1, b: 9}, the computed: {sum() {
return this.a + this.b;
},
noop() {}},mounted() {
setTimeout(() => {
console.log('It's all worked out');
}, 1000);
}
functionMvvm(options = {}) {// initialize computed by pointing this to the instance + initcompute.call (this); New Compile(options.el, this); // Execute the mounted hook function + options.mounted. Call (this); // This implements the mounted hook function}function initComputed() {
let vm = this;
let computed = this.$options.computed; // Obtain computed attributes from options {sum: ƒ, noop: Var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var Key, {// determine whether the key is an object or a function in the computed field // If the key is a function, then the get method will be adjusted. // If the key is an object, then manually adjust the get method.sum() {returnthis.a + this.b; }, they get values of a and b and then call the get method // so there is no need for new Watcher to listen for changes to get: typeof computed[key] ==='function' ? computed[key] : computed[key].get,
set() {}}); }); }Copy the code
Writing these contents is not a little, finally do a formal summary
conclusion
MVVM implemented by yourself consists of the following things
- Data hijacking via get and set of object.defineProperty
- Data broker to this by traversing data
- Compile data with {{}}
- Synchronize data with views through a publish-subscribe pattern
- Pass pass pass, received, thank the senior officials for staying
supplement
In view of the above code in the implementation of the compilation of the time will still have some small bugs, after another study and expert guidance, improve the compilation, please see the revised code below
Fixed: two adjacent {{}} re matches, the last one does not compile correctly to the corresponding text, such as {{albu.name}} {{singer}}
functionCompile(el, vm) {Compile(el, vm) {functionReplace (frag) {// omit...if (node.nodeType === 3 && reg.test(txt)) {
function replaceTxt() { node.textContent = txt.replace(reg, (matched, placeholder) => { console.log(placeholder); Song, album. Name, singer... new Watcher(vm, placeholder, replaceTxt); // Listen for changes and match the replacementreturn placeholder.split('. ').reduce((val, key) => {
returnval[key]; }, vm); }); }; / / replace replaceTxt (); }}}Copy the code
The main implementation of the above code relies on the reduce method, which executes the callback function for each element in the array in turn
If anything is unclear, let’s separate out the Reduce part and look at it again
// Split each of the matched values'song'.split('. ') = > ['song'] = > ['song'].reduce((val, key) => val[key])'song'] = >'Jay Chou'// Let's look at the following example:'album.name'.split('. ') = > ['album'.'name'] = > ['album'.'name'Val [key] => reduce(val[key] => val[key]);'album'] // then return the VM ['album'] this object is passed to the next call to val // and becomes vm['album'] ['name'] = >'Chopin in November'
return placeholder.split('. ').reduce((val, key) => {
return val[key];
}, vm);
Copy the code
Reduce is useful in many ways, such as calculating the sum of arrays is a more common method, there is also a more useful way to use the two-dimensional array flatten, you may want to take a look at the last
let arr = [
[1, 2],
[3, 4],
[5, 6]
];
let flatten = arr.reduce((previous, current) => {
returnprevious.concat(current); }); console.log(flatten); // [1, 2, 3, 4, 5, 6] Flatten = arr.reduce((a, b) => [...a,...b]); console.log(flatten); // [1, 2, 3, 4, 5, 6]Copy the code
Here’s the github address for comparison
Thanks again for watching, brothers and sisters! This is really the last look, has been to the end!