Mask

Mask 查看完整档案

广州编辑广东工业大学  |  网络工程 编辑  |  填写所在公司/组织 qzcmask.github.io 编辑
编辑

Hope,life and growth are all worth to fighting.

个人动态

Mask 关注了用户 · 2019-06-27

CodeCloud @wujunze

一个程序猿,喜欢学习技术,喜欢交流技术

关注 402

Mask 收藏了文章 · 2019-04-21

第二集: 从零开始实现一套pc端vue的ui组件库(icon组件)

第二集: 从零开始实现(icon组件)

本集定位:
这套ui组件我本来是先从button做的, 但是我发现每个组件都要用到icon这个组件, 如果没有他, 很多组件没法扩展, 而且icon本身不依赖其他组件, 所以还是先把它作为本篇文章的重点吧.
icon组件
读过element-ui源码的同学都知道, 他们选择的是字体图标的方式来做icon组件的, 而我的这套ui在写法与用法上参考了他们的做法, 但组件本身是靠svg来书写的,其中的区别还是简单阐述一下把.
icon font 与 svg

  • 1.icon font做为字体无法支持多色图形,这就很尴尬了.
  • 2.icon font主要在页面用Unicode符号调用对应的图标,这种方式不管是浏览器,搜索引擎和对无障碍方面的能力都没有SVG好.
  • 3.icon font采用的是字体渲染,icon font在一倍屏幕下渲染效果并不好,在细节部分锯齿还是很明显的,SVG上面我说过它是图形所以在浏览器中使用的是图形渲染,所以SVG却没有这种问题.
  • 4.兼容性较差,支持 ie9+,及现代浏览器.
  • 5.浏览器渲染svg的性能一般,还不如png。

行动起来
上一集基本环境已经搭建好了, 这里我们采用'bem'的思想, 来构建组件的样式, 所有的样式抽离在一个文件夹里面, 做到组件本身没有样式代码,我们来先书写组件的代码.结构如下:
图片描述

index.js文件里面是导出这个组件:

import Icon from './main/icon.vue'
// 明白vue.use方法原理的同学都能明白这段代码的意义
// 当被use的时候, 进行icon组件的渲染
Icon.install = function(Vue) {
  Vue.component(Icon.name, Icon);
};

export default Icon

这样单抽出来做一个文件的好处是,更好的工程化, 保证职能的单一性.

main文件夹
为什么main文件夹里面只有一个文件还要单独抽成一个文件夹??, 原因是有的组件可能要配合自己独有的容器组件一起使用, 比如一个button的包裹容器, 他可以让内部的每个button有相同的上下左右距离, 相同的圆角等等...

icon.vue文件

  • 第一步: 使用svg, 当然要去下载svg图片了, 本篇推荐使用大家都在用的阿里巴巴矢量图标库,选择自己喜欢的图标放入购物车选项.

图片描述

  • 第二步: 放入工程,点击添加入项目, 如果没有项目要点击新建项目来完成此操作.

图片描述

  • 第三步: 复制链接到你的script标签里面引入, 在index.html里面就可以, 下面会讲遇到的问题与优化.

图片描述

// 使用方法如下:
<svg class="icon">
// 把名字写在下面的'xxx'处,就可以正常显示图标了;
    <use xlink:href="#icon-xxx"></use>
</svg>
  • 第四步: 开始正式书写组件.

    • 1:先定义一个最简单的组件模板, 他仅仅支持颜色的调整, 与icon的调整
    • 2:svg的颜色控制, 需要通过fill属性, 我经常面试遇到说自己用过svg图片, 但是没听说过fill属性的尴尬场面😅
    • 3:不传颜色的时候,svg默认是原色
<template>
  <div 
    class='cc-icon'
    }">
    <svg :style="{fill:color}">
      <use :xlink:href='`#icon-${name}`'></use>
    </svg>
  </div>
</template>
<script>
export default {
name: "ccIcon",
  props: {
    color: String,
    name: {
      type: String,
      required: true
    }
  }
};
</script>
  • 第五步: 基本的结构已经有了, 现在要考虑的就是我们的组件还需要什么功能? .
  • 1:控制图标的大小, 这个还是需要的
props: {// 接受一个size属性
    size: { // 因为用户可能传带单位的与不带单位的
      type: [Number, String],
      default: "1em"
    }
  },
computed: { // 计算属性里面对这个值进行操作, 类型如果是数字, 就我们来给他加上单位吧,也就是默认单位是'px'
    iconSize() {
      if (typeof this.size === "number") return this.size + "px";
      else return this.size;
    }
  }
<template>
  <div 
    class='cc-icon'
    height: iconSize, // 在此使用
    width: iconSize,  // 在此使用
    }">
    <svg :style="{fill:color}">
      <use :xlink:href='`#icon-${name}`'></use>
    </svg>
  </div>
</template>
  • 2:如果是加载的icon, 我们需要让他旋转一下, 这里我是这么做的, 所有的加载图标, 我的命名都是load加数字, 所以检测字符串里面是否有这个关键词就好了
<svg :style="{fill:color}"
         :class="[
          'cc-icon-'+name,
          {  
          //icon-loading这个class名, 只有在icon的name属性里面有load字段的时候加上, 这个属性名里面的属性就是旋转.'~'位运算符是很高效的书写方式, 也就是对位求反,1变0,0变1,也就是求二进制的反码, 这里不懂的话, 先看犀牛书, 简单的说就是把-1转成0了, 变成了布尔中的false;
            'icon-loading':~name.indexOf('load')
          }
         ]">
      <use :xlink:href='`#icon-${name}`'></use>
    </svg>
  • 3:实际使用的时候我发现, 不给div加上 display: 'inline-flex',属性, icon有时候对不整齐, 所以还是加上为妙,这个地方以后应该还会弄一弄, 感觉还会有一段故事.
<div 
    class='cc-icon'
    :style="{
    display: 'inline-flex',
    height: iconSize,
    width: iconSize,
    }">
  • 4: 接下来就是为了配合其他组件的 disabled 状态 把图标本身也置为灰色, 这样写很方便
<svg :style="{fill:color}"
         :class="[
          'cc-icon-'+name,
          { 
            'icon-loading':~name.indexOf('load'),
            'is-disabled':disabled // 在这里进行了定义
          }
         ]">
      <use :xlink:href='`#icon-${name}`'></use>
    </svg>
     props: {
    disabled: Boolean // 当然是布尔类型
  },

小知识点: props里面定义了默认类型是布尔,则用户可以直接在标签上写属性名, 不给属性值也是可以代表true的, 但是默认不是布尔, 不给属性值的话, 组件里面得到的就是undefined;

  • 第六步: 书写样式吧

图片描述

icon.scss文件
1:disabled状态时禁止点击
2:icon-loading 时旋转
@import './common/mixin.scss';
@import "./common/animation.scss";

@include b(icon) {
    .#{$state-prefix}disabled {
        cursor: not-allowed;
        fill: $--color-disabled;
    }
    .icon-loading {
        animation: rotating 1s infinite linear;
    }
}

mixin.scss文件里面的b方法

抽象出$namespace方便管理
@mixin b($block) {
    $B: $namespace+'-'+$block !global;
    .#{$B} {
        @content;
    }
}

animation.scss文件

@keyframes rotating {
    0% {
      transform: rotateZ(0deg);
    }
    100% {
      transform: rotateZ(360deg);
    }
  }

var.scss文件

// lulu的美工不好, 只是想做出点不同样子的东西玩玩
// 基本色
$--color-black:#000000 !default;
$--color-white:#FFFFFF !default;
// 基本色鲜艳
$--color-nomal:#409EFF !default;
$--color-success:#7CCD7C !default;
$--color-warning:#FFB90F !default;
$--color-danger: #FF0000 !default;
$--color-disabled: #bbbbbb !default;
$--color-difference: #F5F7FA !default;
// 字体
$--size-big: 16px;
$--size-nomal: 15px;
$--size-small: 14px;
// 输入框
$--color-input-placeholder:#aaa
  • 第六步: 上面说的svg文件引用的改良, 可以把svg文件下载到本地, 在别人因为这个ui组件的时候不会引用index.html文件, 而且大部分组件都引用了icon,我们可以去阿里巴巴矢量图标库进行操作

点击下载到本地
图片描述
取出js文件, 让在本地
图片描述

用index.js文件引用这个js文件就完成了!!

结束
icon组件是最小的组件, 其他组件的代码量与难道都大的多, 详细code与用法可以访问下面的地址, npm同步更新!
下一期是关于button的,我们一起交流吧

github:项目地址
个人博客:个人博客

查看原文

Mask 分享了头条 · 2019-04-18

mini-koa是一个精简版koa和koa-router的结合,是一个值得学习Node网络框架封装的项目。

赞 0 收藏 9 评论 0

Mask 发布了文章 · 2019-04-18

不到300行代码构建精简的koa和koa-router(mini-koa)

mini-koa

前言

鉴于之前使用expresskoa的经验,这两天想尝试构建出一个koa精简版,利用最少的代码实现koa和koa-router,同时也梳理一下Node.js网络框架开发的核心内容。

实现后的核心代码不超过300行,源代码配有详细的注释。

核心设计

API调用

mini-koa的API设计中,参考koa和koa-routerAPI调用方式。

Node.js的网络框架封装其实并不复杂,其核心点在于http/httpscreateServer方法上,这个方法是http请求的入口。

首先,我们先回顾一下用Node.js来启动一个简单服务。

// https://github.com/qzcmask/mini-koa/blob/master/examples/simple.js
const http = require('http')
const app = http.createServer((request, response) => {
  response.end('hello Node.js')
})
app.listen(3333, () => {
  console.log('App is listening at port 3333...')
})

路由原理

既然我们知道Node.js的请求入口在createServer方法上,那么我们可以在这个方法中找出请求的地址,然后根据地址映射出监听函数(通过get/post等方法添加的路由函数)即可。

其中,路由列表的格式设计如下:

// binding的格式
{
'/': [fn1, fn2, ...],
'/user': [fn, ...],
...
}
// fn/fn1/fn2的格式
{
  method: 'get/post/use/all',
  fn: '路由处理函数'
}

难点分析

next()方法设计

我们知道在koa中是可以添加多个url监听函数的,其中决定是否传递到下一个监听函数的关键在于是否调用了next()函数。如果调用了next()函数则先把路由权转移到下一个监听函数中,处理完毕再返回当前路由函数。

mini-koa中,我把next()方法设计成了一个返回Promise fullfilled的函数(这里简单设计,不考虑next()传参的情况),用户如果调用了该函数,那么就可以根据它的值来决定是否转移路由函数处理权。

判断是否转移路由函数处理权的代码如下:

let isNext = false
const next = () => {
  isNext = true
  return Promise.resolve()
}
await router.fn(ctx, next)
if (isNext) {
  continue
} else {
  // 没有调用next,直接中止请求处理函数
  return
}

use()方法设计

mini-koa提供use方法,可供扩展日志记录/session/cookie处理等功能。

use方法执行的原理是根据请求地址在执行特定路由函数之前先执行mini-koa调用use监听的函数

所以这里的关键点在于怎么找出use监听的函数列表,假设现有监听情况如下:

app.use('/', fn1)
app.use('/user', fn2)

如果访问的url/user/add,那么fn1和fn2都必须要依次执行。

我采取的做法是先根据/字符来分割请求url,然后循环拼接,查看路由绑定列表(binding)中有没有要use的函数,如果发现有,添加进要use的函数列表中,没有则继续下一次循环。

详细代码如下:

// 默认use函数前缀
let prefix = '/'
// 要预先调用的use函数列表
let useFnList = []

// 分割url,使用use函数
// 比如item为/user/a/b映射成[('user', 'a', 'b')]
const filterUrl = url.split('/').filter(item => item !== '')
// 该reduce的作用是找出本请求要use的函数列表
filterUrl.reduce((cal, item) => {
  prefix = cal
  if (this.binding[prefix] && this.binding[prefix].length) {
    const filters = this.binding[prefix].filter(router => {
      return router.method === 'use'
    })
    useFnList.push(...filters)
  }
  return (
    '/' +
    [cal, item]
      .join('/')
      .split('/')
      .filter(item => item !== '')
      .join('/')
  )
}, prefix)

ctx.body响应

通过ctx.body = '响应内容'的方式可以响应http请求。它的实现原理是利用了ES6Object.defineProperty函数,通过设置它的setter/getter函数来达到数据追踪的目的。

详细代码如下:

// 追踪ctx.body赋值
Object.defineProperty(ctx, 'body', {
  set(val) {
    // set()里面的this是ctx
    response.end(val)
  },
  get() {
    throw new Error(`ctx.body can't read, only support assign value.`)
  }
})

子路由mini-koa-router设计

子路由mini-koa-router设计这个比较简单,每个子路由维护一个路由监听列表,然后通过调用mini-koaaddRoutes函数添加到主路由列表上。

mini-koaaddRoutes实现如下:

addRoutes(router) {
  if (!this.binding[router.prefix]) {
    this.binding[router.prefix] = []
  }
  // 路由拷贝
  Object.keys(router.binding).forEach(url => {
    if (!this.binding[url]) {
      this.binding[url] = []
    }
    this.binding[url].push(...router.binding[url])
  })
}

用法

使用示例如下,源代码可以在github上找到:

// examples/server.js
// const { Koa, KoaRouter } = require('mini-koa')
const { Koa, KoaRouter } = require('../index')
const app = new Koa()
// 路由用法
const userRouter = new KoaRouter({
  prefix: '/user'
})

// 中间件函数
app.use(async (ctx, next) => {
  console.log(`请求url, 请求method: `, ctx.req.url, ctx.req.method)
  await next()
})

// 方法示例
app.get('/get', async ctx => {
  ctx.body = 'hello ,app get'
})

app.post('/post', async ctx => {
  ctx.body = 'hello ,app post'
})

app.all('/all', async ctx => {
  ctx.body = 'hello ,/all 支持所有方法'
})

// 子路由使用示例
userRouter.post('/login', async ctx => {
  ctx.body = 'user login success'
})

userRouter.get('/logout', async ctx => {
  ctx.body = 'user logout success'
})

userRouter.get('/:id', async ctx => {
  ctx.body = '用户id: ' + ctx.params.id
})

// 添加路由
app.addRoutes(userRouter)

// 监听端口
app.listen(3000, () => {
  console.log('> App is listening at port 3000...')
})

总结

挺久没有造轮子了,这次突发奇想造了个精简版的koa,虽然跟常用的koa框架有很大差别,但是也实现了最基本的API调用和原理。

造轮子是一件难能可贵的事,程序员在学习过程中不应该崇尚拿来主义,学习到一定程度后,要秉持能造就造的态度,去尝试理解和挖掘轮子背后的原理和思想。

当然,通常来说,自己造的轮子本身不具备多大的实用性,没有经历过社区大量的测试和实际应用场景的打磨,但是能加深自己的理解和提高自己的能力也是一件值得坚持的事。

人生是一段不断攀登的高峰,只有坚持向前,才能看到新奇的东西。

最后附上项目的Github地址,欢迎Star或Fork支持,谢谢。
查看原文

赞 12 收藏 9 评论 0

Mask 评论了文章 · 2019-04-14

ES6关于Promise的用法

Node的产生,大大推动了Javascript这门语言在服务端的发展,使得前端人员可以以很低的门槛转向后端开发。

当然,这并不代表迸发成了全栈。全栈的技能很集中,绝不仅仅是前端会写一些HTML和一些交互,后台熟悉数据库的增删查改

想必接触过Node的人都知道,Node是以异步(Async)回调著称的,其异步性提高了程序的执行效率,但同时也减少了程序的可读性。如果我们有几个异步操作,并且后一个操作需要前一个操作返回的数据才能执行,这样按照Node的一般执行规律,要实现有序的异步操作,通常是一层加一层嵌套下去。

为了解决这个问题,ES6提出了Promise的实现。

含义

Promise 对象用于一个异步操作的最终完成(或失败)及其结果值的表示。简单点说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。

它的一般表示形式为:

new Promise(
    /* executor */
    function(resolve, reject) {
        if (/* success */) {
            // ...执行代码
            resolve();
        } else { /* fail */
            // ...执行代码
            reject();
        }
    }
);

其中,Promise中的参数executor是一个执行器函数,它有两个参数resolvereject。它内部通常有一些异步操作,如果异步操作成功,则可以调用resolve()来将该实例的状态置为fulfilled,即已完成的,如果一旦失败,可以调用reject()来将该实例的状态置为rejected,即失败的。

我们可以把Promise对象看成是一条工厂的流水线,对于流水线来说,从它的工作职能上看,它只有三种状态,一个是初始状态(刚开机的时候),一个是加工产品成功,一个是加工产品失败(出现了某些故障)。同样对于Promise对象来说,它也有三种状态:

  1. pending
    初始状态,也称为未定状态,就是初始化Promise时,调用executor执行器函数后的状态。
  2. fulfilled
    完成状态,意味着异步操作成功。
  3. rejected
    失败状态,意味着异步操作失败。

它只有两种状态可以转化,即

  • 操作成功
    pending -> fulfilled
  • 操作失败
    pending -> rejected

并且这个状态转化是单向的,不可逆转,已经确定的状态(fulfilled/rejected)无法转回初始状态(pending)。

方法

Promise.prototype.then()

Promise对象含有then方法,then()调用后返回一个Promise对象,意味着实例化后的Promise对象可以进行链式调用,而且这个then()方法可以接收两个函数,一个是处理成功后的函数,一个是处理错误结果的函数。

如下:

var promise1 = new Promise(function(resolve, reject) {
  // 2秒后置为接收状态
  setTimeout(function() {
    resolve('success');
  }, 2000);
});

promise1.then(function(data) {
  console.log(data); // success
}, function(err) {
  console.log(err); // 不执行
}).then(function(data) {
  // 上一步的then()方法没有返回值
  console.log('链式调用:' + data); // 链式调用:undefined 
}).then(function(data) {
  // ....
});

在这里我们主要关注promise1.then()方法调用后返回的Promise对象的状态,是pending还是fulfilled,或者是rejected?

返回的这个Promise对象的状态主要是根据promise1.then()方法返回的值,大致分为以下几种情况:

  1. 如果then()方法中返回了一个参数值,那么返回的Promise将会变成接收状态。
  2. 如果then()方法中抛出了一个异常,那么返回的Promise将会变成拒绝状态。
  3. 如果then()方法调用resolve()方法,那么返回的Promise将会变成接收状态。
  4. 如果then()方法调用reject()方法,那么返回的Promise将会变成拒绝状态。
  5. 如果then()方法返回了一个未知状态(pending)的Promise新实例,那么返回的新Promise就是未知状态。
  6. 如果then()方法没有明确指定的resolve(data)/reject(data)/return data时,那么返回的新Promise就是接收状态,可以一层一层地往下传递。

转换实例如下:

var promise2 = new Promise(function(resolve, reject) {
  // 2秒后置为接收状态
  setTimeout(function() {
    resolve('success');
  }, 2000);
});

promise2
  .then(function(data) {
    // 上一个then()调用了resolve,置为fulfilled态
    console.log('第一个then');
    console.log(data);
    return '2';
  })
  .then(function(data) {
    // 此时这里的状态也是fulfilled, 因为上一步返回了2
    console.log('第二个then');
    console.log(data);  // 2

    return new Promise(function(resolve, reject) {
      reject('把状态置为rejected error'); // 返回一个rejected的Promise实例
    });
  }, function(err) {
    // error
  })
  .then(function(data) {
    /* 这里不运行 */
    console.log('第三个then');
    console.log(data);
    // ....
  }, function(err) {
    // error回调
    // 此时这里的状态也是fulfilled, 因为上一步使用了reject()来返回值
    console.log('出错:' + err); // 出错:把状态置为rejected error
  })
  .then(function(data) {
    // 没有明确指定返回值,默认返回fulfilled
    console.log('这里是fulfilled态');
});

Promise.prototype.catch()

catch()方法和then()方法一样,都会返回一个新的Promise对象,它主要用于捕获异步操作时出现的异常。因此,我们通常省略then()方法的第二个参数,把错误处理控制权转交给其后面的catch()函数,如下:

var promise3 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    reject('reject');
  }, 2000);
});

promise3.then(function(data) {
  console.log('这里是fulfilled状态'); // 这里不会触发
  // ...
}).catch(function(err) {
  // 最后的catch()方法可以捕获在这一条Promise链上的异常
  console.log('出错:' + err); // 出错:reject
});

Promise.all()

Promise.all()接收一个参数,它必须是可以迭代的,比如数组

它通常用来处理一些并发的异步操作,即它们的结果互不干扰,但是又需要异步执行。它最终只有两种状态:成功或者失败

它的状态受参数内各个值的状态影响,即里面状态全部为fulfilled时,它才会变成fulfilled,否则变成rejected

成功调用后返回一个数组,数组的值是有序的,即按照传入参数的数组的值操作后返回的结果。如下:

// 置为fulfilled状态的情况
var arr = [1, 2, 3];
var promises = arr.map(function(e) {
  return new Promise(function(resolve, reject) {
    resolve(e * 5);
  });
});

Promise.all(promises).then(function(data) {
    // 有序输出
  console.log(data); // [5, 10, 15]
  console.log(arr); // [1, 2, 3]
});
// 置为rejected状态的情况
var arr = [1, 2, 3];
var promises2 = arr.map(function(e) {
  return new Promise(function(resolve, reject) {
    if (e === 3) {
      reject('rejected');
    }
    resolve(e * 5);
  });
});

Promise.all(promises2).then(function(data) {
  // 这里不会执行
  console.log(data);
  console.log(arr);
}).catch(function(err) {
  console.log(err); // rejected
});

Promise.race()

Promise.race()和Promise.all()类似,都接收一个可以迭代的参数,但是不同之处是Promise.race()的状态变化不是全部受参数内的状态影响,一旦参数内有一个值的状态发生的改变,那么该Promise的状态就是改变的状态。就跟race单词的字面意思一样,谁跑的快谁赢。如下:

var p1 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 300, 'p1 doned');
});

var p2 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 50, 'p2 doned');
});

var p3 = new Promise(function(resolve, reject) {
  setTimeout(reject, 100, 'p3 rejected');
});

Promise.race([p1, p2, p3]).then(function(data) {
  // 显然p2更快,所以状态变成了fulfilled
  // 如果p3更快,那么状态就会变成rejected
  console.log(data); // p2 doned
}).catch(function(err) {
  console.log(err); // 不执行
});

Promise.resolve()

Promise.resolve()接受一个参数值,可以是普通的值具有then()方法的对象Promise实例。正常情况下,它返回一个Promise对象,状态为fulfilled。但是,当解析时发生错误时,返回的Promise对象将会置为rejected态。如下:

// 参数为普通值
var p4 = Promise.resolve(5);
p4.then(function(data) {
  console.log(data); // 5
});


// 参数为含有then()方法的对象
var obj = {
  then: function() {
    console.log('obj 里面的then()方法');
  }
};

var p5 = Promise.resolve(obj);
p5.then(function(data) {
  // 这里的值时obj方法里面返回的值
  console.log(data); // obj 里面的then()方法
});


// 参数为Promise实例
var p6 = Promise.resolve(7);
var p7 = Promise.resolve(p6);

p7.then(function(data) {
  // 这里的值时Promise实例返回的值
  console.log(data); // 7
});

// 参数为Promise实例,但参数是rejected态
var p8 = Promise.reject(8);
var p9 = Promise.resolve(p8);

p9.then(function(data) {
  // 这里的值时Promise实例返回的值
  console.log('fulfilled:'+ data); // 不执行
}).catch(function(err) {
  console.log('rejected:' + err); // rejected: 8
});

Promise.reject()

Promise.reject()和Promise.resolve()正好相反,它接收一个参数值reason,即发生异常的原因。此时返回的Promise对象将会置为rejected态。如下:

var p10 = Promise.reject('手动拒绝');
p10.then(function(data) {
  console.log(data); // 这里不会执行,因为是rejected态
}).catch(function(err) {
  console.log(err); // 手动拒绝
}).then(function(data) {
 // 不受上一级影响
  console.log('状态:fulfilled'); // 状态:fulfilled
});

总之,除非Promise.then()方法内部抛出异常或者是明确置为rejected态,否则它返回的Promise的状态都是fulfilled态,即完成态,并且它的状态不受它的上一级的状态的影响。

总结

大概常用的方法就写那么多,剩下的看自己实际需要再去了解。

解决Node回调地狱的不止有Promise,还有Generator和ES7提出的Async实现。

方法不在多,而在于精

查看原文

Mask 收藏了文章 · 2019-01-08

原生 js 实现移动端 Touch 轮播图

image

Touch 轮播图

touch轮播图其实就是通过手指的滑动,来左右切换轮播图,下面我们通过一个案例,来实现下。

1. html 结构

结构上,还是用ulli来存放轮播图片,olli来存放轮播小圆点:

image

2. 样式初始化

html的一些标签,都会有一些默认样式,比如body标签默认是有一个边距的,为了不影响美观,我们需要清除掉。
/* 清除标签默认边距 */
body,ul,li,ol,img {
    margin: 0;
    padding: 0;
}

/* 清除 ul 等标签前面的“小圆点” */
ul,li,ol {
    list-style-type: none;
}

/* 图片自适应 */
img {
    width: 100%;
    height: auto;
    border: none;
    /* ie8 */
    display: block;
    -ms-interpolation-mode: bicubic; /*为了照顾ie图片缩放失真*/
}

image

3. 添加样式

在前面讲特效的时候,我们说过如何使用原生js实现移一个轮播图的概念,但是当时的方式是通过li浮动,这里给大家介绍一种新的方——定位。

思路:

  • ul外层的盒子一个相对定位;
  • 这里的ul高度不能写死,它应该是li撑开的高度,但是由于li绝对定位,没办法撑开这个高度,所以这里的ul需要在js里面动态设置高度;
  • li设置相对定位,并且lefttop都为0,再给li添加一个transform:translateX(300%)属性,目的是初始化显示的图片为空,然后在js里只需要动态设置每个litranslateX值,即可实现轮播;
  • 设置小圆点区域,因为小圆点个数未知,所以ol的宽度也未知,想要让一个未知宽度的盒子水平居中,可以使用absolute定位结合left百分比的方式实现;
  • ol下面的li设置一个宽高添加圆角边框属性,并且左浮动,这样就能显示一排空心的小圆点了;
  • 最后,添加一个样式类,里面设置一个背景属性,用来显示当前展示图片对应的小圆点。
/* 轮播图最外层盒子 */
.carousel {
    position: relative;
    overflow: hidden;
}

.carousel ul {
    /* 这个高度需要在JS里面动态添加 */
}

.carousel ul li {
    position: absolute;
    width: 100%;
    left: 0;
    top: 0;
    /* 使用 transform:translaX(300%) 暂时将 li 移动到屏幕外面去*/
    -webkit-transform: translateX(300%);
    transform: translateX(300%);
}

/* 小圆点盒子 */
.carousel .points {
    /* 未知宽度的盒子,使用 absolute 定位,结合 transform 的方式进行居中 */
    position: absolute;
    left: 50%;
    bottom: 10px;
    transform: translateX(-50%);
}

/* 小圆点 */
.carousel .points li {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    border: 1px solid #fff;
    float: left;
    margin: 0 2px;
}

/* 选中小圆点的样式类 */
.carousel .points li.active {
    background-color: #fff;
}

image

4. js 准备工作

先不考虑别的,js在初始化的时候,首先要做的就是给ul添加上一个高度,不然图片是不显示的。
  • UL动态设置高度
  • 动态生成小圆点 (根据图片的张数创建小圆点个数,i=0 添加active
  • 初始化三个li的基本位置

    • 定义三个变量,分别用来存储三个li的下(left存储最后一张图片的下标,centerright分别存储第一张和第二张的下标)
    • 通过数组[下标]的方式给三个li设置定位后left方向的位置
var carousel = document.querySelector('.carousel');
var carouselUl = carousel.querySelector('ul');
var carouselLis = carouselUl.querySelectorAll('li');
var points = carousel.querySelector('ol');
// 屏幕的宽度(轮播图显示区域的宽度)
var screenWidth = document.documentElement.offsetWidth;

// 1- ul设置高度
carouselUl.style.height = carouselLis[0].offsetHeight + 'px';

// 2- 生成小圆点
for(var i = 0; i < carouselLis.length; i++){
    var li = document.createElement('li');
    if(i == 0){
        li.classList.add('active');
    }//
    points.appendChild(li);
}

// 3- 初始三个 li 固定的位置
var left = carouselLis.length - 1;
var center = 0;
var right = 1;

// 归位
carouselLis[left].style.transform = 'translateX('+ (-screenWidth) +'px)';
carouselLis[center].style.transform = 'translateX(0px)';
carouselLis[right].style.transform = 'translateX('+ screenWidth +'px)';

image

效果图:

image

5. 添加定时器,让图片动起来

轮播图都会自己轮播,所以需要用到定时器,每隔一段时间执行一次轮转函数。
  • 添加定时器,定时器里面轮转下标
  • 极值判断
  • 设置过渡(替补的那张不需要过渡)
  • 归位
  • 小圆点焦点联动
var timer = null;
// 调用定时器
timer = setInterval(showNext, 2000);

// 轮播图片切换
function showNext(){
    // 轮转下标
    left = center;
    center = right;
    right++;
    // 极值判断
    if(right > carouselLis.length - 1){
        right = 0;
    }

    //添加过渡
    carouselLis[left].style.transition = 'transform 1s';
    carouselLis[center].style.transition = 'transform 1s';
    // 右边的图片永远是替补的,不能添加过渡
    carouselLis[right].style.transition = 'none';
    // 归位
    carouselLis[left].style.transform = 'translateX('+ (-screenWidth) +'px)';
    carouselLis[center].style.transform = 'translateX(0px)';
    carouselLis[right].style.transform = 'translateX('+ screenWidth +'px)';
    // 自动设置小圆点
    setPoint();
}

// 动态设置小圆点的active类
var pointsLis = points.querySelectorAll('li');
function setPoint(){
    for(var i = 0; i < pointsLis.length; i++){
        pointsLis[i].classList.remove('active');
    }
    pointsLis[center].classList.add('active');
}

image

效果图:

image

6. touch 滑动

移动端的轮播图,配合touch滑动事件,效果更加友好。
  • 分别绑定三个touch事件

    • touchstart里面记录手指的位置,清除定时器,记录时间
    • touchmove里面获取差值,同时清除过渡,累加上差值的值
    • touchend里面判断是否滑动成功,滑动的依据是滑动的距离(绝对值)
  • 超过屏幕的三分之一或者滑动的时间小于300毫秒同时距离大于30(防止点击就跑)的时候都认为是滑动成功
  • 在滑动成功的条件分支里面在判断滑动的方向,根据方向选择调用上一张还是下一张的逻辑
  • 在滑动失败的条件分支里面添加上过渡,重新进行归位
  • 重启定时器
var carousel = document.querySelector('.carousel');
var carouselUl = carousel.querySelector('ul');
var carouselLis = carouselUl.querySelectorAll('li');
var points = carousel.querySelector('ol');
// 屏幕的宽度
var screenWidth = document.documentElement.offsetWidth;
var timer = null;

// 设置 ul 的高度
carouselUl.style.height = carouselLis[0].offsetHeight + 'px';

// 动态生成小圆点
for (var i = 0; i < carouselLis.length; i++) {
    var li = document.createElement('li');
    if (i == 0) {
        li.classList.add('active');
    }
    points.appendChild(li);
}

// 初始三个固定的位置
var left = carouselLis.length - 1;
var center = 0;
var right = 1;

// 归位(多次使用,封装成函数)
setTransform();

// 调用定时器
timer = setInterval(showNext, 2000);

// 分别绑定touch事件
var startX = 0;  // 手指落点
var startTime = null; // 开始触摸时间
carouselUl.addEventListener('touchstart', touchstartHandler); // 滑动开始绑定的函数 touchstartHandler
carouselUl.addEventListener('touchmove', touchmoveHandler);   // 持续滑动绑定的函数 touchmoveHandler
carouselUl.addEventListener('touchend', touchendHandeler);    // 滑动结束绑定的函数 touchendHandeler

// 轮播图片切换下一张
function showNext() {
    // 轮转下标
    left = center;
    center = right;
    right++;
    // 极值判断
    if (right > carouselLis.length - 1) {
        right = 0;
    }
    //添加过渡(多次使用,封装成函数)
    setTransition(1, 1, 0);
    // 归位
    setTransform();
    // 自动设置小圆点
    setPoint();
}

// 轮播图片切换上一张
function showPrev() {
    // 轮转下标
    right = center;
    center = left;
    left--;
    // 极值判断
    if (left < 0) {
        left = carouselLis.length - 1;
    }
    //添加过渡
    setTransition(0, 1, 1);
    // 归位
    setTransform();
    // 自动设置小圆点
    setPoint();
}

// 滑动开始
function touchstartHandler(e) {
    // 清除定时器
    clearInterval(timer);
    // 记录滑动开始的时间
    startTime = Date.now();
    // 记录手指最开始的落点
    startX = e.changedTouches[0].clientX;
}
// 滑动持续中
function touchmoveHandler(e) {
    // 获取差值 自带正负
    var dx = e.changedTouches[0].clientX - startX;
    // 干掉过渡
    setTransition(0, 0, 0);
    // 归位
    setTransform(dx);
}
// 滑动结束
function touchendHandeler(e) {
    // 在手指松开的时候,要判断当前是否滑动成功
    var dx = e.changedTouches[0].clientX - startX;
    // 获取时间差
    var dTime = Date.now() - startTime;
    // 滑动成功的依据是滑动的距离(绝对值)超过屏幕的三分之一 或者滑动的时间小于300毫秒同时滑动的距离大于30
    if (Math.abs(dx) > screenWidth / 3 || (dTime < 300 && Math.abs(dx) > 30)) {
        // 滑动成功了
        // 判断用户是往哪个方向滑
        if (dx > 0) {
            // 往右滑 看到上一张
            showPrev();
        } else {
            // 往左滑 看到下一张
            showNext();
        }
    } else {
        // 添加上过渡
        setTransition(1, 1, 1);
        // 滑动失败了
        setTransform();
    }

    // 重新启动定时器
    clearInterval(timer);
    // 调用定时器
    timer = setInterval(showNext, 2000);
}
// 设置过渡
function setTransition(a, b, c) {
    if (a) {
        carouselLis[left].style.transition = 'transform 1s';
    } else {
        carouselLis[left].style.transition = 'none';
    }
    if (b) {
        carouselLis[center].style.transition = 'transform 1s';
    } else {
        carouselLis[center].style.transition = 'none';
    }
    if (c) {
        carouselLis[right].style.transition = 'transform 1s';
    } else {
        carouselLis[right].style.transition = 'none';
    }
}

// 封装归位
function setTransform(dx) {
    dx = dx || 0;
    carouselLis[left].style.transform = 'translateX(' + (-screenWidth + dx) + 'px)';
    carouselLis[center].style.transform = 'translateX(' + dx + 'px)';
    carouselLis[right].style.transform = 'translateX(' + (screenWidth + dx) + 'px)';
}
// 动态设置小圆点的active类
var pointsLis = points.querySelectorAll('li');

function setPoint() {
    for (var i = 0; i < pointsLis.length; i++) {
        pointsLis[i].classList.remove('active');
    }
    pointsLis[center].classList.add('active');
}

image

7. 完整代码

一定要注意,碰到在js里面动态设定高度的时候,如果页面一加载就需要设置,那么就要用window.onload事件。

示例代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <!-- 添加视口 -->
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>原生 js 实现 Touch 轮播图</title>
  <style>
    /* 清除标签默认边距 */
    body,
    ul,
    li,
    ol,
    img {
      margin: 0;
      padding: 0;
    }

    /* 清除 ul 等标签前面的“小圆点” */
    ul,
    li,
    ol {
      list-style-type: none;
    }

    /* 图片自适应 */
    img {
      width: 100%;
      height: auto;
      border: none;
      /* ie8 */
      display: block;
      -ms-interpolation-mode: bicubic;
      /*为了照顾ie图片缩放失真*/
    }

    /* 轮播图最外层盒子 */
    .carousel {
      position: relative;
      overflow: hidden;
    }

    .carousel ul {
      /* 这个高度需要在JS里面动态添加 */
    }

    .carousel ul li {
      position: absolute;
      width: 100%;
      left: 0;
      top: 0;
      /* 使用 transform:translaX(300%) 暂时将 li 移动到屏幕外面去*/
      -webkit-transform: translateX(300%);
      transform: translateX(300%);
    }

    /* 小圆点盒子 */
    .carousel .points {
      /* 未知宽度的盒子,使用 absolute 定位,结合 transform 的方式进行居中 */
      position: absolute;
      left: 50%;
      bottom: 10px;
      transform: translateX(-50%);
    }

    /* 小圆点 */
    .carousel .points li {
      width: 5px;
      height: 5px;
      border-radius: 50%;
      border: 1px solid #fff;
      float: left;
      margin: 0 2px;
    }

    /* 选中小圆点的样式类 */
    .carousel .points li.active {
      background-color: #fff;
    }
  </style>
</head>

<body>
  <section class="carousel">
    <ul>
      <li><a href="#"><img data-original="images/imgs/banner01.jpg" alt=""></a></li>
      <li><a href="#"><img data-original="images/imgs/banner02.jpg" alt=""></a></li>
      <li><a href="#"><img data-original="images/imgs/banner03.jpg" alt=""></a></li>
      <li><a href="#"><img data-original="images/imgs/banner04.jpg" alt=""></a></li>
      <li><a href="#"><img data-original="images/imgs/banner05.jpg" alt=""></a></li>
    </ul>
    <ol class="points"></ol>
  </section>
</body>


<script>
  window.onload = function () {
    var carousel = document.querySelector('.carousel');
    var carouselUl = carousel.querySelector('ul');
    var carouselLis = carouselUl.querySelectorAll('li');
    var points = carousel.querySelector('ol');
    // 屏幕的宽度
    var screenWidth = document.documentElement.offsetWidth;
    var timer = null;

    // 设置 ul 的高度
    carouselUl.style.height = carouselLis[0].offsetHeight + 'px';

    // 动态生成小圆点
    for (var i = 0; i < carouselLis.length; i++) {
      var li = document.createElement('li');
      if (i == 0) {
        li.classList.add('active');
      }
      points.appendChild(li);
    }

    // 初始三个固定的位置
    var left = carouselLis.length - 1;
    var center = 0;
    var right = 1;

    // 归位(多次使用,封装成函数)
    setTransform();

    // 调用定时器
    timer = setInterval(showNext, 2000);

    // 分别绑定touch事件
    var startX = 0; // 手指落点
    var startTime = null; // 开始触摸时间
    carouselUl.addEventListener('touchstart', touchstartHandler); // 滑动开始绑定的函数 touchstartHandler
    carouselUl.addEventListener('touchmove', touchmoveHandler); // 持续滑动绑定的函数 touchmoveHandler
    carouselUl.addEventListener('touchend', touchendHandeler); // 滑动结束绑定的函数 touchendHandeler

    // 轮播图片切换下一张
    function showNext() {
      // 轮转下标
      left = center;
      center = right;
      right++;
      // 极值判断
      if (right > carouselLis.length - 1) {
        right = 0;
      }
      //添加过渡(多次使用,封装成函数)
      setTransition(1, 1, 0);
      // 归位
      setTransform();
      // 自动设置小圆点
      setPoint();
    }

    // 轮播图片切换上一张
    function showPrev() {
      // 轮转下标
      right = center;
      center = left;
      left--;
      // 极值判断
      if (left < 0) {
        left = carouselLis.length - 1;
      }
      //添加过渡
      setTransition(0, 1, 1);
      // 归位
      setTransform();
      // 自动设置小圆点
      setPoint();
    }

    // 滑动开始
    function touchstartHandler(e) {
      // 清除定时器
      clearInterval(timer);
      // 记录滑动开始的时间
      startTime = Date.now();
      // 记录手指最开始的落点
      startX = e.changedTouches[0].clientX;
    }
    // 滑动持续中
    function touchmoveHandler(e) {
      // 获取差值 自带正负
      var dx = e.changedTouches[0].clientX - startX;
      // 干掉过渡
      setTransition(0, 0, 0);
      // 归位
      setTransform(dx);
    }
    // 滑动结束
    function touchendHandeler(e) {
      // 在手指松开的时候,要判断当前是否滑动成功
      var dx = e.changedTouches[0].clientX - startX;
      // 获取时间差
      var dTime = Date.now() - startTime;
      // 滑动成功的依据是滑动的距离(绝对值)超过屏幕的三分之一 或者滑动的时间小于300毫秒同时滑动的距离大于30
      if (Math.abs(dx) > screenWidth / 3 || (dTime < 300 && Math.abs(dx) > 30)) {
        // 滑动成功了
        // 判断用户是往哪个方向滑
        if (dx > 0) {
          // 往右滑 看到上一张
          showPrev();
        } else {
          // 往左滑 看到下一张
          showNext();
        }
      } else {
        // 添加上过渡
        setTransition(1, 1, 1);
        // 滑动失败了
        setTransform();
      }

      // 重新启动定时器
      clearInterval(timer);
      // 调用定时器
      timer = setInterval(showNext, 2000);
    }
    // 设置过渡
    function setTransition(a, b, c) {
      if (a) {
        carouselLis[left].style.transition = 'transform 1s';
      } else {
        carouselLis[left].style.transition = 'none';
      }
      if (b) {
        carouselLis[center].style.transition = 'transform 1s';
      } else {
        carouselLis[center].style.transition = 'none';
      }
      if (c) {
        carouselLis[right].style.transition = 'transform 1s';
      } else {
        carouselLis[right].style.transition = 'none';
      }
    }

    // 封装归位
    function setTransform(dx) {
      dx = dx || 0;
      carouselLis[left].style.transform = 'translateX(' + (-screenWidth + dx) + 'px)';
      carouselLis[center].style.transform = 'translateX(' + dx + 'px)';
      carouselLis[right].style.transform = 'translateX(' + (screenWidth + dx) + 'px)';
    }
    // 动态设置小圆点的active类
    var pointsLis = points.querySelectorAll('li');

    function setPoint() {
      for (var i = 0; i < pointsLis.length; i++) {
        pointsLis[i].classList.remove('active');
      }
      pointsLis[center].classList.add('active');
    }
  }
</script>

</html>

效果图:

image

查看原文

Mask 收藏了文章 · 2018-12-08

ECMAScript 2016、2017和2018中所有新特性。

阿里云最近在做活动,低至2折,有兴趣可以看看:
https://promotion.aliyun.com/...

为了保证的可读性,本文采用意译而非直译。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

跟踪JavaScript (ECMAScript)中的新内容是很困难的,而且更难找到有用的代码示例。

因此,在本文中将介绍 TC39(最终草案) 在ES2016、ES2017和ES2018中添加的已完成提案中列出的所有18个特性,并给出有用的示例。

图片描述

1.Array.prototype.includes

include 是数组上的一个简单实例方法,可以轻松查找数组中是否有指定内容(包括 NaN)。

图片描述

2.求幂操作符

像加法和减法这样的数学运算分别有像 + 和 - 这样运算符。与它们类似,** 运算符通常用于指数运算。在ECMAScript 2016中,引入了 ** 代替 Math.pow。

图片描述



图片描述

1.Object.values()

Object.values()是一个类似于Object.keys()的新函数,但返回对象自身属性的所有值,不包括原型链中的任何值。

图片描述

2.Object.entries()

Object.entries()与Object.keys 类似,但它不是仅返回键,而是以数组方式返回键和值。 这使得在循环中使用对象或将对象转换为映射等操作变得非常简单。

例一:

图片描述

例二:

图片描述

3.字符串填充

在String.prototype中添加了两个实例方法:String.prototype.padStart 和 String.prototype.padEnd, 允许在初始字符串的开头或末尾追加/前置空字符串或其他字符串。

'someString'.padStart(numberOfCharcters [,stringForPadding]); 

'5'.padStart(10) // '          5'
'5'.padStart(10, '=*') //'=*=*=*=*=5'
'5'.padEnd(10) // '5         '
'5'.padEnd(10, '=*') //'5=*=*=*=*='

当我们想要在漂亮的打印显示或终端打印进行对齐时,这非常有用。

3.1 padStart 例子:

在下面的例子中,有一个不同长度的数字列表。我们希望在“0”为追加符让所有项长度都为10位,以便显示,我们可以使用padStart(10, '0')轻松实现这一点。

图片描述

3.2 padEnd 例子:

当我们打印多个不同长度的项目并想要右对齐它们时,padEnd非常有用。

下面的示例是关于padEnd、padStart和 Object.entries 的一个很好的实际示例:

图片描述

const cars = {
  '🚙BMW': '10',
  '🚘Tesla': '5',
  '🚖Lamborghini': '0'
}

Object.entries(cars).map(([name, count]) => {
  console.log(`${name.padEnd(20, ' -')}  Count: ${count.padStart(3, '0')}`)
})
// 打印
// 🚙BMW - - - - - - -  Count: 010
// 🚘Tesla - - - - - -  Count: 005
// 🚖Lamborghini - - -  Count: 000

3.3 ⚠️ 注意padStart和padEnd 在Emojis和其他双字节字符上的使用

Emojis和其他双字节字符使用多个unicode字节表示。所以padStart padEnd可能不会像预期的那样工作!⚠️

例如:假设我们要垫达到10个字符的字符串的心❤️emoji。结果如下:

'heart'.padStart(10, "❤️"); // prints.. '❤️❤️❤heart'

这是因为 ❤️ 长2个字节(' u2764 uFE0F')! 单词 heart 是5个字符,所以我们只剩下5个字符来填充。 所以 JS 使用 ('u2764uFE0F' ) 填充两颗心并生成 ❤️❤️。 对于最后一个,它只使用 ('u2764uFE0F' ) 的第一个字节(u2764)来生成,所以是 ❤;

4.Object.getOwnPropertyDescriptors

此方法返回给定对象的所有属性的所有属性(包括getter setter set方法),添加这个的主要目的是允许浅 拷贝/克隆到另一个对象中的对象,类似 bject.assign。

Object.assign 浅拷贝除原始对象的 getter 和 setter 方法之外的所有属性。

下面的示例显示了 Object.assign 和 Object.getOwnPropertyDescriptors 以及Object.defineProperties 之间的区别,以将原始对象 Car 复制到新对象 ElectricCar 中。 可以看到使用 Object.getOwnPropertyDescriptors,discount 的 getter 和 setter 函数也被复制到目标对象中。

图片描述

使用 Object.defineProperties

图片描述

var Car = {
  name: 'BMW',
  price: 1000000,
  set discount(x) {
   this.d = x;
  },
  get discount() {
   return this.d;
  },
 };
 console.log(Object.getOwnPropertyDescriptor(Car, 'discount'));
 // 打印
 // { 
 //   get: [Function: get],
 //   set: [Function: set],
 //   enumerable: true,
 //   configurable: true
 // }
 // 使用 Object.assign 拷贝对象
 const ElectricCar = Object.assign({}, Car);
 //Print details of ElectricCar object's 'discount' property
 console.log(Object.getOwnPropertyDescriptor(ElectricCar, 'discount'));
 // 打印
 // { 
 //   value: undefined,
 //   writable: true,
 //   enumerable: true,
 //   configurable: true 
   
 // }
 // 
 //⚠️请注意,“discount” 属性的 ElectricCar 对象中缺少getter和setter!👎👎
 
 //Copy Car's properties to ElectricCar2 using Object.defineProperties 
 //and extract Car's properties using Object.getOwnPropertyDescriptors
 const ElectricCar2 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(Car));
 //Print details of ElectricCar2 object's 'discount' property
 console.log(Object.getOwnPropertyDescriptor(ElectricCar2, 'discount'));
 //prints..
 // { get: [Function: get],  👈🏼👈🏼👈🏼
 //   set: [Function: set],  👈🏼👈🏼👈🏼
 //   enumerable: true,
 //   configurable: true 
 // }
 // 请注意,在ElectricCar2对象中存在“discount”属性的getter和setter !


5.函数参数的尾逗号

ES2017允许函数的最后一个参数有尾逗号(trailing comma), 此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。这一变化将鼓励开发人员停止丑陋的“行以逗号开头”的习惯。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。

图片描述

6.Async/Await

到目前为止,个人感受是这是最重要和最有用的功能。 async 函数允许我们不处理回调地狱,并使整个代码看起来很简单。

async 关键字告诉 JavaScript 编译器以不同的方式对待函数。每当编译器到达函数中的 await 关键字时,它就会暂停。它假定 wait 之后的表达式返回一个 promise ,并在进一步移动之前等待该 promise 被 resolved 或 rejected。

在下面的示例中,getAmount 函数调用两个异步函数getUser和getBankBalance。使用 async await更加优雅和简单达到有有序的调用 getUser 与 getBankBalance。

图片描述

6.1.async 函数默认返回一个 promise

如果您正在等待 async 函数的结果,则需要使用 Promise 的 then 语法来捕获其结果。

在以下示例中,我们希望使用 console.log 来打印结果但是不在 doubleAndAdd 函数里面操作。 因为 async 返回是一个 promise 对象,所以可以在 then 里面执行我们一些打印操作。

图片描述

6.2 并行调用 async/await

在前面的例子中,我们调用doubleAfterlSec ,但每次我们等待一秒钟(总共2秒)。 相反,我们可以使用 Promise.all 将它并行化为一个并且互不依赖于。

图片描述

6.3 async/await 函数对错误的处理

在使用async/wait时,有多种方法可以处理错误。

方法一:在函数内使用 try catch

图片描述

async function doubleAndAdd(a, b) {
  try {
   a = await doubleAfter1Sec(a);
   b = await doubleAfter1Sec(b);
  } catch (e) {
   return NaN; //return something
  }
 return a + b;
 }

 doubleAndAdd('one', 2).then(console.log); // NaN
 doubleAndAdd(1, 2).then(console.log); // 6
 function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
   setTimeout(function() {
    let val = param * 2;
    isNaN(val) ? reject(NaN) : resolve(val);
   }, 1000);
  });
 }

方法二:在 await 后使用 catch 捕获错误

图片描述

// 方法二:在 await 后使用 catch 获取错误
async function doubleAndAdd(a, b) {
  a = await doubleAfter1Sec(a).catch(e => console.log('"a" is NaN')); // 👈
  b = await doubleAfter1Sec(b).catch(e => console.log('"b" is NaN')); // 👈
  if (!a || !b) {
   return NaN;
  }
  return a + b;
 }
 
 doubleAndAdd('one', 2).then(console.log); // NaN  and logs:  "a" is NaN
 doubleAndAdd(1, 2).then(console.log); // 6
 function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
   setTimeout(function() {
    let val = param * 2;
    isNaN(val) ? reject(NaN) : resolve(val);
   }, 1000);
  });
 }

方法三:在整个的 async-await 函数捕获错误

图片描述

//方法三:在整个的 async-await 函数捕获错误
async function doubleAndAdd(a, b) {
  a = await doubleAfter1Sec(a);
  b = await doubleAfter1Sec(b);
  return a + b;
 }
 
 doubleAndAdd('one', 2)
 .then(console.log)
 .catch(console.log); // 👈👈🏼<------- use "catch"
 function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
   setTimeout(function() {
    let val = param * 2;
    isNaN(val) ? reject(NaN) : resolve(val);
   }, 1000);
  });
 }


图片描述

7.共享内存 和 Atomics

这是一个巨大的、相当高级的特性,是JS引擎的核心增强。

其主要原理是在 JavaScript 中引入某种多线程特性,以便JS开发人员将来可以通过允许自己管理内存而不是让 JS 引擎管理内存来编写高性能的并发程序。

这是通过一种名为 SharedArrayBuffer (即 共享数组缓冲区) 的新类型的全局对象实现的,该对象本质上是将数据存储在共享内存空间中。因此,这些数据可以在主JS线程和web工作线程之间共享。

到目前为止,如果我们想在主 JS 线程和 web 工作者之间共享数据,我们必须复制数据并使用postMessage 将其发送到另一个线程。

你只需使用SharedArrayBuffer,数据就可以立即被主线程和多个web工作线程访问。workers 之间的协调变得更简单和更快(与 postMessage() 相比)。

但是在线程之间共享内存会导致竞争条件。为了帮助避免竞争条件,引入了 “Atomics” 全局对象。 Atomics 提供了各种方法来在线程使用其数据时锁定共享内存。 它还提供了安全地更新该共享内存中的搜索数据的方法。

如果你这对个感兴趣,可以阅读以下文章:

8. Tagged Template literal restriction removed

首先,我们需要知道的什么是 Template literals(“标记的模板文字”),以便更好地理解这个特性。Template literals是一个ES2015特性,它使用反引号包含一个字符串字面量,并且支持嵌入表达式和换行,如:

图片描述

下面的例子显示,我们的自定义“Tag” 函数 greet 添加了一天中的时间,比如“Good Morning!” “Good afternoon” 等等,取决于一天中的时间字符串的文字和返回自定义字符串。

图片描述

function greet(hardCodedPartsArray, ...replacementPartsArray) {
  console.log(hardCodedPartsArray); //[ 'Hello ', '!' ]
  console.log(replacementPartsArray); //[ 'Raja' ]
  let str = '';
  hardCodedPartsArray.forEach((string, i) => {
   if (i < replacementPartsArray.length) {
    str += `${string} ${replacementPartsArray[i] || ''}`;
   } else {
    str += `${string} ${timeGreet()}`; //<-- 追加 Good morning/afternoon/evening here
   }
  });
  return str;
 }

 const firstName = 'Raja';
 const greetings = greet`Hello ${firstName}!`; //👈🏼<-- Tagged literal
 console.log(greetings); //'Hello  Raja! Good Morning!' 🔥
 function timeGreet() {
  const hr = new Date().getHours();
  return hr < 12
   ? 'Good Morning!'
   : hr < 18 ? 'Good Afternoon!' : 'Good Evening!';
 }

现在我们讨论了什么是“标记”函数,许多人希望在不同的领域中使用这个特性,比如在Terminal中用于命令,在组成 uri 的 HTTP 请求中,等等。

⚠️ 带标记字符串文字的问题

问题是ES2015和ES2016规范不允许使用像“u”(unicode)、“x”(十六进制)这样的转义字符,除非它们看起来完全像“ u00A9”或u{2F804}或xA9。

因此,如果你有一个内部使用其他域规则(如终端规则)的标记函数,可能需要使用看起来不像 u0049或 u {@ F804}的 ubla123abla,那么你会得到一个语法错误,

function myTagFunc(str) { 
 return { "cooked": "undefined", "raw": str.raw[0] }
} 

var str = myTagFunc `hi \ubla123abla`; //call myTagFunc

str // { cooked: "undefined", raw: "hi \\unicode" }

9.用于正则表达式的“dotall”标志

目前在正则表达式中,虽然点(“.”)应该匹配单个字符,但它不匹配像 n r f 等新行字符。

例如:

//Before
/first.second/.test('first\nsecond'); //false

这种增强使 点 运算符能够匹配任何单个字符。为了确保它不会破坏任何东西,我们需要在创建正则表达式时使用s标志。

//ECMAScript 2018
/first.second/s.test('first\nsecond'); //true   Notice: /s 👈🏼

更多的方法,请看这里

图片描述

10.RegExp Named Group Captures

这种增强 RegExp特性借鉴于像Python、Java等其他语言,因此称为“命名组”。这个特性允许编写开发人员以(<name>…)格式为 RegExp 中的组的不同部分提供名称(标识符),使用可以用这个名称轻松地获取他们需要的任何组。

10.1 Named group 的基础用法

在下面的示例中,我们使用 (?<year>) (?<month>) 和 (?<day>) 名称对日期正则表达式的不同部分进行分组。结果对象现在将包含一个groups属性,该属性具有 year、month和 day 的相应值。

图片描述

let re1 = /(\d{4})-(\d{2})-(\d{2})/;
let result1 = re1.exec('2015-01-02');
console.log(result1);
// [ '2015-01-02', '2015', '01', '02', index: 0, input: '2015-01-02' ]

// ECMAScript 2018
let re2 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result2 = re2.exec('2015-01-02');
console.log(result2);

// ["2015-01-02", "2015", "01", "02", index: 0, input: "2015-01-02", 
//    groups: {year: "2015", month: "01", day: "02"}
// ]

console.log(result2.groups.year); // 2015

10.2 在 regex 内使用 Named groups

使用\k<组名>格式来反向引用正则表达式本身中的组,例如:

图片描述

// 在下面的例子中,我们有一个包合的“水果”组。
// 它既可以配“苹果”,也可以配“橘子”,
// 我们可以使用 “\k<group name>” (\k<fruit>) 来反向引用这个组的结果,
// 所以它可以匹配“=”相同的单词

let sameWords = /(?<fruit>apple|orange)=\k<fruit>/u;

sameWords.test('apple=apple') // true
sameWords.test('orange=orange') // true
sameWords.test('apple=orange') // false

10.3 在 String.prototype.replace 中使用 named groups

在 String.prototype.replace 方法中使用 named groups。所以我们能更快捷的交换词。

例如,把 “firstName, lastName” 改成 “lastName, firstName”。

图片描述

let re = /(?<firstName>[A-Za-z]+) (?<lastName>[A-Za-z]+$)/u;

'Hello World'.replace(re, `$<lastName>, $<firstName>`) // "World, Hello"

11.对象的 Rest 属性

rest操作 …(三个点)允许挑练我们需要的属性。

11.1 通过 Rest 解构你需要的属性

图片描述

let { firstName, age, ...remaining } = {
  firstName: '王',
  lastName: '智艺',
  age: 27,
  height: '1.78',
  race: '黄'
}

firstName; // 王
age; // 27
remaining; // { lastName: "智艺", height: "1.78", race: "黄" }

12.对象的扩展属性

扩展 和 解析 的 三个点是一样的,但是不同的是你可以用 扩展 去新建或者组合一个新对象。

扩展 是对齐赋值的右运算符, 而 解构 是左运算符。

图片描述

const person = { fName: '小明', age: 20 };
const account = { name: '小智', amount: '$1000'};

const personAndAccount = { ...person, ...account };
personAndAccount; // {fName: "小明", age: 20, name: "小智", amount: "$1000"}

13.正则表达式反向(lookbehind)断言

断言(Assertion)是一个对当前匹配位置之前或之后的字符的测试, 它不会实际消耗任何字符,所以断言也被称为“非消耗性匹配”或“非获取匹配”。

正则表达式的断言一共有 4 种形式:

  • (?=pattern) 零宽正向肯定断言(zero-width positive lookahead assertion)
  • (?!pattern) 零宽正向否定断言(zero-width negative lookahead assertion)
  • (?<=pattern) 零宽反向肯定断言(zero-width positive lookbehind assertion)
  • (?<!pattern) 零宽反向否定断言(zero-width negative lookbehind assertion)

你可以使用组(?<=…) 去正向断言,也可以用 (?<!…) 去取反。

正向断言: 我们想确保 # 在 winning 之前。(就是#winning),想正则匹配返回 winning。下面是写法:

图片描述

反向断言:匹配一个数字,有 € 字符而没有 $ 字符在前面的数字。

图片描述

更多内容可以参考:S2018 新特征之:正则表达式反向(lookbehind)断言

14. RegExp Unicode Property Escapes

用正则去匹配 Unicode 字符是很不容易的。像 w , W , d 这种只能匹配英文字符和数字。但是其他语言的字符怎么办呢,比如印度语,希腊语?

例如 Unicode 数据库组里把所有的印度语字符,标识为 Script = Devanagari。还有一个属性 Script_Extensions, 值也为 Devanagari。 所以我们可以通过搜索 Script=Devanagari,得到所有的印度语。

Devanagari 可以用于印度的各种语言,如Marathi, Hindi, Sanskrit。

在 ECMAScript 2018 里, 我们可以使用 p 和 {Script=Devanagari} 匹配那些所有的印度语字符。也就是说 p{Script=Devanagari} 这样就可以匹配。

图片描述

//The following matches multiple hindi character
/^\p{Script=Devanagari}+$/u.test('हिन्दी'); //true  
//PS:there are 3 hindi characters h

同理,希腊语的语言是 Script_Extensions 和 Script 的值等于 Greek 。也就是用Script_Extensions=Greek or Script=Greek 这样就可以匹配所有的希腊语,也就是说,我们用 p{Script=Greek} 匹配所有的希腊语。

进一步说,Unicode 表情库里存了各式各样的布尔值,像 Emoji, Emoji_Component, Emoji_Presentation, Emoji_Modifier, and Emoji_Modifier_Base 的值,都等于 true。所以我们想搜 Emoji 等于 ture,就能搜到所有的表情。

我们用 \p{Emoji} ,\Emoji_Modifier 匹配所有的表情。

图片描述

参考文献:

  1. ECMAScript 2018 Proposal
  2. https://mathiasbynens.be/note...

15. Promise.prototype.finally()

finally() 是 Promise 新增的一个实例方法。意图是允许在 resolve/reject 之后执行回调。finally 没有返回值,始终会被执行。

让我们看看各种情况。

图片描述

图片描述

图片描述

图片描述

16.异步迭代(Asynchronous Iteration)

这是一个极其好用的新特性。让我们能够非常容易的创建异步循环代码。

图片描述

原文: Here are examples of everything new in ECMAScript 2016, 2017, and 2018

你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

查看原文

Mask 收藏了文章 · 2018-12-08

你不知道的Node.js性能优化,读了之后水平直线上升

本文由云+社区发表

“当我第一次知道要这篇文章的时候,其实我是拒绝的,因为我觉得,你不能叫我写马上就写,我要有干货才行,写一些老生常谈的然后加上好多特技,那个 Node.js 性能啊好像 Duang~ 的一下就上去了,那读者一定会骂我,Node.js 根本没有这样搞性能优化的,都是假的。” ------ 斯塔克·成龙·王


1、使用最新版本的 Node.js

仅仅是简单的升级 Node.js 版本就可以轻松地获得性能提升,因为几乎任何新版本的 Node.js 都会比老版本性能更好,为什么?

Node.js 每个版本的性能提升主要来自于两个方面:

  • V8 的版本更新;
  • Node.js 内部代码的更新优化。

例如最新的 V8 7.1 中,就优化了某些情形下闭包的逃逸分析,让 Array 的一些方法得到了性能提升:

img

Node.js 的内部代码,随着版本的升级,也会有明显的优化,比如下面这个图就是 require 的性能随着 Node.js 版本升级的变化:

img

每个提交到 Node.js 的 PR 都会在 review 的时候考虑会不会对当前性能造成衰退。同时也有专门的 benchmarking 团队来监控性能变化,你可以在这里看到 Node.js 的每个版本的性能变化:

https://benchmarking.nodejs.org/

所以,你可以完全对新版本 Node.js 的性能放心,如果发现了任何在新版本下的性能衰退,欢迎提交一个 issue。

如何选择 Node.js 的版本?

这里就要科普一下 Node.js 的版本策略:

  • Node.js 的版本主要分为 Current 和 LTS;
  • Current 就是当前最新的、依然处于开发中的 Node.js 版本;
  • LTS 就是稳定的、会长期维护的版本;
  • Node.js 每六个月(每年的四月和十月)会发布一次大版本升级,大版本会带来一些不兼容的升级;
  • 每年四月发布的版本(版本号为偶数,如 v10)是 LTS 版本,即长期支持的版本,社区会从发布当年的十月开始,继续维护 18 + 12 个月(Active LTS + Maintaince LTS);
  • 每年十月发布的版本(版本号为奇数,例如现在的 v11)只有 8 个月的维护期。

举个例子,现在(2018年11月),Node.js Current 的版本是 v11,LTS 版本是 v10 和 v8。更老的 v6 处于 Maintenace LTS,从明年四月起就不再维护了。去年十月发布的 v9 版本在今年六月结束了维护。

img

对于生产环境而言,Node.js 官方推荐使用最新的 LTS 版本,现在是 v10.13.0。


2、使用 fast-json-stringify 加速 JSON 序列化

在 JavaScript 中,生成 JSON 字符串是非常方便的:

const json = JSON.stringify(obj)

但很少人会想到这里竟然也存在性能优化的空间,那就是使用 JSON Schema 来加速序列化。

在 JSON 序列化时,我们需要识别大量的字段类型,比如对于 string 类型,我们就需要在两边加上 ",对于数组类型,我们需要遍历数组,把每个对象序列化后,用 , 隔开,然后在两边加上 [],诸如此类等等。

如果已经提前通过 Schema 知道每个字段的类型,那么就不需要遍历、识别字段类型,而可以直接用序列化对应的字段,这就大大减少了计算开销,这就是 fast-json-stringfy 的原理。

根据项目中的跑分,在某些情况下甚至可以比 JSON.stringify 快接近 10 倍!

img

一个简单的示例:

const fastJson = require('fast-json-stringify')
const stringify = fastJson({
    title: 'Example Schema',
    type: 'object',
    properties: {
        name: { type: 'string' },
        age: { type: 'integer' },
        books: {
            type: 'array',
            items: {
                type: 'string',
                uniqueItems: true
            }
        }
    }
})

console.log(stringify({
    name: 'Starkwang',
    age: 23,
    books: ['C++ Primier', '響け!ユーフォニアム~']
}))
//=> {"name":"Starkwang","age":23,"books":["C++ Primier","響け!ユーフォニアム~"]}

在 Node.js 的中间件业务中,通常会有很多数据使用 JSON 进行,并且这些 JSON 的结构是非常相似的(如果你使用了 TypeScript,更是这样),这种场景就非常适合使用 JSON Schema 来优化。


3、提升 Promise 的性能

Promise 是解决回调嵌套地狱的灵丹妙药,特别是当自从 async/await 全面普及之后,它们的组合无疑成为了 JavaScript 异步编程的终极解决方案,现在大量的项目都已经开始使用这种模式。

但是优雅的语法后面也隐藏着性能损耗,我们可以使用 github 上一个已有的跑分项目进行测试,以下是测试结果:

file                               time(ms)  memory(MB)
callbacks-baseline.js                   380       70.83
promises-bluebird.js                    554       97.23
promises-bluebird-generator.js          585       97.05
async-bluebird.js                       593      105.43
promises-es2015-util.promisify.js      1203      219.04
promises-es2015-native.js              1257      227.03
async-es2017-native.js                 1312      231.08
async-es2017-util.promisify.js         1550      228.74

Platform info:
Darwin 18.0.0 x64
Node.JS 11.1.0
V8 7.0.276.32-node.7
Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz × 4

我们可以从结果中看到,原生 async/await + Promise 的性能比 callback 要差很多,并且内存占用也高得多。对于大量异步逻辑的中间件项目而言,这里的性能开销还是不能忽视的。

通过对比可以发现,性能损耗主要来自于 Promise 对象自身的实现,V8 原生实现的 Promise 比 bluebird 这样第三方实现的 Promise 库要慢很多。而 async/await 语法并不会带来太多的性能损失。

所以对于大量异步逻辑、轻量计算的中间件项目而言,可以在代码中把全局的 Promise 换为 bluebird 的实现:

global.Promise = require('bluebird');

4、正确地编写异步代码

使用 async/await 之后,项目的异步代码会非常好看:

const foo = await doSomethingAsync();
const bar = await doSomethingElseAsync();

但因此,有时我们也会忘记使用 Promise 给我们带来的其它能力,比如 Promise.all() 的并行能力:

// bad
async function getUserInfo(id) {
    const profile = await getUserProfile(id);
    const repo = await getUserRepo(id)
    return { profile, repo }
}

// good
async function getUserInfo(id) {
    const [profile, repo] = await Promise.all([
        getUserProfile(id),
        getUserRepo(id)
    ])
    return { profile, repo }
}

还有比如 Promise.any()(此方法不在ES6 Promise标准中,也可以使用标准的 Promise.race() 代替),我们可以用它轻松实现更加可靠快速的调用:

async function getServiceIP(name) {
    // 从 DNS 和 ZooKeeper 获取服务 IP,哪个先成功返回用哪个
    // 与 Promise.race 不同的是,这里只有当两个调用都 reject 时,才会抛出错误
    return await Promise.any([
        getIPFromDNS(name),
        getIPFromZooKeeper(name)
    ])
} 

5、优化 V8 GC

关于 V8 的垃圾回收机制,已经有很多类似的文章了,这里就不再重复介绍。推荐两篇文章:

我们在日常开发代码的时候,比较容易踩到下面几个坑:

坑一:使用大对象作为缓存,导致老生代(Old Space)的垃圾回收变慢

示例:

const cache = {}
async function getUserInfo(id) {
    if (!cache[id]) {
        cache[id] = await getUserInfoFromDatabase(id)
    }
    return cache[id]
}

这里我们使用了一个变量 cache 作为缓存,加速用户信息的查询,进行了很多次查询后,cache 对象会进入老生代,并且会变得无比庞大,而老生代是使用三色标记 + DFS 的方式进行 GC 的,一个大对象会直接导致 GC 花费的时间增长(而且也有内存泄漏的风险)。

解决方法就是:

  • 使用 Redis 这样的外部缓存,实际上像 Redis 这样的内存型数据库非常适合这种场景;
  • 限制本地缓存对象的大小,比如使用 FIFO、TTL 之类的机制来清理对象中的缓存。

坑二:新生代空间不足,导致频繁 GC

这个坑会比较隐蔽。

Node.js 默认给新生代分配的内存是 64MB(64位的机器,后同),但因为新生代 GC 使用的是 Scavenge 算法,所以实际能使用的内存只有一半,即 32MB。

当业务代码频繁地产生大量的小对象时,这个空间很容易就会被占满,从而触发 GC。虽然新生代的 GC 比老生代要快得多,但频繁的 GC 依然会很大地影响性能。极端的情况下,GC 甚至可以占用全部计算时间的 30% 左右。

解决方法就是,在启动 Node.js 时,修改新生代的内存上限,减少 GC 的次数:

node --max-semi-space-size=128 app.js

当然有人肯定会问,新生代的内存是不是越大越好呢?

随着内存的增大,GC 的次数减少,但每次 GC 所需要的时间也会增加,所以并不是越大越好,具体数值需要对业务进行压测 profile 才能确定分配多少新生代内存最好。

但一般根据经验而言,分配 64MB 或者 128MB 是比较合理的


6、正确地使用 Stream

Stream 是 Node.js 最基本的概念之一,Node.js 内部的大部分与 IO 相关的模块,比如 http、net、fs、repl,都是建立在各种 Stream 之上的。

下面这个经典的例子应该大部分人都知道,对于大文件,我们不需要把它完全读入内存,而是使用 Stream 流式地把它发送出去:

const http = require('http');
const fs = require('fs');

// bad
http.createServer(function (req, res) {
    fs.readFile(__dirname + '/data.txt', function (err, data) {
        res.end(data);
    });
});

// good
http.createServer(function (req, res) {
    const stream = fs.createReadStream(__dirname + '/data.txt');
    stream.pipe(res);
});

在业务代码中合理地使用 Stream 能很大程度地提升性能,当然是但实际的业务中我们很可能会忽略这一点,比如采用 React 服务器端渲染的项目,我们就可以用 renderToNodeStream

const ReactDOMServer require('react-dom/server')
const http = require('http')
const fs = require('fs')
const app = require('./app')

// bad
const server = http.createServer((req, res) => {
    const body = ReactDOMServer.renderToString(app)
    res.end(body)
});

// good
const server = http.createServer(function (req, res) {
    const stream = ReactDOMServer.renderToNodeStream(app)
    stream.pipe(res)
})

server.listen(8000)

使用 pipeline 管理 stream

在过去的 Node.js 中,处理 stream 是非常麻烦的,举个例子:

source.pipe(a).pipe(b).pipe(c).pipe(dest)

一旦其中 source、a、b、c、dest 中,有任何一个 stream 出错或者关闭,会导致整个管道停止,此时我们需要手工销毁所有的 stream,在代码层面这是非常麻烦的。

所以社区出现了 pump 这样的库来自动控制 stream 的销毁。而 Node.js v10.0 加入了一个新的特性:stream.pipeline,可以替代 pump 帮助我们更好的管理 stream。

一个官方的例子:

const { pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');

pipeline(
    fs.createReadStream('archive.tar'),
    zlib.createGzip(),
    fs.createWriteStream('archive.tar.gz'),
    (err) => {
        if (err) {
            console.error('Pipeline failed', err);
        } else {
            console.log('Pipeline succeeded');
        }
    }
);

实现自己的高性能 Stream

在业务中你可能也会自己实现一个 Stream,可读、可写、或者双向流,可以参考文档:

Stream 虽然很神奇,但自己实现 Stream 也可能会存在隐藏的性能问题,比如:

class MyReadable extends Readable {
    _read(size) {
        while (null !== (chunk = getNextChunk())) {
            this.push(chunk);
        }
    }
}

当我们调用 new MyReadable().pipe(xxx) 时,会把 getNextChunk() 所得到的 chunk 都 push 出去,直到读取结束。但如果此时管道的下一步处理速度较慢,就会导致数据堆积在内存中,导致内存占用变大,GC 速度降低。

而正确的做法应该是,根据 this.push() 返回值选择正确的行为,当返回值为 false 时,说明此时堆积的 chunk 已经满了,应该停止读入。

class MyReadable extends Readable {
    _read(size) {
        while (null !== (chunk = getNextChunk())) {
            if (!this.push(chunk)) {
                return false  
            }
        }
    }
}

这个问题在 Node.js 官方的一篇文章中有详细的介绍:Backpressuring in Streams


7、C++ 扩展一定比 JavaScript 快吗?

Node.js 非常适合 IO 密集型的应用,而对于计算密集的业务,很多人都会想到用编写 C++ Addon 的方式来优化性能。但实际上 C++ 扩展并不是灵丹妙药,V8 的性能也没有想象的那么差。

比如,我在今年九月份的时候把 Node.js 的 net.isIPv6() 从 C++ 迁移到了 JS 的实现,让大多数的测试用例都获得了 10%- 250% 不等的性能提升(具体PR可以看这里)。

JavaScript 在 V8 上跑得比 C++ 扩展还快,这种情况多半发生在与字符串、正则表达式相关的场景,因为 V8 内部使用的正则表达式引擎是 irregexp,这个正则表达式引擎比 boost 中自带的引擎(boost::regex)要快得多。

还有一处值得注意的就是,Node.js 的 C++ 扩展在进行类型转换的时候,可能会消耗非常多的性能,如果不注意 C++ 代码的细节,性能会很大地下降。

这里有一篇文章对比了相同算法下 C++ 和 JS 的性能(需翻墙):How to get a performance boost using Node.js native addons。其中值得注意的结论就是,C++ 代码在对参数中的字符串进行转换后(String::Utf8Value转为std::string),性能甚至不如 JS 实现的一半。只有在使用 NAN 提供的类型封装后,才获得了比 JS 更高的性能。

img

换句话说,C++ 是否比 JavaScript 更加高效需要具体问题具体分析,某些情况下,C++ 扩展不一定就会比原生 JavaScript 更高效。如果你对自己的 C++ 水平不是那么有信心,其实还是建议用 JavaScript 来实现,因为 V8 的性能比你想象的要好得多。


8、使用 node-clinic 快速定位性能问题

说了这么多,有没有什么可以开箱即用,五分钟见效的呢?当然有。

node-clinicNearForm 开源的一款 Node.js 性能诊断工具,可以非常快速地定位性能问题。

npm i -g clinic
npm i -g autocannon

使用的时候,先开启服务进程:

clinic doctor -- node server.js

然后我们可以用任何压测工具跑一次压测,比如使用同一个作者的 autocannon(当然你也可以使用 ab、curl 这样的工具来进行压测。):

autocannon http://localhost:3000

压测完毕后,我们 ctrl + c 关闭 clinic 开启的进程,就会自动生成报告。比如下面就是我们一个中间件服务的性能报告:

img

我们可以从 CPU 的使用曲线看出,这个中间件服务的性能瓶颈不在自身内部的计算,而在于 I/O 速度太慢。clinic 也在上面告诉我们检测到了潜在的 I/O 问题。

下面我们使用 clinic bubbleprof 来检测 I/O 问题:

clinic bubbleprof -- node server.js

再次进行压测后,我们得到了新的报告:

img

这个报告中,我们可以看到,http.Server 在整个程序运行期间,96% 的时间都处于 pending 状态,点开后,我们会发现调用栈中存在大量的 empty frame,也就是说,由于网络 I/O 的限制,CPU 存在大量的空转,这在中间件业务中非常常见,也为我们指明了优化方向不在服务内部,而在服务器的网关和依赖的服务相应速度上。

想知道如何读懂 clinic bubbleprof 生成的报告,可以看这里:https://clinicjs.org/bubblepr...

同样,clinic 也可以检测到服务内部的计算性能问题,下面我们做一些“破坏”,让这个服务的性能瓶颈出现在 CPU 计算上。

我们在某个中间件中加入了空转一亿次这样非常消耗 CPU 的“破坏性”代码:

function sleep() {
    let n = 0
    while (n++ < 10e7) {
        empty()
    }
}
function empty() { }

module.exports = (ctx, next) => {
    sleep()
    // ......
    return next()
}

然后使用 clinic doctor,重复上面的步骤,生成性能报告:

img

这就是一个非常典型的同步计算阻塞了异步队列的“病例”,即主线程上进行了大量的计算,导致 JavaScript 的异步回调没法及时触发,Event Loop 的延迟极高。

对于这样的应用,我们可以继续使用 clinic flame 来确定到底是哪里出现了密集计算:

clinic flame -- node app.js

压测后,我们得到了火焰图(这里把空转次数减少到了100万次,让火焰图看起来不至于那么极端):

img

从这张图里,我们可以明显看到顶部的那个大白条,它代表了 sleep 函数空转所消耗的 CPU 时间。根据这样的火焰图,我们可以非常轻松地看出 CPU 资源的消耗情况,从而定位代码中哪里有密集的计算,找到性能瓶颈。

此文已由作者授权腾讯云+社区发布


查看原文

Mask 赞了文章 · 2018-12-04

前端与编译原理——用JS写一个JS解释器

图片描述

说起编译原理,印象往往只停留在本科时那些枯燥的课程和晦涩的概念。作为前端开发者,编译原理似乎离我们很远,对它的理解很可能仅仅局限于“抽象语法树(AST)”。但这仅仅是个开头而已。编译原理的使用,甚至能让我们利用JS直接写一个能运行JS代码的解释器。

项目地址:https://github.com/jrainlau/c...

在线体验:https://codepen.io/jrainlau/p...

一、为什么要用JS写JS的解释器

接触过小程序开发的同学应该知道,小程序运行的环境禁止new Functioneval等方法的使用,导致我们无法直接执行字符串形式的动态代码。此外,许多平台也对这些JS自带的可执行动态代码的方法进行了限制,那么我们是没有任何办法了吗?既然如此,我们便可以用JS写一个解析器,让JS自己去运行自己。

在开始之前,我们先简单回顾一下编译原理的一些概念。

二、什么是编译器

说到编译原理,肯定离不开编译器。简单来说,当一段代码经过编译器的词法分析、语法分析等阶段之后,会生成一个树状结构的“抽象语法树(AST)”,该语法树的每一个节点都对应着代码当中不同含义的片段。

比如有这么一段代码:

const a = 1
console.log(a)

经过编译器处理后,它的AST长这样:

{
  "type": "Program",
  "start": 0,
  "end": 26,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 11,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 11,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 10,
            "end": 11,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "ExpressionStatement",
      "start": 12,
      "end": 26,
      "expression": {
        "type": "CallExpression",
        "start": 12,
        "end": 26,
        "callee": {
          "type": "MemberExpression",
          "start": 12,
          "end": 23,
          "object": {
            "type": "Identifier",
            "start": 12,
            "end": 19,
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "start": 20,
            "end": 23,
            "name": "log"
          },
          "computed": false
        },
        "arguments": [
          {
            "type": "Identifier",
            "start": 24,
            "end": 25,
            "name": "a"
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}
常见的JS编译器有babylonacorn等等,感兴趣的同学可以在AST explorer这个网站自行体验。

可以看到,编译出来的AST详细记录了代码中所有语义代码的类型、起始位置等信息。这段代码除了根节点Program外,主体包含了两个节点VariableDeclarationExpressionStatement,而这些节点里面又包含了不同的子节点。

正是由于AST详细记录了代码的语义化信息,所以Babel,Webpack,Sass,Less等工具可以针对代码进行非常智能的处理。

三、什么是解释器

如同翻译人员不仅能看懂一门外语,也能对其艺术加工后把它翻译成母语一样,人们把能够将代码转化成AST的工具叫做“编译器”,而把能够将AST翻译成目标语言并运行的工具叫做“解释器”。

在编译原理的课程中,我们思考过这么一个问题:如何让计算机运行算数表达式1+2+3:

1 + 2 + 3

当机器执行的时候,它可能会是这样的机器码:

1 PUSH 1
2 PUSH 2
3 ADD
4 PUSH 3
5 ADD

而运行这段机器码的程序,就是解释器。

在这篇文章中,我们不会搞出机器码这样复杂的东西,仅仅是使用JS在其runtime环境下去解释JS代码的AST。由于解释器使用JS编写,所以我们可以大胆使用JS自身的语言特性,比如this绑定、new关键字等等,完全不需要对它们进行额外处理,也因此让JS解释器的实现变得非常简单。

在回顾了编译原理的基本概念之后,我们就可以着手进行开发了。

四、节点遍历器

通过分析上文的AST,可以看到每一个节点都会有一个类型属性type,不同类型的节点需要不同的处理方式,处理这些节点的程序,就是“节点处理器(nodeHandler)”

定义一个节点处理器:

const nodeHandler = {
  Program () {},
  VariableDeclaration () {},
  ExpressionStatement () {},
  MemberExpression () {},
  CallExpression () {},
  Identifier () {}
}

关于节点处理器的具体实现,会在后文进行详细探讨,这里暂时不作展开。

有了节点处理器,我们便需要去遍历AST当中的每一个节点,递归地调用节点处理器,直到完成对整棵语法书的处理。

定义一个节点遍历器(NodeIterator):

class NodeIterator {
  constructor (node) {
    this.node = node
    this.nodeHandler = nodeHandler
  }

  traverse (node) {
    // 根据节点类型找到节点处理器当中对应的函数
    const _eval = this.nodeHandler[node.type]
    // 若找不到则报错
    if (!_eval) {
      throw new Error(`canjs: Unknown node type "${node.type}".`)
    }
    // 运行处理函数
    return _eval(node)
  }

}

理论上,节点遍历器这样设计就可以了,但仔细推敲,发现漏了一个很重要的东西——作用域处理。

回到节点处理器的VariableDeclaration()方法,它用来处理诸如const a = 1这样的变量声明节点。假设它的代码如下:

  VariableDeclaration (node) {
    for (const declaration of node.declarations) {
      const { name } = declaration.id
      const value = declaration.init ? traverse(declaration.init) : undefined
      // 问题来了,拿到了变量的名称和值,然后把它保存到哪里去呢?
      // ...
    }
  },

问题在于,处理完变量声明节点以后,理应把这个变量保存起来。按照JS语言特性,这个变量应该存放在一个作用域当中。在JS解析器的实现过程中,这个作用域可以被定义为一个scope对象。

改写节点遍历器,为其新增一个scope对象

class NodeIterator {
  constructor (node, scope = {}) {
    this.node = node
    this.scope = scope
    this.nodeHandler = nodeHandler
  }

  traverse (node, options = {}) {
    const scope = options.scope || this.scope
    const nodeIterator = new NodeIterator(node, scope)
    const _eval = this.nodeHandler[node.type]
    if (!_eval) {
      throw new Error(`canjs: Unknown node type "${node.type}".`)
    }
    return _eval(nodeIterator)
  }

  createScope (blockType = 'block') {
    return new Scope(blockType, this.scope)
  }
}

然后节点处理函数VariableDeclaration()就可以通过scope保存变量了:

  VariableDeclaration (nodeIterator) {
    const kind = nodeIterator.node.kind
    for (const declaration of nodeIterator.node.declarations) {
      const { name } = declaration.id
      const value = declaration.init ? nodeIterator.traverse(declaration.init) : undefined
      // 在作用域当中定义变量
      // 如果当前是块级作用域且变量用var定义,则定义到父级作用域
      if (nodeIterator.scope.type === 'block' && kind === 'var') {
        nodeIterator.scope.parentScope.declare(name, value, kind)
      } else {
        nodeIterator.scope.declare(name, value, kind)
      }
    }
  },

关于作用域的处理,可以说是整个JS解释器最难的部分。接下来我们将对作用域处理进行深入的剖析。

五、作用域处理

考虑到这样一种情况:

const a = 1
{
  const b = 2
  console.log(a)
}
console.log(b)

运行结果必然是能够打印出a的值,然后报错:Uncaught ReferenceError: b is not defined

这段代码就是涉及到了作用域的问题。块级作用域或者函数作用域可以读取其父级作用域当中的变量,反之则不行,所以对于作用域我们不能简单地定义一个空对象,而是要专门进行处理。

定义一个作用域基类Scope

class Scope {
  constructor (type, parentScope) {
    // 作用域类型,区分函数作用域function和块级作用域block
    this.type = type
    // 父级作用域
    this.parentScope = parentScope
    // 全局作用域
    this.globalDeclaration = standardMap
    // 当前作用域的变量空间
    this.declaration = Object.create(null)
  }

  /*
   * get/set方法用于获取/设置当前作用域中对应name的变量值
     符合JS语法规则,优先从当前作用域去找,若找不到则到父级作用域去找,然后到全局作用域找。
     如果都没有,就报错
   */
  get (name) {
    if (this.declaration[name]) {
      return this.declaration[name]
    } else if (this.parentScope) {
      return this.parentScope.get(name)
    } else if (this.globalDeclaration[name]) {
      return this.globalDeclaration[name]
    }
    throw new ReferenceError(`${name} is not defined`)
  }

  set (name, value) {
    if (this.declaration[name]) {
      this.declaration[name] = value
    } else if (this.parentScope[name]) {
      this.parentScope.set(name, value)
    } else {
      throw new ReferenceError(`${name} is not defined`)
    }
  }

  /**
   * 根据变量的kind调用不同的变量定义方法
   */
  declare (name, value, kind = 'var') {
    if (kind === 'var') {
      return this.varDeclare(name, value)
    } else if (kind === 'let') {
      return this.letDeclare(name, value)
    } else if (kind === 'const') {
      return this.constDeclare(name, value)
    } else {
      throw new Error(`canjs: Invalid Variable Declaration Kind of "${kind}"`)
    }
  }

  varDeclare (name, value) {
    let scope = this
    // 若当前作用域存在非函数类型的父级作用域时,就把变量定义到父级作用域
    while (scope.parentScope && scope.type !== 'function') {
      scope = scope.parentScope
    }
    this.declaration[name] = new SimpleValue(value, 'var')
    return this.declaration[name]
  }

  letDeclare (name, value) {
    // 不允许重复定义
    if (this.declaration[name]) {
      throw new SyntaxError(`Identifier ${name} has already been declared`)
    }
    this.declaration[name] = new SimpleValue(value, 'let')
    return this.declaration[name]
  }

  constDeclare (name, value) {
    // 不允许重复定义
    if (this.declaration[name]) {
      throw new SyntaxError(`Identifier ${name} has already been declared`)
    }
    this.declaration[name] = new SimpleValue(value, 'const')
    return this.declaration[name]
  }
}

这里使用了一个叫做simpleValue()的函数来定义变量值,主要用于处理常量:

class SimpleValue {
  constructor (value, kind = '') {
    this.value = value
    this.kind = kind
  }

  set (value) {
    // 禁止重新对const类型变量赋值
    if (this.kind === 'const') {
      throw new TypeError('Assignment to constant variable')
    } else {
      this.value = value
    }
  }

  get () {
    return this.value
  }
}

处理作用域问题思路,关键的地方就是在于JS语言本身寻找变量的特性——优先当前作用域,父作用域次之,全局作用域最后。反过来,在节点处理函数VariableDeclaration()里,如果遇到块级作用域且关键字为var,则需要把这个变量也定义到父级作用域当中,这也就是我们常说的“全局变量污染”。

JS标准库注入

细心的读者会发现,在定义Scope基类的时候,其全局作用域globalScope被赋值了一个standardMap对象,这个对象就是JS标准库。

简单来说,JS标准库就是JS这门语言本身所带有的一系列方法和属性,如常用的setTimeoutconsole.log等等。为了让解析器也能够执行这些方法,所以我们需要为其注入标准库:

const standardMap = {
  console: new SimpleValue(console)
}

这样就相当于往解析器的全局作用域当中注入了console这个对象,也就可以直接被使用了。

六、节点处理器

在处理完节点遍历器、作用域处理的工作之后,便可以来编写节点处理器了。顾名思义,节点处理器是专门用来处理AST节点的,上文反复提及的VariableDeclaration()方法便是其中一个。下面将对部分关键的节点处理器进行讲解。

在开发节点处理器之前,需要用到一个工具,用于判断JS语句当中的returnbreakcontinue关键字。

关键字判断工具Signal

定义一个Signal基类:

class Signal {
  constructor (type, value) {
    this.type = type
    this.value = value
  }

  static Return (value) {
    return new Signal('return', value)
  }

  static Break (label = null) {
    return new Signal('break', label)
  }

  static Continue (label) {
    return new Signal('continue', label)
  }

  static isReturn(signal) {
    return signal instanceof Signal && signal.type === 'return'
  }

  static isContinue(signal) {
    return signal instanceof Signal && signal.type === 'continue'
  }

  static isBreak(signal) {
    return signal instanceof Signal && signal.type === 'break'
  }

  static isSignal (signal) {
    return signal instanceof Signal
  }
}

有了它,就可以对语句当中的关键字进行判断处理,接下来会有大用处。

1、变量定义节点处理器——VariableDeclaration()

最常用的节点处理器之一,负责把变量注册到正确的作用域。

  VariableDeclaration (nodeIterator) {
    const kind = nodeIterator.node.kind
    for (const declaration of nodeIterator.node.declarations) {
      const { name } = declaration.id
      const value = declaration.init ? nodeIterator.traverse(declaration.init) : undefined
      // 在作用域当中定义变量
      // 若为块级作用域且关键字为var,则需要做全局污染
      if (nodeIterator.scope.type === 'block' && kind === 'var') {
        nodeIterator.scope.parentScope.declare(name, value, kind)
      } else {
        nodeIterator.scope.declare(name, value, kind)
      }
    }
  },

2、标识符节点处理器——Identifier()

专门用于从作用域中获取标识符的值。

  Identifier (nodeIterator) {
    if (nodeIterator.node.name === 'undefined') {
      return undefined
    }
    return nodeIterator.scope.get(nodeIterator.node.name).value
  },

3、字符节点处理器——Literal()

返回字符节点的值。

  Literal (nodeIterator) {
    return nodeIterator.node.value
  }

4、表达式调用节点处理器——CallExpression()

用于处理表达式调用节点的处理器,如处理func()console.log()等。

  CallExpression (nodeIterator) {
    // 遍历callee获取函数体
    const func = nodeIterator.traverse(nodeIterator.node.callee)
    // 获取参数
    const args = nodeIterator.node.arguments.map(arg => nodeIterator.traverse(arg))

    let value
    if (nodeIterator.node.callee.type === 'MemberExpression') {
      value = nodeIterator.traverse(nodeIterator.node.callee.object)
    }
    // 返回函数运行结果
    return func.apply(value, args)
  },

5、表达式节点处理器——MemberExpression()

区分于上面的“表达式调用节点处理器”,表达式节点指的是person.sayconsole.log这种函数表达式。

  MemberExpression (nodeIterator) {
    // 获取对象,如console
    const obj = nodeIterator.traverse(nodeIterator.node.object)
    // 获取对象的方法,如log
    const name = nodeIterator.node.property.name
    // 返回表达式,如console.log
    return obj[name]
  }

6、块级声明节点处理器——BlockStatement()

非常常用的处理器,专门用于处理块级声明节点,如函数、循环、try...catch...当中的情景。

  BlockStatement (nodeIterator) {
    // 先定义一个块级作用域
    let scope = nodeIterator.createScope('block')

    // 处理块级节点内的每一个节点
    for (const node of nodeIterator.node.body) {
      if (node.type === 'VariableDeclaration' && node.kind === 'var') {
        for (const declaration of node.declarations) {
          scope.declare(declaration.id.name, declaration.init.value, node.kind)
        }
      } else if (node.type === 'FunctionDeclaration') {
        nodeIterator.traverse(node, { scope })
      }
    }

    // 提取关键字(return, break, continue)
    for (const node of nodeIterator.node.body) {
      if (node.type === 'FunctionDeclaration') {
        continue
      }
      const signal = nodeIterator.traverse(node, { scope })
      if (Signal.isSignal(signal)) {
        return signal
      }
    }
  }

可以看到这个处理器里面有两个for...of循环。第一个用于处理块级内语句,第二个专门用于识别关键字,如循环体内部的breakcontinue或者函数体内部的return

7、函数定义节点处理器——FunctionDeclaration()

往作用当中声明一个和函数名相同的变量,值为所定义的函数:

  FunctionDeclaration (nodeIterator) {
    const fn = NodeHandler.FunctionExpression(nodeIterator)
    nodeIterator.scope.varDeclare(nodeIterator.node.id.name, fn)
    return fn    
  }

8、函数表达式节点处理器——FunctionExpression()

用于定义一个函数:

  FunctionExpression (nodeIterator) {
    const node = nodeIterator.node
    /**
     * 1、定义函数需要先为其定义一个函数作用域,且允许继承父级作用域
     * 2、注册`this`, `arguments`和形参到作用域的变量空间
     * 3、检查return关键字
     * 4、定义函数名和长度
     */
    const fn = function () {
      const scope = nodeIterator.createScope('function')
      scope.constDeclare('this', this)
      scope.constDeclare('arguments', arguments)

      node.params.forEach((param, index) => {
        const name = param.name
        scope.varDeclare(name, arguments[index])
      })

      const signal = nodeIterator.traverse(node.body, { scope })
      if (Signal.isReturn(signal)) {
        return signal.value
      }
    }

    Object.defineProperties(fn, {
      name: { value: node.id ? node.id.name : '' },
      length: { value: node.params.length }
    })

    return fn
  }

9、this表达式处理器——ThisExpression()

该处理器直接使用JS语言自身的特性,把this关键字从作用域中取出即可。

  ThisExpression (nodeIterator) {
    const value = nodeIterator.scope.get('this')
    return value ? value.value : null
  }

10、new表达式处理器——NewExpression()

this表达式类似,也是直接沿用JS的语言特性,获取函数和参数之后,通过bind关键字生成一个构造函数,并返回。

  NewExpression (nodeIterator) {
    const func = nodeIterator.traverse(nodeIterator.node.callee)
    const args = nodeIterator.node.arguments.map(arg => nodeIterator.traverse(arg))
    return new (func.bind(null, ...args))
  }

11、For循环节点处理器——ForStatement()

For循环的三个参数对应着节点的inittestupdate属性,对着三个属性分别调用节点处理器处理,并放回JS原生的for循环当中即可。

  ForStatement (nodeIterator) {
    const node = nodeIterator.node
    let scope = nodeIterator.scope
    if (node.init && node.init.type === 'VariableDeclaration' && node.init.kind !== 'var') {
      scope = nodeIterator.createScope('block')
    }

    for (
      node.init && nodeIterator.traverse(node.init, { scope });
      node.test ? nodeIterator.traverse(node.test, { scope }) : true;
      node.update && nodeIterator.traverse(node.update, { scope })
    ) {
      const signal = nodeIterator.traverse(node.body, { scope })
      
      if (Signal.isBreak(signal)) {
        break
      } else if (Signal.isContinue(signal)) {
        continue
      } else if (Signal.isReturn(signal)) {
        return signal
      }
    }
  }

同理,for...inwhiledo...while循环也是类似的处理方式,这里不再赘述。

12、If声明节点处理器——IfStatemtnt()

处理If语句,包括ifif...elseif...elseif...else

  IfStatement (nodeIterator) {
    if (nodeIterator.traverse(nodeIterator.node.test)) {
      return nodeIterator.traverse(nodeIterator.node.consequent)
    } else if (nodeIterator.node.alternate) {
      return nodeIterator.traverse(nodeIterator.node.alternate)
    }
  }

同理,switch语句、三目表达式也是类似的处理方式。

---

上面列出了几个比较重要的节点处理器,在es5当中还有很多节点需要处理,详细内容可以访问这个地址一探究竟。

七、定义调用方式

经过了上面的所有步骤,解析器已经具备处理es5代码的能力,接下来就是对这些散装的内容进行组装,最终定义一个方便用户调用的办法。

const { Parser } = require('acorn')
const NodeIterator = require('./iterator')
const Scope = require('./scope')

class Canjs {
  constructor (code = '', extraDeclaration = {}) {
    this.code = code
    this.extraDeclaration = extraDeclaration
    this.ast = Parser.parse(code)
    this.nodeIterator = null
    this.init()
  }

  init () {
    // 定义全局作用域,该作用域类型为函数作用域
    const globalScope = new Scope('function')
    // 根据入参定义标准库之外的全局变量
    Object.keys(this.extraDeclaration).forEach((key) => {
      globalScope.addDeclaration(key, this.extraDeclaration[key])
    })
    this.nodeIterator = new NodeIterator(null, globalScope)
  }

  run () {
    return this.nodeIterator.traverse(this.ast)
  }
}

这里我们定义了一个名为Canjs的基类,接受字符串形式的JS代码,同时可定义标准库之外的变量。当运行run()方法的时候就可以得到运行结果。

八、后续

至此,整个JS解析器已经完成,可以很好地运行ES5的代码(可能还有bug没有发现)。但是在当前的实现中,所有的运行结果都是放在一个类似沙盒的地方,无法对外界产生影响。如果要把运行结果取出来,可能的办法有两种。第一种是传入一个全局的变量,把影响作用在这个全局变量当中,借助它把结果带出来;另外一种则是让解析器支持export语法,能够把export语句声明的结果返回,感兴趣的读者可以自行研究。

最后,这个JS解析器已经在我的Github上开源,欢迎前来交流~

https://github.com/jrainlau/c...

参考资料

从零开始写一个Javascript解析器

微信小程序也要强行热更代码,鹅厂不服你来肛我呀

jkeylu/evil-eval

查看原文

赞 189 收藏 122 评论 14

Mask 收藏了文章 · 2018-11-21

ES6 系列之我们来聊聊装饰器

Decorator

装饰器主要用于:

  1. 装饰类
  2. 装饰方法或属性

装饰类

@annotation
class MyClass { }

function annotation(target) {
   target.annotated = true;
}

装饰方法或属性

class MyClass {
  @readonly
  method() { }
}

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

Babel

安装编译

我们可以在 Babel 官网的 Try it out,查看 Babel 编译后的代码。

不过我们也可以选择本地编译:

npm init

npm install --save-dev @babel/core @babel/cli

npm install --save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties

新建 .babelrc 文件

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", {"loose": true}]
  ]
}

再编译指定的文件

babel decorator.js --out-file decorator-compiled.js

装饰类的编译

编译前:

@annotation
class MyClass { }

function annotation(target) {
   target.annotated = true;
}

编译后:

var _class;

let MyClass = annotation(_class = class MyClass {}) || _class;

function annotation(target) {
  target.annotated = true;
}

我们可以看到对于类的装饰,其原理就是:

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

装饰方法的编译

编译前:

class MyClass {
  @unenumerable
  @readonly
  method() { }
}

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

function unenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

编译后:

var _class;

function _applyDecoratedDescriptor(target, property, decorators, descriptor, context ) {
    /**
     * 第一部分
     * 拷贝属性
     */
    var desc = {};
    Object["ke" + "ys"](descriptor).forEach(function(key) {
        desc[key] = descriptor[key];
    });
    desc.enumerable = !!desc.enumerable;
    desc.configurable = !!desc.configurable;

    if ("value" in desc || desc.initializer) {
        desc.writable = true;
    }

    /**
     * 第二部分
     * 应用多个 decorators
     */
    desc = decorators
        .slice()
        .reverse()
        .reduce(function(desc, decorator) {
            return decorator(target, property, desc) || desc;
        }, desc);

    /**
     * 第三部分
     * 设置要 decorators 的属性
     */
    if (context && desc.initializer !== void 0) {
        desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
        desc.initializer = undefined;
    }

    if (desc.initializer === void 0) {
        Object["define" + "Property"](target, property, desc);
        desc = null;
    }

    return desc;
}

let MyClass = ((_class = class MyClass {
    method() {}
}),
_applyDecoratedDescriptor(
    _class.prototype,
    "method",
    [readonly],
    Object.getOwnPropertyDescriptor(_class.prototype, "method"),
    _class.prototype
),
_class);

function readonly(target, name, descriptor) {
    descriptor.writable = false;
    return descriptor;
}

装饰方法的编译源码解析

我们可以看到 Babel 构建了一个 _applyDecoratedDescriptor 函数,用于给方法装饰。

Object.getOwnPropertyDescriptor()

在传入参数的时候,我们使用了一个 Object.getOwnPropertyDescriptor() 方法,我们来看下这个方法:

Object.getOwnPropertyDescriptor() 方法返回指定对象上的一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

顺便注意这是一个 ES5 的方法。

举个例子:

const foo = { value: 1 };
const bar = Object.getOwnPropertyDescriptor(foo, "value");
// bar {
//   value: 1,
//   writable: true
//   enumerable: true,
//   configurable: true,
// }

const foo = { get value() { return 1; } };
const bar = Object.getOwnPropertyDescriptor(foo, "value");
// bar {
//   get: /*the getter function*/,
//   set: undefined
//   enumerable: true,
//   configurable: true,
// }

第一部分源码解析

在 _applyDecoratedDescriptor 函数内部,我们首先将 Object.getOwnPropertyDescriptor() 返回的属性描述符对象做了一份拷贝:

// 拷贝一份 descriptor
var desc = {};
Object["ke" + "ys"](descriptor).forEach(function(key) {
    desc[key] = descriptor[key];
});
desc.enumerable = !!desc.enumerable;
desc.configurable = !!desc.configurable;

// 如果没有 value 属性或者没有 initializer 属性,表明是 getter 和 setter
if ("value" in desc || desc.initializer) {
    desc.writable = true;
}

那么 initializer 属性是什么呢?Object.getOwnPropertyDescriptor() 返回的对象并不具有这个属性呀,确实,这是 Babel 的 Class 为了与 decorator 配合而产生的一个属性,比如说对于下面这种代码:

class MyClass {
  @readonly
  born = Date.now();
}

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

var foo = new MyClass();
console.log(foo.born);

Babel 就会编译为:

// ...
(_descriptor = _applyDecoratedDescriptor(_class.prototype, "born", [readonly], {
    configurable: true,
    enumerable: true,
    writable: true,
    initializer: function() {
        return Date.now();
    }
}))
// ...

此时传入 _applyDecoratedDescriptor 函数的 descriptor 就具有 initializer 属性。

第二部分源码解析

接下是应用多个 decorators:

/**
 * 第二部分
 * @type {[type]}
 */
desc = decorators
    .slice()
    .reverse()
    .reduce(function(desc, decorator) {
        return decorator(target, property, desc) || desc;
    }, desc);

对于一个方法应用了多个 decorator,比如:

class MyClass {
  @unenumerable
  @readonly
  method() { }
}

Babel 会编译为:

_applyDecoratedDescriptor(
    _class.prototype,
    "method",
    [unenumerable, readonly],
    Object.getOwnPropertyDescriptor(_class.prototype, "method"),
    _class.prototype
)

在第二部分的源码中,执行了 reverse() 和 reduce() 操作,由此我们也可以发现,如果同一个方法有多个装饰器,会由内向外执行。

第三部分源码解析

/**
 * 第三部分
 * 设置要 decorators 的属性
 */
if (context && desc.initializer !== void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    desc.initializer = undefined;
}

if (desc.initializer === void 0) {
    Object["define" + "Property"](target, property, desc);
    desc = null;
}

return desc;

如果 desc 有 initializer 属性,意味着当装饰的是类的属性时,会将 value 的值设置为:

desc.initializer.call(context)

而 context 的值为 _class.prototype,之所以要 call(context),这也很好理解,因为有可能

class MyClass {
  @readonly
  value = this.getNum() + 1;

  getNum() {
    return 1;
  }
}

最后无论是装饰方法还是属性,都会执行:

Object["define" + "Property"](target, property, desc);

由此可见,装饰方法本质上还是使用 Object.defineProperty() 来实现的。

应用

1.log

为一个方法添加 log 函数,检查输入的参数:

class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

function log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function(...args) {
    console.log(`Calling ${name} with`, args);
    return oldValue.apply(this, args);
  };

  return descriptor;
}

const math = new Math();

// Calling add with [2, 4]
math.add(2, 4);

再完善点:

let log = (type) => {
  return (target, name, descriptor) => {
    const method = descriptor.value;
    descriptor.value =  (...args) => {
      console.info(`(${type}) 正在执行: ${name}(${args}) = ?`);
      let ret;
      try {
        ret = method.apply(target, args);
        console.info(`(${type}) 成功 : ${name}(${args}) => ${ret}`);
      } catch (error) {
        console.error(`(${type}) 失败: ${name}(${args}) => ${error}`);
      }
      return ret;
    }
  }
};

2.autobind

class Person {
  @autobind
  getPerson() {
      return this;
  }
}

let person = new Person();
let { getPerson } = person;

getPerson() === person;
// true

我们很容易想到的一个场景是 React 绑定事件的时候:

class Toggle extends React.Component {

  @autobind
  handleClick() {
      console.log(this)
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        button
      </button>
    );
  }
}

我们来写这样一个 autobind 函数:

const { defineProperty, getPrototypeOf} = Object;

function bind(fn, context) {
  if (fn.bind) {
    return fn.bind(context);
  } else {
    return function __autobind__() {
      return fn.apply(context, arguments);
    };
  }
}

function createDefaultSetter(key) {
  return function set(newValue) {
    Object.defineProperty(this, key, {
      configurable: true,
      writable: true,
      enumerable: true,
      value: newValue
    });

    return newValue;
  };
}

function autobind(target, key, { value: fn, configurable, enumerable }) {
  if (typeof fn !== 'function') {
    throw new SyntaxError(`@autobind can only be used on functions, not: ${fn}`);
  }

  const { constructor } = target;

  return {
    configurable,
    enumerable,

    get() {

      /**
       * 使用这种方式相当于替换了这个函数,所以当比如
       * Class.prototype.hasOwnProperty(key) 的时候,为了正确返回
       * 所以这里做了 this 的判断
       */
      if (this === target) {
        return fn;
      }

      const boundFn = bind(fn, this);

      defineProperty(this, key, {
        configurable: true,
        writable: true,
        enumerable: false,
        value: boundFn
      });

      return boundFn;
    },
    set: createDefaultSetter(key)
  };
}

3.debounce

有的时候,我们需要对执行的方法进行防抖处理:

class Toggle extends React.Component {

  @debounce(500, true)
  handleClick() {
    console.log('toggle')
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        button
      </button>
    );
  }
}

我们来实现一下:

function _debounce(func, wait, immediate) {

    var timeout;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

function debounce(wait, immediate) {
  return function handleDescriptor(target, key, descriptor) {
    const callback = descriptor.value;

    if (typeof callback !== 'function') {
      throw new SyntaxError('Only functions can be debounced');
    }

    var fn = _debounce(callback, wait, immediate)

    return {
      ...descriptor,
      value() {
        fn()
      }
    };
  }
}

4.time

用于统计方法执行的时间:

function time(prefix) {
  let count = 0;
  return function handleDescriptor(target, key, descriptor) {

    const fn = descriptor.value;

    if (prefix == null) {
      prefix = `${target.constructor.name}.${key}`;
    }

    if (typeof fn !== 'function') {
      throw new SyntaxError(`@time can only be used on functions, not: ${fn}`);
    }

    return {
      ...descriptor,
      value() {
        const label = `${prefix}-${count}`;
        count++;
        console.time(label);

        try {
          return fn.apply(this, arguments);
        } finally {
          console.timeEnd(label);
        }
      }
    }
  }
}

5.mixin

用于将对象的方法混入 Class 中:

const SingerMixin = {
  sing(sound) {
    alert(sound);
  }
};

const FlyMixin = {
  // All types of property descriptors are supported
  get speed() {},
  fly() {},
  land() {}
};

@mixin(SingerMixin, FlyMixin)
class Bird {
  singMatingCall() {
    this.sing('tweet tweet');
  }
}

var bird = new Bird();
bird.singMatingCall();
// alerts "tweet tweet"

mixin 的一个简单实现如下:

function mixin(...mixins) {
  return target => {
    if (!mixins.length) {
      throw new SyntaxError(`@mixin() class ${target.name} requires at least one mixin as an argument`);
    }

    for (let i = 0, l = mixins.length; i < l; i++) {
      const descs = Object.getOwnPropertyDescriptors(mixins[i]);
      const keys = Object.getOwnPropertyNames(descs);

      for (let j = 0, k = keys.length; j < k; j++) {
        const key = keys[j];

        if (!target.prototype.hasOwnProperty(key)) {
          Object.defineProperty(target.prototype, key, descs[key]);
        }
      }
    }
  };
}

6.redux

实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

有了装饰器,就可以改写上面的代码。

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {};

相对来说,后一种写法看上去更容易理解。

7.注意

以上我们都是用于修饰类方法,我们获取值的方式为:

const method = descriptor.value;

但是如果我们修饰的是类的实例属性,因为 Babel 的缘故,通过 value 属性并不能获取值,我们可以写成:

const value = descriptor.initializer && descriptor.initializer();

参考

  1. ECMAScript 6 入门
  2. core-decorators
  3. ES7 Decorator 装饰者模式
  4. JS 装饰器(Decorator)场景实战

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。



本文作者:冴羽

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

查看原文

认证与成就

  • 获得 195 次点赞
  • 获得 12 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 10 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-06-04
个人主页被 1.2k 人浏览