preface

This year, OKR set a KR to share front-end technologies once every quarter. There are still more than ten days left until 2020, and I have been busy with business development and have no time to prepare and learn high-end topics. Therefore, I have no choice but to talk about vue.js, which is frequently used but does not really understand its internal principles.

Because this article is a front-end technology to share the speech, so try not to stick vue.js source code, because stick code in the actual share, more boring, the effect is not good, and more in the form of pictures and words to express.

Shared goals:

  • Understand the componentization mechanism of vue.js
  • Understand the responsive system principle of vue. js
  • Understand Virtual DOM and Diff principle in vue. js

Share Keynote: Vue.js framework analysis. Key

The original address

Summary of Vue. Js

Vue is a set of progressive MVVM frameworks for building user interfaces. How do you think about incremental? Progressive meaning: minimum mandatory claims.

Vue.js includes declarative rendering, component-based systems, client routing, large-scale state management, build tools, data persistence, cross-platform support, etc. However, in the actual development, developers are not forced to follow a specific function, but gradually expand according to the needs.

The core library of Vue.js is only concerned with view rendering, and due to its progressive nature, vue.js is easy to integrate with third-party libraries or existing projects.

Mechanism of components

Definition: A component is an independent encapsulation of a function and style, allowing HTML elements to be extended so that code can be reused, making development flexible and more efficient.

Like HTML elements, components of vue.js have externally passed properties (prop) and events. In addition, components have their own state (data) and computed properties calculated from data and state (computed), and the dimensions combine to determine what the components ultimately look like and the logic for their interactions.

The data transfer

Each component is scoped in isolation, which means that there should be no references to data between components, and even if there are references, components are not allowed to manipulate data outside of the component itself. Vue allows you to pass prop data to components that explicitly declare the prop field. Declare a Child component as follows:

<! -- child.vue -->
<template>
    <div>{{msg}}</div>
</template>
<script>
export default {
    props: {
        msg: {
            type: String.default: 'hello world' // If default is a reference type, return function}}}</script>
Copy the code

The parent component passes data to this component:

<! -- parent.vue -->
<template>
    <child :msg="parentMsg"></child>
</template>
<script>
import child from './child';
export default {
    components: {
        child
    },
    data () {
        return {
            parentMsg: 'some words'}}}</script>
Copy the code

events

Vue internally implements an EventBus system, called EventBus. The idea of using EventBus as a bridge in Vue is that each component instance of Vue inherits EventBus, accepting the event $ON and sending the event $emit.

If the child.vue component wants to modify the parentMsg data of the parent. Vue component, what can be done? In order to ensure the traceability of data flow, it is not recommended to modify the MSG field of prop in the component directly, and in the example, it is a non-reference String, so it cannot be modified directly. In this case, the event of modifying parentMsg should be passed to child.vue. Let child.vue trigger the event to modify parentMsg. Such as:

<! -- child.vue -->
<template>
    <div>{{msg}}</div>
</template>
<script>
export default {
    props: {
        msg: {
            type: String.default: 'hello world'}},methods: {
        changeMsg(newMsg) {
            this.$emit('updateMsg', newMsg); }}}</script>
Copy the code

The parent component:

<! -- parent.vue -->
<template>
    <child :msg="parentMsg" @updateMsg="changeParentMsg"></child>
</template>
<script>
import child from './child';
export default {
    components: {
        child
    },
    data () {
        return {
            parentMsg: 'some words'}},methods: {
        changeParentMsg: function (newMsg) {
            this.parentMsg = newMsg
        }
    }
}
</script>
Copy the code

Parent component parent. Vue passes the updateMsg event to child component Child. vue. When the child component instantiates, the child component registers the updateMsg event with the $ON function.

In addition to event passing between parent and child components, a Vue instance can be used to bridge data communication between parent and child components at multiple levels, such as:

const eventBus = new Vue();

// The parent component uses $on to listen for events
eventBus.$on('eventName', val => {
    //  ...do something
})

// The child component fires the event using $emit
eventBus.$emit('eventName'.'this is a message.');
Copy the code

In addition to $ON and $EMIT, the event bus system provides two other methods, $once and $OFF, with all events as follows:

  • $on: Monitors and registers events.
  • $emit: Triggers the event.
  • $once: registers the event. The event is allowed to be triggered only once. The event is removed immediately after the event is triggered.
  • $off: Removes the event.

Content distribution

Vue implements a content distribution system that follows the Draft Web Components specification, using

elements as an outlet to host the distribution.

Slots slots, also a component’s HTML template, are displayed or not, and how they are displayed is up to the parent component. In fact, the two core issues of a slot are highlighted here: whether to display and how to display.

Slot and default slot, named slot.

The default slot

A component can have only one slot of this type, as opposed to a named slot.

Such as:

<template>
<! -- Parent component parent-vue -->
<div class="parent">
    <h1>The parent container</h1>
    <child>
        <div class="tmpl">
            <span>1 the menu</span>
        </div>
    </child>
</div>
</template>
Copy the code
<template>
<! -- Child.vue -->
<div class="child">
    <h1>Child components</h1>
    <slot></slot>
</div>
</template>
Copy the code

As shown above, the slot tag of the child component is replaced by the div.tmpl passed in by the parent component at render time.

A named slot

An anonymous slot is called an anonymous slot because it does not have a name attribute. Then, the slot becomes a named slot by adding the name attribute. A named slot can appear N times in a component, in different locations, with different name attributes.

Such as:

<template>
<! -- Parent component parent-vue -->
<div class="parent">
    <h1>The parent container</h1>
    <child>
        <div class="tmpl" slot="up">
            <span>The menu up 1</span>
        </div>
        <div class="tmpl" slot="down">
            <span>The menu down - 1</span>
        </div>
        <div class="tmpl">
            <span>Menu - > 1</span>
        </div>
    </child>
</div>
</template>
Copy the code
<template>
    <div class="child">
        <! -- named slot -->
        <slot name="up"></slot>
        <h3>Here are the child components</h3>
        <! -- named slot -->
        <slot name="down"></slot>
        <! -- Anonymous slot -->
        <slot></slot>
    </div>
</template>
Copy the code

As shown above, the slot tag replaces the value of the slot attribute passed in by the parent to the child tag.

If the slot name value is default, the slot name value is default. If the slot name value is default, the slot name value is default.

Scope slot

A scoped slot can be either a default slot or a named slot. The difference is that a scoped slot can bind data to a slot label so that its parent component can get data from a child component.

Such as:

<template>
    <! -- parent.vue -->
    <div class="parent">
        <h1>This is the parent component</h1>
        <current-user>
            <template slot="default" slot-scope="slotProps">
                {{ slotProps.user.name }}
            </template>
        </current-user>
    </div>
</template>
Copy the code
<template>
    <! -- child.vue -->
    <div class="child">
        <h1>This is the child component</h1>
        <slot :user="user"></slot>
    </div>
</template>
<script>
export default {
    data() {
        return {
            user: {
                name: 'xiao zhao'}}}}</script>
Copy the code

In the example above, when rendering the default slot, the child component passes the user data to the slot tag. During rendering, the parent component can obtain the user data through the slot-scope property and render the view.

Slot implementation principle: $slot. Default: vm.$slot.default: vm.$slot. XXX: vm.$slot. In this case, data can be passed to the slot, or if there is data, the slot can be a scoped slot.

So far, the parent-child component relationship is shown as follows:

Template rendering

At the heart of Vue.js is declarative rendering. Unlike imperative rendering, declarative rendering simply tells the program what we want it to look like and lets the program do the rest. Imperative rendering, on the other hand, requires the command program to perform the rendering step by step according to the command. The following example distinguishes:

var arr = [1.2.3.4.5];

// Imperative rendering, care about each step, care about the flow. Do it by command
var newArr = [];
for (var i = 0; i < arr.length; i++) {
    newArr.push(arr[i] * 2);
}

// Declarative rendering, do not care about the intermediate process, only need to care about the result and implementation conditions
var newArr1 = arr.map(function (item) {
    return item * 2;
});
Copy the code

Vue.js implements directives such as if, for, events, and data binding, allowing for concise template syntax to declaratively render data out of view.

Template compilation

Why template compilation? In fact, the template syntax in our component is not parsed by the browser because it is not the correct HTML syntax, whereas template compilation is the compilation of the component’s template into executable JavaScript code, which translates the template into an actual rendering function.

Template compilation is divided into three stages, parse, optimize, generate, and finally generate the render function.

Parse stage: The template is parsed as a string using the expression in progress to obtain instructions, class, style and other data and generate an abstract syntax tree AST.

Optimize phase: Find static nodes in the AST and mark them up for later VNode patch comparison. Nodes marked as static are ignored in subsequent diff algorithms without detailed comparisons.

Generate phase: Concatenate string of render function based on AST structure.

precompiled

For Vue components, template compilation is compiled only once, when the component is instantiated, and not after the rendering function is generated. Therefore, compiling is a performance drain on the component runtime. The purpose of template compilation is simply to convert the template into a render function, and this process can be completed during project construction.

For example, vue-loader of Webpack relies on vue-template-compiler module. During webpack construction, template is precompiled into render function, and the template compilation process can be directly skipped at runtime.

Looking back, the Runtime needs to be just the render function, and once we have pre-compiled, we just need to make sure that the render function is generated during the build. Like React, after adding JSX’s syntactic sugar compiler babel-plugin-transform-vue-jsx, we can write render functions directly in vue components using JSX syntax.

<script>
export default {
    data() {
        return {
            msg: 'Hello JSX.'
        }
    },
    render() {
        const msg = this.msg;
        return <div>
            {msg}
        </div>; }}</script>
Copy the code

With JSX, we can use HTML tags directly in our JAVASCRIPT code, and we no longer need to declare the template after we declare the render function. Of course, if we declare both the template tag and the render function, the template compilation will override the original render function during the build process, with the template taking precedence over the directly written render function.

Compared to Template, JSX is more flexible and has a natural advantage in dealing with complex components. Template, though a bit dull, has a simpler, more intuitive, and more maintainable code structure that is more consistent with the separation of view and logic.

Note that the resulting render function is run wrapped in the with syntax.

summary

Vue component transmits data through PROP and implements EventBus, which is integrated to monitor event registration, trigger event, and distribute content through slot.

In addition, the implementation of a declarative template system, at runtime or precompilation is to compile templates, generate rendering functions for component rendering view.

Responsive system

Vue. Js is a MVVM JS framework, when the data model data is modified, the view will be automatically updated, that is, the framework helps us complete the DOM update operation, without us manually operating DOM. When we assign values to the data, Vue tells all the components that depend on the data model that the data you depend on has been updated and you need to re-render. At this point, the component will re-render, completing the update of the view.

The data model evaluates property listeners

Within the component, you can define a data model, data, computed property, and a listener, watch, for each component.

Data model: During the creation of the Vue instance, each attribute of the data model data is added to the responsive system. When the data is changed, the view gets the response and updates synchronously. Data must be returned as a function. Data wrapped without return will be visible globally in the project, resulting in variable pollution. Variables in data wrapped with return only take effect in the current component and do not affect other components.

Computed properties: Computed results are cached based on component responsiveness dependencies. They are recalculated only if the relevant responsive dependencies change, that is, only if the responsive data it depends on (data, PROP, computed itself) changes. So when should you use computed properties? Expressions in templates are very convenient, but they are designed for simple calculations. Putting too much logic into a template can make it too heavy and difficult to maintain. For any complex logic, you should use computed properties.

Listener: As its name suggests, the listener Watch can monitor changes in responsive data, including Data, PROP, computed, and handle changes in responsive data accordingly. This approach is most useful when asynchronous or expensive operations need to be performed when data changes.

Response principle

In Vue, all attributes under the data model are proxyed by Vue using Object.defineProperty (Vue3.0 uses Proxy). The core mechanism of responsiveness is the observer mode, in which data is the observed party and changes are notified to all observers so that the observers can respond, for example, when the observer is a view, the view can make updates to the view.

Vue. Js has three important concepts: Observer, Dep and Watcher.

A publisher – the Observer

Observe plays the role of publisher. His primary role is to call defineReactive when the component VM is initialized, using object.defineProperty to hijack/listen on each of the child attributes of the Object. That is, add getters and setters for each property to make the corresponding property value reactive.

During component initialization, the initState function is called and initState, initProps, and initComputed methods are executed internally to initialize data, Prop, and computed, respectively, so that they become responsive.

When initializing props, traverse all props, call defineReactive, make each prop value reactive, and then mount it to _props. XXX is then propped to vm._props.

Similarly, when initializing data, as with prop, we traverse all data, call defineReactive, make each data attribute value reactive, then mount it to _data, and then proxy vm. XXX to vm._data.xxx via proxy.

To initialize computed, first create an observer Object, computed-watcher, then iterate over each attribute of computed, call the defineComputed method on each attribute value, and use Object.defineProperty to make it responsive, Proxy it to a component instance and you can access XXX to compute properties through vm. XXX.

Dispatch center/subscriber -Dep

Dep plays the role of scheduling center/subscriber. In the process of calling defineReactive to make the attribute value responsive, it instantiates a Dep for each attribute value. Its main function is to manage the Watcher, collect the observer and notify the observer of target updates. The observer list (dep.subs) is iterated over, notifying all watchers and letting subscribers perform their own update logic.

The task of its DEP is to collect its dependencies internally by calling the dep.depend() method in the getter method of the property and saving the observer (that is, the Watcher, be it the component’s render function, computed, or the property listener watch). In the setter method of the property, the dep.notify() method is called to notify all observers to perform the update, completing the dispatch of the update.

Observer – Watcher

Watcher plays the role of subscriber/observer. His main role is to provide a callback function for the observed properties and collect dependencies. When the observed value changes, Watcher receives notification from the dispatch center Dep and triggers the callback function.

Watcher can be divided into three categories: normal-watcher, computed- Watcher and render- Watcher.

  • Normal-watcher: defined in the component hook function watch, that is, any change in the listening property triggers the defined callback function.

  • Computed – Watcher: Defined in the component hook function computed, each computed property generates a corresponding Watcher object, but this type of Watcher has one characteristic: When the calculated property depends on other data, the property is not recalculated immediately, but only computed later when the property needs to be read elsewhere, which is called lazy computing.

  • Render – Watcher: Every component has a render-watcher that is called to update the component’s view when properties in data/computed change.

The three types of Watcher also have a fixed execution order, namely, computed-render -> normal-watcher -> render- Watcher. This ensures that, as much as possible, the computed property is the latest value when the component view is updated. If render- Watcher is ranked before compute-render, the computed value will be old when the page is updated.

summary

The Observer is responsible for intercepting data, Watcher is responsible for subscribing and observing data changes, Dep is responsible for receiving subscriptions and notifying observers, and Dep is responsible for receiving publications and notifying all Watcher.

Virtual DOM

In Vue, the template is compiled into a browser-executable render function, which is then mounted in a render- Watcher with a responsive system. When data changes, The dispatch center Dep tells the render- Watcher to perform the Render function to complete the view rendering and update.

The whole process seems smooth, but when the Render function is executed, if the DOM is completely removed and rebuilt each time, there is a huge performance cost, because we know that the browser DOM is “expensive”, and when we update the DOM frequently, there will be some performance issues.

To solve this problem, Vue abstracts the browser’S DOM using JS objects. This abstraction is called the Virtual DOM. Each node of the Virtual DOM is defined as a VNode. When the render function is executed each time, Vue Diff compares the vNodes before and after the update to find as few real DOM nodes as possible that we need to update, and then only updates the nodes that need to be updated. This solves the performance problem caused by frequent DOM updates.

VNode

A VNode is a virtual representation of a real DOM node. Each component instance of a Vue is mounted with a $createElement function, from which all VNodes are created.

For example, create a div:

// declare render function
render: function (createElement) {
    // You can also create a VNode with this.$createElement
    return createElement('div'.'hellow world');
}
Hellow world
Copy the code

After the render function is executed, the vNodes are mapped according to the VNode Tree to generate the real DOM to complete the rendering of the view.

Diff

Diff compares old and new VNodes, and then makes minimal changes to the view based on the comparison, rather than redrawing the entire view to the new VNode to improve performance.

patch

The diff inside vue.js is called patch. Its DIff algorithm compares tree nodes of the same layer instead of searching the tree layer by layer, so the time complexity is only O(n), which is a fairly efficient algorithm.

The function sameVnode is used to determine whether the new and old nodes are the same: the key value and tag must be the same, etc., and return true, otherwise false.

Before patch, check whether the old and new Vnodes meet sameVnode(oldVnode, newVnode) condition, and then enter the process patchVnode. Otherwise, it will be judged as different nodes, and the old nodes will be removed and new nodes will be created.

patchVnode

The main function of patchVnode is to determine how to update child nodes.

  1. If both the old and new vNodes are static, have the same key (representing the same node), and the new VNode is clone or once (marked with the V-once attribute, rendering only once), then only the DOM and VNode need to be replaced.

  2. If both the old and new nodes have children, the diff operation is performed on the children to perform updateChildren, which is also the core of the DIff operation.

  3. If the old node has no children and the new node has children, clear the text content of the old DOM node and add children to the current DOM node.

  4. When a new node has no children and an old node has children, all children of that DOM node are removed.

  5. When both old and new nodes have no children, it is simply a text replacement.

updateChildren

The core of Diff is to compare the data of the new parent node to determine how to operate the child node. In the process of comparison, since the old child node has a reference to the current real DOM, the new child node is only a VNode array, so in the process of traversal, if the real DOM needs to be updated, The real DOM operation will be performed directly on the old child node, and the new parent node will be synchronized to the end of the traversal.

OldStartIdx oldEndIdx newStartIdx newEndIdx oldStartIdx newEndIdx oldStartIdx oldEndIdx newEndIdx oldStartIdx newEndIdx A node whose index is between oldStartIdx and oldEndIdx represents the traversed node in the laozi node, so a node less than oldStartIdx or greater than oldEndIdx represents the untraversed node. Similarly, in the new child node array, the node whose index is between newStartIdx and newEndIdx is the node that is traversed in the parent node, so less than newStartIdx or greater than newEndIdx is the node that is not traversed.

With each traversal, the distance between oldStartIdx and oldEndIdx and newStartIdx and newEndIdx gets closer to the middle. The loop ends when oldStartIdx > oldEndIdx or newStartIdx > newEndIdx.

In traversal, extract the Vnode corresponding to index 4:

  • OldStartIdx: oldStartVnode
  • OldEndIdx: oldEndVnode
  • NewStartIdx: newStartVnode
  • NewEndIdx: newEndVnode

During diff, if a key exists and sameVnode is satisfied, the DOM node is reused; otherwise, a new DOM node is created.

First, oldStartVnode and oldEndVnode are compared with newStartVnode and newEndVnode in pairs. There are altogether 4 comparison methods: 2*2=4.

Case 1: If oldStartVnode and newStartVnode meet sameVnode, oldStartVnode and newStartVnode make patchVnode, and oldStartIdx and newStartIdx move right.

Case 2: Similar to case 1, when oldEndVnode and newEndVnode satisfy sameVnode, oldEndVnode and newEndVnode make patchVnode, and oldEndIdx and newEndIdx move left.

Case 3: If oldStartVnode and newEndVnode satisfy sameVnode, oldStartVnode is already behind oldEndVnode. At this point, when oldStartVnode and newEndVnode conduct patchVnode, the real DOM node of oldStartVnode needs to be moved to the back of oldEndVnode, and oldStartIdx moves to the right and newEndIdx moves to the left.

Situation 4: Similar to case 3, when oldEndVnode and newStartVnode meet sameVnode, it indicates that oldEndVnode has preceded oldStartVnode. At this point, when oldEndVnode and newStartVnode conduct patchVnode, the real DOM node of oldEndVnode should be moved to the front of oldStartVnode, and oldStartIdx should be moved to the right and newEndIdx to the left.

If none of the four conditions is satisfied, the node that matches newStartVnode with sameVnode is searched between oldStartIdx and oldEndIdx. If there is one, the actual DOM of the matching node is moved to the front of oldStartVnode.

If newStartVnode does not exist, newStartVnode is a new node. Create a new node before oldStartVnode.

The loop ends when oldStartIdx > oldEndIdx or newStartIdx > newEndIdx, at which point we need to deal with vNodes that have not been traversed.

OldStartIdx > oldEndIdx indicates that the old node has been traversed, but the new node has not been traversed. In this case, the new node needs to be created after oldEndVnode.

At this point, the child nodes are matched. Here is an example patch process diagram:

conclusion

To borrow an official picture:

Vue. Js implements a set of declarative rendering engines and compiles declarative templates into rendering functions at Runtime or precompile time, mounted in Watcher, in rendering functions (touch), A responsive system collects as Dependency on observers using getter methods for responsive data and notifies all observers of updates using setter methods for responsive data. The Watcher will Trigger the component’s render function, which generates a new Virtual DOM Tree. At this point, Vue will Diff the new and old Virtual DOM trees to find out the real DOM to be operated and update it.

The original address

reference

  • Talk about VueJs componentized programming

  • In-depth understanding of slot and slot-scope in VUE

  • Vue responsive system – Observe, Watcher, DEP

  • Vue. Js technology revealed

  • VirtualDOM with Diff (Vue implementation)