digression

For us programmers, it’s better to do it yourself than to look at the code. So I plan to write this series, also can be a summary of their own learning, if there is a bad place to write, please point out more. This series of articles won’t update very quickly, because I’ll be writing more code wherever I go. I have to go to work like everyone else, so I don’t have much time. Currently, the update schedule is expected to be one or two times a week.

As for the content of this chapter, it was originally planned to be combined with the content of the next chapter diff, but it was divided into two parts because it might be too long. The next section will probably be updated during weekends.

The target

As usual, the goal of this section is to insert the generated DOM into the container div (#app). Let’s start with the goal trigger and work through the process from the bottom up.

  1. In order to insert the DOM, we have to have onemountMethod, which should take two arguments, the first target container (#app) and the second DOM tree, similar to the following:
mount('#app', domTree);
Copy the code
  1. Then thisdomTreeHow come? It should be generated through our Virtual DOM.
domTree = createDOMTree(vnodeTree)
Copy the code
  1. Same question as above,vnodeTreeIt should be generated by JSX parsing
vnodeTree = parse(jsx)
Copy the code

Now that we have defined our goals, we can begin the process step by step

Start with parsing

Previously we parsed JSX through our analytic functions in the previous chapter. It returns an object containing tag, attr, and children, which we parse first after calling the beforeMount lifecycle hook.

xm._callHook.call(xm, 'beforeMount');
    
// Generate a vNode and cache it on the instance
xm.$vnodeTree = parseJsxObj(xm.$render());
Copy the code

ParseJsxObj should return a vNode, so we need a vNode class:

class VNode {
  constructor(tagMsg) {
    // If it is a JSXObj object, parse it
    if(tagMsg instanceof JSXObj) {
      this.tag = tagMsg.tag;
      this.children = [];
      this.attrs = {};
      this.events = {};
      // Check whether it is a native tag
      // NativeTags is an array of NativeTags,
      // In addition, I also extended a method on the prototype to allow users to customize NativeTags by calling NativeTags.push()
      if(NativeTags.includes(this.tag)) this.isNativeTag = true;
      // If not, perform componentization
      else {
        // It is not a component
        this.isNativeTag = false;
      }
      Attrs is processed to separate out attributes and events
      tagMsg.attrs && Object.entries(tagMsg.attrs).forEach(([key, value]) = > {
        // A string starting with an uppercase letter on+ is an event
        if(key.match(/on[A-Z][a-zA-Z]*/)) {
          const eventName = key.substring(2.3).toLowerCase() + key.substring(3);
          this.events[eventName] = value;
        }
        // Otherwise, it is an attribute
        else this.attrs[key] = value; })}// Handle the null node
    // For conditional rendering, null is returned in JSX for items that are not displayed
    else if(tagMsg === null) {
      this.tag = null;
    }
    // If it is not, it is treated as a text node by default. The tag attribute of the text node is an empty string
    else {
      this.tag = ' ';
      this.text = tagMsg; }}// The child node is not handled in the constructor, but is added through the instance's active call to this method
  addChild(child) {
    this.children.push(child);
  }
  // Add real DOM attributes to cache real DOM
  addElement(el) {
    this.el = el; }}Copy the code

Now that you have the VNode class, you are ready to complete the parseJsxObj method

// Parse JSX, return VNodeTree
The jsxObj argument can be an object (plain node), a string (text node), or null
export const parseJsxObj = function(jsxObj) {
  const vnodeTree = new VNode(jsxObj);
  // Insert the child node into the parent node recursively
  jsxObj && jsxObj.children && jsxObj.children.forEach(item= > vnodeTree.addChild(parseJsxObj(item)));
  return vnodeTree;
}
Copy the code

At this point, we have generated our vnodeTree, so let’s move on to the next step.

Generate a DOM tree

Now that we have our vnodeTree, we can generate a DOM tree from the vnodeTree. The createDOMTree method should return either a DOM object or an object containing the DOM

The first parameter is an instance of Xue. The purpose of this is to bind this to the event method during the event binding process
const element = createDOMTree(xm, xm.$vnodeTree)
Copy the code

Based on this, we need an Element class for dom-related operations, which will be used for all subsequent DOM operations

class Element {
  // Pass xm as above
  constructor(vnode, xm) {
    this.xm = xm;
    // If it is null, no processing is done
    if(vnode.tag === null) return;
    // Non-text node
    if(vnode.tag ! = =' ') {
      this.el = document.createElement(vnode.tag);
      // Bind attributes
      Object.entries(vnode.attrs).forEach(([key, value]) = > {
        this.addAttribute(key, value);
      });
      // Bind events
      Object.entries(vnode.events).forEach(([key, value]) = > {
        this.addEventListener(key, value.bind(xm));
      });
    }
    // Text node
    else this.el = document.createTextNode(vnode.text);

  }
  // The child node is not processed in the constructor, adding the child node through an external active call to this method
  appendChild(element) {
    this.el.appendChild(element.el);
  }
  // Add attributes for className and style
  // class is a reserved word, style takes an object
  addAttribute(name, value) {
    if(name === 'className') {
      this.el.setAttribute('class', value);
    }
    else if(name === 'style') {
      Object.entries(value).forEach(([styleKey, styleValue]) = > {
        this.el.style[styleKey] = styleValue; })}else {
      this.el.setAttribute(name, value); }}// Add event listener
  addEventListener(name, handler) {
    this.el.addEventListener(name, handler);
  }
  // Remove event listening
  removeEventListener(name, handler) {
    this.el.removeEventListener(name, handler); }}Copy the code

Create an Element in the same way as create a VNode.

// The update operation is involved. For the first rendering, it takes only one parameter, which is the same logic as the VNode generation above
export const createDOMTree = function(xm, vnodeTree) {
  const elementTree = new Element(vnodeTree, xm);
  // Recursive calls to add child nodes
  vnodeTree.children.forEach(item= > elementTree.appendChild(createDOMTree(xm, item)));
  // Cache the current DOM object in VNode. You can modify the DOM directly after you find the difference during diff
  vnodeTree.addElement(elementTree);
  return elementTree;
}
Copy the code

Now that we have our DOM, we need to implement the mount method to mount the DOM to our page

// Here I attach the _mount method to the prototype
Xue.prototype._mount = function(dom) {
  const root = this.$options.root;
  // If it is a string, the corresponding node is our root node
  if(typeof root === 'string') this.$el = document.querySelector(root);
  // This corresponds to the logic of the componentized part
  else if(root instanceof HTMLElement) this.$el = root;
  this.$el.appendChild(dom);
}
Copy the code

This chapter summarizes

Finally, in our init function, what we add this time is actually this:

xm._callHook.call(xm, 'beforeMount');

/ / generated vnode
xm.$vnodeTree = parseJsxObj(xm.$render());

// Generate and mount the DOM
xm._mount.call(xm, createDOMTree(xm, xm.$vnodeTree).el);

xm._callHook.call(xm, 'mounted');
Copy the code

This is the end of the chapter, and you have successfully mounted the component for the first time. Next, we need to complete the component update. The following is a preview of the next section update, which is to prepare for the update.

Do some preparatory work for update

First of all, the Update method is directly related to Wacther, so let’s refine our old Watcher class

/ / Watcher
let id = 0;
class Watcher {
  // cb is the callback executed by watcher. Type indicates the type of watcher: render or user
  // Only consider the render part first, then implement the user
  constructor(cb, type) {
    this.id = id++;
    this.deps = [];
    this.type = type;
    this.cb = cb;
  }
  addDep(dep) {
    const depIds = this.deps.map(item= > item.id);
    if(dep && ! depIds.includes(dep.id))this.deps.push(dep);
  }
  run() {
    this.cb(); }}Copy the code

And then in init, we’re going to modify the parameters that we passed in in new Watcher

// the argument passed in the init function
new Watcher((a)= > {
  // Invoke the beforeUpdate hook
  xm._callHook.call(xm, 'beforeUpdate');
  // Generate a new vNode
  const newVnodeTree = parseJsxObj(xm.$render());
  // A new vnode is returned after the update
  Xm is passed in so that after the update, this still points to the Xue instance, which is mainly applied to the event handler
  xm.$vnodeTree = update(xm, newVnodeTree, xm.$vnodeTree);
}, 'render');
Copy the code

In the previous section, we queued the Watcher array when we sent the update, but when we triggered the update, we did it synchronously, which caused every time we updated the dependency, Render and diff all over again, which is a waste of performance, so we’ll make it asynchronous. In fact, it is a nextTick process, so the nextTick should be encapsulated first:

// Return a Promise that results in resolve
function nextTick() {
  // Why not use setTimeOut? If setTimeOut is used, because setTimeOut is a macro task, our update process will be blocked by other micro tasks, which will affect performance
  // For updates, it should be executed immediately after the main thread completes execution, and should not be blocked
  return Promise.resolve();
}
export default nextTick;
Copy the code

With our nextTick, we need to rework the queue code:

let queue = [];
let waiting = false;
export const addUpdateQueue = function(watchers) {
  const queueSet = new Set([...queue, ...watchers]);
  queue = [...queueSet];
  // Sort to ensure that the parent component's watcher is generated with the child component first
  // Of course, the component part is not finished, so it can be ignored here
  queue.sort((a, b) = > a.id - b.id);
  // Use waiting variable control so that the process of traversing Watcher is executed only once
  if(! waiting) { waiting =true;
    nextTick().then((a)= > {
      // We need to get queue.length dynamically, because other dependencies may change during Watcher traversing the queue
      // This case has not been dealt with here and will be covered in a later section
      for(let i = 0; i < queue.length; i++) {
        // Execute Watcher's callback
        queue[i].run();
        // After traversal, reset waiting
        if(i === queue.length - 1) waiting = false; }}); }}Copy the code

This concludes the chapter, and the update process will be explained in detail in the next section. Stay tuned……

Github address: Click here to jump to

Chapter one: starting from scratch, using the idea of Vue, develop a own JS framework (I) : basic architecture

Chapter 3: start from scratch, using the idea of Vue, develop a own JS framework (I) : Update and diff

Chapter 4: starting from scratch, using the idea of Vue, develop a own JS framework (4) : componentization and routing components