美文网首页
javascript高级程序设计(第22章)-- 高级技巧

javascript高级程序设计(第22章)-- 高级技巧

作者: 穿牛仔裤的蚊子 | 来源:发表于2018-06-01 19:31 被阅读0次

    第二十二章:高级技巧

    本章内容:

    • 使用高阶函数
    • 防篡改对象
    • Yieding Timers

    22.1 高级函数

    22.1.3 惰性载入

    因为浏览器之间的行为差异,多数javascript代码包含了大量的if语句,将执行引导到正确的代码中。看看上面一章的createXHR()函数

    function createXHR(){
        if(typeof XMLHttpRequest != 'undefined'){
            return new XMLHttpRequest();
        } else if(typeof ActiveXObject != 'undefined'){
            if(typeof arguments.callee.activeXString != 'string'){
                // 跳过    
            }
            return new ActiveXObject(arguments.callee.activeXString)
        } else {
            throw new Error('No XHR object')
        }
    }
    

    每次调用createXHR的时候,它都要对浏览器所支持的能力仔细检查,影响效率。如果if语句不必每次执行,这种解决方案就是惰性载入。

    惰性摘入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式。

    第一种方案

    在函数被调用时再处理函数。该函数会被覆盖为另一个按合适方式执行的函数

    function createXHR(){
        if(typeof XMLHttpRequest != 'undefined'){
            createXHR = function(){
                  return new XMLHttpRequest();
            }
        } else if(typeof ActiveXObject != 'undefined'){
            createXHR = function(){
                if(typeof arguments.callee.activeXString != 'string'){
                // 跳过    
            }
            return new ActiveXObject(arguments.callee.activeXString)
            }
        } else {
            createXHR = function(){
                throw new Error('No XHR object')
            }
        }
        
        return createXHR();
    }
    

    在这种惰性摘入的createXHR()中,if语句的每一个分支都会重新覆盖createXHR变量赋值,有效的覆盖了原有的函数。最后一步就是调用新赋值的函数

    第二种方案

    在声明函数的时候就指定适当的函数。这样,在第一次调用的时候就不会损失性能,

    var createXHR = (function(){
        if(typeof XMLHttpRequest != 'undefined'){
            return function(){
                new XMLHttpRequest();
            }
        } else if(typeof ActiveXObject != 'undefined'){
            return function(){
                if(typeof arguments.callee.activeXString != 'string'){
                // 跳过    
                }
                 return new ActiveXObject(arguments.callee.activeXString)
            }
        } else {
            return function(){
                 throw new Error('No XHR object')
            }
        }
    })();
    

    这个例子使用的技巧是创建一个匿名、自执行的函数,用以确定使用哪一个函数实现。另外每个分支都会返回正确的函数定义,以便立即将其赋值给createXHR()。

    惰性载入函数的优点就是指在执行分支的时候牺牲一点性能。

    22.1.4 函数绑定

    函数绑定要创建一个函数,可以在特定的this环境中以指定参数调用另一个函数。这技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同事保留代码执行环境。

    var handler = {
        message: 'Event handled',
        handleClick: function(event){
            alert(this.message);
        }
    }
    var btn = document.querySelector('#my-btn');
    btn.addEventListener('click',handler.handleClick);
    

    点击按钮实际显示结果为undefined。这个问题是处理函数直接引用对象的方法,方法被独立调用,没有保存handler.handleClick()的环境,this最后指向的是DOM元素而非handler。

    mark

    从图可知当前活动对象有两个变量event和this。 this指向的伪button元素

    我们可以用闭包来修正这个问题

    var handler = {
        message: 'Event handled',
        handleClick: function(event){
            alert(this.message);
        }
    }
    var btn = document.querySelector('#my-btn');
    btn.addEventListener('click',function(event){handler.handleClick(event)});
    

    创建多个闭包变得难以理解,于是很多庫都实现了一个bind函数

    // 自己定义bind 函数
    function bind(fn,context){
        return function(){
            return fn.apply(context,context);
        }
    }
    
    // 调用
    btn.addEventListener('click',bind(handler.handleClick, handler));
    

    ECMAScript5为所有函数定义了一个原生的bind方法

    btn.addEventListener('click',handler.handleClick.bind(handler));
    

    只要是将某个函数指针以值得形式进行传递,同时该函数必须在特定环境中执行,被绑定函数的效用就凸显出来了。他们主要用于事件处理、setTimeout()和setInterval()。

    22.1.5 函数柯里化

    与函数紧密相关的主题是函数柯里化(function curring),它用于创建已经设置好一个或者多个参数的函数。函数柯里化的基本方法与函数绑定以一样。使用一个闭包返回一个函数。两者的区别在于,当函数调用的时候,返回的函数还需要设置一些传入的参数。可以看下面的例子

    function add(num1, num2){
        return num1 + num2;
    }
    function curriedAdd(num2){
        return 5 + num2;
    }
    alert(add(5,2)); // 7
    alert(curriedAdd(2)); // 7
    

    尽管上面的例子并非柯里化的函数,但很好讲出了其概念。

    柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传递要柯里化的函数和必要参数。下面是创建柯里化的函数的通用方式

    // 自己定义curry函数
    function curry(fn){
        var args = Array.prototype.slice.call(arguments,1); //获取除了fn,要传递给curry的参数
        return function(){
            var innerArgs = Array.prototype.slice.call(arguments); // 内部函数的参数,以后的真正传参
            var finalArgs = args.concat(innerArgs);
            return fn.apply(null,finalArgs)
        }
    }
    
    function add(num1,num2){
        return num1 + num2;
    }
    var curriedAdd = curry(add,5);
    alert(curriedAdd(2)); //7
    

    函数柯里化还常常作为函数绑定的一部分,构造更复杂的bind

    // 增强型bind 可以传递参数
    function bind(fn,context){
        var args = Array.prototype.slice.call(arguments, 2);
        return function(){
            var innerArgs = Array.prototype.slice.call(arguments);
            var finalArgs = args.concat(args,innerArgs);
            return fn.apply(context,finalArgs);
        }
    }
    var handler = {
        message: 'Event handled',
        handleClick: function(name,event){
            alert(this.message +":" +name +"," +event.type);
        }
    }
    var btn = document.querySelector('#my-btn');
    btn.addEventListener('click',bind(handler.handleClick,handler,'my-btn'));
    

    es5中的bind()方法也实现了柯里化,只要在this的值后面再传入另一个参数即可:

    btn.addEventListener('click',handler.handleClick.bind(handler,'my-btn');
    

    延伸阅读1: 深入详解函数的柯里化

    柯里化是指这样一个函数(假设叫做createCurry),他接收函数A作为参数,运行后能够返回一个新的函数。并且这个新的函数能够处理函数A的剩余参数。

    这样的定义可能不太好理解,我们可以通过下面的例子配合理解。

    假如有一个接收三个参数的函数A。

    function A(a, b, c) {
        // do something
    }
    

    又假如我们有一个已经封装好了的柯里化通用函数createCurry。他接收bar作为参数,能够将A转化为柯里化函数,返回结果就是这个被转化之后的函数。

    var _A = createCurry(A);
    

    那么_A作为createCurry运行的返回函数,他能够处理A的剩余参数。因此下面的运行结果都是等价的。

    _A(1, 2, 3);
    _A(1, 2)(3);
    _A(1)(2, 3);
    _A(1)(2)(3);
    A(1, 2, 3);
    

    函数A被createCurry转化之后得到柯里化函数_A,_A能够处理A的所有剩余参数。因此柯里化也被称为部分求值

    在简单的场景下,我们可以不用借助柯里化通用式来转化得到柯里化函数,我们可以凭借眼力自己封装。

    例如有一个简单的加法函数,他能够将自身的三个参数加起来并返回计算结果。

    function add(a, b, c) {
        return a + b + c;
    }
    

    那么add函数的柯里化函数_add则可以如下:

    function _add(a) {
        return function(b) {
            return function(c) {
                return a + b + c;
            }
        }
    }
    

    因此下面的运算方式是等价的。

    add(1, 2, 3);
    _add(1)(2)(3);
    

    当然,柯里化通用式具备更加强大的能力,我们靠眼力自己封装的柯里化函数则自由度偏低。因此我们仍然需要知道自己如何去封装这样一个柯里化的通用式。

    首先通过_add可以看出,柯里化函数的运行过程其实是一个参数的收集过程,我们将每一次传入的参数收集起来,并在最里层里面处理。因此我们在实现createCurry时,可以借助这个思路来进行封装。

    // 简单实现,参数只能从右到左传递
    function createCurry(func,args){
        args = args || [];
        var arity = func.length;
        return function(){
            var _args = [].slice.apply(arguments);
            var finalArgs = args.concat(_args);
            if(finalArgs.length < arity){
                return createCurry.call(this,func,finalArgs)
            }
            return func.apply(this,finalArgs)
        }
    }
    
    function add(num1,num2,num3){
        return num1 + num2 + num3;
    }
    
    var _add = createCurry(add);
    console.log(_add(1,2)(3))  // 6
    

    createCurry函数的封装借助闭包与递归,实现了一个参数收集,并在收集完毕之后执行所有参数的一个过程。

    因此聪明的读者可能已经发现,把函数经过createCurry转化为一个柯里化函数,最后执行的结果,不是正好相当于执行函数自身吗?柯里化是不是把简单的问题复杂化了?

    如果你能够提出这样的问题,那么说明你确实已经对柯里化有了一定的了解。柯里化确实是把简答的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更加多的自由度。而这里对于函数参数的自由处理,正是柯里化的核心所在。

    我们来举一个非常常见的例子。

    如果我们想要验证一串数字是否是正确的手机号,那么按照普通的思路来做,大家可能是这样封装,如下:

    function checkPhone(phoneNumber) {
        return /^1[34578]\d{9}$/.test(phoneNumber);
    }
    

    而如果我们想要验证是否是邮箱呢?这么封装:

    function checkEmail(email) {
        return /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/.test(email);
    }
    

    我们还可能会遇到验证身份证号,验证密码等各种验证信息,因此在实践中,为了统一逻辑,,我们就会封装一个更为通用的函数,将用于验证的正则与将要被验证的字符串作为参数传入。

    function check(reg, targetString) {
        return reg.test(targetString);
    }
    

    但是这样封装之后,在使用时又会稍微麻烦一点,因为会总是输入一串正则,这样就导致了使用时的效率低下。

    check(/^1[34578]\d{9}$/, '14900000088');
    check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com');
    

    那么这个时候,我们就可以借助柯里化,在check的基础上再做一层封装,以简化使用。

    var _check = createCurry(check);
    
    var checkPhone = _check(/^1[34578]\d{9}$/);
    var checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
    

    最后在使用的时候就会变得更加直观与简洁了。

    checkPhone('183888888');
    checkEmail('xxxxx@test.com');
    

    经过这个过程我们发现,柯里化能够应对更加复杂的逻辑封装。当情况变得多变,柯里化依然能够应付自如。

    我们继续来思考一个例子。这个例子与map有关,由于我们没有办法确认一个数组在遍历时会执行什么操作,因此我们只能将调用for循环的这个统一逻辑封装起来,而具体的操作则通过参数传入的形式让使用者自定义。这就是map函数。

    但是,这是针对了所有的情况我们才会这样想。

    实践中我们常常会发现,在我们的某个项目中,针对于某一个数组的操作其实是固定的,也就是说,同样的操作,可能会在项目的不同地方调用很多次。

    于是,这个时候,我们就可以在map函数的基础上,进行二次封装,以简化我们在项目中的使用。假如这个在我们项目中会调用多次的操作是将数组的每一项都转化为百分比 1 --> 100%。

    普通思维下我们可以这样来封装。

    // 普通思维下 数组的每一项都转化为百分比 1 --> 100%。 
    function getNewArray(array) {
        return array.map(function(item){
            return item * 100 + '%'
        })
    }
    getNewArray([1, 2, 3, 0.12]);   // ['100%', '200%', '300%', '12%'];
    

    而如果借助柯里化来二次封装这样的逻辑,则会如下实现:

    function _map(func,array){
        return array.map(func);
    }
    
    var _getNewArray =  createCurry(_map);
    
    var getNewArray  = _getNewArray(function(item){
        return item * 100 + '%'
    })
    
    console.log(getNewArray([1, 2, 3, 0.12]));  // ['100%', '200%', '300%', '12%'];
    console.log(getNewArray([0.01, 1])); // ['1%', '100%']
    
    

    如果我们的项目中的固定操作是希望对数组进行一个过滤,找出数组中的所有Number类型的数据。借助柯里化思维我们可以这样做。

    function _filter(func,array){
        return array.map(func);
    }
    var _find = createCurry(_filter);
    
    var findNumber = _find(function(item) {
        if (typeof item == 'number') {
            return item;
        }
    })
    findNumber([1, 2, 3, '2', '3', 4]); // [1, 2, 3, 4]
    
    // 当我们继续封装另外的过滤操作时就会变得非常简单
    // 找出数字为20的子项
    var find20 = _find(function(item, i) {
        if (typeof item === 20) {
            return i;
        }
    })
    find20([1, 2, 3, 30, 20, 100]);  // 4
    
    // 找出数组中大于100的所有数据
    var findGreater100 = _find(function(item) {
        if (item > 100) {
            return item;
        }
    })
    findGreater100([1, 2, 101, 300, 2, 122]); // [101, 300, 122]
    

    我采用了与check例子不一样的思维方向来想大家展示我们在使用柯里化时的想法。目的是想告诉大家,柯里化能够帮助我们应对更多更复杂的场景。

    22.3 高级定时器

    关于这一章可以参考第13章--事件。

    Javascript是运行在单线程的环境中,而定时器仅仅是计划在未来的某个时间执行,执行时机是不能确定的。实际上,浏览器负责进行排序,指派某一段代码在某个时间点运行的优先级。(这里还分宏任务,和微任务)

    除了主javascript执行进程外,还有需要再进程下一次空闲时执行的代码队列。随着页面在其生命周期中的推移,代码会按照执行顺序添加到对应的队列中,并在下一个可能的时间里执行。当接受到某个ajax相应时,回调函数的代码会被添加到队列。在Javascript中没有任何代码时立即执行的,但一旦进程空闲则尽快执行。

    定时器对队列的工作方式是,当特定时间过去后将代码插入。注意:给队列添加代码并不意味着对它执行。

    var btn = document.querySelector('#my-btn');
    btn.onclick = function(){
        setTimeout(function(){
           // dosomething
        },250)
        // dosomething
    }
    

    在这里给一个按钮设置了一个事件处理程序,事件处理程序设置了一个250ms后调用的定时器。点击按钮之后,首先onclick事件处理程序加入到队列。改程序执行后才能设置定时器,再有250ms后,指定的代码才被添加到队列中等待执行。

    关于定时器要记住最重要的事,指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时实际执行代码。假设前面的例子onclick时间处理程序要执行300ms。那么定时器的代码至少要在定时器设置后的300ms后才会执行。

    mark

    如图所示,尽管255ms处添加了定时器代码,但这个时候还不能执行,因为onclick事件处理程序仍在继续。

    22.3.1 重复定时器

    使用setInterval()创建的定时器确保了定时器代码规则的插入队列中。这个方式问题在于,定时器代码可能在代码再次被添加到队列之前还没有完成执行。当使用setInterval时,仅当没有该定时器的任何其他代码时,才能被添加到队列中。

    这种重复定时器规则有两个问题:(1)某些间隔会被跳过。(2)多个定时器的代码执行之间的间隔可能会比预期的小。

    假设onclick事件处理程序为200ms的间隔,事件处理函数得300ms多一点的时间完成完成。就会同时出现跳过间隔连续运行定时器代码的问题。

    mark

    这里例子的第一个定时器是在205ms被添加到队列中的,但是知道300ms处才能够执行。当执行这个定时器代码时,在405ms处又给队列添加了另外一个副本。在下一个间隔即605ms处,第一个定时器还在运行,队列中已经有一个定时器的代码实例了。结果是这个不会被添加到队列中。

    为了避免setInterval()重复定时器的2个缺点,可以用如下模式使用链式setTimeout()

    setTimeout(function(){
        // 处理中
        setTimeout(arguments.callee,interval)
    },interval)
    

    这样做的好处是在前一个定时器代码执行之前,不会向队列添加新的定时器代码,确保不会有任何的缺失间隔。

    arguments.callee在非严格模式下获取当前执行函数的引用。

    22.3.2 Yielding Processes

    在展开该循环之前,你需要回答以下两个重要的问题。

    • 该处理的是否必须同步完成?如果这个数据的处理会造成其他运行的阻塞,那么最好不要改动它。
    • 数据是否必须按照顺序完成?

    当你发现某个循环占用了大量的时间,同时对于上面的问题,你的回答是‘否’。

    定时器分割这个循环,这是一种数组分块(array chunking)的技术,小块小块的处理数组。基本思路是为要处理的项目创建一个队列,然后使用定时器取下笑一个要处理的项目进行处理,接着在设置另一个定时器。基本模式如下:

    setTimeout(function(){
        // 取出下一个条目并处理
        var item = array.shift(); //取出队列的第一个
        process(item);
        
        // 如果还有条目
        if(array.length > 0){
            setTimeout(arguments.callee,100)
        }
    },100)
    

    要实现数组分块非常简单,可以使用下面函数

    function chunck(array, process, context){
        setTimeout(function(){
            var item = array.shift();
            process.call(context,item);
            
            if(array.length > 0){
                 setTimeout(arguments.callee,100)
            }
        },100)    
    }
    

    下面是实例:

    var data = [111,222,333,444,555,666,777,888,123,234,345,456,678,789,890];
    function printValue(item){
        console.log(item);
    }
    chunk(data,printValue);
    

    应该当心的是,传递的是引用类型的数组,当处理数据的时候,数组条目也在发生改变。如果想保持数组不变,应该将数组的克隆传递给chunk

    chunk(data.concat(),printValue);
    

    数组分块的重要性在于它可以将多个项目的处理在执行队列上分开,在每个项目处理之后,给与其他的浏览器处理的机会运行,这样就可以避免长时间运行脚本的错误。

    22.3.3 函数节流

    某些高频率的更改可能会让浏览器崩溃。为了绕开这个问题,可以使用定时器对该函数进行节流。

    函数节流的背后思想是:某些代码不可以在没有时间间隔的情况下连续执行。

    第一次调用函数的时候,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另外一个。如果一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为新的一个定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。

    var processor = {
        timeoutId: null,
        // 实际进行处理的方法
        performProcessing: function(){
            //实际执行的方法
        },
        // 初始处理调用方法
        process: function(){
            clearTimeout(this.timeOutId);
            var that = this;
            
            this.timeoutId = setTimeOut(function(){
                that.performProcessing();
            },100)
        }
    }
    
    processor.process();
    

    这个模式可以使用throttle函数来简化。

    function throttle(method,context){
        clearTimeout(method.tId);
        method.tId = setTimeout(function(){
            method.call(context);
        },100)
    }
    

    throttle()函数接受两个参数:要执行的函数与在哪个作用域执行。上面这个函数首先清除之前设置的任何定时器。定时器ID是存储在函数的tId属性中。如果这是第一次对这个方法调用throttle()的话,那么这段代码会生成该属性。

    function clickBox(){
        console.log('click');
    }
    
    window.onresize = function(){
        throttle(clickBox)
    }
    

    我觉得上面的throttle实现有问题

    延伸阅读2:Debounce 和 Throttle 的原理及实现

    在处理诸如 resizescrollmousemovekeydown/keyup/keypress 等事件的时候,通常我们不希望这些事件太过频繁地触发,尤其是监听程序中涉及到大量的计算或者有非常耗费资源的操作。

    可以参看这个 Demo 体会下。

    Debounce

    DOM 事件里的 debounce 概念其实是从机械开关和继电器的“去弹跳”(debounce)衍生 出来的,基本思路就是把多个信号合并为一个信号。这篇文章 解释得非常清楚,感兴趣的可以一读。

    在 JavaScript 中,debounce 函数所做的事情就是,强制一个函数在某个连续时间段内只执行一次,哪怕它本来会被调用多次。我们希望在用户停止某个操作一段时间之后才执行相应的监听函数,而不是在用户操作的过程当中,浏览器触发多少次事件,就执行多少次监听函数。

    比如,在某个 3s 的时间段内连续地移动了鼠标,浏览器可能会触发几十(甚至几百)个 mousemove 事件,不使用 debounce 的话,监听函数就要执行这么多次;如果对监听函数使用 100ms 的“去弹跳”,那么浏览器只会执行一次这个监听函数,而且是在第 3.1s 的时候执行的。

    现在,我们就来实现一个 debounce 函数。

    实现

    我们这个 debounce 函数接收两个参数,第一个是要“去弹跳”的回调函数 fn,第二个是延迟的时间 delay

    实际上,大部分的完整 debounce 实现还有第三个参数 immediate ,表明回调函数是在一个时间区间的最开始执行(immediatetrue)还是最后执行(immediatefalse),比如 underscore 的 _.debounce。本文不考虑这个参数,只考虑最后执行的情况,感兴趣的可以自行研究。

    /**
    *
    * @param fn {Function}   实际要执行的函数
    * @param delay {Number}  延迟时间,也就是阈值,单位是毫秒(ms)
    *
    * @return {Function}     返回一个“去弹跳”了的函数
    */
    var debounce = function(fn,delay){
        // 定时器事件
        var time;
        return function(){
            // 保存函数调用时的上下文和参数,传递给 fn
            var context = this;
            var args = arguments;
    
            clearTimeout(time);
            time = setTimeout(function(){
                fn.apply(context,args);
            },delay)
        }
    }
    

    其实思路很简单,debounce 返回了一个闭包,这个闭包依然会被连续频繁地调用,但是在闭包内部,却限制了原始函数 fn 的执行,强制 fn 只在连续操作停止后只执行一次。

    debounce 的使用方式如下:

    window.onresize = debounce(function (e) {
        console.log(111)
    }, 250)
    
    Throttle

    throttle 的概念理解起来更容易,就是固定函数执行的速率,即所谓的“节流”。正常情况下,mousemove 的监听函数可能会每 20ms(假设)执行一次,如果设置 200ms 的“节流”,那么它就会每 200ms 执行一次。比如在 1s 的时间段内,正常的监听函数可能会执行 50(1000/20) 次,“节流” 200ms 后则会执行 5(1000/200) 次。

    我们先来看 Demo。可以看到,不管鼠标移动的速度是慢是快,“节流”后的监听函数都会“匀速”地每 250ms 执行一次。

    实现

    debounce 类似,我们这个 throttle 也接收两个参数,一个实际要执行的函数 fn,一个执行间隔阈值 threshhold

    同样的,throttle 的更完整实现可以参看 underscore 的 _.throttle

    // 这里实现一个简单版本的, 最后为了保证最后一次,设置了个定时器。
    var throttle = function(fn,threshhold){
        // 定时器事件
        var last;
        var timer;
        threshhold = threshhold || 250;
    
        return function(){
            var context = this;
            var args = arguments;
            var now = +new Date();
    
            // 定时器保证最后一次
            if(last && now < last + threshhold){
                clearTimeout(timer);
                timer = setTimeout(function(){
                    last = now;
                    fn.apply(context,args);
                },threshhold)
            } else {
                // 在时间区间的最开始和到达指定间隔的时候执行一次 fn
                last = now
                fn.apply(context,args);
            }
        }
            }
    

    throttle 常用的场景是限制 resizescroll 的触发频率。以 scroll 为例,查看这个 Demo 感受下。

    可视化解释

    如果还是不能完全体会 debouncethrottle 的差异,可以到 这个页面 看一下两者可视化的比较。

    总结

    debounce 强制函数在某段时间内只执行一次,throttle 强制函数以固定的速率执行。在处理一些高频率触发的 DOM 事件的时候,它们都能极大提高用户体验。

    22.4 自定义事件

    事件是一种观察者的设计模式,这是一种创建松散耦合代码的技术。对象可以发布事件,用来表示该对象生命周期中有某个有趣的时刻到了。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码相应。

    观察者模式由两类对象组成:主体和观察者

    主体负责发布事件,同时观察者通过订阅这些事件来观察主体。该模式的一个重要概念就是主体并不知道观察者的任何事情。

    自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现这种的基本模式如下:

    function EventTarget(){
        this.handlers = {};
    }
    
    EventTarget.prototype = {
        constructor: EventTarget,
        addHandler: function(type, handler){
            if(typeof this.handlers[type] == 'undefined'){
                this.handlers[type] = []
            }
            this.handlers[type].push(handler)
        },
        fire: function(event){
            if(!event.target){
                event.target = this;
            }
            if( this.handlers[event.type] instanceof Array){
                var handlers = this.handlers[event.type];
                for(var i=0, len=handlers.length; i<len; i++){
                    handlers[i](event); //绑定上的监听全部都要执行一遍
                }
            }
        },
        removeHandler: function(type,handler){
            if(this.handlers[type] instanceof Array){
                var handlers = this.handlers[type];
                for(var i=0, len=handlers.length; i<len; i++){
                    if(handlers[i] == handler){
                        break;
                    }
                }
                handlers.splice(i,1);
            }
        }
    }
    

    然后使用EventTarget类型的自定义事件可以如下使用:

    // 创建主体对象
    var target = new EventTarget();
    
    //添加一个事件
    target.addHandler("message",handleMeessage);
    
    //触发事件
    target.fire({type:'message',message:'hello'});
    
    // 删除事件
    target.removeHandler("message",handleMeessage);
    
    //再次触发事件
    target.fire({type:'message',message:'hello'});
    

    如果每个对象都有对其他所有对象的引用,那么整个代码就会紧密耦合,同时维护也变得困难,因为对某个对象的修改会影响到其他对象。是由自定义事件可以解耦相关对象。在很多情况下触发事件的代码和监听事件的代码时完全分离的。

    小结:

    • 可以使用惰性载入,将任何代码分支推迟到第一次调用函数的时候。
    • 函数绑定可以让你创建始终在指定环境中运行的函数,同时函数柯里化可以让你创建已经填了某些参数的函数。
    • 将绑定和柯里化组合起来,就能够给你一种在任何环境中以任意参数执行任意函数的方法。

    javaScript中可以使用setTimeOut()和setInterval()如下创建定时器。

    • 定时器代码时放在一个等待区域,知道时间间隔到了之后,此时将代码添加到Javascript的处理队列中,等待下一次Javascript进程空闲时被执行。
    • 每次一段代码执行结束之后,都会有一段空闲时间进行浏览器处理。
    • 这种行为意味着,可以使用定时器将长时间运行的脚本氛围一小块一小块可以在以后运行的代码段。这种做法有助于Web应用对用户交互有更积极的相应。

    javascript中经常以事件的形式应用观察者模式。虽然事件常常和DOM一起用,但是你也可以通过实现自定义事件在自己的代码中应用。实现自定义事件有助于将不同部分的代码相互之间解耦。

    参考:

    前端基础进阶(八):深入详解函数的柯里化

    Debounce 和 Throttle 的原理及实现

    相关文章

      网友评论

          本文标题:javascript高级程序设计(第22章)-- 高级技巧

          本文链接:https://www.haomeiwen.com/subject/pcgksftx.html