基于VUE2 v-if、v-show指令源码分析及原生代码简单实现v-if、v-show指令

前言

说起vue的v-if和v-show指令,我们多少有些了解,尤其是当讨论它两的区别时,我们可能会脱口而出它们的操作方式不同,v-if是通过操作元素节点的删除和添加来控制相关模块在视图中的显隐,v-show是通过控制style的display属性控制相关模块在视图中的显隐,v-if会导致重排开销较大,v-show不会导致重排,适合用于值变化频繁的场景。

但是当问起v-if、v-show的实现原理,以及如何自己写一个类似的指令,可能大家就比较陌生了。

因此本文通过分析一下v-if、v-show的源码,简单梳理一下v-if、v-show实现的原理,并基于相应的原理,自己手写一个简单的v-if、v-show。

在vue的源码中,对v-if、v-show的功能操作是基于已被编译后的虚拟dom。所谓的虚拟dom可以简单里理解就是一个json对象,通过对象的形式来表达元素标签,至于为什么采用虚拟dom有兴趣的同学可以先自行去学习一下。

而且对于v-if、v-show来说,其核心的部分,除了如何操作dom节点,或者display之外,还有就是如何响应式的响应操作,这个部分相对较为复杂,但其并不影响我们去理解v-if、v-show操作dom节点和样式,因此本文不做讨论。

源码分析

下面我们就v-if、v-show源码做如下分析:

v-if源码(vue2)

/* @flow */

import {
    
     addIfCondition } from 'compiler/parser/index' // 添加if条件
import {
    
     getAndRemoveAttr, addRawAttr } from 'compiler/helpers' // 获取、删除、添加元素属性attribute

// 判断元素标签是否含有v-if、v-else-if、v-else指令
function hasConditionDirective (el: ASTElement): boolean {
    
    
  for (const attr in el.attrsMap) {
    
    
    if (/^v\-if|v\-else|v\-else\-if$/.test(attr)) {
    
    
      return true
    }
  }
  return false
}

// 获取先前条件
function getPreviousConditions (el: ASTElement): Array<string> {
    
    
  const conditions = []
  if (el.parent && el.parent.children) {
    
    
    for (let c = 0, n = el.parent.children.length; c < n; ++c) {
    
    
      // $flow-disable-line
      const ifConditions = el.parent.children[c].ifConditions
      if (ifConditions) {
    
    
        for (let i = 0, l = ifConditions.length; i < l; ++i) {
    
    
          const condition = ifConditions[i]
          if (condition && condition.exp) {
    
    
            conditions.push(condition.exp)
          }
        }
      }
    }
  }
  return conditions
}

// 预转换v-if
export function preTransformVIf (el: ASTElement, options: WeexCompilerOptions) {
    
    
  // 判断元素标签是否含有v-if、v-else-if、v-else指令
  if (hasConditionDirective(el)) {
    
    
    let exp
    // 从attrsMap对象中删除v-if属性,并返回v-if属性表达式
    const ifExp = getAndRemoveAttr(el, 'v-if', true /* remove from attrsMap */)
    // 从attrsMap对象中删除v-else-if属性,并返回v-else-if属性表达式
    const elseifExp = getAndRemoveAttr(el, 'v-else-if', true)
    // don't need the value, but remove it to avoid being generated as a
    // custom directive(不需要该值,但将其删除以避免作为自定义指令生成)
    // 从attrsMap对象中删除v-else属性,并返回v-else属性表达式
    getAndRemoveAttr(el, 'v-else', true)
    if (ifExp) {
    
    
      // 如果存在v-if属性表达式添加v-if条件
      exp = ifExp
      addIfCondition(el, {
    
     exp: ifExp, block: el })
    } else {
    
    
      // 如果存在v-else-if属性表达式添加v-else-if条件
      elseifExp && addIfCondition(el, {
    
     exp: elseifExp, block: el })
      // 获取先前条件(这里猜测是用于v-if语法判断进行错误提示)
      const prevConditions = getPreviousConditions(el)
      if (prevConditions.length) {
    
    
        const prevMatch = prevConditions.join(' || ')
        exp = elseifExp
          ? `!(${
      
      prevMatch}) && (${
      
      elseifExp})` // v-else-if
          : `!(${
      
      prevMatch})` // v-else
      } else if (process.env.NODE_ENV !== 'production' && options.warn) {
    
    
        options.warn(
          `v-${
      
      elseifExp ? ('else-if="' + elseifExp + '"') : 'else'} ` +
          `used on element <${
      
      el.tag}> without corresponding v-if.`
        )
        return
      }
    }
    // 将处理后的v-if、v-else-if、v-else表达式重新添加到该元素属性中
    addRawAttr(el, '[[match]]', exp)
  }
}

通过上面代码可以了解到v-if的一个大概逻辑就是,先判断元素标签上是否含有v-if类型标签,如果有先把含有v-if的表达式的元素从当前的虚拟dom节点中删除并暂存起来,然后通过js代码if else 函数逻辑,将表达式值为真的元素标签重新添加到要渲染的虚拟dom节点中。

v-show的就相对简单些,重点就是对display的设置,如下:

el.style.display = value ? originalDisplay : 'none'

通过上述代码的分析,相比大家大概理解了v-if和v-show的实现逻辑,以及为什么v-if会导致重排而v-show不会。

原生实现

另外,基于上述原理,本人自己写了一个简单的v-if和v-show指令,不同于vue的虚拟dom,本文是通过js直接操作dom来实现的,其目的也是为了让大家能够有个清洗的认识,其次为了保证整理逻辑不太复杂方便理解,因此本文也没有对v-if、v-else-if、v-else,进行一个很深的逻辑判断,仅仅只是对v-if、v-else-if、v-show功能进行一个简单实现,如图:
在这里插入图片描述

具体代码如下:

<!DOCTYPE html>
<html>

<body>
  <div v-if="1+1=== 2" htmll="123">我要显示</div>
  <div v-if="1+1!==2" htmll="123">我不能显示</div>
  <div v-else-if="1+2===3">
    我也能显示
  </div>
  <div v-else-if="false">
    我也不能显示
  </div>
  <div v-show="true">我是show显示</div>
  <div v-show="false">我是show隐藏</div>

  <script>
    // 获取根元素
    const el = document.querySelector('body')
    // 获取子节点
    const nodeList = el.childNodes

    // 遍历子节点
    for (let i = 0; i < nodeList.length; i++) {
      
      
      const childNode = nodeList[i]

      // 判断子元素是否含有v-if、v-else-if、v-show属性
      const has = hasIf(childNode)
      // 如果存在v-if、v-else-if、v-show属性,则执行响应操作
      if (has) {
      
      
        // 根据v-if、v-else-if、v-show的值操作相应元素
        operateNode(childNode, has)
      }

    }

    // 删除节点
    function operateNode(el, attr) {
      
      
      // 获取v-if、v-else-if、v-show的值
      const attrValue = el.attributes.getNamedItem(attr) ? 		      el.attributes.getNamedItem(attr).value : null

      // 执行v-if、v-else-if、v-show表达式
      const fun = new Function('', 'return (' + attrValue + ')')
      const res = fun()
      // v-if、v-else-if、v-show的值为真时正常显示
      if (res) return
      // 非v-show指令时采用删除子节点的方式进行操作
      attr !== 'v-show' && el.parentNode && el.parentNode.removeChild(el)
      // v-show指令时通过设置display的值控制显隐
      attr === 'v-show' && (el.style.display = 'none')

    }

    // 判断是否含有v-if、v-else-if、v-show属性
    function hasIf(el) {
      
      
      // 子元素没有属性时直接返回
      if (!el.attributes) return
      const e = el.attributes

      for (let i = 0; i < e.length; i++) {
      
      
        // 通过正则匹配v-if、v-else-if、v-show指令
        if (/^v\-if|v\-else\-if|v\-show$/.test(e[i].name)) {
      
      
          return e[i].name
        }
      }
    }


  </script>
</body>

</html>

上述就是本文的内容,如果对你有帮助,请点赞加收藏,如果有什么建议,欢迎留言,或私聊。谢谢!

猜你喜欢

转载自blog.csdn.net/My_Soul_/article/details/127783883
今日推荐