Knockout 技术深入解析
1. 绑定处理程序的运行机制
1.1 绑定处理程序排序
绑定处理程序排序时,会遍历提供的绑定,跳过已处理的绑定。若绑定有
after
属性,则开始依赖检查。以下是相关代码:
// Next add the current binding
result.push({ key: bindingKey, handler: binding });
}
bindingsConsidered[bindingKey] = true;
});
return result;
这个函数的处理流程如下:
1. 遍历绑定,跳过已处理的绑定。
2. 若有
after
属性,开始依赖检查。
3. 将当前绑定推入跟踪依赖的数组。
4. 遍历
after
属性中的每个绑定。
5. 若依赖绑定已在依赖数组中,抛出异常,因为存在循环依赖。
6. 若依赖绑定未找到,递归进入循环处理程序检查其依赖。
7. 检查完依赖绑定后,移除依赖数组的最后一个元素,将当前绑定推入结果数组和已处理绑定数组。
8. 若未来绑定需要该绑定作为依赖,循环处理程序将立即返回,表明未来绑定可安全继续。
1.2 运行绑定处理程序
获取绑定处理程序的正确顺序后,会对其进行迭代。会进行最后一次安全检查,确保若节点是注释节点,绑定处理程序允许用于虚拟元素。然后在
try/catch
块中调用
init
和
update
函数。
// Run init, ignoring any dependencies
var handlerInitFn = bindingKeyAndHandler.handler["init"];
if (typeof handlerInitFn == "function") {
ko.dependencyDetection.ignore(function() {
var initResult = handlerInitFn(node,
getValueAccessor(bindingKey),
allBindings,
bindingContext['$data'],
bindingContext);
// If this binding handler claims to control descendant
bindings, make a note of this
if (initResult && initResult['controlsDescendantBindings']) {
if (bindingHandlerThatControlsDescendantBindings !==
undefined)
throw new Error("Multiple bindings (" +
bindingHandlerThatControlsDescendantBindings + " and " +
bindingKey + ") are trying to control descendant bindings
of the same element. You cannot use these bindings
together on the same element.");
bindingHandlerThatControlsDescendantBindings = bindingKey;
}
});
}
init
函数在禁用依赖检测的作用域中运行,因为它不会运行两次。
init
处理程序传递所有必需的参数,并检查结果以确定该处理程序是否要控制后代绑定。若不是第一个控制后代绑定的处理程序,Knockout 会抛出异常。
// Run update in its own computed wrapper
var handlerUpdateFn = bindingKeyAndHandler.handler["update"];
if (typeof handlerUpdateFn == "function") {
ko.dependentObservable(
function() {
handlerUpdateFn(node,
getValueAccessor(bindingKey),
allBindings,
bindingContext['$data'],
bindingContext);
},
null,
{ disposeWhenNodeIsRemoved: node }
);
}
update
处理程序在计算可观察对象中运行,当依赖项更改时会自动重新运行。这是 Knockout 的一个优点,因为绑定处理程序在可观察依赖项更改时会自动重新运行,因为它们本身就在可观察对象内部。
1.3 绑定处理程序执行结果返回
遍历完所有绑定处理程序后,
applyBindingsToNodeInternal
会返回一个对象,该对象根据
init
处理程序的结果标志告诉调用者是否递归到当前节点的子节点:
return {
'shouldBindDescendants': bindingHandlerThatControls
DescendantBindings === undefined
};
2. 模板系统
2.1 模板绑定处理程序
模板绑定的
init
函数能识别模板可以是命名模板(从源加载)或内联模板(使用绑定元素的内容加载)。
'init': function(element, valueAccessor) {
// Support anonymous templates
var bindingValue = ko.utils.unwrapObservable(valueAccessor());
if (typeof bindingValue == "string" || bindingValue['name']) {
// It's a named template - clear the element
ko.virtualElements.emptyNode(element);
} else {
var templateNodes = ko.virtualElements.childNodes(element),
container = ko.utils.moveCleanedNodesToContainer
Element(templateNodes);
new ko.templateSources.anonymousTemplate
(element)['nodes'](container);
}
return { 'controlsDescendantBindings': true };
}
若绑定值是字符串,或绑定值是具有
name
属性的对象,则使用命名源,只需清空节点。命名源在模板名称更改时需要更改,所以实际渲染模板的所有工作都在
update
方法中。
若为匿名模板,
moveCleanedNodesToContainerElement
会从元素中移除子节点并将其放入
div
容器,但该
div
容器不会放入 DOM。会使用该元素创建一个新的匿名模板源,并将
div
容器传递给模板的
nodes
函数。
nodes
函数使用
utils.domData
存储容器。
模板源是模板引擎用于提供渲染模板所需 DOM 的对象。它必须提供返回包含要使用节点的容器的
nodes
函数,或提供相同内容的字符串化版本的
text
函数。
ko.templateSources
数组包含两种模板源类型:用于命名源的
domElement
和用于内联源的
anonymousTemplate
。
最后,
init
函数返回
{ 'controlsDescendantBindings': true }
。
update
函数有三个不同的分支:渲染单个模板的分支、使用
foreach
渲染模板数组的分支,以及若存在
if
(或
ifnot
)绑定且为
false
则移除所有内容的分支。最后一个分支无需过多解释,前两个分支在功能上非常相似:它们调用模板引擎的
renderTemplate
方法,该方法返回一个 DOM 节点数组,然后将这些节点添加到 DOM 中。之后,它们分别对模板调用
applyBindings
。
2.2 模板引擎
模板引擎负责生成 DOM 节点,但它本身只是一个基类,不能单独使用。调用基模板引擎的
renderTemplate
方法时,它会调用
makeTemplateSource
方法,并将结果传递给
renderTemplateSource
方法。
默认的
makeTemplateSource
方法接受一个模板参数。若模板是字符串,它会尝试查找具有该名称的脚本并创建一个
domElement
源。若模板是节点,它会从中创建并返回一个新的
anonymousTemplate
源。
默认的
renderTemplateSource
方法未实现,调用时会抛出错误。模板实现必须重写此方法才能正常工作。
Knockout 开箱即用地提供了两种模板引擎实现:原生和
jQuery.tmpl
。
jQuery.tmpl
引擎自 2011 年以来就未再开发,继续包含在标准发行版中可能更多是为了向后兼容。这里我们关注原生模板引擎,它重写
renderTemplateSource
方法如下:
function (templateSource, bindingContext, options) {
// IE<9 cloneNode doesn't work properly
var useNodesIfAvailable = !(ko.utils.ieVersion < 9),
templateNodesFunc = useNodesIfAvailable ?
templateSource['nodes'] : null,
templateNodes = templateNodesFunc ? templateSource['nodes']() :
null;
if (templateNodes) {
return ko.utils.makeArray(templateNodes.cloneNode
(true).childNodes);
} else {
var templateText = templateSource['text']();
return ko.utils.parseHtmlFragment(templateText);
}
};
若存在
nodes
,它将用于获取模板节点容器,克隆它并返回。若在较新的 IE 中
clone
方法不起作用,或者未提供
nodes
,则
ko.utils
会解析文本源并返回。模板引擎不会将节点添加到 DOM 中,也不会绑定它们,它只是返回节点。模板绑定在从模板引擎获取生成的模板后会处理这部分工作。
3. ko.utils 命名空间
ko.utils
命名空间是 Knockout 的实用函数集合。并非所有这些函数都公开暴露,至少不是以可用的方式。Knockout 的压缩过程会混淆其中一半以上的函数。由于未混淆的方法是 Knockout 承诺提供的公共 API,更改它们将是重大更改。尽管将
ko.utils
上的所有暴露方法视为 API 的一部分,但 Knockout 并未为它们提供任何文档。
以下是截至 Knockout 3.2 的
ko.utils
上的公共函数完整列表:
| 函数名 | 功能描述 |
| ---- | ---- |
|
addOrRemoveItem(array, item, included)
| 若
included
为
true
,则将
item
添加到
array
中(若不存在);若
included
为
false
,则从
array
中移除
item
(若存在)。 |
|
arrayFilter(array, predicate)
| 使用
predicate(element, index)
返回
array
中使
predicate
返回
true
的元素数组。 |
|
arrayFirst(array, predicate, predicateOwner)
| 使用
predicate.call(predicateOwner, element, index)
返回
array
中第一个使
predicate
返回
true
的元素。
predicateOwner
是可选参数,用于控制
predicate
中的
this
。 |
|
arrayForEach(array, action)
| 对
array
中的每个元素调用
action(element, index)
。 |
|
arrayGetDistinctValues(array)
| 返回一个仅包含
array
中不同元素的数组,使用
ko.utils.arrayIndexOf
确定唯一性。 |
|
arrayIndexOf(array, item)
| 若
Array.prototype.indexOf
存在,则调用它;否则手动遍历数组并返回索引,若元素未找到则返回 -1。这是针对 Internet Explorer 9 以下版本的填充方法。 |
|
arrayMap(array, mapping)
| 对
array
中的每个元素调用
mapping(element, index)
,返回一个新数组。 |
|
arrayPushAll(array, valuesToPush)
| 将
valuesToPush
参数推入
array
参数。该函数处理
valuesToPush
类似数组但不是真正数组的情况,例如
HTMLCollection
,在这种情况下调用
array.push.apply(array, valuesToPush)
通常会失败。 |
|
arrayRemoveItem(array, itemToRemove)
| 根据
item
的索引,通过拼接或移位操作从
array
中移除
item
。 |
|
domData
| 该对象提供
get
、
set
和
clear
方法,用于处理 DOM 节点上的任意键/值对。Knockout 内部使用它来跟踪绑定信息,但也可用于存储任何内容。 |
|
domNodeDisposal
| 该对象提供与 DOM 清理任务相关的实用工具:
-
addDisposeCallback(node, callback)
:使用
domData
为节点添加回调。若 Knockout 通过模板或控制流移除节点,将使用该回调。
-
cleanNode(node)
:运行所有使用
addDisposeCallback
注册的关联处置回调。此函数别名为
ko.cleanNode
。
-
cleanExternalData(node)
:使用 jQuery 的
cleanData
函数移除 jQuery 插件添加的数据。若未找到 jQuery,则不执行任何操作。
-
removeDisposeCallback(node, callback)
:从节点的
domData
函数中移除回调。
-
removeNode(node)
:使用
cleanNode
清理节点,然后将其从 DOM 中移除。此函数别名为
ko.removeNode
。 |
|
Extend(target, source)
| 这是一个普通的扩展方法,它将
source
上的所有属性添加或覆盖到
target
上,使用
hasOwnProperty
过滤
source
属性。 |
|
fieldsIncludedWithJsonPost
| 若未指定
includeFields
选项,这是用于
postJson
的默认字段数组。 |
|
getFormFields(form, fieldName)
| 返回表单中与
fieldname
匹配的所有输入或文本区域字段,
fieldname
可以是字符串、正则表达式或具有接受字段名称的测试谓词的对象。 |
|
objectForEach(obj, action)
| 对
obj
中的每个属性调用
action(properyName, propetyValue)
,使用
hasOwnProperty
过滤属性。 |
|
parseHtmlFragment(html)
| 若 jQuery 存在,则使用其
parseHTML
函数;否则使用简单的内部 HTML 解析器。返回 DOM 节点。 |
|
parseJson(jsonString)
| 通过解析提供的字符串返回一个 JavaScript 对象。若
JSON
对象存在,则使用它;否则使用
new Function
。 |
|
peekObservable(value)
| 与
ko.unwrap
类似,是一种安全方法。若
value
是可观察对象,它将返回其
peek
结果;否则直接返回该值。 |
|
postJson(urlOrForm, data, options)
| 通过创建一个新表单,将其附加到 DOM 并调用
submit
方法来执行 POST 请求。表单将使用
data
创建其字段。若
urlOrForm
是表单,若其字段与
options['includeFields']
匹配(若
options['includeFields']
不存在,则为
fieldsIncludedWithJsonPost
),则将其包含在
data
中,并且其
action
将用作 URL。 |
|
Range(min, max)
| 返回一个包含
min
和
max
之间值的数组,对两个参数都使用
ko.unwrap
。 |
|
registerEventHandler(element, eventType, handler)
| 将事件处理程序附加到元素。尽可能使用 jQuery,若可用则使用
addEventListener
,最后使用
attachEvent
(Internet Explorer)。若使用
attachEvent
,则注册一个处置处理程序以调用
detachEvent
,因为 IE 不会自动执行此操作。 |
|
setHtml(node, html)
| 清空节点的内容,展开 HTML,并使用
jQuery.html
(若可用)或
parseHtmlFragement
设置节点的 HTML。 |
|
stringifyJson(data, replacer, space)
| 使用
ko.unwrap
处理可观察数据并调用
JSON.stringify
。
replacer
和
space
参数是可选的。若
JSON
对象不存在,则抛出异常。 |
|
toggleDomNodeCssClass(node, classNames, shouldHaveClass)
| 使用
shouldHaveClass
布尔值向节点添加或移除所有
classNames
。 |
|
triggerEvent(element, eventType)
| 在元素上触发事件。在适用时使用 jQuery,并处理在 IE 和 jQuery 中引发
click
事件的已知问题。 |
|
unwrapObservable(value)
| 这是
ko.unwrap
的原始名称,为了向后兼容而保留。它将返回可观察对象的基础值,若不是可观察对象则返回该值本身。 |
4. 总结与其他相关要点
4.1 整体理解
虽然这里并非对相关技术的详尽剖析,但应该能让大家对其核心功能的实现方式有一个较好的理解。涵盖了依赖跟踪、原型链、绑定表达式解析、绑定处理程序的运行、模板处理以及
ko.utils
命名空间等方面。了解这些系统的内部工作原理有助于在遇到棘手问题时进行故障排除。
4.2 其他相关技术点
- MVVM 模式 :MVVM 模式是一种重要的设计模式,包含视图(View)和视图模型(ViewModel)。视图模型负责处理业务逻辑和数据,视图则负责展示数据。不过在实际应用中,要注意避免视图和视图模型的代码过于繁杂。
-
路由系统
:在单页面应用(SPA)中,路由系统起着关键作用。可以通过配置路由规则,实现页面的导航和切换。例如,使用
router对象的相关属性和方法来控制路由的激活、导航等操作。
graph LR
A[开始] --> B[配置路由规则]
B --> C[监听路由变化]
C --> D{路由匹配?}
D -- 是 --> E[激活相应路由]
D -- 否 --> F[显示默认页面]
E --> G[渲染页面组件]
G --> H[结束]
F --> H
- 组件系统 :组件是可复用的代码单元,能够提高代码的可维护性和复用性。可以通过基本组件注册、AMD 注册等方式来注册组件,还能观察组件参数的变化,实现组件的生命周期管理。
-
绑定处理程序
:绑定处理程序是 Knockout 的核心部分,分为简单绑定处理程序和高级绑定处理程序。简单绑定处理程序可以实现基本的 DOM 操作和动画效果,高级绑定处理程序则可以处理复杂的数据绑定和暴露 API。
- 简单绑定处理程序示例:
ko.bindingHandlers.slideVisible = {
init: function(element, valueAccessor) {
var value = ko.unwrap(valueAccessor());
$(element).toggle(value);
},
update: function(element, valueAccessor) {
var value = ko.unwrap(valueAccessor());
value ? $(element).slideDown() : $(element).slideUp();
}
};
- 高级绑定处理程序示例:
ko.bindingHandlers.chart = {
init: function(element, valueAccessor, allBindings) {
// 初始化图表
},
update: function(element, valueAccessor, allBindings) {
// 更新图表数据
}
};
4.3 实用工具函数
ko.utils
命名空间中的实用工具函数为开发提供了便利,以下是一些常用函数的使用示例:
-
数组操作函数
:
var array = [1, 2, 3, 4, 5];
var filteredArray = ko.utils.arrayFilter(array, function(item) {
return item > 3;
});
console.log(filteredArray); // 输出: [4, 5]
- DOM 操作函数 :
var node = document.getElementById('myNode');
ko.utils.setHtml(node, '<p>New content</p>');
- 事件处理函数 :
var element = document.getElementById('myButton');
ko.utils.registerEventHandler(element, 'click', function() {
console.log('Button clicked');
});
4.4 性能优化
在开发过程中,需要关注性能问题,避免出现性能瓶颈。可以通过以下方式进行性能优化:
-
限制活动绑定
:避免在不必要的元素上应用绑定,减少绑定处理程序的执行次数。
-
使用委托事件
:将事件处理程序绑定到父元素上,通过事件冒泡来处理子元素的事件,减少事件处理程序的数量。
-
优化可观察循环
:避免在可观察对象的循环中进行复杂的计算,减少不必要的重新计算。
4.5 其他注意事项
-
使用
self关键字 :在 JavaScript 中,this关键字的指向可能会发生变化,使用self关键字可以避免这种问题。
var viewModel = function() {
var self = this;
self.name = ko.observable('John');
self.sayHello = function() {
alert('Hello, ' + self.name());
};
};
- 处理异步操作 :在处理异步操作时,可以使用 Promise 或回调函数来确保代码的顺序执行。
function asyncOperation() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Async operation completed');
}, 1000);
});
}
asyncOperation().then(function(result) {
console.log(result);
});
通过对这些技术点的深入理解和应用,可以更好地利用相关技术来开发高效、可维护的应用程序。在实际开发中,要根据具体需求选择合适的技术和方法,不断优化代码,提高开发效率和应用性能。
超级会员免费看
51

被折叠的 条评论
为什么被折叠?



