138 張圖帶你 MySQL 入門

SQL 基礎使用

MySQL 是一種關係型數據庫,說到關係,那麼就離不開表與表之間的關係,而最能體現這種關係的其實就是我們接下來需要介紹的主角 SQL,SQL 的全稱是 Structure Query Language ,結構化的查詢語言,它是一種針對錶關聯關係所設計的一門語言,也就是說,學好 MySQL,SQL 是基礎和重中之重。SQL 不只是 MySQL 中特有的一門語言,大多數關係型數據庫都支持這門語言。

下面我們就來一起學習一下這門非常重要的語言。

查詢語言分類

在了解 SQL 之前我們需要知道下面這幾個概念

  • 數據定義語言: 簡稱DDL (Data Definition Language),用來定義數據庫對象:數據庫、表、列等;
  • 數據操作語言: 簡稱DML (Data Manipulation Language),用來對數據庫中表的記錄進行更新。關鍵字: insert、update、delete等
  • 數據控制語言: 簡稱DCL(Data Control Language),用來定義數據庫訪問權限和安全級別,創建用戶等。關鍵字: grant等
  • 數據查詢語言: 簡稱DQL(Data Query Language),用來查詢數據庫中表的記錄,關鍵字: select from where等

DDL 語句

創建數據庫

下面就開始我們的 SQL 語句學習之旅,首先你需要啟動 MySQL 服務,我這裡是 mac 電腦,所以我直接可以啟動

然後我們使用命令行的方式連接數據庫,打開 iterm,輸入下面

MacBook:~ mr.l$ mysql -uroot -p

就可以連接到數據庫了

在上面命令中,mysql 代表客戶端命令,- u 表示後面需要連接的用戶,-p 表示需要輸入此用戶的密碼。在你輸入用戶名和密碼后,如果成功登陸,會显示一個歡迎界面(如上圖 )和 mysql> 提示符。

歡迎界面主要描述了這些東西

  • 每一行的結束符,這裏用 ; 或者 \g 來表示每一行的結束
  • Your MySQL connection id is 4,這個記錄了 MySQL 服務到目前為止的連接數,每個新鏈接都會自動增加 1 ,上面显示的連接次數是 4 ,說明我們只連接了四次
  • 然後下面是 MySQL 的版本,我們使用的是 5.7
  • 通過 help 或者 \h 命令來显示幫助內容,通過 \c 命令來清除命令行 buffer。

然後需要做的事情是什麼?我們最終想要學習 SQL 語句,SQL 語句肯定是要查詢數據,通過數據來體現出來表的關聯關係,所以我們需要數據,那麼數據存在哪裡呢?數據存儲的位置被稱為 表(table),表存儲的位置被稱為 數據庫(database),所以我們需要先建數據庫後面再建表然後插入數據,再進行查詢。

所以我們首先要做的就是創建數據庫,創建數據庫可以直接使用指令

CREATE DATABASE dbname;

進行創建,比如我們創建數據庫 cxuandb

create database cxuandb;

注意最後的 ; 結束語法一定不要丟掉,否則 MySQL 會認為你的命令沒有輸出完,敲 enter 後會直接換行輸出

創建完成后,會提示 Query OK, 1 row affected,這段語句什麼意思呢? Query OK 表示的就是查詢完成,為什麼會显示這個?因為所有的 DDL 和 DML 操作執行完成后都會提示這個, 也可以理解為操作成功。後面跟着的 **1 row affected ** 表示的是影響的行數,() 內显示的是你執行這條命令所耗費的時間,也就是 0.03 秒。

上圖我們成功創建了一個 cxuandb 的數據庫,此時我們還想創建一個數據庫,我們再執行相同的指令,結果提示

提示我們不能再創建數據庫了,數據庫已經存在。這時候我就有疑問了,我怎麼知道都有哪些數據庫呢?別我再想創建一個數據庫又告訴我已經存在,這時候可以使用 show databases 命令來查看你的 MySQL 已有的數據庫

show databases;

執行完成后的結果如下

因為數據庫我之前已經使用過,這裏就需要解釋一下,除了剛剛新創建成功的 cxuandb 外,informationn_schemaperformannce_schemasys 都是系統自帶的數據庫,是安裝 MySQL 默認創建的數據庫。它們各自表示

  • informationn_schema: 主要存儲一些數據庫對象信息,比如用戶表信息、權限信息、分區信息等
  • performannce_schema: MySQL 5.5 之後新增加的數據庫,主要用於收集數據庫服務器性能參數。
  • sys: MySQL 5.7 提供的數據庫,sys 數據庫裡面包含了一系列的存儲過程、自定義函數以及視圖來幫助我們快速的了解系統的元數據信息。

其他所有的數據庫都是作者自己創建的,可以忽略他們。

在創建完數據庫之後,可以用如下命令選擇要操作的數據庫

use cxuandb

這樣就成功切換為了 cxuandb 數據庫,我們可以在此數據庫下進行建表、查看基本信息等操作。

比如想要看康康我們新建的數據庫裏面有沒有其他表

show tables;

果然,我們新建的數據庫下面沒有任何錶,但是現在,我們還不進行建表操作,我們還是先來認識一下數據庫層面的命令,也就是其他 DDL 指令

刪除數據庫

如果一個數據庫我們不想要了,那麼該怎麼辦呢?直接刪掉數據庫不就好了嗎?刪表語句是

drop database dbname;

比如 cxuandb 我們不想要他了,可以通過使用

drop database cxuandb;

進行刪除,這裏我們就不進行演示了,因為 cxuandb 我們後面還會使用。

但是這裏注意一點,你刪除數據庫成功後會出現 0 rows affected,這個可以不用理會,因為在 MySQL 中,drop 語句操作的結果都是 0 rows affected

創建表

下面我們就可以對錶進行操作了,我們剛剛 show tables 發現還沒有任何錶,所以我們現在進行建表語句

CREATE TABLE 表名稱
(
列名稱1 數據類型 約束,
列名稱2 數據類型 約束,
列名稱3 數據類型 約束,
....
)

這樣就很清楚了吧,列名稱就是列的名字,緊跟着列名後面就是數據類型,然後是約束,為什麼要這麼設計?舉個例子你就清楚了,比如 cxuan 剛被生出來就被打印上了標籤

比如我們創建一個表,裏面有 5 個字段,姓名(name)、性別(sex)、年齡(age)、何時雇傭(hiredate)、薪資待遇(wage),建表語句如下

create table job(name varchar(20), sex varchar(2), age int(2), hiredate date, wage decimal(10,2));

事實證明這條建表語句還是沒問題的,建表完成后可以使用 DESC tablename 查看錶的基本信息

DESC 命令會查看錶的定義,但是輸出的信息還不夠全面,所以,如果想要查看更全的信息,還要通過查看錶的創建語句的 SQL 來得到

show create table job \G;

可以看到,除了看到表定義之外,還看到了表的 engine(存儲引擎) 為 InnoDB 存儲引擎,\G 使得記錄能夠豎著排列,如果不用 \G 的話,效果如下

刪除表

表的刪除語句有兩種,一種是 drop 語句,SQL 語句如下

drop table job

一種是 truncate 語句,SQL 語句如下

truncate table job

這兩者的區別簡單理解就是 drop 語句刪除表之後,可以通過日誌進行回復,而 truncate 刪除表之後永遠恢復不了,所以,一般不使用 truncate 進行表的刪除。‘

修改表

對於已經創建好的表,尤其是有大量數據的表,如果需要對錶做結構上的改變,可以將表刪除然後重新創建表,但是這種效率會產生一些額外的工作,數據會重新加載近來,如果此時有服務正在訪問的話,也會影響服務讀取表中數據,所以此時,我們需要表的修改語句來對已經創建好的表的定義進行修改。

修改表結構一般使用 alter table 語句,下面是常用的命令

ALTER TABLE tb MODIFY [COLUMN] column_definition [FIRST | AFTER col_name];

比如我們想要將 job 表中的 name 由 varchar(20) 改為 varchar(25),可以使用如下語句

alter table job modify name varchar(25);

也可以對錶結構進行修改,比如增加一個字段

alter table job add home varchar(30);

將新添加的表的字段進行刪除

alter table job drop column home;

可以對表中字段的名稱進行修改,比如吧 wage 改為 salary

alter table job change wage salary decimal(10,2);

修改字段的排列順序,我們前面介紹過修改語法涉及到一個順序問題,都有一個可選項 **first | after ** column_name,這個選項可以用來修改表中字段的位置,默認 ADD 是在添加為表中最後一個字段,而 CHANGE/MODIFY 不會改變字段位置。比如

alter table job add birthday after hiredate;

可以對錶名進行修改,例如將 job 表改為 worker

alter table job rename worker;

DML 語句

有的地方把 DML 語句(增刪改)和 DQL 語句(查詢)統稱為 DML 語句,有的地方分開,我們目前使用分開稱呼的方式

插入

表創建好之後,我們就可以向表裡插入數據了,插入記錄的基本語法如下

INSERT INTO tablename (field1,field2) VALUES(value1,value2);

例如,向中插入以下記錄

insert into job(name,sex,age,hiredate,birthday,salary) values("cxuan","男",24,"2020-04-27","1995-08-22",8000);

也可以不用指定要插入的字段,直接插入數據即可

insert into job values("cxuan02","男",25,"2020-06-01","1995-04-23",12000);

這裏就有一個問題,如果插入的順序不一致的話會怎麼樣呢?

對於含可空字段、非空但是含有默認值的字段、自增字段可以不用在 insert 后的字段列表出現,values 後面只需要寫對應字段名稱的 value 即可,沒有寫的字段可以自動的設置為 NULL、默認值或者自增的下一個值,這樣可以縮短要插入 SQL 語句的長度和複雜性。

比如我們設置一下 hiredate、age 可以為 null,來試一下

insert into job(name,sex,birthday,salary) values("cxuan03","男","1992-08-23",15000);

我們看一下實際插入的數據

我們可以看到有一行兩個字段显示 NULL。在 MySQL 中,insert 語句還有一個很好的特性,就是一次可以插入多條記錄

INSERT INTO tablename (field1,field2) VALUES
(value1,value2),
(value1,value2),
(value1,value2),
...;

可以看出,每條記錄之間都用逗號進行分割,這個特性可以使得 MySQL 在插入大量記錄時,節省很多的網絡開銷,大大提高插入效率。

更新記錄

對於表中已經存在的數據,可以通過 update 命令對其進行修改,語法如下

UPDATE tablename SET field1 = value1, field2 = value2 ;

例如,將 job 表中的 cxuan03 中 age 的 NULL 改為 26,SQL 語句如下

update job set age = 26 where name = 'cxuan03';

SQL 語句中出現了一個 where 條件,我們會在後面說到 where 條件,這裏簡單理解一下它的概念就是根據哪條記錄進行更新,如果不寫 where 的話,會對整個表進行更新

刪除記錄

如果記錄不再需要,可以使用 delete 命令進行刪除

DELETE FROM tablename [WHERE CONDITION]

例如,在 job 中刪除名字是 cxuan03 的記錄

delete from job where name = 'cxuan03';

在 MySQL 中,刪除語句也可以不指定 where 條件,直接使用

delete from job

這種刪除方式相當於是清楚表的操作,表中所有的記錄都會被清除。

DQL 語句

下面我們一起來認識一下 DQL 語句,數據被插入到 MySQL 中,就可以使用 SELECT 命令進行查詢,來得到我們想要的結果。

SELECT 查詢語句可以說是最複雜的語句了,這裏我們只介紹一下基本語法

一種最簡單的方式就是從某個表中查詢出所有的字段和數據,簡單粗暴,直接使用 SELECT *

SELECT * FROM tablename;

例如我們將 job 表中的所有數據查出來

select * from job;

其中 * 是查詢出所有的數據,當然,你也可以查詢出指定的數據項

select name,sex,age,hiredate,birthday,salary from job;

上面這條 SQL 語句和 select * from job 表是等價的,但是這種直接查詢指定字段的 SQL 語句效率要高。

上面我們介紹了基本的 SQL 查詢語句,但是實際的使用場景會會比簡單查詢複雜太多,一般都會使用各種 SQL 的函數和查詢條件等,下面我們就來一起認識一下。

去重

使用非常廣泛的場景之一就是 去重,去重可以使用 distinct 關鍵字來實現

為了演示效果,我們先向數據庫中插入批量數據,插入完成后的表結構如下

下面我們使用 distinct 來對 age 去重來看一下效果

你會發現只有兩個不同的值,其他和 25 重複的值被過濾掉了,所以我們使用 distinct 來進行去重

條件查詢

我們之前的所有例子都是查詢全部的記錄,如果我們只想查詢指定的記錄呢?這裏就會用到 where 條件查詢語句,條件查詢可以對指定的字段進行查詢,比如我們想查詢所有年齡為 24 的記錄,如下

select * from job where age = 24;

where 條件語句後面會跟一個判斷的運算符 =,除了 = 號比較外,還可以使用 >、<、>=、<=、!= 等比較運算符;例如

select * from job where age >= 24;

就會從 job 表中查詢出 age 年齡大於或等於 24 的記錄

除此之外,在 where 條件查詢中還可以有多個並列的查詢條件,比如我們可以查詢年齡大於等於 24,並且薪資大雨 8000 的記錄

select * from job where age >= 24 and salary > 8000;

多個條件之間還可以使用 or、and 等邏輯運算符進行多條件聯合查詢,運算符會在以後章節中詳細講解。

排序

我們會經常有這樣的需求,按照某個字段進行排序,這就用到了數據庫的排序功能,使用關鍵字 order by 來實現,語法如下

SELECT * FROM tablename [WHERE CONDITION] [ORDER BY field1 [DESC|ASC] , field2 [DESC|ASC],……fieldn [DESC|ASC]]

其中 DESC 和 ASC 就是順序排序的關鍵字,DESC 會按照字段進行降序排列,ASC 會按照字段進行升序排列,默認會使用升序排列,也就是說,你不寫 order by 具體的排序的話,默認會使用升序排列。order by 後面可以跟多個排序字段,並且每個排序字段可以有不同的排序順序。

為了演示功能,我們先把表中的 salary 工資列進行修改,修改完成后的表記錄如下

下面我們按照工資進行排序,SQL 語句如下

select * from job order by salary desc;

語句執行完成后的結果如下

這是對一個字段進行排序的結果,也可以對多個字段進行排序,但是需要注意一點

根據 order by 後面聲名的順序進行排序,如果有三個排序字段 A、B、C 的話,如果 A 字段排序字段的值一樣,則會根據第二個字段進行排序,以此類推。

如果只有一個排序字段,那麼這些字段相同的記錄將會無序排列。

限制

對於排序后的字段,或者不排序的字段,如果只希望显示一部分的話,就會使用 LIMIT 關鍵字來實現,比如我們只想取前三條記錄

select * from job limit 3;

或者我們對排序后的字段取前三條記錄

select * from job order by salary limit 3;

上面這種 limit 是從表記錄的第 0 條開始取,如果從指定記錄開始取,比如從第二條開始取,取三條記錄,SQL 如下

select * from job order by salary desc limit 2,3;

limit 一般經常和 order by 語法一起實現分頁查詢。

注意:limit 是 MySQL 擴展 SQL92 之後的語法,在其他數據庫比如 Oracle 上就不通用,我犯過一個白痴的行為就是在 Oracle 中使用 limit 查詢語句。。。

聚合

下面我們來看一下對記錄進行匯總的操作,這類操作主要有

  • 匯總函數,比如 sum 求和、count 統計數量、max 最大值、min 最小值等
  • group by,關鍵字表示對分類聚合的字段進行分組,比如按照部門統計員工的數量,那麼 group by 後面就應該跟上部門
  • with 是可選的語法,它表示對匯總之後的記錄進行再次匯總
  • having 關鍵字表示對分類后的結果再進行條件的過濾。

看起來 where 和 having 意思差不多,不過它們用法不一樣,where 是使用在統計之前,對統計前的記錄進行過濾,having 是用在統計之後,是對聚合之後的結果進行過濾。也就是說 where 永遠用在 having 之前,我們應該先對篩選的記錄進行過濾,然後再對分組的記錄進行過濾。

可以對 job 表中員工薪水進行統計,選出總共的薪水、最大薪水、最小薪水

select sum(salary) from job;

select max(salary),min(salary) from job;

比如我們要統計 job 表中人員的數量

select count(1) from job;

統計完成后的結果如下

我們可以按照 job 表中的年齡來進行對應的統計

select age,count(1) from job group by age;

既要統計各年齡段的人數,又要統計總人數

select age,count(1) from job group by age with rollup;

在此基礎上進行分組,統計數量大於 1 的記錄

select age,count(1) from job group by age with rollup having count(1) > 1;

表連接

表連接一直是筆者比較痛苦的地方,曾經因為一個表連接掛了面試,現在來認真擼一遍。

表連接一般體現在表之間的關係上。當需要同時显示多個表中的字段時,就可以用表連接來實現。

為了演示表連接的功能,我們為 job 表加一個 type 字段表示工作類型,增加一個 job_type 表表示具體的工作種類,如下所示

下面開始我們的演示

查詢出 job 表中的 type 和 job_type 表中的 type 匹配的姓名和工作類型

select job.name,job_type.name from job,job_type where job.type = job_type.type;

上面這種連接使用的是內連接,除此之外,還有外連接。那麼它們之間的區別是啥呢?

內連接:選出兩張表中互相匹配的記錄;

外連接:不僅選出匹配的記錄,也會選出不匹配的記錄;

外連接分為兩種

  • 左外連接:篩選出包含左表的記錄並且右表沒有和它匹配的記錄
  • 右外連接:篩選出包含右表的記錄甚至左表沒有和它匹配的記錄

為了演示效果我們在 job 表和 job_type 表中分別添加記錄,添加完成后的兩表如下

下面我們進行左外連接查詢:查詢出 job 表中的 type 和 job_type 表中的 type 匹配的姓名和工作類型

select job.name,job_type.name from job left join job_type on job.type = job_type.type;

查詢出來的結果如下

可以看出 cxuan06 也被查詢出來了,而 cxuan06 他沒有具體的工作類型。

使用右外連接查詢

select job.name,job_type.name from job right join job_type on job.type = job_type.type;

可以看出,job 表中並沒有 waiter 和 manager 的角色,但是也被查詢出來了。

子查詢

有一些情況,我們需要的查詢條件是另一個 SQL 語句的查詢結果,這種查詢方式就是子查詢,子查詢有一些關鍵字比如 in、not in、=、!=、exists、not exists 等,例如我們可以通過子查詢查詢出每個人的工作類型

select job.* from job where type in (select type from job_type);

如果自查詢數量唯一的話,還可以用 = 來替換 in

select * from job where type = (select type from job_type);

意思是自查詢不唯一,我們使用 limit 限制一下返回的記錄數

select * from job where type = (select type from job_type limit 1,1);

在某些情況下,子查詢可以轉換為表連接

聯合查詢

我們還經常會遇到這樣的場景,將兩個表的數據單獨查詢出來之後,將結果合併到一起進行显示,這個時候就需要 UNION 和 UNION ALL 這兩個關鍵字來實現這樣的功能,UNION 和 UNION ALL 的主要區別是 UNION ALL 是把結果集直接合併在一起,而 UNION 是將 UNION ALL 后的結果進行一次 DISTINCT 去除掉重複數據。

比如

select type from job union all select type from job_type;

它的結果如下

上述結果是查詢 job 表中的 type 字段和 job_type 表中的 type 字段,並把它們進行匯總,可以看出 UNION ALL 只是把所有的結果都列出來了

使用 UNION 的 SQL 語句如下

select type from job union select type from job_type;

可以看出 UNION 是對 UNION ALL 使用了 distinct 去重處理。

DCL 語句

DCL 語句主要是管理數據庫權限的時候使用,這類操作一般是 DBA 使用的,開發人員不會使用 DCL 語句。

關於幫助文檔的使用

我們一般使用 MySQL 遇到不會的或者有疑問的東西經常要去查閱網上資料,甚至可能需要去查 MySQL 官發文檔,這樣會耗費大量的時間和精力。

下面教你一下在 MySQL 命令行就能直接查詢資料的語句

按照層次查詢

可以使用 ? contents 來查詢所有可供查詢的分類,如下所示

? contents;

我們輸入

? Account Management

可以查詢具體關於權限管理的命令

比如我們想了解一下數據類型

? Data Types

然後我們想了解一下 VARCHAR 的基本定義,可以直接使用

? VARCHAR

可以看到有關於 VARCHAR 數據類型的詳細信息,然後在最下面還有 MySQL 的官方文檔,方便我們快速查閱。

快速查閱

在實際應用過程中,如果要快速查詢某個語法時,可以使用關鍵字進行快速查詢,比如我們使用

? show

能夠快速列出一些命令

比如我們想要查閱 database 的信息,使用

SHOW CREATE DATABASE cxuandb;

MySQL 數據類型

MySQL 提供很多種數據類型來對不同的常量、變量進行區分,MySQL 中的數據類型主要是 數值類型、日期和時間類型、字符串類型 選擇合適的數據類型進行數據的存儲非常重要,在實際開發過程中,選擇合適的數據類型也能夠提高 SQL 性能,所以有必要認識一下這些數據類型。

數值類型

MySQL 支持所有標準的 SQL 數據類型,這些數據類型包括嚴格數據類型的嚴格數值類型,這些數據類型有

  • INTEGER
  • SMALLINT
  • DECIMAL
  • NUMERIC。

近似數值數據類型 並不用嚴格按照指定的數據類型進行存儲,這些有

  • FLOAT
  • REAL
  • DOUBLE PRECISION

還有經過擴展之後的數據類型,它們是

  • TINYINT
  • MEDIUMINT
  • BIGINT
  • BIT

其中 INT 是 INTEGER 的縮寫,DEC 是 DECIMAL 的縮寫。

下面是所有數據類型的匯總

整數

在整數類型中,按照取值範圍和存儲方式的不同,分為

![image-20200613091331344](/Users/mr.l/Library/Application Support/typora-user-images/image-20200613091331344.png)

  • TINYINT ,佔用 1 字節
  • SMALLINT,佔用 2 字節
  • MEDIUMINT,佔用 3 字節
  • INT、INTEGER,佔用 4 字節
  • BIGINT,佔用 8 字節

五個數據類型,如果超出類型範圍的操作,會發生錯誤提示,所以選擇合適的數據類型非常重要。

還記得我們上面的建表語句么

我們一般會在 SQL 語句的數據類型後面加上指定長度來表示數據類型許可的範圍,例如

int(7)

表示 int 類型的數據最大長度為 7,如果填充不滿的話會自動填滿,如果不指定 int 數據類型的長度的話,默認是 int(11)

我們創建一張表來演示一下

create table test1(aId int, bId int(5));

/* 然後我們查看一下錶結構 */
desc test1;

整數類型一般配合 zerofill 來使用,顧名思義,就是用 0 進行填充,也就是数字位數不夠的空間使用 0 進行填充。

分別修改 test1 表中的兩個字段

alter table test1 modify aId int zerofill;

alter table test1 modify bId int(5) zerofill;

然後插入兩條數據,執行查詢操作

如上圖所示,使用zerofill 可以在数字前面使用 0 來進行填充,那麼如果寬度超過指定長度後會如何显示?我們來試驗一下,向 aId 和 bId 分別插入超過字符限制的数字

會發現 aId 已經超出了指定範圍,那麼我們對 aId 插入一個在其允許範圍之內的數據

會發現,aId 已經插進去了,bId 也插進去了,為什麼 bId 显示的是 int(5) 卻能夠插入 7 位長度的數值呢?

所有的整數都有一個可選屬性 UNSIGNED(無符號),如果需要在字段裏面保存非負數或者是需要較大上限值時,可以使用此選項,它的取值範圍是正常值的下限取 0 ,上限取原值的 2 倍。如果一個列為 zerofill ,會自動為該列添加 UNSIGNED 屬性。

除此之外,整數還有一個類型就是 AUTO_INCREMENT,在需要產生唯一標識符或者順序值時,可利用此屬性,這個屬性只用於整数字符。一個表中最多只有一個 AUTO_INCREMENT 屬性,一般用於自增主鍵,而且 NOT NULL,並且是 PRIMARY KEY UNIQUE 的,主鍵必須保證唯一性而且不為空。

小數

小數說的是啥?它其實有兩種類型;一種是浮點數類型,一種是定點數類型;

浮點數有兩種

  • 單精度浮點型 – float 型
  • 雙精度浮點型 – double 型

定點數只有一種 decimal。定點數在 MySQL 內部中以字符串的形式存在,比浮點數更為準確,適合用來表示精度特別高的數據。

浮點數和定點數都可以使用 (M,D) 的方式來表示,M 表示的就是 整數位 + 小數位 的数字,D 表示位於 . 後面的小數。M 也被稱為精度 ,D 被稱為標度。

下面通過示例來演示一下

首先建立一個 test2

CREATE TABLE test2 (aId float(6,2) default NULL, bId double(6,2) default NULL,cId decimal(6,2) default NULL)

然後向表中插入幾條數據

insert into test2 values(1234.12,1234.12,1234.12);

這個時候显示的數據就是

然後再向表中插入一些約束之外的數據

insert into test2 values(1234.123,1234.123,1234.123);

發現插入完成后還显示的是 1234.12,小數位第三位的值被捨去了。

現在我們把 test2 表中的精度全部去掉,再次插入

alter table test2 modify aId float;

alter table test2 modify bId double;

alter table test2 modify cId decimal;

先查詢一下,發現 cId 捨去了小數位。

然後再次插入 1.23,SQL 語句如下

insert into test2 values(1.23,1.23,1.23);

結果如下

這個時候可以驗證

  • 浮點數如果不寫精度和標度,會按照實際的精度值進行显示
  • 定點數如果不寫精度和標度,會按照 decimal(10,0) 來進行操作,如果數據超過了精度和標題,MySQL 會報錯

位類型

對於位類型,用於存放字段值,BIT(M) 可以用來存放多位二進制數,M 的範圍是 1 – 64,如果不寫的話默認為 1 位。

下面我們來掩飾一下位類型

新建一個 test3 表,表中只有一個位類型的字段

create table test3(id bit(1));

然後隨意插入一條數據

insert into test3 values(1);

發現無法查詢出對應結果。

然後我們使用 hex()bin() 函數進行查詢

發現能夠查詢出對應結果。

也就是說當數據插入 test3 時,會首先把數據轉換成為二進制數,如果位數允許,則將成功插入;如果位數小於實際定義的位數,則插入失敗。如果我們像表中插入數據 2

insert into test3 values(2);

那麼會報錯

因為 2 的二進制數表示是 10,而表中定義的是 bit(1) ,所以無法插入。

那麼我們將表字段修改一下

然後再進行插入,發現已經能夠插入了

日期時間類型

MySQL 中的日期與時間類型,主要包括:YEAR、TIME、DATE、DATETIME、TIMESTAMP,每個版本可能不同。下錶中列出了這幾種類型的屬性。

下面分別來介紹一下

YEAR

YEAR 可以使用三種方式來表示

  • 用 4 位的数字或者字符串表示,兩者效果相同,表示範圍 1901 – 2155,插入超出範圍的數據會報錯。
  • 以 2 位字符串格式表示,範圍為 ‘00’‘99’。‘00’‘69’ 表示 20002069,‘70’‘99’ 表示1970~1999。‘0’ 和 ‘00’ 都會被識別為 2000,超出範圍的數據也會被識別為 2000。
  • 以 2 位数字格式表示,範圍為 199。169 表示 2001~2069, 70~99 表示 1970~1999。但 0 值會被識別為0000,這和 2 位字符串被識別為 2000 有所不同

下面我們來演示一下 YEAR 的用法,創建一個 test4 表

create table test4(id year);

然後我們看一下 test4 的表結構

默認創建的 year 就是 4 位,下面我們向 test4 中插入數據

insert into test4 values(2020),('2020');

然後進行查詢,發現表示形式是一樣的

使用兩位字符串來表示

delete from test4;

insert into test4 values ('0'),('00'),('11'),('88'),('20'),('21');

使用兩位数字來表示

delete from test4;

insert into test4 values (0),(00),(11),(88),(20),(21);

發現只有前兩項不一樣。

TIME

TIME 所表示的範圍和我們預想的不一樣

我們把 test4 改為 TIME 類型,下面是 TIME 的示例

alter table test4 modify id TIME;

insert into test4 values ('15:11:23'),('20:13'),('2 11:11'),('3 05'),('33');

結果如下

DATE

DATE 表示的類型有很多種,下面是 DATE 的幾個示例

create table test5 (id date);

查看一下 test5 表

然後插入部分數據

insert into test5 values ('2020-06-13'),('20200613'),(20200613);

DATE 的表示一般很多種,如下所示 DATE 的所有形式

  • ‘YYYY-MM-DD’
  • ‘YYYYMMDD’
  • YYYYMMDD
  • ‘YY-MM-DD’
  • ‘YYMMDD’
  • YYMMDD

DATETIME

DATETIME 類型,包含日期和時間部分,可以使用引用字符串或者数字,年份可以是 4 位也可以是 2 位。

下面是 DATETIME 的示例

create table test6 (id datetime);

insert into test4 values ('2020-06-13 11:11:11'),(20200613111111),('20200613111111'),(20200613080808);

TIMESTAMP

TIMESTAMP 類型和 DATETIME 類型的格式相同,存儲 4 個字節(比DATETIME少),取值範圍比 DATETIME 小。

下面來說一下各個時間類型的使用場景

  • 一般表示年月日,通常用 DATE 類型;

  • 用來表示時分秒,通常用 TIME 表示;

  • 年月日時分秒 ,通常用 DATETIME 來表示;

  • 如果需要插入的是當前時間,通常使用 TIMESTAMP 來表示,TIMESTAMP 值返回后显示為 YYYY-MM-DD HH:MM:SS 格式的字符串,

  • 如果只表示年份、則應該使用 YEAR,它比 DATE 類型需要更小的空間。

每種日期類型都有一個範圍,如果超出這個範圍,在默認的 SQLMode 下,系統會提示錯誤,並進行零值存儲。

下面來解釋一下 SQLMode 是什麼

MySQL 中有一個環境變量是 sql_mode ,sql_mode 支持了 MySQL 的語法、數據校驗,我們可以通過下面這種方式來查看當前數據庫使用的 sql_mode

select @@sql_mode;

一共有下面這幾種模式

來源於 https://www.cnblogs.com/Zender/p/8270833.html

字符串類型

MySQL 提供了很多種字符串類型,下面是字符串類型的匯總

下面我們對這些數據類型做一個詳細的介紹

CHAR 和 VARCHAR 類型

CHAR 和 VARCHAR 類型很相似,導致很多同學都會忽略他們之間的差別,首先他倆都是用來保存字符串的數據類型,他倆的主要區別在於存儲方式不同。CHAR 類型的長度就是你定義多少显示多少。佔用 M 字節,比如你聲明一個 CHAR(20) 的字符串類型,那麼每個字符串佔用 20 字節,M 的取值範圍時 0 – 255。VARCHAR 是可變長的字符串,範圍是 0 – 65535,在字符串檢索的時候,CHAR 會去掉尾部的空格,而 VARCHAR 會保留這些空格。下面是演示例子

create table vctest1 (vc varchar(6),ch char(6));

insert into vctest1 values("abc  ","abc  ");

select length(vc),length(ch) from vctest1;

結果如下

可以看到 vc 的字符串類型是 varchar ,長度是 5,ch 的字符串類型是 char,長度是 3。可以得出結論,varchar 會保留最後的空格,char 會去掉最後的空格。

BINARY 和 VARBINARY 類型

BINARY 和 VARBINARY 與 CHAR 和 VARCHAR 非常類似,不同的是它們包含二進制字符串而不包含非二進制字符串。BINARY 與 VARBINARY 的最大長度和 CHAR 與 VARCHAR 是一樣的,只不過他們是定義字節長度,而 CHAR 和 VARCHAR 對應的是字符長度。

BLOB 類型

BLOB 是一個二進制大對象,可以容納可變數量的數據。有 4 種 BLOB 類型:TINYBLOB、BLOB、MEDIUMBLOB 和 LONGBLOB。它們區別在於可容納存儲範圍不同。

TEXT 類型

有 4 種 TEXT 類型:TINYTEXT、TEXT、MEDIUMTEXT 和 LONGTEXT。對應的這 4 種 BLOB 類型,可存儲的最大長度不同,可根據實際情況選擇。

ENUM 類型

ENUM 我們在 Java 中經常會用到,它表示的是枚舉類型。它的範圍需要在創建表時显示指定,對 1 – 255 的枚舉需要 1 個字節存儲;對於 255 – 65535 的枚舉需要 2 個字節存儲。ENUM 會忽略大小寫,在存儲時都會轉換為大寫。

SET 類型

SET 類型和 ENUM 類型有兩處不同

  • 存儲方式

SET 對於每 0 – 8 個成員,分別佔用 1 個字節,最大到 64 ,佔用 8 個字節

  • Set 和 ENUM 除了存儲之外,最主要的區別在於 Set 類型一次可以選取多個成員,而 ENUM 則只能選一個。

MySQL 運算符

MySQL 中有多種運算符,下面對 MySQL 運算符進行分類

  • 算術運算符
  • 比較運算符
  • 邏輯運算符
  • 位運算符

下面那我們對各個運算符進行介紹

算術運算符

MySQL 支持的算術運算符包括加、減、乘、除和取余,這類運算符的使用頻率比較高

下面是運算符的分類

運算符 作用
+ 加法
減法
* 乘法
/, DIV 除法,返回商
%, MOD 除法,返回餘數

下面簡單描述了這些運算符的使用方法

  • + 用於獲得一個或多個值的和
  • - 用於從一個值減去另一個值
  • * 用於兩數相乘,得到兩個或多個值的乘積
  • / 用一個值除以另一個值得到商
  • % 用於一個值除以另一個值得到餘數

在除法和取余需要注意一點,如果除數是 0 ,將是非法除數,返回結果為 NULL。

比較運算符

熟悉了運算符,下面來聊一聊比較運算符,使用 SELECT 語句進行查詢時,MySQL 允許用戶對表達式的兩側的操作數進行比較,比較結果為真,返回 1, 比較結果為假,返回 0 ,比較結果不確定返回 NULL。下面是所有的比較運算符

運算符 描述
= 等於
<> 或者是 != 不等於
<=> NULL 安全的等於,也就是 NULL-safe
< 小於
<= 小於等於
> 大於
>= 大於等於
BETWEEN 在指定範圍內
IS NULL 是否為 NULL
IS NOT NULL 是否為 NULL
IN 存在於指定集合
LIKE 通配符匹配
REGEXP 或 RLIKE 正則表達式匹配

比較運算符可以用來比較数字、字符串或者表達式。数字作為浮點數進行比較,字符串以不區分大小寫的方式進行比較。

  • = 號運算符,用於比較運算符兩側的操作數是否相等,如果相等則返回 1, 如果不相等則返回 0 ,下面是具體的示例,NULL 不能用於比較,會直接返回 NULL
  • <> 號用於表示不等於,和 = 號相反,示例如下
  • <=> NULL-safe 的等於運算符,與 = 號最大的區別在於可以比較 NULL 值
  • < 號運算符,當左側操作數小於右側操作數時,返回值為 1, 否則其返回值為 0。
  • 和上面同理,只不過是滿足 <= 的時候返回 1 ,否則 > 返回 0。這裏我有個疑問,為什麼
select 'a' <= 'b';  /* 返回 1 */

/*而*/

select 'a' >= 'b'; /* 返回 0 呢*/
  • 關於 >>= 是同理

  • BETWEEN 運算符的使用格式是 a BETWEEN min AND max ,當 a 大於等於 min 並且小於等於 max 時,返回 1,否則返回 0 。操作數類型不同的時候,會轉換成相同的數據類型再進行處理。比如

  • IS NULLIS NOT NULL 表示的是是否為 NULL,ISNULL 為 true 返回 1,否則返回 0 ;IS NOT NULL 同理
  • IN 這個比較操作符判斷某個值是否在一個集合中,使用方式是 xxx in (value1,value2,value3)
  • LIKE 運算符的格式是 xxx LIKE %123%,比如如下

當 like 後面跟的是 123% 的時候, xxx 如果是 123 則返回 1,如果是 123xxx 也返回 1,如果是 12 或者 1 就返回 0 。123 是一個整體。

  • REGEX 運算符的格式是 s REGEXP str ,匹配時返回值為 1,否則返回 0 。

後面會詳細介紹 regexp 的用法。

邏輯運算符

邏輯運算符指的就是布爾運算符,布爾運算符指返回真和假。MySQL 支持四種邏輯運算符

運算符 作用
NOT 或 ! 邏輯非
AND 或者是 && 邏輯與
OR 或者是 || 邏輯或
XOR 邏輯異或

下面分別來介紹一下

  • NOT 或者是 ! 表示的是邏輯非,當操作數為 0(假) ,則返回值為 1,否則值為 0。但是有一點除外,那就是 NOT NULL 的返回值為 NULL
  • AND&& 表示的是邏輯與的邏輯,當所有操作數為非零值並且不為 NULL 時,結果為 1,但凡是有一個 0 則返回 0,操作數中有一個 null 則返回 null
  • OR|| 表示的是邏輯或,當兩個操作數均為非 NULL 值時,如有任意一個操作數為非零值,則結果為 1,否則結果為 0。
  • XOR 表示邏輯異或,當任意一個操作數為 NULL 時,返回值為 NULL。對於非 NULL 的操作數,如果兩個的邏輯真假值相異,則返回結果 1;否則返回 0。

位運算符

一聽說位運算,就知道是和二進制有關的運算符了,位運算就是將給定的操作數轉換為二進制后,對各個操作數的每一位都進行指定的邏輯運算,得到的二進制結果轉換為十進制后就說是位運算的結果,下面是所有的位運算。

運算符 作用
& 位與
| 位或
^ 位異或
位取反
>> 位右移
<< 位左移

下面分別來演示一下這些例子

  • 位與 指的就是按位與,把 & 雙方轉換為二進制再進行 & 操作

按位與是一個數值減小的操作

  • 位或 指的就是按位或,把 | 雙方轉換為二進制再進行 | 操作

位或是一個數值增大的操作

  • 位異或 指的就是對操作數的二進制位做異或操作
  • 位取反 指的就是對操作數的二進制位做 NOT 操作,這裏的操作數只能是一位,下面看一個經典的取反例子:對 1 做位取反,具體如下所示:

為什麼會有這種現象,因為在 MySQL 中,常量数字默認會以 8 個字節來显示,8 個字節就是 64 位,常量 1 的二進製表示 63 個 0,加 1 個 1 , 位取反后就是 63 個 1 加一個 0 , 轉換為二進制后就是 18446744073709551614,我們可以使用 select bin() 查看一下

  • 位右移 是對左操作數向右移動指定位數,例如 50 >> 3,就是對 50 取其二進制然後向右移三位,左邊補上 0 ,轉換結果如下
  • 位左移 與位右移相反,是對左操作數向左移動指定位數,例如 20 << 2

MySQL 常用函數

下面我們來了解一下 MySQL 函數,MySQL 函數也是我們日常開發過程中經常使用的,選用合適的函數能夠提高我們的開發效率,下面我們就來一起認識一下這些函數

字符串函數

字符串函數是最常用的一種函數了,MySQL 也是支持很多種字符串函數,下面是 MySQL 支持的字符串函數表

函數 功能
LOWER 將字符串所有字符變為小寫
UPPER 將字符串所有字符變為大寫
CONCAT 進行字符串拼接
LEFT 返回字符串最左邊的字符
RIGHT 返回字符串最右邊的字符
INSERT 字符串替換
LTRIM 去掉字符串左邊的空格
RTRIM 去掉字符串右邊的空格
REPEAT 返回重複的結果
TRIM 去掉字符串行尾和行頭的空格
SUBSTRING 返回指定的字符串
LPAD 用字符串對最左邊進行填充
RPAD 用字符串對最右邊進行填充
STRCMP 比較字符串 s1 和 s2
REPLACE 進行字符串替換

下面通過具體的示例演示一下每個函數的用法

  • LOWER(str) 和 UPPER(str) 函數:用於轉換大小寫
  • CONCAT(s1,s2 … sn) :把傳入的參數拼接成一個字符串

上面把 c xu an 拼接成為了一個字符串,另外需要注意一點,任何和 NULL 進行字符串拼接的結果都是 NULL。

  • LEFT(str,x) 和 RIGHT(str,x) 函數:分別返回字符串最左邊的 x 個字符和最右邊的 x 個字符。如果第二個參數是 NULL,那麼將不會返回任何字符串
  • INSERT(str,x,y,instr) : 將字符串 str 從指定 x 的位置開始, 取 y 個長度的字串替換為 instr。
  • LTRIM(str) 和 RTRIM(str) 分別表示去掉字符串 str 左側和右側的空格
  • REPEAT(str,x) 函數:返回 str 重複 x 次的結果
  • TRIM(str) 函數:用於去掉目標字符串的空格
  • SUBSTRING(str,x,y) 函數:返回從字符串 str 中第 x 位置起 y 個字符長度的字符串
  • LPAD(str,n,pad) 和 RPAD(str,n,pad) 函數:用字符串 pad 對 str 左邊和右邊進行填充,直到長度為 n 個字符長度
  • STRCMP(s1,s2) 用於比較字符串 s1 和 s2 的 ASCII 值大小。如果 s1 < s2,則返回 -1;如果 s1 = s2 ,返回 0 ;如果 s1 > s2 ,返回 1。
  • REPLACE(str,a,b) : 用字符串 b 替換字符串 str 種所有出現的字符串 a

數值函數

MySQL 支持數值函數,這些函數能夠處理很多數值運算。下面我們一起來學習一下 MySQL 中的數值函數,下面是所有的數值函數

函數 功能
ABS 返回絕對值
CEIL 返回大於某個值的最大整數值
MOD 返回模
ROUND 四舍五入
FLOOR 返回小於某個值的最大整數值
TRUNCATE 返回数字截斷小數的結果
RAND 返回 0 – 1 的隨機值

下面我們還是以實踐為主來聊一聊這些用法

  • ABS(x) 函數:返回 x 的絕對值
  • CEIL(x) 函數: 返回大於 x 的整數
  • MOD(x,y),對 x 和 y 進行取模操作
  • ROUND(x,y) 返回 x 四舍五入后保留 y 位小數的值;如果是整數,那麼 y 位就是 0 ;如果不指定 y ,那麼 y 默認也是 0 。
  • FLOOR(x) : 返回小於 x 的最大整數,用法與 CEIL 相反
  • TRUNCATE(x,y): 返回数字 x 截斷為 y 位小數的結果, TRUNCATE 知識截斷,並不是四舍五入。
  • RAND() :返回 0 到 1 的隨機值

日期和時間函數

日期和時間函數也是 MySQL 中非常重要的一部分,下面我們就來一起認識一下這些函數

函數 功能
NOW 返回當前的日期和時間
WEEK 返回一年中的第幾周
YEAR 返回日期的年份
HOUR 返回小時值
MINUTE 返回分鐘值
MONTHNAME 返回月份名
CURDATE 返回當前日期
CURTIME 返回當前時間
UNIX_TIMESTAMP 返回日期 UNIX 時間戳
DATE_FORMAT 返回按照字符串格式化的日期
FROM_UNIXTIME 返回 UNIX 時間戳的日期值
DATE_ADD 返回日期時間 + 上一個時間間隔
DATEDIFF 返回起始時間和結束時間之間的天數

下面結合示例來講解一下每個函數的使用

  • NOW(): 返回當前的日期和時間
  • WEEK(DATE) 和 YEAR(DATE) :前者返回的是一年中的第幾周,後者返回的是給定日期的哪一年
  • HOUR(time) 和 MINUTE(time) : 返回給定時間的小時,後者返回給定時間的分鐘
  • MONTHNAME(date) 函數:返回 date 的英文月份
  • CURDATE() 函數:返回當前日期,只包含年月日
  • CURTIME() 函數:返回當前時間,只包含時分秒
  • UNIX_TIMESTAMP(date) : 返回 UNIX 的時間戳
  • FROM_UNIXTIME(date) : 返回 UNIXTIME 時間戳的日期值,和 UNIX_TIMESTAMP 相反
  • DATE_FORMAT(date,fmt) 函數:按照字符串 fmt 對 date 進行格式化,格式化后按照指定日期格式显示

具體的日期格式可以參考這篇文章 https://blog.csdn.net/weixin_38703170/article/details/82177837

我們演示一下將當前日期显示為年月日的這種形式,使用的日期格式是 %M %D %Y

  • DATE_ADD(date, interval, expr type) 函數:返回與所給日期 date 相差 interval 時間段的日期

interval 表示間隔類型的關鍵字,expr 是表達式,這個表達式對應後面的類型,type 是間隔類型,MySQL 提供了 13 種時間間隔類型

表達式類型 描述 格式
YEAR YY
MONTH MM
DAY DD
HOUR 小時 hh
MINUTE mm
SECOND ss
YEAR_MONTH 年和月 YY-MM
DAY_HOUR 日和小時 DD hh
DAY_MINUTE 日和分鐘 DD hh : mm
DAY_SECOND 日和秒 DD hh :mm :ss
HOUR_MINUTE 小時和分 hh:mm
HOUR_SECOND 小時和秒 hh:ss
MINUTE_SECOND 分鐘和秒 mm:ss
  • DATE_DIFF(date1, date2) 用來計算兩個日期之間相差的天數

查看離 2021 – 01 – 01 還有多少天

流程函數

流程函數也是很常用的一類函數,用戶可以使用這類函數在 SQL 中實現條件選擇。這樣做能夠提高查詢效率。下錶列出了這些流程函數

函數 功能
IF(value,t f) 如果 value 是真,返回 t;否則返回 f
IFNULL(value1,value2) 如果 value1 不為 NULL,返回 value1,否則返回 value2。
CASE WHEN[value1] THEN[result1] …ELSE[default] END 如果 value1 是真,返回 result1,否則返回 default
CASE[expr] WHEN[value1] THEN [result1]… ELSE[default] END 如果 expr 等於 value1, 返回 result1, 否則返回 default

其他函數

除了我們介紹過的字符串函數、日期和時間函數、流程函數,還有一些函數並不屬於上面三類函數,它們是

函數 功能
VERSION 返回當前數據庫的版本
DATABASE 返回當前數據庫名
USER 返回當前登陸用戶名
PASSWORD 返回字符串的加密版本
MD5 返回 MD5 值
INET_ATON(IP) 返回 IP 地址的數字錶示
INET_NTOA(num) 返回数字代表的 IP 地址

下面來看一下具體的使用

  • VERSION: 返回當前數據庫版本
  • DATABASE: 返回當前的數據庫名
  • USER : 返回當前登錄用戶名
  • PASSWORD(str) : 返回字符串的加密版本,例如
  • MD5(str) 函數:返回字符串 str 的 MD5 值
  • INET_ATON(IP): 返回 IP 的網絡字節序列
  • INET_NTOA(num)函數:返回網絡字節序列代表的 IP 地址,與 INET_ATON 相對

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

到“十二五末”北汽新能源車產能將達15萬輛

北京汽車集團有限公司副總經理兼北汽股份有限公司總裁韓永貴稱,到“十二五”末,北京將形成15萬輛的生產能力,18萬套控制系統和15萬套電機驅動系統的零部件產能,總的營業收入將超過150億元。

北汽集團已開發出迷迪純電動車、E150EV純電動小型轎車、威望306EV純電動微客、C70GB中高端純電動轎車等乘用車產品,從兩噸到十六噸的純電動卡車,純電動及混合動力LNG客車等節能與新能源的產品,形成了覆蓋乘用車、卡車、客車領域十個平臺,十八個產品系列,四十餘款新能源汽車車型。

北汽集團累計在全國銷售及投入示範運營的純電動車、混合動力車、LNG、CNG等新能源汽車已超過七千輛,涵蓋了公車、環衛車、公務車、計程車、治安巡邏車、物流車等多個領域。120輛北汽純電動計程車在北京市密雲、平谷、懷柔三個區縣投入運營,日均行使里程在120公里左右,最高可達200公里,助力北京成為首個實現十城千輛發展目標的城市。

韓永貴指出,北汽集團下一步著力推動新能源車的產業化,特別是面向私人用車市場。北汽集團已經和國家電網、中石化等簽署了戰略合作協定,在采育新能源區已經建立了36個電裝充電站,給純電動車用戶提供便利條件。

除了基礎設施外,北汽集團還注重新能源汽車的售前售後服務。比如,新能源汽車體驗中心、多媒體展示中心、車輛展示中心、試車跑道等吸引用戶試乘試駕;組建了遠端、監控系統對北汽生產的所有電動車進行適時監控,還組建了專業的售後服務隊進行24小時保姆式的服務。

韓永貴透露,北汽集團正在組織一個純電動全新平臺車型的開發,該平臺與以往對傳統車的改進不同,是一款完全基於新平臺基礎開發的新能源汽車,將很快推出。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

全球首款量產版太陽能電動敞篷跑車亮相多哈

Qatar可持續發展博覽會(QSE)日前在多哈展覽中心舉行,由Qatar太陽能技術公司(QSTec)和Solarworld聯合開發的全球首款量產版電動敞篷跑車,Tesla Roadster正式首次在中東亮相。

該款跑車每次充電可以行駛300公里,0到100公里加速時間僅需3.6秒,動力來自太陽能電池,使其在行駛過程中二氧化碳排放量為零。 Tesla的太陽能電池由SolarWorld研發SunCarport,跟傳統的電動車相比,SunCarport能夠為電動車提供媲美超級跑車的性能而尾氣排放量為零。

QSTec CEO Dr Khalid Klefeekh Al Hajri表示,清潔能源在可持續發展中扮演著越來越重要的角色,Tesla打破了對於電動車性能不足的固有觀念,類似於SunCarport這樣的新技術正在提升新能源的開發潛力。

source:peninsula

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

第九届北京国际纯电动车、混合动力车暨新能源汽车及配套设施展览会

主办单位:中国汽车工程学会电动汽车分会、中国电工技术学会电动车辆专业委员会
承办单位:北京泽安达展览有限公司
支持单位: 北京市人民政府

2013第九届北京国际纯电动车、混合动力车暨新能源汽车及配套设施展览会”已历经8载,是国内最早的已具规模的专业的电动汽车行业年度盛会。将于2013年7月6日-8日在北京-中国国际展览中心(老馆)继续举办。
上届(第八届)展会于2012年7月3日—5日在北京-中国国际展览中心(老馆)取得圆满成功;国内外包括北汽、长安、一汽、长城、时风、益高、韩国LG、美国Remy、日本安川、天能、超威、ITT、英飞凌、中航锂电、玉柴、国电、中国兵器、珠海泰坦、浙江万马等260家整车、零部件、充电站设施及相关企业参展、展出面积13000平方米、专业观众25000多人次。

本届展会预计展出四个馆、20000平方米;参展企业预计300家:专业观众30000人次。展会活动形式多样,主要包括展览会开幕式、论坛、主题会议、颁奖晚宴、热点问题沙龙、电动汽车产业发展研讨会、电动车辆学术年会、电动汽车电池电机专题研讨、电动汽车充电站建设技术设施专题展、节能与新能源车示范推广城市政府职能部门和公交公司领导与整车企业互动等。活动将邀请科技部、发改委、工信部等有关政府部门主管领导和欧洲、美洲、亚太等地区及国家的行业协会组织代表出席。

地 址:北京市东城区安定门外大街208号三利大厦7017室
电 话:86-10-51236248
传 真:86-10-51236249
网 址:www.ievechina.com
邮 箱:bj_evs@sina.com
联系人:刘杰

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

2013自行車展-自行車輛更來電

2013自行車展盛大展出,根據集邦科技旗下綠能事業部EnergyTrend的現場了解,除了汽車廠豐田汽車提出了鋰電池在電動載具的應用看法外,自行車方面,包括了變速器廠以及電池廠也紛紛對於電動自行車提出各自的最新的商品展示,無論是零組件廠以及整車廠,都讓參觀者對於車輛電動化的延伸發展,產生的極大期待,讓電動自行車從過去的代步、健身兼具延伸續航距離,發展到未來將整合隨身電子裝置,讓人們對電動車量的普及充滿樂觀。

豐田汽車鋰電池電動車開賣

雖然全球電動汽車銷售市況不如預期,以Nissan leaf來看,銷量逐步好轉,目前在日本每月銷售約800輛、北美則有1,600輛,截至目前累計約兩萬多輛。根據自行車研討會的演說分享,豐田汽車所寄予厚望的重量級車款Prius α 預計在2013年底上市(全球),並且為首度配置的鋰電池中階車款,電池位置也因為體積輕巧而配置於車室內。豐田目前使用NCA材料做為正極材料,電池芯單元為3.7V, 5Ah,總電池容量約4.4kWh,2014年將推出新款車款IQ,12kWh,預計仍是使用NCA材料(圖一)。
 
圖一 豐田汽車

 
   
變速器、系統整合、電池模組熱鬧參與

作為自行車變速系統龍頭大廠,Shimano也將電子化與變速系統做了創新結合。在Shimano E tube計畫裡頭,將以變速系統Di2做為變速系統電子化為主要訴求,初期將落實在維修體系以及變速資訊的便利性上,未來更有可能藉著電池系統的搭配,提供車主更多的資訊來源(圖二),進而成為讓消費者改變使用習慣的革命。
 
圖二 Shimano E tube應用展示

 
三星SDI做為目前全球第二大的電池芯廠(僅次於合併後的Sanyo + Panasonic),除了在消費型應用的鋰電池產業著墨甚多外,近年也積極切入電動工具以及電動自行車等利基市場,更從過去的電池芯供應,逐步跨足到電池模組(圖三)。

以電動自行車來看,此次展出全方位的自行車電池,可提供後架、坐管、下管等不同位置的應用模組。目前全球各地電動自行車因使用族群不同而衍生出不同擺放位置的設計,中國市場大多以坐管以及後架位置為主要設計,主要訴求在荷重與價格;歐洲則集中在下管及座管位置,用於強調於設計外觀一體性,根據會場的展示DM,目前三星主推2.1Ah以及2.8Ah兩顆自行車專用三元材料電池芯,最大放電率可達到3C(C-rate),已可滿足各種路況的電動自行車的使用需求。
 
圖三 三星自行車電池解決方案展示


   
中華車在配合工業局補助政策,全力推廣電動機車銷售,並且在2012年繳出了六千輛的銷售佳績,未來也將從整車品牌退居幕後,藉著曾經扮演整車零組件整合的豐富經驗,轉型成為關鍵零組件提供者,本次特別展示了關鍵零組件品牌GreenTrans,提供中華車的Power Kit(圖四),包含了扭力感測器、LCD顯示、以及馬達驅動系統,2012年起已與台灣多家自行車廠合作,包括永祺、世同、達生、紹凱、吉安等,都採用中華的Power Kit系統組裝成車。

圖四 中華電動車套件供應分類展示


   
台灣電池模組大廠新普科技也展示了已耕耘三年的電動自行車商品,本次特別針對歐洲市場,與美國品牌Specialize、法國品牌BTwin進行合作(圖五),分別展出下管以及後座電池組產品。

與Specialize合作的登山車,下管產品特色在於電池與車架進行整合,呈現高度客製化,與客戶的關係也將因此而更形緊密。另外BTwin所推出的tilt折疊車系列,目前定位在堅固耐用,為了維持車體的強度,電池模組僅在延伸支架上做結合。新普科技憑藉著對於電池芯的品質掌握度,再加上電源管理的豐富經驗,相信未來在電動自行車市場後市可期。

圖五 新普科技電池模組展示

 
   
另一家作風一向低調的台灣電池模組大廠順達科技也積極跨足各種電池類型應用,於2012年與台達電合作的備用電源產品首次發表即得到設計大獎的殊榮外,此次自行車展也不遑多讓,搭配合作的車廠於現場展出了電動自行車模組的電池模組應用,與中國第一大馬達與控制器廠安乃達做配合,搭配了恪萊博(climbull)各系列整車系統(圖六),這樣的策略合作與全系列搭配的組合,也產生令人期待的商品。

恪萊博的產品策略主打戶外運動客層,屬於高階客群,有別於一般通勤族,由於戶外運動屬於白領階級活動,產品單價較高,且一般通勤族需要載重負荷,容易導致產品壽命偏低,對品牌形象反而產生負面影響,因而改以戶外運動客層為主要訴求對象為主,從應用定位面來扭轉可能造成的問題。而整車產品賣點在於搭配新型中置高速馬達,結合其馬達減速系統,可與外變速系統做最有效益的齒輪比搭配,進而提升產品的壽命與動力表現。

圖六 順達科技電池模組展示

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

Swagger之外的選擇

今天給大家安利一款接口文檔生成器——JApiDocs。

swagger想必大家都用過吧,非常方便,功能也十分強大。如果要說swaager有什麼缺點,想必就是註解寫起來比較麻煩。如果我說有一款不用寫註解,就可以生成文檔的工具,你心動了嗎?他就是我們今天的主角——JApiDocs。

下面我們一起來看看如何使用!

一、添加依賴

<dependency>
  <groupId>io.github.yedaxia</groupId>
  <artifactId>japidocs</artifactId>
  <version>1.3</version>
</dependency>

二、配置生成參數

我們新建一個項目,然後隨便寫一個main方法,增加生成文檔的配置,然後運行main方法。

DocsConfig config = new DocsConfig();
config.setProjectPath("F:\\Java旅途\\japi-docs"); // 項目根目錄
config.setProjectName("japi-docs"); // 項目名稱
config.setApiVersion("V1.0");       // 聲明該API的版本
config.setDocsPath("F:\\test"); // 生成API 文檔所在目錄
config.setAutoGenerate(Boolean.TRUE);  // 配置自動生成
Docs.buildHtmlDocs(config); // 執行生成文檔

三、編碼規範

由於JApiDocs是通過解析Java源碼來實現的,因此如果要想實現想要的文檔,還是需要遵循一定的規範。

3.1 類註釋、方法註釋和屬性註釋

如果我們想生成類的註釋,我們可以直接在類上加註釋,也可以通過加@description來生成。

/**
 * 用戶接口類
 */
@RequestMapping("/api/user")
@RestController
public class UserController {}

/**
 * @author Java旅途
 * @Description 用戶接口類
 * @Date 2020-06-15 21:46
 */
@RequestMapping("/api/user")
@RestController
public class UserController {}

如果我們想生成方法的註釋,只能直接加註釋,不能通過加@description來生成。

/**
 * 查詢用戶
 * @param age 年齡
 * @return R<User>
*/
@GetMapping("/list")
public R<User> list(@RequestParam int age){

    User user = new User("Java旅途", 18);
    return R.ok(user);
}

JApiDocs可以自動生成實體類,關於實體類屬性的註釋有三種方式,生成的效果都是一樣的,如下:

/**
 * 用戶名稱
 */
private String name;
/**
 * 用戶年齡
 */
private int age;
// 用戶名稱
private String name;
// 用戶年齡
private int age;
private String name;// 用戶名稱
private int age;// 用戶年齡

他除了支持咱們常用的model外,還支持IOS的model生成效果如下:

3.2 請求參數

如果提交的表單是 application/x-www-form-urlencoded 類型的key/value格式,則我們通過@param註解來獲取參數,在參數後面添加註釋,示例如下:

/**
  * @param age 年齡
  */
@GetMapping("/list")
public R<User> list(@RequestParam int age){
    User user = new User("Java旅途", 18);
    return R.ok(user);
}

生成的文檔效果如下:

請求參數

參數名 類型 必須 描述
age int 年齡

如果提交的表單是 application/json 類型的json數據格式,如下:

/**
  * @param user
  * @return
  */
@PostMapping("/add")
public R<User> add(@RequestBody User user){
    return R.ok(user);
}

生成的文檔效果如下:

請求參數

{
  "name": "string //用戶名稱",
  "age": "int //用戶年齡"
}

3.3 響應結果

我們知道,如果Controller聲明了@RestController,SpringBoot會把返回的對象直接序列成Json數據格式返回給前端。 JApiDocs也利用了這一特性來解析接口返回的結果,但由於JApiDocs是靜態解析源碼的,因此你要明確指出返回對象的類型信息,JApiDocs支持繼承、泛型、循環嵌套等複雜的類解析。

因此我們不需要再寫註釋,它會根據我們的返回結果進行解析,效果如下:

返回結果:

{
  "code": "int",
  "msg": "string",
  "data": {
    "name": "string //用戶名稱",
    "age": "int //用戶年齡"
  }
}

最終,我們生成的接口文檔,如下:

四、高級配置

4.1 @ApiDoc

如果你不希望把所有的接口都導出,我們可以在配置中設置config.setAutoGenerate(Boolean.FALSE);然後再想要生成的接口上添加@ApiDoc。

@ApiDoc有以下三個屬性:

  • result: 這個可以直接聲明返回的對象類型,如果你聲明了,將會覆蓋SpringBoot的返回對象
  • url: 請求URL,擴展字段,用於支持非SpringBoot項目
  • method: 請求方法,擴展字段,用於支持非SpringBoot項目
@ApiDoc(result = User.class, url = "/api/user/view", method = "post")

4.2 @Ignore

如果你不想導出對象裏面的某個字段,可以給這個字段加上@Ignore註解,這樣JApiDocs導出文檔的時候就會自動忽略掉了。

public class User {
    @Ignore
    private int age;
}

五、總結

JApiDocs就介紹到這裏了,優勢劣勢大家很容易就看出來了。幾乎不需要註釋即可生成接口文檔,僅有的幾個註釋我們也可以通過ide來自動生成。但是JApiDocs不具備swagger在線調試功能。如果有一天JApiDocs支持在線調試后,那時候肯定會有一大波追隨者,畢竟寫代碼的誰喜歡寫多餘的註解!~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

十萬同時在線用戶,需要多少內存?——Newbe.Claptrap 框架水平擴展實驗

Newbe.Claptrap 項目是筆者正在構建以反應式Actor模式事件溯源為理論基礎的一套服務端開發框架。本篇我們將來了解一下框架在水平擴展方面的能力。

前情提要

時隔許久,今日我們再次見面。首先介紹一下過往的項目情況:

第一次接觸本框架的讀者,可以先點擊此處閱讀本框架相關的基礎理論和工作原理。

日前,我們也編寫了一些預熱文章和工具,讀者可以通過以下鏈接進行了解:

  • 談反應式編程在服務端中的應用,數據庫操作優化,從 20 秒到 0.5 秒
  • docker-mcr 助您全速下載 dotnet 鏡像
  • Newbe.Claptrap 項目周報 1 – 還沒輪影,先用輪跑

今日主題

今天,我們來做一套實驗預演,來驗證 Newbe.Claptrap 框架,如何通過水平擴展的形式來適應逐漸增長的同時在線用戶數。

由於此次實驗涉及的內容很多,因此筆者將內容進行了歸類,讀者可以按照自己的興趣閱讀相關的章節:

  • 業務需求說明
  • 調用時序關係
  • 物理結構設計
  • 實際測試數據
  • 源碼構建說明
  • 常見問題解答

業務需求說明

先看看今天要實現的業務場景:

  • 用戶通過 API 登錄後生成一個 JWT token
  • 用戶調用 API 時驗證 JWT token 的有效性
  • 沒有使用常規的 JWS 公私鑰方式進行 JWT token 頒發,而是為每個用戶單獨使用 secret 進行哈希驗證
  • 驗證看不同的在線用戶需要消耗的內存情況
  • 用戶登錄到生成 token 所消耗時間不得超過 200 ms
  • tokn 的驗證耗時不得超過 10 ms

吹牛先打草稿

筆者沒有搜索到於 “在線用戶數” 直接相關的理論定義,因此,為了避免各位的理解存在差異。筆者先按照自己的理解來點明:在線用戶數到底意味着什麼樣的技術要求?

未在線用戶若上線,不應該受到已在線用戶數的影響

如果一個用戶登錄上線需要消耗 100 ms。那麼不論當前在線的用戶數是十人還是百萬人。這個登錄上線所消耗的時間都不會明顯的超過 100 ms。

當然,有限的物理硬件肯定會使得,當在線用戶數超過一個閾值(例如兩百萬)時,新用戶登錄上線會變慢甚至出錯。

但是,增加物理機器就能提高這個閾值,我們就可以認為水平擴展設計是成功的。

對於任意一個已在線用戶,得到的系統性能反饋應當相同

例如已在線的用戶查詢自己的訂單詳情,需要消耗 100 ms。那麼當前任何一個用戶進行訂單查詢的平均消耗都應該穩定在 100 ms。

當然,這裏需要排除類似於 “搶購” 這種高集中性能問題。此處主要還是討論日常穩定的容量增加。(我們以後會另外討論 “搶購” 這種問題)

具體一點可以這樣理解。假設我們做的是一個雲筆記產品。

那麼,如果增加物理機器就能增加同時使用雲筆記產品的用戶數,而且不犧牲任何一個用戶的性能體驗,我們就認為水平擴展設計是成功的。

在此次的實驗中,若用戶已經登錄,則驗證 JWT 有效性的時長大約為 0.5 ms。

調用時序關係

簡要說明:

  1. 客戶端發起登錄請求將會逐層傳達到 UserGrain 中
  2. UserGrain 將會在內部激活一個 Claptrap 來進行維持 UserGrain 中的狀態數據。包括用戶名、密碼和用於 JWT 簽名的 Secret。
  3. 隨後的生成 JWT 生成和驗證都將直接使用 UserGrain 中的數據。由於 UserGrain 中的數據是在一段時間內是 “緩存” 在內存中的。所以之後的 JWT 生成和驗證將非常快速。實測約為 0.5 ms。

物理結構設計

如上圖所示,便是此次進行測試的物理組件:

名稱 說明
WebAPI 公開給外部調用 WebAPI 接口。提供登錄和驗證 token 的接口。
Orleans Cluster 託管 Grain 的核心進程.
Orleans Gateway 於 Orleans Cluster 基本相同,但是 WebAPI 只能與 Gateway 進行通信
Orleans Dashboard 於 Orleans Gateway 基本相同,但增加了 Dashboard 的展示,以查看整個 Orleans 集群的情況
Consul 用於 Orleans 集群的集群發現和維護
Claptrap DB 用於保存 Newbe.Claptrap 框架的事件和狀態數據
Influx DB & Grafana 用於監控 Newbe.Claptrap 相關的性能指標數據

此次實驗的 Orleans 集群節點的數量實際上是 Cluster + Gateway + Dashboard 的總數。以上的劃分實際上是由於功能設定的不同而進行的區分。

此次測試 “水平擴展” 特性的物理節點主要是 Orleans Cluster 和 Orleans Gateway 兩個部分。將會分別測試以下這些情況的內存使用情況。

Orleans Dashboard Orleans Gateway Orleans Cluster
1 0 0
1 1 1
1 3 5

此次實驗採用的是 Windows Docker Desktop 結合 WSL 2 進行的部署測試。

以上的物理結構實際上是按照最為此次實驗最為複雜的情況設計的。實際上,如果業務場景足夠簡單,該物理結構可以進行裁剪。詳細可以查看下文 “常見問題解答” 中的說明。

實際測試數據

以下,分別對不同的集群規模和用戶數量進行測試

0 Gateway 0 Cluster

默認情況下,剛剛啟動 Dashboard 節點時,通過 portainer 可以查看 container 佔用的內存約為 200 MB 左右,如下圖所示:

通過測試控制台,向 WebAPI 發出 30,000 次請求。每批 100 個請求,分批發送。

經過約兩分鐘的等待后,再次查看內存情況,約為 9.2 GB,如下圖所示:

因此,我們簡單的估算每個在線用戶需要消耗的內存情況約為 (9.2*1024-200)/30000 = 0.3 MB。

另外,可以查看一些輔助數據:

CPU 使用情況

網絡吞吐量

Orleans Dashboard 情況。左上角的 TOTAL ACTIVATIONS 中 30,000 即表示當前內存中存在的 UserGrain 數量,另外的 3 個為 Dashboard 使用的 Grain。

Grafana 中查看 Newbe.Claptrap 的事件平均處理時長約為 100-600 ms。此次測試的主要是內存情況,處理時長的採集時間為 30s 一次,因此樣本數並不多。關於處理時長我們將在後續的文章中進行詳細測試。

Grafana 中查看 Newbe.Claptrap 的事件的保存花費的平均時長約為 50-200 ms。事件的保存時長是事件處理的主要部分。

Grafana 中查看 Newbe.Claptrap 的事件已處理總數。一種登錄了三萬次,因此事件總數也是三萬。

1 Gateway 1 Cluster

接下來,我們測試額外增加兩個節點進行測試。

還是再提一下,Orleans 集群節點的數量實際上是 Cluster + Gateway + Dashboard 的總數。因此,對比上一個測試,該測試的節點數為 3。

測試得到的內存使用情況如下:

用戶數 節點平均內存 內存總佔用
10000 1.8 GB 1.8*3 = 5.4 GB
20000 3.3 GB 3.3*3 = 9.9 GB
30000 4.9 GB 4.9*3 = 14.7 GB

那麼,以三萬用戶為例,平均每個用戶佔用的內存約為 (14.7*1024-200*3)/30000 = 0.48 MB

為什麼節點數增加了,平均消耗內存上升了呢?筆者推測,沒有進行過驗證:節點增加,實際上節點之間的通訊還需要消耗額外的內存,因此平均來說有所增加。

3 Gateway 5 Cluster

我們再次增加節點。總結點數為 1 (dashboard) + 3 (cluster) + 5 (gateway) = 9 節點

測試得到的內存使用情況如下:

用戶數 節點平均內存 內存總佔用
20000 1.6 GB 3.3*9 = 14.4 GB
30000 2 GB 4.9*9 = 18 GB

那麼,以三萬用戶為例,平均每個用戶佔用的內存約為 (18*1024-200*9)/30000 = 0.55 MB

十萬用戶究竟要多少內存?

以上所有的測試都是以三萬為用戶數進行的測試,這是一個特殊的数字。因為繼續增加用戶數的話,內存將會超出測試機的內存余量。(求贊助兩條 16G)

如果繼續增加用戶數,將會開始使用操作系統的虛擬內存。雖然可以運行,但是運行效率會降低。原來登錄可能只需要 100 ms。使用到虛擬內存的用戶則需要 2 s。

因此,速度降低的情況下,在驗證需要多少內存意義可能不大。

但是,這不意味着不能夠繼續登錄,以下便是 1+1+1 的情況下,十萬用戶全部登錄后的情況。(有十萬用戶同時在線,加點內存吧,不差錢了。)

源碼構建說明

此次測試的代碼均可以在文末的樣例代碼庫中找到。為了方便讀者自行實驗,主要採用的是 docker-compose 進行構建和部署。

因此對於測試機的唯一環境需求就是要正確的安裝好 Docker Desktop 。

可以從以下任一地址獲取最新的樣例代碼:

  • https://github.com/newbe36524/Newbe.Claptrap.Examples
  • https://gitee.com/yks/Newbe.Claptrap.Examples

快速啟動

使用控制台進入 src/Newbe.Claptrap.Auth/LocalCluster 文件夾。運行以下命令便可以在本地啟動所有的組件:

1
docker-compose up -d

途中需要拉取一些託管於 Dockerhub 上的公共鏡像,請確保本地已經正確配置了相關的加速器,以便您可以快速構建。可以參看這篇文檔進行設置

成功啟動之後可以通過 docker ps 查看到所有的組件。

1
2
3
4
5
6
7
8
9
10
11
12
13
PS>docker ps
CONTAINER ID        IMAGE                                                                            COMMAND                  CREATED             STATUS              PORTS                                                                                                                              NAMES
66470e5393e2        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-webapi          "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    0.0.0.0:10080->80/tcp                                                                                                              localcluster_webapi_1
3bbaf5538ab9        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-backendserver   "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    80/tcp, 443/tcp, 0.0.0.0:19000->9000/tcp, 0.0.0.0:32785->11111/tcp, 0.0.0.0:32784->30000/tcp                                       localcluster_dashboard_1
3f60f51e4641        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-backendserver   "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    80/tcp, 443/tcp, 9000/tcp, 0.0.0.0:32787->11111/tcp, 0.0.0.0:32786->30000/tcp                                                      localcluster_cluster_gateway_1
7d516ada2b26        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-backendserver   "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    80/tcp, 443/tcp, 9000/tcp, 30000/tcp, 0.0.0.0:32788->11111/tcp                                                                     localcluster_cluster_core_1
fc89fcd973f9        grafana/grafana                                                                  "/run.sh"                4 hours ago         Up 6 seconds        0.0.0.0:23000->3000/tcp                                                                                                            localcluster_grafana_1
1f10ed0eb25f        postgres                                                                         "docker-entrypoint.s…"   4 hours ago         Up About an hour    0.0.0.0:32772->5432/tcp                                                                                                            localcluster_claptrap_db_1
d5d2bec74311        adminer                                                                          "entrypoint.sh docke…"   4 hours ago         Up About an hour    0.0.0.0:58080->8080/tcp                                                                                                            localcluster_adminer_1
4c4be69f2f41        bitnami/consul                                                                   "/opt/bitnami/script…"   4 hours ago         Up About an hour    8300-8301/tcp, 8500/tcp, 8301/udp, 8600/tcp, 8600/udp                                                                              localcluster_consulnode3_1
88811d3aa0d2        influxdb                                                                         "/entrypoint.sh infl…"   4 hours ago         Up 6 seconds        0.0.0.0:29086->8086/tcp                                                                                                            localcluster_influxdb_1
d31c73b62a47        bitnami/consul                                                                   "/opt/bitnami/script…"   4 hours ago         Up About an hour    8300-8301/tcp, 8500/tcp, 8301/udp, 8600/tcp, 8600/udp                                                                              localcluster_consulnode2_1
72d4273eba2c        bitnami/consul                                                                   "/opt/bitnami/script…"   4 hours ago         Up About an hour    0.0.0.0:8300-8301->8300-8301/tcp, 0.0.0.0:8500->8500/tcp, 0.0.0.0:8301->8301/udp, 0.0.0.0:8600->8600/tcp, 0.0.0.0:8600->8600/udp   localcluster_consulnode1_1

啟動完成之後,便可以通過以下鏈接來查看相關的界面

地址 說明
http://localhost:19000 Orleans Dashboard 查看 Orleans 集群中各節點的狀態
http://localhost:10080 Web API 基地址,此次使用所測試的 API 基地址
http://localhost:23000 Grafana 地址,查看 Newbe.Claptrap 相關的性能指標情況

源碼構建

使用控制台進入 src/Newbe.Claptrap.Auth 文件夾。運行以下命令便可以在本地完成代碼的構建:

1
2
./LocalCluster/pullimage.cmd
docker-compose build

pullimage.cmd 使用了筆者編寫的 docker-mcr 加速器功能。您可以通過該文檔來了解其工作原理

等待構建完畢之後,本地便生成好了相關的鏡像。接下來便可以初次嘗試在本地啟動應用:

使用控制台進入 src/Newbe.Claptrap.Auth/LocalCluster 文件夾。運行以下命令便可以啟動相關的容器:

1
docker-compose up -d

常見問題解答

文中為何沒有說明代碼和配置的細節?

本文主要為讀者展示該方案的實驗可行性,具體應該如何應用 Newbe.Claptrap 框架編寫代碼,並非本文的主旨,因此沒有提及。

當然,另外一點就是目前框架沒有最終定版,所有內容都有可能發生變化,講解代碼細節意義不大。

但可以提前說明的是:編寫非常簡單,由於本樣例的業務需求非常簡單,因此代碼內容也不多。全部都可以在示例倉庫中找到。

用 Redis 存儲 Token 也可以實現上面的需求,為什麼要選擇這個框架?

目前來說,筆者沒有十足的理由說服讀者必須使用哪種方案,此處也只是提供一種可行方案,至於實際應該選擇哪種方案,應該有讀者自己來考量,畢竟工具是否趁手還是需要試試才知道。

如果是最多 100 個在線用戶,那怎麼裁剪系統?

必要的組件只有 Orleans Dashboard 、 WebAPI 和 Claptrap Db。其他的組件全部都是非必要的。而且如果修改代碼, Orleans Dashboard 和 WebAPI 是可以合併的。

所以最小規模就是一個進程加一個數據庫。

Grafana 為什麼沒有報表?

Grafana 首次啟動之後需要手動的創建 DataSource 和導入 Dashboard.

本實驗相關的參數如下:

DataSource

  • URL: http://influxdb:8086
  • Database: metricsdatabase
  • User: claptrap
  • Password: claptrap

點擊此處獲取 Dashboard 定義文件

測試機的物理配置是什麼?

沒有專門騰內存,未開始測試前已佔用 16GB 內存。以下是測試機的身材數據(洋垃圾,3500 元左右):

處理器 英特爾 Xeon (至強) E5-2678 v3 @ 2.50GHz 12 核 24 線程
主板 HUANANZHI X99-AD3 GAMING (Wellsburg)
顯卡 Nvidia GeForce GTX 750 Ti (2 GB / Nvidia)
內存 32 GB (三星 DDR3L 1600MHz) 2013 年產 高齡內存
主硬盤 金士頓 SA400S37240G (240 GB / 固態硬盤)

如果您有更好的物理配置,相信可以得出更加優秀的數據。

即使是 0.3 MB 平均每用戶的佔用的我也覺得太高了

框架還在優化。未來會更好。

最後但是最重要!

最近作者正在構建以反應式Actor模式事件溯源為理論基礎的一套服務端開發框架。希望為開發者提供能夠便於開發出 “分佈式”、“可水平擴展”、“可測試性高” 的應用系統 ——Newbe.Claptrap

本篇文章是該框架的一篇技術選文,屬於技術構成的一部分。如果讀者對該內容感興趣,歡迎轉發、評論、收藏文章以及項目。您的支持是促進項目成功的關鍵。

GitHub 項目地址:https://github.com/newbe36524/Newbe.Claptrap

Gitee 項目地址:https://gitee.com/yks/Newbe.Claptrap

如果你對該項目感興趣,你可以通過 github issues 提交您的看法。

如果您無法正常訪問 github issue,您也可以發送郵件到 newbe-claptrap@googlegroups.com 來參与我們的討論。

點擊鏈接 QQ 交流【Newbe.Claptrap】:https://jq.qq.com/?_wv=1027&k=5uJGXf5。

​​​​​​​

    • 本文作者: newbe36524
    • 本文鏈接: https://www.newbe.pro/Newbe.Claptrap/How-Many-RAMs-In-Used-While-There-Are-One-Hundred-Thousand-Users-Online/
    • 版權聲明: 本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

Spring Boot入門系列(十五)Spring Boot 開發環境熱部署

在實際的項目開發過中,當我們修改了某個java類文件時,需要手動重新編譯、然後重新啟動程序的,整個過程比較麻煩,特別是項目啟動慢的時候,更是影響開發效率。其實Spring Boot的項目碰到這種情況,同樣也同樣需要經歷重新編譯、重新啟動程序的過程。 只不過 Spring Boot 提供了一個spring-boot-devtools的模塊,使得 Spring Boot應用支持熱部署,無需手動重啟Spring Boot應用,,提高開發者的開發效率。接下來,聊一聊Spring Boot 開發環境熱部署。

 

一、原理

devtools 使用了兩個類加載器(ClassLoader),一個是 Base類加載器(base classloader ):加載那些不會改變的類,如:第三方Jar包等,而另一個是 Restart類加載器(restart classloader):負責加載那些正在開發的會改變的類。這樣在有代碼更改的時候,因為重啟的時候只是加載了在開發的Class類,沒有重新加載第三方的jar包,所以實現了較快的重啟時間。

devtools 監聽classpath下的文件變動(發生在保存時機),並且會立即重啟應用。從而實現類文件和屬性文件的熱部署。

 

二、快速配置

1、pom配置

引入devtools的依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <!-- optional=true, 依賴不會傳遞, 該項目依賴devtools;之後依賴boot項目的項目如果想要使用devtools, 需要重新引入 -->
    <optional>true</optional>
</dependency>

注意:optional=true, 依賴不會傳遞, 該項目依賴devtools;之後依賴boot項目的項目如果想要使用devtools, 需要重新引入。

 

2、application.properties配置

在application.properties中配置devtools。

# 關閉緩存即時刷新
#spring.thymeleaf.cache=false

#熱部署生效
spring.devtools.restart.enabled=true
#設置重啟的目錄
spring.devtools.restart.additional-paths=src/main/java
#classpath目錄下的WEB-INF文件夾內容修改不重啟
spring.devtools.restart.exclude=WEB-INF/**

說明:

devtools可以實現頁面熱部署,即頁面修改後會立即生效,需要將application.properties文件中配置spring.thymeleaf.cache=false。

devtools會監聽classpath下的文件變動,並且會立即重啟應用。

 

3、IDEA配置

如果idea是新安裝的或者之前就沒有配置過,發現改變代碼項目熱部署不成功。當我們修改了Java類后,IDEA默認是不自動編譯的,而spring-boot-devtools又是監測classpath下的文件發生變化才會重啟應用。

所以需要設置IDEA的自動編譯:

(1)File-Settings-Compiler-Build Project automatically

(2)ctrl + shift + alt + /,選擇Registry,勾上 Compiler autoMake allow when app running 

這樣,就可以使用devtools實現熱部署了。

 

最後

以上,就把如何配置Spring Boot 開發環境熱部署介紹完了。還是比較簡單的,大家自己去研究吧。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

手摸手帶你理解Vue響應式原理

前言

響應式原理作為 Vue 的核心,使用數據劫持實現數據驅動視圖。在面試中是經常考查的知識點,也是面試加分項。

本文將會循序漸進的解析響應式原理的工作流程,主要以下面結構進行:

  1. 分析主要成員,了解它們有助於理解流程
  2. 將流程拆分,理解其中的作用
  3. 結合以上的點,理解整體流程

文章稍長,但部分是代碼,還請耐心觀看。為了方便理解原理,文中的代碼會進行簡化,如果可以請對照源碼學習。

主要成員

在響應式原理中,ObserveDepWatcher 這三個類是構成完整原理的主要成員。

  • Observe,響應式原理的入口,根據數據類型處理觀測邏輯
  • Dep,依賴收集器,屬性都會有一個Dep,方便發生變化時能夠找到對應的依賴觸發更新
  • Watcher,用於執行更新渲染,組件會擁有一個渲染Watcher,我們常說的收集依賴,就是收集 Watcher

下面來看看這些類的實現,包含哪些主要屬性和方法。

Observe:我會對數據進行觀測

溫馨提示:代碼里的序號對應代碼塊下面序號的講解

// 源碼位置:/src/core/observer/index.js
class Observe {
  constructor(data) {
    this.dep = new Dep()
    // 1
    def(data, '__ob__', this)
    if (Array.isArray(data)) {
      // 2
      protoAugment(data, arrayMethods)
      // 3
      this.observeArray(data)
    } else {
      // 4
      this.walk(data)
    }
  }
  walk(data) {
    Object.keys(data).forEach(key => {
      defineReactive(data, key, data[key])
    })
  }
  observeArray(data) {
    data.forEach(item => {
      observe(item)
    })
  }
}
  1. 為觀測的屬性添加 __ob__ 屬性,它的值等於 this,即當前 Observe 的實例
  2. 為數組添加重寫的數組方法,比如:pushunshiftsplice 等方法,重寫目的是在調用這些方法時,進行更新渲染
  3. 觀測數組內的數據,observe 內部會調用 new Observe,形成遞歸觀測
  4. 觀測對象數據,defineReactive 為數據定義 getset ,即數據劫持

Dep:我會為數據收集依賴

// 源碼位置:/src/core/observer/dep.js
let id = 0
class Dep{
  constructor() {
    this.id = ++id // dep 唯一標識
    this.subs = [] // 存儲 Watcher
  }
  // 1
  depend() {
    Dep.target.addDep(this)
  }
  // 2
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 3
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

// 4
Dep.target = null

export function pushTarget(watcher) {
  Dep.target = watcher
} 

export function popTarget(){
  Dep.target = null
}

export default Dep
  1. 數據收集依賴的主要方法,Dep.target 是一個 watcher 實例
  2. 添加 watcher 到數組中,也就是添加依賴
  3. 屬性在變化時會調用 notify 方法,通知每一個依賴進行更新
  4. Dep.target 用來記錄 watcher 實例,是全局唯一的,主要作用是為了在收集依賴的過程中找到相應的 watcher

pushTargetpopTarget 這兩個方法顯而易見是用來設置 Dep.target的。Dep.target 也是一個關鍵點,這個概念可能初次查看源碼會有些難以理解,在後面的流程中,會詳細講解它的作用,需要注意這部分的內容。

Watcher:我會觸發視圖更新

// 源碼位置:/src/core/observer/watcher.js
let id = 0
export class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.id = ++id  // watcher 唯一標識
    this.vm = vm
    this.cb = cb
    this.options = options
    // 1
    this.getter = exprOrFn
    this.deps = []
    this.depIds = new Set()

    this.get()
  }
  run() {
    this.get()
  }
  get() {
    pushTarget(this)
    this.getter()
    popTarget(this)
  }
  // 2
  addDep(dep) {
    // 防止重複添加 dep
    if (!this.depIds.has(dep.id)) {
      this.depIds.add(dep.id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
  // 3
  update() {
    queueWatcher(this)
  }
}
  1. this.getter 存儲的是更新視圖的函數
  2. watcher 存儲 dep,同時 dep 也存儲 watcher,進行雙向記錄
  3. 觸發更新,queueWatcher 是為了進行異步更新,異步更新會調用 run 方法進行更新頁面

響應式原理流程

對於以上這些成員具有的功能,我們都有大概的了解。下面結合它們,來看看這些功能是如何在響應式原理流程中工作的。

數據觀測

數據在初始化時會通過 observe 方法來調用 Observe

// 源碼位置:/src/core/observer/index.js
export function observe(data) {
  // 1
  if (!isObject(data)) {
    return
  }
  let ob;
  // 2
  if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observe) {
    ob = data.__ob__
  } else {
    // 3
    ob = new Observe(data)
  }
  return ob
}

在初始化時,observe 拿到的 data 就是我們在 data 函數內返回的對象。

  1. observe 函數只對 object 類型數據進行觀測
  2. 觀測過的數據都會被添加上 __ob__ 屬性,通過判斷該屬性是否存在,防止重複觀測
  3. 創建 Observe 實例,開始處理觀測邏輯

對象觀測

進入 Observe 內部,由於初始化的數據是一個對象,所以會調用 walk 方法:

walk(data) {
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key])
  })
}

defineReactive 方法內部使用 Object.defineProperty 對數據進行劫持,是實現響應式原理最核心的地方。

function defineReactive(obj, key, value) {
  // 1
  let childOb = observe(value)
  // 2
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        // 3
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newVal) {
      if (newVal === value) {
        return
      }
      value = newVal
      // 4
      childOb = observe(newVal)
      // 5
      dep.notify()
      return value
    }
  })
}
  1. 由於值可能是對象類型,這裏需要調用 observe 進行遞歸觀測
  2. 這裏的 dep 就是上面講到的每一個屬性都會有一個 dep,它是作為一個閉包的存在,負責收集依賴和通知更新
  3. 在初始化時,Dep.target 是組件的渲染 watcher,這裏 dep.depend 收集的依賴就是這個 watcherchildOb.dep.depend 主要是為數組收集依賴
  4. 設置的新值可能是對象類型,需要對新值進行觀測
  5. 值發生改變,dep.notify 通知 watcher 更新,這是我們改變數據后能夠實時更新頁面的觸發點

通過 Object.defineProperty 對屬性定義后,屬性的獲取觸發 get 回調,屬性的設置觸發 set 回調,實現響應式更新。

通過上面的邏輯,也能得出為什麼 Vue3.0 要使用 Proxy 代替 Object.defineProperty 了。Object.defineProperty 只能對單個屬性進行定義,如果屬性是對象類型,還需要遞歸去觀測,會很消耗性能。而 Proxy 是代理整個對象,只要屬性發生變化就會觸發回調。

數組觀測

對於數組類型觀測,會調用 observeArray 方法:

observeArray(data) {
  data.forEach(item => {
    observe(item)
  })
}

與對象不同,它執行 observe 對數組內的對象類型進行觀測,並沒有對數組的每一項進行 Object.defineProperty 的定義,也就是說數組內的項是沒有 dep 的。

所以,我們通過數組索引對項進行修改時,是不會觸發更新的。但可以通過 this.$set 來修改觸發更新。那麼問題來了,為什麼 Vue 要這樣設計?

結合實際場景,數組中通常會存放多項數據,比如列表數據。這樣觀測起來會消耗性能。還有一點原因,一般修改數組元素很少會直接通過索引將整個元素替換掉。例如:

export default {
    data() {
        return {
            list: [
                {id: 1, name: 'Jack'},
                {id: 2, name: 'Mike'}
            ]
        }
    },
    cretaed() {
        // 如果想要修改 name 的值,一般是這樣使用
        this.list[0].name = 'JOJO'
        // 而不是以下這樣
        // this.list[0] = {id:1, name: 'JOJO'}
        // 當然你可以這樣更新
        // this.$set(this.list, '0', {id:1, name: 'JOJO'})
    }
}

數組方法重寫

當數組元素新增或刪除,視圖會隨之更新。這並不是理所當然的,而是 Vue 內部重寫了數組的方法,調用這些方法時,數組會更新檢測,觸發視圖更新。這些方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

回到 Observe 的類中,當觀測的數據類型為數組時,會調用 protoAugment 方法。

if (Array.isArray(data)) {
  protoAugment(data, arrayMethods)
  // 觀察數組
  this.observeArray(data)
} else {
  // 觀察對象
  this.walk(data)
}

這個方法里把數組原型替換為 arrayMethods ,當調用改變數組的方法時,優先使用重寫后的方法。

function protoAugment(data, arrayMethods) {
  data.__proto__ = arrayMethods
}

接下來看看 arrayMethods 是如何實現的:

// 源碼位置:/src/core/observer/array.js
// 1
let arrayProto = Array.prototype
// 2
export let arrayMethods = Object.create(arrayProto)

let methods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'reverse',
  'sort',
  'splice'
]

methods.forEach(method => {
  arrayMethods[method] = function(...args) {
    // 3
    let res = arrayProto[method].apply(this, args)
    let ob = this.__ob__
    let inserted = ''
    switch(method){
      case 'push':
      case 'unshift':
        inserted = args
        break;
      case 'splice':
        inserted = args.slice(2)
        break;
    }
    // 4
    inserted && ob.observeArray(inserted)
    // 5
    ob.dep.notify()
    return res
  }
})
  1. 將數組的原型保存起來,因為重寫的數組方法里,還是需要調用原生數組方法的
  2. arrayMethods 是一個對象,用於保存重寫的方法,這裏使用 Object.create(arrayProto) 創建對象是為了使用者在調用非重寫方法時,能夠繼承使用原生的方法
  3. 調用原生方法,存儲返回值,用於設置重寫函數的返回值
  4. inserted 存儲新增的值,若 inserted 存在,對新值進行觀測
  5. ob.dep.notify 觸發視圖更新

依賴收集

依賴收集是視圖更新的前提,也是響應式原理中至關重要的環節。

偽代碼流程

為了方便理解,這裏寫一段偽代碼,大概了解依賴收集的流程:

// data 數據
let data = {
    name: 'joe'
}

// 渲染watcher
let watcher = {
    run() {
        dep.tagret = watcher
        document.write(data.name)
    }
}

// dep
let dep = [] // 存儲依賴 
dep.tagret = null // 記錄 watcher

// 數據劫持
let oldValue = data.name
Object.defineProperty(data, 'name', {
   get(){
       // 收集依賴
       dep.push(dep.tagret)
       return oldValue
   },
   set(newVal){
       oldValue = newVal
       dep.forEach(watcher => {
           watcher.run()
       })
       
   }
})

初始化:

  1. 首先會對 name 屬性定義 getset
  2. 然後初始化會執行一次 watcher.run 渲染頁面
  3. 這時候獲取 data.name,觸發 get 函數收集依賴。

更新:

修改 data.name,觸發 set 函數,調用 run 更新視圖。

真正流程

下面來看看真正的依賴收集流程是如何進行的。

function defineReactive(obj, key, value) {
  let childOb = observe(value)
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend() // 收集依賴
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newVal) {
      if (newVal === value) {
        return
      }
      value = newVal
      childOb = observe(newVal)
      dep.notify()
      return value
    }
  })
}

首先初始化數據,調用 defineReactive 函數對數據進行劫持。

export class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.getter = exprOrFn
    this.get()
  }
  get() {
    pushTarget(this)
    this.getter()
    popTarget(this)
  }
}

初始化將 watcher 掛載到 Dep.targetthis.getter 開始渲染頁面。渲染頁面需要對數據取值,觸發 get 回調,dep.depend 收集依賴。

class Dep{
  constructor() {
    this.id = id++
    this.subs = []
  }
  depend() {
    Dep.target.addDep(this)
  }
}

Dep.targetwatcher,調用 addDep 方法,並傳入 dep 實例。

export class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.deps = []
    this.depIds = new Set()
  }
  addDep(dep) {
    if (!this.depIds.has(dep.id)) {
      this.depIds.add(dep.id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
}

addDep 中添加完 dep 后,調用 dep.addSub 並傳入當前 watcher 實例。

class Dep{
  constructor() {
    this.id = id++
    this.subs = []
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
}

將傳入的 watcher 收集起來,至此依賴收集流程完畢。

補充一點,通常頁面上會綁定很多屬性變量,渲染會對屬性取值,此時每個屬性收集的依賴都是同一個 watcher,即組件的渲染 watcher

數組的依賴收集

methods.forEach(method => {
  arrayMethods[method] = function(...args) {
    let res = arrayProto[method].apply(this, args)
    let ob = this.__ob__
    let inserted = ''
    switch(method){
      case 'push':
      case 'unshift':
        inserted = args
        break;
      case 'splice':
        inserted = args.slice(2)
        break;
    }
    // 對新增的值觀測
    inserted && ob.observeArray(inserted)
    // 更新視圖
    ob.dep.notify()
    return res
  }
})

還記得重寫的方法里,會調用 ob.dep.notify 更新視圖,__ob__ 是我們在 Observe 為觀測數據定義的標識,值為 Observe 實例。那麼 ob.dep 的依賴是在哪裡收集的?

function defineReactive(obj, key, value) {
  // 1
  let childOb = observe(value)
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend()
        // 2
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newVal) {
      if (newVal === value) {
        return
      }
      value = newVal
      childOb = observe(newVal)
      dep.notify()
      return value
    }
  })
}
  1. observe 函數返回值為 Observe 實例
  2. childOb.dep.depend 執行,為 Observe 實例的 dep 添加依賴

所以在數組更新時,ob.dep 內已經收集到依賴了。

整體流程

下面捋一遍初始化流程和更新流程,如果你是初次看源碼,不知道從哪裡看起,也可以參照以下的順序。由於源碼實現比較多,下面展示的源碼會稍微刪減一些代碼

初始化流程

入口文件:

// 源碼位置:/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

_init

// 源碼位置:/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // mergeOptions 對 mixin 選項和傳入的 options 選項進行合併
      // 這裏的 $options 可以理解為 new Vue 時傳入的對象
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    // 初始化數據
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      // 初始化渲染頁面 掛載組件
      vm.$mount(vm.$options.el)
    }
  }
}

上面主要關注兩個函數,initState 初始化數據,vm.$mount(vm.$options.el) 初始化渲染頁面。

先進入 initState

// 源碼位置:/src/core/instance/state.js 
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // data 初始化
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

function initData (vm: Component) {
  let data = vm.$options.data
  // data 為函數時,執行 data 函數,取出返回值
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 這裏就開始走觀測數據的邏輯了
  observe(data, true /* asRootData */)
}

observe 內部流程在上面已經講過,這裏再簡單過一遍:

  1. new Observe 觀測數據
  2. defineReactive 對數據進行劫持

initState 邏輯執行完畢,回到開頭,接下來執行 vm.$mount(vm.$options.el) 渲染頁面:

$mount:

// 源碼位置:/src/platforms/web/runtime/index.js 
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

mountComponent:

// 源碼位置:/src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    // 數據改變時  會調用此方法
    updateComponent = () => {
      // vm._render() 返回 vnode,這裏面會就對 data 數據進行取值
      // vm._update 將 vnode 轉為真實dom,渲染到頁面上
      vm._update(vm._render(), hydrating)
    }
  }
  
  // 執行 Watcher,這個就是上面所說的渲染wacther 
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

Watcher:

// 源碼位置:/src/core/observer/watcher.js 
let uid = 0

export default class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.id = ++id
    this.vm = vm
    this.cb = cb
    this.options = options
    // exprOrFn 就是上面傳入的 updateComponent
    this.getter = exprOrFn

    this.deps = []
    this.depIds = new Set()

    this.get()
  }
  get() {
    // 1. pushTarget 將當前 watcher 記錄到 Dep.target,Dep.target 是全局唯一的
    pushTarget(this)
    let value
    const vm = this.vm
    try {
    // 2. 調用 this.getter 相當於會執行 vm._render 函數,對實例上的屬性取值,
    //由此觸發 Object.defineProperty 的 get 方法,在 get 方法內進行依賴收集(dep.depend),這裏依賴收集就需要用到 Dep.target
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      // 3. popTarget 將 Dep.target 置空
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
}

至此初始化流程完畢,初始化流程的主要工作是數據劫持、渲染頁面和收集依賴。

更新流程

數據發生變化,觸發 set ,執行 dep.notify

// 源碼位置:/src/core/observer/dep.js 
let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // 執行 watcher 的 update 方法
      subs[i].update()
    }
  }
}

wathcer.update

// 源碼位置:/src/core/observer/watcher.js 
/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
update () {
  /* istanbul ignore else */
  if (this.lazy) {  // 計算屬性更新
    this.dirty = true
  } else if (this.sync) {  // 同步更新
    this.run()
  } else {
    // 一般的數據都會進行異步更新
    queueWatcher(this)
  }
}

queueWatcher:

// 源碼位置:/src/core/observer/scheduler.js

// 用於存儲 watcher
const queue: Array<Watcher> = []
// 用於 watcher 去重
let has: { [key: number]: ?true } = {}
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  let watcher, id

  // 對 watcher 排序
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    // run方法更新視圖
    watcher.run()
  }
}
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    // watcher 加入數組
    queue.push(watcher)
    // 異步更新
    nextTick(flushSchedulerQueue)
  }
}

nextTick

// 源碼位置:/src/core/util/next-tick.js

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // 遍歷回調函數執行
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 將回調函數加入數組
  callbacks.push(() => {
    if (cb) {
      cb.call(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 遍歷回調函數執行
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

這一步是為了使用微任務將回調函數異步執行,也就是上面的p.then。最終,會調用 watcher.run 更新頁面。

至此更新流程完畢。

寫在最後

如果沒有接觸過源碼的同學,我相信看完可能還是會有點懵的,這很正常。建議對照源碼再自己多看幾遍就能知道流程了。對於有基礎的同學就當做是複習了。

想要變強,學會看源碼是必經之路。在這過程中,不僅能學習框架的設計思想,還能培養自己的邏輯思維。萬事開頭難,遲早都要邁出這一步,不如就從今天開始。

簡化后的代碼我已放在 github,有需要的可以看看。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

野火滅村奪74命 雅典同時燃15場森林火 希臘總理:很奇怪

摘錄自2018年7月25日東森新聞台北報導

希臘首都雅典近日高溫達到40℃,野火23日下午起失控焚燒,強風助長下蔓延燒至林地和村落,死亡人數超過74人,至少有187人受傷,甚至有「滅村」情形。這是十年來發生最嚴重的森林大火,當局已經進入緊急狀態,忙著撤離居民和遊客。

希臘總理齊普拉斯(Alexis Tsipras)24日宣布為期三天的全國性哀悼:「這個國家正面臨一場無法用言語形容的悲劇。」希臘同時也呼籲歐盟能提供國際援助,目前當局已布署直升機降水下來,提供民眾逃難的時間。

齊普拉斯與政府官員也派了幾架由美國政府提供的無人駕駛飛機追查任何可疑的行動,他們認為雅典的東、北、西方等地區同時發生15場森林大火是一件很奇怪的事情,也不知道是什麼原因點燃野火。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧