深刻v-if的工作原理

<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>

人肉单步调试:

  1. 调用createApp依据入参生成全局作用域rootScope,创立根上下文rootCtx
  2. 调用mount<div v-scope="App"></div>构建根块对象rootBlock,并将其作为模板执行解析解决;
  3. 解析时辨认到v-scope属性,以全局作用域rootScope为根底运算失去部分作用域scope,并以根上下文rootCtx为底本一起构建新的上下文ctx,用于子节点的解析和渲染;
  4. 获取$template属性值并生成HTML元素;
  5. 深度优先遍历解析子节点(调用walkChildren);
  6. 解析<span v-if="status === 'offline'"> OFFLINE </span>

解析<span v-if="status === 'offline'"> OFFLINE </span>

书接上一回,咱们持续人肉单步调试:

  1. 辨认元素带上v-if属性,调用_if原指令对元素及兄弟元素进行解析;
  2. 将附带v-if和跟紧其后的附带v-else-ifv-else的元素转化为逻辑分支记录;
  3. 循环遍历分支,并为逻辑运算后果为true的分支创立块对象并销毁原有分支的块对象(首次渲染没有原分支的块对象),并提交渲染工作到异步队列。
// 文件 ./src/walk.ts// 为便于了解,我对代码进行了精简export const walk = (node: Node, ctx: Context): ChildNode | null | void {  const type = node.nodeType  if (type == 1) {    // node为Element类型    const el = node as Element    let exp: string | null    if ((exp = checkAttr(el, 'v-if'))) {      return _if(el, exp, ctx) // 返回最近一个没有`v-else-if`或`v-else`的兄弟节点    }  }}
// 文件 ./src/directives/if.tsinterface Branch {  exp?: string | null // 该分支逻辑运算表达式  el: Element // 该分支对应的模板元素,每次渲染时会以该元素为模板通过cloneNode复制一个实例插入到DOM树中}export const _if = (el: Element, exp: string, ctx: Context) => {  const parent = el.parentElement!  /* 锚点元素,因为v-if、v-else-if和v-else标识的元素可能在某个状态下都不位于DOM树上,   * 因而通过锚点元素标记插入点的地位信息,当状态发生变化时则能够将指标元素插入正确的地位。   */  const anchor = new Comment('v-if')  parent.insertBefore(anchor, el)  // 逻辑分支,并将v-if标识的元素作为第一个分支  const branches: Branch[] = [    {      exp,       el    }  ]  /* 定位v-else-if和v-else元素,并推入逻辑分支中   * 这里没有管制v-else-if和v-else的呈现程序,因而咱们能够写成   * <span v-if="status=0"></span><span v-else></span><span v-else-if="status === 1"></span>   * 但成果为变成<span v-if="status=0"></span><span v-else></span>,最初的分支永远没有机会匹配。   */  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'))    ) {      // 从在线模板移除分支节点      parent.removeChild(elseEl)      branches.push({ exp: elseExp, el: elseEl })    }    else {      break    }  }  // 保留最近一个不带`v-else`和`v-else-if`节点作为下一轮遍历解析的模板节点  const nextNode = el.nextSibling  // 从在线模板移除带`v-if`节点  parent.removeChild(el)  let block: Block | undefined // 以后逻辑运算构造为true的分支对应块对象  let activeBranchIndex: number = -1 // 以后逻辑运算构造为true的分支索引  // 若状态发生变化导致逻辑运算构造为true的分支索引发生变化,则须要销毁原有分支对应块对象(蕴含停止旗下的副作用函数监控状态变动,执行指令的清理函数和递归触发子块对象的清理操作)  const removeActiveBlock = () => {    if (block) {      // 从新插入锚点元素来定位插入点      parent.insertBefore(anchor, block.el)      block.remove()      // 解除对已销毁的块对象的援用,让GC回收对应的JavaScript对象和detached元素      block = undefined    }  }  // 向异步工作对抗压入渲染工作,在本轮Event Loop的Micro Queue执行阶段会执行一次  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}

上面咱们看看子块对象的构造函数和insertremove办法

// 文件 ./src/block.tsexport class Block {  constuctor(template: Element, parentCtx: Context, isRoot = false) {    if (isRoot) {      // ...    }    else {      // 以v-if、v-else-if和v-else分支的元素作为模板创立元素实例      this.template = template.cloneNode(true) as Element    }    if (isRoot) {      // ...    }    else {      this.parentCtx = parentCtx      parentCtx.blocks.push(this)      this.ctx = createContext(parentCtx)    }  }  // 因为以后示例没有用到<template>元素,因而我对代码进行了删减  insert(parent: Element, anchor: Node | null = null) {    parent.insertBefore(this.template, anchor)  }  // 因为以后示例没有用到<template>元素,因而我对代码进行了删减  remove() {    if (this.parentCtx) {      // TODO: function `remove` is located at @vue/shared      remove(this.parentCtx.blocks, this)    }    // 移除以后块对象的根节点,其子孙节点都一并被移除    this.template.parentNode!.removeChild(this.template)     this.teardown()  }  teardown() {    // 先递归调用子块对象的清理办法    this.ctx.blocks.forEach(child => {      child.teardown()    })    // 蕴含停止副作用函数监控状态变动    this.ctx.effects.forEach(stop)    // 执行指令的清理函数    this.ctx.cleanups.forEach(fn => fn())  }}

深刻v-for的工作原理

<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>

人肉单步调试:

  1. 调用createApp依据入参生成全局作用域rootScope,创立根上下文rootCtx
  2. 调用mount<div v-scope="App"></div>构建根块对象rootBlock,并将其作为模板执行解析解决;
  3. 解析时辨认到v-scope属性,以全局作用域rootScope为根底运算失去部分作用域scope,并以根上下文rootCtx为底本一起构建新的上下文ctx,用于子节点的解析和渲染;
  4. 获取$template属性值并生成HTML元素;
  5. 深度优先遍历解析子节点(调用walkChildren);
  6. 解析<option v-for="val in values" v-key="val">I'm the one of options</option>

解析<option v-for="val in values" v-key="val">I'm the one of options</option>

书接上一回,咱们持续人肉单步调试:

  1. 辨认元素带上v-for属性,调用_for原指令对该元素解析;
  2. 通过正则表达式提取v-for中汇合和汇合元素的表达式字符串,和key的表达式字符串;
  3. 基于每个汇合元素创立独立作用域,并创立独立的块对象渲染元素。
// 文件 ./src/walk.ts// 为便于了解,我对代码进行了精简export const walk = (node: Node, ctx: Context): ChildNode | null | void {  const type = node.nodeType  if (type == 1) {    // node为Element类型    const el = node as Element    let exp: string | null    if ((exp = checkAttr(el, 'v-for'))) {      return _for(el, exp, ctx) // 返回最近一个没有`v-else-if`或`v-else`的兄弟节点    }  }}
// 文件 ./src/directives/for.ts/* [\s\S]*示意辨认空格字符和非空格字符若干个,默认为贪心模式,即 `(item, index) in value` 就会匹配整个字符串。 * 批改为[\s\S]*?则为懈怠模式,即`(item, index) in value`只会匹配`(item, index)` */const forAliasRE = /([\s\S]*?)\s+(?:in)\s+([\s\S]*?)/// 用于移除`(item, index)`中的`(`和`)`const stripParentRE= /^\(|\)$/g// 用于匹配`item, index`中的`, index`,那么就能够抽取出value和index来独立解决const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/type KeyToIndexMap = Map<any, number>// 为便于了解,咱们假如只承受`v-for="val in values"`的模式,并且所有入参都是无效的,对入参有效性、解构等代码进行了删减export const _for = (el: Element, exp: string, ctx: Context) => {  // 通过正则表达式抽取表达式字符串中`in`两侧的子表达式字符串  const inMatch = exp.match(forAliasRE)  // 保留下一轮遍历解析的模板节点  const nextNode = el.nextSibling  // 插入锚点,并将带`v-for`的元素从DOM树移除  const parent = el.parentElement!  const anchor = new Text('')  parent.insertBefore(anchor, el)  parent.removeChild(el)  const sourceExp = inMatch[2].trim() // 获取`(item, index) in value`中`value`  let valueExp = inMatch[1].trim().replace(stripParentRE, '').trim() // 获取`(item, index) in value`中`item, index`  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)    // 将表达式序列化,如`value`序列化为`"value"`,这样就不会参加前面的表达式运算    if (keyAttr === 'key') keyExp = JSON.stringify(keyExp)  }  let match  if (match = valueExp.match(forIteratorRE)) {    valueExp = valueExp.replace(forIteratorRE, '').trim() // 获取`item, index`中的item    indexExp = match[1].trim()  // 获取`item, index`中的index  }  let mounted = false // false示意首次渲染,true示意从新渲染  let blocks: Block[]  let childCtxs: Context[]  let keyToIndexMap: KeyToIndexMap // 用于记录key和索引的关系,当产生从新渲染时则复用元素  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]  }  // 以汇合元素为根底创立独立的作用域  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)    // 为每个子元素创立独立的作用域    const childCtx = createScopedContext(ctx, data)    // key表达式在对应子元素的作用域下运算    const key = keyExp ? evaluate(childCtx.scope, keyExp) : index    map.set(key, index)    childCtx.key = key    return childCtx  }  // 为每个子元素创立块对象  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) // 运算出`(item, index) in items`中items的实在值    const prevKeyToIndexMap = keyToIndexMap    // 生成新的作用域,并计算`key`,`:key`或`v-bind:key`    ;[childCtxs, keyToIndexMap] = createChildContexts(source)    if (!mounted) {      // 为每个子元素创立块对象,解析子元素的子孙元素后插入DOM树      blocks = childCtxs.map(s => mountBlock(s, anchor))      mounted = true    }    // 因为咱们示例只钻研动态视图,因而从新渲染的代码,咱们前面再深刻理解吧  })  return nextNode}

总结

咱们看到在v-ifv-for的解析过程中都会生成块对象,而且是v-if的每个分支都对应一个块对象,而v-for则是每个子元素都对应一个块对象。其实块对象不单单是管控DOM操作的单元,而且它是用于示意树结构不稳固的局部。如节点的减少和删除,将导致树结构的不稳固,把这些不稳固的局部打包成独立的块对象,并封装各自构建和删除时执行资源回收等操作,这样不仅进步代码的可读性也进步程序的运行效率。

v-if的首次渲染和从新渲染采纳同一套逻辑,但v-for在从新渲染时会采纳key复用元素从而提高效率,能够从新渲染时的算法会复制不少。下一篇咱们将深刻理解v-for在从新渲染时的工作原理,敬请期待:)