这是专门探索 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(),删除现有的响应性属性要使用数组的更新检测Proxylet 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 实例 ReflectReflect 是一个内置的对象,它提供拦截 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 //trueReflect 写法:Reflect.has(Object,’name’) //true3. 可变参数形式的构造函数 一般写法: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,800bank.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 instancevar 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.createElementReact.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…代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》!