```
## 解决方案二(升级方案一)
### 原理
使用 less 对公共代码(方案一)封装,同时增加媒体查询分别对不同 DPR 的设备,进行不同的缩放
```less
.border(
@borderWidth: 1px;
@borderStyle: solid;
@borderColor: @lignt-gray-color;
@borderRadius: 0) {
position: relative;
&:before {
content: '';
position: absolute;
width: 98%;
height: 98%;
top: 0;
left: 0;
transform-origin: left top;
-webkit-transform-origin: left top;
box-sizing: border-box;
pointer-events: none;
}
@media (-webkit-min-device-pixel-ratio: 2) {
&:before {
width: 200%;
height: 200%;
-webkit-transform: scale(.5);
}
}
@media (-webkit-min-device-pixel-ratio: 2.5) {
&:before {
width: 250%;
height: 250%;
-webkit-transform: scale(.4);
}
}
@media (-webkit-min-device-pixel-ratio: 2.75) {
&:before {
width: 275%;
height: 275%;
-webkit-transform: scale(1 / 2.75);
}
}
@media (-webkit-min-device-pixel-ratio: 3) {
&:before {
width: 300%;
height: 300%;
transform: scale(1 / 3);
-webkit-transform: scale(1 / 3);
}
}
.border-radius(@borderRadius);
&:before {
border-width: @borderWidth;
border-style: @borderStyle;
border-color: @borderColor;
}
}
.border-all(
@borderWidth: 1px;
@borderStyle: solid;
@borderColor: @lignt-gray-color;
@borderRadius: 0) {
.border(@borderWidth; @borderStyle; @borderColor; @borderRadius);
}
```
## 其他方案:
- 使用图片:兼容性最好,灵活行最差,不能改变颜色、长度
- 使用 `viewport` 和 `rem`,`js` 动态改变 `viewport` 中 `scale` 缩放,缺点在于不适用于已有的项目,例如:使用 `vh` 和 `vw` 布局的
```html
```
- 使用 css 渐变`linear-gradient`或者`box-shadow`
**上述 3 种方案均有致命缺陷暂不推荐使用**
## 兼容性
最后看一下兼容性如何,主要是伪元素、`transform:scale` 和`min-device-pixel-ratio` 这几个关键词的兼容性



## 开源库的解决方案
### vant 组件库
[跳去 github 查看相关代码](https://github.com/youzan/vant/blob/dev/src/style/mixins/hairline.less)
使用`less`写的
```less
.hairline-common() {
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
}
.hairline(@color: @border-color) {
.hairline-common();
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
border: 0 solid @color;
transform: scale(0.5);
}
```
也是采用第一种解决方案
### ant-design-mobile 组件库
[跳去 github 查看相关代码](https://github.com/ant-design/ant-design-mobile/blob/master/components/style/mixins/hairline.less)
```less
.scale-hairline-common(@color, @top, @right, @bottom, @left) {
content: '';
position: absolute;
background-color: @color;
display: block;
z-index: 1;
top: @top;
right: @right;
bottom: @bottom;
left: @left;
}
.hairline(@direction, @color: @border-color-base) when (@direction = 'top') {
border-top: 1PX solid @color;
html:not([data-scale]) & {
@media (min-resolution: 2dppx) {
border-top: none;
&::before {
.scale-hairline-common(@color, 0, auto, auto, 0);
width: 100%;
height: 1PX;
transform-origin: 50% 50%;
transform: scaleY(0.5);
@media (min-resolution: 3dppx) {
transform: scaleY(0.33);
}
}
}
}
}
.hairline(@direction, @color: @border-color-base) when (@direction = 'right') {
border-right: 1PX solid @color;
html:not([data-scale]) & {
@media (min-resolution: 2dppx) {
border-right: none;
&::after {
.scale-hairline-common(@color, 0, 0, auto, auto);
width: 1PX;
height: 100%;
background: @color;
transform-origin: 100% 50%;
transform: scaleX(0.5);
@media (min-resolution: 3dppx) {
transform: scaleX(0.33);
}
}
}
}
}
.hairline(@direction, @color: @border-color-base) when (@direction = 'bottom') {
border-bottom: 1PX solid @color;
html:not([data-scale]) & {
@media (min-resolution: 2dppx) {
border-bottom: none;
&::after {
.scale-hairline-common(@color, auto, auto, 0, 0);
width: 100%;
height: 1PX;
transform-origin: 50% 100%;
transform: scaleY(0.5);
@media (min-resolution: 3dppx) {
transform: scaleY(0.33);
}
}
}
}
}
.hairline(@direction, @color: @border-color-base) when (@direction = 'left') {
border-left: 1PX solid @color;
html:not([data-scale]) & {
@media (min-resolution: 2dppx) {
border-left: none;
&::before {
.scale-hairline-common(@color, 0, auto, auto, 0);
width: 1PX;
height: 100%;
transform-origin: 100% 50%;
transform: scaleX(0.5);
@media (min-resolution: 3dppx) {
transform: scaleX(0.33);
}
}
}
}
}
.hairline(@direction, @color: @border-color-base, @radius: 0) when (@direction = 'all') {
border: 1PX solid @color;
border-radius: @radius;
html:not([data-scale]) & {
@media (min-resolution: 2dppx) {
position: relative;
border: none;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 200%;
height: 200%;
border: 1PX solid @color;
border-radius: @radius * 2;
transform-origin: 0 0;
transform: scale(0.5);
box-sizing: border-box;
pointer-events: none;
// @media (min-resolution: 3dppx) {
// width: 300%;
// height: 300%;
// border-radius: @radius * 3;
// transform: scale(0.33);
// }
}
}
}
}
```
这个值得研究下,比 vant 和 第一种解决方案有点不同,主要在于处理了 DPR 为 2 和为 3 的两种情况,相比来说更加完善。
> 这里 PX 大写,为了防止插件将 px 转成 rem 等单位
# 总结
通过该文,你大概了解 1px 问题的来龙去脉了吧,也明白了如何解决相关问题,如果这票文章能解决你的疑问或者工作中问题,不妨点个赞收藏下。
由于技术水平有限,文章中如有错误地方,请在评论区指出,感谢!
接下我应该会关于**移动端 H5 布局问题和一些踩坑**进行一段学习工作总结,不妨点个关注。
[issue评论区](https://github.com/suoyuesmile/suo-blog/issues/40)
================================================
FILE: articals/h5/0002.md
================================================
# 前言
最近写第三个移动端 H5 的项目了,准备记录下自己在 H5 项目中的一些实践探索。移动端 H5 与 PC 端开发最大的区别之一,大概就是**响应式布局**问题。
那么下面我们来聊聊移动端响应式布局,了解他的来龙去脉,对现有的最佳解决方案探索。
## 问题
全文将围绕下面几个问题进行论述和展开:
- **写移动端 H5 相关页面,相比 PC 端有哪些值得注意的点?**
- **关于H5 响应式布局有哪些解决方案?**
- **什么是 rem?如何在项目中完美使用它?**
- **vh/vw 是最佳解决方案吗?它有什么优势和缺陷**
- **大型开源库里面常用解决方案是什么?**
- **怎样快速搭建一套移动端布局解决方案?**
# 由来
## 概念
#### 什么是 H5 技术?
H5 这个命名本身是一个很不讨巧的命名,咋一眼看上去认为 HTML5,或者第 5 级标题的标签,对一些造成一些不小的误解。
> 比如:我的一个某后端同事,谈论到 H5 很简单,HTML 之前我也学过一些,以后要是你请假,我来帮你写。
> 我是一脸蒙蔽,H5 === HTML?
再看看,搜索引擎中H5是什么?(引用来自谷歌词条第一页)

如此看来,将 H5 视作 HTML 的大有人在,而 H5 这个概念只在中国特有,所以对外国人来说他们也认为是 HTML, 所以,对于外国人和非这个领域的人来说,他们存在一样的误解。
目前的 H5 算是一个比较大的概念了,我认为的 H5 技术是**一系列移动端 web 前端技术的集合** 大致用一个韦恩图表示如下

我们这里只谈 web 前端中 H5 技术,H5 技术本身是用于**移动端的 web 网页**。由于App本身有个 “ webview ” 的容器,在容器里可以运行 web 前端相关代码,由此 H5 和原生 App 结合又衍生出来了 **Hybrid App 技术**。
#### Hybrid App 技术大致原理

这是我给公司同事普及 H5 知识绘制的图像。
# 实践
## 解决方案一:rem + pxToRem
### 概念
css 中用于计量的单位有两种,一种是**绝对单位**,另一种是**相对单位**
#### 绝对单位

对于绝对单位,一般来说常用的也就 `px`, 其他的可能打印需要用到
#### 相对单位

对于相对单位来说,`em` 和 `rem` 属于一对,`vw` 和 `vh` 属于一对。
前一对相对于**字体大小**,区别在于 `rem` 相对于**根字体**,对于我们控制整体的大小相对容易些,所以我们可以使用它来控制整个页面的缩放。
后一对,相对于视窗的大小,这个将在下一个节中发挥主要作用。
### 原理
1. 监听屏幕视窗的宽度,通过一定比例换算赋值给`html`的`font-size`。此时,根字体大小就会随屏幕宽度而变化。
2. 将 `px` 转换成 `rem`, 常规方案有两种,一种是利用`sass`/`less`中的自定义函数 `pxToRem`,写`px`时,利用`pxToRem`函数转换成 `rem`。另外一种是直接写`px`,编译过程利用插件全部转成`rem`。这样 dom 中元素的大小,就会随屏幕宽度变化而变化了。
### 实现
1. 动态更新根字体大小
```js
const MAX_FONT_SIZE = 420
// 定义最大的屏幕宽度
document.addEventListener('DOMContentLoaded', () => {
const html = document.querySelector('html')
let fontSize = window.innerWidth / 10
fontSize = fontSize > MAX_FONT_SIZE ? MAX_FONT_SIZE : fontSize
html.style.fontSize = fontSize + 'px'
})
```
2. `px` 转 `rem`
#### `pxToRem` 方案一
```scss
$rootFontSize: 375 / 10;
// 定义 px 转化为 rem 的函数
@function px2rem ($px) {
@return $px / $rootFontSize + rem;
}
.demo {
width: px2rem(100);
height: px2rem(100);
}
```
#### `pxToRem`方案二
`vue-cli3` 中配置 装 `postcss-pxtorem` 插件就可以了,其他平台大致差不多
```js
const autoprefixer = require('autoprefixer')
const pxtorem = require('postcss-pxtorem')
module.exports = {
// ...
css: {
sourceMap: true,
loaderOptions: {
postcss: {
plugins: [
autoprefixer(),
pxtorem({
rootValue: 37.5,
propList: ['*']
})
]
}
}
}
}
```
[点击快速配置 H5 项目工程](https://github.com/suoyuesmile/vue-h5-awsome)
继续探索[postcss-pxtorem](https://github.com/cuth/postcss-pxtorem/blob/master/index.js)插件源码,查看它实现的原理
```js
function createPxReplace (rootValue, unitPrecision, minPixelValue) {
return function (m, $1) {
if (!$1) return m;
var pixels = parseFloat($1);
if (pixels < minPixelValue) return m;
var fixedVal = toFixed((pixels / rootValue), unitPrecision);
return (fixedVal === 0) ? '0' : fixedVal + 'rem';
};
}
```
`px`变换成 `rem` 主要是这个函数,当然里面有很多可配置的参数, 核心原理和我们方案一差不多,方便在于,不需要每次写`px`都要加上一个函数,代码也清晰很多
> 是不是所有元素 `px` 都要转换成 `rem`呢?,那可不一定哦,border 中的 `px` 不应该转 rem,涉及到另外一个 1px 的问题,上一篇文章详细论述过,避免 px 转 rem,将 border 中的 px 大写成 PX/Px/pX
1px 适配问题请移至 [吃透移动端 1px](https://juejin.im/post/5df3053ce51d45583d425ada)
## 解决方案二:vh + vw
### 原理
**`vw` 相对于视窗宽度的单位,随宽度变化而变化。由此看来,方案一其实是方案二的一种 Hack, 通过使用监听实现了方案二的效果**
### 实现
与 rem 类似做法,直接使用 [postcss-px-to-viewport](https://github.com/evrone/postcss-px-to-viewport/blob/master/index.js) 插件进行配置, 配置方式也是和 [postcss-pxtorem](https://github.com/cuth/postcss-pxtorem/blob/master/index.js) 大同小异
我们看看插件的原理是不是也是一样的
```js
function createPxReplace(opts, viewportUnit, viewportSize) {
return function (m, $1) {
if (!$1) return m;
var pixels = parseFloat($1);
if (pixels <= opts.minPixelValue) return m;
var parsedVal = toFixed((pixels / viewportSize * 100), opts.unitPrecision);
return parsedVal === 0 ? '0' : parsedVal + viewportUnit;
};
}
```
**果然呢,连方法名、变量名、代码逻辑,都一摸一样哈哈哈,谁抄谁,我就不指出来啦 - -**
## 其他解决方案
| | 方案 | 缺陷
| ---------- | ------------ | ------------|
|1|百分比|高度无法百分比|
|2|媒体查询 + meta 中 viewport |不同设备宽度不同,缩放比无法完全确定|
|3|flex |还是无法解决宽度超出问题|
上面方案均存在致命缺陷,不推荐使用它完成移动端布局计算。
> flex 与 rem 结合使用更佳
## 兼容性
上述两种方案,**兼容性主要在于 rem,vh,vw 关键词上**

`rem`在移动端表现高达 100%,令人惊叹!


`vh vw` 表现惨不忍睹
**不得不说 `rem` 仍然是移动端 h5 布局的最佳方案**
## 开源库解决方案
### vant 组件库

vant 组件库中,默认采用 px 做计量单位,如果需要使用 rem,直接使用插件完美适配。
对于 vw 方案,vant 也是可以通过插件将 px 转成 vw,对于 vw 可能会存在一些坑点。
### ant-design-mobile 组件库
ant-design-mobile 组件库仍然使用 `px` 单位
```less
@hd: 1px; // 基本单位
// 字体尺寸
// ---
@font-size-icontext: 10 * @hd;
@font-size-caption-sm: 12 * @hd;
@font-size-base: 14 * @hd;
@font-size-subhead: 15 * @hd;
@font-size-caption: 16 * @hd;
@font-size-heading: 17 * @hd;
// 圆角
// ---
@radius-xs: 2 * @hd;
@radius-sm: 3 * @hd;
@radius-md: 5 * @hd;
@radius-lg: 7 * @hd;
@radius-circle: 50%;
// 边框尺寸
// ---
@border-width-sm: 1PX;
@border-width-md: 1PX;
@border-width-lg: 2 * @hd;
```
与 `vant` 组件一样,还是由开发者来决定到底用哪一种方案
这种把选择权交给开发者,算是一种开源库的最灵活的做法了。
# 总结
通过该文,你大概了解 H5 问题的来龙去脉了吧,也明白了如何解决移动端响应式布局问题,如果这票文章能解决你的疑问或者工作中问题,不妨点个赞收藏下。
由于技术水平有限,文章中如有错误地方,请在评论区指出,感谢!
上一篇文章 解决了 1px 问题,这篇文章解决了响应式布局问题,
接下我应该会继续研究下关于 H5 一些踩坑总结,之后应该回去研究下 vue 最新的源码再进行分享,想持续了解更多,不妨点个关注。
================================================
FILE: articals/h5/0003.md
================================================
# 前言
作为一个开发了多个 H5 项目的前端工程师,在开发过程中难免会遇到一些兼容性等**爬过坑**的问题。现在我将这些问题一一汇总一下,并在后面给出**坑产生的原理**,和**现阶段常规的填坑方案**。由此来做一个阶段性的总结。
> 常规操作哈,**点赞**后再观看呗!你的**点赞**就是我创作的动力之一!
# 问题
下面列举了我遇到的一些常规问题,如有遇到其他问题请在评论区补充,之后我也会实践后加以补充,感谢!(经常更新该文)
## 移动端 H5 相关问题汇总:
- **1px 问题**
- **响应式布局**
- **iOS 滑动不流畅**
- **iOS 上拉边界下拉出现白色空白**
- **页面件放大或缩小不确定性行为**
- **click 点击穿透与延迟**
- **软键盘弹出将页面顶起来、收起未回落问题**
- **iPhone X 底部栏适配问题**
- **保存页面为图片和二维码问题和解决方案**
- **微信公众号 H5 分享问题**
- **H5 调用 SDK 相关问题及解决方案**
- **H5 调试相关方案与策略**
## 移动端 H5 相关基础技术概览

# 原理与实践
之前两篇文章已经详细的论述了**1px** 问题与 **响应式布局**问题,并给出了原理和解决方案。
> 防止丢失,**点赞收藏**后跳转至快捷通道:[**1px**](https://juejin.im/entry/5df32ffd6fb9a016194afb5e)通道与[响应式布局](https://juejin.im/entry/5df613c3f265da33ca400e72)通道
接下来呢,我们看看其他问题的原理和解决方案吧。
> 以下解决方案,均经过我测试成功,健康安全,请放下食用。由于篇幅原因,某些非核心解决方案的实现细节暂未谈论,需要自行研究。
## iOS 滑动不流畅
### 表现
上下滑动页面会产生卡顿,手指离开页面,页面立即停止运动。整体表现就是滑动不流畅,没有滑动惯性。
### 产生原因
**为什么 iOS 的 webview 中 滑动不流畅,它是如何定义的?**
最终我在 `safari` 文档里面寻找到了答案(文档链接在参考资料项)。

原来在 iOS 5.0 以及之后的版本,滑动有定义有两个值 `auto` 和 `touch`,默认值为 `auto`。
```css
-webkit-overflow-scrolling: touch; /* 当手指从触摸屏上移开,会保持一段时间的滚动 */
-webkit-overflow-scrolling: auto; /* 当手指从触摸屏上移开,滚动会立即停止 */
```
### 解决方案
#### 1.在滚动容器上增加滚动 touch 方法
将`-webkit-overflow-scrolling` 值设置为 `touch`
```css
.wrapper {
-webkit-overflow-scrolling: touch;
}
```
> 设置滚动条隐藏: `.container ::-webkit-scrollbar {display: none;}`
可能会导致使用`position:fixed;` 固定定位的元素,随着页面一起滚动
#### 2.设置 overflow
设置外部 `overflow` 为 `hidden`,设置内容元素 `overflow` 为 `auto`。内部元素超出 body 即产生滚动,超出的部分 body 隐藏。
```css
body {
overflow-y: hidden;
}
.wrapper {
overflow-y: auto;
}
```
> 两者结合使用更佳!
## iOS 上拉边界下拉出现白色空白
### 表现
手指按住屏幕下拉,屏幕顶部会多出一块白色区域。手指按住屏幕上拉,底部多出一块白色区域。
### 产生原因
在 iOS 中,手指按住屏幕上下拖动,会触发 `touchmove` 事件。这个事件触发的对象是整个 `webview` 容器,容器自然会被拖动,剩下的部分会成空白。
### 解决方案
#### 1. 监听事件禁止滑动
移动端触摸事件有三个,分别定义为
```
1. touchstart :手指放在一个DOM元素上。
2. touchmove :手指拖曳一个DOM元素。
3. touchend :手指从一个DOM元素上移开。
```
显然我们需要控制的是 `touchmove` 事件,由此我在 W3C 文档中找到了这样一段话
> Note that the rate at which the user agent sends touchmove events is implementation-defined, and may depend on hardware capabilities and other implementation details.
> If the preventDefault method is called on the first touchmove event of an active touch point, it should prevent any default action caused by any touchmove event associated with the same active touch point, such as scrolling.
**`touchmove` 事件的速度是可以实现定义的,取决于硬件性能和其他实现细节**
**`preventDefault` 方法,阻止同一触点上所有默认行为,比如滚动。**
由此我们找到解决方案,通过监听 `touchmove`,让需要滑动的地方滑动,不需要滑动的地方禁止滑动。
> 值得注意的是我们要过滤掉具有滚动容器的元素。
实现如下:
```js
document.body.addEventListener('touchmove', function(e) {
if(e._isScroller) return;
// 阻止默认事件
e.preventDefault();
}, {
passive: false
});
```
#### 2. 滚动妥协填充空白,装饰成其他功能
在很多时候,我们可以不去解决这个问题,换一直思路。根据场景,**我们可以将下拉作为一个功能性的操作**。
**比如: 下拉后刷新页面**

## 页面放大或缩小不确定性行为
### 表现
双击或者双指张开手指页面元素,页面会放大或缩小。
### 产生原因
HTML 本身会产生放大或缩小的行为,比如在 PC 浏览器上,可以自由控制页面的放大缩小。但是在移动端,我们是不需要这个行为的。所以,我们需要禁止该不确定性行为,来提升用户体验。
### 原理与解决方案
HTML `meta` 元标签标准中有个 中 `viewport` 属性,用来控制页面的缩放,一般用于移动端。如下图 MDN 中介绍

移动端常规写法
```html
```
因此我们可以设置 `maximum-scale`、`minimum-scale` 与 `user-scalable=no` 用来避免这个问题
```html
```
## click 点击事件延时与穿透
### 表现
监听元素 `click` 事件,点击元素触发时间延迟约 `300ms`。
点击蒙层,蒙层消失后,下层元素点击触发。
### 产生原因
#### 为什么会产生 click 延时?
iOS 中的 safari,为了实现双击缩放操作,在单击 300ms 之后,如果未进行第二次点击,则执行 `click` 单击操作。也就是说来判断用户行为是否为双击产生的。但是,在 App 中,无论是否需要双击缩放这种行为,`click` 单击都会产生 300ms 延迟。
>
#### 为什么会产生 click 点击穿透?
双层元素叠加时,在上层元素上绑定 `touch` 事件,下层元素绑定 `click` 事件。由于 `click` 发生在 `touch` 之后,点击上层元素,元素消失,下层元素会触发 `click` 事件,由此产生了点击穿透的效果。
### 原理与解决方案
#### 解决方案一:使用 touchstart 替换 click
前面已经介绍了,移动设备不仅支持点击,还支持几个触摸事件。
那么我们现在基本思路就是用 `touch` 事件代替`click` 事件。
将 `click` 替换成 `touchstart` 不仅解决了 `click` 事件都延时问题,还解决了穿透问题。因为穿透问题是在 `touch` 和 `click` 混用时产生。
在原生中使用
```js
el.addEventListener("touchstart", () => { console.log("ok"); }, false);
```
在 vue 中使用
```html
点击
```
开源解决方案中,也是既提供了 `click` 事件,又提供了`touchstart` 事件。如 vant 中的 `button` 组件

**那么,是否可以将 `click` 事件全部替换成 `touchstart` 呢?为什么开源框架还会给出 `click` 事件呢?**
我们想象一种情景,同时需要点击和滑动的场景下。如果将 `click` 替换成 `touchstart` 会怎样?
> 事件触发顺序: `touchstart`, `touchmove`, `touchend`, `click`。
很容易想象,在我需要`touchmove`滑动时候,优先触发了`touchstart`的点击事件,是不是已经产生了冲突呢?
所以呢,在具有滚动的情况下,还是建议使用 `click` 处理。
在接下来的`fastclick`开源库中也做了如下处理。
针对 `touchstart` 和 `touchend`,截取了部分源码。
```js
// If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
// 1) the user does a fling scroll on the scrollable layer
// 2) the user stops the fling scroll with another tap
// then the event.target of the last 'touchend' event will be the element that was under the user's finger
// when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
// is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
this.updateScrollParent(targetElement);
```
```js
// Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
// and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
scrollParent = targetElement.fastClickScrollParent;
if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
return true;
}
```
主要目的就是,在使用 `touchstart` 合成 `click` 事件时,保证其不在滚动的父元素之下。
#### 解决方案二: 使用 fastclick 库
使用 `npm/yarn` 安装后使用
```js
import FastClick from 'fastclick';
FastClick.attach(document.body, options);
```
同样,使用`fastclick`库后,`click` 延时和穿透问题都没了
按照我的惯例,只要涉及开源库,那么我们一定要去了解它实现的原理。主要是将现有的原生事件集合封装合成一个兼容性较强的事件集合。
[fastclick源码](https://github.com/ftlabs/fastclick/blob/master/lib/fastclick.js) 核心代码不长, 1000 行不到。有兴趣可以了解一下!
## 软键盘将页面顶起来、收起未回落问题
### 表现
Android 手机中,点击 `input` 框时,键盘弹出,将页面顶起来,导致页面样式错乱。
移开焦点时,键盘收起,键盘区域空白,未回落。
### 产生原因
我们在app 布局中会有个固定的底部。安卓一些版本中,输入弹窗出来,会将解压 `absolute` 和 `fixed` 定位的元素。导致可视区域变小,布局错乱。
### 原理与解决方案
软键盘将页面顶起来的解决方案,主要是通过监听页面高度变化,强制恢复成弹出前的高度。
```js
// 记录原有的视口高度
const originalHeight = document.body.clientHeight || document.documentElement.clientHeight;
window.onresize = function(){
var resizeHeight = document.documentElement.clientHeight || document.body.clientHeight;
if(resizeHeight < originalHeight ){
// 恢复内容区域高度
// const container = document.getElementById("container")
// 例如 container.style.height = originalHeight;
}
}
```
键盘不能回落问题出现在 iOS 12+ 和 wechat 6.7.4+ 中,而在微信 H5 开发中是比较常见的 Bug。
兼容原理,1.判断版本类型 2.更改滚动的可视区域
```js
const isWechat = window.navigator.userAgent.match(/MicroMessenger\/([\d\.]+)/i);
if (!isWechat) return;
const wechatVersion = wechatInfo[1];
const version = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/);
// 如果设备类型为iOS 12+ 和wechat 6.7.4+,恢复成原来的视口
if (+wechatVersion.replace(/\./g, '') >= 674 && +version[1] >= 12) {
window.scrollTo(0, Math.max(document.body.clientHeight, document.documentElement.clientHeight));
}
```
> `window.scrollTo(x-coord, y-coord)`,其中`window.scrollTo(0, clientHeight)`恢复成原来的视口
## iPhone X系列安全区域适配问题
### 表现
头部刘海两侧区域或者底部区域,出现刘海遮挡文字,或者呈现黑底或白底空白区域。
### 产生原因
iPhone X 以及它以上的系列,都采用**刘海屏设计**和**全面屏手势**。头部、底部、侧边都需要做特殊处理。才能适配 iPhone X 的特殊情况。
### 解决方案
**设置安全区域,填充危险区域,危险区域不做操作和内容展示。**
> 危险区域指头部不规则区域,底部横条区域,左右触发区域。

具体操作为:`viewport-fit` `meta` 标签设置为 `cover`,获取所有区域填充。
判断设备是否属于 iPhone X,给头部底部增加**适配层**
> `viewport-fit` 有 3 个值分别为:
> - `auto`:此值不影响初始布局视图端口,并且整个web页面都是可查看的。
> - `contain`:
视图端口按比例缩放,以适合显示内嵌的最大矩形。
> - `cover`:视图端口被缩放以填充设备显示。强烈建议使用 `safe area inset` 变量,以确保重要内容不会出现在显示之外。
#### 设置 viewport-fit 为 `cover`
```html
```
#### 增加适配层
使用 `safe area inset` 变量
```css
/* 适配 iPhone X 顶部填充*/
@supports (top: env(safe-area-inset-top)){
body,
.header{
padding-top: constant(safe-area-inset-top, 40px);
padding-top: env(safe-area-inset-top, 40px);
padding-top: var(safe-area-inset-top, 40px);
}
}
/* 判断iPhoneX 将 footer 的 padding-bottom 填充到最底部 */
@supports (bottom: env(safe-area-inset-bottom)){
body,
.footer{
padding-bottom: constant(safe-area-inset-bottom, 20px);
padding-bottom: env(safe-area-inset-bottom, 20px);
padding-top: var(safe-area-inset-bottom, 20px);
}
}
```
> `safe-area-inset-top`, `safe-area-inset-right`, `safe-area-inset-bottom`, `safe-area-inset-left`
`safe-area-inset-*`由四个定义了视口边缘内矩形的 `top`, `right`, `bottom` 和 `left` 的环境变量组成,这样可以安全地放入内容,而不会有被非矩形的显示切断的风险。对于矩形视口,例如普通的笔记本电脑显示器,其值等于零。 对于非矩形显示器(如圆形表盘,`iPhoneX` 屏幕),在用户代理设置的四个值形成的矩形内,所有内容均可见。
其中 `env()` 用法为 `env(
, ? )`,第一个参数为自定义的区域,第二个为备用值。
其中 `var()` 用法为 `var( , ? )`,作用是在 `env()` 不生效的情况下,给出一个备用值。
`constant()` 被 `css` 2017-2018 年为草稿阶段,是否已被标准化未知。而其他iOS 浏览器版本中是否有此函数未知,作为兼容处理而添加进去。
详情请查看文章末尾的参考资料。
#### 兼容性

## 页面生成为图片和二维码问题
### 表现
在工作中有需要将页面生成图片或者二维码的需求。可能我们第一想到的,交给后端来生成更简单。但是这样我们需要把页面代码全部传给后端,网络性能消耗太大。
### 解决方案
#### 生成二维码
使用 QRCode 生成二维码
```js
import QRCode from 'qrcode';
// 使用 async 生成图片
const options = {};
const url = window.location.href;
async url => {
try {
console.log(await QRCode.toDataURL(url, options))
} catch (err) {
console.error(err);
}
}
```
将 `await QRCode.toDataURL(url, options)` 赋值给 图片 `url` 即可
#### 生成图片
主要是使用 `htmlToCanvas` 生成 `canvas` 画布
```js
import html2canvas from 'html2canvas';
html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
});
```
但是不单单在此处就完了,由于是 `canvas` 的原因。移动端生成出来的图片比较模糊。
我们使用一个新的 `canvas` 方法多倍生成,放入一倍容器里面,达到更加清晰的效果,通过超链接下载图片
**下载文件简单实现,更完整的实现方式之后更新**
```js
const scaleSize = 2;
const newCanvas = document.createElement("canvas");
const target = document.querySelector('div');
const width = parseInt(window.getComputedStyle(target).width);
const height = parseInt(window.getComputedStyle(target).height);
newCanvas.width = width * scaleSize;
newCanvas.height = widthh * scaleSize;
newCanvas.style.width = width + "px";
newCanvas.style.height =width + "px";
const context = newCanvas.getContext("2d");
context.scale(scaleSize, scaleSize);
html2canvas(document.querySelector('.demo'), { canvas: newCanvas }).then(function(canvas) {
// 简单的通过超链接设置下载功能
document.querySelector(".btn").setAttribute('href', canvas.toDataURL());
}
```
> 根据需要设置 `scaleSize` 大小
## 微信公众号分享问题
### 表现
在微信公众号 H5 开发中,页面内部点击分享按钮调用 SDK,方法不生效。
### 解决方案
#### 解决方法:添加一层蒙层,做分享引导。
因为页面内部点击分享按钮无法直接调用,而分享功能需要点击右上角更多来操作。
然后用户可能不知道通过右上角小标里面的功能分享。又想引导用户分享,这时应该怎么做呢?
技术无法实现的,从产品出发。

**如果技术上实现复杂,或者直接不能实现。不要强行钻牛角尖哦,学会怼产品,也是程序员必备的能力之一。**
## H5 调用 SDK 相关解决方案
### 产生原因
在 Hybrid App 中使用 H5 是最常见的不过了,刚接触的,肯定会很生疏模糊。不知道 H5 和 Hybrid 是怎么交互的。怎样同时支持 iOS 和 Android 呢?现在来谈谈 Hybrid 技术要点,**原生与 H5 的通信**。
### 解决方案

使用 `DSBridge` 同时支持 iOS 与 Android
> 文档见参考资料
#### SDK小组 提供方法
1. 注册方法 `bridge.register`
```js
bridge.register('enterApp', function() {
broadcast.emit('ENTER_APP')
})
```
2. 回调方法 `bridge.call`
```js
export const getSDKVersion = () => bridge.call('BLT.getSDKVersion')
```
#### 事件监听与触发法
```js
const broadcast = {
on: function(name, fn, pluralable) {
this._on(name, fn, pluralable, false)
},
once: function(name, fn, pluralable) {
this._on(name, fn, pluralable, true)
},
_on: function(name, fn, pluralable, once) {
let eventData = broadcast.data
let fnObj = { fn: fn, once: once }
if (pluralable && Object.prototype.hasOwnProperty.call(eventData, 'name')) {
eventData[name].push(fnObj)
} else {
eventData[name] = [fnObj]
}
return this
},
emit: function(name, data, thisArg) {
let fn, fnList, i, len
thisArg = thisArg || null
fnList = broadcast.data[name] || []
for (i = 0, len = fnList.length; i < len; i++) {
fn = fnList[i].fn
fn.apply(thisArg, [data, name])
if (fnList[i].once) {
fnList.splice(i, 1)
i--
len--
}
}
return this
},
data: {}
}
export default broadcast
```
#### 踩坑注意
方法调用前,一定要判断 SDK 是否提供该方法
如果 Android 提供该方法,iOS 上调用就会出现一个方法**调用失败等弹窗**。
怎么解决呢?
提供一个判断是否 Android、iOS。根据设备进行判断
```
export const hasNativeMethod = (name) =>
return bridge.hasNativeMethod('BYJ.' + name)
}
export const getSDKVersion = function() {
if (hasNativeMethod('getSDKVersion')) {
bridge.call('BYJ.getSDKVersion')
}
}
```
> 同一功能需要iOS,Android方法名相同,这样更好处理哦
## H5 调试相关方案策略
### 表现
调试代码一般就是为了**查看数据**和**定位 bug**。分为两种场景,一种是开发和测试时调试,一种是生产环境上调试。
> 为什么有生产环境上调试呢?有些时候测试环境上没法复现这个 bug,测试环境和生产环境不一致,此时就需要紧急生产调试。
在 PC 端开发时,我们可以直接掉出控制台,使用浏览器提供的工具操作devtools或者查看日志。但是在 App 内部我们怎么做呢?
### 原理与解决方案
#### 1. `vconsole` 控制台插件
使用方法也很简单
```js
import Vconsole from 'vconsole'
new Vconsole()
```

有兴趣看看它实现的基本原理,我们关注的点应该在 **vsconsole 如何打印出我们所有 log 的** [腾讯开源vconsole](https://github.com/Tencent/vConsole/blob/dev/src/core/core.js)
上述方法仅用于开发和测试。**生产环境中不允许出现,所以,使用时需要对环境进行判断。**
```js
import Vconsole from 'vconsole'
if (process.env.NODE_ENV !== 'production') {
new Vconsole()
}
```
#### 2. 代理 + spy-debugger
操作稍微有点麻烦,不过我会详细写出,大致分为 4 个步骤
1. 安装插件(全局安装)
```sh
sudo npm install spy-debugger -g
```
2. 手机与电脑置于同一 wifi 下,手机设置代理
设置手机的 HTTP 代理,代理 IP 地址设置为 PC 的 IP 地址,端口为`spy-debugger`的启动端口
> spy-debugger 默认端口:9888
> Android :设置 - WLAN - 长按选中网络 - 修改网络 - 高级 - 代理设置 - 手动
> IOS :设置 - Wi-Fi - 选中网络, 点击感叹号, HTTP 代理手动
3. 手机打开浏览器或者 app 中 H5 页面
4. 打开桌面日志网站进行调试,点击 npm 控制台监听地址。查看抓包和 H5 页面结构
**这种方式可以调试生成环境的页面,不需要修改代码,可以应付大多数调试需求**
# 总结
本篇文章耗费作者一个多星期的业余时间,存手工敲打 4500 +字,同时收集,整理之前很多坑点和边写作边**思考**和**总结**。如果能对你有帮助,便是它最大的价值。都看到这里还不**点赞**,太过不去啦!😄
由于技术水平有限,文章中如有错误地方,请在评论区指出,感谢!
关于移动端 H5 的文章告一段落了,之后实践中遇到的问题都将在此文中更新。另外准备做一个移动端 H5 开源项目。多关注下 [我的github](https://github.com/suoyuesmile/suo-blog)动态哦!
之后,应该回去研究下**开源和面试题相关内容**分享,想持续了解更多,不妨**点赞**和**关注**呗。
# 参考资料
- [Safari CSS Reference](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariCSSRef/Articles/StandardCSSProperties.html#//apple_ref/css/property/-webkit-overflow-scrolling)
- [MDN touch 事件]()
- [MDN css var()](https://developer.mozilla.org/zh-CN/docs/Web/CSS/var)
- [MDN css env()](https://developer.mozilla.org/zh-CN/docs/Web/CSS/env)
- [csswg env() drafts](https://drafts.csswg.org/css-env-1/)
- [fastclick 源码](https://github.com/ftlabs/fastclick/blob/master/lib/fastclick.js)
- [DSBridge-Android](https://github.com/wendux/DSBridge-Android) & [DSBridge-iOS](https://github.com/wendux/DSBridge-IOS)
- [qrcodejs 源码](https://github.com/davidshimjs/qrcodejs)
- [html2canvas 源码](https://github.com/niklasvh/html2canvas)
- [关于H5页面在iPhoneX适配](https://www.cnblogs.com/lolDragon/p/7795174.html)
- [vant 相关文档](https://youzan.github.io/vant/#/zh-CN/button)
================================================
FILE: articals/interview/0001.md
================================================
首先呢,统计一下情况:
本人校招进入新三板上市企业工作一年,并有半年多一线互联网实习经验
来到杭州投出简历 24 封,收到面试邀约 10 次
`result = 正式offer * 2 + 口头offer * 1`
令人头疼的是并没拿到自己想要的 offer,理想的公司大多连面试机会都不给...
伤脑筋...明天还有两家电话面试,过了大半个月,还是打算继续面下去...
### 理想公司:
- ***服:简历都没过(惨)还被一个蚂***外包招聘diss我这样的一年工作经验都外包都够呛(晕)
- *赞:投了很多遍,简历全部卡在`HR`上了(惨)
- *猫:没敢投(吓)
- **车:一下午全部面完,环境一般,每轮面试都等了很长时间,两轮技术面后又和HR聊了一个多小时技术问题,表示很无奈,从此对大搜车印象大大折扣。回去等通知,一周之后说经验不符。敢情经验不符,你让我去面试个球(气)
- *吧:HR主动找,问
```js
你会 React ?我答会
你会移动端吗?我答会
你有过 H5 经验吗?我答有
对不起,面试官说经验不符
???(懵)
```
- **顺:只招应届生了
### 考题汇总:
- `this` 指向 * 10 (必考有没有,这都不知道还学啥JavaScript--笑)
- 同步异步或者事件机制 * 8
- `Vue` 双向绑定实现原理 * 8
- 箭头函数 * 6(考察 ES6 使用情况)
- `call` `apply` `bind` 的使用和区别 * 6 (问到`this`很可能问到这些)
- 常用 `Array` 函数 * 6
- `Vuex` 应用及其原理 * 6
- `Vue` 父子兄弟通信 * 5
- `Redux` 原理 * 5
- `dom` 事件 * 5
- `Vdom` 原理及`diff`算法 * 5
- `Promise`机制 * 5
- 原型链及面向对象相关知识 * 4
- `HTTPS` 安全性 * 3
- 闭包 * 3
- 变量提升 * 3
- `HTTP` 状态码 * 2
- `Proxy` 和 其他一些 `ES6` 新特性
- 防抖与节流 * 1
- 深浅拷贝原理和实现 * 1
- `getComputedStyle` * 1
- `rem` * 1 (考察关于移动端布局到问题)
- 其他忘记了...
### 聊聊面试题
#### `this` 指向什么? 一起源于 `this`
《You dou't kown JavaScript》 这本书讲等最清楚了
首先是两个误解:
1. `this` 指向自身,根据英语翻译来说,这是一种自然的想法,其实不然,这是一个误解。
2. `this` 指向函数作用域,这个在某种情况下是正确的
其次是四种情况
1. 默认绑定
```js
var name = 'lufei'
function show() {
var name = 'namei'
console.log(this.name)
}
show()
// lufei
```
可以看出最后 `this` 绑定在全局对象上,所以结果是 `lufei`
2. 隐式绑定
```js
function show() {
var member = 'namei'
console.log(this.member)
}
var member = 'zoro'
var caomao = {
member: 'lufei',
showMember: show
}
caomao.showMember()
// lufei
```
这里最后通过 `caomao` 来调用这个函数,函数中的 `this` 则被绑定到 `caomao` 这个对象上
3. 显式绑定
```js
var caomao = {
member: 'lufei'
}
var member = 'zoro'
function show() {
var member = 'namei'
console.log(this.member)
}
show.call(caomao)
// lufei
```
通过 `call`,`apply`,`bind` 我们可以显示的改变 `this` 的绑定
4. `new` 绑定
最后一种是使用 `new` 调用函数,或者说是构造函数调用时
```
function SeaPoacherBoat(member) {
this.member = member
}
var caomao = new SeaPoacherBoat('lufei')
console.log(caomao.member) // lufei
```
这段代码会执行以下操作:
1. 创建一个全新的对象
2. 进行原型(`prototype`)链接
3. 将 新对象 绑定到函数调用的 `this`
4. 如果没有返回其他对象,则自动返回一个新对象
它们绑定的优先级是 `new` > 显示绑定 > 隐式绑定 > 默认,这也是很容易理解的,`new` 是生成了一个全新的对象优先级是最高的,显示绑定函数要起作用优先级一定要高于隐式绑定,默认绑定是最低的这个也无可厚非
最后一句话总结 **`this` 是运行期间绑定,和它声明的环境无关,只与调用它的对象有关**
#### 我们知道了它的指向,要想改变它怎么办呢?
改变 `this` 指向最直接的方法是 `call`, `apply`, `bind`,来看一下下面这段代码
```js
var name = '草帽海贼团'
var caomao = {
name: '路飞'
}
function printMember(arg1, arg2) {
var name = '娜美'
console.log(this.name)
console.log(arg1, arg2)
}
printMember('山治', '索隆') // 草帽海贼团 山治 索隆
printMember.call(caimao, '山治', '索隆') // 路飞 山治 索隆
printMember.apply(caimao, ['山治', '索隆']) // 路飞 山治 索隆
printMember.bind(caimao, '山治', '索隆')() // 路飞 山治 索隆
```
根据上面代码,`this` 现在指向的 `window` 对象,所以打印的是草帽海贼团而不是娜美
下面我们通过三种方式将 `this` 指针绑定到 `caomao` 这个对象,所以最后打印的都是路飞
很明显它们的区别无非就在于形式的不同,以`call`为基础来说,`apply` 是将后面的参数合成一个数组一起传人函数,`bind`最后返回的是一个函数,只有调用这个函数后才算执行。
有一种特殊情况就是把 `null` 和 `undefined` 作为`this`的绑定对象传人进去,这样的实际情况是采用的默认绑定原则
那么这有什么用途呢?常见用于**展开数组**来,看一段代码
```js
function print(a, b) {
console.log(a, b)
}
print.apply(null, [1, 2])
```
还有呢, 使用`bind`用于**柯里化**
```js
var foo = print.bind(null, 1)
foo(2)
```
也就是延迟执行最终的结果
**是不是所有函数都可以绑定`this` 呢?**
没错你可能很快想到就是箭头函数,普通函数来说,`this`是运行期绑定,而 `ES6`新规则里箭头函数并没有绑定 `this`,它是不存在 `this`的绑定的。那在箭头函数中存在`this` 会怎么样呢
```js
// 情景一,全局范围内调用箭头函数
var foo = () => { console.log(this) }
foo() // window
// 情景二,对象中调用
function monkey() {
var bar = () => console.log(this)
bar()
}
var obj = {
monkey: monkey
}
var other = {
obj: obj
}
other.obj.monkey() // { monkey }
```
之前很多人对箭头函数中的 `this` 都有一些误解,认为箭头函数中的 `this` 自身绑定或者是任何绑定在最终调用方式上。其实不然,从上面代码中我们可以看出箭头中的 `this` 绑定在离最近外层的对象 obj 上, 而不是最终调用对象 other 上。
#### 我们知道了 `this` 的指向的对象调用它的函数,那么调用它的时候到底发生了什么?我们需要知道JS执行机制到底是怎么样的
```js
console.log(run)
var run
function run(run) {
var run = 2
this.run = run
console.log(this, arguments)
console.log(run)
}
run('1')
console.log(run)
run = 3
console.log(run)
```

我们来分析一下它的运行方式
首先开始预解析,它有个规则是**变量声明提升**,我们可以知道函数声明会被提升到最上面,其次是变量。**声明后的变量不会重复声明**,所以第二次声明的变量不生效,我们手动来做一次转换
```
// 提升函数声明
function run(run) {
console.log(this, arguments)
var run = 2
this.run = run
console.log(run)
}
// 提升变量声明
var run
console.log(run)
run('1')
console.log(run)
run = 3
console.log(run)
```
所以第一个 log 会打印出函数而不是变量
开始按顺序执行下面的语句,此时遇到一个`run()`的调用
将`run`推入到执行栈中,进行以下几个步骤:
1. 绑定 `this` 和 初始化参数, 根据之前谈到的规则,`this` 绑定到调用它的全局对象 `window`,所以第二个 log 打印出 `window`对象和传递过来的参数
2. 同样在函数作用域中开始执行预解析,执行语句,函数中又定义了一个`run`。我们知道作用域原理是就近查找,存在一个屏蔽作用,`run` 函数作用域中的 `run` 此刻就是 `2`,所以第三个 log 会打印出 `2`, `this.run = run` 将全局中的 `run` 赋值为 `2`
3. 执行完成后,`run` 函数出栈,继续执行全局语句,`run` 的值已经被改变成 `2`,所以此刻第四个 log 也打印出了 `2`,最终又被改变成 `3`,所以最后一个 log ,打印出了 `3`
### 聊聊框架
Vue 和 React 你学哪一个?
哎呀!头疼, 能不能不要选啊,选 `React` 都说 `React`待遇好,大厂都爱`React`。
HR问,你`Vue` 多少经验?没有,bye~,不符合项目经验。那就学 Vue 呗,好上手,越来越流行。
HR问,你`React` 多少经验?没有,bye~,不符合项目经验。- -
大家仰望天空叹息到,学不动了...不怕学不动了,就怕学到的东西马上就过时了。
所以只有学到**真正底层基础思想**的东西才是真正重要的。
就算哪天没有这些框架了也能马上撸一个出来。
### 分享想法
最近找工作萌生了一个 开源 idea
- 痛点一:每次制作需要去找模版,去制作模版,而对于我们前端而言,更加擅长制作个性在线简历,可不可以考虑使用前端技术制作一个在线简历模版,开源免费供大家使用,大家开发自己的主题,大家不需要花钱去模版网站找那些并不适合自己的简历模版。
- 痛点二:对于前端来说,知识点太杂,面试考点太多。简历不单单是一个介绍,也是我们技术的一个总结。通过开发一个工具自动识别我们简历里面的技术点,模拟面试官给出一些面试考察的题目,让我们知道自己的不足,持续的学习和进步。
- 痛点三:对于简历,我们不应该一份简历进行海投,应该针对不同的岗位职责进行修改。所以我们可以开发一个输入职位地址,自动匹配简历与岗位中的差异并标记。这样我们就知道自身与岗位职责之间又多大差距,然后努力去接近它。同时我们需要对不同简历进行管理。
以上都是目前各大招聘网站和简历制作网站所没有的需求,这是我们技术人员和求职者自身的诉求。
而目前各大招聘网站更加注重招聘者的需求,往往不会管应聘者的需求,甚至要求各种付费的一些功能。
这个开源项目不是为了取代招聘网站,而是做一个从学习->工作->求职环节的一个补充。
需求
- v1.0.0 在线简历编辑功能,自己制作主题功能...
- v2.0.0 技术匹配识别功能,模拟面试功能,面试评分功能...
- v3.0.0 简历管理功能,职位匹配功能...
- v4.0.0 内推,代码练习,文章订阅...
技术实现
待定...
### 开源计划
https://github.com/suoyuesmile/resume-promotion

### 感谢阅读
第二次发文章,技术不到位多多海涵,里面技术点一部分来自 `You don't kown JavaScript`,另一部分来自平时看过的文章和平时的总结。以后会持续更新,也算是对自己技术相关原理的总结。推荐大家去认真看看这本书,相信你会有更大的收获。
高清电子书资源(请私聊,我单独发给你)

### 下期预告(待定)
#### 函数又来自哪?同时数组又来自哪?原型链又是什么东西?
#### 我们已经有了函数,为什么还要箭头函数?箭头函数仅仅是替代函数写法吗?
#### 我们知道 Vue 通过 `Object.defineProperty()` 劫持对象,那么它数组又是怎么劫持的呢?
#### 函数有个执行栈,我们知道同步函数是在执行栈里执行,那异步函数呢?
#### 说到异步,怎么实现异步的?异步与同步最大的区别在于什么?
#### 回调函数可以实现异步,为什么还要用 `Promise`?
#### `Promise` 异步就很好用了,为啥又有 `async` 和 `await`
------------------------------更新-------------------------------------
2019.7.3 更新 工作已确定,帮忙内推的朋友多谢了...去了一家小型待遇还不错的小公司
2019.12.6 更新 前一天还在谈论技术方案,今天公司倒闭缱散了,又成自由人了... 希望下家稳定些...
[掘金地址](https://juejin.im/post/5d14bb9a5188255d3f6ca8f6)
[issue评论区](https://github.com/suoyuesmile/suo-blog/issues/37)
================================================
FILE: articals/js/0001.md
================================================
对写文章这件事已经阁了3个月了,工作太忙很难抽出大块时间来总结写作。现在稍微闲赋些,准备好好对以前的技术做一下总结。为什么要写关于 `Promise` 呢?有以下 3 点
1. 之前面试时候,面试官问 `Promise` 问的比较多,实在要对它进行一个好好总结了,也是 JavaScript 比较难懂的一个技术点,平时我面试别人时候,也喜欢问 `Promise` 相关的,保证他在工作中能够熟练运用,自己能封装`axios`,能控制比较复杂的同步异步流程等...
2. 这三个月的工作中,自己也总是遇到关于 `Promise` 的一些运用场景,有时候又比较疑惑一些地方,也是为自己总结一下 `Promise`,以后在工作中更加运用自如。
接下来我从最基础的同步异步谈起,再通过图示谈他的语法,如何自己写一个`Promise`,再谈一下我在工作中遇到的 `Promise`,最后谈一下 新的语法 `async` 和 `await `与 `Promise` 的结合和比较
3. 网上关于`Promise`的介绍大多数是一些语法的介绍,大多没有结合场景和同步异步相关理解来谈。对于理解`Promise`的前世今生还是不够深入,不够具体。
### 理解同步异步
#### 理解JS执行原理
```js
console.log(1)
setTimeout(() => {
console.log(2)
}, 1000)
console.log(3)
```

如果延迟时间为`0`呢
```js
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
console.log(3)
```

```js
console.log(1)
setTimeout(() => {
console.log(2)
}, 2000)
console.log(3)
setTimeout(() => {
console.log(4)
}, 1000)
console.log(5)
setTimeout(() => {
console.log(6)
}, 0)
console.log(7)
setTimeout(() => {
console.log(8)
}, 1000)
console.log(9)
```

这里可以发现几点
- 1,3,5,7,9 是同步代码,6,4,8,2是异步代码,同步先于异步执行
- 单看同步代码,按位置顺序依次执行
- 单看异步代码,不同执行时间,越长越靠后执行。相同执行时间,按位置顺序依次执行
这里我们总结下同步与异步的规律
- **代码中存在异步函数,不管需要时间多久,都要在同步完成后执行**
- **同步按位置顺序执行;异步按时间长短执行,同样时间长短时,按位置顺序执行**
这是一个怎样的机制,JS引擎又是如何处理的呢?
我找到了一张这样的图

首先来解释一下这个图:这是JS的事件循环,分为3个步骤
1. 引擎将同步、异步函数按次序载入到执行栈里面
2. 执行栈的将异步函数放入异步线程里面
3. 线程根据任务完成时间依次推入任务队列中执行
如何处理异步呢, 最初的方法是使用回调
#### 理解回调
引入一下知乎上的高赞回答

现在我们把它用代码写出来看看
```js
function fetchSomething() {
console.log('去取货!')
}
function buySomething(callback) {
console.log('没货了!')
setTimeout(() => {
console.log('有货了!')
callback()
}, 1000)
}
buySomething(fetchSomething)
```

用简单的语言描述就是:**将一个函数作为参数传给另一个函数调用**
要搞清楚的一点是,回调和异步没有直接的关系,也可以同步回调,也可以异步回调。我们是通过回调这个机制来实现异步的操作,例如
```js
// 这个一个请求的异步函数,来实现异步操作
function getSomePeopleName(params, callback) {
setTimeout(() => {
let data
if (params === 'suo') {
data = 'yue'
}
console.log(callback(data))
}, 1000)
}
console.log(getSomePeopleName('suo', (data) => '我是回调函数:' + data))
```

回调函数的结果一定是在回调函数里面,如果我是这样一个流程呢?
A -> B -> C -> D
```js
function A(callback) {
console.log('开始执行A')
setTimeout(() => {
callback('A')
}, 500)
}
function B(callback) {
console.log('开始执行B')
setTimeout(() => {
callback('B')
}, 400)
}
function C(callback) {
console.log('开始执行C')
callback('C')
}
function D(callback) {
console.log('开始执行D')
setTimeout(() => {
callback('D')
}, 200)
}
A((a) => {
B((b) => {
C((c) => {
D((d) => {
console.log(a, b, c, d)
})
})
})
})
```

由上可知,回调函数有几个特定
- 使用回调函数嵌套,回调函数一定在上一个回调之后执行,用于可以控制流程,不会出现异步函数在执行顺序的混乱,即使同步异步函数混合
- 回调函数解构嵌套,代码不够清晰,俗称回调地狱
那么有没有更好的异步操作机制呢?这里我们就要谈到 `Promise` 了
### 深入 Promise
#### 为什么要有Promise?
#### ES6 标准中 Promise 语法
- `Promise`是一个构造函数
我们写一个简单的 `Promise` 看看
```js
new Promise(() => {})
```

```js
// 有一个参数executor的构造函数
// executor 也是一个函数,具有两个参数 resolve, reject 是两个回调函数,当执行到回调函数时,会执行
const isResolve = true
new Promise((resolve, reject) => {
if (isResolve) {
resolve()
} else {
reject()
}
})
```

可以看到 `Promise` 的状态从`pending`变成`resolved`
如果将 `isResolve` 置为 `false` 呢?

`Promise` 状态从 `pending` 变成 `rejected` 同时抛出一个异常,并且异常未被捕获,所以我们写Promise时候一定要加上catch 来捕获异常
```js
const isResolve = false
new Promise((resolve, reject) => {
if (isResolve) {
resolve()
} else {
reject('我拒绝你')
}
}).catch((err) => {
console.log('捕获异常', err)
})
```

现在我们把异常捕获到了,但是神奇的事情发生了,异常状态应该是`rejected`,怎么变成 `resolved`了呢?
我们或许很纳闷,这个放在后面讨论,我们先看看 `then` 怎么处理的
```js
const isResolve = true
new Promise((resolve, reject) => {
if (isResolve) {
resolve('通过')
} else {
reject('我拒绝你')
}
}).then((res) => {
console.log('resolve', res)
}, (res) => {
console.log('reject', res)
})
```

当`isResolve = false` 时

我们可以看到 `then` 是怎么处理的
1. 如果 前面`resolve()`调用,Promise状态为 `resolved`,`then`则执行第一个回调函数参数
2. 如果 前面`reject()`调用,Promise状态为 `rejected`,`then`则执行第二个回调函数参数
3. **then 执行回调函数之后Promise 的 状态都为 resolved**
下面进一步验证下
```js
const isResolve = false
new Promise((resolve, reject) => {
if (isResolve) {
resolve('通过')
} else {
reject('我拒绝你')
}
}).catch(err => {
console.log('我捕获到了', err)
}).then((res) => {
console.log('resolve', res)
}, (res) => {
console.log('reject', res)
})
```

果然呢,`catch`之后,`then`还会去执行,并且`resolve` 的参数为 `undefined`
问题是现在我们如何控制一个流程呢?`Promise`并不能直接给我们进行长流程的分支选择
下面有一个简单流程

尝试使用`Promise`去控制流程
```js
const process = [102, 204]
new Promise((resolve, reject) => {
if (process[0] === 101) {
setTimeout(() => {
console.log(101)
resolve(101)
}, 1000)
} else {
setTimeout(() => {
console.log(102)
reject(102)
}, 1000)
}
}).then(() => {
if (process[1] === 201) {
setTimeout(() => {
console.log(201)
}, 500)
} else {
setTimeout(() => {
console.log(202)
}, 500)
}
}, () => {
if (process[1] === 203) {
setTimeout(() => {
console.log(203)
}, 500)
} else {
setTimeout(() => {
console.log(204)
}, 500)
}
}).catch((err) => {
console.log(err)
})
```

下面我们试试更加复杂的流程
#### Promise 调用链:then 的作用
单个`Promise`没办法控制长流程,我们怎么将`Promise`形成一个控制链呢,需要理解`then`的返回在其中起到的作用
```js
promise.then(onFulfilled, onRejected)
```
- 接收两个参数,`onFulfilled` 在`promise`的 `resolved` 状态被调用
- 接收两个参数,`onRejected` 在`promise`的 `rejected` 状态被调用
- 返回值比较复杂,下面用表格列出来
| 序号 | 场景 | 返回的Promise状态改变为 | 回调函数参数值 |
| ---- | ----------------- | ----------------------- | ------------------------- |
| 1 | 返回1 个值 | resolved | 返回值 |
| 2 | 没有返回 | resolved | undefined |
| 3 | 抛出错误 | rejected | 错误 |
| 4 | resolved的Promise | resolved | 返回的Promise回调的参数值 |
| 5 | rejected的Promise | rejected | 返回的Promise回调的参数值 |
| 6 | pending的Promise | pending | 返回的Promise回调的参数值 |
我们可以看出想要控制`then` 的后续流程,必须通过这 6 种情况来控制
下面来测试一下 这 6 种情况是否符合我们的预期
1. 返回一个值
```js
new Promise((resolve, reject) => {
resolve('通过')
}).then((res) => {
console.log('1', res)
return res // then的返回值
}).then((res) => {
console.log('2', res)
})
```

2. 不返回

3. 抛出错误

4. resolved的Promise

5. rejected的Promise

6. pending的Promise

#### 看图写代码

分析特点:
这样就比较复杂了,还要考虑暂停的问题,但是根据我们上面测试到的,通过`then`的返回值控制流程也没有想象那么难
```js
task([101, 201])
function task(testPath) {
console.log('开始测试', testPath)
new Promise((resolve, reject) => {
console.log('000')
if (101 === testPath[0]) {
resolve('101')
} else {
reject('102')
}
}).then((res) => {
console.log('201', '上一个返回:' + res)
return new Promise(() => {})
}, (res) => {
if (202 === testPath[1]) {
console.log('202', '上一个返回:' + res)
return '202'
} else {
console.log('203', '上一个返回:' + res)
throw '203'
}
}).then((res) => {
console.log('301', '上一个返回:' + res)
return Promise.resolve('301')
}, (res) => {
console.log('302', '上一个返回:' + res)
return Promise.resolve('302')
}).then((res) => {
console.log('401', '上一个返回:' + res)
return Promise.reject('401')
}).catch((err) => {
console.log(err)
})
}
```



- `Promise.resolve()` 等同用 `new Promise((resolve, reject) => resolve())`
- `Promise.reject()` 等同用 `new Promise((resolve, reject) => reject())`
- `Promise.all()`
```js
// 作为参数promise 数组中,所有promise状态都是resolved才回调then第一个,只要有一个reject就reject
Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]).then((res) => {
console.log('通过', res)
}, (res) => {
console.log('拒绝', res)
})
```

返回值为全部值的一个数组
```js
Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.reject(3)]).then((res) => {
console.log('通过', res)
}, (res) => {
console.log('拒绝', res)
})
```

返回值仅返回拒绝的那个
- `Promise.race()`
```js
Promise.race([new Promise((resolve, reject) => {
setTimeout(() => {
reject(1)
}, 1001)
}), new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 1002)
})]).then((res) => {
console.log('通过', res)
}).catch((res) => {
console.log('拒绝', res)
})
```

race 相当于竞赛,多个`Promise`竞赛,谁先状态变成`resolved`和`rejected`,谁就执行下面的回调(根据时间来抉择)
## 下节预告
### Promise 到 generator 再到 async & await
### Promise 实际应用
#### 对异步请求封装
#### 流程控制
### 手写一个Promise
## 后记
总结一下,从同步异步到回调,在到`Promise`的语法和应用全部都可以在谷歌浏览器的控制台中输出测试。通过一点点代码的编写和输出,才会让我们思维更清晰,对`Promise`的理解更深刻。之后再总结工作中用到的`Promise`,以后也会慢慢将`async`、`await`结合P`romise`来谈关于 es6 以后异步相关的新特性。
================================================
FILE: articals/js/0002.md
================================================
Promise 处理异步代码相对于纯回调函数比较有序明了,但是对于同步函数写法还是挺繁琐的,下面有两种语法糖让异步更加清晰简洁
### 生成器
#### generator 函数
像指针一样下移,有点像在debug代码
```js
function* gen() {
yield 1
yield 2
yield 3
}
let g = gen()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
```

- 中断并完成下移`return()`
```js
function* gen() {
yield 1
yield 2
yield 3
}
let g = gen()
console.log(g.next())
console.log(g.next())
console.log(g.return('完成'))
console.log(g.next())
```

- 中断并抛出异常 `throw()`
```js
function* gen() {
try {
yield 1
yield 2
yield 3
} catch(e) {
console.log(e)
}
}
let g = gen()
console.log(g.next())
console.log(g.next())
console.log(g.throw('异常'))
console.log(g.next())
```

如果在异步函数里面使用 `generator` 函数怎样?
```js
function promise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('resolved')
}, 1000)
})
}
function* generate() {
promise().then((res) => {
console.log(res)
})
}
function fn() {
const res = generate().next()
console.log(res)
}
fn()
```

现在发现,异步代码不需要`then`回调了,看起来和同步函数写法一样
不过现在我们有了`async`、`await`函数,将生成器进一步封装,也可以说出语法糖
### async、await函数
使用Promise 写法写一个简单的例子
```js
function promise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('resolved')
}, 1000)
})
}
function fn() {
promise().then((res) => {
console.log(res)
})
}
fn()
```

改写成 `async` 函数
```js
async function asyncFn() {
console.log(await promise())
}
asyncFn()
```

**async 和 then 一样 可以达到同一个的效果,而且代码中没有回调函数的影子了**
多条异步链的情况
```js
function promise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
}
function fn() {
const p = promise().then((res) => {
console.log('第一个then', res)
return res === 2 ? new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 500)
}) : new Promise((resolve, reject) => {
setTimeout(() => {
reject(3)
}, 500)
})
}).then((res) => {
console.log('第二个then', res)
}).catch((err) => {
console.log('异常', err)
})
}
fn()
```

使用 async 改写
```js
function promise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
}
function promise2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 500)
})
}
function promise3() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3)
}, 500)
})
}
async function asyncFn() {
const step1_res = await promise()
console.log('第一步的结果:', step1_res)
try {
if (step1_res === 2) {
console.log('第二步的结果:', await promise2())
} else {
console.log('第二步的拒绝的结果:', await promise3())
}
}
catch(err) {
console.log('异常:', err)
}
}
asyncFn()
```

**用同步的代码写出了异步的效果**
================================================
FILE: articals/react/0001.md
================================================
2019年7月3日 天气小晴
现在早上9点,准时开始学习 React
### 半个小时过一遍 React 官方文档和教程 (9:00 - 9:30)

#### 扫视一遍它的基本介绍(5分钟)
我们看到下面介绍了三个 React 最重要的特性:
- **声明式**:它是说:React 创建用户界面很简单,当数据改变就会高效的更新和立即渲染组件。同时声明式可以让你的代码更加**可预测和易调试**。
怎么就是可预测的和易调试呢,暂时不是很理解
- **组件化**:可以创建一个管理自身状态的组件,通过组合这些组件来创造复杂的UI。因为组件逻辑是用 JavaScript 写的而不是模版,所以你可以传更丰富的数据到你的APP,同时还可以与 DOM 解藕。
- **一学多用**:不需要新的技术栈,重新写新的代码就可以开发一些新的功能。比如用作服务器渲染,`React Native` 编写移动端代码。

再往下扫,给出了 React 的 4 个简单的小例子
- **Helloworld(认识 组件 和 props)**
```jsx
class HelloMessage extends React.Component {
render() {
return (
Hello {this.props.name}
);
}
}
ReactDOM.render(
,
document.getElementById('hello-example')
);
```
看到它使用 ES6 的继承,继承了一个组件的基类,此刻充满好奇心的你,肯定会想我继承的这个 `React.Component` 到底是啥?(先留着之后去看 React 的源码)
没有写构造函数,说明它直接继承了基类的构造函数
里面有一个 `render` 函数 `return` 一个貌似模版的东西,这个就是最简单的组件
紧接着下面一个 `ReactDOM.render`函数接受两个参数一个是组件标签,一个`id`查找,现在大概的意思就是`render`函数就是要把组件渲染到找到`id`的这个地方。
组件标签里面的属性就给组件中作为`this.props`的属性。而且我们知道**父组件通过 `props` 可以将数据传递给子组件**。
- **定时器(认识 状态state)**
```jsx
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { seconds: 0 };
}
tick() {
this.setState(state => ({
seconds: state.seconds + 1
}));
}
componentDidMount() {
this.interval = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return (
Seconds: {this.state.seconds}
);
}
}
ReactDOM.render(
,
document.getElementById('timer-example')
);
```
我们看到第二个例子会发现里面多了很多其他的东西:
- 构造函数
- state
- 声明周期函数
发现我们可以在组件里面定义组件的构造函数,而且可以在构造函数里面初始化一个名为 状态(state)的东东。
**怎样去理解状态(state)这个词?**
我们假设组件是有生命的,它必然有个生命周期。大致分给三个阶段:
出生----> 变化----> 死亡 (对应) 创建----> 更新----> 销毁
伴随着生命周期的变化,它身上的属性也在发生变化,比如体温、寿命、心情、皮肤好坏、细胞生命力,甚至思维模式。我们可以把**这些变化的属性成为状态(state)**
但是作为它的创建者,我们即为它的上帝,所以需要控制管理它的状态的变化。所以我们在每一个阶段的变化中加了一个钩子,在相应的阶段去定义一些程序。
这些大概是我看完这段代码最粗浅的理解吧 。
- **Todo(一个小型增删改查APP)**
```jsx
class TodoApp extends React.Component {
constructor(props) {
super(props);
this.state = { items: [], text: '' };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
render() {
return (
TODO
);
}
handleChange(e) {
this.setState({ text: e.target.value });
}
handleSubmit(e) {
e.preventDefault();
if (!this.state.text.length) {
return;
}
const newItem = {
text: this.state.text,
id: Date.now()
};
this.setState(state => ({
items: state.items.concat(newItem),
text: ''
}));
}
}
class TodoList extends React.Component {
render() {
return (
{this.props.items.map(item => (
{item.text}
))}
);
}
}
ReactDOM.render(
,
document.getElementById('todos-example')
);
```
这个主要是对一下事件的一些运用,增加,删除数组的元素然后更新 dom 相关的操作。在React 中我们能很容易办到。
- **输入绑定(认识插件和双向绑定)**
```js
class MarkdownEditor extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = { value: 'Hello, **world**!' };
}
handleChange(e) {
this.setState({ value: e.target.value });
}
getRawMarkup() {
const md = new Remarkable();
return { __html: md.render(this.state.value) };
}
```
全部快速过一遍,尝试跟着敲一遍。哦,原来 React 是这么用的!!!
**总结一下 React 可以做什么呢?**
- 创建一个简单组件,父组件可以通过 `props` 传值给子组件
- 还可以创建拥有状态的组件,添加生命周期钩子管理组件状态
- 对数据增删改查,渲染在 `UI` 上
- 通过事件实现表单的双向绑定
差不多知道怎么用了,我们现在系统的来看一下文档,给以后深入研究铺路
接下来我们去系统的看看它相关的一些概念...
#### 开始进入DOC 系统性的学习下有关 React 的文档概念相关(20分钟)
1. 安装运行 React
安装运行有三种方式:
- 直接使用 script 标签引入
```html
```
- 在 node 环境下安装
```sh
npx create-react-app my-app
cd my-app
npm start
```
- 使用 CDN
2. React 的相关概念
- 简单的例子
```jsx
ReactDOM.render(
Hello, world! ,
document.getElementById('root')
);
```
大致理解为通过ReactDOM对象的render函数,将`Hello, world! `挂载在 id 为 root 下面
- 我们来了解下JSX,长这个样子:
```jsx
const element = Hello, world! ;
```
这个写法称为JSX, 它既不是字符串,也不是HTML,而是JavaScript 的一种扩展。
看到它有几个特点:
**它可以赋值给 一个引用**
**插入变量和函数**
它可以使用`{}`插入变量和函数
```jsx
const element = Hello, {name} ;
// or
const element = (
Hello, {formatName(user)}!
);
```
**插入在属性值中**
```jsx
const element = ;
```
**可以嵌套子节点**
```jsx
const element = (
Hello!
Good to see you here.
);
```
**防止xss攻击**
```js
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = {title} ;
```
**通过babel编译后长这个样子**
```jsx
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
```
- 渲染节点是怎么渲染的
不像浏览器DOM, React 节点是一些简单的对象,所以很容易创建,而且它主要关心的是需要更新的部分。
HTML 中始终存在一个唯一的 root 节点`
`,React DOM 回去管理这个root 下面的东西。
然后把 JSX 和 这个root节点,传递给 `ReactDOM.render()`,使用这个方法吧节点渲染出来
**那么它是怎么更新的呢?**
**React 节点是 immutable(不变的),一旦创建就不能改变它的子节点和属性**,所以唯一能更新的方法就是,创建一个新的节点,传到`ReactDOM.render()`重新渲染
现在就很清楚了,也就是它要更新UI,存在两个状态,一个是更新前到,一个是更新后的
而**React 仅仅会更新它必须更新的部分,也就是差异的部分**
这个可以放到后面研究,大概是使用 diff 算法比较 虚拟dom树 差异后来更新真实dom
现在我们大致清楚它是如何渲染和更新的了
- 组件和 props
组件和函数一样接受一个输入值,返回一个React element
**创建组件的两种方式:**
通过函数创建
```jsx
function Welcome(props) {
return Hello, {props.name} ;
}
```
通过ES 6 class 创建
```jsx
class Welcome extends React.Component {
render() {
return Hello, {this.props.name} ;
}
}
```
**组件也可以组合成一个复杂的组件**
```jsx
return (
);
```
**组件的扩展**
```jsx
function Avatar(props) {
return (
);
}
```
**父组件传值到子组件(props)**
> All React components must act like pure functions with respect to their props.
> 它这里强调了就像纯函数一样,所有 React 组件中`props`也是一样不能直接改变它
- 状态和生命周期
之前也提到,大概组件会有三个阶段,每个阶段具体会有几个生命周期方法,大概罗列一下:
**载入(挂载阶段 Mounting...)**:`constructor`(构造)、 static getDerivedStateFromProps(处于初始化挂载和更新之间,返回一个更新需要的对象或者null(不更新))、render(渲染 DOM )、componentDidMount(完成挂载)
**变化(更新阶段 Updating...)**:static getDerivedStateFromProps(同上)、shouldComponentUpdate(更新前,决定是否更新,返回false不更新)、render(更新 DOM)、getSnapshotBeforeUpdate(更新完成前捕获DOM的信息,然后返回一个参数给`DidUpdate` 使用)、componentDidUpdate(更新完成)
**销毁(卸载阶段 Unmounting...)**:componentWillUnmount(卸载前)
还有 2 个**错误处理**的钩子 static getDerivedStateFromError(),componentDidCatch()
具体了解访问 https://reactjs.org/docs/react-component.html
**如何使用生命周期钩子呢?**
将生命周期钩子添加到Class中
```jsx
class Clock extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
//...
}
render() {
return (
Hello, world!
);
}
}
```
常用做法
```js
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
```
在`componentDidMount()`设置定时器(异步操作),`componentWillUnmount()`清除相关异步操作
**如何更新状态呢?**
使用`this.setState({comment: 'Hello'});`更新状态,**不要直接修改state,因为这样不会重新渲染组件**
- 事件机制
事件处理和 DOM0 很相似但是有这些不同的地方
```
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
Click me
);
}
```
**驼峰而不是全小写**
**传递函数作为事件处理而不是字符串**
**需要使用 `preventDefault` 而不是返回`false`阻止浏览器默认事件**
我们可以通过 箭头函数将 `this` 绑定到点击事件处理函数上
```jsx
handleClick = () => {
console.log('this is:', this);
}
```
或者在初始化时候就绑定
```jsx
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// This binding is necessary to make `this` work in the callback
this.handleClick = this.handleClick.bind(this);
}
```
- 条件渲染
大概可以分为两种方式
**在render函数外部判断,通过return 返回或者引用不同的 React element**
```jsx
if (isLoggedIn) {
button = ;
} else {
button = ;
}
```
**在render函数内部判断,使用三元操作符来判断**
```jsx
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
{isLoggedIn ? (
) : (
)}
);
}
```
- 列表和 key
使用数组`map()`来渲染多组件
```jsx
const listItems = numbers.map((number) =>
{number}
);
```
渲染多组件需要使用 唯一的Key 来提高渲染性能,这个也和 diff 算法有关,大概是Key识别组件后,就可以直接复用了旧VDOM的节点了
```jsx
{props.posts.map((post) =>
{post.title}
)}
```
>We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state. Check out Robin Pokorny’s article for an in-depth explanation on the negative impacts of using an index as a key. If you choose not to assign an explicit key to list items then React will default to using indexes as keys.
**如果列表的顺序可能改变的话,这里不推荐使用 index 作为 keys,它可能造成性能问题**。大概是因为数组顺序改变了和 改变前的index 和 改变后的index 与组件不对应,这样也是没法复用的。(猜测)这里放在后续研究,这篇文章有讲 https://medium.com/@robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318
- 表单相关
这里提到了一个新的名词“"受控组件",大概意思就是它可以通过输入来控制和改变自己的状态,一般组件只能通过 `setState()`改变状态,而它可以通过输入改变,它是受到用户输入控制的。
大概有以下几种:` input`, `textarea`,`select` ...
我们这样可以通过这种特性来实现一个双向绑定, 这里暂时留着后面补充
- 状态提升
这个为来解决两个组件值同步问题的,有点像联级查询
通常来说,组件与组件之间是独立的,一个组件的状态改变不会影响另外一个组件。但是我们现在需要一个组件状态变化,另外一个组件状态也会变化,那该怎么办呢?
现在就又一种方式,给两个组件加一个相同的状态作为桥梁,一旦这个状态改变,同时改变这两个状态的其他状态,这样就达到了更新另一个组件状态的目的。
```jsx
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature});
}
handleFahrenheitChange(temperature) {
this.setState({scale: 'f', temperature});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
);
}
}
```
- 组合和继承
组合和继承真的是老生常谈的话题,设计模式里面推荐是使用组合,原因就在于**组合是松耦合的模式,继承是紧耦合的模式**。父亲的改变必定会影响到儿子的改变,所以会谨慎使用集成。
除非是父是很稳定不变的,像接口那样只是一个约束,而且约束原则不会频繁变化,使用继承是恰当的。但是一般情况使用组合实现松耦合。
3. 高级指南(这里还有很多地方不是特别理解,后续添加)
- Higher-Order Components(高阶组件)
- Optimizing Performance(性能优化)
- Reconciliation(协调)
- Refs (引用)
- Portals (门)
- Render Props(渲染 Props)
- Static Type Checking(静态类型检查)
- Uncontrolled Components(非受控组件)
- Web Components(web 组件)
4. 了解相关API(这里还有很多地方不是特别理解,后续添加)
- React
- React.Component
- ReactDOM
- ReactDOMServer
- Shallow Renderer
- JS Environment Requirements
- Glossary
5. 研究新特性(Hooks)
**Hooks 是什么?**
**为了让你放心大胆的使用,它给出了你使用Hooks的三大好处**
- 完全选择性加入:你不想用可以不需要学习和使用,使用它不需要重写你已有的组件代码
- 100%的向后兼容:它不会中断现有的改变
- 现在就可以用:16.8 之后就可以用了
**为啥要用Hooks?**
**它可以让你不使用class也可以使用状态和其他功能**
- 组件之间的逻辑很难重用
- 组件太复杂难以理解
- class会造成迷惑
可能这些问题,在真正投入使用,真正实践的时候才会遇到吧,暂时先不讨论
**看看它怎么去重用逻辑和去除 class 的?**
```
mport React, { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
You clicked {count} times
setCount(count + 1)}>
Click me
);
}
```
这里它通过 `useState` 来接受一个初始的状态,调用一个`setCount`函数来控制状态的逻辑.
点击按钮,就会调用这个函数,一旦调用这个函数,就触发了状态的变更。
也就是说我们可以任意时刻去管理状态了,函数组件中就可以了。
这个变更带来的一个直接的好处就是:我们完全可以将组件的某些处理状态的逻辑抽离出来单独处理。
这大概是我最初浅的认识,还需要实践中检验,之后会给出例子。
**看看它的API**

6. FAQ
- Virtual DOM and Internals(虚拟DOM 和它内部实现介绍-fiber)
之后再去研究
### 一个小时做一个后台项目的一个小订单管理页面
#### 提出一个需求(2分钟)
```js
/**
* 订单管理页面,包含3个部分
* 1. 筛选栏,包含日期选择、单选、输入、查询按钮
* 2. 表格,包含ID,商品信息,商品数量,金额,状态,操作
* 3. 分页
*/
```
#### 选择技术方案(3分钟)
尽可能的简单快速的实现它
- UI组件 antd
- 预处理 sass
- 获取数据 axios
- 模拟数据 json-sever
- 测试 jest
- 路由,状态管理,持续集成...暂不需要
#### 动手简单的实践一下(45分钟)
由于篇幅原因,具体实践请看下篇文章…这里跳转(之后添加链接)
#### 效果展示和调优(10分钟)
大概差不多是这种效果
 这个不是最终效果,还需要调优...
### 深入技术栈来研究 (10:00 - 11:00)
知乎上/掘金上搜索一些讲 React 比较好的书,诶,发现两本,很快搞到资源了
- 《React 精髓》

- 《深入 React 技术栈》

浅读这两本之后会写一篇博客,对里面知识和作者想法进行梳理
开始大致过一遍这两本书的内容
#### 总结下阅读这两本书的感觉
两本书写的都很好,而且它们刚好侧重点不同,前者侧重于里面**核心api的设计思想和理念**,后者侧重于**实际开发中的一些技术难点**
*之后会有精读的文章出来,请持续关注…*
### 深入源码(11:00 - 11:30)
#### 阅读 React 源码(15分钟)
进入 `github` 仓库 https://github.com/facebook/react
头一眼看到全是代码很懵,不知道看啥好
首先来搜一些目录结构图,我们要知道
- 哪个版本?
- 哪里上手?
- 我们应该看什么?
- 这些目录都是做什么用的?
- 需要掌握哪些东西?
- 看完后有什么收获?
*之后会有深入研究的文章出来,请持续关注*
#### 阅读 antd 源码 (15分钟)
现在还有点时间,我们跑去 antd 的github里面,看看它的各个组件是怎么写的
尝试自己写一个 `dialog` 和 `table` 以后再做
差不多粗略看完源码我们对 React 有些许了解了
现在开始了解一下它的相关生态了
### 逛逛生态圈(11:30 - 12:00)
#### 组件库(10分钟)
- antd

- antd pro

- 其他组件库
#### 状态管理(10分钟)
- flux
- redux
- saga
- react-redux
- dva
https://dvajs.com/

>React and redux based, lightweight and elm-style framework.
>D.Va拥有一部强大的机甲,它具有两台全自动的近距离聚变机炮、可以使机甲飞跃敌人或障碍物的推进器、 还有可以抵御来自正面的远程攻击的防御矩阵。
**Dva 是基于 React + Redux + Saga 的最佳实践沉淀**
```js
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
reducers: {
add(state) {
const newCurrent = state.current + 1;
return { ...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1};
},
},
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
subscriptions: {
keyboardWatcher({ dispatch }) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
},
});
```
- immutable(不变性的库)
#### 路由(3分钟)
- react-router(这个同Vue Router 区别不大)
#### node 相关(4分钟)
- Umi JS(一个插件,构建,测试,打包工具集)

- SSR
*后续会着重研究SSR相关原理和实践*
#### 其他(3分钟)
- Jest
(单元测试相关 facebook 出版,react 默认)
两个优势,第一个是一次安装全部拥有,
二是比之前 Karma 系列多了一个快照测试,主要是用了比对dom结构,进行回归测试相关
- 其他UI框架
*自制简单开源 React UI 计划中...*
### 后记
惨痛经历:本来写了 3400 多字,有次突然发现少了一些篇章,然后急忙按撤回,撤回最后成了全空状态。自动保存后,草稿全被覆盖,丢失了,后来又重新写的,悲惨。
提醒大家如果想写博客,一定不要在线写,直接本地写,写完复制到在线,加图片后直接发布。
### 感谢阅读
感谢你能阅读到这个地方,还没放弃…
**作者能力有限,里面或许有些许文字错误,和一些技术上错误的理解,欢迎在评论中指出改正**。
本篇文章只是记录了本人 3 个小时学习 React 的经历,可能对那些学习的比较零散而没有章法的人有一些帮助,通过**分享出一个学习路线,让他们系统性的构建知识**。要想真正掌握 React 里面的相关技术还是要花很长时间的。
如果你想去深入研究下它的话,希望你能看完本篇文章后,可以花更多时间去琢磨里面的一些点和程序的设计。
最后希望各位的技术都有长足的进步,而不是仅仅局限在一些工作的业务上。
### 下期预告
找工作结束了,下周大概就会正式入职了,写作博客的时间会相对比较少,但是还是希望多多去研究学习,总结一下学到的点,分享给大家。
下期博文的话,内容大概是**关于 Vue 的一些思考和在工作中运用的实践总结**(时间待定)
还有就是关于上一期后面面试题的研究,也会慢慢研究,之后会出一个系统性的总结(待定)
### 原创声明
本文完全原创,全部是经过作者3个小时的学习,和一天多思考总结出来的成果。允许转载,但需标明**作者和原文链**,谢谢!
[issue评论区](https://github.com/suoyuesmile/suo-blog/issues/38)
================================================
FILE: articals/vue/0001.md
================================================
2019年6月6号,为了爱情,我离开工作了一年多的广州来到了杭州这个互联网城市。开始我的前端面试之旅...
放下拧螺丝的扳手,开始造起了飞机...
面试的第一家,一开始就问 **Vue 双向绑定怎么实现**。
一脸蒙蔽,之前看过源码,但是没有深入研究,只回答出了使用 `Object.defineProperty`
```js
Object.defineProperty(obj, prop, {
// ...
get: function() {}
set: function() { // ... }
})
```
要是再给我一次机会我会这样回答
**Vue 双向绑定,使用数据劫持和发布订阅模式实现的**
然后我再画一个图来描述整个实现过程是怎样的
vue2.0 采用的是 `Object.defineProperty` 进行数据劫持的
主要实现原理是使用描述对象中的set方法进行拦截,并发送订阅器信号
```js
// ...
let dep = new Dep()
return Object.defineProperty(obj, prop, {
// ...
get: function(key) {
dep.target = this
dep.addSub()
// ...
}
set: function(newVal) {
val = newVue;
// 发送一个dep信号
dep.notify()
// ...
}
})
```
而 vue3.0 中可能会采用 `Proxy` 来实现数据劫持
```js
let target = {}
let p = new Proxy(target, {
set: function() {
//...
},
get: function() {
//...
}
})
```
*为啥呢?*
我们知道 `Object.defineProperty` 是有局限性的,他的拦截的 target 就是单纯的对象的key的值
所以呢,对象属性的删减,数组,数组长度的改变,它就没法进行劫持了
而 ES6 的新特性,`Proxy`,它可以拦截对象,数组几乎一切对象包装类型
但是 `Proxy `没法兼容 IE,所以 Vue3.0 底层还是采用 `Object.defineProperty`
而 使用 `Proxy` 作为一个 `api `,也就是说:
我们不兼容IE, 就大胆用 `Proxy` 双向绑定而且不会有属性删减和数组劫持不到的问题
我们要兼容IE,就用原来的双向绑定,但是要注意它的不能劫持部分变化的缺陷
从上图我们可以看到,`Observer` 观察了 `object` 值的变化,这是一种观察者模式
而 `Observer `将观察的信号发布给订阅器这是一种 发布订阅模式
#### 那么观察者模式与发布订阅模式有什么区别呢?
我们先谈观察者模式
*什么是观察者模式,首先有一个观察者,一个被观察者,被观察者这里是数据,而观察者是Observer,被观察者发生变化时,主动发生信号给被观察者*
按照这个思路来说,我们也能想象尤大,当时设计双向绑定时候,思考怎样去监听这个数据的变化,也就是如何使用观察者模式来实现,而恰好对一个对象的处理中有个对象方法我们可以使用,就是 `Object.defineProperty`
#### 假如没有这个方法我们怎么实现呢?
这就是 angular 的另外一种实现方式脏检测,也就是不停的轮询数据的变化情况,显然脏检测对性能消耗比较大
#### 再谈谈发布订阅模式
在软件架构中,发布订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。
这里很明显了,区别就在于,不同于观察者和被观察者,发布者和订阅者是互相不知道对方的存在的,发布者只需要把消息发送到订阅器里面,订阅者只管接受自己需要订阅的内容
由此发布订阅模式是一种松耦合的关系,`watcher` 和 `Observer` 之间是互相不受影响
## 后记
感谢观看,第一次发文章
[issue评论区](https://github.com/suoyuesmile/suo-blog/issues/36)
================================================
FILE: articals/vue/0002.md
================================================
# 前言
之前使用过 Vue 开发后台、中台项目,也做过移动端 H5,弄过一点小的前端架构。每做一个项目都会收获了不一样的经验和理解。下面我把这些点点滴滴的经验总结下来,做一个系列的文章分享和阶段性的总结。
> 常规操作,先点赞后观看哦!你的点赞是我创作的动力之一!
## 概览

## 问题
> 我将从 16 个方面来论述 vue 开发过程中的一些技巧和原理。当然由于篇幅有限,先论述前 8 个问题,下节将完成全系列内容。
本篇文章将围绕下列问题进行论述:
- **如何规范你的 git 提交,并自动生成并提交日志?**
- **如何配置和使用 Sass 和 PUG 提升你的编码效率?**
- **如何处理你的代码风格问题,以及如何使用 perttier 与 eslint 解决效率风格两难问题?**
- **如何管理页面的路由,如何编写异步路由?**
- **如何编写组件,引入组件库?**
- **如何管理你的资源,如何引入图标,样式?**
- **如何封装你的 axios,管理你的api?**
- **如何使用 mock 模拟你的数据,实现真正意义的前后端分离?**
# 实践
> 实践之前:我希望你有如下准备,或者知识储备。
> - 了解 `npm/yarn/git/sass/pug/vue/vuex/vue-router/axios/mock/ssr/jest`
的使用和原理。
> - 当然上面知识不了解也没关系哈哈哈,文章中会提到大致用法和作用。
## 如何规范 git 提交
> 代码提交记录是一个很好的代码修改日志。规范的代码提交记录,不管在平时代码开发维护过程中,还是在定位 bug 或者回退版本来说都是极为重要。
### 原理
两种做法:
- 自己手动规范 git 的提交原则或者团队统一制定。这个靠自觉,好习惯养成之后就没问题来
- 使用插件规范,比如下面这种
为了规范提交,我使用了如下插件:
- commitizen
- conventional-changelog
- cz-conventional-changelog
- conventional-changelog-cli
#### 解决方案
#### 安装系列插件依赖
```sh
yarn add -D commitizen conventional-changelog cz-conventional-changelog
```
> 安装依赖时,要注意是否是生产环境需要的。显然 `commitizen` 只在开发环境中使用。`-D` 只在 `dev` 环境使用
#### 配置依赖路径
在 `package.json` 中添加配置
```js
{
//...
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
```
在命令行中输入
```sh
git add -A
git-cz
```
出现了交互输入方式,规范你的 `commit` 输入格式

#### 生成 CHANGELOG
```sh
npm i -g conventional-changelog-cli
```
增加一个npm 命令,快速生成日志
```js
"genlog": "conventional-changelog -p angular -i .github/CHANGELOG.md -s"
```
使用`yarn`命令生成日志
```sh
yarn genlog
```
自动生成的log
```md
# 0.1.0 (2019-12-27)
### Features
* **git:** 增加commitizen工具规范提交 ([58e3937](https://github.com/suoyuesmile/suo-design-pro/commit/58e39370aa838fd99312f73b37d092ffadc85990))
```
## 如何管理代码风格
> 较统一的代码风格利于阅读,也利于协作。
### 原理与解决方案
使用 eslint 约束基本风格和语法,使用 prettier 自动格式化你的代码。
### 实践
#### 安装 eslint 依赖
```json
{
"eslint": "^5.16.0",
"eslint-config-standard": "^6.2.1",
"eslint-friendly-formatter": "^2.0.7",
"eslint-loader": "^2.1.2",
"eslint-plugin-html": "^2.0.1",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^2.3.1",
"eslint-plugin-vue": "^5.0.0"
}
```
使用两个插件,一个 `plugin:vue/essential`,一个是 `standard`。
`vue/essential` 为了在 vue 里面也可以生效。另一个是 `standard`。
[standard 标准文档](https://github.com/standard/standard/blob/master/docs/RULES-zhcn.md#javascript-standard-style)
> 使用 recommend 也可以,采用推荐 lint,更加轻量化
```js
module.exports = {
root: true,
env: {
node: true
},
extends: ['plugin:vue/essential', 'standard'],
rules: {
quotes: ['error', 'single'],
indent: ['error', 2, { MemberExpression: 'off' }],
'arrow-parens': 0,
'no-loop-func': 2,
'space-before-function-paren': ['error', 'never'],
indent: ['error', 2, { SwitchCase: 1 }]
},
parserOptions: {
parser: require.resolve('babel-eslint'),
ecmaVersion: 2018,
sourceType: 'module'
}
}
```
#### 可以自定义 rules 的规则
> rules 的规则 { 规则名:[是否关闭/规则等级,配置的值,只对部分配置] }
> `indent: ['error', 2, { SwitchCase: 1 }]` 兼容 prettier,prettier 会将代码格式化成 eslint 报错的情况。
> 规则等级:0 关闭 1 警告 2 报错
### 使用 prettier
#### 配置 prettier 文件
```js
{
"printWidth": 150,
"singleQuote": true,
"trailingComma": "none",
"semi": false,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "always",
"proseWrap": "preserve",
"overrides": [
{
"files": ["*.json", ".eslintrc", ".tslintrc", ".prettierrc", ".tern-project"],
"options": {
"parser": "json",
"tabWidth": 2
}
},
{
"files": "*.{css,sass,scss,less}",
"options": {
"parser": "css",
"tabWidth": 2
}
},
{
"files": "*.ts",
"options": {
"parser": "typescript"
}
},
{
"files": "*.vue",
"options": {
"parser": "vue"
}
},
{
"files": "*.md",
"options": {
"parser": "markdown"
}
}
]
}
```
#### 开启 vscode 自动格式化
```
{
// prettier
"prettier.singleQuote": true,
"prettier.semi": false,
"prettier.tabWidth": 2,
"[javascript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
```
## 如何提升编码效率
### 原理与解决方案
我主要从 3 个方面来做一些编码效率上的改进
- 升级你的 vue-cli 减少 webpack 配置的成本
- 使用 sass,利用里面函数、mixins、变量提升 css 文件的复用
- 使用 pug,减少 html 的代码编写量
#### 实践
> `vue-cli3+、vue-cli4+` 相比于 `vue-cli2+` 最大的改变就是将约定俗称的配置,全部公共化了,也就是做了一次二次封装。这样的好处在于,我们不必要在繁多的配置代码中寻找需要的配置。
简单新建一个配置入口就能操作我们大多数想要的功能。在 `root` 目录下新建一个 `vue.config.js` 文件,作为我们 `webpack` 的配置文件。
**初始化 vue 配置**
```
const autoprefixer = require('autoprefixer')
module.exports = {
publicPath: process.env === 'production' ? '' : '/',
outputDir: 'dist',
assetsDir: 'static',
filenameHashing: true,
lintOnSave: true,
runtimeCompiler: false,
transpileDependencies: [/\/node_modules\/vue-echarts\//, /\/node_modules\/resize-detector\//],
productionSourceMap: false
}
```
简单的配置完成后,我们引入一个 `sass` 工具用于编写 `sass`文件
> 用法见 sass 参考资料!
### 使用 Sass
#### 安装与使用
```sh
yarn add -D sass sass-loader
```
### 如何处理样式
在 `assets` 目录中建立一个 styles 文件专门来存放样式文件,新增入口`index.scss`文件,便于 JavaScript 引入,增加 utils.scss、reset.scss、varibles 文件。
> 这些样式工具都是为了提升我们 `scss` 开发效率,具有畅快的开发体验!
#### 使用 varibles 变量文件
为了提升我们代码的可读性,复用性。使用 `sass` 变量必不可少。
还有一点就是利于全局修改样式,如果需要更换皮肤这个功能,我们只需要更改全局的主题色,即可更换主题,那样更加方便。
```scss
// 主题色
$color-red: #ff3333;
$color-purple: #ff33a9;
$color-orange: #ff8833;
$color-blue: #3377ff;
// 文字色
$color-black: #000;
$color-dark: #333;
$color-deep: #555;
$color-pl: #999999;
$color-weak: #B3B3B3;
$color-white: #fff;
// 背景色
$bg-bar: #F9F9F9;
$bg-page: #F3F3F3;
$bg-page-light: #F9F9F9;
```
使用变量之后,sass 文件不会直接生效,至少在 vue 文件 里面是访问不到的。
需要在 `vue.config.js` 里面增加如下配置。
```js
module.exports = {
// ...
css: {
sourceMap: true,
loaderOptions: {
sass: {
prependData: `
@import "@/assets/styles/variable.scss";
`
}
}
}
}
```
#### 覆盖默认样式
常规操作, 引入 reset.scss 将默认样式覆盖掉
```scss
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
html, body {
width: 100%;
height: 100%;
overflow: auto;
margin: 0;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
```
#### 使用样式工具集
有时候我们发现,光是引入变量还不够,变量工具只能允许我们在 css 类文件中使用。假如我想着模版中直接使用样式,有没有更快的方案呢?
当然有的,我们可以自定义一个常用的样式工具集。设置一些背景颜色、字体颜色、盒子模型中的常规操作。
> 要是有设计规范更好哦,我也是常常向设计师提出要求,一定要制定出一套产品的设计规范。
```scss
// utils 工具
// 颜色
.bg-red {background-color: $color-red!important;}
.bg-purple {background-color: $color-purple!important;}
.bg-orange {background-color: $color-orange!important;}
.bg-blue {background-color: $color-blue!important;}
.color-red {color: $color-red!important;}
.color-purple {color: $color-purple!important;}
.color-orange {color: $color-orange!important;}
.color-blue {color: $color-blue!important;}
.text-black {color: #000;}
.text-dark {color: #333;}
.text-deep {color: #555;}
.text-weak {color: #B3B3B3;}
.text-white {color: #fff;}
// 字体
.f10 {font-size: 10px;}
.f12 {font-size: 12px;}
.f14 {font-size: 14px;}
.f15 {font-size: 15px;}
.f17 {font-size: 17px;}
.f20 {font-size: 20px;}
.f24 {font-size: 24px;}
// 文字对齐
.tl {text-align: left;}
.tc {text-align: center;}
.tr {text-align: right;}
// 浮动与清除浮动
.fl {float: left;}
.fr {float: right;}
.fix {*zoom: 1;}
.fix:after{display:table; content:''; clear:both;}
// 显示
.dn{display:none;}
.di{display:inline;}
.db{display:block;}
.dib{display:inline-block;}
.dt{display:table;}
div.dib{*display:inline; *zoom:1;}
.vm {vertical-align: middle;}
.vib {display:inline-block; vertical-align: middle;}
// 定位
.pr {position: relative;}
.pa {position: absolute;}
.pf {position: fixed;}
// 盒子模型
.ml4 {margin-left: 4px;}
.mr4 {margin-right: 4px;}
.mt4 {margin-top: 4px;}
.mb4 {margin-bottom: 4px;}
.ml8 {margin-left: 8px;}
.mr8 {margin-right: 8px;}
.mt8 {margin-top: 8px;}
.mb8 {margin-bottom: 8px;}
.ml12 {margin-left: 12px;}
.mr12 {margin-right: 12px;}
.mt12 {margin-top: 12px;}
.mb12 {margin-bottom: 12px;}
.ml16 {margin-left: 16px;}
.mr16 {margin-right: 16px;}
.mt16 {margin-top: 16px;}
.mb16 {margin-bottom: 16px;}
.ml20 {margin-left: 20px;}
.mr20 {margin-right: 20px;}
.mt20 {margin-top: 20px;}
.mb20 {margin-bottom: 20px;}
.ml24 {margin-left: 24px;}
.mr24 {margin-right: 24px;}
.mt24 {margin-top: 24px;}
.mb24 {margin-bottom: 24px;}
.ml10 {margin-left: 10px;}
.mr10 {margin-right: 10px;}
.mt10 {margin-top: 10px;}
.mb10 {margin-bottom: 10px;}
.ml15 {margin-left: 15px;}
.mr15 {margin-right: 15px;}
.mt15 {margin-top: 15px;}
.mb15 {margin-bottom: 15px;}
// 按钮禁用
.disabled{outline:0 none; cursor:default!important; opacity:.4; filter:alpha(opacity=40); -ms-pointer-events:none; pointer-events:none;}
```
#### 增加样式入口文件
最后一步,新建一个入口文件,将样式工具类全部导入进来,供主程序引入。
```scss
// index.scss 文件
import './reset.scss';
import './varibles.scss';
improt './utils/scss';
```
在 `main.js` 中直接引入 `index.scss`
```js
import '@/assets/styles/index.scss'
```
### vue 中写样式要注意哪些方面,有哪些技巧呢?
#### 避免全局污染
在页面中写 `css/scss` 加上 `scoped`,`scoped` 的功能就是使页面的样式是局部的,不让影响其他页面的样式。
#### bem 规范
我们大多数人时候会遇到问题,**样式嵌套太多了怎么命名**
> BEM是块(block)、元素(element)、修饰符(modifier)的简写,由 Yandex 团队提出的一种前端 CSS 命名方法论。
名字太长易读性太差
```scss
.cardbox {
.cardbox-card {
.cardbox-card-wrapper
.cardbox-card-wrapper-header {
.cardbox-card-wrapper-header-title {
// ...
}
}
.cardbox-card-wrapper-body{
.cardbox-card-item {
.cardbox-card-item-title {
// ...
}
}
}
}
}
}
```
#### bem 使用方式
`block-name__element-name--color`
- 区分块,子元素,修饰元素
- 块,页面中独立的单元
- 子元素,块里面的儿子 `card__item` 使用 `__` 连接
- 子元素长命名使用 - 连接
- 修饰(易变的)`card__item--warning` 使用 `--`
我们使用 bem 改造样式
```scss
.cardbox {
&__header {
&__title {
//...
}
}
&__body {
&__item {
&__title {
//...
}
}
}
}
```
> bem 一般推荐子元素嵌套尽量在2-3层以内
但是我们发现样式子元素嵌套有点多,使用了两重子元素嵌套。
大致原理是**尝试分离父子元素的关系,把卡片本身当作一个块看待。**
下面来试着去减少嵌套:
```scss
.cardbox {
&__header {
&__title {
//...
}
}
&__body {
.card {
&__title {
//...
}
}
}
}
```
现在编写样式效率提高也更加规范了,那么编写 HTML 也是有很多累赘的代码。
比如大多数标签都是前开后闭的。通过 pug 我们可以省略很多字符的敲打,下面我们谈谈如何使用 pug 编写模版。
> 当然喜欢哪种 HTML 编写风格见人见智啦,我自己更加倾向 pug,那种缩进和简洁的表达,有种在写 scss 的感觉。
### 如何使用 pug
#### 类似 sass,首先安装 pug 和 pug 的 loader
```
yarn add -D pug pug-html-loader pug-plain-loader
```
#### 完成配置
```js
module.exports = {
// ...
chainWebpack: (config) => {
config.module
.rule('pug')
.test(/\.pug$/)
.use('pug-html-loader')
.loader('pug-html-loader')
.end()
}
}
```
#### 编写 pug 代码
使用 scss 工具与 pug 完美搭配,少写很多代码
```html
// 登陆
.login
h1.login__title.ml15 注册/登陆
.login__form.mt15.ml15
van-field.login__form__input(placeholder="输入手机号" v-model="phone")
.login__form__protocol.mt15
.login__form__protocol__tips.dib.text-weak 注册或登录即表示同意
.login__form__protocol__name.dib.color-orange 《用户协议》
app-button.mt15(size="large"
theme="orange"
:disabled="phone.length !== 11"
@click="handleSubmit"
) 下一步
```
我们已经引入了样式,接下来我将谈谈其他资源的引入
## 如何管理你的资源
### 原理与解决方案
我暂时把资源分为下面几种
- 字体
- ICON
- 图片
- 样式

把他们各自新建一个目录,都放在 assets 目录下面分门别类,供其他地方调用。
使用 alias 更好重命名,使之更便捷的访问到。
增加 `vue.config.js` 配置 ,设置`assets`别名
```js
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
//...
chainWebpack: (config) => {
config.resolve.alias.set('@', resolve('src')).set('@assets', resolve('src/assets'))
}
}
```
### ICON
#### 引入 iconfont
1. [iconfont](https://www.iconfont.cn/manage/index) 阿里图标项目中下载,将整个项目图标包一起下载下来

2. 引入 iconfont 样式
需要下面四个文件
- `iconfont.eot`
- `iconfont.ttf`
- `iconfont.woff`
- `iconfont.woff2`
3. 项目中引入 iconfont
让 `icon` 组件化
```html
```
#### 引入图片作为 ICON
让图片组件化,我们再来写一个 img 组件
```
img(
:src="require(`@/assets/images/${name}.png`)"
v-bind="$attrs"
v-on="$listeners"
:style="{'width': width ? width + 'px' : size + 'px', 'height': height ? height + 'px' : size + 'px' }")
```
## 如何管理你的路由
### 原理与解决方案
使用 vue-router,使用`import()` 生成异步路由,只有在访问时候才会加载模块。
为什么使用 `import()` 会异步加载模块?
> MDN:在您希望按照一定的条件或者按需加载模块的时候,动态import() 是非常有用的。而静态型的 import 是初始化加载依赖项的最优选择,使用静态 import 更容易从代码静态分析工具和 tree shaking 中受益。
说白了就是起到一个**按需加载**的目的。现在大多数实现的按需加载,基本都是依赖 `import()` 这个方法。
#### 安装 vue-router
```sh
yarn add vue-router
```
安装完 `router`,在编写 `router` 先创建页面
#### 新建一个空页面
src 目录下新增 views 目录存放页面文件。创建 index 目录和 home 页面
```html
.home 首页
```
#### 编写路由
```js
const routes = [
{
// 首页
path: '/',
name: 'index',
redirect: '/home',
component: App,
children: [
{
// 首页
path: 'home',
name: 'home',
// 路由懒加载
component: () =>
import(
/* webpackChunkName: "index" */ '../views/index/home.vue'
)
}
]
}
]
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes: routes,
base: process.env.BASE_URL,
props: true
})
export default router
```
#### 为了消除 # 显得路径更加好看简洁,我们采用 history 模式,但是 history 模式有个问题就是,异步路由没有缓存在页面中,第一次进入页面会找不到
在 vue.config.js 中增加配置,开发环境可以访问到,恢复正常
```js
module.exports = {
// ...
devServer: {
historyApiFallback: true
}
}
```
> 关于路由还有很多可以研究到地方,可以自行研究哦!
## 组件化开发
### 原理与解决方案
一般来说,我们根据组件的复用度,分给基础(公共)组件和业务组件。
为了节省时间,快速开发,这里基础组件大部分引用开源组件。当然不能直接就用哦。
一般要进行二次封装,也就是高阶组件开发。
1. 通过修改和覆盖当前组件的样式来达到修改样式的作用。
2. 通过拦截事件来更改js的逻辑。
下面我们先引入 `vant` 组件
### 实践
#### 引入 vant
```sh
yarn add vant
```
### 对基础组件进行二次封装和改造
下面 7 步来写好一个公共组件
1. 新建一个 `components` 目录来存放基础组件
2. 基础组件命名为 `app-xxx` 或 `appXxx`,新建一个 `app-button` 目录,新建 `index.vue`
3. 根据设计稿设计和编写组件
> 编写组件之前首先要设计组件,根据组件的不变性和可变性原则编写。不变性是组件的核心,可变性根据参数对组件对相关部分进行调节,实现可选择的功能。
4. 实现组件
```html
div.dib
van-button.btn(
@click="$emit('click')"
:class="getClass" v-bind="$attrs"
:style="{'width': size === 'large' ? '345px': '', 'backgroundColor': getBgColor, borderColor: getBgColor, color: getBgColor}")
slot
```
> ::v-deep 样式深覆盖,scoped 情况下覆盖组件样式,不改变其样式
5. 写好基础组件 README,为什么要写文档呢?如果多人开发同一个项目,基础组件会被其他人引用。方便其他人使用,所以要写文档。
> 一句话言:只要可能被其他人用的公共方法和组件,注释或文档是很重要的,对自己的代码负责哈。
其他用法参照 vant
6. 全局引用,基础组件许多页面都会用到,将其设置成全局组件,其他地方就不必再引用了哦。
新建一个文件 `global` 存放全局组件注册,在 `main.js` 引入
```js
import Vue from 'vue'
import appButton from '@/components/app-button'
Vue.component('app-button', appButton)
```
7. 写好 `demo`,即使暂时不写单元测试,也要写好一个 `demo`,使之能正常的运行
添加 demo 页面和路由
```html
div(class="base")
// 按钮组件
app-button.mt4(theme="blue") 确认支付
app-button(theme="orange") 确认支付
app-button(theme="purple") 确认支付
app-button.mt4(theme="red") 确认支付
app-button(theme="grey") 确认支付
app-button.mt4(theme="blue" size="large") 修改地址
app-button.mt4(theme="orange" size="large") 修改地址
app-button.mt4(theme="purple" size="large") 修改地址
app-button.mt4(theme="red" size="large") 修改地址
app-button.mt4(theme="grey" size="large") 修改地址
app-button.mt4(theme="blue" type="minor") 确认收货
app-button(theme="orange" type="minor") 确认收货
app-button(theme="purple" type="minor") 确认收货
app-button(theme="red" type="minor") 修改地址
app-button(theme="grey" type="minor") 修改地址
app-button.mt4(theme="blue" type="rect") 确认收货
app-button(theme="orange" type="rect") 确认收货
app-button(theme="purple" type="rect") 确认收货
app-button(theme="red" type="rect") 修改地址
app-button(theme="grey" type="rect") 修改地址
```
#### 实现效果

## 如何封装请求
### 原理与解决方案
基本上就是对 axios 的封装,封装主要有两个目的。
- 修改一些基础的配置:请求地址,超时,其他的杂七杂八的
- 统一操作:统一处理错误,统一处理请求参数和格式,响应参数和格式。统一处理 message,统一拦截挂载等等。
网上已经有很多类似的文章了, 我这里给出我常用的封装方案。
### 实践
#### 根据不同环境设置请求地址
```js
// .env-default.js 文件
// 不同环境访问不同的路径
const api = {
develop: 'http://xxxx:8080',
mock: 'http://xxxx',
feature: 'http://xxxx',
test: 'http://xxxx',
production: 'http://xxxx'
}
export const baseURL = api[process.env.NODE_ENV || 'dev']
```
> 因为每个人开发环境,mock环境不一定相同,这个文件建议 gitignore忽略掉。模版可以写在 readme 文档中,启动项目时加入文件。
#### 新建一个 `utils` 工具
我们现在将 axios 封装成我们自己需要的配置,然后定义四个常用的请求方法供调用
```js
// utils/http.js 文件
import axios from 'axios'
import { baseURL } from '../../.env-defalut.js'
// axios 配置
const defaultBaseUrl = 'http://localhost:8080/'
// 默认超时时间
axios.defaults.timeout = 15000
// 数据接口域名统一配置.env
axios.defaults.baseURL = baseURL || defaultBaseUrl
// http request 拦截器
axios.interceptors.request.use(
(config) => {
config.headers = {
}
return config
},
(err) => {
return Promise.reject(err)
}
)
// http response 拦截器
axios.interceptors.response.use(
(response) => {
return response
},
(error) => {
const data = error.response.data
return Promise.reject(data || error)
}
)
export default axios
/**
* fetch 请求方法
* @param {*} url
* @param {*} params
*/
export function fetch(url, params = {}) {
return new Promise((resolve, reject) => {
axios
.get(url, {
params: params
})
.then((response) => {
resolve(response.data)
})
.catch((err) => {
reject(err)
})
})
}
/**
* post 请求方法,发送数据格式 json
* @param {*} url
* @param {*} data
*/
export function post(
url,
data = {},
config = {
transformRequest: [
function(fData, headers) {
headers['Content-Type'] = 'application/json'
return JSON.stringify(fData)
}
]
}
) {
return new Promise((resolve, reject) => {
axios.post(url, data, config).then(
(response) => {
resolve(response.data)
},
(err) => {
reject(err)
}
)
})
}
/**
* patch 请求方法,发送数据格式 json
* @param {*} url
* @param {*} data
*/
export function patch(url, data = {}) {
return new Promise((resolve, reject) => {
axios
.patch(url, data, {
transformRequest: [
function(fData, headers) {
headers['Content-Type'] = 'application/json'
return JSON.stringify(fData)
}
]
})
.then(
(response) => {
resolve(response.data)
},
(err) => {
reject(err)
}
)
})
}
export function del(url, data) {
return new Promise((resolve, reject) => {
axios.delete(url, { data }).then(
(response) => {
resolve(response.data)
},
(err) => {
reject(err)
}
)
})
}
```
## 如何管理 api
### 原理
首先要制定一个 api 的原则
我的原则一般是这些:
- 干净纯粹
- 尽量不要处理数据
- 独立单一不要互相依赖
好处在于:不在 api 里面处理数据,api里面的接口和接口文档上一样。避免别人引用我的api,还要去看代码,只需要看文档就好了。
> 例子:想象这样一种情况,别人引用了我的 api,突然发现响应数据不对。首先它排查到页面数据没更改。看了api文档,数据也没问题,最后发现我在写 api 的时候进行了处理,这个 api 呢又不能改动,改了影响我自己的模块。只能它在重新写一个api,这样显得很繁杂了,不够干净优雅。
```js
import { fetch, post } from '@/utils/http'
// 用户登陆
export const login = data => post('/user/login', data)
// 获取用户信息
export const getUserInfo = (data) => fetch('/api/user/info', data)
```
如果需要处理数据,要么使用一个中间工具处理,要么在页面里面处理。当然还是实际问题实际分析。
## 如何使用`mock`模拟数据
### 原理与解决方案
一般就是两种方案,一是模拟后端,使用远程在线 JSON 服务器。另外一种搭建本地 JSON 或者 使用现成的 Node 服务器拦截请求。
> 这两种方式各有千秋,没有优劣之分,适合就是最好的。
### 远程在线`mock`
我用过的远程在线`mock`
- apizza:好用,功能齐全,喜欢他的文件展开目录`api`,免费版只支持 2 个人共同编辑
- swagger:开源免费,`api`管理太凌乱,
- rap/rap2:开源免费,可以搭建本地`api`,需要自己搭建
使用远程`mock`的优点:
- 不需要在项目内部增加`mock`
- 功能更加全面完善
- 可以在接口文档基础上`mock`,与接口文档放在一起查看更加方便。
缺点:需要自己另外搭建服务器,只支持静态的`mock`,不能与单元测试结合使用
### 本地JSON mock
- 使用 `webpack` 内部 mock 配置
```js
devServer: {
// 接口未实现情况下,使用mock
before: require('./mock')
}
```
基本原理:主要是使用 node 读取文件,转换成 JSON 格式,使用`mock.js` 模拟数据,最后 webpack 拦截请求生成`json`响应数据
```js
const Mock = require('mockjs')
module.exports = (app) => {
function getJsonFile (filePath) {
var json = fs.readFileSync(path.resolve(__dirname, filePath), 'utf-8')
return JSON.parse(json)
},
const returnMock = (datafile, res, req) => {
setTimeout(() => {
var json
if (/\.json$/.test(datafile)) {
// json文件暴露的是mock的模板
json = getJsonFile(datafile)
} else if (/\.js$/.test(datafile)) {
json = require(datafile)(req.query)
}
res.json(Mock.mock(json))
}, 500)
}
}
```
- 使用 `json-server` 搭建
主要分为下面几步
1. `npm` 安装 `json-server`
2. 编写 `npm` 脚本命令,引入 `mock` 配置文件
3. 编写 `mock` 路由匹配规则
> 比较简单这里不详细描述了!
本地的缺点在于需要
- 前端需要根据`api`文档写`mock`数据格式
- 功能没有远程`mock`那么完善,支持`restful`需要去研究下
- 也是需要配置相关`mock`工具
优点在于
- 不用查看编辑`api`文档
- 在代码中就可以更改和查看`mock`数据
- 支持使用`JavaScipt`动态处` mock`,可以与单元测试结合使用
# 总结
本篇文章耗费作者一个多星期的业余时间,存手工敲打 6000+字,同时收集,整理之前很多技巧和边写作边**思考**和**总结**。如果能对你有帮助,便是它最大的价值。都看到这里还不**点赞**,太过不去啦!😄
由于技术水平有限,文章中如有错误地方,请在评论区指出,感谢!
文中大多数代码将在[suo-design-pro]( https://github.com/suoyuesmile/suo-design-pro) 中更新
> 项目有时间会尽量完善
写实践总结性文章真的很耗费时间。如何文章中有帮到你的地方**分享下**呗,让更多人看到!
# 下节内容预告
- **如何编写原生组件,以及组件编写的思考与原则?**
- **如何使用vuex 以及它的应用场景和原理**
- **如何使用过滤器,编写自己的过滤器**
- **如何使用 Jest 测试你的代码?TDD 与 BDD 的比较**
- **回顾 vue 的生命周期,他们都使用哪些场景?**
- **使用 mixins 与 组件的区别,如何充分的复用代码?**
- **vue 技术栈架构设计上的思考,如何编写自己的组件库?**
- **vue ssr 原理及其应用**
# 参考资料
- [commitizen](https://github.com/commitizen/cz-cli)
- [MDN import()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import)
- [vant 文档](https://youzan.github.io/vant/#/zh-CN/intro)
- [eslint 文档](https://eslint.org/)
- [vue-router 文档](https://router.vuejs.org/zh/)
- [sass 文档](https://www.sasscss.com/getting-started/)
- [axios 文档](https://github.com/axios/axios)
# 开源项目
上述功能配置,均**将**在 [suo-design-pro](https://github.com/suoyuesmile/suo-design-pro) 中实现,希望能持续关注!
================================================
FILE: booknotes/bst.md
================================================
# 二叉搜索树
### 概述
- 循关键码访问call by key: 关键码之间可以比较,比对
- 词条entity: key - value
- 比较器:重载操纵符实现
- 判等器:重载操纵符实现
- Binary Search Tree: 节点 ~ 词条 ~ 关键码, 处处满足顺序性
- 顺序性:任意节点均不小于左后代,不大于右后代
- 单调性:中序遍历,必然单调非降,充要性
- 宏微观:微观满足顺序性,宏观满足单调性
```
template class BST: public public BinTree {
public: //二叉树派生
virtual BinNodePosi(T) & search( const T &) ; //查找
virtual BinNodePosi(T) insert( const T &) ; //插入
virtual bool remove( const T &) ; //删除
protected:
BinNodePosi(T) _hot; //命中节点的父亲
BinNodePosi(T) connect34(BinNodePosi(T), BinNodePosi(T), //3+4重构
BinNodePosi(T), BinNodePosi(T), BinNodePosi(T),
BinNodePosi(T), BinNodePosi(T));
BinNodePosi(T) rotateAt( BinNodePosi(T) ); //旋转
}
```
### BST : 查找、插入、删除、平衡
#### 查找
- 策略:减而治之,仿照二分查找策略
```
template BinNodePosi(T) & BST::search(const T & e) {
return searchIn( _root, e, _hot = NULL) ;
}
//典型的尾递归,当前树根目标关键码,记忆热点
static BinNodePosi(T) & searchIn(BinNodePosi(T) & v, const T & e, BinNodePosi(T) & hot ) {
if( !v || (e == v->data) ) //确定失败,成功,或者
return v;
hot = v; //记下当前节点,然后。。
return searchIn( ( (e < v->data ) ? v->lchild : v->rchild ) , e, hot);
} //运行时间正比于返回节点的深度
```
- 接口语义:返回值的引用值
- - 成功时,指向一个关键码为e且真实存在的节点, 失败时,指向试图转向的空节点NULL
- - 增加哨兵,失败时,将此空节点转换为一个关键码为e且真实存在的节点
#### 插入
- 策略:在查找的基础上替换失败的空节点
```
//BST插入
template BinNodePosi(T) BST:: insert(const T & e) {
BinNodePosi(T) & x = search( e ); //查找目标
if( !x ) { //禁止雷同,失败时才插入操作
x = new BinNode(e, _hot); //创建新节点以hot为父亲
_size++;
updataHeightAbove(x); //更新全树规模,并更新历代祖先规模
}
return x; //无论e是否存在原树中,总有x->data == e
} //总体复杂度O(h)
```
#### 删除
- 策略:先确认目标再删除
```
template bool BST:: remove( const T & e) {
BinNodePosi(T) & x = search( e ); //定位目标
if(!x) return false; //确认目标存在
removeAt(x , _hot); //分两大类实施 删除
_size--; //更新规模
updataHeightAbove( _hot );
return true;
} //复杂度O(h)
```
- 情况一
```
template static BinNodePosi(T) removeAt(BinNodePosi(T) & x, BinNodePosi(T) & hot){
BinNodePosi(T) w = x; //实际被摘除的节点
BinNodePosi(T) succ = NULL; //实际被删除的节点的接替者
if( ! HasLchild(*x) ) succ = x = x->rchild; //左子树为空
else if( ! HasRchild(*x) ) succ = x = x->lchild; //右子树为空
else { //左右并存的状况
hot = w->parent;
if( succ ) succ->parent = hot;
release( w->data ); //释放被摘除的节点
release( w );
return succ; 返回接替者
}
}
```
- 情况二
```
template static BinNodePosi(T) removeAt( BinNodePosi(T) &x, BinNodePosi(T) & hot ) {
BinNodePosi(T) w = x; //实际被摘除的节点
BinNodePosi(T) succ = NULL; //实际被删除的节点的接替者
if( ! HasLchild(*x) ) succ = x = x->rchild; //左子树为空
else if( ! HasRchild(*x) ) succ = x = x->lchild; //右子树为空
else {
w = w->succ;
swap( x->data, w->data);
BinNodePosi(T) u = w->parent;
( u == x ? u->rChild : u->lChild ) = succ = w->rChild;
}//O(h)
```
#### 平衡与等价
> BST的查找,插入,删除复杂度都为O(h),但是高度不能很好控制,效率并不理想
- 生成BST:随机生成的高度为O(logn),有重复的组合,随机组合的BST生成的O(n^1/2)
- 理想平衡:节点数固定,兄弟子树高度越接近(平衡),全树倾向更低
- 由n个节点组成的二叉树,高度不低于logn时-----恰为logn为理想平衡树,概率低,适当降低标准
- 退一步海阔天空:高度渐进不超过O(logn)
- 适度平衡的二叉树,称平衡二叉树BBST
- 非平衡二叉树等价平衡二叉树
- 中序遍历的歧义性:拓扑结构不同,中序遍历相同
- 等价BST:3个规律,上下可变,左右不能乱,旋转变换
- zig变换(旋转调整)
- 准则:局部性(常数节点数),操作数至多O(logn)
### AVL : 重平衡、插入、删除、重构
- 平衡因子:balFac(V) = height( lc(v) ) - height( rc(v) )
- G.Adelson-Velsky & E.Landis (1962) 所有 | balFac | <= 1
- AVL = 适度平衡:高度为h的AVL树,至少包含S(h) = fib(h+3) -1个节点
- 接口:
```
#define Balance(x) ( stature( (x).lChild ) == stature( (x).rChild ) )
#define BalFac(x) ( stature( (x).lChild ) - stature( (x).rChild ) )
#define AvlBalanced(x) ( ( -2 < BalFac(x) ) && ( BalFac(x) < 2 ) )
template class AVL: public BST {
public:
BinNodePosi(T) insert( const T &); //插入重写
bool remove( const T &); //删除重写
}
```
- 插入节点会导致祖先发生失衡,删除后只有一个节点失衡,相反插入操作更简单
- 插入:单旋 ,g经过单旋后复衡,子树高度复原,更高祖先也必平衡,全树复衡
- 时间:旋转O(1) 一致向右zagzag,一致向左zigzig,双旋,zigzag和zagzig情况
```
//插入
template BinNodePosi(T) AVL::insert( const T & e) {
BinNodePosi(T) & x = search( e );
if( x ) return x;
x = new BinNode( e, _hot );
_size++;
BinNodePosi(T) xx = x;
for ( BinNodePosi(T) g = x->parenr; g; g = g->parent )
if ( !AvlBalanced( *g ) ) {
FromParentIo( *g ) = rotateAt( tallerChild( tallerChild(g) ) );
break;
}else
updateHeight( g );
return xx;
}
```
- 删除
================================================
FILE: booknotes/compute.md
================================================
# 算法分析
### 计算
*Dijkstra名言:计算机科学,就是计算的科学*
- 绳索计算机和算法:古埃及人的算法,线段垂直
- 尺规计算机和算法:线段三等分
- **总结:计算 = 信息处理,
借助某种工具,尊尊某种规则**
> 算法:特定计算模型下,解决问题的指令序列
1. 输入:待处理的信息
2. 输出:经处理的信息
3. 正确性:的确可以解决指定的问题
4. 确定性:基本操作组成的序列
5. 可行性:每个操作都可以实现,在常数时间内完成
6. 有穷性:如何输入,有穷次操作可以,得到输出结果
- 可行性:所有操作都可以兑现,例子3步把大象装到冰箱
- 有穷性:Hailstone(n)算法
```
int hailstone(int n) {
int length = 1;
while(1 < n){
n%2==0 ? n/=2 : n=3*n+1;
length++;
}
}
```
**结论:所有的n是否都可以有穷的步骤,还没有结论**
> 程序 != 算法
### 计算模型
*凯文名言:如果你需要改进某种东西,你首先的学会如何测量它*
- 算法分析两个主要方面:正确性,成本
- 成本:时间成本,空间成本
- 怎么衡量时间成本:特定算法+不同示例
- 稳妥起见:关注最坏情况
- 同一问题多种算法,评价其优劣:实验统计不足够
- 图灵机模型
- - Tape(带):均匀划分单元格,各注一字符,默认为'#'
- - Alphabet:字符种类有限
- - Head:总是对准某一单元格,并可以读取修改其中的字符,
每经过一个节拍,可转向左侧或右侧
- - State TM:有限机状态
- - Transition Function:(q, c, d, L/R, p)
若当前状态为q,字符为c,将当前字符改写为d,转向左侧/右侧;转入p状态,一旦转入h停机
功能:二进制非负整数加一,全1的后缀转为0,原低位的0翻转为1
- RAM模型:寄存器顺序编号,总数没有限制
**总结:图灵机,RAM模型为度量算法性能提供了准确的尺度**
### 大O记号
*陶渊明名言:好读书不求甚解(更多的看重长远,主流)*
- 渐进分析: 考虑n >> 2, 对规模为n的输入,需执行的基本操作次数,存储单元数
- 大O记号(big-O notation):**T(n) = O( f(n) )** <= **存在c > 0, n >> ,有 T(n) < c * f(n)**
- 常系数可忽略:
O( c * f(n) )= O ( f(n) )
- 低次项可忽略:
O ( n^a + n^b ) = O( n^a ) , a > b
- 其他记号:最好,平均情况
- 常数O(1):2 = 2013 = 2013^2013 = O(1),不含转向
- 对数O(logn):**非常高效,无限接近与常数**
- - 常底数无所谓
所有 a, b > 0, log(a) n = log(a) b * log(b)n = O( log(b) n )
- - 常数次幂无所谓
- - 对数多项式
- 多项式(O(n ^c)):**直接往高处化,通常认为令人满意了**
- - 线性(O(n))
- - 从O(n)~ O(n^2):编程题主要覆盖的范围
- - 幂
- 指数(a ^n):计算成本增长极快,
和多项式之间有个分水岭(有效算法到无效算法)
```
问题:2-Subset
S包含n个正整数,存在S = 2m
是否有子集T,满足存在 T = m?
美国大选说事
```
**不存在可在多项式时间内解决该问题**
### 算法分析
*夸赞欧拉:像欧拉一样像呼吸一样自如*
- 主要任务:正确性+复杂度
- C++基本指令:等价于RAM的基本指令,二者相当
- - 分支转向:goto //出于结构化被隐藏了
- - 迭代循环:for()、while()、…… //本质上就是if + goto
- - 调用加递归 //本质上也是goto
- 分析方法:迭代(级数求和)、递归(递归跟踪+递推方程)、猜测
- 级数:
- - 算数级数(与末项平方同阶):1 + 2 + …… + n = n (n+1) /2 = O(n^2)
- - 幂级数(比末项高出一阶):1^C + 2^C + …… n^C = O( n^(C+1) )
- - 几何级数(与末项同阶):a0 + a^1 + …… a^n = O(a^n)
- - 收敛级数: O(1)
- - 调和级数:1 + 1/2 + …… 1/n = O(logn)
- - 对数级数:log1 + log2 + …… + logn = log(n!) = O(nlogn)
- 循环与级数:
- - 二重循环复杂度:O(n^2)
```
for(int i=0 ; i 估算:1天多少秒?
**1天 = 24hr * 60min * 60sec = 25 * 4000 = 100000 sec**
### 迭代与递归
**凡治众如治寡,分数是也**
- 例子求和
```
int SumI(int A[], int n){
int sum = 0; //O(1)
for (int i = 0; i < n; ++i)
{
sum += A[i]; //O(1)
}
return sum; //O(1)
}
// 时间复杂度:T(n) = O(n)
// 空间复杂度:O(1)
```
- 减而治之:
> 求解一个大规模的问题,划分成两个子问题:其一平凡,其二规模缩减(单调性)
```
int SumJ(int A[], int n){
return (n < 1) ? 0 : SumJ(A, n-1) + A[n+1] ;
} //递归
递归跟踪:T(n) = O(1)*(n+1) = O(n)
递推方程:T(n) = T(n-1) + O(1) T(0) = O(1) ==> T(n) = O(n)
```
- 例子:数组倒置
```
void reverse(int * Am int lo, int hi);
//递归版
if(lo < hi) { swap(A[lo], A[hi]); reverse(A, lo+1, hi-1); }
```
- 分而治之:
> 求解一个大规模的问题,划分成多个子问题,规模相当,分别求解子问题
```
int sum(int A[], int lo, int hi){
if(lo == hi) return A[lo];
int mi = (lo+hi) >> 1;
return sum(A, lo, mi) + sum(A, mi + 1, hi);
}//二分递归
递归跟踪:T(n) = O(1) * (2^0 + ... + 2^logn) = O(n)
递推方程:T(n) = O(1) + 2T(n/2), T(1) = O(1) T(n) = O(n);
```
- 例子:MAX2,[lo, hi) 找出最大和次大的
```
if( A[x1 = lo] < A[x2 = lo + 1]) swap(x1, x2);
for(int i = lo + 2; i < hi; i++)
if(A[x2] < A[i])
if(A[x1] < A[x2 =i])
swap(x1, x2);
复杂度:最好的 n-1次比较,最坏的2n-3次比较
改进:分而治之 最坏5n/3-2
```
### 动态规划(DSA优化)
> 运转-->正确-->快速
- 例子:fib()(递归)
```
// fib(n) = f(n-1) + f(n-2) {0, 1, 1, 2, 3, 5, 8 ...}
int fib(n) { return (2 > n) ? n : fib(n-1) + fib(n-2); }
// fib(43) ==> 巨大的延迟
//时间复杂度分析
//递推公式:T(n-1) + T(n-2) + 1, T(1) = T(0) = 1, S(n) = fib(n+1) = 1.68... ^n
//==> O(2^n) 指数复杂度 超过100项,需要几百年
//递归跟踪:大量重复的递归实例
```
- 例子:fib()(迭代)
```
//方法A(记忆)
//制表查询
//方法B(动态规划)
//颠倒计算方向,自底而上
int fib2(int n){
int f = 0, g = 1; //用2个参数存储中间指
while(0 < n--){
g = g + f;
f = g - f;
}
return g;
}
//空间上只需要O(1)
```
- 例子:最长公共子序列(递归-->动态规划)
```
/*
* 最长公共子序列
* 可行---->递归策略---->减而治之---->平凡情况
* 效率---->动态规划---->自底而上---->迭代策略
* 对于序列A[0,n],B[0,m], LCS(A,B)无非3种情况
* 1) n=-1或m=-1,作空序列
* 2) A[n] = 'X' = B[m] 则LCS( A[0,n), B[0,m), 'X' )
* 3) A[n] != B[m] 则 LCS( A[0,n], B[0,m) ) 与 LCS( A[0,n), B[0,m] ) 中取更长者(分而治之)
* 采用倒推式方式求公共子序列,用二维数组存储中间值
*
*/
//递归
#include
#include
using namespace std;
int LCS(string, int, string, int);
int LCS2(string, int, string, int);
int main(void){
string a, b;
cin >> a >> b;
cout << LCS(a, a.length()-1, b, b.length()-1) << endl;
// cout << a << b << endl;
return 0;
}
//递归
int LCS(string a, int m, string b, int n){
if(m == -1 || n == -1)
return 0;
else if(a[m] == b[n])
return LCS(a, m-1, b, n-1) + 1;
else
return LCS(a, m, b, n-1) > LCS(a, m-1, b, n)
? LCS(a, m, b, n-1)
: LCS(a, m-1, b, n);
}
//动态规划
int LCS(string a, int m, string b, int n){
int arr[m][n] = {0}; //用数组存储中间值
if(a[0] == a[0])
}
```
================================================
FILE: booknotes/demo/AVL.class.cpp
================================================
#define Balance(x) ( stature( (x).lChild ) == stature( (x).rChild ) )
#define BalFac(x) ( stature( (x).lChild ) - stature( (x).rChild ) )
#define AvlBalanced(x) ( ( -2 < BalFac(x) ) && ( BalFac(x) < 2 ) )
template class AVL: public BST {
public:
BinNodePosi(T) insert( const T &); //插入重写
bool remove( const T &); //删除重写
}
//插入
template BinNodePosi(T) AVL::insert( const T & e) {
BinNodePosi(T) & x = search( e );
if( x ) return x;
x = new BinNode( e, _hot );
_size++;
BinNodePosi(T) xx = x;
for ( BinNodePosi(T) g = x->parenr; g; g = g->parent )
if ( !AvlBalanced( *g ) ) {
FromParentIo( *g ) = rotateAt( tallerChild( tallerChild(g) ) );
break;
}else
updateHeight( g );
return xx;
}
================================================
FILE: booknotes/demo/BST.class.cpp
================================================
template class BST: public public BinTree {
public: //二叉树派生
virtual BinNodePosi(T) & search( const T &) ; //查找
virtual BinNodePosi(T) insert( const T &) ; //插入
virtual bool remove( const T &) ; //删除
protected:
BinNodePosi(T) _hot; //命中节点的父亲
BinNodePosi(T) connect34(BinNodePosi(T), BinNodePosi(T), //3+4重构
BinNodePosi(T), BinNodePosi(T), BinNodePosi(T),
BinNodePosi(T), BinNodePosi(T));
BinNodePosi(T) rotateAt( BinNodePosi(T) ); //旋转
}
//BST查找
template BinNodePosi(T) & BST::search(const T & e) {
return searchIn( _root, e, _hot = NULL) ;
}
//典型的尾递归,当前树根目标关键码,记忆热点
static BinNodePosi(T) & searchIn(BinNodePosi(T) & v, const T & e, BinNodePosi(T) & hot ) {
if( !v || (e == v->data) ) //确定失败,成功,或者
return v;
hot = v; //记下当前节点,然后。。
return searchIn( ( (e < v->data ) ? v->lchild : v->rchild ) , e, hot);
} //运行时间正比于返回节点的深度
//BST插入
template BinNodePosi(T) BST:: insert(const T & e) {
BinNodePosi(T) & x = search( e ); //查找目标
if( !x ) { //禁止雷同,失败时才插入操作
x = new BinNode(e, _hot); //创建新节点以hot为父亲
_size++;
updataHeightAbove(x); //更新全树规模,并更新历代祖先规模
}
return x; //无论e是否存在原树中,总有x->data == e
}
//BST删除
template bool BST:: remove( const T & e) {
BinNodePosi(T) & x = search( e ); //定位目标
if(!x) return false; //确认目标存在
removeAt(x , _hot); //分两大类实施 删除
_size--; //更新规模
updataHeightAbove( _hot );
return true;
} //复杂度O(n)
//情况一
template static BinNodePosi(T) removeAt(BinNodePosi(T) & x, BinNodePosi(T) & hot){
BinNodePosi(T) w = x; //实际被摘除的节点
BinNodePosi(T) succ = NULL; //实际被删除的节点的接替者
if( ! HasLchild(*x) ) succ = x = x->rchild; //左子树为空
else if( ! HasRchild(*x) ) succ = x = x->lchild; //右子树为空
else { //左右并存的状况
hot = w->parent;
if( succ ) succ->parent = hot;
release( w->data ); //释放被摘除的节点
release( w );
return succ; //返回接替者
}
}
//化繁为简
template static BinNodePosi(T) removeAt( BinNodePosi(T) &x, BinNodePosi(T) & hot ) {
BinNodePosi(T) w = x; //实际被摘除的节点
BinNodePosi(T) succ = NULL; //实际被删除的节点的接替者
if( ! HasLchild(*x) ) succ = x = x->rchild; //左子树为空
else if( ! HasRchild(*x) ) succ = x = x->lchild; //右子树为空
else {
w = w->succ;
swap( x->data, w->data);
BinNodePosi(T) u = w->parent;
( u == x ? u->rChild : u->lChild ) = succ = w->rChild;
}
} //O(h)
================================================
FILE: booknotes/demo/BTNode.h
================================================
template struct BTNode {
BTNodePosi(T) parent; //父
Vector key; //数值向量
Vector< BTNodePosi(T) > child; //孩子向量
BTNode() { parent = NULL; child.insert(0, NULL); }
BTNode( T e, BTNodePosi(T) lc = NULL, BTNodePosi(T) rc = NULL) {
parent = NULL; //根节点
key.insert( 0, e ); //仅一个关键码,以及两个孩子
child.insert( 0, lc );
child.insert( 1, rc );
if( lc ) lc->parent = this;
if( rc ) rc->parent = this;
}
}
================================================
FILE: booknotes/demo/BTree.class.cpp
================================================
================================================
FILE: booknotes/demo/BinNode.h
================================================
#define BinNodePosi(T) BinNode*
template struct BinNode {
BinNodePosi(T) parent, lChild, rChild; //父亲孩子
T data; //数据
int height; //高度
int size(); //规模
BinNodePosi(T) insertAsLc(T const &); //插入左子
BinNodePosi(T) insertAsRc(T const &); //插入右子
BinNodePosi(T) succ(); //(中序)后继
template void travLevel( VST & ); //层次遍历
template void travIn( VST & ); //中序遍历
template void travPost( VST & ); //后序遍历
} //O(1)
//插入左子
template BinNodePosi(T) BinNode::insertAsLc(T const &) {
return lChild = new BinNode( e, this);
}
//插入右子
template BinNodePosi(T) BinNode::insertAsRc(T const &) {
return rChild = new BinNode( e, this);
}//O(1)
//规模
template int BinNode::size() {
int s = 1;
if (lChild)
s += lChild->size();
if (rChild)
s += rChild->size();
return s;
}//O(n)
================================================
FILE: booknotes/demo/BinTree.class.cpp
================================================
#define stature ((p) ? (p)->height : -1)
template class BinTree {
protected:
int _size; //规模
BinNodePosi(T) _root; //根节点
virtual int updateHeight( BinNodePosi(T) x );
void updateHeightAbout( BinNodePosi(T) x );
public:
int size() const { return _size; }
bool empty() const { return _root; }
BinNodePosi(T) root() const { return _root; }
//接入删除分离
//遍历
}
//更新高度
template