javascript - VueJS/JS DOM Watch/Observer 多阶段渲染场景

标签 javascript vue.js

场景:

我正在开发一个 Vue 滚动组件,它包含动态数量的 HTML 部分,然后动态构建垂直页面导航,允许用户滚动或跳转到 onScroll 上的页面位置。

详情:

一个。 在我的示例中,我的滚动组件包含 3 个部分。所有部分 id 都以 "js-page-section-{{index}}" 开头

湾。 目标是获取部分节点列表(上图),然后根据在查询匹配选择器条件中找到的 n 个节点动态构建垂直页面 (nav) 导航。因此,三个部分将导致三个页面部分导航项。所有侧边导航都以 “js-side-nav-{{index}}>". 开头

C。 呈现侧面导航后,我需要查询所有导航节点以控制类、高度、显示、不透明度等。即 document.querySelectorAll('*[id^="js-side-nav"]');
编辑

根据一些研究,这里是我的问题的选项。我的问题再次是 3 阶段 DOM 状态管理,即第 1 步。读取所有等于 x 的节点,然后第 2 步。根据文档中的 n 个节点构建侧导航滚动,然后第 3 步。读取所有导航节点以与滚动同步文档节点数:

  • 创建某种事件系统是$emit() && $on .在我看来,这很快就会变得一团糟,感觉就像一个糟糕的解决方案。我发现自己很快跳到 $root
  • Vuex .但这感觉有点矫枉过正
  • sync .有效,但实际上是用于父子属性状态管理,但这又需要 $emit() && $on .
  • Promise .基于服务类。这似乎是正确的解决方案,但坦率地说,管理多个 promise 变得有点痛苦。
  • 我尝试使用 Vue $ref,但坦率地说,它似乎更适合管理状态,而不是多阶段 DOM 操作,其中观察者事件方法更好。
  • 似乎可行的解决方案是 Vues $nextTick() .这似乎类似于 AngularJS $digest .本质上它是一个 . setTimeout() .类型方法只是暂停下一个摘要周期。也就是说,存在滴答声不同步所需时间的情况,因此我构建了一个节流方法。以下是有值(value)的代码更新。

  • 使用 nextTick() 重构的 watch
            watch: {
                'page.sections':  {
                    handler(nodeList, oldNodeList){
                        if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
                            return this.$nextTick(this.sideNavInit);
                        }
                    },
                    deep: true
                },
            },
    

    重构的 Vue 组件
    <template>
        <div v-scroll="handleScroll">
            <nav class="nav__wrapper" id="navbar-example">
                <ul class="nav">
                    <li role="presentation"
                        :id="sideNavPrefix + '-' + (index + 1)"
                        v-for="(item, key,index) in page.sections">
                        <a :href="'#' + getAttribute(item,'id')">
                        <p class="nav__counter" v-text="('0' + (index + 1))"></p>
                            <h3 class="nav__title" v-text="getAttribute(item,'data-title')"></h3>
                            <p class="nav__body" v-text="getAttribute(item,'data-body')"></p>
                        </a>
                    </li>
                </ul>
            </nav>
            <slot></slot>
        </div>
    </template>
    
    <script>
        import ScrollPageService from '../services/ScrollPageService.js';
    
        const _S = "section", _N = "sidenavs";
    
        export default {
            name: "ScrollSection",
            props: {
                nodeId: {
                    type: String,
                    required: true
                },
                sideNavActive: {
                    type: Boolean,
                    default: true,
                    required: false
                },
                sideNavPrefix: {
                    type: String,
                    default: "js-side-nav",
                    required: false
                },
                sideNavClass: {
                    type: String,
                    default: "active",
                    required: false
                },
                sectionClass: {
                    type: String,
                    default: "inview",
                    required: false
                }
            },
            directives: {
                scroll: {
                    inserted: function (el, binding, vnode) {
                        let f = function(evt) {
                            if (binding.value(evt, el)) {
                                window.removeEventListener('scroll', f);
                            }
                        };
                        window.addEventListener('scroll', f);
                    }
                },
            },
            data: function () {
                return {
                    scrollService: {},
                    page: {
                        sections: {},
                        sidenavs: {}
                    }
                }
            },
            methods: {
                getAttribute: function(element, key) {
                    return element.getAttribute(key);
                },
                updateViewPort: function() {
                    if (this.scrollService.isInCurrent(window.scrollY)) return;
    
                    [this.page.sections, this.page.sidenavs] = this.scrollService.updateNodeList(window.scrollY);
    
                },
                handleScroll: function(evt, el) {
                    if ( !(this.isScrollInstance()) ) {
                        return this.$nextTick(this.inViewportInit);
                    }
    
                    this.updateViewPort();
                },
                getNodeList: function(key) {
                    this.page[key] = this.scrollService.getNodeList(key);
                },
                isScrollInstance: function() {
                    return this.scrollService instanceof ScrollPageService;
                },
                sideNavInit: function() {
                    if (this.isScrollInstance() && this.scrollService.navInit(this.sideNavPrefix, this.sideNavClass)) this.getNodeList(_N);
                },
                inViewportInit: function() {
                    if (!(this.isScrollInstance()) && ((this.scrollService = new ScrollPageService(this.nodeId, this.sectionClass)) instanceof ScrollPageService)) this.getNodeList(_S);
                },
                isNodeList: function(nodes) {
                    return NodeList.prototype.isPrototypeOf(nodes);
                },
            },
            watch: {
                'page.sections':  {
                    handler(nodeList, oldNodeList){
                        if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
                            return this.$nextTick(this.sideNavInit);
                        }
                    },
                    deep: true
                },
            },
            mounted() {
                return this.$nextTick(this.inViewportInit);
            },
        }
    
    </script>
    
    

    结束编辑

    原帖

    问题与疑问:

    问题:

    部分的查询和导航的渲染工作正常。但是,查询 nav 元素会失败,因为 DOM 尚未完成渲染。因此,我不得不使用 setTimeout()功能。即使我使用 watch ,我仍然被迫使用超时。

    问题:

    Vue 或 JS 中是否有一个 promise 或观察者可以用来检查 DOM 何时完成渲染导航元素,以便我可以阅读它们?在 AngularJS 中我们可以使用 $observe 的例子

    HTML 示例
        <html>
            <head></head>
            <body>
                <scroll-section>
                    <div id="js-page-section-1"
                         data-title="One"
                         data-body="One Body">
                    </div>
                    <div id="js-page-section-2"
                         data-title="Two"
                         data-body="Two Body">
                    </div>
                    <div id="js-page-section-3"
                         data-title="Three"
                         data-body="THree Body">
                    </div>
                </scroll-section>
            </body>
        </html>
    

    Vue 组件
    <template>
        <div v-scroll="handleScroll">
            <nav class="nav__wrapper" id="navbar-example">
                <ul class="nav">
                    <li role="presentation"
                        :id="[idOfSideNav(key)]"
                        v-for="(item, key,index) in page.sections.items">
                            <a :href="getId(item)">
                            <p class="nav__counter">{{key}}</p>
                                <h3 class="nav__title" v-text="item.getAttribute('data-title')"></h3>
                                <p class="nav__body" v-text="item.getAttribute('data-body')"></p>
                            </a>
                    </li>
                </ul>
            </nav>
    
            <slot></slot>
    
        </div>
    </template>
    
    <script>
        export default {
            name: "ScrollSection",
    
            directives: {
                scroll: {
                    inserted: function (el, binding, vnode) {
                        let f = function(evt) {
                            _.forEach(vnode.context.page.sections.items, function (elem,k) {
                                if (window.scrollY >= elem.offsetTop && window.scrollY <= (elem.offsetTop + elem.offsetHeight)) {
                                    if (!vnode.context.page.sections.items[k].classList.contains("in-viewport") ) {
                                        vnode.context.page.sections.items[k].classList.add("in-viewport");
                                    }
                                    if (!vnode.context.page.sidenavs.items[k].classList.contains("active") ) {
                                        vnode.context.page.sidenavs.items[k].classList.add("active");
                                    }
                                } else {
                                    if (elem.classList.contains("in-viewport") ) {
                                        elem.classList.remove("in-viewport");
                                    }
                                    vnode.context.page.sidenavs.items[k].classList.remove("active");
                                }
                            });
    
                            if (binding.value(evt, el)) {
                                window.removeEventListener('scroll', f);
                            }
                        };
    
                        window.addEventListener('scroll', f);
                    },
                },
    
            },
            data: function () {
                return {
                    page: {
                        sections: {},
                        sidenavs: {}
                    }
                }
            },
            methods: {
                handleScroll: function(evt, el) {
                    // Remove for brevity
                },
                idOfSideNav: function(key) {
                    return "js-side-nav-" + (key+1);
                },
                classOfSideNav: function(key) {
                    if (key==="0") {return "active"}
                },
                elementsOfSideNav:function() {
                    this.page.sidenavs = document.querySelectorAll('*[id^="js-side-nav"]');
                },
                elementsOfSections:function() {
                    this.page.sections = document.querySelectorAll('*[id^="page-section"]');
                },
    
            },
            watch: {
                'page.sections': function (val) {
                    if (_.has(val,'items') && _.size(val.items)) {
                        var self = this;
                        setTimeout(function(){
                            self.elementsOfSideNavs();
                        }, 300);
                    }
                }
            },
            mounted() {
                this.elementsOfSections();
            },
    
        }
    
    
    </script>
    

    最佳答案

    我希望我能帮助你解决我要在这里发布的内容。我的一个 friend 开发了一个我们在几个地方使用的功能,阅读你的问题让我想起了它。

    "Is there a promise or observer in Vue or JS I can use to check to see when the DOM has finished rendering the nav elements so that I can then read them?"


    我想到了这个功能( source ),在下面。它接受一个函数(观察)并尝试多次满足它。
    我相信您可以在组件创建或页面初始化的某个时候使用它;我承认我不太了解您的情况。但是,您的一些问题立即让我想到了这个功能。 “......等待某事发生,然后让其他事情发生。”
    <> @Markkop该片段/函数的创建者 =)
    /**
     * Waits for object existence using a function to retrieve its value.
     *
     * @param { function() : T } getValueFunction
     * @param { number } [maxTries=10] - Number of tries before the error catch.
     * @param { number } [timeInterval=200] - Time interval between the requests in milis.
     * @returns { Promise.<T> } Promise of the checked value.
     */
    export function waitForExistence(getValueFunction, maxTries = 10, timeInterval = 200) {
      return new Promise((resolve, reject) => {
        let tries = 0
        const interval = setInterval(() => {
          tries += 1
          const value = getValueFunction()
          if (value) {
            clearInterval(interval)
            return resolve(value)
          }
    
          if (tries >= maxTries) {
            clearInterval(interval)
            return reject(new Error(`Could not find any value using ${tries} tentatives`))
          }
        }, timeInterval)
      })
    }
    
    例子
    function getPotatoElement () {
      return window.document.querySelector('#potato-scroller')
    }
    
    function hasPotatoElement () {
      return Boolean(getPotatoElement())
    }
    
    // when something load
    window.document.addEventListener('load', async () => {
      // we try sometimes to check if our element exists
      const has = await waitForExistence(hasPotatoElement)
      if (has) {
        // and if it exists, we do this
        doThingThatNeedPotato()
      }
    
      // or you could use a promise chain
      waitForExistence(hasPotatoElement)
        .then(returnFromWaitedFunction => { /* hasPotatoElement */
           if (has) {
             doThingThatNeedPotato(getPotatoElement())
           }
        }) 
    })
    
    

    关于javascript - VueJS/JS DOM Watch/Observer 多阶段渲染场景,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54282942/

    相关文章:

    javascript - window.setInterval 是否有最大延迟限制

    npm - Vue 模块,moment-timezone - 如何正确加载 moment-timezone 以及如何使用 2012-2022 数据

    laravel - vuex 存储在 PAGE REFRESH 上丢失数据

    javascript - vue.js - 响应数据错误未定义

    typescript - 为什么 vue 在第一次编译时无法识别 TS 语法,但在第二次编译时可以正常工作?

    javascript - 每当我调用保存方法时调用保存方法

    javascript - Node.js + Socket.io : Client not receiving message

    javascript - KnockoutJS 绑定(bind)选择

    javascript - 如何向 JSON 对象添加值?

    javascript - Vue.js PWA 插件部署到子文件夹时不加载 service-worker.js (Github Pages)