scratch軟件的邏輯不復(fù)雜,就是用blockly生成語句塊,然后用虛擬機(jī)抽象成底層語法,最后再調(diào)用render渲染到界面,但是因?yàn)榫W(wǎng)上幾乎沒有資料,源代碼又嵌套的極深,看起來還是很頭疼的,所以我把我這一周看代碼的心得分享一些出來,以后再慢慢更新.希望大家也能少走些彎路.
首先什么是虛擬機(jī) : 用來屏蔽底層硬件差異和dom渲染差異 , 使得程序可以跨端移植 , react本質(zhì)上也是虛擬機(jī),虛擬dom屏蔽設(shè)備渲染差異( dom只有pc瀏覽器能識(shí)別 , 但虛擬dom是js對(duì)象 , 因而在手機(jī)上能解析成viewPort),native屏蔽底層硬件差異 ,使得程序可以在Android和ios都可以運(yùn)行 . scratch-vm作用:使用虛擬io屏蔽底層差異,使用render屏蔽ui差異,使軟件可以跨端
命名規(guī)則:靜止?fàn)顟B(tài)的標(biāo)簽叫做target(包含stage和sprite),運(yùn)行狀態(tài)的標(biāo)簽叫做thread.舞臺(tái)叫playgroud,渲染/停止渲染叫做grow/ungrow,監(jiān)視器叫monitor(每個(gè)target可以有一個(gè)),backpack是背包,workspace是舞臺(tái)上block的合集,runtime是內(nèi)核.editingTarget是正在編輯的target,custom和backdrop都是target的皮膚,僅適用的對(duì)象不同,sprite的皮膚叫做custom,stage的皮膚叫做backdrop , custom可以有多個(gè) ,因?yàn)槎鄠€(gè)皮膚幀可以產(chǎn)生動(dòng)畫
monitor 類似于看門狗,由于動(dòng)畫是連續(xù)快速渲染的,有時(shí)會(huì)渲出undefined等情況,這樣會(huì)造成丟幀現(xiàn)象,所以我們把每一幀圖像在update時(shí)強(qiáng)制用monitor進(jìn)行檢查,如果丟幀就回滾回上一幀,這樣就感受不到丟幀了, monitor可以手動(dòng)增刪更新和隱藏顯示,在vm/engine/runtime 在2076行左右
scratch運(yùn)行時(shí)虛擬機(jī)分為標(biāo)準(zhǔn)模式(standard,按照默認(rèn)機(jī)器頻率刷新),兼容模式(compatibility,渲染速度是30tps,人眼會(huì)感受到畫面頻閃,但動(dòng)作流暢性要好一些,可以兼容2.0的項(xiàng)目,2.0是as語言編寫的,我猜測應(yīng)該是使用的n制渲染來兼容差值掃描渲染,由于n制不是串行掃描,如果使用3.0模式的渲染可能會(huì)飆到120fps,所以他搞了這樣一種兼容方案)和嵌入模式(加速模式,重繪次數(shù)會(huì)變少).
runtime:用于存儲(chǔ)block,sprite和虛擬IO,內(nèi)置一個(gè)sequencer隊(duì)列(這是一個(gè)全局任務(wù)隊(duì)列,每次對(duì)target操作都會(huì)入隊(duì),在js時(shí)鐘tick時(shí)執(zhí)行,直至為空為止),一個(gè)targets隊(duì)列(每個(gè)tagers[i]生命周期與對(duì)應(yīng)的target關(guān)聯(lián),target是局部的,可以被銷毀,在需要時(shí)會(huì)重新創(chuàng)建,類似于路由模式,而sequencer是全局的,所有對(duì)target的操作都要在這里排隊(duì))
標(biāo)簽切換 在vm/engine/runtime 900行左右,主要控制在scratch中各個(gè)sprite的標(biāo)簽欄的切換,排序和銷毀,標(biāo)簽不能被執(zhí)行,除非接收到廣播消息或者有用戶交互時(shí)才會(huì)自執(zhí)行.標(biāo)簽的管理在targets隊(duì)列當(dāng)中,標(biāo)簽之間的通信參見第七條
target并不是從runtime中初始化的,而是在vm外殼文件(virtual-machine)中通過io函數(shù)調(diào)用的,區(qū)分sb2,sb3的文件結(jié)構(gòu)載入,downloadProjectId是從網(wǎng)絡(luò)下載,loadProjectLocal是從本地加載,fromJSOM可以加載2.0版本(3.0有特殊的meta字段,2.0是as格式的腳本文件,如果載入之后發(fā)現(xiàn)是2.0,會(huì)zip壓縮之后再blob二進(jìn)制化,這樣3.0版本就能識(shí)別了),項(xiàng)目工程載入之后才能installtarget,target類位于vm/engine/target,通過調(diào)用target中的函數(shù)就可以控制注冊(cè)的block了,例如在lib/empty-assets中有個(gè)空的項(xiàng)目文件,到當(dāng)項(xiàng)目加載時(shí),調(diào)用了vm外殼中的的installTargets函數(shù),會(huì)將target中的custom,objname,scripts等屬性加載進(jìn)來.
線程間的通信(重要) runtime管理著sprite,runtime與sprite之間用廣播來通信,但當(dāng)sprite之間需要通信時(shí),這變的很復(fù)雜,所以runtime在線程調(diào)度層面實(shí)現(xiàn)了一個(gè)迷你redux,在dispatch文件夾中.central-dispatch是一個(gè)單例模式,會(huì)在vm外殼runtime初始化的時(shí)候調(diào)用他一次(virtual-machine第55行,壞點(diǎn)監(jiān)控寫的非常秀,值得借鑒)把vm設(shè)置成中央總線,他有一個(gè)services對(duì)象來注冊(cè)全局路由,之后在文件中引入dispatch就可以在sprite之間互傳參數(shù)了,在/extension-support/extension-manager中,初始化extension時(shí)調(diào)用dispatch.addWork(),會(huì)為extension新建一個(gè)工作線程,加入到線程池,由于在vm中installTargets時(shí)會(huì)生成一個(gè)extensionPromises隊(duì)列,先異步加載ext再加載target,這樣就保證了每個(gè)target至少會(huì)有一個(gè)worker,這樣就實(shí)現(xiàn)了多線程計(jì)算,target中會(huì)被注入一個(gè)dispatch變量,使用dispatch就可以對(duì)總線狀態(tài)進(jìn)行推送(例如dispatch.call(serviceName)),進(jìn)而實(shí)現(xiàn)了線程調(diào)度.
block渲染(重要) 當(dāng)vm啟動(dòng)時(shí),在runtime入口定義了defaultBlockPackages類,這里面聲明了每個(gè)block塊的功能函數(shù)(比如repeatUntil,moveTo等),在vm/engine/runtime 715行,有一個(gè)_registerBlockPackages函數(shù),會(huì)加載所有block塊動(dòng)作,然后通過聲明基類函數(shù)getPrimitives取得各個(gè)模塊中的block預(yù)定義動(dòng)作,之后通過訂閱分發(fā)(路由模式)的方式生成packagePrimitives類,這樣block塊的特定功能就都可以在vm中使用了(此時(shí)已block已按category分類好),但是這種方式無法處理包含交互的block(如control和event,因?yàn)樗麄儾粌H要響應(yīng)用戶的操作,又要監(jiān)聽其他事件,如greenFlag被點(diǎn)擊等),所以他們?cè)诨愔杏謹(jǐn)U展了hat類,其實(shí)就是一個(gè)二級(jí)路由,原理同上,類似的二級(jí)路由還有monitor類,具體功能參見第二條
關(guān)于hat,這一般是一個(gè)target程序的起點(diǎn)(除非有event的全局廣播事件),vm啟動(dòng)時(shí)會(huì)在runtime維護(hù)一個(gè)hat隊(duì)列,類似于microTick,vm內(nèi)核中有一大堆類step(stepTreads,stepTread等等)函數(shù)會(huì)調(diào)用sequencer,sequencer中有個(gè)_step函數(shù),_step會(huì)將所有隊(duì)列中的函數(shù)執(zhí)行完然后銷毀自己,然后在新的tick如果產(chǎn)生新的任務(wù)則重啟隊(duì)列,所以hat隊(duì)列是全局共享的.
關(guān)于虛擬IO,虛擬IO分為mouse,keyborad,BLE,clock等(名詞解釋:ble是藍(lán)牙,bt是BT下載,clock是系統(tǒng)時(shí)鐘,cloud是云端,keyboard是鍵盤,mouse是鼠標(biāo),mousewheel是鼠標(biāo)滾輪滾動(dòng),video是視頻渲染),stage中的所有按鍵,點(diǎn)擊事件全部被注冊(cè)到了lib/vm-listener-hoc中,然后從該文件中轉(zhuǎn)發(fā)給對(duì)應(yīng)的虛擬IO處理,當(dāng)有點(diǎn)擊事件發(fā)生時(shí),vm-listener首先捕獲事件,然后把消息推送至虛擬機(jī),虛擬機(jī)會(huì)定位到對(duì)應(yīng)的sprite或者帶有hats的sprite,執(zhí)行注冊(cè)的函數(shù).
虛擬IO的作用:scratch中自定義了一套key,每個(gè)key對(duì)應(yīng)一個(gè)sprite動(dòng)作(如uparrow,spaceDown等,scratch只認(rèn)這套key,不認(rèn)其他字符),虛擬鍵盤IO建立起了Ascll碼與scratch-key之間的聯(lián)系,而虛擬鼠標(biāo)IO主要是捕獲鼠標(biāo)在stage上的位置(舞臺(tái)中心默認(rèn)為(0,0),是一個(gè)480360的區(qū)域,如果鼠標(biāo)不在此區(qū)域,在查詢鼠標(biāo)位置時(shí),會(huì)強(qiáng)制設(shè)置成邊緣坐標(biāo),如左上角(-240,-180),如果在區(qū)域內(nèi),會(huì)通過遍歷sprite的drawableID來找到target,然后調(diào)用target中對(duì)應(yīng)的執(zhí)行程序,所以在只在gui當(dāng)中改變舞臺(tái)位置,渲染區(qū)域并不會(huì)改變,因?yàn)槌?80360的區(qū)域都被強(qiáng)制替換了),而虛擬ble,bt都是基于websocket的rpc,都是異步調(diào)用,與ajax的用法差不多
關(guān)于渲染:由于動(dòng)作是線性并可預(yù)測結(jié)束位置(比如:step 5 ),所以渲染器在簡單粗暴的比較前后兩幀的區(qū)別,如果沒有找到區(qū)別,則把上一幀直接作為動(dòng)作結(jié)束并返回
targets = targets.filter(target => !!target); 這是對(duì)象去空操作
return !target.hasOwnProperty('isOriginal') || target.isOriginal; 這是剔除clone的操作,因?yàn)閠arget.isOriginal為false時(shí),代表這是clone的對(duì)象,由于isOriginal屬性可能有也可能沒有,直接判斷target.isOriginal有可能會(huì)報(bào)錯(cuò),所以要先加一個(gè)hasOwnProperty判斷,如果不存在這個(gè)屬性就不往后判斷了
Object.is()比較兩個(gè)對(duì)象是否嚴(yán)格相等,因?yàn)?==有時(shí)候會(huì)有比較嚴(yán)重問題:如 +0 === -0 //true , NaN === NaN // false
static get RUNTIME_STARTED (){ return XXX},這是靜態(tài)繼承,他是屬性不是函數(shù),近似看成this.RUNTIME_STARTED就可以了,可以像這樣全局調(diào)用vm.RUNTIME_STARTED
gui界面 在vm/engine/runtime 在500行左右通過靜態(tài)繼承定義,(更改舞臺(tái)渲染器大小是在這里) 在外部使用vm.XXX調(diào)用,實(shí)質(zhì)上是在調(diào)用函數(shù)的取值器get() (更改舞臺(tái)大小和位置是個(gè)巨大的工程,因?yàn)閟cratch在虛擬IO(video,mouse),lib,gui的css和渲染器(render)中都定義了具體的數(shù)值,其他地方我暫時(shí)還沒發(fā)現(xiàn),稍有修改都可能引起bug)
gui界面的命名規(guī)則(重要):頂層藍(lán)框(有sratch圖標(biāo),可選語言)區(qū)域叫做menu-bar,下方選擇代碼/造型/聲音區(qū)域叫做tab-list(點(diǎn)擊可切換tabPanel),然后是下方:
一,左邊的2/3區(qū)域:左側(cè)舞臺(tái)叫做gui-blocks(可以選擇語句塊并拖放出來),其中最左側(cè)選擇類型欄叫做(blocklyToolbox,包含上方的scratchCategoryMenu(內(nèi)含運(yùn)動(dòng),聲音,外觀等選項(xiàng))和下方的gui_extension(添加擴(kuò)展按鈕)),中側(cè)叫做blocklyFlyout(顧名思義,當(dāng)拖拽block進(jìn)入此區(qū)域會(huì)被刪除)最右層叫做blocklyWorkspace(能夠盛放拖拽出的block,他的右下角有一個(gè)blocklyZoom組件(用來控制代碼塊舞臺(tái)的縮放),blocklyWorkspace還定義了一堆瀏覽器組件,比如ScrollbarVertical(這是一個(gè)水平滾動(dòng)條),比如blocklyFlyoutBackground(這是一個(gè)描邊功能),不能理解他為什么不用系統(tǒng)自帶的,費(fèi)力寫這么一大片代碼,然后看起來比系統(tǒng)默認(rèn)的還要丑..)
二,右邊1/3區(qū)域:上側(cè)叫做stage,其中包含(stage-header和stage-canvas(渲染舞臺(tái))),下側(cè)叫做gui-target,它是左右結(jié)構(gòu),左側(cè)叫做sprite-selector(選擇角色)(內(nèi)含sprite-info(設(shè)置大小,方向,名稱什么的),sprite-selector-item(角色圖標(biāo))和sprite-selector-add-button(角色選擇按鈕)),右側(cè)叫做target-pane(內(nèi)含stage-selector-header(舞臺(tái)字樣),selector-costume-canvas(渲染舞臺(tái)背景),stage-selector-add-button(舞臺(tái)選擇按鈕))
與scratch通信:在lib/vm-listener-hoc中定義,主要是把vm當(dāng)中導(dǎo)出的各種狀態(tài)映射到reducer當(dāng)中.
block定義結(jié)構(gòu),樣式和行為,Toolbox定義類型,用于在workspace中分類顯示,Blockly是總的控制函數(shù).block分為block和shadow類型(shadow是占位符,不是塊,提供block的默認(rèn)值,當(dāng)有其他其他塊占據(jù)他的位置時(shí)會(huì)被覆蓋),statement block指的是沒有輸出的block(只能在變量中添加,不能在流程中添加)
Blockly.Extensions.registerMutator(name, mixinObj, opt_helperFn, opt_blockList) 這是注冊(cè)皮膚編輯器,Blockly.Extensions.register在scratch中被影射成了Blockly.ScratchBlocks.VerticalExtensions,而Blockly.ScratchBlocks追蹤到最后是一個(gè)goog.require('Blockly.ScratchBlocks'),所有的goog.require函數(shù)鏈接一般指向的是輸出文件夾的一個(gè).compress.js文件,如果沒有就是指向scratch-block/core文件夾,如果還沒有就指向一個(gè).py文件,可以直接編譯出來,具體在package.json中配置
vm是在containers/gui.jsx中啟動(dòng)的,scratch中components是純函數(shù)組件,而在containers文件夾中會(huì)把同名components與redux和vm連接,同時(shí)進(jìn)行國際化,組件節(jié)流,版本控制,虛擬IO監(jiān)聽等操作,邏輯非常清晰.所有ui狀態(tài)在reducer/gui.js中進(jìn)行組裝然后統(tǒng)一導(dǎo)出,但是要注意scratch根目錄下的index.js是個(gè)假的入口文件,reducer真正是在lib/app-state-hoc中的AppStateHOC類組裝的,這是一個(gè)中間件,在入口函數(shù)render-gui中GUI組件使用compose函數(shù)進(jìn)行柯里化(將f(a)(b)(c)(d)變成f(a,b,c)(d)叫做柯里化)封裝了AppStateHOC, HashParserHOC,TitledHOC三個(gè)中間件,而AppStateHOC通過判斷是否需要加載paint和gui來加載不同的store,因此<Provider>也在這個(gè)組件當(dāng)中,guiMiddleware是一個(gè)封裝了throttle的柯里化函數(shù),按照中間件模式調(diào)用,用于為組件節(jié)流(如果一秒內(nèi)點(diǎn)了很多次,只會(huì)執(zhí)行兩次),封裝之后返回了經(jīng)過國際化(多語言模塊)和節(jié)流處理的高級(jí)組件(節(jié)流的實(shí)現(xiàn):當(dāng)createStore擁有enhancer參數(shù)時(shí),會(huì)返回一個(gè)enhancer(createStore)(reducer,state)的高級(jí)組件,這樣使用enhancer就可以實(shí)現(xiàn)組件的功能模塊化,類似于鏈?zhǔn)秸{(diào)用(一個(gè)函數(shù)只完成一個(gè)功能))
一次完整的vm調(diào)用過程,加載時(shí)先全局加載虛擬機(jī),初始化虛擬IO(參見第10,11條),然后查看網(wǎng)絡(luò)請(qǐng)求中l(wèi)ocation對(duì)象的hash,如果不能識(shí)別,直接在本地新建工程,并為工程賦予唯一ID值,如果能識(shí)別,從網(wǎng)絡(luò)或從本地加載工程(參考第六條),將舞臺(tái)推入render生成渲染器,再把渲染器推入vm,然后調(diào)用Blockly.inject函數(shù)在一個(gè)dom(類似于div#id)上面,初始化workspace和flyoutWorkspace樣式(之所以有兩個(gè)workspace,是因?yàn)閣orkspace的宿主是target,當(dāng)sprite銷毀時(shí)這個(gè)workspace也就銷毀了,而flyoutWorkspace宿主是vm,他負(fù)責(zé)將各個(gè)sprite中拖出來的block干掉,因此不能銷毀,另外Blockly.inject的option對(duì)象中可以設(shè)置toolbox選項(xiàng),能夠加載外部xml,而且inject調(diào)用了core/DragSurfaceSvg,這是一個(gè)svg繪圖程序,理論上應(yīng)該也可以在這里渲染block出來,然而他只在這里渲染了兩個(gè)workspace出來,不明白他為什么還要再去渲染一遍),然后為workspace建立blockListener(在vm/engine/block中定義,為block建立的通用(非特定)動(dòng)作函數(shù),如move,delete,create,click什么的,特定的block動(dòng)作函數(shù)的加載參見第八條),然后為flyoutWorkspace建立flyoutBlockListener(追了好久發(fā)現(xiàn)居然和blockListener一毛一樣,move事件(回調(diào)中有parent和input),change事件(回調(diào)中有name和value),所以如果需要給所有的block加統(tǒng)一的事件(僅限field和mutation)可以在這里添加 engine/blocks.js 第294行blocklyListen函數(shù))
ui數(shù)據(jù)在reducer/gui.js中進(jìn)行組裝
guiMiddleware是一個(gè)封裝了throttle的柯里化函數(shù),按照中間件模式調(diào)用,用于為組件節(jié)流(如果一秒內(nèi)點(diǎn)了很多次,只會(huì)執(zhí)行兩次)
index.js是個(gè)假的組裝文件,reducer真正是在lib/app-state-hoc中的AppStateHOC類,這是一個(gè)中間件,在入口函數(shù)render-gui中GUI組件使用compose函數(shù)柯里化封裝 AppStateHOC, HashParserHOC,TitledHOC三個(gè)中間件
AppStateHOC通過判斷是否需要加載paint和gui來加載不同的store,<Provider>就在這個(gè)組件當(dāng)中,返回了經(jīng)過國際化(多語言模塊)和節(jié)流處理的高級(jí)組件(節(jié)流的實(shí)現(xiàn):當(dāng)createStore擁有enhancer函數(shù)時(shí),會(huì)返回一個(gè)enhancer(createStore)(reducer,state)的高級(jí)組件,這樣使用enhancer就可以實(shí)現(xiàn)組件的功能模塊化,類似于鏈?zhǔn)秸{(diào)用(一個(gè)函數(shù)只完成一個(gè)功能))