Bidirectional binding in Vue
This article explains the simplified Vue model and the basic principles of bidirectional binding. At the end of the article, there will be Demo code for you to debug. It is not difficult to understand bidirectional binding. Learned learned!” Exactly how Vue relates this to component rendering is unclear. I have been in this state before, and I have experienced it. What? You say you don’t feel that way? ! That’s all right…
So this article will not go into the basics of bidirectional binding, but by default you have some knowledge of bidirectional binding, and want to understand how it relates to the Vue rendering process. Hope you can learn something here
If you don’t know anything about bidirectional binding, you can stamp here the basic principles of Vue bidirectional binding
The first step
Let’s start by defining a super-simplified VueClass class to emulate Vue. The structure of the class is very simple to focus on the implementation logic of bidirectional binding
- To have a
render
Function (external pass) - A function that has an update component
updateComponent
Why is there a render function here? First we need to know what the render function does that is relevant to bidirectional binding. If you are not familiar with render, you must know
if you have written Vue. This is defined in Vue. Your browser does not recognize the template tag. Finally, the render function is generated, so the last thing the browser executes is this render function
class VueClass {
constructor(options) {
// Only data is concerned
this.data = options.data
// The Vue compiler compiles the template to render
this.render = options.render
}
// Update component, which can be interpreted as a VueClass render function
updateComponent() {
// Build a VNode tree (not a VNode tree)
this.render()
// ...
// Omit the Dom mounting operation}}Copy the code
When you write code using a template, you bind some data to it, like this:
<template>{{msg}}</template>
Copy the code
To display MSG, you need to get its value, so render gets the variable bound to the template when it is called!! The render function is called to retrieve variables that are bound to the template. , which is important for understanding two-way binding. That’s why I’ll define another Render in the simplified model, and then we’ll simulate Render getting the template variables.
Step 2: Basic principles of bidirectional binding
All right, that code aside, the implementation of the critical code observe above, we only consider target as an object, and Vue does some extra work with arrays, which we won’t mention here.
We recursively configure every property of the target object, intercepting its getters and setters, so that we can do whatever we want with it when the property is accessed or assigned (rendering).
Yes, this is all very familiar. When you access a property to make a change, the setter for that property fires. If you can insert a rendering logic to rerender the page, that’s two-way binding.
function observe(target) {
if(target && typeoftarget ! = ='object') return
Object.keys(target).forEach(key= > {
defineReactive(target, key, target[key])
})
}
function defineReactive(obj, key, val) {
// We recurse to the subattribute. We do not need to check whether the argument is valid. We check inside observe, and if val is not an object, no operation is performed
observe(val)
Object.defineProperty(obj, key, {
/ / can be enumerated
enumerable: true./ / can be configured
configurable: true.get() {
// Triggers when the property is accessed
return val
},
set(value) {
// Triggers when a property is assigned
val = value
}
})
}
Copy the code
However, how to achieve the specific, Vue uses the classic observer mode, the design mode of this piece recommended a big gold booklet, please support the genuine edition, knowledge is priceless, the observer mode of the example is really too vivid!! I’ve gone I’ve gone.
We need two classes, Dep and Watcher. If you want to know why we need two classes, we need two classes: observer mode, there must be subscribers and publishers, regardless of Dep and Watcher, we need two classes.
Observer mode
If you don’t understand the observer model, don’t worry. Let’s take a look at the big picture. Dep is the product manager, and Watcher is the painless programmer.
Now the company has started a new project, which is to “render the page”, but the specific requirements are not out (no one has changed the data yet), so the product manager (Dep) created a group of “XX2077 Iteration”, since Dep is the leader of the group, of course, he has the authority to pull people into the group, after pulling the Watcher into the group, Probably no one in the group to talk, because the needs have not been decided, everyone can go to do their own things, do not disturb each other.
Suddenly one day, the requirements document came out, and the page had to be rerendered according to the document (the customer changed the data), and the product manager (Dep) immediately notified everyone, “The 2077 requirements document is finally out! Everybody to work, work!” So the Watcher set to work…
In this case, the operation of pulling people into the group is to add subscribers, publish requirements update documents in the group, which is to publish news, and subscribers start to work. This is the observer mode
For simplicity, the Watcher class only uses a rendering Watcher. This Watcher can only do one thing, that is, render the page, so we can also call it a rendering Watcher. The actual Vue source code is passed in a callback function (which can do anything). If you don’t, I’ll have a little bit of code to explain that
// Product Manager Dep category
class Dep {
constructor() {
// Create a group
this.deps = []
}
Sub is a Watcher
addSub(sub) {
this.deps.push(sub)
}
// Let's get started
notify() {
for(let i=0,j=this.deps.length; i<j; i++) {
// Every worker in this group has to work
this.deps[i].update()
}
}
}
// The programmer Watcher class
class Watcher {
// Initialize by passing a VueClass instance. The worker must know who the boss is, otherwise who pays the wages?
constractor(vm) {
this.vm = vm
}
// The worker starts to work
update() {
// I'm rendering Watcher. (rendering)}}Copy the code
Site Watcher
We now have three classes VueClass, Dep, Watcher, and a black technology function observe
setter
In the example above, the product manager Dep has the ability to sense changes in the requirements document (the customer modifies the data), so we put it in the setter of the corresponding property, and as soon as the data is modified, the setter is called, the product manager Dep can sense the change, send a notification, and everybody gets to work
Property data modification ->> Setter triggered!! ->> Call deps.notify to notify all Watcher
Now we’re going to reinvent defineReactive, we’ve defined getters and setters for every property, and I said it’s for interception, so let’s do whatever we want
function observe() {
/ /... There is no change
}
function defineReactive(obj, key, val) {
// Treat each attribute as a project team, and assign a product manager to each project team
let deps = new Dep()
observe(val)
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
// Dep.target is global and the value is the Watcher that is currently waiting to be grouped
if(Dep.target) {
deps.addSub(Dep.target)
}
return val
},
set(value) {
// Do a simple judgment, if the value has not changed, do nothing
if(val === value) return
val = value
// ** When the value of this attribute is updated, notify all workers to work **
deps.notify()
}
})
}
class Watcher {
constructor(vm) {
this.vm = vm
Dep.target = this
// Initialize Watcher by calling the render function once
this.vm.updateComponent()
// Add finished, cross off the name
Dep.target = null
}
update() {
// This worker is specialized in rendering
this.vm.updateComponent()
}
}
Copy the code
getter
Why do we call the render function when Watcher is initialized? Remember I said that the render function internally gets variables that are bound to the template! Watcher’s initialization step looks like this:
Initialize Watcher object ->> vm.updateComponent ->> vm.render ->> trigger getter!! ->> Add Watcher to the project
This is where Vue is clever. It does a bunch of things by triggering getters for the properties of the object. As for whether it makes sense to call the render function when Watcher is initialized. Vue’s render Watcher is created when the component is initialized. Vue defines the render function, but does not call it itself. Instead, the render Watcher is called by a rendering worker. This is true for the first render and for subsequent renderings.
It’s not very intuitive to say that, but let me give you an example of another Watcher, where the Watcher in our example has a fixed job, but in fact, when he initializes the Watcher, he passes an EXP to say who he needs to focus on, and calls back cb to say what he needs to do, okay
// exp to specify the bound value of the object
// cb specifies what to do after triggering the watcher.
class WatcherOther {
constructor(vm, exp, cb) {
this.vm = vm
this.exp = exp
this.cb = cb
this.val = this.get()
}
get() {
Dep.target = this
/ / and render watcher get exp this property as the value of the corresponding trigger its getter, add watcher into corresponding Dep
let value = this.vm.data.exp
Dep.target = null
return value
}
update() {
this.cb()
}
}
Copy the code
I hope you understand what I mean when I simplify the Watcher class to render the Watcher class. I don’t want to add some callback logic to the Watcher class. It doesn’t make any sense to understand the core of bidirectional binding
At this point, the process is almost complete, but there is one last thing left to do to get the demo running and allow it: fake the operation inside render to retrieve the template parameters
Take the previous code and do 🌰, assuming that there is such a template function, put it in the compiler, it will generate a render function
<template>{{msg}}</template>
<script>
let vm = new VueClass({
data: {
msg: 'hello 2077'}})</script>
Copy the code
The template will compile to a render function, which is as simple as that, and create a VueClass instance as follows:
let vm = new VueClass({
data: {
msg: 'hello 2077'
},
render() {
let msg = this.data.msg
console.log('Start to generate virtual nodes, found users in use${msg}! `)}})Copy the code
The above code is directly handwritten, no test, only logic, the following complete code, you can copy down their own run:
class VueClass {
constructor(options) {
this.data = options.data;
// The Vue compiler compiles the template to render
this.render = options.render;
}
// Update component, which can be interpreted as a VueClass render function
updateComponent() {
// Build a VNode tree (not a VNode tree)
this.render();
// ...
// Omit the Dom mounting operation}}function observe(target) {
if (target && typeoftarget ! = ="object") return;
Object.keys(target).forEach((key) = > {
defineReactive(target, key, target[key]);
});
}
function defineReactive(obj, key, val) {
// Treat each attribute as a project team, and assign a product manager to each project team
let deps = new Dep();
observe(val);
Object.defineProperty(obj, key, {
/ / can be enumerated
enumerable: true./ / can be configured
configurable: true.get() {
// Dep.target is global and the value is the Watcher that is currently waiting to be grouped
if (Dep.target) {
deps.addSub(Dep.target);
}
return val;
},
set(value) {
// Do a simple judgment, if the value has not changed, do nothing
if (val === value) return;
val = value;
// ** When the value of this attribute is updated, notify all workers to work **deps.notify(); }}); }// Product Manager Dep category
class Dep {
constructor() {
// Create a group
this.deps = [];
}
Sub is a Watcher
addSub(sub) {
this.deps.push(sub);
}
// Let's get started
notify() {
for (let i = 0, j = this.deps.length; i < j; i++) {
// Every worker in this group has to work
this.deps[i].update(); }}}// The programmer Watcher class
class Watcher {
// Initialize by passing a VueClass instance. The worker must know who the boss is, otherwise who pays the wages?
constructor(vm) {
this.vm = vm;
Dep.target = this;
// Initialize Watcher by calling the render function once
this.vm.updateComponent();
// Add finished, cross off the name
Dep.target = null;
}
// The worker starts to work
update() {
// This worker is specialized in rendering
this.vm.updateComponent(); }}// Start the test
let vm = new VueClass({
data: {
msg: "hello 2077".other: "other text"
},
render() {
console.log('I get this.data.msg, and the value isThe ${this.data.msg}Starts to generate a VNode tree); }});/ / to monitor the vm. The data
observe(vm.data);
// Create the RENDER Watcher for the VM instance. This process automatically starts the first render
new Watcher(vm);
// Modify the data attribute and see what happens.
vm.data.msg = "2077 is off again.";
vm.data.other = "other text changed"
Copy the code
Amazing!! If vm.data.msg is changed, it triggers the render function, but if vm.data.other is changed, it does not. The reason is that vm.data.other is not bound to the template. Then its getter won’t be fired, so you won’t add a Watcher to it
So if you look at this code and think about it a little bit you might have a few questions
-
Why give each property value new a Dep when there is at most one Watcher per Dep in the example and there is no need to store Watcher in an array
A: In this example, there is only one render Watcher, but in real life the user will also write watch on some properties (which you do), so you need to add a new Watcher for this property. When the value changes, deps.notify will call multiple Watcher in turn
-
If you attach another data to the template and modify it, doesn’t the render Watcher trigger the render multiple times
A: If you can think of this question, congratulations, you have basically fully understood Vue bidirectional binding principle. In our example, binding multiple pieces of data does trigger render multiple times, and changing the value of the same piece of data multiple times within a single tick will also trigger render multiple times. In a tick, you can use a map to store all the Watcher ids that have been fired. If you find a duplicate Watcher ID, do not fire it again.
-
Are there any other differences between a rendering Watcher and a regular Watcher, other than what they do?
A: there are! A render Watcher is automatically added to the deps of each property that is called by the render function, while a normal Watcher is added to the specified binding property
In fact, two-way binding this piece, Vue has done other optimization, here is not an example, if you are interested, strongly recommend yourself to read the source code, maybe you will be very painful when you read, but after the pain, you will find that they have the texture of the improvement.
Finally, I hope this article can help you, even a little bit, MY writing style is limited, has tried its best to express, if there is a mistake or logic writing is not clear, welcome to point out, common progress!
May you have fire in your heart and light in your eyes!