Petite Vue source code analysis - the working principle of v-if and v-for

Delve into how v-if works

<div v-scope="App"></div>

<script type="module">
  import { createApp } from 'https://unpkg.com/petite-vue?module'

  createApp({
    App: {
      $template: `
      <span v-if="status === 'offline'"> OFFLINE </span>
      <span v-else-if="status === 'UNKOWN'"> UNKOWN </span>
      <span v-else> ONLINE </span>
      `,
    }
    status: 'online'
  }).mount('[v-scope]')
</script>

Human flesh single step commissioning:

  1. Call createApp to generate the global scope rootScope according to the input parameters and create the root context rootCtx;
  2. Call mount to build the root block object rootBlock for < div V-Scope = "app" > < / div >, and use it as a template for parsing;
  3. The v-scope attribute is recognized during parsing, the local scope is obtained based on the global scope rootScope, and a new context ctx is constructed based on the root context rootCtx for the parsing and rendering of child nodes;
  4. Get the value of $template attribute and generate HTML elements;
  5. Depth first traversal and resolution of child nodes (call walkChildren);
  6. Parse < span V-IF = "status = = =" offline '"> offline</span>

Parse < span V-IF = "status = = =" offline '"> offline</span>

Next time, we continue the human flesh one-step debugging:

  1. Identify the element with v-if attribute and call_ The if original instruction parses the element and its sibling elements;
  2. Convert the elements with v-if and immediately followed by v-else-if and v-else into logical branch records;
  3. Loop through the branches, create block objects for the branches whose logical operation result is true, destroy the block objects of the original branches (render the block objects without the original branches for the first time), and submit the rendering task to the asynchronous queue.
// Documents/ src/walk.ts

// For ease of understanding, I have simplified the code
export const walk = (node: Node, ctx: Context): ChildNode | null | void {
  const type = node.nodeType
  if (type == 1) {
    // node is of Element type
    const el = node as Element

    let exp: string | null

    if ((exp = checkAttr(el, 'v-if'))) {
      return _if(el, exp, ctx) // Returns the latest sibling node without 'v-else-if' or 'v-else'
    }
  }
}
// Documents/ src/directives/if.ts

interface Branch {
  exp?: string | null // The branch logic operation expression
  el: Element // The template element corresponding to this branch will be used as a template to copy an instance through cloneNode and insert it into the DOM tree during each rendering
}

export const _if = (el: Element, exp: string, ctx: Context) => {
  const parent = el.parentElement!
  /* Anchor element. Because the elements identified by v-if, v-else-if and v-else may not be located on the DOM tree in a certain state,
   * Therefore, by marking the position information of the insertion point through the anchor element, the target element can be inserted into the correct position when the state changes.
   */
  const anchor = new Comment('v-if')
  parent.insertBefore(anchor, el)

  // Logical branch, and take the element identified by v-if as the first branch
  const branches: Branch[] = [
    {
      exp, 
      el
    }
  ]

  /* Locate the v-else-if and v-else elements and push them into the logical branch
   * There is no control over the order in which v-else-if and v-else appear, so we can write
   * <span v-if="status=0"></span><span v-else></span><span v-else-if="status === 1"></span>
   * But the effect is to become < span V-IF = "status = 0" > < / span > < span v-else > < / span >, and the last branch will never have a chance to match.
   */
  let elseEl: Element | null
  let elseExp: string | null
  while ((elseEl = el.nextElementSibling)) {
    elseExp = null
    if (
      checkAttr(elseEl, 'v-else') === '' ||
      (elseExp = checkAttr(elseEl, 'v-else-if'))
    ) {
      // Remove branch node from online template
      parent.removeChild(elseEl)
      branches.push({ exp: elseExp, el: elseEl })
    }
    else {
      break
    }
  }

  // Save the latest node without 'v-else' and 'v-else-if' as the template node for the next round of traversal resolution
  const nextNode = el.nextSibling
  // Remove node with 'v-if' from online template
  parent.removeChild(el)

  let block: Block | undefined // The block object corresponding to the branch whose current logical operation structure is true
  let activeBranchIndex: number = -1 // Branch index whose current logical operation structure is true

  // If the state changes, leading to the change of the branch index with the logical operation structure of true, the block object corresponding to the original branch needs to be destroyed (including the side-effect function under the suspension flag, monitoring the state change, executing the cleaning function of the instruction and recursively triggering the cleaning operation of the sub block object)
  const removeActiveBlock = () => {
    if (block) {
      // Reinsert the anchor element to locate the insertion point
      parent.insertBefore(anchor, block.el)
      block.remove()
      // Dereference the destroyed block object and let the GC recycle the corresponding JavaScript object and detached element
      block = undefined
    }
  }

  // Push the rendering task into the asynchronous task, which will be executed once in the Micro Queue execution phase of this round of Event Loop
  ctx.effect(() => {
    for (let i = 0; i < branches.length; i++) {
      const { exp, el } = branches[i]
      if (!exp || evaluate(ctx.scope, exp)) {
        if (i !== activeBranchIndex) {
          removeActiveBlock()
          block = new Block(el, ctx)
          block.insert(parent, anchor)
          parent.removeChild(anchor)
          activeBranchIndex = i
        }
        return
      }
    }

    activeBranchIndex = -1
    removeActiveBlock()
  })

  return nextNode
}

Let's take a look at the constructor, insert and remove methods of sub block objects

// Documents/ src/block.ts

export class Block {
  constuctor(template: Element, parentCtx: Context, isRoot = false) {
    if (isRoot) {
      // ...
    }
    else {
      // Use the elements of v-if, v-else-if and v-else branches as templates to create element instances
      this.template = template.cloneNode(true) as Element
    }

    if (isRoot) {
      // ...
    }
    else {
      this.parentCtx = parentCtx
      parentCtx.blocks.push(this)
      this.ctx = createContext(parentCtx)
    }
  }
  // Since the < template > element is not used in the current example, I have deleted the code
  insert(parent: Element, anchor: Node | null = null) {
    parent.insertBefore(this.template, anchor)
  }

  // Since the < template > element is not used in the current example, I have deleted the code
  remove() {
    if (this.parentCtx) {
      // TODO: function `remove` is located at @vue/shared
      remove(this.parentCtx.blocks, this)
    }
    // Remove the root node of the current block object, and its descendants will be removed together
    this.template.parentNode!.removeChild(this.template) 
    this.teardown()
  }

  teardown() {
    // First recursively call the cleanup method of the sub block object
    this.ctx.blocks.forEach(child => {
      child.teardown()
    })
    // Contains the stop side effect function to monitor the state change
    this.ctx.effects.forEach(stop)
    // Cleanup function for executing instructions
    this.ctx.cleanups.forEach(fn => fn())
  }
}

Delve into how v-for works

<div v-scope="App"></div>

<script type="module">
  import { createApp } from 'https://unpkg.com/petite-vue?module'

  createApp({
    App: {
      $template: `
      <select>
        <option v-for="val of values" v-key="val">
          I'm the one of options
        </option>
      </select>
      `,
    }
    values: [1,2,3]
  }).mount('[v-scope]')
</script>

Human flesh single step commissioning:

  1. Call createApp to generate the global scope rootScope according to the input parameters and create the root context rootCtx;
  2. Call mount to build the root block object rootBlock for < div V-Scope = "app" > < / div >, and use it as a template for parsing;
  3. The v-scope attribute is recognized during parsing, the local scope is obtained based on the global scope rootScope, and a new context ctx is constructed based on the root context rootCtx for the parsing and rendering of child nodes;
  4. Get the value of $template attribute and generate HTML elements;
  5. Depth first traversal and resolution of child nodes (call walkChildren);
  6. Parse < option V-for = "Val in values" v-key = "Val" > I'm the one of options < / option >

Parse < option V-for = "Val in values" v-key = "Val" > I'm the one of options < / option >

Next time, we continue the human flesh one-step debugging:

  1. Identify the element with the v-for attribute and call_ The for original instruction parses the element;
  2. Extract the expression string of set and set element in v-for and the expression string of key through regular expression;
  3. Create separate scopes based on each collection element and separate block object rendering elements.
// Documents/ src/walk.ts

// For ease of understanding, I have simplified the code
export const walk = (node: Node, ctx: Context): ChildNode | null | void {
  const type = node.nodeType
  if (type == 1) {
    // node is of Element type
    const el = node as Element

    let exp: string | null

    if ((exp = checkAttr(el, 'v-for'))) {
      return _for(el, exp, ctx) // Returns the latest sibling node without 'v-else-if' or 'v-else'
    }
  }
}
// Documents/ src/directives/for.ts

/* [\s\S]*Indicates that several white space characters and non white space characters are recognized. The default is greedy mode, that is, '(item, index) in value' will match the whole string.
 * Change to [\ s\S] *? It is lazy mode, that is ` (item, index) in value ` will only match ` (item, index)`
 */
const forAliasRE = /([\s\S]*?)\s+(?:in)\s+([\s\S]*?)/
// Used to remove ` (` and `) in ` (item, index) ``
const stripParentRE= /^\(|\)$/g
// Used to match `, index 'in ` item, index', then you can extract value and index for independent processing
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/

type KeyToIndexMap = Map<any, number>

// We only accept the form of "value in" and "value in" for the convenience of understanding, and we only accept the validity of all parameters
export const _for = (el: Element, exp: string, ctx: Context) => {
  // Extract the sub expression strings on both sides of 'in' in the expression string through regular expression
  const inMatch = exp.match(forAliasRE)

  // Save the template node for the next round of traversal resolution
  const nextNode = el.nextSibling

  // Insert the anchor point and remove the element with 'v-for' from the DOM tree
  const parent = el.parentElement!
  const anchor = new Text('')
  parent.insertBefore(anchor, el)
  parent.removeChild(el)

  const sourceExp = inMatch[2].trim() // Get 'value' in ` (item, index) in value '`
  let valueExp = inMatch[1].trim().replace(stripParentRE, '').trim() // Get 'item, index' in ` (item, index) in value '`
  let indexExp: string | undefined

  let keyAttr = 'key'
  let keyExp = 
    el.getAttribute(keyAttr) ||
    el.getAttribute(keyAttr = ':key') ||
    el.getAttribute(keyAttr = 'v-bind:key')
  if (keyExp) {
    el.removeAttribute(keyExp)
    // Serialize the expression, such as' value 'to' value ', so that it will not participate in subsequent expression operations
    if (keyAttr === 'key') keyExp = JSON.stringify(keyExp)
  }

  let match
  if (match = valueExp.match(forIteratorRE)) {
    valueExp = valueExp.replace(forIteratorRE, '').trim() // Get item in 'item, index'
    indexExp = match[1].trim()  // Get the index in 'item, index'
  }

  let mounted = false // false indicates the first rendering, and true indicates re rendering
  let blocks: Block[]
  let childCtxs: Context[]
  let keyToIndexMap: KeyToIndexMap // It is used to record the relationship between key and index. When re rendering occurs, elements are reused

  const createChildContexts = (source: unknown): [Context[], KeyToIndexMap] => {
    const map: KeyToIndexMap = new Map()
    const ctxs: Context[] = []

    if (isArray(source)) {
      for (let i = 0; i < source.length; i++) {
        ctxs.push(createChildContext(map, source[i], i))
      }
    }  

    return [ctxs, map]
  }

  // Create separate scopes based on collection elements
  const createChildContext = (
    map: KeyToIndexMap,
    value: any, // the item of collection
    index: number // the index of item of collection
  ): Context => {
    const data: any = {}
    data[valueExp] = value
    indexExp && (data[indexExp] = index)
    // Create a separate scope for each child element
    const childCtx = createScopedContext(ctx, data)
    // The key expression operates under the scope of the corresponding child element
    const key = keyExp ? evaluate(childCtx.scope, keyExp) : index
    map.set(key, index)
    childCtx.key = key

    return childCtx
  }

  // Create a block object for each child element
  const mountBlock = (ctx: Conext, ref: Node) => {
    const block = new Block(el, ctx)
    block.key = ctx.key
    block.insert(parent, ref)
    return block
  }

  ctx.effect(() => {
    const source = evaluate(ctx.scope, sourceExp) // Calculate the real value of items in ` (item, index) in items'
    const prevKeyToIndexMap = keyToIndexMap
    // Generate a new scope and calculate 'key', 'key: key' or 'v-bind:key'`
    ;[childCtxs, keyToIndexMap] = createChildContexts(source)
    if (!mounted) {
      // Create a block object for each child element, parse the child elements of the child element and insert the DOM tree
      blocks = childCtxs.map(s => mountBlock(s, anchor))
      mounted = true
    }
    // Since our example only studies static views, we'll learn more about the re rendered code later
  })

  return nextNode
}

summary

We can see that block objects are generated during the parsing of v-if and v-for, and each branch of v-if corresponds to a block object, while v-for corresponds to a block object for each child element. In fact, the block object is not only a unit for controlling DOM operations, but also a part used to represent the unstable structure of the tree. For example, the addition and deletion of nodes will lead to the instability of the tree structure. These unstable parts are packaged into independent block objects and encapsulated to perform resource recovery and other operations during their construction and deletion, which not only improves the readability of the code, but also improves the running efficiency of the program.

The first rendering and re rendering of v-if adopt the same set of logic, but v-for will use key to reuse elements when re rendering, so as to improve efficiency, and the algorithm can be copied a lot when re rendering. In the next article, we will learn more about how v-for works when re rendering. Please look forward to:)

Keywords: Javascript

Added by insanityonline on Mon, 07 Mar 2022 09:09:25 +0200