function makeReactive (obj, key) {
let val = obj[key]
Object.defineProperty(obj, key, {
get () {
return val // 只需返回缓存的值
},
set (newVal) {
val = newVal // 保存newVal
notify(key) // 忽略当前值
}
})
}
function observeData (obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
makeReactive(obj, key)
}
}
}
observeData(data)
function Seer (dataObj) {
let signals = {}
observeData(dataObj)
// Besides the reactive data object, we also want to return and thus expose the observe and notify functions.
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)
}
}
}
}
const App = new Seer({
title: 'Game of Thrones',
firstName: 'Jon',
lastName: 'Snow',
age: 25
})
// To subscribe and react to changes made to the reactive App object:
App.observe('firstName', () => console.log(App.data.firstName))
App.observe('lastName', () => console.log(App.data.lastName))
// To trigger the above callbacks simply change the values like this:
App.data.firstName = 'Sansa'
App.data.lastName = 'Stark'
// First we need to get the node that we want to keep updating.
const h1Node = document.querySelector('h1')
function syncNode (node, obj, property) {
// Initialize the h1’s textContent value with the observed object’s property value
node.textContent = obj[property]
// Start observing the property using our Seer instance App.observe method.
App.observe(property, value => node.textContent = obj[property] || '')
}
syncNode(h1Node, App.data, 'title')
<!-- 'title' is the property which value we want to show inside the <h1> element -->
<h1 s-text="title">Title comes here</h1>
function parseDOM (node, observable) {
// We get all nodes that have the s-text custom attribute
const nodes = document.querySelectorAll('[s-text]')
// For each existing node, we call the syncNode function
nodes.forEach((node) => {
syncNode(node, observable, node.attributes['s-text'].value)
})
}
// Now all we need to do is call it with document.body as the root node. All `s-text` nodes will automatically create bindings to the corresponding reactive property.
parseDOM(document.body, App.data)
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)
}
}
// We can safely parse the DOM looking for bindings after we converted the dataObject.
parseDOM(document.body, obj)
}
function syncNode (node, observable, property) {
node.textContent = observable[property]
// We remove the `Seer.` as it is now available for us in our scope.
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)
})
}
}
随着对健壮和交互式Web界面的需求不断增长,许多开发人员已开始采用响应式编程
在开始实现自己的响应式引擎之前,让我们快速解释一下什么是响应式编程。维基百科为我们提供了 响应式界面 实现的经典示例-即电子表格。定义= A1 + B1之类的公式将在A1或B1更改时更新单元格。 这样的公式可以被认为是 计算值。
这篇文章我们不讨论如何实现计算值。我们要首先为我们的响应引擎奠定基础。
引擎
当前,已经有很多现成的方法来解决如何观察和响应应用状态变化问题。
getter / setter
方法来创建可观察的数据模型。在本教程中,我们将采用 getters/setters 的方式来观察状态变化并对状态变化做出反应。
可观察对象
让我们从一个
data
对象开始,我们要观察其属性。首先,我们创建两个函数,这些函数将使用 getter / setter 功能将对象的属性转换为可观察的属性。
通过运行observeData(data),我们将对象转换为能够被观察的对象。现在,我们有了一种可以在值更改时创建通知的方法。
响应改变
在开始通知之前,我们需要可以实际通知的内容。这是一个可以使用观察者模式的完美示例。在这种情况下,我们将使用 signals 实现。
现在,我们可以使用如下观察函数:
observe('propertyName',callback)
,其中callback
是每次属性值更改时都应调用的函数。当我们多次观察某个属性时,每个回调将存储在相应属性的信号数组中。这样,我们可以存储所有回调并可以轻松访问它们。现在,对于您之前看到的notify函数。
如您所见,现在每次只要有一个属性发生变化改,就会调用分配的signalHandlers。
因此,我们将其全部包装到一个工厂函数中,以传递必须具有反应性的数据对象。我将命名为
Seer
。我们最终得到这样的结果:我们现在要做的就是创建一个新的反应对象。由于公开的 notify 和 observe 功能,我们可以观察和响应对对象所做的更改。
很简单,不是吗?现在我们已经涵盖了基本的响应式引擎,让我们对其进行一些使用。我提到过,对于前端编程采用了更具响应性的方法,我们不必担心每次更改后手动更新DOM之类的事情。
有很多方法可以做到这一点。我猜现在最流行的一种是所谓的虚拟DOM。如果您有兴趣学习如何创建自己的虚拟DOM实现,那么已经有不错的教程。但是,这里我们将采用一种更简单的方法。
假设我们的HTML如下所示:
html <h1>Title comes here</h1>
负责更新DOM的函数如下所示:
这将起作用,但实际上需要我们做大量工作才能将所有DOM元素实际绑定到所需的数据模型。
这就是为什么我们可以更进一步并实现所有这些自动化。 如果您熟悉AngularJS或Vue.js,您一定会记得使用自定义HTML属性,例如ng-bind或v-text。 我们将在这里创建类似的东西! 我们的自定义属性称为s-text。 我们将寻找它来在DOM和数据模型之间创建绑定。
让我们更新HTML:
总结
现在我们有了解析DOM并将节点绑定到数据模型的方法,让我们将这两个函数添加到 Seer 工厂函数中,在初始化时我们将解析DOM。
结果应如下所示:
上面的代码可以在这里找到:github.com/shentao/seer
OK,这是有关制作自己的响应式引擎系列文章的第一部分。 接下来,我们就开始研究如何创建计算属性,其中每个属性都有自己的可跟踪依赖项。