preface

Some time ago I developed strView.js, which is a JS library that can convert strings into views. What does that mean? Like this code:

<! 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>Strview.js</title>
</head>

<body>
    <div id="app"></div>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/strview.global.js"></script>
    <script>
        Strview.createView({
            el: "#app".data: {
                msg: 'Hello World'
            },
            template: `<p>{msg}</p>`});</script>
</body>

</html>
Copy the code

The following page is displayed:

You’ll see a Hello World message displayed on the page, and we’ll see that the HTML code has no tags and no Hello World text except for one ID name that is the app tag. At this point, moving on, in the JS code, we introduce strView.js, and we call it a createView method, passing in an object. We find the Hello World string in the object, and we see in the template property that it corresponds to a tag, which is the tag

{MSG}

. In addition, we see the MSG character wrapped in {}. Corresponds to the MSG property in the data object, which happens to have a value of Hello World. Let’s now change the value of the MSG property to see if the page has changed.

Above is a GIF.

Sure enough, it changed, so we know that strView.js converts strings to views in this way.

Strview.js: strview.js: strview.js: strview.js: strview.js: strview.js

www.maomin.club/site/strvie…

Next, we’ll take a look at the strview.js source code to see how it is implemented.

Analyze the source

The strview.js version for this analysis is 1.9.0

First, we get the source code. Here we use strview.js in production, which is the address in the example above:

Cdn.jsdelivr.net/npm/strview…

Let’s take a look at the source code, plus blank lines, source code altogether 125 lines. Uncompressed, it’s only 4KB.

var Strview = (function (exports) {
    'use strict';

    // global object
    const globalObj = {
        _nHtml: []._oHtml: []._el: null._data: null._template: null._sourceTemplate: null
    };

    // initialization
    function createView(v) {
        globalObj._data = v.data;
        globalObj._template = v.template;
        globalObj._sourceTemplate = v.template;
        globalObj._el = v.el;
        v.el ? document.querySelector(v.el).insertAdjacentHTML("beforeEnd", render(globalObj._template)) : console.error("Error: Please set el property!");
    }

    // event listeners
    function eventListener(el, event, cb) {
        document.querySelector(el).addEventListener(event, cb);
    }

    // processing simple values
    function ref() {
        return new Proxy(globalObj._data, {
            get: (target, key) = > {
                return target[key]
            },
            set: (target, key, newValue) = > {
                target[key] = newValue;
                setTemplate();
                return true; }})}// reactiveHandlers
    const reactiveHandlers = {
        get: (target, key) = > {
            if (typeof target[key] === 'object'&& target[key] ! = =null) {
                return new Proxy(target[key], reactiveHandlers);
            }
            return Reflect.get(target, key);
        },
        set: (target, key, value) = > {
            Reflect.set(target, key, value);
            setTemplate();
            return true}};// respond to complex objects
    function reactive() {
        return new Proxy(globalObj._data, reactiveHandlers)
    }

    // update the view
    function setTemplate() {
        const oNode = document.querySelector(globalObj._el);
        const nNode = toHtml(render(globalObj._sourceTemplate));
        compile(oNode, 'o');
        compile(nNode, 'n');
        if (globalObj._oHtml.length === globalObj._nHtml.length) {
            for (let index = 0; index < globalObj._oHtml.length; index++) {
                constelement = globalObj._oHtml[index]; element.textContent ! == globalObj._nHtml[index].textContent && (element.textContent = globalObj._nHtml[index].textContent); }}}// judge text node
    function isTextNode(node) {
        return node.nodeType === 3;
    }

    // compile DOM
    function compile(node, type) {
        const childNodesArr = node.childNodes;
        for (let index = 0; index < Array.from(childNodesArr).length; index++) {
            const item = Array.from(childNodesArr)[index];
            if (item.childNodes && item.childNodes.length) {
                compile(item, type);
            } else if(isTextNode(item) && item.textContent.trim().length ! = =0) {
                type === 'o'? globalObj._oHtml.push(item) : globalObj._nHtml.push(item); }}}// string to DOM
    function toHtml(domStr) {
        const parser = new DOMParser();
        return parser.parseFromString(domStr, "text/html");
    }

    // template engine
    function render(template) {
        const reg = / \ {(. +?) The \} /;
        if (reg.test(template)) {
            const key = reg.exec(template)[1];
            if (globalObj._data.hasOwnProperty(key)) {
                template = template.replace(reg, globalObj._data[key]);
            } else {
                template = template.replace(reg, eval(`globalObj._data.${key}`));
            }
            return render(template)
        }

        return template;
    }

    // exports
    exports.createView = createView;
    exports.eventListener = eventListener;
    exports.reactive = reactive;
    exports.ref = ref;

    Object.defineProperty(exports.'__esModule', { value: true });

    return exports; } ({}));Copy the code

First, we’ll see that the outermost layer defines a Strview variable, exposes it, and assigns an execute now function (IIFE) to the variable.

Let’s take a look at the immediate function.

var Strview = (function (exports) {
// ...} ({}));Copy the code

An exports parameter is passed and an empty object is passed immediately.

And then, let’s look at what’s inside the function.

We’ll see that there are a lot of variables and a lot of methods in the function, so let’s do it by function.

First, we see a global object with several properties defined in it. This is to reduce global variable pollution, JS can be arbitrarily defined to save all application resources of global variables, but global variables can weaken the flexibility of the program, increase the coupling between modules. One way to minimize the use of global variables is to create only one global variable in your application.

// global object
const globalObj = {
    _nHtml: [].// Store a new DOM array
    _oHtml: [].// Store the old DOM array
    _el: null.// Mount the DOM node
    _data: null.// Store data
    _template: null.// Template string
    _sourceTemplate: null // Source template string
};
Copy the code

Then, we move on to the initialization phase, which converts the template string into a view.

// initialization
function createView(v) {
    globalObj._data = v.data;
    globalObj._template = v.template;
    globalObj._sourceTemplate = v.template;
    globalObj._el = v.el;
    v.el ? document.querySelector(v.el).insertAdjacentHTML("beforeEnd", render(globalObj._template)) : console.error("Error: Please set el property!");
}
Copy the code

We see that the createView method passes in an argument to the object we passed in earlier:

Strview.createView({
        el: "#app".data: {
            msg: 'Hello World'
        },
        template: `<p>{msg}</p>`});Copy the code

We see that the attributes in the passed object are assigned to the global object globalObj. Check whether v. l is true in the last line, if so execute this line:

document.querySelector(v.el).insertAdjacentHTML("beforeEnd", render(globalObj._template)) 
Copy the code

This line of code executes the insertAdjacentHTML() method, which is interpreted this way on MDN.

The insertAdjacentHTML() method parses the specified text into an Element and inserts the resulting node into the specified location in the DOM tree. It does not reparse the element it is using, so it does not destroy existing elements within the element. This avoids the extra serialization step and makes it faster than using innerHTML directly.

The second argument that the insertAdjacentHTML() method passes in is a DOMString to be parsed as an HTML or XML element and inserted into the DOM tree. The render(GlobalobJ._template) method is the DOMString returned.

If false, console.error(” error: Please set el property!” ), output an error in the browser.

Now that this uses the render(globalobj._template) method, let’s take a look.

// template engine
function render(template) {
    const reg = / \ {(. +?) The \} /;
    if (reg.test(template)) {
        const key = reg.exec(template)[1];
        if (globalObj._data.hasOwnProperty(key)) {
            template = template.replace(reg, globalObj._data[key]);
        } else {
            template = template.replace(reg, eval(`globalObj._data.${key}`));
        }
        return render(template)
    }

    return template;
}
Copy the code

First, the Render (template) method takes an argument, the first of which is the template string.

Then, let’s look at the method. First, we define the re /\{(.+?). \}/, which matches the contents of {} in the template string. If the match is true, this logic is entered:

const key = reg.exec(template)[1];
if (globalObj._data.hasOwnProperty(key)) {
    template = template.replace(reg, globalObj._data[key]);
} else {
    template = template.replace(reg, eval(`globalObj._data.${key}`));
}
return render(template)
Copy the code

Exec (template)[1] const key = reg.exec(template)[1], which uses the reg.exec() method, MDN interprets it as follows:

The exec() method performs a search match in a specified string. Returns a result array or NULL.

JavaScript RegExp objects are stateful when the global or sticky flag bits are set (e.g. /foo/g or /foo/y). They record the position since the last successful match in the lastIndex property. With this feature, exec() can be used to iterate over multiple matches in a single String (including captured matches), whereas string.prototype.match () only returns matched results.

So, with this method we get the {} in the template string, which is typically an attribute in the _data data we access. First, we determine if the globalobj._data object has the key, and if so, we use the string replacement method replace to replace the corresponding placeholder key with the corresponding value. The recursion continues until reg.test(template) returns false. Finally, the Render () method returns the processed template.

After looking at the Render () method, let’s look at the event handling phase, which is the eventListener() method.

// event listeners
function eventListener(el, event, cb) {
    document.querySelector(el).addEventListener(event, cb);
}
Copy the code

The method is simple: the first argument is passed to a DOM selector, the second argument to an event name, and the third argument to a callback function.

Finally, let’s take a look at strview.js’s data response system.

// processing simple values
function ref() {
    return new Proxy(globalObj._data, {
        get: (target, key) = > {
            return target[key]
        },
        set: (target, key, newValue) = > {
            target[key] = newValue;
            setTemplate();
            return true; }})}// reactiveHandlers
const reactiveHandlers = {
    get: (target, key) = > {
        if (typeof target[key] === 'object'&& target[key] ! = =null) {
            return new Proxy(target[key], reactiveHandlers);
        }
        return Reflect.get(target, key);
    },
    set: (target, key, value) = > {
        Reflect.set(target, key, value);
        setTemplate();
        return true}};// respond to complex objects
function reactive() {
    return new Proxy(globalObj._data, reactiveHandlers)
}
Copy the code

The above code is mainly the implementation of reactive() and ref(). The reactive() method is designed for complex data processing, such as nested objects and arrays. The ref() method is mainly for simple data processing, such as raw values and a single non-nested object.

Both of them are based on Proxy proxies for data interception and response, as defined in MDN.

Proxy objects are used to create a Proxy for an object to intercept and customize basic operations (such as property lookup, assignment, enumeration, function calls, and so on).

The first parameter to both Proxy objects is globalobJ._data, which we defined at initialization, and the second parameter is an object that normally takes a function as a property. The get() and set() methods are defined, where get() is the catcher for the property read and set() is the catcher for the property set.

The difference between reactive() and ref() is that the reactive() method implements recursion by adding nested object judgments.

We see in the set() method that they both call the setTemplate() method. Now, let’s look at this method.

// update the view
function setTemplate() {
    const oNode = document.querySelector(globalObj._el);
    const nNode = toHtml(render(globalObj._sourceTemplate));
    compile(oNode, 'o');
    compile(nNode, 'n');
    if (globalObj._oHtml.length === globalObj._nHtml.length) {
        for (let index = 0; index < globalObj._oHtml.length; index++) {
            constelement = globalObj._oHtml[index]; element.textContent ! == globalObj._nHtml[index].textContent && (element.textContent = globalObj._nHtml[index].textContent); }}}Copy the code

First, we get the DOM node we mounted at initialization, and then we pass in the Render (Globalobj._sourceTemplate) method as the first argument using the toHtml() method.

Let’s start with the toHtml() method, where the first argument, domStr, is render(GlobalobJ._sourceTemplate).

// string to DOM
function toHtml(domStr) {
    const parser = new DOMParser();
    return parser.parseFromString(domStr, "text/html");
}
Copy the code

In the first line of the toHtml() method we instantiate a DOMParser object. Once you’ve created a parse object, you can use its parseFromString method to parse an HTML string.

Then, back in the setTemplate() method, the variable nNode is assigned toHtml(render(Globalobj._sourceTemplate)), which is processed as a DOM object.

Next, execute the compile() method.

compile(oNode, 'o');
compile(nNode, 'n');
Copy the code

Let’s look at this compile() method.

// compile DOM
function compile(node, type) {
    const childNodesArr = node.childNodes;
    for (let index = 0; index < Array.from(childNodesArr).length; index++) {
        const item = Array.from(childNodesArr)[index];
        if (item.childNodes && item.childNodes.length) {
            compile(item, type);
        } else if(isTextNode(item) && item.textContent.trim().length ! = =0) {
            type === 'o'? globalObj._oHtml.push(item) : globalObj._nHtml.push(item); }}}Copy the code

This method iterates through the DOM elements and stores each entry into the array we initialized, globalobJ._ohtml and GlobalobJ._nhtml, using the isTextNode() method.

// judge text node
function isTextNode(node) {
    return node.nodeType === 3;
}
Copy the code

The first argument to this method is a Node whose nodeType property equals 3 indicates that the Node is a text Node.

Finally, we go back to the setTemplate() method and execute the following code:

if (globalObj._oHtml.length === globalObj._nHtml.length) {
    for (let index = 0; index < globalObj._oHtml.length; index++) {
        const element = globalObj._oHtml[index];
        element.textContent !== globalObj._nHtml[index].textContent && (element.textContent = globalObj._nHtml[index].textContent);
    }
}
Copy the code

_oHtml[index].textContent = globalobJ._ohtml [index].textContent If not, simply assign GlobalobJ._nhtml [index]. TextContent to GlobalobJ._ohtml [index]. TextContent to complete the update.

Finally, we assign the defined methods to the passed exports object and return it.

// exports
exports.createView = createView;
exports.eventListener = eventListener;
exports.reactive = reactive;
exports.ref = ref;

Object.defineProperty(exports.'__esModule', { value: true });

return exports;
Copy the code

Object. DefineProperty (exports, ‘__esModule’, {value: true}) could also be exports.__esModule = true. This ostensibly identifies an exported object as an ES module.

With the continuous development of JS and the emergence of Node.js, JS gradually has a modular scheme. Before ES6, the most famous is CommonJS/AMD, AMD does not mention the basic use now. CommonJS is adopted by Node.js today, coexisting with the ES module. Since CommonJS was chosen as the early modular solution of Node.js, there are still a lot of CommonJS modules on NPM, and the JS community can’t get rid of CommonJS any time soon.

Webpack implements a CommonJS modular solution that supports packaging CommonJS modules as well as ES modules. The ES module is not completely compatible with CommonJS module. CommonJS module.exports has no corresponding expression in ES module, which is different from the default export default.

__esModule is compatible with ES module import CommonJS module default export scheme.

conclusion

At this point, strview.js source code analysis is completed. Thanks for reading ~

Development version

StrviewCLI is recommended for StrviewApp project scaffolding.

https://github.com/maomincoding/strview-app
Copy the code

The production version

Direct introduction of CDN links, current version is 1.9.0.

https://cdn.jsdelivr.net/npm/[email protected]/dist/strview.global.js
Copy the code

About the author

Author: Vam’s Golden Bean Road.

Currently, he focuses on front-end technology and is active in multiple technology communities. CSDN blog star of the Year 2019, CSDN blog has reached millions of visitors. Nuggets blog post repeatedly pushed to the home page, the total page view has reached hundreds of thousands.

In addition, my public number: front-end experience robbed road, the public continues to update the latest front-end technology and related technical articles.

Welcome attention, let us together in front of the road on the disaster!