## 1. Project Overview & Quickstart (caorushizi/TRANSLATION)

# TRANSLATION

Provides tools to internationalize your application

### Repository Details
- **Repository:** [caorushizi/TRANSLATION](https://github.com/caorushizi/TRANSLATION)
- **GitHub Stars:** ⭐ 6,615
- **Primary Language:** PHP

*Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.*

## 2. Official Technical Reference & Guides (caorushizi/caorushizi.github.io)

## File: README.md

# 使用说明

- 安装 hugo

```shell script
git submodule update --init --recursive
```

- 本地运行

```shell script
hugo server
```

---

## File: config.yml

baseURL: "https://blog.ziying.site/"
title: 士子☀️的博客
paginate: 15

enableRobotsTXT: true
buildDrafts: false
buildFuture: false
buildExpired: false

minify:
  disableXML: true
  minifyOutput: true

outputs:
  home:
    - HTML
    - RSS
    - JSON # is necessary

languageCode: "zh"
defaultContentLanguage: "zh"
defaultContentLanguageInSubdir: true

params:
  env: development
  title: ExampleSite
  keywords: [ Blog, 士子☀的博客, 曹儒士子, 博客, 个人博客 ]
  author: 曹儒士子
  DateFormat: "2006年01月02日"
  defaultTheme: auto
  disableThemeToggle: false

  ShowReadingTime: false
  ShowShareButtons: false
  ShowPostNavLinks: true
  ShowBreadCrumbs: false
  ShowCodeCopyButtons: false
  disableSpecial1stPost: true
  disableScrollToTop: false
  comments: true
  hidemeta: false
  hideSummary: false
  showtoc: true
  tocopen: true
  searchHidden: false

  assets:
    favicon: "/favicon.ico"

  label:
    text: "曹儒士子的博客"

  cover:
    hidden: true # hide everywhere but not in structured data
    hiddenInList: true # hide on list pages and home
    hiddenInSingle: true # hide on single page

  fuseOpts:
    isCaseSensitive: false
    shouldSort: true
    location: 0
    distance: 1000
    threshold: 0.4
    minMatchCharLength: 0
    keys: [ "title", "permalink", "summary", "content" ]

  profileMode:
    enabled: false

taxonomies:
  tag: tags
  category: categories
  series: series

menu:
  main:
    - identifier: index
      name: 首页
      url:
      weight: 10
    - identifier: search
      name: 搜索
      url: search
      weight: 30
    - identifier: archives
      name: 时间轴
      url: archives
      weight: 40

---

## File: archetypes/default.md

---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

---

## File: content/about.md

---
title: 关于
layout: about
---


测试页面

---

## File: content/archives.md

---
title: 时间轴
layout: archives
---

---

## File: content/categories.md

---
title: "分类"
layout: categories
---

---

## File: content/search.md

---
title: "搜索"
layout: "search"
summary: "search"
placeholder: "请输入要搜索的关键字"
---

---

## File: content/tags.md

---
title: "标签"
layout: tags
---

---

## File: content/posts/2022.02.05 vue react webpack.md

[Binary file]

---

## File: content/posts/2022.02.05 前端基础.md

[Binary file]

---

## File: content/posts/2022.02.05 脚本编程.md

---
title: "掌握shell脚本编程 进阶Linux高手"
layout: "posts"
date: 2022-02-05T17:51:41+08:00
categories: ["我的开源软件"]
tags: ["shell", "脚本编程", "学习", "工作"]
---

# shell 基础
## 自定义变量


变量名遵循原则

- 变量是任何字母、数字和下划线组成的字符串，且不能以数字开头
- 区分字母大小写，例如 Var1 和 var1 是不同的
- 变量、等号、值中间不能出现任何空格

​

变量引用

- var1=10 等号前后不能有空格
- echo $var1
```shell
#!/bin/bash

var1=abc

echo $var1
```
## 位置变量
当一个命令或者脚本执行的是哦户，后面可以跟多个参数，我们使用位置参数变量来表示这些参数

| 位置参数变量 | 含义 |
| --- | --- |
| $n | n为数字，$0 代表脚本本身，$1~$9代表1-9个参数，10以上的参数需要大括号包含，如${10} |
| $@ | 命令行所有参数，但是每个参数区别对待 |
| $* | 命令行所有参数，所有参数视为一个整体 |
| $# | 参数的个数 |

## 环境变量
简介

- linux是一个多租户的操作系统，针对不同的用户都会有一个转悠的运行环境
- 不同用户的专有环境就是一组默认环境变量的组合

​

环境变量的分类

- 对所有用户生效的环境变量		/ect/profile
- 对特定用户的生效的环境变量	~/.bashrc 或者 ~/.bash_profile
- 练市有效的环境变量			脚本或者命令行使用export

​

常用的环境变量

| 环境变量 | 含义 |
| --- | --- |
| PATH | 命令搜索的路径 |
| HOME | 用户的家目录的路径 |
| LOGNAME | 用户登录名 |
| PWD | 当前所在的路径 |
| HISTFILE | 历史命令的保存文件 |
| HISTSIZE | 历史命令保存的最大行数 |
| HOSTNAME | 主机名 |
| SHELL | 用户当前使用的SHELL |
| PS1 | 以及命令提示符 |
| TMOUT | 用户和系统交互过程的超时值 |
| IFS | 系统输入的分隔符 |
| OFS | 系统输出的分隔符 |

环境变量一般都是大写
​

## 管道

- 将系统上所有的软件包列出来，然后去搜索Python相关的软件包
- 解决方法：使用命令列出系统已有的所有软件包，然后把结果重定向到文件中，然后在文件中搜索python
- rpm -qa > all_soft.txt 在利用vim去搜索python

​

使用管道解决问题

- 列出所有的软件包，然后将结果传递给后续命令处理
- rpm -qa | grep python | wc -l

​

管道定义

- 将一个命令的输出作为另一个命令的输入
- 从某种意义上来说，是重定向的简易实现

​

## 退出状态码

- 所有的shell命令都使用退出状态码来告知shell它已经执行完毕
- 退出状态码是一个0~255的整数值
- Linux 提供了一个$?来捕获退出状态码的值

​




实践用法

| 状态码 | 含义 |
| --- | --- |
| 0 | 命令执行成功的情况 |
| 非0 | 不成功的情况 |

## 改变退出状态码exit命令

- 退出状态码是以上一条指令的返回结果为准
- exit $exit_code
- exit 48 或者 exit 125 或者 exit 0

​

## 单引号和双引号有哪些坑

- 处理变量时，单引号和双引号的区别
- 处理字符串变量时，单引号和双引号的区别
- 单引号套用是需要注意那些区别
- sed 和 awk 中需要注意单双引号的那些差异

​

# 判断和控制
## 使用 if-then 语句
```shell
if command | condition
then
	commands
fi
```
```shell
if command | condition
then
	commands
else
	commands
fi
```
## 嵌套if
```shell
if command | condition
then
	commands
elif command | condition
	commands
else
	commands
fi
```
## 条件测试 - 数值比较


- 需要使用方括号包裹条件
- command 返回0， 执行 if 中的语句
- condition true 执行 if 中的语句




## 字符串比较



## 文件比较


## 复合条件
```shell
var1=56
var2=23
var3=89

if [ $var1 -gt $var2 ] && [ $var3 -lt $var3 ];then
	echo "$var1 < $var2 并且 $var2 > $var3"
fi
```
## if-then 中使用双括号

- 使用双括号进行算数运算



```shell
if (( expression )); then
	command
fi
```




- 双括结构中，变量名引用可以加$，也可以不加
- 运算符前后可以有空格，也可以没有
- 可以用于if、for、while等循环控制结构中
- 多个运算符使用逗号分割

​

## 双方括号

- 双括号结构中，变量名引用必须加$
- 双方括号]]前面必须有空格，[[后面也必须有空格

​

# case 命令
```shell
case $var in
	pattern1)
  	commands
    ;;
  pattern2)
  	commands
    ;;
  *)
  	commands
    ;;
esac
```


# for 命令
```shell
for vat in list
do
	commands
done
```
## for 读取列表值
```shell
for i in Beijing Shanghai Nanjing
do
	echo ''
done

for i in {1..20}
do
	echo "number is $i"
done
```


## for 循环读取变量的值
```shell
IFS=":"

list="Zhangsan Lisi Mike"
for i in $list
do
	echo "number is $i"
done
```


## for 循环从命令执行结果读取值
```shell
# 命令替换
for i in $(ls /opt)
do
	echo 'all list'
done
```
## 
## c语言风格的for循环


```shell
for (( i=10; i < 50; i++ ))
do
	echo 'current'
done
```


## while 循环命令
```shell
while command
do
	commands
done
```


## until 命令
```shell
until command
do
	echo "number is $1"
done
```


## 控制循环的break指令
break n
退出深层的循环
​

## 处理for循环的输出
可以使用重定向和管道输出来对for循环的结果进行处理。


# 变量的高级用法
## 变量的替换






## 变量的测试



## 字符串处理
计算字符串长度



获取子串在字符串中的索引位置



获取子串的长度



抽取子串



## 字符串处理完整的脚本



## 命令替换

- ``command``
- `$(command)`

​

## 有类型的变量

- declare命令和typeset命令两者等价
- declare、typeset命令都是用来定义变量类型的




`declare -a ("a" "b")`


- `+a`取消
- `-a`设置
## bash 数学运算 expr

- `expr $num1 operator $num2`
- `$(($num1 operator $num2))`





## bash 数学运算之bc

- bc 是bash 内建的运算器，支持浮点数运算
- 内建变量scale可以设置，默认为0



`echo "24/7" | bc`

# 函数的高级用法
## 函数的定义和使用

- Linux Shell 中的函数和大多数编程语言中的函数一样
- 将相似的任务或者代码封装到函数中，供其他地方调用

​

第一种格式
```shell
name()
{
	command
}
```
第二种格式
```shell
function name
{
	command
}
```

- 直接使用函数名调用，可以将其想象成shell中的一条命令
- 函数内部可以直接使用参数$1,$2$n
- 函数调用： function_name $1 $2

​

```shell
ps -ef | grep nginx | grep -v grep
```
## 向函数传递参数
```shell
function name
{
	echo "Hello $1"
}
```


## 函数的返回值


使用return返回值

- 使用return 返回值只能返回1-255的整数
- 函数使用 return 返回值，通常只是用来供其他地方调用获取状态，因此通常仅返回 0 或 1；0 表示成功，1表示失败

​

使用echo返回值

- 使用echo可以返回任何字符串结果
- 通常用于返回数据，比如一个字符串值或者列表值

​

## 全部变量和局部变量
全局变量

- 不做特殊生命，shell中变量都是全局变量
- Tips 大型脚本程序中函数慎用全局变量



局部变量

- 定义变量时，使用local 关键字
- 函数内和外若存在同名的变量，则函数内部变量覆盖外部变量

​

## 函数库
为什么要定义函数库

- 经常使用的重读代码封装成函数文件
- 一般不直接执行，而是通过其他脚本调用

​

经验之谈

- 库文件名的后缀是任意的，但一般使用.lib
- 库文件通常没有可执行的权限的
- 库文件无需和脚本在拥挤目录，秩序在脚本引用时指定
- 第一行一般使用#!/bin/echo， 输出警告信息，避免用户执行

​

# Shell 编程中的常用工具
## 文件查找 find 命令
find 命令语法格式
`find [路径] [选项] [操作]`
​

选项参数对照表




```
find命令总结：

	常用选项：
	
		-name			查找/etc目录下以conf结尾的文件	find /etc -name '*conf'
		-iname			查找当前目录下文件名为aa的文件，不区分大小写	find . -iname aa
		-user			查找文件属主为hdfs的所有文件	find . -user hdfs
		-group			查找文件属组为yarn的所有文件	find . -group yarn		
		-type			
		
			f		文件				find . -type f 
			d		目录				find . -type d
			c		字符设备文件		find . -type c
			b		块设备文件			find . -type b
			l		链接文件			find . -type l
			p		管道文件			find . -type p
			
		
		-size
		
			-n		小于大小n的文件
			+n		大于小于n的文件
			
			例子1：查找/etc目录下小于10000字节的文件		find /etc -size -10000c
			例子2：查找/etc目录下大于1M的文件				find /etc -size +1M
		
		-mtime		
		
			-n		n天以内修改的文件
			+n		n天以外修改的文件
			n		正好n天修改的文件
			
			例子1：查找/etc目录下5天之内修改且以conf结尾的文件	find /etc -mtime -5 -name '*.conf'
			例子2：查找/etc目录下10天之前修改且属主为root的文件	find /etc -mtime +10 -user root
			
		-mmin
			
			-n		n分钟以内修改的文件
			+n		n分钟以外修改的文件
			
			例子1：查找/etc目录下30分钟之前修改的文件		find /etc -mmin +30
			例子2：查找/etc目录下30分钟之内修改的目录		find /etc -mmin -30 -type d
		
		-mindepth n		表示从n级子目录开始搜索
		
			例子：在/etc下的3级子目录开始搜索		find /etc -mindepth 3 
		
		-maxdepth n		表示最多搜索到n级子目录
		
			例子1：在/etc下搜索符合条件的文件，但最多搜索到2级子目录		find /etc -maxdepth 3 -name '*.conf'
			例子2：
				find ./etc/ -type f -name '*.conf' -size +10k -maxdepth 2
		
			
		
		
	了解选项：
	
		-nouser		查找没有属主的用户
		
			例子：find . -type f -nouser
		
		-nogroup	查找没有属组的用户
		
			例子：find . -type f -nogroup
		
		-perm
		
			例子：find . -perm 664
		
		-prune
		
			通常和-path一起使用，用于将特定目录排除在搜索条件之外
			例子1：查找当前目录下所有普通文件，但排除test目录
				find . -path ./etc -prune -o -type f
			例子2：查找当前目录下所有普通文件，但排除etc和opt目录
				find . -path ./etc -prune -o -path ./opt -prune -o -type f
			例子3：查找当前目录下所有普通文件，但排除etc和opt目录，但属主为hdfs
				find . -path ./etc -prune -o -path ./opt -prune -o -type f -a -user hdfs
			例子4：查找当前目录下所有普通文件，但排除etc和opt目录，但属主为hdfs，且文件大小必须大于500字节
				find . -path ./etc -prune -o -path ./opt -prune -o -type f -a -user hdfs -a -size +500c
		
		-newer file1
		
			例子：find /etc -newer a
			
	操作：
	
	
		-print		打印输出
		
		-exec		对搜索到的文件执行特定的操作，格式为-exec 'command' {} \;
		
			例子1：搜索/etc下的文件(非目录)，文件名以conf结尾，且大于10k，然后将其删除
			
				find ./etc/ -type f -name '*.conf' -size +10k -exec rm -f {} \;
				
			例子2：将/var/log/目录下以log结尾的文件，且更改时间在7天以上的删除
			
				find /var/log/ -name '*.log' -mtime +7 -exec rm -rf {} \;
				
			例子3：搜索条件和例子1一样，只是不删除，而是将其复制到/root/conf目录下
			
				find ./etc/ -size +10k -type f -name '*.conf' -exec cp {} /root/conf/ \;
				
		-ok			和exec功能一样，只是每次操作都会给用户提示
		
	逻辑运算符：
	
		-a			与
		-o			或
		-not|!		非
		
		例子1：查找当前目录下，属主不是hdfs的所有文件		
		
			find . -not -user hdfs 	|	find . ! -user hdfs
		
		例子2：查找当前目录下，属主属于hdfs，且大小大于300字节的文件
		
			find . -type f -a -user hdfs -a -size +300c
			
		例子3：查找当前目录下的属主为hdfs或者以xml结尾的普通文件
			
			find . -type f -a \( -user hdfs -o -name '*.xml' \)
				
```
## 
## 常用的查找命令
find、locate、whereis 和 which 总结及使用场景分析
​

locate 命令介绍

- 文件查找命令，所属软件包mlocate
- 不同于find命令是在整块磁盘中搜索，locate命令是在数据库文件中查找
- find 是默认全部匹配，locate 则是默认部分匹配

​

updatedb命令

- 用户更新/var/lib/mlocate/mlocate.db
- 所使用配置文件/etc/updatedb.conf
- 改命令在后台cron计划中定期执行

​

whereis 命令

- 作用：查找某个命令的二进制程序文件、帮助文档、源代码文件




which 命令

- 作用：今查找二进制文件




各个命令使用场景的推荐

| 命令 | 适用场景 | 优缺点 |
| --- | --- | --- |
| find | 查找某一类文件，比如文件名部分一致 | 功能强大，速度慢 |
| locate | 只能查找单个文件 | 功能单一，速度快 |
| whereis | 查找程序的可执行文件、帮助文档 | 不常用 |
| which | 只查找程序的可执行文件 | 常用语查找程序的绝对路径 |



# grep 和 egrep

1. grep [option] [pattern] [file1,file2...]
1. command | grep [option] [pattern]




不常用



# sed
sed （stream editor），流编辑器。对标准输出或文件逐行进行处理
​

语法格式

- `stdout | sed [option] "pattern command"`
- `sed [option] "pattern command" file`



sed 选项



## sed 中 pattern 详解



## sed 中的编辑命令详解



| 类别 | 编辑命令 | 含义 |
| --- | --- | --- |
| 查询 | p | 打印 |
| 新增 | a | 行后追加 |
|  | i | 行前追加 |
|  | r | 外部文件读入，行后追加 |
|  | w | 匹配行写入外部文件 |
| 删除 | d | 删除 |
| 修改 | s/old/new | 将行内第一个old换成new |
|  | s/old/new/g | 将行内全部old更换为new |
|  | s/old/new/2 | 将行内前两个old更换成new |
|  | s/old/new/ig | 将行内的old全部更换为new，忽略大小写 |

`&` 和 `\1` 都可以代表反向引用，`\1`需要将前面匹配到的区域用括号括起来
​

## 利用 sed 删除特定的内容

## 利用 sed 修改文件内容



# awk
简介：awk 是一个文本处理工具，通常用于处理数据并生成结果报告


awk 的工作模式



语法格式：

- `awk 'BEGIN{}parrern{commands}END{}' file_name`
- `standard output | awk 'BEGIN{}parttern{commands}END{}'`

​




## awk 的内置变量




## awk 的格式化输出




## awk 模式匹配的两种用法

- RegExp
- 关系运算符匹配





## awk 动作表达式中的算数运算符



## awk 中条件语句




## awk 中的字符串函数




## awk 选项总结





## awk 中数组中的用法
shell中数组的用法

---

## File: content/posts/_index.md

---
title: "文章列表"
---

---

## File: content/posts/expire/2019.10.08 python 数据分析.md

---
layout: post
title: "python 数据分析"
layout: "posts"
date: 2019-10-08 23:14:00 +0800
categories: ["Python 学习"]
tags: ["数据分析"]
url: /post/python-shujufenxi/
---




# python与数据分析概况

## 数据分析概况

- 数据分析，是指用**适当的**分析方法，对收集的数据进行分析，总结规律，提取有价值的信息，形成有效结论的过程。

- 数据分析的作用
    - 现状分析
    - 原因分析
    - 预测分析

- 数据分析的流程
    1. 明确目的和思路
      - 做任何事情都要有目标，一切都要以解决问题为中心
      - 搭建分析框架，确定从哪几个角度，用那些指标进行分析
    2. 数据准备
      - 主要数据来源有数据库、公开出版物、互联网以及市场调查
    3. 数据处理
      - 数据处理是指对收集到的数据进行加工整理，形成适合数据分析的样式，是数据分析必不可少的阶段
    4. 数据分析
      - 基础分析方法：对比分析、分组分析、结构分析、分布分析、交叉分析、矩阵分析等方法；
      - 高级分析方法：回归分析、聚类分析、决策树、神经网络、因子分析、时间序列分析等方法；
    5. 数据展示
      - 数据分析结果主要通过表格和图形的方式来呈现，即用图表说话
    6. 报告撰写
      - 数据分析报告是对整个数据分析的过程的一个总结和呈现，通过报告，把数据分析的起因、过程、结果以及建议完整诚信出来，供决策者参考

## python 概况（3W2H法）


- What：什么是python： 一种免费的、自由的编程语言，是一款强大的数据分析、数据挖掘的工具。
- Who：谁在使用python：系统应用、互联网、统计分析、数据挖掘、数据可视化等
- Why：为什么使用python：python之一种解释性、动态语言，具有明确而高效的语法；python不断地从其他优秀的数学软件引入高效的数据开发包；python被称为可执行的伪代码，有着优美的代码风格。
- How：如何学习python
- How Continue：如何持续地去提高python的技能：实战案例，熟练操作；理解算法，实战操作。

# python 安装

## 安装anaconda
## 使用anaconda
   
# 数据准备
## python 数据类型

1. 定义：按照python定义的格式，将数据的数据类型告知python；

  ```python
  1  '1'   True   False
  ```
2. 赋值：将定义好的数据，传递给变量的类型；

  ```python
  a = 1, b = True, c = 2
  ```

3. 变量：数据赋值的对象，通过变量去操作数据；
  ```python
  d = a * c, b = 3
  ```

## 变量的命名规则

1. 由a-z，A-Z，数字，下划线（\_）组成，首字母不能为数字和下划线（\_）；
2. 大小写敏感，变量a和变量A是不同的变量；
3. 变量名不能为python中的保留字；

```python
# python 中的保留字
and or not assert finally exec break
for pass class from print continue global
raise def if return del import try elif
in while else is with except lambda yield
```


## 三种常用的数据类型

|类型|注释|
|--|--:|
|Logical|逻辑型|
|Numberic|数值型|
|Character|字符型|

1. Logical逻辑型：布尔型，只有两种取值（0和1、真和假）

  | 值 | 注释 |
  |--|--:|
  | True | 真 |
  | False | 假 |

  运算规则

  | 运算符 | 注释 | 运算规则 |
  |-- |--: |--:|
  | & | 与 | 两个逻辑型数据中，一个逻辑型数据为假，则结果为假 |
  | \| | 或 | 两个逻辑型数据中，一个逻辑型数据为真，则结果为真 |
  | not | 非 | 取相反值，真非为假，假非为真 |

2. Numberic 数值型：实数，包含正数负数和0

  运算规则

  | 运算符 | 注释 | 运算规则 |
  |-- |--: |--:|
  | + | 加 |  |
  | - | 减 |  |
  | * | 乘 |  |
  | / | 除 |  |

3. Character字符型

  character字符型：字符型数据代表了所有可定义的字符；

  定义方式：使用''或者""或者""" """将其包含起来即可；

  运算规则：无；

## 数据结构

数据结构是指相互之间存在一种后者多种特定关系十数据类型的集合

| 类型 | 注释 |
|--|--:|
| Series | 序列 |
| DataFrame | 数据框 |



1. Series序列：序列是用来储存一行或者一列数据，以及与之相关的索引的集合

```python
from pandas import Series

x = Series(['a', True, 1])

x = Series(['a', True, 1], index=['first','second','third'])

x[1]

x['second']

# 向序列中追加值
x = x.append(Series([2]))

# 判断值是否序列中
'2' in x.values

# 切片
x[1: 3]

# 定位获取，这个方法经常用于随机抽样
x[[0, 2, 1]]

# 根据index删除
x.drop(0)
x.drop('index')

# 根据位置删除
x.drop(x.index(3))

#根据值删除
x['2'!=x.values]

```

2. DataFrame数据框：数据框适用于多行和多列的数据集合

数据框的概念图解




```python
from pandas import DataFrame

df = DataFrame({
    'age': [21, 22, 23],
    'name': ['KEN', 'John', 'JIMI']
})

df = DataFrame(
    data={
        'age': [21, 22, 23],
        'name': ['KEN', 'John', 'JIMI']
    },
    index=['first', 'second', 'third']
)

#按列访问
df['age']

df[['age', 'name']]

#按行访问
df[1:2]

#按行索引访问
df.loc[['first', 'second']]

#按行列号访问
df.iloc[0:1, 0:1]

#按行索引，列名访问
df.at['first', 'name']

#修改列名
df.columns
df.columns=['age2', 'name2']

#修改行索引
df.index
df.index = range(1,4)
df.index

#根据行索引删除
df.drop(1, axis=0)
#默认参数axis=0
#根据列名进行删除
df.drop('age2', axis=1)

#增加行，
#注意，这种方法，
#效率非常低，
#不应该用于遍历中
df.loc[len(df)] = [24, "KENKEN"]

#增加列
df['newColumn'] = [2, 4, 6]
```
## 向量化运算

定义：向量化运算是一种特殊的并行计算的方式，他可以在同一时间执行多次操作，通常是对不同的数据执行同样的一个或者一批指令，或者把指令应用于同一个数组（向量）。

1. 生成等差数列

```python
numpy.arange('开始值', '结束值', '步长')
```

2. 四则运算

规则：相同位置的数据进行运算，结果保留在相同的位置

3. 函数运算

规则：相同位置的数据进行函数的计算，函数返回结果留在相同的位置

4. 向量化运算的原则

原则：代码中尽可能避免显式的for循环，过早优化是魔鬼。

```python
# -*- coding: utf-8 -*-

#生成一个整数的等差序列
#局限，只能用于遍历
r1_10 = range(1, 10, 2)

for i in r1_10:
    print(i)

r1_10 = range(0.1, 10, 2)


#生成一个小数的等差序列
import numpy
numpy.arange(0.1, 0.5, 0.01)

r = numpy.arange(0.1, 0.5, 0.01)

#向量化计算，四则运算
r + r
r - r
r * r
r / r

#函数式的向量化计算
numpy.power(r, 5)

#向量化运算，比较运算
r>0.3
#结合过滤进行使用
r[r>0.3]

#矩阵运算
numpy.dot(r, r.T)

sum(r*r)

from pandas import DataFrame
df = DataFrame({
    'column1': numpy.random.randn(5),
    'column2': numpy.random.randn(5)
})

df.apply(min)

df.apply(min, axis=1)

#判断每个列，值是否都大于0
df.apply(
    lambda x: numpy.all(x>0),
    axis=1
)
#结合过滤
df[df.apply(
    lambda x: numpy.all(x>0),
    axis=1
)]

```

# 数据处理
## 数据导入、数据导出

1. 数据存在的形式

| 类型 | 存储形式 | 备注 |
| :--: | :--: | :--: |
| 文件 | CSV | 用“，”分割列的文件 |
| 文件 |Excel | 微软办公软件Excel文件 |
| 文件 | TXT | 普通文本文件 |
| 文件 | 。。。 | 其他数据 |
| 数据库 | MySQL | 广泛使用的免费开源数据库 |
| 数据库 | Access | 微软办公软件Access |
| 数据库 | SQL Server | 微软企业级数据库 |
| 数据库 | 。。。 | 其他数据库 |


2. 数据导入

- 导入CSV文件

使用read_csv函数导入CSV文件

```python
from pandas import read_csv

df = read_csv('./1.csv')

```

- 导入文本文件

使用read_table函数导入普通文本文件

| 参数 | 注释 |
| :--: | :--: |
| file | 文件路径 |
| names | 列名，默认为文件中第一行作为列名 |
| sep | 分隔符，默认为空，表示默认导入一列 |
| encoding | 设置文件编码，再导入中文的时候需要设置为UTF-8 |

```python
from pandas import read_table

df = read_table('./2.txt')

df = read_table(
    './2.txt',
    names=['age', 'name'],
    sep=','
)
```

- 导入Excel文件

使用read_excel函数导入Excel文件

| 参数 | 注释 |
| :--: | :--: |
| filename | 文件路径 |
| sheetname | Sheet的名字 |
| names | 列名，默认为文件中第一行作为列名 |

```python
from pandas import read_excel;

df = read_excel(
    './3.xlsx',
    sheetname='data'
)
```

3. 数据导出

- 导出文本文件

使用to_csv导出文本文件

| 参数 | 注释 |
| --: | --: |
| filepath | 导出文件的默认路径 |
| sep | 分隔符，默认使用“，” |
| index | 是否导出行序号，默认True |
| header | 是否导出列名，默认为True |

```python
from pandas import DataFrame;

df = DataFrame({
    'age': [21, 22, 23],
    'name': ['KEN', 'John', 'JIMI']
})

df.to_csv("./df.csv")

df.to_csv("./df.csv", index=False)
```

## 重复值处理

把数据结构中，行相同数据只保留一行

```python
from pandas import read_csv

df = read_csv('./data.csv')

df

#找出行重复的位置
dIndex = df.duplicated()

#根据某些列，找出重复的位置
dIndex = df.duplicated('id')
dIndex = df.duplicated(['id', 'key'])

#根据返回值，把重复数据提取出来
df[dIndex]

#直接删除重复值
#默认根据所有的列，进行删除
newDF = df.drop_duplicates()
#当然也可以指定某一列，进行重复值处理
newDF = df.drop_duplicates('id')
```

## 缺失值处理

缺失值的产生：有些信息暂时无法获取，有些信息被遗漏或者错误处理了

缺失值的处理方式：数据补齐，删除对应确实行，不处理

```python
from pandas import read_csv

df = read_csv('./data.csv')

df = read_csv('./data2.csv', na_values=['a','b'])

#找出空值的位置
isNA = df.isnull()

#获取出空值所在的行
df[isNA.any(axis=1)]
df[isNA[['key']].any(axis=1)]
df[isNA[['key', 'value']].any(axis=1)]

df.fillna('未知')

#直接删除空值
newDF = df.dropna()
```

## 空格值处理

清除字符型数据左右的空格

```python
from pandas import read_csv

df = read_csv('./data.csv')

newName = df['name'].str.lstrip()

newName = df['name'].str.rstrip()

newName = df['name'].str.strip()

df['name'] = newName

```

## 字段抽取

是根据已知列数据的开始和结束位置，抽取出新的列


```python
from pandas import read_csv

df = read_csv('./data.csv')

df['tel'] = df['tel'].astype(str)

#运营商
bands = df['tel'].str.slice(0, 3)
#地区
areas = df['tel'].str.slice(3, 7)
#号码段
nums = df['tel'].str.slice(7, 11)

#赋值回去
df['bands'] = bands
df['areas'] = areas
df['nums'] = nums
```
## 字段拆分

字段拆分是指按照固定的字符，拆分已有的字符

split(sep, n, expand=False)

- sep：用于分割字符串
- n：分割为多少列
- expand：是否展开数据框，默认为False

返回值：

- 如果expand为True，则返回DataFrame
- 如果expand为False，则返回series

```python
from pandas import read_csv

df = read_csv('./data.csv')

newDF = df['name'].str.split(' ', 1, True)

newDF.columns = ['band', 'name']
```

## 记录抽取

记录抽取是指根据一定的条件，对数据进行抽取。

dataframe[condition]

常用的条件类型
- 比较运算：大于（>），小鱼（<），大于等于（>=），小于等于（<=），不等于（!=）
- 范围运算：between(left, right)
- 空值匹配：pandas.isnull(colunm)
- 字符匹配：str.contains(patten, na=False)
- 逻辑运算：与（&），或（|），取反（not）



```python
import pandas

df = pandas.read_csv('./data.csv', sep="|")

#单条件
df[df.comments>10000]
#多条件
df[df.comments.between(1000, 10000)]
#过滤空值所在行
df[pandas.isnull(df.title)]
#根据关键字过滤
df[df.title.str.contains('台电', na=False)]
#~为取反
df[~df.title.str.contains('台电', na=False)]
#组合逻辑条件
df[(df.comments>=1000) & (df.comments<=10000)]
```
## 随机抽样

随机抽样是指随机从数据中按照一定的行数或者比例抽取数据

随机抽样函数：DataFrame.sample(n, frac, replace=False)
- n：按个数抽样
- frac：按百分比抽样
- replace：是否可放回抽样，默认False不可放回



```python
# -*- coding: utf-8 -*-
import numpy
import pandas

data = pandas.read_csv('./data.csv')

#设置随机种子
numpy.random.seed(seed=2)

#按照个数抽样
data.sample(n=10)
#按照百分比抽样
data.sample(frac=0.02)
#是否可放回抽样，
#replace=True，可放回,
#replace=False，不可放回
data.sample(n=10, replace=True)


#典型抽样，分层抽样
gbr = data.groupby("class")

gbr.groups

typicalNDict = {1: 2, 2: 4, 3: 6}

def typicalSampling(group, typicalNDict):
    name = group.name
    n = typicalNDict[name]
    return group.sample(n=n)

result = data.groupby(
    'class', group_keys=False
).apply(typicalSampling, typicalNDict)

typicalFracDict = {1: 0.2, 2: 0.4, 3: 0.6}

def typicalSampling(group, typicalFracDict):
    name = group.name
    frac = typicalFracDict[name]
    return group.sample(frac=frac)

result = data.groupby(
    'class', group_keys=False
).apply(typicalSampling, typicalFracDict)

```

## 记录合并

记录合并是指将两个结构相同的数据框合并成一个数据框。


```python
import pandas
from pandas import read_csv

data1 = read_csv('./data1.csv', sep="|")
data2 = read_csv('./data2.csv', sep="|")
data3 = read_csv('./data3.csv', sep="|")
data = pandas.concat([data1, data2, data3])

data = pandas.concat([
    data1[[0, 1]],
    data2[[1, 2]],
    data3[[0, 2]]
])

```

## 字段合并

字段合并是指将同一个数据框中的不同列进行合并，型号才能新的列。

```python
from pandas import read_csv

df = read_csv(
    './data.csv',
    sep=" ",
    names=['band', 'area', 'num']
)

df = df.astype(str)

tel = df['band'] + df['area'] + df['num']

df['tel'] = tel

```
## 字段匹配

字段匹配是指根据各表的关键字段把各表所需的记录下来一一对应起来

merge(x, y, left_on, right_on)
- x：第一个数据框
- y：第二个数据框
- left_on：第一个数据框用于匹配的列
- right_on：第二个数据框用于匹配的列

```python
import pandas

items = pandas.read_csv(
    './data1.csv',
    sep='|',
    names=['id', 'comments', 'title']
)

prices = pandas.read_csv(
    './data2.csv',
    sep='|',
    names=['id', 'oldPrice', 'nowPrice']
)

#默认只是保留连接上的部分
itemPrices = pandas.merge(
    items,
    prices,
    left_on='id',
    right_on='id'
)

#即使连接不上，也保留左边没连上的部分
itemPrices = pandas.merge(
    items,
    prices,
    left_on='id',
    right_on='id',
    how='left'
)

#即使连接不上，也保留右边没连上的部分
itemPrices = pandas.merge(
    items,
    prices,
    left_on='id',
    right_on='id',
    how='right'
)

#即使连接不上，也保留所有没连上的部分
itemPrices = pandas.merge(
    items,
    prices,
    left_on='id',
    right_on='id',
    how='outer'
)

```

## 简单计算

简单计算是指通过对已有字段的加减乘除等运算得出新的字段


```python
import pandas

data = pandas.read_csv('./data.csv', sep="|")

data['total'] = data.price * data.num

#注意，用点的方式，虽然可以访问，但是并没有组合进数据框中
data = pandas.read_csv('./data.csv', sep="|")

data.total = data.price * data.num
```
## 数据标准化

数据标准化是指将数据按比例缩放，使之落入到特定的区间之中

```python
import pandas

data = pandas.read_csv('./data.csv')

data['scale'] = round(
    (data.score-data.score.min())/(data.score.max()-data.score.min())
    , 2
)

```

## 数据分组

数据分组是指根据分析对象的特征，按照一定的数值指标，把数据分析对象划分为不同的区间进行研究，以揭示其内在的联系和规律。

cut函数：cut(series, bins, right=True, labels=None)
- series：需要分组的数据
- bins：分组的划分数组
- right：分组的时候右边是否闭合
- labels：分组的自定义标签，可以不自定义

```python
import pandas

data = pandas.read_csv('./data.csv', sep='|')

bins = [
    min(data.cost)-1, 20, 40, 60,
    80, 100, max(data.cost)+1
]

data['cut'] = pandas.cut(data.cost, bins)

data['cut'] = pandas.cut(data.cost, bins, right=False)

labels = [
    '20以下', '20到40', '40到60',
    '60到80', '80到100', '100以上'
]

data['cut'] = pandas.cut(
    data.cost, bins,
    right=False, labels=labels
)

```

## 时间处理

时间处理是指字符型的时间格式数据，转换成为时间型数据的过程。


```python
import pandas

data = pandas.read_csv('./data.csv', encoding='utf8')

data['时间'] = pandas.to_datetime(
    data.注册时间,
    format='%Y/%m/%d'
)

data['格式化时间'] = data.时间.dt.strftime('%Y-%m-%d')

data['时间.年'] = data['时间'].dt.year
data['时间.月'] = data['时间'].dt.month
data['时间.周'] = data['时间'].dt.weekday
data['时间.日'] = data['时间'].dt.day
data['时间.时'] = data['时间'].dt.hour
data['时间.分'] = data['时间'].dt.minute
data['时间.秒'] = data['时间'].dt.second

```

## 时间抽取

时间抽取是指根据一定条件，对时间格式的数据进行抽取。

```python
# -*- coding: utf-8 -*-
import pandas

data = pandas.read_csv('./data.csv', encoding='utf8')

dateparse = lambda dates: pandas.datetime.strptime(
    dates, '%Y%m%d'
)

data = pandas.read_csv(
    './data.csv',
    encoding='utf8',
    parse_dates=['date'],
    date_parser=dateparse,
    index_col='date'
)

#根据索引进行抽取
import datetime

dt1 = datetime.date(year=2016,month=2,day=1);
dt2 = datetime.date(year=2016,month=2,day=5);

data.ix[dt1: dt2]
data.ix[[dt1,dt2]]

#根据时间列进行抽取
data = pandas.read_csv(
    './data.csv',
    encoding='utf8',
    parse_dates=['date'],
    date_parser=dateparse,
)

data[(data.date>=dt1) & (data.date<=dt2)]

```
## 虚拟变量

虚拟变量也叫哑变量和离散特征编码，可用来表示分类变量、非数量因素可能产生的影响。

- 离散特征的取值之间有大小的意义：例如尺寸（L,XL,XXL）

    pandas.Series.map(dict)

- 离散特征的取值之间没有大小的意义：例如颜色（红色，蓝色，绿色）

    pandas.get_dummies(data, prefix=None, prefix_sep='', dummy_na=False, columns=None, drop_first=False)

    - data：要处理的dataframe
    - prefix：列名的前缀在多个列有相同的离散项时候使用
    - prefix_sep：前缀和离散值得分隔符，默认为下划线，默认即可
    - dummy_na：是否把NA值，作为一个离散值进行处理，默认不处理
    - columns：要处理的列名如果不指定该列，那默认处理所有列
    - drop_first：是否从备用选项中删除第一个，建模的时候为避免共线性使用

```python
# -*- coding: utf-8 -*-
import pandas

data = pandas.read_csv('./data.csv', encoding='utf8')

data['Education Level'].drop_duplicates()

"""
博士后    Post-Doc
博士      Doctorate
硕士      Master's Degree
学士      Bachelor's Degree
副学士    Associate's Degree
专业院校  Some College
职业学校  Trade School
高中      High School
小学      Grade School
"""
educationLevelDict = {
    'Post-Doc': 9,
    'Doctorate': 8,
    'Master\'s Degree': 7,
    'Bachelor\'s Degree': 6,
    'Associate\'s Degree': 5,
    'Some College': 4,
    'Trade School': 3,
    'High School': 2,
    'Grade School': 1
}

data['Education Level Map'] = data[
    'Education Level'
].map(
    educationLevelDict
)

data['Gender'].drop_duplicates()

dummies = pandas.get_dummies(
    data,
    columns=['Gender'],
    prefix=['Gender'],
    prefix_sep="_",
    dummy_na=False,
    drop_first=False
)

dummies['Gender'] = data['Gender']

```
# 数据分析
## 基本统计
基本统计分析：描述性统计分析，用来概括失误整体情况以及事物之间的联系（即事物的基本特征），以发现其内在规律的统计分析方法

常用统计指标



- 计数
- 求和
- 平均值
- 方差
- 标准差

```python
import pandas

data = pandas.read_csv('./data.csv')

data.score.describe()

data.score.size
data.score.max()
data.score.min()
data.score.sum()
data.score.mean()
data.score.var()
data.score.std()

#累计求和
data.score.cumsum()

#最大值和最小值所在位置
data.score.argmin()
data.score.argmax()

data.score.quantile(
    0.3,
    interpolation="nearest"
)

```

## 分组分析

分组分析是指根据分组字段，将分析对象划分为不同部分，以进行对比分析各组之间的差异性的一种分析方法。

常用的统计指标
- 计数
- 求和
- 平均值

```python
import numpy
import pandas

data = pandas.read_csv('./data.csv')

aggResult = data.groupby(
    by=['class']
)['score'].agg({
    '总分': numpy.sum,
    '人数': numpy.size,
    '平均值': numpy.mean
})

```
## 分布分析

分布分析是指根据分析目的，将数据（定量数据）进行等距后者不等距的分组，进行研究各组分布规律的一种分析方法。

```python
import numpy
import pandas

data = pandas.read_csv('./data.csv')

bins = [
    min(data.年龄)-1, 20, 30, 40, max(data.年龄)+1
]
labels = [
    '20岁以及以下', '21岁到30岁', '31岁到40岁', '41岁以上'
]

data['年龄分层'] = pandas.cut(
    data.年龄,
    bins,
    labels=labels
)

aggResult = data.groupby(
    by=['年龄分层']
)['年龄'].agg({
    '人数': numpy.size
})

pAggResult = round(
    aggResult/aggResult.sum(),
    2
)*100
pAggResult['人数'].map('{:,.2f}%'.format)

```

## 交叉分析

交叉分析是指分析两个或者两个以上的分组变量之间的关系，已交叉表的形式进行变量之间的对比分析。



交叉计数函数：pivot_table(values, index, columns, aggfunc, fill_value)
- values：数据透视表中的值
- index：数据透视表中的行
- columns：数据统计表中的列
- aggfunc：统计函数
- fill_value：NA值得统一替换

```python
import numpy
import pandas

data = pandas.read_csv('./data.csv')

bins = [
    min(data.年龄)-1, 20, 30, 40, max(data.年龄)+1
]
labels = [
    '20岁以及以下', '21岁到30岁', '31岁到40岁', '41岁以上'
]

data['年龄分层'] = pandas.cut(
    data.年龄,
    bins,
    labels=labels
)

ptResult = data.pivot_table(
    values=['年龄'],
    index=['年龄分层'],
    columns=['性别'],
    aggfunc=[numpy.size]
)

```
## 结构分析

结构分析是指在分组以及交叉的基础之上，计算各组成部分所占的比重，进而分析总体的内部特征的一种分析方法。

```python
import numpy
import pandas

data = pandas.read_csv('./data.csv')

bins = [
    min(data.年龄)-1, 20, 30, 40, max(data.年龄)+1
]
labels = [
    '20岁以及以下', '21岁到30岁', '31岁到40岁', '41岁以上'
]

data['年龄分层'] = pandas.cut(
    data.年龄,
    bins,
    labels=labels
)

ptResult = data.pivot_table(
    values=['年龄'],
    index=['年龄分层'],
    columns=['性别'],
    aggfunc=[numpy.size]
)

ptResult.sum()
# 0 按列运算
ptResult.sum(axis=0)
# 1 按行运算
ptResult.sum(axis=1)
ptResult.div(ptResult.sum(axis=1), axis=0)
ptResult.div(ptResult.sum(axis=0), axis=1)

```
## 相关分析

相关分析是指研究两个或者两个以上的随机变量之间相互依存关系的方向和密切程度的方法。

线性相关主要采用皮尔逊相关系数r来度量连续变量之间线性相关强度。
- 0<=|r|<0.3 低度相关
- 0.3<=|r|<0.8 中度相关
- 0.8<=|r|<=1 高度相关

```python
# -*- coding: utf-8 -*-
import pandas

data = pandas.read_csv('./data.csv')

#先来看看如何进行两个列之间的相关度的计算
data['人口'].corr(data['文盲率'])

#多列之间的相关度的计算方法
#选择多列的方法
data[[
    '超市购物率', '网上购物率', '文盲率', '人口'
]]

data[[
    '超市购物率', '网上购物率', '文盲率', '人口'
]].corr()

```

## RFM分析

RFM分析是根据客户活跃度和交易金额贡献，进行客户价值的细分的一种方法；



- R：客户最近交易的时间间隔
- F：客户最近的交易次数
- M：客户最近的交易金额

```python
import numpy
import pandas

data = pandas.read_csv('./data.csv')

data['DealDateTime'] = pandas.to_datetime(
    data.DealDateTime,
    format='%Y/%m/%d'
)

data['DateDiff'] = pandas.to_datetime(
    'today'
) - data['DealDateTime']

data['DateDiff'] = data['DateDiff'].dt.days

R_Agg = data.groupby(
    by=['CustomerID']
)['DateDiff'].agg({
    'RecencyAgg': numpy.min
})

F_Agg = data.groupby(
    by=['CustomerID']
)['OrderID'].agg({
    'FrequencyAgg': numpy.size
})

M_Agg = data.groupby(
    by=['CustomerID']
)['Sales'].agg({
    'MonetaryAgg': numpy.sum
})

aggData = R_Agg.join(F_Agg).join(M_Agg)

bins = aggData.RecencyAgg.quantile(
    q=[0, 0.2, 0.4, 0.6, 0.8, 1],
    interpolation='nearest'
)
bins[0] = 0
labels = [5, 4, 3, 2, 1]
R_S = pandas.cut(
    aggData.RecencyAgg,
    bins, labels=labels
)

bins = aggData.FrequencyAgg.quantile(
    q=[0, 0.2, 0.4, 0.6, 0.8, 1],
    interpolation='nearest'
)
bins[0] = 0;
labels = [1, 2, 3, 4, 5];
F_S = pandas.cut(
    aggData.FrequencyAgg,
    bins, labels=labels
)

bins = aggData.MonetaryAgg.quantile(
    q=[0, 0.2, 0.4, 0.6, 0.8, 1],
    interpolation='nearest'
)
bins[0] = 0
labels = [1, 2, 3, 4, 5]
M_S = pandas.cut(
    aggData.MonetaryAgg,
    bins, labels=labels
)

aggData['R_S']=R_S
aggData['F_S']=F_S
aggData['M_S']=M_S

aggData['RFM'] = 100*R_S.astype(int) + 10*F_S.astype(int) + 1*M_S.astype(int)

bins = aggData.RFM.quantile(
    q=[
        0, 0.125, 0.25, 0.375, 0.5,
        0.625, 0.75, 0.875, 1
    ],
    interpolation='nearest'
)
bins[0] = 0
labels = [1, 2, 3, 4, 5, 6, 7, 8]
aggData['level'] = pandas.cut(
    aggData.RFM,
    bins, labels=labels
)

aggData = aggData.reset_index()

aggData.sort(
    ['level', 'RFM'],
    ascending=[1, 1]
)

aggData.groupby(
    by=['level']
)['CustomerID'].agg({
    'size':numpy.size
})

```

## 矩阵分析

矩阵分析是指根据事物的重要属性作为分析依据，进行关联分析，找出解决问题的一种方法。

```python

import pandas
import matplotlib
import matplotlib.pyplot as plt

mainColor = (42/256, 87/256, 141/256, 1);

#设置字体
font = {
    'family': 'SimHei',
    'size': 20    
}
matplotlib.rc('font', **font);


data = pandas.read_csv('./data.csv')

fig = plt.figure(
    figsize=(30, 20),
    dpi=80
)

sp = fig.add_subplot(111)

sp.set_xlim([
    0,
    data.GDP.max()*1.1
])
sp.set_ylim([
    0,
    data.population.max()*1.1
])

#关闭坐标轴、坐标轴的刻度值
#sp.axis('off')
sp.get_xaxis().set_ticks([])
sp.get_yaxis().set_ticks([])

#画点
sp.scatter(
    data.GDP, data.population,
    alpha=0.5, s=200, marker="o",
    edgecolors=mainColor, linewidths=5
)

#画均值线
sp.axvline(
    x=data.GDP.mean(),
    linewidth=1, color=mainColor
)
sp.axhline(
    y=data.population.mean(),
    linewidth=1, color=mainColor
)

sp.axvline(
    x=0,
    linewidth=3, color=mainColor
)
sp.axhline(
    y=0,
    linewidth=3, color=mainColor
)

sp.set_xlabel('GDP')
sp.set_ylabel('人口')

#画标签
data.apply(
    lambda row: plt.text(
            row.GDP,
            row.population,
            row.province,
            fontsize=15
        ),
    axis=1
)

plt.show()


```
# 数据可视化
## 散点图

散点图是指以一个变量为横坐标，另外一个坐标为纵坐标，利用散点的分布形态反应变量之间关系的一种图形。

散点图绘制函数：plot(x, y, '.', color=(r,g,b))
- x,y：X轴和Y轴的序列
- . o 表示小点还是大点
- color：散点的颜色可用rgb来定义，也可用英文字母定义

```python
# -*- coding: utf-8 -*-

import pandas
import matplotlib
import matplotlib.pyplot as plt

data = pandas.read_csv('./data.csv')

mainColor = (42/256, 87/256, 141/256, 1)

font = {
    'size': 20,
    'family': 'SimHei'
}
matplotlib.rc('font', **font)

#%matplotlib qt

#plt.grid(True)

#小点
plt.xlabel('广告费用', color=mainColor)
plt.ylabel('购买用户数', color=mainColor)
plt.tick_params(axis='x', colors=mainColor)
plt.tick_params(axis='y', colors=mainColor)
plt.plot(
    data['广告费用'],
    data['购买用户数'],
    '.', color=mainColor
)

#大点
plt.xlabel('广告费用', color=mainColor)
plt.ylabel('购买用户数', color=mainColor)
plt.tick_params(axis='x', colors=mainColor)
plt.tick_params(axis='y', colors=mainColor)
plt.plot(
    data['广告费用'],
    data['购买用户数'],
    "o", color=mainColor
)

```


## 折线图

折线图也成趋势图，它是用直线段将个数据点链接起来而组成的图形，以折线的方式显示数据变化的趋势。

折线绘图函数：plot(x, y, style, color, lonewidth)

- style：画线的样式
    - \- 连续的曲线
    - -- 连续的虚线
    - -. 连续带圆点的曲线
    - : 由点连成的曲线
    - . 小点，散点图
    - o 大点，散点图
    - ， 像素点（更小的点）的散点图
    - \* 五角星的点的散点图
- color：画线的颜色
- linewidth：线的宽度

```python
import pandas
import matplotlib
from matplotlib import pyplot as plt

data = pandas.read_csv('./data.csv')

#对日期格式进行转换
data['购买日期'] = pandas.to_datetime(
    data['日期']
)


mainColor = (42/256, 87/256, 141/256, 1);

font = {
    'size': 20,
    'family': 'SimHei'
}
matplotlib.rc('font', **font)

#%matplotlib qt

plt.xlabel(
    '购买日期',
    color=mainColor
)
plt.ylabel(
    '购买用户数',
    color=mainColor
)
plt.tick_params(
    axis='x',
    colors=mainColor
)
plt.tick_params(
    axis='y',
    colors=mainColor
)

#'-'	顺滑的曲线
plt.plot(
    data['购买日期'],
    data['购买用户数'],
    '-', color=mainColor
)

plt.title('购买用户数')
plt.show()

#设置线条粗细
plt.plot(
    data['购买日期'],
    data['购买用户数'],
    '-', color=mainColor,
    lineWidth=10
)

#'--'	虚线
plt.plot(data['购买日期'], data['购买用户数'], '--');
#'-.'	线加点
plt.plot(data['购买日期'], data['购买用户数'], '-.');
#':'	由点组成的曲线
plt.plot(data['购买日期'], data['购买用户数'], ':');
#'.'	散点图
plt.plot(data['购买日期'], data['购买用户数'], '.');
#','	像素点的散点图
plt.plot(data['购买日期'], data['购买用户数'], ',');
#'o'	大点的散点图
plt.plot(data['购买日期'], data['购买用户数'], 'o');
#'v'	下三角标记的散点图
plt.plot(data['购买日期'], data['购买用户数'], 'v');
#'^'	上上角标记的散点图
plt.plot(data['购买日期'], data['购买用户数'], '^');
#'<'	左角标记的散点图
plt.plot(data['购买日期'], data['购买用户数'], '<');
#'>'	右角标记的散点图
plt.plot(data['购买日期'], data['购买用户数'], '>');
#'1'	伞形下的标记散点图
#'2'	伞形上的标记散点图
#'3'	伞形左的标记散点图
#'4'	伞形右的标记散点图
plt.plot(data['购买日期'], data['购买用户数'], '4');
#'s'	正方形标记的散点图
plt.plot(data['购买日期'], data['购买用户数'], 's');
#'p'	五角形标记的散点图
plt.plot(data['购买日期'], data['购买用户数'], 'p');
#'*'	五角星标记的散点图
plt.plot(data['购买日期'], data['购买用户数'], '*');
#'h'	多边形标记的散点图
#'H'	hexagon2 marker
plt.plot(data['购买日期'], data['购买用户数'], 'h');
#'+'	plus marker
#'x'	x marker
#'D'	diamond marker
#'d'	thin_diamond marker
plt.plot(data['购买日期'], data['购买用户数'], 'D');
#'|'	vline marker
#'_'	hline marker
plt.plot(data['购买日期'], data['购买用户数'], '|');


```

## 饼图


饼图又称圆形图，是一个划分为几个扇形的圆形统计图，它能够直观的反映出来个体与总体之间的比例关系

绘制饼图的函数：pie(x, labels, colors, explode, autopct)
- x：绘制图形的序列
- labels：饼图各部分的标签的序列
- colors：饼图各部分的颜色
- explode：需要突出的块状序列
- autopct：饼图占比的显示格式，%.02f：保留两位小数

```python
# -*- coding: utf-8 -*-

import numpy
import pandas
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.font_manager as font_manager

#%matplotlib qt
#设置不在交互式命令行绘图，在弹出新的窗口进行绘图

data = pandas.read_csv('./data.csv')

result = data.groupby(
    by=['通信品牌'],
    as_index=False
)['号码'].agg({
    '用户数': numpy.size
})

#设置长宽分辨率
plt.figure(figsize=(30, 30), dpi=80)

#使用绝对路径获取字体的名称的方法
fontProp = font_manager.FontProperties(
    fname="C:\\Windows\\Fonts\\FZSTK.TTF"
)
#设置字体
font = {
    'family': fontProp.get_name(),
    'size': 20
}
matplotlib.rc('font', **font)

#设置为横轴和纵轴等长的饼图
#也就是圆形的饼图，而非椭圆形的饼图
plt.axis('equal')

plt.pie(
    result['用户数'],
    labels=result['通信品牌'],
    autopct='%.2f%%'
)


#设置突出的部分
explode = (0.1, 0.2, 0.3)
plt.axis('equal')
plt.pie(
    result['用户数'],
    labels=result['通信品牌'],
    autopct='%.2f%%',
    explode=explode,
    startangle=67
)

plt.show()

```


## 柱形图

柱形图是一种以长方形的单位长度，根据数据大小绘制的统计图，用来比较两个或者两个以上的数据（时间或者类别）

柱状图的绘制函数：
- bar(left, height, width, color)
- barh(bottom, width, height, color)

1. 柱形图的绘制

```python
# -*- coding: utf-8 -*-

import numpy
import pandas
import matplotlib
from matplotlib import pyplot as plt

font = {
    'family' : 'SimHei'
}
matplotlib.rc('font', **font)

data = pandas.read_csv('./data.csv')

result = data.groupby(
    by=['手机品牌'],
    as_index=False
)['月消费（元）'].agg({
    '月消费': numpy.sum
})

#竖向柱形图
index = numpy.arange(
    result.月消费.size
)
plt.bar(index, result['月消费'])
plt.show()

#优化点1、配置颜色
mainColor = (42/256, 87/256, 141/256, 1)
plt.bar(
    index, result['月消费'],
    color=mainColor
)
plt.show()

#优化点2、配置X轴刻度
plt.bar(
    index, result['月消费'],
    color=mainColor
)
plt.xticks(index, result.手机品牌)
plt.show()

#优化点3、对数据排序后再绘图
sgb = result.sort_values(
    by="月消费",
    ascending=False
)
plt.bar(
    index, sgb.月消费,
    color=mainColor
)
plt.xticks(index, sgb.手机品牌)
plt.show()


#横向柱形图
plt.barh(
    index, sgb.月消费,
    color=mainColor
)
plt.yticks(index, sgb.手机品牌)
plt.show()

```

2. 分组柱形图
```python
# -*- coding: utf-8 -*-

import numpy
import pandas
import matplotlib
from matplotlib import pyplot as plt

font = {
    'family' : 'SimHei'
};
matplotlib.rc('font', **font);

data = pandas.read_csv('./data.csv')

result = data.pivot_table(
    values='月消费（元）',
    index='手机品牌',
    columns='通信品牌',
    aggfunc=numpy.sum
)

index = numpy.arange(len(result))
minColor = (42/256, 87/256, 141/256, 1/3)
midColor = (42/256, 87/256, 141/256, 2/3)
maxColor = (42/256, 87/256, 141/256, 3/3)

#使用排列的方式，把数据排列放好，即为多维条形图
plt.bar(
    index, result['全球通'],
    color=minColor, width=1/4
)
plt.bar(
    index+1/4, result['动感地带'],
    color=midColor, width=1/4
)
plt.bar(
    index+2/4, result['神州行'],
    color=maxColor, width=1/4
)
plt.xticks(index+1/3, result.index)
plt.legend(['全球通', '动感地带', '神州行'])
plt.show()

#优化一下，对数据进行一个排序
result = result.sort_values(
    by="神州行", ascending=False
)

plt.bar(
    index, result['神州行'],
    color=maxColor, width=1/4
)
plt.bar(
    index+1/4, result['动感地带'],
    color=midColor, width=1/4
)
plt.bar(
    index+2/4, result['全球通'],
    color=minColor, width=1/4
)
plt.xticks(index+1/3, result.index)
plt.legend(['神州行', '动感地带', '全球通'])
plt.show()

```

3. 堆积柱形图

```python
# -*- coding: utf-8 -*-

import numpy
import pandas
import matplotlib
from matplotlib import pyplot as plt

font = {
    'family' : 'SimHei'
}
matplotlib.rc('font', **font)

data = pandas.read_csv('./data.csv')

result = data.pivot_table(
    values='月消费（元）',
    index='手机品牌',
    columns='通信品牌',
    aggfunc=numpy.sum
)

index = numpy.arange(len(result))
minColor = (42/256, 87/256, 141/256, 1/3)
midColor = (42/256, 87/256, 141/256, 2/3)
maxColor = (42/256, 87/256, 141/256, 3/3)

result = result.sort_values(
    by="神州行", ascending=False
)

#使用排列的方式，把数据堆叠放好，即为多维条形图
plt.bar(
    index, result['神州行'],
    color = maxColor
)
plt.bar(
    index, result['动感地带'],
    bottom=result['神州行'],
    color = midColor
)
plt.bar(
    index, result['全球通'],
    bottom=result['神州行']+result['动感地带'],
    color = minColor
)
plt.xticks(index, result.index)
plt.legend(['神州行', '动感地带', '全球通'])
plt.show()

```

4. 双向柱形图

```python
# -*- coding: utf-8 -*-
import numpy
import pandas
import matplotlib
from matplotlib import pyplot as plt

font = {
    'family' : 'SimHei'
}
matplotlib.rc('font', **font)

#解决负号是一个矩形的问题
matplotlib.rcParams['axes.unicode_minus']=False  

data = pandas.read_csv('./data.csv')

result = data.pivot_table(
    values='月消费（元）',
    index='手机品牌',
    columns='通信品牌',
    aggfunc=numpy.sum
);

index = numpy.arange(len(result));
minColor = (42/256, 87/256, 141/256, 1/3)
midColor = (42/256, 87/256, 141/256, 2/3)
maxColor = (42/256, 87/256, 141/256, 3/3)

result = result.sort_values(
    by="神州行",
    ascending=False
)

#使用排列的方式，把数据堆叠放好，即为多维条形图
plt.barh(
    index,
    result['动感地带'],
    color = minColor
)
plt.barh(
    index,
    -result['神州行'],
    color = maxColor
)
plt.yticks(index, result.index)
plt.legend(['动感地带', '神州行'])
plt.show()

```

## 直方图

直方图是用一系列等宽不等高的长方形来绘制的，高度表示数据范围的间隔，高度表示在给定间隔内数据出现的频数，变化的高度形态表示数据的分布。

直方图绘图的函数：hist(x, color, bins, cumulative=False)
- x：需要进行绘制的向量
- color：直方图的填充颜色
- bins：设置直方图的分组个数
- cumulative：设置是否累计，默认为False

```python
# -*- coding: utf-8 -*-
import pandas
import matplotlib
from matplotlib import pyplot as plt

font = {
    'family' : 'SimHei'
}
matplotlib.rc('font', **font)

data = pandas.read_csv('./data.csv')

mainColor = (42/256, 87/256, 141/256, 1)

plt.hist(data['购买用户数'], color=mainColor)
plt.show()

plt.hist(data['购买用户数'], bins=20, color=mainColor)
plt.show()

plt.hist(
    data['购买用户数'], bins=20,
    cumulative=True, color=mainColor
)
plt.show()

```


## 地图

```python

```

## 热力地图

```python

```

---

## File: content/posts/expire/2019.12.08 python 数据挖掘.md

[Binary file]

---

## File: content/posts/expire/2020.03.19 流媒体简介.md

---
layout: post
title:  "流媒体简介"
layout: "posts"
date:   2020-03-19 15:57:00 +0800
categories: ['杂七杂八']
tags: ["视频下载"]
url: /post/Introduction-to-streaming-media/
---


### 什么是流媒体

> **流媒体**（英语：**Streaming media**）是指将一连串的[多媒体](https://zh.wikipedia.org/wiki/多媒體)数据压缩后，经过[互联网](https://zh.wikipedia.org/wiki/網際網路)分段发送数据，在互联网上即时传输影音以供观赏的一种技术与过程，此技术使得数据[数据包](https://zh.wikipedia.org/wiki/封包)得以像流水一样发送，如果不使用此技术，就必须在使用前[下载](https://zh.wikipedia.org/wiki/下载)整个媒体文件。

### 视频文件格式

视频文件格式被称为**容器格式**

编码格式则为容器中视频流的压缩编码格式

### 编码格式

没有编码的数字视频数据量很大储存困难、传输困难

国际上制定视频编码技术的组织有两家：国际电联（ITU-T）、国际标准化组织（ISO）

国际电联：H261、H262、H263+

国际标准化组织：MPEG1、MEPG2、MEPG4

H264 是两个组织联合组件的联合视频组。

### 流媒体

3 中主流的流媒体协议：HTTP、RTSP、RTMP

HLS（HTTP Live Streaming）是苹果公司提出的基于 HTTP 的流媒体传输协议，在开始会话时客户端会下载一个包含元数据的 extended M3U（m3u8）playlist，用于寻找可用的流媒体

HLS 协议规定：视频封装格式为 ts、视频编码格式是 H264

### 代码仓库

100 行代码搞定 HTL 下载：[github地址](https://github.com/caorushizi/mediago "Github 地址")

---

## File: content/posts/expire/2020.07.21 函数式编程与JS异步编程、手写Promise.md

---
layout: post
title: "函数式编程与JS异步编程、手写Promise"
layout: "posts"
date: 2020-07-21 22:42:00 +0800
categories: ["大前端"]
tags: []
url: /post/part-1-module-1-homework/
---

### 简答题

- 谈谈你是如何理解JS异步编程的，EventLoop、消息队列都是做什么的，什么是宏任务，什么是微任务？

javascript 为了实现浏览器页面的交互，不得不使用单线程的方式渲染页面，以保证 dom 可以被正确的渲染。然而单线程就以为着浏览器有多个任务的时候需要排队依次执行，这样就会造成阻塞。event loop 主线程从消息队列中读取事件，这个过程是一直重复循环的，所以整个机制被称作event loop。消息队列是暂时存放异步任务的地方，我们的异步代码会存放到消息队列中，等到同步代码执行完毕以后，event loop会从消息队列中依次取出异步任务放到调用栈中再次执行。宏任务可以理解是每次执行栈执行的代码就是一个宏任务（包括每次从事件队列中获取一个事件回调并放到执行栈中执行）。微任务可以理解是在当前 task 执行结束后立即执行的任务。也就是说，在当前task任务后，下一个task之前，在渲染之前。

### 代码题

#### 一、将下面的异步代码使用Promise的方式改进

```js
setTimeout(function () {
    var a = 'hello'
    setTimeout(function () {
        var b = 'lagou'
        setTimeout(function () {
            var c = 'I ❤️ U'
            console.log(a + b + c)
        }, 10)
    }, 10)
}, 10)
```

```js
function genPromise (text) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(text)
    }, 10)
  })
}

(async () => {
  const a = await genPromise('hello')
  const b = await genPromise('lagou')
  const c = await genPromise('I ❤️ U')
  console.log(a + b + c)
})()
```

#### 二、基于以下代码完成下面的四个练习

```js
const fp = require('lodash/fp')
// 数据
// horsepower 马力， dollar_value 价格，in_stock 库存
const cars = [
    { name: 'Ferrari FF', horsepower: 660, dollar_value: 700000, in_stock: true },
    { name: 'Spyker C12 Zagato', horsepower: 650, dollar_value: 648000, in_stock: false },
    { name: 'Jaguar XKR-S', horsepower: 550, dollar_value: 132000, in_stock: false },
    { name: 'Audi R8', horsepower: 525, dollar_value: 114200, in_stock: false },
    { name: 'Aston Martin One-77', horsepower: 750, dollar_value: 1850000, in_stock: true },
    { name: 'Pagani Huayra', horsepower: 700, dollar_value: 1300000, in_stock: false },
]
```

- 练习1：使用函数组合 fp.flowRight() 重新实现下面这个函数

```js
let isLastInStock = function (cars) {
    // 获取最后一条数据
    let last_car = fp.last(cars)
    // 获取最后一条数据的 in_stock 属性值
    return fp.prop('in_stock', last_car)
}
```

```js
const isLastInStock = fp.flowRight(fp.prop("in_stock"), fp.last);
console.log(isLastInStock(cars));
```

- 练习2：使用 fp.flowRight()、fp.prop()和fp.first()获取第一个 car 的 name

```js
const lastCarName = fp.flowRight(fp.prop("name"), fp.first);
console.log(lastCarName(cars));
```

- 练习3：使用帮助函数 _average 重构 averageDollarValue， 使用函数组合的方式实现

```js
let _average = function (xs) {
  return fp.reduce(fp.add, 0, xs) / xs.length
} // <- 无须改动
let averageDollarValue = function (cars) {
  let dollar_values = fp.map(function (car) {
    return car.dollar_value
  }, cars)
  return _average(dollar_values)
}
```

```js
const dollar_value = fp.map(function(car) {
  return car.dollar_value;
});
const averageDollarValue = fp.flowRight(_average, dollar_value);
```

- 练习4：使用flowRight写一个sanitizeNames()函数，返回一个下划线连接的小写字符串，把数组中的 name 转换为这种形式：例如：sanitizeNames(["Hello World"]) => ['hello_world']

```js
let _underscore = fp.replace(/\W+/g, '_') // <-- 无须改动，并在 sanitizeNames 中使用它
```

```js
const sanitizeNames = fp.map(fp.flowRight(_underscore, fp.toLower));
```

#### 三、基于下面提供的代码，完成后续的四个练习

```js
// support.js
class Container {
  static of (value) {
    return new Container(value)
  }

  constructor (value) {
    this._value = value
  }

  map (fn) {
    return Container.of(fn(this._value))
  }
}

class Maybe {
  static of (x) {
    return new Maybe(x)
  }

  isNothing () {
    return this._value === null || this._value === undefined
  }

  constructor (x) {
    this._value = x
  }

  map (fn) {
    return this.isNothing() ? this : Maybe.of(fn(this._value))
  }
}

module.exports = { Maybe, Container }
```

- 练习1：使用fp.add(x,y) 和 fp.map(f, x)创建一个能让 functor 里的值增加的函数ex1

```js
// app.js
const fp = require('lodash/fp')
const {Maybe,Container} = require('./support')
let maybe = Maybe.of([5,6,1])
let ex1 = ()=>{
// 你需要实现的函数。。。
}
```

```js
const addSome = fp.map(fp.add(1));
maybe.map(addSome).map(console.log);
```

- 练习2：实现一个函数 ex2， 能够使用 fp.first 获取列表中的第一个元素

```js
// app.js
const fp = require('lodash/fp')
const { Maybe, Container } = require('./support')
let xs = Container.of(['do', 'ray', 'me', 'fa', 'so', 'la', 'ti', 'do'])
let ex2 = () => {
// 你需要实现的函数。。。
}
```

```js
xs.map(fp.first).map(console.log);
```

- 练习3：实现一个函数ex3，使用safeProp和fp.first 找到user的名字和首字母

```js
// app.js
const fp = require('lodash/fp')
const { Maybe, Container } = require('./support')
let safeProp = fp.curry(function (x, o) {
  return Maybe.of(o[x])
})
let user = { id: 2, name: 'Albert' }
let ex3 = () => {
  // 你需要实现的函数。。。
}
```

```js
safeProp("name", user).map(fp.first).map(console.log)
```

- 练习4： 使用Maybe重写 ex4 ，不要有 if 语句

```js
// app.js
const fp = require('lodash/fp')
const { Maybe, Container } = require('./support')
let ex4 = function (n) {
  if (n) {
    return parseInt(n)
  }
}
```

```js
const { Maybe } = require("./support");
let ex4 = function(n) {
  Maybe.of(n).map(parseInt).map(console.log)
};
ex4(null)
```

#### 四、手写实现 MyPromise 源码

要求：尽可能还原Promise中的每一个API，并通过注释的方式描述思路和原理

```js
const PENDING = 'pending' // pending
const FULFILLED = 'fulfilled' // fulfilled
const REJECTED = 'rejected' // rejected

export default class MyPromise {
  constructor (executor) {
    try {
      executor(this.resolve, this.reject)
    } catch (e) {
      this.reject(e)
    }
  }

  // promsie 状态
  status = PENDING
  // 成功之后的值
  value = undefined
  // 失败后的原因
  reason = undefined
  // 成功回调
  successCallback = []
  // 失败回调
  failCallback = []

  resolve = value => {
    // 如果状态不是等待 阻止程序向下执行
    if (this.status !== PENDING) return
    // 将状态更改为成功
    this.status = FULFILLED
    // 保存成功之后的值
    this.value = value
    // 判断成功回调是否存在 如果存在 调用
    while (this.successCallback.length) this.successCallback.shift()()
  }
  reject = reason => {
    // 如果状态不是等待 阻止程序向下执行
    if (this.status !== PENDING) return
    // 将状态更改为失败
    this.status = REJECTED
    // 保存失败后的原因
    this.reason = reason
    // 判断失败回调是否存在 如果存在 调用
    // this.failCallback && this.failCallback(this.reason);
    while (this.failCallback.length) this.failCallback.shift()()
  }

  then (successCallback, failCallback) {
    // 参数可选
    successCallback = successCallback ? successCallback : value => value
    // 参数可选
    failCallback = failCallback ? failCallback : reason => { throw reason }
    let promsie2 = new MyPromise((resolve, reject) => {
      // 判断状态
      if (this.status === FULFILLED) {
        setTimeout(() => {
          try {
            let x = successCallback(this.value)
            // 判断 x 的值是普通值还是promise对象
            // 如果是普通值 直接调用resolve
            // 如果是promise对象 查看promsie对象返回的结果
            // 再根据promise对象返回的结果 决定调用resolve 还是调用reject
            resolvePromise(promsie2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      } else if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            let x = failCallback(this.reason)
            // 判断 x 的值是普通值还是promise对象
            // 如果是普通值 直接调用resolve
            // 如果是promise对象 查看promsie对象返回的结果
            // 再根据promise对象返回的结果 决定调用resolve 还是调用reject
            resolvePromise(promsie2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      } else {
        // 等待
        // 将成功回调和失败回调存储起来
        this.successCallback.push(() => {
          setTimeout(() => {
            try {
              let x = successCallback(this.value)
              // 判断 x 的值是普通值还是promise对象
              // 如果是普通值 直接调用resolve
              // 如果是promise对象 查看promsie对象返回的结果
              // 再根据promise对象返回的结果 决定调用resolve 还是调用reject
              resolvePromise(promsie2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
        this.failCallback.push(() => {
          setTimeout(() => {
            try {
              let x = failCallback(this.reason)
              // 判断 x 的值是普通值还是promise对象
              // 如果是普通值 直接调用resolve
              // 如果是promise对象 查看promsie对象返回的结果
              // 再根据promise对象返回的结果 决定调用resolve 还是调用reject
              resolvePromise(promsie2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
      }
    })
    return promsie2
  }

  finally (callback) {
    return this.then(value => {
      return MyPromise.resolve(callback()).then(() => value)
    }, reason => {
      return MyPromise.resolve(callback()).then(() => { throw reason })
    })
  }

  catch (failCallback) {
    return this.then(undefined, failCallback)
  }

  static all (array) {
    let result = []
    let index = 0
    return new MyPromise((resolve, reject) => {
      function addData (key, value) {
        result[key] = value
        index++
        if (index === array.length) {
          resolve(result)
        }
      }

      for (let i = 0; i < array.length; i++) {
        let current = array[i]
        if (current instanceof MyPromise) {
          // promise 对象
          current.then(value => addData(i, value), reason => reject(reason))
        } else {
          // 普通值
          addData(i, array[i])
        }
      }
    })
  }

  static resolve (value) {
    if (value instanceof MyPromise) return value
    return new MyPromise(resolve => resolve(value))
  }
}

function resolvePromise (promsie2, x, resolve, reject) {
  if (promsie2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  if (x instanceof MyPromise) {
    // promise 对象
    // x.then(value => resolve(value), reason => reject(reason));
    x.then(resolve, reject)
  } else {
    // 普通值
    resolve(x)
  }
}
```

---

## File: content/posts/expire/2020.07.26 ES 新特性与 TypeScript、JS 性能优化.md

---
layout: post
title: "ES 新特性与 TypeScript、JS 性能优化"
layout: "posts"
date: 2020-07-26 18:34:00 +0800
categories: ["大前端"]
tags: []
url: /post/part-1-module-2-note/
---

### ECMAScript 概述

最佳实践：不用var，主要使用const，配合let

---

## File: content/posts/expire/2020.08.03 ES 新特性与 TypeScript、JS 性能优化.md

---
layout: post
title: "ES 新特性与 TypeScript、JS 性能优化"
layout: "posts"
date: 2020-08-03 22:00:00 +0800
categories: ["大前端"]
tags: []
url: /post/part-1-module-2-homework/
---

### 简答题

一、请说出下列最终的执行结果，并解释为什么。

```js
var a = []
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i)
  }
}
a[6]()
```

最终结果是在控制台输出10；for循环中使用var定义变量没有块级作用域，在a\[6\]方式执行的时候会根据js的词法作用域向上查找找到的i是已经经过++操作的10；

二、请说出下列最终的执行结果，并解释为什么。

```js
var tmp = 123
if (true) {
  console.log(tmp)
  let tmp
}
```

最终结果会出现引用错误；在if的代码块中出现了let声明变量，所以在if代码块中就出现了tmp变量的暂时性死区，js的编译器会抛出引用错误；

三、结合ES6新语法，用最简单的方式找出数组中的最小值。

```js
var arr = [12, 34, 32, 89, 4]
```

```js
Math.min(...arr)
```

四、请详细说明var， let， const三种声明的方式之间的具体差别。

var声明变量是会出现变量提升，而let和const不会；var可以重复定义，而let和const不行；let和var定义的变量可以被改变，而const不可以；

五、请说出下面代码最终输出的结果，并解释为什么。

```js
var a = 10
var obj = {
  a: 20,
  fn () {
    setTimeout(() => {
      console.log(this.a)
    })
  }
}

obj.fn()
```

执行的结果是20；箭头函数不改变运行时this的指向，所以log中的this指向距离最近的fn中的this，以上代码等同于

```js
var a = 10
var obj = {
  a: 20,
  fn () {
    let _this = this
    setTimeout(() => {
      console.log(_this.a)
    })
  }
}
obj.fn()
```

六、简述 Symbol 类型的用途。

Symbol可以声明一个独一无二的变量可以用作对象的键，可以用来做依赖注入中的服务名称或者控制器名称。同时Symbol中中定义了一些常量可以用来迭代数组等等。

七、说说什么是浅拷贝，什么是深拷贝？

浅拷贝是指将"引用"的地址存储在变量中，该地址中的某一些变化可以通过新变量访问；深拷贝是将"引用"通过递归等手段将其中"值"分别赋值；

八、请简述Typescript 和Javascript之间的关系。

javascript可以直接运行在V8引擎中，而ts需要通过tsc将ts代码编译成js代码才能运行在v8上；在es6没有普及的时候可以通过ts使用javascript中的高级语法；

九、请谈谈你所认为的Typescript优缺点。

typescript可以大大降低在生产中类型引起的一系列问题，因为在ts编译的时候会进行类型检查；在ts中可以自由的使用js中的一些没有普及的语法；但是ts学习成本比较高，在已有的项目中使用ts会存在大量代码重构带来的高成本；

十、描述引用计数的工作原理和优缺点。

引用计数用来记录当前有多少指针指向同一块动态分配的内存。当有指针指向这块内存时，计数器加1；当指向此内存的指针销毁时，计数器减1。当引用计数为0时，表示此块内存没有被任何指针指向，此块被共享的动态内存才能被释放。
当变量循环引用时无法判断变量是否需要销毁，导致无法判断内存泄漏；优点就是算法相对来说比较简单容易实现；发现垃圾时立即回收；最大限度减少程序暂停

十一、描述标记整理算法的工作流程。

标记整理可以看做是标记清除的加强算法；标记阶段的操作和标记清除一致；清除阶段会先执行整理，移动对象位置，整理内存；

十二、描述V8中新生代存储区垃圾回收的流程。

V8内存一分为二，小空间用于存储新生代对象（32M\|16M）用于回收存活时间比较短的对象；回收过程采用复制算法+标记整理；新生代内存区分为二个等大小空间；使用空间为From，空闲空间为To;活动对象的存储于From空间；标记整理后将活动对象拷贝To;From与To交换空间完成释放。

十三、描述增量标记算法在何时使用以及工作原理。

在V8清除老生代对象时为提高清除效率优化时使用；清除时，程序执行然后遍历对象进行标记，之后交替执行程序和增量标操作，标记完成后，进行清除操作，完成垃圾回收；

---

## File: content/posts/expire/2020.08.05 开发脚手架及封装自动化构建工作流.md

---
layout: post
title: "开发脚手架及封装自动化构建工作流"
layout: "posts"
date: 2020-08-05 13:35:00 +0800
categories: ["大前端"]
tags: []
url: /post/part-2-module-1-homework/
---

### 简答题

#### 1、谈谈你对工程化的初步认识，结合你之前遇到过的问题说出三个以上工程化能够解决问题或者带来的价值。

答：前端工程化课题提高开发效率，避免人工操作失误。可以开发业务工程脚手架，自动化构建，以及组件化、模块化等；可以使用工程化在发布生产前添加代码质量检查工具，提升开发时的效率以及在开发中不必要的错误；

#### 2、你认为脚手架除了为我们创建项目结构，还有什么更深的意义？

提升效率，可以快速搭建一个基础项目架构；规范化 对技术选型、项目结构等做一些规范，以降低沟通成本

### 编程题
#### 1、概述脚手架实现的过程，并使用 NodeJS 完成一个自定义的小型脚手架工具

答：1、创建一个仓库(目录) 2、通过npm init 初始化 3、创建一个入口js文件 4、package.json 文件增加"bin":"入口文件路径" 5、通过npm link命令连接全局 6、命令行运行脚手架命令 7、后面可以通过npm publish发布到npm仓库

#### 2、尝试使用 Gulp 完成项目的自动化构建

package.json
```json
{
  "name": "pages-boilerplate",
  "version": "0.1.0",
  "private": true,
  "description": "Always a pleasure scaffolding your awesome static sites.",
  "keywords": [
    "pages-boilerplate",
    "boilerplate",
    "pages",
    "zce"
  ],
  "homepage": "https://github.com/zce/pages-boilerplate#readme",
  "bugs": {
    "url": "https://github.com/zce/pages-boilerplate/issues"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/zce/pages-boilerplate.git"
  },
  "license": "MIT",
  "author": {
    "name": "zce",
    "email": "w@zce.me",
    "url": "https://zce.me"
  },
  "scripts": {
    "clean": "gulp clean",
    "lint": "gulp lint",
    "serve": "gulp serve",
    "build": "gulp build",
    "start": "gulp start",
    "deploy": "gulp deploy --production"
  },
  "browserslist": [
    "last 1 version",
    "> 1%",
    "maintained node versions",
    "not dead"
  ],
  "dependencies": {
    "bootstrap": "4.4.1",
    "jquery": "3.4.1",
    "popper.js": "1.16.1"
  },
  "devDependencies": {
    "@babel/core": "^7.11.4",
    "@babel/preset-env": "^7.11.0",
    "browser-sync": "^2.26.12",
    "del": "^5.1.0",
    "gulp": "^4.0.2",
    "gulp-babel": "^8.0.0",
    "gulp-clean-css": "^4.3.0",
    "gulp-htmlmin": "^5.0.1",
    "gulp-if": "^3.0.0",
    "gulp-imagemin": "^7.1.0",
    "gulp-load-plugins": "^2.0.4",
    "gulp-sass": "^4.1.0",
    "gulp-swig": "^0.9.1",
    "gulp-uglify": "^3.0.2",
    "gulp-useref": "^4.0.1"
  },
  "engines": {
    "node": ">=6"
  }
}
```

gulpfile.js
```js
// 实现这个项目的构建任务
const { src, dest, watch, series, parallel } = require('gulp')
const del = require('del')
const gulpLoadPlugins = require('gulp-load-plugins')
const plugins = gulpLoadPlugins()

const browserSync = require('browser-sync')
const bs = browserSync.create()

const data = require('./package.json')

// 编译scss
const style = () => {
  return src('src/assets/styles/*.scss')
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist/css'))
}

// 编译JS
const js = () => {
  return src('src/assets/scripts/*.js')
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('dist/js'))
}
// 处理图片
const img = () => {
  return src('src/assets/images/**')
    .pipe(plugins.imagemin())
    .pipe(dest('dist/img'))
}
// 处理字体
const font = () => {
  return src('src/assets/fonts/**')
    .pipe(plugins.imagemin())
    .pipe(dest('dist/font'))
}

// 拷贝静态资源
const extra = () => {
  return src('public/**')
    .pipe(dest('dist/public'))
}
// 处理HTML文件
const html = () => {
  return src(['src/*.html', 'src/layouts/*.html', 'src/partials/*.html'])
    .pipe(plugins.swig({ data: { pkg: data } }))
    .pipe(dest('dist'))
}

// 清除构建后的目录
const clean = () => {
  return del('dist')
}

const server = () => {
  watch('src/assets/styles/*.scss', style)
  watch('src/assets/scripts/*.js', js)
  watch(['src/assets/fonts/**', 'src/assets/images/**'], bs.reload)

  bs.init({
    notify: false,
    files: 'dist',
    server: {
      baseDir: ['dist', 'src', 'public'],
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}

// 文件引用处理
const ref = () => {
  return src('dist/*.html')
    .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({ collapseWhitespace: true, minifyCSS: true, minifyJS: true })))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(dest('dist'))
}

const develop = series(parallel(style, js, html), ref, server)
const build = series(clean, parallel(style, js, html, img, extra, font))
module.exports = {
  clean,
  develop,
  build
}

```

#### 3、使用 Grunt 完成项目的自动化构建

package.json
```json
{
  "name": "pages-boilerplate",
  "version": "0.1.0",
  "private": true,
  "description": "Always a pleasure scaffolding your awesome static sites.",
  "keywords": [
    "pages-boilerplate",
    "boilerplate",
    "pages",
    "zce"
  ],
  "homepage": "https://github.com/zce/pages-boilerplate#readme",
  "bugs": {
    "url": "https://github.com/zce/pages-boilerplate/issues"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/zce/pages-boilerplate.git"
  },
  "license": "MIT",
  "author": {
    "name": "zce",
    "email": "w@zce.me",
    "url": "https://zce.me"
  },
  "scripts": {
    "clean": "gulp clean",
    "lint": "gulp lint",
    "serve": "gulp serve",
    "build": "gulp build",
    "start": "gulp start",
    "deploy": "gulp deploy --production"
  },
  "browserslist": [
    "last 1 version",
    "> 1%",
    "maintained node versions",
    "not dead"
  ],
  "dependencies": {
    "bootstrap": "4.4.1",
    "jquery": "3.4.1",
    "popper.js": "1.16.1"
  },
  "devDependencies": {
    "@babel/core": "^7.11.4",
    "@babel/preset-env": "^7.11.0",
    "grunt": "^1.3.0",
    "grunt-babel": "^8.0.0",
    "grunt-contrib-clean": "^2.0.0",
    "grunt-contrib-uglify": "^5.0.0",
    "grunt-contrib-watch": "^1.1.0",
    "grunt-copy": "^0.1.0",
    "grunt-sass": "^3.1.0",
    "load-grunt-tasks": "^5.1.0",
    "sass": "^1.26.10"
  },
  "engines": {
    "node": ">=6"
  }
}
```

gruntfile.js

```js
const sass = require('sass')
const loadGruntTask = require('load-grunt-tasks')// 加载grunt插件
module.exports = grunt => {
  grunt.initConfig({
    babel: {
      options: {
        presets: ['@babel/preset-env']
      },
      main: {
        files: {
          'dist/assets/scripts/main.js': 'src/assets/scripts/main.js'
        }
      }
    },
    // 压缩js
    uglify: {
      my_target: {
        files: {
          'dist/assets/scripts/main.min.js': ['dist/assets/scripts/main.js']
        }
      }
    },
    // 处理scss文件
    sass: {
      options: {
        sourceMap: true,
        implementation: sass//使用sass处理编译
      },
      main: {//配置目标
        files: {
          'dist/assets/styles/main.css': 'src/assets/styles/main.scss'
        }
      }
    },
    // 拷贝静态文件
    copy: {
      main: {
        expand: true,
        src: 'public/*',
        dest: 'dist/',
      }
    },
    clean: {
      dist: 'dist/**'
    },
    // 监视文件变化
    watch: {
      js: {
        files: ['src/assets/scripts/*.js'],
        task: ['babel']
      },
      css: {
        files: ['src/assets/styles/*.scss'],
        task: ['sass']
      }
    }
  })
  grunt.loadNpmTasks('grunt-contrib-clean') //删除构建后的文件
  loadGruntTask(grunt)//自动加载所有插件
  grunt.registerTask('develop', ['sass', 'babel', 'watch'])
  grunt.registerTask('default', ['clean', 'copy', 'sass', 'babel', 'uglify'])
}
```


### 2-3 题项目基础代码下载地址：

> 百度网盘：https://pan.baidu.com/s/1AyGApMTFEfCeGfQBdykOGg 提取码: bw3r

说明：

本次作业中的编程题要求大家完成相应代码后（二选一）

1.  简单录制一个小视频介绍一下实现思路，并演示一下相关功能。
2.  提交一个项目说明文档，要求思路流程清晰。
> 最终将录制的视频或说明文档和代码统一提交至作业仓库。

---

## File: content/posts/expire/2020.08.23 开发脚手架及封装自动化构建工作流.md

[Binary file]

---

## File: content/posts/expire/2020.09.07 手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法.md

---
layout: post
title: "手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法"
layout: "posts"
date: 2020-09-07 12:18:00 +0800
categories: ["大前端"]
tags: []
url: /post/part-3-module-1-homework/
---

### 一、简答题

#### 当我们点击按钮的时候动态给 data 增加的成员是否是响应式数据，如果不是的话，如何把新增成员设置成响应式数据，它的内部原理是什么。

```javascript
let vm = new Vue({
  el: '#el',
  data: {
    o: 'object',
    dog: {}
  },
  method: {
    clickHandler () {
      // 该 name 属性是否是响应式的
      this.dog.name = 'Trump'
    }
  }
})
```

1. 不是响应式的，对于已经创建的实例，Vue不允许动态添加根级别的响应式属性
2. 使用Vue.set(vm.dog, 'name', 'dog_name')或this.$set(this.dog, 'name', 'dog_name')
3. this.$set在new Vue()时候就被注入到Vue的原型上，set方法内部仍是调用了defineReactive()方法进行响应式处理

##### 请简述 Diff 算法的执行过程

1. diff的过程就是调用名为patch(el, vnode)/patch(oldVnode, vnode)的函数，比较新旧节点，一边比较一边给真实DOM打补丁
2. patch里会调用sameVnode(oldVnode, vonde)，根据返回结果：
    - true: 则执行patchVnode
    - false: 则用vnode替换oldVnode
3. patchVnode(oldNode, vnode, insertedVnodeQueue)
    - 找到对应的真实DOM，成为el
    - 判断vnode和oldVnode是否指向同一个对象，如果是，直接return
    - 如果它们都有文本节点并且不相等，那么将el的文本节点设置为vnode的文本文本节点
    - 如果oldVnode有子节点而vnode没有，则删除el的子节点
    - 如果oldVnode没有子节点而vnode有，则将vnode的子节点真实化之后加到el
    - 如果两者都有子节点，则执行updateChildren(parentElm, oldCh, newCh)函数比较子节点(key很重要)

### 二、编程题

#### 模拟 VueRouter 的 hash 模式的实现，实现思路和 History 模式类似，把 URL 中的 # 后面的内容作为路由的地址，可以通过 hashchange 事件监听路由地址的变化。

```javascript
let _Vue = null
export default class VueRouter {
  static install (Vue) {
    // 1. 判断当前插件是否已经被安装
    if (VueRouter.install.installed) return
    VueRouter.install.installed = true

    // 2. 把 Vue 构造函数记录到全局变量
    _Vue = Vue

    // 3. 把创建Vue实例时传入的router对象注入到Vue实例上
    _Vue.mixin({
      beforeCreate () {
        // 如果是vue实例就执行，如果是组件就不执行
        if (this.$options.router) {
          _Vue.prototype.$router = this.$options.router
        }
      }
    })
  }

  constructor (options) {
    this.options = options
    this.routeMap = {}
    // 创建响应式对象
    this.data = _Vue.observable({
      current: '#/'
    })
    this.init()
  }

  init () {
    this.createRouteMap()
    this.initComponents(_Vue)
    this.initEvent()
  }

  createRouteMap () {
    // 遍历所有的路由规则，把路由规则解析成键值对的形式 存储到routeMap中
    this.options.routes.forEach(route => {
      this.routeMap['#' + route.path] = route.component
    })
  }

  initComponents (Vue) {
    Vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        return h('a', {
          attrs: {
            href: '#' + this.to
          },
        }, [this.$slots.default])
      },
    })

    const self = this;
    Vue.component('router-view', {
      render (h) {
        // 当前路由组件，找不到时返回 404 组件
        const component = self.routeMap[self.data.current] || self.routeMap['#*']
        return h(component)
      }
    })
  }

  initEvent () {
    // 初始化时，将 hash 地址，也就是 包括#以及后面的地址 存储到 this.data.current
    this.data.current = window.location.hash
    // 监听 hashchange 事件，hash 改变时更新 this.data.current
    window.addEventListener('hashchange', () => {
      this.data.current = window.location.hash
    })
  }
}
```

#### 在模拟 Vue.js 响应式源码的基础上实现 v-html 指令，以及 v-on 指令。

```javascript
// 处理 v-html 指令
function htmlUpdater (node, value, key) {
    node.innerHTML = value
    new Watcher(this.vm, key, newValue => {
        node.innerHTML = newValue
    })
}
// 处理 v-on 指令
function onUpdater (node, value, key, eventType) {
    node.addEventListener(eventType, value)
    new Watcher(this.vm, key, newValue => {
        node.removeEventListener(eventType, value)
        node.addEventListener(eventType, newValue)
    })
}
```

#### 参考 Snabbdom 提供的电影列表的示例，利用Snabbdom 实现类似的效果，如图：



```javascript
let snabbdom = require('snabbdom');
let patch = snabbdom.init([ // Init patch function with chosen modules
  require('snabbdom/modules/class').default, // makes it easy to toggle classes
  require('snabbdom/modules/props').default, // for setting properties on DOM elements
  require('snabbdom/modules/style').default, // handles styling on elements with support for animations
  require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
let h = require('snabbdom/h').default; // helper function for creating vnodes

// 页面容器
let container = document.getElementById('container');

// 原始数据
let data = new Array(10).fill(0).map((_, i) => i).map(index => ({
  index,
  content: '-' + index + '--' + randomString(20) + '_lxcan',
  time: Date.now()
}));

// 保存 vnode
let vnode = initVnode(data);

// 首次渲染
patch(container, vnode);

// 得到页面需要渲染的 vnode
function initVnode (data) {
  return h('div.wrap', {
    style: {
      width: '600px',
      margin: '50px auto',
      padding: '20px',
      border: '1px solid #000'
    }
  }, [
    h('header.header-wrap',  [
      h('button', { style: { 'margin-right': '10px' }, on: { click: [changeSort, 'order'] } }, '顺序排列'),
      h('button', { style: { 'margin-right': '10px' }, on: { click: [changeSort, 'reorder'] } }, '倒序排列'),
      h('button', { style: { 'margin-right': '10px' }, on: { click: [changeSort, 'random'] } }, '随机排列'),
      h('button', { on: { click: addItem } }, '添加1项')
    ]),
    h('table', [
      h('tr', [
        h('th', '序号'),
        h('th', '内容'),
        h('th', '添加时间')
      ]),
      h('tbody', data.map(getTrVnode))
    ])
  ]);
}

// 获取表格行的 vnode
function getTrVnode(item) {
  return h('tr', [
    h('td', item.index),
    h('td', item.content),
    h('td', item.time)
  ])
}

// 点击按钮，改变排序
function changeSort (type) {  
  if (type === 'order') {
    // 顺序排列
    data.sort((a, b) => a.index - b.index);
  } else if (type === 'reorder') {
    // 倒序排列
    data.sort((a, b) => b.index - a.index);
  } else if (type === 'random') {
    // 随机排列
    data.sort(() => Math.random() > 0.5 ? -1 : 1)
  }
  render(data);
}

// 添加一项
function addItem () {
  const total = data.length;
  const item = {
    index: total,
    content: '-' + total + '--' + randomString(20) + '_lxcan',
    time: Date.now()
  };
  data.unshift(item);
  render(data);
}

// 数据改变，再次渲染
function render (data) {
  vnode = patch(vnode, initVnode(data))
}

// 辅助函数，获取随机字符串
function randomString(len) {
  len = len || 32;
  var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';    /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
  var maxPos = $chars.length;
  var pwd = '';
  for (i = 0; i < len; i++) {
    pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
  }
  return pwd;
}
```

---

## File: content/posts/expire/2020.09.10 Vue.js 源码剖析-响应式原理、虚拟 DOM、模板编译和组件化.md

[Binary file]

---

## File: content/posts/expire/2020.09.23 Vuex 数据流管理及Vue.js 服务端渲染（SSR）.md

---
layout: post
title: "Vuex 数据流管理及Vue.js 服务端渲染（SSR）"
layout: "posts"
date: 2020-09-23 15:31:00 +0800
categories: ["大前端"]
tags: []
url: /post/part-3-module-3-homework/
---

### 使用NuxtJS实现 RealWorld 全部功能 https://demo.realworld.io/#/

> 要求：发布码云上，可以直接展示网页

- 仓库地址： <https://gitee.com/caorushizi/realworld>
- 预览：<http://ziying.site/realworld/>

---

## File: content/posts/expire/2020.11.01 搭建自己的SSR、静态站点生成（SSG）及封装 Vue.js 组件库.md

---
layout: post
title: "搭建自己的SSR、静态站点生成（SSG）及封装 Vue.js 组件库"
layout: "posts"
date: 2020-09-23 15:31:00 +0800
categories: ["大前端"]
tags: []
url: /post/part-3-module-4-homework/
---

### 作业要求

- 使用 Gridsome 实现下面博客内容。
- 实现左侧前四个侧边栏的效果和内容。
- 项目地址 <https://github-laziji.github.io/#/user/new/main>



- 仓库地址： <https://gitee.com/caorushizi/vblog>
- 预览：<http://ziying.site/vblog>

---

## File: content/posts/expire/2020.11.03 Vue.js 3.0 Composition APIs 及 3.0 原理剖析.md

[Binary file]

---

## File: content/posts/expire/2020.11.08 云存储客户端(1) 基础环境搭建.md

---
layout: post
title: "云存储客户端(1) 基础环境搭建"
layout: "posts"
date: 2020-11-08 23:14:00 +0800
categories: ["我的开源软件"]
tags: ["github", "开源软件"]
url: /post/oss-client-1/
---


从 2020 年初到现在已经开坑了快一年了。其实我最初想做一个完全模仿百度网盘的客户端程序，那时候还是 2019 年年初的时候，最初就是想做一个客户端，用来上传静态的网站到七牛云方便一些，以下是软件的截图。



当时也只是想做一做玩一玩，基本功能做完之后也就没有什么进度了。知道今年年初的时候才有个想法把这个客户端完善一下。下面是新坑的页面截图：







## 开坑 oss-client：基础环境搭建

### 使用 electron-forge

`electron-forge`是用于创建，发布和安装现代`Electron`应用程序的完整工具，[electron forge 官网](https://www.electronforge.io/)。二话不说直接上手搞。

使用官方推荐的脚手架工具和内置的模板

```shell
npx create-electron-app oss-client --template=typescript-webpack
```

初始化的过程还是很愉快的，至少我在初始化的时候没有遇到什么大坑，如果 npm 默认的源比较慢的话，推荐这个[华为云镜像](https://mirrors.huaweicloud.com/)。还有一个需要注意的是 Windows 上安装 node-gyp 出现问题的话可以看一下[这篇文章](https://simulatedgreg.gitbooks.io/electron-vue/content/en/getting_started.html#a-note-for-windows-users)，一般来说 Windows 电脑上只要安装了 visual studio 和 python2.7 的话 electron 环境是没有什么问题的。
这个框架是官方默认的 webpack + typescript 配置。如果电脑上安装了 yarn 的话 electronforge 会默认使用 yarn 来初始化项目的。下面我们默认使用 yarn 来创建开发环境。下面是初始化完成后的目录结构。

```text
│  .eslintrc.json               - eslint 配置文件
│  .gitignore                   - git 忽略的配置文件
│  package.json                 - npm 项目的配置文件
│  tsconfig.json                - typescript 配置文件
│  webpack.main.config.js       - electron 中主线程的 webpack 配置
│  webpack.plugins.js           - electronforge 配置插件列表
│  webpack.renderer.config.js   - electron 渲染进程的 webpack 配置
│  webpack.rules.js             - electronforge 配置文件
│  yarn.lock                    - yarn 版本款控制文件
└─ src                          - 源代码目录文件
index.css
index.html
index.ts
renderer.ts
```

接下来开始配置 eslint + prettier

### eslint 和 prettier

[eslint](https://eslint.org/)： 查找并修复 JavaScript 代码中的问题。

[prettier](https://prettier.io/)：prettier 是一个固执己见的代码格式化程序（不知道这个固执己见的怎么理解，反正我是觉得用了它之后代码漂亮了很多，和团队中的同事代码风格一致了，之前都不敢碰同事的代码😂）

简而言之 eslint 的规则可以避免程序中的代码错误，而 prettier 可以让团队之间代码风格保持一致。二话不说直接上手。

我们使用社区中最严格的 airbnb 风格（后来很多校验规则我都手动去掉了😂）

npmjs 官网上有两个 airbnb 的解决方案：eslint-config-airbnb 和 eslint-config-airbnb-base。eslint-config-airbnb-base 中没有对 React 的语法校验，当然我们选择 eslint-config-airbnb。

如果你的 npm 版本大于 5 可以直接使用（如果是自己用的开发环境还是建议安装一下最新版本的 node 和 npm，因为 prettier 2 版本中已经不兼容 node10 以下的版本了）

```shell
npx install-peerdeps --dev eslint-config-airbnb
```
如果你的 npm 版本小于 5 的话，在 linux/unix 中可以使用
```shell
(
 export PKG=eslint-config-airbnb;
 npm info "$PKG@latest" peerDependencies --json | command sed 's/[\{\},]//g ; s/: /@/g' | xargs npm install --save-dev "$PKG@latest"
)
```
windows 用户可以安装使用 install-peerdeps 命令行工具
```shell
npm install -g install-peerdeps
install-peerdeps --dev eslint-config-airbnb
```
详情见[eslint-config-airbnb](https://www.npmjs.com/package/eslint-config-airbnb)
这里我们执行命令的时候还是要换一下源

```shell
npx install-peerdeps --dev eslint-config-airbnb -r registry https://mirrors.huaweicloud.com/repository/npm/
```
安装完成之后需要在项目中`.eslintrc.json`中配置一下

```json
{
 "env": {
  "browser": true,
  "es6": true,
  "node": true
  },
 "extends": [
  "eslint:recommended",
  "plugin:@typescript-eslint/eslint-recommended",
  "plugin:@typescript-eslint/recommended",
  "plugin:import/errors",
  "plugin:import/warnings",
  "airbnb",
  "airbnb/hooks"
  ],
 "parser": "@typescript-eslint/parser"
}
```
然后添加 prettier
```shell
yarn add --dev --exact prettier
```
如果项目中有 gitignore 和 eslintignore 那么 prettierignore 是基于这些文件的
安装 eslint-plugin-prettier 和 eslint-config-prettier，在`.eslintrc.json`文件中继续配置

```json
{
 "env": {
  "browser": true,
  "es6": true,
  "node": true
  },
 "extends": [
  "eslint:recommended",
  "plugin:@typescript-eslint/eslint-recommended",
  "plugin:@typescript-eslint/recommended",
  "plugin:import/errors",
  "plugin:import/warnings",
  "airbnb",
  "airbnb/hooks",
  "prettier"
  ],
 "plugins": [
  "prettier"
  ],
 "rules": {
  "prettier/prettier": "error"
  },
 "parser": "@typescript-eslint/parser"
}
```
请务必确保在 extends 数组中 prettier 在最后面，prettier 会把其他项配置重写。详见[eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier)、[eslint-config-prettier](https://github.com/prettier/eslint-config-prettier)。
接下来要配置 react，要安装 react 和 react-dom

```shell
yarn add react react-dom
yarn add @types/react @types/react-dom --dev
```
在 eslint 中 rules 配置中添加 "react/jsx-filename-extension": "off" 来关闭 jsx 对文件类型的限制
使用 normalize.css 来覆盖浏览器中的默认样式

```shell
yarn add normalize.css
```
将 src 目录中的 renderer.ts 改为 renderer.tsx，内容如下
```jsx
import React from "react";
import reactDom from "react-dom";
import "normalize.css/normalize.css";

const App = () => {
 return <div>hello react</div>;
};

reactDom.render(<App />, document.getElementById("root"));
```
将 src 文件夹下的 index.html 内容改为
```html
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>Hello World!</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
```
然后运行 npm start 看到 页面中显示 hello react 就证明基本环境已经搭建完毕！

---

## File: content/posts/expire/2021.01.23 React 基础回顾.md

---
layout: post
title: "React 基础回顾"
layout: "posts"
date: 2021-01-26 21:46:00 +0800
categories: ["大前端"]
tags: []
url: /post/part-4-module-1-react-basic/
---

### 1. React 介绍



React 是一个用于构建用户界面的 JavaScript 库，它只负责应用的视图层，帮助开发人员构建快速且交互式的 web 应用程序。

React 使用组件的方式构建用户界面。

### 2. JSX 语法

在 React 中使用 JSX 语法描述用户界面，它是一种 JavaScript 语法扩展。

在 React 代码执行之前，Babel 会将 JSX 语法转换为标准的 JavaScript API。

JSX 语法就是一种语法糖，让开发人员使用更加舒服的代码构建用户界面。

#### 2.1 在 JSX 中使用表达式

```react
const user = {
  firstName: 'Harper',
  lastName: 'Perez'
}
function formatName(user) {
  return user.firstName + ' ' + user.lastName;
}
const element = <h1>Hello, {formatName(user)}!</h1>;
```

JSX 本身其实也是一种表达式，将它赋值给变量，当作参数传入，作为返回值都可以。

```react
function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;
  }
  return <h1>Hello, Stranger.</h1>;
}
```

#### 2.2 属性

如果属性值为字符串类型，需要加引号，属性名称推荐采用驼峰式命名法。

```react
const element = <div greeting="hello"></div>;
```

如果属性值为JavaScript表达式，属性值外面加大括号。

```react
const element = <img src={user.avatarUrl} />;
// 注意大括号外面不能加引号，JSX 会将引号当中的内容识别为字符串而不是表达式
```

#### 2.3 JSX 单标记必须闭合

如果 JSX 是单标记，必须闭合，否则报错。

```react
const element = <img src={user.avatarUrl} />
const element = <input type="text"/>
```

#### 2.4 className

为 JSX 标记添加类名需要使用 className，而不是class。

```react
const element = <img src={user.avatarUrl} className="rounded"/>;
```

#### 2.5  JSX 自动展开数组

```react
const ary = [<p>哈哈</p>, <p>呵呵</p>, <p>嘿嘿</p>];
const element = (
	<div>{ary}</div>
);
// 解析后
/*
	<div>
		<p>哈哈</p>
		<p>呵呵</p>
		<p>嘿嘿</p>
	</div>
*/
```

#### 2.6 三元运算

```react
{ boolean ? <div>Hello React</div> : null }
{ boolean && <div>Hello React</div> }
```

#### 2.7 循环

```react
const persons = [{
  id: 1,
  name: '张三',
  age: 20
}, {
  id: 2,
  name: '李四',
  age: 15
}, {
  id: 3,
  name: '王五',
  age: 22
}]
```

```react
<ul>
  { persons.map(person => <li key={person.id}> {person.name} {person.age} </li>) }
</ul>
```

#### 2.8 事件

```react
{/* 第一个参数即是事件对象 不需传递 */}
<button onClick={this.eventHandler}>按钮</button>
{/* 需要传递事件对象 */}
<button onClick={e=>this.eventHandler('arg',e)}>按钮</button>
{/* 最后一个参数即是事件对象 不需传递 */}
<button onClick={this.eventHandler.bind(null, 'arg')}>按钮</button>
```

```react
constructor () {
  this.eventHandler = this.eventHandler.bind(this)
}
eventHandler () {}
<button onClick={this.eventHandler}>按钮</button>
```

#### 2.9 样式

##### 2.9.1 行内样式

```react
class App extends Component {
  render() {
    const style = {width: 200, height: 200, backgroundColor: 'red'};
    return <div style={style}></div>
  }
}
```

##### 2.9.2 外链样式

```react
// Button.js
import styles from './Button.module.css';
class Button extends Component {
  render() {
    return <button className={styles.error}>Error Button</button>;
  }
}
```

##### 2.9.3 全局样式

```react
import './styles.css'
```

#### 2.10 ref 属性

##### 2.10.1 createRef

```react
class Input extends Component {
  constructor() {
    super()
    this.inputRef = React.createRef()
  }
  render() {
    return (
      <div>
        <input type="text" ref={this.inputRef} />
        <button onClick={() => console.log(this.inputRef.current)}> button </button>
      </div>
    )
  }
}
```

##### 2.10.2 函数参数

```react
class Input extends Component {
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input = input)} />
        <button onClick={() => console.log(this.input)}>button</button>
      </div>
    )
  }
}
```

##### 2.10.3 ref 字符串

不推荐使用，在严格模式下报错。

```react
class Input extends Component {
  render() {
    return (
      <div>
        <input type="text" ref="username" />
        <button onClick={() => console.log(this.refs.username)}>button</button>
      </div>
    )
  }
}
```

##### 2.10.4 获取组件实例

点击按钮让 input 文本框获取焦点。

input 文本框以及让文本框获取焦点的方法定义在 Input 组件中，在 App 组件中引入 Input 组件，按钮定义在 App 组件中。

```react
// Input.js
class Input extends Component {
  constructor() {
    super()
    this.inputRef = React.createRef()
    this.focusInput = this.focusInput.bind(this)
  }
  focusInput() {
    this.inputRef.current.focus()
  }
  render() {
    return (
      <div>
        <input type="text" ref={this.inputRef} />
      </div>
    )
  }
}
```

```react
// App.js
class App extends Component {
  constructor() {
    super()
    this.InputComponentRef = React.createRef()
  }
  render() {
    return (
      <div className="App">
        <Input ref={this.InputComponentRef} />
        <button onClick={() => this.InputComponentRef.current.focusInput()}>button</button>
      </div>
    )
}
```



### 3. 组件

#### 3.1 什么是组件

React 是基于组件的方式进行用户界面开发的. 组件可以理解为对页面中某一块区域的封装。



#### 3.2 创建组件

##### 3.2.1 创建类组件

```react
import React, { Component } from 'react';
class App extends Component {
    render () {
        return <div>Hello, 我是类组件</div>
    }
}
```

##### 3.2.2 创建函数组件

```react
const Person = () => {
     return <div>Hello, 我是函数型组件</div>;
}
```

**注意事项**

1. 组件名称首字母必须大写，用以区分组件和普通标签。
2. jsx语法外层必须有一个根元素

#### 3.3 组件 props

##### 3.3.1 props 传递数据

在调用组件时可以向组件内部传递数据，在组件中可以通过 props 对象获取外部传递进来的数据。

```react
<Person name="乔治" age="20"/>
<Person name="玛丽" age="10"/>
```

```react
// 类组件
class Person extends Component {
  render() {
    return (
      <div>
        <h3>姓名：{this.props.name}</h3>
        <h4>年龄：{this.props.age}</h4>
      </div>
    );
  }
}
```

```react
// 函数组件
const Person = props => {
  return (
    <div>
      <h3>姓名：{props.name}</h3>
      <h4>年龄：{props.age}</h4>
    </div>
  );
}
```

**注意：**

1. props 对象中存储的数据是只读的，不能在组件内部被修改。
2. 当 props 数据源中的数据被修改后，组件中的接收到的 props 数据会被同步更新。( 数据驱动DOM )

##### 3.3.2 设置 props 默认值

```react
class App extends Component {
    static defaultProps = {}
}
```

```react
function ThemedButton(props) {
}
ThemedButton.defaultProps = {
  theme: "secondary",
  label: "Button Text"
};
```

##### 3.3.3 组件 children

通过 props.children 属性可以获取到在调用组件时填充到组件标签内部的内容。

```react
<Person>组件内部的内容</Person>
```

```react
const Person = (props) => {
    return (
    	<div>{props.children}</div>
    );
}
```

##### 3.3.4 单向数据流

1.  在React中, 关于数据流动有一条原则, 就是单向数据流动, 自顶向下, 从父组件到子组件.

2.  单向数据流特性要求我们共享数据要放置在上层组件中.

3.  子组件通过调用父组件传递过来的方法更改数据.

4.  当数据发生更改时, React会重新渲染组件树.

5.  单向数据流使组件之间的数据流动变得可预测. 使得定位程序错误变得简单.



#### 3.4 类组件状态 state

##### 3.4.1 定义组件状态

类组件除了能够从外部 (props) 接收状态数据以外还可以拥有自己的状态 (state)，此状态在组件内部可以被更新，状态更新 DOM 更新。

组件内部的状态数据被存储在组件类中的 state 属性中，state 属性值为对象类型，属性名称固定不可更改。

```react
class App extends Component {
  constructor () {
    super()
    this.state = {
      person: { name: '张三', age: 20 },
    }
  }
  render () {
    return (
      <div>
        {this.state.person.name}
        {this.state.person.age}
      </div>
    );
  }
}
```

##### 3.4.2 更改组件状态

state 状态对象中的数据不可直接更改，如果直接更改 DOM 不会被更新，要更改 state 状态数据需要使用 setState方法。

```react
class App extends Component {
  constructor () {
    this.state = {
      person: { name: '张三', age: 20 },
    }
    this.changePerson = this.changePerson.bind(this)
  }
	changePerson () {
    this.setState({
      person: {
        name: '李四',
        age: 15
      }
    })
  }
  render() {
    return (
      <div>
        {this.state.person.name}
        {this.state.person.age}
        <button onClick={this.changePerson}>按钮</button>
      </div>
    );
  }
}
```

##### 3.4.3 双向数据绑定

双向数据绑定是指，组件类中更新了状态，DOM 状态同步更新，DOM 更改了状态，组件类中同步更新。组件 <=> 视图。

要实现双向数据绑定需要用到表单元素和 state 状态对象。

```react
class App extends Component {
  constructor () {
    this.state = {
      name: "张三"
    }
    this.nameChanged = this.nameChanged.bind(this)
  }
  nameChanged (event) {
    this.setState({name: event.target.value});
  }
  render() {
    return (
      <div>
        <div>{this.state.name}</div>
        <Person name={this.state.name} changed={this.nameChanged}/>
      </div>
    )
  }
}
```

```react
const Person = props => {
	return <input type="text" value={props.name} onChange={props.changed}/>;
}
```

#### 3.5 类组件生命周期函数



在组件完成更新之前需要做某种逻辑或者计算，就需要用到快照

```react
componentDidUpdate(prevProps, prevState, snapshot) {}
```

getSnapshotBeforeUpdate 方法会在组件完成更新之前执行，用于执行某种逻辑或计算，返回值可以在 componentDidUpdate 方法中的第三个参数中获取，就是说在组件更新之后可以拿到这个值再去做其他事情。

```react
getSnapshotBeforeUpdate(prevProps, prevState) {
  return 'snapshot'
}
```

#### 3.6 Context 

通过 Context 可以跨层级传递数据



```react
// userContext.js
import React from "react"

const userContext = React.createContext("default value")
const UserProvider = userContext.Provider
const UserConsumer = userContext.Consumer

export { UserProvider, UserConsumer }
```

```react
// App.js
import { UserProvider } from "./userContext"
class App extends Component {
  render() {
    return (
      <UserProvider value="Hello React Context">
        <A />
      </UserProvider>
    )
  }
}
```

```react
// C.js
import { UserConsumer } from "./userContext"

export class C extends Component {
  render() {
    return (
      <div>
        <UserConsumer>
          {username => {
            return <div>{username}</div>
          }}
        </UserConsumer>
      </div>
    )
  }
}
```

context 的另一种用法

```react
// userContext.js
export default userContext
```

```react
// C.js
import userContext from "./userContext"

export class C extends Component {
  static contextType = userContext
  render() {
    return (
      <div>
        {this.context}
      </div>
    )
  }
}
```

### 4. 表单

#### 4.1 受控表单

表单控件中的值由组件的 state 对象来管理，state对象中存储的值和表单控件中的值时同步状态的

```react
class App extends Component {
  constructor () {
    this.state = { username: "" }
    this.nameChanged = this.nameChanged.bind(this)
  }
  
  nameChanged (e) {
    this.setState({username: e.target.value})
  }
  render() {
    return (
      <form>
        <p>{this.state.username}</p>
        <input type="text" value={this.state.username} onChange={this.nameChanged}/>
      </form>
    )
  }
}
```

#### 4.2 非受控表单

表单元素的值由 DOM 元素本身管理。

```react
class App extends Component {
  constructor () {
    this.onSubmit = this.onSubmit.bind(this)
  }
  onSubmit(e) {
    console.log(this.username.value)
    e.preventDefault();
  }
  render(
    <form onSubmit={this.onSubmit}>
      <input type="text" ref={username => this.username = username}/>
    </form>
  )
}
```

### 5. 路由

url地址与组件之间的对应关系，访问不同的url地址显示不同的组件。

下载：`npm install react-router-dom`

#### 5.1.1 路由基本使用

```react
// App.js
import React from 'react';
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
function Index() {
	return <div>首页</div>;
}
function News() {
	return <div>新闻</div>;
}
function App() {
  return (
    <Router>
      <div>
        <Link to="/index">首页</Link>
        <Link to="/news">新闻</Link>
      </div>
      <div>
        <Route path="/index" component={Index}/>
        <Route path="/news" component={News}/>
      </div>
    </Router>
  );
}
```

#### 5.1.2 路由嵌套

```react
function News(props) {
  return (
    <div>
      <div>
        <Link to={`${props.match.url}/company`}>公司新闻</Link>
        <Link to={`${props.match.url}/industry`}>行业新闻</Link>
      </div>
      <div>
        <Route path={`${props.match.path}/company`} component={CompanyNews} />
        <Route path={`${props.match.path}/industry`} component={IndustryNews}/>  
      </div>	
    </div>
  );
}

function CompanyNews() {
	return <div>公司新闻</div>
}
function IndustryNews() {
	return <div>行业新闻</div>
}
```

#### 5.1.3 路由传参

```react
import url from 'url';
class News extends Component {
  constructor(props) {
    super(props);
    this.state = {
      list: [{
        id: 1,
        title: '新闻1'
      }, {
        id: 2,
        title: '新闻2'
      }]
    }
  }
    
  render() {
    return (
      <div>
        <div>新闻列表组件</div>
        <ul>
          this.state.list.map((item, index) => {
            return (
              <li key={index}>
                <Link to={`/detail?id=${item.id}`}>{item.title}</Link>
              </li>
            );
          })
        </ul>
      </div>
    );
  }
}
class Detail extends Component {
  constructor(props) {
    super(props);
  }
	const { query } = url.parse(this.props.location.search, true);
	console.log(query); // {id: 1}
  render() {
    return <div>新闻详情</div>
  }
}
```

#### 5.1.4 路由重定向

```react
import { Redirect } from 'react-router-dom';

class Login extends Component {
  render() {
    if (this.state.isLogin) {
      return <Redirect to="/"/>
    }
  }
}
```

---

## File: content/posts/expire/2021.01.23 React 设计原理解密及核心源码解读.md

---
layout: post
title: "React 设计原理解密及核心源码解读"
layout: "posts"
date: 2021-01-23 15:06:00 +0800
categories: ["大前端"]
tags: []
url: /post/part-4-module-1-note/
---

### React 基础回顾

[笔记](/post/part-4-module-1-react-basic/)

### Virtual DOM & Diff 算法

[笔记](/post/part-4-module-1-virtualdom&diff/)

[github](https://github.com/caorushizi/tiny-react)

---

## File: content/posts/expire/2021.01.23 VirtualDOM 及 Diff 算法.md

---
layout: post
title: "VirtualDOM 及 Diff 算法"
layout: "posts"
date: 2021-01-26 21:46:00 +0800
categories: ["大前端"]
tags: []
url: /post/part-4-module-1-virtualdom&diff/
---

##  Virtual DOM 及 Diff 算法

### 1.  JSX 到底是什么

使用 React 就一定会写 JSX，JSX 到底是什么呢？它是一种 JavaScript 语法的扩展，React 使用它来描述用户界面长成什么样子。虽然它看起来非常像 HTML，但它确实是 JavaScript 。在 React 代码执行之前，Babel 会对将 JSX 编译为 React API.

```react
<div className="container">
  <h3>Hello React</h3>
  <p>React is great </p>
</div>
```

```react
React.createElement(
  "div",
  {
    className: "container"
  },
  React.createElement("h3", null, "Hello React"),
  React.createElement("p", null, "React is great")
);
```

从两种语法对比来看，JSX 语法的出现是为了让 React 开发人员编写用户界面代码更加轻松。

[Babel REPL](https://babeljs.io/repl)

### 2. DOM 操作问题

在现代 web 应用程序中使用 JavaScript 操作 DOM 是必不可少的，但遗憾的是它比其他大多数 JavaScript 操作要慢的多。

大多数 JavaScript 框架对于 DOM 的更新远远超过其必须进行的更新，从而使得这种缓慢操作变得更糟。

例如假设你有包含十个项目的列表，你仅仅更改了列表中的第一项，大多数 JavaScript 框架会重建整个列表，这比必要的工作要多十倍。

更新效率低下已经成为严重问题，为了解决这个问题，React 普及了一种叫做 Virtual DOM 的东西，Virtual DOM 出现的目的就是为了提高 JavaScript 操作 DOM 对象的效率。

### 3. 什么是 Virtual DOM

在 React 中，每个 DOM 对象都有一个对应的 Virtual DOM 对象，它是 DOM 对象的 JavaScript 对象表现形式，其实就是使用 JavaScript 对象来描述 DOM 对象信息，比如 DOM 对象的类型是什么，它身上有哪些属性，它拥有哪些子元素。

可以把 Virtual DOM 对象理解为 DOM 对象的副本，但是它不能直接显示在屏幕上。

```react
<div className="container">
  <h3>Hello React</h3>
  <p>React is great </p>
</div>
```

```react
{
  type: "div",
  props: { className: "container" },
  children: [
    {
      type: "h3",
      props: null,
      children: [
        {
          type: "text",
          props: {
            textContent: "Hello React"
          }
        }
      ]
    },
    {
      type: "p",
      props: null,
      children: [
        {
          type: "text",
          props: {
            textContent: "React is great"
          }
        }
      ]
    }
  ]
}
```

### 4. Virtual DOM 如何提升效率

精准找出发生变化的 DOM 对象，只更新发生变化的部分。

在 React 第一次创建 DOM 对象后，会为每个 DOM 对象创建其对应的 Virtual DOM 对象，在 DOM 对象发生更新之前，React 会先更新所有的 Virtual DOM 对象，然后 React 会将更新后的 Virtual DOM 和 更新前的 Virtual DOM 进行比较，从而找出发生变化的部分，React 会将发生变化的部分更新到真实的 DOM 对象中，React 仅更新必要更新的部分。

Virtual DOM 对象的更新和比较仅发生在内存中，不会在视图中渲染任何内容，所以这一部分的性能损耗成本是微不足道的。



```react
<div id="container">
	<p>Hello React</p>
</div>
```

```react
<div id="container">
	<p>Hello Angular</p>
</div>
```

```react
const before = {
  type: "div",
  props: { id: "container" },
  children: [
    {
      type: "p",
      props: null,
      children: [
        { type: "text", props: { textContent: "Hello React" } }
      ]
    }
  ]
}
```

```react
const after = {
  type: "div",
  props: { id: "container" },
  children: [
    {
      type: "p",
      props: null,
      children: [
        { type: "text", props: { textContent: "Hello Angular" } }
      ]
    }
  ]
}
```

### 5. 创建 Virtual DOM

在 React 代码执行前，JSX 会被 Babel 转换为 React.createElement 方法的调用，在调用 createElement 方法时会传入元素的类型，元素的属性，以及元素的子元素，createElement 方法的返回值为构建好的 Virtual DOM 对象。

```react
{
  type: "div",
  props: null,
  children: [{type: "text", props: {textContent: "Hello"}}]
}
```

``` react
/**
 * 创建 Virtual DOM
 * @param {string} type 类型
 * @param {object | null} props 属性
 * @param  {createElement[]} children 子元素
 * @return {object} Virtual DOM
 */
function createElement (type, props, ...children) {
	return {
    type,
    props,
    children
  } 
}
```

从 createElement 方法的第三个参数开始就都是子元素了，在定义 createElement 方法时，通过 `...children` 将所有的子元素放置到 children 数组中。

```react
const virtualDOM = (
  <div className="container">
    <h1>你好 Tiny React</h1>
    <h2>(编码必杀技)</h2>
    <div>
      嵌套1 <div>嵌套 1.1</div>
    </div>
    <h3>(观察: 这个将会被改变)</h3>
    {2 == 1 && <div>如果2和1相等渲染当前内容</div>}
    {2 == 2 && <div>2</div>}
    <span>这是一段内容</span>
    <button onClick={() => alert("你好")}>点击我</button>
    <h3>这个将会被删除</h3>
    2, 3
  </div>
)
console.log(virtualDOM)
```

通过以上代码测试，发现返回的 Virtual DOM 存在一些问题，第一个问题是文本节点被直接放入到了数组中



而我们期望是文本节点应该是这样的

```react
children: [
  {
    type: "text",
    props: {
      textContent: "React is great"
    }
  }
]
```

通过以下代码对 Virtual DOM 进行改造，重新构建 Virtual DOM。

```javascript
// 将原有 children 拷贝一份 不要在原有数组上进行操作
const childElements = [].concat(...children).map(child => {
  // 判断 child 是否是对象类型
  if (child instanceof Object) {
    // 如果是 什么都不需要做 直接返回即可
    return child
  } else {
    // 如果不是对象就是文本 手动调用 createElement 方法将文本转换为 Virtual DOM
    return createElement("text", { textContent: child })
  }
})
return {
  type,
  props,
  children: childElements
}
```



通过观察返回的 Virtual DOM，文本节点已经被转化成了对象类型的 Virtual DOM，但是布尔值也被当做文本节点被转化了，在 JSX 中，如果 Virtual DOM 被转化为了布尔值或者null，是不应该被更新到真实 DOM 中的，所以接下来要做的事情就是清除 Virtual DOM 中的布尔值和null。

```react
// 由于 map 方法无法从数据中刨除元素, 所以此处将 map 方法更改为 reduce 方法
const childElements = [].concat(...children).reduce((result, child) => {
  // 判断子元素类型 刨除 null true false
  if (child != null && child != false && child != true) {
    if (child instanceof Object) {
      result.push(child)
    } else {
      result.push(createElement("text", { textContent: child }))
    }
  }
  // 将需要保留的 Virtual DOM 放入 result 数组
  return result
}, [])
```

在 React 组件中，可以通过 props.children 获取子元素，所以还需要将子元素存储在 props 对象中。

```javascript
return {
  type,
  props: Object.assign({ children: childElements }, props),
  children: childElements
}
```

### 6. 渲染 Virtual DOM 对象为 DOM 对象

通过调用 render 方法可以将 Virtual DOM 对象更新为真实 DOM 对象。

在更新之前需要确定是否存在旧的 Virtual DOM，如果存在需要比对差异，如果不存在可以直接将 Virtual DOM 转换为 DOM 对象。 

目前先只考虑不存在旧的 Virtual DOM 的情况，就是说先直接将 Virtual DOM 对象更新为真实 DOM 对象。

```react
// render.js
export default function render(virtualDOM, container, oldDOM = container.firstChild) {
  // 在 diff 方法内部判断是否需要对比 对比也好 不对比也好 都在 diff 方法中进行操作  
  diff(virtualDOM, container, oldDOM)
}
```

```react
// diff.js
import mountElement from "./mountElement"

export default function diff(virtualDOM, container, oldDOM) {
  // 判断 oldDOM 是否存在
  if (!oldDOM) {
    // 如果不存在 不需要对比 直接将 Virtual DOM 转换为真实 DOM
    mountElement(virtualDOM, container)
  }
}
```

在进行 virtual DOM 转换之前还需要确定 Virtual DOM 的类 Component VS Native Element。

类型不同需要做不同的处理 如果是 Native Element 直接转换。

如果是组件 还需要得到组件实例对象 通过组件实例对象获取组件返回的 virtual DOM 然后再进行转换。

目前先只考虑 Native Element 的情况。

```react
// mountElement.js
import mountNativeElement from "./mountNativeElement"

export default function mountElement(virtualDOM, container) {
  // 通过调用 mountNativeElement 方法转换 Native Element
  mountNativeElement(virtualDOM, container)
}
```

```react
// mountNativeElement.js
import createDOMElement from "./createDOMElement"

export default function mountNativeElement(virtualDOM, container) {
  const newElement = createDOMElement(virtualDOM)
  container.appendChild(newElement)
}
```

```react
// createDOMElement.js
import mountElement from "./mountElement"
import updateElementNode from "./updateElementNode"

export default function createDOMElement(virtualDOM) {
  let newElement = null
  if (virtualDOM.type === "text") {
    // 创建文本节点
    newElement = document.createTextNode(virtualDOM.props.textContent)
  } else {
    // 创建元素节点
    newElement = document.createElement(virtualDOM.type)
    // 更新元素属性
    updateElementNode(newElement, virtualDOM)
  }
  // 递归渲染子节点
  virtualDOM.children.forEach(child => {
    // 因为不确定子元素是 NativeElement 还是 Component 所以调用 mountElement 方法进行确定
    mountElement(child, newElement)
  })
  return newElement
}

```

### 7. 为元素节点添加属性

```react
// createDOMElement.js
// 看看节点类型是文本类型还是元素类型
if (virtualDOM.type === "text") {
  // 创建文本节点 设置节点内容
  newElement = document.createTextNode(virtualDOM.props.textContent)
} else {
  // 根据 Virtual DOM type 属性值创建 DOM 元素
  newElement = document.createElement(virtualDOM.type)
  // 为元素设置属性
  updateElementNode(newElement, virtualDOM)
}
```

```react
export default function updateElementNode(element, virtualDOM) {
  // 获取要解析的 VirtualDOM 对象中的属性对象
  const newProps = virtualDOM.props
  // 将属性对象中的属性名称放到一个数组中并循环数组
  Object.keys(newProps).forEach(propName => {
    const newPropsValue = newProps[propName]
    // 考虑属性名称是否以 on 开头 如果是就表示是个事件属性 onClick -> click
    if (propName.slice(0, 2) === "on") {
      const eventName = propName.toLowerCase().slice(2)
      element.addEventListener(eventName, newPropsValue)
      // 如果属性名称是 value 或者 checked 需要通过 [] 的形式添加
    } else if (propName === "value" || propName === "checked") {
      element[propName] = newPropsValue
      // 刨除 children 因为它是子元素 不是属性
    } else if (propName !== "children") {
      // className 属性单独处理 不直接在元素上添加 class 属性是因为 class 是 JavaScript 中的关键字
      if (propName === "className") {
        element.setAttribute("class", newPropsValue)
      } else {
        // 普通属性
        element.setAttribute(propName, newPropsValue)
      }
    }
  })
}
```

### 8. 渲染组件

#### 8.1 函数组件

在渲染组件之前首先要明确的是，组件的 Virtual DOM 类型值为函数，函数组件和类组件都是这样的。

```react
// 原始组件
const Heart = () => <span>&hearts;</span>
```

```react
<Heart />
```

```react
// 组件的 Virtual DOM
{
  type: f function() {},
  props: {}
  children: []
}
```

在渲染组件时，要先将 Component 与 Native Element 区分开，如果是 Native Element 可以直接开始渲染，如果是组件，特别处理。

```react
// mountElement.js
export default function mountElement(virtualDOM, container) {
  // 无论是类组件还是函数组件 其实本质上都是函数 
  // 如果 Virtual DOM 的 type 属性值为函数 就说明当前这个 Virtual DOM 为组件
  if (isFunction(virtualDOM)) {
    // 如果是组件 调用 mountComponent 方法进行组件渲染
    mountComponent(virtualDOM, container)
  } else {
    mountNativeElement(virtualDOM, container)
  }
}

// Virtual DOM 是否为函数类型
export function isFunction(virtualDOM) {
  return virtualDOM && typeof virtualDOM.type === "function"
}
```

在 mountComponent 方法中再进行函数组件和类型的区分，然后再分别进行处理。

```react
// mountComponent.js
import mountNativeElement from "./mountNativeElement"

export default function mountComponent(virtualDOM, container) {
  // 存放组件调用后返回的 Virtual DOM 的容器
  let nextVirtualDOM = null
  // 区分函数型组件和类组件
  if (isFunctionalComponent(virtualDOM)) {
    // 函数组件 调用 buildFunctionalComponent 方法处理函数组件
    nextVirtualDOM = buildFunctionalComponent(virtualDOM)
  } else {
    // 类组件
  }
  // 判断得到的 Virtual Dom 是否是组件
  if (isFunction(nextVirtualDOM)) {
    // 如果是组件 继续调用 mountComponent 解剖组件
    mountComponent(nextVirtualDOM, container)
  } else {
    // 如果是 Navtive Element 就去渲染
    mountNativeElement(nextVirtualDOM, container)
  }
}

// Virtual DOM 是否为函数型组件
// 条件有两个: 1. Virtual DOM 的 type 属性值为函数 2. 函数的原型对象中不能有render方法
// 只有类组件的原型对象中有render方法 
export function isFunctionalComponent(virtualDOM) {
  const type = virtualDOM && virtualDOM.type
  return (
    type && isFunction(virtualDOM) && !(type.prototype && type.prototype.render)
  )
}

// 函数组件处理 
function buildFunctionalComponent(virtualDOM) {
  // 通过 Virtual DOM 中的 type 属性获取到组件函数并调用
  // 调用组件函数时将 Virtual DOM 对象中的 props 属性传递给组件函数 这样在组件中就可以通过 props 属性获取数据了
  // 组件返回要渲染的 Virtual DOM
  return virtualDOM && virtualDOM.type(virtualDOM.props || {})
}
```

#### 8.2 类组件

类组件本身也是 Virtual DOM，可以通过 Virtual DOM 中的 type 属性值确定当前要渲染的组件是类组件还是函数组件。

在确定当前要渲染的组件为类组件以后，需要实例化类组件得到类组件实例对象，通过类组件实例对象调用类组件中的 render 方法，获取组件要渲染的 Virtual DOM。

类组件需要继承 Component 父类，子类需要通过 super 方法将自身的 props 属性传递给 Component 父类，父类会将 props 属性挂载为父类属性，子类继承了父类，自己本身也就自然拥有props属性了。这样做的好处是当 props 发生更新后，父类可以根据更新后的 props 帮助子类更新视图。

假设以下代码就是我们要渲染的类组件：

```react
class Alert extends TinyReact.Component {
  constructor(props) {
    // 将 props 传递给父类 子类继承父类的 props 子类自然就有 props 数据了
    // 否则 props 仅仅是 constructor 函数的参数而已
    // 将 props 传递给父类的好处是 当 props 发生更改时 父类可以帮助更新 props 更新组件视图
    super(props)
    this.state = {
      title: "default title"
    }
  }
  render() {
    return (
      <div>
        <h2>{this.state.title}</h2>
        <p>{this.props.message}</p>
      </div>
    )
  }
}

TinyReact.render(<Alert message="Hello React" />, root)
```

```react
// Component.js 父类 Component 实现
export default class Component {
  constructor(props) {
    this.props = props
  }
}
```

在 mountComponent 方法中通过调用 buildStatefulComponent 方法得到类组件要渲染的 Virtual DOM

```react
// mountComponent.js
export default function mountComponent(virtualDOM, container) {
  let nextVirtualDOM = null
  // 区分函数型组件和类组件
  if (isFunctionalComponent(virtualDOM)) {
    // 函数组件
    nextVirtualDOM = buildFunctionalComponent(virtualDOM)
  } else {
    // 类组件
    nextVirtualDOM = buildStatefulComponent(virtualDOM)
  }
  // 判断得到的 Virtual Dom 是否是组件
  if (isFunction(nextVirtualDOM)) {
    mountComponent(nextVirtualDOM, container)
  } else {
    mountNativeElement(nextVirtualDOM, container)
  }
}

// 处理类组件
function buildStatefulComponent(virtualDOM) {
  // 实例化类组件 得到类组件实例对象 并将 props 属性传递进类组件
  const component = new virtualDOM.type(virtualDOM.props)
  // 调用类组件中的render方法得到要渲染的 Virtual DOM
  const nextVirtualDOM = component.render()
  // 返回要渲染的 Virtual DOM
  return nextVirtualDOM
}
```

### 9. Virtual DOM 比对

在进行 Virtual DOM 比对时，需要用到更新后的 Virtual DOM 和更新前的 Virtual DOM，更新后的 Virtual DOM 目前我们可以通过 render 方法进行传递，现在的问题是更新前的 Virtual DOM 要如何获取呢？

对于更新前的 Virtual DOM，对应的其实就是已经在页面中显示的真实 DOM 对象。既然是这样，那么我们在创建真实DOM对象时，就可以将 Virtual DOM 添加到真实 DOM 对象的属性中。在进行 Virtual DOM 对比之前，就可以通过真实 DOM 对象获取其对应的 Virtual DOM 对象了，其实就是通过render方法的第三个参数获取的，container.firstChild。

在创建真实 DOM 对象时为其添加对应的 Virtual DOM 对象

 ```react
// mountElement.js
import mountElement from "./mountElement"

export default function mountNativeElement(virtualDOM, container) {
  // 将 Virtual DOM 挂载到真实 DOM 对象的属性中 方便在对比时获取其 Virtual DOM
  newElement._virtualDOM = virtualDOM
}
 ```



#### 9.1 Virtual DOM 类型相同

Virtual DOM 类型相同，如果是元素节点，就对比元素节点属性是否发生变化，如果是文本节点就对比文本节点内容是否发生变化

要实现对比，需要先从已存在 DOM 对象中获取其对应的 Virtual DOM 对象。

```react
// diff.js
// 获取未更新前的 Virtual DOM
const oldVirtualDOM = oldDOM && oldDOM._virtualDOM
```

判断 oldVirtualDOM 是否存在， 如果存在则继续判断要对比的 Virtual DOM 类型是否相同，如果类型相同判断节点类型是否是文本，如果是文本节点对比，就调用 updateTextNode 方法，如果是元素节点对比就调用 setAttributeForElement 方法

```react
// diff.js
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
  if (virtualDOM.type === "text") {
    // 文本节点 对比文本内容是否发生变化
    updateTextNode(virtualDOM, oldVirtualDOM, oldDOM)
  } else {
    // 元素节点 对比元素属性是否发生变化
    setAttributeForElement(oldDOM, virtualDOM, oldVirtualDOM)
  }
```

updateTextNode 方法用于对比文本节点内容是否发生变化，如果发生变化则更新真实 DOM 对象中的内容，既然真实 DOM 对象发生了变化，还要将最新的 Virtual DOM 同步给真实 DOM 对象。

```react
function updateTextNode(virtualDOM, oldVirtualDOM, oldDOM) {
  // 如果文本节点内容不同
  if (virtualDOM.props.textContent !== oldVirtualDOM.props.textContent) {
    // 更新真实 DOM 对象中的内容
    oldDOM.textContent = virtualDOM.props.textContent
  }
  // 同步真实 DOM 对应的 Virtual DOM
  oldDOM._virtualDOM = virtualDOM
}
```

setAttributeForElement 方法用于设置/更新元素节点属性

思路是先分别获取更新后的和更新前的 Virtual DOM 中的 props 属性，循环新 Virtual DOM 中的 props 属性，通过对比看一下新 Virtual DOM 中的属性值是否发生了变化，如果发生变化 需要将变化的值更新到真实 DOM 对象中

再循环未更新前的 Virtual DOM 对象，通过对比看看新的 Virtual DOM 中是否有被删除的属性，如果存在删除的属性 需要将 DOM 对象中对应的属性也删除掉

```react
// updateNodeElement.js
export default function updateNodeElement(
  newElement,
  virtualDOM,
  oldVirtualDOM = {}
) {
  // 获取节点对应的属性对象
  const newProps = virtualDOM.props || {}
  const oldProps = oldVirtualDOM.props || {}
  Object.keys(newProps).forEach(propName => {
    // 获取属性值
    const newPropsValue = newProps[propName]
    const oldPropsValue = oldProps[propName]
    if (newPropsValue !== oldPropsValue) {
      // 判断属性是否是否事件属性 onClick -> click
      if (propName.slice(0, 2) === "on") {
        // 事件名称
        const eventName = propName.toLowerCase().slice(2)
        // 为元素添加事件
        newElement.addEventListener(eventName, newPropsValue)
        // 删除原有的事件的事件处理函数
        if (oldPropsValue) {
          newElement.removeEventListener(eventName, oldPropsValue)
        }
      } else if (propName === "value" || propName === "checked") {
        newElement[propName] = newPropsValue
      } else if (propName !== "children") {
        if (propName === "className") {
          newElement.setAttribute("class", newPropsValue)
        } else {
          newElement.setAttribute(propName, newPropsValue)
        }
      }
    }
  })
  // 判断属性被删除的情况
  Object.keys(oldProps).forEach(propName => {
    const newPropsValue = newProps[propName]
    const oldPropsValue = oldProps[propName]
    if (!newPropsValue) {
      // 属性被删除了
      if (propName.slice(0, 2) === "on") {
        const eventName = propName.toLowerCase().slice(2)
        newElement.removeEventListener(eventName, oldPropsValue)
      } else if (propName !== "children") {
        newElement.removeAttribute(propName)
      }
    }
  })
}
```

以上对比的仅仅是最上层元素，上层元素对比完成以后还需要递归对比子元素

```react
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
    // 递归对比 Virtual DOM 的子元素
    virtualDOM.children.forEach((child, i) => {
      diff(child, oldDOM, oldDOM.childNodes[i])
    })
  }
```



#### 9.2 Virtual DOM 类型不同

当对比的元素节点类型不同时，就不需要继续对比了，直接使用新的 Virtual DOM 创建 DOM 对象，用新的 DOM 对象直接替换旧的 DOM 对象。当前这种情况要将组件刨除，组件要被单独处理。

```react
// diff.js
else if (
  // 如果 Virtual DOM 类型不一样
  virtualDOM.type !== oldVirtualDOM.type &&
  // 并且 Virtual DOM 不是组件 因为组件要单独进行处理
  typeof virtualDOM.type !== "function"
) {
  // 根据 Virtual DOM 创建真实 DOM 元素
  const newDOMElement = createDOMElement(virtualDOM)
  // 用创建出来的真实 DOM 元素 替换旧的 DOM 元素
  oldDOM.parentNode.replaceChild(newDOMElement, oldDOM)
} 
```

#### 9.3 删除节点

删除节点发生在节点更新以后并且发生在同一个父节点下的所有子节点身上。

在节点更新完成以后，如果旧节点对象的数量多于新 VirtualDOM 节点的数量，就说明有节点需要被删除。



```react
// 获取就节点的数量
let oldChildNodes = oldDOM.childNodes
// 如果旧节点的数量多于要渲染的新节点的长度
if (oldChildNodes.length > virtualDOM.children.length) {
  for (
    let i = oldChildNodes.length - 1;
    i > virtualDOM.children.length - 1;
    i--
  ) {
    oldDOM.removeChild(oldChildNodes[i])
  }
}
```

#### 9.4 类组件状态更新

以下代码是要更新状态的类组件，在类组件的 state 对象中有默认的 title 状态，点击 change title 按钮调用 handleChange 方法，在 handleChange 方法中调用 this.setState 方法更改 title 的状态值。

```react
class Alert extends TinyReact.Component {
  constructor(props) {
    super(props)
    this.state = {
      title: "default title"
    }
    // 更改 handleChange 方法中的 this 指向 让 this 指向类实例对象
    this.handleChange = this.handleChange.bind(this)
  }
  handleChange() {
    // 调用父类中的 setState 方法更改状态
    this.setState({
      title: "changed title"
    })
  }
  render() {
    return (
      <div>
        <h2>{this.state.title}</h2>
        <p>{this.props.message}</p>
        <button onClick={this.handleChange}>change title</button>
      </div>
    )
  }
}
```

setState 方法是定义在父类 Component 中的，该方法的作用是更改子类的 state，产生一个全新的 state 对象。

```react
// Component.js
export default class Component {
  constructor(props) {
    this.props = props
  }
  setState (state) {
    // setState 方法被子类调用 此处this指向子类实例对象
    // 所以改变的是子类的 state 对象
    this.state = Object.assign({}, this.state, state)
  }
}
```

现在子类已经可以调用父类的 setState 方法更改状态值了，当组件的 state 对象发生更改时，要调用 render 方法更新组件视图。

在更新组件之前，要使用更新的 Virtual DOM 对象和未更新的 Virtual DOM 进行对比找出更新的部分，达到 DOM 最小化操作的目的。

在 setState 方法中可以通过调用 this.render 方法获取更新后的 Virtual DOM，由于 setState 方法被子类调用，this 指向子类，所以此处调用的是子类的 render 方法。

```react
// Component.js
setState(state) {
  // setState 方法被子类调用 此处this指向子类
  // 所以改变的是子类的 state
  this.state = Object.assign({}, this.state, state)
  // 通过调用 render 方法获取最新的 Virtual DOM
  let virtualDOM = this.render()
}
```

要实现对比，还需要获取未更新前的 Virtual DOM，按照之前的经验，我们可以从 DOM 对象中获取其对应的 Virtual  DOM 对象，未更新前的 DOM 对象实际上就是现在在页面中显示的 DOM 对象，我们只要能获取到这个 DOM 对象就可以获取到其对应的 Virtual DOM 对象了。

页面中的 DOM 对象要怎样获取呢？页面中的 DOM 对象是通过 mountNativeElement 方法挂载到页面中的，所以我们只需要在这个方法中调用 Component 类中的方法就可以将 DOM 对象保存在 Component 类中了。在子类调用 setState 方法的时候，在 setState 方法中再调用另一个获取 DOM 对象的方法就可以获取到之前保存的 DOM 对象了。

```react
// Component.js
// 保存 DOM 对象的方法
setDOM(dom) {
  this._dom = dom
}
// 获取 DOM 对象的方法
getDOM() {
  return this._dom
}
```

接下来我们要研究一下在 mountNativeElement 方法中如何才能调用到 setDOM 方法，要调用 setDOM 方法，必须要得到类的实例对象，所以目前的问题就是如何在 mountNativeElement 方法中得到类的实例对象，这个类指的不是Component类，因为我们在代码中并不是直接实例化的Component类，而是实例化的它的子类，由于子类继承了父类，所以在子类的实例对象中也是可以调用到 setDOM 方法的。

mountNativeElement 方法接收最新的 Virtual DOM 对象，如果这个 Virtual DOM 对象是类组件产生的，在产生这个 Virtual DOM 对象时一定会先得到这个类的实例对象，然后再调用实例对象下面的 render 方法进行获取。我们可以在那个时候将类组件实例对象添加到 Virtual DOM 对象的属性中，而这个 Virtual DOM 对象最终会传递给 mountNativeElement  方法，这样我们就可以在 mountNativeElement 方法中获取到组件的实例对象了，既然类组件的实例对象获取到了，我们就可以调用 setDOM 方法了。

在 buildClassComponent 方法中为 Virtual DOM 对象添加 component 属性， 值为类组件的实例对象。

```react
function buildClassComponent(virtualDOM) {
  const component = new virtualDOM.type(virtualDOM.props)
  const nextVirtualDOM = component.render()
  nextVirtualDOM.component = component
  return nextVirtualDOM
}
```

在 mountNativeElement 方法中获取组件实例对象，通过实例调用调用 setDOM 方法保存 DOM 对象，方便在对比时通过它获取它的 Virtual DOM 对象

```react
export default function mountNativeElement(virtualDOM, container) {
  // 获取组件实例对象
  const component = virtualDOM.component
  // 如果组件实例对象存在
  if (component) {
    // 保存 DOM 对象
    component.setDOM(newElement)
  }
}
```

接下来在 setState 方法中就可以调用 getDOM 方法获取 DOM 对象了

```react
setState(state) {
  this.state = Object.assign({}, this.state, state)
  let virtualDOM = this.render()
  // 获取页面中正在显示的 DOM 对象 通过它可以获取其对象的 Virtual DOM 对象
  let oldDOM = this.getDOM()
}
```

现在更新前的 Virtual DOM 对象和更新后的 Virtual DOM 对象就都已经获取到了，接下来还要获取到真实 DOM 对象父级容器对象，因为在调用 diff 方法进行对比的时候需要用到

```react
setState(state) {
  this.state = Object.assign({}, this.state, state)
  let virtualDOM = this.render()
  let oldDOM = this.getDOM()
  // 获取真实 DOM 对象父级容器对象
  let container = oldDOM.parentNode
}
```

接下来就可以调用 diff 方法进行比对了，比对后会按照我们之前写好的逻辑进行 DOM 对象更新，我们就可以在页面中看到效果了

```react
setState(state) {
    this.state = Object.assign({}, this.state, state)
    let virtualDOM = this.render()
    let oldDOM = this.getDOM()
    let container = oldDOM.parentNode
    // 比对
    diff(virtualDOM, container, oldDOM)
  }
```

#### 9.5 组件更新

在 diff 方法中判断要更新的 Virtual DOM 是否是组件。

如果是组件再判断要更新的组件和未更新前的组件是否是同一个组件，如果不是同一个组件就不需要做组件更新操作，直接调用 mountElement 方法将组件返回的 Virtual DOM 添加到页面中。

如果是同一个组件，就执行更新组件操作，其实就是将最新的 props 传递到组件中，再调用组件的render方法获取组件返回的最新的 Virtual DOM 对象，再将 Virtual DOM 对象传递给 diff 方法，让 diff 方法找出差异，从而将差异更新到真实 DOM 对象中。

在更新组件的过程中还要在不同阶段调用其不同的组件生命周期函数。

在 diff 方法中判断要更新的 Virtual DOM 是否是组件，如果是组件又分为多种情况，新增 diffComponent 方法进行处理

```react
else if (typeof virtualDOM.type === "function") {
  // 要更新的是组件
  // 1) 组件本身的 virtualDOM 对象 通过它可以获取到组件最新的 props
  // 2) 要更新的组件的实例对象 通过它可以调用组件的生命周期函数 可以更新组件的 props 属性 可以获取到组件返回的最新的 Virtual DOM
  // 3) 要更新的 DOM 象 在更新组件时 需要在已有DOM对象的身上进行修改 实现DOM最小化操作 获取旧的 Virtual DOM 对象
  // 4) 如果要更新的组件和旧组件不是同一个组件 要直接将组件返回的 Virtual DOM 显示在页面中 此时需要 container 做为父级容器
  diffComponent(virtualDOM, oldComponent, oldDOM, container)
}
```

在 diffComponent 方法中判断要更新的组件是未更新前的组件是否是同一个组件

```react
// diffComponent.js
export default function diffComponent(virtualDOM, oldComponent, oldDOM, container) {
  // 判断要更新的组件和未更新的组件是否是同一个组件 只需要确定两者使用的是否是同一个构造函数就可以了
  if (isSameComponent(virtualDOM, oldComponent)) {
    // 属同一个组件 做组件更新  
  } else {
    // 不是同一个组件 直接将组件内容显示在页面中
  }
}
// virtualDOM.type 更新后的组件构造函数
// oldComponent.constructor 未更新前的组件构造函数
// 两者等价就表示是同一组件
function isSameComponent(virtualDOM, oldComponent) {
  return oldComponent && virtualDOM.type === oldComponent.constructor
}
```

如果不是同一个组件的话，就不需要执行更新组件的操作，直接将组件内容显示在页面中，替换原有内容

```react
// diffComponent.js
else {
  // 不是同一个组件 直接将组件内容显示在页面中
  // 这里为 mountElement 方法新增了一个参数 oldDOM 
  // 作用是在将 DOM 对象插入到页面前 将页面中已存在的 DOM 对象删除 否则无论是旧DOM对象还是新DOM对象都会显示在页面中
  mountElement(virtualDOM, container, oldDOM)
}
```

在 mountNativeElement 方法中删除原有的旧 DOM 对象

```javascript
// mountNavtiveElement.js
export default function mountNativeElement(virtualDOM, container, oldDOM) {
 // 如果旧的DOM对象存在 删除
  if (oldDOM) {
    unmount(oldDOM)
  }
}
```

```react
// unmount.js
export default function unmount(node) {
  node.remove()
}
```

如果是同一个组件的话，需要执行组件更新操作，需要调用组件生命周期函数

先在 Component 类中添加生命周期函数，子类要使用的话直接覆盖就可以

```react
// Component.js
export default class Component {
  // 生命周期函数
  componentWillMount() {}
  componentDidMount() {}
  componentWillReceiveProps(nextProps) {}
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps != this.props || nextState != this.state
  }
  componentWillUpdate(nextProps, nextState) {}
  componentDidUpdate(prevProps, preState) {}
  componentWillUnmount() {}
}
```

新建 updateComponent 方法用于更新组件操作，并在 if 成立后调用

```react
// diffComponent.js
if (isSameComponent(virtualDOM, oldComponent)) {
  // 属同一个组件 做组件更新
  updateComponent(virtualDOM, oldComponent, oldDOM, container)
}
```

在 updateComponent 方法中调用组件的生命周期函数，更新组件获取最新 Virtual DOM，最终调用 diff 方法进行更新

```react
import diff from "./diff"

export default function updateComponent(
  virtualDOM,
  oldComponent,
  oldDOM,
  container
) {
  // 生命周期函数
  oldComponent.componentWillReceiveProps(virtualDOM.props)
  if (
    // 调用 shouldComponentUpdate 生命周期函数判断是否要执行更新操作
    oldComponent.shouldComponentUpdate(virtualDOM.props)
  ) {
    // 将未更新的 props 保存一份
    let prevProps = oldComponent.props
    // 生命周期函数
    oldComponent.componentWillUpdate(virtualDOM.props)
    // 更新组件的 props 属性 updateProps 方法定义在 Component 类型
    oldComponent.updateProps(virtualDOM.props)
    // 因为组件的 props 已经更新 所以调用 render 方法获取最新的 Virtual DOM
    const nextVirtualDOM = oldComponent.render()
    // 将组件实例对象挂载到 Virtual DOM 身上
    nextVirtualDOM.component = oldComponent
    // 调用diff方法更新视图
    diff(nextVirtualDOM, container, oldDOM)
    // 生命周期函数
    oldComponent.componentDidUpdate(prevProps)
  }
}
```

```react
// Component.js
export default class Component {
  updateProps(props) {
    this.props = props
  }
}
```

### 10. ref 属性

为节点添加 ref 属性可以获取到这个节点的 DOM 对象，比如在 DemoRef 类中，为 input 元素添加了 ref 属性，目的是获取 input DOM 元素对象，在点击按钮时获取用户在文本框中输入的内容

```react
class DemoRef extends TinyReact.Component {
  handle() {
    let value = this.input.value
    console.log(value)
  }
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input = input)} />
        <button onClick={this.handle.bind(this)}>按钮</button>
      </div>
    )
  }
}
```

实现思路是在创建节点时判断其 Virtual DOM 对象中是否有 ref 属性，如果有就调用 ref 属性中所存储的方法并且将创建出来的DOM对象作为参数传递给 ref 方法，这样在渲染组件节点的时候就可以拿到元素对象并将元素对象存储为组件属性了。

```react
// createDOMElement.js
if (virtualDOM.props && virtualDOM.props.ref) {
  virtualDOM.props.ref(newElement)
}
```

在类组件的身上也可以添加 ref 属性，目的是获取组件的实例对象，比如下列代码中，在 DemoRef 组件中渲染了 Alert 组件，在 Alert 组件中添加了 ref 属性，目的是在 DemoRef 组件中获取 Alert 组件实例对象。

```react
class DemoRef extends TinyReact.Component {
  handle() {
    let value = this.input.value
    console.log(value)
    console.log(this.alert)
  }
  componentDidMount() {
    console.log("componentDidMount")
  }
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input = input)} />
        <button onClick={this.handle.bind(this)}>按钮</button>
        <Alert ref={alert => (this.alert = alert)} />
      </div>
    )
  }
}
```

实现思路是在 mountComponent 方法中，如果判断了当前处理的是类组件，就通过类组件返回的 Virtual DOM 对象中获取组件实例对象，判断组件实例对象中的 props 属性中是否存在 ref 属性，如果存在就调用 ref 方法并且将组件实例对象传递给 ref 方法。

```react
// mountComponent.js
let component = null
  if (isFunctionalComponent(virtualDOM)) {}
	else {
    // 类组件
    nextVirtualDOM = buildStatefulComponent(virtualDOM)
    // 获取组件实例对象
    component = nextVirtualDOM.component
  }
	// 如果组件实例对象存在的话
	if (component) {
   	// 判断组件实例对象身上是否有 props 属性 props 属性中是否有 ref 属性
    if (component.props && component.props.ref) {
      // 调用 ref 方法并传递组件实例对象
      component.props.ref(component)
    }
  }

```

代码走到这，顺便处理一下组件挂载完成的生命周期函数

```react
// 如果组件实例对象存在的话
if (component) {
  component.componentDidMount()
}
```

### 11. key 属性

在 React 中，渲染列表数据时通常会在被渲染的列表元素上添加 key 属性，key 属性就是数据的唯一标识，帮助 React 识别哪些数据被修改或者删除了，从而达到 DOM 最小化操作的目的。

key 属性不需要全局唯一，但是在同一个父节点下的兄弟节点之间必须是唯一的。

也就是说，在比对同一个父节点下类型相同的子节点时需要用到 key 属性。

#### 11.1 节点对比

实现思路是在两个元素进行比对时，如果类型相同，就循环旧的 DOM 对象的子元素，查看其身上是否有key 属性，如果有就将这个子元素的 DOM 对象存储在一个 JavaScript 对象中，接着循环要渲染的 Virtual DOM 对象的子元素，在循环过程中获取到这个子元素的 key 属性，然后使用这个 key 属性到 JavaScript 对象中查找 DOM 对象，如果能够找到就说明这个元素是已经存在的，是不需要重新渲染的。如果通过key属性找不到这个元素，就说明这个元素是新增的是需要渲染的。

```react
// diff.js
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
  // 将拥有key属性的元素放入 keyedElements 对象中
  let keyedElements = {}
  for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {
    let domElement = oldDOM.childNodes[i]
    if (domElement.nodeType === 1) {
      let key = domElement.getAttribute("key")
      if (key) {
        keyedElements[key] = domElement
      }
    }
  }
}
```

```react
// diff.js
// 看一看是否有找到了拥有 key 属性的元素
let hasNoKey = Object.keys(keyedElements).length === 0

// 如果没有找到拥有 key 属性的元素 就按照索引进行比较
if (hasNoKey) {
  // 递归对比 Virtual DOM 的子元素
  virtualDOM.children.forEach((child, i) => {
    diff(child, oldDOM, oldDOM.childNodes[i])
  })
} else {
  // 使用key属性进行元素比较
  virtualDOM.children.forEach((child, i) => {
    // 获取要进行比对的元素的 key 属性
    let key = child.props.key
    // 如果 key 属性存在
    if (key) {
      // 到已存在的 DOM 元素对象中查找对应的 DOM 元素
      let domElement = keyedElements[key]
      // 如果找到元素就说明该元素已经存在 不需要重新渲染
      if (domElement) {
        // 虽然 DOM 元素不需要重新渲染 但是不能确定元