天天用v-show和v-if还不知道怎么实现?今天就让你极速玩转

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

介绍

相信无数的开发者天天都在使用vue,其中v-show和v-if绑定语法几乎每个页面都会多少用到,但是考虑过他们是如何实现的吗?本期的案例就是制作一个miniVue只讲解v-show与v-if的实现,因为有点击所以我们把click事件顺便也简单实现下。

VID_20211011_090151.gif

如图所示,我们做的找个案例是要,点击clickBox后,ifBoX与showBox会交替显隐,我们这就开始咯~

正文

1.基本结构

<div id="app">
    <div class="box-wrapper">
        <div class="box style-click" @click="changeBox">Click Box</div>
        <div class="box style-if" m-if="ifBox">IF Box</div>
        <div class="box style-show" m-show="showBox" style="display: flex;">Show Box</div>
    </div>
</div>

<script type="module">
    import MS from "./js/MS";
    let ms = new MS({
        el: "#app",
        data() {
            return {
                showBox: true,
                ifBox: false,
            };
        },
        methods: {
            changeBox() {
                this.showBox = !this.showBox;
                this.ifBox = !this.ifBox;
            },
        },
        watch: {
          showBox(newValue, oldValue) {
            console.log(`showBox:${oldValue}->${newValue}`);
          },
          ifBox(newValue, oldValue) {
            console.log(`ifBox:${oldValue}->${newValue}`);
          },
        } 
    });
</script>
复制代码

基本结构与vue书写语法基本一样,只是为了区分,我们把v-if与v-show换成m-if和m-show。

所有的实现逻辑将在MS.js完成,接下来,就要实现他。

2.逻辑结构

我们可以看到,我们在上面的html中,使用的el,data,methods,watch这些都是与vue一模一样的。所以我们先创建MS类,做一个简单的逻辑结构去收集他们。

class MS {
  constructor(options) {
    this.$el = document.querySelector(options.el);
    this.$data = options.data && options.data();
    this.$methods = options.methods;
    this.$watch = options.watch;
    this.$dataPool = new Map();
    this.$eventPool = new Map();
    this.init();
  }
  init() {
    this.initData();
    this.initDom(this.$el);
    this.initRender();
    this.bindEvent();
  }
  initData() {}
  initDom(el) {}
  initRender() {}
  renderNode(key, value) {}
  bindEvent() {}
  }
}

export default MS;
复制代码
  • $el:记录主容器
  • $data:收集了传入的数据
  • $methods:收集了传入的方法
  • $watch:收集了传入要监听的内容
  • $dataPool:数据池,主要存放数据和对应节点
  • $eventPool:事件池,主要存放数据和对应事件

我们在初始化时要先要初始化这些数据然后绑定dom和事件,然后渲染出来。

3.初始化数据

initData() {
    const {$data,$watch} = this;
    for (const key in $data) {
        if (Object.hasOwnProperty.call($data, key)) {
            Object.defineProperty(this, key, {
                get() {
                    return $data[key]
                },
                set(value) {
                    let oldValue = $data[key];
                    $data[key] = value;
                    this.renderNode(key, value);
                    $watch[key]&&$watch[key](value,oldValue);
                }
            })
        }
    }
}
复制代码

这里了解过vue2源码的同学应该很清楚,我们先遍历绑定下数据,用Object.defineProperty跟其做数据代理,设置get和set方法。

现在期望的是:

  • get:返回当前key要查的值
  • set:设置新值,当然先记录一下旧值,因为我们后面会在$watch做监听把新值和旧值都要传过去,当然还要把当前key的数据改成新值,然后将会用renderNode方法做渲染。

4.初始化DOM

initDom(el) {
    let _childNodes = el.childNodes;
    if (!_childNodes.length) return false;
    _childNodes.forEach(node => {
        if (node.nodeType == 1) {
            let mShow = node.getAttribute("m-show");
            let mIf = node.getAttribute("m-if");
            let mClick = node.getAttribute("@click")

            if (mShow) {
                this.$dataPool.set(node, {
                    type: "show",
                    key: mShow
                })
            }

            if (mIf) {
                this.$dataPool.set(node, {
                    type: "if",
                    key: mIf
                })
            }

            if (mClick) {
                this.$eventPool.set(node, {
                    type: "click",
                    key: mClick,
                    event: this.$methods[mClick]
                })
            }
        }
        this.initDom(node)
    })
}
复制代码

这里我们从父容器拿到下面的子节点,遍历他这些字节点,如果出现如m-show,m-if,@click这些绑定语法,就要收集下来,存储到对应的数据池 d a t a P o o l 和事件池 dataPool和事件池 eventPool中。这样一旦触发当中的变量或者事件,他们的节点也会捕获到。

5.绑定事件

bindEvent() {
    for (const [dom, obj] of this.$eventPool) {
        dom.addEventListener(obj.type, obj.event.bind(this), false)
    }
}
复制代码

刚刚我们在事件池$eventPool收集的事件,我们还要根据其dom一一的去绑定。

6.渲染DOM

我们先将怎么初始化渲染,代码如下:

initRender() {
    for (const [dom, obj] of this.$dataPool) {
        let value = this.$data[obj.key];
        switch (obj.type) {
            case "show":
                obj.value = dom.style.display;
                !value && (dom.style.display = "none");
                break;
            case "if":
                obj.comment = document.createComment("m-if");
                !value && dom.parentNode.replaceChild(obj.comment, dom)
                break;
            default:
                break;
        }
    }
}
复制代码

也刚刚我们在数据池$dataPool收集的数据,要根据其dom一一的去绑定。这里我们仅仅处理show和if两种类型。

  • show:很简单,我们存一下原始的style.display的值,他的显隐就是不断改变style.display的值是不是none罢了。

  • if:或许,我们咋一开始想不到,因为他是用document.createComment创建并保存一个虚的节点,然后根据他的显隐来是否替换原来的dom。

微信截图_20211011112740.png

当然,我们每次数据变动还要重新渲染要改变数据的节点。

renderNode(key, value) {
    for (const [dom, obj] of this.$dataPool) {
        if (key == obj.key) {
            switch (obj.type) {
                case "show":
                    dom.style.display = !value ? "none" : obj.value;
                    break;
                case "if":
                    !value ? dom.parentNode.replaceChild(obj.comment, dom) :
                    obj.comment.parentNode.replaceChild(dom, obj.comment);
                    break;
                default:
                    break;
            }
        }
    }
}
复制代码

重新渲染,跟上面初始化渲染差不多,只是不需要再记录和创建了,只做更改和替换。

结语

写到这里我们就实现了一个自己的v-if和v-show语法,也可以监听到他的改变。本质还是依赖收集,根据变量的改变而改变从而渲染dom,show用style.display去实现,而if用document.createComment方法生成节点去替换原来的。当然你也可以,再此基础上继续扩展,完成跟vue一样的有趣能力。

VID_20211011_113527.gif

猜你喜欢

转载自juejin.im/post/7017650102273572900