乐趣区

关于javascript:不需要框架的客户端JavaScript数据绑定

最近我始终在思考纯 JavaScript 的性能。这是一门在过来几年里有显著倒退的语言。许多风行的库(如模块加载器)和框架(如 Angular,Vue.js 和 React)被创立,以解决原始的、过期的实现中存在的缺点和差距。随着 ECMAScript 6 / 2015,我置信这些限度大部分曾经隐没了。许多重要的性能都是开箱即用的,例如:

  • 反对模块和动静加载。
  • 拦挡和治理 route 的能力
  • 一个内置的 DOM 查问机制,防止了对 jQuery 的需要。
  • 本地模板反对
  • 可重复使用网络组件

最新的 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,抛出一个异样,因为它是只读的(计算的)。这里是它的应用状况。

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); 

其余值也能够反复这样做。

提醒: 当然,这只是一个简略的例子。如果你心愿应用数字输出,你可能须要转换数值,并为单选列表等元素编写不同的处理程序,但个别的概念是一样的。

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

申明式数据绑定

咱们的指标是防止通过元素的 id 来加载元素,而是简略地将它们间接绑定到观测值上。我为这个工作抉择了一个描述性的属性,并将其称为data-bind。我用一个指向某个上下文上的属性的值来申明这个属性,所以它看起来是这样的。

<label for="firstName">
  <div>First Name:</div><input type="text" data-bind="first" id="firstName" />
</label>

为了把事件连接起来,我能够重用现有的 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 等框架中取得的益处。然而,重要的是要跟上最新的原生停顿,不要把框架看成能够解决所有问题的 “ 黄金工具 ”。依附框架意味着通过设置、配置和保护来裸露本人的开销,冒着安全漏洞的危险,而且在很多状况下,还要部署大型的有效载荷。你必须雇佣相熟该框架细微差别的人才,或者对他们进行培训,并跟上更新的步调。理解原生代码可能只是为你节俭了一个构建过程,并实现了在古代浏览器中 “ 只是工作 “ 的场景,而不须要大量的代码。

判若两人,我欢送你的反馈、想法、评论和问题。

致敬。

系列的一部分。Vanilla.js
过来我曾写过对于古代 Web 开发的 “ 三个 D “ 的文章。

古代网络开发的三个 D

通过学习依赖注入、申明式语法和数据绑定,理解古代 JavaScript 框架(如 Angular、React 和 Vue)的历史和合成。

退出移动版