JavaScript是如何工作的:編寫(xiě)自己的Web開(kāi)發(fā)框架 + React及其虛擬DOM原理

摘要: 深入JS系列19叙赚。

Fundebug經(jīng)授權(quán)轉(zhuǎn)載肠套,版權(quán)歸原作者所有截碴。

這是專門(mén)探索 JavaScript 及其所構(gòu)建的組件的系列文章的第 19 篇憔辫。

如果你錯(cuò)過(guò)了前面的章節(jié),可以在這里找到它們:

響應(yīng)式原理

Proxy 允許我們創(chuàng)建一個(gè)對(duì)象的虛擬代理(替代對(duì)象),并為我們提供了在訪問(wèn)或修改原始對(duì)象時(shí)窟蓝,可以進(jìn)行攔截的處理方法(handler)罪裹,如 set()、get() 和 deleteProperty() 等等运挫,這樣我們就可以避免很常見(jiàn)的這兩種限制(vue 中):

  • 添加新的響應(yīng)性屬性要使用 Vue.$set()状共,刪除現(xiàn)有的響應(yīng)性屬性要使用
  • 數(shù)組的更新檢測(cè)

Proxy

let proxy = new Proxy(target, habdler);
  • target:用 Proxy 包裝的目標(biāo)對(duì)象(可以是數(shù)組對(duì)象,函數(shù)谁帕,或者另一個(gè)代理)
  • handler:一個(gè)對(duì)象峡继,攔截過(guò)濾代理操作的函數(shù)

實(shí)例方法

方法 描述
handler.apply() 攔截 Proxy 實(shí)例作為函數(shù)調(diào)用的操作
handler.construct() 攔截 Proxy 實(shí)例作為函數(shù)調(diào)用的操作
handler.defineProperty() 攔截 Object.defineProperty() 的操作
handler.deleteProperty() 攔截 Proxy 實(shí)例刪除屬性操作
handler.get() 攔截 讀取屬性的操作
handler.set() 截 屬性賦值的操作
handler.getOwnPropertyDescriptor() 攔截 Object.getOwnPropertyDescriptor() 的操作
handler.getPrototypeOf() 攔截 獲取原型對(duì)象的操作
handler.has() 攔截 屬性檢索操作
handler.isExtensible() 攔截 Object.isExtensible() 操作
handler.ownKeys() 攔截 Object.getOwnPropertyDescriptor() 的操作
handler.preventExtension() 截 Object().preventExtension() 操作
handler.setPrototypeOf() 攔截Object.setPrototypeOf()操作
Proxy.revocable() 創(chuàng)建一個(gè)可取消的 Proxy 實(shí)例

Reflect

Reflect 是一個(gè)內(nèi)置的對(duì)象,它提供攔截 JavaScript 操作的方法匈挖。這些方法與處理器對(duì)象的方法相同碾牌。Reflect不是一個(gè)函數(shù)對(duì)象,因此它是不可構(gòu)造的儡循。

與大多數(shù)全局對(duì)象不同舶吗,Reflect沒(méi)有構(gòu)造函數(shù)。你不能將其與一個(gè)new運(yùn)算符一起使用择膝,或者將Reflect對(duì)象作為一個(gè)函數(shù)來(lái)調(diào)用誓琼。Reflect的所有屬性和方法都是靜態(tài)的(就像Math對(duì)象)。

為什么要設(shè)計(jì) Reflect 肴捉?

1. 更加有用的返回值

早期寫(xiě)法:

try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

Reflect 寫(xiě)法:

if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

2. 函數(shù)式操作

早期寫(xiě)法:

'name' in Object //true

Reflect 寫(xiě)法:

Reflect.has(Object,'name') //true

3. 可變參數(shù)形式的構(gòu)造函數(shù)

一般寫(xiě)法:

var obj = new F(...args)

Reflect 寫(xiě)法:

var obj = Reflect.construct(F, args)

當(dāng)然還有很多腹侣,大家可以自行到 MND 上查看

什么是代理設(shè)計(jì)模式

代理模式(Proxy),為其他對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪問(wèn)齿穗。代理模式使得代理對(duì)象控制具體對(duì)象的引用傲隶。代理幾乎可以是任何對(duì)象:文件,資源缤灵,內(nèi)存中的對(duì)象伦籍,或者是一些難以復(fù)制的東西±渡梗現(xiàn)實(shí)生活中的一個(gè)類比可能是銀行賬戶的訪問(wèn)權(quán)限。

例如帖鸦,你不能直接訪問(wèn)銀行帳戶余額并根據(jù)需要更改值芝薇,你必需向擁有此權(quán)限的人(在本例中 你存錢(qián)的銀行)詢問(wèn)。

var account = {
    balance: 5000
}

var bank = new Proxy(account, {
    get: function (target, prop) {
        return 9000000;
    }
});

console.log(account.balance); // 5,000 
console.log(bank.balance);    // 9,000,000 
console.log(bank.currency);   // 9,000,000 

在上面的示例中作儿,當(dāng)使用 bank 對(duì)象訪問(wèn) account 余額時(shí)洛二,getter 函數(shù)被重寫(xiě),它總是返回 9,000,000 而不是屬性值攻锰,即使屬性不存在晾嘶。

var bank = new Proxy(account, {
    set: function (target, prop, value) {
        // Always set property value to 0
        return Reflect.set(target, prop, 0); 
    }
});

account.balance = 5800;
console.log(account.balance); // 5,800

bank.balance = 5400;
console.log(account.balance); // 0

通過(guò)重寫(xiě) set 函數(shù),可以修改其行為娶吞±萦兀可以更改要設(shè)置的值,更改其他屬性妒蛇,甚至根本不執(zhí)行任何操作机断。

響應(yīng)式

現(xiàn)在已經(jīng)對(duì)代理設(shè)計(jì)模式的工作方式有了基本心,讓就開(kāi)始編寫(xiě) JavaScript 框架吧绣夺。

為了簡(jiǎn)單起見(jiàn)吏奸,將模擬 AngularJS 語(yǔ)法。聲明控制器并將模板元素綁定到控制器屬性:

<div ng-controller="InputController">
    <!-- "Hello World!" -->
    <input ng-bind="message"/>   
    <input ng-bind="message"/>
</div>

<script type="javascript">
  function InputController () {
      this.message = 'Hello World!';
  }
  angular.controller('InputController', InputController);
</script>

首先陶耍,定義一個(gè)帶有屬性的控制器奋蔚,然后在模板中使用這個(gè)控制器。最后烈钞,使用 ng-bind 屬性啟用與元素值的雙向綁定泊碑。

解析模板并實(shí)例化控制器

要使屬性綁定,需要獲得一個(gè)控制器來(lái)聲明這些屬性棵磷, 因此蛾狗,有必要定義一個(gè)控制器并將其引入框架中。

在控制器聲明期間仪媒,框架將查找?guī)в?ng-controller 屬性的元素。

如果它符合其中一個(gè)已聲明的控制器谢鹊,它將創(chuàng)建該控制器的新實(shí)例算吩,這個(gè)控制器實(shí)例只負(fù)責(zé)這個(gè)特定的模板。

var controllers = {};
var addController = function (name, constructor) {
    // Store controller constructor
    controllers[name] = {
        factory: constructor,
        instances: []
    };
    
    // Look for elements using the controller
    var element = document.querySelector('[ng-controller=' + name + ']');
    if (!element){
       return; // No element uses this controller
    }
    
    // Create a new instance and save it
    var ctrl = new controllers[name].factory;
    controllers[name].instances.push(ctrl);
    
    // Look for bindings.....
};

addController('InputController', InputController);

這是手動(dòng)處理的控制器變量聲明佃扼。 controllers 對(duì)象包含通過(guò)調(diào)用 addController 在框架內(nèi)聲明的所有控制器偎巢。

對(duì)于每個(gè)控制器,保存一個(gè) factory 函數(shù)兼耀,以便在需要時(shí)實(shí)例化一個(gè)新控制器压昼,該框架還存儲(chǔ)模板中使用的相同控制器的每個(gè)新實(shí)例求冷。

查找 bind 屬性

現(xiàn)在,已經(jīng)有了控制器的一個(gè)實(shí)例和使用這個(gè)實(shí)例的一個(gè)模板窍霞,下一步是查找具有使用控制器屬性的綁定的元素匠题。

    var bindings = {};
    
    // Note: element is the dom element using the controller
    Array.prototype.slice.call(element.querySelectorAll('[ng-bind]'))
        .map(function (element) {
            var boundValue = element.getAttribute('ng-bind');
    
            if(!bindings[boundValue]) {
                bindings[boundValue] = {
                    boundValue: boundValue,
                    elements: []
                }
            }
    
            bindings[boundValue].elements.push(element);
        });

上述中,它存儲(chǔ)對(duì)象的所有綁的值定但金。該變量包含要與當(dāng)前值綁定的所有屬性和綁定該屬性的所有 DOM 元素韭山。

代碼部署后可能存在的BUG沒(méi)法實(shí)時(shí)知道,事后為了解決這些BUG冷溃,花了大量的時(shí)間進(jìn)行l(wèi)og 調(diào)試钱磅,這邊順便給大家推薦一個(gè)好用的BUG監(jiān)控工具 Fundebug

雙向綁定

在框架完成了初步工作之后似枕,接下就是有趣的部分:雙向綁定盖淡。它涉及到將 controller 屬性綁定到 DOM 元素,以便在代碼更新屬性值時(shí)更新 DOM凿歼。

另外禁舷,不要忘記將 DOM 元素綁定到 controller 屬性。這樣毅往,當(dāng)用戶更改輸入值時(shí)牵咙,它將更新 controller 屬性嵌洼,接著顿膨,它還將更新綁定到此屬性的所有其他元素岖赋。

使用代理檢測(cè)代碼的更新

如上所述熙揍,Vue3 組件中通過(guò)封裝 proxy 監(jiān)聽(tīng)響應(yīng)屬性更改埋嵌。 這里僅為控制器添加代理來(lái)做同樣的事情彤灶。

// Note: ctrl is the controller instance
var proxy = new Proxy(ctrl, {
    set: function (target, prop, value) {
        var bind = bindings[prop];
        if(bind) {
            // Update each DOM element bound to the property  
            bind.elements.forEach(function (element) {
                element.value = value;
                element.setAttribute('value', value);
            });
        }
        return Reflect.set(target, prop, value);
    }
});

每當(dāng)設(shè)置綁定屬性時(shí)康吵,代理將檢查綁定到該屬性的所有元素孙乖,然后用新值更新它們戒幔。

在本例中吠谢,我們只支持 input 元素綁定,因?yàn)橹辉O(shè)置了 value 屬性诗茎。

響應(yīng)事件

最后要做的是響應(yīng)用戶交互工坊,DOM 元素在檢測(cè)到值更改時(shí)觸發(fā)事件。

監(jiān)聽(tīng)這些事件并使用事件的新值更新綁定屬性敢订,由于代理王污,綁定到相同屬性的所有其他元素將自動(dòng)更新。

Object.keys(bindings).forEach(function (boundValue) {
  var bind = bindings[boundValue];
  
  // Listen elements event and update proxy property   
  bind.elements.forEach(function (element) {
    element.addEventListener('input', function (event) {
      proxy[bind.boundValue] = event.target.value; // Also triggers the proxy setter
    });
  })  
});

React && Virtual DOM

接著將學(xué)習(xí)了解決如何使用單 個(gè)HTML 文件運(yùn)行 React楚午,解釋這些概念:functional component昭齐,函數(shù)組件, JSX 和 Virtual DOM矾柜。

React 提供了用組件構(gòu)建代碼的方法阱驾,收下就谜,創(chuàng)建 watch 組 件。

<!-- Skipping all HTML5 boilerplate -->
<script src="https://unpkg.com/react@16.2.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.2.0/umd/react-dom.development.js"></script>

<!-- For JSX support (with babel) -->
<script src="https://unpkg.com/babel-standalone@6.24.2/babel.min.js" charset="utf-8"></script> 

<div id="app"></div> <!-- React mounting point-->

<script type="text/babel">
  class Watch extends React.Component {
    render() {
      return <div>{this.props.hours}:{this.props.minutes}</div>;
    }
  }

  ReactDOM.render(<Watch hours="9" minutes="15"/>, document.getElementById('app'));
</script>

忽略依賴項(xiàng)的 HTML 樣板和腳本里覆,剩下的幾行就是 React 代碼丧荐。首先,定義 Watch 組件及其模板租谈,然后掛載React 到 DOM中篮奄,來(lái)渲染 Watch 組件。

向組件中注入數(shù)據(jù)

我們的 Wacth 組件很簡(jiǎn)單 割去,它只展示我們傳給它的時(shí)和分鐘窟却。

你可以嘗試修改這些屬性的值(在 React中稱為 props )。它將最終顯示你傳給它的內(nèi)容呻逆,即使它不是數(shù)字夸赫。

const Watch = (props) =>
  <div>{props.hours}:{props.minutes}</div>;

ReactDOM.render(<Watch hours="Hello" minutes="World"/>, document.getElementById('app'));

props 只是通過(guò)周?chē)M件傳遞給組件的數(shù)據(jù),組件使用 props 進(jìn)行業(yè)務(wù)邏輯和呈現(xiàn)咖城。

但是一旦 props 不屬于組件茬腿,它們就是不可變的(immutable)。因此宜雀,提供 props 的組件是能夠更新props 值的唯一代碼切平。

使用 props 非常簡(jiǎn)單,使用組件名稱作為標(biāo)記名稱創(chuàng)建 DOM 節(jié)點(diǎn)。 然后給它以 props 名的屬性,接著通過(guò)組件中的 this.props 可以獲得傳入的值辐董。

那些不帶引號(hào)的 HTML 呢悴品?

注意到 render 函數(shù)返回的不帶引號(hào)的 HTML, 這個(gè)使用是 JSX 語(yǔ)法简烘,它是在 React 組件中定義 HTML 模板的簡(jiǎn)寫(xiě)語(yǔ)法苔严。

// Equivalent to JSX: <Watch hours="9" minutes="15"/>
React.createElement(Watch, {'hours': '9', 'minutes': '15'});

現(xiàn)在你可能希望避免使用 JSX 來(lái)定義組件的模板,實(shí)際上孤澎,JSX 看起來(lái)像 語(yǔ)法糖届氢。

以下代碼片段,分別使用 JSX 和 React 語(yǔ)法以構(gòu)建相同結(jié)果覆旭。

// Using JS with React.createElement
React.createElement('form', null, 
  React.createElement('div', {'className': 'form-group'},
    React.createElement('label', {'htmlFor': 'email'}, 'Email address'),
    React.createElement('input', {'type': 'email', 'id': 'email', 'className': 'form-control'}),
  ),
  React.createElement('button', {'type': 'submit', 'className': 'btn btn-primary'}, 'Submit')
)

// Using JSX
<form>
  <div className="form-group">
    <label htmlFor="email">Email address</label>
    <input type="email" id="email" className="form-control"/>
  </div>
  <button type="submit" className="btn btn-primary">Submit</button>
</form>

進(jìn)一步探索虛擬 DOM

最后一部分比較復(fù)雜退子,但是很有趣,這將幫助你了解 React 底層的原理姐扮。

更新頁(yè)面上的元素 (DOM樹(shù)中的節(jié)點(diǎn)) 涉及到使用 DOM API絮供。它將重新繪制頁(yè)面,但可能很慢(請(qǐng)參閱本文了解原因)茶敏。

許多框架,如 React 和 Vue.js 繞過(guò)了這個(gè)問(wèn)題缚俏,它們提出了一個(gè)名為虛擬 DOM 的解決方案惊搏。

{
   "type":"div",
   "props":{ "className":"form-group" },
   "children":[
     {
       "type":"label",
       "props":{ "htmlFor":"email" },
       "children":[ "Email address"]
     },
     {
       "type":"input",
       "props":{ "type":"email", "id":"email", "className":"form-control"},
       "children":[]
     }
  ]
}

想法很簡(jiǎn)單贮乳。讀取和更新 DOM 樹(shù)非常昂貴。因此恬惯,盡可能少地進(jìn)行更改并更新盡可能少的節(jié)點(diǎn)向拆。

減少對(duì) DOM API 的調(diào)用及將 DOM 樹(shù)結(jié)構(gòu)保存在內(nèi)存中, 由于討論的是 JavaScript 框架酪耳,因此選擇JSON 數(shù)據(jù)結(jié)構(gòu)比較合理浓恳。

這種處理方式會(huì)立即展示了虛擬 DOM 中的變化。

此外虛擬 DOM 會(huì)先緩存一些更新操作碗暗,以便稍后在真正 DOM 上渲染颈将,這個(gè)樣是為了頻繁操作重新渲染造成一些性能問(wèn)題。

你還記得 React.createElement 嗎言疗? 實(shí)際上晴圾,這個(gè)函數(shù)作用是 (直接調(diào)用或通過(guò) JSX 調(diào)用) 在 Virtual DOM 中 創(chuàng)建一個(gè)新節(jié)點(diǎn)。

要應(yīng)用更新噪奄,Virtual DOM核心功能將發(fā)揮作用死姚,即 協(xié)調(diào)算法,它的工作是提供最優(yōu)的解決方案來(lái)解決以前和當(dāng)前虛擬DOM 狀態(tài)之間的差異勤篮。

原文:

A quick guide to learn React and how its Virtual DOM works

How to Improve Your JavaScript Skills by Writing Your Own Web Development Framework

關(guān)于Fundebug

Fundebug專注于JavaScript都毒、微信小程序、微信小游戲碰缔、支付寶小程序账劲、React Native、Node.js和Java線上應(yīng)用實(shí)時(shí)BUG監(jiān)控手负。 自從2016年雙十一正式上線涤垫,F(xiàn)undebug累計(jì)處理了9億+錯(cuò)誤事件,付費(fèi)客戶有Google竟终、360蝠猬、金山軟件、百姓網(wǎng)等眾多品牌企業(yè)统捶。歡迎大家免費(fèi)試用榆芦!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市喘鸟,隨后出現(xiàn)的幾起案子匆绣,更是在濱河造成了極大的恐慌,老刑警劉巖什黑,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件崎淳,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡愕把,警方通過(guò)查閱死者的電腦和手機(jī)拣凹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)森爽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人嚣镜,你說(shuō)我怎么就攤上這事爬迟。” “怎么了菊匿?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵付呕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我跌捆,道長(zhǎng)徽职,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任疹蛉,我火速辦了婚禮活箕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘可款。我一直安慰自己育韩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布闺鲸。 她就那樣靜靜地躺著筋讨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪摸恍。 梳的紋絲不亂的頭發(fā)上悉罕,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音立镶,去河邊找鬼壁袄。 笑死,一個(gè)胖子當(dāng)著我的面吹牛媚媒,可吹牛的內(nèi)容都是我干的嗜逻。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼缭召,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼栈顷!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起嵌巷,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤萄凤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后搪哪,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體靡努,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了颤难。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片神年。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡已维,死狀恐怖行嗤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垛耳,我是刑警寧澤栅屏,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站堂鲜,受9級(jí)特大地震影響栈雳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜缔莲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一哥纫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧痴奏,春花似錦蛀骇、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至檐晕,卻和暖如春暑诸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辟灰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工个榕, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人芥喇。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓西采,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親乃坤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子苛让,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

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

  • 本筆記基于React官方文檔,當(dāng)前React版本號(hào)為15.4.0湿诊。 1. 安裝 1.1 嘗試 開(kāi)始之前可以先去co...
    Awey閱讀 7,709評(píng)論 14 128
  • 作為一個(gè)合格的開(kāi)發(fā)者狱杰,不要只滿足于編寫(xiě)了可以運(yùn)行的代碼。而要了解代碼背后的工作原理厅须;不要只滿足于自己的程序...
    六個(gè)周閱讀 8,449評(píng)論 1 33
  • 40仿畸、React 什么是React?React 是一個(gè)用于構(gòu)建用戶界面的框架(采用的是MVC模式):集中處理VIE...
    萌妹撒閱讀 1,017評(píng)論 0 1
  • 原教程內(nèi)容詳見(jiàn)精益 React 學(xué)習(xí)指南,這只是我在學(xué)習(xí)過(guò)程中的一些閱讀筆記错沽,個(gè)人覺(jué)得該教程講解深入淺出簿晓,比目前大...
    leonaxiong閱讀 2,839評(píng)論 1 18
  • PC瀏覽器上的tab 標(biāo)簽里的圖標(biāo)的展示問(wèn)題。 優(yōu)化:小圖盡量使用精靈圖千埃。北京大圖如果能使用.jpg,就不使用.p...
    戒惜舍得閱讀 151評(píng)論 0 0