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、金山软件、百姓网等众多品牌企业。欢迎你们免费试用

相关文章
相关标签/搜索