九旬

九旬 查看完整档案

北京编辑北京交通大学  |  计算机 编辑北京  |  前端开发 编辑 ansonznl.github.io 编辑
编辑

每月至少总结一篇技术文章

博客地址:https://ansonznl.github.io

个人动态

九旬 发布了文章 · 4月13日

事件的防抖和节流

事件的防抖和节流

防抖和节流函数是我们经常用到的函数,在实际的开发过程中,如 scroll、resize、click、键盘等事件很容易被多次触发,频繁的触发回调会导致页面卡顿和抖动,为了避免这种情况,需要使用节流和防抖的方法来减少无用的操作和网络请求,也是面试中经常遇到的问题,需要牢牢掌握。

防抖和节流的本质

都是闭包的形式存在的.

他们通过对事件的回调函数进行包裹、以保存自由变量的形式来缓存时间信息,最后使用 setTimeout 来控制事件的触发频率。

节流:第一个人说了算

节流(Throttle)的中心思想在于:在某段时间内不过你触发了多少次,我都只认第一次,并且在计时结束时给出响应。

/**
 * 函数节流
 * 作用:一段时间内的多次操作,只按照第一次触发开始计算,并在计时结束时给予响应。
 * 场景:如输入搜索功能
 * @param fn 需要进行节流操作的事件函数
 * @param interval 间隔时间
 * @returns {Function}
 */
function throttle(fn, interval = 500) {
  let last = 0;
  return function (...args) {
    let now = +new Date();
    if (now - last > interval) {
      last = now;
      fn.call(this, args);
    }
  };
}
/**
 * 步骤
 * 接受一个函数,和一个触发间隔时间,时间默认是 500ms
 * 默认赋值为0
 * 将多个参数解构为一个参数数组
 * 记录本次触发回调的时间
 * 判断上次触发的时间和本次之间的间隔是否大于我们设定的阈值
 * 将本次触发的时间赋值给last,用于下次判断
 * 使用call调用传入的回调函数,并传入参数
 *
 */

使用:在 onScorll 中使用节流

// 使用 throttle 来包装 scorll 的回调函数,设置间隔时间为1s
const better_scorll = throttle(() => {
  console.log("触发了滚动事件");
}, 1000);
document.addEventListenner("scorll", better_scorll);
// 1s内,无论触发多少次,都只从第一次触发之后的1s后给出响应。

防抖:最后一个人说了算

防抖的中心思想在于:我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次

/**
 * 函数防抖
 * 作用:一段时间内的多次操作,只执行最后一次。
 * 场景:如点击登录/注册/支付等按钮时
 * @param fn 需要进行防抖操作的事件函数
 * @param delay 延迟时间
 * @returns {Function}
 */
function debounce(fn, delay = 500) {
  let timer = null;
  return function (...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.call(this, args);
    }, delay);
  };
}
/**
 * 接受一个函数,和一个触发间隔时间,时间默认是 500ms
 * 定时器 id 默认赋值为null
 * 将多个参数解构为一个参数数组
 * 判断timer是否存在,如存在就取消定时器
 * 然后创建一个新的定时器,并将id赋值给timer
 * 然后如果再次点击重复上面的操作,一直到delay时间内没点时,定时器执行
 * 执行时:使用call调用传入的回调函数,并传入参数
 *
 */

使用:在 onScorll 中使用防抖

// 用debounce来包装scroll的回调
const better_scroll = debounce(() => console.log("触发了滚动事件"), 1000);
document.addEventListener("scroll", better_scroll);

用 Throttle 来优化 Debounce

debounce 的问题是它太有耐心了,试想,如果用户的操作十分频繁————他每次都不等 debounce 设置的 delay 的时间结束就进行下一次操作,于是每次 debounce 都会为用户重新生成定时器,回调函数被延迟了一次又一次,用户迟迟得不到响应,用户也会对这个页面产生“页面卡死”了的观感。

为了避免弄巧成拙,我们需要借力 Throttle 的思想,打造一个“有底线”的 debounce ,等你可以,但我有我的原则:delay 时间内,我可以为你重新生成定时器,但是只要 delay 时间一到,我就必须给用户一个响应。

这种 Throttle 和 debounce 合体的思想,已经被很多成熟的前端库应用到他们的加强版 throttle 函数中了。

/**
 * 加强版节流函数
 * 作用:delay时间内的多次操作将会重新生成定时器,但只要delay时间一到就执行一次。
 * 场景:如点击登录/注册/支付等按钮时
 * @param fn 需要进行防抖操作的事件函数
 * @param delay 延迟时间
 * @returns {Function}
 */
function throttle(fn, delay = 500) {
  let last = 0;
  let timer = null;
  return function (...args) {
    let now = +new Date();
    if (now - last < delay) {
      // 如果间隔时间小于我们设定的阈值,则重新生成一个定时器
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => {
        last = now;
        fn.call(this, args);
      }, delay);
    } else {
      // 如果 间隔时间大于设定的阈值,就执行一次。
      last = now;
      fn.call(this, args);
    }
  };
}
/**
 * 接受一个函数和延迟时间,延迟时间默认是500ms
 * 定义一个开始执行的时间戳和定时器id,赋予默认值
 * 返回一个函数,并将参数转为数组。
 * 函数内,拿到当前的时间戳
 * 判断,是否小于间隔时间:
 * 小于:则清楚定时器,然后重新生成定时器。定时器内直接赋值,然后call函数,
 * 大于:直接赋值,然后call函数,
 */

使用:在 onScorll 中使用加强版 throttle

// 用throttle来包装scroll的回调
const better_scroll = throttle(() => console.log("触发了滚动事件"), 1000);
document.addEventListener("scroll", better_scroll);
查看原文

赞 3 收藏 3 评论 0

九旬 发布了文章 · 4月13日

首屏优化之懒加载

首屏优化之懒加载

懒加载(Lazy-Load)。它是针对图片加载时机的优化:在一些图片量比较大的网站(比如电商网站首页,或者团购网站、小游戏首页等),如果我们尝试在用户打开页面的时候,就把所有的图片资源加载完毕,那么很可能会造成白屏、卡顿等现象,因为图片真的太多了,一口气处理这么多任务,浏览器做不到啊!

目的

懒加载的目的是当页面的图片进入到用户的可视范围之内在加载图片的一种优化方式。

可以增加首屏加载的速度,毕竟,用户点开页面的瞬间,呈现给他的只是首屏,我们只要把首屏的资源图片加载处理就可以了,至于下面的图片,当用户下滑当当前位置的时候,在加载出来也是没问题的,对于性能压力也小了,用户体验也没有变差。

原理

在页面初始化的时候,
<img>图片的src实际上是放在data-src属性上的,当元素处于可视范围内的时候,就把data-src赋值给src属性,完成图片加载。

// 在一开始加载的时候
<img data-data-original="http://xx.com/xx.png" data-original="" />

// 在进入可视范围内时
<img data-data-original="http://xx.com/xx.png" data-original="http://xx.com/xx.png" />

使用背景图来实现,原理也是一样的,把background-image放在,在可视范围时,就把data-src赋值给src属性,完成图片加载。

// 在一开始加载的时候
<div
  data-data-original="http://xx.com/xx.png"
  style="background-image: none;background-size: cover;"
></div>

// 在进入可视范围内时
<div
  data-data-original="http://xx.com/xx.png"
  style="background-image: url(http://xx.com/xx.png);background-size: cover;"
></div>

实现一个懒加载

基于上面的实现思路,自己实现一个懒加载。

新建一个 index.html 中,为这些图片预置 img 标签:

<head>
  <style>
    .img {
      width: 200px;
      height: 200px;
      background-color: gray;
      margin-bottom: 20px;
    }

    .pic {
      width: 100%;
      height: 100%;
    }
  </style>
</head>
<!-- 图片来自网络,侵删。 -->

<body>
  <div class="container">
    <div class="img">
      <!-- 注意我们并没有为它引入真实的src -->
      <img
        class="pic"
        alt="加载中"
        data-data-original="https://tse1-mm.cn.bing.net/th/id/OIP.8OrEFn_rKe82kqAWFjTuMwHaEo?pid=Api&rs=1"
      />
    </div>
    <div class="img">
      <img
        class="pic"
        alt="加载中"
        data-data-original="https://ssl.tzoo-img.com/images/tzoo.94911.0.910013.seoul-nami.jpg?width=1080"
      />
    </div>
    <div class="img">
      <img
        class="pic"
        alt="加载中"
        data-data-original="https://tse4-mm.cn.bing.net/th/id/OIP.ZitgAuABnwkrGn4lid2ZmQHaEK?pid=Api&rs=1"
      />
    </div>
    <div class="img">
      <img
        class="pic"
        alt="加载中"
        data-data-original="http://pic34.photophoto.cn/20150315/0034034862056002_b.jpg"
      />
    </div>
    <div class="img">
      <img
        class="pic"
        alt="加载中"
        data-data-original="http://img.mp.sohu.com/upload/20170724/32d4409f34194b029ed287abf1c99b70_th.png"
      />
    </div>
    <div class="img">
      <img
        class="pic"
        alt="加载中"
        data-data-original="https://pic6.wed114.cn/20180829/2018082910075991913520.jpg"
      />
    </div>
    <div class="img">
      <img
        class="pic"
        alt="加载中"
        data-data-original="https://tse4-mm.cn.bing.net/th/id/OIP.PZdPKj3sXEX2jLrepx3MUwHaEo?pid=Api&rs=1"
      />
    </div>
    <div class="img">
      <img
        class="pic"
        alt="加载中"
        data-data-original="https://pic6.wed114.cn/20180829/2018082910075831439349.jpg"
      />
    </div>
    <div class="img">
      <img
        class="pic"
        alt="加载中"
        data-data-original="https://pic6.wed114.cn/20180829/2018082910075468043336.jpg"
      />
    </div>
    <div class="img">
      <img
        class="pic"
        alt="加载中"
        data-data-original="https://tse2-mm.cn.bing.net/th/id/OIP.CRYz5Bv4vylsMh83G4CsLgHaFj?pid=Api&rs=1"
      />
    </div>
  </div>
</body>

在懒加载的实现中,有两个关键的数值:一个是当前可视区域的高度,另一个是元素距离可视区域顶部的高度

当前可视区域的高度,在现代浏览器及 IE9 以上的浏览器中,可以使用window.innerHeight属性获取,在低版本的 IE 中使用document.documentElment.clientHeight 获取,这里我们兼容两种情况:

const viewHeight = window.innerHeight || document.documentElement.clientHeight;

元素距离可视区域顶部的高度,这里我们用 getBoundingClientRect()方法来获取返回元素的大小和相对于尺寸的位置,对于该 API,MDN 的解释是:

Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。

返回的属性中有一个相对于可视区域顶部的高度也就是top属性,刚好就是我们需要的元素距离顶部的距离。

这样,两个属性就都得到了。

我们利用当前可视区域的高度大于等于元素距离可视区域顶部的高度就可以确定,该元素是否已经进入到了可视范围:

<script>
  // 获取所有的图片标签
  const imgs = document.getElementsByTagName("img");
  // 获取可视区域的高度
  const viewHeight =
    window.innerHeight || document.documentElement.clientHeight;
  // num用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
  let num = 0;

  function lazyload() {
    console.log("滚动...");
    for (let i = num; i < imgs.length; i++) {
      // 用可视区域高度减去元素顶部距离可视区域顶部的高度
      let distance = viewHeight - imgs[i].getBoundingClientRect().top;
      // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
      if (distance >= 0) {
        // 给元素写入真实的src,展示图片
        imgs[i].src = imgs[i].getAttribute("data-src");
        // 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
        num = i + 1;
      }
    }
  }

  // 防抖函数
  function debounce(fn, delay = 500) {
    let timer = null;
    return function (...args) {
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => {
        fn.call(this, args);
      }, delay);
    };
  }

  // 是的页面初始化是加载首屏图片
  window.onload = lazyload;
  // 监听Scroll事件,为了防止频繁调用,使用防抖函数优化一下
  window.addEventListener("scroll", debounce(lazyload, 600), false);
</script>

图片懒加载-线上预览

小结

  • 先收集到页面中所有的img(也可以用class)。
  • 获取到视图高度,计算显示的img,避免重复赋值src
  • 当滑动向下滑动鼠标,会触发onScroll事件(防抖),然后触发计算是否赋值src

参考资料

查看原文

赞 5 收藏 2 评论 0

九旬 发布了文章 · 4月13日

使用Documentfragment优化DOM操作

DocumentFragment

DocumentFragment 是什么 ?

DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的 document 一样,存储由节点(nodes)组成的文档结构。与 document 相比,最大的区别是 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。
———— MDN

使用场景

如果要在一个 ul 节点中一次性插入 10000 个 li 元素怎么办?
<ul id="parent">
  ...
</ul>

方法一

你可能第一个想到的方法,也是最简单的方式:

var parent = document.getElementById("parent");
for (var i = 0; i < 10000; i++) {
  var child = document.createElement("li");
  var text = document.createTextNode("" + i);
  child.appendChild(text);
  parent.appendChild(child);
}

不过众所周知的原因,对 DOM 反复操作会导致页面重绘、回流,效率非常低,而且页面可能会被卡死。

方法二

当然,更多能想到的方式应该是,先创造一个 div 节点,在内存中直接操作节点,然后把所有节点都凑在一起之后再跟 DOM 树进行交互,把所有节点都串在一个 div 上,然后再把 div 挂到 DOM 树上:

var parent = document.getElementById("parent");
var div = document.createElement("div");
for (var i = 0; i < 10000; i++) {
  var child = document.createElement("li");
  var text = document.createTextNode("" + i);
  child.appendChild(text);
  div.appendChild(child);
}
parent.appendChild(div);

只跟 DOM 树交互一次,性能方面肯定是大有改善的,不过额外插入了一个 div,如果说不是跟 div 之类的节点进行交互呢,比如在 table 中插入 thtd

方法三

这个时候,就到了 DocumentFragment 上场了,翻译过来就是文档片段的意思。

简单来说就是在内存中提供了一个 DOM 对象,当我们需要频繁操作 DOM 的时候,可以在内存先中创建一个 DocumentFragment 文档片段,然后对这个文档片段中进行一系列频繁的 DOM 操作,当操作结束后直接插入真实的 DOM 节点中,这样所有的节点会被一次插入到真实的文档中,而这个操作仅发生一个重绘的操作。

可以当做是上面一个例子的不需要 div 中转版本,插入的时候,直接用其子元素替换其本身,非常完美。

var parent = document.getElementById("parent");
var frag = document.createDocumentFragment();
for (var i = 0; i < 10000; i++) {
  var child = document.createElement("li");
  var text = document.createTextNode("" + i);
  child.appendChild(text);
  frag.appendChild(child);
}
parent.appendChild(frag);

总结

当应对于频繁的DOM操作的场景,可以使用DocumentFragment

查看原文

赞 2 收藏 1 评论 0

九旬 发布了文章 · 4月13日

深入理解浏览器缓存机制

浏览器缓存机制

前端缓存分为网络(HTTP)缓存和浏览器本地储存。

http-cache.jpg

HTTP 缓存机制

请移步:网络协议-HTTP-缓存缓存机制

浏览器本地储存

我们先来通过表格学习下这几种存储方式的区别

特性CookielocalStoragesessionStorageindexedDB
数据声明周期一般由服务器生成,可以设置过期时间除非被清理,否在一直存在页面关闭就清理除非被清理,否在一直存在
数据储存大小4k5M 左右5M 左右理论无限
与服务端通信请求时会携带在 Http 的 header 中,对于请求性能稍有影响不参与不参与不参与

<!-- | 用途 | 权限验证等 -->

Cookie

主要用于存储一下用户相关的信息,如登录、权限、token 等,但是不宜过大,因为每次 http 请求都会带上,所以会稍微影响性能。
对于 cookie 来说,还需要注意一些安全性。

| 属性 | 作用 |
| value | 如何用于保护用户登录态,应该将值加密 |
| http-only | 不能通过 JS 访问 Cookie,减少 XSS 攻击 |
| secure | 只能在协议为 HTTPS 的请求中携带 |
| same-site | 规定浏览器不能再跨域请求中携带 Cookie,减少 CSRF 攻击 |

Cookie 的本职工作并非是本地存储,而是“维持状态”。
因为 HTTP 是一种无状态的协议,也就是说,客户端请求一次,服务端就响应一次,中间没有留下任何信息。
就像一个经常和你聊天的朋友,天南地北的都什么都聊,可每次你都不知道你们上一次聊得是什么,以及他叫什么名字。
那怎么办才能让他知道我是我呢?
这时候就需要 Cookie 了,Cookie 说白了就是一个存储在浏览器里的一个小小的文本文件,它附着在 HTTP 请求上,在浏览器和服务器之间“飞来飞去”。
它可以携带用户信息,当服务器检查 Cookie 的时候,便可以获取到客户端的状态,也就可以证明我是谁了。
Cookie 是以键值对的形式存储的。

优点

  • 后端设置
  • 解决鉴权问题

缺点

  • 只有 4m,太小
  • 过量的 Cookie 会带来巨大的性能浪费
  • 不能跨域

Web Storage

localStorage

  • 本地永久储存,除非手动清除,否在一直存在
  • 大小:5M 左右
  • 用于储存稳定的资源:如 CSS、js、小图等。

sessionStorage

  • 页面回话存储,关闭页面自动清除。
  • 大小:5M 左右
  • 用于临时的数据:如 token、个人信息、权限、购物车信息等

需要注意的是localStorage和sessionStorage都是遵循同源策略的:

    • localStorage只要在相同的协议、相同的主机名(二级域名也不行)、相同的端口下,就能读取/修改到同一份localStorage数据。

      • sessionStorage比localStorage更严苛一点,除了协议、主机名(二级域名也不行)、端口外,还要求在同一窗口(也就是浏览器的标签页)下。

使用

  • 存储数据:setItem()
    localStorage.setItem('user_name', 'xiuyan')
  • 读取数据: getItem()
    localStorage.getItem('user_name')
  • 删除某一键名对应的数据: removeItem()
    localStorage.removeItem('user_name')
  • 清空数据记录:clear()
    localStorage.clear()

indexedDB

IndexedDB 是一个运行在浏览器上的非关系型数据库。既然是数据库了,那就不是 5M、10M 这样小打小闹级别了。

理论上来说,IndexedDB 是没有存储上限的(一般来说不会小于 250M)。它不仅可以存储字符串,还可以存储二进制数据。

本人用的也不是很多,具体用法可以参考:浏览器数据库 IndexedDB 入门教程

PWA

PWA(Progressive web apps,渐进式 Web 应用)运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序。

这些应用无处不在、功能丰富,使其具有与原生应用相同的用户体验优势。 这组文档和指南告诉您有关 PWA 的所有信息。

其实我的理解,就是在浏览器或者其他客户端应用缓存一个 webapp,一次使用,就将代码都缓存到本地,再次打开无需重复加载。

是不是觉得很熟悉,这不就是微信小程序吗?

其实现在的微信小程序、快应用都算是一种 PWA 的实现。

比如:vuePress 就支持转 PWA 应用

参考:MDN-PWA

查看原文

赞 1 收藏 2 评论 0

九旬 关注了用户 · 4月12日

思否小姐姐 @sfxiaojiejie

大家好呀!这里是思否小姐姐的主页。
大家在社区玩耍时遇到的任何问题都可以来咨询小姐姐哦!
欢迎大家➕小姐姐的微信:mgr_segmentfault

关注 43

九旬 发布了文章 · 4月12日

使用JavaScript学习设计模式

前言

去年的时候先是看了修言大佬的性能优化掘金小册子,收获良多。

之后紧接着买了这本JavaScript 设计模式核⼼原理与应⽤实践,刚好最近有小册免费学的活动,就赶紧把这篇笔记整理出来了,并且补充了小册子中的没有写到的其余设计模式,学习过程中结合 JavaScript 编写的例子,以便于理解和加深印象。

与其说是一篇文章,其实更像是一篇总结性质的学习笔记。

为什么要学习设计模式?

学习之前,先了解什么是设计模式?

设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。

简答理解 它是一套被反复使用、多人知晓的、经过分类的、代码设计经验总结。

烹饪有菜谱,游戏有攻略,每个领域都存在一些能够让我们又好又快地达成目标的“套路”。在程序世界,编程的“套路”就是设计模式。

学习它也就是学习这个编程世界的套路,对以后升级打怪打装备有很大的帮助。在瞬息万变的前端领域,设计模式也是一种“一次学习,终生受用”知识。

设计模式的原则

描述一个不断发生的重复的问题,以及该问题的解决方案的核心。
这样,你就能一次又一次的使用该方案而不必做重复劳动。

一大法则:

  • 迪米特法则:又叫最少知识法则,一个软件实体应该尽可能少的语其他实体发生相互作用,每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

五大原则:

  • 单一职责原则:一个类,应该仅有一个引起它变化的原因,简而言之,就是功能要单一。
  • 开放封闭原则:对扩展开放,对修改关闭。
  • 里氏替换原则:基类出现的地方,子类一定出现。
  • 接口隔离原则:一个借口应该是一种角色,不该干的事情不敢,该干的都要干。简而言之就是降低耦合、减低依赖。
  • 依赖翻转原则:针对接口编程,依赖抽象而不依赖具体。

JavaScript 中常用的是单一功能和开放封闭原则。

高内聚和低耦合

通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性好。我们使用设计模式最终的目的是实现代码的 高内聚 和 低耦合。

举例一个现实生活中的例子,例如一个公司,一般都是各个部门各司其职,互不干涉。各个部门需要沟通时通过专门的负责人进行对接。

在软件里面也是一样的 一个功能模块只是关注一个功能,一个模块最好只实现一个功能,这个是所谓的内聚

模块与模块之间、系统与系统之间的交互,是不可避免的, 但是我们要尽量减少由于交互引起的单个模块无法独立使用或者无法移植的情况发生, 尽可能多的单独提供接口用于对外操作, 这个就是所谓的低耦合

封装变化

在实际开发过程中,不发生变化的代码基本是不存在的,所以我要将代码的变化最小化。

设计模式的核心就是去观察你整个逻辑里的变与不变,然后将不变分离,达到使变化的部分灵活、不变的地方稳定的目的。

设计模式的种类

常用的可以分为创建型、结构型、行为型三类,一共 23 种模式。

创建型:

结构型:

行为型:

创建型

工厂模式

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
在 JS 中其实就是借助构造函数实现。

例子

某个班级要做一个录入系统,录入一个人,就要写一次。

let liMing = {
  name: "李明",
  age: 20,
  sex: "男",
};

如果多个录入,则可以创建一个类。

class Student {
  constructor(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
  }
}
let zhangSan = new Student("张三", 19, "男");

工厂模式是将创建对象的过程单独封装,使用使只需要无脑传参就行了,就像一个工厂一样,只要给够原料,就可以轻易的制造出成品。

小结

  • 构造函数和创建者分离,对 new 操作进行封装
  • 符合开放封闭原则

单例模式

单例模式的定义:保证一个类仅有一个实例,并且提供一个访问它的全局变量。

实现的方法为前判断实例是否存在,如果存在直接返回,不存在则创建在返回,这就确保了一个类只有一个实例对象。

比如:Vuex、jQuery

例子

使用场景:一个单一对象,比如:弹窗,无论点击多少次,弹窗只应被创建一次,实现起来也很简单,用一个变量缓存就行了。

【点击查看Demo】:单例模式-在线例子

如上面这个弹框,只有在第一次点击按钮时才会创建弹框,之后都不会在创建,而是使用之前创建的弹框。

如此,便是实现了一个应用于单例模式的弹框。

小结

  • 维持一个实例,如果已经创建,就直接返回
  • 符合开放封闭原则

原型模式

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

例子

在 JavaScript 中,实现原型模式是在 ECMAscript5 中,提出的 Object.create 方法,使用现有的对象来提供创建的对象__proto__

var prototype = {
  name: "Jack",
  getName: function() {
    return this.name;
  },
};

var obj = Object.create(prototype, {
  job: {
    value: "IT",
  },
});

console.log(obj.getName()); // Jack
console.log(obj.job); // IT
console.log(obj.__proto__ === prototype); //true

有原型就有原理性了

构造器模式

在面向对象的编程语言中,构造器是一个类中用来初始化新对象的特殊方法。并且可以接受参数用来设定实例对象的属性的方法
function Car(model, year, miles) {
  this.model = model;
  this.year = year;
  this.miles = miles;
  // this.info = new CarDetail(model)
  // 属性也可以通过 new 的方式产生
}

// 覆盖原型对象上的toString
Car.prototype.toString = function() {
  return this.model + " has done " + this.miles + " miles";
};

// 使用:
var civic = new Car("Honda Civic", 2009, 20000);
var mondeo = new Car("Ford Mondeo", 2010, 5000);
console.log(civic.toString()); // Honda Civic has done 20000 miles
console.log(mondeo.toString()); // Ford Mondeo has done 5000 miles

其实就是利用原型链上被继承的特性,实现了构造器。

抽象工厂模式

抽象工厂模式(Abstract Factory)就是通过类的抽象使得业务适用于一个产品类簇的创建,而不负责某一类产品的实例。

JS 中是没有直接的抽象类的,abstract 是个保留字,但是还没有实现,因此我们需要在类的方法中抛出错误来模拟抽象类,如果继承的子类中没有覆写该方法而调用,就会抛出错误。

const Car = function() {};
Car.prototype.getPrice = function() {
  return new Error("抽象方法不能调用");
};

面向对象的语言里有抽象工厂模式,首先声明一个抽象类作为父类,以概括某一类产品所需要的特征,继承该父类的子类需要实现父类中声明的方法而实现父类中所声明的功能:

/**
 * 实现subType类对工厂类中的superType类型的抽象类的继承
 * @param subType 要继承的类
 * @param superType 工厂类中的抽象类type
 */
const VehicleFactory = function(subType, superType) {
  if (typeof VehicleFactory[superType] === "function") {
    function F() {
      this.type = "车辆";
    }
    F.prototype = new VehicleFactory[superType]();
    subType.constructor = subType;
    subType.prototype = new F(); // 因为子类subType不仅需要继承superType对应的类的原型方法,还要继承其对象属性
  } else throw new Error("不存在该抽象类");
};
VehicleFactory.Car = function() {
  this.type = "car";
};
VehicleFactory.Car.prototype = {
  getPrice: function() {
    return new Error("抽象方法不可使用");
  },
  getSpeed: function() {
    return new Error("抽象方法不可使用");
  },
};
const BMW = function(price, speed) {
  this.price = price;
  this.speed = speed;
};
VehicleFactory(BMW, "Car"); // 继承Car抽象类
BMW.prototype.getPrice = function() {
  // 覆写getPrice方法
  console.log(`BWM price is ${this.price}`);
};
BMW.prototype.getSpeed = function() {
  console.log(`BWM speed is ${this.speed}`);
};
const baomai5 = new BMW(30, 99);
baomai5.getPrice(); // BWM price is 30
baomai5 instanceof VehicleFactory.Car; // true

通过抽象工厂,就可以创建某个类簇的产品,并且也可以通过 instanceof 来检查产品的类别,也具备该类簇所必备的方法。

结构型

装饰器模式

装饰器模式,又名装饰者模式。它的定义是“ 在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求 ”。

装饰器案例

有一个弹窗函数,点击按钮后会弹出一个弹框。

function openModal() {
  let div = document.craeteElement("div");
  div.id = "modal";
  div.innerHTML = "提示";
  div.style.backgroundColor = "gray";
  document.body.appendChlid(div);
}
btn.onclick = () => {
  openModal();
};

但是忽然产品经理要改需求,要把提示文字由“提示”改为“警告”,背景颜色由 gray 改为 red。

听到这个你是不是立马就想直接改动源函数:

function openModal() {
  let div = document.craeteElement("div");
  div.id = "modal";
  div.innerHTML = "警告";
  div.style.backgroundColor = "red";
  document.body.appendChlid(div);
}

但是如果是复杂的业务逻辑,或者这个代码时上任代码留下来的产物,在考虑到以后的需求变化,每次都这样修改确实很麻烦。

而且,直接修改已有的函数体,有违背了我们的“开放封闭原则”,往一个函数塞这么多的逻辑,也违背了“单一职责原则”,所以上面的方法并不是最佳的。

最省时省力的方式是不去关心它现有得了逻辑,只在此逻辑之上扩展新的功能即可,因此装饰器模式就此而生。

// 新逻辑
function changeModal() {
  let div = document.getElemnetById("modal");
  div.innerHTML = "告警";
  div.style.backgroundColor = "red";
}
btn.onclick = () => {
  openModal();
  changeModal();
};

这种通过函数添加新的功能、而又不修改旧逻辑,这就是装饰器的魅力。

ES7 中的装饰器

在最新的 ES7 中有装饰器的提案,但是还未定案,所以语法可能不是最终版,但是思想是一样的。

  1. 装饰类的属性
@tableColor
class Table {
  // ...
}
function tableColor(target) {
  target.color = "red";
}
Table.color; // true

Table这个类,添加一个tableColor的装饰器,即可改变Tablecolor属性

  1. 装饰类的方法
class Person {
  @readonly
  name() {
    return `${this.first} ${this.last}`;
  }
}

Person类的name方法添加只读的装饰器,使得该方法不可被修改。

其实是借助Object.definePropertywirteable特性实现的。

  1. 装饰函数

    因为 JS 中函数存在函数提升,直接使用装饰器并不可取,但是可以使用高级函数的方式实现。

    function doSomething(name) {
      console.log("Hello, " + name);
    }
    function loggingDecorator(wrapped) {
      return function() {
        console.log("fun-Starting");
        const result = wrapped.apply(this, arguments);
        console.log("fun-Finished");
        return result;
      };
    }
    const wrapped = loggingDecorator(doSomething);
    let name = "World";
    
    doSomething(name); // 装饰前
    // output:
    // Hello, World
    
    wrapped(name); // 装饰后
    // output:
    // fun-Starting
    // Hello, World
    // fun-Finished

    上面的装饰器,是给一个函数在执行开始和执行结束分别打印一个 log。

参考

适配器模式

适配器模式的作用是解决两个软件实体间的接口不兼容问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。

简单来说,就是把一个类的接口变成客户端期待的另一种接口,解决兼容问题

比如:axios

例子:一个渲染地图的方法,默认是调用当前地图对象的 show 方法进行渲染操作,当有多个地图,而每个地图的渲染方法都不一样时,为了方便使用者调用,就需要做适配了。

let googleMap = {
  show: () => {
    console.log("开始渲染谷歌地图");
  },
};
let baiduMap = {
  display: () => {
    console.log("开始渲染百度地图");
  },
};
let baiduMapAdapter = {
  show: () => {
    return baiduMap.display();
  },
};
function renderMap(obj) {
  obj.show();
}
renderMap(googleMap); // 开始渲染谷歌地图
renderMap(baiduMapAdapter); // 开始渲染百度地图

这其中对“百度地图”做了适配的处理。

小结

  • 适配器模式主要解决两个接口之间不匹配的问题,不会改变原有的接口,而是由一个对象对另一个对象的包装
  • 适配器模式符合开放封闭原则
  • 把变化留给自己,把统一留给用户。

代理模式

代理模式——在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵桥搭线从而间接达到访问目的,这样的模式就是代理模式。

提起代理(Proxy),对于前端很熟悉的,我能联想到一系列的东西,比如:

  • ES6 新增的 proxy 属性
  • 为了解决跨域问题而经常使用的 webpack 的 proxy 配置和 Nginx 代理
  • 还有“学会上网”所使用的的代理。
  • 等等

事件代理

常见的列表、表格都需要单独处理事件时,使用父级元素事件代理,可以极大的减少代码量。

<div id="father">
  <span id="1">新闻1</span>
  <span id="2">新闻2</span>
  <span id="3">新闻3</span>
  <span id="4">新闻4</span>
  <span id="5">新闻5</span>
  <span id="6">新闻6</span>
  <!-- 7、8... -->
</div>

如上代码,我想点击每个新闻,都可以拿到当前新闻的id,从而进行下一步操作。

如果给每一个span都绑定一个onclick事件,就太耗费性能了,而且写起来也很麻烦。

我们常见的做法是利用事件冒泡的原理,将事件带代理到父元素上,然后统一处理。

let father = document.getElementById("father");
father.addEventListener("click", (evnet) => {
  if (event.target.nodeName === "SPAN") {
    event.preventDefault();
    let id = event.target.id;
    console.log(id); // 拿到id,进行下一步操作
  }
});

虚拟代理

例如:某个花销很大的操作,可以通过虚拟代理的方式延迟到这种需要它的时候才去创建(例如:使用虚拟代理实现图片懒加载)

图片预加载:先通过一张 loading 图占位,然后通过异步的方式加载图片,等图片加载完成之后在使用原图替换 loading 图。

问什么要使用预加载+懒加载?以淘宝举例,商城物品图片多之又多,一次全部请求过来这么多图片无论是对 js 引擎还是浏览器本身都是一个巨大的工作量,会拖慢浏览器响应速度,用户体验极差,而预加载+懒加载的方式会大大节省浏览器请求速度,通过预加载率先加载占位图片(第二次及以后都是缓存中读取),再通过懒加载直到要加载的真实图片加载完成,瞬间替换。这种模式很好的解决了图片一点点展现在页面上用户体验差的弊端。

须知:图片第一次设置 src,浏览器发送网络请求;如果设置一个请求过的 src 那么浏览器则会从缓存中读取 from disk cache

class PreLoadImage {
  constructor(imgNode) {
    // 获取真实的DOM节点
    this.imgNode = imgNode;
  }

  // 操作img节点的src属性
  setSrc(imgUrl) {
    this.imgNode.src = imgUrl;
  }
}

class ProxyImage {
  // 占位图的url地址
  static LOADING_URL = "xxxxxx";

  constructor(targetImage) {
    // 目标Image,即PreLoadImage实例
    this.targetImage = targetImage;
  }

  // 该方法主要操作虚拟Image,完成加载
  setSrc(targetUrl) {
    // 真实img节点初始化时展示的是一个占位图
    this.targetImage.setSrc(ProxyImage.LOADING_URL);
    // 创建一个帮我们加载图片的虚拟Image实例
    const virtualImage = new Image();
    // 监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
    virtualImage.onload = () => {
      this.targetImage.setSrc(targetUrl);
    };
    // 设置src属性,虚拟Image实例开始加载图片
    virtualImage.src = targetUrl;
  }
}

ProxyImage 帮我们调度了预加载相关的工作,我们可以通过 ProxyImage 这个代理,实现对真实 img 节点的间接访问,并得到我们想要的效果。

在这个实例中,virtualImage 这个对象是一个“幕后英雄”,它始终存在于 JavaScript 世界中、代替真实 DOM 发起了图片加载请求、完成了图片加载工作,却从未在渲染层面抛头露面。因此这种模式被称为“虚拟代理”模式。

【点击查看Demo】:虚拟代理-在线例子

缓存代理

缓存代理比较好理解,它应用于一些计算量较大的场景里。在这种场景下,我们需要“用空间换时间”——当我们需要用到某个已经计算过的值的时候,不想再耗时进行二次计算,而是希望能从内存里去取出现成的计算结果。

这种场景下,就需要一个代理来帮我们在进行计算的同时,进行计算结果的缓存了。

例子:对参数求和函数进行缓存代理。

// addAll方法会对你传入的所有参数做求和操作
const addAll = function() {
  console.log("进行了一次新计算");
  let result = 0;
  const len = arguments.length;
  for (let i = 0; i < len; i++) {
    result += arguments[i];
  }
  return result;
};

// 为求和方法创建代理
const proxyAddAll = (function() {
  // 求和结果的缓存池
  const resultCache = {};
  return function() {
    // 将入参转化为一个唯一的入参字符串
    const args = Array.prototype.join.call(arguments, ",");

    // 检查本次入参是否有对应的计算结果
    if (args in resultCache) {
      // 如果有,则返回缓存池里现成的结果
      console.log("无计算-使用缓存的数据");
      return resultCache[args];
    }
    return (resultCache[args] = addAll(...arguments));
  };
})();

let sum1 = proxyAddAll(1, 2, 3); // 进行了一次新计算

let sum2 = proxyAddAll(1, 2, 3); // 无计算-使用缓存的数据

第一次进行计算返回结果,并存入缓存。如果再次传入相同的参数,则不计算,直接返回缓存中存在的结果。

在常见在 HTTP 缓存中,浏览器就相当于进行了一层代理缓存,通过 HTTP 的缓存机制控制(强缓存和协商缓存)判断是否启用缓存。

频繁却变化小的的网络请求,比如getUserInfo,可以使用代理请求,设置统一发送和存取。

小结

  • 代理模式符合开放封闭原则。
  • 本体对象和代理对象拥有相同的方法,在用户看来并不知道请求的是本体对象还是代理对象。

桥接模式

桥接模式:将抽象部分和具体实现部分分离,两者可独立变化,也可以一起工作。

在这种模式的实现上,需要一个对象担任“桥”的角色,起到连接的作用。

例子:

JavaScript 中桥接模式的典型应用是:Array对象上的forEach函数。

此函数负责循环遍历数组每个元素,是抽象部分; 而回调函数callback就是具体实现部分

下方是模拟forEach方法:

const forEach = (arr, callback) => {
  if (!Array.isArray(arr)) return;

  const length = arr.length;
  for (let i = 0; i < length; ++i) {
    callback(arr[i], i);
  }
};

// 以下是测试代码
let arr = ["a", "b"];
forEach(arr, (el, index) => console.log("元素是", el, "位于", index));
// 元素是 a 位于 0
// 元素是 b 位于 1

外观模式

外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。

这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。

例子

外观模式即执行一个方法可以让多个方法一起被调用。

涉及到兼容性,参数支持多个格式、环境等等.. 对外暴露统一的 api

比如自己封装的事件对象包含了阻止冒泡和添加事件监听的兼容方法:

const myEvent = {
    stop (e){
        if(typeof e.preventDefault() == 'function'){
            e.preventDefault();
        }
        if(typeof e.stopPropagation() == 'function'){
            e.stopPropagation()
        }
        // IE
        if(typeOd e.retrunValue === 'boolean'){
            e.returnValue = false
        }
        if(typeOd e.cancelBubble === 'boolean'){
            e.returnValue = true
        }
    }
    addEvnet(dom, type, fn){
        if(dom.addEventListener){
            dom.addEventlistener(type, fn, false);
        }else if(dom.attachEvent){
            dom.attachEvent('on'+type, fn)
        }else{
            dom['on'+type] = fn
        }
    }
}

组合模式

组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。

组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。

例子

想象我们现在手上有多个万能遥控器,当我们回到家中,按一下开关,下列事情将被执行

  • 开门
  • 开电脑
  • 开音乐
// 先准备一些需要批量执行的功能
class GoHome {
  init() {
    console.log("开门");
  }
}
class OpenComputer {
  init() {
    console.log("开电脑");
  }
}
class OpenMusic {
  init() {
    console.log("开音乐");
  }
}

// 组合器,用来组合功能
class Comb {
  constructor() {
    // 准备容器,用来防止将来组合起来的功能
    this.skills = [];
  }
  // 用来组合的功能,接收要组合的对象
  add(task) {
    // 向容器中填入,将来准备批量使用的对象
    this.skills.push(task);
  }
  // 用来批量执行的功能
  action() {
    // 拿到容器中所有的对象,才能批量执行
    this.skills.forEach((val) => {
      val.init();
    });
  }
}

// 创建一个组合器
let c = new Comb();

// 提前将,将来要批量操作的对象,组合起来
c.add(new GoHome()); // 添加'开门'命令
c.add(new OpenComputer()); // 添加'开电脑'命令
c.add(new OpenMusic()); // 添加'开音乐'命令

c.action(); // 执行添加的所有命令

小结

  • 组合模式在对象间形成树形结构
  • 组合模式中对基本对象和组合对象被一致对待
  • 无需关心对象有多少层,调用时只需要在根部进行调用
  • 将多个对象的功能,组装起来,实现批量执行

享元模式

享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。

这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。

特点

  • 共享内存(主要是考虑内存,而非效率)
  • 相同的数据(内存),共享使用

例子

比如常见的事件代理,通过将若干个子元素的事件代理到一个父元素,子元素共同使用一个方法。如果都绑定到<span>标签,对内存开销太大 。

<!-- 点击span,拿到当前的span中的内容 -->
<div id="box">
  <span>1</span>
  <span>2</span>
  <span>3</span>
  <span>4</span>
</div>

<script>
  var box = document.getElementById("box");
  box.addEventListener("click", function(e) {
    let target = e.target;
    if (e.nodeName === "SPAN") {
      alert(target.innerHTML);
    }
  });
</script>

小结

  • 将相同的部分抽象出来
  • 符合开放封闭的原则

行为型

迭代器模式

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露对象的对象的内部表示。

迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,及时不关心对象的内部构造,也可以按照顺序访问其中的每个元素。

简单类说,它的目的就是去遍历一个可遍历的对象。

像 JS 中原生的 forEach、map 等方法都属于是迭代器模式的一种实现,一般来说不用自己去实现迭代器。

在 JS 中有一种类数组的存在,他们没有迭代方法,比如 nodeList、arguments 并不能直接使用迭代方法,需要使用 jQuery 的 each 方法或者将类数组装换为真正的数组在进行迭代。

而在最新的 ES6 中,对有只要有 Iterator 接口的数据类型都可以使用 for..of..进行遍历,而他的底层则是对 next 方法的反复调用,具体参考阮一峰-Iterator 和 for...of 循环

例子

我们可以借助 Iterator 接口自己实现一个迭代器。

class Creater {
  constructor(list) {
    this.list = list;
  }
  // 创建一个迭代器,也叫遍历器
  createIterator() {
    return new Iterator(this);
  }
}
class Iterator {
  constructor(creater) {
    this.list = creater.list;
    this.index = 0;
  }
  // 判断是否遍历完数据
  isDone() {
    if (this.index >= this.list.length) {
      return true;
    }
    return false;
  }
  next() {
    return this.list[this.index++];
  }
}

var arr = [1, 2, 3, 4];
var creater = new Creater(arr);
var iterator = creater.createIterator();
console.log(iterator.list); // [1, 2, 3, 4]
while (!iterator.isDone()) {
  console.log(iterator.next());
  // 1
  // 2
  // 3
  // 4
}

小结

  1. JavaScript 中的有序数据集合有 Array,Map,Set,String,typeArray,arguments,NodeList,不包括 Object
  2. 任何部署了[Symbol.iterator]接口的数据都可以使用 for...of 循环遍历
  3. 迭代器模式使目标对象和迭代器对象分离,符合开放封闭原则

订阅/发布模式(观察者)

发布/订阅模式又叫观察者模式,她定义对象间的一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖他的对象都将得到通知。在 JavaScrtipt 中,我们一般使用时间模型来替代传统的发布/订阅模式。

比如:Vue 中的双向绑定和事件机制。

发布/订阅模式和观察者模式的区别

  • 发布者可以直接处接到订阅的操作,叫观察者模式
  • 发布者不直接触及到订阅者,而是由统一的第三方完成通信操作,叫发布/订阅模式

    发布订阅模式和观察者模式.png

例子

可以自己实现一个事件总线,模拟$emit$on

class EventBus {
  constructor() {
    this.callbacks = {};
  }
  $on(name, fn) {
    (this.callbacks[name] || (this.callbacks[name] = [])).push(fn);
  }
  $emit(name, args) {
    let cbs = this.callbacks[name];
    if (cbs) {
      cbs.forEach((c) => {
        c.call(this, args);
      });
    }
  }
  $off(name) {
    this.callbacks[name] = null;
  }
}
let event = new EventBus();
event.$on("event1", (arg) => {
  console.log("event1", arg);
});

event.$on("event2", (arg) => {
  console.log("event2", arg);
});

event.$emit("event1", 1); // event1 1
event.$emit("event2", 2); // event2 2

策略模式

定义一系列的算法,把他们一个个封装起来,并使他们可以替换。

策略模式的目的就是将算法的使用和算法的实现分离开来。

一个策略模式通常由两部分组成:

  • 一组可变的策略类:封装了具体的算法,负责具体的计算过程
  • 一组不变的环境类:接收到请求后,随后将请求委托到某个策略类

说明环境类要维持对某个策略对象的引用。

例子

通过绩效等级计算奖金,可以轻易的写出如下的代码:

var calculateBonus = function(performanceLevel, salary) {
  if (performanceLevel === "S") {
    return salary * 4;
  }
  if (performanceLevel === "A") {
    return salary * 3;
  }
  if (performanceLevel === "B") {
    return salary * 2;
  }
};

calculateBonus("B", 20000); // 输出:40000
calculateBonus("S", 6000); // 输出:24000

使用策略模式修改代码:

var strategies = {
  S: (salary) => {
    return salary * 4;
  },
  A: (salary) => {
    return salary * 3;
  },
  B: (salary) => {
    return salary * 2;
  },
};
var calculateBonus = function(level, salary) {
  return strategies[level](salary);
};
console.log(calculateBonus("S", 200)); // 输出:800
console.log(calculateBonus("A", 200)); // 输出:600

状态模式

状态模式允许一个对象在其内部状态改变的时候改变

状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。

例子

实现一个交通灯的切换。

点击查看Demo:交通信号灯-在线例子

这时候如果在加一个蓝光的话,可以直接添加一个蓝光的类,然后添加 parssBtn 方法,其他状态都不需要变化。

小结

  • 通过定义不同的状态类,根据状态的改变而改变状态的行为,不必把大量的逻辑都写在被操作对象的类中,而且容易增加新的状态。
  • 符合开放封闭原则

解释器模式

解释器模式(Interpreter):给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

用到的比较少,可以参考两篇文章来理解。

小结

  • 描述语言语法如何定义,如何解释和编译
  • 用于专业场景

中介者模式

中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。

这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护

通过一个中介者对象,其他所有相关对象都通过该对象来通信,而不是相互引用,但其中一个对象发生改变时,只需要通知中介者对象即可。

通过中介者模式可以解除对象与对象之前的耦合关系。

例如:Vuex

middle-parttern.png

参考链接:JavaScript 中介者模式

小结

  • 将各关联对象通过中介者隔离
  • 符合开放封闭原则
  • 减少耦合

访问者模式

在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。

通过这种方式,元素的执行算法可以随着访问者改变而改变。

例子

通过访问者调用元素类的方法。

// 访问者
function Visitor() {
  this.visit = function(concreteElement) {
    concreteElement.doSomething(); // 谁访问,就使用谁的doSomething()
  };
}
// 元素类
function ConceteElement() {
  this.doSomething = function() {
    console.log("这是一个具体元素");
  };
  this.accept = function(visitor) {
    visitor.visit(this);
  };
}
// Client
var ele = new ConceteElement();
var v = new Visitor();
ele.accept(v); // 这是一个具体元素

小结

  • 假如一个对象中存在着一些与本对象不相干(或者关系较弱)的操作,为了避免这些操作污染这个对象,则可以使用访问者模式来把这些操作封装到访问者中去。
  • 假如一组对象中,存在着相似的操作,为了避免出现大量重复的代码,也可以将这些重复的操作封装到访问者中去。

备忘录模式

备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象

例子

实现一个带有保存记录功能的”编辑器“,功能包括

  • 随时记录一个对象的状态变化
  • 随时可以恢复之前的某个状态(如撤销功能)
// 状态备忘
class Memento {
  constructor(content) {
    this.content = content;
  }
  getContent() {
    return this.content;
  }
}

// 备忘列表
class CareTaker {
  constructor() {
    this.list = [];
  }
  add(memento) {
    this.list.push(memento);
  }
  get(index) {
    return this.list[index];
  }
}

// 编辑器
class Editor {
  constructor() {
    this.content = null;
  }
  setContent(content) {
    this.content = content;
  }
  getContent() {
    return this.content;
  }
  saveContentToMemento() {
    return new Memento(this.content);
  }
  getContentFromMemento(memento) {
    this.content = memento.getContent();
  }
}

// 测试代码
let editor = new Editor();
let careTaker = new CareTaker();

editor.setContent("111");
editor.setContent("222");
careTaker.add(editor.saveContentToMemento()); // 存储备忘录
editor.setContent("333");
careTaker.add(editor.saveContentToMemento()); // 存储备忘录
editor.setContent("444");

console.log(editor.getContent()); // 444
editor.getContentFromMemento(careTaker.get(1)); // 撤销
console.log(editor.getContent()); // 333
editor.getContentFromMemento(careTaker.get(0)); // 撤销
console.log(editor.getContent()); // 222

小结

  • 状态对象与使用者分开(解耦)
  • 符合开放封闭原则

模板方法模式

在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。

它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。

感觉用到的不是很多,想了解的可以点击下面的参考链接。

参考:JavaScript 设计模式之模板方法模式

职责链模式

顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。

这种模式给予请求的类型,对请求的发送者和接收者进行解耦。

在这种模式中,通常每个接收者都包含对另一个接收者的引用。

如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。

例子

公司的报销审批流程:组长=》项目经理=》财务总监

// 请假审批,需要组长审批、经理审批、最后总监审批
class Action {
  constructor(name) {
    this.name = name;
    this.nextAction = null;
  }
  setNextAction(action) {
    this.nextAction = action;
  }
  handle() {
    console.log(`${this.name} 审批`);
    if (this.nextAction != null) {
      this.nextAction.handle();
    }
  }
}

let a1 = new Action("组长");
let a2 = new Action("项目经理");
let a3 = new Action("财务总监");
a1.setNextAction(a2);
a2.setNextAction(a3);
a1.handle();
// 组长 审批
// 项目经理 审批
// 财务总监 审批

// 将一步操作分为多个职责来完成,一个接一个的执行,最终完成操作。

小结

  • 可以联想到 jQuery、Promise 这种链式操作
  • 发起者和处理者进行隔离
  • 符合开发封闭原则

命令模式

命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。

请求以命令的形式包裹在对象中,并传给调用对象。

调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。

例子

实现一个编辑器,有很多命令,比如:写入、读取等等。

class Editor {
  constructor() {
    this.content = "";
    this.operator = [];
  }
  write(content) {
    this.content += content;
  }
  read() {
    console.log(this.content);
  }
  space() {
    this.content += " ";
  }
  readOperator() {
    console.log(this.operator);
  }
  run(...args) {
    this.operator.push(args[0]);
    this[args[0]].apply(this, args.slice(1));
    return this;
  }
}

const editor = new Editor();

editor
  .run("write", "hello")
  .run("space")
  .run("write", "zkk!")
  .run("read"); // => 'hello zkk!'

// 输出操作队列
editor.readOperator(); // ["write", "space", "write", "read"]

小结

  • 降低耦合
  • 新的命令可以很容易的添加到系统中

综述

(1)面向对象最终的设计目标:

  • A 可扩展性:有了新的需求,新的性能可以容易添加到系统中,不影响现有的性能,也不会带来新的缺陷。
  • B 灵活性:添加新的功能代码修改平稳地发生,而不会影响到其它部分。
  • C 可替换性:可以将系统中的某些代码替换为相同接口的其它类,不会影响到系统。

(2)设计模式的好处:

  • A 设计模式使人们可以更加简单方便地复用成功的设计和体系结构。
  • B 设计模式也会使新系统开发者更加容易理解其设计思路。

(3)学习设计模式有三重境界(网上看到好多次):

  • 第一重: 你学习一个设计模式就在思考我刚做的项目中哪里能用到(手中有刀,心中无刀)
  • 第二重: 设计模式你都学完了,但是当遇到一个问题的时候,你发现有好几种设计模式供你选择,你无处下(手中有刀,心中也有刀)
  • 第三重:也是最后一重,你可能没有设计模式的概念了,心里只有几大设计原则,等用到的时候信手拈来(刀法的最高境界:手中无刀,心中也无刀)

结语

以下是摘抄自掘金小册-JavaScript 设计模式核⼼原理与应⽤实践的结语。

设计模式的征程,到此就告一段落了。但对各位来说,真正的战斗才刚刚开始。设计模式的魅力,不在纸面上,而在实践中。

学设计模式:

一在多读——读源码,读资料,读好书;

二在多练——把你学到的东西还原到业务开发里去,看看它是否 OK,有没有问题?如果有问题,如何修复、如何优化?没有一种设计模式是完美的,设计模式和人一样,处在动态发展的过程中,并不是只有 GOF 提出的 23 种设计模式可以称之为设计模式。

只要一种方案遵循了设计原则、解决了一类问题,那么它都可以被冠以“设计模式”的殊荣。

在各位从设计模式小册毕业之际,希望大家带走的不止是知识,还有好的学习习惯、阅读习惯。最重要的,是深挖理论知识的勇气和技术攻关的决心。这些东西不是所谓“科班”的专利,而是一个优秀工程师的必须。

参考

来自九旬的原创:博客原文链接
查看原文

赞 7 收藏 6 评论 0

九旬 赞了回答 · 4月1日

是使用js-cookie还是使用vuex?

vuex 不仅仅是拿来存数据的。它是与vue深度结合的组件状态集中管理模式。

除了你 思维中的 “存数据” 外,还包含对于数据的处理“mutation”,处理数据的事件“action”,衍生数据“getter”等等,是一整套的状态管理方案。

而你遇到的问题是如何将数据本地持久化,跟vuex根本不搭噶,你大可以用Storage,存下部分本地数据就好,然后在对应“action”中进行逻辑判断,是从Storage获取数据;还是异步获取数据;还是先从Storage获取再异步更新数据。

关注 4 回答 4

九旬 发布了文章 · 3月31日

Vue2.x 的双向绑定原理及实现

Vue 数据双向绑定原理

Vue 是利用的 Object.defineProperty() 方法进行的数据劫持,利用 set、get 来检测数据的读写。

https://jsrun.net/RMIKp/embed...

MVVM 框架主要包含两个方面,数据变化更新视图,视图变化更新数据。

视图变化更新数据,如果是像 input 这种标签,可以使用 oninput 事件..

数据变化更新视图可以使用 Object.definProperty() 的 set 方法可以检测数据变化,当数据改变就会触发这个函数,然后更新视图。

实现过程

我们知道了如何实现双向绑定了,首先要对数据进行劫持监听,所以我们需要设置一个 Observer 函数,用来监听所有属性的变化。

如果属性发生了变化,那就要告诉订阅者 watcher 看是否需要更新数据,如果订阅者有多个,则需要一个 Dep 来收集这些订阅者,然后在监听器 observer 和 watcher 之间进行统一管理。

还需要一个指令解析器 compile,对需要监听的节点和属性进行扫描和解析。

因此,流程大概是这样的:

  1. 实现一个监听器 Observer,用来劫持并监听所有属性,如果发生变动,则通知订阅者。
  2. 实现一个订阅者 Watcher,当接到属性变化的通知时,执行对应的函数,然后更新视图,使用 Dep 来收集这些 Watcher。
  3. 实现一个解析器 Compile,用于扫描和解析的节点的相关指令,并根据初始化模板以及初始化相应的订阅器。

仿Vue导图.png

显示一个 Observer

Observer 是一个数据监听器,核心方法是利用 Object.defineProperty() 通过递归的方式对所有属性都添加 setter、getter 方法进行监听。

var library = {
  book1: {
    name: "",
  },
  book2: "",
};
observe(library);
library.book1.name = "vue权威指南"; // 属性name已经被监听了,现在值为:“vue权威指南”
library.book2 = "没有此书籍"; // 属性book2已经被监听了,现在值为:“没有此书籍”

// 为数据添加检测
function defineReactive(data, key, val) {
  observe(val); // 递归遍历所有子属性
  let dep = new Dep(); // 新建一个dep
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      if (Dep.target) {
        // 判断是否需要添加订阅者,仅第一次需要添加,之后就不用了,详细看Watcher函数
        dep.addSub(Dep.target); // 添加一个订阅者
      }
      return val;
    },
    set: function(newVal) {
      if (val == newVal) return; // 如果值未发生改变就return
      val = newVal;
      console.log(
        "属性" + key + "已经被监听了,现在值为:“" + newVal.toString() + "”"
      );
      dep.notify(); // 如果数据发生变化,就通知所有的订阅者。
    },
  });
}

// 监听对象的所有属性
function observe(data) {
  if (!data || typeof data !== "object") {
    return; // 如果不是对象就return
  }
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key]);
  });
}
// Dep 负责收集订阅者,当属性发生变化时,触发更新函数。
function Dep() {
  this.subs = {};
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },
  notify: function() {
    this.subs.forEach((sub) => sub.update());
  },
};

思路分析中,需要有一个可以容纳订阅者消息订阅器 Dep,用于收集订阅者,在属性发生变化时执行对应的更新函数。

从代码上看,将订阅器 Dep 添加在 getter 里,是为了让 Watcher 初始化时触发,,因此,需要判断是否需要订阅者。

在 setter 中,如果有数据发生变化,则通知所有的订阅者,然后订阅者就会更新对应的函数。

到此为止,一个比较完整的 Observer 就完成了,接下来开始设计 Watcher.

实现 Watcher

订阅者 Watcher 需要在初始化的时候将自己添加到订阅器 Dep 中,我们已经知道监听器 Observer 是在 get 时执行的 Watcher 操作,所以只需要在 Watcher 初始化的时候触发对应的 get 函数去添加对应的订阅者操作即可。

那给如何触发 get 呢?因为我们已经设置了 Object.defineProperty(),所以只需要获取对应的属性值就可以触发了。

我们只需要在订阅者 Watcher 初始化的时候,在 Dep.target 上缓存下订阅者,添加成功之后在将其去掉就可以了。

function Watcher(vm, exp, cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.value = this.get(); // 将自己添加到订阅器的操作
}

Watcher.prototype = {
  update: function() {
    this.run();
  },
  run: function() {
    var value = this.vm.data[this.exp];
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  },
  get: function() {
    Dep.target = this; // 缓存自己,用于判断是否添加watcher。
    var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
    Dep.target = null; // 释放自己
    return value;
  },
};

到此为止, 简单的额 Watcher 设计完毕,然后将 Observer 和 Watcher 关联起来,就可以实现一个简单的的双向绑定了。

因为还没有设计解析器 Compile,所以可以先将模板数据写死。

将代码转化为 ES6 构造函数的写法,预览试试。

https://jsrun.net/8SIKp/embed...

这段代码因为没有实现编译器而是直接传入了所绑定的变量,我们只在一个节点上设置一个数据(name)进行绑定,然后在页面上进行 new MyVue,就可以实现双向绑定了。

并两秒后进行值得改变,可以看到,页面也发生了变化。

// MyVue
proxyKeys(key) {
    var self = this;
    Object.defineProperty(this, key, {
        enumerable: false,
        configurable: true,
        get: function proxyGetter() {
            return self.data[key];
        },
        set: function proxySetter(newVal) {
            self.data[key] = newVal;
        }
    });
}

上面这段代码的作用是将 this.data 的 key 代理到 this 上,使得我可以方便的使用 this.xx 就可以取到 this.data.xx。

实现 Compile

虽然上面实现了双向数据绑定,但是整个过程都没有解析 DOM 节店,而是固定替换的,所以接下来要实现一个解析器来做数据的解析和绑定工作。

解析器 compile 的实现步骤:

  1. 解析模板指令,并替换模板数据,初始化视图。
  2. 将模板指定对应的节点绑定对应的更新函数,初始化相应的订阅器。

为了解析模板,首先需要解析 DOM 数据,然后对含有 DOM 元素上的对应指令进行处理,因此整个 DOM 操作较为频繁,可以新建一个 fragment 片段,将需要的解析的 DOM 存入 fragment 片段中在进行处理。

function nodeToFragment(el) {
  var fragment = document.createDocumentFragment();
  var child = el.firstChild;
  while (child) {
    // 将Dom元素移入fragment中
    fragment.appendChild(child);
    child = el.firstChild;
  }
  return fragment;
}

接下来需要遍历各个节点,对含有相关指令和模板语法的节点进行特殊处理,先进行最简单模板语法处理,使用正则解析“{{变量}}”这种形式的语法。

function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/; // 匹配{{xx}}
        var text = node.textContent;
        if (self.isTextNode(node) && reg.test(text)) {  // 判断是否是符合这种形式{{}}的指令
            self.compileText(node, reg.exec(text)[1]);
        }
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  // 继续递归遍历子节点
        }
    });
},
function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node, initText);  // 将初始化的数据初始化到视图中
    new Watcher(this.vm, exp, function (value) {  // 生成订阅器并绑定更新函数
        self.updateText(node, value);
    });
},
function updateText (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}

获取到最外层的节点后,调用 compileElement 函数,对所有的子节点进行判断,如果节点是文本节点切匹配{{}}这种形式的指令,则进行编译处理,初始化对应的参数。

然后需要对当前参数生成一个对应的更新函数订阅器,在数据发生变化时更新对应的 DOM。

这样就完成了解析、初始化、编译三个过程了。

接下来改造一个 myVue 就可以使用模板变量进行双向数据绑定了。

https://jsrun.net/K4IKp/embed...

添加解析事件

添加完 compile 之后,一个数据双向绑定就基本完成了,接下来就是在 Compile 中添加更多指令的解析编译,比如 v-model、v-on、v-bind 等。

添加一个 v-model 和 v-on 解析:

function compile(node) {
  var nodeAttrs = node.attributes;
  var self = this;
  Array.prototype.forEach.call(nodeAttrs, function(attr) {
    var attrName = attr.name;
    if (isDirective(attrName)) {
      var exp = attr.value;
      var dir = attrName.substring(2);
      if (isEventDirective(dir)) {
        // 事件指令
        self.compileEvent(node, self.vm, exp, dir);
      } else {
        // v-model 指令
        self.compileModel(node, self.vm, exp, dir);
      }
      node.removeAttribute(attrName); // 解析完毕,移除属性
    }
  });
}
// v-指令解析
function isDirective(attr) {
  return attr.indexOf("v-") == 0;
}
// on: 指令解析
function isEventDirective(dir) {
  return dir.indexOf("on:") === 0;
}

上面的 compile 函数是用于遍历当前 dom 的所有节点属性,然后判断属性是否是指令属性,如果是在做对应的处理(事件就去监听事件、数据就去监听数据..)

完整版 myVue

在 MyVue 中添加 mounted 方法,在所有操作都做完时执行。

class MyVue {
  constructor(options) {
    var self = this;
    this.data = options.data;
    this.methods = options.methods;
    Object.keys(this.data).forEach(function(key) {
      self.proxyKeys(key);
    });
    observe(this.data);
    new Compile(options.el, this);
    options.mounted.call(this); // 所有事情处理好后执行mounted函数
  }
  proxyKeys(key) {
    // 将this.data属性代理到this上
    var self = this;
    Object.defineProperty(this, key, {
      enumerable: false,
      configurable: true,
      get: function getter() {
        return self.data[key];
      },
      set: function setter(newVal) {
        self.data[key] = newVal;
      },
    });
  }
}

然后就可以测试使用了。

https://jsrun.net/Y4IKp/embed...

总结一下流程,回头在哪看一遍这个图,是不是清楚很多了。
vue2.x流程图2.png

可以查看的代码地址:Vue2.x 的双向绑定原理及实现

参考

查看原文

赞 22 收藏 8 评论 0

九旬 赞了文章 · 3月19日

进军高级前端开发工程师必备的知识图谱

一、前沿

全文(含脑图)为个人总结的关于高级前端开发工程师必备的技术能力,欢迎补充。全文结构如下:
前沿:写在正文前的一些话。
脑图:基于知识图谱的脑图,看知识图谱晕的可以欣赏脑图。
知识图谱:脑图无法下手的,参照一下知识图谱。

二、脑图

PS:建议单击图片,点击图片下方的查看原图,放大图片后,再进行脑图的查看

图片描述

三、知识图谱

  • 1.核心技术

    • 1.1 HTML(5)
    • 1.2 JavaScript

      • ES6
      • Vue
      • React
      • Angular
    • 1.3 CSS(3)

      • 布局

        • 基础布局
        • 双飞翼布局
        • 圣杯布局
        • Flex布局
        • Grid布局
      • CSS3D
      • 矩阵
      • 高性能渲染
      • Houdini
  • 2.扩展技术

    • 2.1 后端(至少一种)

      • Node.js

        • 核心API
        • Express
        • Koa
        • Egg
        • 微服务
        • C/C++
      • PHP
      • Go
      • Java
    • 2.2 移动端

      • AMP
      • PWA
      • Flutter
    • 2.3 图形学

      • SVG
      • Canvas
      • Cocos2d
      • WebGL
      • Three.js
    • 2.4 TypeScript
    • 2.5 浏览器特性/兼容性
  • 3.工程开发

    • 3.1 预编译工具

      • Less
      • Sass
      • PostCss
    • 3.2 构建工具

      • Webpack
      • Grunt
      • Gulp
    • 3.3 性能优化

      • FCP
      • FMP
      • 客户端渲染
      • 服务端渲染
      • 雅虎军规
      • ...
    • 3.4 版本管理

      • Git
    • 3.5 高级调试

      • 断点
      • Timeline
      • Profiles
    • 3.6 自动化测试

      • Karma
      • Mocha
      • Jest
    • 3.7 可用性/安全

      • 加密
      • 混淆
      • ...
  • 4.编程思想

    • 4.1 编程范式

      • 面向对象编程
      • 函数式编程
      • 响应式编程
      • 面向切面编程
      • ...
    • 4.2 设计模式(至少掌握最热门的前5种)

      • 1.单例模式
      • 2.代理模式
      • 3.命令模式
      • 4.发布订阅模式
      • 5.职责链模式
      • ...
    • 4.3 设计法则

      • 单一职责原则
      • 开放封闭原则
      • 李氏置换原则
      • 接口独立原则
      • 依赖导致原则
    • 4.4 架构模式

      • MVC
      • MVP
      • MVVM
      • Flux
      • ...
    • 4.5 算法

      • Diff算法
      • 排序算法

        • 冒泡排序
        • 选择排序
        • 插入排序
        • 希尔排序
        • 归并排序
        • 快速排序
        • ...
      • 检索算法

        • 二分法
        • ...
    • 4.6 编程原理

      • v8
      • libv
      • ...
  • 5.能力

    • 5.1 学习能力
    • 5.2 技术能力
    • 5.3 专长(在专业领域解决问题)

      • 前端
      • 后端
      • 移动端
      • 图形学
      • 算法
      • ...
    • 5.4 认知能力

      • 业务理解
      • 需求分析
      • 项目评估
    • 5.5 经验

      • 项目经验
      • 使用经验(问题解决方案)
      • (跨)领域经验
      • 管理经验
    • 5.6 架构能力(核心:判断和取舍)

      • 性能
      • 可用性
      • 伸缩性
      • 扩展性
      • 安全性

更新日期:2019年1月

查看原文

赞 150 收藏 123 评论 19

九旬 关注了用户 · 3月18日

政采云前端团队 @zhengcaiyunqianduantuandui

Z 是政采云拼音首字母,oo 是无穷的符号,结合 Zoo 有生物圈的含义。寄望我们的前端 ZooTeam 团队,不论是人才梯队,还是技术体系,都能各面兼备,成长为一个生态,卓越且持续卓越。

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

关注 2631

认证与成就

  • 获得 150 次点赞
  • 获得 7 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • vant-weapp

    Vant 是有赞前端团队开源的移动端组件库,于 2016 年开源,已持续维护 4 年时间。Vant 对内承载了有赞所有核心业务,对外服务十多万开发者,是业界主流的移动端组件库之一。

注册于 2017-04-17
个人主页被 4.2k 人浏览