ko源码讲解(1)如何实现observable

@songhlc 2018-07-09 14:37:15发表于 yonyouyc/blog

ko源码讲解(1)如何实现observable

1.knockout源码从何看起

一切源头,从package.json看起

"scripts": {
    "prepublish": "grunt",
    "test": "node spec/runner.node.js"
},

基本可以得到的信息,knockout是使用grunt进行构建的,做为一个2011年开始发布的项目来说,这个很正常

转到grunt配置文件:Gruntfile.js,找到关键的build任务

grunt.registerMultiTask('build', 'Build', function() {
    if (!this.errorCount) {
        var output = this.data;
        if (this.target === 'debug') {
            buildDebug(output);
        } else if (this.target === 'min') {
            buildMin(output, this.async());
        }
    }
    return !this.errorCount;
});
function buildDebug(output) {
    var source = [];
    source.push(grunt.config('banner'));
    source.push('(function(){\n');
    source.push('var DEBUG=true;\n');
    source.push(getCombinedSources());
    source.push('})();\n');
    grunt.file.write(output, source.join('').replace(/\r\n/g, '\n'));
}
function getCombinedSources() {
        var fragments = grunt.config('fragments'),
            sourceFilenames = [
                fragments + 'extern-pre.js',
                fragments + 'amd-pre.js',
                getReferencedSources(fragments + 'source-references.js'),
                fragments + 'amd-post.js',
                fragments + 'extern-post.js'
            ],
            flattenedSourceFilenames = Array.prototype.concat.apply([], sourceFilenames),
            combinedSources = flattenedSourceFilenames.map(function(filename) {
                return grunt.file.read('./' + filename);
            }).join('');

        return combinedSources.replace('##VERSION##', grunt.config('pkg.version'));
    }

继续转到靠谱的build/fragments/source-references.js

knockoutDebugCallback([
    'src/namespace.js',
    'src/google-closure-compiler-utils.js',
    'src/version.js',
    'src/options.js',
    'src/utils.js',
    'src/utils.domData.js',
    'src/utils.domNodeDisposal.js',
    'src/utils.domManipulation.js',
    'src/memoization.js',
    'src/tasks.js',
    'src/subscribables/extenders.js',
    'src/subscribables/subscribable.js',
    'src/subscribables/dependencyDetection.js',
    'src/subscribables/observable.js',
    'src/subscribables/observableArray.js',
    'src/subscribables/observableArray.changeTracking.js',
    'src/subscribables/dependentObservable.js',
    'src/subscribables/mappingHelpers.js',
    'src/subscribables/observableUtils.js',
    'src/binding/selectExtensions.js',
    'src/binding/expressionRewriting.js',
    'src/virtualElements.js',
    'src/binding/bindingProvider.js',
    'src/binding/bindingAttributeSyntax.js',
    'src/components/loaderRegistry.js',
    'src/components/defaultLoader.js',
    'src/components/customElements.js',
    'src/components/componentBinding.js',
    'src/binding/defaultBindings/attr.js',
    'src/binding/defaultBindings/checked.js',
    'src/binding/defaultBindings/css.js',
    'src/binding/defaultBindings/enableDisable.js',
    'src/binding/defaultBindings/event.js',
    'src/binding/defaultBindings/foreach.js',
    'src/binding/defaultBindings/hasfocus.js',
    'src/binding/defaultBindings/html.js',
    'src/binding/defaultBindings/ifIfnotWith.js',
    'src/binding/defaultBindings/let.js',
    'src/binding/defaultBindings/options.js',
    'src/binding/defaultBindings/selectedOptions.js',
    'src/binding/defaultBindings/style.js',
    'src/binding/defaultBindings/submit.js',
    'src/binding/defaultBindings/text.js',
    'src/binding/defaultBindings/textInput.js',
    'src/binding/defaultBindings/uniqueName.js',
    'src/binding/defaultBindings/using.js',
    'src/binding/defaultBindings/value.js',
    'src/binding/defaultBindings/visibleHidden.js',
    // click depends on event - The order matters for specs, which includes each file individually
    'src/binding/defaultBindings/click.js',
    'src/templating/templateEngine.js',
    'src/templating/templateRewriting.js',
    'src/templating/templateSources.js',
    'src/templating/templating.js',
    'src/binding/editDetection/compareArrays.js',
    'src/binding/editDetection/arrayToDomNodeChildren.js',
    'src/templating/native/nativeTemplateEngine.js',
    'src/templating/jquery.tmpl/jqueryTmplTemplateEngine.js'
]);

这里可以看到,knockout编译就是按着这个文件的编译顺序进行编译的,大家可以把这个当成一个目录,来进行源码的逐一阅读

2.回到正文,如何实现observable

在讲如何实现observable的时候,我们先回顾一下,knockout如何设计viewmodel的api。

vm = {
    name: ko.observable('ttt')
}
vm.name() // 输出ttt
vm.name('222') // vm的值被设置为222

通过这种api的设计我们可以简单这样抽象出来

ko.observable = function (params) {
    var actualValue = ''
    if (params) {
      actualValue = params
    }
    return function (val) {
      if (val !== undefined) {
        actualValue = val
      } else {
        return actualValue
      }
    }
}
window.vm = {
    name: ko.observable('ttt')
}
vm.name() // ttt
vm.name('222')
vm.name() // 222

恩 效果显著,我们来看看knockout实际是怎么实现的,看代码src/subscribables/observable.js

ko.observable = function (initialValue) {
    // 定义要返回的函数
    function observable() {
    // 通过arguments的方式来判断更直观,有参数则是写,否则则是读
        if (arguments.length > 0) {
            //ko把此对象老的值放到observable[observableLatestValue] 静态变量里了,写入值的时候先判断老值和新值是否一致
            if (observable.isDifferent(observable[observableLatestValue], arguments[0])) {
            // 如果不一致,先触发valueWillMutate,这里会触发beforeChange方法
                observable.valueWillMutate();
                // 修改latestValue赋值为存入的值
                observable[observableLatestValue] = arguments[0];
                // 触发valueHasMutated方法
                observable.valueHasMutated();
            }
            return this; // 允许链式赋值 vm.name('ttt1').name('ttt3')
        }
        else {
            // 读取值,先把当前observable写入依赖链当中(后续再依赖检测的时候再着重介绍)
            ko.dependencyDetection.registerDependency(observable); 
            // 读取的时候返回值
            return observable[observableLatestValue];
        }
    }
    // 定义的时候设置初始值
    observable[observableLatestValue] = initialValue;

    // Inherit from 'subscribable'
    if (!ko.utils.canSetPrototype) {
        // 'subscribable' won't be on the prototype chain unless we put it there directly
        ko.utils.extend(observable, ko.subscribable['fn']);
    }
    // 给observable添加subscribe方法
    ko.subscribable['fn'].init(observable);

    // 设置observable的prototype
    ko.utils.setPrototypeOfOrExtend(observable, observableFn);

    if (ko.options['deferUpdates']) {
        ko.extenders['deferred'](observable, true);
    }

    return observable;
}
var observableFn = {
    'equalityComparer': valuesArePrimitiveAndEqual,
    peek: function() { return this[observableLatestValue]; },
    valueHasMutated: function () {
        this['notifySubscribers'](this[observableLatestValue], 'spectate');
        this['notifySubscribers'](this[observableLatestValue]);
    },
    valueWillMutate: function () { this['notifySubscribers'](this[observableLatestValue], 'beforeChange'); }
};

实现思路基本和我们模拟的打通小异,不过也有很多值得我们学习的地方

3.看完了observable,我们来理解一下observableArray

see src/subsribables/observableArray

ko.observableArray = function (initialValues) {
    initialValues = initialValues || [];
//确保传入的是数组对象否则报错
    if (typeof initialValues != 'object' || !('length' in initialValues))
        throw new Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");
// 其实用的也是observable
    var result = ko.observable(initialValues);
    //然后再prototype里扩展了remove、removeAll、destroy、destroyAll、indexOf、replace、sorted、reverse等方法
    ko.utils.setPrototypeOfOrExtend(result, ko.observableArray['fn']);
    return result.extend({'trackArrayChanges':true});
};

ko.observableArray['fn'] = {
    'remove': function (valueOrPredicate) {
    },

    'removeAll': function (arrayOfValues) {
    },

    'destroy': function (valueOrPredicate) {
    },

    'destroyAll': function (arrayOfValues) {
    },

    'indexOf': function (item) {
    },

    'replace': function(oldItem, newItem) {
    },

    'sorted': function (compareFunction) {
    },
    'reversed': function () {
        return this().slice(0).reverse();
    }
};

完毕,通过简单的两部分代码的讲解,大家能很快的了解了knockout里observable的实现了,下期我们将讲解subscribable和dependencyDetection