• 学习笔记—Promise之高阶函数与设计模式


    日常的学习笔记,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue 全家桶,后续可能还会继续更新 Typescript、Vue3 和 常见的面试题 等等。

    高阶函数

    高阶函数的特点

    • 一个函数的参数是一个函数(回调函数就是一种高阶函数)
    • 一个函数返回一个函数

    我们平时会用到的 reducemap 等方法就是高阶函数。

    before 方法

    假设我们现在有这样一个场景,我们写了一个业务代码,而现在我们需要扩展当前的业务代码。

    function say() {
      // todo something...
      console.log("say");
    }
    

    我们需要在业务代码之前对其进行相应的处理,但是我们如果对业务代码的封装方法进行处理,会使整个代码变得很难处理和复用。

    所以我们需要在 Function.prototype 原型链上绑定一个 before 方法 ,使业务代码调用前,先调用一下这个方法。实现对扩展代码进行统一的管理。

    Function.prototype.before = function (callback) {
      return () => {
        callback();
        this(); // 箭头函数会查找其上级作用域的this指向
      };
    };
    

    (注:我们这里的回调函数需要使用箭头函数,原因是箭头函数不存在 this 指向,他会查找上级作用域的 this 指向

    这样我们在使用业务代码前,就可以直接调用其 回调函数

    let beforeSay = say.before(function () {
      console.log("before say");
    });
    beforeSay(); // before say  say
    

    这里符合高阶函数的两个特点,所以其也是一种 高阶函数

    最终达到了我们想要的效果,业务代码 与 扩展代码 实现了分离。

    同时,我们也可以进行传参。

    function say(a, b) {
      // todo something...
      console.log("say", a, b);
    }
    Function.prototype.before = function (callback) {
      return (...args) => {
        // 箭头函数不存在arguments属性,所以我们使用剩余运算符来进行参数传递
        callback();
        this(...args);
      };
    };
    let beforeSay = say.before(function () {
      console.log("before say");
    });
    beforeSay("hello", "world"); // before say  say
    

    after 方法

    假设现在有这样一串代码,我们需要根据传递的参数来判断何时执行函数。

    let newFn = after(3, function () {
      console.log("after");
    });
    newFn(); // ...
    newFn(); // ...
    newFn(); // after
    

    上面我们传入了一个 3,并传入了一个自定义函数。在第三次时,执行了我们的自定义函数。

    接下来我们来完成 after 函数。

    function after(times, callback) {
      return function () {
        // 自定义内容
        if (--times === 0) {
          callback();
        }
      };
    }
    

    同样是利用 闭包 的思想,完成了函数的封装。

    上述代码同样符合 高阶函数 的特点,所以这也是一种高阶函数

    函数柯理化

    首先,我们可以先看一个这样的需求案例。

    假设我们现在需要对几个数进行求和运算,可能平时我们会直接用下面这个函数进行封装。

    function sum(a, b, c, d, e) {
      return a + b + c + d + e;
    }
    sum(1, 2, 3, 4, 5);
    

    如果我们对传递的参数进行分别传递呢?

    sum(1, 2)(3)(4, 5);
    

    这个时候,我们就无法再用上面的函数进行运算了。

    我们需要用到一个全新的高阶函数,函数柯理化

    const curring = (fn, arr = []) => {
      let len = fn.length;
      return function (...args) {
        let concatVal = [...arr, ...args];
        if (arr.length < len) {
          return curring(fn, arr);
        } else {
          return fn(...concatVal);
        }
      };
    };
    console.log(curring(sum)(1, 2)(3)(4, 5)); // 15
    

    整体思路其实就是将后续传入的所有参数,拼接成一组完整的参数,最终实现 函数柯理化

    异步并发问题

    假设现在有多个异步并发请求,我们该如何同时获得最终结果呢?

    这里我们会用到 Node 中的 fs 模块。

    let fs = require("fs"); // file System
    

    这是一个用来 操作文件 的模块。

    随后我们可以在其子目录下创建两个 .txt 文件,随便往里面写一些内容用来测试。

    image.png

    然后我们来使用 readFile 直接操作文件。

    fs.readFile("./name.txt", "utf8", function (err, data) {
      console.log(data); // zhangsan
    });
    fs.readFile("./test.txt", "utf8", function (err, data) {
      console.log(data); // test
    });
    

    这样我们就模拟了两个异步操作。

    现在我们想将这两个结果直接放到一个变量中。

    let allVal = {};
    fs.readFile("./name.txt", "utf8", function (err, data) {
      allVal.name = data;
    });
    fs.readFile("./test.txt", "utf8", function (err, data) {
      allVal.test = data;
    });
    console.log(allVal); // {}
    

    我们可以看到输出结果是空的,原因是这两个读取操作是异步的。

    那么我们该如何获取这个结果呢?

    我们有如下几个解决方法

    1. 模拟一个 cb 方法并创建一个计数变量 index,每次执行完一个异步方法后,都在 index 上加 1。
    let index = 0;
    const cb = () => {
      if (++index === 2) {
        console.log(allVal);
      }
    };
    fs.readFile("./name.txt", "utf8", function (err, data) {
      allVal.name = data;
      cb();
    });
    fs.readFile("./test.txt", "utf8", function (err, data) {
      allVal.test = data;
      cb();
    });
    

    这样写会有一个问题,就是当我们需要调用的异步方法过多时,会十分难以操作,同时我们还需要创建一个额外的全局变量。

    1. 利用上面的 after 方法 的思路

      function after(times, callback) {
        return function () {
          if (--times === 0) {
            callback();
          }
        };
      }
      let cb = after(2, function () {
        console.log(allVal);
      });
      

      利用闭包的思想,将回调函数存储到堆内存里。直到触发时,再输出结果。

    这样我们就完成了异步并发问题的处理,最优的选择就是利用闭包的方式,也就是上面的 第二种

    两种设计模式

    发布订阅模式

    首先,发布订阅模式分为两个部分,分别是 onemit,同时我们还包含一个存储属性 arr

    • on 就是把一些需要用到的函数维护到一个数组中
    • emit 就是将数组中的函数依次执行
    • arr 用来对函数进行存储
    let event = {
      arr: [], // 作为一个存储属性
      on(fn) {
        this.arr.push(fn);
      },
      emit() {
        this.arr.forEach((fn) => fn());
      },
    };
    

    这样,我们可以用这种设计模式来进行异步操作了。

    我们还是用上述异步操作的例子。

    let fs = require("fs"); // file System
    let allVal = {};
    fs.readFile("./name.txt", "utf8", function (err, data) {
      allVal.name = data;
    });
    fs.readFile("./test.txt", "utf8", function (err, data) {
      allVal.test = data;
    });
    console.log(allVal);
    

    下面我们来进行一下异步存储操作,依次输出我们想要的结果。

    // 绑定输出函数到 on 上,以便我们对结果进行观察
    event.on(function () {
      console.log("读取了一个");
    });
    event.on(function () {
      // 自定义异步操作全部执行完后,需要输出的结果
      if (Object.keys(allVal).length === 2) {
        console.log(allVal);
      }
    });
    // 在每个异步函数下绑定 emit
    fs.readFile("./name.txt", "utf8", function (err, data) {
      allVal.name = data;
      event.emit();
    });
    fs.readFile("./test.txt", "utf8", function (err, data) {
      allVal.test = data;
      event.emit();
    });
    // 最终输出结果: 读取了一个  读取了一个  { name: 'zhangsan', test: 'test' }
    

    我们可以用这种设计模式的开发思想,完成多种需求的开发。

    观察者模式

    首先,这种设计模式既然被称为观察者模式,那么肯定就存在一个 观察者 和一个 被观察者观察者 需要放到 被观察者 中,被观察者 的状态发生变化,会通知 观察者(注:Vue 的双向绑定的实现原理使用的就是 观察者模式

    其内部也是基于 发布订阅模式 实现的,所以我们平时会将 观察者模式发布订阅模式 放到一起理解。

    我们可以通过模拟一个宠物与主人之间的状态关系的例子,来进一步理解一下这个设计模式。

    // 观察者模式
    class Subject {
      // 被观察者
      constructor(name) {
        this.name = name;
        this.observers = [];
        this.state = "开心";
      }
      attach(o) {
        this.observers.push(o);
      }
      setState(newState) {
        this.state = newState;
        this.observers.forEach((fn) => fn.update(this));
      }
    }
    class Observer {
      // 观察者
      constructor(name) {
        this.name = name;
      }
      update(pets) {
        console.log(this.name + "知道了" + pets.name + "的心情十分的" + pets.state);
      }
    }
    let cat = new Subject("花花");
    let master1 = new Observer("大白");
    let master2 = new Observer("小白");
    cat.attach(master1);
    cat.attach(master2);
    cat.setState("伤心");
    // 大白知道了花花的心情十分的伤心
    // 小白知道了花花的心情十分的伤心
    

    本篇文章由莫小尚创作,文章中如有任何问题和纰漏,欢迎您的指正与交流。
    您也可以关注我的 个人站点博客园掘金,我会在文章产出后同步上传到这些平台上。
    最后感谢您的支持!

  • 相关阅读:
    terminal下历史命令自动完成功能history auto complete
    Shell中while循环的done 后接一个重定向<
    python 链接hive
    shell 学习基地
    c++ 获取本地ip地址
    c++ 如何实现,readonly
    c++ 语法
    c++ 信号量
    vim插件介绍
    c++ memset 函数 及 坑
  • 原文地址:https://www.cnblogs.com/moxiaoshang/p/15026237.html
Copyright © 2020-2023  润新知