Linux上TCP的幾個內核參數調優

Linux作為一個強大的操作系統,提供了一系列內核參數供我們進行調優。光TCP的調優參數就有50多個。在和線上問題鬥智斗勇的過程中,筆者積累了一些在內網環境應該進行調優的參數。在此分享出來,希望對大家有所幫助。

調優清單

好了,在這裏先列出調優清單。請記住,這裏只是筆者在內網進行TCP內核參數調優的經驗,僅供參考。同時,筆者還會在餘下的博客裏面詳細解釋了為什麼要進行這些調優!

序號 內核參數 值 備註
1.1 /proc/sys/net/ipv4/tcp_max_syn_backlog 2048
1.2 /proc/sys/net/core/somaxconn 2048
1.3 /proc/sys/net/ipv4/tcp_abort_on_overflow 1
2.1 /proc/sys/net/ipv4/tcp_tw_recycle 0 NAT環境必須為0
2.2 /proc/sys/net/ipv4/tcp_tw_reuse 1
3.1 /proc/sys/net/ipv4/tcp_syn_retries 3
3.2 /proc/sys/net/ipv4/tcp_retries2 5
3.3 /proc/sys/net/ipv4/tcp_slow_start_after_idle 0

tcp_max_syn_backlog,somaxconn,tcp_abort_on_overflow

tcp_max_syn_backlog,somaxconn,tcp_abort_on_overflow這三個參數是關於
內核TCP連接緩衝隊列的設置。如果應用層來不及將已經三次握手建立成功的TCP連接從隊列中取出,溢出了這個緩衝隊列(全連接隊列)之後就會丟棄這個連接。如下圖所示:

從而產生一些詭異的現象,這個現象詭異之處就在於,是在TCP第三次握手的時候丟棄連接

就如圖中所示,第二次握手的SYNACK發送給client端了。所以就會出現client端認為連接成功,而Server端確已經丟棄了這個連接的現象!由於無法感知到Server已經丟棄了連接。
所以如果沒有心跳的話,只有在發出第一個請求后,Server才會發送一個reset端通知這個連接已經被丟棄了,建立連接后第二天再用,也會報錯!所以我們要調大Backlog隊列!

echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog
echo 2048 > /proc/sys/net/core/somaxconn

當然了,為了盡量避免第一筆調用失敗問題,我們也同時要設置

echo 1 > /proc/sys/net/ipv4/tcp_abort_on_overflow

設置這個值以後,Server端內核就會在這個連接被溢出之後發送一個reset包給client端。

如果我們的client端是NIO的話,就可以收到一個socket close的事件以感知到連接被關閉!

注意Java默認的Backlog是50

這個TCP Backlog的隊列大小值是min(tcp_max_syn_backlog,somaxconn,應用層設置的backlog),而Java如果不做額外設置,Backlog默認值僅僅只有50。C語言在使用listen調用的時候需要傳進Backlog參數。

tcp_tw_recycle

tcp_tw_recycle這個參數一般是用來抑制TIME_WAIT數量的,但是它有一個副作用。即在tcp_timestamps開啟(Linux默認開啟),tcp_tw_recycle會經常導致下面這種現象。

也即,如果你的Server開啟了tcp_tw_recycle,那麼別人如果通過NAT之類的調用你的Server的話,NAT後面的機器只有一台機器能正常工作,其它情況大概率失敗。具體原因呢由下圖所示:

在tcp_tw_recycle=1同時tcp_timestamps(默認開啟的情況下),對同一個IP的連接會做這樣的限制,也即之前後建立的連接的時間戳必須要大於之前建立連接的最後時間戳,但是經過NAT的一個IP後面是不同的機器,時間戳相差極大,就會導致內核直接丟棄時間戳較低的連接的現象。由於這個參數導致的問題,高版本內核已經去掉了這個參數。如果考慮TIME_WAIT問題,可以考慮設置一下

echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse

tcp_syn_retries

這個參數值得是client發送SYN如果server端不回復的話,重傳SYN的次數。對我們的直接影響呢就是connet建立連接時的超時時間。當然Java通過一些C原生系統調用的組合使得我們可以進行超時時間的設置。在Linux裏面默認設置是5,下面給出建議值3和默認值5之間的超時時間。

tcp_syn_retries timeout
1 min(so_sndtimeo,3s)
2 min(so_sndtimeo,7s)
3 min(so_sndtimeo,15s)
4 min(so_sndtimeo,31s)
5 min(so_sndtimeo,63s)

下圖給出了,重傳和超時情況的對應圖:

當然了,不同內核版本的超時時間可能不一樣,因為初始RTO在內核小版本間都會有細微的變化。所以,有時候在抓包時候可能會出現(3,6,12……)這樣的序列。當然Java的API有超時時間:

java:
 // 函數調用中攜帶有超時時間
 public void connect(SocketAddress endpoint, int timeout) ;

所以,對於Java而言,這個內核參數的設置沒有那麼重要。但是,有些代碼可能會有忘了設置timeout的情況,例如某個版本的Kafka就是,所以它在我們一些混沌測試的情況下,容災恢復的時間會達到一分多鍾,主要時間就是卡在connect上面-_-!,而這時我們的tcp_syn_retries設置的是5,也即超時時間63s。減少這個恢復時間的手段就是:

echo 3 > /proc/sys/net/ipv4/tcp_syn_retries

tcp_retries2

tcp_retries2這個參數表面意思是在傳輸過程中tcp的重傳次數。但在某個版本之後Linux內核僅僅用這個tcp_retries2來計算超時時間,在這段時間的重傳次數純粹由RTO等環境因素決定,重傳超時時間在5/15下的表現為:

tcp_retries2 對端無響應
5 25.6s-51.2s根據動態rto定
15 924.6s-1044.6s根據動態rto定

如果我們在應用層設置的Socket所有ReadTimeout都很小的話(例如3s),這個內核參數調整是沒有必要的。但是,筆者經常發現有的系統,因為一兩個慢的接口或者SQL,所以將ReadTimeout設的很大的情況。

平常這種情況是沒有問題的,因為慢請求頻率很低,不會對系統造成什麼風險。但是,物理機突然宕機時候的情況就不一樣了,由於ReadTimeOut設置的過大,導致所有落到這台宕機的機器都會在min(ReadTimeOut,(924.6s-1044.6s)(Linux默認tcp_retries2是15))后才能從read系統調用返回。假設ReadTimeout設置了個5min,系統總線程數是200,那麼只要5min內有200個請求落到宕機的server就會使A系統失去響應!

但如果將tcp_retries2設置為5,那麼超時返回時間即為min(ReadTimeOut 5min,25.6-51.2s),也就是30s左右,極大的緩解了這一情況。

echo 5 > /proc/sys/net/ipv4/tcp_retries2

但是針對這種現象,最好要做資源上的隔離,例如線程上的隔離或者機器級的隔離。

golang的goroutine調度模型就可以很好的解決線程資源不夠的問題,但缺點是goroutine裏面不能有阻塞的系統調用,不然也會和上面一樣,但僅僅對於系統之間互相調用而言,都是非阻塞IO,所以golang做微服務還是非常Nice的。當然了我大Java用純IO事件觸發編寫代碼也不會有問題,就是對心智負擔太高-_-!

物理機突然宕機和進程宕不一樣

值得注意的是,物理機宕機和進程宕但內核還存在表現完全不一樣。

僅僅進程宕而內核存活,那麼內核會立馬發送reset給對端,從而不會卡住A系統的線程資源。

tcp_slow_start_after_idle

還有一個可能需要調整的參數是tcp_slow_start_after_idle,Linux默認是1,即開啟狀態。開啟這個參數后,我們的TCP擁塞窗口會在一個RTO時間空閑之後重置為初始擁塞窗口(CWND)大小,這無疑大幅的減少了長連接的優勢。對應Linux源碼為:

static void tcp_event_data_sent(struct tcp_sock *tp,
				struct sk_buff *skb, struct sock *sk){
	// 如果開啟了start_after_idle,而且這次發送的時間-上次發送的時間>一個rto,就重置tcp擁塞窗口
	if (sysctl_tcp_slow_start_after_idle &&
	    (!tp->packets_out && (s32)(now - tp->lsndtime) > icsk->icsk_rto))
		tcp_cwnd_restart(sk, __sk_dst_get(sk));
}

關閉這個參數后,無疑會提高某些請求的傳輸速度(在帶寬夠的情況下)。

echo 0 > /proc/sys/net/ipv4/tcp_slow_start_after_idle

當然了,Linux啟用這個參數也是有理由的,如果我們的網絡情況是時刻在變化的,例如拿個手機到處移動,那麼將擁塞窗口重置確實是個不錯的選項。但是就我們內網系統間調用而言,是不太必要的了。

初始CWND大小

毫無疑問,新建連接之後的初始TCP擁塞窗口大小也直接影響到我們的請求速率。在Linux2.6.32源碼中,其初始擁塞窗口是(2-4個)mss大小,對應於內網估計也就是(2.8-5.6K)(MTU 1500),這個大小對於某些大請求可能有點捉襟見肘。
在Linux 2.6.39以上或者某些RedHat維護的小版本中已經把CWND
增大到RFC 6928所規定的的10段,也就是在內網裡面估計14K左右(MTU 1500)。

Linux 新版本
/* TCP initial congestion window */
#define TCP_INIT_CWND		10

公眾號

關注筆者公眾號,獲取更多乾貨文章

總結

Linux提供了一大堆內參參數供我們進行調優,其默認設置的參數在很多情況下並不是最佳實踐,所以我們需要潛心研究,找到最適合當前環境的組合。

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

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

從零開始學習Prometheus監控報警系統

Prometheus簡介

Prometheus是一個開源的監控報警系統,它最初由SoundCloud開發。

2016年,Prometheus被納入了由谷歌發起的Linux基金會旗下的雲原生基金會( Cloud Native Computing Foundation),並成為僅次於Kubernetes的第二大開源項目。自此,它成為了一個獨立的開源項目,獨立於任何公司進行維護。

Prometheus擁有非常活躍的開發人員和用戶社區,目前在GitHub上已擁有三萬多的Star。

Prometheus特點

  • 提供多維度數據模型,使用指標名稱和鍵值對標識的時間序列數據
  • 提供靈活的PromQL查詢方式,還提供了HTTP查詢接口,可以很方便地結合Grafana等組件展示數據。
  • 不依賴外部存儲,支持單節點的本地存儲。通過Prometheus自帶的時序數據庫,可以完成每秒百萬及的數據存儲,如果需要存儲大量歷史數據,還可以對接第三方的時序數據庫。
  • 時間序列收集通過HTTP的拉取方式進行,並提供了開放的指標數據標準。
  • 支持向中間網關推送時序數據,可以更加靈活地適用於多種監控場景。
  • 支持通過動態服務發現和靜態文件配置獲取監控對象,目前已支持Kubernetes、Etcd、Consul等多種服務發現機制。
  • 支持多種模式的圖形展示和儀錶盤。
  • 大多數Prometheus的組件都是使用Go語言編寫的,這使得它們很容易以二進制文件的形式構建和部署。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

Prometheus架構

Prometheus生態圈由多個組件構成,其中許多組件是可選的:

  • Prometheus Server:用於收集、存儲和查詢時間序列數據。通過靜態配置文件管理監控目標,也可以配合使用動態服務發現的方式動態管理監控目標,並從這些監控目標中獲取數據。它將採集到的數據按照時間序列的方式存儲在本地磁盤當中或者外部的時序數據庫中,可通過PromQL語言對數據的查詢以及分析。
  • Client Library:為被監控的應用生成相應的指標(Metric)數據並暴露給Prometheus Server。當Prometheus Server 來拉取時,直接返回實時狀態的指標數據。
  • Push Gateway:主要用於短期存在的Jobs。由於這類Jobs存在時間較短,可能在Prometheus Server來拉取數據之前就消失了。所以,Jobs可以直接向Push Gateway推送它們的指標數據,然後Prometheus Server再從Push Gateway拉取。
  • Exporters:用於暴露已有的第三方服務的指標數據通過HTTP服務的形式暴露給Prometheus Server,比如HAProxy、StatsD、Graphite等等。Prometheus Server通過訪問該Exporter提供的Endpoint,即可獲取到需要採集的監控數據。
  • Alertmanager:從Prometheus Server接收到告警后,會進行去除重複數據,分組,並路由到對收的接受方式,發出報警。Alertmanager的告警方式非常靈活,支持通過郵件、slack或釘釘等多種途徑發出告警。
  • 一些其他的組件。

下面這張圖展示了Prometheus的架構和各個組件是如何交互和協作的:

其大概的工作流程是:

  1. Prometheus Server直接從HTTP接口或者Push Gateway拉取指標(Metric)數據。
  2. Prometheus Server在本地存儲所有採集的指標(Metric)數據,並在這些數據上運行規則,從現有數據中聚合和記錄新的時間序列,或者生成告警。
  3. Alertmanager根據配置文件,對接收到的告警進行處理,發出報警。
  4. 在Grafana或其他API客戶端中,可視化收集的數據。

Prometheus數據模型

Prometheus會將所有採集到的監控數據以時間序列的方式保存在內存數據庫中,並且定時保存到硬盤上。每一條數據由以下三部分組成:

  • 指標(Metric):由指標名稱和描述當前數據特徵的標籤組成。
  • 時間戳(Timestamp):一個精確到毫秒的時間戳。
  • 數據值(Value):一個float64的浮點型數據表示當前數據的值。

其中,指標(Metric)通過如下格式標識:

<指標名稱>{<標籤名稱>=<標籤值>, ...}

指標名稱(Metric Name)可以反映被監控數據的含義。指標名稱只能由ASCII字符、数字、下劃線以及冒號組成並必須符合正則表達式[a-zA-Z_:][a-zA-Z0-9_:]*。
標籤(Label)反映了當前數據的特徵維度,通過這些維度Prometheus可以對數據進行過濾,聚合等操作。標籤的名稱只能由ASCII字符、数字以及下劃線組成並滿足正則表達式[a-zA-Z_][a-zA-Z0-9_]*。

比如:

prometheus_http_requests_total{code="200",handler="/metrics"}

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

指標類型

Prometheus定義了4種不同的指標類型(Metric Type):

  • Counter(計數器)
  • Gauge(儀錶盤)
  • Histogram(直方圖)
  • Summary(摘要)

Counter(計數器)

Counter類型和計數器一樣,只增不減(除非系統發生重置),一般在定義Counter類型指標的名稱時推薦使用_total作為後綴。

比如,Prometheus Server中prometheus_http_requests_total, 表示Prometheus處理的HTTP請求總數:

# HELP prometheus_http_requests_total Counter of HTTP requests.
# TYPE prometheus_http_requests_total counter
prometheus_http_requests_total{code="200",handler="/api/v1/label/:name/values"} 3
prometheus_http_requests_total{code="200",handler="/api/v1/query"} 5
prometheus_http_requests_total{code="200",handler="/api/v1/query_range"} 15
prometheus_http_requests_total{code="200",handler="/graph"} 3
prometheus_http_requests_total{code="200",handler="/metrics"} 23
prometheus_http_requests_total{code="200",handler="/static/*filepath"} 18
prometheus_http_requests_total{code="302",handler="/"} 1

Gauge(儀錶盤)

Gauge類型側重於反應系統的某一個瞬時的值,這類指標的數據可增可減。

比如,Prometheus Server中go_threads, 表示Prometheus當前go線程的數量:

# HELP go_threads Number of OS threads created.
# TYPE go_threads gauge
go_threads 13

Histogram(直方圖)

Histogram類型由 _bucket{le=” “}, _bucket{le=”+Inf”}, _sum, _count 組成,主要用於表示一段時間範圍內對數據進行採樣,並能夠對其指定區間以及總數進行統計,通常它採集的數據展示為直方圖。

比如,Prometheus Server中prometheus_http_response_size_bytes:

# HELP prometheus_http_response_size_bytes Histogram of response size for HTTP requests.
# TYPE prometheus_http_response_size_bytes histogram
prometheus_http_response_size_bytes_bucket{handler="/",le="100"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="1000"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="10000"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="100000"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="1e+06"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="1e+07"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="1e+08"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="1e+09"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="+Inf"} 1
prometheus_http_response_size_bytes_sum{handler="/"} 29
prometheus_http_response_size_bytes_count{handler="/"} 1

Summary(摘要)

Summary類型由 {quantile=”<φ>”}, _sum, _count 組成,主要用於表示一段時間內數據採樣結果,它直接存儲了分位數據,而不是根據統計區間計算出來的。

比如,Prometheus Server中prometheus_target_interval_length_seconds:

# HELP prometheus_target_interval_length_seconds Actual intervals between scrapes.
# TYPE prometheus_target_interval_length_seconds summary
prometheus_target_interval_length_seconds{interval="15s",quantile="0.01"} 14.9986249
prometheus_target_interval_length_seconds{interval="15s",quantile="0.05"} 14.998999
prometheus_target_interval_length_seconds{interval="15s",quantile="0.5"} 15.0000428
prometheus_target_interval_length_seconds{interval="15s",quantile="0.9"} 15.0012009
prometheus_target_interval_length_seconds{interval="15s",quantile="0.99"} 15.0016468
prometheus_target_interval_length_seconds_sum{interval="15s"} 315.0013755
prometheus_target_interval_length_seconds_count{interval="15s"} 21

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

安裝Prometheus Server

從官方網站(https://prometheus.io/download/)上找到最新版本的Prometheus Sevrer軟件包,如下圖:
根據自己的系統下載對應的壓縮包,這裏以Windows為例,下載prometheus-2.19.0.windows-amd64.tar.gz。

解壓后當前目錄會包含默認的Prometheus配置文件promethes.yml:

# my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
    - targets: ['localhost:9090']

暫且不做修改,雙擊prometheus.exe即可啟動,如下圖:

訪問http://localhost:9090/graph,就可以看到Prometheus自身的監控數據:

尾聲

Prometheus的大致介紹已經告一段落了,但是只是萬里長征的第一步,Prometheus的更多強大功能和使用方法還等待我們去挖掘。

微信公眾號:萬貓學社

微信掃描二維碼

獲得更多Java技術乾貨

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

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

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

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行銷專家,教你從零開始的技巧

大廠需求研發流程,進去前了解一波?

點贊再看,養成習慣,微信搜索【三太子敖丙】關注這個互聯網苟且偷生的程序員。

本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點和系列文章。

前言

我的讀者好像學生居多,然後大家最近問的比較多的一個話題就是大廠的研發流程,都比較好奇,整個流程是怎麼操作的。

我也不多BB了,那下面就跟隨暖男的腳步,走進大廠研發流程吧。

正文

我們先看看一個產品有哪些研發流程,帥丙就用自己接觸的阿里系的研發流程舉例了,這也基本上是互聯網大廠的研發流程了,可能細節有出入,但是絕對大同小異。

我問了下字節,多多,騰訊的朋友出入不大,所以還是具有代表性。

看完流程我們就一個個點的去看看每個環節幹了些啥,我們開發同學在這個環節需要做啥,以及在每個環節的職能。

需求提出:

這個環節主要是產品爸爸給我們提需求,每個需求都是他們從用戶,或者自己絞盡腦汁想出來的,但是產品爸爸還拿不準,不能直接敲定,所以就需要我們大家(產品,UI,前端,後端,客戶端和測試)一起討論一下,看看這個需求是否合理,或者這個需求是否有意義,能否達到預期,技術實現的成本,周期等等。

一旦聊成了,他們就會進入下一個階段,聊不成他會想方設法讓你答應,然後進入下個階段,知道我為啥叫產品爸爸了吧?

需求PRD提出:

這個階段,產品爸爸會根據第一版聊下來的結果,大致出一個Demo版本的PRD,會畫出初版的原型圖,並且配上文字說明,所有涉及到的業務,還有交互細節都會羅列出來。

大致就是下圖這樣:

這個時候大家又會圍繞這一版本去開會討論,敲定細節,這個環節會久點,因為細節比較認真,邏輯也不能出錯,還有UI稿子也得敲定,這裏如果不敲定邏輯,UI提前去畫原型圖,後面假如邏輯推翻,一切重來就會浪費大量時間。

這一環節大家都會把細節問清楚,不了解的點也會去了解,測試,開發,UI我們都會在會議上提出自己的觀點,自己的意見,然後等產品反饋,最後意見一致之後,產品當天就回改出敲定版本。

UI就會按照產品爸爸的意思去作圖,接下來就是交互設計評審了。

交互設計評審:

UI會畫出客戶端,前端,H5開發所需要的UI圖,基本上就是我們看到的產品的樣子了,不過還是要敲定細節,比如按鈕合理不,或者上面數據是否在這展示,或者這裏展示的數據是否合理。

這個環節會比較快,只要UI按照之前敲定的邏輯開發,出入不會很大,一般都是小改。

但是也不乏很多,之前敲定了情況,等UI按照敲定版本出了圖,但是卻發現出圖之後有些不合理的點,比如是否應該在這裏展示GMV(銷售總額),或者是否這樣展示活動規則啥的,會有這種情況,不過是小概率事件,改動也不會特別大。

UI界面:

大家看到的這種操作界面,按鈕,圖標的各種位置和圖案,都是UI在這個階段設計好的。(我什麼都沒暗示,不用關注我的B站)

大家敲定后就進入我們開發人員的回合了。

概要設計:

概要設計,這個是大廠程序員需求下來之後基本上都會做的一步,不過看需求大小,可能很多小需求直接就詳細設計了,也有啥設計都不用做的小改動,具體需求具體分析嘛。

很多不了解的同學可能會問,需要設計什麼呢?為什麼要設計呢?

問得好,經常看我文章的都知道,技術是把雙刃劍,你用了技術之後你是不是需要列出他的優點缺點,出問題之後的解決方案,還有可能出現的問題,注意點等等。

這麼是為了讓你能有把控力,比如你這個需求接入了新技術Es**(**Elasticsearch)你什麼都不管你就是要接入它,你把他開發好了上線了,但是有啥坑你知道么?上線崩了怎麼辦?

不主動,不拒絕,不負責,這是渣男的行徑,我們需要負起責任。

這個環節你需要考慮這個需求涉及到哪些服務了,需要新增哪些接口,修改哪些接口,表有現場的還是要新建表,字段要新建么?

其實遠遠不止這些問題,這就是我們做設計的主要原因,也是大家工作裏面能成長的途徑之一,你以為大佬們的經驗是怎麼來的?

推薦工具:Xmind/ProcessOn

  • Xmind官網地址: https://www.xmind.cn
  • ProcessOn
    在線作圖地址: https://www.processon.com

ProcessOn是我使用最頻繁的工具了,我身邊也有很多小夥伴在用,也推薦大家都使用:

大家在學習,看書等等的時候做個腦圖,後面學習和複習的時候思路會很清晰,而且效率瞬間高很多,形成知識體系。

概要設計一般就是做個大概,給大家看一下我自己在設計ES相關的需求的時候的概設,比較粗糙看個大概就好了:

這個設計好了,就需要給Leader看,看理解程度,一兩次返工是有可能的,如果你像或者像敖丙一樣笨的話,是有可能會被打回N次的,這裏我得提一下,好好做設計好處大大的有,自己體會。

然後會進行一輪測試用例評審,比如你涉及哪些服務,新增了哪些接口,改了哪些接口,都是要同步出來的,至於為啥?

是因為測試會依據這個數據,評估影響範圍,方便他寫測試用例,後面會提到。

詳細設計

小夥伴又要問了啥是詳細設計呀帥丙?

傻瓜,簡單呀,見名知意嘛,概要設計是大概的設計,詳細設計是詳細的設計。

我們研發的時候整個流程往往很複雜,如果你理解不對直接就寫代碼,最後容易造成返工,延期,加班,被罵,心情差,回家吵架,離家出走,露宿街頭,饑寒交迫,被迫吃野味,然後全國。。。。

看到不做詳細設計的後果了吧,其實大家花點時間做詳細設計很有必要,你思路完全清晰了,寫代碼那就是分分鐘的事情,不是嘛?

那再看看帥丙的一個小設計吧,之前文章中大量的流程圖,時序圖都來自它,主要是這玩意還是在線的,都不用下載很方便啊。

詳細設計的工具我用的就是之前提到的在線**作圖神器:**ProcessOn https://www.processon.com

還是我自己之前設計的一些流程圖,大家可以看看:

這個環節一樣重要,這個地方如果你能想好很多細節,開發的時候效率會高很多,像我上面的一些點,基本上就是看着圖開發了。

這個環節一般上不需要Leader參与,但是如果你有疑問或者不了解的點還是要提出來的。

測試用例評審:

上面我們說過,測試會根據你的概要設計,評估你的影響範圍,你的影響點,新增和改動的接口啥的,去編寫自己的測試用例。

測試用例,主要是為了把改動點影響點都考慮到,測全一點,免得上線了影響別的現有業務,也是為了把你開發的功能可能出現的bug給排除了。

我拿個小破站的小用例大家看看,這個比較粗糙但是也有點那味了。

這個環節也會開會討論,也是細節的確定,比如他寫的是否合理,或者有什麼點沒考慮到,大家有沒有補充的。

接口定義&開發&前後端聯調

這個環節其實比較好理解,啥都敲定了,那就開發唄,開發差不多了,就得前後端聯調了。

這裡有個小細節還是想說一下,一般開發前我們都會提前定義數據類型,接口名稱,然後在公司的接口工具上給出鏈接和參數,方便前端爸爸mock數據。

他總不能等我們後端開發完了,才去開發嘛,這樣效率打折扣,所以都是後端先定義好,然後前後端并行開發的。

後端開發好,一般都是會發布到聯調環境,我們有哪些環境,聯調環境在我們所有的環境中處於哪個地位呢?

大家可以看到我列出了我們開發的所有環境。

Tip:日常環境不能由開發人員發布,是因為測試流程比較久,所以不能中斷,如果你一直發布會影響測試的效率,在發布期間他們是沒辦法幹活的,而且很多部門涉及相同的服務,你發布還會影響別人。

測試發布之前,在測試群里問問可以發某個服務么,大家覺得不影響,那麼就可以發了,懂了吧。

預發環境,也叫灰度環境,這是跟線上數據一樣的一個環境,只是只能內網訪問,一般這一步是防止很多是因為日常的數據量不夠真實,數據級別達不到線上的量級無法測出的bug。

扯遠了,聯調完了就是代碼Review了。

代碼Review:

codeReview環節,畫一下重點,這可能是整個研發流程中,讓你成長最快的一個環節,讓組員和Leader Review你的代碼,往往他們能給你很多業務上和技術上的建議和意見。

過來人的經驗你就說香不香吧,以前老大經常沒時間,但是我就是煩着他要Review,後來他說不用review了,但是我還是要組員大佬review,因為我很享受別人對我提建議的時候,這不就是成長,掃盲的好時機嘛。

提測&灰度發布&產品第一次驗收

這一階段就是把代碼都發到日常環境,然後等測試爸爸測試,這個環節開發同學如果沒BUG是比較輕鬆的,等着就好了,可以看看丙丙的文章啊,看看丙丙的B站視頻什麼的。

但是如果你BUG多,那我覺得你可能會生不如死,因為有的bug真的找很久很久的,調用鏈路又長,特別是跨服務又涉及消息隊列,或者第三方的接口什麼的。

img

總之你也不知道會出現什麼bug,我看身邊的大神也只能用經驗避免常見的吭,孰能生巧吧。

發布計劃

敲黑板,這個確實是比較重要的環節,這個環節主要是開發同學和前端同學說好一個發布時間,然後制定一個發布計劃,為啥要發布計劃呢?

我們開發一個需求,可能涉及到N個服務,這些服務是有依賴關係的,那就需要打包,比如訂單系統,依賴人員系統。優惠券系統,也依賴人員系統,然後訂單系統還依賴優惠券系統,是不是有點亂了?

我們看圖:

打包和發布順序原則上是一樣的,從沒完全依賴的服務按照順序發布到最後一個服務。

生成環境上線:

這就是神聖而莊嚴的上線環節,一般在這個環節丙丙都是要洗手洗澡,然後才點下那個神聖的發布按鈕。

一般現在都是自動化發布,界面上點點就好了,記得丙丙大學發布都是進服務器一個個kill進程,替換jar包然後重啟。

現在都是分佈式的集群,這樣發無疑會累死,我之前負責的系統有50多台機器,一般都是4台4台發布。

日誌觀察&產品第二次驗收

一般發布第一批之後不會馬上發布第二批,而是觀察錯誤日誌,看看是否正常,有時候會發現還是會出現異常情況的,那就保留錯誤日誌,然後回滾。

知道解決了再發布,順利的話就沒啥錯誤,一口氣發完了,看了下時間凌晨了,那發完差不多也得回家了。

一次發布可能涉及服務多的話,真的有可能發布這麼久,但是沒辦法,線上出問題就是掉腦袋的事情。

日誌觀察一般公司都有錯誤日誌搜集系統,或者自己登錄跳板機查看就好了。

沒問題,發完之後告訴產品大大就好了。

需求結束

至此基本上一個需求可能就結束了,其實還是很不容易的,短的需求幾天,長的需求幾個月,中間塗塗改改,BUG,技術難點都是你要面對的,不過沒啥大問題,我們技術人嘛皮實能頂。

總結

產品研發流程大家是不是覺得有點複雜,或者覺得很多點有點小題大做了,不瞞你說,剛開始我也這麼認為的,但是隨着時間的推移,你會發現有時候越是這樣規範,越是提升了效率,也提升了產品質量。

對自己設計的嚴苛也會讓你的業務能力提升,開發考慮的點也越來越廣泛,我想大佬應該都是這樣走過去的,那沒啥好說的,我們也走。

最後給大家看看我自己搞的一個項目管理模板吧,基本上能適用大部分項目了,要xmind格式的公眾號回復【項目】即可。

相關資料

準備了很多學習資料給大家https://pan.baidu.com/s/1gM4Ea11ygHuMomT2VQ2aNQ

我是敖丙,一個在互聯網苟且偷生的程序員。

你知道的越多,你不知道的越多,人才們的 【三連】 就是丙丙創作的最大動力,我們下期見!

注:如果本篇博客有任何錯誤和建議,歡迎人才們留言!

文章持續更新,可以微信搜索「 三太子敖丙 」第一時間閱讀,回復【資料】有我準備的一線大廠面試資料和簡歷模板,本文 GitHub https://github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。

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

【其他文章推薦】

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

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

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

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

※網頁設計最專業,超強功能平台可客製化

Project Loom:Reactive模型和協程進行時(翻譯)

Java 15將發布Project Loom的第一個版本。我相信這將改變JVM。在這篇文章中,我想深入探討一下導致我相信這一點的原因。

首先,我們需要了解核心問題。然後,我將嘗試描述以前的技術如何解決它。之後,我們將看到Project Loom採取的方法。最後,我將推斷後者可能對生態系統產生什麼影響。

Project Loom

我們首先必須記住,很長一段時間以來,計算機只有一個內核。即使這樣,還是需要同時運行多個程序:這至少要運行兩個操作系統和適當的程序。

為了實現并行性的幻覺,它依賴於一個技巧。它運行一個程序,如果該程序沒有在特定的時間範圍內完成,它將存儲其狀態以供以後使用。然後,它運行下一個要運行的程序。有幾種算法可用來調度下一個程序:循環調度,加權循環調度等。好處是,所有這些都可以通過操作系統線程的概念很好地從開發人員中抽象出來。該操作系統通吃繁重的護理,包括存儲執行的狀態。但是,線程有兩個缺點:

  • 線程很重,因為它帶有很多狀態
  • 線程需要大量的機器資源來創建
    在所有情況下,現代OS都允許M個線程,其中M是數千個線程。現代機器是多核的,提供N個核,比M少兩個數量級。擁有多個內核(無論是物理內核還是虛擬內核)並不會從根本上改變底層機制:M遠高於N,並且OS負責從線程到內核的映射。

阻塞線程

面的模型在傳統方案中效果很好,但在網絡方案中效果不佳。假設有一個Web服務器需要響應HTTP請求。在過去不太好的時候,CGI是處理請求的一種方法。它將每個請求映射到一個流程,以便處理創建整個新流程所需的請求,並在發送響應后將其清除。

Java EE應用程序服務器極大地改善了這種情況,因為實現將線程保留在池中以供以後重用。但是,想象一下響應的生成需要時間,例如因為它需要訪問數據庫以讀取數據。在數據庫返回數據之前,線程需要等待。

這意味着線程實際上正在等待其生命周期的大部分時間。一方面,此類線程自己不使用任何CPU。另一方面,它使用其他種類的資源,尤其是內存。

同樣,太多線程是操作系統的負擔:操作系統必須在數量有限的CPU內核上平衡大量線程。這花費了寶貴的CPU周期,因此,操作系統正在與應用程序競爭CPU。

現在,併發請求的數量可以遠遠超過服務器可用的線程數量。因此,阻塞的線程浪費資源,並使服務器無響應。為了解決這個問題,可以添加更多的Web服務器來處理負載:這是水平縮放。

在大多數情況下,水平縮放就足夠了。等待阻塞線程所花費的時間是浪費的,但是沒有任何相關的費用…除非一個人的基礎架構位於’雲’中。在那種情況下,人們要為未使用的資源付費:這絕不是一個明智的主意。

單線程,反應式和Kotlin協程模型

對於開發人員來說,管理多個線程很複雜。例如,如果您一直在使用(或開發)Swing應用程序,則可能知道“灰色矩形效果”。當用戶與窗口的交互(例如單擊)啟動長時間運行的任務時,就會發生這種情況。如果將另一個窗口移到Swing窗口上,然後再移出,Swing不會重畫另一個窗口與Swing窗口相交的區域,而不會留下難看的灰色矩形。原因是長時間運行的任務是在“ 事件調度線程”上啟動的,而不是在專用線程上啟動的。而且這很容易避免,甚至不涉及在共享的可變狀態上進行同步!

為了避免這種情況,某些堆棧完全禁止開發人員使用多個線程。例如,Node.js的API僅提供一個非阻塞事件循環線程:提交的函數採用回調的形式。請注意,這不會阻止實現使用多個線程。

該反應的方法是另一種選擇,其實頗為相似。儘管它擺脫了單線程API的限制,並提供了反壓力機制,但它仍然需要非阻塞代碼。由於OS線程很昂貴,因此Reactive將它們池化,並在整個應用程序生命周期中重複使用它們。核心過程是從池中獲取空閑線程,讓其執行代碼,然後將線程釋放回池中。這就是為什麼它需要非阻塞代碼的原因:如果代碼阻塞了,那麼執行線程將不會被釋放,並且池將在某一點或另一點耗盡。

我感興趣地觀察了Reactive模型如何像篝火一樣在Spring生態系統中傳播,儘管我選擇站在一邊。恕我直言,反應式有幾個缺點:

  • 編寫(和閱讀!)反應式代碼的思維方式與編寫傳統代碼的思維方式非常不同。我願意承認改變心態只需要時間,持續時間取決於每個開發人員。
  • 儘管真正的開發人員不會調試,但我知道很多人會調試-包括我自己。由於上述線程切換的魔力,要跟蹤一段代碼及其相關的狀態並不容易。這需要足夠的工具,例如帶有相關插件的 IntelliJ IDEA 。
  • 最後,出於相同的原因,傳統的堆棧跟蹤也無濟於事。一些黑魔法可以繞開它。但是,這不是為了膽小者。有關選項的完整列表,請查看此文檔。

Kotlin語言提供了Reactive方法的替代方法:協程。簡而言之,當使用suspend關鍵字時,Kotlin編譯器會在字節碼中生成一個有限狀態機。好處是在協程塊中調用的函數看起來像是順序執行的,儘管它們是并行執行的-更確切地說,取決於確切的範圍,有可能會執行。

Project Loom和虛擬線程

Reactive模型和Kotlin協程都在客戶端代碼和JVM線程之間添加了一個額外的抽象層。框架/庫的職責是動態地將一個映射到另一個。問題的癥結在於JVM線程是OS線程的薄包裝:請記住,OS線程創建起來很昂貴,並且數量限製為數千個。

Project Loom的目標是實際上將JVM線程與OS線程解耦。
當我第一次意識到該倡議時,其想法是創建一個稱為Fiber(線程,Project Loom,您能抓住麻煩嗎?)的抽象。一個Fiber責任是讓一個操作系統線程,使其運行代碼,釋放回池,就像無棧一樣。

當前的建議有很大的不同:Fiber它沒有使用新的類,而是重新使用了Java開發人員非常熟悉的一個類- java.lang.Thread!

因此,在新的JVM版本中,某些Thread對象可能是重量級的並映射到OS線程,而另一些對象可能是虛擬線程。

Project Loom發布的後續影響

主要問題是,既然JVM API提供了對OS線程的抽象,那麼其他抽象(例如響應式和協程)又會變成什麼樣呢?我對預測不滿意,但以下是Reactive /協程背後的公司可能採取的一些態度:

  • 正面態度,他們意識到自己的框架不再帶來任何附加值,而只是重複。他們停止了開發工作,僅向現有客戶提供維護版本。他們幫助說客戶遷移到新的ThreadAPI,一些幫助可能是以付費諮詢的形式。
  • 反面態度,他們在各自的框架中投入了大量的精力之後,他們決定繼續進行,好像什麼也沒有發生。例如,Spring框架負責實際設計一個共享的Reactive API,稱為Reactive Streams,沒有Spring依賴項。當前有兩種實現,RxJava v2和Pivotal的Project Reactor。另一方面,JetBrains宣傳Kotlin的協程是并行運行代碼的最簡單方法。
  • 中間態度。這兩個框架都將繼續其生命,但是會將它們各自的基礎實現更改為使用虛擬線程。
    由於沉沒成本的謬誤,排在第一位的可能性極小:銷售和市場營銷將努力保持其“競爭優勢”-無論在他們眼中意味着什麼。儘管有些工程師出於相同的原因希望保留現有代碼,但其他一些工程師則將努力使用新的API。因此,我也不相信第二名也會發生。但是,我認為這兩個工程派之間都發揮着力量,然後它們與市場營銷/銷售之間將在#3之間找到平衡。

結論

Project Looms將現有的Thread實現方式從OS線程的映射更改為可以表示此類線程或虛擬線程的抽象。就其本身而言,這是一個有趣的舉動,它在一個平台上歷來比創新更重視向後兼容性。與其他最新的Java版本相比,此功能是真正的遊戲規則改變者。一般而言,開發人員應儘快開始熟悉它。打算學習Reactive和協程的開發人員可能應該退後一步,並評估他們是否應該學習新的ThreadAPI- 是否需要。

翻譯原文

https://blog.frankel.ch/project-loom-reactive-coroutines/

擴展閱讀

Project Loom地址: https://github.com/openjdk/loom
Project Loom現有狀態: http://cr.openjdk.java.net/~rpressler/loom/loom/sol1_part1.html

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

前言

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

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

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

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

主要成員

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

  • 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. 為數組添加重寫的數組方法,比如:push、unshift、splice 等方法,重寫目的是在調用這些方法時,進行更新渲染
  3. 觀測數組內的數據,observe 內部會調用 new Observe,形成遞歸觀測
  4. 觀測對象數據,defineReactive 為數據定義 get 和 set ,即數據劫持

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

pushTarget 和 popTarget 這兩個方法顯而易見是用來設置 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 收集的依賴就是這個 watcher,childOb.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 屬性定義 get 和 set
  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.target,this.getter 開始渲染頁面。渲染頁面需要對數據取值,觸發 get 回調,dep.depend 收集依賴。

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

Dep.target 為 watcher,調用 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行銷專家,教你從零開始的技巧

英研究稱城市臭氧污染與死亡風險增加相關

摘錄自2020年2月17日北京新浪網報導

倫敦大學衛生和熱帶醫學院最近發佈一項研究說,對全球數百個城市的調查發現,人們日常暴露在城市地面臭氧污染中與死亡風險增加有關。

來自該學院以及多個科研機構的國際團隊在最新一期《英國醫學雜誌》上發表文章報告稱,他們採集了1985年至2015年間來自20個國家和地區406個城市的4500多萬例死亡案例相關數據,並分析了這些城市的污染狀況及死亡風險間的關係後發現,城市地面臭氧水平平均每上升10微克/立方米,總體死亡風險就會增加0.18%。也就是說,如果這些城市執行了更嚴格的空氣排放標準,每年共可減少6000多人死亡。

但團隊也表示,這項研究是基於觀察性分析,因此無法直接確立因果關係,研究結果的準確性也受到一些因素的限制。

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

哥倫比亞毒梟艾斯科巴的河馬 意外恢復1萬多年前部分生態系功能

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

寮國大壩潰決! 50億噸蓄水傾瀉、數百人失蹤

摘錄自2018年7月24日自由時報報導

寮國東南部一處水力發電水壩驚傳潰決意外,50億噸的蓄水傾瀉而下,造成數百人失蹤。

綜合外媒報導,位於寮國東南部的阿速坡省(Attapeu)週一(23日)深夜驚傳水壩潰壩,一處用來水力發電的水壩不明原因突然潰決,50億噸的水量瞬間傾瀉而下,下游六個村落遭到淹沒,情況一片混亂,寮國官媒僅表示已有人死亡,不過死亡人數不詳,目前還有數百人失蹤。

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

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

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

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

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

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

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

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

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

【其他文章推薦】

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

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

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

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

※超省錢租車方案

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