blog icon indicating copy to clipboard operation
blog copied to clipboard

JavaScript 常用设计模式

Open yanyue404 opened this issue 6 years ago • 0 comments

前言

当学习深入了解后,发现一些晦涩难懂的技巧与设计模式有关,记录学习日志。

工厂模式

工厂起到的作用就是隐藏了创建实例的复杂度,只需要提供一个函数,把对象放到函数里,用函数封装创建对象的细节。

var Factory = function (type, text) {
  // 判断 this 是否是 Factory的实例,代码执行两次
  if (this instanceof Factory) {
    this[type](text);
  } else {
    return new Factory(type, text);
  }
};

Factory.prototype = {
  javascript: function (text) {
    console.log(text + "javascript");
  },
  nodejs: function (text) {
    console.log(text + "nodejs");
  },
};

const o = Factory("javascript", "万能的"); // log: 万能的javascript
console.log(o instanceof Object); // true
console.log(o instanceof Factory); // true

在 Vue 源码中,你也可以看到工厂模式的使用,比如创建异步组件

export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // 逻辑处理...

  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  );

  return vnode;
}

在上述代码中,我们可以看到我们只需要调用 createComponent 传入参数就能创建一个组件实例,但是创建这个实例是很复杂的一个过程,工厂帮助我们隐藏了这个复杂的过程,只需要一句代码调用就能实现功能。

构造器模式

构造器模式与工厂模式类似,new 这个对象就是创建对象的实例,实例被标识为特定的类型。构造函数模式一般结合原型进行使用,保证每个实例的方法是同一个。

function Vue(options) {
  if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  this._init(options);
}

initMixin(Vue); // _init,_uid,$options 当前 Vue 实例的初始化选项,注意:这是经过 mergeOptions() 后的
stateMixin(Vue); // $data, $props 设为只读属性,继续添加 $set, $delete, $watch
eventsMixin(Vue); // $on, $emit, $off, $once
lifecycleMixin(Vue); // _update, $forceUpdate, $destroy
renderMixin(Vue); // installRenderHelpers 函数中的方法,$nextTick,_render

var vm1 = new Vue();
var vm2 = new Vue();
vm1._init === vm2._init; // true
// el.spec.js
import Vue from 'vue'

describe('Options el', () => {
  it('basic usage', () => {
    const el = document.createElement('div')
    el.innerHTML = '<span>{{message}}</span>'
    const vm = new Vue({
      el,
      data: { message: 'hello world' }
    })
    expect(vm.$el.tagName).toBe('DIV')
    expect(vm.$el.textContent).toBe(vm.message)
  })
}

单例模式

单例模式很常用,比如全局缓存、全局状态管理等这些只需要一个对象,就可以使用单例模式。

单例模式的核心就是保证全局一个类仅有⼀个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不不存在就创建了了再返回,这就确保了了一个类只有一个实例对象。

因为 JS 是门无类的语言,所以别的语言实现单例的方式并不能套入 JS 中,我们只需要用一个闭包变量确保实例只创建一次就行,以下是如何实现单例模式的例子。

class Singleton {
  constructor() {}
}

Singleton.getInstance = (function () {
  let instance;
  return function () {
    if (!instance) {
      instance = new Singleton();
    }
    return instance;
  };
})();

let s1 = Singleton.getInstance();
let s2 = Singleton.getInstance();
console.log(s1 === s2); // true

也可以构建一个工厂函数,用于将正常的类加工成单例模式的类。

function singleton(target) {
  let _instance = null;

  const _wrapper = function singletoned() {
    if (_instance) {
      return _instance;
    } else {
      const ins = Object.create(target.prototype);
      _instance = target.apply(ins, arguments) || ins;
      return _instance;
    }
  };

  return _wrapper;
}

class Target {
  constructor() {
    this.name = "target" + Date.now();
  }
  say() {
    console.log(this.name);
  }
}

const SingleTarget = singleton(Target);

new SingleTarget().say();
new SingleTarget().say();
new SingleTarget().say();

在 Vuex 源码中,你也可以看到单例模式的使用,虽然它的实现方式不大一样,通过一个外部变量来控制只安装一次 Vuex

let Vue; // bind on install

export function install(_Vue) {
  if (Vue && _Vue === Vue) {
    // 如果发现 Vue 有值,就不重新创建实例了
    return;
  }
  Vue = _Vue;
  applyMixin(Vue);
}

模块模式

使用 jquerygetJSON 方法来获取 github repoList 数据列表

<div id="root"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>
  var Module = {
    init: function () {
      this.id = "root";
      this.error = null;
      this.fetchOrderList(); // 若有可以扩展添加结束处理的逻辑
    },
    fetchOrderList: function () {
      var y = this;
      y.render("loading...");
      $.getJSON(
        "https://api.github.com/search/repositories?q=javascript&sort=stars"
      ).then(
        (value) => {
          y.render(value);
        },
        (error) => {
          y.error = error; // 错误标记
          y._fetchDataFailed(error);
        }
      );
    },
    render: function (data) {
      var y = this;
      let html;
      if (y.error === null && typeof data !== "string") {
        html = this._resolveData(data);
      } else {
        html = data;
      }
      document.getElementById(y.id).innerHTML = html;
    },

    // 需要时格式化处理
    _resolveData: function (data) {
      var repos = data.items;
      var repoList = repos.map(function (repo, index) {
        return `<li> <a href=${repo.html_url}>${repo.name}</a> (${repo.stargazers_count} stars) <br /> ${repo.description}</li>`;
      });
      return `<main>
            <h1>Most Popular JavaScript Projects in Github</h1>
            <ol> ${repoList.join("")}</ol>
              </main> `;
    },
    // 错误处理
    _fetchDataFailed: function (error) {
      let errorHtml = `<span>Error: ${error.message}</span>`;
      this.render(errorHtml);
    },
  };
  Module.init();
</script>

发布订阅模式

异步处理逻辑的一种方式,需要做全局存储事件调控中心,在原生开发小程序中有应用,支持先订阅后发布,以及先发布后订阅

注意:使用完成后及时卸载

var Event = (function () {
  var clientList = {},
    pub,
    sub,
    remove;

  var cached = {};

  sub = function (key, fn) {
    if (!clientList[key]) {
      clientList[key] = [];
    }
    // 使用缓存执行的订阅不用多次调用执行
    cached[key + "time"] == undefined ? clientList[key].push(fn) : "";
    if (cached[key] instanceof Array && cached[key].length > 0) {
      //说明有缓存的 可以执行
      fn.apply(null, cached[key]);
      cached[key + "time"] = 1;
    }
  };
  pub = function () {
    var key = Array.prototype.shift.call(arguments),
      fns = clientList[key];
    if (!fns || fns.length === 0) {
      //初始默认缓存
      cached[key] = Array.prototype.slice.call(arguments, 0);
      return false;
    }

    for (var i = 0, fn; (fn = fns[i++]); ) {
      // 再次发布更新缓存中的 data 参数
      cached[key + "time"] != undefined
        ? (cached[key] = Array.prototype.slice.call(arguments, 0))
        : "";
      fn.apply(this, arguments);
    }
  };
  remove = function (key, fn) {
    var fns = clientList[key];
    // 缓存订阅一并删除
    var cachedFn = cached[key];
    if (!fns && !cachedFn) {
      return false;
    }
    if (!fn) {
      fns && (fns.length = 0);
      cachedFn && (cachedFn.length = 0);
    } else {
      if (cachedFn) {
        for (var m = cachedFn.length - 1; m >= 0; m--) {
          var _fn_temp = cachedFn[m];
          if (_fn_temp === fn) {
            cachedFn.splice(m, 1);
          }
        }
      }
      for (var n = fns.length - 1; n >= 0; n--) {
        var _fn = fns[n];
        if (_fn === fn) {
          fns.splice(n, 1);
        }
      }
    }
  };
  return {
    pub: pub,
    sub: sub,
    remove: remove,
  };
})();

适配器模式

适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。

以下是如何实现适配器模式的例子

class Plug {
  getName() {
    return "港版插头";
  }
}

class Target {
  constructor() {
    this.plug = new Plug();
  }
  getName() {
    return this.plug.getName() + " 适配器转二脚插头";
  }
}

let target = new Target();
target.getName(); // 港版插头 适配器转二脚插头

在 Vue 中,我们其实经常使用到适配器模式。比如父组件传递给子组件一个时间戳属性,组件内部需要将时间戳转为正常的日期显示,一般会使用 computed 来做转换这件事情,这个过程就使用到了适配器模式。

代理模式

代理是为了控制对对象的访问,不让外部直接访问到对象。在现实生活中,也有很多代理的场景。比如你需要买一件国外的产品,这时候你可以通过代购来购买产品。

在实际代码中其实代理的场景很多,比如事件代理、图片预加载就用到了代理模式。

先通过一张 loading 图占位,然后通过异步的方式加载图⽚片,等图⽚加载好了再把 完成的图⽚加载到 img 标签⾥里面。

var myImage = (function () {
  var imgNode = document.createElement("img");
  document.body.appendChild(imgNode);
  return {
    setSrc: function (src) {
      imgNode.src = src;
    },
  };
})();
var proxyImage = (function () {
  var img = new Image();
  img.onload = function () {
    setTimeout(() => {
      myImage.setSrc(this.src);
    }, 500);
  };
  return {
    setSrc: function (src) {
      myImage.setSrc("./loading.gif");
      img.src = src;
    },
  };
})();

proxyImage.setSrc(
  "https://www.wangbase.com/blogimg/asset/202001/bg2020013101.jpg"
);

外观模式

外观模式提供了一个接口,隐藏了内部的逻辑,更加方便外部调用。

举个例子来说,我们现在需要实现一个兼容多种浏览器的添加事件方法

function addEvent(elm, evType, fn, useCapture) {
  if (elm.addEventListener) {
    elm.addEventListener(evType, fn, useCapture);
    return true;
  } else if (elm.attachEvent) {
    var r = elm.attachEvent("on" + evType, fn);
    return r;
  } else {
    elm["on" + evType] = fn;
  }
}

对于不同的浏览器,添加事件的方式可能会存在兼容问题。如果每次都需要去这样写一遍的话肯定是不能接受的,所以我们将这些判断逻辑统一封装在一个接口中,外部需要添加事件只需要调用 addEvent 即可。

装饰者模式

装饰者模式在现在的前端开发场景应用很广泛,如:

  • react 的高阶函数
  • react-reduxconnect 方法
  • react-routerwithouter方法
  • antdForm.create方法
  • Taro 编译小程序时 将 getApp()方法使用 @withWeapp('Page') class _C extends Taro.Component {}传入组件中
  • 最后点出来 es6 好用的 { ...data} 解构方法
  • ...
Function.prototype.before = function (beforefn) {
  var __self = this; // 保存原函数的引用
  return function () {
    // 返回包含了原函数和新函数的"代理"函数
    beforefn.apply(this, arguments); // 执行新函数,且保证 this 不被劫持,新函数接受的参数
    // 也会被原封不动地传入原函数,新函数在原函数之前执行
    return __self.apply(this, arguments); // 执行原函数并返回原函数的执行结果,
    // 并且保证 this 不被劫持
  };
};
Function.prototype.after = function (afterfn) {
  var __self = this;
  return function () {
    var ret = __self.apply(this, arguments);
    afterfn.apply(this, arguments);
    return ret;
  };
};
  • 实例存留,装饰者待深入源码研究学习,未完待续 ...
let doSomething = function () {
  console.log(1);
};

doSomething = doSomething
  .before(() => {
    console.log(3);
  })
  .after(() => {
    console.log(2);
  });

doSomething(); // 输出 312

参考

yanyue404 avatar May 23 '18 02:05 yanyue404