微信LazyMan笔试题的深入解析和实现

2017/02/03 · JavaScript
· Javascript,
异步

原文出处: wall_wxk   

用 Async 函数简化异步代码

2017/04/08 · JavaScript
· 异步

原文出处: Joe Zimmerman , Nilson
Jacques   译文出处:oschina   

Promise 在 JavaScript 上发布之初就在互联网上流行了起来 —
它们帮开发人员摆脱了回调地狱,解决了在很多地方困扰 JavaScript
开发者的异步问题。但 Promises
也远非完美。它们一直请求回调,在一些复杂的问题上仍会有些杂乱和一些难以置信的冗余。

随着 ES6 的到来(现在被称作 ES2015),除了引入 Promise
的规范,不需要请求那些数不尽的库之外,我们还有了生成器。生成器可在函数内部停止执行,这意味着可把它们封装在一个多用途的函数中,我们可在代码移动到下一行之前等待异步操作完成。突然你的异步代码可能就开始看起来同步了。

这只是第一步。异步函数因今年加入 ES2017,已进行标准化,本地支持也进一步优化。异步函数的理念是使用生成器进行异步编程,并给出他们自己的语义和语法。因此,你无须使用库来获取封装的实用函数,因为这些都会在后台处理。

运行文章中的 async/await 实例,你需要一个能兼容的浏览器。

终极蛇皮上帝视角之微信小程序之告别“刀耕火种”

2018/08/22 · 基础技术 ·
webpack,
小程序

原文出处: BuptStEve   

开门见山地说,小程序在日常开发中使用原生框架来开发还是挺不方便的,比如:

  • 不支持 npm
  • 不支持各种 CSS 预编译器
  • 不支持配置 Babel 来转换一些 JavaScript 新特性

这样一来和日常开发前端页面的体验相比来说,简直就像在刀耕火种

那么为了解决这些问题,我们能不能将前端开发中常用的 webpack
移植到小程序开发中呢?

当然可以!

图片 1

一、题目介绍

以下是我copy自网上的面试题原文:

实现一个LazyMan,可以按照以下方式调用:
LazyMan(“Hank”)输出:
Hi! This is Hank!

LazyMan(“Hank”).sleep(10).eat(“dinner”)输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~

LazyMan(“Hank”).eat(“dinner”).eat(“supper”)输出
Hi This is Hank!
Eat dinner~
Eat supper~

LazyMan(“Hank”).sleepFirst(5).eat(“supper”)输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper

以此类推。

运行兼容

在客户端,Chrome、Firefox 和 Opera 能很好地支持异步函数。

图片 2

(点击图片进行页面跳转)

从 7.6 版本开始,Node.js 默认启用 async/await。

0.源码地址


  • webpack-simple
    中文件结构和小程序相似。
  • 而在
    webpack-vue
    中还增加了 vue-loader,因此你甚至还能利用 .vue
    文件编写单文件组件。

注:已封装到
https://tuateam.github.io/tua…
中…

图片 3

二、题目考察的点

先声明:我不是微信员工,考察的点是我推测的,可能不是,哈哈!

1.方法链式调用
2.类的使用和面向对象编程的思路
3.设计模式的应用
4.代码的解耦
5.最少知识原则,也即 迪米特法则(Law of Demeter)
6.代码的书写结构和命名

异步函数和生成器对比

这有个使用生成器进行异步编程的实例,用的是 Q 库:

var doAsyncOp = Q.async(function* () {
  var val = yield asynchronousOperation();   console.log(val);
  return val; });

1
2
3
4
5
var doAsyncOp = Q.async(function* () {
  var val = yield asynchronousOperation();
  console.log(val);
  return val;
});

Q.async 是个封装函数,处理场景后的事情。其中 *
表示作为一个生成器函数的功能,yield
表示停止函数,并用封装函数代替。Q.async
将会返回一个函数,你可对它赋值,就像赋值 doAsyncOp 一样,随后再调用。

ES7 中的新语法更简洁,操作示例如下:

async function doAsyncOp () {
  var val = await asynchronousOperation();        console.log(val);
  return val; };

1
2
3
4
5
async function doAsyncOp () {
  var val = await asynchronousOperation();     
  console.log(val);
  return val;
};

差异不大,我们删除了一个封装的函数和 * 符号,转而用 async
关键字代替。yield 关键字也被 await
取代。这两个例子事实上做的事是相同的:在 asynchronousOperation
完成之后,赋值给 val,然后进行输出并返回结果。

1.文件结构

既然用 webpack 来编译源代码,那么很自然的我们的文件结构首先要分为
src/dist/,开发者工具的目标应该是 dist/ 目录。

注:开发者工具打开的应该是根目录,这样可以保存各种设置,可以在
project.config.json 中配置 "miniprogramRoot": "./dist/",

三、题目思路解析

1.看题目输出示例,可以确定这是拟人化的输出,也就是说:应该编写一个类来定义一类人,叫做LazyMan。可以输出名字、吃饭、睡觉等行为。
2.从输出的句子可以看出,sleepFrist的优先级是最高的,其他行为的优先级一致。
3.从三个例子来看,都得先调用LazyMan来初始化一个人,才能继续后续行为,所以LazyMan是一个接口。
4.句子是按调用方法的次序进行顺序执行的,是一个队列。

将 Promises 转换成异步函数

如果我们使用 Vanilla Promises 的话前面的示例将会是什么样?

function doAsyncOp () {
  return asynchronousOperation().then(function(val) {
    console.log(val);     return val;   }); };

1
2
3
4
5
6
function doAsyncOp () {
  return asynchronousOperation().then(function(val) {
    console.log(val);
    return val;
  });
};

这里有相同的代码行数,但这是因为 then 和给它传递的回调函数增加了很多的额外代码。另一个让人厌烦的是两个 return 关键字。这一直有些事困扰着我,因为它很难弄清楚使用
promises 的函数确切的返回是什么。

就像你看到的,这个函数返回一个
promises,将会赋值给 val,猜一下生成器和异步函数示例做了什么!无论你在这个函数返回了什么,你其实是暗地里返回一个
promise 解析到那个值。如果你根本就没有返回任何值,你暗地里返回的 promise
解析为 undefined。

1.1.src/ 中文件结构大概长这样:

. ├── app │ ├── app.js │ ├── app.json │ └── app.scss ├── assets │ └──
vue-logo.png ├── comps │ └── todo │ ├── todo.js │ ├── todo.json │ ├──
todo.less │ └── todo.wxml ├── pages │ └── index │ ├── index.js │ ├──
index.json │ ├── index.less │ └── index.wxml ├── scripts │ ├── const │ │
├── README.md │ │ └── index.js │ └── utils │ ├── README.md │ ├──
event.js │ ├── format.js │ ├── index.js │ └── log.js ├── styles │ ├──
global.styl │ ├── todomvc-app-css.css │ └── todomvc-common-base.css └──
templates └── info.wxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.
├── app
│   ├── app.js
│   ├── app.json
│   └── app.scss
├── assets
│   └── vue-logo.png
├── comps
│   └── todo
│       ├── todo.js
│       ├── todo.json
│       ├── todo.less
│       └── todo.wxml
├── pages
│   └── index
│       ├── index.js
│       ├── index.json
│       ├── index.less
│       └── index.wxml
├── scripts
│   ├── const
│   │   ├── README.md
│   │   └── index.js
│   └── utils
│       ├── README.md
│       ├── event.js
│       ├── format.js
│       ├── index.js
│       └── log.js
├── styles
│   ├── global.styl
│   ├── todomvc-app-css.css
│   └── todomvc-common-base.css
└── templates
    └── info.wxml
  • app/: 应用入口
  • assets/: 资源文件,比如图片
  • comps/: 组件
  • pages/: 页面
  • scripts: 公用代码
  • scripts/const: 常量(已配置别名 @const)
  • scripts/utils: 辅助函数(已配置别名 @utils)
  • styles/: 公用样式
  • templates/: 模板

四、采用观察者模式实现代码

4.1 采用模块模式来编写代码

JavaScript

(function(window, undefined){ })(window);

1
2
3
(function(window, undefined){
 
})(window);

4.2 声明一个变量taskList,用来存储需要队列信息

JavaScript

(function(window, undefined){ var taskList = []; })(window);

1
2
3
(function(window, undefined){
    var taskList = [];
})(window);

队列中,单个项的存储设计为一个json,存储需要触发的消息,以及方法执行时需要的参数列表。比如LazyMan(‘Hank’),需要的存储信息如下。

JavaScript

{ ‘msg’:’LazyMan’, ‘args’:’Hank’ }

1
2
3
4
{
    ‘msg’:’LazyMan’,
    ‘args’:’Hank’
}

当执行LazyMan方法的时候,调用订阅方法,将需要执行的信息存入taskList中,缓存起来。
存储的信息,会先保留着,等发布方法进行提取,执行和输出。

4.3 订阅方法

订阅方法的调用方式设计:subscribe("lazyMan", "Hank")

JavaScript

(function(window, undefined){ var taskList = []; // 订阅 function
subscribe(){ var param = {}, args =
Array.prototype.slice.call(arguments); if(args.length < 1){ throw new
Error(“subscribe 参数不能为空!”); } param.msg = args[0]; // 消息名
param.args = args.slice(1); // 参数列表 if(param.msg == “sleepFirst”){
taskList.unshift(param); }else{ taskList.push(param); } } })(window);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function(window, undefined){
    var taskList = [];
 
    // 订阅
    function subscribe(){
        var param = {},
            args = Array.prototype.slice.call(arguments);
 
        if(args.length < 1){
            throw new Error("subscribe 参数不能为空!");
        }
 
        param.msg = args[0]; // 消息名
        param.args = args.slice(1); // 参数列表
 
        if(param.msg == "sleepFirst"){
            taskList.unshift(param);
        }else{
            taskList.push(param);
        }
    }
})(window);

用一个param变量来组织好需要存储的信息,然后push进taskList中,缓存起来。
特别的,如果是sleepFirst,则放置在队列头部。

4.4 发布方法

JavaScript

(function(window, undefined){ var taskList = []; // 订阅方法 代码…
// 发布 function publish(){ if(taskList.length > 0){
run(taskList.shift()); } } })(window);

1
2
3
4
5
6
7
8
9
10
11
12
(function(window, undefined){
    var taskList = [];
 
        // 订阅方法 代码…
 
    // 发布
    function publish(){
        if(taskList.length > 0){
            run(taskList.shift());
        }
    }
})(window);

将队列中的存储信息读取出来,交给run方法(暂定,后续实现)去执行。这里限定每次发布只执行一个,以维持队列里面的方法可以挨个执行。
另外,这里使用shift()方法的原因是,取出一个,就在队列中删除这一个,避免重复执行。

4.5 实现LazyMan类

JavaScript

// 类 function LazyMan(){}; LazyMan.prototype.eat = function(str){
subscribe(“eat”, str); return this; }; LazyMan.prototype.sleep =
function(num){ subscribe(“sleep”, num); return this; };
LazyMan.prototype.sleepFirst = function(num){ subscribe(“sleepFirst”,
num); return this; };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 类
function LazyMan(){};
 
LazyMan.prototype.eat = function(str){
    subscribe("eat", str);
    return this;
};
 
LazyMan.prototype.sleep = function(num){
    subscribe("sleep", num);
    return this;
};
 
LazyMan.prototype.sleepFirst = function(num){
    subscribe("sleepFirst", num);
    return this;
};

将LazyMan类实现,具有eat、sleep、sleepFrist等行为。
触发一次行为,就在taskList中记录一次,并返回当前对象,以支持链式调用。

4.6 实现输出console.log的包装方法

JavaScript

// 输出文字 function lazyManLog(str){ console.log(str); }

1
2
3
4
// 输出文字
function lazyManLog(str){
    console.log(str);
}

为什么还要为console.log包装一层,是因为在实战项目中,产经经常会修改输出提示的UI。如果每一处都用console.log直接调用,那改起来就麻烦很多。
另外,如果要兼容IE等低级版本浏览器,也可以很方便的修改。
也就是DRY原则(Don’t Repeat Youself)。

4.7 实现具体执行的方法

JavaScript

// 具体方法 function lazyMan(str){ lazyManLog(“Hi!This is “+ str +”!”);
publish(); } function eat(str){ lazyManLog(“Eat “+ str +”~”); publish();
} function sleep(num){ setTimeout(function(){ lazyManLog(“Wake up after
“+ num); publish(); }, num*1000); } function sleepFirst(num){
setTimeout(function(){ lazyManLog(“Wake up after “+ num); publish(); },
num*1000); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 具体方法
function lazyMan(str){
    lazyManLog("Hi!This is "+ str +"!");
 
    publish();
}
 
function eat(str){
    lazyManLog("Eat "+ str +"~");
    publish();
}
 
function sleep(num){
    setTimeout(function(){
        lazyManLog("Wake up after "+ num);
 
        publish();
    }, num*1000);
 
}
 
function sleepFirst(num){
    setTimeout(function(){
        lazyManLog("Wake up after "+ num);
 
        publish();
    }, num*1000);
}

这里的重点是解决setTimeout执行时会延迟调用,也即线程异步执行的问题。只有该方法执行成功后,再发布一次消息publish(),提示可以执行下一个队列信息。否则,就会一直等待。

4.8 实现run方法,用于识别要调用哪个具体方法,是一个总的控制台

JavaScript

// 鸭子叫 function run(option){ var msg = option.msg, args =
option.args; switch(msg){ case “lazyMan”: lazyMan.apply(null,
args);break; case “eat”: eat.apply(null, args);break; case “sleep”:
sleep.apply(null,args);break; case “sleepFirst”:
sleepFirst.apply(null,args);break; default:; } }

1
2
3
4
5
6
7
8
9
10
11
12
13
// 鸭子叫
function run(option){
    var msg = option.msg,
        args = option.args;
 
    switch(msg){
        case "lazyMan": lazyMan.apply(null, args);break;
        case "eat": eat.apply(null, args);break;
        case "sleep": sleep.apply(null,args);break;
        case "sleepFirst": sleepFirst.apply(null,args);break;
        default:;
    }
}

这个方法有点像鸭式辨型接口,所以注释叫鸭子叫
run方法接收队列中的单个消息,然后读取出来,看消息是什么类型的,然后执行对应的方法。

4.9 暴露接口LazyMan,让外部可以调用

JavaScript

(function(window, undefined){ // 很多代码… // 暴露接口 window.LazyMan
= function(str){ subscribe(“lazyMan”, str); setTimeout(function(){
publish(); }, 0); return new LazyMan(); }; })(window);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function(window, undefined){
        // 很多代码…
 
    // 暴露接口
    window.LazyMan = function(str){
        subscribe("lazyMan", str);
 
        setTimeout(function(){
            publish();
        }, 0);
 
        return new LazyMan();
    };
})(window);

接口LazyMan里面的publish方法必须使用setTimeout进行调用。这样能让publish()执行的线程延后,挂起。等链式方法都执行完毕后,线程空闲下来,再执行该publish()
另外,这是一个对外接口,所以调用的时候,同时也会new
一个新的LazyMan,并返回,以供调用。

链式操作

Promise
之所以能受到众人追捧,其中一个方面是因为它能以链式调用的方式把多个异步操作连接起来,避免了嵌入形式的回调。不过
async 函数在这个方面甚至比 Promise 做得还好。

下面演示了如何使用 Promise
来进行链式操作(我们只是简单的多次运行 asynchronousOperation 来进行演示)。

function doAsyncOp() {   return asynchronousOperation()
    .then(function(val) {       return asynchronousOperation(val);
    })     .then(function(val) {
      return asynchronousOperation(val);     })
    .then(function(val) {       return asynchronousOperation(val);
    }); }

1
2
3
4
5
6
7
8
9
10
11
12
function doAsyncOp() {
  return asynchronousOperation()
    .then(function(val) {
      return asynchronousOperation(val);
    })
    .then(function(val) {
      return asynchronousOperation(val);
    })
    .then(function(val) {
      return asynchronousOperation(val);
    });
}

使用 async 函数,只需要像编写同步代码那样调用 asynchronousOperation:

async function doAsyncOp () {   var val = await asynchronousOperation();
  val = await asynchronousOperation(val);
  val = await asynchronousOperation(val);
  return await asynchronousOperation(val); };

1
2
3
4
5
6
async function doAsyncOp () {
  var val = await asynchronousOperation();
  val = await asynchronousOperation(val);
  val = await asynchronousOperation(val);
  return await asynchronousOperation(val);
};

甚至最后的 return 语句中都不需要使用
await,因为用或不用,它都返回了包含了可处理终值的 Promise。

1.2.dist/ 中文件结构大概长这样:

. ├── app.js ├── app.js.map ├── app.json ├── app.wxss ├── assets │ └──
vue-logo.png ├── chunks │ ├── runtime.js │ ├── runtime.js.map │ ├──
scripts.js │ ├── scripts.js.map │ ├── vendors.js │ └── vendors.js.map
├── comps │ └── todo │ ├── todo.js │ ├── todo.js.map │ ├── todo.json │
├── todo.wxml │ └── todo.wxss ├── pages │ └── index │ ├── index.js │ ├──
index.js.map │ ├── index.json │ ├── index.wxml │ └── index.wxss └──
templates └── info.wxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.
├── app.js
├── app.js.map
├── app.json
├── app.wxss
├── assets
│   └── vue-logo.png
├── chunks
│   ├── runtime.js
│   ├── runtime.js.map
│   ├── scripts.js
│   ├── scripts.js.map
│   ├── vendors.js
│   └── vendors.js.map
├── comps
│   └── todo
│       ├── todo.js
│       ├── todo.js.map
│       ├── todo.json
│       ├── todo.wxml
│       └── todo.wxss
├── pages
│   └── index
│       ├── index.js
│       ├── index.js.map
│       ├── index.json
│       ├── index.wxml
│       └── index.wxss
└── templates
    └── info.wxml
  • chunks/: 公共依赖
    • runtime: 是 webapck
      在运行时连接各个模块的代码
    • vendors: 是提取的 node_modules 下的依赖
    • scripts: 是提取的 src/scripts/ 下的依赖

五、总结

1. 好处

使用观察者模式,让代码可以解耦到合理的程度,使后期维护更加方便。
比如我想修改eat方法,我只需要关注eat()LazyMan.prototype.eat的实现。其他地方,我都可以不用关注。这就符合了最少知识原则

2. 不足
LazyMan.prototype.eat这种方法的参数,其实可以用arguments代替,我没写出来,怕弄得太复杂,就留个优化点吧。
使用了unshift和shift方法,没有考虑到低版本IE浏览器的兼容。

并发操作

Promise
还有另一个伟大的特性,它们可以同时进行多个异步操作,等他们全部完成之后再继续进行其它事件。ES2015
规范中提供了 Promise.all(),就是用来干这个事情的。

这里有一个示例:

function doAsyncOp() {   return Promise.all([
    asynchronousOperation(),     asynchronousOperation()
  ]).then(function(vals) {     vals.forEach(console.log);
    return vals;   }); }

1
2
3
4
5
6
7
8
9
function doAsyncOp() {
  return Promise.all([
    asynchronousOperation(),
    asynchronousOperation()
  ]).then(function(vals) {
    vals.forEach(console.log);
    return vals;
  });
}

Promise.all() 也可以当作 async 函数使用:

async function doAsyncOp() {   var vals = await Promise.all([
    asynchronousOperation(),     asynchronousOperation()   ]);
  vals.forEach(console.log.bind(console));   return vals; }

1
2
3
4
5
6
7
8
async function doAsyncOp() {
  var vals = await Promise.all([
    asynchronousOperation(),
    asynchronousOperation()
  ]);
  vals.forEach(console.log.bind(console));
  return vals;
}

这里就算使用了 Promise.all,代码仍然很清楚。

1.3.整个项目文件结构大概长这样:

. ├── README.md ├── dist/ ├── package.json ├── project.config.json ├──
src/ ├── webpack.config.babel.js └── yarn.lock

1
2
3
4
5
6
7
8
.
├── README.md
├── dist/
├── package.json
├── project.config.json
├── src/
├── webpack.config.babel.js
└── yarn.lock
  • src/: 源码
  • dist/: 打包后代码

发表评论

电子邮件地址不会被公开。 必填项已用*标注