preface
As one of the must-test questions in the Vue interview, Vue’s responsive principle, presumably used by the Vue students are not unfamiliar, Vue official documents to pay attention to the responsive problems have also done a detailed description.
However, for those who are new to or don’t know much about it, they may still feel confused: why can’t the object attributes be added or deleted? Why not set array members by index? I believe that after reading this article, you will be suddenly enlightened.
This article will be combined with Vue source code analysis, for the whole responsive principle step by step in-depth. Of course, if you already have some knowledge and understanding of the reactive principle, you can go ahead and implement part of MVVM
The article repository and source code are at πΉπ° fe-Code, welcome to Star.
Vue is not exactly an MVVM model, so read it carefully.
While not entirely following the MVVM model, the design of Vue was inspired by it. Therefore, the variable name VM (short for ViewModel) is often used to represent Vue instances in documentation. – Vue website
Vue official responsive schematic town building.
thinking
Before getting into the subject, let’s consider the following code.
<template>
<div>
<ul>
<li v-for="(v, i) in list" :key="i">{{v.text}}</li>
</ul>
</div>
</template>
<script>
export default{
name: 'responsive'.data() {
return {
list: []}},mounted() {
setTimeout(_= > {
this.list = [{text: Awesome!}, {text: Awesome!}, {text: Awesome!}];
},1000);
setTimeout(_= > {
this.list.forEach((v, i) = > { v.text = i; });
},2000)}}</script>
Copy the code
We know that in Vue, attributes defined in data are data-hijacked via Object.defineProperty to support publish subscriptions for related operations. In our case, data only defines list as an empty array, so Vue hijacks it and adds the corresponding getter/setter.
So at 1 s, reassigning to list with this.list = [{text: 666}, {text: 666}, {text: 666}] triggers the setter and notifys the corresponding observer (in this case template compilation) to make the update.
At 2 s, we iterate through the array again, changing the text property of each list member, and the view is updated again. List [I] = {text: I}, the data will update normally, but the view will not. As mentioned earlier, there is no support for setting array members by index.
But when we use v.text = I, the view updates normally. Why? As mentioned above, Vue can hijack the attributes of data, but the attributes of list members are not data hijack, why can update the view?
This is because when we do setters on a list, we first determine if the new value assigned is an object, and if it is, we hijack it again and add the same observer as the list.
Let’s modify the code a bit more:
// View adds v-if conditional judgment<ul>
<li v-for="(v, i) in list" :key="i" v-if="v.status === '1'">{{v.text}}</li>
</ul>// The status attribute is added for 2 seconds. mounted() { setTimeout(_ => { this.list = [{text: 666}, {text: 666}, {text: 666}]; }, 1000); setTimeout(_ => { this.list.forEach((v, i) => { v.text = i; v.status = '1'; // Add state}); }}, 2000)Copy the code
As above, we added the v-if state judgment in the view, and set the state at 2 s. But instead of showing 0, 1, and 2 at 2s, as we expected, the view stays blank.
This is a mistake many novices make because there are often similar requirements. This is where we mentioned earlier that Vue cannot detect the addition or deletion of object attributes. What should we do if we want to achieve the desired results? Is simple:
// The status attribute is preset when the assignment is performed at 1 s.
setTimeout(_= > {
this.list = [{text: Awesome!.status: '0'}, {text: Awesome!.status: '0'}, {text: Awesome!.status: '0'}];
},1000);
Copy the code
Of course, Vue also provides the vm.$set(target, key, value) method to handle the operation of adding attributes in certain cases, but it is not applicable here.
Vue responsive principle
We’ve given two examples of common mistakes and solutions, but we still only know what to do, not why.
Vue’s data hijacking relies on Object.defineProperty, so some of its features cause this problem. For those of you who don’t know this property look at MDN.
Basic implementation of Object.defineProperty
The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object. – MDN
Look at a basic case of data hijacking, which is the most fundamental dependency of responsiveness.
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true./ / can be enumerated
configurable: true.get: function() {
console.log('get');
return val;
},
set: function(newVal) {
// When setting, you can add corresponding operations
console.log('set'); val += newVal; }}); }let obj = {name: 'Jackie Chan Brother'.say: 'I actually refused to do this game commercial,'};
Object.keys(obj).forEach(k= > {
defineReactive(obj, k, obj[k]);
});
obj.say = 'And THEN I tried it, and I was like, wow, it was fun.';
console.log(obj.name + obj.say);
// Jackie Chan: ACTUALLY, I refused to shoot the advertisement for this game before. Then I tried it. Wow, I am so passionate, it is fun
obj.eat = 'banana'; // ** has no response
Copy the code
As you can see, Object.defineProperty is a hijacking of an existing attribute, so Vue requires that the required data be defined in data beforehand and cannot respond to the addition or deletion of Object attributes. Hijacked attributes have corresponding GET and set methods.
In addition, the Vue documentation states that due to JavaScript limitations, Vue does not support setting array members by index. In this case, it’s actually possible to just hijack an array by subscripting it.
let arr = [1.2.3.4.5];
arr.forEach((v, i) = > { // Hijack by subscript
defineReactive(arr, i, v);
});
arr[0] = 'oh nanana'; // set
Copy the code
So why didn’t Vue do that? Utah’s official answer is performance. For a more detailed analysis of this point, you can move on. Why can’t Vue detect array changes?
Vue source code implementation
The following code is Vue version 2.6.10.
Observer
We know the basic implementation of data hijacking, and take a look at how Vue source code is done.
// observer/index.js
// Observer preprocessing method
export function observe (value: any, asRootData: ? boolean) :Observer | void {
if(! isObject(value) || valueinstanceof VNode) { // Is it an object or a virtual DOM
return
}
let ob: Observer | void
// Check if there is an __ob__ attribute. If there is an __ob__ attribute, return an Observer instance
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if ( // Check if it is a pure objectshouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value) / / create the Observer
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
/ / the Observer instance
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep() // Add an instance of Dep to the Observer for dependency collection, auxiliary vm.$set/ array methods, etc
this.vmCount = 0
// Add an __ob__ attribute to the hijacked object, pointing to its own Observer instance. As a unique identifier of whether or not an Observer.
def(value, '__ob__'.this)
if (Array.isArray(value)) { // Check if it is an array
if (hasProto) { // Determine whether the __proto__ attribute, used to handle array methods, is supported
protoAugment(value, arrayMethods) / / inheritance
} else {
copyAugment(value, arrayMethods, arrayKeys) / / copy
}
this.observeArray(value) // Hijack an array member
} else {
this.walk(value) // Hijacking objects
}
}
walk (obj: Object) { // This method is used only if the value is Object
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // Data hijacking method
}
}
observeArray (items: Array<any>) { // If it is an array, call observe to process the array member
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) // Process the array members in turn}}}Copy the code
__ob__ has a deP attribute, which is used as a collector for vm.$set, array push, etc. Vue then treats objects and arrays separately. Arrays only listen deeply to object members, which is why we can’t manipulate indexes directly. However, some array methods, such as push and pop, can respond properly. This is because of the above processing to determine whether the response object is an array. Let’s look at the code.
// observer/index.js
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
// export function observe
if (Array.isArray(value)) { // Check if it is an array
if (hasProto) { // Determine whether the __proto__ attribute, used to handle array methods, is supported
protoAugment(value, arrayMethods) / / inheritance
} else {
copyAugment(value, arrayMethods, arrayKeys) / / copy
}
this.observeArray(value) // Hijack an array member
}
/ /...
// Inherit arrayMethods directly
function protoAugment (target, src: Object) {
target.__proto__ = src
}
// Copy the array methods in turn
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
// util/lang.js The def method looks like this, used to add attributes to objects
export function def (obj: Object, key: string, val: any, enumerable? : boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable:!!!!! enumerable,writable: true.configurable: true})}Copy the code
You can see that the key point is arrayMethods. Let’s continue:
// observer/array.js
import { def } from '.. /util/index'
const arrayProto = Array.prototype // Store the method on the array prototype
export const arrayMethods = Object.create(arrayProto) // Create a new object instead of changing the array prototype method directly
const methodsToPatch = [
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
// Override the array method above
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (. args) { //
const result = original.apply(this, args) // Execute the specified method
const ob = this.__ob__ // Get the ob instance of the array
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2) // The first two arguments received by splice are subscripts
break
}
if (inserted) ob.observeArray(inserted) // The new part of the original array needs to be reobserved
// notify change
ob.dep.notify() // Publish manually, using an __ob__ deP instance
return result
})
})
Copy the code
As you can see, Vue overwrites some of the array methods and publishes them manually when they are called. But we haven’t seen the data hijacking part of Vue yet. In the first observer function code, there is a defineReactive method. Let’s look at it:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
const dep = new Dep() // instance a Dep instance
const property = Object.getOwnPropertyDescriptor(obj, key) // Get the object's own properties
if (property && property.configurable === false) { // There is no need to hijack if there are no attributes or attributes are not writable
return
}
// Compatible with predefined getters/setters
const getter = property && property.get
const setter = property && property.set
if((! getter || setter) &&arguments.length === 2) { // Initialize val
val = obj[key]
}
// Defaults to listen on child objects, starting with observe and returning an __ob__ attribute, an Observer instance
letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val // Execute the default getter to get the value
if (Dep.target) { // Rely on the collection key
dep.depend() // Dependency collection takes advantage of function closures
if (childOb) { // Add the same dependency if there are child objects
childOb.dep.depend() // Observer this.dep = new dep ();
if (Array.isArray(value)) { // call the array method if value is an array
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// Compare the old value with the new value
// newVal ! == newVal && value ! == value this is a little bit more interesting, but it's really for NaN
if(newVal === value || (newVal ! == newVal && value ! == value)) {return
}
if(process.env.NODE_ENV ! = ='production' && customSetter) {
customSetter()
}
if(getter && ! setter)return
if (setter) { // Execute the default setter
setter.call(obj, newVal)
} else { // No default direct assignmentval = newVal } childOb = ! shallow && observe(newVal)// Whether to observe the newly set value
dep.notify() // published to take advantage of function closures}})}// Handle arrays
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend() // If the array member has __ob__, add the dependency
if (Array.isArray(e)) { // Array members are still arrays, recursive calls
dependArray(e)
}
}
}
Copy the code
Dep
In the above analysis, we understand Vue data hijacking and array method rewriting, but we have a new question, what is Dep for? Dep is a publisher that can be subscribed to by multiple observers.
// observer/dep.js
let uid = 0
export default class Dep {
statictarget: ? Watcher; id: number; subs:Array<Watcher>;
constructor () {
this.id = uid++ / / the only id
this.subs = [] // Set of observers
}
// Add an observer
addSub (sub: Watcher) {
this.subs.push(sub)
}
// Remove the observer
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () { // Core, if there is a dep. target, then the dependency collection operation
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice() // Avoid contaminating the original collection
// If the execution is not asynchronous, sort first to ensure that the observer executes in order
if(process.env.NODE_ENV ! = ='production' && !config.async) {
subs.sort((a, b) = > a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // Publish execution
}
}
}
Dep.target = null // Core, used for closures, holds specific values
const targetStack = []
// Assign the current Watcher to dep. target and add it to the target stack
export function pushTarget (target: ? Watcher) {
targetStack.push(target)
Dep.target = target
}
// Remove the last Watcher and assign the last remaining target stack to dep.target
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]}Copy the code
Watcher
Dep may not be easy to understand in isolation, but let’s look at it in conjunction with Watcher.
// observer/watcher.js
let uid = 0
export default class Watcher {
// ...
constructor (
vm: Component, // Component instance object
expOrFn: string | Function.// The expression, function, or string to observe, as long as it triggers the value operation
cb: Function.// Callback after observed changes
options?: ?Object./ / parametersisRenderWatcher? : boolean// is the observer of the render function
) {
this.vm = vm // Watcher has a VM attribute that indicates which component it belongs to
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this) // Add an observer instance to the component instance's _Watchers attribute
// options
if (options) {
this.deep = !! options.deep/ / depth
this.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.sync// Synchronize execution
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb / / callback
this.id = ++uid // uid for batching //
this.active = true // Whether the observer instance is active
this.dirty = this.lazy // for lazy watchers
// Avoid relying on duplicate collection processing
this.deps = []
this.newDeps = []
this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
? expOrFn.toString()
: ' '
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else { // a string similar to obj.a
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop / / empty functionprocess.env.NODE_ENV ! = ='production' && warn(
`Failed watching path: "${expOrFn}"` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
get () { // Triggers the value operation, which triggers the getter for the property
pushTarget(this) // Dep: assign a value to dep.target
let value
const vm = this.vm
try {
// Core, which adds watcher to the closure by running the observer expression, doing the evaluation, and firing the getter
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
throw e
}
} finally {
if (this.deep) { // If deep monitoring is required, perform the operation on value
traverse(value)
}
// Clean up the dependency collection
popTarget()
this.cleanupDeps()
}
return value
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) { // Avoid relying on duplicate collections
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this) // dep adds subscribers
}
}
}
update () { / / update
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run() // Synchronization runs directly
} else { // If not, join the asynchronous queue for execution
queueWatcher(this)}}}Copy the code
At this point, we can summarize some of the overall flow of a responsive system, also known as the observer pattern: the first step, of course, is to hijack data through the Observer, and then subscribe where necessary (e.g. Template compilation), add an observer (watcher), and immediately add the observer to the Dep by triggering the getter for the specified property via the value operation (which takes advantage of closures for dependency collection), and then notify when the Setter fires. Notify all observers and update them accordingly.
One way to think about the observer model is that the Dep is like the nuggets, and the nuggets have many authors (equivalent to many attributes of Data). Naturally, we all play the role of watcher and follow the authors we are interested in in Dep. For example, we tell jiang SAN Crazy to remind me to read when jiang SAN crazy updates. So whenever there is new content in Jiang SAN Crazy, we will receive a reminder like this: Jiang SAN Crazy has released “2019 Front-end Advanced Road ***”, and then we can watch it.
However, each Watcher can subscribe to many authors, and each author will update the article. So will users who don’t follow Jiang get a reminder? Can’t, send reminder to already subscribed user only, and only jiang SAN crazy updated just remind, you subscribe is Jiang SAN crazy, but stationmaster updated need to remind you? Of course not. That’s what closures need to do.
Proxy
Proxy can be understood as a layer of “interception” before the target object. All external access to the object must pass this layer of interception. Therefore, Proxy provides a mechanism for filtering and rewriting external access. Ruan Yifeng’s Introduction to ECMAScript 6
We all know that Vue 3.0 replaces Object.defineProperty with a Proxy, so what are the benefits of doing so?
The benefits are obvious, such as the two existing problems with Vue above, the inability to respond to the addition and deletion of object attributes and the inability to manipulate array subscript directly, can be solved. The downside, of course, is compatibility issues that Babel has yet to solve.
Basic usage
We use Proxy to implement a simple data hijacking.
let obj = {};
/ / agent obj
let handler = {
get: function(target, key, receiver) {
console.log('get', key);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log('set', key, value);
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
console.log('delete', key);
delete target[key];
return true; }};let data = new Proxy(obj, handler);
// Only data can be used after proxy, otherwise obj will not work
console.log(data.name); // get name γundefined
data.name = 'Yin Tianqiu'; // set name
delete data.name; // delete name
Copy the code
In this case, obj is an empty object that can be added and removed by Proxy to get feedback. Let’s look at array proxies:
let arr = ['Yin Tianqiu'.'I'm an actor'.'Fluttering willow'.'Dead walk-on'];
let array = new Proxy(arr, handler);
array[1] = 'I'll feed you.'; // set 1 I will support you
array[3] = 'Mind your own business first, fool. '; // Set 3 Mind your own business, fool.
Copy the code
Array index Settings are also fully controlled, of course, the use of Proxy is not only these, there are 13 interception operations. Interested students can go to see ruan Yifeng teacher’s book, here is no longer wordy.
Proxy implements the observer pattern
We’ve analyzed the Vue source code and looked at the basics of observer mode. So how do you implement an observer with a Proxy? We can write it very simply:
class Dep {
constructor() {
this.subs = new Set(a);// Set type, guaranteed not to repeat
}
addSub(sub) { // Add subscribers
this.subs.add(sub);
}
notify(key) { // Notify subscribers of updates
this.subs.forEach(sub= >{ sub.update(); }); }}class Watcher { / / observer
constructor(obj, key, cb) {
this.obj = obj;
this.key = key;
this.cb = cb; / / callback
this.value = this.get(); // Get old data
}
get() { // Sets the trigger closure to add itself to deP
Dep.target = this; // Set dep. target to itself
let value = this.obj[this.key];
Dep.target = null; // Set the value to nul
return value;
}
/ / update
update() {
let newVal = this.obj[this.key];
if (this.value ! == newVal) {this.cb(newVal);
this.value = newVal; }}}function Observer(obj) {
Object.keys(obj).forEach(key= > { // Do deep listening
if (typeof obj[key] === 'object') { obj[key] = Observer(obj[key]); }});let dep = new Dep();
let handler = {
get: function (target, key, receiver) {
Dep.target && dep.addSub(Dep.target);
// Target exists, add it to the Dep instance
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
let result = Reflect.set(target, key, value, receiver);
dep.notify(); // Publish
returnresult; }};return new Proxy(obj, handler)
}
Copy the code
The code is short, so it’s all in one piece. Target && Dep.addSub (dep.target) ensures that when the getter for each property fires, it is the current Watcher instance. If closures are hard to understand, consider the example of a for loop that outputs 1, 2, 3, 4, 5.
Take a look at the results:
let data = {
name: 'slag glow'
};
function print1(data) {
console.log('I', data);
}
function print2(data) {
console.log('Me this year', data);
}
data = Observer(data);
new Watcher(data, 'name', print1);
data.name = 'John Steinbeck'; // I am Yang Guo
new Watcher(data, 'age', print2);
data.age = '24'; // I am 24 years old
Copy the code
MVVM
With all that talk, it’s time to practice. Vue greatly improves the productivity of front-end ER, we reference Vue this time to achieve a simple Vue framework.
Vue implementation principle – how to achieve bidirectional binding MVVM
What is MVVM?
A brief introduction to MVVM, a more comprehensive explanation, you can see here the MVVM pattern. The full name of MVVM is Model-view-ViewModel. It is an architectural pattern, which was first proposed by Microsoft and draws on the ideas of MVC and other patterns.
The ViewModel is responsible for synchronizing the Model data to the View and for synchronizing the View’s changes to the data back to the Model. The Model layer, as the data layer, only cares about the data itself, and does not care about how the data is operated and displayed. View is the View layer, which transforms the data model into a UI interface and presents it to the user.
Image from MVVM schema
How to implement an MVVM?
To figure out how to implement an MVVM, we need to at least know what an MVVM has. Let’s see what we want it to look like.
<body>
<div id="app">Name:<input type="text" v-model="name"> <br>Age:<input type="text" v-model="age"> <br>Career:<input type="text" v-model="profession"> <br>
<p>Output: {{info}}</p>
<button v-on:click="clear">empty</button>
</div>
</body>
<script src="mvvm.js"></script>
<script>
const app = new MVVM({
el: '#app'.data: {
name: ' '.age: ' '.profession: ' '
},
methods: {
clear() {
this.name = ' ';
this.age = ' ';
this.profession = ' '; }},computed: {
info() {
return I call `The ${this.name}This year,The ${this.age}, aThe ${this.profession}`; }}})</script>
Copy the code
Operation effect:
Well, it looks like it’s copying some of Vue’s basic features, such as bidirectional binding, computed, V-ON, and so on. To make it easier to understand, let’s draw a schematic.
What do we need to do now? Data hijacking, data brokering, template compilation, publish and subscribe — wait, don’t these terms look familiar? Isn’t that what we did when we analyzed the Vue source code? (Yeah, yeah, it’s a copy of Vue.) OK, we are familiar with data hijacking and publishing and subscribing, but template compilation is still unknown. No hurry, let’s get started.
new MVVM()
Following the schematic, the first step is new MVVM(), which is initialization. What do you do when you initialize it? As you can imagine, data hijacking and initialization of templates (views).
class MVVM {
constructor(options) { / / initialization
this.$el = options.el;
this.$data = options.data;
if(this.$el){ // If there is el, proceed to the next step
new Observer(this.$data);
new Compiler(this.$el, this); }}}Copy the code
There seems to be something missing, computed and methods also need to be dealt with and replaced.
class MVVM {
constructor(options) { / / initialization
// Β·Β·Β· Receive parameters
let computed = options.computed;
let methods = options.methods;
let that = this;
if(this.$el){ // If there is el, proceed to the next step
// Delegate a computed key value to this so that you can access this.$data.info directly and run the method when it is evaluated
// Note that computed requires an agent, not an Observer
for(let key in computed){
Object.defineProperty(this.$data, key, {
enumerable: true.configurable: true.get() {
returncomputed[key].call(that); }})}// Delegate methods directly to this to access this.clear
for(let key in methods){
Object.defineProperty(this, key, {
get(){
returnmethods[key]; }})}}}}Copy the code
In the above code, we put data in this.$data, but remember that we usually use this. XXX to access data. Therefore, data, like computing properties, needs a layer of proxy for easy access. The detailed process of calculating attributes will be covered in the context of data hijacking.
class MVVM {
constructor(options) { / / initialization
if(this.$el){
this.proxyData(this.$data);
/ /... omitted}}proxyData(data) { // Data broker
for(let key in data){
$data.name = this.$data.name = this.$data.name
Object.defineProperty(this, key, {
enumerable: true.configurable: true.get(){
return data[key];
},
set(newVal){ data[key] = newVal; }})}}}Copy the code
Data hijacking, publish and subscribe
After initialization we still have two steps left to process.
new Observer(this.$data); // Data hijacking + publish subscribe
new Compiler(this.$el, this); // Template compilation
Copy the code
Data hijacking and publishing and subscribing, which we’ve been talking about for a long time, should be familiar, so let’s get rid of it.
class Dep { // Publish a subscription
constructor(){
this.subs = []; // Set the watcher
}
addSub(watcher){ / / add a watcher
this.subs.push(watcher);
}
notify(){ / / release
this.subs.forEach(w= >w.update()); }}class Watcher{ / / observer
constructor(vm, expr, cb){
this.vm = vm; / / instance
this.expr = expr; // Observe the expression of the data
this.cb = cb; // Update the triggered callback
this.value = this.get(); // Save the old value
}
get(){ // Activate the data getter to add the subscription
Dep.target = this; // Set to itself
let value = resolveFn.getValue(this.vm, this.expr); / / value
Dep.target = null; // Reset to null
return value;
}
update(){ / / update
let newValue = resolveFn.getValue(this.vm, this.expr);
if(newValue ! = =this.value){
this.cb(newValue);
this.value = newValue; }}}class Observer{ // Data hijacking
constructor(data){
this.observe(data);
}
observe(data){
if(data && typeof data === 'object') {
if (Array.isArray(data)) { // If it is an array, iterate over each member of the array
data.forEach(v= > {
this.observe(v);
});
// Vue also does some special processing here, such as overwriting array methods
return;
}
Object.keys(data).forEach(k= > { // Observe each property of the object
this.defineReactive(data, k, data[k]); }); }}defineReactive(obj, key, value) {
let that = this;
this.observe(value); // The value of the object property, if it is an object or array, observe again
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get(){ // Determine whether to add Watcher to collect dependencies
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newVal){
if(newVal ! == value) { that.observe(newVal);// Observe the new value
value = newVal;
dep.notify(); / / release}})}}Copy the code
Resolvefn. getValue (resolveFn.getValue); resolveFn.getValue (resolveFn.getValue); resolveFn.getValue (resolveFn.getValue); resolveFn.getValue (resolveFn.getValue); resolveFn.getValue (resolveFn.getValue); Let’s take a closer look at this method.
resolveFn = { // Set of utility functions
getValue(vm, expr) { // Returns data for the specified expression
return expr.split('. ').reduce((data, current) = >{
return data[current]; / / this/info, this [obj] [a]}, vm); }}Copy the code
As mentioned in our previous analysis, an expression can be either a string or a function (such as a rendering function) that triggers the value operation. We’re only thinking about strings here, so where do we have expressions like this? For example, {{info}}, for example, v-model=”name” after = is the expression. It could also be of the form OBJ.A. Therefore, reduce is used to achieve a continuous value effect.
Computed attribute computed
Initialization time left a question, because involves the release subscription, so here we are detailed analysis of the properties of trigger process calculation, the initialization, the template used to {{info}}, so at the time of template compilation, You need to trigger a this.info value operation to get the actual value to replace the {{info}} string. Let’s just add an observer to that as well.
compileText(node, '{{info}}'.' ') // Suppose the compile method looks like this, with an initial value of null
new Watcher(this.'info'.() = > {do something}) // We immediately instantiate an observer
Copy the code
What actions are triggered at this point? We know that when new Watcher(), it triggers a value. This. Info will be fetched, and we will delegate it at initialization.
for(let key in computed){
Object.defineProperty(this.$data, key, {
get() {
returncomputed[key].call(that); }})}Copy the code
So in this case, you run a computed method directly, remember what a method looks like?
computed: {
info() {
return I call `The ${this.name}This year,The ${this., the age}, aThe ${this.profession}`; }}Copy the code
In this case, the name, age, and Profession operations are triggered.
defineReactive(obj, key, value) {
/ /...
let dep = new Dep();
Object.defineProperty(obj, key, {
get(){ // Determine whether to add Watcher to collect dependencies
Dep.target && dep.addSub(Dep.target);
return value;
}
/ /...})}Copy the code
This makes full use of the closure feature. Note that you are still in the process of evaluating info because it is a synchronous method, which means that the dep. target exists and is the Watcher for viewing info properties. As a result, the program adds info’s Watcher to the DEP of Name, AGE, and Profession, respectively. In this way, the Watcher of INFO is notified to revalue and update the view if any of these values changes.
Print the DEP for easy comprehension.
Template compilation
In this section, we will compile the HTML template syntax into real data and convert the instructions into corresponding functions.
You can’t avoid manipulating Dom elements during compilation, so a createDocumentFragment method is used to create document fragments. This actually uses the virtual DOM in Vue, and diff algorithms are used to do minimal rendering when updated.
The document fragment exists in memory, not in the DOM tree, so inserting child elements into the document fragment does not cause page backflow (calculation of element position and geometry). Therefore, using document fragments generally results in better performance. – MDN
class Compiler{
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el); // Get the app node
this.vm = vm;
let fragment = this.createFragment(this.el); // Convert the DOM to a document fragment
this.compile(fragment); / / compile
this.el.appendChild(fragment); // Put it back into the DOM after the changeover is complete
}
createFragment(node) { // Convert dom elements into document fragments
let fragment = document.createDocumentFragment();
let firstChild;
// Go to the first child node and put it into the document fragment until there is none, then stop the loop
while(firstChild = node.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
isDirective(attrName) { // Is it a command
return attrName.startsWith('v-');
}
isElementNode(node) { // Is an element node
return node.nodeType === 1;
}
compile(node) { // Compile the node
let childNodes = node.childNodes; // Get all child nodes
[...childNodes].forEach(child= > {
if(this.isElementNode(child)){ // Is an element node
this.compile(child); // Recursively iterate over the child nodes
let attributes = child.attributes;
// Get all attributes of the element node, v-model class, etc
[...attributes].forEach(attr= > { // For example, v-on:click="clear"
let {name, value: exp} = attr; // Struct get "clear"
if(this.isDirective(name)) { // Determine if it is an instruction attribute
let [, directive] = name.split(The '-'); // Structure fetch instruction part V-on :click
let [directiveName, eventName] = directive.split(':'); / / on, click
resolveFn[directiveName](child, exp, this.vm, eventName);
// Execute the corresponding instruction method}})}else{ // Compile text
let content = child.textContent; // Get the text node
if(/ \ {\ {(. +?) \} \} /.test(content)) { // Check whether there is template syntax {{}}
resolveFn.text(child, content, this.vm); // Replace the text}}}); }}// A method to replace text
resolveFn = { // Set of utility functions
text(node, exp, vm) {
// Lazy matching to avoid taking the last curly brace when multiple templates are in a row
/ / {{name}} {{age}} without inert match will take all at a time "{{name}} {{age}}"
["{{name}}", "{{age}}"]
let reg = / \ {\ {(. +?) \} \} /;
let expr = exp.match(reg);
node.textContent = this.getValue(vm, expr[1]); // Trigger view update at compile time
new Watcher(vm, expr[1].() = > { // Setter triggers publication
node.textContent = this.getValue(vm, expr[1]); }); }}Copy the code
While compiling the element node (this.pile (node)), we determine if the element attribute is a directive and invoke the corresponding directive method. So finally, let’s look at some simple implementations of the instructions.
- Bidirectional binding V-Model
resolveFn = { // Set of utility functions
setValue(vm, exp, value) {
exp.split('. ').reduce((data, current, index, arr) = >{ //
if(index === arr.length-1) { // Set the value for the last member
return data[current] = value;
}
return data[current];
}, vm.$data);
},
model(node, exp, vm) {
new Watcher(vm, exp, (newVal) = > { // Add observers, data changes, update views
node.value = newVal;
});
node.addEventListener('input'.(e) = > { // Listen for input events (view changes) that fire to update data
let value = e.target.value;
this.setValue(vm, exp, value); // Set the new value
});
// Triggered at compile time
let value = this.getValue(vm, exp); node.value = value; }}Copy the code
Bidirectional binding should be easy to understand. It should be noted that setValue cannot be set directly with the return value of Reduce. Because at this point the return value, it’s just a value, it doesn’t do the job of reassigning.
- Event binding V-ON
Remember how we handled the methods when we initialized?
for(let key in methods){
Object.defineProperty(this, key, {
get(){
returnmethods[key]; }})}Copy the code
We delegate all methods to this, and we deconstruct the instructions into ‘on’, ‘click’, and ‘clear’ when we compile V-on :click=”clear”.
on(node, exp, vm, eventName) { // Listen for events on the corresponding node and invoke the corresponding method on this when triggered
node.addEventListener(eventName, e= >{ vm[exp].call(vm, e); })}Copy the code
Vue provides many more instructions, such as v-if, which actually adds or removes DOM elements; V-show, where the display attribute of the operation element is block or None; V-html, which adds the instruction value directly to the DOM element, can be implemented using innerHTML, but this operation is unsafe and XSS risky, so Vue also recommends not exposing the interface to the user. There are also v-for, V-slot and other more complex instructions, you can explore for yourself.
conclusion
The full article code is at the article repository πΉπ°fe-code. This issue focuses on the reactive principles of Vue, including data hijacking, publish and subscribe, the difference between Proxy and Object.defineProperty, etc., along with a simple MVVM. As an excellent front-end framework, Vue has a lot to learn, and every detail is worth studying. The follow-up will also bring a series of Vue, javascript and other front-end knowledge points of the article, interested students can pay attention to.
Refer to the article
- Analysis of Vue implementation principle – how to achieve bidirectional binding MVVM
- Vue source code analysis
- About re, recommend yaoJavaScript Regex Mini-BookIt’s very easy to read
Communication group
Qq front-end communication group: 960807765, welcome all kinds of technical exchanges, looking forward to your joining
Afterword.
If you see here, and this article is a little help to you, I hope you can move a small hand to support the author, thank π». If there is something wrong in the article, we are welcome to point out and encourage each other.
- The article warehouse πΉ π° fe – code
- Social chat system (vue + Node + mongodb) – ππ¦πVchat
More articles:
Front end advanced road series
- Vue component communication mode complete version
- JavaScript prototype and prototype chain and Canvas captcha practice
- [2019 front-end advancement] Stop, you this Promise!
From head to toe combat series
- γ γ from head to foot WebRTC + Canvas to achieve a double collaborative Shared sketchpad essay | the nuggets technology
- γ From head to toe γ a multiplayer video chat – front-end WebRTC combat (a)
- A social chat system (vue + Node + mongodb) – ππ¦πVchat
Welcome to pay attention to the public number front-end engine, the first time to get the author’s article push, there are a large number of front-end tycoon quality articles, committed to become the engine to promote the growth of the front-end.