認識各種內存地址_網頁設計公司

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

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

什麼是物理地址?

物理地址就是內存單元的絕對地址,物理地址0x0000就表示內存條的第一個存儲單元,0x0010(16進制)就表示內存條的第17個存儲單元,一個存儲單元是1byte(8bit)。

你問為什麼是1byte?

一個內存條是由若干個黑色的內存顆粒構成的。每一個內存顆粒叫做一個chip。

每個chip中又疊了若干bank

在每個bank內部,就是電容的行列矩陣結構了,每一個元素有8個小電容,存儲8個bit,也就是一個字節。

什麼是線性地址和虛擬地址?

在80286系列以前,CPU只支持實模式操作模式。16位寄存器想要對20位地址線進行尋址,使用分段機制,段基址(16位) x 16(左移4位) + 段內偏移地址(4位)就是物理地址了。
這樣的好處是所見即所得,程序員指定的地址就是物理地址,物理地址對程序員是可見的。同時也帶來了一些問題:
1) 無法支持多任務。
2) 程序的安全性無法得到保證。
(根本原因就是一個程序直接修改了其他程序的內存,導致崩潰)

80286系列則是被設計來解決這些問題,段式訪存得到的改進,原來段基址+段內偏移得到的地址不再是實際的物理地址,而是被稱作為線性地址,要經過一個轉換層轉換才變成一個物理地址。這種CPU操作模式就被稱為保護模式了。保護的是:分清楚各個程序使用的存儲區域,不允許隨便跨界訪問。

80286系列之後就進入32位CPU時代了,32位寄存器可以直接訪問32位地址總線。但是在保護模式下,地址仍然採用“段地址:偏移地址”的方式來表示。
段值仍然由原來的16位的cs、ds等寄存器表示,但是此時它們僅僅是一個索引,這些個索引指向一個數據結構的表項,表項中詳細定義了一個段的起始地址、界限、屬性等內容,這個數據結構,叫做GDT(其實還可能是LDT),GDT中的每一個表項,叫做描述符。
這裏我們就詳細看一下保護模式的尋址方式吧。

1) 尋址時,先找到gdtr寄存器,從中得到GDT的基址。
2) 有了GDT的基址,又有段寄存器中保存的索引,可以得到段寄存器“所指”的那個表項,即所指的那個描述符。
3) 得到了描述符,就可以從描述符中得到該描述符所描述的那個段的起始地址。
4) 有了段的起始地址,將偏移地址拿過來與之相加,便能得到最後的線性地址。
5) 有了線性地址,經過變換,即可得到相應的物理地址。

保護模式雖然解決了內存不被跨界訪問,但是其也帶來了新的問題,那就是內存碎片。
首先我們了解一下內存碎片為何產生:
首先假設我們有10B內存:

ID 首地址 尾地址 長度 狀態
0 0 9 10 空閑

當程序申請一個長度為3的內存空間后:

ID 首地址 尾地址 長度 狀態
0 0 2 3
1 3 9 7 空閑

當程序再申請一個長度為2,以及長度為4的內存空間后:

ID 首地址 尾地址 長度 狀態
0 0 2 3
1 3 4 2
2 5 8 4
3 9 9 1 空閑

此時,只剩1個可用空間。如果這時程序再來申請長度大於1的空間,就申請不了,也就是內存不夠。
現在,釋放掉ID=1的空間:

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

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

ID 首地址 尾地址 長度 狀態
0 0 2 3
1 3 4 2 空閑
2 5 8 4
3 9 9 1 空閑

我們發現,現在可用內存空間為3,但是,這3個空閑空間,並不是連續的。
所以,如果程序現在申請長度為3的內存空間,同樣會申請不了,會出現內存不夠。我們把這種情況,稱之為內存碎片

那內存碎片怎麼解決呢?於是就有了分頁機制,接下來我們詳細講一下分頁:
首先,把物理內存,按照某種尺寸,進行平均分割。比如我現在以2個內存單位,來分割內存,也就是每兩個連續的內存空間,組成一個內存頁:

地址 頁ID 狀態
0 0 空閑
1
2 1 空閑
3
4 2 空閑
5
6 3 空閑
7
8 4 空閑
9

接着,系統同樣需要維護一個內存信息表:

ID 使用的內存頁ID

現在,程序申請長度為3的內存空間,不過由於現在申請的最小單位為頁面,而一個頁面的長度為2,因此現在需要申請2個頁面,也就是4個內存空間。你看,這就浪費了1個內存空間。

地址 頁ID 狀態
0 0
1
2 1
3
4 2 空閑
5
6 3 空閑
7
8 4 空閑
9
ID 使用的內存頁ID
0 0,1

接着,程序再申請長度為1,長度為2的空間:

地址 頁ID 狀態
0 0
1
2 1
3
4 2
5
6 3
7
8 4 空閑
9
ID 使用的內存頁ID
0 0,1
1 2
2 3

釋放掉ID=1,內存頁ID為2的那條內存空間信息:

地址 頁ID 狀態
0 0
1
2 1
3
4 2 空閑
5
6 3
7
8 4 空閑
9
ID 使用的內存頁ID
0 0,1
2 3

現在,就出現了之前的情況:目前一共有4個內存空間,但是不連續。
不過,因為現在是分頁管理機制,因此,現在仍然可以繼續申請長度為4的內存空間。
沒有碎片,能夠盡量地全部用完空間。但仔細想想,這種優勢背後,也是需要付出大量代價的。
分頁的方式下,程序需要記錄內存頁ID,每次使用時,需要從內存頁ID翻譯成實際內存地址,多了一次轉換。
而且這種模式,會浪費一些內存,比如上面申請3個內存空間,實際分配了2個頁面共4個內存空間,浪費了1個內存空間。

還有一個要注意的地方,這個時候”段基址+段內偏移地址”經過段部件處理后得到的線性地址就不再是物理地址了,而是虛擬地址了。
從下圖我們能夠清楚的看出來我們最後的線性地址表示的是頁表的地址,而不是物理地址了。

什麼是邏輯地址和有效地址?

無論CPU在什麼模式下,段內偏移地址又稱為有效地址或者邏輯地址(只是叫法不一樣罷了),例如實模式下mov ax, [0x7c00]0x7c00就是邏輯地址(或有效地址),但這條指令最終操作的物理地址是DS*16+0x7c00
Linux最初就是在32位的80386系列上設計的,並且沒有使用分段機制,所以在Linux上邏輯地址和線性地址就是一回事了。

總結

一圖以概括:

我的博客:https://geanqin.github.io/

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

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

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

分享一個新出爐的JVM里不痛不癢的BUG(Attach機制相關)_網頁設計

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

窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機。

本文來自: PerfMa技術社區

PerfMa(笨馬網絡)官網

概述

老早之前寫過一篇文章,關於attach機制的,可以看下這篇老文章了解一下JVM源碼分析之Attach機制實現完全解讀,比如大家常用的jstack,jmap等工具的主要原理都和attach機制有關,在JVM里處理這些命令的線程主要是Attach Listener這個線程,這個線程在JVM里是唯一的,我之前也一直以為是唯一的,但是我們同事最近在做一個線程分析產品的時候,發現我們抓到了多個Attach Listener線程,這讓我也很疑惑,我第一感覺是不可能,肯定是數據抓錯了,直到親眼看到了兩個同名的Attach Listener線程我才不得不相信原來還真有這種情況。

問題分析

不過從Attach Listener的實現來看,它設計的初衷不應該是一個多線程的設計,於是我昨晚上又翻了一遍代碼,發現還真可能存在這種情況。舉個栗子,當我們很多人同時執行jstack的時候,就可能會發生,當然有個前提是之前都沒有做過任何和attach相關的操作。

Attach Listener線程默認情況下不會在JVM啟動的時候就創建,當然也有一個JVM參數可以指定在JVM啟動的時候就啟動這個線程,這個就不會存在我們今天討論的這個問題了,這個JVM參數是-XX:+StartAttachListener

當我們在運行時觸發attach機制的時候,首先會通過Signal Dispatcher線程來創建Attach Listener線程,代碼如下:

在上面的圈起來的init方法里會創建Attach Listener線程,但是在init方法執行之前會通過_initialized屬性來判斷是否需要創建線程,而_initialized設置為true是在attach_listener_thread_entry里,這個是Attach Listener Thread的entry,也就是當這個線程執行的時候執行的方法。

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

網動是一群專業、熱情、向前行的工作團隊,我們擁有靈活的組織與溝通的能力,能傾聽客戶聲音,激發創意的火花,呈現完美的作品

但是在設置_initialized=true之前,如果有多個請求信號發出了(比如同時又很多jstack命令觸發),可能會創建多個Attach Listener,因為Signal DispatcherAttach Listener線程是異步執行的。

問題復現

為了讓效果更明顯,我們可以在hotspot里修改下代碼重新編譯下再跑demo

在上面函數里加上圈起來的這段代碼,表示在設置_initialized屬性之前停留15s,當進程起來之後,不斷執行jstack <pid>,最終將會看到有非常多的Attach Listener線程

其實問題的根本就是有一個空檔期(設置_initialized為true之前)可能存在多次創建線程的可能。

總結

總的來說,創建Attach Listener線程是通過Signal Dispatcher線程來創建的,但是決定Signal Dispatcher是否可以重複創建Attach Listener線程的標記是在某個Attach Listener線程里設置的,如果沒有及時設置該標記,就可能存在創建多個Attach Listener線程的情況。

 

一起來學習吧:

PerfMa KO 系列課之 JVM 參數【Memory篇】

實戰:一次疑似內存泄漏的問題排查

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

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

擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢

從源碼研究如何不重啟Springboot項目實現redis配置動態切換_包裝設計

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

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

上一篇Websocket的續篇暫時還沒有動手寫,這篇算是插播吧。今天講講不重啟項目動態切換redis服務。

背景

多個項目或微服務場景下,各個項目都需要配置redis數據源。但是,每當運維搞事時(修改redis服務地址或端口),各個項目都需要進行重啟才能連接上最新的redis配置。服務一多,修改各個項目配置然後重啟項目就非常蛋疼。所以我們想要找到一個可行的解決方案,能夠不重啟項目的情況下,修改配置,動態切換redis服務。

如何實現切換redis連接

剛遇到這個問題的時候,想必如果對spring-boot-starter-data-redis不是很熟悉的人,首先想到的就是去百度一下(安慰下自己:不要重複造輪子嘛)。

可是一陣百度之後,你找到的結果可能都是這樣的:

public ValueOperations updateRedisConfig() {
    JedisConnectionFactory jedisConnectionFactory = (JedisConnectionFactory) stringRedisTemplate.getConnectionFactory();
    jedisConnectionFactory.setDatabase(db);
    stringRedisTemplate.setConnectionFactory(jedisConnectionFactory);
    ValueOperations valueOperations = stringRedisTemplate.opsForValue();
    return ValueOperations;

沒錯,絕大多數都是切換redis db的代碼,而沒有切redis服務地址或賬號密碼的。而且天下代碼一大抄,大多數博客都是一樣的內容,這就讓人很噁心。

沒辦法,網上沒有,只能自己造輪子了。不過,從強哥這種懶人思維來說,上面的代碼既然能切庫,那是不是host、username、password也同樣可以,於是我們加入如下代碼:

public ValueOperations updateRedisConfig() {
    JedisConnectionFactory jedisConnectionFactory = (JedisConnectionFactory) stringRedisTemplate.getConnectionFactory();
    jedisConnectionFactory.setDatabase(db);
    jedisConnectionFactory.setHostName(host);
    jedisConnectionFactory.setPort(port);
    jedisConnectionFactory.setPassword(password);
    stringRedisTemplate.setConnectionFactory(jedisConnectionFactory);
    ValueOperations valueOperations = stringRedisTemplate.opsForValue();
    return valueOperations;
}

話不多說,改完重啟一下。額,運行結果並沒有讓我們見證奇迹的時刻。在調用updateRedisConfig方法的之後,使用redisTemplate還是只能切換db,不能進行服務地址或賬號密碼的更新。

這就讓人頭疼了,不過想也沒錯,如果可以的話,網上不應該找不到類似的代碼。那麼,現在該咋辦嘞?

強哥的想法是:redisTemplate每次獲取ValueOperations執行get/set方法的時候,都會去連接redis服務器,那麼我們就從這兩個方法入手看看能不能找得到解決方案。

接下來就是源碼研究的過程啦,有耐心的小夥伴就跟着強哥一起找,只想要結果的就跳到文末吧~

首先來看看入手工具方法set:

 
public boolean set(final String key, Object value) {
  boolean result = false;
  try {
          ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
          operations.set(key, value);
          result = true;
      } catch (Exception e) {
          logger.error("set cache error:", e);
      }
  return result;
}

我們進入到operations.set(key, value);的set方法實現:

public boolean set(String key, Object value) {
        boolean result = false;
    try {
        ValueOperations<Serializable, Object> operations = this.redisTemplate.opsForValue();
        operations.set(key, value);
        result = true;
    } catch (Exception var5) {
      this.logger.error("set error:", var5);
    }
    return result;
}

哦,走的是execute方法,進去看看,具體調用的是AbstractOperations的RedisTemplate的execute方法(中間跳過幾個重載方法跳轉):

public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
    Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
    Assert.notNull(action, "Callback object must not be null");
    RedisConnectionFactory factory = getConnectionFactory();
    RedisConnection conn = null;
    try {
      if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
        conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
      } else {
        conn = RedisConnectionUtils.getConnection(factory);
      }
      boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
      RedisConnection connToUse = preProcessConnection(conn, existingConnection);
      boolean pipelineStatus = connToUse.isPipelined();
      if (pipeline && !pipelineStatus) {
        connToUse.openPipeline();
      }
      RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
      T result = action.doInRedis(connToExpose);
      // close pipeline
      if (pipeline && !pipelineStatus) {
        connToUse.closePipeline();
      }
      // TODO: any other connection processing?
      return postProcessResult(result, connToUse, existingConnection);
    } finally {
      RedisConnectionUtils.releaseConnection(conn, factory);
    }
}

方法內容很長,不過大致可以看出前面是獲取一個RedisConnection對象,後面應該就是命令的執行,為什麼說應該?因為強哥也沒去細看後面的實現,因為我們要關注的就是怎麼拿到這個RedisConnection對象的。

那麼我們走RedisConnectionUtils.getConnection(factory);這句代碼進去看看,為什麼我知道是走這句而不是上面那句,因為強哥沒開事務,如果大家有打斷點,應該默認也是走的這句,跳到具體的實現方法:RedisConnectionUtils.doGetConnection(……):

public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind,
boolean enableTransactionSupport) {
    Assert.notNull(factory, "No RedisConnectionFactory specified");
    RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
    if (connHolder != null) {
      if (enableTransactionSupport) {
        potentiallyRegisterTransactionSynchronisation(connHolder, factory);
      }
      return connHolder.getConnection();
    }
    if (!allowCreate) {
      throw new IllegalArgumentException("No connection found and allowCreate = false");
    }
    if (log.isDebugEnabled()) {
      log.debug("Opening RedisConnection");
    }
    RedisConnection conn = factory.getConnection();
    if (bind) {
      RedisConnection connectionToBind = conn;
      if (enableTransactionSupport && isActualNonReadonlyTransactionActive()) {
        connectionToBind = createConnectionProxy(conn, factory);
      }
      connHolder = new RedisConnectionHolder(connectionToBind);
      TransactionSynchronizationManager.bindResource(factory, connHolder);
      if (enableTransactionSupport) {
        potentiallyRegisterTransactionSynchronisation(connHolder, factory);
      }
      return connHolder.getConnection();
    }
    return conn;
  }

代碼還是很長,話不多說,斷點走的這句:RedisConnection conn = factory.getConnection();那就看看其實現方法吧:JedisConnectionFactory.getConnection(),這個是個關鍵方法:

public RedisConnection getConnection() {
 if (cluster != null) {
   return getClusterConnection();
 }
 Jedis jedis = fetchJedisConnector();
 JedisConnection connection = (usePool ? new JedisConnection(jedis, pool, dbIndex, clientName)
     : new JedisConnection(jedis, null, dbIndex, clientName));
 connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults);
 return postProcessConnection(connection);
}

看到了,代碼很短,但是我們從中可以獲取到的內容卻很多:

第一個判斷是是否有集群,這個強哥項目暫時沒用,所以不管;如果大家有用到,可能要要考慮下裏面的代碼。

Jedis對象是在這裏創建的,熟悉redis的應該都知道:Jedis是Redis官方推薦的Java連接開發工具。直接用它就能執行redis命令。

usePool 這個變量,說明我們連接的redis服務器的時候可能用到了連接池;不知道大家看到usePool會不會有種恍然醒悟的感覺,很可能就是因為我們使用了連接池,所以即使我們之前的代碼中切換了賬號密碼,連接池的連接還是沒有更新導致的處理無效。

我們先看看fetchJedisConnector方法實現:

protected Jedis fetchJedisConnector() {
  try {
    if (usePool && pool != null) {
      return pool.getResource();
    }
 
    Jedis jedis = new Jedis(getShardInfo());
  // force initialization (see Jedis issue #82)
    jedis.connect();
  
    potentiallySetClientName(jedis);
    return jedis;
  } catch (Exception ex) {
throw new RedisConnectionFailureException("Cannot get Jedis connection", ex);
  }
}

哦,可以看到,Jedis對象是根據getShardInfo()構建出來的:

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

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

public BinaryJedis(JedisShardInfo shardInfo) {
  this.client = new Client(shardInfo.getHost(), shardInfo.getPort(), shardInfo.getSsl(), shardInfo.getSslSocketFactory(), shardInfo.getSslParameters(), shardInfo.getHostnameVerifier());
  this.client.setConnectionTimeout(shardInfo.getConnectionTimeout());
  this.client.setSoTimeout(shardInfo.getSoTimeout());
  this.client.setPassword(shardInfo.getPassword());
  this.client.setDb((long)shardInfo.getDb());
}

那就是說,只要我們掌握了這個JedisShardInfo的由來,我們就可以實現redis相關配置的切換。而這個getShardInfo()方法就是返回了JedisConnetcionFactory類的JedisShardInfo shardInfo屬性:

public JedisShardInfo getShardInfo() {
  return shardInfo;
}

那麼如果我們知道了這個shardInfo是如何創建的,是不是就可以干預到RedisConnect的創建了呢?我們來找找它被創建的地方:

走的JedisConnectionFactory的afterPropertiesSet()進去看看:

/*
  * (non-Javadoc)
  * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
  */
public void afterPropertiesSet() {
 if (shardInfo == null) {
   shardInfo = new JedisShardInfo(hostName, port);
   if (StringUtils.hasLength(password)) {
     shardInfo.setPassword(password);
   }  
   if (timeout > 0) {
       setTimeoutOn(shardInfo, timeout);
     }
   }

   if (usePool && clusterConfig == null) {
     this.pool = createPool();
   }
 
   if (clusterConfig != null) {
     this.cluster = createCluster();
   }
}

哦吼~,整篇博文最關鍵的代碼終於出現了。我們可以看到,JedisShardInfo的所有信息都是從JedisConnetionFactory的屬性中來的,包括hostName、port、password、timeout等。而且,如果JedisShardInfo為null時,調用afterPropertiesSet方法會幫我們創建出來。然後,該方法還會幫我們創建新的連接池,簡直完美。最最重要的是,這個方法是public的。

所以,嘿嘿,綜上,我們總結改造的幾個點:

1.連接redis用到了連接池,需要先給他銷毀;

2.創建Jedis的時候,將JedisShardInfo先設為null;

3.手動設置JedisConnetionFactory的hostName、port、password等信息;

4.調用JedisConnetionFactory的afterPropertiesSet方法創建JedisShardInfo;

5.給RedisTemplate設置處理后的JedisConnetionFactory,這樣在下次使用set或get方法的時候就會去創建新改配置的連接池啦。

實現如下:

public void updateRedisConfig() {
  RedisTemplate template = (RedisTemplate) applicationContext.getBean("redisTemplate");
  JedisConnectionFactory redisConnectionFactory = (JedisConnectionFactory) template.getConnectionFactory();
//關閉連接池
  redisConnectionFactory.destroy();
  redisConnectionFactory.setShardInfo(null);
  redisConnectionFactory.setHostName(host);
  redisConnectionFactory.setPort(port);
  redisConnectionFactory.setPassword(password);
  redisConnectionFactory.setDatabase(database);
  //重新創建連接池
  redisConnectionFactory.afterPropertiesSet();
  template.setConnectionFactory(redisConnectionFactory);
}

重啟項目之後,調用這個方法,就可以實現redis庫及服務地址、賬號密碼的切換而無需重啟項目了。

如何實現動態切換

強哥這裏就使用同一配置中心Apollo來進行動態配置的。

首先不懂Apollo是什麼的同學,先Apollo官網半日游吧(直接看官網教程,比看其他博客強)。簡單的說就是一個統一配置中心,將原來配置在項目本地的配置(如:Spring中的application.properties)遷移到Apollo上,實現統一的管理。

使用Apollo的原因,其實就是因為其接入簡單,且具有實時更新回調的功能,我們可以監聽Apollo上的配置修改,實現針對修改的配置內容進行相應的回調監聽處理。

因此我們可以將redis的配置信息配置在Apollo上,然後監聽這些配置。當Apollo上的這些配置修改時,我們在ConfigChangeListener中,調用上面的updateRedisConfig方法就可以實現redis配置的動態切換了。

接入Apollo代碼非常簡單:

Config redisConfig = ConfigService.getConfig("redis");
ConfigChangeListener listener = this::updateRedisConfig;
redisConfig.addChangeListener(listener);

這樣,我們就可以實現具體所謂的動態更新配置啦~

當然,其他有相同功能的配置中心其實也可以,只是強哥項目中暫時用的就是Apollo就拿Apollo來講了。

考慮到篇幅已經很長了,就不多解釋Apollo的使用了,用過的自然看得懂上面的方法,有不懂的也可以留言提問哦。

好了,就到這吧,原創不易,怎麼支持你們知道,那麼下次見啦

關注公眾號獲取更多內容,有問題也可在公眾號提問哦:

強哥叨逼叨

叨逼叨編程、互聯網的見解和新鮮事

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

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

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務

Google Store 推出新年優惠,Pixel 手機、Chromecast、Nest 智慧喇叭直接現折_如何寫文案

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

隨著新年即將來臨,Google Store 近日也宣布推出新年優惠活動,Pixel 手機、Chromecast、Nest 智慧喇叭系列都能祭出折扣,而且這次是直接現折,讓你立刻就能享受優惠價。此外,Pixel 4a 5G 版本的白色款式也已悄悄降臨 Google Store(不過沒有折扣)。

Google Store 推出新年優惠

這次符合 Google Store 新年優惠的 Pixel 手機,只有 Pixel 4a 5G 版本(純粹黑),原價 NT$15,990,現在入手即可省下 NT$1,000,意味著價格變成 NT$14,990。這次也是首次 Pixel 4a 5G 推出現折優惠的活動(之前送折價卷)。就是白因為才剛推出,理所當然還是以原價販售:

Pixel 5 這次就不在名單內,目前依舊以 NT$18,990 的原價販售。而 Nest 智慧喇叭「Nest Mini」與「Nest Audio」兩款都有優惠。

Nest Mini 原價為 NT$1,785,現在現省 NT$400,變成 NT$1,385 的優惠價,兩色都是:

Nest Audio 原價為 NT$3,180,現在現省 NT$350,折扣後價格變成 NT$2,830:

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

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

Chromecast 折扣 NT$150,原價 NT$1,445,現在購買可以 NT$1,295 的優惠價入手:

整體來看,這次 Google Store 的優惠雖然沒有像先前聖誕節、雙11 那麼超值,不過跟平常相比至少有降價,對於有意購買上述產品的人來說,還是可以省下一些荷包,剛好拿來補貼紅包錢。

Google Store 新年優惠預計到 2021 年 2 月 14 日晚上 11:59 點截止。

補充資料

  • 台灣 Google Store:點我前往

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

OPPO Find X3 Pro 更多關鍵規格曝光:將搭載高通 S888 處理器、12GB RAM、256GB ROM_網頁設計公司

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

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

幾週前,爆料大神 Evan Blass 才洩漏 OPPO Find X3 Pro 的官方渲染圖,不過至今關於 OPPO 準備推出 Find X 系列的 Find X3 和 Find X3 Pro 除了後者幾乎確認搭載高通 S888 旗艦處理器,其餘所知的規格其實相當有限。不過稍早網路流出有關 Find X3 Pro 的規格和多個測試平台的截圖畫面,也透露更多 Find X3 Pro 的關鍵規格,其中包括螢幕解析度等資訊。

▲圖片來源:Evan Blass(Voice/@evleaks)

OPPO Find X3 Pro 更多關鍵規格曝光:將搭載高通 S888 處理器、12GB RAM、256GB ROM

▲圖片來源:Evan Blass(Voice/@evleaks)

稍早在酷安網出現多張疑似 OPPO Find X3 Pro 的規格截圖畫面,其型號 PEEM00 與過去的傳聞相符:

▲圖片來源:酷安網

根據手機的截圖顯示, Find X3 Pro 的螢幕解析度為 1440×3216 ,據傳螢幕尺寸為 6.67 吋並將支持自適應調整的 120Hz 更新率(支援 10Hz – 120Hz 可變螢幕更新率)。而 Find X3 系列也將首搭載 OPPO 「全鏈路色彩管理系統」,這套基於硬體所開發的色彩管理系統,該色彩管理系統擁有兩大核心技術,全鏈路 10-bit 及色彩管理,能夠大幅度提升色彩採集的豐富度,在顯示環節則能讓畫質更加清晰且更精準的還原顯示圖像色彩。

硬體規格方面, Find X3 Pro 除了確認搭載 Qualcomm Snapdragon 888 處理器,將具備 12GB RAM 和 256GB ROM ,從之前曝光的安兔兔跑分成績 Find X3 Pro 的成績測得 771,491 分的成績。作業系統方面,將運行 Android 11 並搭配 Color OS 11.2 使用者介面。

▲圖片來源:酷安網

▲圖片來源:酷安網

同時在 Geekbench 資料庫也出現相同型號 PEEM00 的裝置,其單核和多核測試分別獲得 4236 分和 13323 分。不過根據以往經驗 Geekbench 資料庫的內容僅可做為參考,無法確認是真實的規格。

▲圖片來源:Geekbench

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

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

回顧去年 11 月,爆料大神  Evan Blass 首次提及關於將搭載高通 Snapdragon 888 處理器的 OPPO Find X3 Pro(代號:Fussi),當時提及 Find X3 Pro 相機將配備 5000 萬像素四鏡頭主相機,其中標準鏡頭和超廣角鏡頭都將採用 SONY 全新的感光元件(其中包括 SONY IMX766 感光元件),此外也具備 1300 萬像素 2x 望遠鏡頭以及 300 萬像素微距鏡頭。

延伸閱讀:
ROG Phone 5 更多外觀、規格細節曝光:另有 Anime Matrix 顯示版本?

OPPO Find X3 Lite 完整實機開箱照洩漏!只是外觀有點眼熟?

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

國外用戶反應 iPhone、iPad 連接至 M1 Mac 同步之後,Apps 打開都會立刻閃退(附暫時解法)_網頁設計

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

擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢

對於大多數 M1 Mac 用戶來說,使用的手機、平版應該都是 iPhone 與 iPad,而沒用 iCloud 備份的人,你一定是把資料備份在 M1 Mac 上,但目前推薦先不要,因為最近國外就有多位用戶反應,當他們把 iPhone 或 iPad 連接上 M1 Mac 同步之後,應用程式不僅開始不斷閃退無法使用,還沒辦法繼續從 App Store 安裝新 App 與更新。

iPhone、iPad 同步至 M1 Mac 之後,Apps 打開都會立刻閃退

根據外媒 9to5Mac 的報導,在 Apple 官方論壇與 Reddit 上,陸續有用戶回報把 iPhone/iPad 連接到 M1 Mac 同步之後,已安裝的應用程式就無法再繼續使用,一打開就會閃退,可點我觀看影片:

一位用戶說道,他把 iPhone X(iOS 14.3 系統以上) 與 M1 MacBook Pro 連接並同步之後,就沒辦法打開任何第三方的應用程式了,也無法從 App Store 中安裝或更新 Apps,下載進度條瞬間變滿,然後變成雲的下載圖示:

而 Reddit 這位 Mysterious-Pie-8 網友也有詢問 Apple 支援,他表示 Apple 支援部門發現這是 M1 Mac 與 iPhone 同步之後產生的數據損壞問題,目前已經把這情況轉移給工程團隊:

有碰到相同情況的人,現在暫時只能透過還原成出廠設置解決,不過這步驟會清除 iPhone 上的所有內容和設定,因此記得先備份好資料。

事實上這問題早在去年 12 月就有人回報,目前看下來所有 M1 Mac 型號都有案例(Mac mini、MacBook Pro、MacBook Air):

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

網動是一群專業、熱情、向前行的工作團隊,我們擁有靈活的組織與溝通的能力,能傾聽客戶聲音,激發創意的火花,呈現完美的作品

所以說,如果你還不曾透過 M1 Mac 備份 iPhone 或 iPad 資料,記得先不要,不確定是不是每一台都會碰到這問題,至少先等到 Apple 釋出修復的軟體更新後再嘗試。

資料來源:9to5Mac

iOS 14.5 測試版為 iPhone 12 系列加入 5G 雙卡雙待支援,還帶來不少功能呢!

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機。

小米11 國際版將於2/8 晚間發表,有望 3 月在台亮相_潭子電動車

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

有別於一般網頁架設公司,除了模組化的架站軟體,我們的營業主軸還包含:資料庫程式開發、網站建置、網頁設計、電子商務專案開發、系統整合、APP設計建置、專業網路行銷。

在 2020 年 12 月底,小米於中國發表首款搭載高通 Snapdragon 888 處理器的旗艦手機「小米11」,截至目前小米11 也只在中國當地銷售。針對中國以外的市場方面,小米11 國際版除了日前通過 NCC 認證,近期也正式宣佈將於 2 月 8 日晚上 8 點舉行小米11 國際版發表會,未來小米11 也將引進台灣市場販售。

小米11 國際版將於2/8 晚間發表,有望 3 月在台亮相

小米11 國際版多數規格預計和去年底發表的中國版的關鍵關格大致相似,像是搭載 Qualcomm Snapdragon 888 處理器、 6.81 吋 2K(3200×1440 WQHD)E4 材質 AMOLED 四曲面柔性螢幕。
螢幕支持最高 120Hz 螢幕更新率、480Hz 觸控採樣率, 1500nit 峰值亮度、480Hz  觸控採樣率、5,000,000:1 對比度,螢幕也擁有 100% P3 色域和 HDR10+ 認證。
另外,小米11 螢幕玻璃採用康寧 Gorilla Glass Victus 玻璃保護,抗摔性相較前代提升 1.5倍、耐刮性能提升 2 倍。

相機方面,小米 11 採用 1 億 800 萬像素三鏡頭主相機設計,分別為 1 億 800 萬像素像素(1 / 1.33″超大感光元件)標準鏡頭、1300 萬像素 123° 超廣角鏡頭、 500 萬像素 50mm 微距長焦鏡頭,前置鏡頭則配備 2000 萬像素自拍相機。
此外,小米11 內建等效 4600mAh 大電池,支持 55W 有線閃充、50W 無線閃充以及10W 反向無線充電, 55W 有線閃充可在 45 分鐘充滿 100% 電量、50W 無線閃充可在 53 分鐘充滿 100% 電量。

▲圖片來源:小米(中國)

與小米11 中國版最大的差異在於小米11 國際版將加入 GMS 支援,不過目前還無法得知將推出哪幾款配色與容量選擇。雖然,小米11 國際版在下週 2 月 8 日晚上 8 點就會發表,不過台灣市場預計最快要等到 3 月才會引進銷售。

小米11 國際版線上發表會(2021/2/8 20:00開始)

 

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

日本、大陸,發現這些先進的國家已經早就讓電動車優先上路,而且先進國家空氣品質相當好,電動車節能減碳可以減少空污

延伸閱讀:
多款旗艦手機電力續航能力實測,三星 Galaxy S21 Ultra 仍敗給 iPhone 12 Pro Max

小米隔空充電技術正式發表:手持也能隔空充電,「真」無線充電時代來臨!

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

※超省錢租車方案

商務出差、學生出遊、旅遊渡假、臨時用車!GO 神州租賃有限公司!合法經營、合法連鎖、合法租賃小客車!

vue的第一個commit分析_如何寫文案

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

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

為什麼寫這篇vue的分析文章?

對於天資愚鈍的前端(我)來說,閱讀源碼是件不容易的事情,畢竟有時候看源碼分析的文章都看不懂。每次看到大佬們用了1~2年的vue就能掌握原理,甚至精通源碼,再看看自己用了好幾年都還在基本的使用階段,心中總是羞愧不已。如果一直滿足於基本的業務開發,怕是得在初級水平一直待下去了吧。所以希望在學習源碼的同時記錄知識點,可以讓自己的理解和記憶更加深刻,也方便將來查閱。

目錄結構

本文以vue的第一次 commit a879ec06 作為分析版本

├── build
│   └── build.js               // `rollup` 打包配置
├── dist                        
│   └── vue.js    
├── package.json
├── src                        // vue源碼目錄
│   ├── compiler               // 將vue-template轉化為render函數
│   │   ├── codegen.js         // 遞歸ast提取指令,分類attr,style,class,並生成render函數
│   │   ├── html-parser.js     // 通過正則匹配將html字符串轉化為ast
│   │   ├── index.js           // compile主入口
│   │   └── text-parser.js     // 編譯{{}}
│   ├── config.js              // 對於vue的全局配置文件
│   ├── index.js               // 主入口
│   ├── index.umd.js           // 未知(應該是umd格式的主入口)
│   ├── instance               // vue實例函數
│   │   └── index.js           // 包含了vue實例的初始化,compile,data代理,methods代理,watch數據,執行渲染
│   ├── observer               // 數據訂閱發布的實現
│   │   ├── array.js           // 實現array變異方法,$set $remove 實現
│   │   ├── batcher.js         // watch執行隊列的收集,執行
│   │   ├── dep.js             // 訂閱中心實現
│   │   ├── index.js           // 數據劫持的實現,收集訂閱者
│   │   └── watcher.js         // watch實現,訂閱者
│   ├── util                   // 工具函數
│   │   ├── component.js
│   │   ├── debug.js
│   │   ├── dom.js
│   │   ├── env.js             // nexttick實現
│   │   ├── index.js
│   │   ├── lang.js
│   │   └── options.js
│   └── vdom
│       ├── dom.js             // dom操作的封裝
│       ├── h.js               // 節點數據分析(元素節點,文本節點)
│       ├── index.js           // vdom主入口
│       ├── modules            // 不同屬性處理函數
│       │   ├── attrs.js       // 普通attr屬性處理
│       │   ├── class.js       // class處理
│       │   ├── events.js      // event處理
│       │   ├── props.js       // props處理
│       │   └── style.js       // style處理
│       ├── patch.js           // node樹的渲染,包括節點的加減更新處理,及對應attr的處理
│       └── vnode.js           // 返回最終的節點數據
└── webpack.config.js          // webpack配置

從template到html的過程分析

我們的代碼是從new Vue()開始的,Vue的構造函數如下:

constructor (options) {
  // options就是我們對於vue的配置
  this.$options = options
  this._data = options.data
  // 獲取元素html,即template
  const el = this._el = document.querySelector(options.el)
  // 編譯模板 -> render函數
  const render = compile(getOuterHTML(el))
  this._el.innerHTML = ''
  // 實例代理data數據
  Object.keys(options.data).forEach(key => this._proxy(key))
  // 將method的this指向實例
  if (options.methods) {
    Object.keys(options.methods).forEach(key => {
      this[key] = options.methods[key].bind(this)
    })
  }
  // 數據觀察
  this._ob = observe(options.data)
  this._watchers = []
  // watch數據及更新
  this._watcher = new Watcher(this, render, this._update)
  // 渲染函數
  this._update(this._watcher.value)
}

當我們初始化項目的時候,即會執行構造函數,該函數向我們展示了vue初始化的主線:編譯template字符串 => 代理data數據/methods的this綁定 => 數據觀察 => 建立watch及更新渲染

1. 編譯template字符串

const render = compile(getOuterHTML(el))

其中compile的實現如下:

export function compile (html) {
  html = html.trim()
  // 對編譯結果緩存
  const hit = cache[html]
  // parse函數在parse-html中定義,其作用是把我們獲取的html字符串通過正則匹配轉化為ast,輸出如下 {tag: 'div', attrs: {}, children: []}
  return hit || (cache[html] = generate(parse(html)))
}

接下來看看generate函數,ast通過genElement的轉化生成了構建節點html的函數,在genElement將對if for 等進行判斷並轉化( 指令的具體處理將在後面做分析,先關注主流程代碼),最後都會執行genData函數

// 生成節點主函數
export function generate (ast) {
  const code = genElement(ast)
  // 執行code代碼,並將this作為code的global對象。所以我們在template中的變量將指向為實例的屬性 {{name}} -> this.name 
  return new Function (`with (this) { return ${code}}`)
}

// 解析單個節點 -> genData
function genElement (el, key) {
  let exp
  // 指令的實現,實際就是在模板編譯時實現的
  if (exp = getAttr(el, 'v-for')) {
    return genFor(el, exp)
  } else if (exp = getAttr(el, 'v-if')) {
    return genIf(el, exp)
  } else if (el.tag === 'template') {
    return genChildren(el)
  } else {
    // 分別為 tag 自身屬性 子節點數據
    return `__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })`
  }
}

我們可以看看在genData中都做了什麼。上面的parse函數將html字符串轉化為ast,而在genData中則將節點的attrs數據進一步處理,例如class -> renderClass style class props attr 分類。在這裏可以看到 bind 指令的實現,即通過正則匹配 : 和 bind,如果匹配則把相應的 value值轉化為 (value) 的形式,而不匹配的則通過JSON.stringify()轉化為字符串('value')。最後輸出attrs(key-value),在這裏得到的對象是字符串形式的,例如(value)等也僅僅是將變量名,而在generate中通過new Function進一步通過(this.value)得到變量值。

function genData (el, key) {
  // 沒有屬性返回空對象
  if (!el.attrs.length) {
    return '{}'
  }
  // key
  let data = key ? `{key:${ key },` : `{`
  // class處理
  if (el.attrsMap[':class'] || el.attrsMap['class']) {
    data += `class: _renderClass(${ el.attrsMap[':class'] }, "${ el.attrsMap['class'] || '' }"),`
  }
  // attrs
  let attrs = `attrs:{`
  let props = `props:{`
  let hasAttrs = false
  let hasProps = false
  for (let i = 0, l = el.attrs.length; i < l; i++) {
    let attr = el.attrs[i]
    let name = attr.name
    // bind屬性
    if (bindRE.test(name)) {
      name = name.replace(bindRE, '')
      if (name === 'class') {
        continue
      // style處理
      } else if (name === 'style') {
        data += `style: ${ attr.value },`
      // props屬性處理
      } else if (mustUsePropsRE.test(name)) {
        hasProps = true
        props += `"${ name }": (${ attr.value }),` 
      // 其他屬性
      } else {
        hasAttrs = true
        attrs += `"${ name }": (${ attr.value }),`
      }
    // on指令,未實現
    } else if (onRE.test(name)) {
      name = name.replace(onRE, '')
    // 普通屬性
    } else if (name !== 'class') {
      hasAttrs = true
      attrs += `"${ name }": (${ JSON.stringify(attr.value) }),`
    }
  }
  if (hasAttrs) {
    data += attrs.slice(0, -1) + '},'
  }
  if (hasProps) {
    data += props.slice(0, -1) + '},'
  }
  return data.replace(/,$/, '') + '}'
}

而對於genChildren,我們可以猜到就是對ast中的children進行遍歷調用genElement,實際上在這裏還包括了對文本節點的處理。

// 遍歷子節點 -> genNode
function genChildren (el) {
  if (!el.children.length) {
    return 'undefined'
  }
  // 對children扁平化處理
  return '__flatten__([' + el.children.map(genNode).join(',') + '])'
}

function genNode (node) {
  if (node.tag) {
    return genElement(node)
  } else {
    return genText(node)
  }
}

// 解析{{}}
function genText (text) {
  if (text === ' ') {
    return '" "'
  } else {
    const exp = parseText(text)
    if (exp) {
      return 'String(' + escapeNewlines(exp) + ')'
    } else {
      return escapeNewlines(JSON.stringify(text))
    }
  }
}

genText處理了text及換行,在parseText函數中利用正則解析{{}},輸出字符串(value)形式的字符串。

現在我們再看看__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })__h__函數

// h 函數利用上面得到的節點數據得到 vNode對象 => 虛擬dom
export default function h (tag, b, c) {
  var data = {}, children, text, i
  if (arguments.length === 3) {
    data = b
    if (isArray(c)) { children = c }
    else if (isPrimitive(c)) { text = c }
  } else if (arguments.length === 2) {
    if (isArray(b)) { children = b }
    else if (isPrimitive(b)) { text = b }
    else { data = b }
  }
  if (isArray(children)) {
    // 子節點遞歸處理
    for (i = 0; i < children.length; ++i) {
      if (isPrimitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i])
    }
  }
  // svg處理
  if (tag === 'svg') {
    addNS(data, children)
  }
  // 子節點為文本節點
  return VNode(tag, data, children, text, undefined)
}

到此為止,我們分析了const render = compile(getOuterHTML(el)),從elhtml字符串到render函數都是怎麼處理的。

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

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

2. 代理data數據/methods的this綁定

// 實例代理data數據
Object.keys(options.data).forEach(key => this._proxy(key))
// 將method的this指向實例
if (options.methods) {
  Object.keys(options.methods).forEach(key => {
    this[key] = options.methods[key].bind(this)
  })
}

實例代理data數據的實現比較簡單,就是利用了對象的setter和getter,讀取this數據時返回data數據,在設置this數據時同步設置data數據

_proxy (key) {
  if (!isReserved(key)) {
    // need to store ref to self here
    // because these getter/setters might
    // be called by child scopes via
    // prototype inheritance.
    var self = this
    Object.defineProperty(self, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter () {
        return self._data[key]
      },
      set: function proxySetter (val) {
        self._data[key] = val
      }
    })
  }
}

3. Obaerve的實現

Observe的實現原理在很多地方都有分析,主要是利用了Object.defineProperty()來建立對數據更改的訂閱,在很多地方也稱之為數據劫持。下面我們來學習從零開始建立這樣一個數據的訂閱發布體系。

從簡單處開始,我們希望有個函數可以幫我們監聽數據的改變,每當數據改變時執行特定回調函數

function observe(data, callback) {
  if (!data || typeof data !== 'object') {
    return
  }

  // 遍歷key
  Object.keys(data).forEach((key) => {
    let value = data[key];

    // 遞歸遍歷監聽深度變化
    observe(value, callback);

    // 監聽單個可以的變化
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        return value;
      },
      set(val) {
        if (val === value) {
          return
        }

        value = val;

        // 監聽新的數據
        observe(value, callback);
        
        // 數據改變的回調
        callback();
      }
    });
  });
}

// 使用observe函數監聽data
const data = {};
observe(data, () => {
  console.log('data修改');
})

上面我們實現了一個簡單的observe函數,只要我們將編譯函數作為callback傳入,那麼每次數據更改時都會觸發回調函數。但是我們現在不能為單獨的key設置監聽及回調函數,只能監聽整個對象的變化執行回調。下面我們對函數進行改進,達到為某個key設置監聽及回調。同時建立調度中心,讓整個訂閱發布模式更加清晰。

// 首先是訂閱中心
class Dep {
  constructor() {
    this.subs = []; // 訂閱者數組
  }

  addSub(sub) {
    // 添加訂閱者
    this.subs.push(sub);
  }

  notify() {
    // 發布通知
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}

// 當前訂閱者,在getter中標記
Dep.target = null;

// 訂閱者
class Watch {
  constructor(express, cb) {
    this.cb = cb;
    if (typeof express === 'function') {
      this.expressFn = express;
    } else {
      this.expressFn = () => {
        return new Function(express)();
      }
    }
    
    this.get();
  }

  get() {
    // 利用Dep.target存當前訂閱者
    Dep.target = this;
    // 執行表達式 -> 觸發getter -> 在getter中添加訂閱者
    this.expressFn();
    // 及時置空
    Dep.taget = null;
  }

  update() {
    // 更新
    this.cb();
  }

  addDep(dep) {
    // 添加訂閱
    dep.addSub(this);
  }
}

// 觀察者 建立觀察
class Observe {
  constructor(data) {
    if (!data || typeof data !== 'object') {
      return
    }
  
    // 遍歷key
    Object.keys(data).forEach((key) => {
      // key => dep 對應
      const dep = new Dep();
      let value = data[key];
  
      // 遞歸遍歷監聽深度變化
      const observe = new Observe(value);
  
      // 監聽單個可以的變化
      Object.defineProperty(data, key, {
        configurable: true,
        enumerable: true,
        get() {
          if (Dep.target) {
            const watch = Dep.target;
            watch.addDep(dep);
          }
          return value;
        },
        set(val) {
          if (val === value) {
            return
          }
  
          value = val;
  
          // 監聽新的數據
          new Observe(value);
          
          // 數據改變的回調
          dep.notify();
        }
      });
    });
  }
}

// 監聽數據中某個key的更改
const data = {
  name: 'xiaoming',
  age: 26
};

const observe = new Observe(data);

const watch = new Watch('data.age', () => {
  console.log('age update');
});

data.age = 22

現在我們實現了訂閱中心訂閱者觀察者。觀察者監測數據的更新,訂閱者通過訂閱中心訂閱數據的更新,當數據更新時,觀察者會告訴訂閱中心,訂閱中心再逐個通知所有的訂閱者執行更新函數。到現在為止,我們可以大概猜出vue的實現原理:

  1. 建立觀察者觀察data數據的更改 (new Observe)

  2. 在編譯的時候,當某個代碼片段或節點依賴data數據,為該節點建議訂閱者,訂閱data中某些數據的更新(new Watch)

  3. 當dada數據更新時,通過訂閱中心通知數據更新,執行節點更新函數,新建或更新節點(dep.notify())

上面是我們對vue實現原理訂閱發布模式的基本實現,及編譯到更新過程的猜想,現在我們接着分析vue源碼的實現:

在實例的初始化中

// ...
// 為數據建立數據觀察
this._ob = observe(options.data)
this._watchers = []
// 添加訂閱者 執行render 會觸發 getter 訂閱者訂閱更新,數據改變觸發 setter 訂閱中心通知訂閱者執行 update
this._watcher = new Watcher(this, render, this._update)
// ...

vue中數據觀察的實現

// observe函數
export function observe (value, vm) {
  if (!value || typeof value !== 'object') {
    return
  }
  if (
    hasOwn(value, '__ob__') &&
    value.__ob__ instanceof Observer
  ) {
    ob = value.__ob__
  } else if (
    shouldConvert &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 為數據建立觀察者
    ob = new Observer(value)
  }
  // 存儲關聯的vm
  if (ob && vm) {
    ob.addVm(vm)
  }
  return ob
}

// => Observe 函數
export function Observer (value) {
  this.value = value
  // 在數組變異方法中有用
  this.dep = new Dep()
  // observer實例存在__ob__中
  def(value, '__ob__', this)
  if (isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment
    // 數組遍歷,添加變異的數組方法
    augment(value, arrayMethods, arrayKeys)
    // 對數組的每個選項調用observe函數
    this.observeArray(value)
  } else {
    // walk -> convert -> defineReactive -> setter/getter
    this.walk(value)
  }
}

// => walk
Observer.prototype.walk = function (obj) {
  var keys = Object.keys(obj)
  for (var i = 0, l = keys.length; i < l; i++) {
    this.convert(keys[i], obj[keys[i]])
  }
}

// => convert
Observer.prototype.convert = function (key, val) {
  defineReactive(this.value, key, val)
}

// 重點看看defineReactive
export function defineReactive (obj, key, val) {
  // key對應的的訂閱中心
  var dep = new Dep()

  var property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // 兼容原有setter/getter
  // cater for pre-defined getter/setters
  var getter = property && property.get
  var setter = property && property.set

  // 實現遞歸監聽屬性 val = obj[key]
  // 深度優先遍歷 先為子屬性設置 reactive
  var childOb = observe(val)
  // 設置 getter/setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val
      // Dep.target 為當前 watch 實例
      if (Dep.target) {
        // dep 為 obj[key] 對應的調度中心 dep.depend 將當前 wtcher 實例添加到調度中心
        dep.depend()
        if (childOb) {
          // childOb.dep 為 obj[key] 值 val 對應的 observer 實例的 dep
          // 實現array的變異方法和$set方法訂閱
          childOb.dep.depend()
        }

        // TODO: 此處作用未知?
        if (isArray(value)) {
          for (var e, i = 0, l = value.length; i < l; i++) {
            e = value[i]
            e && e.__ob__ && e.__ob__.dep.depend()
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val
      // 通過 getter 獲取 val 判斷是否改變
      if (newVal === value) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 為新值設置 reactive
      childOb = observe(newVal)
      // 通知key對應的訂閱中心更新
      dep.notify()
    }
  })
}

訂閱中心的實現

let uid = 0

export default function Dep () {
  this.id = uid++
  // 訂閱調度中心的watch數組
  this.subs = []
}

// 當前watch實例
Dep.target = null

// 添加訂閱者
Dep.prototype.addSub = function (sub) {
  this.subs.push(sub)
}

// 移除訂閱者
Dep.prototype.removeSub = function (sub) {
  this.subs.$remove(sub)
}

// 訂閱
Dep.prototype.depend = function () {
  // Dep.target.addDep(this) => this.addSub(Dep.target) => this.subs.push(Dep.target)
  Dep.target.addDep(this)
}

// 通知更新
Dep.prototype.notify = function () {
  // stablize the subscriber list first
  var subs = this.subs.slice()
  for (var i = 0, l = subs.length; i < l; i++) {
    // subs[i].update() => watch.update()
    subs[i].update()
  }
}

訂閱者的實現

export default function Watcher (vm, expOrFn, cb, options) {
  // mix in options
  if (options) {
    extend(this, options)
  }
  var isFn = typeof expOrFn === 'function'
  this.vm = vm
  // vm 的 _watchers 包含了所有 watch
  vm._watchers.push(this)
  this.expression = expOrFn
  this.cb = cb
  this.id = ++uid // uid for batching
  this.active = true
  this.dirty = this.lazy // for lazy watchers
  // deps 一個 watch 實例可以對應多個 dep
  this.deps = []
  this.newDeps = []
  this.depIds = Object.create(null)
  this.newDepIds = null
  this.prevError = null // for async error stacks
  // parse expression for getter/setter
  if (isFn) {
    this.getter = expOrFn
    this.setter = undefined
  } else {
    warn('vue-lite only supports watching functions.')
  }
  this.value = this.lazy
    ? undefined
    : this.get()
  this.queued = this.shallow = false
}

Watcher.prototype.get = function () {
  this.beforeGet()
  var scope = this.scope || this.vm
  var value
  try {
    // 執行 expOrFn,此時會觸發 getter => dep.depend() 將watch實例添加到對應 obj[key] 的 dep
    value = this.getter.call(scope, scope)
  }
  if (this.deep) {
    // 深度watch
    // 觸發每個key的getter watch實例將對應多個dep
    traverse(value)
  }
  // ...
  this.afterGet()
  return value
}

// 觸發getter,實現訂閱
Watcher.prototype.beforeGet = function () {
  Dep.target = this
  this.newDepIds = Object.create(null)
  this.newDeps.length = 0
}

// 添加訂閱
Watcher.prototype.addDep = function (dep) {
  var id = dep.id
  if (!this.newDepIds[id]) {
    // 將新出現的dep添加到newDeps中
    this.newDepIds[id] = true
    this.newDeps.push(dep)
    // 如果已在調度中心,不再重複添加
    if (!this.depIds[id]) {
      // 將watch添加到調度中心的數組中
      dep.addSub(this)
    }
  }
}

Watcher.prototype.afterGet = function () {
  // 切除key的getter聯繫
  Dep.target = null
  var i = this.deps.length
  while (i--) {
    var dep = this.deps[i]
    if (!this.newDepIds[dep.id]) {
      // 移除不在expOrFn表達式中關聯的dep中watch的訂閱
      dep.removeSub(this)
    }
  }
  this.depIds = this.newDepIds
  var tmp = this.deps
  this.deps = this.newDeps
  // TODO: 既然newDeps最終會被置空,這邊賦值的意義在於?
  this.newDeps = tmp
}

// 訂閱中心通知消息更新
Watcher.prototype.update = function (shallow) {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync || !config.async) {
    this.run()
  } else {
    // if queued, only overwrite shallow with non-shallow,
    // but not the other way around.
    this.shallow = this.queued
      ? shallow
        ? this.shallow
        : false
      : !!shallow
    this.queued = true
    // record before-push error stack in debug mode
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.debug) {
      this.prevError = new Error('[vue] async stack trace')
    }
    // 添加到待執行池
    pushWatcher(this)
  }
}

// 執行更新回調
Watcher.prototype.run = function () {
  if (this.active) {
    var value = this.get()
    if (
      ((isObject(value) || this.deep) && !this.shallow)
    ) {
      // set new value
      var oldValue = this.value
      this.value = value
      var prevError = this.prevError
      // ...
      this.cb.call(this.vm, value, oldValue)
    }
    this.queued = this.shallow = false
  }
}

Watcher.prototype.depend = function () {
  var i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

wtach回調執行隊列

在上面我們可以發現,watch在收到信息更新執行update時。如果非同步情況下會執行pushWatcher(this)將實例推入執行池中,那麼在何時會執行回調函數,如何執行呢?我們一起看看pushWatcher的實現。

// batch.js
var queueIndex
var queue = []
var userQueue = []
var has = {}
var circular = {}
var waiting = false
var internalQueueDepleted = false

// 重置執行池
function resetBatcherState () {
  queue = []
  userQueue = []
  // has 避免重複
  has = {}
  circular = {}
  waiting = internalQueueDepleted = false
}

// 執行執行隊列
function flushBatcherQueue () {
  runBatcherQueue(queue)
  internalQueueDepleted = true
  runBatcherQueue(userQueue)
  resetBatcherState()
}

// 批量執行
function runBatcherQueue (queue) {
  for (queueIndex = 0; queueIndex < queue.length; queueIndex++) {
    var watcher = queue[queueIndex]
    var id = watcher.id
    // 執行後置為null
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > config._maxUpdateCount) {
        warn(
          'You may have an infinite update loop for watcher ' +
          'with expression "' + watcher.expression + '"',
          watcher.vm
        )
        break
      }
    }
  }
}

// 添加到執行池
export function pushWatcher (watcher) {
  var id = watcher.id
  if (has[id] == null) {
    if (internalQueueDepleted && !watcher.user) {
      // an internal watcher triggered by a user watcher...
      // let's run it immediately after current user watcher is done.
      userQueue.splice(queueIndex + 1, 0, watcher)
    } else {
      // push watcher into appropriate queue
      var q = watcher.user
        ? userQueue
        : queue
      has[id] = q.length
      q.push(watcher)
      // queue the flush
      if (!waiting) {
        waiting = true
        // 在nextick中執行
        nextTick(flushBatcherQueue)
      }
    }
  }
}

4. patch實現

上面便是vue中數據驅動的實現原理,下面我們接着回到主流程中,在執行完watch后,便執行this._update(this._watcher.value)開始節點渲染

// _update => createPatchFunction => patch => patchVnode => (dom api)

// vtree是通過compile函數編譯的render函數執行的結果,返回了當前表示當前dom結構的對象(虛擬節點樹)
_update (vtree) {
  if (!this._tree) {
    // 第一次渲染
    patch(this._el, vtree)
  } else {
    patch(this._tree, vtree)
  }
  this._tree = vtree
}

// 在處理節點時,需要針對class,props,style,attrs,events做不同處理
// 在這裏注入針對不同屬性的處理函數
const patch = createPatchFunction([
  _class, // makes it easy to toggle classes
  props,
  style,
  attrs,
  events
])

// => createPatchFunction返回patch函數,patch函數通過對比虛擬節點的差異,對節點進行增刪更新
// 最後調用原生的dom api更新html
return function patch (oldVnode, vnode) {
  var i, elm, parent
  var insertedVnodeQueue = []
  // pre hook
  for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

  if (isUndef(oldVnode.sel)) {
    oldVnode = emptyNodeAt(oldVnode)
  }

  if (sameVnode(oldVnode, vnode)) {
    // someNode can patch
    patchVnode(oldVnode, vnode, insertedVnodeQueue)
  } else {
    // 正常的不復用 remove insert
    elm = oldVnode.elm
    parent = api.parentNode(elm)

    createElm(vnode, insertedVnodeQueue)

    if (parent !== null) {
      api.insertBefore(parent, vnode.elm, api.nextSibling(elm))
      removeVnodes(parent, [oldVnode], 0, 0)
    }
  }

  for (i = 0; i < insertedVnodeQueue.length; ++i) {
    insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i])
  }

  // hook post
  for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
  return vnode
}

結尾

以上分析了vue從template 到節點渲染的大致實現,當然也有某些地方沒有全面分析的地方,其中template解析為ast主要通過正則匹配實現,及節點渲染及更新的patch過程主要通過節點操作對比來實現。但是我們對編譯template字符串 => 代理data數據/methods的this綁定 => 數據觀察 => 建立watch及更新渲染的大致流程有了個比較完整的認知。

歡迎到前端學習打卡群一起學習~516913974

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

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

簡單的面試經歷和學習分享!_網頁設計公司

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

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

最近在交流群里,有找工作的小夥伴,經常會問到,怎樣去找工作(就是投簡歷)以及如何去準備面試,也有小夥伴會問到一些學習方面的問題,比如如何學習 Linux ,如何學習 stm32等等,今天的文章,就來總結一下我個人的看法。

 

一、關於找工作方面的問題:

 

現在找工作確實越來越來難了,首先在投簡歷第一關,就是刷學歷,一般學歷達不到要求的話,就沒機會進行下一步了(舉個例子,比如去招聘網站上投簡歷,人家招聘要求裏面是本科及以上,這樣的話你投簡歷的話少投,不是不可以投,為啥這樣,下面在找工作第二關會說明)。

 

然後是第二關,你投簡歷的時候,要仔細看一下招聘要求,然後結合自身掌握的技能,適當的去投一些可能學歷不符合的公司,因為你能力達到了招聘要求,這可以去投簡歷試試,一般有些公司如果覺得你技能達到他們的要求,也是會來破格來招聘的(但是一些大廠的話,一般對學歷方面要求很嚴,不管是社招還是應屆,所以這個一般投簡歷沒啥意識,不要浪費時間,這個自己也有親身體會,當然可能也有例外,這都說不定!);總之如果招聘要求裏面有對學歷有硬性要求的,而且註明達不到要求的,不要去投簡歷,這個時候雙方就不要互相打擾了;如果沒有說明都可以去試試,只要你的技能達到招聘要求,機會都是自己爭取來的,不要輕易放棄。

 

第三關,如何寫簡歷,這個想必對於大多數剛畢業的應屆生來說,都是非常缺乏經驗的;那麼重點來了,對於怎樣寫簡歷這個問題,首先不要向寫小說一樣長篇大幅,這樣投的簡歷給 hr 看的話,他(她)根本沒有這麼多時間來看你的簡歷,因為不只有你一個人來投簡歷,所以我們要抓重點來寫,比如說自己掌握哪些技能(在嵌入式方面,你可以寫,熟悉 c、c++ 等編程語言,不要用精通二字,精通二字不要亂用;然後可以寫一寫,你在校做的項目或者參賽經歷都可以精簡寫出來等等);其實關於這方面,我個人覺得boss直聘網站上的簡歷模板就非常好的(如果實在不會做簡歷的話,或者說網上沒有找到合適的模板),自己之前找工作就是用 boss 直聘上的簡歷模板。

 

第四關,如何準備面試;對於這個,對於我們應屆生來說,也是非常欠缺經驗的;這裏介紹一下面試過程(不是規範,但是是自己的親身面試經歷):

  • 一般去公司面試的話,一開始hr會跟你聊一下,主要是大概了解一下你的基本情況(比如說,再次介紹一下自己的一些基本情況,或者說談談你對這個行業的認識等等)。

  • 然後一般的話,都會有一個筆試題;因為我們都是做底層開發的,C語言基礎必考的(也有一些c語言高級用法);一些數據結構(比如棧,隊列、鏈表等)問題在解答題裏面都是會有的;甚至有些會考一些外設協議問題,這方面稍微注意一下數據結構,這幾天和小夥伴交流的時候,就發現他的數據結構知識沒有掌握好。

  • 然後在你做完筆試,一般是現場給你答覆的(當然也有要等幾天的),如果筆試過了話,這時候, hr 會跟你說,我們技術主管會跟再聊一下技術上的問題;技術主管的話,對於應屆生,一般不會問很難的技術問題(社招就可能難了,這個不好說),大部分是一些基礎的問題,主要是考察你的基礎;這裏還有一點要非常注意,就是當你遇到不會的問題的時候,你不要一直在哪裡想,這樣會給別人不好的印象,你可以換個角度說嘛(比如說這個平時很久沒接觸了,只要我一看這個,就能理解和知道的;這個只是我舉個例子來說哈,還是要自己靈活變通的)。

  • 然後技術主管問完了話,覺得你的技術可以的話,一般情況下會當場直接給offer的(當然也有要等幾天,因為有些崗位競爭比較激烈;如果覺的回答技術的時候,回答的不好,然後 hr 叫你回去等消息的話,可以大部分情況下是涼了,這個要心裏有數。),這個時候,會跟老闆再談一談,主要是薪資待遇方面。

  • 在跟老闆談薪資待遇的時候,要說清楚和談清楚——比如說,開工資,你開在招聘時給的薪資範圍之內就行,如果最後沒有達到你所想要的,這個時候,先不要直接拒絕,先可以考慮一下,再回復;還有一些福利待遇,比如五險一金,上升空間,上班時間等等;總之一句話,你真要進公司,把自己要問的,都要問清楚來。

     

 

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

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

二、關於學習方面:

 

第一、關於 Linux 方面的學習;Linux 學習主要是:Linux 應用和 Linux驅動。對於學習 Linux 應用,一般的話,我們大部分是用  ubuntu 來進行學習的,所以首先,你得必須搭建好環境(這裏可以推薦一個公眾號:軟件管家,裏面的軟件都有安裝教程,對於小白非常友好。),一般的話我們都是在虛擬機裏面安裝 Linux 系統的,這樣省事,而且也一般不建議真機安裝 Linux ,對於我們入手的新手來說;然後在安裝好 Linux 系統后,最好快照一下,以免下次打開的時候,出現宕機問題,搞不好你又要去重裝系統,那就浪費時間了;最後就是在 Linux 系統裏面搭建編程環境,比如說安裝 gcc  、vim  、g++ 等,最後就可以開始學習了,對於新手的話,建議看書加視頻結合(視頻的話,可以在 b 站上找,如果覺得不好,也可以自己花點錢去買視頻學習,反正現在花點錢,如果你學到了,到時候出來找到了工作,一下子就賺回來了,這也是給自己投資。);這裏還有一個建議,在學的過程多記筆記和總結,同時也要多和別人交流。

 

第二、驅動方面;這方面的話,自己也是剛學,給的建議不多,開發板一塊,主要是用來做實驗,還有就是c語言基礎要好,最難得是要經的起折騰。

 

第三、關於 stm32 的學習,這方面我不好說,最好的建議就是你可以去招聘網站上去看招聘要求,看看大多數公司要求會哪些方面,然後你可以結合自己買的開發板資料,在學完基礎的部分,然後根據這些招聘要求去適當提高自己和學習。上面的 Linux 學習也是這樣的。

 

第四、關於學習過程中遇到問題,怎麼解決?首先如果是你學過知識點,可以先仔細思考或實驗,當這個方法行不通的話,就可以用搜索引擎去查找(一般情況下,別人也有遇到過的你這樣的問題),這搜索引擎的話,百度、bing、谷歌都可以,只要能夠幫到你的,都要去嘗試;如果這些都解決不了的話,這個時候可以單獨向一些大神請教或者在相關技術交流群里問(不過問問題的時候,要把問題描述清楚,而且也要有禮貌,不要給別人一種就是要給我解決問題的感覺,這一點切記於心!);然後如果是你沒遇到過的知識點,這時候自己可以看看,有沒有手冊說明(比如說我們在搞一款沒遇到的芯片),如果沒有的話,可以去官網查找或者博客裏面查找(這裡在博客裏面查找問題或者知識點的話,我一般喜歡瀏覽量比較大的博客,因為一般情況下,這些有用的,當然也不能那麼絕對!)。

 

三、總結:

 

今天所分享的這兩方面,是在平時和網友交流過程中遇到的,也有我自己親身經歷的(比如找工作方面,當初對於做簡歷和面試,對於剛出來的我也是一張白紙,什麼都不懂,然後後面經歷過很多面試,最後多總結和復盤、走心,慢慢的,自然這些東西你就會了!)。上面的一些看法,可能寫的比較片面(主要是針對剛畢業的應屆生和在校生),希望對你有幫助,如有不足和欠缺的地方,可以在留言下方補充。同時這裏也分享兩個問題給大家思考,或許以後,你可能會遇到這樣的處境:

 

  • 比如說,如果你還在一家公司上班,然後跳槽, hr 會問你,你為啥要辭職,你會怎麼回答?

  • 如果在技術主管問到你不會的技術或者知識點,你會怎樣回答和應對?

更多精彩內容可以關注微信公眾號:

 

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

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

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

02 . Tomcat集群會話共享

redis簡介

redis是一個key-value存儲系統。和Memcached類似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set –有序集合)和hash(哈希類型)。與memcached一樣,為了保證效率,數據都是緩存在內存中。區別的是redis會周期性的把更新的數據寫入磁盤或者把修改操作寫入追加的記錄文件,並且在此基礎上實現master-slave(主從)同步。

Redis詳細請看我專門寫的redis

https://www.cnblogs.com/you-men/tag/Redis/

如何保持session會話

目前,為了使web能適應大規模的訪問,需要實現應用的集群部署。集群最有效的方案就是負載均衡,而實現負載均衡用戶每一個請求都有可能被分配到不固定的服務器上,這樣我們首先要解決session的統一來保證無論用戶的請求被轉發到哪個服務器上都能保證用戶的正常使用,即需要實現session的共享機制。

在集群系統下實現session統一的有如下幾種方案:

1、請求精確定位:sessionsticky,例如基於訪問ip的hash策略,即當前用戶的請求都集中定位到一台服務器中,這樣單台服務器保存了用戶的session登錄信息,如果宕機,則等同於單點部署,會丟失,會話不複製。

2、session複製共享:sessionreplication,如tomcat自帶session共享,主要是指集群環境下,多台應用服務器之間同步session,使session保持一致,對外透明。 如果其中一台服務器發生故障,根據負載均衡的原理,調度器會遍歷尋找可用節點,分發請求,由於session已同步,故能保證用戶的session信息不會丟失,會話複製,。

此方案的不足之處:

必須在同一種中間件之間完成(如:tomcat-tomcat之間).

session複製帶來的性能損失會快速增加.特別是當session中保存了較大的對象,而且對象變化較快時, 性能下降更加顯著,會消耗系統性能。這種特性使得web應用的水平擴展受到了限制。

Session內容通過廣播同步給成員,會造成網絡流量瓶頸,即便是內網瓶頸。在大併發下錶現並不好

3、基於cache DB緩存的session共享

基於memcache/redis緩存的 session 共享

即使用cacheDB存取session信息,應用服務器接受新請求將session信息保存在cache DB中,當應用服務器發生故障時,調度器會遍歷尋找可用節點,分發請求,當應用服務器發現session不在本機內存時,則去cache DB中查找,如果找到則複製到本機,這樣實現session共享和高可用。

nginx+tomcat+redis實現負載均衡、session共享

環境
主機 操作系統 IP地址 硬件/網絡
Nginx CentOS7.3 39.108.140.0 1C2G / 公有雲
Tomcat-1 CentOS7.3 121.36.43.2 1C2G / 公有雲
Tomcat-2 CentOS7.3 49.233.69.195 1C2G / 公有雲
Redis CentOS7.3 116.196.83.113 1C2G / 公有雲
MySQL CentOS7.3 116.196.83.113 1C2G / 公有雲
實驗拓撲

在這個圖中,nginx做為反向代理,實現靜動分離,將客戶動態請求根據權重隨機分配給兩台tomcat服務器,redis做為兩台tomcat的共享session數據服務器,mysql做為兩台tomcat的後端數據庫。

nginx安裝配置

使用Nginx作為Tomcat的負載平衡器,Tomcat的會話Session數據存儲在Redis,能夠實現零宕機的7×24效果。因為將會話存儲在Redis中,因此Nginx就不必配置成stick粘貼某個Tomcat方式,這樣才能真正實現後台多個Tomcat負載平衡。

部署nginx

#!/usr/bin/env bash
# Author: ZhouJian
# Mail: 18621048481@163.com
# Time: 2019-9-3
# Describe: CentOS 7 Install Nginx Source Code Script

version="nginx-1.14.2.tar.gz"
user="nginx"
nginx=${version%.tar*}
path=/usr/local/src/$nginx
echo $path
if ! ping -c2 www.baidu.com &>/dev/null
then
	echo "網絡不通,無法安裝"
	exit
fi

yum install -y gcc gcc-c++ openssl-devel pcre-devel make zlib-devel wget psmisc
if [ ! -e $version ];then
	wget http://nginx.org/download/$version
fi
if ! id $user &>/dev/null
then
	useradd $user -M -s /sbin/nologin
fi

if [ ! -d /var/tmp/nginx ];then
	mkdir -p /var/tmp/nginx/{client,proxy,fastcgi,uwsgi,scgi}
fi
tar xf $version -C /usr/local/src
cd $path
./configure \
--prefix=/usr/local/nginx \
--user=nginx \
--group=nginx \
--with-http_ssl_module \
--with-http_flv_module \
--with-http_stub_status_module \
--with-http_sub_module \
--with-http_gzip_static_module \
--with-http_auth_request_module \
--with-http_random_index_module \
--with-http_realip_module \
--http-client-body-temp-path=/var/tmp/nginx/client \
--http-proxy-temp-path=/var/tmp/nginx/proxy \
--http-fastcgi-temp-path=/var/tmp/nginx/fastcgi \
--http-uwsgi-temp-path=/var/tmp/nginx/uwsgi \
--http-scgi-temp-path=/var/tmp/nginx/scgi \
--with-pcre \
--with-file-aio \
--with-http_secure_link_module && make && make install
if [ $? -ne 0 ];then
	echo "nginx未安裝成功"
	exit
fi

killall nginx
/usr/local/nginx/sbin/nginx
#echo "/usr/local/nginx/sbin/nginx" >> /etc/rc.local
#chmod +x /etc/rc.local
#systemctl start rc-local
#systemctl enable rc-local
ss -antp |grep nginx

配置nginx反向代理:反向代理+負載均衡+健康探測,nginx.conf文件內容:

vim /usr/local/nginx/conf/nginx.conf
worker_processes  4;
events {
        worker_connections  1024;
}
    http {
        include       mime.types;
        default_type  application/octet-stream;
        sendfile        on;
        keepalive_timeout  65;
        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                        '$status $body_bytes_sent "$http_referer" '
                        '"$http_user_agent" "$http_x_forwarded_for"';

    #blog lb by oldboy at 201303
        upstream backend_tomcat {
        #ip_hash;
        server 192.168.6.241:8080   weight=1 max_fails=2 fail_timeout=10s;
        server 192.168.6.242:8080   weight=1 max_fails=2 fail_timeout=10s;
        #server 192.168.6.243:8080   weight=1 max_fails=2 fail_timeout=10s;
        }

        server {
            listen       80;
            server_name  www.98yz.cn;
            charset utf-8;
            location / {
                root html;
                index  index.jsp index.html index.htm;
                    }
            location ~* \.(jsp|do)$ {
            proxy_pass  http://backend_tomcat;
            proxy_redirect off;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
                }
        }

    }
安裝部署tomcat應用程序服務器

在tomcat-1和tomcat-2節點上安裝JDK

在安裝tomcat之前必須先安裝JDK,JDK的全稱是java development kit,是sun公司免費提供的java語言的軟件開發工具包,其中包含java虛擬機(JVM),編寫好的java源程序經過編譯可形成java字節碼,只要安裝了JDK,就可以利用JVM解釋這些字節碼文件,從而保證了java的跨平台性。

安裝JDK,Tomcat 程序

tar xvf jdk-8u151-linux-x64.tar.gz -C /usr/local/
wget https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-8/v8.5.55/bin/apache-tomcat-8.5.55.tar.gz
tar xf apache-tomcat-8.5.55.tar.gz -C /usr/local/
cd /usr/local/
mv apache-tomcat-8.5.55/ tomcat
mv jdk1.8.0_151/ jdk

按照相同方法在tomcat-2也安裝

vim conf/server.xml

// 設置默認虛擬主機,並增加jvmRoute
<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat-1">
  
// 修改默認虛擬主機,並將網站文件路徑指向/web/webapp1,在host段增加context段  
<Host name="localhost"  appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Context docBase="/web/webapp1" path="" reloadable="true"/>
</Host>
  
  
// 增加文檔目錄與測試文件  
mkdir -p /web/webapp1
cd /web/webapp1
cat index.jsp 
<%@page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<html>
    <head>
        <title>tomcat-1</title>
    </head>
    <body>
        <h1><font color="red">Session serviced by tomcat</font></h1>
        <table aligh="center" border="1">
        <tr>
            <td>Session ID</td>
            <td><%=session.getId() %></td>
                <% session.setAttribute("abc","abc");%>
            </tr>
            <tr>
            <td>Created on</td>
            <td><%= session.getCreationTime() %></td>
            </tr>
        </table>
    tomcat-1
    </body>
<html>  
  
  
// 接下來我們將tomcat和nginx都啟動起來,可以發現用戶訪問index.jsp會一會跳轉tomcat1,一會tomcat2,session還不一致  

Tomcat-2節點與tomcat-1節點配置基本類似,只是jvmRoute不同,另外為了區分由哪個節點提供訪問,測試頁標題也不同(生產環境兩個tomcat服務器提供的網頁內容是相同的)。其他的配置都相同。

用瀏覽器訪問nginx主機,驗證負載均衡

驗證健康檢查的方法可以關掉一台tomcat主機,用客戶端瀏覽器測試訪問。

從上面的結果能看出兩次訪問,nginx把訪問請求分別分發給了後端的tomcat-1和tomcat-2,客戶端的訪問請求實現了負載均衡,但sessionid並一樣。所以,到這裏我們準備工作就全部完成了,下面我們來配置tomcat通過redis實現會話保持。

安裝redis
yum -y install gcc
wget http://download.redis.io/releases/redis-4.0.14.tar.gz
tar xvf redis-4.0.14.tar.gz -C /opt/
cd /opt/redis-4.0.14

編譯安裝

# Redis的編譯,只將命令文件編譯,將會在當前目錄生成bin目錄
make && make install  PREFIX=/usr/local/redis
cd ..
mv redis-4.0.14/* /usr/local/redis/

# 創建環境變量
echo 'PATH=$PATH:/usr/local/redis/src/' >> /etc/profile
source /etc/profile

# 此時在任何目錄位置都可以是用redis-server等相關命令
[root@redis1 ~]# redis-
redis-benchmark  redis-check-rdb  redis-sentinel   redis-trib.rb    
redis-check-aof  redis-cli        redis-server 

配置Redis

# 設置後台啟動
# 由於Redis默認是前台啟動,不建議使用.可以修改為後台
daemonize yes


# 禁止protected-mode yes/no(保護模式,是否只允許本地訪問)
protected-mode


# 設置遠程訪問
# Redis默認只允許本機訪問,把bind修改為bind 0.0.0.0 此設置會變成允許所有遠程訪問,如果指定限制訪問,可設置對應IP。
# bind指定是redis所在服務器網卡的IP,不指定本機網卡IP,可能導致你的Redis實例無法啟動
# 如果想限制IP訪問,內網的話通過網絡接口(網卡限定),讓客戶端訪問固定網卡鏈接redis
# 如果是公網,通過iptables指定某個IP允許訪問
bind 0.0.0.0

# 配置Redis日誌記錄
# 找到logfile,默認為logfile "",改為自定義日誌格式
logfile  /var/log/redis_6379.log

# 把requirepass修改為123456,修改之後重啟下服務
requirepass "123456"
# 不重啟Redis設置密碼
# 在配置文件中配置requirepass的密碼(當Redis重啟時密碼依然生效)
127.0.0.1:6379> config set requirepass test123
# 查詢密碼
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "test123"

# 密碼驗證
127.0.0.1:6379> auth test123
OK
127.0.0.1:6379> set name flying
OK
127.0.0.1:6379> get name
"flying"

# 遠程主機連接
# redis-cli  -h  redis_ip -p redis_port -a password

啟動測試

# 放到後台輸出,redis自帶日誌了,可以輸出到黑洞
nohup redis-server /usr/local/redis/redis.conf &> /usr/local/redis/redis.log &

# 關閉命令
redis-cli -h 127.0.0.1 -p 6379 -a 123456 shutdown
# 注意:不建議使用 kill -9,這種方式不但不會做持久化操作,還會造成緩衝區等資源不能優雅關閉。極端情況下造成 AOF 和 複製丟失數據 的情況。
# shutdown 還有一個參數,代表是否在關閉 redis 前,生成 持久化文件,命令為 redis-cli shutdown nosave|save。


# 設置開機自啟動
echo "redis-server /usr/local/redis.conf" >> /etc/rc.local

配置tomcat session redis同步

通過TomcatClusterRedisSessionManager,這種方式支持redis3.0的集群方式
下載TomcatRedisSessionManager-2.0.zip包,https://github.com/ran-jit/tomcat-cluster-redis-session-manager,放到$TOMCAT_HOMA/lib下,並解壓

cd /usr/local/tomcat/lib/
wget https://github.com/ran-jit/tomcat-cluster-redis-session-manager/releases/download/2.0.4/tomcat-cluster-redis-session-manager.zip
unzip tomcat-cluster-redis-session-manager.zip 
cp tomcat-cluster-redis-session-manager/lib/* ./
cp tomcat-cluster-redis-session-manager/conf/redis-data-cache.properties ../conf/
cat ../conf/redis-data-cache.properties     
#-- Redis data-cache configuration
//遠端redis數據庫的地址和端口
#- redis hosts ex: 127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.2:6380, ....
redis.hosts=192.168.6.244:6379
//遠端redis數據庫的連接密碼
#- redis password (for stand-alone mode)
redis.password=pwd@123
//是否支持集群,默認的是關閉
#- set true to enable redis cluster mode
redis.cluster.enabled=false
//連接redis的那個庫
#- redis database (default 0)
#redis.database=0
//連接超時時間
#- redis connection timeout (default 2000)
#redis.timeout=2000
//在這個<Context>標籤裏面配置

vim ../conf/context.xml
<Valve className="tomcat.request.session.redis.SessionHandlerValve" />
<Manager className="tomcat.request.session.redis.SessionManager" />

配置會話到期時間在../conf/web.xml

<session-config>
<session-timeout>60</session-timeout>
</session-config>

啟動tomcat服務

[root@linux-node2 lib]# ../bin/startup.sh

Tomcat-2節點與tomcat-1節點配置相同

測試,我們每次強刷他的sessionID都是一致的,所以我們認為他的session會話保持已經完成,你們也可以選擇換個客戶端的IP地址來測試

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準