JavaScript是如何工作的:編寫自己的Web開發框架 + React及其虛擬DOM原理

摘要: 深入JS系列19。

Fundebug經授權轉載,版權歸原作者所有。

這是專門探索 JavaScript 及其所構建的組件的系列文章的第 19 篇。

如果你錯過了前面的章節,可以在這里找到它們:

響應式原理

Proxy 允許我們創建一個對象的虛擬代理(替代對象),并為我們提供了在訪問或修改原始對象時,可以進行攔截的處理方法(handler),如 set()、get() 和 deleteProperty() 等等,這樣我們就可以避免很常見的這兩種限制(vue 中):

  • 添加新的響應性屬性要使用 Vue.$set(),刪除現有的響應性屬性要使用
  • 數組的更新檢測

Proxy

let proxy = new Proxy(target, habdler);
  • target:用 Proxy 包裝的目標對象(可以是數組對象,函數,或者另一個代理)
  • handler:一個對象,攔截過濾代理操作的函數

實例方法

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

Reflect

Reflect 是一個內置的對象,它提供攔截 JavaScript 操作的方法。這些方法與處理器對象的方法相同。Reflect不是一個函數對象,因此它是不可構造的。

與大多數全局對象不同,Reflect沒有構造函數。你不能將其與一個new運算符一起使用,或者將Reflect對象作為一個函數來調用。Reflect的所有屬性和方法都是靜態的(就像Math對象)。

為什么要設計 Reflect ?

1. 更加有用的返回值

早期寫法:

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

Reflect 寫法:

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

2. 函數式操作

早期寫法:

'name' in Object //true

Reflect 寫法:

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

3. 可變參數形式的構造函數

一般寫法:

var obj = new F(...args)

Reflect 寫法:

var obj = Reflect.construct(F, args)

當然還有很多,大家可以自行到 MND 上查看

什么是代理設計模式

代理模式(Proxy),為其他對象提供一種代理以控制對這個對象的訪問。代理模式使得代理對象控制具體對象的引用。代理幾乎可以是任何對象:文件,資源,內存中的對象,或者是一些難以復制的東西。現實生活中的一個類比可能是銀行賬戶的訪問權限。

例如,你不能直接訪問銀行帳戶余額并根據需要更改值,你必需向擁有此權限的人(在本例中 你存錢的銀行)詢問。

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 

在上面的示例中,當使用 bank 對象訪問 account 余額時,getter 函數被重寫,它總是返回 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

通過重寫 set 函數,可以修改其行為。可以更改要設置的值,更改其他屬性,甚至根本不執行任何操作。

響應式

現在已經對代理設計模式的工作方式有了基本心,讓就開始編寫 JavaScript 框架吧。

為了簡單起見,將模擬 AngularJS 語法。聲明控制器并將模板元素綁定到控制器屬性:

<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>

首先,定義一個帶有屬性的控制器,然后在模板中使用這個控制器。最后,使用 ng-bind 屬性啟用與元素值的雙向綁定。

解析模板并實例化控制器

要使屬性綁定,需要獲得一個控制器來聲明這些屬性, 因此,有必要定義一個控制器并將其引入框架中。

在控制器聲明期間,框架將查找帶有 ng-controller 屬性的元素。

如果它符合其中一個已聲明的控制器,它將創建該控制器的新實例,這個控制器實例只負責這個特定的模板。

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);

這是手動處理的控制器變量聲明。 controllers 對象包含通過調用 addController 在框架內聲明的所有控制器。

對于每個控制器,保存一個 factory 函數,以便在需要時實例化一個新控制器,該框架還存儲模板中使用的相同控制器的每個新實例。

查找 bind 屬性

現在,已經有了控制器的一個實例和使用這個實例的一個模板,下一步是查找具有使用控制器屬性的綁定的元素。

    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);
        });

上述中,它存儲對象的所有綁的值定。該變量包含要與當前值綁定的所有屬性和綁定該屬性的所有 DOM 元素。

代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

雙向綁定

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

另外,不要忘記將 DOM 元素綁定到 controller 屬性。這樣,當用戶更改輸入值時,它將更新 controller 屬性,接著,它還將更新綁定到此屬性的所有其他元素。

使用代理檢測代碼的更新

如上所述,Vue3 組件中通過封裝 proxy 監聽響應屬性更改。 這里僅為控制器添加代理來做同樣的事情。

// 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);
    }
});

每當設置綁定屬性時,代理將檢查綁定到該屬性的所有元素,然后用新值更新它們。

在本例中,我們只支持 input 元素綁定,因為只設置了 value 屬性。

響應事件

最后要做的是響應用戶交互,DOM 元素在檢測到值更改時觸發事件。

監聽這些事件并使用事件的新值更新綁定屬性,由于代理,綁定到相同屬性的所有其他元素將自動更新。

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

接著將學習了解決如何使用單 個HTML 文件運行 React,解釋這些概念:functional component,函數組件, JSX 和 Virtual DOM。

React 提供了用組件構建代碼的方法,收下,創建 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>

忽略依賴項的 HTML 樣板和腳本,剩下的幾行就是 React 代碼。首先,定義 Watch 組件及其模板,然后掛載React 到 DOM中,來渲染 Watch 組件。

向組件中注入數據

我們的 Wacth 組件很簡單 ,它只展示我們傳給它的時和分鐘。

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

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

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

props 只是通過周圍組件傳遞給組件的數據,組件使用 props 進行業務邏輯和呈現。

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

使用 props 非常簡單,使用組件名稱作為標記名稱創建 DOM 節點。 然后給它以 props 名的屬性,接著通過組件中的 this.props 可以獲得傳入的值。

那些不帶引號的 HTML 呢?

注意到 render 函數返回的不帶引號的 HTML, 這個使用是 JSX 語法,它是在 React 組件中定義 HTML 模板的簡寫語法。

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

現在你可能希望避免使用 JSX 來定義組件的模板,實際上,JSX 看起來像 語法糖

以下代碼片段,分別使用 JSX 和 React 語法以構建相同結果。

// 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>

進一步探索虛擬 DOM

最后一部分比較復雜,但是很有趣,這將幫助你了解 React 底層的原理。

更新頁面上的元素 (DOM樹中的節點) 涉及到使用 DOM API。它將重新繪制頁面,但可能很慢(請參閱本文了解原因)。

許多框架,如 React 和 Vue.js 繞過了這個問題,它們提出了一個名為虛擬 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":[]
     }
  ]
}

想法很簡單。讀取和更新 DOM 樹非常昂貴。因此,盡可能少地進行更改并更新盡可能少的節點。

減少對 DOM API 的調用及將 DOM 樹結構保存在內存中, 由于討論的是 JavaScript 框架,因此選擇JSON 數據結構比較合理。

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

此外虛擬 DOM 會先緩存一些更新操作,以便稍后在真正 DOM 上渲染,這個樣是為了頻繁操作重新渲染造成一些性能問題。

你還記得 React.createElement 嗎? 實際上,這個函數作用是 (直接調用或通過 JSX 調用) 在 Virtual DOM 中 創建一個新節點。

要應用更新,Virtual DOM核心功能將發揮作用,即 協調算法,它的工作是提供最優的解決方案來解決以前和當前虛擬DOM 狀態之間的差異。

原文:

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

關于Fundebug

Fundebug專注于JavaScript、微信小程序、微信小游戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了9億+錯誤事件,付費客戶有Google、360、金山軟件、百姓網等眾多品牌企業。歡迎大家免費試用

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容

  • 本筆記基于React官方文檔,當前React版本號為15.4.0。 1. 安裝 1.1 嘗試 開始之前可以先去co...
    Awey閱讀 7,757評論 14 128
  • 作為一個合格的開發者,不要只滿足于編寫了可以運行的代碼。而要了解代碼背后的工作原理;不要只滿足于自己的程序...
    六個周閱讀 8,466評論 1 33
  • 40、React 什么是React?React 是一個用于構建用戶界面的框架(采用的是MVC模式):集中處理VIE...
    萌妹撒閱讀 1,030評論 0 1
  • 原教程內容詳見精益 React 學習指南,這只是我在學習過程中的一些閱讀筆記,個人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,849評論 1 18
  • PC瀏覽器上的tab 標簽里的圖標的展示問題。 優化:小圖盡量使用精靈圖。北京大圖如果能使用.jpg,就不使用.p...
    戒惜舍得閱讀 154評論 0 0