傳小米電動車已進入量產 售價新台幣 20 萬有找

小米在智慧型手機及資訊領域攻城掠地後,顯然創辦人雷軍並未因此滿足,打從更早之前就一直有傳聞小米想要打造車子,而且還是高技術門檻的電動車,如今又有進一步消息指稱,小米電動車已經進入量產階段。   身為 Tesla 在中國市場的第一批車主,雷軍也曾多次親自拜訪 Tesla 的執行長 Elon Musk,交流許多關於電動車的未來、以及對智慧車輛的看法,近期中國網路上盛傳,內部代號「米斯拉」的小米牌電動車已經開始量產,甚至連其代工合作夥伴為比亞迪等資訊都被揭露。   消息也指出,米斯拉除了會是台電動車外,還會搭載小米自家的 MIUI 系統,將擁有豐富的智慧聯網功能;更驚人的是,這樣的一台高科技智慧電動車,售價僅要 39,999 人民幣,折合新台幣不過 20 萬有找,若此消息為真,小米將會在車界掀起另一波破壞巨浪。    

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

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

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

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

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

助力新能源車 意法半導體推新款車規碳化矽二極體

意法半導體推出新款車規碳化矽(SiC)二極體,以滿足電動汽車和插電式混合動力車(PHEVs,Plug-in Hybrids)等新能源汽車對車載充電器(OBCs,on-board battery chargers)在有限空間內處理大功率的苛刻要求。  

  新款二極體採用先進的技術可防止高電流突波燒毀裝置,其過電流保護是額定電流的2.5倍,因此設計人員可選用更小、更經濟實惠且可靠性和效能都不會受到影響的電流更小的二極體。此新碳化矽二極體通過車規產品測試,反向擊穿電壓提高到650V,能滿足設計人員和汽車廠商欲降低電壓補償係數的要求,以確保車載充電半導體元件的標準與瞬間峰值電壓之間有充足的安全邊際。   這次推出的650V二極體包括TO-220AC功率封裝的10A STPSC10H065DY和TO-220AC封裝的12A STPSC12H065DY。此外,TO-220AB封裝的STPSC20H065CTY和TO-247封裝的STPSC20H065CWY是內建2個10A二極體的雙二極體(dual-diode )產品,可最大幅度地提升空間利用度並減少車載充電器的重量。

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

akka-typed(8) – CQRS讀寫分離模式

 前面介紹了事件源(EventSource)和集群(cluster),現在到了討論CQRS的時候了。CQRS即讀寫分離模式,由獨立的寫方程序和讀方程序組成,具體原理在以前的博客里介紹過了。akka-typed應該自然支持CQRS模式,最起碼本身提供了對寫方編程的支持,這點從EventSourcedBehavior 可以知道。akka-typed提供了新的EventSourcedBehavior-Actor,極大方便了對persistentActor的應用開發,但同時也給編程者造成了一些限制。如手工改變狀態會更困難了、EventSourcedBehavior不支持多層式的persist,也就是說通過persist某些特定的event然後在event-handler程序里進行狀態處理是不可能的了。我這裡有個例子,是個購物車應用:當完成支付后需要取個快照(snapshot),下面是這個snapshot的代碼:

 snapshotWhen { (state,evt,seqNr) => CommandHandler.takeSnapshot(state,evt,seqNr) } ... def takeSnapshot(state: Voucher, evt: Events.Action, lstSeqNr: Long)(implicit pid: PID) = { if (evt.isInstanceOf[Events.PaymentMade] || evt.isInstanceOf[Events.VoidVoucher.type] || evt.isInstanceOf[Events.SuspVoucher.type]) if (state.items.isEmpty) { log.step(s"#${state.header.num} taking snapshot at [$lstSeqNr] ...") true } else
        false
    else
      false

}

判斷event類型是沒有問題的,因為正是當前的事件,但另一個條件是購物車必須是清空了的。這個有點為難,因為這個狀態要依賴這幾個event運算的結果才能確定,也就是下一步,但確定結果又需要對購物車內容進行計算,好像是個死循環。在akka-classic里我們可以在判斷了event運算結果后,如果需要改變狀態就再persist一個特殊的event,然後在這個event的handler進行狀態處理。沒辦法,EventSourcedBehavior不支持多層persist,只有這樣做:

 

      case PaymentMade(acct, dpt, num, ref,amount) => ... writerInternal.lastVoucher = Voucher(vchs, vItems) endVoucher(Voucher(vchs,vItems),TXNTYPE.sales) Voucher(vchs.nextVoucher, List()) ...  

 

我只能先吧當前狀態保存下來、進行結單運算、然後清空購物車,這樣snapshot就可以順利進行了。

好了,akka的讀方編程是通過PersistentQuery實現的。reader的作用就是把event從數據庫讀出來后再恢復成具體的數據格式。我們從reader的調用了解一下這個應用里reader的實現細節:

 

    val readerShard = writerInternal.optSharding.get val readerRef = readerShard.entityRefFor(POSReader.EntityKey, s"$pid.shopId:$pid.posId") readerRef ! Messages.PerformRead(pid.shopid, pid.posid,writerInternal.lastVoucher.header.num,writerInternal.lastVoucher.header.opr,bseq,eseq,txntype,writerInternal.expurl,writerInternal.expacct,writerInternal.exppass)

可以看到這個reader是一個集群分片,sharding-entity。想法是每單完成購買后發個消息給一個entity、這個entity再完成reader功能后自動終止,立即釋放出佔用的資源。reader-actor的定義如下:

object POSReader extends LogSupport { val EntityKey: EntityTypeKey[Command] = EntityTypeKey[Command]("POSReader") def apply(nodeAddress: String, trace: Boolean): Behavior[Command] = { log.stepOn = trace implicit var pid: PID = PID("","") Behaviors.supervise( Behaviors.setup[Command] { ctx => Behaviors.withTimers { timer =>
          implicit val ec = ctx.executionContext Behaviors.receiveMessage { case PerformRead(shopid, posid, vchnum, opr, bseq, eseq, txntype, xurl, xacct, xpass) => pid = PID(shopid, posid) log.step(s"POSReader: PerformRead($shopid,$posid,$vchnum,$opr,$bseq,$eseq,$txntype,$xurl,$xacct,$xpass)")(PID(shopid, posid)) val futReadSaveNExport = for { txnitems <- ActionReader.readActions(ctx, vchnum, opr, bseq, eseq, trace, nodeAddress, shopid, posid, txntype) _ <- ExportTxns.exportTxns(xurl, xacct, xpass, vchnum, txntype == Events.TXNTYPE.suspend, { if(txntype == Events.TXNTYPE.voidall) txnitems.map (_.copy(txntype=Events.TXNTYPE.voidall)) else txnitems }, trace)(ctx.system.toClassic, pid) } yield () ctx.pipeToSelf(futReadSaveNExport) { case Success(_) => { timer.startSingleTimer(ReaderFinish(shopid, posid, vchnum), readInterval.seconds) StopReader } case Failure(err) => log.error(s"POSReader: Error: ${err.getMessage}") timer.startSingleTimer(ReaderFinish(shopid, posid, vchnum), readInterval.seconds) StopReader } Behaviors.same case StopReader => Behaviors.same case ReaderFinish(shopid, posid, vchnum) => Behaviors.stopped( () => log.step(s"POSReader: {$shopid,$posid} finish reading voucher#$vchnum and stopped")(PID(shopid, posid)) ) } } } ).onFailure(SupervisorStrategy.restart) }

reader就是一個普通的actor。值得注意的是讀方程序可能是一個龐大複雜的程序,肯定需要分割成多個模塊,所以我們可以按照流程順序進行模塊功能切分:這樣下面的模塊可能會需要上面模塊產生的結果才能繼續。記住,在actor中絕對避免阻塞線程,所有的模塊都返回Future, 然後用for-yield串起來。上面我們用了ctx.pipeToSelf 在Future運算完成后發送ReaderFinish消息給自己,通知自己停止。

在這個例子里我們把reader任務分成:

1、從數據庫讀取事件

2、事件重演一次產生狀態數據(購物車內容)

3、將形成的購物車內容作為交易單據項目存入數據庫

4、向用戶提供的restapi輸出交易數據

event讀取是通過cassandra-persistence-plugin實現的:

    val query = PersistenceQuery(classicSystem).readJournalFor[CassandraReadJournal](CassandraReadJournal.Identifier) // issue query to journal
    val source: Source[EventEnvelope, NotUsed] = query.currentEventsByPersistenceId(s"${pid.shopid}:${pid.posid}", startSeq, endSeq) // materialize stream, consuming events
    val readActions: Future[List[Any]] = source.runFold(List[Any]()) { (lstAny, evl) => evl.event :: lstAny }

這部分比較簡單:定義一個PersistenceQuery,用它產生一個Source,然後run這個Source獲取Future[List[Any]]。

重演事件產生交易數據:

    def buildVoucher(actions: List[Any]): List[TxnItem] = { log.step(s"POSReader: read actions: $actions") val (voidtxns,onlytxns) = actions.asInstanceOf[Seq[Action]].pickOut(_.isInstanceOf[Voided]) val listOfActions = onlytxns.reverse zip (LazyList from 1)   //zipWithIndex
      listOfActions.foreach { case (txn,idx) => txn.asInstanceOf[Action] match { case Voided(_) =>
          case ti@_ => curTxnItem = EventHandlers.buildTxnItem(ti.asInstanceOf[Action],vchState).copy(opr=cshr) if(voidtxns.exists(a => a.asInstanceOf[Voided].seq == idx)) { curTxnItem = curTxnItem.copy(txntype = TXNTYPE.voided, opr=cshr) log.step(s"POSReader: voided txnitem: $curTxnItem") } val vch = EventHandlers.updateState(ti.asInstanceOf[Action],vchState,vchItems,curTxnItem,true) vchState = vch.header vchItems = vch.txnItems log.step(s"POSReader: built txnitem: ${vchItems.txnitems.head}") } } log.step(s"POSReader: voucher built with state: $vchState, items: ${vchItems.txnitems}") vchItems.txnitems }

重演List[Event],產生了List[TxnItem]。

向數據庫里寫List[TxnItem]:

 

 def writeTxnsToDB(vchnum: Int, txntype: Int, bseq: Long, eseq: Long, txns: List[TxnItem])( implicit system: akka.actor.ActorSystem, session: CassandraSession, pid: PID): Future[Seq[TxnItem]] = ???

注意返回結果類型Future[Seq[TxnItem]]。我們用for-yield把這幾個動作串起來:

  val txnitems: Future[List[Events.TxnItem]] = for { lst1 <- readActions    //read list from Source
      lstTxns <- if (lst1.length < (endSeq -startSeq))    //if imcomplete list read again
 readActions else FastFuture.successful(lst1) items <- FastFuture.successful( buildVoucher(lstTxns) ) _ <- JournalTxns.writeTxnsToDB(vchnum,txntype,startSeq,endSeq,items) _ <- session.close(ec) } yield items

注意返回結果類型Future[Seq[TxnItem]]。我們用for-yield把這幾個動作串起來:

  val txnitems: Future[List[Events.TxnItem]] = for { lst1 <- readActions    //read list from Source
      lstTxns <- if (lst1.length < (endSeq -startSeq))    //if imcomplete list read again
 readActions else FastFuture.successful(lst1) items <- FastFuture.successful( buildVoucher(lstTxns) ) _ <- JournalTxns.writeTxnsToDB(vchnum,txntype,startSeq,endSeq,items) _ <- session.close(ec) } yield items

注意:這個for返回的Future[List[TxnItem]],是提供給restapi輸出功能的。在那裡List[TxnItem]會被轉換成json作為post的包嵌數據。

現在所有子任務的返回結果類型都是Future了。我們可以再用for來把它們串起來:

             val futReadSaveNExport = for { txnitems <- ActionReader.readActions(ctx, vchnum, opr, bseq, eseq, trace, nodeAddress, shopid, posid, txntype) _ <- ExportTxns.exportTxns(xurl, xacct, xpass, vchnum, txntype == Events.TXNTYPE.suspend, { if(txntype == Events.TXNTYPE.voidall) txnitems.map (_.copy(txntype=Events.TXNTYPE.voidall)) else txnitems }, trace)(ctx.system.toClassic, pid) } yield ()

說到EventSourcedBehavior,因為用了cassandra-plugin,忽然想起配置文件里新舊有很大區別。現在這個application.conf是這樣的: 

akka { loglevel = INFO actor { provider = cluster serialization-bindings { "com.datatech.pos.cloud.CborSerializable" = jackson-cbor } } remote { artery { canonical.hostname = "192.168.11.189" canonical.port = 0 } } cluster { seed-nodes = [ "akka://cloud-pos-server@192.168.11.189:2551"] sharding { passivate-idle-entity-after = 5 m } } # use Cassandra to store both snapshots and the events of the persistent actors persistence { journal.plugin = "akka.persistence.cassandra.journal" snapshot-store.plugin = "akka.persistence.cassandra.snapshot" } } akka.persistence.cassandra { # don't use autocreate in production
  journal.keyspace = "poc2g" journal.keyspace-autocreate = on journal.tables-autocreate = on snapshot.keyspace = "poc2g_snapshot" snapshot.keyspace-autocreate = on snapshot.tables-autocreate = on } datastax-java-driver { basic.contact-points = ["192.168.11.189:9042"] basic.load-balancing-policy.local-datacenter = "datacenter1" }

akka.persitence.cassandra段落里可以定義keyspace名稱,這樣新舊版本應用可以共用一個cassandra,同時在線。

 

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

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

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

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

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

Python 簡明教程 — 19,Python 類與對象

微信公眾號:碼農充電站pro
個人主頁:https://codeshellme.github.io

那些能用計算機迅速解決的問題,就別用手做了。
—— Tom Duff

目錄

上一節 我們介紹了Python 面向對象的相關概念,我們已經知道類與對象面向對象編程中非常重要的概念。

類就是一個模板,是抽象的。對象是由類創建出來的實例,是具體的。由同一個類創建出來的對象擁有相同的方法屬性,但屬性的值可以是不同的。不同的對象是不同的實例,互不干擾。

1,類的定義

如下,是一個最簡單的類,實際上是一個空類,不能做任何事情:

class People:
    pass

在Python 中定義一個類,需要用到class 關鍵字,後邊是類名,然後是一個冒號:,然後下一行是類中的代碼,注意要有縮進

2,創建對象

People 雖然是一個空類,但依然可以創建對象,創建一個對象的語法為:

對象名 = 類名(參數列表)

參數列表是跟__init__ 構造方法相匹配的,如果沒有編寫__init__ 方法,創建對象時,就不需要寫參數,如下:

>>> p = People()
>>> p
<__main__.People object at 0x7fd30e60be80>
>>> 
>>> p1 = People()
>>> p1
<__main__.People object at 0x7fd30e60be48>

pp1 都是People類的對象。0x7fd30e60be80p 的地址,0x7fd30e60be48p1 的地址。可以看到不同的對象的地址是不同的,它們是兩不同的實例,互不干擾。

3,屬性

類中可以包含屬性類中的變量),創建出來的對象就會擁有相應的屬性,每個對象的屬性的值可以不同。

創建好對象后,可以用如下方法給對象添加屬性:

>>> p = People()
>>> p.name = '小明' # 添加 name 屬性
>>> p.sex = '男'    # 添加 sex 屬性
>>> p.name         # 訪問對象的屬性
'小明'
>>> p.sex          # 訪問對象的屬性
'男'

雖然在技術上可以這樣做,但是一般情況下,我們並不這樣為對象添加屬性,這樣會破壞類的封裝性,使得代碼混亂,不利於維護。

當訪問一個不存在的屬性時,會出現異常:

>>> p.job         # 一個不存在的屬性
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'People' object has no attribute 'job'

我們一般會在__init__ 方法中為類添加屬性並賦值。

4,__init__ 方法

在Python 的類中,以雙下劃線__開頭和結尾的方法,被稱為魔法方法,每個魔法方法都有特定的含義。Python 為我們規定了一些魔法方法,讓我們自己實現這些方法。

__init__ 方法叫做構造方法,用來初始化對象。Python 解釋器會在生成對象時,自動執行構造方法,而無需用戶显示調用。

__init__ 方法不需要有返回值。

類中的所有實例方法 方法,都至少有一個參數,就是self。Python 中的self 相當於C++ 和Java 中的this 指針,都是代表當前對象。只是Python 中的self 需要显示寫在方法的第一個參數,而this 指針則不需要寫在方法參數中。

構造方法一般用於初始化對象的一些屬性,構造函數可以不寫,也可以只有一個self 參數。

當構造函數只有一個self 參數時,創建該類的對象時,不需要添加參數。當構造函數除了self 參數還有其它參數時,創建該類的對象時,則需要添加相匹配的參數。

比如,我們定義一個People 類,它有三個屬性,分別是namesexage

class People:

    def __init__(self, name, sex, age):
        self.name = name
        self.sex = sex
        self.age = age
        print('執行了 __init__ 方法')

    def print_info(self):
        print('people:%s sex:%s age:%s' % (
            self.name, self.sex, self.age))

在這個People 類中除了有一個__init__ 方法外,還有一個print_info 方法,每個方法中的都有self 參數,並且是第一個參數,self 代表當前對象。

在創建該類的對象時,需要傳遞匹配的參數(self 參數不用傳遞):

>>> p = People('小明', '男', 18)
執行了 __init__ 方法
>>> p 
<People.People object at 0x7feb6276bda0>
>>> p.print_info()
people:小明 sex:男 age:18
>>>
>>> p1 = People('小美', '女', 18)
執行了 __init__ 方法
>>> p1
<People.People object at 0x7fd54352be48>
>>> p1.print_info()
people:小美 sex:女 age:18

可以看到,在創建pp1 對象時,字符串執行了 __init__ 方法 被打印了出來,而我們並沒有显示調用該方法,說明__init__ 方法被默認執行了。

對象pp1 是兩個不同的對象,擁有相同的屬性和方法,但是屬性值是不一樣的。兩個對象互不干擾,對象p 的地址為0x7feb6276bda0p1 的地址是0x7fd54352be48

執行代碼p.print_info(),是調用p 對象的print_info() 方法,因為,在定義該方法的時候,只有一個self 參數,所以在調用該方法的時候,不需要有參數。

5,私有屬性和方法

私有屬性

普通的屬性,就像上面的namesexage 屬性,都是公有屬性,在類的外部都可以被任意的訪問,就是可以用對象.屬性名的方式來訪問屬性,如下:

>>> p = People('小明', '男', 18)
執行了 __init__ 方法
>>> p.name  # 訪問屬性
'小明'
>>> p.name = '小麗'  # 修改屬性
>>> p.name  # 訪問屬性
'小麗'

這樣就破壞了數據的封裝性,這種訪問方式是不可控(會不受限制的被任意訪問)的,不利於代碼的維護,不符合面向對象的編程規範。

所以,通常我們會將類中的屬性,改為私有屬性,就是不能以對象.屬性名 這樣的方式訪問類屬性。

在Python 中,通過在屬性名的前邊添加雙下劃線__,來將公有屬性變為私有屬性,如下:

#! /usr/bin/env python3

class People:

    def __init__(self, name, sex, age):
        self.__name = name   # 兩個下劃線
        self.__sex = sex     # 兩個下劃線
        self._age = age      # 一個下劃線
        print('執行了 __init__ 方法')

    def print_info(self):
        print('people:%s sex:%s age:%s' % (
            self.__name, self.__sex, self._age))

這樣就無法通過對象.屬性名的方式來訪問屬性了,如下:

>>> p = People('小美', '女', 18)
執行了 __init__ 方法
>>> p.__name        # 出現異常
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'People' object has no attribute '__name'

但是,Python 中這種私有屬性的方式,並不是真正的私有屬性,Python 只是將__name 轉換為了_People__name,即是在__name 的前邊加上了_類名(_People),我們依然可以這樣訪問__name 屬性:

>>> p._People__name
'小美'

但我們並不提倡這種方式,這會讓代碼變得混亂難懂。

可以注意到,People 類中的_age 屬性是以單下劃線開頭的,這種以單下劃線開頭的屬性是可以在類的外部被訪問的:

>>> p._age
18

但是根據Python 規範,以單下劃線開頭的屬性,也被認為是私有屬性,也不應該在類的外部訪問(雖然在技術上是可以訪問的)。

注意:以雙下劃線__ 開頭且結尾的屬性__xxx__,是特殊屬性,是公有的,可在類的外部訪問

私有方法

私有方法與私有屬性類似,也可以在方法名的前邊加上雙下劃線__,來將某個方法變成私有的,一般不需要被外部訪問的方法,應該將其設置為私有方法

6,setget 方法

為了數據的封裝性,我們不應該直接在類的外部以對象.屬性名的方式訪問屬性,那麼如果我們需要訪問類的屬性該怎麼辦呢?

這時我們需要為每個私有屬性都提供兩個方法:

  • set 方法:用於設置屬性的值
  • get 方法:用於訪問屬性的值

為了減少代碼量,這裏只為__name 屬性設置了這兩個方法,代碼如下:

#! /usr/bin/env python3

class People:

    def __init__(self, name, sex, age):
        self.__name = name
        self.__sex = sex
        self._age = age
        print('執行了 __init__ 方法')

    def print_info(self):
        print('people:%s sex:%s age:%s' % (
            self.__name, self.__sex, self._age))

    # set 和 get 方法
    def set_name(self, name):
        self.__name = name

    def get_name(self):
        return self.__name

用戶可以這樣設置和訪問類的屬性:

>>> from People import People
>>> p = People('小美', '女', 18)
執行了 __init__ 方法
>>> p.get_name()         # 獲取 name 值
'小美'
>>> p.set_name('小麗')   # 設置新的值
>>> p.get_name()        # 再次獲取name 值
'小麗'

因為這種setget 方法,是由類的開發者提供的,是被開發者控制的。

類的開發者會根據需要,來控制類的使用者如何使用該類,即哪些類的屬性和方法應該被使用者訪問,以及如何被使用者訪問。

如此,類的使用者就不能隨便的訪問類中的屬性,這就達到了封裝的目的。

(完。)

推薦閱讀:

Python 簡明教程 — 14,Python 數據結構進階

Python 簡明教程 — 15,Python 函數

Python 簡明教程 — 16,Python 高階函數

Python 簡明教程 — 17,Python 模塊與包

Python 簡明教程 — 18,Python 面向對象

歡迎關注作者公眾號,獲取更多技術乾貨。

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

手摸手帶你理解Vue的Computed原理

前言

computedVue 中是很常用的屬性配置,它能夠隨着依賴屬性的變化而變化,為我們帶來很大便利。那麼本文就來帶大家全面理解 computed 的內部原理以及工作流程。

在這之前,希望你能夠對響應式原理有一些理解,因為 computed 是基於響應式原理進行工作。如果你對響應式原理還不是很了解,可以閱讀我的上一篇文章:手摸手帶你理解Vue響應式原理

computed 用法

想要理解原理,最基本就是要知道如何使用,這對於後面的理解有一定的幫助。

第一種,函數聲明:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 計算屬性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 實例
      return this.message.split('').reverse().join('')
    }
  }
})

第二種,對象聲明:

computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

溫馨提示:computed 內使用的 data 屬性,下文統稱為“依賴屬性”

工作流程

先來了解下 computed 的大概流程,看看計算屬性的核心點是什麼。

入口文件:

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

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

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

export default Vue

_init:

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

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

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

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

initState:

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

initComputed:

// 源碼位置:/src/core/instance/state.js 
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  // 1
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
    
  for (const key in computed) {
    const userDef = computed[key]
    // 2
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if (!isSSR) {
      // create internal watcher for the computed property.
      // 3
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        { lazy: true }
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      // 4
      defineComputed(vm, key, userDef)
    }
  }
}
  1. 實例上定義 _computedWatchers 對象,用於存儲“計算屬性Watcher
  2. 獲取計算屬性的 getter,需要判斷是函數聲明還是對象聲明
  3. 創建“計算屬性Watcher”,getter 作為參數傳入,它會在依賴屬性更新時進行調用,並對計算屬性重新取值。需要注意 Watcherlazy 配置,這是實現緩存的標識
  4. defineComputed 對計算屬性進行數據劫持

defineComputed:

// 源碼位置:/src/core/instance/state.js 
const noop = function() {}
// 1
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 判斷是否為服務端渲染
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    // 2
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    // 3
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // 4
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
  1. sharedPropertyDefinition 是計算屬性初始的屬性描述對象
  2. 計算屬性使用函數聲明時,設置屬性描述對象的 getset
  3. 計算屬性使用對象聲明時,設置屬性描述對象的 getset
  4. 對計算屬性進行數據劫持,sharedPropertyDefinition 作為第三個給參數傳入

客戶端渲染使用 createComputedGetter 創建 get,服務端渲染使用 createGetterInvoker 創建 get。它們兩者有很大的不同,服務端渲染不會對計算屬性緩存,而是直接求值:

function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

但我們平常更多的是討論客戶端渲染,下面看看 createComputedGetter 的實現。

createComputedGetter:

// 源碼位置:/src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    // 1
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 2
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 3
      if (Dep.target) {
        watcher.depend()
      }
      // 4
      return watcher.value
    }
  }
}

這裏就是計算屬性的實現核心,computedGetter 也就是計算屬性進行數據劫持時觸發的 get

  1. 在上面的 initComputed 函數中,“計算屬性Watcher”就存儲在實例的_computedWatchers上,這裏取出對應的“計算屬性Watcher
  2. watcher.dirty 是實現計算屬性緩存的觸發點,watcher.evaluate 對計算屬性重新求值
  3. 依賴屬性收集“渲染Watcher
  4. 計算屬性求值後會將值存儲在 value 中,get 返回計算屬性的值

計算屬性緩存及更新

緩存

下面我們來將 createComputedGetter 拆分,分析它們單獨的工作流程。這是緩存的觸發點:

if (watcher.dirty) {
  watcher.evaluate()
}

接下來看看 Watcher 相關實現:

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    // dirty 初始值等同於 lazy
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

還記得創建“計算屬性Watcher”,配置的 lazy 為 true。dirty 的初始值等同於 lazy。所以在初始化頁面渲染,對計算屬性進行取值時,會執行一次 watcher.evaluate

evaluate() {
  this.value = this.get()
  this.dirty = false
}

求值后將值賦給 this.value,上面 createComputedGetter 內的 watcher.value 就是在這裏更新。接着 dirty 置為 false,如果依賴屬性沒有變化,下一次取值時,是不會執行 watcher.evaluate 的, 而是直接就返回 watcher.value,這樣就實現了緩存機制。

更新

依賴屬性在更新時,會調用 dep.notify:

notify() {
  this.subs.forEach(watcher => watcher.update())
}

然後執行 watcher.update:

update() {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

由於“計算屬性Watcher”的 lazy 為 true,這裏 dirty 會置為 true。等到頁面渲染對計算屬性取值時,符合觸發點條件,執行 watcher.evaluate 重新求值,計算屬性隨之更新。

依賴屬性收集依賴

收集計算屬性Watcher

初始化時,頁面渲染會將“渲染Watcher”入棧,並掛載到Dep.target

在頁面渲染過程中遇到計算屬性,對其取值,因此執行 watcher.evaluate 的邏輯,接着調用 this.get:

get () {
  // 1
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 2
    value = this.getter.call(vm, vm) // 計算屬性求值
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    popTarget()
    this.cleanupDeps()
  }
  return value
}
Dep.target = null
let stack = []  // 存儲 watcher 的棧

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

export function popTarget(){
  stack.pop()
  Dep.target = stack[stack.length - 1]
}

pushTarget 輪到“計算屬性Watcher”入棧,並掛載到Dep.target,此時棧中為 [渲染Watcher, 計算屬性Watcher]

this.getter 對計算屬性求值,在獲取依賴屬性時,觸發依賴屬性的 數據劫持get,執行 dep.depend 收集依賴(“計算屬性Watcher”)

收集渲染Watcher

this.getter 求值完成后popTragte,“計算屬性Watcher”出棧,Dep.target 設置為“渲染Watcher”,此時的 Dep.target 是“渲染Watcher

if (Dep.target) {
  watcher.depend()
}

watcher.depend 收集依賴:

depend() {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

deps 內存儲的是依賴屬性的 dep,這一步是依賴屬性收集依賴(“渲染Watcher”)

經過上面兩次收集依賴后,依賴屬性的 subs 存儲兩個 Watcher,[計算屬性Watcher,渲染Watcher]

為什麼依賴屬性要收集渲染Watcher

我在初次閱讀源碼時,很奇怪的是依賴屬性收集到“計算屬性Watcher”不就好了嗎?為什麼依賴屬性還要收集“渲染Watcher”?

第一種場景:模板里同時用到依賴屬性和計算屬性

<template>
  <div>{{msg}} {{msg1}}</div>
</template>

export default {
  data(){
    return {
      msg: 'hello'
    }
  },
  computed:{
    msg1(){
      return this.msg + ' world'      
    }
  }
}

模板有用到依賴屬性,在頁面渲染對依賴屬性取值時,依賴屬性就存儲了“渲染Watcher”,所以 watcher.depend 這步是屬於重複收集的,但 watcher 內部會去重。

這也是我為什麼會產生疑問的點,Vue 作為一個優秀的框架,這麼做肯定有它的道理。於是我想到了另一個場景能合理解釋 watcher.depend 的作用。

第二種場景:模板內只用到計算屬性

<template>
  <div>{{msg1}}</div>
</template>

export default {
  data(){
    return {
      msg: 'hello'
    }
  },
  computed:{
    msg1(){
      return this.msg + ' world'      
    }
  }
}

模板上沒有使用到依賴屬性,頁面渲染時,那麼依賴屬性是不會收集 “渲染Watcher”的。此時依賴屬性里只會有“計算屬性Watcher”,當依賴屬性被修改,只會觸發“計算屬性Watcher”的 update。而計算屬性的 update 里僅僅是將 dirty 設置為 true,並沒有立刻求值,那麼計算屬性也不會被更新。

所以需要收集“渲染Watcher”,在執行完“計算屬性Watcher”后,再執行“渲染Watcher”。頁面渲染對計算屬性取值,執行 watcher.evaluate 才會重新計算求值,頁面計算屬性更新。

總結

計算屬性原理和響應式原理都是大同小異的,同樣的是使用數據劫持以及依賴收集,不同的是計算屬性有做緩存優化,只有在依賴屬性變化時才會重新求值,其它情況都是直接返回緩存值。服務端不對計算屬性緩存。

計算屬性更新的前提需要“渲染Watcher”的配合,因此依賴屬性的 subs 中至少會存儲兩個 Watcher

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

【其他文章推薦】

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

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

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

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

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

JFinal 開箱評測,這次我是認真的

引言

昨天在看服務器容器的時候意外的遇到了 JFinal ,之前我對 JFinal 的印象僅停留在這是一款國人開發的集成 Spring 全家桶的一個框架。

後來我查了一下,好像事情並沒有這麼簡單。

JFinal 連續好多年獲得 OSChina 最佳開源項目,並不是我之前理解的集成 Spring 全家桶,而是自己開發了一套 WEB + ORM + AOP + Template Engine 框架,大寫的牛逼!

先看下官方倉庫對自己的介紹:

這介紹寫的,簡直是深的我心 為您節約更多時間,去陪戀人、家人和朋友 :)

做碼農這一行,誰不想早點把活做完,能正常下班,而不是天天 996 的福報。

介於這麼優秀的框架自己從來沒了解過,這絕對是一個 Java 老司機梭不能容忍的。

那麼今天我就做一次框架的開箱評測,看看到底能不能做到宣傳語上說的 節約更多的時間 ,到底好不好用。

這可能是業界第一個做框架評測的文章的吧,還是先低調一把:本人能力有限,以下內容如有不對的地方還請各位海涵。

接下來的目的是簡單做一個 Demo ,完成最簡單的 CRUD 操作來體驗下 JFinal 。

構建項目

我懷揣着崇敬的心態打開了 JFinal 的官方文檔。

  • 文檔地址:https://jfinal.com/doc

在官網還看到了示例項目,這個必須 down 下來看一眼,這時一件讓我完全沒想到的事兒發生了,竟然還要我註冊登錄,天啊,這都 2020 年了,下載一個 demo 竟然還要登錄,我是瞎了么。

好吧好吧,你是老大你說了算,誰讓我饞你身子呢。

官方對項目的構建演示是使用的 eclipse ,好吧,你又贏了,我用 idea 照着你的步驟來。

過程其實很簡單,就是創建了一個 maven 項目,然後把依賴引入進去,核心依賴就下面這兩個:

<dependency>
    <groupId>com.jfinal</groupId>
    <artifactId>jfinal-undertow</artifactId>
    <version>2.1</version>
</dependency>

<dependency>
    <groupId>com.jfinal</groupId>
    <artifactId>jfinal</artifactId>
    <version>4.9</version>
</dependency>

全量代碼我就不貼了(畢竟太長),代碼都會提交到代碼倉庫,有興趣的同學可以訪問代碼倉庫獲取。

其實用慣了 SpringBoot 的創建項目的過程,已經非常不習慣用這種方式來構建項目了,排除 IDEA 對 SpringBoot 項目構建的支持,直接訪問 https://start.spring.io/ ,直接勾勾選選把自己需要的依賴選上直接下載導入 IDE 就好了。

不過這個沒啥好說的, SpringBoot 畢竟後面是有一個大團隊在支持的,而 JFinal 貌似開發者只有一個人,能做成這樣基本上也可以說是在開源領域國人的驕傲了。

項目啟動

項目依賴搞好了,接下來第一件事兒就是要想辦法啟動項目了,在 JFinal 中,有一個全局配置類,而啟動項目的代碼也在這裏。

這個類需要繼承 JFinalConfig ,而繼承這個類需要實現下面 6 個抽象方法:

public class DemoConfig extends JFinalConfig {
    public void configConstant(Constants me) {}
    public void configRoute(Routes me) {}
    public void configEngine(Engine me) {}
    public void configPlugin(Plugins me) {}
    public void configInterceptor(Interceptors me) {}
    public void configHandler(Handlers me) {}
}

configConstant

這個方法主要是用來配置 JFinal 的一些常量值,比如:設置 aop 代理使用 cglib,設置日誌使用 slf4j 日誌系統,默認編碼格式為 UTF-8 等等。

下面是我選用的官方文檔給出來的一些配置:

public void configConstant(Constants me) {
    // 配置開發模式,true 值為開發模式
    me.setDevMode(true);
    // 配置 aop 代理使用 cglib,否則將使用 jfinal 默認的動態編譯代理方案
    me.setToCglibProxyFactory();
    // 配置依賴注入
    me.setInjectDependency(true);
    // 配置依賴注入時,是否對被注入類的超類進行注入
    me.setInjectSuperClass(false);
    // 配置為 slf4j 日誌系統,否則默認將使用 log4j
    // 還可以通過 me.setLogFactory(...) 配置為自行擴展的日誌系統實現類
    me.setToSlf4jLogFactory();
    // 設置 Json 轉換工廠實現類,更多說明見第 12 章
    me.setJsonFactory(new MixedJsonFactory());
    // 配置視圖類型,默認使用 jfinal enjoy 模板引擎
    me.setViewType(ViewType.JFINAL_TEMPLATE);
    // 配置 404、500 頁面
    me.setError404View("/common/404.html");
    me.setError500View("/common/500.html");
    // 配置 encoding,默認為 UTF8
    me.setEncoding("UTF8");
    // 配置 json 轉換 Date 類型時使用的 data parttern
    me.setJsonDatePattern("yyyy-MM-dd HH:mm");
    // 配置是否拒絕訪問 JSP,是指直接訪問 .jsp 文件,與 renderJsp(xxx.jsp) 無關
    me.setDenyAccessJsp(true);
    // 配置上傳文件最大數據量,默認 10M
    me.setMaxPostSize(10 * 1024 * 1024);
    // 配置 urlPara 參數分隔字符,默認為 "-"
    me.setUrlParaSeparator("-");
}

這裡是一些項目的通用配置信息,在 SpringBoot 中這種配置信息一般是寫在 yaml 或者 property 配置文件裏面,不過這裏這麼配置我個人感覺無所謂,只是稍微有點不適應。

configRoute

這個方法是配置訪問路由信息,我的示例是這麼寫的:

public void configRoute(Routes me) {
    me.add("/user", UserController.class);
}

看到這裏我想到一個問題,每次我新增一個 Controller 都要來這裏配置下路由信息的話,這也太傻了。

如果是小型項目還好,路由信息不回很多,有個十幾條幾十條足夠用了,如果是一些中大型項目,上百或者上千個 Controller ,我要是都配置在這裏,能找得到么,這裏打個問號。

這裡在實際應用中存在一個致命的問題,在發布版本的時候,做過項目的同學都知道,最少四套環境:開發,測試,UAT,生產。每個環境的代碼功能版本都不一樣,難道我發布之前需要手動人工修改這裏么,這怎麼可能管理的過來。

configEngine

這個是用來配置 Template Engine ,也就是頁面模版的,介於我只想單純的簡單的寫兩個 Restful 接口,這裏我就不做配置了,下面是官方提供的示例:

public void configEngine(Engine me) {
    me.addSharedFunction("/view/common/layout.html");
    me.addSharedFunction("/view/common/paginate.html");
    me.addSharedFunction("/view/admin/common/layout.html");
}

configPlugin

這裡是用來配置 JFinal 的 Plugin ,也就是一些插件信息的,我的代碼如下:

public void configPlugin(Plugins me) {
    DruidPlugin dp = new DruidPlugin(p.get("jdbcUrl"), p.get("user"), p.get("password").trim());
    me.add(dp);

    ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);
    arp.addMapping("user", User.class);
    me.add(arp);
}

我的配置很簡單,前面配置了 Druid 的數據庫連接池插件,後面配置了 ActiveRecord 數據庫訪問插件。

讓我覺得有點傻的地方是我如果要增加 ActiveRecord 數據庫訪問的映射關係,需要手動在這裏增加代碼,比如 arp.addMapping("aaa", Aaa.class); ,還是回到上面的問題,不同的環境之間發布系統需要手動修改這裏,項目不大還能人工管理,項目大的話這裡會成為噩夢。

configInterceptor

這個方法是用來配置全局攔截器的,全局攔截器分為兩類:控制層、業務層,我的示例代碼是這樣的:

public void configInterceptor(Interceptors me) {
    me.add(new AuthInterceptor());
    me.addGlobalActionInterceptor(new ActionInterceptor());
    me.addGlobalServiceInterceptor(new ServiceInterceptor());
}

這裏 me.add(...)me.addGlobalActionInterceptor(...) 兩個方法是完全等價的,都是配置攔截所有 Controller 中 action 方法的攔截器。而 me.addGlobalServiceInterceptor(...) 配置的攔截器將攔截業務層所有 public 方法。

攔截器沒什麼好說的,這麼配置感覺和 SpringBoot 裏面完全一致。

configHandler

這個方法用來配置 JFinal 的 Handler , Handler 可以接管所有 Web 請求,並對應用擁有完全的控制權。

這個方法是一個高階的擴展方法,我只是想寫一個簡單的 CRUD 操作,完全用不着,這裏還是摘抄一個官方的 Demo :

public void configHandler(Handlers me) {
    me.add(new ResourceHandler());
}

配置文件

我看官方的配置文件,結尾竟然是 txt ,這讓我第一眼就開始懷疑人生,為啥配置文件要選用 txt 格式的,而裏面的配置格式,卻和 property 文件一模一樣,難道是為了彰顯個性么,這讓我產生了深深的懷疑。

在前面的那個 DemoConfig 配置類中,是可以通過 Prop 來直接獲取配置文件的內容:

static Prop p;

/**
    * PropKit.useFirstFound(...) 使用參數中從左到右最先被找到的配置文件
    * 從左到右依次去找配置,找到則立即加載並立即返回,後續配置將被忽略
    */
static void loadConfig() {
    if (p == null) {
        p = PropKit.useFirstFound("demo-config-pro.txt", "demo-config-dev.txt");
    }
}

在配置文件這裏雖然引入了環境配置的概念,但是還是略顯粗糙,很多需要配置的內容都沒法配置,而這裡能配置的暫時看下來只有數據庫、緩存服務等有限的內容。

Model 配置

說實話,剛開始看到 Model 這一部分的使用的時候驚呆我了,完全沒想到這麼簡單:

public class User extends Model<User> {

}

就這樣,就可以了,裏面什麼都不用寫,完全顛覆了我之前的認知,難道這個框架會動態的去數據庫找字段么,倒不是智能不智能的問題,如果兩個人一起開發同一個項目,我光看代碼都不知道這個 Model 裏面的屬性有啥,必須要對着數據庫一起看,這個會讓人崩潰的。

後來事實證明我年輕了,代碼還是需要的,只是不用自己寫了, JFinal 提供了一個代碼生成器,相關代碼根據數據庫表自動生成的,生成的代碼就不看了,簡單看下這個自動生成器的代碼:

public static void main(String[] args) {
    // base model 所使用的包名
    String baseModelPackageName = "com.geekdigging.demo.model.base";
    // base model 文件保存路徑
    String baseModelOutputDir = PathKit.getWebRootPath() + "/src/main/java/com/geekdigging/demo/model/base";
    // model 所使用的包名 (MappingKit 默認使用的包名)
    String modelPackageName = "com.geekdigging.demo.model";
    // model 文件保存路徑 (MappingKit 與 DataDictionary 文件默認保存路徑)
    String modelOutputDir = baseModelOutputDir + "/..";
    // 創建生成器
    Generator generator = new Generator(getDataSource(), baseModelPackageName, baseModelOutputDir, modelPackageName, modelOutputDir);
    // 配置是否生成備註
    generator.setGenerateRemarks(true);
    // 設置數據庫方言
    generator.setDialect(new MysqlDialect());
    // 設置是否生成鏈式 setter 方法
    generator.setGenerateChainSetter(false);
    // 添加不需要生成的表名
    generator.addExcludedTable("adv", "data", "rate", "douban2019");
    // 設置是否在 Model 中生成 dao 對象
    generator.setGenerateDaoInModel(false);
    // 設置是否生成字典文件
    generator.setGenerateDataDictionary(false);
    // 設置需要被移除的表名前綴用於生成modelName。例如表名 "osc_user",移除前綴 "osc_"後生成的model名為 "User"而非 OscUser
    generator.setRemovedTableNamePrefixes("t_");
    // 生成
    generator.generate();
}

看到這段代碼我心都涼了,居然是整個數據庫做掃描的,還好是用的 MySQL ,開源免費的,如果是 Oracle ,一個項目就需要一台數據庫或者是一個數據庫集群,這個太有錢了。

當然,這段代碼也提供了排除不需要生成的表名 addExcludedTable() 方法,其實沒什麼使用價值,一個 Oracle 集群上可能有 N 多個項目一起跑,上面的表成百上千張,一個小項目如果只用到十來張表,addExcludedTable() 這個方法光把表名 copy 進去估計一两天都搞不完。

數據庫 CRUD 操作

JFinal 把數據的 CRUD 操作集成在了 Model 上,這種做法如何我不做評價,看下我寫的一個樣例 Service 類:

public class UserService {
    private static final User dao = new User().dao();
    // 分頁查詢
    public Page<User> userPage() {
        return dao.paginate(1, 10, "select *", "from user where age > ?", 18);
    }
    public User findById(String id) {
        System.out.println(">>>>>>>>>>>>>>>>UserService.findById()>>>>>>>>>>>>>>>>>>>>>>>>>");
        return dao.findById(id);
    }
    public void save(User user) {
        System.out.println(">>>>>>>>>>>>>>>>UserService.save()>>>>>>>>>>>>>>>>>>>>>>>>>");
        user.save();
    }
    public void update(User user) {
        System.out.println(">>>>>>>>>>>>>>>>UserService.update()>>>>>>>>>>>>>>>>>>>>>>>>>");
        user.update();
    }
    public void deleteById(String id) {
        System.out.println(">>>>>>>>>>>>>>>>UserService.deleteById()>>>>>>>>>>>>>>>>>>>>>>>>>");
        dao.deleteById(id);
    }
}

這裏的分頁查詢看的我有點懵逼,為啥一句 SQL 非要拆成兩半,總感覺後面那半 from user where age > ? 是 Hibernate 的 HQL ,難道這兩者之間有啥不可告人的秘密么。

其他的普通 CRUD 操作寫法倒是蠻正常的,無任何槽點。

Controller

先上代碼吧,就着代碼嘮:

public class UserController extends Controller {

    @Inject
    UserService service;

    public void findById() {
        renderJson(service.findById("1"));
    }

    public void save() {
        User user = new User();
        user.set("id", "2");
        user.set("create_date", new Date());
        user.set("name", "小紅");
        user.set("age", 24);
        service.save(user);
        renderNull();
    }

    public void update() {
        User user = new User();
        user.set("id", "2");
        user.set("create_date", new Date());
        user.set("name", "小紅");
        user.set("age", 19);
        service.update(user);
        renderNull();
    }

    public void deleteById() {
        service.deleteById(getPara("id"));
        renderNull();
    }
}

首先 Service 使用 @Inject 進行注入,這個沒啥好說的,和 Spring 裏面的 @Autowaire 一樣。

這個類裏面所有實際方法的返回類型都是 void 空類型,返回的內容全靠 render() 進行控制,可以返回 json 也可以返回頁面視圖,也罷,只是稍微有點不適應,這個沒啥問題。

但是接下來這個問題就讓我有點方了,感覺都不是問題,成了缺陷了,獲取參數只提供了兩種方法:

一種是 getPara() 系列方法,這種方法只能獲取到表單提交的數據,基本上類似於 Spring 中的 request.getParameter()

另一種是 getModel / getBean ,首先,這兩個方法接受通過表單提交過來的參數,其次是一定要轉成一個 Model 類。

我就想知道一件事情,如果一個請求的類型不是表單提交,而是 application/json ,怎麼去接受參數,我把文檔翻了好幾遍,都沒找到我想要的 request 對象。

可能只是我沒找到,一個成熟的框架,不應該不支持這種常見的 application/json 的數據提交方式,這不可能的。

還有就是,getModel / getBean 這種方式一定要直接轉化成 Model 類,有時候並不是一件好事,如果當前這個接口的入參格式比較複雜,這種 Model 構造起來還是有一定難度的,尤其是有時候只需要獲取其中的少量數據做解析預處理,完全沒必要解析整個請求數據。

小結

通過一個簡單的 CRUD 操作看下來, JFinal 整體上完成了一個 WEB + ORM 框架該有的東西,只是有些地方做的不是那麼好的,當然,這是和 SpringBoot 做比較。

如果是拿來做一些小東西感覺還是可以值得嘗試的,如果是要做一些企業級的應用,就顯得有些捉襟見肘了。

不過這個項目出來的年代是比較早了,從 2012 年至今已經走過了 8 年的時間了,如果是和當年的 SpringMCV + Spring + ORM 這種框架做比較,我覺得我選的話肯定是會選 JFinal 的。

如果是和現在的 SpringBoot 做比較,我覺得我還是傾向於選擇 SpringBoot ,一個是因為熟悉,另一個是因為 JFinal 很多地方,為了方便開發者使用,把相當多的代碼都封裝起來了,這種做法不能說不好,對於初學者而言肯定是好的,文檔簡單看看,基本上半天到一天就能開始上手幹活的,但是對於一些老司機而言,這樣做會讓人覺得束手束腳的,這也不能做那也不能做。

我自己的示例代碼和官方的 Demo 我一起提交到代碼倉庫了,有需要的同學可以回復 「JFinal」 進行獲取。

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

【其他文章推薦】

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

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

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

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

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

mybatis緩存之一級緩存(一)

對於mybatis框架。彷彿工作中一直是在copy着使用。對於mybatis緩存。並沒有一個準確的認知。趁着假期。學習下mybatis的緩存。這篇主要學習mybatis的一級緩存。

為什麼使用緩存

其實,大家工作久了,就知道很多瓶頸就是在數據庫上。

初識mybatis一級緩存

當然我們還是通過代碼來認識下mybatis的一級緩存

代碼演示

詳細代碼見github,這裏只展示重要的代碼片段

  1. tempMapper.xml
    <select id="getById" resultType="entity.TempEntity">
       select * from  temp where id = #{id}
    </select>
  1. TempTest
public class TempTest {

    Logger logger = Logger.getLogger(this.getClass());
    @Test
    public  void test() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        TempEntity tempEntity1 = sqlSession.selectOne("dao.TempDao.getById", 1);
        logger.info(tempEntity1);
        TempEntity tempEntity2 = sqlSession.selectOne("dao.TempDao.getById", 1);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);
    }
}

3.運行結果

2020-06-26 08:57:37,453 DEBUG [dao.TempDao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 08:57:37,513 DEBUG [dao.TempDao.getById] - ==> Parameters: 1(Integer)
2020-06-26 08:57:37,538 DEBUG [dao.TempDao.getById] - <==      Total: 1
2020-06-26 08:57:37,538 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 08:57:37,538 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 08:57:37,538 INFO [TempTest] - true
  1. 總結

4.1 從上面的結果,我們可以看到,第二次查詢的時候,就直接沒有查詢數據庫,並且返回的是同一個對象。證明第二次走的就是緩存。
4.2 一級緩存是默認開啟的。我們並沒有在代碼中配置任何關於緩存的配置
4.3 代碼回顧

mybatis一級緩存命中原則

mybatis是怎麼樣判斷某兩次查詢是完全相同的查詢?

1.statementId

1.1 mapper.xml

    <select id="getById1" resultType="entity.TempEntity">
       select * from  temp where id = #{id}
    </select>

    <select id="getById2" resultType="entity.TempEntity">
       select * from  temp where id = #{id}
    </select>

1.2 test

    @Test
    public  void test() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        TempEntity tempEntity1 = sqlSession.selectOne("dao.Temp2Dao.getById1", 1);
        logger.info(tempEntity1);
        TempEntity tempEntity2 = sqlSession.selectOne("dao.Temp2Dao.getById2", 1);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);
    }

1.3 結果

2020-06-26 09:19:09,926 DEBUG [dao.Temp2Dao.getById1] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 09:19:09,957 DEBUG [dao.Temp2Dao.getById1] - ==> Parameters: 1(Integer)
2020-06-26 09:19:09,969 DEBUG [dao.Temp2Dao.getById1] - <==      Total: 1
2020-06-26 09:19:09,969 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 09:19:09,969 DEBUG [dao.Temp2Dao.getById2] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 09:19:09,970 DEBUG [dao.Temp2Dao.getById2] - ==> Parameters: 1(Integer)
2020-06-26 09:19:09,970 DEBUG [dao.Temp2Dao.getById2] - <==      Total: 1
2020-06-26 09:19:09,971 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}

1.4 總結
要求查詢的statementId必須完全相同,否則無法命中緩存,即時兩個查詢語句、參數完全相同

2.查詢參數

我們用不同的參數查詢,一個傳1 一個傳2
2.1 test

    @Test
    public  void testParam() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        TempEntity tempEntity1 = sqlSession.selectOne("dao.Temp2Dao.getById1", 1);
        logger.info(tempEntity1);
        TempEntity tempEntity2 = sqlSession.selectOne("dao.Temp2Dao.getById1", 2);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);
    }

2.2 結果

2020-06-26 09:24:33,107 DEBUG [dao.Temp2Dao.getById1] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 09:24:33,148 DEBUG [dao.Temp2Dao.getById1] - ==> Parameters: 1(Integer)
2020-06-26 09:24:33,162 DEBUG [dao.Temp2Dao.getById1] - <==      Total: 1
2020-06-26 09:24:33,162 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 09:24:33,162 DEBUG [dao.Temp2Dao.getById1] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 09:24:33,163 DEBUG [dao.Temp2Dao.getById1] - ==> Parameters: 2(Integer)
2020-06-26 09:24:33,164 DEBUG [dao.Temp2Dao.getById1] - <==      Total: 1
2020-06-26 09:24:33,164 INFO [TempTest] - TempEntity{id=2, value1='22222', value2='bbbb'}
2020-06-26 09:24:33,164 INFO [TempTest] - false


2.3 總結

要求傳遞給sql的傳遞參數相同,否則不會命中緩存

3.分頁參數

3.1 傳不同的分頁參數

    @Test
    public  void testPage() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        RowBounds rowBounds1 = new RowBounds(0,1);
        List<TempEntity> tempEntity1 = sqlSession.selectList("dao.Temp2Dao.getList", null,rowBounds1);
        logger.info(tempEntity1);
        RowBounds rowBounds2 = new RowBounds(0,2);
        List<TempEntity> tempEntity2 = sqlSession.selectList("dao.Temp2Dao.getList",null, rowBounds2);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);
    }

3.2 結果

2020-06-26 10:10:33,060 DEBUG [dao.Temp2Dao.getList] - ==>  Preparing: select * from temp where 1=1 
2020-06-26 10:10:33,101 DEBUG [dao.Temp2Dao.getList] - ==> Parameters: 
2020-06-26 10:10:33,116 INFO [TempTest] - [TempEntity{id=1, value1='11111', value2='aaaaa'}]
2020-06-26 10:10:33,116 DEBUG [dao.Temp2Dao.getList] - ==>  Preparing: select * from temp where 1=1 
2020-06-26 10:10:33,116 DEBUG [dao.Temp2Dao.getList] - ==> Parameters: 
2020-06-26 10:10:33,118 INFO [TempTest] - [TempEntity{id=1, value1='11111', value2='aaaaa'}, TempEntity{id=2, value1='22222', value2='bbbb'}]
2020-06-26 10:10:33,118 INFO [TempTest] - false

3.3 總結

要求分頁參數必須相同,否則無法命中緩存。緩存的粒度是整個分頁查詢結果,而不是結果中的每個對象

4. sql語句

4.1 mapper文件

    <select id="getById" resultType="entity.TempEntity">
       select * from  temp 
       <where>
           <if test="type ==1">
           id = #{id}
            </if>
           <if test="type ==2">
               1=1 and id = #{id}
           </if>
       </where> 
    </select>

這個就不測試了。
4.2 總結
要求傳遞給jdbc的sql 必須完全相同。就算是1=1 不起作用 也不行

5.環境

這裏的環境指的的是<environment id="dev"><environment id="test"> 也是會影響的

    <environments default="dev">
        <environment id="dev">
            <!--指定事務管理的類型,這裏簡單使用Java的JDBC的提交和回滾設置-->
            <transactionManager type="JDBC"></transactionManager>
            <!--dataSource 指連接源配置,POOLED是JDBC連接對象的數據源連接池的實現-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"></property>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"></property>
                <property name="username" value="root"></property>
                <property name="password" value="root"></property>
            </dataSource>
        </environment>
        <environment id="test">
            <!--指定事務管理的類型,這裏簡單使用Java的JDBC的提交和回滾設置-->
            <transactionManager type="JDBC"></transactionManager>
            <!--dataSource 指連接源配置,POOLED是JDBC連接對象的數據源連接池的實現-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"></property>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"></property>
                <property name="username" value="root"></property>
                <property name="password" value="root"></property>
            </dataSource>
        </environment>
    </environments>

總結

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

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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

2020/6/10 JavaScript高級程序設計 BOM

BOM(瀏覽器對象模型):提供用於訪問瀏覽器的對象。

8.1 window對象

 window是BOM的核心對象,表示瀏覽器的一個實例。

  • JavaScript訪問瀏覽器窗口的接口
  • ECMAScript規定的Global對象

8.1.1 全局作用域

全局變量會成為window的屬性,但是定義全局變量和直接在window對象上定義屬性是有差別的——全局變量不能通過delete刪除,但window對象上定義的可以

這是因為使用var添加的window屬性[[Configurable]]被設置為false(不可刪除)。

訪問未聲明的變量會發生錯誤,但通過查詢window對象,可以知道某個可能未聲明的變量是否存在。

//這裡會拋出錯誤,因為oldValue未定義
var newValue = oldValue;

//這裏不會拋出錯誤,因為這是一次屬性查詢
var newValue = window.oldValue;  //newValue的值是undefined

8.1.2 窗口關係及框架

如果頁面中包含框架,則每個框架都擁有自己的window對象,並保存在frames集合中。在frames集合中可以通過數值索引/框架名稱來訪問相應的window對象。每個window對象都有一個name屬性,其中包含框架的名稱。

PS1:對於最高層窗口來說:除非最高層窗口是通過window.open()打開的,否則其window對象的name屬性不會包含任何值。

與框架有關的window對象屬性(同時也是對象):

  • top:始終指向最高(最外)層的框架,也就是瀏覽器窗口。使用它可以正確地在一個框架中訪問另一個框架。因為對任意一個框架中的代碼來說,window對象指向的都是那個框架的特定實例,而非最高層框架。
  • parent:始終指向當前框架的直接上層框架。在沒有框架的情況下,parent等於top。
  • self:始終指向window。引入self的目的僅僅是為了和top和parent對象對應,因此他不包含其他值。

8.1.3 窗口位置

用來確定window對象位置的屬性:screenLeft, screenTop / screenX, screenY,分別表示窗口相對於屏幕左邊和上邊的位置。

兩組方法分別支持的瀏覽器:

screenLeft, screenTop IE、Safari、Opera、Chrome
screenX, screenY Firefox、Safari、Chrome

跨瀏覽器取得窗口位置的代碼

var leftPos = (typeof window.screenLeft == "number") ?
                        window.screenLeft : window.screenX;
var topPos = (typeof window.screenTop == "number") ?
                        window.screenTop : window.screenY;

缺點:

  • screenTop表示的是由從屏幕上邊到window對象表示的頁面可見區域的距離(即頁面可見區域上方瀏覽器工具欄的像素高度)。
  • screenY表示整個瀏覽器窗口相對於屏幕的坐標值(0)。

將窗口精確移動到一個新位置的方法:

  • moveTo():接收最新位置的x,y坐標值。
  • moveBy():接收在水平和垂直方向上移動的像素數。

PS:這兩個方法很可能會被瀏覽器禁用(Opera和IE7+),且不適合框架,只能對最外層window對象使用。

8.1.4 窗口大小

  IE9+、Safari、Firefox Opera Chrome
innerWidth、innerHeight 視圖區大小 該容器中頁面視圖區的大小(減去邊框寬度) 視口大小
outerWidth、outerHeight 瀏覽器窗口本身的尺寸 頁面視圖容器的大小(單個標籤頁對應瀏覽器窗口的大小) 視口大小

document.documentElement.clientWidth / document.documentElement.clientHeight:保存了頁面視口的信息。

resizeTo()和resizeBy():調整瀏覽器窗口的大小。(分別接收新寬度高度和新窗口與原窗口的寬度和高度之差,同樣可能被瀏覽器禁用)

8.1.5 導航和打開窗口

 

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

【其他文章推薦】

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

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

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

※超省錢租車方案

皮草業者毫無改善 芬蘭組織最新調查 「怪物狐狸」慘況依舊

環境資訊中心記者 鄒敏惠 報導

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

中國工業設計「奧斯卡獎」由陽光電源電動車控制器摘取

2014年12月12日,中國工業設計最高榮譽、被媲美為中國工業設計界之「奧斯卡獎」的中國創新設計紅星獎頒獎典禮在北京舉行。陽光電源電動車控制器產品從全球千餘家企業、6000多件參評作品中脫穎而出,獲得紅星獎,也是唯一獲得該獎項的電動車控制器類產品。  

  陽光電源自主研發的電動車控制器系列產品,結合了陽光電源近20年電力電子領域的技術研發和產業應用經驗,相比同類產品,該型號體積更小、重量更輕,適應範圍更廣也更環保。它採用模組化設計功能完善、能耗低、續駛里程長、可靠性高,目前成功應用於合肥、大連、舒城等多個城市電動公車。

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

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

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

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

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