资讯专栏INFORMATION COLUMN

JavaScript 是如何工作的:编写自己的 Web 开发框架 + React 及其虚拟 DOM

余学文 / 2310人阅读

摘要:与大多数全局对象不同,没有构造函数。为什么要设计更加有用的返回值早期写法写法函数式操作早期写法写法可变参数形式的构造函数一般写法写法当然还有很多,大家可以自行到上查看什么是代理设计模式代理模式,为其他对象提供一种代理以控制对这个对象的访问。

这是专门探索 JavaScript 及其所构建的组件的系列文章的第 19 篇。

如果你错过了前面的章节,可以在这里找到它们:

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

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 语法。声明控制器并将模板元素绑定到控制器属性:

首先,定义一个带有属性的控制器,然后在模板中使用这个控制器。最后,使用 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 组 件。






 

忽略依赖项的 HTML 样板和脚本,剩下的几行就是 React 代码。首先,定义 Watch 组件及其模板,然后挂载React 到 DOM中,来渲染 Watch 组件。

向组件中注入数据

我们的 Wacth 组件很简单 ,它只展示我们传给它的时和分钟。

你可以尝试修改这些属性的值(在 React中称为 props )。它将最终显示你传给它的内容,即使它不是数字。

const Watch = (props) =>
  
{props.hours}:{props.minutes}
; ReactDOM.render(, document.getElementById("app"));

props 只是通过周围组件传递给组件的数据,组件使用 props 进行业务逻辑和呈现。

但是一旦 props 不属于组件,它们就是不可变的(immutable)。因此,提供 props 的组件是能够更新props 值的唯一代码。

使用 props 非常简单,使用组件名称作为标记名称创建 DOM 节点。 然后给它以 props 名的属性,接着通过组件中的 this.props 可以获得传入的值。

那些不带引号的 HTML 呢?

注意到 render 函数返回的不带引号的 HTML, 这个使用是 JSX 语法,它是在 React 组件中定义 HTML 模板的简写语法。

// Equivalent to JSX: 
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
进一步探索虚拟 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。

你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108948.html

相关文章

  • JavaScript 如何工作系列文章已更新到22篇

    摘要:为了方便大家共同学习,整理了之前博客系列的文章,目前已整理是如何工作这个系列,可以请猛戳博客查看。以下列出该系列目录,欢迎点个星星,我将更友动力整理理优质的文章,一起学习。 为了方便大家共同学习,整理了之前博客系列的文章,目前已整理 JavaScript 是如何工作这个系列,可以请猛戳GitHub博客查看。 以下列出该系列目录,欢迎点个星星,我将更友动力整理理优质的文章,一起学习。 J...

    lx1036 评论0 收藏0
  • 【全栈React】第1天: 什么 React?

    摘要:本文转载自众成翻译译者链接原文今天,我们从一开始就开始。让我们看看是什么,是什么让运转起来。什么是是一个用于构建用户界面的库。它是应用程序的视图层。所有应用程序的核心是组件。组件是可组合的。虚拟完全存在于内存中,并且是网络浏览器的的表示。 本文转载自:众成翻译译者:iOSDevLog链接:http://www.zcfy.cc/article/3765原文:https://www.ful...

    ralap 评论0 收藏0
  • 一名【合格】前端工程师自检清单

    摘要:在他的重学前端课程中提到到现在为止,前端工程师已经成为研发体系中的重要岗位之一。大部分前端工程师的知识,其实都是来自于实践和工作中零散的学习。一基础前端工程师吃饭的家伙,深度广度一样都不能差。 开篇 前端开发是一个非常特殊的行业,它的历史实际上不是很长,但是知识之繁杂,技术迭代速度之快是其他技术所不能比拟的。 winter在他的《重学前端》课程中提到: 到现在为止,前端工程师已经成为研...

    罗志环 评论0 收藏0
  • 一名【合格】前端工程师自检清单

    摘要:在他的重学前端课程中提到到现在为止,前端工程师已经成为研发体系中的重要岗位之一。大部分前端工程师的知识,其实都是来自于实践和工作中零散的学习。一基础前端工程师吃饭的家伙,深度广度一样都不能差。开篇 前端开发是一个非常特殊的行业,它的历史实际上不是很长,但是知识之繁杂,技术迭代速度之快是其他技术所不能比拟的。 winter在他的《重学前端》课程中提到: 到现在为止,前端工程师已经成为研发体系...

    isaced 评论0 收藏0
  • 你要 React 面试知识点,都在这了

    摘要:是流行的框架之一,在年及以后将会更加流行。于年首次发布,多年来广受欢迎。下面是另一个名为的高阶函数示例,该函数接受另外两个函数,分别是和。将所有较小的函数组合成更大的函数,最终,得到一个应用程序,这称为组合。 React是流行的javascript框架之一,在2019年及以后将会更加流行。React于2013年首次发布,多年来广受欢迎。它是一个声明性的、基于组件的、用于构建用户界面的高...

    klinson 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<