The principle of analysis
Data bidirectional binding is realized through data hijacking
- The listener
Observer
Is used to hijack and listen to all attributes and notify the subscriber of any changes - The subscriber
Watcher
Receives notification of property changes and executes corresponding functions to update the view- Since there are multiple subscribers, a single message subscriber is adopted to facilitate unified management
Dep
Specifically to collect subscribers
- Since there are multiple subscribers, a single message subscriber is adopted to facilitate unified management
- Instruction parser
Complie
.- Scan and parse the instructions for each node, initialize the template data and initialize the corresponding subscriber
As shown in figure:
Version 1.0
Implement the Observer
function Observer(data) {
if(! data ||typeofdata ! = ='object') {
return;
}
Object.keys(data).forEach(key= > {
defineReactive(data, key, data[key]);
});
}
function defineReactive(data, key, val) {
// Recursive subattributes
Observer(val);
// Embed the message subscriber
// Create a corresponding Dep object for each attribute, since each attribute may have multiple subscribers
let dep = new Dep();
// The subscriber Watcher is collected to the Dep in the getter, and all subscriber Watcher is notified by the Dep in the setter
Object.defineProperty(data, key, {
enumerable: true.configurable: true.get: function () {
console.log(`getter -> key :: [[${key}]], value :: [[${val}]] `);
if (Dep.target) { // Whether to add subscribers
dep.addSub(Dep.target); / / subscriber
}
return val;
},
set: function (newVal) {
val = newVal;
console.log(`setter -> key :: [[${key}]], new value :: [[${newVal}]] `);
dep.notify(); // Notify all subscribers subscribed to the property of updates}}); }function Dep() {
this.subs = [];
}
Dep.prototype.addSub = function (watch) {
this.subs.push(watch);
}
Dep.prototype.notify = function () {
this.subs.forEach(watch= > watch.update());
}
Copy the code
Realize the Watcher
function Watcher(data, key, cb) {
this.data = data;
this.cb = cb;
this.key = key;
this.value = this.get();
}
Watcher.prototype = {
get: function () {
Dep.target = this; // Cache the current watcher instance into a global variable to collect the watcher instance into the Dep in the getter
const value = this.data[this.key]; // Force the getter for the data:key property to fire
Dep.target = null; / / release
return value;
},
update: function () {
this.cb(); }}Copy the code
demo
<body>
<h1 id="name">name</h1>
<script src="./myVue.js"></script>
<script>
let data = {
name: 'zheling'}; observer(data);new Watcher(data, 'name'.function () {
document.querySelector('#name').innerHTML = 'hello vue?? ';
});
setTimeout(function () {
data.name = 'hello Vue'
}, 1000);
</script>
</body>
Copy the code
Version 2.0
Realize the Watcher 2.0
function Watcher(vm, key, cb) {
this.vm = vm;
this.data = this.vm.data;
this.cb = cb;
this.key = key;
this.value = this.get();
}
Watcher.prototype = {
get: function () {
Dep.target = this; // Cache the current watcher instance into a global variable to collect the watcher instance into the Dep in the getter
const value = this.data[this.key]; // Force the getter for the data:key property to fire
Dep.target = null; / / release
return value;
},
update: function () {
let oldVal = this.value;
let value = this.data[this.key];
if(value ! == oldVal) {this.value = value;
this.cb.call(this.vm, value, oldVal); }}}Copy the code
MyVue
Associate the Observer with Watcher
function MyVue(data, el, key) {
this.data = data;
this.el = el;
observer(this.data); // Listen for attributes
// Proxy attributes: vm.data.key -> vm.key
Object.keys(this.data).forEach(key= > {
this.proxyKeys(key);
});
new Watcher(this, key, function (value) {
el.innerHTML = value;
});
return this;
}
MyVue.prototype.proxyKeys = function (key) {
Object.defineProperty(this, key, {
enumerable: true.configurable: true.get: function () {
return this.data[key];
},
set: function (newVal) {
this.data[key] = newVal; }}); }Copy the code
demo
<body>
<h1 id="name">name</h1>
<script src="./myVue.js"></script>
<script>
let data = {
name: 'zheling'};let vm = new MyVue(
data,
document.querySelector('#name'),
'name'
);
setTimeout(function () {
vm.data.name = 'hello Vue'
}, 1000);
</script>
</body>
Copy the code
Version 3.0
MyVue
function MyVue(options) {
let { el, data,methods } = options || {};
this.data = data;
this.el = el;
this.methods = methods;
observer(this.data);
Object.keys(this.data).forEach(key= > {
this.proxyKeys(key);
});
// Parse the DOM, create a subscriber and bind the corresponding update function to it
new Compile(this);
return this;
}
MyVue.prototype.proxyKeys = function (key) {
Object.defineProperty(this, key, {
enumerable: true.configurable: true.get: function () {
return this.data[key];
},
set: function (newVal) {
this.data[key] = newVal; }}); }Copy the code
Compile
// Implement the compiler that wraps the above example to initialize the subscriber and parse the DOM
function Compile(vm) {
this.vm = vm;
this.el = document.querySelector(vm.el);
// Get all the nodes under the root container. Since frequent DOM manipulation is required for performance reasons, move them to a fragment for processing
this.fragment = null;
this.init();
}
Compile.prototype = {
init: function () {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
},
nodeToFragment: (el) = > {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// Move the DOM element into the fragment
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
},
// traverses all nodes under el
compileElement: function (el) {
let childNodes = el.childNodes; // A pseudo-array object
let reg = / \ {\ {(. +) \} \} /;
Array.from(childNodes).forEach(node= > {
let text = node.textContent;
if (this.isTextNode(node) && reg.test(text)) { // Matches the {{}} directive
console.log(node);
this.compileText(node, reg.exec(text)[1]); // Get the key in {{key}}
}
// Iterate over the child nodes
if (node.childNodes && node.childNodes.length) {
this.compileElement(node); }}); },compileText: function (node, key) {
// Initialize the data
this.updateText(node, this.vm[key]);
// Create a subscriber and bind the update function
new Watcher(this.vm, key, (value) = > {
this.updateText(node, value);
});
},
updateText: function (node, text) {
node.textContent = text ? text : ' ';
},
isTextNode: function (node) {
return node.nodeType === 3; }},Copy the code
demo
<body>
<div id="app">
<h1>{{name}}</h1>
<span>{{age}}</span>
</div>
<script src="./myVue.js"></script>
<script>
let vm = new MyVue({
el: '#app'.data: {
name: 'zheling'.age: 99}});setTimeout(function () {
vm.name = 'hello Vue'
}, 1000);
</script>
</body>
Copy the code
Version 4.0
Add processing of V-ON/V-model instructions
Compile
Compile.prototype = {
init: function () {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
},
nodeToFragment: (el) = > {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// Move the DOM element into the fragment
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
},
// traverses all nodes under el
compileElement: function (el) {
let childNodes = el.childNodes; // A pseudo-array object
let reg = / \ {\ {(. +) \} \} /;
Array.from(childNodes).forEach(node= > {
let text = node.textContent;
// Whether it is an element node
if (this.isElementNode(node)) {
this.compile(node);
} else if (this.isTextNode(node) && reg.test(text)) { // Matches the {{}} directive
this.compileText(node, reg.exec(text)[1]); // Get the key in {{key}}
}
// Iterate over the child nodes
if (node.childNodes && node.childNodes.length) {
this.compileElement(node); }}); },compileText: function (node, key) {
// Initialize the data
this.updateText(node, this.vm[key]);
// Create a subscriber and bind the update function
new Watcher(this.vm, key, (value) = > {
this.updateText(node, value);
});
},
updateText: function (node, text) {
node.textContent = text ? text : ' ';
},
compile: function (node) {
var nodeAttrs = node.attributes;
var self = this;
// Iterate through node attributes to find the directive starting with v-*
Array.from(nodeAttrs).forEach(function (attr) {
var attrName = attr.name;
if (self.isDirective(attrName)) {
var exp = attr.value;
var dir = attrName.substring(2); // remove 'v-' from 'v- * '
// Select model from model and on from model
if (self.isEventDirective(dir)) { // Event command
self.compileEvent(node, self.vm, exp, dir);
} else { / / v - model instructionself.compileModel(node, exp); } node.removeAttribute(attrName); }}); },compileEvent: function (node, vm, key, dir) {
var eventType = dir.split(':') [1];
var cb = vm.methods && vm.methods[key];
if (eventType && cb) { // Bind (vm) can be used to quickly access MyVue instance
node.addEventListener(eventType, cb.bind(vm), false); }},compileModel: function (node, key) {
var self = this;
var val = this.vm[key];
// Initialize node.value
node.value = typeof val == 'undefined' ? ' ' : val;
// One of the bidirectional bindings: data.field -> input
new Watcher(this.vm, key, function (value) {
node.value = typeof value == 'undefined' ? ' ' : value;
});
// input -> data.field
node.addEventListener('input'.function (e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
self.vm[key] = newValue;
});
},
isDirective: function (attr) {
return attr.indexOf('v-') = =0;
},
isEventDirective: function (dir) {
return dir.indexOf('on:') = = =0;
},
isTextNode: function (node) {
return node.nodeType === 3;
},
isElementNode: node= > node.nodeType === 1,}Copy the code
demo
<body>
<div id="app">
<h2>{{title}}</h2>
<input v-model="name">
<h1>name : {{name}}</h1>
<button v-on:click="clickMe">click me!</button>
</div>
<script src="./myVue.js"></script>
<script>
let vm = new MyVue({
el: '#app'.data: {
title: 'zheling'.name: 'kangshi',},methods: {
clickMe: function () {
this.title = 'new title'; }}});// Verify that the v-model instruction data.name -> input.value direction is properly synchronized
setTimeout(function () {
vm.name = 'hello Vue?? '
}, 5000);
</script>
</body>
Copy the code
Refer to the article
- Vue bidirectional binding principle and implementation