使用 Fragment ,实现 Vue 的模板编译

32 篇文章 0 订阅
16 篇文章 0 订阅

Vue 模板编译:

Vue 是一个 MVVM 的框架,MVVM 就是 Model + View + ViewModel 即数据驱动视图的。

在使用 Vue 开发过程中,我们把写在 template 标签中的内容称之为模板。除去一些html原生的内容还有 solt、v-if、v-on、{{}} 这些原生html不存在的语法,但是浏览器仍然可以识别,其中最重要的一个原因就是 Vue 的模板编译了。Vue 会把用户在 template 标签中写的内容进行编译,找出原生的 html 和非原生的 html 内容,经过处理生成 Fragment ,而 Fragment 会经过patch过程从而得到将渲染的视图中的 Fragment ,最后根据 Fragment 创建真实的 DOM 节点并插入到视图中,最终完成视图的渲染更新。

Vue 模板编译的实现逻辑

  1. 需要获取到创建 Vue 实例传入的根元素
  2. 获取根元素,并使用 Fragment 创建一个文档片段
  3. 将根元素的所有子元素添加到 Fragment 中
  4. 循环这个 fragment 判断元素的类型,同时判断元素是否有子元素,如果有子元素,递归进行处理
  5. 如果是元素节点的话,调用对应的编译方法
    1. 获取元素节点身上的所有自定义属性
    2. 判断这些属性是否有 v- 开头的
    3. 通过字符串切割的方法获取到具体的指令,执行对应的操作
  6. 如果是文本节点的话,调用对应的编译方法
    1. 通过正则匹配元素内字符串是否是插值表达式(也就是{{数据}})
    2. 根据插值表达式,到 Vue 实例的 data 属性中去查找这个属性,并将文本内容替换为这个属性

Vue 模板编译的具体实现

DOM 结构

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>compile</title>
</head>

<body>
  <div id="app">
    <div>
      <p>AABB</p>
    </div>
    <div>
      <span>{{list}}</span>
      <p>{{name}}</p>
      <p v-html="age">我使用了v-html的方法访问了age</p>
    </div>
    <input type="text" v-model="a.b.c.d">
  </div>
  <script src="./iCompile.js"></script>

  <script>
    let vue = new iCompile({
      el: '#app',
      data: {
        a: {
          b: {
            c: {
              d: 'abcd'
            }
          }
        },
        name: 'name',
        age: '35',
        obj: {
          name: 1111
        },
        list: [1, 2, 3, 3, 4]
      }
    })
  </script>
</body>

</html>

Compile 实现逻辑代码

class iCompile {
  constructor(vue) {
    this.$vue = vue // 获取Vue的实例
    this.$el = document.querySelectorAll(vue.el)[0] // 获取根元素
    // 当未查询到/未传入根元素时不执行编译操作
    if (this.$el) {
      let fragment = this.setFragment()
      this.compileDom(fragment)
      document.body.append(fragment)
    }

  }
  /**
   * 1. 创建一个 fragment 文档片段
   * 2. 使用 while 循环将根元素的内容添加到 fragment 中
   * 3. 返回这个 fragment 
   */
  setFragment() {
    let fragment = document.createDocumentFragment()
    let nodes = this.$el.childNodes
    let currentNode
    while (currentNode = nodes[0]) {
      fragment.append(currentNode)
    }
    return fragment
  }
  /**
   * 1. 获取 fragment 的所有子元素
   * 2. 循环子元素判断元素类型, 如果子元素中有子集的话, 调用递归的操作对每一层处理
   * 
   */
  compileDom(el) {
    let childNodes = el.childNodes // 获取子元素
    let reg = /\{\{(.+)\}\}/ // 查找插值表达式
    childNodes.forEach(node => {
      let textContent = node.textContent // 获取元素内容
      let nodeType = node.nodeType // 元素元素类型
      // 元素节点
      if (nodeType == 1) {
        this.compileEl(node)
        // 文本节点, 当元素类型为 3 , 同时使用正则检测到文本节点的内容中有插值表达式,就对这个进行处理
      } else if (nodeType == 3 && reg.test(textContent)) {
        // 因为正则方法 match 方法返回的是一个数组, 他的第一项是原数据内容, 我们不需要这个内容, 所有要把这个切割掉
        let express = textContent.match(reg).slice(1)
        console.log('express: ', express);
        this.compileText(node, express)
      }
      // 如果子集有子元素的话,传入当前子元素让它递归调用
      if (node.childNodes.length) {
        this.compileDom(node)
      }
    })
  }
  /**
   * 根据元素的自定义属性去对元素执行相应的操作
   * 1. 获取元素的自定义属性
   * 2. 
   * 
   */
  compileEl(node) {
    let attributesList = node.attributes; // 获取元素自定义属性
    [...attributesList].forEach(attrs => {
      const name = attrs.name; // 获取属性名
      let attrValue = attrs.value; // 获取属性值
      const provideName = name.substr(0, 2) // 保存属性名前  2  位字符, 用于判断是否是 Vue 的内置指令
      const attrName = name.substr(2) // 保存属性名前  6  位字符, 用于判断这个是 Vue 的哪个指令

      if (provideName == 'v-') {
        // 通过 switch 匹配自定义属性的属性值, 执行不同的操作
        switch (attrName) {
          // 如果是 v-html 的话, 实际跟文本节点的处理方式类似, 调用 Vue 实例上的属性值, 用来替换元素本身的内容
          case "html":
            node.textContent = getVueData(attrValue, this.$vue.data)
            break

          case "model":
            // Vue 的双向绑定, 当元素内容变化时, 需要同步将 data 中的数据更新
            node.addEventListener('input', (e) => {
              let newValue = e.target.value; // 获取当前输入的最新值
               // 根据 v-model 上面绑定的属性值, 将 Vue 中实例对应的数据源变成最新的
              setVueData(attrValue, this.$vue.data, newValue)
              node.value = newValue
              console.log(`修改后的内容为-${getVueData(attrValue, this.$vue.data)}`);
            })
            // 当元素初次加载的时候需要,调用 Vue 实例上的属性值, 用来替换元素本身的内容
            node.value = getVueData(attrValue, this.$vue.data)
            break

          default:

            break
        }

      }
    })

  }
  /**
   * 根据元素内容和插值表达式的值,从 Vue 的实例中查询对应的属性
   * 并替换元素内容本身的内容
   */
  compileText(node, exp) {
    let textContent = node.textContent // 获取元素内容
    exp.forEach(key => {
      textContent = textContent.replace(`{{${key.trim()}}}`, getVueData(key, this.$vue.data))
    })
    node.textContent = textContent
  }
}
/**
 * 这里只考虑了, 在 Vue 中使用 obj.a 的方式
 * 根据字符串column,将字符串切割成逐级的 key 
 * 根据 key 的列表逐级访问, 找到并返回对应的内容
 */
function getVueData(column, $data) {
  let columnList = column.split('.'); // 切割字符串
  let result = $data // 获取Vue 的实例
  columnList.forEach((key) => {
    result = result[key]
  })
  return result
}
/**
 * 这里只考虑了, 在 Vue 中使用 obj.a 的方式
 * 根据字符串column,将字符串切割成逐级的 key 
 * 根据 key 的列表逐级访问, 找到并返回对应的属性
 * 将最新的属性值赋值给 这个属性
 */
function setVueData(column, $data, value) {
  let columnList = column.split('.')
  const len = columnList.length - 1 // 这里保存 len 的作用是判断当前循环是否是最后一项
  let result = $data
  columnList.forEach((key, index) => {
    if (len == index) {
      result[key] = value
    } else {
      result = result[key]
    }
  })
  return $data
}

实现效果编译前 ( 后 )

编译前

在这里插入图片描述

编译后

在这里插入图片描述

从这两张图的对比上可以明显看到我们已经实现了使用 iCompile 方法根据 Vue 实例中 data 属性的值,去处理元素内容中插值表达式和指令去动态渲染元素的内容。

问题解答

Q: Fragment 是什么,主要解决了什么问题 ?
A: Fragment 是原生 JS 的一种创建文档片段的方法,如果我们对根元素中的每个元素做逐个处理的话,这会引起很大的性能问题,会多次触发浏览器的回流和重绘
当我们使用 fragment 相当于是在 JS 内存中创建了一个只存在于内存中的 DOM ,对 Fragment 中的元素内容做修改的话是不会引起 DOM 层面的变更的,对 fragment 中的元素都处理完成后再将这个 fragment 放到根元素中,这仅仅会引起一次的回流和重绘。

Q: 在 compileEl 方法中为什么要使用拓展运算符将元素的自定义属性展开?
A: 因为我们通过 DOM 元素获取到的自定义属性列表是一个伪数组,并不是一个真正的数组,它无法调用数组的一些方法,所以我们需要把它转换为真正的数组,再去逐个处理自定义属性。

Q: 在 setFragment 方法的 while 循环中,为什么要每次获取第一个元素?
A: while 也是 JS 的一种循环机制,他的终止条件是当()中的条件不满足时就会终止。
我们使用 currentNode 每次循环都会去获取一个元素,保存到 fragment 中。这个获取并不是引用,而是直接把子集拿过来,当根源素的子节点为空时,while 循环的条件也就不满足了,就会终止循环。

如果有其他问题,敬请评论区留言

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值