First of all, thanks to React, Vue, Angular, Cycle, JQuery and other third-party JS for the convenience of development.

Common frameworks (libraries) such as Vue and React are collectively referred to as “third-party JS”.

The current state of third-party JS

Whether it is a newcomer or an experienced developer, people in the front-end circle must have heard the name of this type of third-party JS. Partly because they’re so popular:

  • Various articles on the framework comparison, source code analysis to.
  • The number of stars on GitHub is growing rapidly.
  • Various training courses for the framework are emerging.
  • .

On the other hand, it is very convenient to develop with them:

  • Scaffolding tools can be used to build projects quickly with a few lines of command.
  • Reduce a lot of repeated code, structure more clear, readable.
  • There are rich UI libraries and plug-in libraries.
  • .

But the news that GitHub is dropping JQuery got me thinking:

What are the side effects of third-party JS besides convenience? Can we write efficient code without third-party JS?

Side effects of third-party JS

The snowball rolled

If you were asked to develop a project today, what would you do? If you are familiar with React, create-React-app can be used to quickly set up a project.

  • React, react-dom, and react-router-dom already write package.json, but that’s not all.
  • What about HTTP requests? Let’s bring in Axios.
  • What about dates? Introduce a moment or a day.
  • .

It’s important to remember that this kind of “copyism” is “addictive”, so third party dependencies are like a rolling snowball that gets bigger and bigger as development increases. If you use the Webpack-bundle-Analyzer tool to analyze projects, you’ll find that most of the project code is in the node_modules directory, which means it’s all third-party JS, typical of the 80/20 rule (80% of the source code is only 20% of the compiled volume).

Something like this:

You have to start optimizing things like code split (code size is not reduced, it’s just split) and tree shaking (are you sure the only code you’re shaking is the code you really depend on?). , the optimization effect is limited not to say, even worse is dependent on bundling. For example, the date component of the Ant-Design module relies on moment, so moment is introduced when we use it. And EVEN though I found that the smaller DayJS could basically replace the moment function, I was afraid to introduce it because replacing it with the date component would be problematic, and introducing it would increase the size of the project.

Some third party js was called a “bucket”, this term now reminds me of the PC tools software, would you want to install a computer butler, it constantly pop-up prompts you computer not safe, suggest you to install an anti-virus software, and prompt you for a long time didn’t update software, suggest you install any software butler… I was trying to fit one, but I ended up with the whole family.

Tool domesticated

If you look at the users of these third-party JS, you will see the following phenomena:

  • Exclusive. Some developers who use the MV* framework like to take sides in the discussion, for example VueJS are likely to make fun of ReactJS, and Angular developers are likely to spray VueJS.
  • Impetuous. For less experienced developers, the DOM manipulation in JavaScript is inefficient and should be done with third-party JS two-way data binding. Write your own XMLHTTPRequest to send the request how troublesome, to the third party JS call directly better.
  • Limitations. Some interviewees think they are familiar with some third-party JS after they feel good (even a lot of times this kind of “familiar” but also put in quotes), have a grasp of some third-party JS to master the front-end meaning.

These third-party JS were originally intended to improve the development efficiency of the tool, but unwittingly tamed developers, let them have dependence. If every time you are asked to develop a new project, you have to rely on third-party JS scaffolding to build the project before you can start writing code. Chances are you’ve developed an instrumental mindset, like holding a hammer in your hand. Everything is a nail, and your approach to questions and answers is likely to be limited by this. It also means that you are moving further and further away from the underlying native code, and the less familiar you are with native apis, the more dependent you are on third-party JS, and so on.

How do you break this? To recommend zhang Xinxu’s article “Unbreakable Philosophy and Personal Growth” first, of course, is to abandon them. It’s important to note that by giving up I don’t mean that all projects write their own frameworks, which is not efficient. It’s better to try it on projects that have more time and less impact. Such as developing a small tool for internal use within a company, or a small project with a small number of pages and time constraints (depending on personal development speed).

Here are two tips to follow when developing with native apis.

Understand the essence of

Although we don’t use any third-party JS, we can learn the principles and implementation of them. For example, if you know how to implement data binding such as dirty value detection and Object.defineProperty, you can use them when writing code. You will find that there is a long way to go between understanding these principles and actually using them. On the other hand, this can further deepen our understanding of third-party JS.

Of course, our goal is not to recreate a copycatted VERSION of JS, but to appropriately combine, delete and optimize the existing technology and ideas, customize the most appropriate code for the business.

One of the important reasons for the popularity of third-party JS mentioned in this article is that DOM manipulation is optimized or even hidden. JQuery claims to be a DOM manipulation tool, encapsulating the DOM as a JQ object and extending the API. The MV framework replaces JQuery because it goes one step further in DOM manipulation, directly masking the underlying manipulation and mapping data to templates. If these MVS were still thinking at the DOM level, they probably wouldn’t be on the scale they are today. Because DOM masking simply simplifies code, there are also code organization issues to consider when building large projects, namely abstraction and reuse. The way these third-party JS have chosen to do this is to “componentialize,” encapsulating HTML, JS, and CSS into a single scoped component that forms reusable code units.

Let’s do this without introducing any third-party JS.

Dependency free practice

web components

Consider componentization first. Browser natively supports Web Components, which consist of three key technologies that we’ll take a quick look at first.

Custom Elements

A set of JS apis that allow you to customize elements and their behavior and then use them as needed in your user interface. A simple example:

Class LoginForm extends HTMLElement {constructor() { super(); . CustomElements. Define ()'login-form', LoginForm); <! <login-form></login-form>Copy the code

Shadow DOM

A set of JS apis that create a visible DOM tree attached to a DOM element. The root node of this tree is called shadow root. Only shadow root can access the internal Shadow DOM, and external CSS styles do not affect the shadow DOM. This creates a separate scope.

The common Shadow root can be viewed using a browser debugging tool:

A simple example:

// 'open'Const shadow = dom.attachShadow({mode: attachShadow)'open'}) // call shadow dom shadow.appendChild(h1);Copy the code

HTML Templates

HTML template technology consists of two tags:

<! --> <template id="my-paragraph">< p><slot>My paragraph</slot></p> </template> // the use of templatelet template = document.getElementById('my-paragraph');
lettemplateContent = template.content; document.body.appendChild(templateContent); <! -- use slot --> <my-paragraph> <span slot="my-text">Let's have some different text! </span> </my-paragraph> <! <p> <span slot="my-text">Let's have some different text! </span> </p>Copy the code

Some simple examples are also provided on MDN. Here’s a complete example:

const str = `
  <style>
    p {
      color: white;
      background-color: # 666;
      padding: 5px;
    }
  </style>
  <p><slot name="my-text">My default text</slot></p>
`
class MyParagraph extends HTMLElement {
  constructor() {
    super();
    const template = document.createElement('template');
    template.innerHTML = str;
    const templateContent = template.content;
    this.attachShadow({mode: 'open'}).appendChild(
      templateContent.cloneNode(true)); } } customElements.define('my-paragraph', MyParagraph);
Copy the code

Complete components

However, such component functionality is too weak, because many times components need to interact with each other, such as the parent component passing parameters to the child component, and the child component calling the parent component callback function. Since it’s an HTML tag, it’s natural to want to pass it through attributes. Components also happen to have lifecycle functions that listen for property changes, seemingly perfect! But then there are the problems, first of all performance, which increases the number of reads and writes to the DOM. Secondly, there is the problem of data type. HTML tags can only pass simple data like strings, but not complex data like objects, arrays, functions, and so on. You’ll probably want to serialize and deserialize them to do that, not least to make the page ugly (imagine serializing an array of parameters of length 100). Second, the operation is complex, continuous serialization and deserialization is error-prone and increase performance consumption. Third, some data cannot be serialized, such as regular expressions, date objects, etc. The good news is that we can pass parameters by getting DOM instances from selectors. But then you inevitably have to manipulate the DOM, which is not a good way to handle it. Internally, on the other hand, we also need to manipulate the DOM if we need to dynamically display some data to a page.

Component internal views communicate with data

Mapping data to views can be done in the form of data binding, and changes to the view affect how data can be bound in the form of events.

Data binding

How to bind views to data is usually done by using specific template syntax, such as directives. For example, use the X-bind directive to worm data into the text content of a view. We do not consider the performance cost of dirty value detection, so what is left is to use object.defineProperty to listen for attribute changes. It is also important to note that a data can correspond to multiple views, so do not listen directly, but to create a queue for processing. Sort out the implementation ideas:

  1. Select with the selectorx-bindProperty, and the value of that property, for example<div x-bind="text"></div>The property value oftext.
  2. Set up a listening queuedispatcherHolds the attribute values and handlers for the corresponding elements. For example, the above element listens fortextProperty, and the handler isthis.textContent = value;
  3. Build a data modelstate, write the set function of the corresponding attribute, and execute it when the value changesdispatcherFunction in.

Sample code:

// Instruction selector and corresponding handler const map = {'x-bind'(value) {
    this.textContent = undefined === value ? ' ': value; }}; // Set up a listener queue, listen for data object properties worth changing, and then iterate over the execution functionfor (const p in map) {
  forEach(this.qsa(`[${p}]`), dom => {
    const property = attr(dom, p).split('. ').shift();
    this.dispatcher[property] = this.dispatcher[property] || [];
    const fn = map[p].bind(dom);
    fn(this.state[property]);
    this.dispatcher[property].push(fn);
  });
}
for (const property inthis.dispatcher) { defineProperty(property); } // Listen for the data object attribute const defineProperty = p => {const prefix ='_s_';
  Object.defineProperty(this.state, p, {
    get: () => {
      return this[prefix + p];
    },
    set: value => {
      if(this[prefix + p] ! == value) { this.dispatcher[p].forEach(fun => fun(value, this[prefix + p])); this[prefix + p] = value; }}}); };Copy the code

Isn’t this manipulating the DOM? It doesn’t matter, we can put DOM operations into base classes, so we don’t need to touch the DOM for business components.

Summary: use data binding VueJS same way here, but because of the data object properties can only have one set function, so set up a listening to the queue for processing different elements of data binding, this way of queue traversal and AngularJS dirty value detection mechanism is similar, but smaller triggering mechanism is different, the length of the array.

event

The idea of event binding is simpler than data binding, simply listening directly on DOM elements. Let’s use the click event as an example for binding, creating an event-bound directive, such as X-click. Implementation idea:

  1. Use the DOM selector to find thex-clickProperty.
  2. readx-clickThe value of the property, and at this point we need to evaluate the value of the property, because the value of the property might be the name of the function for examplex-click=fn, possibly a function callx-click=fn(a, true).
  3. Judge the underlying data types, such as booleans and strings, and add them to the call argument list.
  4. Add event listeners for DOM elements and call the corresponding function when the event is triggered, passing in parameters.

Sample code:

const map = ['x-click'];
map.forEach(event => {
  forEach(this.qsa(`[${event}Const property = attr(dom, event); // Get the function name const fnName = property.split('(') [0]; Const params = property.indexof (const params = property.indexof ('(') > 0? property.replace(/.*\((.*)\)/,'$1').split(', ') : [];
    letargs = []; // params. ForEach (param => {const p = param.trim(); const str = p.replace(/^'(. *)'$/, '$1').replace(/^"(. *)"$/, '$1');
      if(str ! == p) { // string args.push(str); }else if (p === 'true' || p === 'false') { // boolean
        args.push(p === 'true');
      } else if(! isNaN(p)) { args.push(p * 1); }else{ args.push(this.state[p]); }}); // Listen for events on(event.replace('x-'.' '), dom, e => {// call the function with the argument this[fnName](... params, e); }); }); });Copy the code

Bidirectional data binding for form controls is also easy, that is, setting up the data binding to modify the value and then setting up the event binding to listen for the input event.

Communication between components

After solving the problem of mapping views and data within the components, we can start to solve the problem of communication between components. The component needs to provide a property object to receive parameters, which we set to props.

Parent => child, data pass

To pass a value to the props property of a child component, the parent component needs to get an instance of the child component and then modify the props property. Since DOM manipulation is unavoidable, let’s consider putting DOM manipulation in the base class. So how do you find out which tags are subcomponents and which attributes of subcomponents need to be bound? Can you get it by naming the specification and selecting it? For example, component names that start with CMP – are not supported by the selector support, which, for the most part, both constrain encoding naming and have no specification guarantees. Simply put, there is no static detection mechanism. If a developer writes a component that does not start with CMP -, it will be more difficult to check for data transfer failure at runtime. So there is another place where component names can be collected, and that is by registering component functions. We register components with the customElements. Define function. One way to do this is to override the function directly and record the component name when registering the component, but this is a bit difficult to implement, and it is difficult to change the native API function without affecting the rest of the code. So the compromise is to align the wrapper and then use the wrapped function to register the component. This way we can log all the registered component names and then create instances to get the corresponding props. At the same time, write the set function on the properties of the props object to listen. At this point, we’re only half done, because we haven’t passed the data to the child components. If we don’t want to manipulate the DOM, we can just use the existing data binding mechanism, binding the properties that need to be passed to the data object. Here are some ideas:

  1. Created when writing child componentspropsObject and declare attributes that need to be passed as parameters, such asthis.props = {id: ''}.
  2. Child components are written without nativecustomElements.defineInstead, use wrapped functions such asdefineComponentTo register the component name and the correspondingpropsProperties.
  3. The parent component iterates through the child component as it uses it to find the child component and its correspondingpropsObject.
  4. The child componentspropsThe properties of the object are bound to the data object of the parent componentstateProperty, as the parent componentstateWhen the property value changes, the child component is automatically modifiedpropsAttribute values.

Sample code:

const components = {}; @param {string} Component (tag) name * @param {class} component implementation class */exportConst defineComponent = (name, componentClass) => {// customElements. Define (name, componentClass); Const CMP = document.createElement(name); // Create component instance const CMP = document.createElement(name); / / storage of components and the corresponding props attribute components [name] = Object. GetOwnPropertyNames (CMP) props) | | []; }; Class ChildComponent extends Component {constructor// Props (template, {id: value => {//... }}); } } defineComponent('child-component', ChildComponent); <! Use child component --> <child-component id="myId"></child-component> // Class ParentComponent extends Component {constructor() {
    super(template);
    this.state.myId = 'xxx'; }}Copy the code

There are many areas in the above code that can be further optimized, see the sample code at the end of this article.

Child => parent, callback function

The arguments of the child component are passed back to the parent component in the form of callback functions. The tricky part is calling a function using the scope of the parent component. You can scoped the functions of the parent component and pass in the props object property of the child component so that the component can be called and referenced normally. Because callback functions operate differently from parameters, which are passively received and actively invoked, they need to be declared with an ampersand, for example, referring to the scope object attribute of AngularJS directives. Clear your head:

  1. The properties of the subcomponent class that declares props are callback functions, such asthis.props = {onClick:'&'}.
  2. When the parent component initializes, it passes corresponding properties on the template, such as<child-compoennt on-click="click"></child-component>.
  3. Find the corresponding parent component function based on the child component property value, and then pass in the parent component function binding scope. Such aschildComponent.props.onClick = this.click.bind(this).
  4. A child component calls a parent component function, such asthis.props.onClick(...).

Sample code:

Class ChildComponent extends Component {constructor() {// Use the base class to declare the callback function property super(template, {onClick:'&'}); . this.props.onClick(...) ; } } defineComponent('child-component', ChildComponent); <! -- Use child component in parent --> <child-component on-click="click"></child-component> // Class ParentComponent extends Component {constructor() { super(template); } // Event passing in the base class operation click(data) {... }}Copy the code

Communication across the component hierarchy

Some components need descendant components to communicate, and passing through layers will write a lot of extra code, so we can operate in bus mode. That is to establish a global module, data sender to send messages and data, data receiver to listen.

The sample code

// bus.js // monitor queue const dispatcher = {}; /** * Receive message * name */exportconst on = (name, cb) => { dispatcher[name] = dispatcher[name] || []; const key = Math.random().toString(26).substring(2, 10); Push ({key, fn: cb});returnkey; }; // Send a messageexport const emit = function(name, data) { const dispatchers = dispatcher[name] || []; // Poll the queue and call the function dispatchers.forEach(dp => {dp.fn(data, this); }); }; // Cancel the listenerexportconst un = (name, key) => { const list = dispatcher[name] || []; const index = list.findIndex(item => item.key === key); // Remove the listener from the listener queueif(index > -1) {
    list.splice(index, 1);
    return true;
  } else {
    return false; }}; // ancestor.js import {on} from'./bus.js';

class AncestorComponent extends Component {
  constructor() {
    super();
    on('finish', data => {
      //...
    })    
  }
}

// child.js
class ChildComponent extends Component {
  constructor() {
    super();
    emit('finish', data); }}Copy the code

conclusion

For the detailed code of the base class, please refer to the warehouse address at the end of the article. At present, the project follows the principle of adding on demand, which only realizes some basic operations and does not complete all possible instructions. So it’s not quite a “framework”, it just gives you ideas and confidence to write native code.

Example: github.com/yalishizhud…

Original link: tech.gtxlab.com/web-compone…


Author information: Zhu Delong, Renhe Future Senior front End Engineer.