云天

云天 查看完整档案

广州编辑  |  填写毕业院校不告诉你  |  前端工程师 编辑 www5826.github.io 编辑
编辑

前端

个人动态

云天 收藏了文章 · 9月2日

Javascript块级域内的函数声明提升

主题

昨天我的一个技术交流群里发了一段代码,涉及的是变量和函数的声明提升,执行结果非常让人迷惑,大家讨论许久,还是有地方解释不清楚,到最后,还是发布到stack overflow,通过大佬解惑才弄明白,这里我再整理一下。

我的思路

逐个解析一下讨论到的案例:

没有特别说明的,运行环境就是是chrome77。
  var a
  if(true) {
    a = 5
    function a() {}
    a = 0
    console.log(a)
  }

  console.log(a)

先整理一下我的思路:

  • 首先,全局有一个变量a声明;
  • 然后,有一个if语句块,是一个块级作用域;
  • 块级作用域里面有变量a的赋值,还声明了一个函数a

按照声明提升原则,函数a声明会被提升,这里没有使用letconst,所以我按ES5之前的声明提升规则去分析的。

没有块级作用域,提升到全局,而且函数声明优先于变量声明,所以会覆盖变量a的声明。

那么答案是:

0
0

很遗憾,这个答案是错的,至少现代浏览器是错的(我后来在IE的模拟环境运行,确实是这个结果)。

现在,公布一下正确答案

0
5

这个答案非常让人迷惑不解:

  • 第一个输出0,好理解
  • 第二个输出5,到底是怎么回事?

    • 如果理解成:let a = function(){},那么a=5是暂时性死区;
    • 如果理解成:var a = function(){},那么会和上面一样,输出0 0

大佬的解析

先看看代码运行解析[引用1],我们可以把代码运行看成这样:

var a¹;
 if (true) {
   function a²() {} // hoisted
   a² = 5;
   a¹ = a²; // at the location of the declaration, the variable leaves the block      
   a² = 0;
  console.log(a²)
}
console.log(a¹);

解释一下:

  • 全局的变量a声明,这里没有问题。
  • 重点1,块级域的函数a声明提升,提升到块级域顶部
  • 重点2a=5,这里赋值的对象是本地的函数a,覆盖了。
  • 重点中的重点,执行到函数a声明处,本地的变量a覆盖了全局的变量a。按照[引用2]解释,这里也是提升,也就是说函数a声明提升了两次。到这里疑惑终于解开了!

总结

这里要说明两点:

  • 块级作用域{}内的函数声明,在ES5中是非法的,在ES6中并没有严格规定(考虑到向下兼容性,参考阮老师的ES6入门),也就是这块依赖于实现[引用3]。
  • 在项目里面,不要在块级作用域{}内声明函数,要在全局作用域或者函数作用域的顶层声明函数。

引用参考

  1. [confused about function declaration in { }

](https://stackoverflow.com/que...

  1. [What are the precise semantics of block-level functions in ES6?

](https://stackoverflow.com/que...

  1. web legacy compatibility semantics
查看原文

云天 发布了文章 · 9月2日

事件合成知多少

ps: 本文首发于公众号 话说前端

在目前的项目开发中,由于使用的 angular 1.x 来进行应用层面的开发,那么在处理事件的时候就不得不使用框架自身所带的 ng-click指令来进行事件的绑定,使用 ng-click 进行绑定的话,对于长列表,交互事件多的地方来说,不是一个好的选择。因为会绑定很多的事件。于是就寻求以一种合适的事件代理的方式来尝试替代(这里只讨论 click)。所以来做个总结

要完成这件事,不妨先回顾一下事件流的一些知识

如上图所示,当你点击页面上的元素的时候,事件会依次到达这三个阶段,它们分别是:

    事件捕获阶段 -> 目标阶段 -> 冒泡阶段。

单拿出冒泡阶段来说,意味着当你点击了某一个具体的元素后,那么最后你是可以在document这个层级接收到事件的,这就构成了代理的基础。

接下来就要考虑的问题是:

  1. 我在根节点上的监听方法如何响应组件里面定义的回调函数
  2. 我们的写法如何与 ng-click 保持一致。
  3. 实现事件合成的冒泡与阻止冒泡

先来看正常的 ng-click 指令是如何使用的,如下:

<example
  ng-click="test($event,item)"
></example>

所以只需要实现一个指令,接收一个函数即可(这里只讨论属性为函数的场景)。

回调函数的查找

const clickDirective = function () {
  return {
    restrict: 'A',
    link(scope, element, attr) {
      try {
        if (!attr.mmClick) return;
        const { fn, functionName, paramsArr } = service.parseParams(attr.mmClick, scope);
        // eslint-disable-next-line
        element[0][`rcs-${functionName}`] = { fn, paramsArr };
      } catch (e) {
        console.error(e);
      }
    },
  };
};

如上,我们可以在指令的初始化中,将我们获取到的回调函数以及参数以特定名称的方式挂载到 dom 树上
这里以 rcs 字符开头,这样就能在根节点接收的事件中去寻找到这个回调函数并处理

function executeEvent(target, e) {
  const keys = Object.keys(target);
  for (let i = 0; i < keys.length; i++) {
    if (keys[i].indexOf('rcs') === 0) {
      const { fn, paramsArr } = target[keys[i]];
      fn.call(this, ...paramsArr, { e });
    }
    // 处理是否需要冒泡
    if (e.hasOwnProperty('stopBubble') && e.stopBubble) {
      return;
    }
  }
  if (target.parentNode !== null) {
    return executeEvent(target.parentNode, e);
  }
}

可以看到,上面的函数是一种递归的方式,逐级寻找回调函数,并执行它。这样就完成了函数的定义与执行部分。

合成事件的冒泡与阻止冒泡

与此同时,在一个dom层级上,可以在不同的层级绑定事件,这样在冒泡阶段,会依次执行。那么在如上的函数中也体现了这点。

如上的函数,会把事件对象通过参数的形式给到调用方(别问为啥在最后,因为要兼容已有函数定义),

fn.call(this, ...paramsArr, { e });

在回调函数里,只需要设置这个值即可:

$scope.showGroupCard = function (item, chatInfoType, { e }) {
  e.stopBubble = true;
}

当这个值为 true 的时候,那么 executeEvent 就会终止执行,否则会直到 parentNodenull

最后再来看看使用效果:

<example
  mm-click="test(item)"
></example>

其实到这里,这个事件合成简易版就算完成了。虽然是利用的dom自身的层次性,但查找速度完全不用担心。

另外就是关于回调函数里面的 this 的问题,回调函数需要使用箭头函数来穿透才行,问题不大

函数参数解析

其实这里最难的部分在于指令的函数解析上,一个函数它的参数可以有如下几种(可能我还没罗列完):

  • 简单的基本数据类型
  • 变量(这个变量可能来自自己组件的作用域,来自父组件传递下来的作用域,也可能来自根作用域)
  • 参数是一个函数,或者是一个递归函数
  • 参数是一个对象,对象里面有变量
  • 等等

所以基于此,再结合实际的业务场景,约定了如下:

  • 指令的属性只能是一个函数,不接受其他值
  • 只保留了当前作用域变量,当前变量的调用,舍弃了表达式等写法,
  • 函数参数只是当前组件作用域及作用域链上的,如果需要使用表达式或者其他写法,就把它挂载到当前作用域上。

end

万物起于微忽,量变引起质变

查看原文

赞 0 收藏 0 评论 0

云天 赞了回答 · 2019-06-09

解决前端开发有没有好用的日志框架

我的理解是,在开发环境正常打印log,但是在生产环境希望控制台不要打印log。
使用webpack + TerserPlugin 可以

optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
              warnings: false,
              drop_console: true,
              drop_debugger: true,
              pure_funcs: ['console.log']
          },
        },
      }),
    ],
  },
 

关注 4 回答 3

云天 发布了文章 · 2019-01-14

关于JavaScript运行机制的一些总结

javascript运行机制之前,先看一段代码:

console.log(1)

Promise.resolve().then(function () {
   console.log(2)
})

new Promise(function(resolve, reject){
    console.log(3)
    resolve()
}).then(function () {
    console.log(4)
    setTimeout(function () {
        console.log(5)
    })
})

console.log(6)

setTimeout(function () {
    Promise.resolve().then(function () {
        console.log(7)
        setTimeout(function () {
            console.log(8)
        })
    })
})

如果你看到这段代码,并知道正确的输出顺序。那说明你对这块掌握的差不多了。(直接翻到最后看结果)

好,言归正传。

在说是怎么运行的之前,先看几个概念。

执行上下文(Execution Context)

执行上下文简单来说就是一个执行环境。它有全局环境、函数环境和eval函数环境之分。它会在javascript引擎执行你的脚本的时候去创建。

执行栈(Execution Stack)

执行栈也就是常说的调用栈,它是一种拥有LIFO(后进先出)的数据结构。它会存储代码运行时创建的执行上下文

微任务(micro task)与宏任务(macro task)

javasript中的任务分为微任务和宏任务两种,这两种任务的执行时机是不同的,因此区分js中哪些是宏任务,哪些是微任务则十分重要。我们常见的宏任务有:script任务setTimeoutajax等,常见的微任务比较典型的是:Promise.resolve().then()process.nextTickMutationObserver等。

事件循环(event loop)

js是单线程的,也就是说它一次仅能处理一个任务。但js所在的宿主环境,也就是我们所说的浏览器并不是单线程的(这里宿主环境仅讨论浏览器)。它在遇到一些任务时,比如说setTimeoutevent listener等。它会告诉浏览器:老兄帮个忙,事成后通知我一声,小弟我先干别的事去了。浏览器会回应说:交给我吧,小老弟,事成后我放到任务队列,自己去取啊。于是,js开始执行script任务,执行完了就开始检查有没有微任务啊,没有的话就从任务队列开始取宏任务执行,每执行完一次宏任务,就去看看有没有微任务,有的话就执行完成,再执行宏任务,如此往复。如下:

clipboard.png

了解了这几个概念,再来看看javascript是怎么执行代码的就比较轻松愉快了。开始吧

console.log(1)

Promise.resolve().then(function () {
   console.log(2)
})

new Promise(function(resolve, reject){
    console.log(3)
    resolve()
}).then(function () {
    console.log(4)
    setTimeout(function () {
        console.log(5)
    })
})

console.log(6)

setTimeout(function () {
    Promise.resolve().then(function () {
        console.log(7)
        setTimeout(function () {
            console.log(8)
        })
    })
})

js引擎在执行这段代码的时候,首先将全局执行上下文压入栈中:

clipboard.png

然后呢,在执行的时候会碰到console.log函数,将它压入栈中:

clipboard.png

这个时候,直接执行console函数,并输出1。然后console函数出栈:

clipboard.png

继续往下执行,碰到了Promise.resolve().then(),先将Promise.resolve().then()压入栈中(这里,我为了图方便就把它看成整体了,不然得画很多图)。
clipboard.png
然后执行Promise.resolve().then(),前面说过,这个then()函数是个微任务,它会将传入给它的回调函数加入到微任务队列中。

clipboard.png

然后Promise.resolve().then()就出栈了。

接着执行,遇到promise的构造函数,这个构造函数是一个宏任务,会直接将传递给它的函数压入栈中。

clipboard.png
执行console函数并输出3,执行完,console函数出栈,接着执行resolve()函数,并出栈。
然后继续执行then函数,将传递给then函数的参数函数放到微任务队列中:

clipboard.png

继续来,继续往下执行。碰到console.log(6),二话不说,直接压入栈中,执行,输出6,出栈,一气呵成。

接着,引擎碰到了setTimeout函数,这家伙是个宏任务,但同时它会将传递给它的函数,加入到任务队列中:

clipboard.png
好了,到此第一波宏任务就全部执行完毕。接着,引擎就会去看一下微任务队列中有没有任务,如果有的话,执行它们。

现在看到的是,微任务队列中有两个任务。按照队列的先入先出规则,先从function () {console.log(2)}开始执行。先是函数入栈,然后执行函数,输出2,然后函数出栈。

接着执行下面这段代码:

console.log(4)
setTimeout(function () {
   console.log(5)
})

先从console.log(4)开始,先将它入栈,然后执行它,输出4,然后函数出栈。

接着执行:

setTimeout(function () {
   console.log(5)
})

function () {
    console.log(5)
}

加入到任务队列中去

clipboard.png

先执行:

function(){
Promise.resolve().then(function () {
        console.log(7)
        setTimeout(function () {
            console.log(8)
        })
    })
}

这里执行这个函数的时候遇到一个微任务,将这个微任务添加到微任务队列,如下:

clipboard.png

这批次的宏任务就执行完毕了,接着就回去检查微任务队列中有没有待执行的任务。一看还真有两个小可爱等待执行,于是没什么好说的,直接拧出去就执行

先是执行console.log(7),然后输出7。接着执行setTimeout,将传递给他的任务添加到任务队列中去:

clipboard.png

最后就剩这两个函数了,按照队列的先入后出一次执行吧,输出58

好了,最后的结果就是1,3,6,2,4,7,5,8。你写对了了吗?

查看原文

赞 1 收藏 0 评论 0

云天 关注了专栏 · 2018-12-16

腾讯Bugly

Bugly是腾讯内部产品质量监控平台的外发版本,其主要功能是App发布以后,对用户侧发生的crash以及卡顿现象进行监控并上报,让开发同学可以第一时间了解到app的质量情况,及时机型修改。

关注 294

云天 收藏了文章 · 2018-12-10

有赞零售小票打印跨平台解决方案

作者:王前、林昊(鱼干)

一、背景

零售商家的日常经营中,小票打印的场景无处不在,顾客的每笔消费都会收到商家打印出的消费小票,这个是顾客的消费凭证,所以小票的内容对顾客和商家都尤为重要。对于有赞零售应用软件来说,小票打印功能也是必不可少的,诸多业务场景都需要提供相应的小票打印能力。

  • 打印需求端

图片描述

  • 小票业务场景

图片描述

  • 小票打印机设备类型

图片描述

过去我们存在的痛点:

  1. 每个端各自实现一套打印流程,方案不统一。导致每次修改都会三端修改,而且 iOS 和 Android 必须依赖发版才可上线,不具有动态性,而且研发效率比较低。
  2. 打印小票的业务场景比较多,每个业务都自己实现模板封装及打印逻辑,模板及逻辑不统一,维护成本大。
  3. 多种小票设备的适配,对于每个端来说都要适配一遍。

<font color=red>其中最主要的痛点还是在于第一点,多端的不统一问题。由于不统一,导致开发和维护的成本成倍级增长。</font>

针对以上痛点,小票打印技术方案需要解决的三个主要问题:

  1. iOS 、安卓和网页端的零售软件都需要提供小票样式设置和打印的能力,如何降低小票打印代码的维护和更新成本。
  2. 如何定制显示不同业务场景的小票内容:不同业务场景下的小票信息都不尽相同,比如购物小票和退款小票,商品信息的样式是一样的,但是支付信息是不一样的,购物小票应当显示顾客的支付信息,退款小票显示商家退款信息。
  3. 如何更灵活的适配多种多样的小票打印机,从连接方式上分为蓝牙连接和 WIFI 连接,从纸张样式分为 80mm 和 58mm 两种宽度。

二、整体解决方案

针对以上三个问题,我们提出了一个涉及前端、移动端和服务端的跨平台解决方案,

  • 架构图

图片描述

架构设计的核心在于通过 JS 实现支持跨平台的小票解析脚本,并具有动态更新的优势;通过服务端下发可编辑的样式模板实现小票内容的灵活定制;客户端启动 JS 执行器执行 JS 小票脚本引擎(以下简称:JS 引擎)并负责打印机设备的连接管理。

1 、JS 引擎设计

JS 引擎主要能力就是处理小票模版和业务数据,将业务数据整合到模版中(处理不了的交给移动端处理,比如图片),然后将整合模版数据转换成打印指令返给移动端。

  • 整体处理流程图

图片描述

  • 结构设计

图片描述

* 小票格式中,打印机是一行一行的输出。那么基本输出布局单位,我们定义为 layout
* 默认一行有一个内容块,即一个 layout 里面有一个 content object
* 当一行有多列内容的时候,即一个 layout 里面包含 N 个 content object 。 各自内容块有 pagerWeight 代表每个内容的宽度占比
* 每一行的后面的是一个占位符,用数据模型的 key 做占位

小票 layout 样式描述:

图片描述

content block 内容块:

图片描述

不同类型内容所支持的能力:

图片描述

  • 模版编译

这里使用了 HandleBars.js 作为模板编译的库。此外,目前还额外提供了部分能力支持。

自定义能力:

图片描述

  • 打印机设备适配

主要进行适配指令集解析适配,根据连接不同设备进行不同指令解析。目前已适配设备:365wifi 、 sunmi 、 sprt80 、 sprt58 、 wangpos 、 aclas 、 xprinter 。如果连接未适配的设备抛出找不到相应打印机解析器 error。

  • 调用对应打印机的 parser 指令解析流程

图片描述

  • 兼容性问题

    • 切纸:支持外部传入是否需要切纸,防止外部发送打印指令时加入切纸指令后重复切纸问题,默认加切纸指令。
    • 一机多尺寸打印:存在一台打印机支持两种纸张打印( 80mm 、 58mm ),这时需要从外部传入打印尺寸,默认 80mm 。比如,sunmiT1 支持 80mm 和 58mm 打印,默认是 80mm 。
  • 容错处理

    • 由于模版解析有一定格式要求,所以一些特殊字符及转移字符存在数据中会存在解析错误。所以 JS 在传入数据时,做了一层过滤,将 "\\" 、 "n" 、 "b" ... 等字符去掉或替换,保证打印。
    • 如果在解析过程中存在错误,将抛出异常给移动端捕获。

2 、模板管理服务

小票模板的动态编辑和下发,模版动态配置信息存储和各业务全量模版存储,提供移动端动态配置信息接口,拉取业务小票模版接口,各业务方业务数据接口。

  • 整体处理流程图

图片描述

  • 小票基础模版库存储示例

图片描述

shopId:店铺 ID

business:业务方

type:打印内容类型

content:layout 中 content 内容

sortWeight:排序比重,用于输出模板 layout 顺序

  • 动态设置数据存储示例

图片描述

shopId:店铺 ID

business:业务方

type:打印内容类型

params:需要替换填充的内容

  • 接口返回整合后的小票模版 json
{
    "business": "shopping",
    "shopId": 111111,
    "id": 321,
    "version": 0,
    "layouts": [{
                "name": "LOGO",
                "content": "[{\"content\":\"http://www.test.com/test.jpg\",\"contentType\":\"image\",\"textAlign\":\"center\",\"width\":45}]"
                },{
                "name": "电话",
                "content": "[{\"content\":\"电话:{{mobile}}\",\"contentType\":\"text\",\"textAlign\":\"left\",\"fontSize\":\"default\",\"pagerWeight\":1}]"
                },...]
}

其中相关动态数据后端已经做过整合替换,需要替换的业务数据保留在模板 json 中,等获取业务数据后由 JS 引擎进行替换。
上面 json 中 http://www.test.com/test.jpg 就是动态整合替换数据,{{mobile}} 是一个需要替换的业务数据。

3 、移动端

移动端除了动态模版配置之外,主要的就是打印流程。移动端只需要关心需要打印什么业务小票,然后去后端拉取业务小票模版和业务数据,将拉取到的数据传给 JS 引擎进行预处理,返回模版中处理不了的图片 url 信息,然后移动端进行下载图片,进行二值转换,输出像素的 16 进制字符串,替换原来模版中的 url ,最后将连接的打印机类型和处理后的模版传给 JS 引擎进行打印指令转换返回给打印机打印。

  • 动态模版配置

图片描述

动态配置小票内容,支持 LOGO 、店铺数据、营销活动配置等。左侧为在 80mm 和 58mm 上预览样式。通过动态配置模版,实现后端接口模版更新,然后可以实时同步修改打印内容。网页零售软件上动态配置内容和移动端一样。

  • 打印业务流程

图片描述

该业务流程,移动端完全脱离数据,只需要做一些额外能力以及传输功能,有效解决了业务数据修改依赖移动端发版的问题。 Android 和 iOS 流程统一。

三、移动端功能设计

1 、动态化

动态化在本解决方案里是必不可少的一环,实时更新业务数据模板依赖于后端,但是 JS 解析引擎的下发要依靠移动端来实现,为了及时修复发现的 JS 问题或者快速适配新设备等功能。更新流程图如下:

图片描述

这里说明一下,因为可能会出现执行 JS 的过程中,正在执行本地 JS 文件更新,导致执行 JS 出错。所以在完成本地更新后会发送一个通知,告知业务方 JS 已更新完成,这时业务方可根据自身需求做逻辑处理,比如重新加载 JS 进行处理业务。

2 、JS 执行器

iOS 使用 JavaScriptCore 框架,Android 使用 J2V8 框架,具体框架的介绍这里就不说明了。JS 执行器设计包含加载指定 JS 文件,调用 JS 方法,获取 JS 属性,JS 异常捕获。

    /**
     初始化 JSExecutor

     @param fileName JS 文件名
     @return JSExecutor
     */
    - (instancetype)initWithScriptFile:(NSString *)fileName;

    /**
     加载 JS 文件

     @param fileName JS 文件名
     */
    - (void)loadSriptFile:(NSString *)fileName;

    /**
     执行 JS 方法

     @param functionName 方法名
     @param args 入参
     @return 方法返回值
     */
    - (JSValue *)runJSFunction:(NSString *)functionName args:(NSArray *)args;

    /**
     获取 JS 属性

     @param propertyName 属性名
     @return 属性值
     */
    - (JSValue *)getJSProperty:(NSString *)propertyName;

    /**
     JS 异常捕获

     @param handler 异常捕获回调
     */
    - (void)catchExceptionWithHandler:(JSExceptionHandler)handler;

加载 JS 文件方法,可以加载动态下发的 JS 。逻辑是先判断本地下发的文件是否存在,如果存在就加载下发 JS ,否则加载 app 中 bundle 里面的 JS 文件。

    - (void)loadSriptFile:(NSString *)fileName{
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        if (paths.count > 0) {
            NSString *docDir = [paths objectAtIndex:0];
            NSString *docSourcePath = [docDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.js", fileName]];
            NSFileManager *fm = [NSFileManager defaultManager];
            if ([fm fileExistsAtPath:docSourcePath]) {
                NSString *jsString = [NSString stringWithContentsOfFile:docSourcePath encoding:NSUTF8StringEncoding error:nil];
                [self.content evaluateScript:jsString];
                return;
            }
        }
        NSString *sourcePath = [[YZCommonBundle bundle] pathForResource:fileName ofType:@"js"];
        NSAssert(sourcePath, @"can't find jscript file");
        NSString *jsString = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
        [self.content evaluateScript:jsString];
    }

这时候可能会有人疑问,为什么这里是直接强制加载本地下发 JS ,而不是对比版本优先加载。这里主要有两点原因:

  • 动态下发 JS 文件,就是为了补丁或者优化更新,所以一般新版本下发配置不会存在
  • 为了支持 JS 版本回滚

JS 异常捕获功能,将异常抛出给业务方,可以让调用者各自实现逻辑处理。

3 、缓存优化

由于模板和数据都在后端,需要拉取两次接口进行打印,所以需要提供一套缓存机制来提高打印体验。由于业务数据需要实时拉取,所以必须走接口,模板相对于业务数据来说,可以允许一定的延迟。所以,模板采用本地文件缓存,业务数据采用和业务打印页面挂钩的内存缓存,业务数据只需要第一次打印是请求接口,重新打印直接使用。

流程图:
图片描述

本缓方案存会存在偶现的模板不同步问题,在即将打印时,如果网页后台修改了模板,就会出现本次打印模板不是最新的,但是在下一次打印时就会是最新的了。由于出现的几率比较低,模板也允许有一点延迟,所以不会影响整体流程。

对于离线场景,我们在 app 中存放一个最小可用模板,专门用于离线下小票打印使用。为什么是最小可用模板,因为离线下,业务数据及一些其他数据有可能不全,所以最小可用模板可以保证打印出来的数据准确性。

4 、图片处理

由于 JS 引擎是不能解析图片文件的,所以在最初模板中存在图片链接时,全部由移动端进行处理,然后进行替换。图片处理主要就是下载图片,图片压缩,二值图处理,图片像素点压缩(打印指令要求),每个字节转换成 16 进制,拼接 16 进制字符串。

  • 下载图片

采用 SDWebImage 进行下载缓存,创建并行队列进行多图片下载,每下载成功一张后回到主线程进行后续的相关处理。所有图片都处理完成或,回调给 JS 引擎进行指令解析。

  • 图片压缩

根据 JS 引擎模板要求的 width(必须是 8 的倍数,后续说明),进行等比例压缩,转换成 jpg 格式,过滤掉 alpha 通道。

  • 二值图处理

遍历每一个像素点,进行 RGB 取值,然后算出 RGB 均值与 255 的比值,根据比值进行取值 0 或 255 。这里没有使用直方图寻找阈值 T 的方式进行处理,是出于性能和时间考虑。

  • 像素点压缩

由于打印机指令要求,需要对转换成二值后的每个点进行 width 上压缩,需要将 8 个字节压缩到 1 个字节,这里也是为什么图片压缩时 width 必须是 8 的倍数的原因,否则打印出来的图片会错位。

图片描述

  • 16 进制字符串

因为打印机打印图片接收的是 16 进制字符串,所以需要将处理后的每个字节转换成 16 进制字符,然后拼成一个字符串。

5 、实现多次打印

由于业务场景需要,需要自动打印多张小票,所以设计了多次打印逻辑。由于每次打印都是异步线程中,所以不可以直接循环打印,这里使用信号量 dispatch_semaphore_t ,在异步线程中创建和 wait 信号量,每次打印完成回调线程中 signal 信号量,实现多次打印,保证每次打印依次进行。如果中途打印出错,则终止后续打印。

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        for (int i = 1; i <= printCount; i++) {
            if (stop) {
                break;
            }
            [self print:template andCompletionBlock:^(State state, NSString *errorStr) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (errorStr.length > 0 || i == printCount) {
                        if (completion) {
                            completion(state, errorStr);
                        }
                        stop = YES;
                    }
                    dispatch_semaphore_signal(semaphore);
                });
            }];
            dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 15*NSEC_PER_SEC));
        }
    });

四、总结与展望

本方案已经实施,在零售 app 中使用来看,已经满足目前大部分业务场景及需求,后续的开发及维护成本也会大幅度降低,提高了研发效率,接入新业务小票也比较方便。客户使用上来说,使用体验和以前没有较大差别,同时在处理客户反映的问题来说,也可以做到快速修改,实时下发等。不过目前还存在一些不足点,比如说图片打印的功能,还不能完全满足所有图片都做到完美打印,毕竟图片处理考虑到性能体验方面;还有模板后续可以增加版本号,这样在模板存在异常时也可以回滚或兼容处理等;再者就是缓存优化可以后续进一步优化体验,比如加入模板推送,本地缓存优化等。

参考链接

图片描述

查看原文

云天 回答了问题 · 2018-12-08

解决前端实现局部刷新分页,但浏览器刷新后总是会回到第一页

其实你这个做一个store本地化就可以了。原因是你刷新的时候,store会被初始化,因为它存在内存中

关注 6 回答 5

云天 收藏了文章 · 2018-10-21

浅说虚拟列表的实现原理

列表数据的展示优化 一文中,提到了对于列表形态的数据展示的按需渲染。这种方式是指根据容器元素的高度以及列表项元素的高度来显示长列表数据中的某一个部分,而不是去完整地渲染长列表,以提高无限滚动的性能。而按需显示方案的实现就是本文标题中说的虚拟列表。

虚拟列表的实现有多种方案,本文以 react-virtual-list 组件为基础进行分析。原文链接:https://github.com/dwqs/blog/...

什么是虚拟列表?

在正文之前,先对虚拟列表做个简单的定义。

根据上文,虚拟列表是按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。

简而言之,虚拟列表指的就是「可视区域渲染」的列表。有三个概念需要了解一下:

  • 滚动容器元素:一般情况下,滚动容器元素是 window 对象。然而,我们可以通过布局的方式,在某个页面中任意指定一个或者多个滚动容器元素。只要某个元素能在内部产生横向或者纵向的滚动,那这个元素就是滚动容器元素考虑每个列表项只是渲染一些纯文本。在本文中,只讨论元素的纵向滚动。
  • 可滚动区域:滚动容器元素的内部内容区域。假设有 100 条数据,每个列表项的高度是 50,那么可滚动的区域的高度就是 100 * 50。可滚动区域当前的具体高度值一般可以通过(滚动容器)元素的 scrollHeight 属性获取。用户可以通过滚动来改变列表在可视区域的显示部分。
  • 可视区域:滚动容器元素的视觉可见区域。如果容器元素是 window 对象,可视区域就是浏览器的视口大小(即视觉视口);如果容器元素是某个 div 元素,其高度是 300,右侧有纵向滚动条可以滚动,那么视觉可见的区域就是可视区域。

实现虚拟列表就是在处理用户滚动时,要改变列表在可视区域的渲染部分,其具体步骤如下:

  • 计算当前可见区域起始数据的 startIndex
  • 计算当前可见区域结束数据的 endIndex
  • 计算当前可见区域的数据,并渲染到页面中
  • 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上
  • 计算 endIndex 对应的数据相对于可滚动区域最底部的偏移位置 endOffset,并设置到列表上

建议参考下图理解一下上面的步骤:

步骤图

元素 L 代指当前列表中的最后一个元素

从上图可以看出,startOffsetendOffset 会撑开容器元素的内容高度,让其可持续的滚动;此外,还能保持滚动条处于一个正确的位置。

为什么需要虚拟列表?

虚拟列表是对长列表的一种优化方案。在前端开发中,会碰到一些不能使用分页方式来加载列表数据的业务形态,我们称这种列表叫做长列表。比如,在一些外汇交易系统中,前端会准实时的展示用户的持仓情况(收益、亏损、手数等),此时对于用户的持仓列表一般是不能分页的。

在本篇文章中,我们把长列表定义成数据长度大于 999,并且不能使用分页的形式来展示的列表。

如果对长列表不作优化,完整地渲染一个长列表,到底需要多长时间呢?接下来会写一个简单的 demo 来测试以下。

本文 demo 的测试环境:Macbook Pro(Core i7 2.2G, 16G), Chrome 69,React 16.4.1

在 demo 中,我们先测一下浏览器渲染 10000 个简单的节点需要多长时间:

import React from 'react'

const count = 10000

function createMarkup (doms) {
  return doms.length ? { __html: doms.join(' ') } : { __html: '' }
}

export default class DOM extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      simpleDOMs: []
    }

    this.onCreateSimpleDOMs = this.onCreateSimpleDOMs.bind(this)
  }

  onCreateSimpleDOMs () {
    const array = []

    for (var i = 0; i < count; i++) {
      array.push('<div>' + i + '</div>')
    }

    this.setState({
      simpleDOMs: array
    })
  }

  render () {
    return (
      <div style={{ marginLeft: '10px' }}>
        <h3>Creat large of DOMs:</h3>
        <button onClick={this.onCreateSimpleDOMs}>Create Simple DOMs</button>
        <div dangerouslySetInnerHTML={createMarkup(this.state.simpleDOMs)} />
      </div>
    )
  }
}

当点击 Button 时,会调用 onCreateSimpleDOMs 创建 10000 个简单节点。从 Chrome 的 Performance 标签页看到的数据如下:

simple doms

从上图可以看到,从 Event Click 到 Paint,总共用了大约 693ms,渲染时的主要时间消耗情况如下:

  • Recalculate Style:40.80ms
  • Layout:518.55ms
  • Update Layer Tree:11.84ms
在 Recalculate Style 和 Layout 阶段,ReactDOM 调用了 setInnerHTML 方法,其内部主要通过 innerHTML 方法,将创建好的 html 片段添加到对应节点

然后,我们创建 10000 个稍微复杂点的节点。修改组件如下:

import React from 'react'

function createMarkup (doms) {
  return doms.length ? { __html: doms.join(' ') } : { __html: '' }
}

export default class DOM extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      complexDOMs: []
    }

    this.onCreateComplexDOMs = this.onCreateComplexDOMs.bind(this)
  }

  onCreateComplexDOMs () {
    const array = []
    for (var i = 0; i < 5000; i++) {
      array.push(`
        <div class='list-item'>
          <p>#${i} eligendi voluptatem quisquam</p>
          <p>Modi autem fugiat maiores. Doloremque est sed quis qui nobis. Accusamus dolorem aspernatur sed rem.</p>
        </div>
      `)
    }

    this.setState({
      complexDOMs: array
    })
  }

  render () {
    return (
      <div style={{ marginLeft: '10px' }}>
        <h3>Creat large of DOMs:</h3>
        <button onClick={this.onCreateComplexDOMs}>Create Complex DOMs</button>
        <div dangerouslySetInnerHTML={createMarkup(this.state.complexDOMs)} />
      </div>
    )
  }
}

当点击 Button 时,会调用 onCreateComplexDOMs。从 Chrome 的 Performance 标签页看到的数据如下:

complex doms

从上图可以看到,从 Event Click 到 Paint,总共用了大约 964.2ms,渲染时的主要时间消耗情况如下:

  • Recalculate Style:117.07ms
  • Layout:538.00ms
  • Update Layer Tree:31.15ms

对于上述测试各进行 5 次,然后取各指标的平均值,统计结果如下:

-Recalculate StyleLayoutUpdate Layer TreeTotal
渲染简单节点199.66ms523.72ms12.572ms735.952ms
渲染复杂节点114.684ms806.05ms31.328ms952.512ms
  1. Total = Recalculate Style + Layout + Update Layer Tree
  2. demo 的测试代码:test code

从上面的测试结果中可以看到,渲染 10000 个节点就需要 700ms+,实际业务中的列表每个节点都需要 20 个左右的节点,布局也会复杂很多,在 Recalculate Style 和 Layout 阶段也会耗费更长的时间。那么,700ms 也仅能渲染 300 ~ 500 个左右的列表项,所以完整的长列表渲染基本上很难达到业务上的要求的。而非完整的长列表渲染一般有两种方式:按需渲染和延迟渲染(即懒渲染)。常见的无限滚动便是延迟渲染的一种实现,而虚拟列表则是按需渲染的一种实现。

延迟渲染不在本文讨论范围。接下来,本文会简单介绍虚拟列表的一种实现方案。

实现

本章节将会创建一个 VirtualizedList 组件,并结合代码,慢慢梳理虚拟列表的实现。

为了简化,我们设定 window 为滚动容器元素,给 htmlbody 元素均添加样式规则 height: 100%,设定可视区域为浏览器的窗口大小。VirtualizedList 在 DOM 元素的布局上将参考Twitter 的移动端

class VirtualizedList extends Component {
  constructor (props) {
    super(props)
    
    this.state = {
      startOffset: 0,
      endOffset: 0,
      visibleData: []
    }
    
    this.data = new Array(1000).fill(true)
    this.startIndex = 0
    this.endIndex = 0
    this.scrollTop = 0
  }
  
  render () {
    const {startOffset, endOffset} = this.state
    
    return (
      <div className='wrapper'>
        <div style={{ paddingTop: `${startOffset}px`, paddingBottom: `${endOffset}px` }}>
          {
            // render list
          }
        </div>
      </div>
    )
  }
}

在虚拟列表上的实现上,也分为两种情形:列表项是固定高度的和列表项是动态高度的。

列表项是固定高度的

既然列表项是固定高度的,那约定没个列表项的高度为 60,列表数据的长度为 1000。

首先,我们根据可视区域的高度估算可视区域能渲染的元素个数:

const height = 60
const bufferSize = 5
// ...

this.visibleCount = Math.ceil(window.clientHeight / height)

然后,计算 startIndexendIndex,并先初始化初次需要渲染的数据:

// ...

updateVisibleData (scrollTop) {
  const visibleData = this.data.slice(this.startIndex, this.endIndex)
  const endOffset = (this.data.length - this.endIndex) * height
    
  this.setState({
    startOffset: 0,
    endOffset,
    visibleData
  })
}

componentDidMount () {
  // 计算可渲染的元素个数
  this.visibleCount = Math.ceil(window.innerHeight / height) + bufferSize
  this.endIndex = this.startIndex + this.visibleCount
  this.updateVisibleData()
}

如上文所说,endOffset 是计算 endIndex 对应的数据相对于可滚动区域底部的偏移位置。在本 demo 中,可滚动区域的高度就是 1000 60,因而 endIndex 对应的数据相距底部的偏移就是 (1000 - endIndex) 60。

由于是初始化初次需要渲染的数据,因而 startOffset 的初始值是 0。

根据上述代码,可以得知,要计算可见区域需要渲染的数据,只要计算出 startIndex 就行,因为 visibleCount 是一个定值,bufferSize 是一个缓冲值,用来增加一定的缓存区域,让正常滑动速度的时候不会显得那么突兀。而 endIndex 的值就等于 startIndex 加上 visibleCount;同时,当用户滚动改变可见区域的数据时,还需要计算 startOffset 的值,以保证新的数据会出现在用户浏览器的视口中:

startOffset

如果不计算 startOffset 的值,那本应该渲染在可视区域内的元素会渲染到可视区域之外。从上图可以看到,startOffset 的值就是元素8的上边框 (可视区域内最上面一个元素) 到元素1的上边框的偏移量。元素8称为 锚点元素,即可视区域内的第一个元素。 因而,我们需要定义一个变量来缓存锚点元素的一些位置信息,同时也要缓存已渲染的元素的位置信息:

// ...
// 缓存已渲染元素的位置信息
this.cache = []
// 缓存锚点元素的位置信息
this.anchorItem = {
  index: 0, // 锚点元素的索引值
  top: 0, // 锚点元素的顶部距离第一个元素的顶部的偏移量(即 startOffset)
  bottom: 0 // 锚点元素的底部距离第一个元素的顶部的偏移量
}
// ...

cachePosition (node, index) {
  const rect = node.getBoundingClientRect()
  const top = rect.top + window.pageYOffset
  
  this.cache.push({
    index,
    top,
    bottom: top + height
  })
}

// ...

方法 cachePosition 会在每个列表项组件渲染完后(componentDidMount)进行调用,node 是对应的列表项节点元素,index 是节点的索引值:

// Item.jsx

// ...
componentDidMount () {
  this.props.cachePosition(this.node, this.props.index)
}

render () {
  /* eslint-disable-next-line */
  const {index} = this.props

  return (
    <div className='list-item' ref={node => { this.node = node }}>
      <p>#${index} eligendi voluptatem quisquam</p>
      <p>Modi autem fugiat maiores. Doloremque est sed quis qui nobis. Accusamus dolorem aspernatur sed rem.</p>
    </div>
  )
}
// ...

缓存了锚点元素和已渲染元素的位置信息之后,接下来就可以处理用户的滚动行为了。以用户向下滚动(scrollTop 值增大的方向)为例:

// ...
// 计算 startIndex 和 endIndex
updateBoundaryIndex (scrollTop) {
  scrollTop = scrollTop || 0
  //用户正常滚动下,根据 scrollTop 找到新的锚点元素位置
  const anchorItem = this.cache.find(item => item.bottom >= scrollTop)

  this.anchorItem = {
    ...anchorItem
  }

  this.startIndex = this.anchorItem.index
  this.endIndex = this.startIndex + this.visibleCount
}

// 滚动事件处理函数
handleScroll (e) {
  if (!this.doc) {
    // 兼容 iOS Safari/Webview
    this.doc = window.document.body.scrollTop ? window.document.body : window.document.documentElement
  }

  const scrollTop = this.doc.scrollTop
  if (scrollTop > this.scrollTop) {
    if (scrollTop > this.anchorItem.bottom) {
      this.updateBoundaryIndex(scrollTop)
      this.updateVisibleData()
    }
  } else if (scrollTop < this.scrollTop) {
    // 向上滚动(`scrollTop` 值减小的方向)
  }

  this.scrollTop = scrollTop
}
// ...

在滚动事件处理函数中,会去更新 startIndexendIndex 以及新的锚点元素的位置信息(即更新 startOffset),然后就可以动态的去更新可视区域的渲染数据了:

demo.gif

完整的代码在可以戳:固定高度的虚拟列表实现

列表项是动态高度的

这种情形下,实现的思路和列表项固高大同小异。而小异之处就在于缓存列表项的位置信息时,怎么拿到列表项的精确高度?首先要更改 cachePosition 的部分逻辑:

// ...
cachePosition (node, index) {
  const rect = node.getBoundingClientRect()
  const top = rect.top + window.pageYOffset

  this.cache.push({
    index,
    top,
    bottom: top + rect.height // 将 height 更为 rect.height
  })
}
// ...

由于列表项的高度不固定,那要怎么计算 visibleCount 呢?我们先考虑每个列表项只是渲染一些纯文本。在实际项目中,有的列表项可能只有一行文本,有的列表项可能有多行文本,此时,我们要基于项目的实际情况,给列表项一个预估的高度estimatedItemHeight

比如,有一个长列表要渲染用户的文章摘要,并规定摘要显示不超过三行,那么我们取列表的前 10 个列表项的高度平均值作为预估高度。当然,为了预估高度更精确,我们是可以扩大取样样本的。

既然有了预估高度,那么将原先代码中的 height 替换成 estimatedItemHeight,就可以计算出 visibleCount 了:

// ...
const estimatedItemHeight = 80

// ...

// 计算可渲染的元素个数
this.visibleCount = Math.ceil(window.innerHeight / estimatedItemHeight) + bufferSize

// ...

我们通过 faker.js 来创建一些随机数据,并赋值给 data

// ...
function fakerData () {
  const a = []
  for (let i = 0; i < 1000; i++) {
    a.push({
      id: i,
      words: faker.lorem.words(),
      paragraphs: faker.lorem.sentences()
    })
  }

  return a
}
// ...

this.data = fakerData()

// ...

修改一下列表项的 render 逻辑,其它不变:

// Item.jsx

// ...

render () {
  /* eslint-disable-next-line */
  const {index, item} = this.props

  return (
    <div className='list-item' style={{ height: 'auto' }} ref={node => { this.node = node }}>
      <p>#${index} {item.words}</p>
      <p>{item.paragraphs}</p>
    </div>
  )
}
// ...

此时,列表项的高度已经是动态的了,根据渲染的实际情况,我们给的预估高度是 80:

demo2.gif

完整的代码在可以戳:动态高度的虚拟列表实现

那如果列表项渲染的不是纯文本呢?比如渲染的是图文,那在 Item 组件的 componentDidMount 去调用 cachePosition 方法时,能拿到对应节点的正确高度吗?在渲染图文的情况下,因为图片会发起网络请求,此时并不能保证在列表项组件挂载(执行 componentDidMount)的时候图片渲染好了,那此时对应节点的高度就是不准确的,因而在用户滚动改变可见区域渲染的数据时,就可能出现元素相互重叠的情况:

error

在这种情况下,如果我们能监听 Item 组件节点的大小变化就能获取其正确的高度了。ResizeObserver 或许就可以满足我们的需求,其提供了监听 DOM 元素大小变化的能力,但在撰写本文时,仅 Chrome 67 及以上版本支持,其它主流浏览器均为提供支持。以下是我搜集的一些资料,供你参考(自备梯子):

总结

在本文中,首先对虚拟列表进行了简单的定义,然后从长列表的角度分析了为什么需要虚拟列表,最后就列表项固高和不固高两个场景下以一个简单的 demo 详细讲述了虚拟列表的实现思路。

在列表项是动态高度的场景下,分析了渲染纯文本和图文混合的场景。前者给出了一个具体的 demo,针对后者对于怎么监听元素大小的变化提供了参考的 ResizeObserver 方案。基于 ResizeObserver 的方案呢,我也实现了一个支持渲染图文混合(当然也支持纯文本)的虚拟列表组件 react-virtual-list,供你参考。

当然,这并不是唯一一种实现虚拟列表的方案。在组件 react-virtual-list 的实现过程中,也阅读了不同虚拟列表组件的源码,如: react-tiny-virtual-list、react-window、react-virtualized 等,后续的系列文章我会从源码的角度逐一分析。

原文:https://github.com/dwqs/blog/...

参考

查看原文

云天 评论了文章 · 2018-06-21

context来了,也许该放手redux or mobx...

老铁,学不动了?不要慌,耽误不了你几分钟...(说谎脸,汗)

long long ago

使用react的同胞们,也许都苦恼过其状态管理以及组件之间的数据传递和共享(笨重的方式通过props依次往子组件传递)。

这时候,redux(mobx类似)出现了,我们累死累活的从水深火热中解放了(第三方的库相对较复杂和麻烦)。。。

那还没有接触过redux或者还在使用redux路上的同学怎么办?不要着急,更简单的东西来了(前方高能,请各老铁注意!): Context API!!!(React v16.3.0+) 戳我查看更新日志

先来安利一个todolist的栗子:

栗子

戳我查看源码和demo

Context是什么?

Context直译就是上下文,是 React 16.3.0开始提供的一个官方API,它无需通过props的方式就可以完成项目中由上而下及组件之间的数据传递和共享,即你也不用依赖任何第三方的状态数据插件库就可以完成这项工作任务。

官方推荐使用的情况是:当需要用到全局数据的时候,比如:主题,多语言制或者用户登录授权等等。 ==当然:== 你无需这么死板,当需要用到多层级的组件数据传递或者复杂的数据共享场景也可以使用context api,也可以用来做缓存使用。

Context简单使用

// 1.使用React.createContext创建context提供者Provider 和 context订阅者cunsomer

const {Provider, Consumer} = React.createContext(defaultValue); // defaultValue根据使用场景设置

// 2.设置Provider组件
// 一般包裹需要订阅的子组件的顶层父组件
// value设置需要传递和共享的数据以及改变数据的函数等
// 为了避免没必要的重绘和渲染,value的数据属性值通过组件state设置

<Provider value={/* some value */}>
    {/* some component with comsumer */}
</Provider>

// 3.设置Consumer组件
// 通过函数作为子元素的方式,订阅context的变换

<Consumer>
  {value => /* render something based on the context value */}
</Consumer>

// 组合Provider 和 Consumer即可大功告成

<ProviderComponent>
    <ConsumerComponent>{somechildren}</ConsumerComponent>
</ProviderComponent>

// 其他更多用法,比如生命周期函数调用(可点击上面demo查看),高阶组件等浏览一下文档即会,非常简单

简单总结

创建卖家和买家,通过大家都非常熟悉的React组件方式来进行买卖交易,如此简单的Context用法,大家还在等什么,赶紧丢掉其他的第三方库!!

结语: 小小皮一下,大家不要打我,新功能可以尝试,也需要大家的讨论和研究,类似redux的第三方库还是非常强大~~(前端攻城狮③群:743490497,欢迎大家进群讨论)

更多资料请查看以下链接:

https://reactjs.org/docs/context.html

https://css-tricks.com/digging-into-react-context/

查看原文

云天 回答了问题 · 2018-06-21

redux里如何处理数据?

这个的话,state里面的数据一般是通用型的数据,然后再根据某个组件需要的数据结构按需映射即可。至于你说在哪里做这一步,在mapStateToProps里面去做,这样能根据组件的需要进行合适的映射,state的数据结构最好是设计的扁平化一点儿,希望对你有帮助

关注 2 回答 1

认证与成就

  • 获得 11 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-03-03
个人主页被 344 人浏览