Vue源码阅读(18):v-if、v-else、v-else-if 指令的源码解析

本文详细解析了Vue中v-if、v-else和v-else-if指令的底层实现,包括模板字符串如何转化为抽象语法树(AST),AST如何生成代码字符串,并通过代码实例展示了不同条件下的vnode生成过程,深入理解Vue的条件渲染机制。
摘要由CSDN通过智能技术生成

 我的开源库:

前面的 17 篇文章对 Vue 底层的核心运行机制进行了解析,接下来,开始对 Vue 中的众多特性进行针对化的讲解。

首先讲讲大家在工作中经常用到的 v-if、v-else、v-else-if 指令,这些指令是在模板编译阶段实现的。

1,v-if

v-if 的用法如下所示:

<template>
  <div id="app">
    <h1 v-if="isShow">Hello Vue h1</h1>
  </div>
</template>
<script>
  export default {
    name: 'App',
    data(){
      return {
        isShow: true
      }
    },
    methods: {}
  }
</script>

如果 v-if 后面的表达式为 true 的话,h1 标签就会被渲染;

如果 v-if 后面的表达式为 false 的话,h1 标签就不会被渲染;

v-if 的用法大家都很熟悉了,接下来开始研究 v-if 的底层实现原理。

1-1,模板字符串 --> 抽象语法树

首先看看上面例子中的模板字符串生成的抽象语法树是什么样子的。

{
  attrs: [{name: "id", value: "\"app\""}],
  attrsList: [{name: "id", value: "app"}],
  attrsMap: {id: "app"},
  children: [{
    attrsList: [],
    attrsMap: {v-if: "isShow"},
    children: [{type: 3, text: "Hello Vue h1", static: true}],
    if: "isShow",
    ifConditions: [{
      exp: "isShow",
      block: {type: 1, tag: "h1", attrsList: Array(0), attrsMap: {…}, parent: {…}, …},
    }],
    ifProcessed: undefined,
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, parent: undefined, …},
    plain: true,
    static: false,
    staticRoot: false,
    tag: "h1",
    type: 1
  }],
  parent: undefined,
  plain: false,
  static: false,
  staticRoot: false,
  tag: "div",
  type: 1
}

上面生成的抽象语法树中,与 v-if 特性有关联的属性有三个,分别是:if、ifConditions、ifProcessed,他们的作用如下所示:

  • if:用于标识当前的 AST 节点有没有使用 v-if 特性。
  • ifProcessed:用于标识当前 AST 节点的 v-if 特性有没有被处理,防止出现重复处理 v-if 特性的情况。
  • ifConditions:该属性是一个对象,里面有两个属性,分别是:exp 和 block,exp 是 v-if 指令后面的表达式,block 是当 exp 表达式为 true 时,应该生成对应代码字符串的 AST 节点。

这三个属性将用于 v-if 相关代码字符串的生成。

1-2,抽象语法树 --> 代码字符串

抽象语法树生成代码字符串是编译器中的代码生成器的内容,与 v-if 相关的代码生成器源码如下所示。

function genElement (el, state) {
  // 首次执行到这里,!el.ifProcessed 为 true
  // 下次执行到这里,!el.ifProcessed 为 false,因为在 genIf 函数中,el.ifProcessed 被设为了 true
  if (el.if && !el.ifProcessed) {
    // 用于处理 v-if
    return genIf(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      // 代码会执行到这个分支,生成 _c('h1',[_v(\"Hello Vue h1\")]) 代码字符串
      const data = el.plain ? undefined : genData(el, state)

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    return code
  }
}

function genIf (
  el,
  state,
  altGen,
  altEmpty
) {
  // ifProcessed 属性用于判断 el 的 v-if 有没有被处理过,避免重复处理。
  el.ifProcessed = true; // avoid recursion
  // 调用 genIfConditions 进行代码字符串的生成
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions (
  conditions,
  state,
  altGen,
  altEmpty
) {
  // 在当前的例子中,conditions 的值是 
  //  [{
  //    exp: "isShow",
  //    block: {type: 1, tag: "h1", attrsList: Array(0), attrsMap: {…}, parent: {…}, …},
  //  }]
  // 数组中只有一个对象

  // 如果 conditions 数组为空的话,则返回 _e()
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  // 从数组中的头部取出数据,在当前的例子中,这一次取出数据之后,conditions 数组就变成了空数组了
  var condition = conditions.shift();
  // 如果 condition 对象中有 exp 属性的话,则进入此逻辑。
  // 注意:v-else 对应的 condition 对象没有 exp 属性
  if (condition.exp) {
    // 拼接并返回代码字符串:(isShow)?_c('h1',[_v(\"Hello Vue h1\")]):_e()
    // _c('h1',[_v(\"Hello Vue h1\")]) 代码字符串由 genTernaryExp 函数生成。
    // _e() 代码字符串会再次调用 genIfConditions 函数生成,因为在此例子中,conditions 已经变成了空数组,所以这次调用的 genIfConditions 函数会返回 _e(),对应的逻辑看上面的代码。
    // 如果 v-if 和 v-else-if 搭配使用的话,在这里再次调用 genIfConditions 函数的时候 conditions 数组就不是空数组,最终生成的代码字符串是 嵌套的三元表达式 结构
    return ("(" + (condition.exp) + ")?" + (genTernaryExp(condition.block)) + ":" + (genIfConditions(conditions, state, altGen, altEmpty)))
  } else {
    // 该分支逻辑用于生成 v-else AST 节点的代码字符串
    return ("" + (genTernaryExp(condition.block)))
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  // 调用 genTernaryExp 函数生成 _c('h1',[_v(\"Hello Vue h1\")]) 代码字符串,其内部调用 genElement(el, state) 生成该代码字符串
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}

 源码的详细解释都在注释中,大家看注释即可理解,生成的代码字符串如下所示:

with(this){
  return _c(
    'div',
    {attrs:{\"id\":\"app\"}},
    [(isShow)?
        _c('h1',[_v(\"Hello Vue h1\")])
        :_e()
    ]
  )
}

可以发现,v-if 特性生成的代码字符串是一个三元表达式,如果 exp 也就是 isShow 变量为 true 的话,则 render 函数生成的 vnode 将会包含 h1 vnode 节点。如果 isShow 变量为 false 的话,render 函数生成的 vnode 将不会包含 h1 vnode 节点,而是一个注释 vnode 节点作为占位。

1-3,isShow 不同状态下,render 函数生成的 vnode

如果 isShow 为 true 的话,生成的 vnode 节点如下图所示:

app div vnode 对象的 children 属性内有一个 h1 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h1 元素。

如果 isShow 为 false 的话,生成的 vnode 节点如下图所示:

可以发现,如果 isShow 的值为 false 生成的 vnode,children 属性中的 vnode 是一个占位用的注释 vnode,这种节点在页面上显示任何内容。

2,v-else

如果上面的 v-if 理解了的话,v-else 也就很简单了,我们以下面的例子开始讨论。

<template>
  <div id="app">
    <h1 v-if="isShow">Hello Vue h1</h1>
    <h2 v-else>Hello Vue h2</h2>
  </div>
</template>
<script>
  export default {
    name: 'App',
    data(){
      return {
        isShow: false
      }
    },
    methods: {}
  }
</script>

 如果 isShow 为 true 的话,页面渲染 h1 节点,如果 isShow 为 false 的话,页面渲染 h2 节点。

2-1,模板字符串 --> 抽象语法树

生成的抽象语法树如下所示:

与上面生成的抽象语法树唯一的不同点是 ifConditions 数组中有两个对象,第一个 condition 对象对应 v-if="isShow",第二个 condition 对象对应 v-else。

{
  attrs: [{name: "id", value: "\"app\""}],
  attrsList: [{name: "id", value: "app"}],
  attrsMap: {id: "app"},
  children: [{
    attrsList: [],
    attrsMap: {v-if: "isShow"},
    children: [{type: 3, text: "Hello Vue h1", static: true}],
    if: "isShow",
    ifConditions: [{
      exp: "isShow",
      block: {type: 1, tag: "h1", attrsList: Array(0), attrsMap: {…}, parent: {…}, …},
    },{
      exp: undefined,
      block: {type: 1, tag: "h2", attrsList: Array(0), attrsMap: {…}, parent: {…}, …}
    }],
    ifProcessed: undefined,
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, parent: undefined, …},
    plain: true,
    static: false,
    staticRoot: false,
    tag: "h1",
    type: 1
  }],
  parent: undefined,
  plain: false,
  static: false,
  staticRoot: false,
  tag: "div",
  type: 1
}

2-2,抽象语法树 --> 代码字符串

生成的代码字符串如下所示:

with(this){
  return _c(
    'div',
    {attrs:{\"id\":\"app\"}},
    [(isShow)?
        _c('h1',[_v(\"Hello Vue h1\")]):
        _c('h2',[_v(\"Hello Vue h2\")])
    ]
  )
}

 可以看到 v-if 和 v-else 的组合生成的代码字符串是一个三元表达式,如果 isShow 为 true 的话,生成 h1 的 vnode,如果 isShow 为 false 的话,生成 h2 的 vnode。

2-3,isShow 不同状态下,render 函数生成的 vnode

如果 isShow 为 true 的话,生成的 vnode 节点如下图所示:

app div vnode 对象的 children 属性内有一个 h1 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h1 元素。

如果 isShow 为 false 的话,生成的 vnode 节点如下图所示:

app div vnode 对象的 children 属性内有一个 h2 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h2 元素。

3,v-else-if

v-else-if 可以搭配 v-if 使用,其底层的本质是生成多层嵌套的三元表达式,我们直接看例子。

<template>
  <div id="app">
    <h1 v-if="score >= 80">优秀</h1>
    <h2 v-else-if="score >= 60">及格</h2>
    <h3 v-else>不及格</h3>
  </div>
</template>
<script>
  export default {
    name: 'App',
    data(){
      return {
        score: 60
      }
    },
    methods: {}
  }
</script>

如果 score >= 80 的话,页面渲染 h1 标签;

如果 80 > score >= 60 的话,页面渲染 h2 标签;

如果 score < 60 的话,页面渲染 h3 标签; 

3-1,模板字符串 --> 抽象语法树

生成的抽象语法树如下所示:

该例子生成的抽象语法树的特点是 ifConditions 数组中有三个对象。第一个 condition 对象对应 v-if="score >= 80",第二个 condition 对象对应 v-else-if="score >= 60",第三个 condition 对象对应 v-else。

{
  attrs: [{name: "id", value: "\"app\""}],
  attrsList: [{name: "id", value: "app"}],
  attrsMap: {id: "app"},
  children: [{
    attrsList: [],
    attrsMap: {v-if: "isShow"},
    children: [{type: 3, text: "Hello Vue h1", static: true}],
    if: "isShow",
    ifConditions: [{
      exp: "score >= 80",
      block: {type: 1, tag: "h1", attrsList: Array(0), attrsMap: {…}, parent: {…}, …}
    },{
      exp: "score >= 60",
      block: {type: 1, tag: "h2", attrsList: Array(0), attrsMap: {…}, parent: {…}, …}
    },{
      exp: undefined,
      block: {type: 1, tag: "h3", attrsList: Array(0), attrsMap: {…}, parent: {…}, …}
    }],
    ifProcessed: undefined,
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, parent: undefined, …},
    plain: true,
    static: false,
    staticRoot: false,
    tag: "h1",
    type: 1
  }],
  parent: undefined,
  plain: false,
  static: false,
  staticRoot: false,
  tag: "div",
  type: 1
}

3-2,抽象语法树 --> 代码字符串

生成的代码字符串如下所示:

with(this){
  return _c(
    'div',
    {attrs:{\"id\":\"app\"}},
    [(score >= 80)?
        _c('h1',[_v(\"优秀\")]):
        (score >= 60)?
          _c('h2',[_v(\"及格\")]):
          _c('h3',[_v(\"不及格\")])])
}

 v-if 和 v-else-if 搭配生成的代码字符串是嵌套的三元表达式的结构。如果 score >= 80 的话,生成 h1 的 vnode,如果 80 > score >= 60 的话,生成 h2 的 vnode,如果 score < 60 的话,生成 h3 的 vnode。

3-3,score 不同数值,render 函数生成的 vnode

如果 score >= 80 的话,生成的 vnode 节点如下图所示:

app div vnode 对象的 children 属性内有一个 h1 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h1 元素。

 如果 80 > score >= 60 的话,生成的 vnode 节点如下图所示:

app div vnode 对象的 children 属性内有一个 h2 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h2 元素。

 如果 score < 60 的话,生成的 vnode 节点如下图所示:

app div vnode 对象的 children 属性内有一个 h3 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h3 元素。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
v-model 是 Vue.js 中常用的一个指令,它可以在表单元素上创建双向数据绑定。在 Vue.js 中,指令是一种特殊的属性,用于添加特定的行为。v-model 指令实际上是一个语法糖,它将表单元素的 value 属性绑定到 Vue 实例中的数据对象的一个属性上,并且在用户输入时自动更新这个属性的值。 下面是一个简单的示例: ```html <input type="text" v-model="message"> ``` 上述代码中,v-model 指令将输入框的值与 Vue 实例中的 message 属性进行双向绑定。当用户在输入框中输入内容时,message 属性的值会自动更新,反之亦然。 接下来,我们来深入分析 v-model 的源码实现。 首先,我们需要了解 v-model 在编译阶段是如何被解析的。在 Vue.js 中,编译阶段会将模板代码解析为抽象语法树(AST)。当编译器遇到 v-model 指令时,它会将它解析为一个对象: ```javascript { name: 'model', rawName: 'v-model', value: 'message', expression: '"message"', arg: null, modifiers: {} } ``` 在这个对象中,name 属性表示指令的类型,value 属性表示指令绑定的值,expression 属性表示值的表达式,arg 属性表示指令的参数,modifiers 属性表示指令的修饰符。 接下来,编译器会根据指令的类型和绑定的值生成对应的代码,这些代码会在运行时执行,从而实现指令的功能。对于 v-model 指令,编译器会生成以下代码: ```javascript { key: "value", expression: `message`, arg: null, mode: 'twoWay', directive: 'model', modifiers: {} } ``` 这段代码中,key 属性表示绑定的属性名,expression 属性表示绑定的属性值,mode 属性表示数据绑定的模式,directive 属性表示指令的类型,modifiers 属性表示指令的修饰符。 在运行时,Vue.js 会在组件渲染过程中对这些指令进行解析和处理。对于 v-model 指令Vue.js 会根据指令的模式(单向绑定或双向绑定)生成对应的数据绑定代码。对于单向绑定,Vue.js 会在组件初始化时将属性值赋给表单元素的 value 属性。对于双向绑定,Vue.js 会在表单元素的 input 和 change 事件中更新属性值。 下面是一段简化版的 v-model 指令处理代码: ```javascript function bindModel(el, binding, vnode) { const value = binding.value const mode = binding.mode || 'oneWay' const key = binding.key || 'value' if (mode === 'oneWay') { el.value = value } else if (mode === 'twoWay') { el.value = value el.addEventListener('input', () => { vnode.context[key] = el.value }) } } Vue.directive('model', { bind: bindModel, update: bindModel }) ``` 在这段代码中,bindModel 函数用于处理 v-model 指令。它首先获取指令的 value、mode 和 key 属性,然后根据 mode 属性判断数据绑定的模式。如果是单向绑定,直接将属性值赋给表单元素的 value 属性;如果是双向绑定,同时绑定 input 事件,当用户输入时更新属性值。最后,Vue.js 会将这个指令注册到指令系统中,以便在组件渲染时自动调用。 总的来说,v-model 指令的实现原理其实很简单,就是通过数据绑定来实现表单元素和数据对象之间的双向绑定。但是,在实际开发中,还有很多细节需要注意,比如不同表单元素的处理方式、不同数据类型的转换等等。因此,如果想要深入理解 v-model 指令的实现原理,需要对 Vue.js 的组件渲染机制有比较深入的了解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值