Abstract: In-depth JS series 19.
- How does JavaScript work: Write your own Web development framework + React and its virtual DOM principle
- Author: Front-end xiaozhi
FundebugReproduced with authorization, copyright belongs to the original author.
This is the 19th article in a series devoted to exploring JavaScript and the components it builds.
If you missed the previous chapters, they can be found here:
- How JavaScript works: An overview of the engine, runtime, and call stack!
- How JavaScript works: Dive into the V8 Engine & 5 Tips for Writing Optimized Code!
- How JavaScript works: Memory Management + How to Handle 4 common Memory Leaks!
- How JavaScript works: Event loops and the rise of asynchronous programming + 5 ways to code better with async/await!
- How JavaScript works: Explore websocket and HTTP/2 with SSE + how to choose the right path!
- How JavaScript works: Compare to WebAssembly and its usage scenarios!
- How JavaScript works: Building blocks for Web Workers + 5 scenarios to use them!
- How JavaScript works: Service Worker lifecycle and Usage Scenarios!
- How JavaScript works: The Web push notification mechanism!
- How JavaScript works: Use MutationObserver to keep track of DOM changes!
- How JavaScript works: Render Engines and tips for optimizing their performance!
- How JavaScript works: Dive deep into the Web Layer + How to optimize Performance and security!
- How JavaScript works: CSS and JS animation fundamentals and how to optimize their performance!
- How JavaScript works: Parsing, Abstract Syntax Tree (AST) + 5 tips to Speed up Compilation!
- How JavaScript works: Delving into classes and inheriting internals + Converting between Babel and TypeScript!
- How JavaScript works: Storage Engine + How to choose the right storage API!
- How JavaScript works: Internal structure of Shadow DOM + How to write independent components!
- How JavaScript works: WebRTC and peer-to-Peer Mechanisms!
Response principle
Proxy allows us to create a virtual Proxy of an object (surrogate object) and provides us with methods (handlers) that we can intercept when accessing or modifying the original object, such as set(), get(), and deleteProperty(). This way we can avoid two very common restrictions (in VUE) :
- Vue.$set() is used to add new responsiveness attributes and to delete existing responsiveness attributes
- Array update detection
Proxy
let proxy = new Proxy(target, habdler);
Copy the code
- Target: a target object wrapped in a Proxy (can be an array object, a function, or another Proxy)
- Handler: An object that intercepts functions for filtering agent operations
Instance methods
methods | describe |
---|---|
handler.apply() | Intercepts the operation of a Proxy instance as a function call |
handler.construct() | Intercepts the operation of a Proxy instance as a function call |
handler.defineProperty() | Intercepting the operation of Object.defineProperty() |
handler.deleteProperty() | Intercepts the Proxy instance delete attribute operation |
handler.get() | Intercepts an operation to read a property |
handler.set() | The operation of a truncated attribute assignment |
handler.getOwnPropertyDescriptor() | Interception Object. GetOwnPropertyDescriptor () operation |
handler.getPrototypeOf() | Intercepts the operation to retrieve the prototype object |
handler.has() | Intercept property retrieval operations |
handler.isExtensible() | Intercepting the Object.isextensible () operation |
handler.ownKeys() | Interception Object. GetOwnPropertyDescriptor () operation |
handler.preventExtension() | Cut the Object (). PreventExtension () operation |
handler.setPrototypeOf() | Intercepting the Object.setPrototypeof () operation |
Proxy.revocable() | Create a cancelable Proxy instance |
Reflect
Reflect is a built-in object that provides methods to intercept JavaScript operations. These methods are the same as those of the processor object. Reflect is not a function object, so it is not constructible.
Unlike most global objects, Reflect has no constructor. You cannot use it with a new operator, or call the Reflect object as a function. All attributes and methods of Reflect are static (just like Math objects).
Why Reflect?
1. More useful return values
Earlier written:
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
Copy the code
Reflect writing:
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
Copy the code
2. Functional operations
Earlier written:
'name' in Object //true
Copy the code
Reflect writing:
Reflect.has(Object.'name') //true
Copy the code
3. Variable argument constructors
General writing:
var obj = newF(... args)Copy the code
Reflect writing:
var obj = Reflect.construct(F, args)
Copy the code
Of course, there are many, you can go to MND to view
What is the proxy design pattern
Proxy, which provides a Proxy for other objects to control access to this object. The proxy pattern allows proxy objects to control references to concrete objects. A proxy can be almost any object: a file, a resource, an in-memory object, or something that is hard to copy. A real-life analogy might be access to a bank account.
For example, you can’t directly access your bank account balance and change the value as needed; you have to ask the person who does (in this case, your bank).
var account = {
balance: 5000
}
var bank = new Proxy(account, {
get: function (target, prop) {
return 9000000; }});console.log(account.balance); // 5,000
console.log(bank.balance); / / 9000000
console.log(bank.currency); / / 9000000
Copy the code
In the example above, when the Bank object is used to access the account balance, the getter function is overridden to always return 9,000,000 instead of the property value, even if the property does not exist.
var bank = new Proxy(account, {
set: function (target, prop, value) {
// Always set property value to 0
return Reflect.set(target, prop, 0); }}); account.balance =5800;
console.log(account.balance); // 5,800
bank.balance = 5400;
console.log(account.balance); / / 0
Copy the code
By overriding the set function, you can modify its behavior. You can change the value to be set, change other properties, or even do nothing at all.
responsive
Now that you have a basic understanding of how the proxy design pattern works, let’s start writing JavaScript frameworks.
For simplicity, AngularJS syntax will be simulated. Declare controllers and bind template elements to controller properties:
<div ng-controller="InputController">
<! -- "Hello World!" -->
<input ng-bind="message"/>
<input ng-bind="message"/>
</div>
<script type="javascript">
function InputController (a) {
this.message = 'Hello World! ';
}
angular.controller('InputController', InputController);
</script>
Copy the code
First, define a controller with properties, and then use this controller in the template. Finally, use the ng-bind attribute to enable bidirectional binding to element values.
Parse the template and instantiate the controller
To bind properties, you need to get a controller to declare these properties, so it is necessary to define a controller and introduce it into the framework.
During controller declarations, the framework looks for elements with the NG-Controller attribute.
If it matches one of the declared controllers, it creates a new instance of that controller, which is only responsible for this particular template.
var controllers = {};
var addController = function (name, constructor) {
// Store controller constructor
controllers[name] = {
factory: constructor, instances: [] }; // Look for elements using the controller var element = document.querySelector('[ng-controller=' + name + ']'); if (! element){return; // No element uses this controller
}
// Create a new instance and save it
var ctrl = new controllers[name].factory;
controllers[name].instances.push(ctrl);
// Look for bindings.....
};
addController('InputController', InputController);
Copy the code
This is the controller variable declaration handled manually. The Controllers object contains all controllers declared within the framework by calling addController.
For each controller, a Factory function is saved to instantiate a new controller if needed, and the framework also stores each new instance of the same controller used in the template.
Find the bind attribute
Now that you have an instance of a controller and a template that uses that instance, the next step is to look for elements that have bindings that use controller properties.
var bindings = {};
// Note: element is the dom element using the controller
Array.prototype.slice.call(element.querySelectorAll('[ng-bind]'))
.map(function (element) {
var boundValue = element.getAttribute('ng-bind');
if(! bindings[boundValue]) { bindings[boundValue] = {boundValue: boundValue,
elements: []
}
}
bindings[boundValue].elements.push(element);
});
Copy the code
Above, it stores all the binding values of the object. This variable contains all the attributes to bind to the current value and all the DOM elements to bind to that attribute.
The bugs that may exist after code deployment cannot be known in real time. In order to solve these bugs, I spent a lot of time on log debugging. Incidentally, I recommend a good BUG monitoring tool for youFundebug.
Two-way binding
Now that the framework has done its initial work, it’s time for the fun part: bidirectional binding. It involves binding the Controller property to a DOM element so that the DOM is updated when the code updates the property value.
Also, don’t forget to bind the DOM element to the Controller property. This way, when the user changes the input value, it updates the Controller property, and then it updates all the other elements bound to that property.
Use proxies to detect code updates
As mentioned above, the Vue3 component listens for response property changes through an encapsulation proxy. Here you just add a proxy for the controller to do the same thing.
// Note: ctrl is the controller instance
var proxy = new Proxy(ctrl, {
set: function (target, prop, value) {
var bind = bindings[prop];
if(bind) {
// Update each DOM element bound to the property
bind.elements.forEach(function (element) {
element.value = value;
element.setAttribute('value', value);
});
}
return Reflect.set(target, prop, value); }});Copy the code
Whenever a binding property is set, the agent checks all elements bound to the property and updates them with the new value.
In this case, we only support the input element binding because only the value attribute is set.
Respond to events
The last thing to do in response to user interaction is for the DOM element to fire an event when a value change is detected.
Listen for these events and update the binding property with the new value of the event, and all other elements bound to the same property are automatically updated thanks to the proxy.
Object.keys(bindings).forEach(function (boundValue) {
var bind = bindings[boundValue];
// Listen elements event and update proxy property
bind.elements.forEach(function (element) {
element.addEventListener('input'.function (event) {
proxy[bind.boundValue] = event.target.value; // Also triggers the proxy setter}); })});Copy the code
React && Virtual DOM
You’ll then learn how to run React using a single HTML file, explaining concepts such as Functional Component, function component, JSX, and Virtual DOM.
React provides a way to build code from components.
<! -- Skipping all HTML5 boilerplate -->
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<! -- For JSX support (with babel) -->
<script src="https://unpkg.com/[email protected]/babel.min.js" charset="utf-8"></script>
<div id="app"></div> <! -- React mounting point-->
<script type="text/babel">
class Watch extends React.Component {
render() {
return <div>{this.props.hours}:{this.props.minutes}</div>;
}
}
ReactDOM.render(<Watch hours="9" minutes="15"/>, document.getElementById('app'));
</script>
Copy the code
Ignore the HTML boilerplate and script for the dependencies, and the remaining lines are the React code. First, define the Watch component and its template, then mount React into the DOM to render the Watch component.
Inject data into the component
Our Wacth component is simple; it just displays the hours and minutes we pass to it.
You can try to modify the values of these properties (called props in React). It will eventually display what you passed to it, even if it’s not a number.
const Watch = (props) =>
<div>{props.hours}:{props.minutes}</div>;
ReactDOM.render(<Watch hours="Hello" minutes="World"/>, document.getElementById('app'));
Copy the code
Props is just data passed to the component through the surrounding component, and the component uses props for business logic and rendering.
But when props are not components, they are immutable. Therefore, the component that provides props is the only code that can update the props value.
Using props is very simple, creating DOM nodes using component names as tag names. It is then given the props name property, and the value passed in can be obtained from the this.props component.
What about HTML without quotes?
Notice that the render function returns unquoted HTML. This use is JSX syntax, which is a shorthand syntax for defining HTML templates in the React component.
// Equivalent to JSX: <Watch hours="9" minutes="15"/>
React.createElement(Watch, {'hours': '9'.'minutes': '15'});
Copy the code
Now you might want to avoid using JSX to define component templates; in fact, JSX looks like syntactic sugar.
The following code snippet builds the same result using the JSX and React syntax, respectively.
// Using JS with React.createElement
React.createElement('form'.null,
React.createElement('div', {'className': 'form-group'},
React.createElement('label', {'htmlFor': 'email'}, 'Email address'),
React.createElement('input', {'type': 'email'.'id': 'email'.'className': 'form-control'}),
),
React.createElement('button', {'type': 'submit'.'className': 'btn btn-primary'}, 'Submit'))// Using JSX
<form>
<div className="form-group">
<label htmlFor="email">Email address</label>
<input type="email" id="email" className="form-control"/>
</div>
<button type="submit" className="btn btn-primary">Submit</button>
</form>
Copy the code
Explore the virtual DOM further
The last part is a bit more complex, but interesting, and will help you understand the underlying principles of React.
Updating elements on the page (nodes in the DOM tree) involves using the DOM API. It will redraw the page, but it can be slow (see this article to see why).
Many frameworks such as React and vue.js get around this problem by proposing a solution called the virtual DOM.
{
"type":"div"."props": {"className":"form-group" },
"children":[
{
"type":"label"."props": {"htmlFor":"email" },
"children": ["Email address"] {},"type":"input"."props": {"type":"email"."id":"email"."className":"form-control"},
"children"] : []}}Copy the code
The idea is simple. Reading and updating DOM trees is expensive. Therefore, make as few changes as possible and update as few nodes as possible.
Reduce calls to the DOM API and keep the DOM tree structure in memory, and since we’re talking about a JavaScript framework, it makes sense to choose a JSON data structure.
This treatment immediately shows the changes in the virtual DOM.
In addition, the virtual DOM will cache some update operations so that they can be rendered on the real DOM later, so frequent re-rendering can cause performance problems.
Do you remember React. CreateElement? In effect, this function creates a new node in the Virtual DOM (either directly or through JSX).
To apply the update, the core feature of the Virtual DOM comes into play, namely the coordination algorithm, whose job is to provide the optimal solution to resolve the difference between the previous and current Virtual DOM states.
The original:
A quick guide to learn React and how its Virtual DOM works
How to Improve Your JavaScript Skills by Writing Your Own Web Development Framework
About Fundebug
Fundebug focuses on real-time BUG monitoring for JavaScript, wechat applets, wechat games, Alipay applets, React Native, Node.js and Java online applications. Since its launch on November 11, 2016, Fundebug has handled more than 900 million error events, and paid customers include Google, 360, Kingsoft, Minming.com and many other brands. Welcome to try it for free!