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!