Samgao0312 / Blog

MIT License
1 stars 1 forks source link

如何使用JS构建响应式引擎. 第一部分: 观察对象 #59

Open Samgao0312 opened 4 years ago

Samgao0312 commented 4 years ago

image

随着对健壮和交互式Web界面的需求不断增长,许多开发人员已开始采用响应式编程

在开始实现自己的响应式引擎之前,让我们快速解释一下什么是响应式编程。维基百科为我们提供了 响应式界面 实现的经典示例-即电子表格。定义= A1 + B1之类的公式将在A1或B1更改时更新单元格。 这样的公式可以被认为是 计算值

这篇文章我们不讨论如何实现计算值。我们要首先为我们的响应引擎奠定基础。

引擎

当前,已经有很多现成的方法来解决如何观察和响应应用状态变化问题。

在本教程中,我们将采用 getters/setters 的方式来观察状态变化并对状态变化做出反应。

注意:为了尽可能简单,该代码缺乏对非原始数据类型嵌套属性的支持,并且缺少许多必需的合理检查

可观察对象

让我们从一个data对象开始,我们要观察其属性。

let data = {
  firstName: 'Jon',
  lastName: 'Snow',
  age: 25
}

首先,我们创建两个函数,这些函数将使用 getter / setter 功能将对象的属性转换为可观察的属性。

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)

通过运行observeData(data),我们将对象转换为能够被观察的对象。现在,我们有了一种可以在值更改时创建通知的方法。

响应改变

在开始通知之前,我们需要可以实际通知的内容。这是一个可以使用观察者模式的完美示例。在这种情况下,我们将使用 signals 实现。

let signals = {} // Signals 从一个空对象开始

function observe (property, signalHandler) {
  if(!signals[property]) signals[property] = [] // 我们将signalHandler推入信号数组,这实际上为我们提供了回调函数数组

  signals[property].push(signalHandler) // 我们将signalHandler推入信号数组,这实际上为我们提供了回调函数数组
}

现在,我们可以使用如下观察函数:observe('propertyName',callback),其中 callback 是每次属性值更改时都应调用的函数。当我们多次观察某个属性时,每个回调将存储在相应属性的信号数组中。这样,我们可以存储所有回调并可以轻松访问它们。

现在,对于您之前看到的notify函数。

function notify (signal, newVal) {
  if(!signals[signal] || signals[signal].length < 1) return // 如果没有信号处理程序,则提早返回

  signals[signal].forEach((signalHandler) => signalHandler()) // 我们调用每个被观察属性的 signalHandler 
}

如您所见,现在每次只要有一个属性发生变化改,就会调用分配的signalHandlers。

因此,我们将其全部包装到一个工厂函数中,以传递必须具有反应性的数据对象。我将命名为Seer。我们最终得到这样的结果:

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

我们现在要做的就是创建一个新的反应对象。由于公开的 notify 和 observe 功能,我们可以观察和响应对对象所做的更改。

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'

很简单,不是吗?现在我们已经涵盖了基本的响应式引擎,让我们对其进行一些使用。我提到过,对于前端编程采用了更具响应性的方法,我们不必担心每次更改后手动更新DOM之类的事情。

有很多方法可以做到这一点。我猜现在最流行的一种是所谓的虚拟DOM。如果您有兴趣学习如何创建自己的虚拟DOM实现,那么已经有不错的教程。但是,这里我们将采用一种更简单的方法。

假设我们的HTML如下所示:html <h1>Title comes here</h1>

负责更新DOM的函数如下所示:

// 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')

这将起作用,但实际上需要我们做大量工作才能将所有DOM元素实际绑定到所需的数据模型。

这就是为什么我们可以更进一步并实现所有这些自动化。 如果您熟悉AngularJS或Vue.js,您一定会记得使用自定义HTML属性,例如ng-bind或v-text。 我们将在这里创建类似的东西! 我们的自定义属性称为s-text。 我们将寻找它来在DOM和数据模型之间创建绑定。

让我们更新HTML:

<!-- '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)

总结

现在我们有了解析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)
      }
    }
    // 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)
    })
  }
}

上面的代码可以在这里找到:github.com/shentao/seer

OK,这是有关制作自己的响应式引擎系列文章的第一部分。 接下来,我们就开始研究如何创建计算属性,其中每个属性都有自己的可跟踪依赖项。