What is virtual DOM? How to implement a virtual DOM?

Keywords: Vue

1, What is virtual DOM

Virtual DOM (virtual DOM)  ) I believe everyone is familiar with this concept   React   reach   Vue  , fictitious   DOM   It brings cross platform capability (react native) to both frameworks   and   Weex)

In fact, it's just a layer of abstraction from the real DOM, using JavaScript   Object (VNode)   Node) as the basic tree, the node is described by the attributes of the object. Finally, the tree can be mapped to the real environment through a series of operations

In a Javascript object, the virtual DOM   As a   Object object. It contains at least three attributes: tag name (tag), attribute (attrs) and child element object (children). The names of these three attributes may be different in different frameworks

The purpose of creating virtual DOM is to better render the virtual nodes into the page view, so the nodes of the virtual DOM object and the properties of the real DOM take care of each other

Virtual DOM technology is also used in vue

Define real DOM

<div id="app">
    <p class="p">Node content</p>
    <h3>{{ foo }}</h3>
</div>

Instantiate vue

const app = new Vue({
    el:"#app",
    data:{
        foo:"foo"
    }
})

By observing the render of render, we can get the virtual DOM

(function anonymous() {
 with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',{staticClass:"p"},
       [_v("Node content")]),_v(" "),_c('h3',[_v(_s(foo))])])}})

Through VNode, vue can create nodes, delete nodes and modify nodes for this abstract tree. Some minimum units to be modified are obtained through diff algorithm, and then the view is updated, which reduces dom operations and improves performance

2, Why do I need virtual DOM

DOM is very slow, its elements are very large, and most of the performance problems of pages are caused by Dom operations

A real DOM node, even the simplest div, contains many attributes. You can print it and feel it intuitively:

It can be seen that the cost of operating DOM is still expensive. Frequent operations will still cause page jams, affecting the user experience

for instance:

When you use the traditional native api or jQuery to operate the DOM, the browser will execute the process from beginning to end from building the DOM tree

When you need to update 10 DOM nodes in one operation, the browser is not so intelligent. After receiving the first DOM update request, you don't know that there are 9 subsequent update operations, so you will execute the process immediately and finally execute the process 10 times

Through VNode, 10 DOM nodes are also updated. Instead of operating the DOM immediately, the virtual DOM saves the diff content updated 10 times to a local js object, and finally attach es the js object to the DOM tree at one time to avoid a lot of unnecessary calculations

Many people think that the biggest advantage of virtual DOM is diff algorithm, which reduces the performance consumption caused by JavaScript operating real dom. Although this virtual DOM brings one advantage, it is not all. The biggest advantage of virtual DOM is that it abstracts the original rendering process and realizes cross platform capability, not limited to browser DOM, but Android and IOS The native components of can be recent hot applets or various GUI s

3, How to implement virtual DOM

First, let's look at the structure of VNode in vue

Source location: src/core/vdom/vnode.js

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  functionalContext: Component | void; // only for functional component root nodes
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    /*Label name of the current node*/
    this.tag = tag
    /*The object corresponding to the current node contains some specific data information. It is a VNodeData type. You can refer to the data information in VNodeData type*/
    this.data = data
    /*The child node of the current node is an array*/
    this.children = children
    /*The text of the current node*/
    this.text = text
    /*The real dom node corresponding to the current virtual node*/
    this.elm = elm
    /*The namespace of the current node*/
    this.ns = undefined
    /*Compile scope*/
    this.context = context
    /*Functional component scope*/
    this.functionalContext = undefined
    /*The key attribute of the node is used as the flag of the node for optimization*/
    this.key = data && data.key
    /*option options for components*/
    this.componentOptions = componentOptions
    /*The instance of the component corresponding to the current node*/
    this.componentInstance = undefined
    /*Parent node of the current node*/
    this.parent = undefined
    /*In short, it is whether it is native HTML or just plain text. innerHTML is true and textContent is false*/
    this.raw = false
    /*Static node flag*/
    this.isStatic = false
    /*Insert as follow node*/
    this.isRootInsert = true
    /*Is it a comment node*/
    this.isComment = false
    /*Is it a clone node*/
    this.isCloned = false
    /*Is there a v-once instruction*/
    this.isOnce = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next https://github.com/answershuto/learnVue*/
  get child (): Component | void {
    return this.componentInstance
  }
}

Here is a brief description of VNode:

  • Of all objects   context   Options all point to   Vue   example

  • elm   Property points to its corresponding real   DOM   node

vue generates VNode through createElement

Source location: src/core/vdom/create-element.js

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

You can see createElement above   The method is actually right  _ createElement   Method to judge the input of parameters

export function _createElement(
    context: Component,
    tag?: string | Class<Component> | Function | Object,
    data?: VNodeData,
    children?: any,
    normalizationType?: number
): VNode | Array<VNode> {
    if (isDef(data) && isDef((data: any).__ob__)) {
        process.env.NODE_ENV !== 'production' && warn(
            `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
            'Always create fresh vnode data objects in each render!',
            context`
        )
        return createEmptyVNode()
    }
    // object syntax in v-bind
    if (isDef(data) && isDef(data.is)) {
        tag = data.is
    }
    if (!tag) {
        // in case of component :is set to falsy value
        return createEmptyVNode()
    }
    ... 
    // support single function children as default scoped slot
    if (Array.isArray(children) &&
        typeof children[0] === 'function'
    ) {
        data = data || {}
        data.scopedSlots = { default: children[0] }
        children.length = 0
    }
    if (normalizationType === ALWAYS_NORMALIZE) {
        children = normalizeChildren(children)
    } else if ( === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children)
    }
 //  Create VNode
    ...
}

You can see that _createelementreceives five parameters:

  • context   express   VNode   The context is   Component   type

  • Tag represents a tag. It can be a string or a string   Component

  • data   express   VNode   Data, it is a   VNodeData   type

  • children   Represents the current   The child node of VNode, which is of any type

  • normalizationType   It represents the type of child node specification. Different types of specifications have different methods, mainly for reference   render   Is the function compiled or written by the user

According to normalization type   children have different definitions for their types

if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
} else if ( === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
}

The simpleNormalizeChildren method call scenario is   render   Functions are compiled

normalizeChildren method call scenarios are divided into the following two types:

  • render   The function is handwritten by the user

  • compile   slot,v-for   Nested arrays will be generated when

Both simpleNormalizeChildren and normalizeChildren are used to standardize children   Becomes a type   VNode   of   Array), I won't expand here

The source code of standardized children is located at src/core/vdom/helpers/normalzie-children.js

After normalizing children, create VNode

let vnode, ns
//  Judge tag
if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    //  If it is a built-in node, create a normal VNode directly
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // component
    //  If it is a component type, the VNode node will be created through createComponent
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}

createComponent is also used to create VNode

Source location: src/core/vdom/create-component.js

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }
 //  Build subclass constructor  
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  //  Install the component hook function and merge the hook function into data.hook
  installComponentHooks(data)

  //Instantiating a VNode returns that the VNode of the component does not have children
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

Let's briefly mention the three key processes for creating VNode from createComponent:

  • Construct subclass constructor Ctor

  • installComponentHooks install component hook function

  • instantiation   vnode

Summary

createElement   establish   VNode   The process of each   VNode   have   children,children   Each element is also a vnode, which forms a virtual tree structure to describe the real DOM tree structure

Posted by social_experiment on Sun, 24 Oct 2021 21:56:26 -0700