JavaScript是如何工做的:編寫本身的Web開發框架 + React及其虛擬DOM原理

摘要: 深刻JS系列19。javascript

Fundebug經受權轉載,版權歸原做者全部。html

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

若是你錯過了前面的章節,能夠在這裏找到它們:vue

響應式原理

Proxy 容許咱們建立一個對象的虛擬代理(替代對象),併爲咱們提供了在訪問或修改原始對象時,能夠進行攔截的處理方法(handler),如 set()、get() 和 deleteProperty() 等等,這樣咱們就能夠避免很常見的這兩種限制(vue 中):java

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

Proxy

let proxy = new Proxy(target, habdler);
複製代碼
  • target:用 Proxy 包裝的目標對象(能夠是數組對象,函數,或者另外一個代理)
  • handler:一個對象,攔截過濾代理操做的函數

實例方法node

方法 描述
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不是一個函數對象,所以它是不可構造的。react

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

爲何要設計 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、金山軟件、百姓網等衆多品牌企業。歡迎你們免費試用

相關文章
相關標籤/搜索