jQuery 소스 해석(2)-Callback,Deferred 비동기 프로그래밍

32885 단어 jquerycallbackdeferred
잡담
이 글은 한 달 전이면 발표해야 한다.표를 뛰어넘는 이유는 표준적인promise/A+규범이 궁금해서es6의promise를 배웠고 취미로 을 완전하게 배웠습니다.
본고의 목적은 jQuery가 대한promise 실현(즉 Deferred, 비표준적인promise 실현)을 분석하는 동시에 관찰자 모델의 능력을 분석하고 발굴하는 데 있다.읽은 후에 아래의 블로그의 비동기 프로그래밍 부분을 참고하여 Promise,Generator,Async를 이해하는 것을 권장합니다.
ECMAScript 6 사양 요약(긴 문서 작성)http://blog.csdn.net/vbdfforever/article/details/50727462
도입부
전통적인 비동기 프로그래밍은 리셋 함수의 형식을 사용한다. 리셋 함수에서 리셋 함수를 호출할 때 층층이 끼워 넣고 모든 리셋 내부에서 단독으로 오류를 포착해야 한다. 왜냐하면 실행 상하문이 동기화되는 과정에서 이미 사라져서 거슬러 올라갈 수 없기 때문이다.
/*      */
step1(function (error, value1) {
    step2(value1, function(error, value2) {
        try {
            // Do something with value2
        } catch(e) {
            // ...
        }
    });
});

주 논리와 리셋 함수 간의 결합 (분리 플러그인) 을 해제하고 실행의 비동기성을 확보하는 새로운 방식이 필요하다.
성명식, 명령식 두 가지 사고방식이 있다.성명식의 해결 이런 문제에 대해 동기화 방식으로 비동기 코드를 작성하거나 심지어 오류 포착을 하려면 언어 차원의 해결이 필요하거나 적어도 스스로 간단한 컴파일러를 써야 한다.우리는 웹 앱을 실현할 필요가 없고 도구, 라이브러리 형식으로만 존재하는 구성 요소이기 때문에 기존의 문법 구조에서 명령식 방식만 고려한다.
명령식의 방법은 체인식으로 호출하는데 가장 직접적인 것은 바로 아래의 이런 사고방식이다(리셋 사이는 모두 분리된다)
step1().anApi(step2).anApi(step3).catchError(errorFun)

이벤트 대기 자체가javascipt의 운행을 막지 않기 때문에 그림의 step2, step3, errorFun은 저장되어 내부에 알맞을 때 촉발되어야 합니다.'발표 이벤트, 구독이 터지기를 기다린다'는 과정, 즉 관찰자 모드(발표-구독 모드라고도 부른다)와 유사하다는 것을 발견했다.
다음은 장난감 코드로 어떻게 실현되었는지 보여 준다.
//    (  ,    、    )
function watch() {
    var cache = [];
    return {
        done: function(callback) {
            cache.push(callback);
        },
        resolve: function() {
            for (var i=0; i<cache.length; i++) {
                cache[i].apply(this, arguments);
            }
        }
    }
}

function somethingAsync() {
    // some code...
    var lis = watch();
       = function() {
        lis.resolve();
    }
    return lis;  //             
}
somethingAsync().done(fn1).done(fn2);

Callback
관찰자 모드에서 리셋 함수의 귀속을 풀 수 있습니다.하지만 여기서는 두 가지 기능을 사용자 정의해야 합니다.
1. 지연.사건에 대해 촉발할 때 감청이 없으면 놓친다.트리거할 때의 파라미터를 저장하고, 리셋을 추가할 때 이 파라미터가 저장된 값이 있는지 판단하고, 즉시 호출할지 결정합니다.
2、once.리셋은 한 번만 촉발할 수 있다.
여기에 갈고리라는 개념을 소개할 필요가 있다.프로그램이 다른 곳에 갈고리를 매립함으로써 서로 다른 특성과 기능 지원을 증가시킬 수 있다.마찬가지로 관찰자 모델은 서로 다른 수요에 따라 서로 다른 기능을 맞춤형으로 만들어야 한다.Deferred뿐만 아니라 관찰자 모델을 많이 사용하지만 수요의 기능 특징이 다르다.Query가Callback을 추상화하는 목적은 가능한 한 관찰자 모델의 잠재력을 발굴하여 한match 여러 케이스의 강력한 관찰자 모델을 실현하는 것이다. 또한 순환 호출 상황을 고려하여 Deferred뿐만 아니라 관찰자 모델을 빌려야 하는 대부분의 다른 장소에 다시 사용할 수 있다.예를 들어 교체기를 실현할 때 어떤returnfalse는 종료를 표시하지만 어떤returnfalse는 영향을 주지 않는다. 두 가지를 모두 지원하려면 하나의 인삼을 추가해야 한다. 이곳의 사고방식은 문자열 파라미터를 전송하여 코드에 있는 갈고리의 상태를 지정하는 것이다.
Callback에서는 memory 지연 (add 시 설정), once 단일 터치 후 lock 잠금 상태 (fire 시 설정), unique 리셋 재설정 (add 시 설정), stop Onfalse (fire 내 반복 판단) 를 지원합니다. + 의 형식으로 내부에 기본적인 Fire(또 하나의 기본적인add가 있는데 다른 인터페이스가 외부 호출된add 내부에 직접 박혀 있지 않기 때문에)와 Fire,fireWith 외관이 있다.잠금, 비활성화 기능이 추가되었습니다.아이디어는locked=true를 통해 외부 호출을 봉쇄하는 Fire 관련 인터페이스를 잠그는 것입니다. (지연memory 파라미터가 존재하는 것을 제외하고add 인터페이스는 내부 Fire 동작을 호출할 수 있습니다.)list="잠금add 동작을 통해 잠금합니다. 따라서locked (잠금),locked +list (비활성화).
캘백은 1.12 버전에서 1.11 버전보다 훨씬 우아하고 의미가 뚜렷하다.list는 리셋 목록을 대표합니다. Fire를 호출하여list 리셋 목록을 훑어볼 때 리셋 함수 자체가dd나fire를 내부적으로 호출할 수 있으므로 고려해야 합니다.dd일 때 아무런 영향이 없습니다. 동적 판단list만 필요합니다.length가 좋습니다. Fire일 때 임무를 작업 목록에 저장해야 합니다.queue는 작업 목록에 해당합니다. 매번 Fire가 사용해야 하는 매개 변수가 저장되어 있습니다. (매개 변수는 모두 그룹 형식이기 때문에undefined가 아닐 것입니다.)Firing을 사용하여 표시가 Fire 단계에 속하는지 확인합니다.fire 과정 중queue가 지속됩니다.shift () 를 누르고 리셋을 반복합니다.외관fire 인터페이스,locked의 상황을 차단할 수 있으며,queue에서push 인자를 사용하지 않습니다.지연된 효과 때문에dd에서는 직접 실행과 관련됩니다. 복잡도를 줄이기 위해 내부 Fire 인터페이스를 통해firingIndex로 실행을 시작하는 색인 위치를 지정합니다.
[원본 코드]
// #410,Array.prototype.indexOf   ,     
jQuery.inArray = function( elem, arr, i ) {
    var len;

    if ( arr ) {
        if ( var indexOf = [].indexOf ) {
            return indexOf.call( arr, elem, i );
        }

        len = arr.length;
        // x?(x?x:x):x
        i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;

        for ( ; i < len; i++ ) {

            // Skip accessing in sparse arrays
            if ( i in arr && arr[ i ] === elem ) {
                return i;
            }
        }
    }

    return -1;
}

// #3159,     'once memory' -> {'once': true, 'memory': true}
function createOptions( options ) {
    var object = {};
    jQuery.each( options.match( /\S+/g ) || [], function( _, flag ) {
        object[ flag ] = true;
    } );
    return object;
}

// #3189,           ,          
// 
// options -> 4   (  ),   
// once:             
// memory:                ,            
// unique:          
// stopOnFalse:     false    
jQuery.Callbacks = function( options ) {

    //     
    options = typeof options === "string" ?
        createOptions( options ) :
        jQuery.extend( {}, options );

    var //     fire    ,          ,             
        firing,

        //             
        memory,

        //               
        fired,

        //     fire    
        locked,

        //     
        list = [],

        //   fire  (         )       
        queue = [],

        //     list     ,      add      
        firingIndex = -1,

        //     fire  
        fire = function() {

            //         ,      fire  
            locked = options.once;

            //       、     
            fired = firing = true;
            for ( ; queue.length; firingIndex = -1 ) {
                // fire         ,    
                memory = queue.shift();
                //   
                while ( ++firingIndex < list.length ) {

                    //       false,     stopOnFalse  ,      
                    if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && options.stopOnFalse ) {

                        // queue     list       ,  
                        firingIndex = list.length;
                        //            ,          false
                        memory = false;
                    }
                }
            }

            //       ,queue             
            if ( !options.memory ) {
                memory = false;
            }
            //   firing  
            firing = false;

            //      (  once),  fire   ,        add(     fire)    ,     disable (locked+list)
            if ( locked ) {

                // 'once memory'
                if ( memory ) {
                    list = [];

                // disable()
                } else {
                    list = "";
                }
            }
        },

        // return self
        self = {

            //     ,         。        fire
            add: function() {
                if ( list ) {

                    //       add,           ,memory  fire  ,        (        fire,     memory)
                    if ( memory && !firing ) {
                        firingIndex = list.length - 1;
                        queue.push( memory );
                    }

                    //     add,  [fn1,[fn2,[fn3,fn4]]] -> fn1,fn2,fn3,fn4
                    ( function add( args ) {
                        jQuery.each( args, function( _, arg ) {
                            if ( jQuery.isFunction( arg ) ) {
                                if ( !options.unique || !self.has( arg ) ) {
                                    list.push( arg );
                                }
                            } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {

                                // Inspect recursively
                                add( arg );
                            }
                        } );
                    } )( arguments );

                    //     
                    if ( memory && !firing ) {
                        fire();
                    }
                }
                //   
                return this;
            },

            //     ,     。        ,      remove ,         ,   firingIndex  
            remove: function() {
                jQuery.each( arguments, function( _, arg ) {
                    var index;
                    // Array.prototype.indexOf     , index     
                    while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
                        list.splice( index, 1 );

                        //   firingIndex
                        if ( index <= firingIndex ) {
                            firingIndex--;
                        }
                    }
                } );
                return this;
            },

            //          ,             
            has: function( fn ) {
                return fn ?
                    // Array.prototype.indexOf     
                    jQuery.inArray( fn, list ) > -1 :
                    list.length > 0;
            },

            //   list
            empty: function() {
                //   list  "" 
                if ( list ) {
                    list = [];
                }
                return this;
            },

            //   。list add,locked   fire  
            disable: function() {
                locked = queue = [];
                list = memory = "";
                return this;
            },
            disabled: function() {
                return !list;
            },

            //   ,locked   fire  ,      add       fire
            lock: function() {
                locked = true;
                //    (     memory   false)     ,     
                if ( !memory ) {
                    self.disable();
                }
                return this;
            },
            locked: function() {
                return !!locked;
            },

            //      (memory[0]   ,memory[1]     )  queue,      fire
            fireWith: function( context, args ) {
                if ( !locked ) {
                    args = args || [];
                    args = [ context, args.slice ? args.slice() : args ];
                    queue.push( args );
                    if ( !firing ) {
                        fire();
                    }
                }
                return this;
            },

            //     this
            fire: function() {
                self.fireWith( this, arguments );
                return this;
            },

            //      
            fired: function() {
                return !!fired;
            }
        };

    return self;
};

Deferred
Deferred는 jQuery 내부의promise 구현이고 내부는 지연(파라미터 기억)+oncelock(상태 잠금)의 관찰자 모델을 사용합니다.세 가지 상태가 있습니다: 정상적인 상태는'notify'(oncelock이 없음), 성공한 상태는'resolve', 실패한 상태는'reject'입니다. 각 상태는 관찰자 대상을 사용합니다.트리거에 성공하거나 실패할 때, 반대 상태는 비활성화되지만, notify 상태는 트리거가 지나치면 lock만 잠그는 것을 비활성화하지 않습니다. (add 지연 호출만 가능하고 외부 트리거는 불가능합니다.)
jQuery의 실현 특징은 자유롭고 유연하다는 것이다.이것도 단점이라고 할 수 있다.프로미스/A+ 기준이랑 많이 다르네.
jQuery에 자동적인 오류 포착이 없습니다. 자각에 의존합니다. 리퀘스트 상태의 설정 자체도 오류 설정을 위한 것 같지 않습니다. 만약에 코드를 너무 엉망으로 써서 적당한 곳에서 리퀘스트를 포착하지 않으면 오류를 잡을 수 없습니다.표준 중의reject 포지셔닝은 오류를 던지는 것이다. 이것은 대량의 실천이 성공을 제외하고는 주로 오류 처리에 사용된다는 것을 증명한 것 같다.그리고 만약에 정말 오류를 처리해야 한다면done도 다음 프로미스를 촉발할 수 없고 then의 실현만 가공할 수 있다.done/fail는Callback의list 목록에 직접 리셋을 추가하여 동기화하고 리셋 사이에 다른 단계로 기다리지 않습니다.모든then(fun)promise를 되돌려줍니다.Callback의list 목록에fun을 실행하고then내deferred 대상의 리셋 함수를 터치합니다.fun이promise 대상을 되돌려주면 그 다음.done/fail( newDefer.resolve/reject )에 비동기적인 리셋을 실현합니다.
Deferred도 두 가지 프로그래밍 방식의 초기 형태를 사용했다. 하나는 deferred를 하나의 대상으로 하고 필요할 때 deferred를 감싸는 것이다. 다른 하나는 함수인 Deferred(fun)를 감싸는 것이다. 함수 안에 업무 논리를 봉인하는 것이다. 장점은 주입에 의존하는 방식으로 기능을 실현할 수 있고 외부의 인터페이스를 노출하는 것을 줄일 수 있다. 만약에 자주 사용하는 것이 적으면 일시적으로 뜻대로 되지 않을 수도 있다.물론 Deferred는 두 가지 프로그래밍 방식을 모두 사용했기 때문에 노출 인터페이스를 줄이는 특징은 이용되지 않았다.표준적인 실현에서 두 번째 방식만 사용했고, 진정한 의미는resolve/reject 인터페이스를 숨겼습니다. (즉 완전한 deferred를 되돌려주는 것이 아닙니다.)
[원본 코드]
// #3384,Deferred,       (      ,  add/done    ,             ,    then     add/done)
jQuery.Deferred = function( func ) {
    var tuples = [

            // action, add listener, listener list, final state
            [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ],
            [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ],
            [ "notify", "progress", jQuery.Callbacks( "memory" ) ]
        ],
        //     
        state = "pending",

        //   resolve/reject   promise
        promise = {
            state: function() {
                return state;
            },
            always: function() {
                deferred.done( arguments ).fail( arguments );
                return this;
            },

            //   :  then      deferred   promise
            then: function( /* fnDone, fnFail, fnProgress */ ) {
                var fns = arguments;

                //     ,    deferred,  deferred.promise()
                return jQuery.Deferred( function( newDefer ) {
                    jQuery.each( tuples, function( i, tuple ) {
                        // tuples   tuple       
                        var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];

                        // tuples   tuple   [ 'done' | 'fail' | 'progress' ]
                        // promise[ 'done' | 'fail' | 'progress' ]        
                        deferred[ tuple[ 1 ] ]( function() {
                            var returned = fn && fn.apply( this, arguments );

                            //   promise deferred   ,    newDefer    
                            if ( returned && jQuery.isFunction( returned.promise ) ) {
                                returned.promise()
                                    .progress( newDefer.notify )
                                    .done( newDefer.resolve )
                                    .fail( newDefer.reject );
                            } else {

                                //  promise  , done/fail    ,          promise   。      ,      ,   done/fail     argument
                                newDefer[ tuple[ 0 ] + "With" ](
                                    this === promise ? newDefer.promise() : this,
                                    fn ? [ returned ] : arguments
                                );
                            }
                        } );
                    } );
                    fns = null;
                } ).promise();
            },

            //     ,    resolve/reject   promise  ,     
            //       ,   deferred  
            promise: function( obj ) {
                return obj != null ? jQuery.extend( obj, promise ) : promise;
            }
        },
        deferred = {};

    //   ,             [  ]
    promise.pipe = promise.then;

    //  promise     Callback     done(  add)/fail/progress  
    //  deferred     Callback     resolve/resolveWith(  fireWith)/reject/rejectWith
    jQuery.each( tuples, function( i, tuple ) {
        //        Callback
        var list = tuple[ 2 ],
            //     
            stateString = tuple[ 3 ];

        // promise[ done | fail | progress ] = list.add
        promise[ tuple[ 1 ] ] = list.add;

        // 'resolved' 'rejected'
        if ( stateString ) {
            list.add( function() {

                // state = [ resolved | rejected ]
                state = stateString;

            // [ reject_list | resolve_list ].disable(       ); progress_list.lock(progress  )
            // ^     ,0^1 = 1,1^1 = 0,(          1,    0)
            }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
        }

        // deferred[ resolve | reject | notify ]
        deferred[ tuple[ 0 ] ] = function() {
            deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments );
            return this;
        };
        deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
    } );

    //       deferred,promise   deferred     。deferred.promise() -> promise
    promise.promise( deferred );

    //   fun,      deferred(           )
    if ( func ) {
        func.call( deferred, deferred );
    }

    //   deferred
    return deferred;
};

when when 방법은 deferred의promise 대상을 되돌려줍니다.여러 개의 파라미터를 받아들여 promise 인터페이스가 없는 파라미터는resolved 상태로 하고, 파라미터가 모두resolved 상태로 변하면 when의deferred의resolve를 터치합니다.매개 변수가 Reject가 되면 deferred의 Reject를 터치합니다.파라미터가 notify를 호출할 때, 호출할 때마다 한 번씩 실행됩니다.Reject가 트리거 항목을 사용하는 트리거 파라미터를 제외하고resolve와reject는 모두 하나의 매개 변수 그룹을 사용하여 트리거한다. 그룹의 매개 변수는 when의 매개 변수에 대응하는 트리거 파라미터이고 when의 매개 변수에 대응하는 비promise 대상에 대응하는 트리거 파라미터는 그들 자신이다.
when은 또한 하나의 매개 변수만 있고 프로미스 방법이 있을 때 이 매개 변수를 직접 사용하여 성공적인 조작을 촉발하고 비용을 절약할 수 있음을 고려하여 방법은 처음에 이 최적화를 했다.따라서 이런 상황은 해당 대상이 직접 관리한다.트리거된 매개 변수 규칙의 불일치, 개인적으로는 우아하지 않다고 생각하고 업데이트 Fun 리arguments.length<=1시에도 일치하지 않습니다.
// #3480
jQuey.when = function( subordinate /* , ..., subordinateN */ ) {
    var i = 0,
        resolveValues = slice.call( arguments ),
        length = resolveValues.length,

        //           promise  
        remaining = length !== 1 ||
            ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,

        //    Deferred  ,       promise      
        deferred = remaining === 1 ? subordinate : jQuery.Deferred(),

        updateFunc = function( i, contexts, values ) {
            // progress   、resolve   (           )
            return function( value ) {
                //           
                contexts[ i ] = this;
                //   resolve/progress                  
                values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;

                //      progress  
                if ( values === progressValues ) {
                    deferred.notifyWith( contexts, values );

                //     resolve。     0     defer resolve,  resolve          
                } else if ( !( --remaining ) ) {
                    deferred.resolveWith( contexts, values );
                }
            };
        },

        progressValues, progressContexts, resolveContexts;

    // length 0  if ( !remaining ){}    resolve, 1        ,
    if ( length > 1 ) {
        //           
        progressValues = new Array( length );
        progressContexts = new Array( length );
        resolveContexts = new Array( length );
        for ( ; i < length; i++ ) {
            if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
                resolveValues[ i ].promise()
                    .progress( updateFunc( i, progressContexts, progressValues ) )
                    .done( updateFunc( i, resolveContexts, resolveValues ) )
                    .fail( deferred.reject );
            } else {
                //     promise         -1
                --remaining;
            }
        }
    }

    //          ,    resolved  ,     resolve
    if ( !remaining ) {
        deferred.resolveWith( resolveContexts, resolveValues );
    }

    return deferred.promise();
};

마무리:es6규범으로 정리된 비동기 프로그래밍 1절을 참고하는 것을 권장합니다.글의 첫머리에 주소를 제시하였다.

좋은 웹페이지 즐겨찾기