preface
This is also a result of blogging. I had messed around with a template engine when I was writing a blog, and I thought I would optimize it later and put in a Vue responsive principle to make it a small “framework” that would reduce the amount of component-based front-end code I needed to write a blog. This is just a small project for myself to learn the responsive principle of Vue2.0. The implementation of the code may be very preliminary, and some places will be poor, please forgive me. This article picks up where we left off with the implementation of reactive.
Project source: Portal implementation effect: My blog is transformed with this small framework
What is the MVVM pattern[1][2]?
Model-view-viewmodel abstracts the state and behavior of the View, allowing us to separate the UI from the business logic. Of course the ViewModel already does this for us, fetching the data from the Model and helping with the business logic involved in displaying content in the View.
The MVVM pattern is composed of the following three core components, each with its own unique role:
- Model – A data Model that contains business and validation logic
- View – Defines the structure, layout, and appearance of the View on the screen
- ViewModel – Act as the emissary between “View” and “Model”, helping to process all the business logic of View, connecting the View layer and Model layer through two-way data binding.
The responsive principle of Vue2.0
How is vUE’s responsiveness implemented? Object.defineproperty is the key. It can be used to convert all incoming VUE instance properties into getters/setters. Something like this:
function defineReactive(obj, key, value) {
let val = value
Object.defineProperty(obj, key, {
enumerable: true./ / can be enumerated
configurable: true.// The property descriptor can be changed and deleted from the corresponding object only if and if the property is configured differently. The default is false.
get: function () {
/ / val closures
return val
},
set: function (newVal) {
// Val is always in the closure, and after this is set, the latest value will be retrieved when we get it
val = newVal
}
})
}
Copy the code
We can listen when obJ pairs change the key value. So how to implement the relevant operation after the data changes?
Vue’s responsiveness consists of three major parts: Observer, Dep and Watcher.
Observer
: places each target object (i.edata
) to convert togetter/setter
Form for dependency collection and scheduling updates. For each object in reactive data, there is oneObserver
An instance of theDep
:Observer
The instancedata
In the triggergetter
When,Dep
The instance will collect dependenciesWatcher
Instance,Dep
An instance is an administrator that can manage more than oneWatcher
Instance, whendata
When it changes, it passesDep
Instance toWatcher
Instance sends notifications for updates. One property for each object in reactive dataDep
An instance of theWatcher
: is an observer object. After relying on collectionWatcher
Objects can beDep
Instance management, data changesDep
The instance will notifyWatcher
Instance, and then fromWatcher
Instance to update the view.
The idea of responsive implementation
An observe function is passed in as data and returns an Observer instance. The flow of the observe function is:
- Checks whether the passed object is an array or an object, otherwise returns
undefined
- If so, it determines whether it is already responsive, and if so, it returns what already exists
Observer
Instance, otherwise instantiate a new oneObserver
Instance and return.
/** * listen * @param {Object} data */
export default function observe(data) {
if (typeofdata ! = ='object' || data === null) {
// Not objects or arrays
return
}
let ob
if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observer) {
ob = data.__ob__ //__ob__ is an Observer instance of defined responsive data
} else {
ob = new Observer(data)
}
return ob
}
Copy the code
Observer
The realization of the class
So let’s look at the Observer class definition:
class Observer {
constructor(data) {
this.data = data // Initialize the property
this.dep = new Dep()// Initialize the deP instance, which is used in array listening
def(data, '__ob__'.this) // Add a property to the object itself pointing to the responsive object to determine whether the data has become responsive and can be found through the data object
if (Array.isArray(data)) {// Check whether it is an array
if ('__proto__' in {}) {
data.__proto__ = arrProto
} else {
addSelfMethod(data, arrProto, arrayKeys)
}
this.observeArr(data)
} else {// If the object is listening on the object
this.observeObj(data)
}
}
// Listen to the object
observeObj(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
// Listen for arrays
observeArr(arr) {
for (let i = 0; i < arr.length; i++) {
observe(arr[i])
}
}
}
Copy the code
From the definition, there are several key points in the instantiation of the Observer class: instance attribute DEP, array listener, and object listener, so let’s look at the implementation of the DEP class.
Dep
The realization of the class
export default class Dep {
constructor() {
this.subs = [] // Array to manage the Watcher instance
}
addSub(sub) { // Add the Watcher instance
this.subs.push(sub)
}
notify(isArrayMethod = false) { // Call the update method of the Watcher instance
this.subs.forEach(function (sub) {
sub.update(isArrayMethod)
})
}
}
Dep.target = null // Static property for adding Watcher instances
Copy the code
As you can see from the above code, the implementation of the Dep class is relatively simple, just need to maintain an array management Watcher instance, you can add the Watcher instance, notify the Watcher instance, and call its update method. The notify parameter isArrayMethod is not implemented in the vUE source code. It is only used to identify whether array changes are generated by array methods, and then respond to them.
Now that the implementation of the Dep class is clear, let’s go back to the instantiation of the Observer class and first look at how to implement object listening.
Object listening
In the definition of the Observer class, listening on an object is to call defineReactive on each attribute of the object, which is a key part of the reactive approach, as shown in the following code.
@param {Object} data * @param {string} Key attribute name * @param {*} val value */
function defineReactive(obj, key, value) {
let val = value
let childOb = observe(val) If the value is not an object or array, childOb is undefined
let dep = childOb ? childOb.dep : new Dep()// If the attribute is an object or an array, it is managed by the attribute dep of its Observer instance; otherwise, it is instantiated by a deP instance, using closures to manage the original value
Object.defineProperty(obj, key, {
enumerable: true./ / can be enumerated
configurable: true.// The property descriptor can be changed and deleted from the corresponding object only if and if the property is configured differently. The default is false.
get: function () {
/ / val closures
if (Dep.target) { // Add an instance of watcher
dep.addSub(Dep.target)
}
return val
},
set: function (newVal) {
// If the object's attribute is an object or an array, then because it is a reference type, after the value changes, we need to inherit the 'Watcher' instance of the original responsive data deP management, and then listen deeply
if (childOb) {
let temp = dep
childOb = observe(newVal) // Recursive depth traversal to achieve depth monitoringchildOb.dep.subs.push(... temp.subs) dep = childOb.dep }// Val is always in the closure, and after this is set, the latest value will be retrieved when we get it
val = newVal
dep.notify()
}
})
}
Copy the code
In the above code, there are three special places:
let dep = childOb ? childOb.dep : new Dep()
Why is the property an object or an arrayObserver
Instance propertiesdep
To manage? In the process of listening on an array, you need this property to implement listening on array methodsget
In the function, with respect todep
managementWatcher
Example that part of the codeDep.target
What is?Dep.target
What’s stored here isWatcher
Instance, because only one can exist at a timeWatcher
Instances are managed. It may not be clear, but it will be explained later in the dependency collectionset
Recursive traversal of new values in functions is problematic.- I’m explaining in the code comment that it’s not exactly right if the property of the object is
Object
orArray
If the new value is of the same type and the dependent attributes inside the value are the same, this is fine. - If the property corresponding to the object was the original value, the new value is
Object
orArray
, then the new value cannot be converted to the response - If the corresponding property of the object is
Object
orArray
, and the new value is the original value, this code will report an error. - There is a line of code that aims to bring the original responsive data
dep
The management ofWatcher
Instance inherited, but I didn’t take into account the originalWatcher
Instances where this may result if not neededdep
thesubs
The array gets bigger and bigger, and then it runs out of memory. - But it’s enough to explain the response, so this code is not optimized.
- I’m explaining in the code comment that it’s not exactly right if the property of the object is
// If the object's attribute is an object or an array, then because it is a reference type, after the value changes, we need to inherit the 'Watcher' instance of the original responsive data deP management, and then listen deeply
if (childOb) {
let temp = dep
childOb = observe(newVal) // Recursive depth traversal to achieve depth monitoringchildOb.dep.subs.push(... temp.subs)// Inherit the original responsive data deP managed 'watcher' instance
dep = childOb.dep
}
Copy the code
Array listening
In the Observer class definition, Array listening is implemented like this:
if ('__proto__' in {}) {
data.__proto__ = arrProto
} else {
addSelfMethod(data, arrProto, arrayKeys)
}
this.observeArr(data)
Copy the code
In this code, observeArr doesn’t have to be explained. The emphasis is on the implementation of data.__proto__ = arrProto and addSelfMethod. Before explaining both of these, it is important to understand that array listening refers to listening for changes caused by some common array methods. Changes of the type arr[0]=4 and arr.length=1 cannot be listened for.
Here we take advantage of the principle of prototype chain. Let’s take a look at the implementation of arrProto and arrayKeys.
// Redefine the array prototype
const oldArrayProperty = Array.prototype
// Create a new object, the prototype points to oldArrayProperty, and extending the new method does not affect the prototype
const arrProto = Object.create(oldArrayProperty) ; ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'].forEach(
(methodName) = > {
arrProto[methodName] = function (. args) {
const result = oldArrayProperty[methodName].call(this. args)// Execute the original array method
const ob = this.__ob__ // Get the corresponding 'Observer' instance
let inserted
switch (methodName) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArr(inserted) // Listen for new values
ob.dep.notify(true) // Notification of change
return result
}
}
)
export default arrProto
const arrayKeys = Object.getOwnPropertyNames(arrProto)
Copy the code
__proto__ is not a standard attribute, so some browsers may not implement it. If it is, then let __proto__ of the original array point to the object we have changed. This will allow us to listen on array methods. If it does not exist, addSelfMethod is called to add the corresponding non-enumerable method to the object to implement the interception.
/** * * @param {obj} target * @param {*} src * @param {*} keys */
function addSelfMethod(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])// Add the corresponding method to the array object itself}}Copy the code
It is already clear why a DEP instance exists on an Observer instance, and why objects or arrays should be managed by a DEP instance on an Observer instance. This is because it can be accessed and called inside non-definereActive functions.
At this point, with the exception of Watcher, the main responsive core is complete. So, let’s put together a simple reactive class.
The basis ofMVue
The realization of the class
As a framework, there must be an entry point, which is a reactive instance, which is responsible for converting data to reactive, and some lifecycle hooks, which are relatively easy to implement.
import observe from './observer/index'
import { proxy } from './utils/index'
export default class MVue {
constructor(options) {
const { el, data, methods, created } = options
if (data) {
this.data = typeof data === 'function' ? data() : data // The function gets the return value
proxy(this.data, this)
observe(this.data) // Convert the response
}
Object.assign(this, methods)
if (el) {
this.elSelector = el
}
created && created.call(this)}}Copy the code
This implements a simple reactive class that calls observe to implement reactive data, where the proxy function accesses the instance. A is equivalent to.data.a, with the following code:
export function proxy(data, mVue) {
const me = mVue
Object.keys(data).forEach(function (key) {
Object.defineProperty(me, key, {
configurable: false.enumerable: true.get: function () {
return me.data[key]
},
set: function (newVal) {
me.data[key] = newVal
}
})
})
}
Copy the code
Depend on the collection
Now that you have responsive instances, and data changes can be monitored, how do you do that? This refers to dependency collection. For dependency collection lessons refer to this article. As I understand it, dependent collection is the reactive data used in the template to perform rendering based on changes in the data. That is, using the properties of a reactive object, add a Watcher instance to the corresponding DEP instance.
So let’s go back to the get function in the defineReactive function that the object listens on.
get: function () {
/ / val closures
if (Dep.target) { // Add an instance of watcher
dep.addSub(Dep.target)
}
return val
}
Copy the code
When we access the response data, we go through the get function. If we need to collect dependencies, then the dep. target value is the Watcher instance to be managed. Dep.target = null if no collection is required.
So let’s tidy up the idea of relying on collection: Suppose we need the value of attribute A of reactive data in our template. During template compilation, we instantiate a Watcher instance and define the related operations. Then we point dep. target to the Watcher instance to access this attribute in the reactive instance. Target = null and wait for the next collection.
So how do you access the data of a reactive instance?
@param {String} expPath variable path */
export function parsePath(expPath) {
let path = expPath
// Implement access to arrays similar to arr[0]
if (path.indexOf('[')) {
path = path.replace(/\[/g.'. ')
path = path.replace(/\]/g.'. ')
if (/ \. $/.test(path)) {
path = path.slice(0, path.length - 1)}if (/ \ \. /.test(path)) {
path = path.replace('.. '.'. ')}}const bailRE = /[^\w.$]/
if (bailRE.test(path)) {
return
}
const segments = path.split('. ')
return function (object) {
let obj = object
for (let i = 0; i < segments.length; i++) {
if (typeof obj === 'undefined') return ' '
let exp = segments[i]
obj = obj[exp]
}
if (typeof obj === 'undefined') return ' '
return obj
}
}
Copy the code
The above function is to achieve access to responsive data. Passing in the corresponding expression (such as A.B.C) will obtain the corresponding function. Passing in the corresponding responsive instance, the function can obtain the value of the variable by executing the function, that is, calling the GET function.
Watcher
The realization of the class
All that’s left is an executor responsible for executing the action, Watcher, and here’s the code.
import { parsePath } from '.. /utils/index'
import Dep from '.. /observer/dep.js'
export default class Watcher {
constructor(mVue, exp, callback) {
this.callback = callback // The callback function
this.mVue = mVue // Reactive instances
this.exp = exp / / expression
Dep.target = this // Start relying on collections
this.value = this.get() / / call the get
Dep.target = null // Add
this.update() // This is the first execution
}
async update(isArrayMethod = false) {
const value = this.get()
if (this.value ! == value || isArrayMethod) {this.callback(value, this.value) // Call the callback
this.value = value
}
}
get() {
const getter = parsePath(this.exp)
return getter.call(this.mVue, this.mVue)
}
}
Copy the code
As you can see from the above code, dependency collection takes place at instantiation time. We should just define callbacks at template compilation time.
conclusion
And if I write that, I’m pretty much done with the response. And finally, the whole idea.
-
MVVM mode introduction ↩︎
-
Vue. Js and MVVM ↩ ︎