手写v-if和v-show(2)-具体实现过程

892 阅读1分钟

在 手写手写v-if 和 v-show(1) - 掘金 (juejin.cn)-创建代码结构中,整体的代码结构,以及我们要做的事情以及表述完了. 接下来就是一一实现每一个方法了.

一、实现响应式数据

观察:

  1. data返回函数的数据中, 我们可以在别的地方直接使用this来进行调用
  2. 如果变量是基本数据类型,  变量一旦发生了变化,视图就发生了变化

会有如下代码

Vue.prototype._init = function (vm, template, methods) {
  initData(vm);
};
function initData(vm) {
  var _data = vm.$data;
  for (var key in _data) {
    (function (key) {
      Object.definedPrototype(vm, key, {
        get: function () {
          return _data[key];
        },
        set: function (newVal) {
          _data[key] = newVal;
        },
      });
    })(key);
  }
}

如此,把data中返回的数据就可以直接通过 vue 的实例来直接访问到了. 这样实现了第一点.至于第二点, 就需要在set的方法的时候,调用了视图更新的逻辑.

set: function(newVal) {
    _data[key] = newVal
    update(vm, key)
}

update方法需要当前vue的实例, 以及当前修改的变量.

二、template 模版数据处理

template中存在如下数据类型:

  1. 需要转化为真实 DOM 的节点数据
  2. v-if 和 v-show 和对应变量的attribute参数
  3. @click和对应方法的  的attribute参数

三个不同类型的数据都是我们需要处理的. 由于第 2 点和第 3 点需要用在别的地方,所以我们需要想办法将他们给存储起来.

为了方便存储数据,我们使用可以以对象为键名的Map来进行存储

var showPool = new Map();
var eventPool = new Map();

function initPool(vm, container, methods) {
  var _allNodes = container.getElementsByTagName("*");
  for (var i = 0; i < _allNodes.length; i++) {
    var node = _allNodes[i];
    var vIfData = node.getAttribute("v-if");
    var vShowData = node.getAttribute("v-show");
    var eventData = node.getAttribute("@click");

    if (vIfData) {
      showPool.set(node, {
        type: "if",
        prop: vIfData,
      });
    } else if (vShowData) {
      showPool.set(node, {
        tyep: "show",
        prop: vShowData,
      });
    }
    if (eventData) {
      eventPool.set(node, methods[eventData]);
    }
  }
}

上面的eventData没有使用else if 是因为同一个节点上面是有可能同时存在v-if/v-show@click同时存在的

自此,我们需要进一步处理的数据都存储起来. 但是还有一个问题,我处理从template中拿到了想要的数据之后, 然后一个问题需要处理.这些attribute里面的对应的变量以及拿到了. 这个时候就需要我们把这些对于 html 解析无效的东西.

在循环中,进入判断之后,就将其删除掉

node.removeAttribute("v-if");
node.removeAttribute("v-show");
node.removeAttribute("@click");

三、事件绑定

vue 中的事件处理一样是不需要使用this.methods.clickShow1()这样的方式,直接通过调用 vue 的实例化对象就可以调用.

function bindEvent(vm, eventPool) {
  for (var [dom, handle] of eventPool) {
    vm[handle] = handle;
    dom.addEventListenner("click", vm[handle].bind(vm), false);
  }
}

其中bind(vm)为了确保在 vue 中能够指向 vue 实例.

四、界面渲染

需要的各种数据已经齐备了. 这个事件就需要使用这些数据来渲染界面了.

  function render(vm, container, showPool ) {
    var _data = vm.$date
    var _el = vm.$el
    for (var [dom, info] of showPool) {
      switch info.type {
        case 'if':
                info.comment = document.createComment(["v-if"])
                !_data[info.prop] && dom.parentNode.replaceChild(info.comment, dom)
          break
        case 'show':
          !_data[info.prop] && dom.style.display = 'none'
                break
        default:
          break
      }
    }
    _el.append(container)
  }

此时看界面,不出意外,应该已经渲染完毕了.

使用_data[info.prop]来进行判断,而不是直接使用vm[info.prop]. 就是为了让变量不污染全局. 区别于在实例中直接this.xxx来注册的变量

五、更新数据

响应式的数据变化了之后,需要重新渲染对应的界面.通常这个发生在initData方法的set中.

// initData()
Object.definedPrototype(vm, key, {
  get: function () {
    return _data[key];
  },
  set: function (newVal) {
    _data[key] = newVal;
    update(vm, key, showPool);
  },
});
function update(vm, key, showPool) {
  const _data = vm.$data; // 这样写是为了,让你明白这变量是在data中定义的,还是直接绑定到vue实例上面的
  for (var [dom, info] of showPool) {
    if (info.prop === key) {
      switch (info.type) {
        case "if":
          !_data[key]
            ? dom.parentNode.replaceChild(info.comment, dom)
            : info.comment.parentNode.replaceChild(dom, info.comment);
          break;
        case "show":
          let displayValue = _data[info.prop] ? "inline" : "none";
          dom.style.display = displayValue;
          break;
        default:
          break;
      }
    }
  }
}

值得注意的是,在if的判断中, 当我们使用了注释标签对节点进行替换之后,就不存在原来的dom节点了.但是info.comment一直都没有被删除,所以是直接将info.comment替换成存储的dom节点.

涉及到的知识点

  1. Node.parentNode()
  2. Node.replaceChild()
  3. Object.definedPrototype
  4. Map集合的使用