Big front-end encyclopedia, front-end encyclopedia, record the front-end related field knowledge, convenient follow-up access and interview preparation
Key words: publish subscribe, Observer, data hijacking, dependency collection, Object.defineProperty, Proxy, Observer, Compiler, Watcher, Dep…
- How does vUE’s bidirectional binding work?
- How does Vue collect dependencies?
- Describe the process from Vue Template to render
How does vUE’s bidirectional binding work?
Vue. js adopts data hijacking combined with publiser-subscriber mode. It hijabs (monitors) the setter and getter of each attribute through object-defineProperty () method provided by ES5, releases messages to subscribers when data changes, and triggers corresponding listening callback. And because synchronization is triggered on different data, changes can be sent precisely to the bound view, rather than checking all data at once.
Specific steps:
-
You need the observer to recursively traverse the data objects, including the attributes of the child property objects, with getters and setters so that if you assign a value to that object, the setter will be triggered, and you’ll be listening for changes in the data
-
Compile parses the template instructions, replaces variables in the template with data, initializes the render page view, and binds the corresponding node of each instruction to update function, adds subscribers to listen to the data, receives notification once the data changes, and updates the view
-
Watcher subscribers serve as a bridge between the Observer and Compile. They do the following:
- Add yourself to the attribute subscriber (DEP) during self instantiation
- There must be an update() method itself
- If you can call your own update() method and trigger the callback bound with compile when the dep.notify() property changes, you are done
-
MVVM, as a data binding entry, integrates observer, Compile and Watcher, monitors its model data changes through observer, and compiles template instructions through compile. Finally, use the communication bridge between the Observer and compile built by Watcher to achieve data changes –> attempt to update; View interactive Changes (INPUT) –> Bidirectional binding effect of data model changes
Compare versions
- Vue is bidirectional binding based on dependency collection
- Before version 3.0, object.definePropetry was used
- New in version 3.0 uses Proxy
1. Advantages of bidirectional binding based on data hijacking/dependency collection
- Without the need for display calls, Vue uses data hijacking + publish-subscribe to directly notify changes and drive views
- Get the exact change data directly, hijacking the property setter. When the property value changes, we can get the exact change newValue without additional diff operations
2. The Object. DefineProperty shortcomings
- Cannot listen on arrays: because arrays have no getters and setters, and because arrays are of indeterminate length, too long is too much of a performance burden
- Only properties can be listened on, not the entire object, and loop properties need to be traversed
- You can only listen for attribute changes, not for attribute deletions
3. The benefits of the proxy
- You can listen to arrays
- Listening on the entire object is not a property
- 13 ways to intercept, much more powerful
- It is more immutable to return a new object rather than modify the original;
4. The disadvantage of the proxy
- Poor compatibility and can not be smoothed by polyfill;
How does Vue collect dependencies?
When each VUE component is initialized, the component’s data is initialized to change the normal object into a responsive object, and the dependency collection logic is performed during this process
function defieneReactive(obj, key, val){
const dep = new Dep();
/ /...
Object.defineProperty(obj, key,{
/ /...
get: function reactiveGetter(){
if(Dep.target){
dep.depend();
/ /...
}
return val
}
})
}
Copy the code
Const dep=new dep () instantiates an instance of dep, which can then be collected in get via dep.depend()
Dep
Dep is the core of the entire dependency collection
class Dep {
static target;
subs;
constructor () {...this.subs = [];
}
addSub (sub) { / / add
this.subs.push(sub)
}
removeSub (sub) { / / remove
remove(this.sub, sub)
}
depend () { / / target to add
if(Dep.target){
Dep.target.addDep(this)
}
}
notify () { / / response
const subs = this.subds.slice();
for(let i = 0; i < subs.length; i++){ subs[i].update() } } }Copy the code
Dep is a class containing a static property that refers to a globally unique Watcher, ensuring that only one Watcher is evaluated at a time. Subs is a Watcher array
watcher
class Watcher { getter; .constructor (vm, expression){
...
this.getter = expression;
this.get();
}
get () {
pushTarget(this);
value = this.getter.call(vm, vm)
...
return value
}
addDep (dep){
...
dep.addSub(this)}... }function pushTarget (_target) {
Dep.target = _target
}
Copy the code
Watcher is a class that defines methods. The dependency collection related functions are get and addDep
process
When instantiating Vue, rely on the collection related procedures
Initialize the state initState, which transforms the data into a reactive object via defineReactive, where the getter part is collected.
Initialization ends with the mount process, which instantiates watcher and executes the this.get() method inside watcher
updateComponent = () = >{
vm._update(vm._render())
}
new Watcher(vm,updateComponent)
Copy the code
PushTarget in the get method essentially assigns dep. target to the current watcher,this.getter.call(vm,vm), where the getter executes vm._render(), which triggers the getter for the data object
The getter for each object value holds a DEP. Dep.depend () is called when the getter is triggered. Dep.target.adddep (this) is called.
Dep.addsub () will subscribe the current watcher to the subs of the Dep held by the Dep. This is in order to notify the subs of the Dep when the data changes
So in vm._render(), all the getters for the data are fired, and a dependency collection is completed
Describe the process from Vue Template to render
Process analysis
Vue templates are compiled as follows: template-ast-render function
Vue converts the template to the render function by executing compileToFunctions in the template compilation
Main cores for compileToFunctions:
- Call the parse method to convert template to an AST tree (abstract syntax tree)
The purpose of parse is to convert a template into an AST tree, which is a way to describe the entire template in the form of A JS object.
Parsing process: The template is parsed sequentially using regular expressions. When the start tag, close tag, and text are parsed, the corresponding callback functions are executed respectively to form the AST tree
There are three types of AST element nodes (type) : normal element –1, expression –2, and plain text –3
- Optimize static nodes
This process mainly analyzes which are static nodes and makes a mark on them, which can be directly skipped for later update rendering, and static nodes are optimized
Walk through the AST in depth to see if the node element of each subtree is a static node or a static node root. If they are static nodes, the DOM they generate never changes, which is optimized for runtime template updates
- Generate code
Compile the AST abstract syntax tree into a Render string, put the static part into a staticRender, and finally generate the Render Function with new Function(Render)
Implement two-way data binding
Index. Js
- Data hijacking _proxyData to synchronize data to vm: this.data.name => this.name
- New Observer data hijacking
- Compiler generates the render function string with(code)…
observer.js
- Recursive traversal, data hijacking defineReactive, create dep = new dep ()
- Object. Property get attribute added to subs of deP instance: dep.addSub(dep.target)
- Dep. notify notifies all Watcher of subs to update => patch and dom update
compiler.js
- Compilation of the template
- For loop traverses childNodes (text, element nodes)
- Create an observer new Watcher() and mount the instance to dep.target
watcher.js
- Dep.target = this, put the watcher on dep. target
- Dom update method, followed by patch method
Dep. Js subscribers
- AddSub, push watcher into subs
- Notify notifies all watchers in subs and calls the update method
The demo code
index.html
<! 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>Document</title>
</head>
<body>
<div id="app">
<p>MSG: {{MSG}}</p>
<p>Age: {{age}}</p>
<div v-text="msg"></div>
<input v-model="msg" type="text" />
</div>
<script type="module">
import Vue from "./js/vue.js";
let vm = new Vue({
el: "#app".data: {
msg: "123".age: 21,}});window.vm = vm
</script>
</body>
</html>
Copy the code
vue.js
import Observer from "./observer.js";
import Compiler from "./compiler.js";
export default class Vue {
constructor(options) {
this.$options = options || {};
this.$el =
typeof options.el === "string"
? document.querySelector(options.el)
: options.el;
this.$data = options.data || {};
// See why there are two repeated operations here.
// Repeat twice to convert data to reactive
// In obsever. Js, all the attributes of data are added to the data itself in response to the getter
// In vue. Js, all attributes of data are added to vue so that aspect operations can be accessed directly from vue instances or by using this in vue
this._proxyData(this.$data);
// Use Obsever to convert data into responsive form
new Observer(this.$data);
// Compile the template
new Compiler(this);
}
// Register attributes in data to Vue
_proxyData(data) {
Object.keys(data).forEach((key) = > {
// Data hijacking
// Convert each data attribute to add to Vue into a getter setter method
Object.defineProperty(this, key, {
// The Settings can be enumerated
enumerable: true.// The Settings can be configured
configurable: true.// Get data
get() {
return data[key];
},
// Set the data
set(newValue) {
// Determine whether the new value is equal to the old value
if (newValue === data[key]) return;
// Set the new valuedata[key] = newValue; }}); }); }}Copy the code
observer.js
import Dep from "./dep.js";
export default class Observer {
constructor(data) {
// To iterate over data
this.walk(data);
}
// Iterate over data to be responsive
walk(data) {
// Determine whether data is null and object
if(! data ||typeofdata ! = ="object") return;
/ / traverse data
Object.keys(data).forEach((key) = > {
// Change to responsive
this.defineReactive(data, key, data[key]);
});
}
// Change to responsive
// The difference between vue.js and vue
// In vue. Js, attributes are given to vue to be converted into getter setters
// Turn a property in data into a getter
defineReactive(obj, key, value) {
// If it is an object type, walk will be called in response. If it is not an object type, walk will be returned
this.walk(value);
const _this = this;
// Create a Dep object
let dep = new Dep();
Object.defineProperty(obj, key, {
// The Settings are enumerable
enumerable: true.// The Settings are configurable
configurable: true./ / get the value
get() {
// Add the observer object dep. target to represent the observer
Dep.target && dep.addSub(Dep.target);
return value;
},
/ / set the value
set(newValue) {
// Determine whether the old value is equal to the new value
if (newValue === value) return;
// Set the new value
value = newValue;
// If newValue is an object, the properties in the object should also be set to reactive
_this.walk(newValue);
// Trigger notification to update viewdep.notify(); }}); }}Copy the code
compiler.js
import Watcher from "./watcher.js";
export default class Compiler {
// VM refers to the Vue instance
constructor(vm) {
/ / get the vm
this.vm = vm;
/ / get the el
this.el = vm.$el;
// Compile the template
this.compile(this.el);
}
// Compile the template
compile(el) {
// Get the child node if forEach traversal is used to convert the pseudo-array to a real array
let childNodes = [...el.childNodes];
childNodes.forEach((node) = > {
// Compile according to different node types
// Nodes of text type
if (this.isTextNode(node)) {
// Compiles the text node
this.compileText(node);
} else if (this.isElementNode(node)) {
// Element node
this.compileElement(node);
}
// Determine if there are still children to consider recursion
if (node.childNodes && node.childNodes.length) {
// Continue to compile the template recursively
this.compile(node); }}); }// Determine if it is a text node
isTextNode(node) {
return node.nodeType === 3;
}
// Compile text nodes (simple implementation)
compileText(node) {
// The core idea is to find the variables inside the regular expression by removing {{}} from the regular expression
// Go to Vue to find this variable and assign it to Node.textContent
let reg = / \ {\ {(. +?) \} \} /;
// Get the text content of the node
let val = node.textContent;
// Check whether there is {{}}
if (reg.test(val)) {
// get the contents of {{}}
let key = RegExp.$1.trim();
// Make the substitution and assign to node
node.textContent = val.replace(reg, this.vm[key]);
// Create an observer
new Watcher(this.vm, key, newValue= >{ node.textContent = newValue; }); }}// Check if it is an element node
isElementNode(node) {
return node.nodeType === 1;
}
// Determine if the attribute of the element is a vue directive
isDirective(attr) {
return attr.startsWith("v-");
}
// Compile element nodes only handle instructions here
compileElement(node) {
// Get all the attributes above the element node! [...node.attributes].forEach((attr) = > {
// Get the attribute name
let attrName = attr.name;
// check if the command starts with v-
if (this.isDirective(attrName)) {
// Remove v- easy to operate
attrName = attrName.substr(2);
// Get the value of the instruction as MSG in v-text = "MSG"
// MSG as the key goes to Vue to find this variable
let key = attr.value;
// Instruction operations execute instruction methods
// A lot of vue directives. In order to avoid a lot of if judgments, write a uapdate method here
this.update(node, key, attrName); }}); }// Add instruction methods and execute
update(node, key, attrName) {
// Add textUpdater to handle v-text methods
// We should call a built-in textUpdater method
// It doesn't matter what suffix you add but define the corresponding method
let updateFn = this[attrName + "Updater"];
// The built-in method can be called if it exists
updateFn && updateFn(node, key, this.vm[key], this);
}
// Specify the corresponding method in advance such as v-text
// Use the same as Vue
textUpdater(node, key, value, context) {
node.textContent = value;
// Create observer 2
new Watcher(context.vm, key, (newValue) = > {
node.textContent = newValue;
});
}
// v-model
modelUpdater(node, key, value, context) {
node.value = value;
// Create an observer
new Watcher(context.vm, key, (newValue) = > {
node.value = newValue;
});
// Two-way binding is implemented here to listen for input events to modify properties in data
node.addEventListener("input".() = > {
console.log('+ + + + + + + + +', node.value) context.vm[key] = node.value; }); }}Copy the code
watcher.js
import Dep from "./dep.js";
/** * Update */
export default class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
// key is the key in data
this.key = key;
// Call the callback function to update the view
this.cb = cb;
// Store the observer in dep.target
Dep.target = this;
// Old data should be compared when updating the view
// The vm[key] triggers the get method
// The observer was added to the dep.subs in get via dep.addSub(dep.target)
this.oldValue = vm[key];
// dep. target does not need to exist because the above operation is already saved
Dep.target = null;
}
// The required methods in the observer are used to update the view
update() {
// Get a new value
let newValue = this.vm[this.key];
// Compare the old and new values
if (newValue === this.oldValue) return;
// Call the specific update method
this.cb(newValue); }}Copy the code
dep.js
export default class Dep {
constructor() {
// Store the observer
this.subs = [];
}
// Add an observer
addSub(sub) {
// Check whether the observer exists and whether the friend update method is used
if (sub && sub.update) {
this.subs.push(sub); }}// Notification method
notify() {
// Trigger the update method for each observer
this.subs.forEach((sub) = >{ sub.update(); }); }}Copy the code