There are tons of articles on VUE two-way data binding on the web, and this article is just a summary.
VUE bidirectional data binding uses documentFragment, object.defineproperty, proxy and publish and subscribe mode. The following introduces these knowledge points respectively, and then uses them to write a JS native bidirectional data binding case.
DocumentFragment
Create a new blank document fragment. DocumentFragments are DOM nodes. They are not part of the main DOM tree. The usual use case is to create a document fragment, attach elements to the document fragment, and then attach the document fragment to the DOM tree. In the DOM tree, the document fragment is replaced by all of its child elements. Because the document fragment exists in memory, not in the DOM tree, inserting child elements into the document fragment does not cause page reflow (calculations of element position and geometry). Therefore, the use of document fragments is often used to optimize performance.
Demo
<body>
<ul data-uid="ul"></ul>
</body>
<script>
let ul = document.querySelector(`[data-uid="ul"]`),
docfrag = document.createDocumentFragment();
const browserList = [
"Internet Explorer"."Mozilla Firefox"."Safari"."Chrome"."Opera"
];
browserList.forEach((e) => {
let li = document.createElement("li");
li.textContent = e;
docfrag.appendChild(li);
});
ul.appendChild(docfrag);
</script>
Copy the code
defineProperty
The attributes of objects are divided into data attributes and accessor attributes. If you want to change the default properties of an Object, you must use the Object.defineProperty method, which takes three parameters: the Object of the attribute, the name of the attribute, and a descriptor Object.
Data attributes:
A data property contains a location from which a data value can be read and written, and a data property has four properties that describe its behavior.
Configurable
: indicates whether the application can passdelete
Redefining a property by deleting it, changing its properties, or changing it to an accessor property. The default value is true.Enumberable
: indicates whether the attribute can be returned through a for-in loop. The default value istrue
.Writable
: Indicates whether the value of an attribute can be modified. The default value istrue
.Value
: Contains the data value for this property. When reading property values, read from this position; When finalizing the property value, save the new value in this location. The default value istrue
.
Accessor properties:
Accessor properties do not contain data values; They contain a pair of getters and setters (two are not required). When the accessor property is read, the getter function is called, which returns a valid value; When the accessor property is written, the setter function, which determines how to process the data, is called and the new value is passed in. The accessor property has the following four properties.
Configurable
: indicates whether the application can passdelete
Delete attributes to redefine attributes, can modify attributes, or can change attributes to data attributes. The default value istrue
.Enumerable
: indicates whether the application can passfor-in
The loop returns the property. The default value istrue
.Get
: The function that is called when a property is read. The default value isundefined
.Set
: The function that is called when the property is finalized. The default value isundefined
.
Demo
var book = {
_year: 2018,
edition: 1
};
Object.defineProperty(book, "year", {
get: function() {return this._year;
},
set: function(newVal){
if(newVal > 2008){ this._year = newVal; this.edition += newVal - 2008; }}}); book.year = 2019; console.log(book._year); //2019 console.log(book.edition); / / 12Copy the code
Object. DefineProperty defects:
- Data hijacking can only be carried out for attributes, and deep traversal is required for JS object hijacking.
- You can’t listen for changes to the data in arrays, but through some
hack
Methods to achieve, e.gpush
,pop
,shift
,unshift
,splice
,sort
,reverse
.See the documentation for
proxy
The new ES6 approach, which can be understood as a layer of “interception” in front of the target object, provides a mechanism for filtering and overwriting external access to the object. The word Proxy is used to mean that it acts as a Proxy for certain operations. Proxy supports the following methods:
get()
: intercepts reading of object properties.set()
: Intercepts the setting of object properties.apply()
: intercepts function calls,call
andapply
Operation.has()
: This method takes effect when determining whether an object has an attribute and returns a Boolean value. It takes two parameters: the target object and the name of the property to be queried.construct()
: Used for interceptionnew
Command. Parameters:target
(Target object),args
(constructor argument object),newTarget
(When creating an instance object,new
The constructor of the command function).deleteProperty()
Intercept:delete proxy[propKey]
Returns a Boolean value.defineProperty()
Intercept:object.defineProperty
Operation.getOwnPropertyDescriptor()
Intercept:object.getownPropertyDescriptor()
, returns a property description object orundefined
.getPrototypeOf()
: Used to intercept and retrieve object prototypes. Can interceptObject.prototype.__proto__
,Object.prototype.isPrototypeOf()
,Object.getPrototypeOf()
,Reflect.getPrototypeOf()
,instanceof
isExtensible()
Intercept:Object.isExtensible
Operation to return a Boolean value.ownKeys()
: Intercepts the reading of an object’s own properties. Can interceptObject.getOwnPropertyNames()
,Object.getOwnPropertySymbols()
,Object.keys()
,for... in
Cycle.preventExtensions()
Intercept:Object.preventExtensions()
, returns a Boolean value.setPrototypeOf()
Intercept:Object.setPrototypeOf
Methods.revocable()
: Returns a cancelableproxy
Instance.
Demo
<body>
<input type="text" id="input">
<p id="p"></p>
</body>
<script>
const input = document.getElementById('input');
const p = document.getElementById('p');
const obj = {};
const newObj = new Proxy(obj, {
get: function(target, key, receiver) {
console.log(`getting ${key}! `);return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(target, key, value, receiver);
if (key === 'text') {
input.value = value;
p.innerHTML = value;
}
returnReflect.set(target, key, value, receiver); }}); input.addEventListener('keyup'.function(e) {
newObj.text = e.target.value;
});
</script>
Copy the code
Design pattern – Publish subscribe pattern
The observer and publish-subscribe modes are easy to mix, so here’s a quick distinction.
- Observer pattern: An object (called subject) maintains a series of dependent objects (called observers), notifying them automatically of any changes in state (observers).
- Publish subscribe mode: Based on a topic/event channel, the object that wants to receive notifications (called subscriber) subscribes to a topic through a custom event, and the object whose event is activated (called publisher) is notified by publishing a topic event.
Differences:
- The Observer pattern defines a one-to-many dependency by requiring observers to subscribe to events that change content;
- Publish/Subscribe uses a topic/event channel between the subscriber and publisher;
- In observer mode, the observer is “forced” to perform content change events (Subject content events). In publish/subscribe mode, subscribers can customize event handlers;
- There is a strong dependency between two objects in observer mode. Publish/subscribe the degree of coupling between two objects.
Demo
// vm.$on
export function eventsMixin(Vue: Class<Component>) {
const hookRE = /^hook:/
// The parameter type is a string or an array of strings
Vue.prototype.$on = function (event: string | Array<string>, fn: Function) :Component {
const vm: Component = this
// The incoming type is array
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn)
// Pass in the appropriate callback}}else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true}}return vm
}
// vm.$emit
Vue.prototype.$emit = function (event: string) :Component {
const vm: Component = this
if(process.env.NODE_ENV ! = ='production') {
const lowerCaseEvent = event.toLowerCase()
if(lowerCaseEvent ! == event && vm._events[lowerCaseEvent]) { tip(`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}". `)}}let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments.1)
for (let i = 0, l = cbs.length; i < l; i++) {
try {
cbs[i].apply(vm, args)// Execute the callback passed in earlier
} catch (e) {
handleError(e, vm, `event handler for "${event}"`)}}}return vm
}
}
Copy the code
MVVM process analysis
The following native MVVM small framework mainly for Compile(template compilation), Observer(data hijacking), Watcher(data listening) and Dep(publish subscription) several parts to implement. The process can be referred to the following figure:
Mvm. HTML page, instantiating a VUE object
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="message.a">
<ul>
<li>{{message.a}}</li>
</ul>
{{name}}
</div>
<script src="mvvm.js"></script>
<script src="compile.js"></script>
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
let vm = new MVVM({
el:'#app'.data: {
message: {
a: 'hello'
},
name: 'haoxl'}})</script>
</body>
</html>
Copy the code
Mvvm. js is mainly used to hijack data and mount nodes to $EL and data to $data.
class MVVM{
constructor(options) {
// Mount the parameters to the MVVM instance
this.$el = options.el;
this.$data = options.data;
// Start compiling if there is a template to compile
if(this.$el){
// Data hijacking - Change all attributes of an object to get and set methods
new Observer(this.$data);
// Delegate data from this.$data to this
this.proxyData(this.$data);
// Compile with data and elements
new Compile(this.$el, this);
}
}
proxyData(data){
Object.keys(data).forEach(key= >{
Object.defineProperty(this, key, {
get(){
return data[key]
},
set(newValue){
data[key] = newValue
}
})
})
}
}
Copy the code
Observer.js uses Object.defineProerty to hijack data and respond to data changes in combination with a publish-and-subscribe pattern.
class Observer{
constructor(data){
this.observe(data);
}
observe(data){
// Change the data attribute to set and get. If data is not an object, return it directly
if(! data ||typeofdata ! = ='object') {return;
}
// To hijack the data one by one, obtain the key and value of the data first
Object.keys(data).forEach(key= > {
/ / hijacked
this.defineReactive(data, key, data[key]);
this.observe(data[key]);// Recursive hijacking of objects in data
});
}
defineReactive(obj, key, value) {
let that = this;
let dep = new Dep();// Each change corresponds to an array that holds all the updated operations
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.// The method that fires when the value is specified
get(){// The method to call when the value is specified
Dep.target && dep.addSub(Dep.target)
return value;
},
// The method that fires when an assignment is performed
set(newValue){
// Assign new values to attributes in data
if(newValue ! == value){// If the object continues to hijack
that.observe(newValue);
value = newValue;
dep.notify();// Notify everyone that the data is updated}})}}//
class Dep{
constructor() {// An array of subscriptions
this.subs = []
}
// Add a subscription
addSub(watcher){
this.subs.push(watcher);
}
notify(){
// Call watcher's update method
this.subs.forEach(watcher= >watcher.update()); }}Copy the code
watcher.js
// The purpose of the observer is to add an observer to the element that needs to be changed, and execute the corresponding method when the data changes
class Watcher{
constructor(vm, expr, cb){
this.vm = vm;
this.expr = expr;
this.cb = cb;
// Get the old value
this.value = this.get();
}
getVal(vm, expr){
expr = expr.split('. ');
return expr.reduce((prev,next) = > {//vm.$data.a.b
return prev[next];
}, vm.$data)
}
get(){
Dep.target = this;// Assign the instance to target
let value = this.getVal(this.vm, this.expr);
Dep.target = null;//
return value;// Return the old value
}
// External exposure method
update(){
// Update will be triggered when the value changes, fetching the new value. The old value is already stored in value
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value;
if(newValue ! == oldValue){this.cb(newValue);// Call the watch callback}}}Copy the code
Compile. Js creates DOM nodes from the DocumentFragment document and then renders the data to this region using the regular parsing {{}}.
class Compile{
constructor(el, vm){
//el is the root node of the MVVM instance
this.el = this.isElementNode(el) ? el:document.querySelector(el);
this.vm = vm;
// Start compiling if the element is available
if(this.el) {
//1. Move these real DOM fragments into memory
let fragment = this.node2fragment(this.el);
//2. Compile => Extract the desired element node V-model or text node {{}}
this.compile(fragment);
//3. Insert the compiled fragment into the page
this.el.appendChild(fragment); }}/* Helper method */
// Determine if it is an element
isElementNode(node){
return node.nodeType === 1;
}
// Is it a command
isDirective(name){
return name.includes('v-');
}
/* Core method */
// Put everything in el into memory
node2fragment(el){
// Document fragmentation - document fragmentation in memory
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;// Nodes in memory
}
// Compile the element
compileElement(node){
// Get all attributes of the node
let attrs = node.attributes;
Array.from(attrs).forEach(attr= > {
// Check whether the attribute name contains v-
let attrName = attr.name;
if(this.isDirective(attrName)){
// take the corresponding value and put it in the node
let expr = attr.value;
// There may be multiple instructions, such as V-model, V-text, v-html, so take the corresponding method to compile
let [,type] = attrName.split(The '-');[v,model] [v,model]
CompileUtil[type](node, this.vm, expr)
}
})
}
compileText(node){
/ / take {{}}
let expr = node.textContent;
let reg = /\{\{([^}]+)\}\}/g;
if(reg.test(expr)){
CompileUtil['text'](node, this.vm, expr);
}
}
compile(fragment){
// Children of the current parent node, containing text nodes, array like objects
let childNodes = fragment.childNodes;
// Convert to an array and loop through the type of each node
Array.from(childNodes).forEach(node= > {
if(this.isElementNode(node)) {// is the element node
// Compile the element
this.compileElement(node);
// If it is an element node, you need to recurse again
this.compile(node)
}else{// is a text node
// Compile text
this.compileText(node); }}}})Temporarily only / / compile method of v - model and corresponding method {{}}
CompileUtil = {
getVal(vm, expr){
expr = expr.split('. ');
return expr.reduce((prev,next) = > {//vm.$data.a.b
return prev[next];
}, vm.$data)
},
getTextVal(vm, expr){// Get the compiled text content
return expr.replace(/\{\{([^}]+)\}\}/g, (... arguments)=>{return this.getVal(vm, arguments[1])
})
},
text(node, vm, expr){// Text processing
let updateFn = this.updater['textUpdater'];
// convert {{message.a}} to the value inside
let value = this.getTextVal(vm, expr);
// Match {{}} with the re, and replace the value inside
expr.replace(/\{\{([^}]+)\}\}/g, (... arguments)=>{// Add an observer when you encounter a variable in the template that needs to be replaced with a data value
// When a variable is reassigned, the method that updates the value node to the Dom is called
// The observe.js get method is called after new (instantiated)
new Watcher(vm, arguments[1],(newValue)=>{
// If the data changes, the text node needs to retrieve the dependent attributes to update the content of the text
updateFn && updateFn(node,this.getTextVal(vm, expr)); })})// Execute if there is a text processing method
updateFn && updateFn(node,value)
},
setVal(vm, expr, value){//[message,a] assigns a value to the text
expr = expr.split('. ');// Split the object into an array
/ / convergence
return expr.reduce((prev, next, currentIndex) = > {
// If the assignment starts at the last item of the object, such as message:{a:1} will be broken into message.a = 1
if(currentIndex === expr.length- 1) {return prev[next] = value;
}
return prev[next]// TODO
},vm.$data);
},
model(node, vm, expr){// Input box processing
let updateFn = this.updater['modelUpdater'];
// Add a monitor. When data changes, the watch callback should be called
new Watcher(vm, expr, (newValue)=>{
// cb is called when the value changes, passing the new value back
updateFn && updateFn(node,this.getVal(vm, expr))
});
// Add an input event to the input
node.addEventListener('input', (e) => {
let newValue = e.target.value;
this.setVal(vm, expr, newValue);
});
// Execute if there is a text processing method
updateFn && updateFn(node,this.getVal(vm, expr))
},
updater: {
// Update the text
textUpdater(node, value){
node.textContent = value
},
// Update the value of the input boxmodelUpdater(node, value){ node.value = value; }}}Copy the code
Summary: First, Vue uses the DocumentFragment to hijack all the nodes contained in the root element. These nodes include not only tag elements, but also text and even newline carriage returns. Vue then uses defindProperty() to make all the data in the data accessor properties of Vue, so that every time the data is modified, the corresponding property’s get and set methods are triggered. Then compile and process the dom nodes hijacked, traverse all nodes, determine the nodeType according to nodeType, and determine whether the node needs to be compiled according to the node’s own attributes (whether there are v-model and other attributes) or the content of the text node (whether it conforms to the format of {{text interpolation}}). For the V-Model, the binding event changes the data in the Vue when entered. For the text node, put it into the observer list as an observer watcher. When the Vue data changes, there will be a topic object, which will release the change message to the observers in the list, and the observers will update themselves and change the display in the node, so as to achieve the purpose of bidirectional binding.