settimeout需要清除吗_不需要框架的客户端JavaScript数据绑定

最近我一直在思考纯JavaScript的功能。这是一门在过去几年里有显著发展的语言。许多流行的库(如模块加载器)和框架(如Angular,Vue.js和React)被创建,以解决原始的、过时的实现中存在的缺陷和差距。随着ECMAScript 6 / 2015,我相信这些限制大部分已经消失了。许多重要的功能都是开箱即用的,例如:

  • 支持模块和动态加载。
  • 拦截和管理route的能力
  • 一个内置的DOM查询机制,避免了对jQuery的需求。
  • 本地模板支持
  • 可重复使用网络组件
2d1f74b96cf99970dd770934c4309b6c.png

最新的JavaScript版本没有完全支持的一个功能是_databinding_。但是实现它有多难呢?如果你使用重型框架的唯一动机是支持数据绑定,你可能会感到惊讶!

观察变化

首先需要的是观察变化的能力。这很容易通过一个Observable类来实现。这个类需要做三件事。

  1. 跟踪一个值
  2. 允许听众订阅更改
  3. 当值发生变化时通知监听者

下面是一个简单的实现。

class Observable {  constructor(value) {    this._listeners = [];    this._value = value;  }  notify() {    this._listeners.forEach(listener => listener(this._value));  }  subscribe(listener) {    this._listeners.push(listener);  }  get value() {    return this._value;  }  set value(val) {    if (val !== this._value) {      this._value = val;      this.notify();    }  }} 

这个简单的类,利用内置的类suport(不需要TypeScript!)很好地处理了一切。这里是我们的新类的一个使用实例,它创建了一个可观察的类,监听变化,并将其记录到控制台。

const name = new Observable("Jeremy");name.subscribe((newVal) => console.log(`Name changed to ${newVal}`));name.value = "Doreen";// logs "Name changed to Doreen" to the console 

这很容易,但计算值呢?例如,你可能有一个依赖于多个输入的输出属性。让我们假设我们需要跟踪名和姓,这样我们就可以暴露一个全名的属性。那是如何工作的呢?

计算值("可观察链")

事实证明,利用JavaScript对继承的支持,我们可以扩展Observable类来处理计算值。这个类需要做一些额外的工作。

  1. 跟踪计算新属性的函数。
  2. 理解依赖性,即观察到的属性,计算出的属性所依赖的。
  3. 订阅依赖关系的变化,以便对计算的属性进行重新评估。

这个类实现起来比较容易。

class Computed extends Observable {  constructor(value, deps) {    super(value());    const listener = () => {      this._value = value();      this.notify();    }    deps.forEach(dep => dep.subscribe(listener));  }  get value() {    return this._value;  }  set value(_) {    throw "Cannot set computed property";  }} 

它接收函数和依赖关系,并将初始值作为种子。它监听依赖关系的变化并重新评估计算值。最后,它覆盖了setter,抛出一个异常,因为它是只读的(计算的)。这里是它的使用情况。

edd2a8cb5918c9b6c8a150cab9250550.png
const first = new Observable("Jeremy");const last = new Observable("Likness");const full = new Computed(() => `${first.value} ${last.value}`.trim(), [first, last]);first.value = "Doreen";console.log(full.value);// logs "Doreen Likness" to the console 

现在我们可以跟踪我们的数据,但是HTML DOM呢?

双向数据绑定

对于双向数据绑定,我们需要用观察到的值来初始化一个DOM属性,并在该值变化时更新它。我们还需要检测DOM更新的时间,以便将新的值传递给数据。使用内置的DOM事件,这就是设置输入元素的双向数据绑定的代码样子。

const bindValue = (input, observable) => {    input.value = observable.value;    observable.subscribe(() => input.value = observable.value);    input.onkeyup = () => observable.value = input.value;} 

看起来并不难,是吗?假设我有一个输入元素,其id属性设置为first,我可以这样接线。

const first = new Observable("Jeremy");const firstInp = document.getElementById("first");bindValue(firstInp, first); 

其他值也可以重复这样做。

提示: 当然,这只是一个简单的例子。如果你希望使用数字输入,你可能需要转换数值,并为单选列表等元素编写不同的处理程序,但一般的概念是一样的。

如果我们能尽量减少代码绑定和声明式数据绑定就更好了。让我们来探讨一下这个问题。

dfb97b37bfd85071ad3103b5874a2a29.png

声明式数据绑定

我们的目标是避免通过元素的id来加载元素,而是简单地将它们直接绑定到观测值上。我为这个任务选择了一个描述性的属性,并将其称为data-bind。我用一个指向某个上下文上的属性的值来声明这个属性,所以它看起来是这样的。

  
First Name:

为了把事情连接起来,我可以重用现有的dataBind实现。首先,我设置了一个要绑定的上下文。然后,我配置上下文并应用绑定。

const bindings = {};const app = () => {  bindings.first = new Observable("Jeremy");  bindings.last = new Observable("");  bindings.full = new Computed(() =>       `${bindings.first.value} ${bindings.last.value}`.trim(),       [bindings.first, bindings.last]);  applyBindings();};setTimeout(app, 0); 

setTimeout给出了初始渲染周期的完成时间。现在我实现了解析声明和绑定声明的代码。

const applyBindings = () => {    document.querySelectorAll("[data-bind]").forEach(elem => {      const obs = bindings[elem.getAttribute("data-bind")];    bindValue(elem, obs);  });} 

这段代码抓取每个带有data-bind属性的标签,将其作为索引来引用上下文中的可观察项,然后调用dataBind操作。

附注:Eval 上下文

数据绑定并不总是像指向一个观测值的名称那么简单。在许多情况下,你可能想要Eval一个表达式。如果你能约束上下文,使表达式不影响其他表达式或执行不安全的操作,那就更好了。这也是可能的。考虑一下表达式a+b。有几种方法可以 "在上下文中 "约束它。第一种,也是最不安全的,就是在特定上下文中使用eval。下面是示例代码。

const strToEval = "this.x = this.a + this.b";const context1 = { a: 1, b: 2 };const context2 = { a: 3, b: 5 };const showContext = ctx => console.log(`x=${ctx.x}, a=${ctx.a}, b=${ctx.b}`);const evalInContext = (str, ctx) => (function (js) { return eval(js); }).call(ctx, str);showContext(context1);// x=undefined, a=1, b=2 showContext(context2);// x=undefined, a=3, b=5 evalInContext(strToEval, context1);evalInContext(strToEval, context2);showContext(context1);// x=3, a=1, b=2 showContext(context2);// x=8, a=3, b=5 

这使得上下文可以改变,但有几个缺陷。使用 "this "的惯例很笨拙,而且有很多潜在的安全漏洞。只要添加一个window.location.hrefault语句,你就明白了。一个更安全的方法是只允许返回值的Eval,然后把它们包在一个动态函数中。下面的代码就可以做到这一点,而且没有导航的副作用。

const strToEval = "a + b; window.location.href='https://blog.jeremylikness.com/';";const context1 = { a: 1, b: 2 };const context2 = { a: 3, b: 5 };const evalInContext = (str, ctx) => (new Function(`with(this) { return ${str} }`)).call(ctx);console.log(evalInContext(strToEval, context1));// 3 console.log(evalInContext(strToEval, context2));// 8 

通过这个小技巧,你可以在特定的上下文中安全地评估表达式。

结束语

我并不反对框架。我已经构建了一些令人难以置信的大型企业Web应用,这些应用的成功很大程度上得益于我们从使用Angular等框架中获得的好处。然而,重要的是要跟上最新的原生进展,不要把框架看成可以解决所有问题的 "黄金工具"。依靠框架意味着通过设置、配置和维护来暴露自己的开销,冒着安全漏洞的风险,而且在很多情况下,还要部署大型的有效载荷。你必须雇佣熟悉该框架细微差别的人才,或者对他们进行培训,并跟上更新的步伐。了解原生代码可能只是为你节省了一个构建过程,并实现了在现代浏览器中 "只是工作 "的场景,而不需要大量的代码。

如果大家想从事前端不知道怎么入门的,可以在留言区评论“学习”我把我多年的经验分享给大家,还有一些学习资料(点赞+转发)

15c6d2e88b47ac36b40700c6e5060e93.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值