preface
Now that the front-end interview Vue is asking about the responsivity principle and how to implement it, it’s probably not enough to simply say that hijacking properties via Object.defineProperty().
This article through learning documents and video tutorials to achieve a simple handwritten Vue source code to achieve two-way data binding, parsing instructions.
The source address
Refer to the article
Several ways to implement bidirectional binding
At present, several mainstream MVC (VM) frameworks have implemented one-way data binding, and my understanding of two-way data binding is nothing more than adding change(input) events to inputting elements (input, Textare, etc.) on the basis of one-way binding to dynamically modify model and view. So there is no need to worry too much about the implementation of one-way or bidirectional binding.
There are several ways to implement data binding:
Publisher - subscriber mode (backbone.js)
Dirty checking (angular.js)
Data hijacking (vue.js)
- The publisher-subscriber model
Set (‘property’, value) is too low now, we would prefer to update the data with vm.property = value. Automatically update the view at the same time, so there are two ways.
- Dirty value check
Angular.js uses dirty value detection to determine whether or not a view has changed. The easiest way to do this is to use setInterval() polling to detect changes in data. Of course, Google is not so low, angular only checks dirty value when a specified event is triggered, which looks like this
* DOM events, such as when the user enters text, clicks a button, etc. (ng-click) * XHR response events ($http* Browser location change event ($location) * Timer event ($timeout.$interval) *$diaest() or selections apply ()Copy the code
- The data was hijacked
Vue.js hijacks the setter and getter of each attribute through object.defineProperty () in the way of data hijacking combined with publiser-subscriber mode, and publishes messages to subscribers when data changes, triggering corresponding listener callback.
Vue source code implementation
index.html
<! DOCTYPE html> <html> <head> <meta charset="utf-8" />
<title></title>
<script type="text/javascript" src="./compile.js"></script>
<script type="text/javascript" src="./observe.js"></script>
<script type="text/javascript" src="./myvue.js"></script>
</head>
<body>
<div id="app">
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.sex}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<div v-text="msg"></div>
<div>{{msg}}</div>
<div v-text="person.name"></div>
<div v-html="htmlStr"></div>
<input type="text" v-model="msg" />
<button type="button" v-on:click="btnClick">v-on: event </button> <buttontype="button" @click="btnClick"</button> </div> <scripttype="text/javascript">
let vm = new Myvue({
el: '#app',
data: {
person: {
name: 'Just tomato and eggs.',
age: 18,
sex: 'male'
},
msg: 'Learn how TO implement MVVM',
htmlStr: ' I am HTML instruction render
'
},
methods: {
btnClick() {
console.log(this.msg)
}
}
})
</script>
</body>
</html>
Copy the code
Step 1 – Implement an instruction parser (Compile)
Compile mainly does is parses the template instructions, replaces the variables in the template with data, initializes the render page view, binds the corresponding node of each instruction to update function, adds the subscriber that listens to the data, receives notification when the data changes, updates the view
myvue.js
Const compileUtils = {/* * node expr value vm Myvue instance * eventName specifies the eventName of the current directive. */ / since the directive bound attribute may be primitive or reference type, the value getValue(expr, expr, expr, expr, expr, expr, expr, expr, expr, The vm) {// reduce() method performs a reducer function (in ascending order) provided by you on each element in the array, summarizing its results into a single return value.return expr.split('. ').reduce((data, currentVal) => {
return data[currentVal]
}, vm.$data}, // input two-way data bindingsetThe Value(expr, VM, inputVal) {// reduce() method performs a reducer function (in ascending order) provided by you on each element in the array and summarizes its results into a single return Value.return expr.split('. ').reduce((data, currentVal) => {// Assign the current value to data[currentVal] = inputVal console.log(data); }, vm.$data{{person. Name}}--{{person. Age}} {getContentVal(expr, vm) {returnexpr.replace(/\{\{(.+?) \}\}/g, (... Args) => {// Get attributes in {{}}returnThis.getvalue (args[1], vm)}) {this.getValue(args[1], vm)}) {letvalue; // Handle the format of {{}}if (expr.indexOf('{{')! == -1) { value = expr.replace(/\{\{(.+?) \}\}/g, (... Args) => {// Bind observer new Watcher(vm, args[1], (newValue) => {// handle {{person.name}}--{{person.age}} otherwise update the value to replace this.updater. textUpDater(node, This.getcontentval (expr, vm))}) // Get attributes in {{}}return this.getValue(args[1], vm)
})
} else{ new Watcher(vm, expr, (newValue) => { this.upDater.textUpDater(node, Value = this.getValue(expr, vm)} this.updater. textUpDater(node, value)} HTML (node, expr, vm) {const value = this.getValue(expr, vm) // HtmlUpDater (node, newValue)}) // Update the tool class this.updater. htmlUpDater(node, value)}, Model (node, expr, vm) {const value = this.getValue(expr, vm) // (newValue) => { this.upDater.modelUpDater(node, newValue) }) node.addEventListener('input', (e) = > {/ / set the value of enclosing setValue (expr, vm, e. arget. Value)}) / / update tool. This upDater. ModelUpDater (node, value)}, On (node, expr, vm, eventName) {const fn = vm$options.methods && vm.$options.methods[expr]
// console.log(fn);
node.addEventListener(eventName, fn.bind(vm), false}, // Update tool class upDater: TextUpDater (node, value) {node.textContent = value}, // htmlUpDater(node, value) Value) {node.innerhtml = value}, // update function modelUpDater(node, value) { node.value = value } } } // Myvue class Myvue { constructor(options) { this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el{// 1. Implement a data observer new Observe(this).$data// 2. Implement an instruction parser new Compile(this.$el, this) // 3. Implement this proxy, access data can be directly through this.$data)
}
}
proxyData(data) {
for (const key in data) {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newValue) {
data[key] = newValue
}
})
}
}
}
Copy the code
compile.js
// The instruction parser class Compile {constructor(el, vm) {// determines whether the currently passed EL is an element node // document.querySelector returns the first element of the descendant of the element matching the specified selector group. this.el = this.isElementNode(el) ? el : document.querySelector(el) this.vm = vm // 1. Match the node content and the instruction to replace the corresponding content, because each match and replace will result in page backflow and redraw, so use the document fragment object // to get the document fragment object, Putting it in memory reduces page backflow and redrawing of const fragment = this.node2fragment (this.el) // 2. This.compile (fragment) // 3 AppendChild (fragment)} isElementNode(node) {this.el.appendChild(fragment)}returnNode.nodetype === 1} // Remove all child elements from the current root element layer by layer into the document fragment, To reduce page re-flow and re-paint node2Fragment (el) {/ / create a document fragment object const fragments = document. CreateDocumentFragment ()letfirstChild; // Appends all children of the current EL node object to the document shard objectwhile (firstChild = el.firstChild) {
fragment.appendChild(firstChild)
}
returnFragment} // Compile (fragment) {// 1. Get all the child nodes, the current array of child nodes is a pseudo-array, ForEach (child => {// Determine whether the current node is an element node or a text nodeif(this.iselementNode (child)) {this.iselementNode (child)}elseThis.piletext (child)} {// Compile the text node this.piletext (child)} // recursively traverse the current node with child node objectsif(child.childnodes && child.childnodes.length) {this.compile(child)}})} // compileElement node to compileElement(node) { Const attributes = [...node.attributes]; Attributes. ForEach (attr => {// Get directive name and value to const {name, value} = attr // determine if the current attribute is directive or nativeif(this.isdirective (name)) {// Take a directive, no need for v-const directive = name.split(this.isdirective (name))The '-'Const [dirName, eventName] = directive.split(const [dirName, eventName] = directive.split()':') // Update data to compileUtils[dirName](node, value, this.vm, eventName) // Remove the attribute node.removeAttribute('v-' + directive)
} else if(this.iseventName (name)) {this.iseventName (name)) {this.iseventName (name)) {this.iseventName (name); Const eventName = name.split(const eventName = name.split(The '@'[1] // Update data, data-driven view compileUtils['on'](node, value, this.vm, EventName)}})} // Compile the text node compileText(node) {// Node.textContent gets the text and matches const Content = of type {{}} template string node.textContentif(/ \ {\ {(. +?) \}\}/.test(content)) { compileUtils['text'IsDirective (attrName) {// startsWith() {// startsWith() {} (node, content, this.vm)}} And return according to the judgment resulttrue 或 false.return attrName.startsWith('v-'IsEventName (attrName) {return attrName.startsWith(The '@')}}Copy the code
Step 2 – Implement a Data Listener (Observer)
Obeject.defineproperty () to listen for property changes and then recursively traverse the data object that you need to observe, including the property of the child property object, with setters and getters so that assigning a value to that object will trigger the setter, So you can listen for changes in the data.
observer.js
Class Observe {constructor(data) {this.observe(data)} Observe (data) {if (data && typeof data === 'object') { // console.log(data); Key => {this.definereactive (data, key, data[key])})}} Const dep = new dep () // console.log(dep); Object. DefineProperty (obj, key, {// obj is an existing Object, key is an attribute, and the third argument is an attribute descriptor enumerable:true// Enumerable: Can be an enumerable (for in), the defaultfalse
configurable: false, // Whether it can be deleted, defaultfalse/ / to getget() { // console.log(dep.target); // Add an observer to the Dep when the subscription data changes.returnValue}, // setset: (newValue) => {// the newValue also needs to hijack its property this.observe(newValue).if(newValue ! == value) {value = newValue} // Notify the subscriber to find the corresponding observer, notify the observer to update the view dep.notify()}})}}Copy the code
Step 3 – Implement a Watcher to update the view
When initializing an instance of MyVue, pass object. The get attribute of defineProperty() is used to add an observer, and the notify() is triggered to call the upDate method to upDate the view when set changes the attribute
Class constructor {constructor(vm, expr, constructor) Cb) {this.vm = vm this.expr = expr this.cb = cb this.oldValue = this.getoldValue ()getOldValueDep.target = this const oldValue = compileutils.getValue (this.expr, This.vm) // Destroy the observer on Dep. Target = null} // Update the viewupDateConst newValue = compileutils.getvalue (this.expr, this.vm) {// fetch newValue const newValue = compileutils.getvalue (this.expr, this.vm)if(newValue ! == this.oldValue) {this.cb(newValue)}} // Subscriber class Dep {constructor() {this.subs = []} // Collect observer addSub(watcher) {this.subs.push(watcher)} // notify the observer to update the viewnotify() {
this.subs.forEach(watcher => {
watcher.upDate()
})
}
}
Copy the code
Interview questions – Explain what you understand about MVVM responsiveness
Vue uses data hijacking and publiser-subscriber mode, using Object.defineProperty () to hijack getters and setters for properties, publish messages to dependent collectors when data changes, notify observers, and make corresponding callbacks to update views.
Specifically: MVVM as the binding entrance, integrate Observe,Compil and Watcher, monitor model changes through Observe, parse and compile template instructions through Compil, and finally use Watcher to build a communication bridge between Observe and Compil. So as to achieve two-way binding effect of data change => update view, view interaction change (INPUT) => data model change.
conclusion
This article mainly describes the principle and implementation of bidirectional binding by several ways to achieve the implementation of Observer, Compile, Watcher, MVVM these modules. And according to the process of thinking, I gradually explained some details and key content points. Of course, there are many imperfections, but you must have a deeper understanding of how to achieve two-way data binding.
This article is also by viewing the Vue source code analysis article, and B station related video summary, as the saying goes good memory is better than bad writing, even if they copy again can be more impressive.
Finally, thank you for reading!
❤️ do me a favor
If you find this article inspiring, I’d like to ask you to do me a small favor:
1. Like this article so that more people can see it.
2. Pay attention to the public account “Tomatology Front end”, I will regularly update and release front-end related information and project case experience for your reference.