哪些年,我們玩過的Git

作者:玩世不恭的Coder
公眾號:玩世不恭的Coder
時間:2020-06-05
說明:本文為原創文章,未經允許不可轉載,轉載前請聯繫作者

哪些年,我們玩過的Git

前言一、前期工作常用基本概念的理解Git環境的搭建用戶名和郵箱的配置二、Git的理論基礎工作區域工作流程版本庫的初始化文件的四種狀態Git的初步操作三、關於文件的各種操作文件修改之後版本的回退撤銷修改文件的刪除四、本地項目遠程提交到Github倉庫五、Git的分支管理分支的創建與合併單人分支合併時的衝突解決多人協作下的衝突解決六、標籤管理總結參考資料

前言

關於Git,相信每一位該領域的朋友都了解其的強大。Git是一種廣受歡迎的代碼管理工具,在實際開發中,我們可以通過Git和團隊更好管理我們的項目版本,也大大提高了團隊的開發效率。在實際使用Git的過程中,我們一般只需要掌握其中的十幾條命令就夠用了,Taoye之前對Git也只是停留在會用的狀態,而由於對Git內部的一些細節平時接觸比較少,所以還是會有一點盲區存在。所以,乘着考研結束的這段空閑時間,對之前學習過的Git做一個整理,一方面分享給各位讀者,另一方面也方便自己日後的複習。本文主要介紹了在實際開發過程中所常用的一些Git操作,由於博主技術水平有限,在內容上的表述難免會有疏忽和遺漏,也懇請各位Coder多多指教。

一、前期工作

常用基本概念的理解

  • 版本控制

所謂的版本控制是指對軟件開發過程中各種程序代碼、配置文件及說明文檔等文件變更的管理,是軟件配置管理的核心思想之一。簡單的講,就是方便我們對項目代碼的各種版本進行管理。

我們可以舉個例子來進行說明一下:相信每一位學生,無論是大學畢業或是研究生畢業,都避免不了完成一篇學術性的論文。我們都知道,學術性論文的撰寫都是一個長期的過程,在這個過程也會經歷不斷地反覆修改,這樣就會產生多個論文版本。相信每一位有過此經歷的同學都會感到痛苦,版本多了甚至都不知道每一個版本的論文內容都修改了啥。而我們的Git就能夠很好的管理各種版本的論文,在每一次提交的時候,都可以在comment中記錄我們對論文的修改內容,並且每一個版本的論文都可以進行回滾等操作,也就是隨意的切換各種版本。如此一來,豈不快哉???

此外,我們還可以使用廖大大提到的例子來解釋一下:在許多遊戲中,都會有一個存檔的操作,如果你對接下來的挑戰Boss沒有足夠的信心,就可以進行一次存檔,當我們的角色因為挑戰Boss而喪命,我們就可以讀取其中的存檔而從當前狀態繼續遊戲,而不需要從頭再來,假如遊戲有保存多個存檔的操作,我們還可以讀取不同的存檔來繼續遊戲。同樣地,在我們對文件進行修改到一定程度的時候,就可以保存一個“快照”,一旦我們對自己的操作的不滿意,就可以進行恢復或是回滾操作,這樣就可以“穿梭”到操作之前狀態繼續工作,而無需從頭再來。

  • 工作區(Working Directory)

可以理解成就是電腦本地磁盤的目錄,比如我們在本地創建了一個temp目錄,那這個目錄就叫做工作區。

  • 暫存區(Staging area)

一般存放在”git目錄”下的index文件(.git/index)中,所以我們把暫存區有時也叫作索引(index)。

  • 版本庫(Repository)

我們的工作區有個隱藏目錄.git,它就是Git的本地版本庫。

對於以上部分概念,有些讀者可能不是很了解,大家可以閱讀下面內容之後再回過頭來進行推敲,相信大家一定會有更加透徹的理解。

Git環境的搭建

對於Git的安裝,在前面我們講解Hexo搭建博客的時候有介紹過,這裏我們再簡單的回顧一下。

你可在git官網中根據自己的需要進行下載:https://git-scm.com/。打開之後你將看到如下內容,就無腦download for Windows

將其下載到指定的磁盤,然後Windows系統下傻瓜式安裝即可。安裝好后我們打開cmd終端(win+r -> 輸入cmd -> 回車),執行git --version,若出現git version 2.19.2.windows.1之類的版本輸出,那麼恭喜你已經成功安裝Git。

對於Linux操作系統下,我們可以直接通過命令的形式來進行安裝:

1# Linux下安裝Git
2sudo apt-get install git

用戶名和郵箱的配置

我們在安裝完成Git之後,首先第一步要做的就是配置Git的用戶名和郵箱,這個是提交項目的用戶的唯一標識。

1# 配置用戶名和郵箱,配置好之後會寫入C:\Users\M的.gitfig文件中
2git config --global user.name "username"
3git config --global user.email "email@qq.com"

global說明:global代表全局的意思,也就是說我們在之後提交的每一個項目採用的都是該用戶名和郵箱。假如我們需要在不同的項目中使用不同的用戶名和郵箱,我們則不需要添加–global參數。

我們將此信息配置好之後,就會寫入C:\Users\M中的,gitfig文件中。此外,我們也可以用過以下命令來查詢我們所配置的用戶名和郵箱:

1# 查詢配置
2git config -l

二、Git的理論基礎

該部分的內容來自:https://www.cnblogs.com/best/p/7474442.html

工作區域

Git本地有三個工作區域:工作目錄(Working Directory)、暫存區(Stage/Index)、資源庫(Repository或Git Directory)。如果在加上遠程的git倉庫(Remote Directory)就可以分為四個工作區域。文件在這四個區域之間的轉換關係如下:

  • Workspace:工作區,就是你平時存放項目代碼的地方
  • Index / Stage:暫存區,用於臨時存放你的改動,事實上它只是一個文件,保存即將提交到文件列表信息
  • Repository:倉庫區(或本地倉庫),就是安全存放數據的位置,這裏面有你提交到所有版本的數據。其中HEAD指向最新放入倉庫的版本
  • Remote:遠程倉庫,託管代碼的服務器,可以簡單的認為是你項目組中的一台電腦用於遠程數據交換

本地的三個區域確切的說應該是git倉庫中HEAD指向的版本

  • Directory:使用Git管理的一個目錄,也就是一個倉庫,包含我們的工作空間和Git的管理空間。
  • WorkSpace:需要通過Git進行版本控制的目錄和文件,這些目錄和文件組成了工作空間。
  • .git:存放Git管理信息的目錄,初始化倉庫的時候自動創建。
  • Index/Stage:暫存區,或者叫待提交更新區,在提交進入repo之前,我們可以把所有的更新放在暫存區。
  • Local Repo:本地倉庫,一個存放在本地的版本庫;HEAD會只是當前的開發分支(branch)。
  • Stash:隱藏,是一個工作狀態保存棧,用於保存/恢復WorkSpace中的臨時狀態。

工作流程

git的工作流程一般是這樣的:

1. 在工作目錄中添加、修改文件;

2. 將需要進行版本管理的文件放入暫存區域;

3. 將暫存區域的文件提交到git倉庫。

因此,git管理的文件有三種狀態:已修改(modified),已暫存(staged),已提交(committed)

版本庫的初始化

我們需要創建一個目錄來作為我們的項目根目錄,進入到該目錄之後,右鍵git bash來啟動git的操作窗口

1# 選擇對應的目錄,右鍵點擊git bash,然後創建一個目標項目目錄,並進入該目錄
2mkdir temp_project
3cd temp_project

之後,我們需要將創建的目錄初始化為Git所能識別倉庫,可以通過git init來實現。初始化完成之後,就會在該目錄中自動創建出一個.git的隱藏目錄,關於我們項目的版本信息都會存儲在該目錄當中。

1# 初始化一個倉庫,之後會多出.git隱藏文件
2git init

以上就是我們自定義一個倉庫的過程。此外,我們還可以基於已經存在Git倉庫來進行操作,在Github中查找自己想要的Git倉庫,複製其鏈接,然後通過git clone來對該倉庫進行克隆,從而將該倉庫下載到我們的本地目錄中:

1# 使用clone克隆Github中已經存在的倉庫
2git clone XXXXXX.git

文件的四種狀態

我們要對文件的版本進行控制,首先需要明白當前文件處於什麼樣的狀態,對於不同狀態下的文件,我們可以進行不同的操作。而在Git中,文件主要是有四種狀態:

  • Untracked: 未跟蹤, 此文件在文件夾中, 但並沒有加入到git庫, 不參与版本控制. 通過git add 狀態變為Staged.
  • Unmodify: 文件已經入庫, 未修改, 即版本庫中的文件快照內容與文件夾中完全一致. 這種類型的文件有兩種去處, 如果它被修改, 而變為Modified. 如果使用git rm移出版本庫, 則成為Untracked文件
  • Modified: 文件已修改, 僅僅是修改, 並沒有進行其他的操作. 這個文件也有兩個去處, 通過git add可進入暫存staged狀態, 使用git checkout 則丟棄修改過, 返回到unmodify狀態, 這個git checkout即從庫中取出文件, 覆蓋當前修改
  • Staged: 暫存狀態. 執行git commit則將修改同步到庫中, 這時庫中的文件和本地文件又變為一致, 文件為Unmodify狀態. 執行git reset HEAD filename取消暫存, 文件狀態為Modified

Git的初步操作

了解了文件的狀態之後,我們不妨模擬一個關於論文的例子來初步了解下版本庫以及文件的簡單操作:

  • 創建一個工作區,並進行初始化
1# 創建一個工作區
2mkdir my_dir
3# 進入工作區
4cd my_dir
5# 通過init命令將工作區轉化成Git可以管理的倉庫
6git init
7# 在該倉庫中創建一個paper.txt,用於編寫我們的論文
8touch paper.txt
  • 我們在paper.txt之中編輯如下內容:
1# 我們在`paper.txt`之中編輯如下內容:
2Oh, my god, I will write my graduate paper.
3Come on, Taoye.
  • 通過add命令將paper.txt文件添加到暫存區
1# 通過add命令將paper.txt文件添加到暫存區
2$ git add paper.txt
  • 通過commit命令將暫存區中的內容提交到倉庫
1# 通過commit命令將暫存區中的內容提交到倉庫,指定提交paper.txt文件
2$ git commit -m "start writing my paper" paper.txt
3
4# 對工作區中的文件統一提交至暫存區
5$ git commit -m "XXXXXXXXXXX"

使用commit命令之後,就能將我們暫存區中的內容提交至倉庫中,其中的-m參數表示的是提交說明,用於解釋說明本次提價的內容。提交完成之後,我們可以在Git中通過git status來查看文件的狀態:

1# 查看指定文件的狀態
2git status paper.txt
3# 查看所有文件的狀態
4git status

以上操作過程如下圖所示:

三、關於文件的各種操作

文件修改之後

在上節中,我們已經介紹了文件的四種狀態,並且以關於論文的例子來初步了解了版本庫以及文件庫的簡單操作。

假設現在論文指導老師對Taoye提出了要求:“Taoye同學,時間來不及了,你今天必須給我完成論文的摘要部分!!!否則,後果自負!!!”

“Excuse me?今天?我遊戲打得正嗨呢。面對老師如此強硬的要求,沒辦法了,只能暫停上王者的上分階段了,開始剛論文。”對此,Taoye進行了一個騷氣的三下五除二操作,廣泛的涉獵各種優秀的學者文獻並進行構思,迅速開寫paper.txt中的摘要部分,完成之後的內容如下所示:

1Oh, my god, I will write my graduate paper.
2Come on, Taoye!
3
4I have finished the summary today!

之後,我們再通過git status命令來查看一下該論文文件的狀態:

1$ git status paper.txt
2On branch master
3Changes not staged for commit:
4  (use "git add <file>..." to update what will be committed)
5  (use "git checkout -- <file>..." to discard changes in working directory)
6
7        modified:   paper.txt
8
9no changes added to commit (use "git add" and/or "git commit -a")

此時,我們可以發現工作區中的內容發生了修改,並且與我們上一次提交后版本庫中的內容不一致,這個時候,Git就會提醒我們需要再次進行add、commit操作來將修改后的文件添加並提交至版本庫。另外,我們也可以使用diff命令來查看據上次提交所修改的內容:

1# 查看據上次提交所修改的內容
2git diff paper.txt

當確認文件修改的內容無誤之後,我們就可以使用add、commit操作來提交我們的文件到版本庫中:

1# 添加至暫存區
2git add paper.txt
3# 提交至版本庫
4git commit -m "finished the summary" paper.txt
5# 查看狀態
6git status paper.txt

OK,Taoye從白天剛到深夜。如次一來,總算是完成了老師給的要求,興奮的將內容提交給老師審閱,然後又繼續打王者了。

版本的回退

Taoye連續打了12h的遊戲,有點累了。重新打開自己完美的論文欣賞一下,卻突然發現有幾個錯別字,這種低級錯誤實屬不應該出現啊。思慮半刻,Taoye修改之後,再次add commit就完事了:

paper.txt修改之後的內容:

1Oh, my god, I will write my graduate paper.
2Come on, Taoye!
3
4I have finished the summary today!
5Some typos have been fixed.

再次add、commit操作:

1git add paper.txt
2git commit -m "fixed the typos" paper.txt

也就是說,現在我們論文總共是有三個版本,分別是:初始化的論文、完成摘要的論文、修改錯別字后的論文。我們可以通過log命令來查看各種版本的論文文件:

1# 显示各種版本文件的詳細信息
2$ git log paper.txt
3# 通過一行簡單显示文件的信息
4$ git log --pretty=oneline paper.txt
5d7938315aa0f4a4d40c6a94a10ab8db25b50e23b (HEAD -> master) fixed the typos
6454cc579f0fe51fdfd97132384a9c5fcaa1993c2 finished the summary
7dc1dcd9501dec52e6160ce98bb5c118abb805289 start writing my paper

從以上log的輸出信息,我們可以知道所提交文件的所有歷史記錄,其中記錄了提交時間、提交作者(郵箱)、提交說明(-m)等信息。並且如果我們仔細一看,會發現每一個版本的文件都會對應一個commit id,這個id其實就相當於每一個版本的唯一標識。比如,從上面的輸出結果,我們可以得到第一個版本的commit id = dc1dcd9501dec52e6160ce98bb5c118abb805289,而這個commit id的作用就是方便我們日後自由“穿梭”到各個版本(就是一種穿越時空的意思)。

Taoye的論文指導老師審閱了摘要內容之後,正言厲色的說道:“你這寫的啥玩意兒?牛頭不對馬嘴,而且居然還有錯別字?”

沒辦法了,Taoye只能虛心接受老師的批評,再次苦逼的修改論文了。

我們現在論文是版本三,有沒有一種快速有效的方法回退到版本一呢???在Git中,我們可以通過git reset --hard來實現這個需求,也就是版本的回退。版本的回退可以用兩種形式,一種是通過HEAD來基於當前版本進行回退,另一種是通過commit id來回退到指定的版本,其用法如下所示:

1# HEAD表示的是當前版本,可以通過HEAD^回退到上一個版本
2git reset --hard HEAD^
3git reset --hard HEAD^^     # 回退到上上版本,回退到多少個版本之前就使用多少個^
4git reset --hard HEAD~50    # 回退到50個版本之前
5
6# 指定commit id來進行版本回退
7git reset --hard dc1dcd

值得注意的是,我們通過指定id來進行版本回退的時候,由於id過長,我們沒必要全寫,只需要傳入前幾個字符保證id的唯一性即可。有使用過Docker的朋友,應該會熟悉,我們在指定容器的時候,也是類似的操作。

下面Taoye版本三的論文迅速回退到版本一,騷操作如下:

1$ git reset --hard dc1dcd
2$ cat paper.txt
3Oh, my god, I will write my graduate paper.
4Come on, Taoye.

關於版本穿梭的簡單解釋:其實,在Git中,有個HEAD指針用於指向當前版本,當我們進行版本回退的時候,其實就是改變HEAD的指向,指向對應的版本也就實現了版本的回退。這有點類似於數據結構中鏈表的操作。

此外,我們還可以通過git reflog來查看當前版本文件的變換:

1$ git reflog paper.txt
2dc1dcd9 HEAD@{1}: reset: moving to dc1dcd                       # 版本的回退(回退到版本一)
3d793831 (HEAD -> master) HEAD@{2}: commit: fixed the typos      # 第三次提交(版本三)
4454cc57 HEAD@{3}: commit: finished the summary                  # 第二次提交(版本二)
5dc1dcd9 HEAD@{4}: commit (initial): start writing my paper      # 第一次提交(版本一)

撤銷修改

假設Taoye在寫論文的時候,每天都在不斷地修修改改,心裏面非常的煩躁,很不是滋味。於是乎,在paper.txt中添加如下一句話:

1I don't like my tutor, he often criticizes me.

然而,在打算提交的時候,想了想還是有點不太妥,要是因為這麼一句話,最終導致無法畢業那就完蛋了。對此,我們在對文件commit之前使用’git status paper.txt’命令發現,可以通過checkout --來進行修改撤銷,撤銷至修之前的狀態,操作如下:

1$ git checkout -- paper.txt
2$ cat paper.txt     # 執行之後,可以發現已經撤銷到無最後一句的狀態

以上是發生在我們對工作區中的文件進行修改,但是還沒有執行git add操作,將工作區中的paper.txt添加至暫存區中的場景,已達到撤銷至修改之前的狀態。假設我們在對paper.txt修改之後,再執行git add paper.txt命令將文件添加至暫存區,那麼我們該怎樣撤銷呢?按照思路,我們可以先通過git reset HEAD將暫存區中的內容撤銷掉放回工作區,然後撤銷工作區即可實現需求,對此,有如下操作:

1# 將暫存區中的內容撤銷掉放回工作區
2git reset HEAD paper.txt
3# 撤銷工作區修改的內容
4git checkout -- paper.txt

如此一來,就完美的將I don't like my tutor, he often criticizes me.撤銷掉,Taoye也就能順利畢業了。關於撤銷,要記得與版本回退區分開來,撤銷是我們在對文件進行修改但是還沒有進行commit的時候發生的,而版本回退是在執行了commit提交操作之後發生的。

文件的刪除

在上面的內容中,我們已經詳細的介紹了關於文件的修改、版本回退、撤銷等操作,下面我們來講講文件在刪除之後應該會出現哪些操作。

假設現在出現了這麼一種情況:Taoye有個頑皮的妹妹,她在用我電腦的時候,不小心將我的paper.txt論文文件從本地磁盤刪除了。一氣之下,Taoye將妹妹關進了小黑屋自我反省七天。悲劇啊,Taoye忙活了將近一個月的論文就此煙消雲散,Git該如何挽回這樣的結局呢?

paper.txt文件刪除之後,我們使用git status來查看一下文件的狀態:

1$ git status
2On branch master
3Changes not staged for commit:
4  (use "git add/rm <file>..." to update what will be committed)
5  (use "git checkout -- <file>..." to discard changes in working directory)
6
7        deleted:    paper.txt
8
9no changes added to commit (use "git add" and/or "git commit -a")

在Git看來,你將文件從工作區中刪除,其實也是對文件的一種修改。也就是說,現在我們的工作區中的內容與版本庫中的內容不一致(工作區沒有了paper.txt文件,而版本庫依然存在paper.txt文件)。為了將兩者空間中的內容達到一致,我們現在有兩種選擇,一是將版本庫中的paper.txt文件刪除掉,二是將工作區中的paper.txt文件恢復。

有了以上的思路,我們可以有如下操作:

1# 將版本庫中的paper.txt文件刪除掉,需要執行commit才會生效
2git rm -f paper.txt
3git commit -m "delete the paper.txt"
4
5# 將工作區中的paper.txt文件恢復
6git checkout -- paper.txt

對於文件的刪除,需要注意的是,只有提交到版本庫的文件,才能進行恢復,對於為提交到版本庫的文件是無法進行恢復的。

四、本地項目遠程提交到Github倉庫

Taoye在之前參加2019年騰訊微信小程序比賽的時候,開發了一個關於偵探推理的項目。Taoye現在想要將該項目從本地提交到Github,該如何實現呢?

我們在實現該需求之前,首先需要將本地與Github進行聯通。對此我們應該通過ssh在本地生成一個公鑰,然後在Github中配置該公鑰。操作如下:

1# 1、生成公鑰,執行之後會在.ssh目錄中生成秘鑰文件,其中id_rsa.pub表示的是公鑰
2cd ~/.ssh
3ssh-keygen -t rsa 26647879@qq.com
4# 2、進入github,settings -> SSH keys -> add key,然後將id_rsa.put中的公鑰複製進去
5# 3、配置好公鑰之後,需要驗證本地與github的連通性
6ssh -T git@github.com

在確認本地與Github聯通之後,我們就能正常地將項目從本地遠程提交到Github中了。

  • 登錄Github,創建一個目標倉庫,取名為detective,用來存儲我們的項目,並複製其中的.git鏈接地址
  • 在Github創建倉庫之後,需要將該遠程倉庫與本地關聯起來
1# 在本地關聯目標倉庫,方便之後將項目推送至該遠程倉庫
2git remote add origin https://XXXXXX.git
  • 進入我們的本地項目,然後初始化為git可管理的倉庫
1cd detective
2git init
  • 將工作區中項目的所有文件添加至暫存區
1git add ./*
2

  • 將暫存區中的內容提交到版本庫當中
1git commit -m "commit the detective project" ./*
  • 將項目添加到版本庫之後,我們就可以將該項目推送至遠程倉庫了
1# 第一次推送
2git push -u origin master
3# 推送之後,如果我們的項目發生了修改,我們可以不用在使用-u參數進行推送
4git push origin master
5
6# 另外,如果有需要的話,我們還可以使用clone命令將遠程倉庫克隆到本地
7git clone https://XXXXXX.git

五、Git的分支管理

分支是Git當中一個非常重要的概念,分支有點類似於樹枝,也就意味着為了避免影響主線的正常開發,我們可以將任務從主線中分離開來,從而形成一個分支,之後的任務都是在分支中來完成的,當任務完成之後,就可以將完整的任務從分支提交到主線。

在前面版本回退一節當中,我們知道,每一次的提交都會產生一個版本,多次提交自然也就會產生多個版本。我們可以將每一個版本看做是一個珠子,而多個版本就會通過一條線串聯起來,從而形成一條版本鏈。這個版本鏈其實就是一個master分支,也就是我們上面所說的主線,我們每一次的提交都是基於master分支來完成的,而HEAD指針則是用來指向當前分支(在沒有其他分支的前提下,就是指向master)。

下面的圖文講解分支的內容,來自廖大大的教程:https://www.liaoxuefeng.com/wiki/896043488029600/900003767775424

一開始的時候,master分支是一條線,Git用master指向最新的提交,再用HEAD指向master,就能確定當前分支,以及當前分支的提交點:

每次提交,master分支都會向前移動一步,這樣,隨着你不斷提交,master分支的線也越來越長。

當我們創建新的分支,例如dev時,Git新建了一個指針叫dev,指向master相同的提交,再把HEAD指向dev,就表示當前分支在dev上:

你看,Git創建一個分支很快,因為除了增加一個dev指針,改改HEAD的指向,工作區的文件都沒有任何變化!

不過,從現在開始,對工作區的修改和提交就是針對dev分支了,比如新提交一次后,dev指針往前移動一步,而master指針不變:

假如我們在dev上的工作完成了,就可以把dev合併到master上。Git怎麼合併呢?最簡單的方法,就是直接把master指向dev的當前提交,就完成了合併:

所以Git合併分支也很快!就改改指針,工作區內容也不變!

合併完分支后,甚至可以刪除dev分支。刪除dev分支就是把dev指針給刪掉,刪掉后,我們就剩下了一條master分支:

分支的創建與合併

在上面,已經介紹了分支的概念及其斷鏈、成鏈的原理過程,下面我們通過Git命令來完成分支的創建與合併。

在Git中,我們可以通過git branch來查看有哪些分支、git branch xxx來創建一個分支,其中帶有*號的表示當前分支、git checkout xxx來切換分支:

1# 創建一個名為dev的分支
2$ git branch dev
3# 從master分支切換到dev分支中
4$ git checkout dev
5# 查看當前有多少分支
6$ git branch
7
8# 此外,我們還實現分支的創建、切換這兩個操作合併執行
9$ git checkout -b dev

現在,我們不妨在剛剛創建的dev分支中對paper.txt的內容進行編輯,在最下方添加這麼一句話:I'm writing the content of my paper.。編輯完成並保存之後,我們提交到版本 庫,並再次切換到master分支,使用cat命令來查看paper.txt的內容。

1$ git checkout dev      # 切換到dev分支
2$ vim paper.txt         # 編輯paper.txt,並增添一句話
3$ git add paper.txt     # 添加到暫存區
4$ git commit -m "writing the content of this paper" paper.txt   # 提交到版本庫
5$ cat paper.txt         # 查看paper.txt內容
6$ git checkout master   # 再次切換到master分支
7$ cat paper.txt         # 查看paper.txt內容

採用cat命令兩次查看paper.txt內容時,我們會發現在執行后一次命令時,paper.txt中的內容並沒有添加新增的一句話,這主要是因為我們剛剛採用vim編輯paper.txt的時候是基於dev分支進行的,提交也是提交到dev分支,而非master分支,所以當我們切換到master分支的時候並不能夠看見paper.txt編輯后的內容。如下圖所示:

而我們要想在master分支中查看到paper.txt的新內容,則需要將dev分支合併到master主分支中才行,採用的是git merge命令,操作如下:

1$ git checkout master
2$ git merge dev

合併完成之後,dev的任務已經完成了,也就沒有必要存在了,可以通過git branch -d xxx來刪除分支:

1$ git branch -d env
2Deleted branch env (was e9c7421).

單人分支合併時的衝突解決

在編程的世界里,多進程佔據了一個舉足輕重的地位。在高併發、高流量的場景下,我們一般通過多進程來提高項目的服務效率,以便提高用戶體驗。話雖如此,但是在使用多進程的時候,許多問題同樣會慢慢浮出水面。同樣地,Git分支雖然能夠方便多個的用戶協同開發,但是將多個不同內容的分支進行合併的時候卻會產生衝突,作為一個對技術有追求的Coder,我們應該要理解為什麼會產生衝突,以及產生衝突后我們應該怎樣解決。

出現衝突的場景:

  1. 切換到dev分支后,對paper.txt進行編輯,然後將保存后的文件提交到版本庫中。
  2. 切換到master分支,對paper.txt進行編輯,將保存后的文件提交到版本庫。
  3. master分支下,將dev分支進行合併。
 1# 1、在dev分支中對paper.txt文件進行編輯,並提交到版本庫
2git checkout dev
3git vim paper.txt
4git add paper.txt
5git commit -m "the first operation" paper.txt
6
7# 2、在master分支中對paper.txt文件進行編輯,並提交到版本庫
8git checkout master
9git vim paper.txt
10git add paper.txt
11git commit -m "the second operation" paper.txt
12
13# 3、在master分支中,將dev分支合併
14git merge dev

此時,在我們執行merge命令進行分支合併的時候,會出現如下內容:

1$ git merge dev
2Auto-merging paper.txt
3CONFLICT (content): Merge conflict in paper.txt
4Automatic merge failed; fix conflicts and then commit the result.

從如上Git給我們提供的信息可以知道,此時已經產生衝突,我們必須手動解決衝突才能再次提交,使用git status paper.txt也能查看產生衝突的信息。我們通過vim打開paper.txt文件可以看見如下內容:

 1Oh, my god, I will write my graduate paper.
2Come on, Taoye!
3
4I have finished the summary today!
5Some typos have been fixed.
6I'm writing the content of my paper.
7<<<<<<< HEAD
8
9the second operation.
10=======
11
12the first operation.
13>>>>>>> dev
14

對此,我們要想解決衝突,則需要在master主分支中手動編輯該文件,編輯並保存完成之後,在將文件提交到版本庫即可:

1vim paper.txt
2git add paper.txt
3git commit -m "solve the conflict" paper.txt
4
5# 查看分支的合併情況
6git log --graph --pretty=oneline --abbrev-commit

下面原則來自廖大大的Git教程:https://www.liaoxuefeng.com/wiki/896043488029600/900005860592480
在實際開發中,我們應該按照幾個基本原則進行分支管理:

  1. 首先,master分支應該是非常穩定的,也就是僅用來發布新版本,平時不能在上面幹活;
  2. 那在哪幹活呢?幹活都在dev分支上,也就是說,dev分支是不穩定的,到某個時候,比如1.0版本發布時,再把dev分支合併到master上,在master分支發布1.0版本;
  3. 你和你的小夥伴們每個人都在dev分支上幹活,每個人都有自己的分支,時不時地往dev分支上合併就可以了。

所以,團隊合作的分支看起來就像這樣:

多人協作下的衝突解決

現在導師給Taoye提出的新需求是:两天內完成論文的初稿並提交。

Taoye大量的時間都花費在開黑中了,两天內完成這個任務對Taoye來說有點困難。於是乎,Taoye想要找妹妹幫忙完成論文的part1部分,而自己完成part2部分。如此一來,兩個進程協同進行,也就能完美的實現了導師給的需求。妙哉,妙哉!Taoye心想到這,立馬提交自己論文的半成品到遠程倉庫,然後給妹妹提供一個git鏈接供其clone。

1# Taoye將本地論文提交到遠程倉庫
2$ git push origin master
1# 妹妹將遠程倉庫克隆到本地
2$ git clone XXXXXX.git

OK,既然兩人的本地都有了論文文件,那麼接下來就要開始并行完成各自的任務了,為了避免自己的操作給主分支帶來不好的影響,於是在master分支中創建一個dev分支用來編輯文件。由於妹妹的效率比Taoye要快,所以率先完成了論文part1部分的內容,對此,妹妹有如下操作:

1# 妹妹的操作
2$ git branch dev
3$ git checkout dev
4$ vim paper.txt

並且在paper.txt添加內容:sister have finished the part1.,part1部分的內容完成之後,將dev分支迅速推送至遠程倉庫:

1$ git add paper.txt
2$ git commit -m "sister have finished the part1" paper.txt
3$ git push origin dev

OK,Taoye交給妹妹的任務已經完成了,於是就興奮的出去玩耍了。畢竟這篇論文是屬於Taoye的,所以還是需要認真的完成,自然花費的時間也就更多了。經歷了一晚上通宵的時間,終於是順利完成了part2部分的內容,於是屁顛屁顛的將論文提交至遠程倉庫:

1# Taoye的操作
2$ git checkout dev
3$ vim paper.txt
4# 在paper.txt添加一句:taoye have finished the part2
5$ git add paper.txt
6$ git commit -m "taoye have finished the part2" paper.txt
7$ git push origin dev

而就在Taoye推送論文到遠程倉庫的時候,由於自己推送內容與妹妹推送的內容不一致,所以導致推送失敗。我們通過Git給出的提示信息可以知道,要想解決這個衝突,首先需要通過pull命令將最新的提交拉取下來,然後與本地合併,解決衝突之後再推送到遠程倉庫。為此,Taoye立馬執行了pull命令:

1$ git pull

而在執行pull命令的時候,糟糕的問題又接踵而至了,Git提示說:There is no tracking information for the current branch.,也就是說本地的分支與遠程沒有建立鏈接。對此,我們可以建立鏈接后再執行pull命令:

1$ git branch --set-upstream-to=origin/dev dev
2$ git pull

雖然可以執行pull命令,但是會出現衝突提示,所以我們需要首先手動解決衝突,解決的方式和上節中一樣:對paper.txt文件進行編輯,然後提交並推送至遠程倉庫。所以,Taoye對paper.txt文件進行編輯之後,內容如下:

1taoye have finished the part1
2taoye have finished the part2

編輯好后,將文件保存並推送至遠程倉庫:

1$ git add paper.txt
2$ git commit -m "finished the part1 and part2" paper.txt
3$ git push origin dev

所以,在多人協作工作時我們一般准守如下過程:

  1. 完成任務后首先使用git push origin xxx推送至遠程倉庫
  2. 如果推送失敗,則需要執行git pull命令將最新的提交拉取下來
  3. 如果拉取失敗,則可能需要建立連接,所以執行git branch --set-upstream-to=origin/xxx xxx命令
  4. 解決之後,再次執行git pull命令嘗試拉取最新提交
  5. 此時,我們需要對衝突文件進行修改,等到修改完成之後,將文件推送至遠程倉庫

六、標籤管理

在上面的內容中,我們有介紹過,可以根據版本號(很長的字符串)來實現任意版本之間的穿越。但是通過這種較長字符串版本號的形式對用戶並不是特別友好,看的腦闊有點疼。所以我們一般可以給每一個版本貼上一個標籤,並且可以通過這個標籤來定位任意的版本,從而更加容易的實現版本穿越等需求。

在這,我們也可以將標籤看做是版本號的一個別名,使用標籤就相當於使用版本號,兩者的作用是等價的。如果有使用過Docker的話,應該會知道,在Docker中,我們每創建一個容器,Docker都會分配一個容器id以便於我們定位對應的容器,而我們自己也可以為每一個容器定義一個別名。我們在使用一系列的Docker命令對容器進行操作的時候,既可以通過容器id,也可以通過別名的形式來進行操作。

Git中,我們主要是採用tag命令來管理標籤,由於標籤比較的簡單,這裏就不一一講解了,與標籤相關的命令主要有以下一些:

 1$ git tag                       # 查看所有標籤
2$ git tag v1.0                  # 沒有指定版本,則默認給當前版本打標籤
3# 查看各個版本信息(版本號)
4$ git reflog paper.txt | git log --pretty=oneline paper.txt
5$ git tag v0.9 a35d6b           # 根據版本號來打標籤
6# 打標籤的同時,給標籤添上相應的說明
7$ git tag -a v0.1 -m "version 0.1 released" a35d6b
8$ git show v1.0                 # 查看對應的標籤信息
9$ git tag -d v0.9               # 刪除標籤
10
11$
 git reset --hard v0.9         # 根據標籤實現版本穿越

總結

總的來講,Git上手還是很快的,關鍵在於平時需要加強Git操作命令的訓練。以上關於Git的命令只是一部分,但如果掌握上述內容,還是可以輕鬆滿足Git的實際需求的。如果有興趣想要了解更多關於Git的知識,可以自行查閱相關的文檔或是書籍來進一步的學習。

參考資料

一個小時學會Git:https://www.cnblogs.com/best/p/7474442.html#!comments
廖雪峰Git教程:https://www.liaoxuefeng.com/wiki/896043488029600

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

【其他文章推薦】

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

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

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

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

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

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

四、歸併排序 && 快速排序

一、歸併排序 Merge Sort

1.1、實現原理

  • 如果要排序一個數組,我們先把數組從中間分成前後兩部分,然後對前後兩部分分別排序,再將排好序的兩部分合併在一起,這樣整個數組就都有序了。
  • 歸併排序使用的就是分治思想。分治,顧名思義,就是分而治之,將一個大問題分解成小的子問題來解決。小的子問題解決了,大問題也就解決了。
  • 分治思想跟遞歸思想很像。分治算法一般都是用遞歸來實現的。 分治是一種解決問題的處理思想,遞歸是一種編程技巧,這兩者並不衝突。
  • 寫遞歸代碼的技巧就是,分析得出遞推公式,然後找到終止條件,最後將遞推公式翻譯成遞歸代碼。所以,要想寫出歸併排序的代碼,我們先寫出歸併排序的遞推公式。
  • 遞推公式:erge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))
  • 終止條件:p >= r 不用再繼續分解
  • merge_sort(p…r)表示,給下標從 p 到 r 之間的數組排序。
  • 我們將這個排序問題轉化為了兩個子問題, merge_sort(p…q) 和 merge_sort(q+1…r),其中下標 q 等於 p 和 r 的中間位置,也就是 (p+r)/2。
  • 當下標從 p 到 q 和從 q+1 到 r 這兩個子數組都排好序之後,我們再將兩個有序的子數組合併在一起,這樣下標從 p 到 r 之間的數據就也排好序了。
  • 實現思路如下:
/**
 * 歸併排序
 * @param arr 排序數據
 * @param n   數組大小
 */
public static void merge_sort(int[] arr, int n) {
    merge_sort_c(arr, 0, n - 1);
}

// 遞歸調用函數
public static void merge_sort_c(int[] arr, int p, int r) {
    // 遞歸終止條件
    if (p >= r) {
        return;
    }
    // 取p到r之間的中間位置q
    int q = (p + r) / 2;

    // 分治遞歸
    merge_sort_c(arr, p, q);
    merge_sort_c(arr, q + 1, r);
    // 將 arr[p...q] 和 arr[q+1...r] 合併為 arr[p...r]
    merge(arr[p...r],arr[p...q],arr[q + 1...r]);
}
  • merge(arr[p…r], arr[p…q], arr[q + 1…r]) 這個函數的作用就是,將已經有序的 arr[p…q] 和 arr[q+1…r] 合併成一個有序的數組,並且放入 arr[p…r]。
  • 如下圖所示,我們申請一個臨時數組 tmp,大小與 arr[p…r] 相同。
  • 我們用兩個游標 i 和 j,分別指向 arr[p…q] 和 arr[q+1…r] 的第一個元素。
  • 比較這兩個元素 arr[i] 和 arr[j],如果 arr[i] <= arr[j],我們就把 arr[i] 放入到臨時數組 tmp,並且 i 后移一位,否則將 arr[j] 放入到數組 tmp,j 后移一位。
  • 繼續上述比較過程,直到其中一個子數組中的所有數據都放入臨時數組中,再把另一個數組中的數據依次加入到臨時數組的末尾,這個時候,臨時數組中存儲的就是兩個子數組合併之後的結果了。
  • 最後再把臨時數組 tmp 中的數據拷貝到原數組 arr[p…r] 中。
/**
 * merge 合併函數
 * @param arr 數組
 * @param p   數組頭
 * @param q   數組中間位置
 * @param r   數組尾
 */
public static void merge(int[] arr, int p, int q, int r) {
    if (r <= p) return;

    // 初始化變量i j k
    int i = p;
    int j = q + 1;
    int k = 0;

    // 申請一個大小跟A[p...r]一樣的臨時數組
    int[] tmp = new int[r - p + 1];

    // 比較排序移動到臨時數組
    while ((i <= q) && (j <= r)) {
        if (arr[i] <= arr[j]) {
            tmp[k++] = arr[i++];
        } else {
            tmp[k++] = arr[j++];
        }
    }

    // 判斷哪個子數組中有剩餘的數據
    int start = i, end = q;
    if (j <= r) {
        start = j;
        end = r;
    }

    // 將剩餘的數據拷貝到臨時數組tmp
    while (start <= end) {
        tmp[k++] = arr[start++];
    }

    // 將tmp中的數組拷貝回 arr[p...r]
    for (int a = 0; a <= r - p; a++) {
        arr[p + a] = tmp[a];
    }
}

1.2、性能分析

  • 歸併排序穩不穩定關鍵要看 merge() 函數,也就是兩個有序子數組合併成一個有序數組的那部分代碼。
  • 在合併的過程中,如果 arr[p…q] 和 arr[q+1…r] 之間有值相同的元素,那我們可以像偽代碼中那樣,先把 arr[p…q] 中的元素放入 tmp 數組。
  • 這樣就保證了值相同的元素,在合併前後的先後順序不變。所以,歸併排序是一個穩定的排序算法
  • 其時間複雜度是非常穩定的,不管是最好情況、最壞情況,還是平均情況,時間複雜度都是 O(nlogn)
  • 歸併排序的合併函數,在合併兩個有序數組為一個有序數組時,需要藉助額外的存儲空間。
  • 儘管每次合併操作都需要申請額外的內存空間,但在合併完成之後,臨時開闢的內存空間就被釋放掉了。在任意時刻,CPU 只會有一個函數在執行,也就只會有一個臨時的內存空間在使用。
  • 臨時內存空間最大也不會超過 n 個數據的大小,所以空間複雜度是 O(n),不是原地排序算法。

二、快速排序 Quicksort

2.1、實現原理

  • 快排的思想是:如果要排序數組中下標從 p 到 r 之間的一組數據,可以選擇 p 到 r 之間的任意一個數據作為 pivot(分區點)。
  • 遍歷 p 到 r 之間的數據,將小於 pivot 的放到左邊,將大於 pivot 的放到右邊,將 pivot 放到中間。
  • 經過這一步驟之後,數組 p 到 r 之間的數據就被分成了三個部分,前面 p 到 q-1 之間都是小於 pivot 的,中間是 pivot,後面的 q+1 到 r 之間是大於 pivot 的。
  • 根據分治、遞歸的處理思想,可以用遞歸排序下標從 p 到 q-1 之間的數據和下標從 q+1 到 r 之間的數據,直到區間縮小為 1,就說明所有的數據都有序了。
  • 用遞推公式來將上面的過程寫出來的話,就是這樣:quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)。
  • 終止條件:p >= r
/**
 * 快速排序
 * @param arr 排序數組
 * @param p 數組頭
 * @param r 數組尾
 */
public static void quickSort(int[] arr, int p, int r) {
    if (p >= r) 
        return;
    // 獲取分區點 並移動數據
    int q = partition(arr, p, r);
    quickSort(arr, p, q - 1);
    quickSort(arr, q + 1, r);
}

partition() 分區函數:

  • 是隨機選擇一個元素作為 pivot(一般情況下,可以選擇 p 到 r 區間的最後一個元素),然後對 arr[p…r] 分區,並將小於 pivot 的放右邊,大於的放左邊,函數返回 pivot 的下標。

partition() 的實現有兩種方式:

  • 一種是不考慮空間消耗,此時非常簡單。

    • 申請兩個臨時數組 X 和 Y,遍歷 arr[p…r],將小於 pivot 的元素都拷貝到臨時數組 X,將大於 pivot 的元素都拷貝到臨時數組 Y,最後再將數組 X 和數組 Y 中數據順序拷貝到arr[p…r]。
    /**
     * 分區函數方式一
     *
     * @param arr 數組
     * @param p   上標
     * @param r   下標
     * @return 函數返回 pivot 的下標
     */
    public static int partition1(int[] arr, int p, int r) {
        int[] xArr = new int[r - p + 1];
        int x = 0;
    
        int[] yArr = new int[r - p + 1];
        int y = 0;
    
        int pivot = arr[r];
    
        // 將小於 pivot 的元素都拷貝到臨時數組 X,將大於 pivot 的元素都拷貝到臨時數組 Y
        for (int i = p; i < r; i++) {
            // 小於 pivot 的存入 xArr 數組
            if (arr[i] < pivot) {
                xArr[x++] = arr[i];
            }
            // 大於 pivot 的存入 yArr 數組
            if (arr[i] > pivot) {
                yArr[y++] = arr[i];
            }
        }
    
        int q = x + p;
        // 再將數組 X 和數組 Y 中數據順序拷貝到 arr[p…r]
        for (int i = 0; i < x; i++) {
            arr[p + i] = xArr[i];
        }
        arr[q] = pivot;
        for (int i = 0; i < y; i++) {
            arr[q + 1 + i] = yArr[i];
        }
    
        return q;
    }
    
  • 另外一種有點類似選擇排序。

    • 我們通過游標 i 把 arr[p…r-1] 分成兩部分。arr[p…i-1] 的元素都是小於 pivot 的,我們暫且叫它“已處理區間”,arr[i…r-1] 是“未處理區間”。
    • 我們每次都從未處理的區間 arr[i…r-1] 中取一個元素 arr[j],與 pivot 對比,如果小於 pivot,則將其加入到已處理區間的尾部,也就是 arr[i]的位置。
    • 在數組某個位置插入元素,需要搬移數據,非常耗時。此時可以採用交換,在 O(1) 的時間複雜度內完成插入操作。需要將 arr[i] 與 arr[j] 交換,就可以在 O(1)時間複雜度內將 arr[j] 放到下標為 i 的位置。
    /**
     * 分區函數方式二
     * @param arr 數組
     * @param p   上標
     * @param r   下標
     * @return 函數返回pivot的下標
     */
    public static int partition2(int[] arr, int p, int r) {
        int pivot = arr[r];
        int i = p;
        for (int j = p; j < r; j++) {
            if (arr[j] < pivot) {
                if (i == j) {
                    ++i;
                } else {
                    int tmp = arr[i];
                    arr[i++] = arr[j];
                    arr[j] = tmp;
                }
            }
        }
        int tmp = arr[i];
        arr[i] = arr[r];
        arr[r] = tmp;
        return i;
    }
    

2.2、性能分析

  • 因為分區的過程涉及交換操作,如果數組中有兩個相同的元素,比如序列 6, 8, 7, 6, 3, 5, 9, 4,在經過第一次分區操作之後,兩個 6 的相對先後順序就會改變。所以,快速排序並不是穩定的排序算法
  • 按照上面的第二種分區方式,快速排序只涉及交換操作,所以空間複雜度為 Q(1),是原地排序算法
  • 時間複雜度為 Q(nlogn),最差為Q(n²)

三、兩者對比

歸併排序 快速排序
排序思想 處理過程由下到上,先處理子問題,然後在合併 由上到下,先分區,在處理子問題
穩定性
空間複雜度 Q(n) Q(1) 原地排序算法
時間複雜度 都為 O(nlogn) 平均為 O(nlogn),最差為 O(n²)
  • 歸併之所以是非原地排序算法,主要原因是合併函數無法在原地執行。快速排序通過設計巧妙的原地分區函數,可以實現原地排序,解決了歸併排序佔用太多內存的問題。
  • 歸併排序算法是一種在任何情況下時間複雜度都比較穩定的排序算法,這也使它存在致命的缺點,即歸併排序不是原地排序算法,空間複雜度比較高,是 O(n)。正因為此,它也沒有快排應用廣泛。

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

【其他文章推薦】

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

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

※想知道最厲害的網頁設計公司"嚨底家"!

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

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

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

循序漸進VUE+Element 前端應用開發(7)— 介紹一些常規的JS處理函數

在我們使用VUE+Element 處理界面的時候,往往碰到需要利用JS集合處理的各種方法,如Filter、Map、reduce等方法,也可以涉及到一些對象屬性賦值等常規的處理或者遞歸的處理方法,以前對於這些不是很在意,但往往真正使用的時候,需要了解清楚,否則很容易腦袋出現短路的情況。本篇隨筆列出一些在VUE+Element 前端開發中經常碰到的JS處理場景,供參考學習。

1、常規集合的filter、map、reduce處理方法

filter函數的主要用途是對數組元素進行過濾,並返回一個符合條件的元素的數組

const nums = [10,20,30,111,222,333]
let newNums=nums.filter(function(n){
    return n<100
})

輸出:[10,20,30]

 

map函數是對數組每個元素的映射操作,並返回一個新數組,原數組不會改變將newNums中每個数字乘2

const nums = [10,20,30,111,222,333]
let newNums=nums.filter(function(n){
    return n*2
})

輸出:[20,40,60,222,666]

 

reduce函數主要用於對數組所有元素的匯總操作,如全部相加、相乘等

const nums = [10,20,30,111,222,333]
let newNums=nums.reduce(function(preValue,n){
    return PreValue+n
},0)

輸出:726

 

有時候可以結合幾種處理方式一起,如下綜合案例所示。

const nums = [10,20,30,111,222,333]
let newNums=nums.filter(function(n){
    return n<100
}).map(function(n){
    return n*2
}).reduce(function(preValue,n){
    return preValue+n
},0)

結果:120

 

另外還有一個數組集合的find方法,和filter方法類似。

find()方法主要用來返回數組中符合條件的第一個元素(沒有的話,返回undefined)

 var Array = [1,2,3,4,5,6,7];
 var result = Array.find(function(value){
     return value > 5;   //條件
 });
 console.log(result);//6
 console.log(Array);//[1,2,3,4,5,6,7]

 

同樣我們也可以在vue裏面,利用require.context的處理機制,遍歷文件進行處理,也需要用到了filter,如下代碼所示。

下面代碼是我對某個文件夾裏面的文件進行一個過濾處理操作

const req = require.context('vue-awesome/icons', true, /\.js$/)
const requireAll = requireContext => requireContext.keys()

const re = /\.\/(.*)\.js/

const vueAwesomeIcons = requireAll(req).filter((key) => { return key.indexOf('index.js') < 0 }).map(i => {
  return i.match(re)[1]
})

export default vueAwesomeIcons

 

2、遞歸處理

有時候,我們需要從一個JSON集合裏面,由於集合是嵌套的,如children裏面還有chilren集合,根據某個關鍵屬性進行查詢,這種處理方式就要用到遞歸了。

例如我定義的一個菜單集合裏面,就是這樣一個嵌套的結構,需要根據名稱來獲得對應的對象的時候,就涉及到了一個遞歸處理函數。

首先我們來看看菜單的JSON集合。

// 此菜單數據一般由服務器端返回
export const asyncMenus = [
  {
    id: '1',
    pid: '-1',
    text: '首頁',
    icon: 'dashboard',
    name: 'dashboard'
  },
  {
    id: '2',
    pid: '-1',
    text: '產品信息',
    icon: 'table',
    children: [
      {
        id: '2-1',
        pid: '2',
        text: '產品展示',
        name: 'product-show',
        icon: 'table'
      }]
  },
  {
    id: '3',
    pid: '-1',
    text: '雜項管理',
    icon: 'example',
    children: [
      {
        id: '3-1',
        pid: '3',
        text: '圖標管理',
        name: 'icon',
        icon: 'example'
      },
      {
        id: '3-3',
        pid: '3',
        text: '樹功能展示',
        name: 'tree',
        icon: 'tree'
      },
      {
        id: '3-2',
        pid: '3',
        text: '二級菜單2',
        icon: 'tree',
        children: [
          {
            id: '3-2-2',
            pid: '3-2',
            text: '三級菜單2',
            name: 'menu1-1',
            icon: 'form'
          }
        ]
      }
    ]
  }
]

如果我們需要根據ID來遍歷查詢,就是一個典型的遞歸查詢處理。

    // 根據菜單id來獲取對應菜單對象
    FindMenuById(menuList, menuid) {
      for (var i = 0; i < menuList.length; i++) {
        var item = menuList[i];
        if (item.id && item.id === menuid) {
          return item
        } else if (item.children) {
          var foundItem = this.FindMenuById(item.children, menuid)
          if (foundItem) { // 只有找到才返回
            return foundItem
          }
        }
      }
    }

這裏值得注意的是,不能在遞歸的時候,使用下面直接返回


return this.FindMenuById(item.children, menuid)

而需要判斷是否有結果在進行返回,否則嵌套遞歸就可能返回undefined類型

  var foundItem = this.FindMenuById(item.children, menuid)
  if (foundItem) { // 只有找到才返回
    return foundItem
  }

 

3、forEach遍歷集合處理

在很多場合,我們也需要對集合進行一個forEach的遍歷處理,如下根據它的鍵值進行處理,註冊全局過濾器的處理操作

// 導入全局過濾器
import * as filters from './filters'
// 註冊全局過濾器
Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key])
})

或者我們在通過API方式獲取數據后,對集合進行處理的操作

    // 獲取產品類型,用於綁定字典等用途
    GetProductType().then(data => {
      if (data) {
        this.treedata = [];// 樹列表清空
        data.forEach(item => {
          this.productTypes.set(item.id, item.name)
          this.typeList.push({ key: item.id, value: item.name })

          var node = { id: item.id, label: item.name }
          this.treedata.push(node)
        })

        // 獲取列表信息
        this.getlist()
      }
    });

又或者請求字典數據的時候,進行一個非空值的判斷處理。

      // 使用字典類型,從服務器請求數據
      GetDictData(this.typeName).then(data => {
        if (data) {
          data.forEach(item => {
            if (item && typeof (item.Value) !== 'undefined' && item.Value !== '') {
              that.dictItems.push(item)
            }
          });
        }
      })

forEach()方法也是用於對數組中的每一個元素執行一次回調函數,但它沒有返回值(或者說它的返回值為undefined,即便我們在回調函數中寫了return語句,返回值依然為undefined)

注意: 如果forEach里有兩個參數,則第一個參數為該集合里的元素,第二個參數為集合的索引;

 

4、Object.assign賦值方法

在有些場合,我們需要把全新的集合,複製到另一個對象上,替換原來對象的屬性值,那麼我們可以利用Object對象的assign方法。

如在編輯界面展示的時候,把請求到的對象屬性複製到表單對象上。

      var param = { id: id }
      GetProductDetail(param).then(data => {
        Object.assign(this.editForm, data);
      })

或者查詢的時候,獲得查詢條件,進行部分替換

      // 構造常規的分頁查詢條件
      var param = {
        type: this.producttype === 'all' ? '' : this.producttype,
        pageindex: this.pageinfo.pageindex,
        pagesize: this.pageinfo.pagesize
      };

      // 把SearchForm的條件加入到param裏面,進行提交查詢
      param.type = this.searchForm.ProductType // 轉換為對應屬性
      Object.assign(param, this.searchForm);

 

5、slice() 方法

slice() 方法可從已有的數組中返回選定的元素。

語法如下所示。

arrayObject.slice(start,end)

如下案例所示。

let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)

或者我們結合filter函數對圖標集合進行獲取部分處理

vueAwesomeIconsFiltered: function() {
  const that = this
  var list = that.vueAwesomeIcons.filter(item => { return item.indexOf(that.searchForm.label) >= 0 })
  if (that.searchForm.pagesize > 0) {
    return list.slice(0, that.searchForm.pagesize)
  } else {
    return list;
  }
}

 

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

【其他文章推薦】

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

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

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

「一帶一路」再受挫 泰國喊停中國湄公河開發計畫

摘錄自2020年2月6日聯合新聞網報導

泰國宣布停止由中國主導的湄公河開發計畫,理由是當地民眾反對,且北京不願提高經費在要開發的地區進行進一步調查。泰國政府副發言人特薩拉薩納庫爾(Trisulee Trisaranakul)表示,泰國政府內閣會議4日決定放棄這個項目。

路透報導,中國在2001年提出計畫,對湄公河進行疏浚,以方便大型貨船從中國的雲南省沿湄公河把商品運往泰國、老撾的港口和東南亞其他地區。

但這個計畫遭到湄公河沿岸的泰國社區民眾和環保人士的反對。他們擔心疏浚計畫會影響環境,而且只能讓中國獲利。一份泰國政府內閣的文件顯示,中國去年通知湄公河流沿岸的國家說,中國不再計畫繼續推動這個項目,可老撾和緬甸境內的疏浚工作依然繼續進行。

土地水文
生物多樣性
土地利用
國際新聞
泰國
湄公河
一帶一路
商業開發
水文

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

【其他文章推薦】

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

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

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

特斯拉達成「第100萬輛」電動車里程碑 單季產能也意外曝光

摘錄自2020年3月11日ETtoday報導

自2003年創立的美國電動車廠特斯拉(Tesla),近日在一輛紅色Model Y從產線離開後,正式達成量產第100萬台電動車的里程碑。儘管目前Model Y電動休旅車尚未進入交車階段,但執行長Elon Musk也提前在推特上特別慶祝一番。

實際來看,特斯拉達成量產100萬輛的里程碑並不讓人意外,因為除了美國Fremont工廠的每年50萬輛的產能外,特斯拉上海工廠也在去年正式完工, 並預計在今年生產15萬輛新車,若非新冠病毒(COVID-19)疫情影響,這項紀錄將會更早達成。

從特斯拉Fremont工廠、上海工廠的年產量來看,預計很快就會迎來第200萬輛的紀錄。

生活環境
能源轉型
國際新聞
美國
特斯拉
電動車
交通運輸

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

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

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

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

防疫比環保重要?新罕布什爾州暫時重推塑膠袋

摘錄自2020年03月22日中央通訊社美國報導

美國新罕布什爾州長蘇努努今天(22日)敦促州內民眾,將可重複使用的環保袋留在家中暫時不用。他說,為對抗武漢肺炎,在此過渡期,購物宜用店家提供的新塑膠袋或紙袋。

此舉主要是愈發擔心賣場員工面對店內人潮擁擠,加上新型冠狀病毒疾病(COVID-19,武漢肺炎)具高度傳染性,且可能附著於各種物件表面,增加感染風險。州長蘇努努(Christopher Sununu)今天推文說:「由於確認(武漢肺炎病毒)社區傳染,顧及賣場裝袋人員、雜貨商和顧客潛在風險,購物者將環保袋暫留家中,這很重要。」

本週在新英格蘭醫學期刊(New England Journal of Medicine)發表的研究顯示,武漢肺炎病毒可在空氣中存活數小時,而在不同的物件表面甚至可存活數天之久。

公害污染
污染治理
國際新聞
美國
環保袋
武漢肺炎
疫情下的食衣住行
廢棄物

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

疫情衝擊泰國觀光業 大象賺不到生活費恐餓死

摘錄自2020年3月31日中央社報導

隨著武漢肺炎大流行讓觀光客銳減,倡議人士警告,在泰國旅遊業工作的許多飢餓並遭長時間鍊住的大象,可能會因沒有收入而餓死、被賣給動物園,或轉入非法伐木業。

在疫情爆發前,在泰國旅遊業工作的約2000頭大象生活已不容易,時常傳出虐待情事,好馴化牠們提供乘載服務或在動物秀上表演雜耍賺錢。但因全球旅遊業停擺,大象現在甚至賺不到一天必須的300公斤食物錢。

大象生態營與保育人士警告,再不獲得緊急援助,象群即將面臨飢餓及新的剝削威脅。泰國大象聯盟協會(Thai Elephant Alliance Association)主席提拉帕(Theerapat Trungprakan)表示,隨著病毒重挫泰國觀光業,約2000頭大象現在失業了。

國際新聞
泰國
大象
觀光業
動物與大環境變遷
武漢肺炎
展示動物
動物福利

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

※回頭車貨運收費標準

荷蘭研究:城市污水測得病毒 或可扮演疫情預警

摘錄自2020年3月31日中央社報導

荷蘭科學家可以在一個城市的2019冠狀病毒疾病(COVID-19,武漢肺炎)病例通報前,在都市污水中發現引發這種疾病的冠狀病毒,顯示這或許有可能成為新疫情早期預警系統。

所謂的SARS-CoV-2冠狀病毒通常會從感染者的糞便中排出。位於荷蘭中部城市尼沃海恩(Nieuwegein)的KWR水資源研究所(KWR Water Research Institute)的首席微生物學家麥德瑪(Gertjan Medema)和他的同僚今(30日)表示,儘管污水不太可能成為重要的傳播途徑。但病原體在社區裡不斷增加的循環會增加它流入下水道系統的數量。

他們3月5日在阿默斯福特(Amersfoort)一座污水處理廠發現武漢肺炎的遺傳物質,當時這個位於阿姆斯特丹東南方約50公里處的城市還沒傳出任何病例。荷蘭2月27日出現境內首起2019冠狀病毒疾病病例,幾天後南部的醫療人員感染生病,顯示疫情已在社區蔓延。

他們表示,即使在武漢肺炎盛行率還很低時,就可以在污水中偵測到引發這種疾病的冠狀病毒,顯示這可以當成監控這種病毒在人口中循環的高敏感度工具。

公害污染
污染治理
國際新聞
荷蘭
疫情
水污染

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

分解塑膠救星?科學家在垃圾場中發現吃聚氨酯的微生物

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

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

【其他文章推薦】

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

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

※想知道最厲害的網頁設計公司"嚨底家"!

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

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

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

Java多線程之深入解析ThreadLocal和ThreadLocalMap

ThreadLocal概述

ThreadLocal是線程變量,ThreadLocal中填充的變量屬於當前線程,該變量對其他線程而言是隔離的。ThreadLocal為變量在每個線程中都創建了一個副本,那麼每個線程可以訪問自己內部的副本變量。

它具有3個特性:

  1. 線程併發:在多線程併發場景下使用。
  2. 傳遞數據:可以通過ThreadLocal在同一線程,不同組件中傳遞公共變量。
  3. 線程隔離:每個線程變量都是獨立的,不會相互影響。

在不使用ThreadLocal的情況下,變量不隔離,得到的結果具有隨機性。

public class Demo {
    private String variable;

    public String getVariable() {
        return variable;
    }

    public void setVariable(String variable) {
        this.variable = variable;
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                demo.setVariable(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
            }).start();
        }
    }
}

輸出結果:

Thread-2 Thread-2
Thread-4 Thread-4
Thread-1 Thread-2
Thread-0 Thread-2
Thread-3 Thread-3

View Code

在不使用ThreadLocal的情況下,變量隔離,每個線程有自己專屬的本地變量variable,線程綁定了自己的variable,只對自己綁定的變量進行讀寫操作。

public class Demo {
    private ThreadLocal<String> variable = new ThreadLocal<>();

    public String getVariable() {
        return variable.get();
    }

    public void setVariable(String variable) {
        this.variable.set(variable);
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                demo.setVariable(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
            }).start();
        }
    }
}

輸出結果:

Thread-0 Thread-0
Thread-1 Thread-1
Thread-2 Thread-2
Thread-3 Thread-3
Thread-4 Thread-4

View Code

synchronized和ThreadLocal的比較

上述需求,通過synchronized加鎖同樣也能實現。但是加鎖對性能和併發性有一定的影響,線程訪問變量只能排隊等候依次操作。TreadLocal不加鎖,多個線程可以併發對變量進行操作。

public class Demo {
    private String variable;
    public String getVariable() {
        return variable;
    }

    public void setVariable(String variable) {
        this.variable = variable;
    }

    public static void main(String[] args) {
        Demo demo = new Demo1();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                synchronized (Demo.class){
                    demo.setVariable(Thread.currentThread().getName());
                    System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
                }
            }).start();
        }
    }
}

ThreadLocal和synchronized都是用於處理多線程併發訪問資源的問題。ThreadLocal是以空間換時間的思路,每個線程都擁有一份變量的拷貝,從而實現變量隔離,互相不干擾。關注的重點是線程之間數據的相互隔離關係。synchronized是以時間換空間的思路,只提供一個變量,線程只能通過排隊訪問。關注的是線程之間訪問資源的同步性。ThreadLocal可以帶來更好的併發性,在多線程、高併發的環境中更為合適一些。

ThreadLocal使用場景

轉賬事務的例子

JDBC對於事務原子性的控制可以通過setAutoCommit(false)設置為事務手動提交,成功后commit,失敗后rollback。在多線程的場景下,在service層開啟事務時用的connection和在dao層訪問數據庫的connection應該要保持一致,所以併發時,線程只能隔離操作自已的connection。

解決方案1:service層的connection對象作為參數傳遞給dao層使用,事務操作放在同步代碼塊中。

存在問題:傳參提高了代碼的耦合程度,加鎖降低了程序的性能。

解決方案2:當需要獲取connection對象的時候,通過ThreadLocal對象的get方法直接獲取當前線程綁定的連接對象使用,如果連接對象是空的,則去連接池獲取連接,並通過ThreadLocal對象的set方法綁定到當前線程。使用完之後調用ThreadLocal對象的remove方法解綁連接對象。

ThreadLocal的優勢:

  1. 可以方便地傳遞數據:保存每個線程綁定的數據,需要的時候可以直接獲取,避免了傳參帶來的耦合。
  2. 可以保持線程間隔離:數據的隔離在併發的情況下也能保持一致性,避免了同步的性能損失。

ThreadLocal的原理

每個ThreadLocal維護一個ThreadLocalMap,Map的Key是ThreadLocal實例本身,value是要存儲的值。

每個線程內部都有一個ThreadLocalMap,Map裏面存放的是ThreadLocal對象和線程的變量副本。Thread內部的Map通過ThreadLocal對象來維護,向map獲取和設置變量副本的值。不同的線程,每次獲取變量值時,只能獲取自己對象的副本的值。實現了線程之間的數據隔離。

JDK1.8的設計相比於之前的設計(通過ThreadMap維護了多個線程和線程變量的對應關係,key是Thread對象,value是線程變量)的好處在於,每個Map存儲的Entry數量變少了,線程越多鍵值對越多。現在的鍵值對的數量是由ThreadLocal的數量決定的,一般情況下ThreadLocal的數量少於線程的數量,而且並不是每個線程都需要創建ThreadLocal變量。當Thread銷毀時,ThreadLocal也會隨之銷毀,減少了內存的使用,之前的方案中線程銷毀后,ThreadLocalMap仍然存在。

ThreadLocal源碼解析

set方法

首先獲取線程,然後獲取線程的Map。如果Map不為空則將當前ThreadLocal的引用作為key設置到Map中。如果Map為空,則創建一個Map並設置初始值。

get方法

首先獲取當前線程,然後獲取Map。如果Map不為空,則Map根據ThreadLocal的引用來獲取Entry,如果Entry不為空,則獲取到value值,返回。如果Map為空或者Entry為空,則初始化並獲取初始值value,然後用ThreadLocal引用和value作為key和value創建一個新的Map。

 

remove方法

刪除當前線程中保存的ThreadLocal對應的實體entry。

initialValue方法

該方法的第一次調用發生在當線程通過get方法訪問線程的ThreadLocal值時。除非線程先調用了set方法,在這種情況下,initialValue才不會被這個線程調用。每個線程最多調用依次這個方法。

該方法只返回一個null,如果想要線程變量有初始值需要通過子類繼承ThreadLocal的方式去重寫此方法,通常可以通過匿名內部類的方式實現。這個方法是protected修飾的,是為了讓子類覆蓋而設計的。

ThreadLocalMap源碼分析

ThreadLocalMap是ThreadLocal的靜態內部類,沒有實現Map接口,獨立實現了Map的功能,內部的Entry也是獨立實現的。

與HashMap類似,初始容量默認是16,初始容量必須是2的整數冪。通過Entry類的數據table存放數據。size是存放的數量,threshold是擴容閾值。

 Entry繼承自WeakReference,key是弱引用,其目的是將ThreadLocal對象的生命周期和線程生命周期解綁。

弱引用和內存泄漏

內存溢出:沒有足夠的內存供申請者提供

內存泄漏:程序中已動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等驗證后溝。內存泄漏的堆積會導致內存溢出。

弱引用:垃圾回收器一旦發現了弱引用的對象,不管內存是否足夠,都會回收它的內存。

內存泄漏的根源是ThreadLocalMap和Thread的生命周期是一樣長的。

如果在ThreadLocalMap的key使用強引用還是無法完全避免內存泄漏,ThreadLocal使用完后,ThreadLocal Reference被回收,但是Map的Entry強引用了ThreadLocal,ThreadLocal就無法被回收,因為強引用鏈的存在,Entry無法被回收,最後會內存泄漏。

在實際情況中,ThreadLocalMap中使用的key為ThreadLocal的弱引用,value是強引用。如果ThreadLocal沒有被外部強引用的話,在垃圾回收的時候,key會被清理,value不會。這樣ThreadLocalMap就出現了為null的Entry。如果不做任何措施,value永遠不會被GC回收,就會產生內存泄漏。

ThreadLocalMap中考慮到這個情況,在set、get、remove操作后,會清理掉key為null的記錄(將value也置為null)。使用完ThreadLocal后最後手動調用remove方法(刪除Entry)。

也就是說,使用完ThreadLocal后,線程仍然運行,如果忘記調用remove方法,弱引用比強引用可以多一層保障,弱引用的ThreadLocal會被回收,對應的value會在下一次ThreadLocalMap調用get、set、remove方法的時候被清除,從而避免了內存泄漏。

Hash衝突的解決

ThreadLocalMap的構造方法

構造函數創建一個長隊為16的Entry數組,然後計算firstKey的索引,存儲到table中,設置size和threshold。

firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1)用來計算索引,nextHashCode是Atomicinteger類型的,Atomicinteger類是提供原子操作的Integer類,通過線程安全的方式來加減,適合高併發使用。

每次在當前值上加上一個HASH_INCREMENT值,這個值和斐波拉契數列有關,主要目的是為了讓哈希碼可以均勻的分佈在2的n次方的數組裡,從而盡量的避免衝突。

當size為2的冪次的時候,hashCode & (size – 1)相當於取模運算hashCode % size,位運算比取模更高效一些。為了使用這種取模運算, 所有size必須是2的冪次。這樣一來,在保證索引不越界的情況下,減少衝突的次數。

ThreadLocalMap的set方法

ThreadLocalMao使用了線性探測法來解決衝突。線性探測法探測下一個地址,找到空的地址則插入,若整個空間都沒有空餘地址,則產生溢出。例如:長度為8的數組中,當前key的hash值是6,6的位置已經被佔用了,則hash值加一,尋找7的位置,7的位置也被佔用了,回到0的位置。直到可以插入為止,可以將這個數組看成一個環形數組

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準