本系列一共七章,Github 地址请查阅这里,原文地址请查阅这里。javascript
这是编写 JavaScript 框架系列第四章。本章将会阐述脏检查和数据存取器绑定技术,并指出他们的优缺点。java
数据绑定是一个通用的技术,用来绑定来自提供者和消费者的数据源并同步它们。git
这是一个通用定义,归纳了数据绑定技术的通用构建模块github
handler()
。以上的步骤在不一样的数据绑定技术中会以不一样的方式实现。接下来将会介绍两种技术,即脏检查和存取器方法。他们都有优缺点,我将在介绍后简要讨论。数组
脏检查是最为人熟知的数据绑定方法。它的概念很简单,不须要复杂的语言特性,这使得它能够做为一个很好的候选缺省选择。bash
定义提供者和消费者不要求任何特别的语法,只须要一个简单的 JavaScript 对象。框架
const provider = {
message: 'Hello World'
}
const consumer = document.createElement('p')
复制代码
同步一般是提供者上的属性变化触发。须要监听变化的属性,必须明确映射到各自的 handler()
函数。异步
observe(provider, 'message', message => {
consumer.innerHTML = message
})
复制代码
observe()
函数只保存 (provider, property) -> handler
映射供之后使用。ide
function observe(provider, prop, handler) {
provider._handlers[prop] = handler
}
复制代码
这样,咱们就有一个定义提供者和消费者的语法,以及一种为属性改变而注册 handler()
函数的方法。咱们库的公共 API 已经准备好了,如今介绍其内部实现。函数
脏检查因一个缘由而被称为脏。它定时检查而不是直接监听属性变化。从如今起,咱们把这个检查称为摘要周期。一个摘要周期遍历每个由 observe()
函数添加的 (provider, property) -> handler
入口,而且检查自上次遍历以来属性值是否发生变化。若是变化则运行 handler()
函数。一个简单的实现相似以下:
function digest() {
providers.forEach(digestProvider)
}
function digestProvider (provider) {
for (let prop in provider._handlers) {
if (provider._prevValues[prop] !== provider[prop]) {
provider._prevValues[prop] = provider[prop]
handler(provider[prop])
}
}
}
复制代码
digest()
函数须要一直不停地运行以确保状态同步。
存取器技术如今是热点。由于须要 ES5 getter/ setter 功能而未受普遍支持,可是它因语法优雅而弥补了这个弱点。
定义提供者须要特殊的语法。简单的提供者对象必须被传入 observable
函数,这个函数能够用来把提供者对象转化为一个被监听的对象。
const provider = observable({
greeting: 'Hello',
subject: 'World'
})
const consumer = document.createElement('p')
复制代码
简单的 handler()
映射语法弥补了这种小的不便。使用脏检查,咱们不得不以下显式地定义每一个被监听的属性。
observe(provider, 'greeting', greeting => {
consumer.innerHTML = greeting + ' ' + provider.subject
})
observe(provider, 'subject', subject => {
consumer.innerHTML = provider.greeting + ' ' + subject
})
复制代码
这显得很啰嗦和笨拙。访问器技术能够自动检测在 handler()
函数中使用过的提供者属性,这样容许咱们简化以上代码。
observe(() => {
consumer.innerHTML = provider.greeting + ' ' + provider.subject
})
复制代码
observe()
的实现和脏检查的不一样。它只是执行了传入的 handler()
函数,而且当handler()
运行的时候把函数标识为目前激活状态。
let activeHandler
function observe(handler) {
activeHandler = handler
handler()
activeHandler = undefined
}
复制代码
注意,咱们如今利用了 JavaScript 的单线程特性,使用惟一的 activeHandler
变量来记录目前运行的 handler()
的函数。
这就是存取器技术名字的由来。提供者使用 getters/setters 来扩展,这两个方法在后台进行复杂的工做。思路是以以下方式拦截提供者属性的存取操做。
activeHandler
在运行,保存 (provider, property) -> activeHandler
映射以备后用。(provide, property)
映射的 handler()
函数。如下代码展现了一个提供者的属性变化的简单的实现过程。
function observableProp(provider, prop) {
const value = provider[prop]
Object.defineProperty(provider, prop, {
get () {
if (activeHandler) {
provider._handlers[prop] = activeHandler
}
return value
},
set (newValue) {
value = newValue
const handler = obj._handlers[prop]
if (handler) {
activeHandler = handler
handler()
activeHandler = undefined
}
}
})
}
复制代码
在前面一节提到的 observable()
函数递归遍历提供者的属性,而且使用 observableProp()
函数来转化全部属性为可监测。
function observable (provider) {
for (let prop in provider) {
observableProp(provider, prop)
if (typeof provider[prop] === 'object') {
observable(provider[prop])
}
}
}
复制代码
这是一个很是简单的实现,可是对于比较这两种技术已经足够了。
本节将会归纳性地指出脏检查和存取器技术的优缺点。
脏检查不须要语法来定义的提供者和消费者,可是把 handler()
和 (provider, property)
进行映射配对是笨拙和不灵活的。
存取器技术要求用 observable()
来封装提供者,可是自动 handler()
映射弥补了这个不足。对于使用数据绑定的大型项目来讲,这是必须的功能。
脏检查由于糟糕的性能问题而广受垢病。它不得不在每一个摘要周期可能屡次检查每一个 (provider, property) -> handler
入口。并且,由于它没法知晓何时属性值发生变化,因此即使应用处于闲置状态也必须保持运转。
存取器速度更快,可是若是是监听一个很大的对象的话会不可避免地下降性能。使用存取器来替换提供者的每一个属性常常会致使过分滥用。一个方案是在须要的时候动态构建存取器树而不是一开始就成批地建立。还有一种可替代的简单方案便是把不须要监听的属性用 noObserve()
函数封装起来,这个能够告诉 observable()
不要处理这些属性。使人沮丧的是,这会引入一些额外的语法。
脏检查天生支持 expando(动态添加)和存取器属性。
存取器技术在这里有一个弱点。由于 Expando 属性不在初始的存取树上面,因此不支持存取器技术。举个栗子,这会致使数组问题,可是能够在添加了一个属性以后经过手动运行 observableProp()
来解决。由于存取器不能被存取器二次封装,因此存取属性是不支持的。一般的解决方法是使用 computed()
函数而不是 getter。这将引入更多的自定义语法。
脏检查没有给咱们太多选择的自由,由于咱们不知道何时属性值真正发生了改变。handler()
函数只能经过不断地运行 digest() 循环来异步执行。
存取器技术建立的存取器是同步触发的,因此咱们能够自由选择。能够选择立刻运行 handler()
,或者将其保存在稍后异步执行的批处理中。前一种技术给予咱们可预测性的优势,然后者能够经过移除重复项来提高性能。