手牽手,從零學習Vue原始碼 系列二(變化偵測篇)
系列文章:
手牽手,從零學習Vue原始碼 系列一(前言-目錄篇)
手牽手,從零學習Vue原始碼 系列二(變化偵測篇)
陸續更新中...
預計八月中旬更新完畢。
1 概述
Vue最大的特點之一就是資料驅動檢視,那麼什麼是資料驅動檢視呢?
其實,我們可以把資料理解為狀態,而檢視就是使用者可直觀看到頁面。頁面不可能是一成不變的,它應該是動態變化的,而它的變化也應該是有跡可尋的,或者是由使用者操作引起的,亦或者是由後端資料變化引起的,當狀態發生改變時,頁面也就應該隨之而變化,所以我們就可以得到如下一個公式:
UI = render(state)
「上述公式中:狀態state是輸入,頁面UI輸出,狀態輸入一旦變化了,頁面輸出也隨之而變化。我們把這種特性稱之為資料驅動檢視。」
我們可以把上述公式拆成三部分:state、render()以及UI。我們知道state和UI都是使用者定的,而不變的是這個render()。所以Vue就扮演了render()這個角色,當Vue發現state變化之後,經過一系列加工,最終將變化反應在UI上。
那麼第一個問題來了,Vue怎麼知道state變化了呢?
變化偵測
「那麼第一個問題來了,Vue怎麼知道state變化了呢?」 那麼,這就引出了Vue中的變化偵測。
變化偵測就是追蹤狀態,亦或者說是資料的變化,一旦發生了變化,就要去更新檢視。
變化偵測可不是個新名詞,它在目前的前端三大框架中均有涉及。在Angular中是通過髒值檢查流程來實現變化偵測;在React是通過對比虛擬DOM來實現變化偵測,而在Vue中也有自己的一套變化偵測實現機制。
2.Object的變化偵測
在上一篇文章中,我們知道:資料驅動檢視的關鍵點則在於我們如何知道資料發生了變化,只要知道資料在什麼時候變了,那麼問題就變得迎刃而解,我們只需在資料變化的時候去通知檢視更新即可。
要想知道資料什麼時候被讀取了或資料什麼時候被改寫了,其實不難,JS為我們提供了Object.defineProperty方法,通過該方法我們就可以輕鬆的知道資料在什麼時候發生變化。
「那麼,我們從原始碼出發,學習在Vue中是如何對資料進行變化偵測的。」
2.1 使Object資料變得“可觀測”
資料的每次讀和寫能夠被我們看的見,即我們能夠知道資料什麼時候被讀取了或資料什麼時候被改寫了,我們將其稱為資料變的‘可觀測’。
要將資料變的‘可觀測’,我們就要藉助前言中提到的「Object.defineProperty」方法了,在本文中,我們就使用這個方法使資料變得“可觀測”。
首先,我們定義一個數據物件「car」:
let car = { 'brand':'BMW', 'price':3000 }
我們定義了這個「car」的品牌「brand」是「BMW」,價格「price」是「3000」。現在我們可以通過「car.brand」和「car.pric」e直接讀寫這個「car」對應的屬性值。但是,當這個「car」的屬性被讀取或修改時,我們並不知情。那麼應該如何做才能夠讓「car」主動告訴我們,它的屬性被修改了呢?
接下來,我們使用「Object.defineProperty」改寫上面的例子:
let car = {} let val = 3000 Object.defineProperty(car, 'price', { enumerable: true, configurable: true, get(){ console.log('price屬性被讀取了') return val }, set(newVal){ console.log('price屬性被修改了') val = newVal } })
通過「Object.defineProperty」方法給「car」定義了一個「price」屬性,並把這個屬性的讀和寫分別使用「get」和「set」進行攔截,每當該屬性進行讀或寫操作的時候就會觸發
get()
和set()
。如下圖: 可以看到,「car」已經可以主動告訴我們它的屬性的讀寫情況了,這也意味著,這個「car」的資料物件已經是“可觀測”的了。為了把「car」的所有屬性都變得可觀測,我們可以編寫如下程式碼:
/** * Observer類會通過遞迴的方式把一個物件的所有屬性都轉化成可觀測物件 */ export class Observer { constructor (value) { this.value = value // 給value新增一個__ob__屬性,值為該value的Observer例項 // 相當於為value打上標記,表示它已經被轉化成響應式了,避免重複操作 def(value,'__ob__',this) if (Array.isArray(value)) { // 當value為陣列時的邏輯 // ... } else { this.walk(value) } } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } } /** * 使一個物件轉化成可觀測物件 * @param { Object } obj 物件 * @param { String } key 物件的key * @param { Any } val 物件的某個key的值 */ function defineReactive (obj,key,val) { // 如果只傳了obj和key,那麼val = obj[key] if (arguments.length === 2) { val = obj[key] } if(typeof val === 'object'){ new Observer(val) } Object.defineProperty(obj, key, { enumerable: true, configurable: true, get(){ console.log(`${key}屬性被讀取了`); return val; }, set(newVal){ if(val === newVal){ return } console.log(`${key}屬性被修改了`); val = newVal; } }) }
在上面的程式碼中,我們定義了
observer
類,它用來將一個正常的object
轉換成可觀測的object
。並且給
value
新增一個ob
屬性,值為該value
的Observer
例項。這個操作相當於為value
打上標記,表示它已經被轉化成響應式了,避免重複操作然後判斷資料的型別,只有
object
型別的資料才會呼叫walk
將每一個屬性轉換成getter/setter
的形式來偵測變化。最後,在defineReactive
中當傳入的屬性值還是一個object
時使用new observer(val)
來遞迴子屬性,這樣我們就可以把obj
中的所有屬性(包括子屬性)都轉換成getter/seter
的形式來偵測變化。也就是說,只要我們將一個object
傳到observer
中,那麼這個object
就會變成可觀測的、響應式的object
。那麼現在,我們就可以這樣定義「car」:
let car = new Observer({ 'brand':'BMW', 'price':3000 })
這樣,「car」的兩個屬性都變得可觀測了。
2.2 依賴收集
2.2.1 什麼是依賴收集
在上一章中,我們邁出了第一步:讓
object
資料變的可觀測。變的可觀測以後,我們就能知道資料什麼時候發生了變化,那麼當資料發生變化時,我們去通知檢視更新就好了。那麼問題又來了,檢視那麼大,我們到底該通知誰去變化?總不能一個數據變化了,把整個檢視全部更新一遍吧,這樣顯然是不合理的。此時,你肯定會想到,視圖裡誰用到了這個資料就更新誰唄。對!你想的沒錯,就是這樣。視圖裡誰用到了這個資料就更新誰,我們換個優雅說法:我們把"誰用到了這個資料"稱為"誰依賴了這個資料",我們給每個資料都建一個依賴陣列(因為一個數據可能被多處使用),誰依賴了這個資料(即誰用到了這個資料)我們就把誰放入這個依賴陣列中,那麼當這個資料發生變化的時候,我們就去它對應的依賴陣列中,把每個依賴都通知一遍,告訴他們:"你們依賴的資料變啦,你們該更新啦!"。這個過程就是依賴收集。
2.2.2 何時收集依賴?何時通知依賴更新?
明白了什麼是依賴收集後,那麼我們到底該在何時收集依賴?又該在何時通知依賴更新?
其實這個問題在上一小節中已經回答了,我們說過:誰用到了這個資料,那麼當這個資料變化時就通知誰。所謂誰用到了這個資料,其實就是誰獲取了這個資料,而可觀測的資料被獲取時會觸發「getter」屬性,那麼我們就可以在「getter」中收集這個依賴。同樣,當這個資料變化時會觸發「setter」屬性,那麼我們就可以在「setter」中通知依賴更新。
總結一句話就是:在「getter」中收集依賴,在「setter」中通知依賴更新。
2.2.3 把依賴收集到哪裡
明白了什麼是依賴收集以及何時收集何時通知後,那麼我們該把依賴收集到哪裡?
在2.2.1小節中也說了,我們給每個資料都建一個依賴陣列,誰依賴了這個資料我們就把誰放入這個依賴陣列中。單單用一個數組來存放依賴的話,功能好像有點欠缺並且程式碼過於耦合。我們應該將依賴陣列的功能擴充套件一下,更好的做法是我們應該為每一個數據都建立一個依賴管理器,把這個資料所有的依賴都管理起來。OK,到這裡,我們的依賴管理器「Dep類」應運而生,程式碼如下:
export default class Dep { constructor () { this.subs = [] } addSub (sub) { this.subs.push(sub) } // 刪除一個依賴 removeSub (sub) { remove(this.subs, sub) } // 新增一個依賴 depend () { if (window.target) { this.addSub(window.target) } } // 通知所有依賴更新 notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } /** * Remove an item from an array */ export function remove (arr, item) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } }
在上面的依賴管理器「Dep類」中,我們先初始化了一個「subs陣列」,用來存放依賴,並且定義了幾個例項方法用來對依賴進行新增,刪除,通知等操作。
有了依賴管理器後,我們就可以在「getter」中收集依賴,在「setter」中通知依賴更新了,程式碼如下:
function defineReactive (obj,key,val) { if (arguments.length === 2) { val = obj[key] } if(typeof val === 'object'){ new Observer(val) } const dep = new Dep() //例項化一個依賴管理器,生成一個依賴管理陣列dep Object.defineProperty(obj, key, { enumerable: true, configurable: true, get(){ dep.depend() // 在getter中收集依賴 return val; }, set(newVal){ if(val === newVal){ return } val = newVal; dep.notify() // 在setter中通知依賴更新 } }) }
在上述程式碼中,我們在「getter」中呼叫了
dep.depend()
方法收集依賴,在「setter」中呼叫dep.notify()
方法通知所有依賴更新。2.2.4 依賴到底是誰
通過上一章節,我們明白了什麼是依賴?何時收集依賴?以及收集的依賴存放到何處?那麼我們收集的依賴到底是誰?
雖然我們一直在說”誰用到了這個資料誰就是依賴“,但是這僅僅是在口語層面上,那麼反應在程式碼上該如何來描述這個”誰“呢?
其實在Vue中還實現了一個叫做「Watcher」的類,而「Watcher類」的例項就是我們上面所說的那個"誰"。換句話說就是:誰用到了資料,誰就是依賴,我們就為誰建立一個「Watcher例項****。在之後資料變化時,我們不直接去通知依賴更新,而是通知依賴對應的」Watch例項**,由「Watcher例項」去通知真正的檢視。
「Watcher類」的具體實現如下:
export default class Watcher { constructor (vm,expOrFn,cb) { this.vm = vm; this.cb = cb; this.getter = parsePath(expOrFn) this.value = this.get() } get () { window.target = this; const vm = this.vm let value = this.getter.call(vm, vm) window.target = undefined; return value } update () { const oldValue = this.value this.value = this.get() this.cb.call(this.vm, this.value, oldValue) } } /** * Parse simple path. * 把一個形如'data.a.b.c'的字串路徑所表示的值,從真實的data物件中取出來 * 例如: * data = {a:{b:{c:2}}} * parsePath('a.b.c')(data) // 2 */ const bailRE = /[^\w.$]/ export function parsePath (path) { if (bailRE.test(path)) { return } const segments = path.split('.') return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }
誰用到了資料,誰就是依賴,我們就為誰建立一個「Watcher例項」,在建立「Watcher例項」的過程中會自動的把自己新增到這個資料對應的依賴管理器中,以後這個「Watcher例項」就代表這個依賴,當資料變化時,我們就通知「Watcher例項」,由「Watcher例項」再去通知真正的依賴。
那麼,在建立「Watcher例項」的過程中它是如何的把自己新增到這個資料對應的依賴管理器中呢?
下面我們分析「Watcher類」的程式碼實現邏輯:
當例項化「Watcher類」時,會先執行其建構函式; 在建構函式中呼叫了
this.get()
例項方法; 在get()
方法中,首先通過window.target = this
把例項自身賦給了全域性的一個唯一物件window.target
上,然後通過let value = this.getter.call(vm, vm)
獲取一下被依賴的資料,獲取被依賴資料的目的是觸發該資料上面的getter
,上文我們說過,在getter
裡會呼叫dep.depend()
收集依賴,而在dep.depend()
中取到掛載window.target
上的值並將其存入依賴陣列中,在get()
方法最後將window.target
釋放掉。 而當資料變化時,會觸發資料的setter
,在setter
中呼叫了dep.notify()
方法,在dep.notify()
方法中,遍歷所有依賴(即watcher例項
),執行依賴的update()
方法,也就是Watcher類
中的update()
例項方法,在update()
方法中呼叫資料變化的更新回撥函式,從而更新檢視。 簡單總結一下就是:Watcher先把自己設定到全域性唯一的指定位置(window.target)
,然後讀取資料。因為讀取了資料,所以會觸發這個資料的getter。接著,在getter中就會從全域性唯一的那個位置讀取當前正在讀取資料的Watcher
,並把這個watcher
收集到Dep
中去。收集好之後,當資料發生變化時,會向Dep中的每個Watcher
傳送通知。通過這樣的方式,Watcher
可以主動去訂閱任意一個數據的變化。為了便於理解,我們畫出了其關係流程圖,如下圖:以上,就徹底完成了對「Object資料的偵測」,依賴收集,依賴的更新等所有操作。
2.2.5 不足之處
雖然我們通過
Object.defineProperty
方法實現了對object
資料的可觀測,但是這個方法僅僅只能觀測到object
資料的取值及設定值,當我們向object
資料裡新增一對新的「key/value」或刪除一對已有的key/value
時,它是無法觀測到的,導致當我們對object
資料新增或刪除值時,無法通知依賴,無法驅動檢視進行響應式更新。當然,
Vue
也注意到了這一點,為了解決這一問題,Vue
增加了兩個全域性「API:Vue.set」和「Vue.delete」,這兩個API
的實現原理將會在後面學習全域性API的時候說到。2.2.6 小結
首先,我們通過「Object.defineProperty」方法實現了對
object
資料的可觀測,並且封裝了Observer類
,讓我們能夠方便的把object
資料中的所有屬性(包括子屬性)都轉換成「getter/seter」的形式來偵測變化。接著,我們學習了什麼是依賴收集?並且知道了在getter中收集依賴,在setter中通知依賴更新,以及封裝了依賴管理器Dep,用於儲存收集到的依賴。
最後,我們為每一個依賴都建立了一個Watcher例項,當資料發生變化時,通知Watcher例項,由Watcher例項去做真實的更新操作。
其整個流程大致如下:
Data通過observer轉換成了getter/setter的形式來追蹤變化。 當外界通過Watcher讀取資料時,會觸發getter從而將Watcher新增到依賴中。 當資料發生了變化時,會觸發setter,從而向Dep中的依賴(即Watcher)傳送通知。 Watcher接收到通知後,會向外界傳送通知,變化通知到外界後可能會觸發檢視更新,也有可能觸發使用者的某個回撥函式等。
3.Array的變化偵測
3.1 前言
上一節文章中我們介紹了
Object
資料的變化偵測方式,本篇文章我們來看一下對Array型
資料的變化Vue
是如何進行偵測的。為什麼「Object」資料和「Array型」資料會有兩種不同的變化偵測方式?
這是因為對於
Object
資料我們使用的是JS提供的物件原型上的方法「Object.defineProperty」,而這個方法是物件原型上的,所以Array
無法使用這個方法,所以我們需要對Array型
資料設計一套另外的變化偵測機制。萬變不離其宗,雖然對
Array型
資料設計了新的變化偵測機制,但是其根本思路還是不變的。那就是:還是在獲取資料時收集依賴,資料變化時通知依賴更新。下面我們就通過原始碼來看看
Vue對Array型
資料到底是如何進行變化偵測的。3.2 在哪裡收集依賴
首先還是老規矩,我們得先把用到
Array型
資料的地方作為依賴收集起來,那麼第一問題就是該在哪裡收集呢?其實
Array型
資料的依賴收集方式和Object資料
的依賴收集方式相同,都是在「getter」中收集。那麼問題就來了,不是說Array
無法使用「Object.defineProperty」方法嗎?無法使用怎麼還在「getter」中收集依賴呢?其實不然,我們回想一下平常在開發的時候,在元件的**data8*中是不是都這麼寫的:
data(){ return { arr:[1,2,3] } }
想想看,arr這個資料始終都存在於一個
object資料
物件中,而且我們也說了,誰用到了資料誰就是依賴,那麼要用到arr這個資料,是不是得先從object資料
物件中獲取一下arr資料
,而從object資料
物件中獲取arr
資料自然就會觸發arr
的「getter」,所以我們就可以在「getter」中收集依賴。總結一句話就是:
Array型
資料還是在「getter」中收集依賴。3.3 使Array型資料可觀測
上一章節中我們知道了
Array型
資料還是在「getter」中收集依賴,換句話說就是我們已經知道了Array型
資料何時被讀取了。回想上一篇文章中介紹
Object資料
變化偵測的時候,我們先讓Object資料
變的可觀測,即我們能夠知道資料什麼時候被讀取了、什麼時候發生變化了。同理,對於Array型資料
我們也得讓它變的可觀測,目前我們已經完成了一半可觀測,即我們只知道了Array型資料
何時被讀取了,而何時發生變化我們無法知道,那麼接下來我們就來解決這一問題:當Array型資料
發生變化時我們如何得知?相關推薦
手牽手,從零學習Vue原始碼 系列二(變化偵測篇)
系列文章: 手牽手,從零學習Vue原始碼 系列一(前言-目錄篇) 手牽手,從零學習Vue原始碼 系列二(變化偵測篇) 陸續更新中... 預計八月中旬更新完畢。 1 概述 Vue最大的特點之一就是資料驅動檢視,那麼什麼是資料驅動檢視呢? 其實,我們可以把資料理解為狀態,而檢視就是使用者可直觀看到頁面。頁面不可
從零實現Vue的元件庫(二)-Slider元件實現
實現一個Slider元件,方便使用者通過拖動滑塊在一個固定區間內進行選擇,增強互動細節。 概述: 在使用者手動一些限定數字時,如果採用輸入框的形式,會需要提示資訊和錯誤資訊來引導使用者,這就存在一些冗餘操作。所以衍生出Slider元件,方便使用者拖動來選定一個值。 該元件的痛點在於:
從零實現Vue的元件庫(四)- File-Reader實現
實現一個File-Reader元件用來讀取本地資源。 概述: 在使用者手動上傳一些資源的時候,需要分為兩步,第一步是將其從本地讀取出來,得到一個file物件,然後再上傳至伺服器。該元件用於第一步,然後可通過後續進一步封裝程Upload元件。 該元件的痛點在於: 新增拖拽上傳的功能;
從零實現Vue的元件庫(五)- Breadcrumb 實現
顯示當前頁面的路徑,快速返回之前的任意頁面。 該元件的痛點在於: 採用vnode設定擴充套件性較好的分隔符; 利用vue-router高亮已選中的路徑。 1. 例項 程式碼 <!-- 基礎用法 --> <fat-breadcrumb
從零實現Vue的元件庫(六)- Hover-Tip 實現
常用於展示滑鼠 hover 時的提示資訊。 該元件的痛點在於: 純CSS實現; 如何利用slot使元件易擴充套件,能夠適應多種場景。 1. 例項 程式碼 <!-- 基礎用法 --> <fat-hovertip> <te
造輪子:搭建一個簡單的nodejs伺服器,從零開始搭建一個自用網站(0)
伺服器用的是阿里雲最早期的伺服器低配版本1Gcpu,512M記憶體,20G硬碟,1M頻寬,平常只是用來做測試,目前只處理業務邏輯,網站的設計上儘量避免佔用太多的頻寬, 靜態檔案的儲存用的是阿里雲oss,100G空間,夠放視訊,圖片什麼的, html/js/c
從零實現Vue的元件庫(九)- InputNumber 實現
基於 Input 元件進行拓展的 InputNumber 元件 InputNumer 元件的難點在於: 實現滑鼠長按,計數器數值變動; 導致 InputNumber 元件的值變化,有以下操作v-model繫結值的變化,加、減按鈕,input元件的輸入,需要對上述結果進行處理,所
3,從零開始搭建SSHM開發框架(整合Spring MVC)
目錄 本專題部落格已共享在(這個可能會更新的稍微一些) 1.修改pom.xml,增加spring-mvc 的依賴 <project xmlns="http://maven.apache.org/POM/4.
spring cloud 入門(四)【Eureka註冊中心,微服務之間服務呼叫方式二(FeignClient進行服務呼叫)】
FeignClient 支隊服務消費方進行修改,服務提供方不需要修改 還是對 User 進行修改 UserApplication 中新增 @EnableFeignClients UserApplication 程式碼如下: pac
深度學習Tensorflow生產環境部署(下·模型部署篇)
前一篇講過環境的部署篇,這一次就講講從程式碼角度如何匯出pb模型,如何進行服務呼叫。 1 hello world篇 部署完docker後,如果是cpu環境,可以直接拉取tensorflow/serving,如果是GPU環境則麻煩點,具體參考前一篇,這裡就不再贅述了。 cpu版本的可以直接拉取t
Hibernate的學習之路十二(session的快照機制)
前言 本片文章主要是說明了,hibernate的快照機制,能夠自動更新,不用update。 分析 在建立session這個一級快取物件的時候,session分為2塊區域,一個是快取區域。一個是快照
Android菜鳥之學習android原始碼之二(SystemUI導航欄初步認識及修改)
涉及到系統的定製開發,不可缺少的一個就是系統導航欄和狀態列的修改,而這部分的修改通常都涉及到SyetemUI這個系統應用的修改,它的路徑通常是位於platform\frameworks\base\packages\SystemUI。 先來說說導航欄的修改吧,導航
#Java學習之路——基礎階段二(第十四篇)
out 出現 萬能 -c ack 分隔 status osi 版本 我的學習階段是跟著CZBK黑馬的雙源課程,學習目標以及博客是為了審查自己的學習情況,畢竟看一遍,敲一遍,和自己歸納總結一遍有著很大的區別,在此期間我會參雜Java瘋狂講義(第四版)裏面的內容。 前言:此隨
手牽手,使用uni-app從零開發一款視訊小程式 (系列上 準備工作篇)
系列文章 手牽手,使用uni-app從零開發一款視訊小程式 (系列上 準備工作篇) 手牽手,使用uni-app從零開發一款視訊小程式 (系列下 開發實戰篇) 前言 好久不見,很久沒更新部落格了,前段時間在深圳出差,胡吃海喝頹廢了很久,不想每天下班刷抖音、打遊戲虛度光陰,準備把之前做的一個小程式案例詳細的介
手牽手,使用uni-app從零開發一款視訊小程式 (系列下 開發實戰篇)
系列文章 手牽手,使用uni-app從零開發一款視訊小程式 (系列上 準備工作篇) 手牽手,使用uni-app從零開發一款視訊小程式 (系列下 開發實戰篇) 掃碼體驗,先睹為快 可以掃描下微信小程式的二維碼,體驗一下開發完畢的效果: 程式碼地址: GitHub : https://github.co
【轉】手摸手,帶你用vue擼後臺 系列二(登錄權限篇)
userinfo ogr abort 變化 再次 狀態碼 quest -o 監聽 前言 拖更有點嚴重,過了半個月才寫了第二篇教程。無奈自己是一個業務猿,每天被我司的產品虐的死去活來,之前又病了一下休息了幾天,大家見諒。 進入正題,做後臺項目區別於做其它的項目,權限驗證與
學習筆記GAN001:生成式對抗網絡,只需10步,從零開始到調試
sar quest 從零開始 http demo pip lib download mark 生成式對抗網絡(gennerative adversarial network,GAN),目前最火的非監督深度學習。一個生成網絡無中生有,一個判別網絡推動進化。學技術,不先著急看書
教程 | 僅需六步,從零實現機器學習演算法!
從頭開始寫機器學習演算法能夠獲得很多經驗。當你最終完成時,你會驚喜萬分,而且你明白這背後究竟發生了什麼。 有些演算法比較複雜,我們不從簡單的演算法開始,而是要從非常簡單的演算法開始,比如單層感知器。 本文以感知器為例,通過以下 6 個步驟引導你從頭開始寫演算法: ●  
梳理Python基本認識基本型別,從零開始,學習Python
先羅列一下Python提供的基本資料型別:數值(整型、浮點型、複數、布林型等)、字串、列表、元組、字典、集合等,將它們簡單分類如下: 推薦下小編的Python學習群;629440234,不管你是小白還是大牛,小編我都歡迎,不定期分享乾貨,包括小編自己整理的一份2018最新的Python和0基礎入
python 認真學習的第天,從零開始
#剪刀石頭布的小遊戲 #和第一天的程式碼相比,通過編寫函式的方式來顯示電腦出的什麼,程式碼沒有那麼臃腫 import random i=random.randint(1,3) user=int(input('請出拳(1剪