react事件源碼步步調(diào)

本文通過一個簡短的實例&控制臺調(diào)試引镊,了解react事件處理的全過程誉券。下面是測試用代碼指厌,使用控制臺可以清晰看到函數(shù)執(zhí)行過程中參數(shù)變化以及方法所屬模塊&調(diào)用棧,所以本文圖片較多踊跟。


class RemoveBtn extends Component {
    clickHandler = () => {
        this.props.handleClick();
    }
    render(){
        return(
            <button onClick={this.clickHandler}>togglage測試組件</button>
        )
    }
}

class Root extends Component {
    
    clickHandler = () => {
        alert('hanlder is1 perform')
    }
    render(){
        return (
            <div className="first">
                <RemoveBtn handleClick = {this.clickHandler}/>
            </div>
        )
    } 
}

ReactDOM.render(<Root />, document.getElementById('root'));

1 事件綁定

1.1 綁定的結果

事件綁定結果

說明: 這里的backend.js是react調(diào)試工具的腳本不用考慮踩验。

圖中可見只有在document上綁定了名為dispatchEvent的來自于 ReactEventListener.js模塊的事件處理函數(shù)。

1.2 事件綁定的過程

ReactDOM.render(<Root />, document.getElementById('root'));

一切開始于ReactDOM.render調(diào)用的ReactMount.jsrender方法商玫。忽略掉實例化組建的過程箕憾,詳細調(diào)用可以查看截圖右側的調(diào)用棧。

ReactDom.render 將react組件渲染到指定的容器上

_renderSubtreeIntoContainer -> mountComponentIntoNode -> mountComponent[reactReconciler.js] -> _updateDOMProperties

判斷綁定事件還是刪除事件-w1564

_updateDOMProperties函數(shù)在mountComponent拳昌,unmountComponentupdateComponent階段都有調(diào)用袭异,它是檢查屬性變化,調(diào)優(yōu)性能的重要方法炬藤。下圖節(jié)選處理事件綁定部分代碼御铃,方法中有指向上次屬性值得lastProp, nextProp是當前屬性值,這里nextProp是我們綁定給組件的onclick事件處理函數(shù)沈矿。nextProp 不為空調(diào)用enqueuePutListener綁定事件為空則注銷事件綁定畅买。

queuePutListener

enqueuePutListener 這個方法只在瀏覽器環(huán)境下執(zhí)行,傳給listenTo參數(shù)分別是事件名稱'onclick'和代理事件的綁定dom细睡。如果是fragement 就是根節(jié)點(在reactDom.render指定的),不是的話就是document帝火。listenTo 用于綁定事件到 document 溜徙,下面交由事務處理的是回調(diào)函數(shù)的存儲,便于調(diào)用犀填。ReactBrowserEventEmitter 文件中的 listenTo 看做事件處理的源頭蠢壹。

listenTo
  listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    var isListening = getListeningForDocument(mountAt);
    // 獲取 registrationName(注冊事件名稱)的topLevelEvent(頂級事件類型)
    var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];

    for (var i = 0; i < dependencies.length; i++) {
      var dependency = dependencies[i];
      if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
        if (dependency === 'topWheel') {
           ...         
        } else if (dependency === 'topScroll') {
               ...
        } else if (dependency === 'topFocus' || dependency === 'topBlur') {
                ...
        } else if (topEventMapping.hasOwnProperty(dependency)) {
        // 獲取 topLevelEvent 對應的瀏覽器原生事件
          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
        }
        isListening[dependency] = true;
      }
    }
  },

對于同一個事件,例如click有兩個事件 onClick(在冒泡階段觸發(fā)) onClickCapture(在捕獲階段觸發(fā))兩個事件名九巡,這個冒泡和捕獲都是react事件模擬出來的图贸。綁定到 document上面的事件基本上都是在冒泡階段(對 whell, focus, scroll 有額外處理),如下圖 click 事件綁定執(zhí)行的如下。

listenTo

topEventMappingtopLevlelEvent 瀏覽器事件對照關系冕广,mountAt 是綁定對象是函數(shù)接收第二個參數(shù)疏日,也就是上文的doc(document)。

ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent 對所傳的target做了非空判斷后調(diào)用 EventListener.listen 傳參數(shù)分別是:事件對象撒汉, 瀏覽器原生事件名稱沟优, 指定了頂級事件類型的事件處理函數(shù)(bind函數(shù))ReactEventListener.dispatchEvent.bind(null, topLevelType)

EventListener.listen

EventListener.listen 將事件綁定到target上睬辐。
回到上文利用事務存儲事件部分挠阁,這里調(diào)用的putListener方法

調(diào)用 EventPluginHub.putListener 第一個參數(shù)是組件事例宾肺,第二個是‘onClick’,第三個是我們寫的事件處理函數(shù)

listenerBank存儲的listener

putListener 將事件處理函數(shù)存儲到listenerBank[registrationName][key]上其中registrationName是事件名稱侵俗,.${_rootNodeID}``作為key值锨用,處理函數(shù)作為value存儲。下面調(diào)用的方法有對與safraiclick`事件的兼容處理隘谣。
至此事件綁定告一段落了增拥。

2 事件處理

event pooling事件池
合成事件是 pooled(循環(huán)使用的),這意味著合成事件對象會被重復使用洪橘,所有的屬性在被調(diào)用以后會被值為null跪者,該機制用于性能優(yōu)化,因此你不可以異步訪問事件熄求。除非調(diào)用 event.persist()渣玲,該方法不會不會把事件放入事件池中,保持event對象不被重置允許代碼的引用到弟晚。

事件觸發(fā)后執(zhí)行dispatchEvent方法忘衍,該方法第一個參數(shù)是綁定時bind的 topLevelEvent這里是 topClick,此處調(diào)用TopLevelCallbackBookKeeping.getPooled函數(shù)先去事件池中取可以復用的卿城,沒有的話初始化新的枚钓。

這個bookKeeping初始化很簡單,就是把頂級事件類型瑟押,原生事件對象搀捷,空的父組件列表放在一個對象上。

獲取bookKeeping-w1062

reactUpdate.batchedUpdates是用事務封裝了handleTopLevelImpl(bookKeeping)多望。

getEventTarget 返回的是對應的Dom節(jié)點
ReactDOMComponentTree.getClosestInstanceFromNode 返回對應的 reactDomComponent

執(zhí)行事件回調(diào)前,先由當前組件向上遍歷它的所有父組件嫩舟。保存到bookKeeping.ancestors這個數(shù)組中。因為事件回調(diào)中可能會改變DOM結構,所以要先遍歷好組件層級怀偷,防止與已緩存ReactMount's node相矛盾家厌。之后就是依次掉調(diào)用 ReactEventListener._handleTopLevel

最后一個參數(shù)通過getEventTarget函數(shù)兼容svg以及safraitextNode 這里最終返回的是觸發(fā)事件的DOM節(jié)點。

handleTopLevel函數(shù)經(jīng)由EventPluginHub處理 top level Event椎工,在EventPluginHub處理過程中不同的plugin可以創(chuàng)建派發(fā)相應的事件饭于。第一行是構造出合成事件,第二行就是交由事務處理事件维蒙。

2.1 構建react事件

extractEvent 讓已注冊的plugin處理相應的的topLevelType掰吕。下圖看到在運行過程中已注冊的plugin只有五個分別是

 ReactInjection.EventPluginHub.injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin
  });

extractEvent會依次調(diào)用每個pluginextractEvents方法,第一個處理的是SimpleEventPlugin颅痊,該plugin處理了絕大部分的事件畴栖,本例 onClick 就是其中之一。

SimpleEventPlugin.extractEvent

經(jīng)由一個switch(topLevelType)確定該react事件的構造函數(shù)為SyntheticMouseEvent

SimpleEventPlugin.extractEvent 根據(jù) topLevelEvent 處理事件

上文看到 topClick 使用 syntheticMouseEvent 作為事件構造函數(shù)八千。

這里調(diào)用的EventConstructor.getPooled就是開篇提到的事件池吗讶,先看有沒有可以復用的事件對象沒有的話在重新實例一個燎猛。

-w944

這里SyntheticMouseEvent調(diào)用 SyntheticUIEvent, SyntheticUIEvent調(diào)用 SyntheticEventSyntheticEvent構造函數(shù)這部分代碼相對較長照皆,函數(shù)注釋中說道重绷,該方法應該盡量減少調(diào)用的頻率,使用pooling(回收再利用|池)機制膜毁。在構建時候會通過判斷isPersistent屬性來判斷調(diào)用后是否放入池中昭卓。使用者可以通過調(diào)用 persist方法來改變這個值。
而后執(zhí)行的是 EventPropagators.accumulateTwoPhaseDispatches(event)
這個方法經(jīng)歷層層跳轉瘟滨,詳情可見調(diào)用棧候醒,最后到traverseTwoPhase這個函數(shù)。inst 為 觸發(fā)事件的reactDomComponent,fnaccumulateDirectionDispatches, arg 為合成事件杂瘸。

function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
    path.push(inst);
    inst = inst._hostParent;
  }
  var i;
  for (i = path.length; i-- > 0;) {
    fn(path[i], 'captured', arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

path 為收集的以target起始到根節(jié)點為止的組件倒淫,本例中兩個。用于后續(xù)模擬事件的捕獲和冒泡败玉。

traverseTwoPhase

之后按照從外到內(nèi)捕獲從里到外冒泡的順序調(diào)用 accumulateDirectionDispatches(path[i], 'captured', arg)該方法將合成事件與處理函數(shù)聯(lián)系起來敌土。

這里 listenerAtPhase -> getListener[EventPluginHub.js] 獲取事件處理函數(shù)。
在事件綁定中最后把所有的事件處理放在一個對象上listenerBank运翼。

-w594

通過注冊類型獲取到對應類型的所有處理函數(shù)返干,使用.${reactDomComponent._rootNodeID} 找到對應虛擬Dom上的事件處理函數(shù)。

/**
  * @param {object} inst reactDOMComponent 實例 (虛擬DOM)
  * @param {string} registrationName 注冊事件名
  * * /
 getListener: function (inst, registrationName) {
    // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
    // live here; needs to be moved to a better place soon
    // 獲取同類型的所有處理函數(shù)
    var bankForRegistrationName = listenerBank[registrationName];
    if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
      return null;
    }
    // 獲取 .${reactDomComponent._rootNodeID}`
    var key = getDictionaryKey(inst);
    // 返回指定虛擬DOM上的事件處理函數(shù)
    return bankForRegistrationName && bankForRegistrationName[key];
  }


獲取事件處理函數(shù)后血淌,將它和響應的reactDOMComponent分別添加到隊列中矩欠。accumulateInto用于將內(nèi)容添加到現(xiàn)有隊列中,傳入原來隊列和要添加到隊列中的內(nèi)容悠夯。

accumulateInto

至此事件已經(jīng)封裝準備好了晚顷。

2.2 事件分發(fā)

事件分發(fā)

承接上文封裝好的event對象。使用runEventQueueInBatch開始事件分發(fā)疗疟。

這里第一行用于將事件放入隊列processEventQueue中,其內(nèi)部調(diào)用的還是accumulateInto方法瞳氓。
第二行策彤,processEventQueue派發(fā)所有在事件隊列processEventQueue中的合成事件。

首先將隊列中的內(nèi)容取出匣摘,清空隊列店诗,以防處理中隊列變化。

simulated:為true表示React測試代碼音榜,我們一般都是false
此注解出自參考文章一

這里forEachAccumulate就是對第一個參數(shù)執(zhí)行foreach調(diào)用第二個參數(shù)庞瘸。

executeDispatchesAndReleaseTopLevel -> executeDispatchesAndRelease 該函數(shù) -> EventPluginUtils.executeDispatchesInOrder,并將沒有調(diào)用persist的事件對象回收到事件池赠叼。

EventPluginUtils.executeDispatchesInOrder

處理函數(shù)是多個擦囊,則依次執(zhí)行违霞。本例中只有一個處理函數(shù) -> executeDispatch。執(zhí)行后設置 event._dispatchListenerevent._dispatchInstances 為 null瞬场。

executeDispatch

通過EventPluginUtils.getNodeFromInstance獲取響應的對應的真實DOM節(jié)點作為事件的currentTarget买鸽。
本例執(zhí)行85行 這里的type 為click,func為事件處理函數(shù)贯被, event為合成事件對象眼五。
在生產(chǎn)環(huán)境中,會直接調(diào)用事件處理函數(shù)彤灶,開發(fā)環(huán)境中會模擬瀏覽器事件看幼。

模擬過程如下。

這里在創(chuàng)建的fakeElement上綁定事件幌陕,之后模擬事件觸發(fā)(執(zhí)行本例中的事件處理函數(shù))诵姜,再注銷事件綁定。

到此為止這個事件已經(jīng)處理完苞轿,接下來就是把這個事件屬性置為null茅诱,然后把它放入事件池中了。判斷是否強制了調(diào)用了persistent搬卒,沒有的話就釋放事件對象瑟俭。

其實這里可以看到事件池有一個上線就是10,當可用的對象大于10也不會再往里面添加了契邀。
最后看一下事件的 destructor 方法

這里獲取所有的屬性設置為null摆寄,并且再訪問該事件對象時會預警提醒。

至此事件處理完成坯门。

3 事件機制總結

這里是源碼注釋的翻譯

  • 頂級代理是用于捕獲多數(shù)原生瀏覽器事件微饥,這些只會在主線程發(fā)生,并由reactEventLister 負責處理古戴,reactEventLister 是被注入的因此可以支持插件事件資源欠橘,這是唯一在主線程執(zhí)行的。
  • 封裝了頂層事件(TopLevelEvent)來應對瀏覽器異常现恼。這個在工作線程完成肃续。
  • 傳遞原生事件以及封裝的頂層事件名稱到 EventPluginHub,他會遍歷插件是否要執(zhí)行某些合成事件叉袍。
  • EventPluginHub 獲取響應的事件監(jiān)聽器始锚,以及Dom綁定到生成的事件對象上。
  • EventPluginHub 將會派發(fā)事件

3.1 各種事件名

主要三個事件:regiestrationName(注冊事件名)喳逛,topLevelType(頂層事件)瞧捌,(原生事件)

事件綁定階段,從組件屬性中獲取’注冊事件名‘,會區(qū)分捕獲和默認冒泡事件名姐呐,這里的注冊名為react對外暴露的事件殿怜,包含自定義事件。
頂層事件是react封裝EventPlugin處理的單位皮钠,react對外暴露的事件是由一個多個事件模擬而成的稳捆。
原生事件是最終綁定到目標元素上的事件,和頂層事件對應關系為一對一的關系麦轰。在綁定給document的是使用bind函數(shù)乔夯,固定第一個參數(shù)——topLevelEvent的函數(shù)。因此當事件觸發(fā)后使用的款侵。

// 本例中
// regiestrationName(注冊名)
onClick
onClickCapture

// topLevelType (頂層事件類型)
topClick

// native Event (原生事件) | dependence
click

// regiestrationName => topLevelType
EventPluginRegisterName.registionNameDependencies

// topLevelType => native event
topEventMapping[位于reactBrowserEventEmitter.js]

3.2 事件全局代理(target)

根據(jù)不同的topLevelType對應的瀏覽器事件末荐,綁定到target上(如果是fragement 就是根節(jié)點(在reactDom.render指定的),不是的話就是document)ReactEventListener.dispatchEvent.bind(null, topLevelType)新锈。

3.3 事件存儲

當組件渲染和更新的時候會調(diào)用_updateDomPorperties方法檢查屬性變化甲脏,這里執(zhí)行reactBrowerEventEmitter模塊下的listenTo對不同事件進行了兼容處理后最終調(diào)用 EventPluginHub.js模塊下的 putListener方法,將事件處理函數(shù)妹笆,以 .${reactDomComponent._rootNodeID}為key值放在listenerBank[registrationName]對象上块请。

3.4 阻止事件冒泡

通過事件綁定的分析會發(fā)現(xiàn),無論注冊的是onClick 還是 onClickCapture 最后都是調(diào)用 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent 在冒泡階段觸發(fā)的事件拳缠, 也會發(fā)現(xiàn)在沒有執(zhí)行事件處理函數(shù)的時候墩新,事件就已經(jīng)eventQuene中,那是不是就意味著調(diào)用e.stopPropagation()就不能阻止事件冒泡了呢窟坐。

事件處理函數(shù)中獲取的事件是合成事件對象海渊,合成事件對象也是有stopPropagation方法的。

合成事件對象的stopPropagation方法

注意這里的最后一行哲鸳,這里執(zhí)行的函數(shù)為事件isPropagationStopped方法賦值了一個只會返回true的函數(shù)臣疑。而在一次調(diào)用事件處理函數(shù)的過程中,每一次都會調(diào)用事件對象的該方法徙菠。

因此使用e.stopPropagation()不能組織原生事件冒泡讯沈,但是模擬到阻止事件冒泡的效果的。

react 文檔說明
更多可參考[4]

3.5 事件相關文件

synthetcEvent 封裝合成事件基類

原型方法:

  • preventDefault()
  • stopPropergation()
  • persist() 調(diào)用后isPersist = true, 此事件對象將不會被銷毀復用(進入事件池)
  • isPersist
  • desturctor() 事件觸發(fā)后(isPersist!==true), 清空事件對象屬性婿奔。

**靜態(tài)方法: **

  • arugumentClass
// @prarm interface 需要定義的事件對象屬性
// @param Class 子類
SyntheticEvent.augmentClass = function(Class, Interface) {
  var Super = this;
  var E = function() {};
  E.prototype = Super.prototype;
  var prototype = new E();

  Object.assign(prototype, Class.prototype);
  // 子類繼承基類原型上的方法
  Class.prototype = prototype;
  Class.prototype.constructor = Class;
    // 合并interface
  Class.Interface = Object.assign({}, Super.Interface, Interface);
  Class.augmentClass = Super.augmentClass;
  // 為子類添加事件池相關屬性和方法
  addEventPoolingTo(Class);
};
  • eventPool[]
  • getPooled()
    參數(shù)同構造函數(shù)傳參缺狠,判斷事件池中是否有可用事件,有的復用脸秽,沒有新建。
  • release(event)
    判斷事件對象是否 isPersist過 沒有的話調(diào)用對象的 destructor蝴乔, 之后將其添加入事件池记餐。

參考
React源碼分析7 — React合成事件系統(tǒng)
看源碼react事件機制
React源碼解讀系列 – 事件機制
react 合成事件和原生事件的阻止冒泡

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市薇正,隨后出現(xiàn)的幾起案子片酝,更是在濱河造成了極大的恐慌囚衔,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雕沿,死亡現(xiàn)場離奇詭異练湿,居然都是意外死亡,警方通過查閱死者的電腦和手機审轮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門肥哎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人疾渣,你說我怎么就攤上這事篡诽。” “怎么了榴捡?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵杈女,是天一觀的道長。 經(jīng)常有香客問我吊圾,道長达椰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任项乒,我火速辦了婚禮啰劲,結果婚禮上,老公的妹妹穿的比我還像新娘板丽。我一直安慰自己呈枉,他們只是感情好,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布埃碱。 她就那樣靜靜地躺著猖辫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪砚殿。 梳的紋絲不亂的頭發(fā)上啃憎,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機與錄音似炎,去河邊找鬼辛萍。 笑死,一個胖子當著我的面吹牛羡藐,可吹牛的內(nèi)容都是我干的贩毕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼仆嗦,長吁一口氣:“原來是場噩夢啊……” “哼辉阶!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤谆甜,失蹤者是張志新(化名)和其女友劉穎垃僚,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體规辱,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡谆棺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了罕袋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片改淑。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖炫贤,靈堂內(nèi)的尸體忽然破棺而出溅固,到底是詐尸還是另有隱情,我是刑警寧澤兰珍,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布侍郭,位于F島的核電站,受9級特大地震影響掠河,放射性物質(zhì)發(fā)生泄漏亮元。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一唠摹、第九天 我趴在偏房一處隱蔽的房頂上張望爆捞。 院中可真熱鬧,春花似錦勾拉、人聲如沸煮甥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽成肘。三九已至,卻和暖如春斧蜕,著一層夾襖步出監(jiān)牢的瞬間双霍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工批销, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留洒闸,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓均芽,卻偏偏與公主長得像丘逸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子掀宋,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,515評論 25 707
  • 版權聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉載囤萤。 PS:轉載請注明出處作者:TigerChain地址:http...
    TigerChain閱讀 8,368評論 1 9
  • 原教程內(nèi)容詳見精益 React 學習指南,這只是我在學習過程中的一些閱讀筆記是趴,個人覺得該教程講解深入淺出涛舍,比目前大...
    leonaxiong閱讀 2,810評論 1 18
  • 成果: Django的簡介 Django的基本教程這個是菜鳥教程中的,包含了安裝和一些基本的使用唆途,講的還可以 介紹...
    泠泠七弦客閱讀 394評論 0 0
  • 在師父的教導下富雅,我對代碼的書寫規(guī)范也是越來越有強迫癥了。我的代碼若能有幸被你看見肛搬,則不難發(fā)現(xiàn)有一些規(guī)律没佑。 *.ht...
    依暄閱讀 1,928評論 8 8