v-if & v-show原理解析及最简模型实现

2022 年 7 月 11 日 星期一
/ ,
16

v-if & v-show原理解析及最简模型实现

前期准备

视图层准备了四个盒子,相同宽高,并且给他们各自不同的颜色以区分,并且全部设置为左浮动,这样能观察到v-if显隐的排序问题。

<!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>Document</title>
    <style>
      .box1 {
        float: left;
        width: 100px;
        height: 120px;
        background-color: red;
      }
      .box2 {
        float: left;
        width: 100px;
        height: 120px;
        background-color: green;
      }
      .box3 {
        float: left;
        width: 100px;
        height: 120px;
        background-color: blue;
      }
      .box4 {
        float: left;
        width: 100px;
        height: 120px;
        background-color: gray;
      }
      .box-container {
        overflow: hidden;
      }
    </style>
  </head>
  <body>
    <div class="wrapper">
      <div class="box-container">
        <div class="box1" v-if="boxView1">box1</div>
        <div class="box2" v-show="boxView2">box2</div>
        <div class="box3" v-show="boxView3">box3</div>
        <div class="box4" v-if="boxView4">box4</div>
      </div>
      <div class="control">
        <button @click="changeView1">change view 1</button>
        <button @click="changeView2">change view 2</button>
        <button @click="changeView3">change view 3</button>
        <button @click="changeView4">change view 4</button>
      </div>
    </div>
    <script src="./index.js"></script>
    <script>
      const app = new HyVue({
        el: '.wrapper',
        data: {
          boxView1: false,
          boxView2: false,
          boxView3: false,
          boxView4: false,
        },
        methods: {
          changeView1() {
            this.boxView1 = !this.boxView1;
            console.log('changeView1');
          },
          changeView2() {
            this.boxView2 = !this.boxView2;
            console.log('changeView2');
          },
          changeView3() {
            this.boxView3 = !this.boxView3;
            console.log('changeView3');
          },
          changeView4() {
            this.boxView4 = !this.boxView4;
            console.log('changeView4');
          },
        },
      });
    </script>
  </body>
</html>

js部分

class初始化

js部分定义一个类,在使用new调用该类进行初始化,将传入的options解构出挂载元素el,数据层data,事件层methods

class HyVue {
  constructor(options) {
    const { el, data, methods } = options;
    this.el = document.querySelector(el);
    this.data = data;
    this.methods = methods;
  }
}

创建视图和数据的关联

视图要进行变化,必须拿到相关的元素做处理,而且元素和数据肯定要关联起来,这样在找到元素时也能找到数据,同时找到数据也能找到元素。此时可使用Map来做映射处理,所以我们定义一个showPool为视图池,eventPool为事件池。将data和对应的dom元素关联并添加到showPool;将event和对应的dom元素关联并添加到eventPool。

class HyVue {
  constructor(options) {
    const { el, data, methods } = options;
    this.el = document.querySelector(el);
    this.data = data;
    this.methods = methods;
    this.showPool = new Map();
    this.eventPool = new Map();
  }

初始化数据(将data中的数据代理到实例的this)

这个步骤将用defineProperty将数据代理到this,这样在绑定的方法中用this访问时可以直接访问到data而不用再进到data对象去访问。在vue2中我们可以使用this直接访问到data的数据也是这个原理。

class HyVue {
  constructor(options) {
    const { el, data, methods } = options;
    this.el = document.querySelector(el);
    this.data = data;
    this.methods = methods;
    this.showPool = new Map();
    this.eventPool = new Map();
    this.init();
  }
  init() {
    //初始化数据,将data中的数据代理到this中
    this.initData();
  }
  initData() {
    const dataKey = Object.keys(this.data);
    for (const key of dataKey) {
      Object.defineProperty(this, key, {
        get: function () {
          return this.data[key];
        },
        set: function (newValue) {
          this.data[key] = newValue;
        },
      });
    }
  }

初始化dom

这一步为关键步骤,我们需要在视图池和事件池中添加关联,以在后续可以找到某个绑定的dom元素所对应的data或方法。

class HyVue {
  constructor(options) {
    const { el, data, methods } = options;
    this.el = document.querySelector(el);
    this.data = data;
    this.methods = methods;
    this.showPool = new Map();
    this.eventPool = new Map();
    this.init();
  }
  init() {
    //初始化数据,将data中的数据代理到this中
    this.initData();
    //初始化dom 往视图池和事件池中添加关联
    this.initDom();
    //初始化视图,根据视图池中的关联,决定元素的显隐,
    this.initView(this.showPool);
    //初始化方法,为事件池中关联元素绑定事件
    this.initMethods();
  }
  initData() {
    // ...略
  }
  initDom() {
    const traverseNode = (node) => {
      if (!node.childNodes.length) return;
      const nodes = node.childNodes;
      for (const dom of nodes) {
                //此时nodeType===1为元素节点
        if (dom.nodeType === 1) {
                    //找到有以下属性的dom元素
          const v_if = dom.getAttribute('v-if');
          const v_show = dom.getAttribute('v-show');
          const v_event = dom.getAttribute('@click');
          if (v_if) {
                        //创建v_if关联
            this.showPool.set(dom, {
              type: 'v-if',
              show: this[v_if],
              dataName: v_if,
            });
          } else if (v_show) {
                        //创建v_show关联
            this.showPool.set(dom, {
              type: 'v-show',
              show: this[v_show],
              dataName: v_show,
            });
          }
          if (v_event) {
                        //创建事件关联
            this.eventPool.set(dom, {
              method: this.methods[v_event],
              dataName: v_event,
              event: 'click',
            });
          }
                    //递归查询
          traverseNode(dom);
        }
      }
    };
    traverseNode(this.el);
  }

初始化视图

此时视图层和数据层的关联已经创建完了,下一步是不是应该将视图和数据联系起来了?如果dom元素属性为v-if,那么其绑定的data为false时,将不对其进行渲染,重点来了我们应该如何隐藏该元素? 第一种办法直截了当,把该元素删掉,后续显示的时候再创建。哈哈,很明显这种办法行不通,因为你不知道创建该元素后应该放在那个位置,appendChild?那么创建的元素会在所有子元素的尾部,这样dom结构就乱了;insertChild?此时的位置是未知的,如果要知道在哪个元素的前面,这样需要重复做复杂的记录操作。所以有一个解决办法,vue也是使用了这个办法,使用注释来替换该元素,然后记录该注释,之后如果要显示该元素,那么就用该dom节点来替换注释。

class HyVue {
  constructor(options) {
    const { el, data, methods } = options;
    this.el = document.querySelector(el);
    this.data = data;
    this.methods = methods;
    this.showPool = new Map();
    this.eventPool = new Map();
    this.init();
  }
  init() {
    //初始化数据,将data中的数据代理到this中
    this.initData();
    //初始化dom 往视图池和事件池中添加关联
    this.initDom();
    //初始化视图,根据视图池中的关联,决定元素的显隐,
    this.initView(this.showPool);
    //初始化方法,为事件池中关联元素绑定事件
    this.initMethods();
  }
  initData() {
    // ... 略
  }
  initDom() {
    // ... 略
  }
  initView(data, showPool) {
    //同时负责视图层的更新
    this.domChange(null, this.showPool);
  }
  domChange(data, showPool) {
    if (!data) {
            //初始化
      showPool.forEach((view, dom) => {
        switch (view.type) {
          case 'v-if':
            view.comment = document.createComment(view.dataName || 'unknown');
            view.show ? '' : dom.parentNode.replaceChild(view.comment, dom);
            break;
          case 'v-show':
            view.show
              ? (dom.style.display = 'block')
              : (dom.style.display = 'none');
            break;
        }
      });
    }
  }

初始化方法

这个没什么好说的,把事件池eventPool遍历一遍,使用dom.addEventListener绑定事件,但是有一点要注意,此时所绑定的事件的this是指向该元素的,所以需要用Func.bind(this)显式绑定this到实例,这样才能访问实例里的data。

class HyVue {
  constructor(options) {
    const { el, data, methods } = options;
    this.el = document.querySelector(el);
    this.data = data;
    this.methods = methods;
    this.showPool = new Map();
    this.eventPool = new Map();
    this.init();
  }
  init() {
    //初始化数据,将data中的数据代理到this中
    this.initData();
    //初始化dom 往视图池和事件池中添加关联
    this.initDom();
    //初始化视图,根据视图池中的关联,决定元素的显隐,
    this.initView(this.showPool);
    //初始化方法,为事件池中关联元素绑定事件
    this.initMethods();
  }
  initData() {
    // ... 略
  }
  initDom() {
    // ... 略
  }
  initView(data, showPool) {
    // ... 略
  }
  domChange(showPool) {
    // ... 略
  }
  initMethods() {
    this.eventPool.forEach((data, dom) => {
      dom.addEventListener(data.event, data.method.bind(this));
    });
  }
}

执行方法,触发视图变化

执行方法后,我们需要知道此时哪个数据变化了,而且要把变化的值给到对应的dom处理逻辑去处理。此时defineProperty的set方法就派上用场了,set方法可以监听到数据变化,我们可以在此方法中去触发视图的更新。又因为初始化视图和更新视图的逻辑差不多,就在domChange方法中处理了,通过data来判断此时是初始化还是更新操作。遍历视图池,找到该变量key值相等的dom元素进行处理。

完整代码: javascript class HyVue { constructor(options) { const { el, data, methods } = options; this.el = document.querySelector(el); this.data = data; this.methods = methods; this.showPool = new Map(); this.eventPool = new Map(); this.init(); } init() { //初始化数据,将data中的数据代理到this中 this.initData(); //初始化dom 往视图池和事件池中添加关联 this.initDom(); //初始化视图,根据视图池中的关联,决定元素的显隐, this.initView(this.showPool); //初始化方法,为事件池中关联元素绑定事件 this.initMethods(); } initData() { const dataKey = Object.keys(this.data); for (const key of dataKey) { Object.defineProperty(this, key, { get: function () { return this.data[key]; }, set: function (newValue) { this.data[key] = newValue; this.domChange( { key, newValue, }, this.showPool ); }, }); } } initDom() { const traverseNode = (node) => { if (!node.childNodes.length) return; const nodes = node.childNodes; for (const dom of nodes) { if (dom.nodeType === 1) { const v_if = dom.getAttribute('v-if'); const v_show = dom.getAttribute('v-show'); const v_event = dom.getAttribute('@click'); if (v_if) { this.showPool.set(dom, { type: 'v-if', show: this[v_if], dataName: v_if, }); } else if (v_show) { this.showPool.set(dom, { type: 'v-show', show: this[v_show], dataName: v_show, }); } if (v_event) { this.eventPool.set(dom, { method: this.methods[v_event], dataName: v_event, event: 'click', }); } traverseNode(dom); } } }; traverseNode(this.el); } initView(data, showPool) { //同时负责视图层的更新 this.domChange(null, this.showPool); } domChange(data, showPool) { if (!data) { showPool.forEach((view, dom) => { switch (view.type) { case 'v-if': view.comment = document.createComment(view.dataName || 'unknown'); view.show ? '' : dom.parentNode.replaceChild(view.comment, dom); break; case 'v-show': view.show ? (dom.style.display = 'block') : (dom.style.display = 'none'); break; } }); } else { showPool.forEach((view, dom) => { if (view.dataName === data.key) { switch (view.type) { case 'v-if': data.newValue ? view.comment.parentNode.replaceChild(dom, view.comment) : dom.parentNode.replaceChild(view.comment, dom); break; case 'v-show': data.newValue ? (dom.style.display = 'block') : (dom.style.display = 'none'); break; } view.show = data.newValue; } }); } } initMethods() { this.eventPool.forEach((data, dom) => { dom.addEventListener(data.event, data.method.bind(this)); }); } }

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...