实现目录锚点导航(第二弹)

293 阅读2分钟

之前分享过一篇通过文章内容标题生成目录实现锚点定位的实现 juejin.cn/post/703738…

最近遇到同样展示文章详情并需要展示目录的需要,尝试使用不同的方式来实现,这次利用DocumentFragment来渲染目录,以此分享下

关于 DocumentFragment ,引用自MDN的说法

DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的Document使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染) ,且不会导致性能等问题。
可以使用document.createDocumentFragment方法或者构造函数来创建一个空的DocumentFragment

从MDN的说明中,我们得知DocumentFragments是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。

这个是之前生成文章目录节点的方式

    <div class="aside-body">
      <ul class="aside-article-catalog">
        <li v-for="(item,index) in docMenu" :key="item.id" :class="`level_${item.level}`">
          <a :href="'#' + item.id" :class="{active: active === index }" @click="handlerSroll($event, item.id,index)">{{ item.text }}</a>
        </li>
      </ul>
    </div>
initArt() {
      let markMenu = []
      setTimeout(() => {
        const articleDom = document.querySelector('.article')
        if (articleDom) {
          for (let ele of articleDom.children) {
            const i = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].indexOf(ele.tagName)
            if (i > -1 && ele.textContent) {
              ele.setAttribute('id', 'markMenu_' + markMenu.length)
              ele.setAttribute('name', 'markMenu_' + markMenu.length)
              markMenu.push({
                level: i,
                text: ele.textContent,
                id: 'markMenu_' + markMenu.length,
                name:'markMenu_' + markMenu.length
              })
            }
          }
        }
        //docMenu为目录数据
        this.docMenu = markMenu
      })
    },

使用DocumentFragment的方式来实现同样的效果,利用DocumentFragment将获取每个标题标签创建节点,生成文档片段最后一次性插入到文档中,而这个操作仅发生一个重渲染的操作,它的变化不会引起DOM树的重新渲染的操作(reflow) ,且不会导致性能等问题

   <!-- 目录部分 -->
   <div
     class="aside-body"
     v-html="html"
     @click.prevent="handlerSroll">
   </div>
   
   // 通过
   generateCatalogue() {
     let fragment = document.createDocumentFragment()
     const element = document.querySelector('.article')
     let catalogueRoot = '<div id="side-article-catalog">'
     const childrenNodes = element
       ? this.getTitleNodeList(element).filter((node) => node.innerText)
       : []
     childrenNodes.map((node, index) => {
       const catalogueNode = `<div class="level_${node.tagName}" id="level_${node.tagName}_${index}" title="${node.innerText}"><a href="#${node.id}">${node.innerText}</a></div>`
       catalogueRoot += catalogueNode
     })
     catalogueRoot += '</div>'

     fragment.innerHTML = catalogueRoot
     return fragment.innerHTML
   },
   getTitleNodeList(element) {
     if (element.children) {
       const menuList = element.querySelectorAll('h1,h2,h3,h4,h5,h6')
       for (let i = 0; i < menuList.length; i++) {
         const markMenuClass = Math.random().toString(36).slice(-8)
         menuList[i].setAttribute('id', `markMenu_${markMenuClass}`)
       }
       return Array.from(menuList)
     } else {
       return []
     }
   },

最后一步就是点击跳转的操作,这里利用a标签的href属性达到锚点的效果,也是最关键的一步

handlerSroll(event) {
  const { target } = event
  const anchor = target.getAttribute('href')

  if (!anchor) return
  const anchorElement = document.querySelector(anchor)
  const scrollConfig = {
    behavior: 'smooth',
    block: 'start',
    inline: 'nearest'
   }
  if (target && target.scrollIntoView) {
    anchorElement.scrollIntoView(scrollConfig)
  }
}