At present, a very important function of front-end framework is component, through which we can realize the abstraction and reuse of each module on the page. In this section, we introduce the realization of componentization by imitating the component el-Alert in Element. Of course, I have simplified some details in this process. It just abstracts the core of the process

We want to do something like this: when the page loads, there is only one button:Click this button to display the alert component, and then click the cross to the right of the alert component to hide it, something like this:

Since clicking the cross to the right of alert makes the component hide this feature involves a component’s callback, we’ll think about it later

So, based on our experience with Vue or React, the component definition code might look like this: render of the parent App:

render: function () { let children = [ this.createElement('button', { class: 'click-show', on: { click: This.showalert}}, 'show ')] if (this.alertVisible) {// let o = this.createElement('my-alert', Push (o)} return this.createElement('div', {class: 'alert-wrap'}, children)} {props: {content: 'left text'}}) children.Copy the code

My-alert is our custom component. According to the componentization design idea, my-Alert will be defined by an additional object:

let MyAlert = {
  props: {
    content: {
      default: ''
    },
  },
  data: function () {
    return {
    }
  },
  methods: {
  },
  render: function () {
    let children = [
      this.createElement('div', { class: 'text' }, this.content)
    ]
    return this.createElement('div', { class: 'alert' }, children)
  }
}
Copy the code

As you can see, the my-Alert component is the same as the top-level App component, including data, methods, render, etc. The difference is that the My-Alert component also has props, which represents the properties transmitted from the parent component to the child component, and the Content property represents the text displayed inside

We can consider the general idea of implementing this feature: Since my – alert and app, so both must also go through initData, initMethods initialization, renderAndCollectDependencies (rendering) and collect rely on process, CreateElement is called during render to create the DOM

When we call createElement, we’ll see a browser-supported div, header, etc., and we’ll see our own my-Alert tag. When we do that, we’ll instantiate the myAlert component. Take the DOM after render and place it in the children of the render method of the parent component

To rearrange, the process is as follows:

App.init -> App.update -> App.render -> App.createElement
							 -> MyAlert.init -> MyAlert.update -> MyAlert.render -> MyAlert.createElement
Copy the code

So with that in mind, we have to think about some of the problems that arise in this process, right?

1. How to associate a custom tag with a component object?

What does this question mean? React/Vue/React/React/React/React/React/React/React/React/React/React/React/React/React/React/React/React/React/React/React/React/React For example, in our code above, my-Alert in the render of the parent App is the child component

Since my-alert will eventually be created by createElement, createElement currently only supports creating HTML built-in DOM objects. To create custom components, you must do something special if your function tagName is not a browser-supported tagName

The createElement tagName is a string called ‘my-alert’. How do you find the definition of alert by ‘my-alert’?

This is the first problem we need to solve

2. When will parent and child components be instantiated? How does the process go from parent component instantiation to child component instantiation?

For the root component, the timing of its instantiation is obvious, and for Vue, usually in the entry file main.js:

import App from './App.vue' new Vue({ ... App })Copy the code

But when are the components in render under the App root initialized? What is the difference between the initialization of these components and the root component?

3. How does the child component know where to insert it? How is the timing of its insertion different from that of the root component? (This question can also be asked: What about the location and timing of insertion of child components?)

So we can start by looking at how the root component is inserted into the parent and for the root component, we give el, we tell it to insert at the end where el is, okay

For child components, however, there is no such parameter, and the insertion position depends on the structure of the parent component

The child component is inserted in the render process of the parent component. The parent component encounters an unknown label in the render process and starts to find the corresponding component definition of the label. After finding the component definition, it instantiates, init, and update the parent component

It is worth noting that we inserted the root component earlier by getting the parent of this.el and this.renderedElement and its position in all sibling nodes to find which child element to insert under which parent element

This. El, this.renderedElement, and nextElementSibling will not have a value when the DOM object is first created. So how is this place compatible with update child components?

= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = along we initially put forward the train of thought, with the above problems, we began to implement componentization step by step to make our framework

Where to start? We could actually go down the code we implemented in the last video and see where we get stuck app.init -> app.update -> app.render and all of this stuff seems to work but then when we go to createElement, Unknown my-alert will be encountered, this logic is not before so we start from here:

ViewComponent.prototype.createElement = function (tagName, attrs, childNodes) {
  if (isReservedTag(tagName)) {
    return createHTMLElement(tagName, attrs, childNodes)
  } else if (xxx) {
    return createComponent(tagName, attrs, childNodes)
  }
}
Copy the code

In this code, you can see that we do different processing according to the tagName. The else if part is the processing of the component, and the isReservedTag part is the processing of the browser-supported tag

If you can find the definition of the component based on the tagName, then you can go to this branch

Vue indexes the component to be used by defining the components property in the options of the component. Therefore, when defining the component, we need to add the components property if we use other components. Here we also implement the same method as Vue:

new ViewComponent({
  el: '#app',
  components: {
    MyAlert: MyAlert
  },
  data: function () {
    return {
      alertVisible: false
    }
  },
  methods: {
    showAlert: function () {
      this.alertVisible = true
    }
  },
Copy the code

Of course, there are local components in Vue, as well as global components, and Vue provides the global component registration method through the following function path: import Vue from ‘Vue’ -> initGlobalAPI -> initAssetRegisters

When a user registers a component through Vue.component, it is placed into Vue.options.com Ponents, and each component is merged into its own components by calling mergeOptions when initialized. Then the component itself can get the global component

In fact, I think it’s possible to put global components together in one place. When looking for components in createElement, look first for components defined by the component, and if you don’t find them, look globally

Of course, we’re not going to implement that logic here, just know that there’s a way to implement it, and it’s not that complicated

Going back to our code, now that we have the schema, it’s much easier to implement:

ViewComponent.prototype.createElement = function (tagName, attrs, childNodes) {
  if (isReservedTag(tagName)) {
    return createHTMLElement(this, tagName, attrs, childNodes)
  } else if (isValidComponent(this, tagName)) {
    return createComponent(this, tagName, attrs, childNodes)
  }
}

function isValidComponent (vm, tagName) {
  let normalizedComponentName = kebabToCamelCase(tagName)
  return vm.components[normalizedComponentName]
}
Copy the code

Along the way, createComponent implements the DOM object returned by Render, which hangs on the renderedElement property of the ViewComponent instance object. Whether you call Render or get renderedElement and hang it on an instance, you need a ViewComponent instance, so here’s where to instantiate the child components:

function createComponent (vm, tagName, attrs, childNodes) { let normalizedComponentName = kebabToCamelCase(tagName) let componentOptions = vm.components[normalizedComponentName] || {} let propsData = attrs.props componentOptions.propsData = propsData // The second question I mentioned earlier, Child component instantiation of timing let componentInstance = new ViewComponent (componentOptions) return componentInstance. RenderedElement}Copy the code

We can also see that we took over props from the component definition and put them in options. We’ll talk about properties later

That brings us to the answer to our second question

Let’s explore what goes wrong when instantiating a child component with a new ViewComponent

Note that in the isValidComponent and createComponent methods, we fetch components directly from the VM, so we need to initialize the options components on this. The same goes for props:

function ViewComponent (options) { this.el = options.el ? document.querySelector(options.el) : PropOptions = options. Props this.data = options.data this.methods = options.methods This.render = options.render // options.ponents = options.ponents this.$options = options  this.renderedElement = null this.init() }Copy the code

Next, ViewComponent constructors began to walk the init – > renderAndCollectDependencies – > update

Our third question in the update began to manifest itself

The third problem, however, was solved with very little code:

ViewComponent. Prototype. Update = function () {/ / clean up old DOM let oldRenderedElement = this. RenderedElement | | this. El / / I'm going to assign a value to the renderedElement, RenderedElement = this.render() if (oldRenderedElement) {let parent = oldRenderedElement.parentNode let sibling = oldRenderedElement.nextElementSibling parent.removeChild(oldRenderedElement)  if (sibling) { parent.insertBefore(this.renderedElement, sibling) } else { parent.appendChild(this.renderedElement) } } }Copy the code

As you can see, we have only added an if judgment in the outermost layer, which is compatible with the update case when the child component is instantiated

That is, since this.renderedElement is undefined and this.el is not passed, oldRenderedElement will also be undefined when the child component is instantiated. We don’t know or can’t find its parentNode at this point, so we won’t append it to parentNode at this point

When does it append to parentNode?

In fact, the update (which is also the first update) on the instantiation of the child component is done here, and after that, Will be returned in turn to renderAndCollectDependencies, init, ViewComponent constructor, the parent component in the createComponent (App) call

The createComponent method uses the componentInstance local variable to retrieve the My-Alert child. With this componentInstance, we can retrieve the DOM object created by the componentInstance: componentInstance.renderedElement

function createComponent (vm, tagName, attrs, childNodes) { let normalizedComponentName = kebabToCamelCase(tagName) let componentOptions = vm.components[normalizedComponentName] || {} let propsData = attrs.props let events = attrs.on componentOptions.propsData = propsData componentOptions.parentListeners = events componentOptions.$parent = vm let ComponentInstance = new ViewComponent(componentOptions) This is to create a good child components DOM object return componentInstance. RenderedElement}Copy the code

After that, the createComponent method completes, and returns to createElement of the parent App, render of the parent App, and inserts it as a child of the parent:

render: function () { let children = [ this.createElement('button', { class: 'click-show', on: { click: this.showAlert } }, Let o = this.alert ('my-alert', {props: _props)] if (this.alertVisible) {// o _props = this.alert ('my-alert', {props: {content: 'left text'}, on: {close: This.hidealert}}) // o will be placed in children.children.push (o)} // Children will be inserted into the parent component return this.createElement('div', {class: 'alert-wrap' }, children)Copy the code

To summarize: The createComponent returns the result that will eventually be used as the return value of our own call to createElement in the Render method in the parent component definition (a bit verbose, but seems simple enough).

The answer to the third question we raised above is that the child does not insert into the parent component in the update, but rather delays the insertion into the parent component’s Render as children

CreateComponent returns the DOM obtained by calling Render during the instantiation of the MyAlert subcomponent:

  render: function () {
    let children = [
      this.createElement('div', { class: 'text' }, this.content)
    ]
    return this.createElement('div', { class: 'alert' }, children)
  }
Copy the code

If a child component encounters a custom tag during render, then it will go again: This.createcomponent -> new ViewComponent(componentOptions) -> this.init -> this.update OldRenderedElement will also be undefined, and insertion will be deferred to the render function of the parent component

As an aside: since we haven’t introduced the concept of a virtual DOM yet, which means that the createElement call generates the real DOM directly, the timing of the child’s insertion into the parent may be slightly different later when we use the virtual DOM, as we’ll explain in more detail

My feelings after writing this article: When we usually finish a function, whether in business or in the framework design, we can anticipate various problems before doing this function and solve the problems while doing it. In fact, there are two completely different effects

Anticipating problems before doing them is a good indicator of a developer’s ability to deconstruct, think, and analyze problems

Unfortunately, the problems I just presented in this article are actually problems I encountered in implementing the framework after reading the source code many times, and I put it first when I wrote it

So, good refueling in the future