How is the Vue2. X response formula principle implemented?
One of the most unique features of Vue is its non-invasive responsive system. So what is the reactive principle?
The data model is just a normal JavaScript object, and as we modify the data, the view is updated, eliminating tedious DOM manipulation and improving development efficiency. In short, as the data changes, the view updates.
Once you know the concept, how does it work?
You use the getter and setter methods in Object.defineProperty() and the observer pattern in the design pattern.
So, let’s look at Object.defineProperty() first. In MDN it is explained like this: the object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object.
let data = {
msg:'hello'
};
let vm = {};
Object.defineProperty(vm, 'msg', {
enumerable: true.// enumerable (traversable)
configurable: true.// configurable (can be deleted using delete, can be redefined via defineProperty)
// Execute when the value is fetched
get() {
console.log('get', data.msg);
return data.msg
},
// Execute when set value
set(newVal) {
if (newVal === data.msg) {
return
}
data.msg = newVal;
console.log('set', data.msg); }})/ / test
console.log(vm.msg);
/* > "get" "hello" > "hello" */
vm.msg = 'world'; // > "set" "world"
Copy the code
After a brief introduction to Object.defineProperty(), there’s the observer pattern, which you might think of as publish-subscribe. In fact, their essence is the same, but there are certain differences.
Let’s start with the publish-subscribe model.
The publisher-subscriber model consists of three modules, publisher, subscriber and unified dispatch center. This unified dispatch center is the equivalent of a newspaper office. The publisher is like a magazine owner who comes to the center to register a magazine, and the subscriber is like a user, and I subscribe to the magazine at the center. Whenever a publisher releases an issue of the magazine, subscribers are notified by the clerk’s office to pick up the new issue. The publish-subscriber pattern is invoked by a unified dispatch center, so publishers and subscribers do not need to know of each other’s existence.
Below, we’ll take a closer look at the publish-subscribe pattern with an example of implementing Vue custom events.
function EventEmitter(){
// Initialize the unified scheduling center
this.subs = Object.create(null); // {'click':[fn1,fn2]}
}
// Register events
EventEmitter.prototype.$on = function (eventType,handler){
console.log(this);
this.subs[eventType]= this.subs[eventType]||[];
this.subs[eventType].push(handler);
}
// Trigger the event
EventEmitter.prototype.$emit = function (eventType,data){
if(this.subs[eventType]){
this.subs[eventType].forEach(handler= >{ handler(data); }); }}/ / test
const em = new EventEmitter();
/ / subscriber
em.$on('click1'.(data) = >{
console.log(data);
})
/ / publisher
em.$emit('click1'.'maomin') //maomin
Copy the code
This custom event is widely used for Vue peer component transfer.
Next, let’s introduce the observer pattern.
The observer pattern is scheduled by the target. For example, when an event is triggered, the target invokes the observer’s methods, so there is a dependency between the observer’s subscriber (the observer) and the publisher (the target).
// Publisher (target)
function Dep(){
this.subs = [];
}
Dep.prototype.addSub = function (sub){
if(sub&&sub.update){
this.subs.push(sub);
}
}
Dep.prototype.notify = function (data){
this.subs.forEach(sub= >{ sub.update(data); })}// Subscriber (observer)
function Watcher(){}
Watcher.prototype.update=function(data){
console.log(data);
}
/ / test
let dep = new Dep();
let watcher = new Watcher();
// Collect dependencies
dep.addSub(watcher);
// Send notifications
dep.notify('1');
dep.notify('2');
Copy the code
The following figure distinguishes the two modes.
Implement the Vue2. X mini version
The purpose of implementing a Vue mini-version is to deepen your understanding of the Vue responsive principle and some of its apis. First of all, we analyze the overall structure of Vue2. X response formula principle.
As shown below:
Next, we will implement a mini Vue based on the process described in this picture. Vue2. X uses the Virtual DOM, but since we only need to implement a mini-version here, we have simplified it by manipulating the DOM directly.
Now, let’s see how I built a Vue Mini.
The first step
The page structure is as follows, we can first introduce the full version of Vue2. X, see the implementation effect.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Vue2.x Reactive</title>
</head>
<body>
<div id="app">
<h2>Text node</h2>
<div>{{msg}}</div>
<div>{{count}}</div>
<div>{{obj.name}}</div>
<div>{{arr[0]}}</div>
<div>{{obj.inner.age}}</div>
<div>{{obj.inner.name}}</div>
<h2>v-text</h2>
<div v-text="msg"></div>
<h2>v-model</h2>
<input type="text" v-model="msg">
<input type="text" v-model="count">
<h2>v-html</h2>
<div v-html="html"></div>
<h2>v-show</h2>
<div v-show="isShow">{{isShow}}</div>
<h2>v-on</h2>
<button v-on:click="handler">handler</button>
<button @click="onClick">onClick</button>
<h2>v-if</h2>
<div>
<p v-if="isIf">{{isIf}}</p>
</div>
</div>
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app'.data() {
return {
msg: 'maomin'.count: 1.obj: {
name: 'hello'.inner: {
age: 17}},arr: ['string1'].html: '<div>{{msg}}</div>'.isShow: false.isIf:true}},methods: {
handler() {
// this.count = 2;
this.isIf = !this.isIf;
},
onClick() {
this.obj.inner.age = 18;
// console.log(this.obj.inner.age);}}});</script>
</body>
</html>
Copy the code
After testing, the full version of Vue2. X looks like this. We will use the Vue mini version to achieve the same page effect.
The second step
We will build the Vue mini-version, which we will call vuemini.js, based on the overall structure diagram and page structure.
Through the overall structure diagram, we can find that there are several constructors including Vue, Observer, Compiler, Dep and Watcher. We’ll start by creating these constructors, which are not defined using the class class because most of the Vue source code also uses constructors, and are relatively extensible.
Vue
/ / instance.
function Vue(options) {
this.$options = options || {};
this._data = typeof options.data === 'function' ? options.data() : options.data || {};
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
// Is responsible for injecting properties from data into Vue instances and converting them into getters/setters
this._proxyData(this._data);
this.initMethods(this, options.methods || {})
// Responsible for calling observer to listen for changes to all properties of data
new Observer(this._data);
// Is responsible for calling compiler parsing instructions/interpolation expressions
new Compiler(this);
}
// Mount properties from data to this
Vue.prototype._proxyData = function (data) {
Object.keys(data).forEach(key= > {
Object.defineProperty(this, key, {
configurable: true.enumerable: true.get() {
return data[key]
},
set(newVal) {
if (newVal === data[key]) {
return} data[key] = newVal; }})})}function noop(a, b, c) {}function polyfillBind(fn, ctx) {
function boundFn(a) {
var l = arguments.length;
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
boundFn._length = fn.length;
return boundFn
}
function nativeBind(fn, ctx) {
return fn.bind(ctx)
}
const bind = Function.prototype.bind
? nativeBind
: polyfillBind;
// Initialize the methods attribute
Vue.prototype.initMethods = function (vm, methods) {
for (var key in methods) {
{
if (typeofmethods[key] ! = ='function') {
warn(
"Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
"Did you reference the function correctly?",
vm
);
}
}
vm[key] = typeofmethods[key] ! = ='function'? noop : bind(methods[key], vm); }}Copy the code
Observer
// Data hijacking.
// Convert attributes in the data (_data) option to responsive data.
function Observer(data) {
this.walk(data);
}
Observer.prototype.walk = function (data) {
if(! data ||typeofdata ! = ='object') {
return
}
Object.keys(data).forEach(key= > {
this.defineReactive(data, key, data[key]);
})
}
Observer.prototype.defineReactive = function (obj, key, val) {
let that = this;
// Collect dependencies
let dep = new Dep();
// If val is an object, convert the attributes inside val to responsive data
this.walk(val);
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
// Collect dependencies
Dep.target && dep.addSub(Dep.target)
return val
},
set(newVal) {
if (newVal === val) {
return
}
val = newVal;
// Reassign attributes in data to convert them to responsive data.
that.walk(newVal);
// Send notificationsdep.notify(); }})}Copy the code
Compiler
// Compile templates, parse instructions/interpolation expressions
// Be responsible for the first rendering of the page
// Re-render the view when the data changes
function Compiler(vm) {
this.el = vm.$el;
this.vm = vm;
// Compile the template immediately
this.compile(this.el);
}
// Compile templates to handle text nodes and element nodes
Compiler.prototype.compile = function (el) {
let childNodes = el.childNodes;
Array.from(childNodes).forEach(node= > {
// Process text nodes
if (this.isTextNode(node)) {
this.compileText(node);
}
// Process element nodes
else if (this.isElementNode(node)) {
this.compileElement(node);
}
// Determine whether a node has children. If there are children, recursively call compile
if (node.childNodes && node.childNodes.length) {
this.compile(node); }})}// Compile text nodes to handle interpolation expressions
Compiler.prototype.compileText = function (node) {
// console.dir(node);
let reg = / \ {\ {(. +?) \} \} /;
let value = node.textContent;
if (reg.test(value)) {
let key = RegExp.$1.trim();
if (this.vm.hasOwnProperty(key)) {
node.textContent = value.replace(reg, typeof this.vm[key] === 'object' ? JSON.stringify(this.vm[key]) : this.vm[key]);
// Create a Watcher object to update the view when data changes
new Watcher(this.vm, key, (newVal) = >{ node.textContent = newVal; })}else {
const str = `this.vm.${key}`;
node.textContent = value.replace(reg, eval(str));
// Create a Watcher object to update the view when data changes
new Watcher(this.vm, key, () = > {
const strw = `this.vm.${key}`;
node.textContent = value.replace(reg, eval(strw)); })}}}// Determine whether the node is a text node
Compiler.prototype.isTextNode = function (node) {
return node.nodeType === 3;
}
// Determine if the node is an element node
Compiler.prototype.isElementNode = function (node) {
return node.nodeType === 1;
}
// Compile element node, process instruction
Compiler.prototype.compileElement = function (node) {
// console.log(node.attributes);
// Iterate over all attribute nodes
Array.from(node.attributes).forEach(attr= > {
let attrName = attr.name;
// console.log(attrName);
// Check if it is a command
if (this.isDirective(attrName)) {
// Judge: such as V-on :click
let eventName;
if (attrName.indexOf(':')! = = -1) {
const strArr = attrName.substr(2).split(':');
attrName = strArr[0];
eventName = strArr[1];
} else if (attrName.indexOf(The '@')! = = -1) {
eventName = attrName.substr(1);
attrName = 'on';
} else {
attrName = attrName.substr(2);
}
let key = attr.value;
this.update(node, key, attrName, eventName); }})}// Determine if element attributes are directives
Compiler.prototype.isDirective = function (attrName) {
return attrName.startsWith('v-') || attrName.startsWith(The '@');
}
// Instruction auxiliary function
Compiler.prototype.update = function (node, key, attrName, eventName) {
let updateFn = this[attrName + 'Updater'];
updateFn && updateFn.call(this, node, this.vm[key], key, eventName);
}
// Process the V-text instruction
Compiler.prototype.textUpdater = function (node, value, key) {
node.textContent = value;
new Watcher(this.vm, key, (newVal) = >{ node.textContent = newVal; })}// Handle v-HTML directives
Compiler.prototype.htmlUpdater = function (node, value, key) {
node.insertAdjacentHTML('beforeend', value);
new Watcher(this.vm, key, (newVal) = > {
node.insertAdjacentHTML('beforeend', newVal); })}// Handle the V-show command
Compiler.prototype.showUpdater = function (node, value, key) {
!value ? node.style.display = 'none' : node.style.display = 'block'
new Watcher(this.vm, key, (newVal) = > {
!newVal ? node.style.display = 'none' : node.style.display = 'block'; })}// Process the V-if directive
Compiler.prototype.ifUpdater = function (node, value, key) {
const nodew = node;
const nodep = node.parentNode;
if(! value) { node.parentNode.removeChild(node) }new Watcher(this.vm, key, (newVal) = > {
console.log(newVal);
!newVal ? nodep.removeChild(node) : nodep.appendChild(nodew);
})
}
// Process the V-ON instruction
Compiler.prototype.onUpdater = function (node, value, key, eventName) {
if (eventName) {
const handler = this.vm.$options.methods[key].bind(this.vm); node.addEventListener(eventName, handler); }}// Process the V-model directive
Compiler.prototype.modelUpdater = function (node, value, key) {
node.value = value;
new Watcher(this.vm, key, (newVal) = > {
node.value = newVal;
})
// Bidirectional binding, view changes update data
node.addEventListener('input'.() = > {
this.vm[key] = node.value; })}Copy the code
Dep
// Publisher.
// Collect dependencies and add all watcher. Notify all observers.
function Dep() {
// Store all watcher
this.subs = [];
}
// Add an observer
Dep.prototype.addSub = function (sub) {
if (sub && sub.update) {
this.subs.push(sub); }}// Send notifications
Dep.prototype.notify = function () {
this.subs.forEach(sub= >{ sub.update(); })}Copy the code
Watcher
function Watcher(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// Record the current watcher object to the Dep class's static property target
Dep.target = this;
if (vm.hasOwnProperty(key)) {
this.oldVal = vm[key];
} else {
const str = `vm.${key}`;
this.oldVal = eval(str);
}
Dep.target = null;
}
// Update the view when data changes
Watcher.prototype.update = function () {
let newVal;
if (this.vm.hasOwnProperty(this.key)) {
newVal = this.vm[this.key];
} else {
const str = `this.vm.The ${this.key}`;
newVal = eval(str);
}
this.cb(newVal);
}
Copy the code
These constructors implement what we call mini-versions, combining them into a single file vuemini.js. In the above prompts page introduction, see the effect.
In addition, I bound an HTML property in data with a value of ‘
‘, and the Maomin text below the V-HTML is rendered as compared to the previous full version.
Mini version of Vue2. X developed by UVU
Next, we’ll take a look at the mini version developed by THE University of Utah, which introduces the Virtual DOM, but is primarily responsive to the principle. We can compare the mini version with the above version to see if there are any similarities.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>vue2mini</title>
</head>
<body>
<div id="app"></div>
<script>
// reactivity ---
let activeEffect
class Dep {
subscribers = new Set(a)depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect= > effect())
}
}
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
function reactive(raw) {
// use Object.defineProperty
// 1. iterate over the existing keys
Object.keys(raw).forEach(key= > {
// 2. for each key: create a corresponding dep
const dep = new Dep()
// 3. rewrite the property into getter/setter
let realValue = raw[key]
Object.defineProperty(raw, key, {
get() {
// 4. call dep methods inside getter/setter
dep.depend()
return realValue
},
set(newValue) {
realValue = newValue
dep.notify()
}
})
})
return raw
}
// vdom ---
function h(tag, props, children) {
return { tag, props, children };
}
function mount(vnode, container, anchor) {
const el = document.createElement(vnode.tag);
vnode.el = el;
// props
if (vnode.props) {
for (const key in vnode.props) {
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
} else{ el.setAttribute(key, vnode.props[key]); }}}if (vnode.children) {
if (typeof vnode.children === "string") {
el.textContent = vnode.children;
} else {
vnode.children.forEach(child= >{ mount(child, el); }); }}if (anchor) {
container.insertBefore(el, anchor)
} else{ container.appendChild(el); }}function patch(n1, n2) {
// Implement this
// 1. check if n1 and n2 are of the same type
if(n1.tag ! == n2.tag) {// 2. if not, replace
const parent = n1.el.parentNode
const anchor = n1.el.nextSibling
parent.removeChild(n1.el)
mount(n2, parent, anchor)
return
}
const el = n2.el = n1.el
// 3. if yes
/ / 3.1 diff props
const oldProps = n1.props || {}
const newProps = n2.props || {}
for (const key in newProps) {
const newValue = newProps[key]
const oldValue = oldProps[key]
if(newValue ! == oldValue) {if(newValue ! =null) {
el.setAttribute(key, newValue)
} else {
el.removeAttribute(key)
}
}
}
for (const key in oldProps) {
if(! (keyin newProps)) {
el.removeAttribute(key)
}
}
/ / diff 3.2 children
const oc = n1.children
const nc = n2.children
if (typeof nc === 'string') {
if(nc ! == oc) { el.textContent = nc } }else if (Array.isArray(nc)) {
if (Array.isArray(oc)) {
// array diff
const commonLength = Math.min(oc.length, nc.length)
for (let i = 0; i < commonLength; i++) {
patch(oc[i], nc[i])
}
if (nc.length > oc.length) {
nc.slice(oc.length).forEach(c= > mount(c, el))
} else if (oc.length > nc.length) {
oc.slice(nc.length).forEach(c= > {
el.removeChild(c.el)
})
}
} else {
el.innerHTML = ' '
nc.forEach(c= > mount(c, el))
}
}
}
// paste all previous code from Codepen
const app = {
data: reactive({
count: 0
}),
render() {
return h('div', {
onClick: () = > {
app.data.count++
}
}, String(app.data.count))
}
}
function mountApp(component, selector) {
let isMounted = false
let oldTree
watchEffect(() = > {
if(! isMounted) { mount(oldTree = component.render(),document.querySelector(selector))
isMounted = true
} else {
const newTree = component.render()
patch(oldTree, newTree)
oldTree = newTree
}
})
}
mountApp(app, '#app')
</script>
</body>
</html>
Copy the code
About the author
Author: Vam’s Golden Bean Road. CSDN blog star of the Year 2019, CSDN blog has reached millions of visitors. Nuggets blog post repeatedly pushed to the home page, the total page view has reached hundreds of thousands.
In addition, my public number: front-end experience robbed road, the public continues to update the latest front-end technology and related technical articles. Welcome to pay attention to my public number, let us together in front of the road experience rob it! Go!