前言

在平时的文字输入时,我总会在中文和英文,中文和数字中间添加一个普通空格 (U+0020)。在文本布局时不会显得那么拥挤,也一定程度上提升了阅读体验。前一段时间即刻 App 添加了一个中西文之前默认添加一个特殊空格的特性,体验之后发现,这样的宽度短于普通空格的特殊空格,即不会出现没有空格时过于拥挤的布局,也不会出现普通空格间距过大的问题。查阅了一些资料后,找到一篇关于 Sapce 的 Unicode 表,想不到空格还有这么多的种类。

之前的开发工作中,出现过一些,长文本在 iOS 设备中不换行的 Bug,这个问题曾经困扰了我许久,后来无意中发现了一个叫 NO-BREAK SPACE 的空格,其 Unicode 为 U+00A0。在 App 中的表现就是,设置了 UILabel 的 numberOfLines 属性为 0 同时 breakMode 设置为 byWords,含有该空格的文本也不会换行,即使限制了 UILabel 的宽度,换行时仍然会拆开单词换行。

鉴于即刻 App 这个新需求带来的完美效果,着手简单实现一下。不过不知道即刻选用的空格种类,这里经过多次比较之后,我使用了 U+2009,被称为 THIN SPACE 的空格。

实现

这个需求,我的第一反应就是使用正则来匹配给定字符串的如下情况

  • 中文+英文
  • 英文+中文
  • 中文+数字
  • 数字+中文
  • 中文+符号
  • 符号+中文
  • 忽略英文和数字的组合,即计量单位,如 10TB

想到用 Swift 来实现正则表达式,就会有点头皮发麻。先不说 NSRegularExpression 的部分 API 中还带有 NSRange。就是正则表达式里面,还需要添加 / 转义符,就是一个噩梦,可读性大大降低。不过,好在 Swift 5 新增的 Raw String 可以完美解决这个问题。用到的所有正则表达式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
let chinese = #"[\u2E80-\u2FFF\u31C0-\u31EF\u3300-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uFE30-\uFE4F]"# // 匹配中文

// 使用 tuple
// base 基础符号。单一符号,不成对
// open 与 close 成对出现的符号
let punc = (
base: #"[@&=_\,\.\?\!\$\%\^\*\-\+\/]"#,
open: #"[\(\[\{\'"<‘“]"#,
close: #"[\)\]\}\'">’”]"#
)

// 拉丁字符。数字、字母等
let latin = #"[A-Za-z0-9\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u1E00-\u1EFF]|\#(punc.base)"#

由于需要匹配的情况有前后之分,下面用一个数组,将上述的正则表达式组合成两个完整的正则表达式。通过 compactMap 获取到最后的 NSRegularExpression 实例数组。

1
2
3
4
let patterns = [
#"(\#(chinese))(\#(latin)|\#(punc.open))"#,
#"(\#(latin)|\#(punc.close))(\#(chinese))"#
].compactMap { try? NSRegularExpression(pattern: $0) }

最后,就可以通过 stringByReplacingMatches(in:options:range:withTemplate:) 方法,通过 template 替换原有的字符串了。

1
2
3
4
5
let unicode = "\u{2009}"
patterns.forEach { (regex) in
result = regex.stringByReplacingMatches(in: result, options: [], range: NSMakeRange(0, result.count), withTemplate: "$1\(unicode)$2")
}
return result

下面的 Gif 比较了不添加空格、添加普通空格和 U+2009 空格的区别。

Gif

今天的练习是关于 JavaScript 的事件流,下面做一些扩展与整理。

事件流

事件流描述的是从页面中接收事件的顺序。IE 和 Netscape 团队提出了两种相反的事件流概念。IE 的事件流是事件冒泡流,而 Netscape 的事件流是事件捕获流。

事件冒泡

事件冒泡,即事件开始时由最具体的元素接收,然后逐层向上传播到较为不具体的节点。例如下面的代码

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang="en">
<head>
<title>Title</title>
</head>
<body>
<div>Click Me!</div>
</body>
</html>

如果单击页面中的 <div> 元素,那么这个 click 事件会按照如下顺序传播:

  1. <div>
  2. <body>
  3. <html>
  4. document

事件捕获

事件捕获的思想是不太具体的节点应该更早的接收到事件,而具体的节点应该最后接收到事件。使用上文提到的例子,点击 <div> 元素时,事件的传播顺序就会变成

  1. document
  2. <html>
  3. <body>
  4. <div>

DOM 事件流

“DOM2 级事件” 规定了事件流包括三个阶段

  1. 捕获阶段 CAPTURING_PHASE
  2. 目标阶段 AT_TARGET
  3. 冒泡阶段 BUBBLING_PHASE

我们可以通过事件对象的 eventPhase 属性,得知事件处于哪个阶段。

DOM 事件在传播时,会从根节点开始往下传递到 target,若注册了事件监听,则监听器处于捕获阶段,为截获事件提供了机会。

target 就是触发事件的具体对象,这时注册在 target 上的事件监听器处于目标阶段。

最后,事件再往上从 target 一路逆向传递到根节点,若注册了事件监听器,则监听器处于冒泡阶段,可以在这个阶段对事件作出响应。

以前面的 HTML 实例代码为例,单击 <div> 元素完整的事件流如下如所示

图片来源于《JavaScript 高级程序设计》

在 DOM 事件流中,实际的目标 <div> 在捕获阶段不会接收到事件。这意味着在捕获阶段,事件从 document 到 <html> 再到 <body> 后就停止了。下一个阶段是处于目标阶段,于是事件在 <div> 上发生,并在事件处理中被看成冒泡阶段的一部分。然后冒泡阶段发生,事件又传回了 document。

事件处理

在今天的练习中,通过 Event​Target​.add​Event​Listener() 来添加事件监听。

语法

target.addEventListener(type, listener[, options]);

target.addEventListener(type, listener[, useCapture]);

方法接收三个参数

type

表示需要监听的事件类型

listener

当所监听的事件类型触发时,接到一个事件通知对象。listener 必须是一个实现了 EventListener 接口的对象,或者是一个函数。

options 可选

  1. capture 默认为 false,即事件只会在冒泡阶段才会被执行。若为 true,即事件在捕获阶段就会被执行
  2. once 表示事件监听被添加后之后执行一次,默认为 false。如果被设置为 true,listener 在被调用后就会被移除。

useCapture 可选

默认为 false,表示注册事件是冒泡事件。若为 true,则表示注册事件为捕获事件。

1
2
3
4
5
6
7
8
9
10
11
12
one.addEventListener('click', (e) => { 
console.log("one capture mode", e.eventPhase);
}, true);
one.addEventListener('click', (e) => {
console.log("one bubble mode", e.eventPhase);
}, false);
two.addEventListener('click', (e) => {
console.log("two capture mode 2");
}, true)
two.addEventListener('click', (e) => { console.log("two bubble mode", e.eventPhase); }, false);
three.addEventListener('click', (e) => { console.log("three capture mode", e.eventPhase); }, true);
three.addEventListener('click', (e) => { console.log("three bubble mode", e.eventPhase); }, false);

打印结果为

one capture mode 1

two capture mode 1

three capture mode 2

three bubble mode 2

tow bubble mode 3

one bubble mode 3

且打印顺序不和事件注册顺序有关。

阻止事件冒泡

1
2
3
4
two.addEventListener('click', (e) => { 
console.log("two capture mode", e.eventPhase);
e.stopPropagation();
}, true);

通过 stopPropagation() 可以阻止事件的冒泡,也阻止了事件的继续捕获。但无法阻止同一个元素其他绑定事件的执行。

使用 stopImmediatePropagation() 即阻止了事件的继续传递,也阻止了同一个元素的其他绑定事件的执行。

今天的练习是使用摄像头,实时记录,并通过 canvas 实时绘制到画布中。

通过 Media​Devices​.get​User​Media() 方法,在通过授权之后,可以获取摄像头图像。具体代码如下

1
2
3
4
5
6
7
8
9
10
11
navigator.mediaDevices.getUserMedia({ video: true, audio: false})
.then((videostream) => {
console.log(videostream);
video.srcObject = videostream;
video.onloadedmetadata = function() {
video.play();
};
})
.catch((error) => {
console.error('OH, Don\'t have permission to use your local cam!', error);
});

阅读更多

今天的练习是对数组的排序练习,特别的是需要去除字符串开头的 a an the 之后进行排序。

去除 a an the 的操作放到了一个 function 里,代码如下

1
2
3
function strip(str) {
return str.replace(/^(a |an |the )/i, '').trim();
}

String.prototype.replace() 接收两个参数,第一个参数可以是一个 正则表达式 或者 待替换的子字符串,第二个参数为 新字符串 或者 函数,返回替换后的新的字符串。

通过 Array.prototype.sort() 排序后,通过 map 方法,转换为 HTML 代码字符串,插入 HTML 代码中。

GitHub 地址

今天的练习实现了一个鼠标移动修改文字阴影的效果。通过添加 'mousemove' 监听鼠标移动事件,计算鼠标移动的距离与阴影偏移的关系,修改文字阴影样式即可。

阅读更多

今天的练习是来了解一下 JavaScript 中关于引用与拷贝的问题。

值类型

在 JavaScript 中,string/number/boolean/null/undefined 等基本类型是值类型。例如下面的例子

1
2
3
4
5
6
7
let age = 100;
let age2 = age;
console.log(age, age2);
// 100 100
age = 200;
console.log(age, age2);
// 200 100

阅读更多


day-13


第十三天的练习是实现页面滚动过程中,滑动到图片位置时,图片从左右伴随动画进入。

Preview

Key Point

  • 监听滚动事件
  • 计算图片出现的位置,添加动画

阅读更多

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×