Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 常用设计模式 #26

Open
yanyue404 opened this issue May 23, 2018 · 0 comments
Open

JavaScript 常用设计模式 #26

yanyue404 opened this issue May 23, 2018 · 0 comments

Comments

@yanyue404
Copy link
Owner

yanyue404 commented May 23, 2018

前言

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

工厂模式

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

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 yanyue404 changed the title Javascript设计模式 设计模式之发布—订阅模式 May 16, 2020
@yanyue404 yanyue404 added Css and removed JavaScript labels May 19, 2020
@yanyue404 yanyue404 changed the title 设计模式之发布—订阅模式 JavaScript 常用设计模式 Apr 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant