JavaScript 是如何工作的:編寫自己的 Web 開發框架 + React 及其虛擬 DOM 原理
這是專門探索 JavaScript 及其所構建的元件的系列文章的第 19 篇。
如果你錯過了前面的章節,可以在這裡找到它們:
- JavaScript 是如何工作的:引擎,執行時和呼叫堆疊的概述!
- JavaScript 是如何工作的:深入V8引擎&編寫優化程式碼的5個技巧!
- JavaScript 是如何工作的:記憶體管理+如何處理4個常見的記憶體洩漏!
- JavaScript 是如何工作的:事件迴圈和非同步程式設計的崛起+ 5種使用 async/await 更好地編碼方式!
- JavaScript 是如何工作的:深入探索 websocket 和HTTP/2與SSE +如何選擇正確的路徑!
- JavaScript 是如何工作的:與 WebAssembly比較 及其使用場景!
- JavaScript 是如何工作的:Web Workers的構建塊+ 5個使用他們的場景!
- JavaScript 是如何工作的:Service Worker 的生命週期及使用場景!
- JavaScript 是如何工作的:Web 推送通知的機制!
- JavaScript 是如何工作的:使用 MutationObserver 跟蹤 DOM 的變化!
- JavaScript 是如何工作的:渲染引擎和優化其效能的技巧!
- JavaScript 是如何工作的:深入網路層 + 如何優化效能和安全!
- JavaScript 是如何工作的:CSS 和 JS 動畫底層原理及如何優化它們的效能!
- JavaScript 是如何工作的:解析、抽象語法樹(AST)+ 提升編譯速度5個技巧!
- JavaScript 是如何工作的:深入類和繼承內部原理+Babel和 TypeScript 之間轉換!
- JavaScript 是如何工作的:儲存引擎+如何選擇合適的儲存API!
- JavaScript 是如何工作的:Shadow DOM 的內部結構+如何編寫獨立的元件!
- JavaScript 是如何工作的:WebRTC 和對等網路的機制!
響應式原理
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 元素。
雙向繫結
在框架完成了初步工作之後,接下就是有趣的部分: 雙向繫結 。它涉及到將 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/[email protected]/umd/react.development.js"></script> <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script> <!-- For JSX support (with babel) --> <script src="https://unpkg.com/[email protected]/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 狀態之間的差異。
原文:
https://medium.freecodecamp.o...
https://medium.freecodecamp.o...