谷雨

谷雨 查看完整档案

杭州编辑厦门理工学院  |  电子信息工程 编辑杭州贝购科技有限公司  |  高级前端研发工程师 编辑填写个人主网站
编辑

虫儿飞,虫儿飞,你在思念谁?

个人动态

谷雨 赞了文章 · 9月11日

vue3带来的新特性/亮点

在这里插入图片描述

1. Performance

vue3在性能方面比vue2快了2倍。

  • 重写了虚拟DOM的实现
  • 运行时编译
  • update性能提高
  • SSR速度提高

2. Tree-shaking support

vue3中的核心api都支持了tree-shaking,这些api都是通过包引入的方式而不是直接在实例化时就注入,只会对使用到的功能或特性进行打包(按需打包),这意味着更多的功能和更小的体积。

3. Composition API

vue2中,我们一般会采用mixin来复用逻辑代码,用倒是挺好用的,不过也存在一些问题:例如代码来源不清晰、方法属性等冲突。基于此在vue3中引入了Composition API(组合API),使用纯函数分隔复用代码。和React中的hooks的概念很相似。

  1. 更好的逻辑复用和代码组织
  2. 更好的类型推导

一个简单的例子

<template>
    <div>X: {{ x }}</div>
    <div>Y: {{ y }}</div>
</template>

<script>
import { defineComponent, onMounted, onUnmounted, ref } from "vue";

const useMouseMove = () => {
    const x = ref(0);
    const y = ref(0);

    function move(e) {
        x.value = e.clientX;
        y.value = e.clientY;
    }

    onMounted(() => {
        window.addEventListener("mousemove", move);
    });

    onUnmounted(() => {
        window.removeEventListener("mousemove", move);
    });

    return { x, y };
};

export default defineComponent({
    setup() {
        const { x, y } = useMouseMove();

        return { x, y };
    }
});
</script>

4. Fragment、Teleport、Suspense

新增的三个组件。

Fragment

在书写vue2时,由于组件必须只有一个根节点,很多时候会添加一些没有意义的节点用于包裹。Fragment组件就是用于解决这个问题的(这和React中的Fragment组件是一样的)。

这意味着现在可以这样写组件了。

/* App.vue */
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

<script>
export default {};
</script>

或者这样

// app.js
import { defineComponent, h, Fragment } from 'vue';

export default defineComponent({
    render() {
        return h(Fragment, {}, [
            h('header', {}, ['...']),
            h('main', {}, ['...']),
            h('footer', {}, ['...']),
        ]);
    }
});

Teleport

Teleport其实就是React中的Portal。Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。
/* App.vue */
<template>
    <div>123</div>
    <Teleport to="#container">
        Teleport
    </Teleport>
</template>

<script>
import { defineComponent } from "vue";

export default defineComponent({
    setup() {}
});
</script>

/* index.html */
<div id="app"></div>
<div id="container"></div>

在这里插入图片描述

Suspense

同样的,这和React中的Supense是一样的。

Suspense 让你的组件在渲染之前进行“等待”,并在等待时显示 fallback 的内容。
// App.vue
<template>
    <Suspense>
        <template #default>
            <AsyncComponent />
        </template>
        <template #fallback>
            Loading...
        </template>
    </Suspense>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import AsyncComponent from './AsyncComponent.vue';

export default defineComponent({
    name: "App",
    
    components: {
        AsyncComponent
    }
});
</script>

// AsyncComponent.vue
<template>
    <div>Async Component</div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

const sleep = () => {
    return new Promise(resolve => setTimeout(resolve, 1000));
};

export default defineComponent({
    async setup() {
        await sleep();
    }
});
</script>

5. Better TypeScript support

在vue2中使用过TypesScript的童鞋应该有过体会,写起来实在是有点难受。vue3则是使用ts进行了重写,开发者使用vue3时拥有更好的类型支持和更好的编写体验。

6. Custom Renderer API

这个api定义了虚拟DOM的渲染规则,这意味着使用自定义API可以达到跨平台的目的。下面是一个简单的例子。

// App.js
import { defineComponent, h } from 'vue';

export default defineComponent({
    render() {
        return h('Article', {
            onClick() {
                console.log('点击文章');
            }
        }, [
            h('Title', { align: 'center' }, '这是文章标题')
        ]);
    }
});

// main.js
import { createRenderer } from 'vue';
import App from './App.js';
import './assets/index.css';

const { createApp } = createRenderer({
    createElement(type) {
        let nodeType = 'div';

        switch(type) {
            case 'Article': nodeType = 'article'; break;
            case 'Title': nodeType = 'h1'; break;
            case 'Content': nodeType = 'p'; break;
        }

        return document.createElement(nodeType);
    },
    insert(child, parent, anchor) {
        parent.insertBefore(child, anchor || null);
    },
    setElementText(node, text) {
        node.textContent = text;
    },
    patchProp(el, key, prevValue, nextValue) {
        console.log(el, key, prevValue, nextValue);

        switch(key) {
            case 'onClick':
                el.addEventListener('click', nextValue);
                break;
            default:
                el.setAttribute(key, nextValue);
        }
    }
});

createApp(App).mount(document.querySelector('#app'));

更多:
使用vue3编写的ToDoList

查看原文

赞 3 收藏 2 评论 0

谷雨 评论了文章 · 2018-07-15

JavaScript 异步、栈、事件循环、任务队列

概览

图片描述
我们经常会听到引擎和runtime,它们的区别是什么呢?

  • 引擎:解释并编译代码,让它变成能交给机器运行的代码(runnable commands)。
  • runtime:就是运行环境,它提供一些对外接口供Js调用,以跟外界打交道,比如,浏览器环境、Node.js环境。不同的runtime,会提供不同的接口,比如,在 Node.js 环境中,我们可以通过 require 来引入模块;而在浏览器中,我们有 window、 DOM。

Js引擎是单线程的,如上图中,它负责维护任务队列,并通过 Event Loop 的机制,按顺序把任务放入栈中执行。而图中的异步处理模块,就是 runtime 提供的,拥有和Js引擎互不干扰的线程。接下来,我们会细说图中的:栈和任务队列。

现在,我们要运行下面这段代码:

function bar() {
    console.log(1);
}

function foo() {
    console.log(2);
    far();
}

setTimeout(() => {
    console.log(3)
});

foo();

它在栈中的入栈、出栈过程,如下图:
图片描述

任务队列

Js 中,有两类任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks)。宏任务队列可以有多个,微任务队列只有一个。那么什么任务,会分到哪个队列呢?

  • 宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.
  • 微任务:process.nextTick, Promise, Object.observer, MutationObserver.

浏览器的 Event Loop

浏览器的 Event Loop 遵循的是 HTML5 标准,而 NodeJs 的 Event Loop 遵循的是 libuv。 区别较大,分开讲。

我们上面讲到,当stack空的时候,就会从任务队列中,取任务来执行。浏览器这边,共分3步:

  1. 取一个宏任务来执行。执行完毕后,下一步。
  2. 取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。
  3. 更新UI渲染。

Event Loop 会无限循环执行上面3步,这就是Event Loop的主要控制逻辑。其中,第3步(更新UI渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新UI成本大,所以,一般都会比较长的时间间隔,执行一次更新。

从执行步骤来看,我们发现微任务,受到了特殊待遇!我们代码开始执行都是从script(全局任务)开始,所以,一旦我们的全局任务(属于宏任务)执行完,就马上执行完整个微任务队列。看个例子:

console.log('script start');

// 微任务
Promise.resolve().then(() => {
    console.log('p 1');
});

// 宏任务
setTimeout(() => {
    console.log('setTimeout');
}, 0);

var s = new Date();
while(new Date() - s < 50); // 阻塞50ms

// 微任务
Promise.resolve().then(() => {
    console.log('p 2');
});

console.log('script ent');


/*** output ***/

// one macro task
script start
script ent

// all micro tasks
p 1
p 2

// one macro task again
setTimeout

上面之所以加50ms的阻塞,是因为 setTimeout 的 delayTime 最少是 4ms. 为了避免认为 setTimeout 是因为4ms的延迟而后面才被执行的,我们加了50ms阻塞。

NodeJs 的 Event Loop

NodeJs 的运行是这样的:

  • 初始化 Event Loop
  • 执行您的主代码。这里同样,遇到异步处理,就会分配给对应的队列。直到主代码执行完毕。
  • 执行主代码中出现的所有微任务:先执行完所有nextTick(),然后在执行其它所有微任务。
  • 开始 Event Loop

NodeJs 的 Event Loop 分6个阶段执行:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

以上的6个阶段,具体处理的任务如下:

  • timers: 这个阶段执行setTimeout()setInterval()设定的回调。
  • pending callbacks: 上一轮循环中有少数的 I/O callback 会被延迟到这一轮的这一阶段执行。
  • idle, prepare: 仅内部使用。
  • poll: 执行 I/O callback,在适当的条件下会阻塞在这个阶段
  • check: 执行setImmediate()设定的回调。
  • close callbacks: 执行比如socket.on('close', ...)的回调。

每个阶段执行完毕后,都会执行所有微任务(先 nextTick,后其它),然后再进入下一个阶段。

Links

查看原文

谷雨 评论了文章 · 2018-07-15

JavaScript 异步、栈、事件循环、任务队列

概览

图片描述
我们经常会听到引擎和runtime,它们的区别是什么呢?

  • 引擎:解释并编译代码,让它变成能交给机器运行的代码(runnable commands)。
  • runtime:就是运行环境,它提供一些对外接口供Js调用,以跟外界打交道,比如,浏览器环境、Node.js环境。不同的runtime,会提供不同的接口,比如,在 Node.js 环境中,我们可以通过 require 来引入模块;而在浏览器中,我们有 window、 DOM。

Js引擎是单线程的,如上图中,它负责维护任务队列,并通过 Event Loop 的机制,按顺序把任务放入栈中执行。而图中的异步处理模块,就是 runtime 提供的,拥有和Js引擎互不干扰的线程。接下来,我们会细说图中的:栈和任务队列。

现在,我们要运行下面这段代码:

function bar() {
    console.log(1);
}

function foo() {
    console.log(2);
    far();
}

setTimeout(() => {
    console.log(3)
});

foo();

它在栈中的入栈、出栈过程,如下图:
图片描述

任务队列

Js 中,有两类任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks)。宏任务队列可以有多个,微任务队列只有一个。那么什么任务,会分到哪个队列呢?

  • 宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.
  • 微任务:process.nextTick, Promise, Object.observer, MutationObserver.

浏览器的 Event Loop

浏览器的 Event Loop 遵循的是 HTML5 标准,而 NodeJs 的 Event Loop 遵循的是 libuv。 区别较大,分开讲。

我们上面讲到,当stack空的时候,就会从任务队列中,取任务来执行。浏览器这边,共分3步:

  1. 取一个宏任务来执行。执行完毕后,下一步。
  2. 取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。
  3. 更新UI渲染。

Event Loop 会无限循环执行上面3步,这就是Event Loop的主要控制逻辑。其中,第3步(更新UI渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新UI成本大,所以,一般都会比较长的时间间隔,执行一次更新。

从执行步骤来看,我们发现微任务,受到了特殊待遇!我们代码开始执行都是从script(全局任务)开始,所以,一旦我们的全局任务(属于宏任务)执行完,就马上执行完整个微任务队列。看个例子:

console.log('script start');

// 微任务
Promise.resolve().then(() => {
    console.log('p 1');
});

// 宏任务
setTimeout(() => {
    console.log('setTimeout');
}, 0);

var s = new Date();
while(new Date() - s < 50); // 阻塞50ms

// 微任务
Promise.resolve().then(() => {
    console.log('p 2');
});

console.log('script ent');


/*** output ***/

// one macro task
script start
script ent

// all micro tasks
p 1
p 2

// one macro task again
setTimeout

上面之所以加50ms的阻塞,是因为 setTimeout 的 delayTime 最少是 4ms. 为了避免认为 setTimeout 是因为4ms的延迟而后面才被执行的,我们加了50ms阻塞。

NodeJs 的 Event Loop

NodeJs 的运行是这样的:

  • 初始化 Event Loop
  • 执行您的主代码。这里同样,遇到异步处理,就会分配给对应的队列。直到主代码执行完毕。
  • 执行主代码中出现的所有微任务:先执行完所有nextTick(),然后在执行其它所有微任务。
  • 开始 Event Loop

NodeJs 的 Event Loop 分6个阶段执行:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

以上的6个阶段,具体处理的任务如下:

  • timers: 这个阶段执行setTimeout()setInterval()设定的回调。
  • pending callbacks: 上一轮循环中有少数的 I/O callback 会被延迟到这一轮的这一阶段执行。
  • idle, prepare: 仅内部使用。
  • poll: 执行 I/O callback,在适当的条件下会阻塞在这个阶段
  • check: 执行setImmediate()设定的回调。
  • close callbacks: 执行比如socket.on('close', ...)的回调。

每个阶段执行完毕后,都会执行所有微任务(先 nextTick,后其它),然后再进入下一个阶段。

Links

查看原文

谷雨 发布了文章 · 2017-09-16

JavaScript 异步、栈、事件循环、任务队列

概览

图片描述
我们经常会听到引擎和runtime,它们的区别是什么呢?

  • 引擎:解释并编译代码,让它变成能交给机器运行的代码(runnable commands)。
  • runtime:就是运行环境,它提供一些对外接口供Js调用,以跟外界打交道,比如,浏览器环境、Node.js环境。不同的runtime,会提供不同的接口,比如,在 Node.js 环境中,我们可以通过 require 来引入模块;而在浏览器中,我们有 window、 DOM。

Js引擎是单线程的,如上图中,它负责维护任务队列,并通过 Event Loop 的机制,按顺序把任务放入栈中执行。而图中的异步处理模块,就是 runtime 提供的,拥有和Js引擎互不干扰的线程。接下来,我们会细说图中的:栈和任务队列。

现在,我们要运行下面这段代码:

function bar() {
    console.log(1);
}

function foo() {
    console.log(2);
    far();
}

setTimeout(() => {
    console.log(3)
});

foo();

它在栈中的入栈、出栈过程,如下图:
图片描述

任务队列

Js 中,有两类任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks)。宏任务队列可以有多个,微任务队列只有一个。那么什么任务,会分到哪个队列呢?

  • 宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.
  • 微任务:process.nextTick, Promise, Object.observer, MutationObserver.

浏览器的 Event Loop

浏览器的 Event Loop 遵循的是 HTML5 标准,而 NodeJs 的 Event Loop 遵循的是 libuv。 区别较大,分开讲。

我们上面讲到,当stack空的时候,就会从任务队列中,取任务来执行。浏览器这边,共分3步:

  1. 取一个宏任务来执行。执行完毕后,下一步。
  2. 取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。
  3. 更新UI渲染。

Event Loop 会无限循环执行上面3步,这就是Event Loop的主要控制逻辑。其中,第3步(更新UI渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新UI成本大,所以,一般都会比较长的时间间隔,执行一次更新。

从执行步骤来看,我们发现微任务,受到了特殊待遇!我们代码开始执行都是从script(全局任务)开始,所以,一旦我们的全局任务(属于宏任务)执行完,就马上执行完整个微任务队列。看个例子:

console.log('script start');

// 微任务
Promise.resolve().then(() => {
    console.log('p 1');
});

// 宏任务
setTimeout(() => {
    console.log('setTimeout');
}, 0);

var s = new Date();
while(new Date() - s < 50); // 阻塞50ms

// 微任务
Promise.resolve().then(() => {
    console.log('p 2');
});

console.log('script ent');


/*** output ***/

// one macro task
script start
script ent

// all micro tasks
p 1
p 2

// one macro task again
setTimeout

上面之所以加50ms的阻塞,是因为 setTimeout 的 delayTime 最少是 4ms. 为了避免认为 setTimeout 是因为4ms的延迟而后面才被执行的,我们加了50ms阻塞。

NodeJs 的 Event Loop

NodeJs 的运行是这样的:

  • 初始化 Event Loop
  • 执行您的主代码。这里同样,遇到异步处理,就会分配给对应的队列。直到主代码执行完毕。
  • 执行主代码中出现的所有微任务:先执行完所有nextTick(),然后在执行其它所有微任务。
  • 开始 Event Loop

NodeJs 的 Event Loop 分6个阶段执行:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

以上的6个阶段,具体处理的任务如下:

  • timers: 这个阶段执行setTimeout()setInterval()设定的回调。
  • pending callbacks: 上一轮循环中有少数的 I/O callback 会被延迟到这一轮的这一阶段执行。
  • idle, prepare: 仅内部使用。
  • poll: 执行 I/O callback,在适当的条件下会阻塞在这个阶段
  • check: 执行setImmediate()设定的回调。
  • close callbacks: 执行比如socket.on('close', ...)的回调。

每个阶段执行完毕后,都会执行所有微任务(先 nextTick,后其它),然后再进入下一个阶段。

Links

查看原文

赞 117 收藏 116 评论 21

谷雨 赞了文章 · 2017-09-11

JavaScript:彻底理解同步、异步和事件循环(Event Loop)

一. 单线程

我们常说“JavaScript是单线程的”。

所谓单线程,是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个。不妨叫它主线程

但是实际上还存在其他的线程。例如:处理AJAX请求的线程、处理DOM事件的线程、定时器线程、读写文件的线程(例如在Node.js中)等等。这些线程可能存在于JS引擎之内,也可能存在于JS引擎之外,在此我们不做区分。不妨叫它们工作线程

二. 同步和异步

假设存在一个函数A:

A(args...);

同步:如果在函数A返回的时候,调用者就能够得到预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。

例如:

Math.sqrt(2);
console.log('Hi');
  • 第一个函数返回时,就拿到了预期的返回值:2的平方根。

  • 第二个函数返回时,就看到了预期的效果:在控制台打印了一个字符串。

所以这两个函数都是同步的。

异步:如果在函数A返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。

例如:

fs.readFile('foo.txt', 'utf8', function(err, data) {
    console.log(data);
});

在上面的代码中,我们希望通过fs.readFile函数读取文件foo.txt中的内容,并打印出来。
但是在fs.readFile函数返回时,我们期望的结果并不会发生,而是要等到文件全部读取完成之后。如果文件很大的话可能要很长时间。

下面以AJAX请求为例,来看一下同步和异步的区别:

  • 异步AJAX:

  • 主线程:“你好,AJAX线程。请你帮我发个HTTP请求吧,我把请求地址和参数都给你了。”

  • AJAX线程:“好的,主线程。我马上去发,但可能要花点儿时间呢,你可以先去忙别的。”

  • 主线程::“谢谢,你拿到响应后告诉我一声啊。”

  • (接着,主线程做其他事情去了。一顿饭的时间后,它收到了响应到达的通知。)

  • 同步AJAX:

  • 主线程:“你好,AJAX线程。请你帮我发个HTTP请求吧,我把请求地址和参数都给你了。”

  • AJAX线程:“......”

  • 主线程::“喂,AJAX线程,你怎么不说话?”

  • AJAX线程:“......”

  • 主线程::“喂!喂喂喂!”

  • AJAX线程:“......”

  • (一炷香的时间后)

  • 主线程::“喂!求你说句话吧!”

  • AJAX线程:“主线程,不好意思,我在工作的时候不能说话。你的请求已经发完了,拿到响应数据了,给你。”

正是由于JavaScript是单线程的,而异步容易实现非阻塞,所以在JavaScript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。异步是这篇文章关注的重点。

三. 异步过程的构成要素

从上文可以看出,异步函数实际上很快就调用完成了。但是后面还有工作线程执行异步任务、通知主线程、主线程调用回调函数等很多步骤。我们把整个过程叫做异步过程。异步函数的调用在整个异步过程中,只是一小部分。

总结一下,一个异步过程通常是这样的:

主线程发起一个异步请求,相应的工作线程接收请求并告知主线程已收到(异步函数返回);主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,通知主线程;主线程收到通知后,执行一定的动作(调用回调函数)。

异步函数通常具有以下的形式:

A(args..., callbackFn)

它可以叫做异步过程的发起函数,或者叫做异步任务注册函数。args是这个函数需要的参数。callbackFn也是这个函数的参数,但是它比较特殊所以单独列出来。

所以,从主线程的角度看,一个异步过程包括下面两个要素:

  • 发起函数(或叫注册函数)A

  • 回调函数callbackFn

它们都是在主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果。

举个具体的例子:

setTimeout(fn, 1000);

其中的setTimeout就是异步过程的发起函数,fn是回调函数。

注意:前面说的形式A(args..., callbackFn)只是一种抽象的表示,并不代表回调函数一定要作为发起函数的参数,例如:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回调函数
xhr.open('GET', url);
xhr.send(); // 发起函数

发起函数和回调函数就是分离的。

四. 消息队列和事件循环

上文讲到,异步过程中,工作线程在异步操作完成后需要通知主线程。那么这个通知机制是怎样实现的呢?答案是利用消息队列和事件循环。

用一句话概括:

工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。

  • 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。

  • 事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。

实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。

事件循环用代码表示大概是这样的:

while(true) {
    var message = queue.get();
    execute(message);
}

那么,消息队列中放的消息具体是什么东西?消息的具体结构当然跟具体的实现有关,但是为了简单起见,我们可以认为:

消息就是注册异步任务时添加的回调函数。

再次以异步AJAX为例,假设存在如下的代码:

$.ajax('http://segmentfault.com', function(resp) {
    console.log('我是响应:', resp);
});

// 其他代码
...
...
...

主线程在发起AJAX请求后,会继续执行其他代码。AJAX线程负责请求segmentfault.com,拿到响应后,它会把响应封装成一个JavaScript对象,然后构造一条消息:

// 消息队列中的消息就长这个样子
var message = function () {
    callbackFn(response);
}

其中的callbackFn就是前面代码中得到成功响应时的回调函数。

主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息(也就是message函数),并执行它。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,AJAX线程在收到HTTP响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。

用图表示这个过程就是:

图片描述

从上文中我们也可以得到这样一个明显的结论,就是:

异步过程的回调函数,一定不在当前这一轮事件循环中执行。

五. 异步与事件

上文中说的“事件循环”,为什么里面有个事件呢?那是因为:

消息队列中的每条消息实际上都对应着一个事件。

上文中一直没有提到一类很重要的异步过程:DOM事件

举例来说:

var button = document.getElement('#btn');
button.addEventListener('click', function(e) {
    console.log();
});

从事件的角度来看,上述代码表示:在按钮上添加了一个鼠标单击事件的事件监听器;当用户点击按钮时,鼠标单击事件触发,事件监听器函数被调用。

从异步过程的角度看,addEventListener函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。

事件的概念实际上并不是必须的,事件机制实际上就是异步过程的通知机制。我觉得它的存在是为了编程接口对开发者更友好。

另一方面,所有的异步过程也都可以用事件来描述。例如:setTimeout可以看成对应一个时间到了!的事件。前文的setTimeout(fn, 1000);可以看成:

timer.addEventListener('timeout', 1000, fn);

六. 生产者与消费者

从生产者与消费者的角度看,异步过程是这样的:

工作线程是生产者,主线程是消费者(只有一个消费者)。工作线程执行异步任务,执行完成后把对应的回调函数封装成一条消息放到消息队列中;主线程不断地从消息队列中取消息并执行,当消息队列空时主线程阻塞,直到消息队列再次非空。

七. 总结一下

最后再用一个生活中的例子总结一下同步和异步:在公路上,汽车一辆接一辆,有条不紊的运行。这时,有一辆车坏掉了。假如它停在原地进行修理,那么后面的车就会被堵住没法行驶,交通就乱套了。幸好旁边有应急车道,可以把故障车辆推到应急车道修理,而正常的车流不会受到任何影响。等车修好了,再从应急车道回到正常车道即可。唯一的影响就是,应急车道用多了,原来的车辆之间的顺序会有点乱。

这就是同步和异步的区别。同步可以保证顺序一致,但是容易导致阻塞;异步可以解决阻塞问题,但是会改变顺序性。改变顺序性其实也没有什么大不了的,只不过让程序变得稍微难理解了一些 :)

PS:ECMAScript 262规范中,并没有对异步、事件队列等概念及其实现的描述。这些都是具体的JavaScript运行时环境使用的机制。本文重点是描述异步过程的原理,为了便于理解做了很多简化。所以文中的某些术语的使用可能是不准确的,具体细节也未必是正确的,例如消息队列中消息的结构。请读者注意。

查看原文

赞 282 收藏 599 评论 63

谷雨 发布了文章 · 2017-09-10

简述JavaScript的垃圾回收机制

不管是高级语言,还是低级语言。内存的管理都是:

  1. 分配内存
  2. 使用内存(读或写)
  3. 释放内存

前两步,大家都没有太大异议。关键是释放内存这一步,各种语言都有自己的垃圾回收(garbage collection, 简称GC)机制。做GC的第一步是判断堆中存的是数据还是指针,是指针的话,说明它被指向活跃的对象。有3种判断方法:

  1. Conservative:如果存储格式是地址,就认为是。C/C++有用到这种算法。
  2. Compiler hints:对于静态语言,比如Java,编译器是知道它是不是指针的,所以可以用这种。
  3. Tagged pointers:JavaScript用的是这种,在字末位进行标识,1为指针。

对于JavaScript而言,最初的垃圾回收机制,是基于引用计次来做的。后来升级为标记清除。

引用计次

当对象被引用次数为0时,就被回收。潜在的一个问题是:循环引用时,两个对象都至少被引用了一次,将不能自动被回收。所以导致,我们常讲的内存泄露。

// 引用计次
var a = {t: 1}; // 对象 `{t: 1}` (以下简称obj)被引用一次
var b = a; // obj 被引用两次
a = null; // obj 现在为1次
b = null; // obj 现在为0次,可回收

// 循环引用
function fn() {
    var a = {};
    var b = {};
    a.b = b;
    b.a = a;
}

fn();

标记清除

这是当前主流的GC算法,V8里面就是用这种。当对象,无法从根对象沿着引用遍历到,即不可达(unreachable),进行清除。对于上面的例子,fn() 里面的 ab 在函数执行完毕后,就不能通过外面的上下文进行访问了,所以就可以清除了。

下面,我们简述下V8的GC机制:

V8的GC机制

在大部分的应用场景:一个新创建的对象,生命周期通常很短。所以,V8里面,GC处理分为两大类:新生代和老生代。

新生代的堆空间为1M~8M,而且被平分成两份(to-space和from-space),通常一个新创建的对象,内存被分配在新生代。当to-space满的时候,to-space和form-space交换位置(此时,to空,from满),并执行GC.如果一个对象被断定为,未被引用,就清除;有被引用,逃逸次数+1(如果此时逃逸次数为2,就移入老生代,否则移入to-space)。

老生代的堆空间大,GC不适合像新生代那样,用平分成两个space这种空间换时间的方式。老生代的垃圾回收,分两个阶段:标记、清理(有Sweeping和Compacting这两种方式)。

标记,采用3色标记:黑、白、灰。步骤如下:

  1. GC开始,所以对象标记为白色。
  2. 根对象标记为黑色,并开始遍历其子节点(引用的对象)。
  3. 当前被遍历的节点,标记为灰色,被放入一个叫 marking bitmap 的栈。在栈中,把当前被遍历的节点,标记为黑色,并出栈,同时,把它的子节点(如果有的话)标记为灰色,并压入栈。(大对象比较特殊,这里不展开)
  4. 当所有对象被遍历完后,就只剩下黑和白。通过Sweeping或Compacting的方式,清理掉白色,完成GC。

小补充:JavaScript的根对象

GC的时候,从根对象开始遍历。在浏览器,根对象是 window;在 Node.js 中,是 global(或称为root).

root.png

Node.js中,每个文件被当做一个模块,所以,当你用 var/let/const 在文件的全局,声明变量的时候,作用域是当前文件(模块)。因此,图中 root.aundefined.

Links:

查看原文

赞 8 收藏 6 评论 2

谷雨 赞了文章 · 2017-08-20

为什么我选择使用 VS Code进行前端开发?

VS Code

没错,我就是来给大家安利 VS Code 的。

对前端来说,这是一款性感无比的 IDE,哦不对应该是编辑器。我们团队有大部分人已经在用了,所以这周五在组内做了一个 VS Code 小分享,来发掘 VSC 一些提高开发效率的小技巧。我相信已经有不少前端在使用它了,所以大家更有必要一起分享下日常神操作了。

为什么选择 VS Code ?

在 VSC 刚出来的时候,我就开始使用了(如何评价 Visual Studio Code?),理由很简单:

  • 开源,免费,颜值高;
  • 微软出品,实力保证。

然而用了一阵发现还是 Sublime 好用,一是刚出来功能不完善,Sublime 一些技巧无法迁移过来,另外就是插件太少,实际开发略显吃力。然随着后面 VSC 一次次更新,不少新 features 着实让人眼前一亮:微软这是在用心做产品呐!以至于现在已经没有什么可以抱怨的了(有趣的是当天尤大也发微博说转投 VSC 了,可以预见这款产品未来会越做越好,方向选对了,就不怕路走错)。

而作为前端,VSC 简直就是为我们量身定制:

VSC 本身是基于当前大火的 TS 来写的,所以对于 TS 的支持自然很好;又是基于 electron 开发,底层 Node.js 对前端来说再熟悉不过了。所以,如果你发现哪里不好用,你可以自己写插件呐!如果一个满足不了,那就写两个。

而对于 VSC 的扩展开发也是相当友好,你只需要5步:

  1. 申请一个 Visual Studio Team Services Account
  2. 添加一个 Personal access token;
  3. 创建一个发布账号,用来发布你的扩展即可(以上操作完全免费);
  4. 使用 VSC 的 Yeoman 脚手架初始化你的扩展项目,之后就是调用官方提供的 API 开发你的扩展即可,就跟开发 Chrome 插件一样;
  5. 使用官方发布工具vsce来发布你的扩展到扩展市场,之后别人就可以搜到你的扩展啦!

所以对于前端来说,都是我们熟悉的技术栈,你可以作为一个使用者,也可以转身变成一个开发贡献者!

而对于 Sublime 和 WebStorm 来说就没有这么方便了,当然你也可以用 Python 或者 Java 来贡献插件,不过对于前端还是稍稍有些门槛。如果仅仅是使用的话,WebStrom 确实也很好用,毕竟人家收了钱,而且你最好买一个高配的电脑,否则代码撸多了,会卡到你怀疑人生。

一些实用扩展和技巧

这才是重点。先贡献下自己的部分扩展列表:



各位要是有啥好用的扩展也分享一下呐,大家一起 get 新姿势!(有趣的是新版的 VSC在扩展栏增加了推荐栏,这样大家能更方便的发现一些精品)

强烈推荐的几个插件:

  • Complete JSDoc Tags(好的注释不仅对项目有用,对 VSC 的代码智能感知也很有用)
  • Dash(如果你购买了 Dash App 的话)
  • EditorConfig for VS Code(统一的编辑配置对团队开发很有用)
  • ESLint(让 VSC 内置 ESLint)
  • Git History(装完输入 git log有惊喜)
  • Git Lens(让本就集成了 git 的 VSC 更加强大)
  • Path Intellisense(文件路径感知扩展)
  • Project Manager(多项目管理神器)
  • Settings Sync(将你所有的编辑器配置同步到 gist,省得在新设备上重新捣鼓)

详细的介绍我就懒得写了,大家自己去探索发现吧,一些有用的资源:

一些小技巧:

  • 每次更新 VSC 后,好好看下更新日志,有惊喜;
  • 有事没事逛逛扩展市场,有惊喜;
  • 有时间仔细看看官方文档,有惊喜;
  • 好好研究下控制面板和快捷键,你会发现很多命令不用记;
  • VSC 本身是默认 git 工作流的,基于 git 项目进行开发体验更佳,不要让自己的工作区处于非 git repo 文件夹。

总结

不管你以前是用 Sublime 还是 WebStorm,又或者是 Atom 和 Eclipse,现在迁移到 VS Code 都是灰常方便的:

最后,我为什么要安利 VS Code?

毕竟用的人越多,插件市场越丰富,解决问题更快捷,交流起来更愉快嘛!

查看原文

赞 49 收藏 150 评论 30

谷雨 赞了文章 · 2017-08-17

前端每周清单:Node.js 微服务实践,Vue.js 与 GraphQL,Angular 组件技巧

前端每周清单第 26 期:Node.js 微服务实践,Vue.js 与 GraphQL,Angular 组件技巧,HeadlessChrome 攻防

作者:王下邀月熊编辑:徐川

前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点;分为新闻热点、开发教程、工程实践、深度阅读、开源项目、巅峰人生等栏目。欢迎关注【前端之巅】微信公众号(ID:frontshow),及时获取前端每周清单。

新闻热点

国内国外,前端最新动态

  • Firefox 55 引入 WebVR 支持:近日发布的 Windows 平台上的 Firefox 55 版本,成为首个支持新 WebVR 标准的桌面浏览器;Mozilla 希望能够以此推动 WebVR 的进一步发展。除此之外,Firefox 55 还支持包括 async generators 在内的众多 ES2017/2018 特性,并且进一步提升了浏览器的性能表现以及安全保障;详细的特性说明请查看原文。
  • Google 开源 JavaScript 机器学习库 DeepLearn.js:作为 Google 开源的可实现硬件加速的机器学习 JavaScript 库,DeepLearn.js 提供高效的机器学习构建模块,使我们能够在浏览器中训练神经网络或在推断模式中运行预训练模型。它提供构建可微数据流图的 API,以及一系列可直接使用的数学函数。
  • Next.js 3.0 发布:Next.js 是用于快速创建 React 应用的零配置、单命令工具链,其内建支持了服务端渲染、代码分割等特性。在 3.0 版本中,Next.js 引入了静态导出功能,可以将 Next.js 应用导出为静态界面;同时添加了动态导入的支持,允许动态导入外部依赖,动态导入 React 组件等操作。
  • Node.js 8.3.0 发布,引入 Ignition 与 TurboFan 执行流:Ignition 与 TurboFan 是 V8 5.9 版本中提供的新的解释器与编译器,它们替换了自 2010 年以来的 Full-codegen 与 Crankshaft,可以阅读这篇文章了解新的执行流带来的巨大性能优化。新版本为 Node.js 带来了极大的性能提升,除此之外新版本还修复了 8.x 系列中存在的部分错误,详细的特性与更新列表可以查看原文。
  • Angular 5 与 Progressive Web Apps:即将于九月份发布的 Angular 5 版本将会是首个由 Google 驱动的 Progressive Web Apps 框架,Angular 5 中提供的特性包括:内建的 Progressive Web Apps 支持、能够移除冗余代码、压缩应用体积的构建优化器、服务端渲染中集成 Material Design 组件等。本文还介绍了对于 Angular 6 的开发计划的简述以及依赖注入、HTML 模板引擎等一系列 Angular 优势的分析。

开发教程

步步为营,掌握基础技能

  • 三周时间打造全栈 JavaScript Web 应用:本文记录了某个编程初学者如何用三周时间,循序渐进地从零构建出,基于 JavaScript 的全栈电子商务应用。本文从最初的产品设计与原型图构建开始,然后介绍了如何选择合适的数据结构与数据库。接下来介绍了如何创建 Github 仓库并且使用敏捷开发流程,最后介绍了如何利用 Express 与 Firebase 搭建服务端、使用 React 以及 Victory.js 构建前端应用等内容;更多 JavaScript 相关学习参考现代 JavaScript 开发:语法基础与实践技巧
  • 利用 GraphQL 创建同构 Vue.js 应用:GraphQL 是由 Facebook 开源的面向接口的查询语言,能够弥补 REST API 中的不足;本文即介绍如何协同使用 Vue.js 与 GraphQL 来开发同构应用。本文首先介绍了如何搭建基本的 GraphQL 服务器,然后讨论了如何在 Vue.js 项目中引入 vue-apollo、ApolloClient 等依赖项并且创建简单的 GraphQL 客户端实例,最后介绍了在 Vue.js 组件中使用 graphql-tag 提供的便捷指令来快速实现前后台的数据查询;更多 GraphQL 相关资料参考这里
  • 利用 Node.js 构建 API Gateway:随着现代业务复杂度的增加,微服务的理念正在得到更多的落地实践;作为微服务架构的重要组成部分,API Gateway 能够为所有的后端服务提供统一的权限校验与客户端协议兼容的抽象层。本文首先介绍了微服务的基础架构与 API Gateway 的概念,然后介绍了面向前端团队的 Node.js API Gateway 组成;接下来详细的分析了 API Gateway 的基础功能需求:路由与版本、迭代式设计、权限校验、数据聚合、数据序列化与反序列化、限流与缓存等等,最后讨论了基于 Express 的 API Gateway 的实现。更多 Node.js 相关资料参考这里
  • 利用 VasSonic 构建高性能 H5 首屏渲染:VasSonic 是由腾讯 VAS 团队开发的轻量级高性能混合框架,它能够有效地提升 Android 与 iOS 平台上网站的首屏加载速度;VasSonic 不仅能够优化服务端渲染地静态或者动态网站,还能够对于 Web 缓存资源进行有效优化。VasSonic 使用了自定义的 URL 连接来替代原本的网络连接来请求 index 界面,因此它能够提前或者并发地请求资源,从而避免了用户额外的等待时间;更多使用信息与特性请参考原本。

工程实践

立足实践,提示实际水平

  • 利用 std/esm 在 Node.js 开发中使用 ES Modules:随着主流浏览器逐步开始支持 ES Modules 标准,越来越多的目光投注于 Node.js 对于 ESM 的支持实现上;Node.js 拟计划在 2020 年发布的 9.x 版本中引入内置的 ESM 支持。而近日正式发布的 @std/esm 为我们提供了高性能的 Node.js 中 CommonJS 与 ES Modules 模块间调用,其能够作用于 Node.js 4.x 以上版本;它能够顺滑地集成到现有的 Webpack、Babel 环境中,并且支持不同模块使用不同的依赖版本。不同于目前的解决方案需要是发布编译之后的 CommonJS 格式的文件,@std/esm 能够以最小的代价的、按需转化的、动态缓存的方式来进行源代码转化。更多 Node.js 相关资料参考这里
  • 使用 Angular 组件的四个技巧:从.5 版本的 AngularJS 开始,组件就成为 Angular 的一部分,它为代码的组织和回收提供了一种便捷的方式。Angular(Angular2 的简称)与其说是 Angular 1.x 的升级,不如说是“续集”,它在移动支持和其他功能的基础上进行了完全地重写。这里,1.x 中使用的控制器完全被组件取代。无论是否曾经使用或想继续坚持 1.x,无论是从零学起还是在跨越阶段,为了确保代码尽可能地优雅且不会过时,你都需要开始使用组件。无论属于以上哪一类,都可以在这里找到很多帮助简化流程的方法。
  • 基于 Shadow DOM 的样式封装:Shadow DOM 是 Web Components 标准的重要组成部分,它能够将 DOM 树进行隔离封装,而本文则是介绍如何利用 Shadow DOM 实现对于样式类的隔离封装。由于 CSS 并没有提供内置的模块化或者作用域机制,而在大型项目中不同组件间的样式又极易引发冲突,因此我们需要选择合适的 CSS 样式隔离方案。目前常用的隔离方案有 BEM 命名策略、IFrame、CSS Modules、CSS-in-JS 等,本文首先盘点了这些方案的优势与不足;然后介绍了 Shadow DOM 的基本原理以及如何应用到样式封装上。更多 CSS/SCSS 相关资料参考这里
  • Headless Chrome 爬虫攻防:Headless Chrome 为我们提供了便捷的自动化浏览器操作方式,也方便我们构建面向动态网页的爬虫;前几日在 Hacker News 上的一篇如何检测 Headless Chrome 的文章,详细列举了可用的辨别是否为 Headless Chrome 的方法,而本文即是讨论了如何反制这些检测方案。方案包括对于 User Agent 检测可以在启动时自定义 User Agent、对于语言与插件的检测可以插入脚本动态修改 language 与 plugins 属性、对于 WebGL Vendor 与 Renderer 可以 Hook 参数调用、对于 Broken Image 可以修改文件描述符等。
  • WebAssembly 在 PSPDFKIT 的实践:随着今年三月份 WebAssembly Community Group 就标准达成一致,越来越多的主流浏览器开始支持 WebAssembly,本文即是 PDF 工具开发者 PSPDFKIT 介绍它们利用 WebAssembly 开发浏览器端渲染的 PDS 预览工具的实践经验。本文首先介绍了 asm.js 的工作原理与编译机制,然后阐述了 WebAssembly 的概念与组成,最后介绍了 WebAssembly 在 PSPDFKit 的实践经验以及他们在将原本大型 C++ 代码库转化为 WebAssembly 格式时的体验;更多 WebAssembly 相关资料参考这里

深度阅读

深度思考,升华开发智慧

  • React 全家桶:石墨文档大前端技术选型分享:技术选型是个很大的话题。对于创业公司而言,为了适应业务节奏,「灵活」与「高开发效率」是技术选型最看重的两点。而这两点也是这些年前端技术井喷时期新出现的技术最注重解决的两个问题。然而石墨文档作为一款拥有众多企业用户的富前端应用,复杂的表格、文档以及离线同步逻辑使得我们对于前端技术栈的工程化和稳定性有很高的要求,考虑到过于新的技术往往生态尚不完善以及相关的「最佳实践」缺乏验证,我们对于这些技术的选择相对谨慎。石墨文档前端团队的所有技术选型也都是围绕如上两点考虑的;更多 React 相关资料参考这里
  • 基于 Vue.js 的原生应用开发:Weex 与 NativeScript 对比:Vue.js 相较于 React 与 Angular 有着更为平滑的学习曲线,不过目前 Vue.js 还没有内建的类似于 React Native 这样的原生应用开发方案。但是 Weex 与 NativeScript 都能够弥补 Vue.js 的这个暂时性不足,每周清单在前几期中也推荐过 NativeScript 与 Vue.js 协同开发的相关文章,本文即是对比 Weex 与 NativeScript 应用在原生开发中的各自优势与不足;更多 Vue.js 相关资料参考这里
  • 你看到的 Node.js 权限校验指南可能都存在着错误:权限校验几乎是每个服务端应用程序的标配,本文作者在搜索学习 Node.js / Express.js 相关的权限校验教程时发现大部分都或多或少地存在着问题,因此编撰了这篇文章以提醒其他开发者。常见的误区可能包括凭证的存储方式、密码的重置策略、API Tokens 的生成与校验、限流等多个方面;更多 Node.js 相关资料参考这里
  • 高性能 Web 动画与交互:到达 60 PFS:为用户提供顺滑的交互与动画体验是大部分 Web 应用的挑战之一,很多开发者着眼于减少首屏加载时间,却忘了去优化接下来的用户交互。本文是来自 Algolia 的工程师介绍他们在构建高性能 Web 动画与交互时的经验技巧,本文首先介绍了常用的性能评测标准以及浏览器的渲染流程,布局、绘制、组合等等。接下来本文介绍了如何充分利用 opacity 与 transform 属性来减少动画消耗、如何强制提升、如何优化动画相关的代码等内容;

开源项目

乐于分享,共推前端发展

  • Nano ID: Nano ID 是轻量级的、支持 URL 的 JavaScript 唯一 ID 生成器,它使用了强力密码加密的随机 API,能够保证生成符号分布的平均性。
  • Resonance:Resonance 是数据驱动的高性能 React 动画库,它使用了 d3-timer 来管理成百上千地状态变化;Resonance 允许开发者以简单而亲切地语法实现高性能的状态更新动画。
  • react-beautiful-dnd:react-beautiful-dnd 是 Atlassian 出品的漂亮易用的 React 列表拖拽功能增强库。目前最流行的 React 拖拽库 react-dnd 提供了相对底层的拖拽 API 支持,而 react-beautiful-dnd 则提供了面向垂直列表的高阶封装;react-beautiful-dnd 仍然处于不断地迭代开发中,很期待它未来提供更多的优秀特性。
  • notifme-sdk:notifme-sdk 是用于简化通知发送流程的 Node.js 库,它允许我们灵活地集成邮件、短信、推送、WebPush 等不同的渠道来发送通知;notifme-sdk 还允许我们自由注册服务提供商,内建的 Fallback 与轮询机制也能进行简单的容错,同时 notifme-sdk 还提供了简单的 UI 控制台以方便我们仅界面化监控。

巅峰人生

  • 如何成为一个合格的技术 Leader?:在即将到来的 10 月份上海 QCon 大会上,百度外卖研发中心总监张灿将带来演讲《向前一步——年轻技术管理者的涅槃重生》,InfoQ 在此之前,对张灿老师进行了一次独家专访,让张灿老师聊聊作为女性技术人的成长感悟与对技术人转向管理者的思考。本文即由采访内容整理而成。

前端之巅

「前端之巅」是InfoQ旗下关注前端技术的垂直社群,加入前端之巅学习群请关注「前端之巅」公众号后回复“加群”。投稿请发邮件到editors@cn.infoq.com,注明“前端之巅投稿”;或者可以阅读往期文章:

前端之巅微信底图-5.jpg

查看原文

赞 10 收藏 34 评论 0

谷雨 赞了文章 · 2017-08-16

防雪崩利器:熔断器 Hystrix 的原理与使用

前言

分布式系统中经常会出现某个基础服务不可用造成整个系统不可用的情况, 这种现象被称为服务雪崩效应. 为了应对服务雪崩, 一种常见的做法是手动服务降级. 而Hystrix的出现,给我们提供了另一种选择.

服务雪崩效应的定义

服务雪崩效应是一种因 服务提供者 的不可用导致 服务调用者 的不可用,并将不可用 逐渐放大 的过程.如果所示:

图片描述

上图中, A为服务提供者, B为A的服务调用者, C和D是B的服务调用者. 当A的不可用,引起B的不可用,并将不可用逐渐放大C和D时, 服务雪崩就形成了.

服务雪崩效应形成的原因

我把服务雪崩的参与者简化为 服务提供者服务调用者, 并将服务雪崩产生的过程分为以下三个阶段来分析形成的原因:

  1. 服务提供者不可用

  2. 重试加大流量

  3. 服务调用者不可用

图片描述

服务雪崩的每个阶段都可能由不同的原因造成, 比如造成 服务不可用 的原因有:

  • 硬件故障

  • 程序Bug

  • 缓存击穿

  • 用户大量请求

硬件故障可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问.
缓存击穿一般发生在缓存应用重启, 所有缓存被清空时,以及短时间内大量缓存失效时. 大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用.
在秒杀和大促开始前,如果准备不充分,用户发起大量请求也会造成服务提供者的不可用.

而形成 重试加大流量 的原因有:

  • 用户重试

  • 代码逻辑重试

在服务提供者不可用后, 用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单.
服务调用端的会存在大量服务异常后的重试逻辑.
这些重试都会进一步加大请求流量.

最后, 服务调用者不可用 产生的主要原因是:

  • 同步等待造成的资源耗尽

当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源. 一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了.

服务雪崩的应对策略

针对造成服务雪崩的不同原因, 可以使用不同的应对策略:

  1. 流量控制

  2. 改进缓存模式

  3. 服务自动扩容

  4. 服务调用者降级服务

流量控制 的具体措施包括:

  • 网关限流

  • 用户交互限流

  • 关闭重试

因为Nginx的高性能, 目前一线互联网公司大量采用Nginx+Lua的网关进行流量控制, 由此而来的OpenResty也越来越热门.

用户交互限流的具体措施有: 1. 采用加载动画,提高用户的忍耐等待时间. 2. 提交按钮添加强制等待时间机制.

改进缓存模式 的措施包括:

  • 缓存预加载

  • 同步改为异步刷新

服务自动扩容 的措施主要有:

  • AWS的auto scaling

服务调用者降级服务 的措施包括:

  • 资源隔离

  • 对依赖服务进行分类

  • 不可用服务的调用快速失败

资源隔离主要是对调用服务的线程池进行隔离.

我们根据具体业务,将依赖服务分为: 强依赖和若依赖. 强依赖服务不可用会导致当前业务中止,而弱依赖服务的不可用不会导致当前业务的中止.

不可用服务的调用快速失败一般通过 超时机制, 熔断器 和熔断后的 降级方法 来实现.

使用Hystrix预防服务雪崩

Hystrix [hɪst'rɪks]的中文含义是豪猪, 因其背上长满了刺,而拥有自我保护能力. Netflix的 Hystrix 是一个帮助解决分布式系统交互时超时处理和容错的类库, 它同样拥有保护系统的能力.

Hystrix的设计原则包括:

  • 资源隔离

  • 熔断器

  • 命令模式

资源隔离

货船为了进行防止漏水和火灾的扩散,会将货仓分隔为多个, 如下图所示:

图片描述

这种资源隔离减少风险的方式被称为:Bulkheads(舱壁隔离模式).
Hystrix将同样的模式运用到了服务调用者上.

在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如:
商品详情展示服务会依赖商品服务, 价格服务, 商品评论服务. 如图所示:

图片描述

调用三个依赖服务会共享商品详情服务的线程池. 如果其中的商品评论服务不可用, 就会出现线程池里所有线程都因等待响应而被阻塞, 从而造成服务雪崩. 如图所示:

图片描述

Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离, 从而避免服务雪崩.
如下图所示, 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用.

图片描述

熔断器模式

熔断器模式定义了熔断器开关相互转换的逻辑:

图片描述

服务的健康状况 = 请求失败数 / 请求总数.
熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的.

  1. 当熔断器开关关闭时, 请求被允许通过熔断器. 如果当前健康状况高于设定阈值, 开关继续保持关闭. 如果当前健康状况低于设定阈值, 开关则切换为打开状态.

  2. 当熔断器开关打开时, 请求被禁止通过.

  3. 当熔断器开关处于打开状态, 经过一段时间后, 熔断器会自动进入半开状态, 这时熔断器只允许一个请求通过. 当该请求调用成功时, 熔断器恢复到关闭状态. 若该请求失败, 熔断器继续保持打开状态, 接下来的请求被禁止通过.

熔断器的开关能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待. 并且熔断器能在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能.

命令模式

Hystrix使用命令模式(继承HystrixCommand类)来包裹具体的服务调用逻辑(run方法), 并在命令模式中添加了服务调用失败后的降级逻辑(getFallback).
同时我们在Command的构造方法中可以定义当前服务线程池和熔断器的相关参数. 如下代码所示:

public class Service1HystrixCommand extends HystrixCommand<Response> {
  private Service1 service;
  private Request request;

  public Service1HystrixCommand(Service1 service, Request request){
    supper(
      Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ServiceGroup"))
          .andCommandKey(HystrixCommandKey.Factory.asKey("servcie1query"))
          .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("service1ThreadPool"))
          .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
            .withCoreSize(20))//服务线程池数量
          .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
            .withCircuitBreakerErrorThresholdPercentage(60)//熔断器关闭到打开阈值
            .withCircuitBreakerSleepWindowInMilliseconds(3000)//熔断器打开到关闭的时间窗长度
      ))
      this.service = service;
      this.request = request;
    );
  }

  @Override
  protected Response run(){
    return service1.call(request);
  }

  @Override
  protected Response getFallback(){
    return Response.dummy();
  }
}

在使用了Command模式构建了服务对象之后, 服务便拥有了熔断器和线程池的功能.
图片描述

Hystrix的内部处理逻辑

下图为Hystrix服务调用的内部逻辑:
图片描述

  1. 构建Hystrix的Command对象, 调用执行方法.

  2. Hystrix检查当前服务的熔断器开关是否开启, 若开启, 则执行降级服务getFallback方法.

  3. 若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否能接收新的请求, 若超过线程池已满, 则执行降级服务getFallback方法.

  4. 若线程池接受请求, 则Hystrix开始执行服务调用具体逻辑run方法.

  5. 若服务执行失败, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.

  6. 若服务执行超时, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.

  7. 若服务执行成功, 返回正常结果.

  8. 若服务降级方法getFallback执行成功, 则返回降级结果.

  9. 若服务降级方法getFallback执行失败, 则抛出异常.

Hystrix Metrics的实现

Hystrix的Metrics中保存了当前服务的健康状况, 包括服务调用总次数和服务调用失败次数等. 根据Metrics的计数, 熔断器从而能计算出当前服务的调用失败率, 用来和设定的阈值比较从而决定熔断器的状态切换逻辑. 因此Metrics的实现非常重要.

1.4之前的滑动窗口实现

Hystrix在这些版本中的使用自己定义的滑动窗口数据结构来记录当前时间窗的各种事件(成功,失败,超时,线程池拒绝等)的计数.
事件产生时, 数据结构根据当前时间确定使用旧桶还是创建新桶来计数, 并在桶中对计数器经行修改.
这些修改是多线程并发执行的, 代码中有不少加锁操作,逻辑较为复杂.

图片描述

1.5之后的滑动窗口实现

Hystrix在这些版本中开始使用RxJava的Observable.window()实现滑动窗口.
RxJava的window使用后台线程创建新桶, 避免了并发创建桶的问题.
同时RxJava的单线程无锁特性也保证了计数变更时的线程安全. 从而使代码更加简洁.
以下为我使用RxJava的window方法实现的一个简易滑动窗口Metrics, 短短几行代码便能完成统计功能,足以证明RxJava的强大:

@Test
public void timeWindowTest() throws Exception{
  Observable<Integer> source = Observable.interval(50, TimeUnit.MILLISECONDS).map(i -> RandomUtils.nextInt(2));
  source.window(1, TimeUnit.SECONDS).subscribe(window -> {
    int[] metrics = new int[2];
    window.subscribe(i -> metrics[i]++,
      InternalObservableUtils.ERROR_NOT_IMPLEMENTED,
      () -> System.out.println("窗口Metrics:" + JSON.toJSONString(metrics)));
  });
  TimeUnit.SECONDS.sleep(3);
}

总结

通过使用Hystrix,我们能方便的防止雪崩效应, 同时使系统具有自动降级和自动恢复服务的效果.

查看原文

赞 98 收藏 171 评论 16

谷雨 赞了文章 · 2017-08-09

Node 性能优化

前言

这篇文章也同样发表在我的个人博客中,欢迎访问:Node 性能优化

没有 profile 谈优化都是耍流氓,性能优化的大前提是 profile ,有数据才能找出程序慢在哪里了。
本篇文章主要介绍 Node 后端的性能优化,前端的同学可以看看 Chrome 的 devtools https://github.com/CN-Chrome-...

一、Web 应用优化

性能的瓶颈往往在 IO

IO 层优化

磁盘 IO 为什么慢

计算机里的常见 IO 有 :

  • CPU 一二级缓存

  • 内存

  • 硬盘

  • 网络

硬盘的 IO 开销是非常昂贵的,硬盘 IO 花费的 CPU 时钟周期是内存的 41000000/250 = 164000 倍。

所有在一般应用中,优化要首先考虑数磁盘 IO , 通常也就是数据层的优化,说到数据库优化,很多人第一时间会想到加索引,但是什么加了索引查询会变快呢?索引要怎么加才合适呢?

为什么索引快

关于索引的原理可以看看这篇文章,索引原理。索引快主要的原因是:

  • 索引占用空间更小,可以有效减少磁盘 IO 次数。

  • 索引可以使用方便快速查询的数据结构,如b+树

索引怎么加

回到我们的主题,没有 profile 谈优化都是耍流氓
以 mongo 为例,mongo 是带有慢查询功能的。
MongoDB 查询优化分析 这篇文章介绍了如何开启和使用 mongo 的慢查询功能。
开启慢查询收集功能后,使用 db.system.profile.find().pretty() 语句可以查询到哪些语句的查询比较慢。以下面这个查询语句为例:

query new_koala.llbrandomredpackage query: { user_id: "56ddb33e23db696f89fdae2a", status: { $ne: 1 } }

查询条件是 user_id、status 两个,所以给这两个字段加上索引可以提高查询速度。
当然,如果 mongo 没有是先开启慢查询,扫描一下 mongo.log 也是个办法。

grep '[0-9][0-9][0-9]ms' /var/log/mongodb/mongodb.log

这样就可以找出所有查询耗时大于100 ms 的记录。然后再对症下药即可。

缓存大法好,有选择地用。

上文有说到,内存 IO 比磁盘 IO 快非常多,所以使用内存缓存数据是有效的优化方法。常用的工具如 redis、memcached 等。
缓存效果显著,所以很多时候一谈到优化,很多人就会想到加缓存,但是使用缓存是有代价的,你需要维护缓存的更新和失效,这是个繁琐的事情,用上了缓存后你会经常碰到缓存没有及时更新带来的问题。
重要的事情说多几遍:
缓存有副作用
缓存有副作用
缓存有副作用

并不是所有数据都需要缓存,访问频率高,生成代价比较高的才考虑是否缓存,也就是说影响你性能瓶颈的考虑去缓存。

而且缓存还有 缓存雪崩缓存穿透 等问题要解决。见 缓存穿透与缓存雪崩

静态文件缓存

静态文件如图片、js 文件等具有不变性,是非常适合做缓存的。
常见的静态文件缓存服务有 nginx、vanish 等。

代码层面优化。

合并查询

在代码这一块,常做的事情是将多次的查询合并为一次,消灭 for 循环,实际上还是减少数据库查询。例如

for user_id in userIds 
     var account = user_account.findOne(user_id)
     

这类代码实际上可以改写成:

var user_account_map = {}   // 注意这个对象将会消耗大量内存。
user_account.find(user_id in user_ids).forEach(account){
    user_account_map[account.user_id] =  account
}
for user_id in userIds 
    var account = user_account_map[user_id]

这样就把 N 次的查询合并为一次。
实际上还是为了减少 IO。

关于过早优化

性能优化的工作做多了以后,往往会陷入一个什么都想着去优化的状态,这样就可能陷入过早优化的深坑中。
这里引用一下其他人的观点
https://www.zhihu.com/questio...
e603afaf28c751376c79b3c11e8950c0.png

二、内存泄露排查

Node 是基于 V8 这个 js 引擎的,这里我们了解下 V8 里的内存相关的知识。

V8 的 GC 垃圾回收机制

V8 的内存分代

在 V8 中,主要将内存分为新生代和老生代两代。新生代的对象为存活时间比较短的对象,老生代中的对象为存活时间较长的或常驻内存的对象。

默认情况下,新生代的内存最大值在 64 位系统和 32 位系统上分别为 32 MB 和 16 MB。V8 对内存的最大值在 64 位系统和 32 位系统上分别为 1464 MB 和 732 MB。

为什么这样分两代呢?是为了最优的 GC 算法。新生代的 GC 算法 Scavenge 速度快,但是不合适大数据量;老生代针使用 Mark-Sweep(标记清除) & Mark-Compact(标记整理) 算法,合适大数据量,但是速度较慢。分别对新旧两代使用更适合他们的算法来优化 GC 速度。

详情参见《深入浅出 nodejs》5.1 V8 的垃圾回收机制与内存限制

V8 的 GC log

在启动程序的时候添加 --trace_gc 参数,V8 在进行垃圾回收的时候,会将垃圾回收的信息打印出来:

➜  $ node --trace_gc aa.js
...
[94036]       68 ms: Scavenge 8.4 (42.5) -> 8.2 (43.5) MB, 2.4 ms [allocation failure].
[94036]       74 ms: Scavenge 8.9 (43.5) -> 8.9 (46.5) MB, 5.1 ms [allocation failure].
[94036] Increasing marking speed to 3 due to high promotion rate
[94036]       85 ms: Scavenge 16.1 (46.5) -> 15.7 (47.5) MB, 3.8 ms (+ 5.0 ms in 106 steps since last GC) [allocation failure].
[94036]       95 ms: Scavenge 16.7 (47.5) -> 16.6 (54.5) MB, 7.2 ms (+ 1.3 ms in 14 steps since last GC) [allocation failure].
[94036]      111 ms: Mark-sweep 23.6 (54.5) -> 23.2 (54.5) MB, 6.2 ms (+ 15.3 ms in 222 steps since start of marking, biggest step 0.3 ms) [GC interrupt] [GC in old space requested].
...

V8 提供了很多程序启动选项:

启动项含义
–max-stack-size设置栈大小
–v8-options打印 V8 相关命令
–trace-bailout查找不能被优化的函数,重写
–trace-deopt查找不能优化的函数

使用 memwatch 模块来检测内存泄露

npm模块 memwatch 是一个非常好的内存泄漏检查工具,让我们先将这个模块安装到我们的app中去,执行以下命令:

npm install --save memwatch

然后,在我们的代码中,添加:

var memwatch = require('memwatch');

然后监听 leak 事件

memwatch.on('leak', function(info) {
 console.error('Memory leak detected: ', info);
});

这样当我们执行我们的测试代码,我们会看到下面的信息:

{
 start: Fri Jan 02 2015 10:38:49 GMT+0000 (GMT),
 end: Fri Jan 02 2015 10:38:50 GMT+0000 (GMT),
 growth: 7620560,
 reason: 'heap growth over 5 consecutive GCs (1s) - -2147483648 bytes/hr'
}
mem

memwatch 发现了内存泄漏!memwatch 判定内存泄漏事件发生的规则如下:

当你的堆内存在5个连续的垃圾回收周期内保持持续增长,那么一个内存泄漏事件被派发

了解更加详细的内容,查看 memwatch

使用 heapdump dump 出 Node 应用内存快照

检测到了内存泄露的时候,我们需要查看当时内存的状态,heapdump 可以抓下当时内存的快照。

memwatch.on('leak', function(info) {
 console.error(info);
 var file = '/tmp/myapp-' + process.pid + '-' + Date.now() + '.heapsnapshot';
 heapdump.writeSnapshot(file, function(err){
   if (err) console.error(err);
   else console.error('Wrote snapshot: ' + file);
  });
});

运行我们的代码,磁盘上会产生一些 .heapsnapshot 的文件到/tmp目录下。

使用 Chrome 的开发者工具分析内存消耗

heapdump 提供的内存快照是可以用 Chrome 的开发者工具来查看的。把 .heapsnapshot 文件导入到 Chrome Developer Tools
576c50513fa2ffff1268ac6e5cb17478.png
0f6a3bbdbc7e9be80de0514f91d40f19.png

怎么使用内存分析工具呢?
Chrome开发者工具之JavaScript内存分析
这篇文件详细介绍了如何使用开发者工具来分析内存的使用情况。可以参考,这里就不细说了。

摘取个例子,使用对比视图。
对比视图 demo
这个例子展示了通过对比前后的内存变化来找出内存泄露的原因,看起来还是很简单方便的。

但是,理想很美好,现实很残酷。下面展示下日常开发中 dump 下的数据。
使用对比视图:
09befe50b36e6f61279a081e5599ed46.png

可以看出 array 是内存增长的主要元凶,但也只能得到这个线索,那具体是那些 array 消耗了内存呢?
点开 array 查看详细信息:

081c75666cbe8c61a9d89a9e52dc47bf.png

一大堆的匿名数组,无法准确查到具体那些 array 消耗了内存。
主要原因是后端使用了 sails 这个 web 框架,框架里的代码量比较多,干扰项太多,无法准确地判断是哪些 function 出现了问题。

内存泄露原因

通常,造成内存泄露的原因有如下几个。

  • 慎用内存当缓存,非用的话控制好缓存的大小和过期时间,防止出现永远无法释放的问题

  • 队列消费不及时,数组、回调,生产者的速度比消费者速度快,堆积了大量生产者导致无法释放作用域或变量

  • 作用域未释放,无法立即回收的内存有全局变量和闭包,尽量使用变量赋值为 null|undefined 来触发回收

这部分的详细解释请参考《深入浅出 nodejs》5.4 内存泄露。

三、优化应用 CPU 瓶颈

上面介绍了 IO 优化,内存优化,使用 Node 做后端的话还会经常碰到 CPU 瓶颈。总所周知,Node 是单线程的,所以对 CPU 密集的运算不是太胜任,所以应该避免使用 Node 来进行 CPU 密集的运算。
那么如果出现了 CPU 类的问题要怎么处理呢?

V8log:

加入 --prof 参数可以在应用结束是收集 log,执行命令之后,会在该目录下产生一个 *-v8.log 的日志文件,我们可以安装一个日志分析工具 tick

tick 工具分析 log

可以分析每个 function 的处理时间。

➜  $ sudo npm install tick -g
➜  $ node-tick-processor *-v8.log
[Top down (heavy) profile]:
  Note: callees occupying less than 0.1% are not shown.

  inclusive      self           name
  ticks   total  ticks   total
    426   36.7%      0    0.0%  Function: ~<anonymous> node.js:27:10
    426   36.7%      0    0.0%    LazyCompile: ~startup node.js:30:19
    410   35.3%      0    0.0%      LazyCompile: ~Module.runMain module.js:499:26
    409   35.2%      0    0.0%        LazyCompile: Module._load module.js:273:24
    407   35.1%      0    0.0%          LazyCompile: ~Module.load module.js:345:33
    406   35.0%      0    0.0%            LazyCompile: ~Module._extensions..js module.js:476:37
    405   34.9%      0    0.0%              LazyCompile: ~Module._compile module.js:378:37
...

前端的同学可以直接在 chrome 里收集 cpu profile 用于分析。

四、使用第三方平台

alinode,基于 Node 运行时的应用性能管理解决方案,笔者没有体验过,不预评价。

五、总结

文章主要介绍的还是后端开发中如何做性能优化的几种方式:

  • 添加索引

  • 接口缓存

  • 静态文件缓存

  • 合并查询
    这几种方法的目的其实都是为了减少 IO。看来 IO 过高是 Node 应用反应慢的主要原因。

此外,文章也介绍了如何排查处理内存泄露和 CPU 过高的问题。这两类问题是也是影响 Node 性能的一大原因。

参考:

《深入浅出 nodejs》朴灵著
MySQL索引原理及慢查询优化
MongoDB 查询优化分析
如何用redis/memcache做Mysql缓存层?
缓存穿透与缓存雪崩
http://www.barretlee.com/blog...
http://www.w3ctech.com/topic/842
https://addyosmani.com/blog/t...
http://m.oschina.net/blog/270248
http://www.cnblogs.com/consta...
http://www.open-open.com/lib/...

查看原文

赞 10 收藏 35 评论 1

认证与成就

  • 获得 147 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • react-ppt

    个人开源项目,这是用 React.js + reveal.js,打造的PPT制作框架。

注册于 2016-04-24
个人主页被 731 人浏览