21、Knockout 技术深入解析

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

通过对这些技术点的深入理解和应用,可以更好地利用相关技术来开发高效、可维护的应用程序。在实际开发中,要根据具体需求选择合适的技术和方法,不断优化代码,提高开发效率和应用性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值