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:
- Call createApp to generate the global scope rootScope according to the input parameters and create the root context rootCtx;
- Call mount to build the root block object rootBlock for < div V-Scope = "app" > < / div >, and use it as a template for parsing;
- 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;
- Get the value of $template attribute and generate HTML elements;
- Depth first traversal and resolution of child nodes (call walkChildren);
- 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:
- Identify the element with v-if attribute and call_ The if original instruction parses the element and its sibling elements;
- Convert the elements with v-if and immediately followed by v-else-if and v-else into logical branch records;
- 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:
- Call createApp to generate the global scope rootScope according to the input parameters and create the root context rootCtx;
- Call mount to build the root block object rootBlock for < div V-Scope = "app" > < / div >, and use it as a template for parsing;
- 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;
- Get the value of $template attribute and generate HTML elements;
- Depth first traversal and resolution of child nodes (call walkChildren);
- 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:
- Identify the element with the v-for attribute and call_ The for original instruction parses the element;
- Extract the expression string of set and set element in v-for and the expression string of key through regular expression;
- 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:)