- 原文地址:How to build a reactive engine in JavaScript. Part 1: Observable objects
- 原文做者:本文已获原做者 Damian Dulisz 受权
- 译文出自:掘金翻译计划
- 译者:IridescentMia
- 校对者:reid3290,malcolmyu
随着对强健、可交互的网站界面的需求不断增多,不少开发者开始拥抱响应式编程规范。javascript
在开始实现咱们本身的响应式引擎以前,快速地解释一下到底什么是响应式编程。维基百科给出一个经典的响应式界面实现的例子 —— 叫作 spreadsheet。定义一个准则,对于 =A1+B1
,只要 A1
或 B1
发生变化,=A1+B1
也会随之变化。这样的准则也能够被理解为是一种 computed value。css
咱们将会在这系列教程的 Part 2 部分学习如何实现 computed value。在那以前,咱们首先须要对响应式引擎有个基础的了解。html
目前有不少不一样解决方案能够观察到应用状态的改变,并对其作出反应。前端
在这篇教程中,咱们将使用 getters/setters 的方式观察并响应变化。java
注意:为了让这篇教程尽可能保持简单,代码缺乏对非初级数据类型或嵌套属性的支持,而且不少内容须要完整性检查,所以毫不能认为这些代码已经能够用于生产环境。下面的代码是受 Vue.js 启发的响应式引擎的实现,使用 ES2015 标准编写。node
让咱们从一个 data
对象开始,咱们想要观察它的属性。react
let data = {
firstName: 'Jon',
lastName: 'Snow',
age: 25
}复制代码
首先从建立两个函数开始,使用 getter/setter 的功能,将对象的普通属性转换成可观察的属性。android
function makeReactive (obj, key) {
let val = obj[key]
Object.defineProperty(obj, key, {
get () {
return val // 简单地返回缓存的 value
},
set (newVal) {
val = newVal // 保存 newVal
notify(key) // 暂时忽略这里
}
})
}
// 循环迭代对象的 keys
function observeData (obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
makeReactive(obj, key)
}
}
}
observeData(data)复制代码
经过运行 observeData(data)
,将原始的对象转换成可被观察的对象;如今当对象的 value 发生变化时,咱们有建立通知的办法。ios
在咱们开始接收 notifying 前,咱们须要一些通知的内容。这里是使用观察者模式的一个极好例子。在这个案例中咱们将使用 signals 实现。git
咱们从 observe
函数开始。
let signals = {} // Signals 从一个空对象开始
function observe (property, signalHandler) {
if(!signals[property]) signals[property] = [] // 若是给定属性没在 signal 中,则建立这个属性的 signal,并将其设置为空数组来存储 signalHandlers
signals[property].push(signalHandler) // 将 signalHandler 存入 signal 数组,高效地得到一组保存在数组中的回调函数
}复制代码
咱们如今能够这样用 observe
函数:observe('propertyName', callback)
,每次属性值发生改变的时候 callback
函数应该被调用。当屡次在一个属性上调用 observe 时,每一个回调函数将被存在对应属性的 signal 数组中。这样就能够存储全部的回调函数而且能够很容易地得到到它们。
如今来看一下上文中提到的 notify
函数。
function notify (signal, newVal) {
if(!signals[signal] || signals[signal].length < 1) return // 若是没有 signal 的处理器则提早 return
signals[signal].forEach((signalHandler) => signalHandler()) // 调用给定属性的每一个 signalHandler
}复制代码
如你所见,如今每次一个属性发生变化,就会调用对其分配的 signalHandlers。
因此咱们把它所有封装起来作成一个工厂函数,传入想要响应的数据对象。我把它命名为 Seer
。咱们最终获得以下:
function Seer (dataObj) {
let signals = {}
observeData(dataObj)
// 除了响应式的数据对象,咱们也须要返回而且暴露出 observe 和 notify 函数。
return {
data: dataObj,
observe,
notify
}
function observe (property, signalHandler) {
if(!signals[property]) signals[property] = []
signals[property].push(signalHandler)
}
function notify (signal) {
if(!signals[signal] || signals[signal].length < 1) return
signals[signal].forEach((signalHandler) => signalHandler())
}
function makeReactive (obj, key) {
let val = obj[key]
Object.defineProperty(obj, key, {
get () {
return val
},
set (newVal) {
val = newVal
notify(key)
}
})
}
function observeData (obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
makeReactive(obj, key)
}
}
}
}复制代码
如今咱们须要作的就是建立一个新的可响应对象。多亏了暴露出来的 notify
和 observe
函数,咱们能够观察到并响应对象的改变。
const App = new Seer({
title: 'Game of Thrones',
firstName: 'Jon',
lastName: 'Snow',
age: 25
})
// 为了订阅并响应可响应 APP 对象的改变:
App.observe('firstName', () => console.log(App.data.firstName))
App.observe('lastName', () => console.log(App.data.lastName))
// 为了触发上面的回调函数,像下面这样简单地改变 values:
App.data.firstName = 'Sansa'
App.data.lastName = 'Stark'复制代码
很简单,是否是?如今咱们讲完了基本的响应式引擎,让咱们来用用它。
我提到过随着前端编程可响应式方法的增多,咱们不能总想着在发生改变后手动地更新 DOM。
有不少方法来完成这项任务。我猜如今最流行的趋势是用虚拟 DOM 的办法。若是你对学习如何建立你本身的虚拟 DOM 实现感兴趣,已经有不少这方面的教程。然而,这里咱们将用到更简单的方法。
HTML 看起来像这样: html<h1>Title comes here</h1>
响应式更新 DOM 的函数看起来像这样:
// 首先须要得到想要保持更新的节点。
const h1Node = document.querySelector('h1')
function syncNode (node, obj, property) {
// 用可见对象的属性值初始化 h1 的 textContent 值
node.textContent = obj[property]
// 开始用咱们的 Seer 的实例 App.observe 观察属性。
App.observe(property, value => node.textContent = obj[property] || '')
}
syncNode(h1Node, App.data, 'title')复制代码
这样作是可行的,可是使用它把全部数据模型绑定到 DOM 元素须要大量的工做。
这就是咱们为何要再向前迈一步,而后将全部这些自动化完成。
若是你熟悉 AngularJS 或者 Vue.js,你确定记得使用自定义属性 ng-bind
或 v-text
。咱们在这里建立相似的东西。
咱们的自定义属性叫作 s-text
。咱们将寻找在 DOM 和数据模型之间创建绑定的方式。
让咱们更新一下 HTML:
<!-- 'title' 是咱们想要在 <h1> 内显示的属性 -->
<h1 s-text="title">Title comes here</h1>
function parseDOM (node, observable) {
// 得到全部具备自定义属性 s-text 的节点
const nodes = document.querySelectorAll('[s-text]')
// 对于每一个存在的节点,咱们调用 syncNode 函数
nodes.forEach((node) => {
syncNode(node, observable, node.attributes['s-text'].value)
})
}
// 如今咱们须要作的就是在根节点 document.body 上调用它。全部的 `s-text` 节点将会自动的建立与之对应的响应式属性的绑定。
parseDOM(document.body, App.data)复制代码
如今咱们能够解析 DOM 而且将数据模型绑定到节点上,把这两个函数添加到 Seer 工厂函数中,这样就能够在初始化的时候解析 DOM。
结果应该像下面这样:
function Seer (dataObj) {
let signals = {}
observeData(dataObj)
return {
data: dataObj,
observe,
notify
}
function observe (property, signalHandler) {
if(!signals[property]) signals[property] = []
signals[property].push(signalHandler)
}
function notify (signal) {
if(!signals[signal] || signals[signal].length < 1) return
signals[signal].forEach((signalHandler) => signalHandler())
}
function makeReactive (obj, key) {
let val = obj[key]
Object.defineProperty(obj, key, {
get () {
return val
},
set (newVal) {
val = newVal
notify(key)
}
})
}
function observeData (obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
makeReactive(obj, key)
}
}
//转换数据对象后,能够安全地解析 DOM 绑定。
parseDOM(document.body, obj)
}
function syncNode (node, observable, property) {
node.textContent = observable[property]
// 移除了 `Seer.` 是由于 observe 函数在可得到的做用域范围以内。
observe(property, () => node.textContent = observable[property])
}
function parseDOM (node, observable) {
const nodes = document.querySelectorAll('[s-text]')
nodes.forEach((node) => {
syncNode(node, observable, node.attributes['s-text'].value)
})
}
}复制代码
JsFiddle 上的例子:
HTML
<h1 s-text="title"></h1>
<div class="form-inline">
<div class="form-group">
<label for="title">Title: </label>
<input
type="text"
class="form-control"
id="title" placeholder="Enter title"
oninput="updateText('title', event)">
</div>
<button class="btn btn-default" type="button" onclick="resetTitle()">Reset title</button>
</div>复制代码
JS
// 代码用了 ES2015,使用兼容的浏览器才能够哦,好比 Chrome,Opera,Firefox
function Seer (dataObj) {
let signals = {}
observeData(dataObj)
return {
data: dataObj,
observe,
notify
}
function observe (property, signalHandler) {
if(!signals[property]) signals[property] = []
signals[property].push(signalHandler)
}
function notify (signal) {
if(!signals[signal] || signals[signal].length < 1) return
signals[signal].forEach((signalHandler) => signalHandler())
}
function makeReactive (obj, key) {
let val = obj[key]
Object.defineProperty(obj, key, {
get () {
return val
},
set (newVal) {
val = newVal
notify(key)
}
})
}
function observeData (obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
makeReactive(obj, key)
}
}
//转换数据对象后,能够安全地解析 DOM 绑定。
parseDOM(document.body, obj)
}
function syncNode (node, observable, property) {
node.textContent = observable[property]
// 移除了 `Seer.` 是由于 observe 函数在可得到的做用域范围以内。
observe(property, () => node.textContent = observable[property])
}
function parseDOM (node, observable) {
const nodes = document.querySelectorAll('[s-text]')
for (const node of nodes) {
syncNode(node, observable, node.attributes['s-text'].value)
}
}
}
const App = Seer({
title: 'Game of Thrones',
firstName: 'Jon',
lastName: 'Snow',
age: 25
})
function updateText (property, e) {
App.data[property] = e.target.value
}
function resetTitle () {
App.data.title = "Game of Thrones"
}复制代码
Resources
EXTERNAL RESOURCES LOADED INTO THIS FIDDLE:
bootstrap.min.css复制代码
Result
上文的代码能够在这里找到: github.com/shentao/see…
这篇是制做你本身的响应式引擎系列文章中的第一篇。
下一篇 将是关于建立 computed properties,每一个属性都有它本身的可追踪依赖。
很是欢迎在评论区提出你对于下一篇文章讲述内容的反馈和想法!
感谢阅读。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。