Vue bidirectional data binding principle
Object.defineProperty
describe
Use to define a new property or modify an existing property on an object, and return the object
grammar
Object.defineProperty(obj, prop, descriptor)
Copy the code
parameter
-
Obj: An object on which attributes must be defined
-
Prop: The name or Symbol of the property that must be defined or modified
-
Descriptor: descriptor of a property to be defined or modified
The details are as follows:
interface Descriptor { // This property can be deleted or changed only if it is trueconfigurable? :boolean; // This property can be enumerated when true (using for... In or Object. The keys ())enumerable? :boolean; // The value corresponding to the property can be any type of value, default is undefinedvalue? :any; // The property value can be overridden only if it is truewritable? :boolean; Getters are a method of obtaining the value of an attributeget? () :any; // The setter is a method of setting the value of a propertyset? (v:any) :void; } Copy the code
Some examples:
// Examples of different signals let a = { p1: 10.p2: 'test' } Object.defineProperty(a, 'p1', { configurable: false.value: 20 }) delete a.p1 // Return false, this property cannot be deleted Copy the code
// Enumerable example let b = { p1: 10.p2: 'test' } console.log(Object.keys(b)) Return ["p1", "p2"] Object.defineProperty(b, 'p1', { enumerable: false }) console.log(Object.keys(b)) / / return/" p2" Copy the code
// Example of writable let c = { p1: 10.p2: 'test' } Object.defineProperty(c, 'p1', { writable: false.value: 20 }) c.p1 / / 20 c.p1 = 30 console.log(c.p1) / / 20 Copy the code
// Examples of getter/setter // Note: The writable and value properties are not allowed when getters or setters are used let d = { p1: 10.p2: 'test' } Object.defineProperty(d, 'p1', { get: function () { // d.p1 returns 100 all the time return 100 }, set: function (newVal) { console.log(`new value:${newVal}`)}})console.log(d.p1) d.p1 = 25 Copy the code
The return value
The object passed to the function. The first parameter, obj
compatibility
There will be compatibility issues below IE8
Implement a minimalist bidirectional binding
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" id="a">
<p id="b"></p>
</div>
<script>
let obj = {}
Object.defineProperty(obj, 'test', {
set: function (newVal) {
document.getElementById("a").value = newVal
document.getElementById("b").innerHTML = newVal
}
})
document.addEventListener('keyup'.function (e) {
obj.test = e.target.value
})
</script>
</body>
</html>
Copy the code
MVVM
The MVVM mode divides the program into three parts: Model, View, and View-Model.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
<script>
const vm = new Vue({
el: '#app'.data: {
text: 'hello world'}})</script>
</body>
</html>
Copy the code
To achieve bidirectional data binding needs to be decomposed into three steps:
- Input box, text text and data in the data binding;
- View => model;
- Model => view; model => view;
DocumentFragment
describe
Document. CreateDocumentFragment (), create a new blank Document fragments (DocumentFragment).
DocumentFragment is a DOM node. 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 and not in the DOM tree, inserting child elements into the document fragment does not cause page backflow (a calculation of element location and geometry). Therefore, using document fragments generally results in better performance.
grammar
// Fragment refers to an empty DocumentFragment object
let fragment = document.createDocumentFragment();
Copy the code
The sample
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<ul id="ul"></ul>
<script>
var element = document.getElementById('ul'); // assuming ul exists
var fragment = document.createDocumentFragment();
var browsers = ['Firefox'.'Chrome'.'Opera'.'Safari'.'Internet Explorer'];
browsers.forEach(function (browser) {
var li = document.createElement('li');
li.textContent = browser;
fragment.appendChild(li);
});
element.appendChild(fragment);
</script>
</body>
</html>
Copy the code
Hijack child node
When Vue compiles, it hijacks all the children of the mount target (nodes in the DOM are automatically removed via the Append method) into the DocumentFragment, processes the DocumentFragment, and then returns the whole DocumentFragment to insert into the mount target.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" id="a">
<p id="b"></p>
</div>
<script>
const dom = nodeToFragment(document.getElementById('app'))
console.log(dom)
function nodeToFragment (node) {
let fragment = document.createDocumentFragment()
let child
while (child = node.firstChild) {
// The appendChild method has a subtle element that removes the child from the original DOM after the call
// Hijack all child nodes of node
fragment.appendChild(child)
}
return fragment
}
</script>
</body>
</html>
Copy the code
Data initialization binding
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
<script>
function nodeToFragment(node, vm) {
let fragment = document.createDocumentFragment()
let child
while (child = node.firstChild) {
compile(child, vm)
fragment.appendChild(child)
}
return fragment
}
function compile(node, vm) {
const reg = / \ {\ {(. *) \} \} /;
// The node type is element
if (node.nodeType === 1) {
const attr = node.attributes;
// Parse the attribute
for (let i = 0; i < attr.length; i++) {
if (attr[i].nodeName === 'v-model') {
let name = attr[i].nodeValue; // Get the name of the property bound to the V-Model
node.value = vm.data[name]; // Assign the data value to the node
node.removeAttribute('v-model')}}}// The node type is text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
let name = RegExp$1.// Get the matched string
name = name.trim()
node.nodeValue = vm.data[name]
}
}
}
function Vue(options) {
this.data = options.data
const id = options.el
const dom = nodeToFragment(document.getElementById(id), this)
// After compiling, return the DOM to the app
document.getElementById(id).appendChild(dom)
}
const vm = new Vue({
el: 'app'.data: {
text: 'hello world'}})</script>
</body>
</html>
Copy the code
Reactive data binding
When we enter data into the input field, we first fire the Input event (or keyUp, change event). In the corresponding event handler, we take the value of the input field and assign it to the TEXT property of the VM instance. We’ll use defineProperty to set text in data to the VM’s getter property, so assigning a value to vm.text will trigger the set method. There are two main things to do in the set method, the first is to update the value of the property, and the second is reserved for task three.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
<script>
function nodeToFragment(node, vm) {
let fragment = document.createDocumentFragment()
let child
while (child = node.firstChild) {
compile(child, vm)
fragment.appendChild(child)
}
return fragment
}
function compile(node, vm) {
const reg = / \ {\ {(. *) \} \} /;
// The node type is element
if (node.nodeType === 1) {
const attr = node.attributes;
// Parse the attribute
for (let i = 0; i < attr.length; i++) {
if (attr[i].nodeName === 'v-model') {
let name = attr[i].nodeValue; // Get the name of the property bound to the V-Model
node.addEventListener('input'.function (e) {
vm[name] = e.target.value
})
node.value = vm[name]; // Assign the data value to the node
node.removeAttribute('v-model')}}}// The node type is text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
let name = RegExp$1.// Get the matched string
name = name.trim()
node.nodeValue = vm[name]
}
}
}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get: function () {
return val
},
set: function (newVal) {
if (newVal === val) return
val = newVal;
console.log(val)
}
})
}
function observe(obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key])
})
}
function Vue(options) {
this.data = options.data
const data = this.data
observe(data, this)
const id = options.el
const dom = nodeToFragment(document.getElementById(id), this)
// After compiling, return the DOM to the app
document.getElementById(id).appendChild(dom)
}
const vm = new Vue({
el: 'app'.data: {
text: 'hello world'}})</script>
</body>
</html>
Copy the code
Subscribe&publish mode
The text property in the data changes, the set method fires, but the contents of the text node do not change. How do you synchronize text nodes that are also bound to text? Here’s another thing: the subscription publishing model.
The subscription publishing pattern (also known as the observer pattern) defines a one-to-many relationship in which multiple observers listen to a topic object at the same time, and all observers are notified when the topic object’s state changes.
The publisher sends a notification => The subject object receives the notification and pushes it to the subscriber => The subscriber performs the corresponding action
// Subscribers
const sub1 = { update: function () { console.log(1)}}const sub2 = { update: function () { console.log(2)}}const sub3 = { update: function () { console.log(3)}}// A theme
function Dep () {
this.subs = [sub1, sub2, sub3]
}
Dep.prototype.notify = function () {
this.subs.forEach(function (sub) {
sub.update()
})
}
/ / publisher
var pub = {
publish: function () {
dep.notify()
}
}
var dep = new Dep()
pub.publish() / / 1 2 3
Copy the code
As mentioned earlier, the second thing you do when the set method fires is to send a notification as the publisher: “I am the property text and I have changed.” The text node acts as the subscriber and performs the corresponding update operations when the message is received.
const adadisPub = {
adadisBook: [].// Adadis salesclerk's little book
subShoe(phoneNumber) { // The buyer is registered in the small book
this.adadisBook.push(phoneNumber)
},
notify() { // The salesman calls the buyer of the notebook
for (const customer of this.adadisBook) {
customer.update()
}
}
}
const customer1 = {
phoneNumber: '152xxx'.update() {
console.log(this.phoneNumber + ': Go to the mall. ')}}const customer2 = {
phoneNumber: '138yyy'.update() {
console.log(this.phoneNumber + ': Buy a pair for my cousin ')
}
}
adadisPub.subShoe(customer1) // Leave the number in the small book
adadisPub.subShoe(customer2)
adadisPub.notify() // Call the buyer to inform them of the arrival
// 152XXX: Go to the mall
// 138yyy: Buy a pair for my cousin
Copy the code
Implementation of bidirectional binding
We have implemented: Modify the input field content => Modify the property value in the event callback => trigger the property’s set method.
Dep.notify () => Triggers the subscriber’s update method => To update the view.
The key logic here is how to add a watcher to the DEP of the associated property.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
<script>
function observe(obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key])
})
}
function defineReactive(obj, key, val) {
// When listening for data, a subject object deP is generated for each property in the data
const dep = new Dep()
Object.defineProperty(obj, key, {
get: function () {
// Add subscriber watcher to the subject object Dep
if (Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set: function (newVal) {
if (newVal === val) return
val = newVal
// Give a notice as the publisher
dep.notify()
}
})
}
function nodeToFragment(node, vm) {
let fragment = document.createDocumentFragment()
let child
while (child = node.firstChild) {
compile(child, vm)
fragment.appendChild(child)
}
return fragment
}
function compile(node, vm) {
const reg = / \ {\ {(. *) \} \} /;
// The node type is element
if (node.nodeType === 1) {
const attr = node.attributes;
// Parse the attribute
let name
for (let i = 0; i < attr.length; i++) {
if (attr[i].nodeName === 'v-model') {
name = attr[i].nodeValue; // Get the name of the property bound to the V-Model
node.addEventListener('input'.function (e) {
vm[name] = e.target.value
})
node.value = vm[name]; // Assign the data value to the node
node.removeAttribute('v-model')}}// As the HTML is compiled, a subscriber Watcher is generated for each node associated with the data binding, and the watcher adds itself to the deP of the corresponding attribute
new Watcher(vm, node, name, 'input');
}
// The node type is text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
let name = RegExp$1.// Get the matched string
name = name.trim()
new Watcher(vm, node, name, 'text'); }}}/ / subscribe
function Watcher(vm, node, name, nodeType) {
// First, we assign ourselves to a global variable dep.target
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
// The update method is executed, and the GET method is executed. The GET method reads the VM's accessor properties, triggering the GET method, which adds the watcher to the DEP of the corresponding accessor property
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function () {
this.get();
// Update the view
if (this.nodeType === 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType === 'input') {
this.node.value = this.value; }},// Get the attribute value in data
get: function () {
// Get the value of the attribute
this.value = this.vm[this.name]; // Trigger get for the corresponding property}}/ / theme
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub);
},
notify: function () {
this.subs.forEach(function (sub) { sub.update(); }); }}function Vue(options) {
this.data = options.data
const data = this.data
observe(data, this)
const id = options.el
const dom = nodeToFragment(document.getElementById(id), this)
// After compiling, return the DOM to the app
document.getElementById(id).appendChild(dom)
}
const vm = new Vue({
el: 'app'.data: {
text: 'hello world'}})</script>
</body>
</html>
Copy the code