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' indicates that the shadow dom can be accessed by the js function 'open'}) // call shadow dom shadow.appendChild(h1);
Copy the code
HTML Templates
HTML template technology consists of two tags:
and
. When you need to reuse the same DOM structures on a page, you can wrap them with the Template tag and reuse them. The slot tag makes templates more flexible, allowing users to customize some of the content in the template. A simple example is as follows:
<! --> <template id="my-paragraph"> <p><slot>My paragraph</slot></p> </template = document.getElementById('my-paragraph'); let templateContent = template.content; document.body.appendChild(templateContent); <! <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:
- Select with the selector
x-bind
Property, and the value of that property, for example<div x-bind="text"></div>
The property value oftext
. - Set up a listening queue
dispatcher
Holds the attribute values and handlers for the corresponding elements. For example, the above element listens fortext
Property, and the handler isthis.textContent = value
; - Build a data model
state
, write the set function of the corresponding attribute, and execute it when the value changesdispatcher
Function in.
Sample code:
// Instruction selector and corresponding handler const map = {'x-bind'(value) {this.textContent = undefined === value? '' : value; }}; // Set up a listener queue. For (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 in this.dispatcher) { defineProperty(property); } // Listen for data object attributes 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:
- Use the DOM selector to find the
x-click
Property. - read
x-click
The 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)
. - Judge the underlying data types, such as booleans and strings, and add them to the call argument list.
- 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']; ForEach (event => {forEach(this.qsa(' [${event}] '), dom => {const property = attr(dom, event); Const fnName = property.split('(')[0]; Const params = property.indexof ('(') > 0? property.replace(/.*\((.*)\)/, '$1').split(',') : []; let args = []; // 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 an event on the event. The replace (' x ', '), the dom, e = > {/ / the function and the incoming parameters 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:
- Created when writing child components
props
Object and declare attributes that need to be passed as parameters, such asthis.props = {id: ''}
. - Child components are written without native
customElements.define
Instead, use wrapped functions such asdefineComponent
To register the component name and the correspondingprops
Properties. - The parent component iterates through the child component as it uses it to find the child component and its corresponding
props
Object. - The child components
props
The properties of the object are bound to the data object of the parent componentstate
Property, as the parent componentstate
When the property value changes, the child component is automatically modifiedprops
Attribute values.
Sample code:
const components = {}; /** * Export const defineComponent = (name, const defineComponent); 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() {// Using the base class to create templates // using the base class to listener functions super(template, {id: value => { // ... }}); } } defineComponent('child-component', ChildComponent); <! --> <child-component id="myId"></child-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:
- The properties of the subcomponent class that declares props are callback functions, such as
this.props = {onClick:'&'}
. - When the parent component initializes, it passes corresponding properties on the template, such as
<child-compoennt on-click="click"></child-component>
. - Find the corresponding parent component function based on the child component property value, and then pass in the parent component function binding scope. Such as
childComponent.props.onClick = this.click.bind(this)
. - A child component calls a parent component function, such as
this.props.onClick(...)
.
Sample code:
Class ChildComponent extends Component {constructor() {constructor() {// Declare the constructor property super(template, {onClick: '&'}); . this.props.onClick(...) ; } } defineComponent('child-component', ChildComponent); <! --> <child-component on-click="click"></child-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 = {}; / * * * * name * / receiving messages export const on = (name, cb) = > {dispatcher [name] = dispatcher [name] | | []; const key = Math.random().toString(26).substring(2, 10); Push ({key, fn: cb}); return key; }; / / send a message export const emit = function (name, data) {const dispatchers = dispatcher [name] | | []; // Poll the queue and call the function dispatchers.forEach(dp => {dp.fn(data, this); }); }; / / cancel listening export const UN = (name, key) = > {const list = dispatcher [name] | | []; const index = list.findIndex(item => item.key === key); If (index > -1) {list.splice(index, 1); if(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.
Concrete example: https://github.com/yalishizhude/web-component
This article may be forwarded or shared, but must retain the complete picture and text information and source, the author reserves the right to investigate all legal responsibility and means ~
Search the concerned public number “Web learning club” ~