原生js实现vue指令

1,063 阅读3分钟

原生js实现vue指令

1 简介

使用原生js实现vue特性。这里分享一个v-show指令的简单例子,代码会更新到 Gitee 上,只考虑了简单实现并没有考虑真实应用场景和代码优化。

2 需求

先简单构建一个需求,如下图:

v-show.prototype.png

vue中实现这样的功能非常简单,因为vue已经提供了v-showv-if这样的指令:

    <template>
        <div>
            <button @click="clickHandle">button</button>
            <h1 v-show="visible">some content</h1>
        </div>
    </template>
    <script>
        export default {
            data(){
                return {
                    visible: true,
                }
            },
            methods:{
                clickHandle(){
                    this.visible = !this.visible;
                }
            }
        }
    </script>

接下来我们看看原生如何实现像v-show这样的指令。

3 设计与实现

3.1 编码设计

我们参考vue的生命周期,只保留需要实现v-show指令的关键步骤:

  • 在初始化时绑定数据和事件
  • 在数据变化时更新dom 我们将这些功能拆分为如下几个部分:

v-show.model.png

3.2 编码实现

  • 我们先定义dataStoredirective分别用于存储数据和指令,这里定义的指令回调接收两个参数,一个是绑定指令的元素el,另一个是指令绑定的dataStore中的属性的值value
    // 用于存储响应式数据
    const dataStore = {
        visible: true,
    }; 
    // directive对象的属性名表示指令名称,属性值是指令的回调函数
    const directive = {
        "v-show": function (el, value) {
          if (value) {
            el.style.display = "";
          } else {
            el.style.display = "none";
          }
        },
    }; 
  • 按照前面章节中的设计将dom结构画出来,并且根据前面的定义为标签添加指令属性:
    <div id="#app">
        <button>
          <span v-show="visible">隐藏</span>
          <span v-show="!visible">显示</span>
        </button>
        <h1 v-show="visible">this is your content</h1>
    </div>
  • 为了实现响应式原理,我们先使用Object.defineProperty来实现一个对象属性监听函数。
    /**
     * 对象属性监听
     * @param {object} obj - 要监听的对象
     * @param {string} key - 要监听的对象属性
     * @param {object} options - 监听选项
     * @param {boolean} [options.immediate] - 是否首次触发回调
     * @param {function} options.callBack - 监听回调
     * @param {*} options.value - 初始值
     */
    function watch(obj, key, options) {
      let temp = options.value;
      if (
        options &&
        options.callBack &&
        typeof options !== "function" &&
        options.immediate
      ) {
        options.callBack(undefined, temp);
      }
      Object.defineProperty(obj, key, {
        set(val) {
          const oldVal = temp;
          temp = val;
          if (options && options.callBack && typeof options !== "function") {
            options.callBack(oldVal, val);
          }
          if (options && typeof options === "function") {
            options(oldVal, val);
          }
        },
        get() {
          return temp;
        },
      });
    }
  • 前面的步骤中我们定义了指令,当然我们需要按照这样的定义去解析我们的指令。这里只实现了对于dataStore中的属性直接取值或者取反值两种逻辑的解析,并没有像vue一样支持一个完整的表达式,那需要更多的编码,这里只是简单实现。当然如果笔者能坚持继续写下去的话,后期会考虑实现这样的功能。
     /**
       * parse the directive in tag
       * @param {HTMLElement} el
       * @param {string} name - directive name
       * @param {function} callBack
       * */
      function directiveParser(el, name, callBack) {
        let prop = name;
        let flag = true;
        if (name.charAt(0) === "!") {
          prop = name.slice(1, name.length);
          flag = false;
        }
        let value = flag ? dataStore[prop] : !dataStore[prop];
        if (typeof callBack === "function") {
          callBack(el, value);
        }
      }
  • 接下来我们还需要一个能够解析dom树的函数,当然在我们的设计中省略了vue中相当多的内容,所以解析逻辑也变得非常简单。当然这种极简设计也是可以扩展的,这里只是对指令业务进行了简单实现,你完全可以实现更多像directiveParser这样的函数去完成各种各样的业务。
      /**
       * parse dom
       * @param {HTMLElement} node
       **/
      function parseDomTree(node) {
        node.getAttributeNames().forEach((el) => {
          if (directive[el]) {
            directiveParser(node, node.getAttribute(el), directive[el]);
          }
        });
        Array.from(node.children).forEach((el) => {
          parseDomTree(el);
        });
      }
  • 在完成前面的编码后我们简单组织一下代码就可以完成了:
      // 省略了前面的步骤中定义的变量、函数以及html结构
      // bind data
      bind();

      // register event
      document.querySelector("button").onclick = () => {
        dataStore.visible = !dataStore.visible;
      };

      // bind watch
      function bind() {
        Object.keys(dataStore).forEach((el) => {
          watch(dataStore, el, {
            immediate: true,
            value: dataStore[el],
            callBack: update,
          });
        });
      }

      // update dom
      function update() {
        parseDomTree(document.querySelector("#app"));
      }

你可以在Gitee上获取完整的示例。