SegmentFault SYJ个人主页最新的文章
2019-04-25T15:17:27+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
JS 异步错误捕获二三事
https://segmentfault.com/a/1190000018985956
2019-04-25T15:17:27+08:00
2019-04-25T15:17:27+08:00
sunyongjian
https://segmentfault.com/u/sunyongjian
14
<h2>引入</h2>
<p>我们都知道 try catch 无法捕获 setTimeout 异步任务中的错误,那其中的原因是什么。以及异步代码在 js 中是特别常见的,我们该怎么做才比较?</p>
<h2>无法捕获的情况</h2>
<pre><code class="js">function main() {
try {
setTimeout(() => {
throw new Error('async error')
}, 1000)
} catch(e) {
console.log(e, 'err')
console.log('continue...')
}
}
main();</code></pre>
<p>这段代码中,setTimeout 的回调函数抛出一个错误,并不会在 catch 中捕获,会导致程序直接报错崩掉。</p>
<p>所以说在 js 中 try catch 并不是说写上一个就可以高枕无忧了。难道每个函数都要写吗,<br>那什么情况下 try catch 无法捕获 error 呢?</p>
<h3>异步任务</h3>
<ul>
<li>
<p>宏任务的回调函数中的错误无法捕获</p>
<p>上面的栗子稍微改一下,主任务中写一段 try catch,然后调用异步任务 task,task 会在一秒之后抛出一个错误。</p>
<pre><code class="js">// 异步任务
const task = () => {
setTimeout(() => {
throw new Error('async error')
}, 1000)
}
// 主任务
function main() {
try {
task();
} catch(e) {
console.log(e, 'err')
console.log('continue...')
}
}
</code></pre>
<p>这种情况下 main 是无法 catch error 的,这跟浏览器的执行机制有关。异步任务由 eventloop 加入任务队列,并取出入栈(js 主进程)执行,而当 task 取出执行的时候, main 的栈已经退出了,也就是上下文环境已经改变,所以 main 无法捕获 task 的错误。</p>
<p>事件回调,请求回调同属 tasks,所以道理是一样的。eventloop 复习可以看这篇<a href="https://link.segmentfault.com/?enc=%2FBYmV6%2F8%2FyZ%2Bk1cjDCaDTQ%3D%3D.F%2BLcZmnJjLV9aPFgkvWxy5eyk2FtvIIFkeTjQWKoQDBbtz%2BA7CidOn64BA3V42qS" rel="nofollow">文章</a></p>
</li>
<li>
<p>微任务(promise)的回调</p>
<pre><code class="js">// 返回一个 promise 对象
const promiseFetch = () =>
new Promise((reslove) => {
reslove();
})
function main() {
try {
// 回调函数里抛出错误
promiseFetch().then(() => {
throw new Error('err')
})
} catch(e) {
console.log(e, 'eeee');
console.log('continue');
}
}</code></pre>
<p>promise 的任务,也就是 then 里面的回调函数,抛出错误同样也无法 catch。因为微任务队列是在两个 task 之间清空的,所以 then 入栈的时候,main 函数也已经出栈了。</p>
</li>
</ul>
<h3>并不是回调函数无法 try catch</h3>
<p>很多人可能有一个误解,因为大部分遇到无法 catch 的情况,都发生在回调函数,就认为回调函数不能 catch。</p>
<p>不全对,看一个最普通的栗子。</p>
<pre><code class="js">// 定义一个 fn,参数是函数。
const fn = (cb: () => void) => {
cb();
};
function main() {
try {
// 传入 callback,fn 执行会调用,并抛出错误。
fn(() => {
throw new Error('123');
})
} catch(e) {
console.log('error');
}
}
main();</code></pre>
<p>结果当然是可以 catch 的。因为 callback 执行的时候,跟 main 还在同一次事件循环中,即一个 eventloop tick。所以上下文没有变化,错误是可以 catch 的。<br>根本原因还是同步代码,并没有遇到异步任务。</p>
<h2>promise 的异常捕获</h2>
<h4>构造函数</h4>
<p>先看两段代码:</p>
<pre><code class="js">function main1() {
try {
new Promise(() => {
throw new Error('promise1 error')
})
} catch(e) {
console.log(e.message);
}
}
function main2() {
try {
Promise.reject('promise2 error');
} catch(e) {
console.log(e.message);
}
}
</code></pre>
<p>以上两个 try catch 都不能捕获到 error,因为 promise 内部的错误不会冒泡出来,而是被 promise 吃掉了,只有通过 promise.catch 才可以捕获,所以用 Promise 一定要写 catch 啊。</p>
<p>然后我们再来看一下使用 promise.catch 的两段代码:</p>
<pre><code class="js">// reject
const p1 = new Promise((reslove, reject) => {
if(1) {
reject();
}
});
p1.catch((e) => console.log('p1 error'));</code></pre>
<pre><code class="js">// throw new Error
const p2 = new Promise((reslove, reject) => {
if(1) {
throw new Error('p2 error')
}
});
p2.catch((e) => console.log('p2 error'));</code></pre>
<p>promise 内部的无论是 reject 或者 throw new Error,都可以通过 catch 回调捕获。</p>
<p>这里要跟我们最开始微任务的栗子区分,promise 的微任务指的是 then 的回调,而此处是 Promise 构造函数传入的第一个参数,new Promise 是同步执行的。</p>
<h4>then</h4>
<p>那 then 之后的错误如何捕获呢。</p>
<pre><code class="js">function main3() {
Promise.resolve(true).then(() => {
try {
throw new Error('then');
} catch(e) {
return e;
}
}).then(e => console.log(e.message));
}</code></pre>
<p>只能是在回调函数内部 catch 错误,并把错误信息返回,error 会传递到下一个 then 的回调。</p>
<h4>用 Promise 捕获异步错误</h4>
<pre><code class="js">
const p3 = () => new Promise((reslove, reject) => {
setTimeout(() => {
reject('async error');
})
});
function main3() {
p3().catch(e => console.log(e));
}
main3();</code></pre>
<p>把异步操作用 Promise 包装,通过内部判断,把错误 reject,在外面通过 promise.catch 捕获。</p>
<h2>async/await 的异常捕获</h2>
<p>首先我们模拟一个请求失败的函数 fetchFailure,fetch 函数通常都是返回一个 promise。</p>
<p>main 函数改成 async,catch 去捕获 fetchFailure reject 抛出的错误。能不能获取到呢。</p>
<pre><code class="js">const fetchFailure = () => new Promise((resolve, reject) => {
setTimeout(() => {// 模拟请求
if(1) reject('fetch failure...');
})
})
async function main () {
try {
const res = await fetchFailure();
console.log(res, 'res');
} catch(e) {
console.log(e, 'e.message');
}
}
main();</code></pre>
<p>async 函数会被编译成好几段,根据 await 关键字,以及 catch 等,比如 main 函数就是拆成三段。</p>
<ol><li>fetchFailure 2. console.log(res) 3. catch</li></ol>
<p>通过 step 来控制迭代的进度,比如 "next",就是往下走一次,从 1->2,异步是通过 Promise.then() 控制的,你可以理解为就是一个 Promise 链,感兴趣的可以去研究一下。 关键是生成器也有一个 "throw" 的状态,当 Promise 的状态 reject 后,会向上冒泡,直到 step('throw') 执行,然后 catch 里的代码 <code> console.log(e, 'e.message');</code> 执行。</p>
<p>明显感觉 async/await 的错误处理更优雅一些,当然也是内部配合使用了 Promise。</p>
<h3>更进一步</h3>
<p>async 函数处理异步流程是利器,但是它也不会自动去 catch 错误,需要我们自己写 try catch,如果每个函数都写一个,也挺麻烦的,比较业务中异步函数会很多。</p>
<p>首先想到的是把 try catch,以及 catch 后的逻辑抽取出来。</p>
<pre><code class="js">
const handle = async (fn: any) => {
try {
return await fn();
} catch(e) {
// do sth
console.log(e, 'e.messagee');
}
}
async function main () {
const res = await handle(fetchFailure);
console.log(res, 'res');
}</code></pre>
<p>写一个高阶函数包裹 fetchFailure,高阶函数复用逻辑,比如此处的 try catch,然后执行传入的参数-函数 即可。</p>
<p>然后,加上回调函数的参数传递,以及返回值遵守 first-error,向 node/go 的语法看齐。如下:</p>
<pre><code class="js">const handleTryCatch = (fn: (...args: any[]) => Promise<{}>) => async (...args: any[]) => {
try {
return [null, await fn(...args)];
} catch(e) {
console.log(e, 'e.messagee');
return [e];
}
}
async function main () {
const [err, res] = await handleTryCatch(fetchFailure)('');
if(err) {
console.log(err, 'err');
return;
}
console.log(res, 'res');
}
</code></pre>
<p>但是还有几个问题,一个是 catch 后的逻辑,这块还不支持自定义,再就是返回值总要判断一下,是否有 error,也可以抽象一下。<br>所以我们可以在高阶函数的 catch 处做一下文章,比如加入一些错误处理的回调函数支持不同的逻辑,然后一个项目中错误处理可以简单分几类,做不同的处理,就可以尽可能的复用代码了。</p>
<pre><code class="js">// 1. 三阶函数。第一次传入错误处理的 handle,第二次是传入要修饰的 async 函数,最后返回一个新的 function。
const handleTryCatch = (handle: (e: Error) => void = errorHandle) =>
(fn: (...args: any[]) => Promise<{}>) => async(...args: any[]) => {
try {
return [null, await fn(...args)];
} catch(e) {
return [handle(e)];
}
}
// 2. 定义各种各样的错误类型
// 我们可以把错误信息格式化,成为代码里可以处理的样式,比如包含错误码和错误信息
class DbError extends Error {
public errmsg: string;
public errno: number;
constructor(msg: string, code: number) {
super(msg);
this.errmsg = msg || 'db_error_msg';
this.errno = code || 20010;
}
}
class ValidatedError extends Error {
public errmsg: string;
public errno: number;
constructor(msg: string, code: number) {
super(msg);
this.errmsg = msg || 'validated_error_msg';
this.errno = code || 20010;
}
}
// 3. 错误处理的逻辑,这可能只是其中一类。通常错误处理都是按功能需求来划分
// 比如请求失败(200 但是返回值有错误信息),比如 node 中写 db 失败等。
const errorHandle = (e: Error) => {
// do something
if(e instanceof ValidatedError || e instanceof DbError) {
// do sth
return e;
}
return {
code: 101,
errmsg: 'unKnown'
};
}
const usualHandleTryCatch = handleTryCatch(errorHandle);
// 以上的代码都是多个模块复用的,那实际的业务代码可能只需要这样。
async function main () {
const [error, res] = await usualHandleTryCatch(fetchFail)(false);
if(error) {
// 因为 catch 已经做了拦截,甚至可以加入一些通用逻辑,这里甚至不用判断 if error
console.log(error, 'error');
return;
}
console.log(res, 'res');
}</code></pre>
<p>解决了一些错误逻辑的复用问题之后,即封装成不同的错误处理器即可。但是这些处理器在使用的时候,因为都是高阶函数,可以使用 es6 的装饰器写法。</p>
<p>不过装饰器只能用于类和类的方法,所以如果是函数的形式,就不能使用了。不过在日常开发中,比如 React 的组件,或者 Mobx 的 store,都是以 class 的形式存在的,所以使用场景挺多的。</p>
<p>比如改成类装饰器:</p>
<pre><code class="js">const asyncErrorWrapper = (errorHandler: (e: Error) => void = errorHandle) => (target: Function) => {
const props = Object.getOwnPropertyNames(target.prototype);
props.forEach((prop) => {
var value = target.prototype[prop];
if(Object.prototype.toString.call(value) === '[object AsyncFunction]'){
target.prototype[prop] = async (...args: any[]) => {
try{
return await value.apply(this,args);
}catch(err){
return errorHandler(err);
}
}
}
});
}
@asyncErrorWrapper(errorHandle)
class Store {
async getList (){
return Promise.reject('类装饰:失败了');
}
}
const store = new Store();
async function main() {
const o = await store.getList();
}
main();</code></pre>
<p>这种 class 装饰器的写法是看到<a href="https://link.segmentfault.com/?enc=U6i0b7lUzfa%2FUUIEt%2BDTsg%3D%3D.nbp4hjYZSsiIOj2UNOPzfJbHrGGMKBR%2By9tBMKxZLzY%3D" rel="nofollow">黄子毅</a> 这么写过,感谢灵感。</p>
<h2>koa 的错误处理</h2>
<p>如果对 koa 不熟悉,可以选择跳过不看。</p>
<p>koa 中当然也可以用上面 async 的做法,不过通常我们用 koa 写 server 的时候,都是处理请求,一次 http 事务会掉起响应的中间件,所以 koa 的错误处理很好的利用了中间件的特性。</p>
<p>比如我的做法是,第一个中间件为捕获 error,因为洋葱模型的缘故,第一个中间件最后仍会执行,而当某个中间件抛出错误后,我期待能在此捕获并处理。</p>
<pre><code class="js">// 第一个中间件
const errorCatch = async(ctx, next) => {
try {
await next();
} catch(e) {
// 在此捕获 error 路由,throw 出的 Error
console.log(e, e.message, 'error');
ctx.body = 'error';
}
}
app.use(errorCatch);
// logger
app.use(async (ctx, next) => {
console.log(ctx.req.body, 'body');
await next();
})
// router 的某个中间件
router.get('/error', async (ctx, next) => {
if(1) {
throw new Error('错误测试')
}
await next();
})
</code></pre>
<p>为什么在第一个中间件写上 try catch,就可以捕获前面中间件 throw 出的错误呢。首先我们前面 async/await 的地方解释过,async 中<code>await handle()</code>,handle 函数内部的 <code>throw new Error</code> 或者 <code>Promise.reject()</code> 是可以被 async 的 catch 捕获的。所以只需要 next 函数能够拿到错误,并抛出就可以了,那看看 next 函数。</p>
<pre><code class="js">// compose 是传入中间件的数组,最终形成中间件链的,next 控制游标。
compose(middlewares) {
return (context) => {
let index = 0;
// 为了每个中间件都可以是异步调用,即 `await next()` 这种写法,每个 next 都要返回一个 promise 对象
function next(index) {
const func = middlewares[index];
try {
// 在此处写 try catch,因为是写到 Promise 构造体中的,所以抛出的错误能被 catch
return new Promise((resolve, reject) => {
if (index >= middlewares.length) return reject('next is inexistence');
resolve(func(context, () => next(index + 1)));
});
} catch(err) {
// 捕获到错误,返回错误
return Promise.reject(err);
}
}
return next(index);
}
}</code></pre>
<p>next 函数根据 index,取出当前的中间件执行。中间件函数如果是 async 函数,同样的转化为 generator 执行,内部的异步代码顺序由它自己控制,而我们知道 async 函数的错误是可以通过 try catch 捕获的,所以在 next 函数中加上 try catch 捕获中间件函数的错误,再 return 抛出去即可。所以我们才可以在第一个中间件捕获。详细代码可以看下<a href="https://link.segmentfault.com/?enc=Vr%2Bj9ZEzAdxWA4i%2FYSezlA%3D%3D.2oH3GEKlPaq7B4646s5D2HniXa61Ee%2BYxvZxINXfyKmlj8Eovc3jNeckFpend790" rel="nofollow">简版 koa</a></p>
<p>然后 koa 还提供了 ctx.throw 和全局的 app.on 来捕获错误。<br>如果你没有写错误处理的中间件,那可以使用 ctx.throw 返回前端,不至于让代码错误。<br>但是 throw new Error 也是有优势的,因为某个中间件的代码逻辑中,一旦出现我们不想让后面的中间件执行,直接给前端返回,直接抛出错误即可,让通用的中间件处理,反正都是错误信息。</p>
<pre><code class="js">// 定义不同的错误类型,在此可以捕获,并处理。
const errorCatch = async(ctx, next) => {
try {
await next();
} catch (err) {
const { errmsg, errno, status = 500, redirect } = err;
if (err instanceof ValidatedError || err instanceof DbError || err instanceof AuthError || err instanceof RequestError) {
ctx.status = 200;
ctx.body = {
errmsg,
errno,
};
return;
}
ctx.status = status;
if (status === 302 && redirect) {
console.log(redirect);
ctx.redirect(redirect);
}
if (status === 500) {
ctx.body = {
errmsg: err.message,
errno: 90001,
};
ctx.app.emit('error', err, ctx);
}
}
}
app.use(errorCatch);
// logger
app.use(async (ctx, next) => {
console.log(ctx.req.body, 'body');
await next();
})
// 通过 ctx.throw
app.use(async (ctx, next) => {
//will NOT log the error and will return `Error Message` as the response body with status 400
ctx.throw(400,'Error Message');
});
// router 的某个中间件
router.get('/error', async (ctx, next) => {
if(1) {
throw new Error('错误测试')
}
await next();
})
// 最后的兜底
app.on('error', (err, ctx) => {
/* centralized error handling:
* console.log error
* write error to log file
* save error and request information to database if ctx.request match condition
* ...
*/
});
</code></pre>
<h2>最后</h2>
<p>本文的代码都存放于<a href="https://link.segmentfault.com/?enc=RJ%2Ftyu9yfZq9pWc8jwSQmw%3D%3D.JrfKXNP86n6AkkO8MzAt4skQV3L2%2BhyXc7xnjdbUVdqFwrBHFkwsm7Y892UxWiVQ3wIG0Yq8az7h60KM0ZlHbA%3D%3D" rel="nofollow">此</a></p>
<p>总的来说,目前 async 结合 promise 去处理 js 的异步错误会是比较方便的。另外,成熟的框架(react、koa)对于错误处理都有不错的方式,尽可能去看一下官方是如何处理的。</p>
<p>这只是我对 js 中处理异步错误的一些理解。不过前端的需要捕获异常的地方有很多,比如前端的代码错误,cors 跨域错误,iframe 的错误,甚至 react 和 vue 的错误我们都需要处理,以及异常的监控和上报,以帮助我们及时的解决问题以及分析稳定性。采取多种方案应用到我们的项目中,让我们不担心页面挂了,或者又报 bug 了,才能安安稳稳的去度假休息😆</p>
<p>最后的最后,blog地址: <a href="https://link.segmentfault.com/?enc=sungkzR6xGTJ%2F4xK9kqy5Q%3D%3D.5LxQJTpysgeU8xPRDekJR%2FvJ1jeY6YdOaGQNpYx6WZEOit7j8HyyJmiAgeDWpaZ%2B" rel="nofollow">https://github.com/sunyongjia...</a></p>
package.json 中的 Module 字段是干嘛的
https://segmentfault.com/a/1190000014286439
2018-04-10T16:06:13+08:00
2018-04-10T16:06:13+08:00
sunyongjian
https://segmentfault.com/u/sunyongjian
27
<h2>引入</h2>
<p>最近团队的一个同学在搞 npm library 源码的调试插件,因为内部的一个组件库含有大量的逻辑,在某个项目中不经意就出现一个磨人的 bug,但是组件库发布都是打包编译后的代码,而且没有 publish src 代码,不方便调试,每次还要 down 一下包的源码,再改下 webpack 的配置(比如 rule 中 exclude 去掉组件库, 改下 resolve ,在 dll 中去掉组件库)。被他们耳语目染了好几天,我就想,记得 npm 包是可以直接引源码的,大概改下 webpack 配置就可以了。然后便找到了 package.json 中 module 字段,并查漏 js 中 tree shaking 的知识,所以我并没有去研究怎么搞那样的一个插件?,而是由 package 中的 module 字段延伸出的一些知识。</p>
<h2>为何有 module</h2>
<p>查阅了 package.json 的<a href="https://link.segmentfault.com/?enc=jnj5T61zC%2BuVOMsV49UKSg%3D%3D.BA8NGafc35SeNhFx5eV%2FJAzA4T5lEyXLRwmxJ2eIuTBAN%2F98y6rZiBT5ftGu7E19" rel="nofollow">文档</a>,并没有找到 module 字段的定义,直到 google 才知道它是 rollup 中最早就提出的概念 --- <a href="https://link.segmentfault.com/?enc=vXoNKyeHpkscJm9iqiMe%2Fg%3D%3D.MUhwkhdyufwF1s6g%2BTXB1dMRwj6TqxfLK2lx4q6%2FE974sqIqdjY1y1KKjzL1WBE3C6v1Nu8Rpqllw0ww2ymNlQ%3D%3D" rel="nofollow">pkg.module</a>。大概就是最早的 npm 包都是基于 CommonJS 规范的,package.json 形如:</p>
<pre><code class="js">
"name": "package1",
"version": "1.0.0",
"main": "lib/index.js"</code></pre>
<p>当 <code>require('package1')</code> 的时候,就会根据 main 字段去查找入口文件。<br>而 es2015 后,js 拥有了 ES Module,相较于之前的模块化方案更爽滑,更优雅,并且 ES 模块也是官方标准(JS 规范),而 CommonJS 模块是一种特殊的传统格式,在 ES 模块被提出之前做为暂时的解决方案。所以 rollup 去利用 ES Module 构建,就可以利用 ES Module 的很多<a href="https://link.segmentfault.com/?enc=aG1bApq87zXgGXkmtM5Jog%3D%3D.Z07Dd5qYcmORRHidXr9d5I0HngHsDLZi8z9xQVqUZwDYvyQCHpA0s%2FxOjKXCug1cNXA6YN0qpnGYqPzedhTlPA%3D%3D" rel="nofollow">特性</a>,从而提高打包的性能,其中提升一个便是 tree shaking,这个我们后面去介绍。在这个构建思想的基础上,开发、产出的 npm 包同样使用 es6 的 module,即可同样受益于 tree shaking 等特性。</p>
<p>而 CommonJS 规范的包都是以 main 字段表示入口文件了,如果使用 ES Module 的也用 main 字段,就会对使用者造成困扰,假如他的项目不支持打包构建,比如大多数 node 项目(尽管 node9+ 支持 ES Module)。这就是库开发者的模块系统跟项目构建的模块系统的冲突,更像是一种规范上的问题。况且目前大部分仍是采用 CommonJS,所以 rollup 便使用了另一个字段:<strong>module</strong>。<br>像这样:</p>
<pre><code class="js">"name": "package1",
"version": "1.0.0",
"main": "lib/index.js",
"module": "es/index.js"</code></pre>
<p>webpack 从版本 2 开始也可以识别 pkg.module 字段。打包工具遇到 package1 的时候,如果存在 module 字段,会优先使用,如果没找到对应的文件,则会使用 main 字段,并按照 CommonJS 规范打包。所以目前主流的打包工具(webpack, rollup)都是支持 pkg.module 的,鉴于其优点,module 字段很有可能加入 package.json 的规范之中。另外,越来越多的 npm 包已经同时支持两种模块,使用者可以根据情况自行选择,并且实现也比较简单,只是模块导出的方式。</p>
<p>注意:虽然打包工具支持了 ES Module,但是并不意味着其他的 es6 代码可以正常使用,因为使用者可能并不会对你的 npm 包做编译处理,比如 webpack rules 中 <code>exclude: /node_modules/</code>,所以如果不是事先约定好后编译或者没有兼容性的需求,你仍需要用 babel 处理,从而产出兼容性更好的 npm 包。还好 <a href="https://link.segmentfault.com/?enc=gPpgFVdPfMpFeyKM90JVUg%3D%3D.UoKMoLeEPujMOz%2BHSszAkw96yQglMugtspiz1a5GJEk%3D" rel="nofollow">rollup</a> 在这方面做的不错,对于 library 开发者更友好一些。</p>
<p>同时支持的效果类似这样:</p>
<p><img width="693" alt="lib" src="<a href="https://link.segmentfault.com/?enc=QRQuMfHF6SdGGjxLJNgPPw%3D%3D.FkGhZTt8k2kFqbWySsW%2FdHBGLnVdLoauxS7RLoxXyaTwMupTLDu7QxtzaR6NjcrThNqSBjAovhakBn0kyBNys%2BHGiR1Zukju9loG9COOLHCKQNMvivsWGRsSrzAyaormKKtDVskGU983nANW0S2RKg%3D%3D" rel="nofollow">https://user-images.githubuse...</a>;></p>
<p><img width="663" alt="es" src="<a href="https://link.segmentfault.com/?enc=Xkb0zYWLRby6yM2KDCGsIg%3D%3D.l6bKlDuh7jB7PAKHScD77QwjSxGPaSY8RJKuibCJy3y28SsIVzc1moT3AhOjmya7ZrvgVRfZVBsZheVRy7o0YPWbpyvDEboUVajq%2BK%2F0zfv7wsbZw%2F0xxv2h7BGnVAVa2ashWyTeKiOOsidPN%2B1E4w%3D%3D" rel="nofollow">https://user-images.githubuse...</a>;></p>
<p>package.json 只需要对应相应的文件就可以了。</p>
<pre><code class="js">"name": "drag-list",
"version": "1.0.0",
"main": "lib/drag-list/index.js",
"module": "es/drag-list/index.js"</code></pre>
<h2>Tree-shaking</h2>
<p>tree-shaking 是近两年才在 JS 中出现的,之前没有的,而模块化的概念是一直都有方案的,只不过直到 ES Module 才有统一的标准趋势。<br>前面提到 rollup 采用 ES Module,带来的一个优点便是 tree shaking,那什么是 tree-shaking 呢。</p>
<p>有一个图片很形象的解释了它的功能。<br><img src="/img/remote/1460000014286444?w=606&h=341" alt="shaking" title="shaking"></p>
<p>tree-shaking 的功能就是把我们 JS 中无用的代码,给去掉,如果把打包工具通过入口文件,产生的依赖树比作 tree,tree-shaking 就是把依赖树中用不到的代码 shaking 掉。</p>
<p>我们通过代码了解下,webpack3.x 下打包验证 tree-shaking。</p>
<pre><code class="js">// 入口文件 index.js
import { func1 } from './export1';
func1();</code></pre>
<pre><code class="js">// export1 文件
export function func1() {
console.log('func1');
}
export function func2() {
console.log('func2');
}</code></pre>
<p>func2 方法虽然导出了,但是在 <code>index.js</code> 中是没有用到的,func2 就是无用代码,最终打包生成的 build 是应该去掉的。</p>
<p>使用最简单的 webpack 配置,不使用 babel,产出 <code>build.js</code>,export1 是这样的:</p>
<pre><code class="js">/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = func1;
/* unused harmony export func2 */
function func1() {
console.log('func1');
}
function func2() {
console.log('func2');
}
/***/ })</code></pre>
<p>我们发现有两行注释,<code>/* harmony export (immutable) </code> 表明代码是有用的,<code>unused harmony export func2</code>表明 func2 是无用代码,说明 webpack 已经识别。不过 webpack 仅仅是做了“标记”,去掉这些代码的能力,是通过插件实现的,常用的便是 unglify。在 plugins 用启用 UglifyJsPlugin 后,查看下 build。</p>
<pre><code class="js">// webpack.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
...
plugins: [
new UglifyJsPlugin(),
]
}
</code></pre>
<p><img src="/img/remote/1460000014286445?w=791&h=65" alt="ugly-export1" title="ugly-export1"></p>
<p>上图即编译后 export1 模块的截图,可以看到 func2 已经被去掉了。不过在我开启 babel-loader 以后,babel 配置就是一个简单的 <code>"presets: ["env"]"</code>,却发现 func2 又回来了,如下:</p>
<p><img src="/img/remote/1460000014286446?w=499&h=367" alt="babel-export1" title="babel-export1"></p>
<p>这是为什么呢。因为 tree-shaking 是依赖 ES Module 的静态加载,而 babel-presets-env 中是包含 <code>ES2015 modules to CommonJS transform</code> 的 plugin,也就是转化成 CommonJS,所以无法识别哪些代码是未引用的,也就是无法 tree-shaking,所以 babel transform 的时候应该保留 ES Module。</p>
<p><img src="/img/remote/1460000014286447" alt="modules-false" title="modules-false"></p>
<p>通过 presets 的 option 选择,设置 modules 为 false 即可。</p>
<p>另外,tree-shaking 并不是完美无缺的,有些情况还无法识别。比如你导入了一个模块,但是这个变量代码中未使用,是不会去掉的,细节可以看这篇<a href="https://link.segmentfault.com/?enc=VHxigqKb1iRxIsRnkwkCxQ%3D%3D.gAplGJ1RvJmX96sg%2FmmYcdVYBlWdWCWz7v7DcYKLOYJWfBZ4tymbb2ctODAqX0XE" rel="nofollow">文章</a></p>
<h3>为什么是 ES Module</h3>
<p>ES Module 之前的 JS 实现模块化,都是基于第三方依赖管理实现的,比如 requirejs,seajs,这都是在代码执行的时候,才知道依赖了哪些模块,常用的 node 中的 commonjs,也是如此</p>
<pre><code class="js">(function (exports, require, module, __filename, __dirname) {
// YOUR CODE INJECTED HERE!
});</code></pre>
<p>所以,当 ES Module 在代码不执行的时候,就可以知道模块的依赖关系,就不难理解是为什么了。</p>
<h2>思考</h2>
<p>我的本意是,可否利用 module 字段的特性,让我的 npm 包支持引入源码,从而可以实现源码调试、并且后编译的效果,不过从目前的规范看来,内部还是可以试一下的,开源的包最好不要这样做,除非你有自己的一套规范以及后编译生态。虽然没有达到目的,不过也后知后觉的了解到 module 的用意,以及 rollup 在开发包时候的妙用,以及 tree-shaking 并不是自己了解的那么美好。</p>
<h3>相关推荐</h3>
<p><a href="https://link.segmentfault.com/?enc=Y0Am38Oo%2F4EJMfVvT861xA%3D%3D.pTFMO74Yt53rXTsdJEjQuXp7x5GEvmdXF4UcE5A1l3oNnYO%2Bd5%2FDb04RZsrULFHh" rel="nofollow">你的Tree-Shaking并没什么卵用</a></p>
<p><a href="https://link.segmentfault.com/?enc=7TJbx9skgXN0n4nhRs0Auw%3D%3D.xwrVHLxk2pDX%2B9%2Fayf0gmRrCxkfxjaLsOLoR3YN6%2BdVIsWZJm0vOZJtSHZ0subDR" rel="nofollow">【译】如何在 Webpack 2 中使用 tree-shaking</a></p>
<p><a href="https://segmentfault.com/a/1190000012515648">手把手带你走进下一代的ES6模块打包工具—Rollup</a></p>
<p>原文:<a href="https://link.segmentfault.com/?enc=t8PUhetyFyshyqwP9xLSFA%3D%3D.ewO%2Bo74o0MnAIoQTuxU9Ig8ChY%2BekPsHeMVRzfFOb8TV%2B8ohWr8ztPoU2Ks%2FMspQ" rel="nofollow">https://github.com/configu/bl...</a></p>
前端面试总结(at, md)
https://segmentfault.com/a/1190000012468918
2017-12-18T10:37:32+08:00
2017-12-18T10:37:32+08:00
sunyongjian
https://segmentfault.com/u/sunyongjian
15
<h2>引入</h2>
<p>面试过去了这么久,把八月份面试题和总结发一下吧,虽然年底大家可能都不换工作~ 还是可以看看的。</p>
<p>关于面试,引用叶老湿的一句话。你的简历是自己工作的答卷,项目经历是你给面试官出的考纲。所以,我的面试一定是与我的简历、工作经历相关的,一些面试题并不一定适用于任何人,但是你可以从中了解他们考察的点,以及侧重点。基础知识可以查漏补缺。</p>
<p><a href="https://link.segmentfault.com/?enc=tY%2FXkUnouS3CZs5tcHm86A%3D%3D.qu9LkRPOlWGt93PygtxSx10w31KwVEqU9OlqYb6Go2QOwg%2Fll39zACz%2FTvMS0wE6" rel="nofollow">原文链接</a></p>
<h2>moka</h2>
<blockquote>一家小型创业公司。因为同事有去面过的,他说面试感觉挺 diao 的,我就想去试一下啦,拿它先热热身,就从boss上投了简历。</blockquote>
<h3>一轮</h3>
<ul>
<li>用 setTimeout 实现 setInterval,可不必关心返回值的类型。<p>我听错了条件,我以为要注意返回值的类型必须跟原生的一样,也就是返回一个 number,并可以 clear。无清除定时器功能的马上就写出来了,但是加清除定时器,还有返回值的问题,就费了半天劲... 也没弄明白。面试官比较着急了,跟我沟通的时候,我才知道返回值不一定非要跟原生的一样。</p>
</li>
<li>给了一个栗子,结合 <code>pdf.js</code> 的示例代码,找出这个栗子中为什么分辨率很低。主要是看你通过调试去解决问题的能力。</li>
<li>然后就各种结合项目各种问呗,如何解决问题的,如何团队协作沟通的。</li>
<li>还给了我一个题,让我回去用 React 实现一个群组的 CheckBox,其实最后看就是 treeselect 的雏形啊。<a href="https://link.segmentfault.com/?enc=N3X4iEfDbxfitTCG1I4EwA%3D%3D.cLEWQOmiKLWlqoifsGy8eniEyZGNtFua9vm67JKbXrVchWW96eJlq%2Bn0p6BEAM0H" rel="nofollow">我的实现</a>
</li>
</ul>
<h3>二面</h3>
<ul>
<li>自我介绍</li>
<li>最近做的一个项目,其中的难点,过程。</li>
<li>最近做没做过技术调研,我捡我了解的说,比如redux和mobx的区别。</li>
<li>做技术分享的时候会关注哪几个点,<br>答: 它的实现原理,简单的一张图。一个简单的demo引入,并看一些实现。充分表现它的优势,然后跟现有的技术栈是否能结合,快速上手。有什么缺陷。</li>
<li>在第一家公司有没有记忆深刻的项目,经历。项目的话就是最开始的时候,让我自己写一个页面的时候,在一周之内搞完。。有一个特效是在轮播图上加放大镜,用swiper和自己写的放大镜。当时各种努力工作终于把这个功能做完,但是忽略了兼容性的问题。在ie8,9 是无法滚动的。然后立马切换到另一个slide.js,替换上之后放大镜失效了。当时好像是插件内部阻止了冒泡行为,我那个放大镜取不到事件了。 因为已经到deadline了,通过我们的leader跟运营人员沟通,先不要这个功能了。就上了。后来查的时候,swiper2的兼容性比较好,ie8.</li>
<li>个人的缺点 。技术上就是还是比较low, 然后就是个人的毅力感觉一般,但是最近健身感觉自己很不错。</li>
<li>怎么算是分布式的应用呢,属于自己挖坑。</li>
<li>问了我好多不是技术“点”的问题,都是大的面,细节很少。记不清了。</li>
</ul>
<h3>三四面</h3>
<p>就是 hr 和 ceo 了,就是问一些离职原因,跟公司价值观方面的问题了。</p>
<h3>总结</h3>
<p>无论是一面的 pdf,让我实现一个功能,还是二面的问题,都是偏重于项目实战的。他们想要的是入职就能干活的人,先能负责某块开发任务。并且在独立解决问题,团队分享方面有所表现。</p>
<h2>腾讯 (omg)</h2>
<h3>一面</h3>
<ul>
<li>平常开发怎么设计 react 组件的。比如 container 组件,业务组件等等的。</li>
<li>手写一个观察者,发布订阅模式。</li>
<li>什么是函数式,跟面向对象有什么区别,因为我简历写了正在学习 fp。</li>
<li>科里化,写一个</li>
<li>cache-control,http-only</li>
<li>url输入到服务器中间发生</li>
<li>redux源码</li>
<li>vue跟angular像不像</li>
<li>vue跟react的模板渲染分别是怎么做的</li>
<li>实现垂直水平居中</li>
</ul>
<h3>二面</h3>
<ul>
<li>react 组件的结构,其实是问组件实例对象的结构,问题没理解。我说在 children 里就能看到。他问是吗,是什么样的,你平常应该用过 children 啊,应该知道啊。我推测 children 是数组,因为有 children.map 方法。他又问一定是吗。<p>这个问题上来就暴露了我对这些细节是没有关注的,而他期望的是,你用 React,连它的实例都没看过,children 可能是一个,可能是多个也不清楚。</p>
</li>
<li>那我再问一个特别简单的问题,组件之间的通信,比如父到子,子到父。再说一个兄弟组件怎么通信,嵌套了好多层呢?<p>嵌套多层的他直接说有好几种方法,你随便说几种吧。我从来木有总结过,整个人有点懵逼,也没有回答好。后来整理了一下 <a href="https://link.segmentfault.com/?enc=xcqhqf3zmjsMuSW7GQpxDQ%3D%3D.d4lOtiNINMNCMuIJfN81Us0h9D50x7Zqb9V9NmIkZ9Nie%2B0c6VkahAOF2oY9YLE6" rel="nofollow">React组件之间的通信 </a></p>
</li>
<li>
<p>再问一个稍微有难度的,组件的 state 嵌套多层的时候,我要让最里面那个 state 变化,怎么做。我开始没理解这是啥意思,他解释说 state 是 <code>{ a: { b: { c: { d: 1 } } }</code> 这样,你 setState 的时候,怎么改属性 d 的。我说一层一层的找进去,setState 新对象。他又说那几层是可以的,假如说一百层呢,也这样吗?我就懵逼了。</p>
<p>他说 react helper里面有个 immutable 的库,就是用来处理这种情况的。</p>
<p>immutable 我是知道的,但是我了解的是通常处理不可变数据的。后来查了一下,应该是这种操作。</p>
<pre><code class="javascript">import { formJS } from 'immutable';
const obj1 = { a: { b: { c: { d: 1 } } } }
const obj2 = Immutable.fromJS(obj1).updateIn(['a', 'b', 'c', 'd'], value => value + 1)
console.log(obj2.toJS); //{ a: { b: { c: { d: 2 } } } }
</code></pre>
</li>
<li>你简历上说你看过 redux 的源码,简单说一下吧。<br>终于认可了我一次,说这个确实看过,能说出来。我有点尴尬了。</li>
<li>你说你读过 《深入浅出 nodejs》,那你在项目中用过 node 吗。</li>
<li>移动端做过吗,适配问题。750px 的设计稿,你怎么做适配,给几种方案。</li>
<li>rem的原理是什么。这个答的也不好</li>
<li>然后就是让我问了一些问题,还跟我说你工作才一两年,你要的这个薪资肯定给不到,腾讯卡工作年限很严格,评级方面。最后就是现在不会直接给你面试结果,他们会横向比较几个候选人。</li>
</ul>
<h3>总结</h3>
<p>腾讯面试的感觉就是,没有那么正式,都是部门的技术直接联系的你,然后二面就是部门负责人了,决定了是否入职。二面给我的感觉就是,他从 React 入手问一些基础的问题(一些需要留心注意的)。如果没有注意这些点,没有总结,或者看过类似的总结文章,是很难有条理的回答出来的。其实我不太喜欢他这种,“我觉得很简单... 你就应该会的” 的方式。但是这次我发现了自己的一个问题,就是面试的时候爱说一些了解的名词,但是实际没用过,恰好这个面试官都比较深入了解,也比较反感,他认为你这是 “不懂装懂”。所以面大公司,不会的还是尽量要说不会,不要犯了面小公司的错误。自己也确实没有做到注意总结,了解也确实比较浅显,为自己以后的工作学习敲响警钟。</p>
<h2>阿里 (天猫)</h2>
<h3>一面</h3>
<blockquote>由于一面是电话面试,主要问了好多项目的问题,如何解决问题的。</blockquote>
<ul>
<li>比如问其中一个项目,查询多字段对应处理的问题,sql语句怎么拼的,你的自定义配置是怎么回事。</li>
<li>session 和 cookie 的区别</li>
<li>使用 redux 和 mobx 的区别</li>
<li>什么是 bfc</li>
<li>node了解多少,用过吗</li>
<li>算法怎么样</li>
<li>好多问题,有点忘了,就是各种知识点... 其实百分之八九十能答出来,他也觉得不错,让我后面好好准备,架构方面,原理方面。</li>
</ul>
<h3>二面</h3>
<blockquote>面对面</blockquote>
<ul>
<li>介绍一下你的工作历程(经验)</li>
<li>我们找一个产品或者项目具体聊聊,让我自己说。</li>
<li>介绍一下项目,技术栈,我是不会问的。</li>
<li>你用过 vue, react。你觉得他们有什么区别。</li>
<li>说一下你是怎么用的 redux</li>
<li>我自己提到了通常放在 container,他问了一个 connect 怎么做到注入 state 的。我就说 connect 是一个高阶组件,注入进来的 store,通过 state 维护?他又问那是怎么实时 render 的?我说 connect 里面应该有 监听 store 的 changes 吧。最后就说没看过 react-redux 的源码,只看过 redux 的源码。</li>
<li>看了我的一篇博客,问了一个 this.children 是实例还是 class</li>
<li>react router 的 hash ,history api 有什么区别,我说了一些表层的区别,url 的不同,实现的原理不同。他继续问还有什么内部的不同吗,我就说不了解了。</li>
<li>mobile 端有接触吗,我说做的不多,主要还是 pc 端吧,我说你可以提问。问了一个viewport的,如果 width=device-width,iphone6 上页面宽是多少,plus 是多少。如果 viewport 里设为 width=375,plus上会缩小还是放大。</li>
</ul>
<h3>总结</h3>
<p>让我自己去聊项目,去考察我的组织、表达能力,以及沟通能力。而且这也最大程度上能了解到,关于这个项目,你自己思考了多少,主导程度。然后考察主要使用的技术栈,了解的深度如何。然后就是他们业务常用的点。刚面完,自己感觉其实还可以,因为都能回答出来,但是可能深度还不够吧。天猫的要求还是比较高的,而且面试官是从杭州过来专门面试的,也有很多候选人同时面试,只能说明自己在这里面还不够出色,不能脱颖而出。</p>
<h2>滴滴</h2>
<h3>一面</h3>
<ul>
<li>简单介绍一下自己</li>
<li>上来就是一个数组的构造函数上提供了什么方法,然后我就一顿说,副作用的,增删改的,map的</li>
<li>indexOf和findIndex的区别,我说没用过 findIndex</li>
<li>
<p>写了一道题,是关于作用域的题。</p>
<pre><code class="javascript">function fun(n, o) {
console.log(o);
return {
fun: function(m) {
return fun(m, n);
}
}
}
fun(0).fun(1).fun(2);
let fn = fun(0).fun(1).fun;
fn(2);
fn(3);</code></pre>
<p>这种题好好看看一般能答出来。</p>
</li>
<li>react 中的某个组件嵌套很深,怎么传递 props,很不错,之前整理过。</li>
<li>redux 和 mobx 的区别。我就从实现的原理,使用方式,结合 react 等方面阐述了一下。</li>
<li>你刚提到了 observer,这觉得它们是怎么实现的。redux(listerners),mobx(get,set) 的方式都说了一下。</li>
<li>observer 是什么模式。</li>
<li>还了解其他的设计模式吗。我他妈一激动把面向对象说出来了,这当然不是设计模式了,是编程思维。</li>
<li>js 模拟一个并发</li>
<li>实现两个 setTimeOut之后再做什么。1.原生实现一个串行的队列。2. 用 promise 去封装一下,然后用 promise.all/generator/async.</li>
<li>算法,问我快速排序,说一下它的原理,我说忘了。</li>
<li>问 es6 主要用那些新语法,我说了几个,箭头函数,解构赋值,const 声明等等... 他说最常用的不应该是 class 吗...</li>
<li>es5 实现一个继承,我差点给他写四五个。刚学 js 的时候整理过 <a href="https://link.segmentfault.com/?enc=iT5cPskUUGj0iYyabcQ0UQ%3D%3D.9TQAZ8j2s4tNy%2F026m7SqAKk%2FeydgxYecrEaV3Ct9uvOjNM4NbIt48zK6JmfeLt1" rel="nofollow">link</a>
</li>
<li>es6 怎么判断一个数组? isArray 啊。 [].isArray ? 我说 Array.isArray(),类上的静态方法。</li>
<li>css 清除浮动用什么,我说就两种,一个是 css 树形 clear,一个触发 bfc。</li>
<li>css3 动画有什么,怎么用。就是考察 transition,translate,animation 啥的。</li>
<li>less 中的 & 代表什么意思。这个倒没关注打包后的代码,想了一下应该是上级作用域的选择器...</li>
<li>自动化工具用什么,我说打包的话就用 webpack,其中又有各种配置,预处理,编译啥的。配置文件自己写过吗,我说写过。</li>
<li>搭建过 react 项目的架子吗,当然。</li>
<li>最后一个问题是,<br><code>function fun() {}</code> 的原型指向哪里 ?<br><code>Function.prototype</code>。</li>
</ul>
<h3>二面</h3>
<ul>
<li>顺了一下我的履历,问这次为什么打算跳槽,再上家公司时间也不长,期待一个什么样的工作。我说希望有一个技术都比我牛逼的团队。</li>
<li>假如说团队里的大部分人技术都不如你,怎么办。我说那也挺好,我可以发挥我的长处,做一个技术 leader 的角色。又问那如果公司不给你一个 leader 的级别呢... 我就说那也挺好的,你可以成为一个重要的角色,不可或缺的人。</li>
<li>如果你作为一个 leader,你需要什么样的下属。 我说一个不可或缺的人,一两个技术比较好的。其他不需要那么技术好,只要能胜任工作就行了。</li>
<li>你觉得你是容易相处的人吗</li>
<li>看你是信息与计算科学的,算法应该不错,手写一下快速排序吧。?我说忘了,我可以写一个冒泡或者插入,然后就写了一个冒泡。</li>
<li>怎么判断一个对象是 object,还是 array。用 <code>Object.prototype.toString.call() </code> 吧。</li>
<li>写一个节流的函数,我之前刚好看了,写了这么一个东西。</li>
</ul>
<pre><code class="javascript">
const throttle = wait => fn => {
var timer;
return (...args) => {
if (!timer) {
timer = setTimeout(() => timer = null, wait);
return fn(...args);
}
}
}</code></pre>
<p>他说,怎么没有清定时器。我说节流函数分两种的吧,举个例子,我这个是按钮点击后,500ms 后的点击才会执行。还有一种是两个点击间隔 500ms 之内,只执行一次,防止连续快速点击。后面的没写,大概是这样</p>
<pre><code class="javascript">const throttle = wait => fn => {
var timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, wait)
}
}</code></pre>
<ul>
<li>什么是科里化,怎么实现一个 curry。curry 利用了什么特性,他说是其实就是闭包</li>
<li>设计模式了解吗?说一下观察者,我说我简单写一个实现吧。就写了一下。</li>
<li>react 的生命周期介绍一下</li>
<li>现在的薪资</li>
<li>你有什么问题<p>我问了一个很扯的问题,我能给团队来带什么?面试官说,不能说带来什么吧。一些本来就存在的问题,很难说加入一个人就带来质的改变什么的。最主要的还是能够独当一面吧,能够负责一个模块。</p>
</li>
</ul>
<h3>三面</h3>
<ul>
<li>前两面的感觉如何。</li>
<li>有没有什么遗憾的地方?就是前两面没大好的地方。</li>
<li>15年毕业,你的第一家公司是 xxx,在那主要收获了什么?</li>
<li>我提到了不断自驱,学习。</li>
<li>最近解决的一个棘手的问题,前端拼 sql</li>
<li>react 的生命周期</li>
<li>现在的薪资,期望薪资</li>
<li>你有什么问题,问了一下面试官的职位,聊团队,以及我加入的项目要做的东西。</li>
<li>最后,玩游戏吗,不玩。打篮球吗,不打。在健身</li>
</ul>
<h3>总结</h3>
<p>来滴滴面试的时候,很自信,所以自我感觉良好。一面是特别注重基础,各个知识点的问答,想起什么来问什么。看你对基础的掌握情况吧。二面我感觉更多的在看你的情商如何了,当然我的情商不是很高,面试官最后也说了,如果我更 open 一些,会更好。说我的简历上也能看出自己尝试了很多东西,可以去突破一下。当然也问了一些技术的问题。三面是团队的前端负责人,从价值观,个人的成长规划,解决问题的能力去考察,也了解了一下期望薪资。最后的问题就是看有没有共同的爱好吧...</p>
<p>相对 tmall,tx 来说,didi 考察基础,但是又没那么深入,主要是对应聘者的级别要求不一样吧。tmall 的社招不仅是看你干活能力,更多的是你会不会去 push 一些东西,其实就是找亮点,你一定要有积极性,比如说推动某个技术在项目的应用,落地。做出一些好的工具,做一些深入的研究,对业务带来了积极影响。</p>
<h2>腾讯 (兴趣阅读)</h2>
<h3>只有一面</h3>
<ul>
<li>各种项目各种问,问的特别细,每一个地方的实现,问你的思考 ?,以及怎么做规划排期,怎么去处理 bug 等等。<p>这块就平常自己项目中,做的足够细致就行了,有时候你提到一个点,他都会对这个点进行深挖,看你对项目的细节是否足够了解,另外还有项目整体上的了解。</p>
</li>
<li>mobx redux 的区别,从简历上挖掘出的点,去看你是否总结。<p>mobx 我做过简单的<a href="https://link.segmentfault.com/?enc=xqG6xjMLYo7JOMB%2BXn%2BWVg%3D%3D.ojaYhycyULULKsSIMbrsblDP3GwC%2FQFA7T2QpbG0G%2BS%2BfNBW4fkRjeYqum9af4ph" rel="nofollow">总结</a>。</p>
</li>
<li>自己做过的前端优化。经典的面试题,回答的点还是很多的,从 web 来说,从[浏览器] 发起一个 [请求],服务器[返回],[页面渲染],[css渲染],都有的说,我也有简单的<a href="https://link.segmentfault.com/?enc=35c%2FsfOpPnxnEUhw%2FOhJ6A%3D%3D.ZgGZCnS6RJap6kYl6QY69DywR51Fc7RHG4Ah7143sEWpIyUX%2BEup9d2oMJVYaR4r" rel="nofollow">总结</a>
</li>
<li>react 和 vue 有什么区别啊,特别注重考察个人的总结;</li>
<li>对前端安全的认识</li>
<li>劫持遇到过吗,主要有什么。我说了一个 dns 劫持。</li>
<li>webpack都是怎么用的</li>
<li>webpack.dll ? context这个参数是干嘛的,我忘了...</li>
<li>react的性能优化有没有了解过啊,你都是怎么做优化的。<br> 说实话,react 的优化我很少做 - -,通常 react 的 diff-dom 带来的便利很少关注它的性能问题。不过面试我当然不会说这个,就说了几个点,shouldUpdate,pureComponent,immutable 等等的。不过他似乎不是很满意,继续问我有没有更深入的,全面的优化。我就说局限于此了。</li>
<li>问我 angular 用的怎么样,我说项目中没用过。</li>
<li>最后聊了一些他们日常的工作,主要是后台管理,运营去推送阅读等。项目目前是 angular 做的,后期会像 React 转。</li>
</ul>
<h3>总结</h3>
<p>自己觉得能回答上来个 70% 左右,他最终告诉我,今天的表现能打个七八分吧(满分十分)。然后他就去跟他的 leader 沟通了,回来也没直接拒绝我,就给了我一些建议,要对项目整体的把控多一些,项目的思考,横向多去了解,跟你配合的同学(rd ?)做了哪些工作,还说不过也不着急,毕竟你还年轻。黑人问号脸。</p>
<p>自己的确没有去带整个项目的经历,更多的是个人负责一个模块,虽然对其他人的工作也有了解,技术上的架构也略知一二,不过还是平常的思维有局限性吧,对整体的思考太少,不过我觉得这虽然是今后发展的方向,但是目前还是专精某些点,纵向多做一些深入工作吧。</p>
<h2>美团 (商超)</h2>
<h3>一面</h3>
<ul>
<li>从html,css开始</li>
<li>了解盒模型吗,现在给定一个 width:200px的盒子,他的width,padding,border,margin都是怎样的</li>
<li>html的标签分哪几种,列举一些</li>
<li>img是哪种? 我回答行内。</li>
<li>然后他就问一个img和一个span,里面一段文字,怎么排列。我说上下,img不是行内吗,怎么会上下。。</li>
<li>如果实现文字环绕排列 img,应该怎么做。图片 float</li>
<li>
<p>这个图片不定宽高,怎么实现在盒子内垂直水平居中。</p>
<ol><li>flex 2. transform: translate(-50%, -50%) 3. display:table-cell</li></ol>
</li>
<li>说一下float这个属性</li>
<li>你提到了bfc,有两个盒子,margin重叠的问题,怎么解决</li>
<li>有一个input,怎么统计他的输入 0/50,除了事件还能怎么统计</li>
<li>css3了解吗,主要用什么,animation,transition,translate,transform 这四个是干嘛的</li>
<li>实现一个进度条加载,从0 到100</li>
<li>'use strict' 是怎么解析的</li>
<li>setTimeout,setInterval。它的参数,如果在setInterval里5ms不断调用,会有什么问题吗</li>
<li>setImmediate 和Process.nextTick 的区别</li>
<li>写一个检测数据类型的方法。直接写了个 Object 的 toString</li>
<li>js 里面的数据类型,把布尔忘了- -</li>
<li>什么是柯理化啊</li>
<li>jquery 用过吗,</li>
<li>https 了解吗</li>
<li>https 一定安全吗,然后怎么解决呢。实现一个站点从 http 到 https 的迁移</li>
<li>localstorage 的跨域问题,最大存储是多少,超出了会怎么办</li>
<li>跟 cookie 的区别</li>
<li>sessionStorage 的区别</li>
<li>http的状态码,200,500,301,302,304</li>
<li>一个url从浏览器输入到解析经历了什么</li>
<li>你提到了三次握手,四次挥手是干嘛的</li>
<li>忘了...</li>
</ul>
<h3>二面</h3>
<ul>
<li>各种优化</li>
<li>之前做的项目大概介绍一下,技术栈等等</li>
<li>各种项目里的问题</li>
<li>有没有什么优化的经验,方案。不局限于打包。</li>
<li>webpack 打包的优化</li>
<li>最近项目解决的一个技术难点</li>
<li>最近在学什么,对什么感兴趣</li>
<li>最近做的最成功的一次分享是什么</li>
<li>是不是热衷于团建。</li>
<li>现在已有 getData(id, callback) 方法,根据 id 发一个请求,并把 data 传给 callback。你写一个getDataCache的方法,实现相同的id,只发一次请求,即数据可以被缓存起来。</li>
<li>说一下http缓存,其实就那几种。</li>
<li>你们用的哪种,我说 304 多一些,为什么不用 200,效率方面 200 更高一些啊。我说的确,我们也是通过 hash 给文件打版本号,结合 maxage,让浏览器判断要不要重新请求的。</li>
<li>怎么学习前端知识啊</li>
<li>最近在看什么书</li>
<li>除了写blog,还有什么其他的,工作之外做的</li>
<li>还有几个忘了</li>
</ul>
<h3>三面</h3>
<blockquote>三面是一个技术总监,感觉人挺不错的。</blockquote>
<ul>
<li>看你之前都是一年一跳槽,是什么考虑呢</li>
<li>为什么要从事前端呢</li>
<li>看你学的是数学,为什么最后学前端。为什么上大学的时候没好好学</li>
<li>对美团怎么看</li>
<li>问你一个算法题</li>
</ul>
<p>规律是这样的:</p>
<pre><code>A B C D ... Z AA AB AC ... AZ BA BB ... CA ... ZA ... ZZ AAA AAB ...
对应:
1 2 3 4 ... 26 27 </code></pre>
<p>做一个程序,让输入一个数字,输出具体的值。比如输入 27,输出 AA。这个问题我也描述不太清,其实就是一个类似进制转化的问题。</p>
<ul>
<li>'192.168.0.1'把它转化成位数。进制的偏移问题</li>
<li>你觉得自己有什么缺点</li>
<li>你觉得你之前做的一件最酷的事情是什么</li>
<li>你有什么想问的。大概了解了一下美团超市是干嘛的,介绍的时候还问,想不想做我们的这个商超,感兴趣吗。当然说感兴趣</li>
</ul>
<h3>总结</h3>
<p>第一面特别细,之前很少问到的 html,css,还有 jQuery,都问了。这些地方还是多少能记着一些的,能回答个差不多。并没有问你项目中使用的框架,可能关注的点不同,他们更关注基础吧。二面其实就关注性能优化比较多了,还问了很多学习,分享方面的,估计是想看你是不是热爱学习啊,学习方法,喜欢分享的话能给团队带来新鲜度,大部分团队都是有每周的分享的。他问的问题我都是比较喜欢的,开放性的话题更多一些,你能更好的展现自己。三面问了一些基础的计算机问题,这些是我不擅长的,不过大概说了一下思路,并用 js 去实现。还有很多就是面试常问的问题啦,面试前一定要考虑,做准备。比如说做过最酷的一件事,无论是什么事,只要你自己觉得酷就行了,个性面试官。总结下来就是,对这个部门的面试感觉,觉得不错,进去做的工作是移动端,这也是我之前很少做的,也不用什么 react。这个部门是一个新成立的,团队也是从 0 开始,这种团队也有好处也有坏处,好处是大家都是新人,项目也是全新的,适合你发挥。不好处就是太新了,加班是肯定的,压力也会大一些,你长时间都会处于业务中。</p>
<h2>最后</h2>
<p>硬广。我们团队在招高级前端开发,大部门是平台技术部下的平台前端,我们负责的方向是专快司乘运营等大型管理系统,会用 React 是硬性条件,期待有 node 开发经验,基础好,爱学习是基本,要有积极主动的推动能力。另外,不喜欢做 pc,不用 react 的,或者有其他喜欢的部门,也可以推荐。先发邮件带简历联系我吧。</p>
<p>邮箱:sunyongjian0108@gmail.com</p>
不可变数据
https://segmentfault.com/a/1190000011992645
2017-11-13T23:25:26+08:00
2017-11-13T23:25:26+08:00
sunyongjian
https://segmentfault.com/u/sunyongjian
3
<h2>不可变数据</h2>
<h3>引入</h3>
<p>我是通过使用 React 才去关注 immutable data 这个概念的。事实上,你去搜 immutable 的 JS 相关文章,也基本都是近两年的,大概是随着 React 的推广才备受关注。但是这篇文章不会去介绍 React 是如何在意 immutable data 的,而是从原生 JS,写一些自己的思考。</p>
<p>个人 blog,欢迎 star。<a href="https://link.segmentfault.com/?enc=IHtEI4etTxC8HTcQSd5kzg%3D%3D.0k8dV%2FknyZ55WK6dUvi8ENMvK6ssTn8S6yhXo61Q8Fk%3D" rel="nofollow">https://github.com/sunyongjian</a></p>
<h3>可变/不可变对象</h3>
<p>可变对象是一个可在其创建后修改状态的对象,而不可变对象则是创建之后,不能再修改状态,对其任何删改操作,都应返回一个新的对象。</p>
<p>一个例子开始:</p>
<pre><code class="js">var x = {
a: 1
}
var y = x;
x.a = 2;
console.log(y); //{ a: 2 }</code></pre>
<p>这在我们刚开始学 js 的时候就知道了,js 中的对象都是参考(reference)类型,<code>x = y</code> 是对象赋值引用,两者共用一个对象的空间,所以 x 改动了,y 自然也改变。</p>
<p>数组也是一样的:</p>
<pre><code class="js">var ary = [1, 2, 3];
var list = ary;
ary.push(4);
console.log(list); // [1, 2, 3, 4]</code></pre>
<p>在 JS 中,objects, arrays,functions, classes, sets, maps 都是可变数据。<br>不过字符串和数字就不会。</p>
<pre><code class="js">var str = 'hello world';
var sub = str;
str = str.slice(0, 5);
console.log(sub); // 'hello world'
var a = 1;
var b = a;
a += 2;
console.log(b); // 1</code></pre>
<p>像这样,<code>sub = str</code>,<code>b = a</code> 的赋值操作,都不会影响之前的数据。</p>
<h3>为什么要有不可变数据</h3>
<p>首先,不可变数据类型是源于函数式编程中的,是一条必备的准则。函数式对数据处理的时候,通过把问题抽象成一个个的纯函数,每个纯函数的操作都会返回新的数据类型,都不会影响之前的数据,保证了变量/参数的不可变性,增加代码可读性。</p>
<p>另外,js 中对象可变的好处可能是为了节约内存,相比字符串、数字,它承载的数据量更大更多,不可变带来每次操作都要产生新的对象,新的数据结构,这与 js 设计之初用来做网页中表单验证等简单操作是有悖的。而且,我们最开始也确实感受到可变带来的便捷,但是反之它带来的副作用远超过这种便捷,程序越大代码的可读性,复杂度也越来越高。</p>
<p>举一个栗子:</p>
<pre><code class="js">
const data = {
name: 'syj',
age: 24,
hobby: 'girl',
location: 'beijing'
}
// 有一个改变年龄的方法
function addAge(obj) {
obj.age += 1;
return obj;
}
// 一个改变地址的方法
function changeLocation(obj, v) {
obj.location = v;
return obj;
}
// 这两个方法我期待的是得到只改变想改变的属性的 data
console.log(addAge(data));
console.log(changeLocation(obj, 'shanghai'));</code></pre>
<p>但实际上 addAge 已经把原始数据 data 改变了,当我再去使用的时候,已经是被污染的数据。这个栗子其实没有那么的典型,因为没有结合业务,但是也可以说明一些问题,就是可变数据带来的不确定影响。这两个函数都是有“副作用”的,即对传入数据做了修改,当你调用两次 addAge,得到的却是两个完全不同的结果,这显然不是我们想要的。如果遵循不可变数据的原则,每次对原始数据结构的修改、操作,都返回新的数据结构,就不会出现这种情况。关于返回新的数据结构,就需要用到数据拷贝。</p>
<h3>数据拷贝</h3>
<p>之前 <code>y = x</code> 这样的操作,显然是无法完成数据拷贝的,这只是赋值引用,为了避免这种对象间的赋值引用,我们应该更多的使用 <code>const</code> 定义数据对象,去避免这种操作。<br>而我们要给新对象(数据)创建一个新的引用,也就是需要数据拷贝。然而对象的数据结构通常是不同的(嵌套程度等),在数据拷贝的时候,需要考虑到这个问题,如果对象是深层次的</p>
<p>比较一下 JS 中几种原生的拷贝方法,了解他们能实现的程度。</p>
<h4>Object.assign</h4>
<p>像这样:</p>
<pre><code class="js">const x = { a: 1 };
const y = Object.assign({}, x);
x.a = 11;
console.log(y); // { a: 1 }</code></pre>
<p>诚然,此次对 y 的赋值,再去改变 x.a 的时候,y.a 并没有发生变化,保持了不变性。你以为就这么简单吗?看另一个栗子:</p>
<pre><code class="js">const x = { a: 1, b: { c: 2 } };
const y = Object.assign({}, x);
x.b.c = 22;
console.log(y); // { a: 1, b: { c: 22}}</code></pre>
<p>对 x 的操作,使 y.b.c 也变成了 22。为什么?因为 Object.assign 是浅拷贝,也就是它只会赋值对象第一层的 kv,而当第一层的 value 出现 object/array 的时候,它还是会做赋值引用操作,即 x,y 的 b 共用一个 <code>{c: 2}</code> 的地址。还有几个方法也是这样的。</p>
<h4>Object.freeze</h4>
<pre><code class="js">const x = { a: 1, b: { c: 2 } };
const y = Object.freeze(x);
x.a = 11;
console.log(y);
x.b.c = 22;
console.log(y); // { a: 1, b: { c: 22}}</code></pre>
<p>freeze,看起来是真的“冻结”了,不可变了,其实效果是一样的,为了效率,做的浅拷贝。</p>
<h4>deconstruction 解构</h4>
<pre><code class="js">const x = { a: 1, b: { c: 2 } };
const y = { ...x };
x.a = 11;
console.log(y);
x.b.c = 22;
console.log(y);</code></pre>
<p>es6 中的新方法,解构。数组也一样:</p>
<pre><code class="js">const x = [1, 2, [3, 4]];
const y = [...x];
x[2][0] = 33;
console.log(y); // [1, 2, [33, 4]]</code></pre>
<p>同样是浅拷贝。</p>
<p>JS 原生对象的方法,是没有给我们提供深拷贝功能的。</p>
<h4>deep-clone</h4>
<p>如何去做深拷贝</p>
<ul><li><p>原生</p></li></ul>
<p>拿上面的栗子来说,我们去实现深拷贝。</p>
<pre><code class="js">const x = { a: 1, b: { c: 2 } };
const y = Object.assign({}, x, {
b: Object.assign({}, x.b)
})
x.b.c = 22;
console.log(y); // { a: 1, b: { c: 2 } }</code></pre>
<p>不过这只是嵌套不多的时候,而更深层次的,就需要更复杂的操作了。实际上,deep-clone 确实没有一个统一的方法,需要考虑的地方挺多,比如效率,以及是否应用场景(是否每次都需要 deep-clone)。还有在 js 中,还要加上 hasOwnProperty 这样的判断。写个简单的方法:</p>
<pre><code class="js">function clone(obj) {
// 类型判断。 isActiveClone 用来防止重复 clone,效率问题。
if (obj === null || typeof obj !== 'object' || 'isActiveClone' in obj) {
return obj;
}
//可能是 Date 对象
const result = obj instanceof Date ? new Date(obj) : {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
obj['isActiveClone'] = null;
result[key] = clone(obj[key]);
delete obj['isActiveClone'];
}
}
return result;
}
var x = {
a: 1,
b: 2,
c: {
d: 3
}
}
console.log(clone(x));</code></pre>
<ul><li><p>JSON</p></li></ul>
<p>最简单,偷懒的一种方式,JSON 的序列化再反序列化。</p>
<pre><code class="js">const y = JSON.parse(JSON.stringify(x));</code></pre>
<p>普通的 string,number,object,array 都是可以做深拷贝的。不过这个方法比较偷懒,是存在坑的,比如不支持 NaN,正则,function 等。举个栗子:</p>
<pre><code class="js">const x = {
a: function() {
console.log('aaa')
},
b: NaN,
}
const y = JSON.parse(JSON.stringify(x));
console.log(y.b);
y.a()
</code></pre>
<p>试一下就知道了。</p>
<ul><li><p>Library</p></li></ul>
<p>通常实现 deep-clone 的库:<code>lodash</code>,<code>$.extend(true, )</code>... 目前最好用的是 <code>immutable.js</code>。 关于 immutable 的常用用法,之后会整理一下。</p>
<h3>数据持久化</h3>
<p>不变性可以让数据持久化变得容易。当数据不可变的时候,我们的每次操作,都不会引起初始数据的改变。也就是说在一定时期内,这些数据是永久存在的,而你可以通过读取,实现类似于“回退/切换快照”般的操作。这是我们从函数式编程来简单理解这个概念,而不涉及硬盘存储或者数据库存储的概念。</p>
<p>首先,无论数据结构的深浅,每次操作都对整个数据结构进行完整的深拷贝,效率会很低。这就牵扯到在做数据拷贝的时候,利用数据结构,做一些优化。例如,我们可以观察某次操作,到底有没有引起深层次数据结构的变化,如果没有,我们是不是可以只做部分改变,而没变化的地方,还是可以共用的。<strong>这就是部分持久化</strong>。我知道的 immutable 就是这么做的,两个不可变数据是会共用某部分的。</p>
<h3>思考</h3>
<ul>
<li>
<p>js 的对象天生是可变的?</p>
<p>我觉得作者应该是设计之初就把 js 作为一种灵活性较高的语言去做的,而不可变数据涉及到数据拷贝的算法问题,深拷贝是可以实现的,但是如何最优、效率最高的实现拷贝,并保持数据不可变。这个地方是可以继续研究的。</p>
</li>
<li>
<p>为什么不可变数据的热度越来越高?</p>
<p>随着 js 应用的场景越来越多,业务场景也越来越复杂,一些早就沉淀下来的编程思维,也被引入 js 中,像 MVC,函数式等等。经典的编程思想,设计模式永远都是不过时的,而不可变数据结构也是如此。而我觉得真正让它受关注的,还是 React 的推出,因为 React 内部就是通过 state/props 比较(<code>===</code>)去判断是否 render 的,三个等号的比较就要求新的 state 必须是新的引用。另外 Redux 在 React 中的广泛应用,也让函数式编程火热,而函数式编程最重要的原则之一就是不可变数据,所以你在使用</p>
</li>
</ul>
<p>Redux 的时候,改变 store 必须返回新的 state。所以,React-Redux 全家桶,让 immutable data 备受关注,而 immutable,就是目前最好的实现方案。</p>
<h3>最后</h3>
<p>之后会探究 immutable data 在 React 中的重要性,包括 diff,re-render,redux。自然而然也可以总结出这方面的 React 性能优化。</p>
JavaScript 异步队列及Co实现
https://segmentfault.com/a/1190000011402741
2017-09-28T20:17:07+08:00
2017-09-28T20:17:07+08:00
sunyongjian
https://segmentfault.com/u/sunyongjian
1
<h2>引入</h2>
<p>队列对于任何语言来说都是重要的,io 的串行,请求的并行等等。在 JavaScript 中,又由于单线程的原因,异步编程又是非常重要的。昨天由一道面试题的启发,我去实现 JS 中的异步队列的时候,借鉴了 express 中间件思想,并发散到 co 实现 与 generator,以及 asyncToGenerator。</p>
<p>本次用例代码都<a href="https://link.segmentfault.com/?enc=Z%2FnTYowxeS9BacqT75GbEg%3D%3D.%2FnJjcbvNd28whfa832Tzs0sEznQaVR%2FWVrKsBfrjSWg11eSN7wOb7Igtoc2EGs0E" rel="nofollow">在此</a>,可以 clone 下来试一下</p>
<h3>异步队列</h3>
<p>很多面试的时候会问一个问题,就是怎么让异步函数可以顺序执行。方法有很多,callback,promise,观察者,generator,async/await,这些 JS 中处理异步编程的,都可以做到这种串行的需求。但是很麻烦的是,处理起来是挺麻烦的,你要不停的手动在上一个任务调用下一个任务。比如 promise,像这样:</p>
<pre><code>a.then(() => b.then(() => c.then(...)))</code></pre>
<p>代码嵌套的问题,有点严重。所以要是有一个队列就好了,往队列里添加异步任务,执行的时候让队列开始 run 就好了。先制定一下 API,我们有一个 queue,队列都在内部维护,通过 queue.add 添加异步任务,queue.run 执行队列,可以先想想。</p>
<p>参照之前 express <a href="https://link.segmentfault.com/?enc=canYA1ObtxSDITwWdkRb8A%3D%3D.p%2F4zXm40Af4MxQaqCVohvq8HdZwMI83VID0S8sBtXvC%2F86Jb8eBqm0pWdwuOTbhqiaF4FhCDuWyYI%2Fu%2B1FZzGLvvXqZctlz88XHwlyvnOqE%3D" rel="nofollow">中间件</a>的实现,给异步任务 async-fun 传入一个 next 方法,只有调用 next,队列才会继续往下走。那这个 next 就至关重要了,它会控制队列往后移一位,执行下一个 async-fun。我们需要一个队列,来保存 async-fun,也需要一个游标,来控制顺序。</p>
<p>以下是我的简单实现:</p>
<pre><code class="javascript">const queue = () => {
const list = []; // 队列
let index = 0; // 游标
// next 方法
const next = () => {
if (index >= list.length - 1) return;
// 游标 + 1
const cur = list[++index];
cur(next);
}
// 添加任务
const add = (...fn) => {
list.push(...fn);
}
// 执行
const run = (...args) => {
const cur = list[index];
typeof cur === 'function' && cur(next);
}
// 返回一个对象
return {
add,
run,
}
}
// 生成异步任务
const async = (x) => {
return (next) => {// 传入 next 函数
setTimeout(() => {
console.log(x);
next(); // 异步任务完成调用
}, 1000);
}
}
const q = queue();
const funs = '123456'.split('').map(x => async(x));
q.add(...funs);
q.run();// 1, 2, 3, 4, 5, 6 隔一秒一个。
</code></pre>
<p>我这里没去构造一个 class,而是通过闭包的特性去处理的。queue 方法返回一个包含 add,run 的对象,add 即为像队列中添加异步方法,run 就是开始执行。在 queue 内部,我们定义了几个变量,list 用来保存队列,index 就是游标,表示队列现在走到哪个函数了,另外,最重要的是 next 方法,它是控制游标向后移动的。</p>
<p>run 函数一旦执行,队列即开始 run。一开始执行队列里的第一个 async 函数,我们把 next 函数传给了它,然后由 async 函数决定什么时候执行 next,即开始执行下一个任务。我们没有并不知道异步任务什么时候才算完成,只能通过打成某种共识,来告知 queue 某个任务完成。就是传给任务的 next 函数。其实 async 返回的这个函数,有一个名字,叫 Thunk,后面我们会简单介绍。</p>
<h3>Thunk</h3>
<p>thunk 其实是为了解决 “传名调用” 的。就是我传给函数 A 一个表达式作参数 x + 1,但是我不确定这个 x + 1 什么时候会用到,以及会不会用到,如果在传入就执行,这个求值是没有必要的。所以就出现了一个临时函数 Thunk,来保存这个表达式,传入函数 A 中,待需要时再调用。</p>
<pre><code class="javascript">const thunk = () => {
return x + 1;
};
const A = thunk => {
return thunk() * 2;
}</code></pre>
<p>嗯... 其实就是一个回调函数...</p>
<h3>暂停</h3>
<p>其实只要某个任务,不继续调用 next,队列就已经不会继续往下走了。比如我们 async 任务里加一个判断(通常是异步 io,请求的容错处理):</p>
<pre><code class="javascript">// queue 函数不变,
// async 加限制条件
const async = (x) => {
return (next) => {
setTimeout(() => {
if(x > 3) {
console.log(x);
q.run(); //重试
return;
}
console.log(x);
next();
}, 1000);
}
}
const q = queue();
const funs = '123456'.split('').map(x => async(x));
q.add(...funs);
q.run();
//打印结果: 1, 2, 3, 4, 4,4, 4,4 一直是 4
</code></pre>
<p>当执行到第四个任务的时候,x 是 4 的时候,不再继续,就可以直接 return,不再调用 next。也有可能是出现错误,我们需要再重试,那就再调用 q.run 就可以了,因为游标保存的就是当前的 async 任务的索引。</p>
<p>另外,还有一种方式,就是添加 stop 方法。虽然感觉上面的方法就 OK 了,但是 stop 的好处在于,你可以主动的停止队列,而不是在 async 任务里加限制条件。当然,有暂停就有继续了,两种方式,一个是 retry,就是重新执行上一次暂停的那个;另一个就是 goOn,不管上次最后一个如何,继续下一个。上代码:</p>
<pre><code class="javascript">const queue = () => {
const list = [];
let index = 0;
let isStop = false;
const next = () => {
// 加限制
if (index >= list.length - 1 || isStop) return;
const cur = list[++index];
cur(next);
}
const add = (...fn) => {
list.push(...fn);
}
const run = (...args) => {
const cur = list[index];
typeof cur === 'function' && cur(next);
}
const stop = () => {
isStop = true;
}
const retry = () => {
isStop = false;
run();
}
const jump = () => {
isStop = false;
next();
}
return {
add,
run,
stop,
retry,
goOn,
}
}
const async = (x) => {
return (next) => {
setTimeout(() => {
console.log(x);
next();
}, 1000);
}
}
const q = queue();
const funs = '123456'.split('').map(x => async(x));
q.add(...funs);
q.run();
setTimeout(() => {
q.stop();
}, 3000)
setTimeout(() => {
q.goOn();
}, 5000)</code></pre>
<p>其实还是加拦截... 只不过从 async 函数中,换到了 next 函数里面,利用 isStop 这个变量切换 true/false,开关暂停。我加了两个定时器,一个是 3 秒后暂停,一个是 5 秒后继续,(请忽略定时器的误差),按道理应该是队列到三秒的时候,也就是第三个任务执行完暂停,然后再隔 2 秒,继续。结果打印到 3 的时候,停住,两秒之后继续 4,5,6.</p>
<p>两种思路,请结合场景思考问题。</p>
<h3>并发</h3>
<p>上面的都是在做串行,假如 run 的时候我要并行呢... 也很简单,把队列一次性跑完就可以了。</p>
<pre><code class="javascript">// 为了代码短一些,把 retry,goOn 先去掉了。
const queue = () => {
const list = [];
let index = 0;
let isStop = false;
let isParallel = false;
const next = () => {
if (index >= list.length - 1 || isStop || isParallel) return;
const cur = list[++index];
cur(next);
}
const add = (...fn) => {
list.push(...fn);
}
const run = (...args) => {
const cur = list[index];
typeof cur === 'function' && cur(next);
}
const parallelRun = () => {
isParallel = true;
for(const fn of list) {
fn(next);
}
}
const stop = () => {
isStop = true;
}
return {
add,
run,
stop,
parallelRun,
}
}
const async = (x) => {
return (next) => {
setTimeout(() => {
console.log(x);
next();
}, 1000);
}
}
const q = queue();
const funs = '123456'.split('').map(x => async(x));
q.add(...funs);
q.parallelRun();
// 一秒后全部输出 1, 2, 3, 4, 5, 6</code></pre>
<p>我添加了一个 parallelRun 方法,用于并行,我觉得还是不要放到 run 函数里面了,抽象单元尽量细化还是。然后还加了一个 isParallel 的变量,默认是 false,考虑到 next 函数有可能会被调用,所以需要加一个拦截,保证不会处乱。</p>
<p>以上就是利用仅用 thunk 函数,结合 next 实现的异步队列控制器,queue,跟你可以把 es6 代码都改成 es5,保证兼容,当然是足够简单的,不适用于负责的场景 ?,仅提供思路。</p>
<h2>generator 与 co</h2>
<p>为什么要介绍 generator,首先它也是用来解决异步回调的,另外它的使用方式也是调用 next 函数,generator 才会往下执行,默认是暂停状态。yield 就相当于上面的 q.add,往队列中添加任务。所以我也打算一起介绍,来更好的拓宽思路。发散思维,相似的知识点做好归纳,然后某一天你就会突然有一种:原来是这么回事,原来 xxx 是借鉴子 yyy,然后你又去研究 yyy - -。</p>
<h3>简介 generator</h3>
<p>简单介绍回顾一下,因为有同学不经常用,肯定会有遗忘。</p>
<pre><code class="javascript">// 一个简单的栗子,介绍它的用法
function* gen(x) {
const y = yield x + 1;
console.log(y, 'here'); // 12
return y;
}
const g = gen(1);
const value = g.next().value; // {value: 2, done: false}
console.log(value); // 2
console.log(g.next(value + 10)); // {value: 12, done: true}
</code></pre>
<p>首先生成器其实就是一个通过函数体内部定义迭代算法,然后返回一个 iterator 对象。关于<a href="https://link.segmentfault.com/?enc=8NWIHh4KchG%2FhPw5cpXtJQ%3D%3D.exodupzX3emD%2FX%2B5Lcd5CdR0wkEifsSSIYyP4SaCdJCmTTl9LLjFbKEBJ4cqUMa3" rel="nofollow">iterator</a>,可以看我另一篇文章。<br>gen 执行返回一个对象 g,而不是返回结果。g 跟其他 iterator 一样,通过调用 next 方法,保证游标 + 1,并且返回一个对象,包含了 value(yield 语句的结果),和 done(迭代器是否完成)。另外,yield 语句的值,比如上面代码中的 y,是下一次调用 next 传入的参数,也就是 value + 10,所以是 12.这样设计是有好处的,因为这样你就可以在 generator 内部,定义迭代算法的时候,拿到上次的结果(或者是处理后的结果)了。</p>
<p>但是 generator 有一个弊端就是不会自动执行,TJ 大神写了一个 co,来自动执行 generator,也就是自动调用 next。它要求 yield 后面的函数/语句,必须是 thunk 函数或者是 promise 对象,因为只有这样才会串联执行完,这跟我们最开始实现 queue 的思路是一样的。co 的实现有两种思想,一个是 thunk,一个是 promise,我们都来试一下。</p>
<h3>Thunk 实现</h3>
<p>还记得最开始的 queue 怎么实现的吗,内部定义 next 函数,来保证游标的前进,async 函数会接收 next,去执行 next。到这里是一样的,我们只要在 co 函数内部定义一个同样的 next 函数,来保证继续执行,那么 generator 是没有提供索引的,不过它提供了 g.next 函数啊,所以我们只需要给 async 函数传 g.next 不就好了,async 就是 yield 后面的语句啊,也就是 g.value。但是并不能直接传 g.next,为什么?因为下一次的 thunk 函数,要通过 g.next 的返回值 value 取到啊,木有 value,下一个 thunk 函数不就没了... 所以我们还是需要定义一个 next 函数去包装一下的。</p>
<p>上代码:</p>
<pre><code class="javascript">const coThunk = function(gen, ...params) {
const g = gen(...params);
const next = (...args) => { // args 用于接收参数
const ret = g.next(...args); // args 传给 g.next,即赋值给上一个 yield 的值。
if(!ret.done) { // 去判断是否完成
ret.value(next); // ret.value 就是下一个 thunk 函数
}
}
next(); // 先调用一波
}
// 返回 thunk 函数的 asyncFn
const asyncFn = (x) => {
return (next) => { // 接收 next
const data = x + 1;
setTimeout(() => {
next && next(data);
}, 1000)
}
}
const gen = function* (x) {
const a = yield asyncFn(x);
console.log(a);
const b = yield asyncFn(a);
console.log(b);
const c = yield asyncFn(b);
console.log(c);
const d = yield asyncFn(c);
console.log(d);
console.log('done');
}
coThunk(gen, 1);
// 2, 3, 4, 5, done
</code></pre>
<p>这里定义的 gen,功能很简单,就是传入参数 1,然后每个 asyncFn 异步累加,即多个异步操作串行,并且下一个依赖上一个的返回值。</p>
<h3>promise 实现</h3>
<p>其实思路都是一样的,只不过调用 next,换到了 co 内部。因为 yield 后面的语句是 promise 对象的话,我们可以在 co 内部拿到了,然后在 <code>g.next().value</code> 的 then 语句执行 next 就好了。</p>
<pre><code class="javascript">// 定义 co
const coPromise = function(gen) {
// 为了执行后的结果可以继续 then
return new Promise((resolve, reject) => {
const g = gen();
const next = (data) => { // 用于传递,只是换个名字
const ret = g.next(data);
if(ret.done) { // done 后去执行 resolve,即co().then(resolve)
resolve(data); // 最好把最后一次的结果给它
return;
}
ret.value.then((data) => { // then 中的第一个参数就是 promise 对象中的 resolve,data 用于接受并传递。
next(data); //调用下一次 next
})
}
next();
})
}
const asyncPromise = (x) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(x + 1);
}, 1000)
})
}
const genP = function* () {
const data1 = yield asyncPromise(1);
console.log(data1);
const data2 = yield asyncPromise(data1);
console.log(data2);
const data3 = yield asyncPromise(data2);
console.log(data3);
}
coPromise(genP).then((data) => {
setTimeout(() => {
console.log(data + 1); // 5
}, 1000)
});
// 一样的 2, 3, 4, 5</code></pre>
<p>其实 co 的源码就是通过这两种思路实现的,只不过它做了更多的 catch 错误的处理,而且支持你 yield 一个数组,对象,通过 promise.all 去实现。另外 yield thunk 函数的时候,它统一转成 promise 去处理了。感兴趣的可以去看一下 <a href="https://link.segmentfault.com/?enc=8vCjNSGzDJkm%2B3hNczD9yQ%3D%3D.4%2BSPZbK5J2xL142v3o%2F4wJLY%2FId72pxk7BqlSVyWpbSPMKZqhT1XM74E0JVo9Jwh" rel="nofollow">co</a>,相信现在一定很明朗了。</p>
<h2>async/await</h2>
<p>现在 JS 中用的最常用的异步解决方案了,不过 async 也是基于 generator 的实现,只不过是做了封装。如果把 async/await 转化成 generate/yield,只需要把 await 语法换成 yield,再扔到一个 generate 函数中,async 的执行换成 coPromise(gennerate) 就好了。</p>
<pre><code class="javascript">const asyncPromise = (x) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(x + 1);
}, 1000)
})
}
async function fn () {
const data = await asyncPromise(1);
console.log(data);
}
fn();
// 那转化成 generator 可能就是这样了。 coPromise 就是上面的实现
function* gen() {
const data = yield asyncPromise(1);
console.log(data);
}
coPromise(gen);</code></pre>
<p>asyncToGenerator 就是这样的原理,事实上 babel 也是这样转化的。</p>
<h2>最后</h2>
<p>我首先是通过 express 的中间件思想,实现了一个 JS 中需求常见的 queue (异步队列解决方案),然后再接着去实现一个简单的 coThunk,最后把 thunk 换成 promise。因为异步解决方案在 JS 中是很重要的,去使用现成的解决方案的时候,如果能去深入思考一下实现的原理,我相信是有助于我们学习进步的。</p>
<p>欢迎 star 个人 blog:<a href="https://link.segmentfault.com/?enc=Ub5Ria2A8c0cdNCsBF6jPw%3D%3D.RASaL9nMZW340TURalhSBsemRTliS60kxDjtnSL6BDhwQEPUqyQJX%2B15IVVJ%2BYgf" rel="nofollow">https://github.com/sunyongjia...</a> ?</p>
你真的会用 Babel 吗?
https://segmentfault.com/a/1190000011155061
2017-09-13T21:05:15+08:00
2017-09-13T21:05:15+08:00
sunyongjian
https://segmentfault.com/u/sunyongjian
106
<h2>引入</h2>
<p>这个问题是对自己的发问,但我相信会有很多跟我一样的同学。<br>对于 babel 的使用,近半年来一直停留在与 webpack 结合使用,以及在浏览器开发环境下。导致很多 babel 的包,我都不清楚他们是干嘛的。比如 babel-register,还有 babel-runtime,各种 presets 的区别,transform-runtime 和 babel-polyfill 的区别,helpers 是干嘛的。尽管网上的 babel 的教程很多了,但是解答自己的一些疑问,还是要花费一些功夫。所以抽出时间来总结一下。如果你对于以上概念已经比较清楚了,就不需要往下看了。</p>
<p>本次的 example 代码都在 <a href="https://link.segmentfault.com/?enc=ocqKQ31gAvLEbQaFGeZ2eA%3D%3D.yoWwMzZyyf%2Bm%2B%2FCI3CZPNd3zZ1VbflpNTfJVRZK0ShdWVhnLIB0kigenn37Gddox" rel="nofollow">github</a> 上,而且每个文件夹都有详细的 README,说明我的使用方式。可以去参照一下用例的使用,并 clone 下来自己研究一下。</p>
<h2>版本变化</h2>
<p>说实话,从我做前端的时候,接触 babel 的时候,就已经是 babel 6 了,但是这不妨碍了解一下它的重大版本变化。<br>上一个版本 babel 5 是全家桶,包括各种package, plugins,尽可能的想通过你的一次安装,达到全能的效果。不过你现在安装<code>npm install babel</code>,会得到一个 warning。babel 6 是 <a href="https://link.segmentfault.com/?enc=Kz42txn9GazIiT3OLoK%2Ffg%3D%3D.dSnhNpgTWTsa0yvUUy0ecQvxSKokiRnmo0GQFQBuZAorTYwK3d1oyQxvKNfvZFxH2kRcTJ7hEXZjG1sSqBqqng%3D%3D" rel="nofollow">2015年10月30号</a>发布,主要做了以下更新:</p>
<ul>
<li>拆分成几个核心包,<code>babel-core</code>,<code>babel-node</code>,<code>babel-cli</code>...</li>
<li>没有了默认的转换,现在你需要手动的添加 plugin。也就是插件化</li>
<li>添加了 preset,也就是预置条件。</li>
<li>增加了 .babelrc 文件,方便自定义的配置。</li>
</ul>
<p>差不多了,我感觉其他的也不需要了解了。</p>
<h2>包</h2>
<p>babel 里面有好多的包,所以必须搞清楚他们都是干嘛的,才能让我们更好的使用这个工具。</p>
<h3>babel-core</h3>
<p>可以看做 babel 的编译器。babel 的核心 api 都在这里面,比如 transform,主要都是处理转码的。它会把我们的 js 代码,抽象成 ast,即 abstract syntax tree 的缩写,是源代码的抽象语法结构的树状表现形式。我们可以理解为,它定义的一种分析 js 语法的树状结构。也就是说 es6 的新语法,跟老语法是不一样的,那我们怎么去定义这个语法呢。所以必须要先转成 ast,去发现这个语法的 kind,分别做对应的处理,才能转化成 es5.</p>
<p>主要 api:</p>
<pre><code class="javascript">var babel = require('babel-core');
var transform = babel.transform;</code></pre>
<ul><li>babel.transform(code: string, options?: Object)</li></ul>
<pre><code class="javascript">transform("code", options) // => { code, map, ast }
</code></pre>
<ul><li>babel.transformFile(filename: string, options?: Object, callback: Function)</li></ul>
<pre><code class="javascript">var path = require('path');
var result = babel.transformFileSync(path.resolve(__dirname) + "/test.js", {
presets: ['env'],
plugins: ['transform-runtime'],
}, function(err, result) {// { code, map, ast }
console.log(result);
});</code></pre>
<ul><li>babel.transformFileSync(filename: string, options?: Object)</li></ul>
<pre><code>var result = babel.transformFileSync(path.resolve(__dirname) + "/test.js", {
presets: ['env'],
plugins: ['transform-runtime'],
});
console.log(result, 'res');
</code></pre>
<ul><li>babel.transformFromAst(ast: Object, code?: string, options?: Object)</li></ul>
<p>反转,你把 ast 传入,解析为 code 代码。</p>
<p><a href="https://link.segmentfault.com/?enc=miVaF2mBufnG8IQn7DblUg%3D%3D.7a2uZDKO%2F1aifEm8v0I1gR7K8%2FRruNgp0h0J8Bgpfepzbperur%2B%2FmZmaXdQBUOmaXN9vjyubtLrCa1NWW%2FB0Ag%3D%3D" rel="nofollow">options</a></p>
<h3>babel-cli</h3>
<p>提供命令行运行 babel。也就是你可以 <code>babel filename</code> 去对文件转码。<br>安装的话</p>
<pre><code>npm install --save-dev babel-cli
npm isntall babel-cli -g</code></pre>
<p>使用对应就是</p>
<pre><code>node_module/.bin/babel script.js --out-file script-compiled.js
babel script.js --out-file script-compiled.js</code></pre>
<p>具体使用还是看<a href="https://link.segmentfault.com/?enc=ORsd8HgUXd42HhiMChLTfA%3D%3D.qdRVljTk7GdFvOi8Dz54cWREWrbaxm5GolFsQkdZvo4IPj1gaUlfd%2FATrc9yMuVh" rel="nofollow">官方文档</a>吧,我就不搬文档了。</p>
<h3>babel-external-helpers</h3>
<p>babel-cli 中的一个command,用来生成一段代码,包含 babel 所有的 helper 函数。</p>
<p>首先我们需要了解什么是 helpers。babel 有很多帮助函数,例如 toArray函数, jsx转化函数。这些函数是 babel transform 的时候用的,都放在 <code>babel-helpers</code>这个包中。如果 babe 编译的时候检测到某个文件需要这些 helpers,在编译成模块的时候,会放到模块的顶部。<br>像这样</p>
<pre><code class="javascript">(function(module, exports, __webpack_require__) {
function _asyncToGenerator(fn) { return function () { }; } // 模块顶部定义 helper
// some code
// async 语法已经被 transform-async-to-generator 转化,再利用 helper 函数包装,消费 generator。
const func = (() => {
var _ref = _asyncToGenerator(function* () {
console.log('begin');
yield new Promise(function (resolve) {
setTimeout(function () {
resolve();
}, 1000);
});
console.log('done');
});
})
})</code></pre>
<p>但是如果多个文件都需要提供,会重复引用这些 helpers,会导致每一个模块都定义一份,代码冗余。所以 babel 提供了这个命令,用于生成一个包含了所有 helpers 的 js 文件,用于直接引用。然后再通过一个 plugin,去检测全局下是否存在这个模块,存在就不需要重新定义了。</p>
<p>使用:</p>
<ol>
<li>
<p>执行 babel-external-helpers 生成 helpers.js 文件,</p>
<pre><code> node_modules/.bin/babel-external-helpers > helpers.js</code></pre>
<p>注意:示例代码的包都是装到项目中的,也就是本地。同样你可以全局安装直接执行。</p>
</li>
<li>
<p>安装 plugin</p>
<pre><code class="javascript">npm install --save-dev babel-plugin-external-helpers</code></pre>
</li>
<li>
<p>然后在 babel 的配置文件加入</p>
<pre><code>{
"plugins": ["external-helpers"]
}</code></pre>
</li>
<li>
<p>入口文件引入 helpers.js</p>
<pre><code>require('./helpers.js');</code></pre>
</li>
</ol>
<p>这样就可以啦,还是可以减少很多代码量的。另外如果使用了 transform-runtime,就不需要生成 helpers.js 文件了,这个在后面的 babel-runtime 再说。</p>
<h3>babel-node</h3>
<p>也是 babel-cli 下面的一个 command,主要是实现了 node 执行脚本和命令行写代码的能力。举两个栗子就清楚了。</p>
<h5>执行脚本</h5>
<p>node 环境肯定是不支持 jsx 的</p>
<pre><code>// test.js
const React = require('react');
const elements = [1, 2, 3].map((item) => {
return (
<div>{item}</div>
)
});
console.log(elements);</code></pre>
<p>执行 test.js,会报错,不认识这个语法。</p>
<pre><code>node test.js //报错</code></pre>
<p>但是使用 babel-node 就可以。</p>
<pre><code> node_modules/.bin/babel-node --presets react test.js</code></pre>
<p>--presets react 是参数,等同于</p>
<pre><code>{
"presets": ["react"]
}</code></pre>
<p>执行正常。</p>
<h5>node 命令行写代码</h5>
<p>注意: 本文所有代码示例,均在 node 版本 4.8.4 下执行。</p>
<p>写个解构赋值的,直接运行 node,不支持。</p>
<p><img src="/img/bVU0w5?w=353&h=110" alt="30377925-1f2112da-98c4-11e7-95ce-ac7f497c0f93.png" title="30377925-1f2112da-98c4-11e7-95ce-ac7f497c0f93.png"></p>
<p>运行 <code>node_modules/.bin/babel-node --presets env</code></p>
<p><img src="/img/bVU0xt?w=578&h=83" alt="30377934-29b8a8a2-98c4-11e7-822d-226f0b9d5b81.png" title="30377934-29b8a8a2-98c4-11e7-822d-226f0b9d5b81.png"></p>
<p>得到 a 的 value 是 1。</p>
<p>通过栗子基本已经介绍了 babel-node 的用法了,就是方便我们平常开发时候,写一些脚本的。所以它不适用于生产环境。另外,babel-node 已经内置了 polyfill,并依赖 babel-register 来编译脚本。好,那 babel-register 是什么呢</p>
<h3>babel-register</h3>
<pre><code class="shell">npm install babel-register --save-dev</code></pre>
<p>babel-node 可以通过它编译代码,可以了解到,它其实就是一个编译器。我们同样可以在代码中引入它 <code>require('babel-register')</code>,并通过 node 执行我们的代码。</p>
<p>它的原理是通过改写 node 本身的 require,添加钩子,然后在 require 其他模块的时候,就会触发 babel 编译。也就是你引入<code>require('babel-register')</code>的文件代码,是不会被编译的。只有通过 require 引入的其他代码才会。我们是不是可以理解,babel-node 就是在内存中写入一个临时文件,在顶部引入 babel-register,然后再引入我们的脚本或者代码?</p>
<p>举个栗子,还是 node 中执行 jsx,要通过 babel 编译。我们可以把 jsx 的代码 a.js 编译完输出到一个 b.js,然后 <code>node b.js</code> 也是可以执行的。但是太麻烦,不利于开发。让我们看一下通过 register 怎么用:</p>
<pre><code class="javascript">// register.js 引入 babel-register,并配置。然后引入要执行代码的入口文件
require('babel-register')({ presets: ['react'] });
require('./test')</code></pre>
<pre><code class="javascript">// test.js 这个文件是 jsx...
const React = require('react');
const elements = [1, 2, 3].map((item) => {
return (
<div>{item}</div>
)
});
console.log(elements);</code></pre>
<pre><code>// 执行
$ node register.js</code></pre>
<p>它的特点就是实时编译,不需要输出文件,执行的时候再去编译。所以它很适用于开发。总结一下就是,多用在 node 跑程序,做实时编译用的,通常会结合其他插件作编译器使用,比如 mocha 做测试的时候。</p>
<p>值得一提的是,babel-register 这个包之前是在 babel-core 下面的,所以也可以 <code>require('babel-core/register')</code> 去引入,跟<code>require('babel-register')</code>是一样的。但是,babel 的团队把 register 独立出来了,而且未来的某一天(升 7.0)会从 babel-core 中废除,所以我们现在最好还是使用 babel-register 吧。<a href="https://link.segmentfault.com/?enc=f%2FdnAIf1HhqaKI8muUsw0g%3D%3D.pL%2FXgXqMpQ%2FT3xBZo5bVe23seGufnpVDCIk74b6l8hAIbBcYDSPjvnK75mN0xOinBjIN1EPMfpAADWWG6iXAexC%2BsGECNDXeSgTVE0TixuE%3D" rel="nofollow">babel-core/register.js</a></p>
<h3>babel-runtime</h3>
<pre><code class="shell">npm install babel-runtime --save</code></pre>
<p>这个包很简单,就是引用了 core-js 和 regenerator,然后生产环境把它们编译到 dist 目录下,做了映射,供使用。那么什么是 core-js 和 regenerator 呢。<br>首先我们要知道上面提到的 babel-core 是对语法进行 transform 的,但是它不支持 build-ints(Eg: promise,Set,Map),prototype function(Eg: array.reduce,string.trim),class static function (Eg:Array.form,Object.assgin),regenerator (Eg:generator,async)等等拓展的编译。所以才要用到 core-js 和 regenerator。</p>
<h4>core-js</h4>
<p>core-js 是用于 JavaScript 的组合式标准化库,它包含 es5 (e.g: object.freeze), es6的 promise,symbols, collections, iterators, typed arrays, es7+提案等等的 polyfills 实现。也就是说,它几乎包含了所有 JavaScript 最新标准的垫片。不过为什么它不把 generator 也实现了... 😁</p>
<pre><code class="javascript">// 比如,只不过需要单个引用
require('core-js/array/reduce');
require('core-js/object/values');</code></pre>
<h4>regenerator</h4>
<p>它是来自于 facebook 的一个库,<a href="https://link.segmentfault.com/?enc=9j8EHYOvK%2F3HHfCWWM6xRA%3D%3D.L7xnRN2mWuLHQLN%2F3GobDN9bXv%2FU6np2YAIGhz1VR5k2w4juotzOcu5lOhLDN2QM" rel="nofollow">链接</a>。主要就是实现了 generator/yeild, async/await。</p>
<p>所以 babel-runtime 是单纯的实现了 core-js 和 regenerator 引入和导出,比如这里是 filter 函数的定义,做了一个中转并处理了 esModule 的兼容。</p>
<pre><code class="javascript">module.exports = { "default": require("core-js/library/fn/array/filter"), __esModule: true };</code></pre>
<h4>helpers</h4>
<p>还记得提 babel-external-helpers 的时候,介绍 helpers 了吗。babel-runtime 里面的 helpers 就相当于我们上面通过 babel-external-helpers 生成的 helpers.js。只不过它把每个 helper 都单独放到一个文件夹里。这样,配合 transform-runtime 使用的时候,需要用 helper 转化的时候,就从 babel-runtime 中直接引用了。</p>
<pre><code class="javascript">var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');
var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);</code></pre>
<h4>文件结构:</h4>
<p><img src="/img/remote/1460000011164344" alt="文件结构" title="文件结构"></p>
<h4>使用</h4>
<p>可以单独引入<code>require('babel-runtime/core-js/object/values');</code></p>
<p>不过这些模块都做了 esModule 的兼容处理,也就是上面引入的模块是<code>{ "default": require("core-js/library/fn/array/filter"), __esModule: true }</code>这样的,要使用还得加上 <code>.default</code>。所以我们期待的是,最好能有帮我们自动处理的插件,<code>babel-plugin-transform-runtime</code>就是用来做这个的。这个我们放到 plugin 去讲。</p>
<h3>babel-polyfill</h3>
<pre><code class="shell">npm install babel-polyfill --save</code></pre>
<p>babel-runtime 已经是一堆 polyfill 了,为什么这里还有一个类似的包,它同样是引用了 core-js 和 regenerator,垫片支持是一样的。官网是这么说的,babel-polyfill 是为了模拟一个完整的ES2015 +环境,旨在用于应用程序而不是库/工具。并且使用babel-node时,这个polyfill会自动加载(这个我们在介绍 babel-node 的最后已经说了)。</p>
<p>也就是说,它会让我们程序的执行环境,模拟成完美支持 es6+ 的环境,毕竟无论是浏览器环境还是 node 环境对 es6+ 的支持都不一样。它是以重载全局变量 (E.g: Promise),还有原型和类上的静态方法(E.g:Array.prototype.reduce/Array.form),从而达到对 es6+ 的支持。不同于 babel-runtime 的是,babel-polyfill 是一次性引入你的项目中的,就像是 React 包一样,同项目代码一起编译到生产环境。</p>
<h4>使用</h4>
<p>我们结合 babel-register 去使用一下</p>
<pre><code class="javascript">// index.js
require('babel-core/register')({});
require('babel-polyfill');
require('./async');</code></pre>
<pre><code class="javascript">// async.js
async function a() {
console.log('begin');
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000)
})
console.log('done');
}
a();</code></pre>
<pre><code>$ node index.js</code></pre>
<p>完美运行。</p>
<h2>plugins</h2>
<p>要说 plugins 就不得不提 babel 编译的过程。babel 编译分为三步:</p>
<ol>
<li>parser:通过 <a href="https://link.segmentfault.com/?enc=rxJ7H1hs8MGT7qW7J%2B1PJA%3D%3D.KH8mgZrLN5zOwfqP7YfkkK57acFS3emti8st6fA9%2BJbI7Yj4Jk%2BAku5CiBOtGO2%2B" rel="nofollow">babylon</a> 解析成 AST。</li>
<li>transform[s]:All the plugins/presets ,进一步的做语法等自定义的转译,仍然是 AST。</li>
<li>generator: 最后通过 <a href="https://link.segmentfault.com/?enc=1weLGVtYSb8SA5mBaVEGYQ%3D%3D.eJjuM858cQ4ZyhdlIIz26YHAzYNTgXFaOz52WuJEJWFtZHZB%2FwVlRGzBvECD4pZJsYXFGchWwWcm6LQTfd0NL49onm4ujqG2qcwivqilxF8%3D" rel="nofollow">babel-generator</a> 生成 output string。</li>
</ol>
<p>所以 plugins 是在第二步加强转译的,所以假如我们自己写个 plugin,应该就是对 ast 结构做一个遍历,操作。</p>
<h3>babel-plugin-transform-runtime</h3>
<p>上面我们知道,transform-runtime 是为了方便使用 babel-runtime 的,它会分析我们的 ast 中,是否有引用 babel-rumtime 中的垫片(通过映射关系),如果有,就会在当前模块顶部插入我们需要的垫片。试一下:</p>
<pre><code class="shell">npm install babel-plugin-transform-runtime</code></pre>
<pre><code class="javascript">// 编译前
console.log(Object.values({ 1: 2 }));
</code></pre>
<pre><code class="shell">node_modules/.bin/babel --plugins transform-runtime values.js</code></pre>
<pre><code class="javascript">// 编译后
'use strict';
var _values = require('babel-runtime/core-js/object/values');
var _values2 = _interopRequireDefault(_values);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
onsole.log((0, _values2.default)({ 1: 2 }));</code></pre>
<p>另外,它还有几个配置</p>
<pre><code class="javascript">// 默认值
{
"plugins": [
["transform-runtime", {
"helpers": true,
"polyfill": true,
"regenerator": true,
"moduleName": "babel-runtime"
}]
]
}</code></pre>
<p>如果你只需要用 regenerator,不需要 core-js 里面的 polyfill 那你就可以在 options 中把 polyfill 设为 false。helpers 设为 false,就相当于没有启用 <code>babel-plugin-external-helpers</code> 的效果,比如翻译 async 的时候,用到了 asyncToGenerator 函数,每个文件还会重新定义一下。moduleName 的话,就是用到的库,你可以把 babel-runtime 换成其他类似的。</p>
<h3>transform-runtime 对比 babel-polyfill</h3>
<p>其实通过上面的介绍我们已经了解他们是干什么的了,这里再稍微总结区分一下吧。我在这里把 babel-runtime 和 babel-plugin-transform-runtime 统称为 transform-runtime,因为一起用才比较好。</p>
<ul>
<li>babel-polyfill 是当前环境注入这些 es6+ 标准的垫片,好处是引用一次,不再担心兼容,而且它就是全局下的包,代码的任何地方都可以使用。缺点也很明显,它可能会污染原生的一些方法而把原生的方法重写。如果当前项目已经有一个 polyfill 的包了,那你只能保留其一。而且一次性引入这么一个包,会大大增加体积。如果你只是用几个特性,就没必要了,如果你是开发较大的应用,而且会频繁使用新特性并考虑兼容,那就直接引入吧。</li>
<li>transform-runtime 是利用 plugin 自动识别并替换代码中的新特性,你不需要再引入,只需要装好 babel-runtime 和 配好 plugin 就可以了。好处是按需替换,检测到你需要哪个,就引入哪个 polyfill,如果只用了一部分,打包完的文件体积对比 babel-polyfill 会小很多。而且 transform-runtime 不会污染原生的对象,方法,也不会对其他 polyfill 产生影响。所以 transform-runtime 的方式更适合开发工具包,库,一方面是体积够小,另一方面是用户(开发者)不会因为引用了我们的工具,包而污染了全局的原生方法,产生副作用,还是应该留给用户自己去选择。缺点是随着应用的增大,相同的 polyfill 每个模块都要做重复的工作(检测,替换),虽然 polyfill 只是引用,编译效率不够高效。<strong>值得注意的是,instance 上新添加的一些方法,babel-plugin-transform-runtime 是没有做处理的,比如 数组的 <code>includes, filter, fill</code> 等,这个算是一个关键问题吧,直接推荐用 polyfill。</strong><a href="https://link.segmentfault.com/?enc=NbB8%2FpwPkRqsZwh3f5n7mg%3D%3D.m0sn2gZ3XK%2BoVjPq6MaGFK%2FtHNctAkzPn0ho6zdnRiwi183BrMtosCeW9VX2FwxZMu0VybE%2FGdx1XOhMdqnCJw%3D%3D" rel="nofollow">link</a>
</li>
</ul>
<p>另外,关于 babel-runtime 为什么是 dependencies 依赖。它只是一个集中了 polyfill 的 library,对应需要的 polyfill 都是要引入项目中,并跟项目代码一起打包的。不过它不会都引入,你用了哪个,plugin 就给你 require 哪个。所以即使你最终项目只是 <code>require('babel-runtime/core-js/object/values')</code>其中的一个文件,但是对于这包来说,也是生产依赖的。</p>
<p><img src="/img/remote/1460000011245833" alt="" title=""></p>
<p>注意:babel-polyfill 并不是一定会污染全局环境,在引入这个 js,并运行的时候,它会先判断当前有没有这个方法,在看要不要重写。如上图</p>
<h2>presets</h2>
<p>各种配置 plugin 实在是费劲,es6+ 编译要加入好多 plugins,比如为了在 node 中使用 esmodule,要把 esmodule 转化成 commomjs,使用 <code>transform-es2015-modules-commonjs</code>,还有 asyncToGenerator,React jsx转化等等,不仅要装好多,还要配好多。</p>
<p>presets 就是 plugins 的组合,你也可以理解为是套餐... 主要有</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=kFT4qEan25EW9mljUvBoHw%3D%3D.mpWrQLiO%2BPS5Ol0IfGBjacfDwDli4a%2Fb7MAhhiNyo9NqXqXdXZmjM0Br2GFa4HKQ" rel="nofollow">env</a></li>
<li><a href="https://link.segmentfault.com/?enc=4Dy1bPezMwhO1MIgIvKVkQ%3D%3D.%2BJ%2BaVcMaeyTM4l9q0AK8P2lAzncRLLv1sMuFEXAjlfYV9NQirfRZOdXMrLmI4Njm" rel="nofollow">es2015</a></li>
<li><a href="https://link.segmentfault.com/?enc=w6VCCK09XFnSr3w%2B37dMHQ%3D%3D.f1nTiiUmKWGWGuWAIVDSfkhfPmKBE5Sfw1vvShgtipxKflhoykIWBmXEMIXj99S2" rel="nofollow">react</a></li>
<li><a href="https://link.segmentfault.com/?enc=TLSgM432KeG8ZPp1u782CQ%3D%3D.%2FAhtzEeR57StVyBtshiw11FHCc%2BUzd%2F8t9yS6cuDO2FDyDPRRqVKHPLrdw7yp9Y6" rel="nofollow">lastet</a></li>
<li>
<a href="https://link.segmentfault.com/?enc=vR73R%2Bb35L6QFifBf6tGYg%3D%3D.xgNatg0pVNUU2A1CXNh2iExqcZHvCEBkPjcoM5Gf%2FbnYDgu%2BIcxO35QKvOKQYu2STVIqUOiqsbrAGqz1nK9jHTzYFKBvOHa%2Fjk6hQz8Nx4M%3D" rel="nofollow">stage-x</a> 具体的语法属于哪个 stage 可参照<a href="https://link.segmentfault.com/?enc=2ZATx2wG7RKPtgHrGOVfcA%3D%3D.XYbeuWin0odr4wf3S10ZGhrDVCmuXHvUa3s0aa6VbjF5fkn2tJq8HlolNv2JiSn1" rel="nofollow">tc39</a>
</li>
</ul>
<p>大部分的 presets 我觉得都不需要介绍了,官网上写的比较详细。而且 babel-preset-lastet 已经废弃,被 babel-preset-env 代替。</p>
<pre><code class="javascript">{ "presets": ["latest"] } === { "presets": ["env"] }
</code></pre>
<h3>babel-preset-env</h3>
<p>这个 preset 真是神器啊,它能根据当前的运行环境,自动确定你需要的 plugins 和 polyfills。通过各个 es标准 feature 在不同浏览器以及 node 版本的支持情况,再去维护一个 feature 跟 plugins 之间的映射关系,最终确定需要的 plugins。</p>
<h4>preset-env 配置</h4>
<p>详情:</p>
<pre><code class="javascript">{
"presets": [
[
"env",
{
"targets": { // 配支持的环境
"browsers": [ // 浏览器
"last 2 versions",
"safari >= 7"
],
"node": "current"
},
"modules": true, //设置ES6 模块转译的模块格式 默认是 commonjs
"debug": true, // debug,编译的时候 console
"useBuiltIns": false, // 是否开启自动支持 polyfill
"include": [], // 总是启用哪些 plugins
"exclude": [] // 强制不启用哪些 plugins,用来防止某些插件被启用
}
]
],
plugins: [
"transform-react-jsx" //如果是需要支持 jsx 这个东西要单独装一下。
]
}</code></pre>
<p>主要介绍 debug 和 很好用的 useBuiltIns 吧。</p>
<h4>debug</h4>
<p>开启debug后,编译结果会得到使用的 targets,plugins,polyfill 等信息</p>
<pre><code class="javascript">Using targets:
{
"chrome": "59",
"android": "4.4.3",
"edge": "14",
"firefox": "54",
"ie": "10",
"ios": "10",
"safari": "7",
"node": "4.8.4"
}
Modules transform: commonjs
Using plugins:
check-es2015-constants {"android":"4.4.3","ie":"10","safari":"7","node":"4.8.4"}
transform-es2015-arrow-functions {"android":"4.4.3","ie":"10","safari":"7","node":"4.8.4"}
transform-es2015-block-scoped-functions {"android":"4.4.3","ie":"10","safari":"7"}
transform-es2015-block-scoping {"android":"4.4.3","ie":"10","safari":"7","node":"4.8.4"}
...
Using polyfills:
es6.typed.array-buffer {"android":"4.4.3","ie":"10","safari":"7","node":"4.8.4"}
es6.typed.int8-array {"android":"4.4.3","ie":"10","safari":"7","node":"4.8.4"}
es6.typed.uint8-array {"android":"4.4.3","ie":"10","safari":"7","node":"4.8.4"}
es6.typed.uint8-clamped-array {"android":"4.4.3","ie":"10","safari":"7","node":"4.8.4"}
es6.typed.int16-array {"android":"4.4.3","ie":"10","safari":"7","node":"4.8.4"}
...</code></pre>
<h4>useBuiltIns</h4>
<p>env 会自动根据我们的运行环境,去判断需要什么样的 polyfill,而且,打包后的代码体积也会大大减小,但是这一切都在使用 useBuiltIns,而且需要你安装 babel-polyfill,并 import。它会启用一个插件,替换你的<code>import 'babel-polyfill'</code>,不是整个引入了,而是根据你配置的环境和个人需要单独的引入 polyfill。 我尝试了一下是否真的有效,下面是我的对比实验过程:</p>
<p><strong>step1: </strong> 首先是这样一段测试编译的代码,有 jsx,Object.values,async。env 的配置除了 useBuiltIns 都跟上面的配置一样。然后通过 webpack + babel-loader 打包,生成 build.js</p>
<pre><code class="javascript">require('./async');
// import 'babel-polyfill';
const React = require('react');
const elements = [1, 2, 3].map((item) => {
return (
<div>{item}</div>
)
});
console.log(elements);
async function a() {
console.log('begin');
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000)
})
console.log('done');
}
a();
console.log(Object.values({ 1: 2 }));
console.log(Array.isArray([]));
</code></pre>
<p><strong>step2:</strong> 然后通过设置不同的参数,打包,获取 build.js,并执行。得到下表</p>
<table>
<thead><tr>
<th> </th>
<th> </th>
</tr></thead>
<tbody>
<tr>
<td>preset-env 条件下</td>
<td>useBuiltIns: true</td>
<td>useBuiltIns: fase</td>
</tr>
<tr>
<td>不引入 polyfill</td>
<td>build.js 代码体积 158k,node build.js 执行报错。</td>
<td>build.js 代码体积 158k,node build.js 执行报错。</td>
<td> </td>
</tr>
<tr>
<td>引入 polyfill</td>
<td>build.js 体积 369k,执行通过。包确实减小了。</td>
<td>因为引入了 polyfill,build.js 代码体积瞬间 420k,执行通过</td>
<td> </td>
</tr>
</tbody>
</table>
<ul><li>用 preset-es2015,并引入 polyfill<br>plugins 多加一个 transform-regenerator,这方面确实不如 env 方便一些。<br>体积 418k,执行通过。不方便在要配好多 plugins。</li></ul>
<p>具体的过程、截图猛戳 <a href="https://link.segmentfault.com/?enc=Ir4FYBRuNWPMWB7Lc9I4sw%3D%3D.unjlPGlAzRQ8m2PaoeR4MTynW%2FXFv9uwZI9ib6o51M%2Bvf7jPOD5UxRcXe1V4FAeY1%2FLHK63AeqG3ymylCa0VVQ%3D%3D" rel="nofollow">这里</a></p>
<p>最终的结论就是,使用了 useBuiltIns 确实体积变小了,比直接 <code>import 'babel-polyfill'</code> 好了许多。</p>
<p><strong>step3:</strong> 然后... 我又试了一下 env 下,使用 transform-runtime。在不加 useBuiltIns,不引入 babel-polyfill 的情况下。build.js 体积234k,执行通过。</p>
<p>咦,这样好像体积更小啊。别忘了,我们的 babel-polyfill 是配置了执行环境的,通过环境看你需要哪些 polyfill。而 transform-runtime,是发现我们代码需要什么 polyfill,当然会少很多了。所以,又回到了用哪个的问题... 😓 参考上面的总结。</p>
<h4>then</h4>
<p>helpers 的问题。开发项目,使用 preset-env,并 <code>import 'babel-polyfill'</code>,但是 helpers 好像没有地方配置。而且我试了两个文件分别用 async 函数,编译后每个模块都定义了 asyncToGenerat 函数,这种情况下我觉得最后就是自己生成一个 helpers.js 文件了。</p>
<h4>总结</h4>
<p>现在看起来开发大点的项目,最好用的配置应该就是 preset-env,确定自己的运行环境,如果有需要,再加上 useBuiltIns,并生成一份 helpers.js 的文件。不过,一切还是看你的需求,我的前提是开发大点的“项目”,不过了解了这些东西,你会做出自己的选择。</p>
<h2>babel 的配置</h2>
<p>目前 babel 官方推荐是写到 .babelrc 文件下,你还可以在 package.json 里面添加 babel 字段。不用配置文件的话,可以把配置当做参数传给 babel-cli</p>
<ul><li>.babelrc</li></ul>
<pre><code>{
"presets": [
"env"
],
"plugins": [
["transform-runtime", {
"helpers": true,
"polyfill": true,
"regenerator": true,
"moduleName": "babel-runtime"
}]
]
}</code></pre>
<ul><li>写到 package.json</li></ul>
<pre><code>"babel": {
"presets": [
"env"
],
}</code></pre>
<ul><li>babel cli</li></ul>
<pre><code>babel script.js --plugins=transform-runtime --presets=env</code></pre>
<h2>配合其他工具</h2>
<h3>webpack</h3>
<p>比较常用,除了 babel 自己的包,多装一个 <code>babel-loader</code> 配合 webpack 使用。并在 webpack.config.js 中加入 loader 的配置</p>
<pre><code class="javascript"> module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/,
}
]
}</code></pre>
<h3>mocha</h3>
<p>项目里的代码都是用 es6+ 写的,但是做单元测试的时候,测试框架并不认识你的什么 esModule,es6+ 的一些语法,mocha 是 node 程序,所以你要把 esModule 转化成 commomjs 之类的。</p>
<p>mocha 是支持编译器的,通过 <code>--compilers </code> 指定,这里我们用 babel,举个栗子</p>
<pre><code class="javascipt">// 求和函数 add.js
const add = (x, y) => x + y;
export default add;
</code></pre>
<pre><code class="javascript">// 测试脚本 add.test.js
import { expect } from 'chai'; // chai 是断言库
import add from './add';
describe('es6 两数相加', () => {
it('2 + 4 = 6', () => {
expect(add(2, 4)).equal(6);
})
});
</code></pre>
<pre><code class="shell">./node_modules/mocha/bin/mocha --compilers js:babel-register add.test.js</code></pre>
<p>因为 mocha 终究是在跑 node 程序的,适用于实时编译,所以可以用 babel-register 做编译器。</p>
<h3>最后</h3>
<p>总结这些东西花了我两三天的时间,虽然搞清楚了这些包是干嘛的,但是又在想到底应不应该花时间研究这些,工具始终是用来使用的,对于 babel 来说更应该研究的是它对 ast 的处理方式?不过看到自己的产出,我觉得是有必要的,另外,因为对工具更进一步的了解,才能更好的在项目中使用它们,而不是一顿照搬,代码是可以正常用了,但是可能会有很多不需要的东西,导致代码体积变大。“割鸡焉用牛刀”,我觉得还是需要有精益求精的精神。希望对你有所帮助。</p>
<p>我的个人博客地址 <a href="https://link.segmentfault.com/?enc=AZGXvzJ%2BaZTeaHG6YUCEEw%3D%3D.Lfa0WQWuCKslamchuoLGmy%2BL0ZYbuvnr1LlxlxbBDFQoMazBkOGSHFL6q17MTl3n" rel="nofollow">https://github.com/sunyongjia...</a> ,欢迎订阅,star,谢谢。</p>
JS中的观察者模式(发布订阅)
https://segmentfault.com/a/1190000007921198
2016-12-26T15:00:08+08:00
2016-12-26T15:00:08+08:00
sunyongjian
https://segmentfault.com/u/sunyongjian
5
<h4>观察者模式</h4>
<blockquote><p>简介</p></blockquote>
<p>观察者模式又称发布订阅模式,是一种最常用的设计模式之一了。讲道理,如果我们写的不是稍微底层的代码,可能不会用到它。 但是有了它会让代码更灵活,更加规整,减少冗余代码,方便分模块,分功能开发。</p>
<blockquote><p>引入</p></blockquote>
<p>在前端业务中,可能用的比较多的地方可能就是自定义事件了。<br>其实浏览器的事件也是观察者模式</p>
<pre><code>
div.onclick = function click() {
console.log('click')
}</code></pre>
<p>这里function click 订阅了 div 的click 事件,当我们的鼠标点击操作,事件发布,对应的function就会执行。这个function click 就是一个观察者。</p>
<blockquote><p>具象化理解</p></blockquote>
<p>其实单纯的看代码实现,也可以理解。但是万物都是有联系的,这些编程模式设计之初也是来源于生活经验吧,所以,具象的理解也是很重要的体验。</p>
<p>我们举一个结婚办酒席的例子。比如你的一个好朋友要结婚了,'结婚'这件事情不是天天发生,一辈子就那么一... 两次(maybe more),所以我们的'去参加他的婚礼'肯定不是天天发生,只是在特定的时候。我肯定不能天天去问他,'今天你结婚吗,我来参加酒席啊'。一次两次还行,天天问,sb啊。假如是一个找不到对象的单身汪,被你天天这么问,还不得杀了你。。</p>
<p>那这里就需要有一个事件发布了,也就是'通知你'。 </p>
<p>我作为一个观察者,去订阅他'结婚' 的这个事件,就是我们是好朋友,他的婚礼我肯定去,我们已经说好了。那么我就是观察者,'我去参加婚礼'就是对应而来的动作。当我订阅了'结婚' 这个事件,我就不需要天天去问他了,我该干嘛干嘛,该去泡妞,约饭,看电影,约... 就干嘛。</p>
<p>当他发布'结婚' 这个事件,通知到我了,我就在特定的时候,去do'参加婚礼酒席'这个行为function ...</p>
<pre><code>//模拟代码
//我订阅了'marry' 事件
wo.on('marry',function(){
//去参加婚礼酒席
})
//然后他发布。比如浏览器的点击
// 对应的我的 function就会执行</code></pre>
<blockquote><p>解耦/模块/功能</p></blockquote>
<p>其实在代码中是需要一个类似于中间服务的,管理发布订阅的中间者。<br>比如浏览器中的事件处理程序,他提供了订阅的接口,然后接收'事件' 信号 发布给你。让js代码跟浏览器之间有了联系,互动。而本来是两个不同的东西。</p>
<p>在我看来,观察者模式最大的好处就是在于解耦,会让我们一锅端的代码,分功能,分模块的抽离开,更加清晰,开发成本变低,也容易维护。<br>比如:</p>
<ul><li>
<p>我们项目里的view 展示层跟model(数据处理)逻辑层,最开始写页面,ajax,字符串拼接,请求回一个接口拼一下,然后给dom。可能我们一个js文件,一个function里面又请求了接口,又去负责 view 的展示。</p>
<pre><code>var xhr = new XMLHttpRequest ()
xhr.open('get',url)
xhr.onreadystatechange = function () {
if(this.readyState !== 4) return
if(this.status === 200) {
divs.innerHTML = '<p>' + this.response + '</p>'
//
}
}
xhr.responseType = 'json'
xhr.send(null)</code></pre>
</li></ul>
<p>其实应该是请求跟 展示渲染分开的。</p>
<pre><code> //请求
function getData () {
var xhr = new XMLHttpRequest ()
xhr.open('get',url)
xhr.onreadystatechange = function () {
if(this.readyState !== 4) return
if(this.status === 200) {
this.emit('渲染')
// 发布
}
}
xhr.responseType = 'json'
xhr.send(null)
}
//渲染
function view () {}
xhr.on('渲染',view)
</code></pre>
<p>直接在状态码200那里放个callback,也能做到。但是,如果我有两个甚至渲染函数,处理不同的东西,我每次还要改成不同的函数吗。 这个相同请求的过程是不是还要写一遍。<br>用观察者的话</p>
<pre><code>function view1 () {}
function view2 () {}
function view3 () {}
function view4 () {}
if(我要渲染view1) {
xhr.on('渲染',view1) //订阅
xhr.on('渲染',view2)
}else{
xhr.on('渲染',view3)
xhr.on('渲染',view4)
}
</code></pre>
<p>好处就在于我的getData这个功能,方法就只负责请求数据,然后他会暴露一个接口,供我去添加方法。这样我的getData 就相对来说是比较完整的功能模块,就算我有再多的情况,我的getData 里面的代码是不会改动的了。</p>
<p>有时候我们经常为了实现业务,添加一个新的功能,而去更改我们之前写好的代码,导致我们本来的功能模块被改的面目全非。<br>而且会有好多的重复代码。<br>过程? or 模块?</p>
<p>当然封好一个 好的完整的功能模块是挺难的一件事情,但我们起码要有个开始。</p>
<p>订阅去添加方法,发布了事件池就执行。</p>
<ul><li>
<p>MV* 类框架</p>
<p>MVC也是一种设计模式,这里面也都应用了观察者。</p>
</li></ul>
<p>他内部也都是各种发布订阅,好像是一个观察者模型,从而实现了一个模拟的内存中的dom改变,计算出那个DOM节点应该改变。当然具体实现要做好多事情...就不...</p>
<ul><li><p>redux</p></li></ul>
<blockquote><p>简单实现一个createstore函数</p></blockquote>
<pre><code>//这是一个工厂函数,可以创建store
const createStore = (reducer) => {
let state; // 定义存储的state
let listeners = [];
// getState的作用很简单就是返回当前是state
const getState = ()=> state;
//定义一个派发函数
//当在外界调用此函数的时候,会修改状态
const dispatch = (action)=>{
//调用reducer函数修改状态,返回一新的状态并赋值给这个局部状态变量
state = reducer(state,action);
//依次调用监听函数,通知所有的监听函数
listeners.forEach(listener => listener());
}
//订阅此状态的函数,当状态发生变化的时候记得调用此监听函数
const subscribe = function(listener){
//先把此监听 加到数组中
listeners.push(listener);
//返回一个函数,当调用它的时候将此监听函数从监听数组移除
return function(){
listeners = listeners.filter(l => l != listener);
}
}
//默认调用一次dispatch给state赋一个初始值
dispatch();
return {
getState,
dispatch,
subscribe
}
}
let store = createStore(reducer);
//把数据渲染到界面上
const render = () => {
document.body.innerText = store.getState();
}
// 订阅状态变化事件,当状态变化时用监听函数
store.subscribe(render);
render();
var INCREASE_ACTION = {type: 'INCREMENT'};
document.addEventListener('click', function (e) {
//触发一个Action
store.dispatch(INCREASE_ACTION);
})</code></pre>
<ul><li><p>在node 中的作用 大多数时候我们不会直接使用 EventEmitter,而是在对象中继承它。包括fs、net、 http 在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。</p></li></ul>
<blockquote><p>实现一个可以发布订阅的类</p></blockquote>
<pre><code>'use strict'
class EmitterEvent {
constructor() {
//构造器。实例上创建一个事件池
this._event = {}
}
//on 订阅
on (eventName, handler) {
// 根据eventName,事件池有对应的事件数组,
就push添加,没有就新建一个。
// 严谨一点应该判断handler的类型,是不是function
if(this._event[eventName]) {
this._event[eventName].push(handler)
} else {
this._event[eventName] = [handler]
}
}
emit (eventName) {
// 根据eventName找到对应数组
var events = this._event[eventName];
// 取一下传进来的参数,方便给执行的函数
var otherArgs = Array.prototype.slice.call(arguments,1)
var that = this
if(events) {
events.forEach((event) => {
event.apply(that, otherArgs)
})
}
}
// 解除订阅
off (eventName, handler) {
var events = this._event[eventName]
if(events) {
this._event[eventName] = events.filter((event) => {
return event !== handler
})
}
}
// 订阅以后,emit 发布执行一次后自动解除订阅
once (eventName, handler) {
var that = this
function func () {
var args = Array.prototype.slice.call(arguments,0)
handler.apply(that, args)
this.off(eventName,func)
}
this.on(eventName, func)
}
}
var event = new EmitterEvent()
function a (something) {
console.log(something,'aa-aa')
}
function b (something) {
console.log(something)
}
event.once('dosomething',a)
event.emit('dosomething', 'chifan')
//event.emit('dosomething')
// event.on('dosomething',a)
// event.on('dosomething',b)
// event.emit('dosomething','chifan')
// event.off('dosomething',a)
// setTimeout(() => {
// event.emit('dosomething','hejiu')
// },2000)
</code></pre>
<p>当我们需要用的时候,只需要继承一下这个EmitterEvent类。要操作的实例就可以用on,emit方法,也就是可以用发布订阅。比如XHR,组件...</p>