View
3
Download
0
Category
Preview:
Citation preview
jQueryDesignPatterns
TableofContents
jQueryDesignPatterns
Credits
AbouttheAuthor
AbouttheReviewer
www.PacktPub.com
eBooks,discountoffers,andmore
Whysubscribe?
Preface
Whatthisbookcovers
Whatyouneedforthisbook
Whothisbookisfor
Conventions
Readerfeedback
Customersupport
Downloadingtheexamplecode
Errata
Piracy
Questions
1.ARefresheronjQueryandtheCompositePattern
jQueryandDOMscripting
ManipulatingtheDOMusingjQuery
MethodChainingandFluentInterfaces
TheCompositePattern
HowtheCompositePatternisusedbyjQuery
ComparingthebenefitsovertheplainDOMAPI
UsingtheCompositePatterntodevelopapplications
Asampleusecase
TheCompositeCollectionImplementation
Anexampleexecution
Alternativeimplementations
TheIteratorPattern
HowtheIteratorPatternisusedbyjQuery
HowitpairswiththeCompositePattern
Wherecanitbeused
Summary
2.TheObserverPattern
IntroducingtheObserverPattern
HowitisusedbyjQuery
ThejQueryonmethod
Thedocument-readyobserver
Demonstrateasampleusecase
Howitiscomparedwitheventattributes
Avoidmemoryleaks
IntroducingtheDelegatedEventObserverPattern
Howitsimplifiesourcode
Comparethememoryusagebenefits
Summary
3.ThePublish/SubscribePattern
IntroducingthePublish/SubscribePattern
HowitdiffersfromtheObserverPattern
HowitisadoptedbyjQuery
CustomeventsinjQuery
ImplementingaPub/Subschemeusingcustomevents
Demonstratingasampleusecase
UsingPub/Subonthedashboardexample
Extendingtheimplementation
Usinganyobjectasabroker
Usingcustomeventnamespacing
Summary
4.DivideandConquerwiththeModulePattern
ModulesandNamespaces
Encapsulatinginternalpartsofanimplementation
AvoidingglobalvariableswithNamespaces
Thebenefitsofthesepatterns
Thewideacceptance
TheObjectLiteralPattern
TheModulePattern
TheIIFEbuildingblock
ThesimpleIIFEModulePattern
HowitisusedbyjQuery
TheNamespaceParameterModulevariant
TheIIFE-containedModulevariant
TheRevealingModulePattern
UsingES5StrictMode
IntroducingES6Modules
UsingModulesinjQueryapplications
Themaindashboardmodule
Thecategoriesmodule
TheinformationBoxmodule
Thecountermodule
Overviewoftheimplementation
Summary
5.TheFacadePattern
IntroducingtheFacadePattern
Thebenefitsofthispattern
HowitisadoptedbyjQuery
ThejQueryDOMTraversalAPI
ThepropertyaccessandmanipulationAPI
UsingFacadesinourapplications
Summary
6.TheBuilderandFactoryPatterns
IntroducingtheFactoryPattern
HowitisadoptedbyjQuery
UsingFactoriesinourapplications
IntroducingtheBuilderPattern
HowitisadoptedbyjQuery’sAPI
HowitisusedbyjQueryinternally
Howtouseitinourapplications
Summary
7.AsynchronousControlFlowPatterns
Programmingwithcallbacks
UsingsimplecallbacksinJavaScript
Settingcallbacksasobjectproperties
UsingcallbacksinjQueryapplications
Writingmethodsthatacceptcallbacks
Orchestratingcallbacks
Queuinginorderexecution
AvoidingtheCallbackHellanti-pattern
Runningconcurrently
IntroducingtheconceptofPromises
UsingPromises
UsingthejQueryPromiseAPI
UsingPromises/A+
ComparingjQueryandA+Promises
Advancedconcepts
ChainingPromises
Handlingthrownerrors
JoiningPromises
HowjQueryusesPromises
TransformingPromisestoothertypes
TransformingtoPromises/A+
TransformingtojQueryPromises
SummarizingthebenefitsofPromises
Summary
8.MockObjectPattern
IntroducingtheMockObjectPattern
UsingMockObjectsinjQueryapplications
Definingtheactualservicerequirements
ImplementingaMockService
UsingtheMockService
Summary
9.Client-sideTemplating
IntroducingUnderscore.js
UsingUnderscore.jstemplatesinourapplications
SeparatingHTMLtemplatesfromJavaScriptcode
IntroducingHandlebars.js
UsingHandlebars.jsinourapplications
SeparatingHTMLtemplatesfromJavaScriptcode
Pre-compilingtemplates
RetrievingHTMLtemplatesasynchronously
Adoptingitinanexistingimplementation
Moderationisbestinallthings
Summary
10.PluginandWidgetDevelopmentPatterns
IntroducingjQueryPlugins
FollowingjQueryprinciples
WorkingonCompositeCollectionObjects
Allowingfurtherchaining
Workingwith$.noConflict()
WrappingwithanIIFE
Creatingreusableplugins
Acceptingconfigurationparameters
WritingstatefuljQueryplugins
ImplementingastatefuljQueryPlugin
Destroyingaplugininstance
Implementinggetterandsettermethods
UsingourplugininourDashboardapplication
UsingthejQueryPluginBoilerplate
Addingmethodstoyourplugin
Choosinganame
Summary
11.OptimizationPatterns
Placingscriptsneartheendofthepage
Bundlingandminifyingresources
UsingIIFEparameters
UsingCDNs
UsingJSDelivrAPI
OptimizingcommonJavaScriptcode
Writingbetterforloops
WritingperformantCSSselectors
WritingefficientjQuerycode
MinimizingDOMtraversals
CachingjQueryobjects
Scopingelementtraversals
ChainingjQuerymethods
Don’toverdoit
ImprovingDOMmanipulations
CreatingDOMelements
Stylingandanimating
Manipulatingdetachedelements
IntroducingtheFlyweightPattern
UsingDelegateObservers
Using$.noop()
Usingthe$.singleplugin
LazyLoadingModules
Summary
Index
jQueryDesignPatterns
jQueryDesignPatternsCopyright©2016PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:February2016
Productionreference:1230216
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
BirminghamB32PB,UK.
ISBN978-1-78588-868-7
www.packtpub.com
CreditsAuthor
ThodorisGreasidis
Reviewer
AamirAfridi
CommissioningEditor
NeilAlexander
AcquisitionEditor
AaronLazar
ContentDevelopmentEditor
RiddhiTuljapurkar
TechnicalEditor
PramodKumavat
CopyEditors
TrishyaHazare
KevinMcGowan
ProjectCoordinator
SanchitaMandal
Proofreader
SafisEditing
Indexer
RekhaNair
Graphics
AbhinashSahu
ProductionCoordinator
ShantanuN.Zagade
CoverWork
ShantanuN.Zagade
AbouttheAuthorThodorisGreasidisisaseniorwebengineerfromGreece.HegraduatedwithhonorsfromtheUniversityofThessaly,holdsapolytechnicdiplomaincomputer,networking,andcommunicationsengineering,andamaster’sdegreeincomputerscience.Heisafull-stackdeveloper,responsibleforimplementinglarge-scalewebapplicationswithintuitiveinterfacesandhigh-availabilitywebservices.
ThodorisispartoftheAngular-UIteamandhasmademanyopensourcecontributions,withaspecialinterestinMozillaprojects.HeisalsoanactivememberoftheAthensAngularJSMeetupandatechnicalreviewerofMasteringjQueryUI,PacktPublishing.
HeisaJavaScriptenthusiastandlovesbitwiseoperations.HisinterestsalsoincludeNodeJS,Python,projectscaffolding,automation,andartificialintelligence,especiallymulti-agentsystems.
Abigthankstoeveryonewhosupportedmeandshowedunderstandingformylimitedfreetimewhilewritingthisbook.
AbouttheReviewerAamirAfridihasbeenpassionateabouttheInternetandwebdevelopmentsince2002.Heholdsamaster’sdegreeine-commerce.Overtheyearsthathavefollowed,hehasworkedforvariouscompaniesandprovidedfrontendengineering,includingmobilewebappsandarchitectureserviceswithafocusonsemanticHTML,CSS,andJavaScript/jQueryandanythingelsehecangethishandson.HehascontributedtoJavaScriptbooksasatechnicalreviewer.Thesedays,heisexploringthemicroservicesarchitecturewithNodeJS,MongoDB,andReactJSatwww.tes.com.Heblogsonhttp://aamirafridi.com.
www.PacktPub.com
eBooks,discountoffers,andmoreDidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusat<customercare@packtpub.com>formoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www2.packtpub.com/books/subscription/packtlib
DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt’sonlinedigitalbooklibrary.Here,youcansearch,access,andreadPackt’sentirelibraryofbooks.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
PrefaceSinceitsintroductionin2006,thejQuerylibraryhasmadeDOMtraversalsandmanipulationsmucheasier.ThishasresultedintheappearanceofWebpageswithincreasinglycomplexuserinteractions,thuscontributingtothematuringofWebasaplatformcapableofsupportinglargeapplicationimplementations.
ThisbookpresentsaseriesofbestpracticesthatmaketheimplementationofWebapplicationsmoreefficient.Moreover,wewillanalyzethemostimportantDesignPatternsthatComputerSciencehastooffer,whichcanbeappliedtoWebdevelopment.Inthisway,wewilllearnhowtoutilizetechniquesthatarethoroughlyusedandtestedinotherfieldsofprogramming,whichwereinitiallycreatedasgenericmethodstomodelsolutionsofcomplexproblems.
InjQueryDesignPatterns,wewillanalyzehowvariousDesignPatternsareutilizedintheimplementationofjQueryandhowtheycanbeusedtoimprovetheorganizationofourimplementations.ByadoptingtheDesignPatternsdemonstratedinthisbook,youwillbeabletocreatebetterorganizedimplementationsthatresolvelargeproblemcategoriesfaster.Moreover,whenusedbyadeveloperteam,theycanimprovethecommunicationbetweenthemandleadtohomogenousimplementation,whereeverypartofthecodeiseasilyunderstoodbyothers.
WhatthisbookcoversChapter1,ARefresheronjQueryandtheCompositePattern,willteachthereaderhowtowritethecodeusingtheCompositePatternandmethodchaining(FluentInterface)byanalyzinghowtheyareusedfortheimplementationofjQueryitself.ItalsodemonstratestheIteratorPatternthatnicelypairswiththeCompositeCollectionobjectsthatjQueryreturns.
Chapter2,TheObserverPattern,willteachyouhowtorespondtouseractionsusingtheObserverPattern.ItalsodemonstrateshowtouseEventDelegationasawaytoreducethememoryconsumptionandcomplexityofthecodethathandlesdynamicallyinjectedpageelements.Finally,itwillteachyouhowtoemitandlistenforCustomEventsinordertoachievegreaterflexibilityandcodedecoupling.
Chapter3,ThePublish/SubscribePattern,willteachyouhowtoutilizethePub/SubPatterntocreateacentralpointtoemitandreceiveapplication-levelevents,asawaytodecoupleyourcodeandbusinesslogicfromtheHTMLthatisusedforpresentation.
Chapter4,DivideandConquerwiththeModulePattern,demonstratesandcomparessomeofthemostcommonlyusedModulePatternsintheindustry.ItwillteachyouhowtostructureyourapplicationinsmallindependentModulesusingNamespacing,leadingtoexpandableimplementationsthatfollowtheSeparationofConcernsprinciple.
Chapter5,TheFacadePattern,willteachyouhowtousetheFacadePatterntowrapcomplexAPIsintosimpleronesthatareabettermatchfortheneedsofyourapplication.Italsodemonstrateshowtochangepartsofyourapplication,whilekeepingthesamemodule-levelAPIsandavoidaffectingtherestofyourimplementation.
Chapter6,TheBuilderandFactoryPatterns,explainstheconceptsofandthedifferencesbetweentheBuilderandFactoryPatterns.Itwillteachyouhowandwhentouseeachofthem,inordertoimprovetheclarityofyourcodebyabstractingthegenerationofcomplexresultsintoseparatededicatedmethods.
Chapter7,AsynchronousControlFlowPatterns,willexplainhowjQuery’sDeferredandPromiseAPIsworkandcomparethemwiththeclassicalCallbacksPattern.YouwilllearnhowtousePromisestocontrolthetheexecutionofasynchronousprocedurestoruneitherinanorderorparalleltoeachother.
Chapter8,MockObjectPattern,teachesyouhowtocreateanduseMockObjectsandServicesasawaytoeasethedevelopmentofyourapplicationandgetasenseofitsfunctionality,longbeforeallitspartsarecompleted.
Chapter9,Client-sideTemplating,demonstrateshowtousetheUnderscore.jsandHandlebars.jstemplatinglibrariesasabetterandfasterwaytocreatecomplexHTMLstructureswithJavaScript.Throughthischapter,youwillgetanoverviewoftheirconventions,evaluatetheirfeatures,andfindtheonethatbestmatchesyourtaste.
Chapter10,PluginandWidgetDevelopmentPatterns,introducesthebasicconceptsandconventionsofjQueryPlugindevelopmentandanalyzesthemostcommonlyuseddesign
patterns,sothatyouwillbeabletoidentifyandusethebestmatchforanyusecase.
Chapter11,OptimizationPatterns,guidesyouwiththebesttipstocreateahighlyefficientandrobustimplementation.Youwillbeabletousethischapterasachecklistofbestpracticesthatimprovetheperformanceandlowerthememoryconsumptionofyourapplications,beforemovingthemtoaproductionenvironment.
WhatyouneedforthisbookInordertoruntheexamplesinthisbook,youwillneedtohaveawebserverinstalledonyoursystemtoservethecodefiles.Forexample,youcanuseApacheorIISorNGINX.InordertomaketheinstallationprocessofApacheeasier,youcanusemorecompletedevelopmentenvironmentsolutions,suchasXAMPPorWAMPServer.
Intermsoftechnicalproficiency,thisbookassumesthatyoualreadyhavesomeexperienceofworkingwithjQuery,HTML,CSS,andJSON.AllthecodesamplesinthebookusejQueryv2.2.0andsomeofthechaptersalsodiscusstherespectiveimplementationinjQueryv1.12.0,whichcanbeusedincasesupportforolderbrowsersisneeded.
WhothisbookisforThisbooktargetsexistingjQuerydevelopersornewdeveloperswhowanttotaketheirskillsandunderstandingtoanadvancedlevel.ItisadetailedintroductiontohowthevariousindustrystandardpatternscanbeappliedtojQueryapplications,andalongwithasetofthebestpractices,itcanhelplargeteamscollaborateandcreatewellorganizedandextendableimplementations.
ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.
Codewordsintext,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:“IntheprecedingCSScode,wefirstdefinedsomebasicstylesforthebox,boxsizer,andclearCSSclasses.”
Ablockofcodeissetasfollows:
$.each([3,5,7],function(index){
console.log(this+1+'!');
});
Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:
$('#categoriesSelector').change(function(){
var$selector=$(this);
varmessage={categoryID:$selector.val()};
broker.trigger('dashboardCategorySelect',[message]);
});
WearefollowingGoogle’sJavaScriptStyleGuide,exceptfromusingfourspacesforindentation,inordertoimprovethereadabilityofthecodeinthebook.Inshort,weareplacingcurlybracketsontopandusesinglequotesforstringliterals.
NoteFormoreinformationonGoogle’sJavaScriptStyleGuideyoucanvisitthefollowingURL:https://google.github.io/styleguide/javascriptguide.xml
Anycommand-lineinputoroutputiswrittenasfollows:
npminstalljquery
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:“ThejQueryObjectreturnedisanArray-likeobjectthatactsasawrapperobjectandcarriesthecollectionoftheretrievedelements.”
NoteWarningsorimportantnotesappearinaboxlikethis.
TipTipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook—whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.
Tosendusgeneralfeedback,simplye-mail<feedback@packtpub.com>,andmentionthebook’stitleinthesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesfromyouraccountathttp://www.packtpub.comforallthePacktPublishingbooksyouhavepurchased.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthecode—wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.
Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusat<copyright@packtpub.com>withalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
QuestionsIfyouhaveaproblemwithanyaspectofthisbook,youcancontactusat<questions@packtpub.com>,andwewilldoourbesttoaddresstheproblem.
Chapter1.ARefresheronjQueryandtheCompositePatternUntiltheWeb2.0erastarted,theWebwasjustadocument-basedmediaandallitofferedwasjustinterconnectingdifferentpages/documentsandclient-sidescriptingthatwasmostlylimitedtoformvalidation.By2005,GmailandGoogleMapswerereleased,andJavaScriptproveditselfasalanguageusedbybigenterprisestocreatelarge-scaleapplicationsandproviderichuserinterfaceinteractions.
EventhoughJavaScripthashadveryfewchangessinceitsoriginalrelease,therewasatremendouschangeintheexpectationsthattheEnterpriseworldhadaboutwhatwebpagesshouldbecapableofdoing.Sincethen,webdeveloperswererequiredtodelivercomplexuserinteractionsand,finally,theterm“webapplication”appearedonthemarket.Asaresult,itstartedtobecomeobviousthattheyshouldcreatesomecodeabstractions,definesomebestpractices,andadoptalltheapplicableDesignPatternsthatcomputersciencehadtooffer.ThewideadoptionofJavaScriptforenterprise-gradeapplicationshelpedtheevolutionofthelanguage,whichwiththeEcmaScript2015/EcmaScript6(ES6)specificationwasexpandedinawaythatallowedevenmoreDesignPatternstobeeasilyutilized.
InAugust2006,thejQuerylibrarywasfirstreleasedbyJohnResigathttp://jquery.com,asanefforttocreateaconvenientAPItolocateDOMelements.Sincethen,ithasbeenanintegralpartofawebdeveloper’stoolkit.jQueryinitscoreusesseveralDesignPatternsandtriestourgetheirusetothedeveloperthroughthemethodsthatitprovides.TheCompositePatternisoneofthemanditisexposedtothedeveloperthroughtheverycorejQuery()method,whichisusedforDOMtraversal,oneofthehighlightsofthejQuerylibrary.
Inthischapter,wewill:
HavearefresheronDOMscriptingusingjQueryIntroducetheCompositePatternSeehowtheCompositePatternisusedbyjQueryDiscussthegainsofferedbyjQueryoverplainJavaScriptDOMmanipulationsIntroducetheIteratorPatternUsetheIteratorPatterninanexampleapplication
jQueryandDOMscriptingByDOMscripting,werefertoanyprocedurethataltersormanipulatestheelementsofawebpageafterithasbeenloadedbythebrowser.TheDOMAPIisaJavaScriptAPIthatwasstandardizedin1998anditprovidestowebdevelopersacollectionofmethodsthatallowthemanipulationoftheDOMtreeelementsthatthebrowsercreatesafterloadingandparsingthewebpage’sHTMLcode.
NoteFormoreinformationontheDocumentObjectMode(DOM)anditsAPIs,youcanvisithttps://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction.
ByutilizingtheDOMAPIintheirJavaScriptcode,webdeveloperscanmanipulatetheDOM’snodesandaddnewelementsorremoveexistingelementsfromthepage.TheprimaryusecaseforDOMscriptingwasinitiallylimitedtoclient-sideformvalidation,butastheyearspassedandJavaScriptgainedthetrustoftheEnterpriseworld,morecomplexuserinteractionsstartedtobeimplemented.
TheinitialversionofthejQuerylibrarywasfirstreleasedinAugust2006andittriedtoeasethewaythewebdevelopersweretraversingandmanipulatingtheDOMtree.Oneofitsmaingoalswastoprovideabstractionsthatresultedinshorter,easier-to-read,andlesserror-pronecode,whilealsoensuringcross-browserinteroperability.
ThesecoreprinciplesthatjQueryfollowsareclearlyvisibleinitshomepage,whereitpresentsitselfas:
…afast,small,andfeature-richJavaScriptlibrary.ItmakesthingslikeHTMLdocumenttraversalandmanipulation,eventhandling,animation,andAjaxmuchsimplerwithaneasy-to-useAPIthatworksacrossamultitudeofbrowsers.Withacombinationofversatilityandextensibility,jQueryhaschangedthewaythatmillionsofpeoplewriteJavaScript.
TheabstractedAPIsthatjQueryprovidedfromthebeginning,andthewaythatdifferentDesignPatternswereorchestrated,ledtowideacceptanceamongthewebdevelopers.Asaresult,thejQuerylibraryisreferencedbymorethan60%ofthemostvisitedwebsitesworldwide,accordingtoseveralsourcessuchasBuiltWith.com(http://trends.builtwith.com/javascript/jQuery).
ManipulatingtheDOMusingjQueryTohavearefresheronjQuery,wewillgothroughanexamplewebpagethatdoessomesimpleDOMmanipulations.Inthisexample,wewillloadasimplystructuredpagethatinitiallylookslikethefollowingfigure:
WewillusesomejQuerycodetochangethepage’scontentandlayoutand,inordertomakeitseffectsclearlyvisible,wewillsetittorunabout700millisecondsafterthepagehasloaded.Theresultofourmanipulationswilllooklikethefollowingfigure:
Nowlet’sreviewtheHTMLcoderequiredfortheprecedingexample:
<!DOCTYPEhtml>
<html>
<head>
<title>DOMManipulations</title>
<linkrel="stylesheet"type="text/css"href="dom-manipulations.css">
</head>
<body>
<h1id="pageHeader">DOMManipulations</h1>
<divclass="boxContainer">
<div>
<pclass="box">
DoingDOMManipulationsiseasywithJS!
</p>
</div>
<div>
<pclass="box">
DoingDOMManipulationsiseasywithJS!
</p>
</div>
<div>
<pclass="box">
DoingDOMManipulationsiseasywithJS!
</p>
</div>
</div>
<pclass="box">
DoingDOMManipulationsiseasywithJS!
</p>
<pclass="box">
DoingDOMManipulationsiseasywithJS!
</p>
<scripttype="text/javascript"src="https://code.jquery.com/jquery-
2.2.0.min.js"></script>
<scripttype="text/javascript"src="jquery-dom-manipulations.js">
</script>
</body>
</html>
TheCSScodeusedisquitesimple,containingonlythreeCSSclassesasfollows:
.box{
padding:7px10px;
border:solid1px#333;
margin:5px3px;
box-shadow:01px2px#777;
}
.boxsizer{
float:left;
width:33.33%;
}
.clear{clear:both;}
TheprecedingcoderesultsinapagelookinglikethefirstfigurewhenopenedinabrowserandbeforeourJavaScriptcodeisexecuted.IntheprecedingCSScode,wefirstdefinedsomebasicstylesforthebox,boxsizer,andclearCSSclasses.Theboxclassstylestheassociatedelementsfoundinthepagebyusingsomepadding,athinborder,somemarginaround,andasmallshadowbelowtheelementsinordertomakethemlooklikeabox.Theboxsizerclasswillmaketheelementsthatuseittotakejust1/3rdofthewidthoftheirparentelementandcreateathree-columnlayout.Finally,theclearclasswillbeusedonanelementasabreakpointforthecolumnlayoutsothatalltheelementsthatfollowwillbepositionedbelowit.TheboxsizerandclearclassesarenotinitiallyusedbyanyelementdefinedintheHTMLcode,butwillbeusedaftertheDOMmanipulationsthatwewilldoinJavaScript.
Inthe<body>elementofourHTML,weinitiallydefinean<h1>headingelementwithIDpageHeadersothatitiseasilyselectablethroughJavaScript.Rightbelowit,wedefinefiveparagraphelements(<p>)withtheboxclass,havingthefirstthreeofthemwrappedinsidethethree<div>elementsandtheninsideanother<div>elementwiththeboxContainerclass.
Reachingourtwo<script>tags,wefirstincludeareferencetothejQuerylibraryfromjQueryCDN.Formoreinformation,youcanvisithttp://code.jquery.com/.Inthesecond<script>tag,wereferencetheJavaScriptfilewiththerequiredcode,forthisexample,whichlooksasfollows:
setTimeout(function(){
$('#pageHeader').css('font-size','3em');
var$boxes=$('.boxContainer.box');
$boxes.append(
'<br/><br/><i>Incaseweneedsimplethings</i>.');
$boxes.parent().addClass('boxsizer');
$('.boxContainer').append('<divclass="clear">');
},700);
AllourcodeiswrappedinsideasetTimeoutcalltodelayitsexecution,accordingtotheusecasedescribedearlier.ThefirstparameterofthesetTimeoutfunctioncallisananonymousfunctionthatwillbeexecutedafteratimerof700millisecondshasexpired,asdefinedinthesecondargument.
Atthefirstlineofouranonymouscallbackfunction,weusethejQuery$()functiontotraversetheDOMandlocatetheelementwiththeIDpageHeader,andusethecss()methodtoincreaseitsfont-sizeto3em.NextweprovideamorecomplexCSSselectortothe$()function,tolocatealltheelementswiththeboxclassthataredescendantsoftheelementwiththeboxContainerclass,andthenstoretheresultinavariablenamed$boxes.
Tip
Variablenamingconventions
Itisacommonpracticeamongdeveloperstousenamingconventionsforvariablesthatholdobjectsofacertaintype.Usingsuchconventionsnotonlyhelpsyourememberwhatthevariableisholding,butalsomakesyourcodeeasiertounderstandbyotherdevelopersofyourteam.AmongjQuerydevelopers,itiscommontousevariablenamesstartingwitha“$”signwhenthevariablestorestheresultofthe$()function(alsoknowasajQuerycollectionobject).
Afterwegetaholdoftheboxelementsthatweareinterestedin,weappendtwobreakingspacesandsomeextratextinitalics,attheendofeachofthem.Then,weusethe$boxesvariableandtraversetheDOMtreeonelevelup,usingtheparent()method.Theparent()methodreturnsadifferentjQueryobjectholdingtheparent<div>elementsofourinitiallyselectedboxesandthenwechainacalltotheaddClass()methodtoassignthemtheboxsizerCSSclass.
TipIfyouneedtotraversealltheparentnodesofaselectedelement,youcanusethe$.fn.parents()method.IfyoujustneedtofindthefirstancestorelementthatmatchesagivenCSSselector,considerusingthe$.fn.closest()methodinstead.
Finally,sincetheboxsizerclassusesfloatstoachievethethree-columnlayout,weneedtoclearthefloatsintheboxContainer.Onceagain,wetraversetheDOMusingthesimple.boxContainerCSSselectorandthe$()function.Then,wecallthe.append()methodtocreateanew<div>elementwiththe.clearCSSclassandinsertitattheendoftheboxContainer.
After700milliseconds,ourjQuerycodewillhavefinished,resultinginthethree-columnlayoutasshownearlier.Initsfinalstate,theHTMLcodeofourboxContainerelementwilllookasfollows:
<divclass="boxContainer">
<divclass="boxsizer">
<pclass="box">
DoingDOMManipulationsiseasywithJS!
<br><br><i>Incaseweneedsimplethings</i>.
</p>
</div>
<divclass="boxsizer">
<pclass="box">
DoingDOMManipulationsiseasywithJS!
<br><br><i>Incaseweneedsimplethings</i>.
</p>
</div>
<divclass="boxsizer">
<pclass="box">
DoingDOMManipulationsiseasywithJS!
<br><br><i>Incaseweneedsimplethings</i>.
</p>
</div>
<divclass="clear"></div>
</div>
MethodChainingandFluentInterfacesActually,intheprecedingexample,wecanalsogoonestepfurtherandcombineallthreebox-relatedcodestatementsintojustone,whichlookssomethingasfollows:
$('.boxContainer.box')
.append('<br/><br/><i>Incaseweneedsimplethings</i>.')
.parent()
.addClass('boxsizer');
ThisSyntaxPatterniscalledMethodChaininganditishighlyrecommendedbyjQueryandtheJavaScriptcommunityingeneral.MethodChainingispartoftheObjectOrientedImplementationPatternofFluentInterfaceswhereeachmethodrelaysitsinstructioncontexttothesubsequentone.
MostjQuerymethodsthatapplyonajQueryobjectalsoreturnthesameoranewjQueryelementcollectionobject.Thisallowsustochainseveralmethods,notonlyresultinginamorereadableandexpressivecodebutalsoreducingtherequiredvariabledeclarations.
TheCompositePatternThekeyconceptoftheCompositePatternistoenableustotreatacollectionofobjectsinthesamewayaswetreatasingleobjectinstance.Manipulatingacompositionbyusingamethodonthecollectionwillresultinapplyingthemanipulationtoeachpartofit.Suchmethodscanbeappliedsuccessfully,regardlessofthenumberofelementsthatarepartofthecompositecollection,orevenwhenthecollectioncontainsnoelements.
Also,theobjectsofacompositecollectiondonotnecessarilyhavetoprovidetheexactsamemethods.TheCompositeObjectcaneitherexposeonlythemethodsthatarecommonamongtheobjectsofthecollection,orcanprovideanabstractedAPIandappropriatelyhandlethemethoddifferentiationsofeachobject.
Let’scontinuebyexploringhowtheintuitiveAPIthatjQueryexposesishighlyinfluencedfromtheCompositePattern.
HowtheCompositePatternisusedbyjQueryTheCompositePatternisanintegralpartofjQuery’sarchitectureandisappliedfromtheverycore$()functionitself.Eachcalltothe$()functioncreatesandreturnsanelementcollectionobject,whichisoftensimplyreferredasajQueryobject.ThisisexactlywhereweseethefirstprincipleoftheCompositePatterns;infact,insteadofreturningasingleelement,the$()functionreturnsacollectionofelements.
ThejQueryobjectreturnedisanArray-likeobjectthatactsasawrapperobjectandcarriesthecollectionoftheretrievedelements.Italsoexposesanumberofextrapropertiesasfollows:
ThelengthoftheretrievedelementcollectionThecontextthattheobjectwasconstructedTheCSSselectorthatwasusedonthe$()functioncallAprevObjectpropertyincaseweneedtoaccessthepreviouselementcollectionafterchainingamethodcall
TipSimpleArray-likeobjectdefinition
AnArray-likeobjectisaJavaScriptobject{}thathasanumericlengthpropertyandtherespectivenumberofproperties,withsequentialnumericpropertynames.Inotherwords,anArray-likeobjectthathasthelength==2propertyisexpectedtoalsohavetwopropertiesdefined,"0"and"1".Giventheaboveproperties,Array-likeobjectsallowyoutoaccesstheircontentusingsimpleforloops,byutilizingJavaScript’sBracketPropertyAccessor’ssyntax:
for(vari=0;i<obj.length;i++){
console.log(obj[i]);
}
WecaneasilyexperimentwiththejQueryobjectsreturnedfromthe$()functionandinspectthepropertiesdescribedabove,byusingthedevelopertoolsofourfavoritebrowser.Toopenthedevelopertoolsonmostofthem,wejustneedtopressF12onWindowsandLinuxorCmd+Opt+IonMac,andrightafterthat,wecanissuesome$()callsintheconsoleandclickonthereturnedobjectstoinspecttheirproperties.
Inthefollowingfigure,wecanseewhattheresultofthe$('#pageHeader')call,whichweusedintheexampleearlier,lookslikeinFirefoxDeveloperTools:
Theresultofthe$('.boxContainer.box')calllooksasfollows:
ThefactthatjQueryusesArray-likeobjectsasawrapperforthereturnedelementsallowsittoexposesomeextramethodsthatapplyonthecollectionreturned.ThisisachievedthroughprototypicalinheritanceofthejQuery.fnobject,resultingineachjQueryobjectalsohavingaccesstoallthemethodsthatjQueryprovides.ThiscompletestheCompositePattern,whichprovidesmethodsthat,whenappliedtoacollection,areappropriatelyappliedtoeachofitsmembers.BecausejQueryusesArray-likeobjectswithprototypicalinheritance,thesemethodscanbeeasilyaccessedaspropertiesoneachjQueryobject,asshownintheexampleinthebeginningofthechapter:$('#pageHeader').css('font-size','3em');.Moreover,jQueryaddssomeextragoodiestoitsDOMmanipulatingcode,followingthegoalofsmallerandlesserror-pronecode.Forexample,whenusingthejQuery.fn.html()methodtochangetheinnerHTMLofaDOMnodethatalreadycontainschildelements,jQueryfirsttriestoremoveanydataandeventhandlersthatareassociatedwiththechildelements,beforeremovingthemfromthepageandappendingtheprovidedHTMLcode.
Let’stakealookathowjQueryimplementsthesecollection-applicablemethods.Forthistask,wecaneitherdownloadandviewthesourcecodefromtheGitHubpageofjQuery(https://github.com/jquery/jquery/releases),orwecanuseatoolsuchasthejQuerySourceViewerthatisavailableathttp://james.padolsey.com/jquery.
NoteDependingontheversionyouareusing,youmightgetdifferentresultstosomedegree.ThemostrecentstablejQueryversionthatwasreleasedandusedasareferencewhilewritingthisbook,wasv2.2.0.
Oneofthesimplestmethodstodemonstratehowmethodsthatapplytocollectionsareimplemented,isjQuery.fn.empty().YoucaneasilylocateitsimplementationinjQuery’ssourcecodebysearchingfor"empty:"orusingthejQuerySourceViewerandsearchingfor"jQuery.fn.empty".Usingeitheroneofthewayswillbringustothefollowingcode:
empty:function(){
varelem,i=0;
for(;(elem=this[i])!=null;i++){
if(elem.nodeType===1){
//Preventmemoryleaks
jQuery.cleanData(getAll(elem,false));
//Removeanyremainingnodes
elem.textContent="";
}
}
returnthis;
}
Asyoucansee,thecodeisnotcomplexatall.jQueryiteratesoveralltheitemsofthecollectionobject(referredtoasthissinceweareinsidethemethodimplementation)byusingaplainforloop.Foreachitemofthecollection,thatis,anElementNode,itclearsanydata-*propertyvaluesusingthejQuery.cleanData()helperfunction,andrightafterthis,itclearsitscontentbysettingittoanemptystring.
NoteFormoreinformationonthedifferentspecifiedNodeTypes,youcanvisithttps://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType.
ComparingthebenefitsovertheplainDOMAPIToclearlydemonstratethebenefitsthattheCompositePatternprovides,wewillrewriteourinitialexamplewithouttheabstractionsthatjQueryoffers.ByusingjustplainJavaScriptandtheDOMAPI,wecanwriteanequivalentcodethatlooksasfollows:
setTimeout(function(){
varheaderElement=document.getElementById('pageHeader');
if(headerElement){
headerElement.style.fontSize='3em';
}
varboxContainerElement=document.getElementsByClassName('boxContainer')
[0];
if(boxContainerElement){
varinnerBoxElements=
boxContainerElement.getElementsByClassName('box');
for(vari=0;i<innerBoxElements.length;i++){
varboxElement=innerBoxElements[i];
boxElement.innerHTML+='<br/><br/><i>Incaseweneedsimple
things</i>.';
boxElement.parentNode.className+='boxsizer';
}
varclearFloatDiv=document.createElement('div');
clearFloatDiv.className='clear';
boxContainerElement.appendChild(clearFloatDiv);
}
},700);
Onceagain,weusesetTimeoutwithananonymousfunctionandset700millisecondsasthesecondparameter.Insidethefunctionitself,weusedocument.getElementByIdtoretrieveelementsthatareknowntohaveauniqueIDinthepage,andlaterdocument.getElementsByClassNamewhenweneedtoretrievealltheelementsthathaveaspecificclass.WealsouseboxContainerElement.getElementsByClassName('box')toretrievealltheelementswiththeboxclassthataredescendantsoftheelementwiththeboxContainerclass.
Themostobviousobservationisthat,inthiscase,weneeded18linesofcodeinordertoachievethesameresults.Forcomparison,whenusingjQuery,weonlyneeded9linesofcode,that’shalfthenumberoflinesofcodecomparedtothelaterimplementation.UsingthejQuery$()functionwithaCSSselectorwasaneasierwaytoretrievetheelementsthatweneeded,anditalsoensurescompatibilitywithbrowsersthatdonotsupportthegetElementsByClassName()method.However,therearemorebenefitsthanjustthecodelinecountandtheimprovedreadability.AsanimplementeroftheCompositePattern,the$()functionalwaysretrieveselementcollections,makingourcodemoreuniformwhencomparedtothedifferentiatedhandlingofeachgetElement*methodweused.Weusethe$()functioninexactlythesameway,regardlessofwhetherwejustwanttoretrieveanelementwithauniqueIDoranumberofelementswithaspecificclass.
AsanextrabenefitofreturningArray-likeobjects,jQuerycanalsoprovidemoreconvenientmethodstotraverseandmanipulatetheDOM,suchasthosewesawinourfirstexample,.css(),.append()and.parent(),whichareaccessibleaspropertiesofthe
returnedobject.Additionally,jQueryalsooffersmethodsthatabstractmorecomplexusecasessuchas.addClass()and.wrap()thathavenoequivalentmethodsavailableaspartoftheDOMAPI.
SincethereturnedjQuerycollectionobjectsdonotdifferinanythingotherthantheelementstheywrap,wecanuseanymethodofthejQueryAPIinthesameway.Aswesawearlier,thesemethodsapplytoeachelementoftheretrievedcollection,regardlessoftheelementcount.Asaresult,wedonotneedaseparateforlooptoiterateovereachretrievedelementandapplyourmanipulationsindividually;instead,weapplyourmanipulations(forexample,.addClass())directlytothecollectionobject.
Tocontinueprovidingthesameexecutionsafetyguarantiesinthelaterexample,wealsoneedtoaddsomeextraifstatementstocheckfornullvalues.Thisisrequiredbecause,forexample,iftheheaderElementisnotfound,anerrorwilloccurandtherestofthelinesofcodewillneverbeexecuted.Someonecouldarguethatthesechecks,suchasif(headerElement)andif(boxContainerElement),arenotrequiredinthisexampleandcanbeomitted.Thismightappeartobecorrectinthisexample,butactuallythisisamongthetopreasonsforerrorswhiledevelopinglarge-scaleapplications,whereelementsarecreated,inserted,andremovedfromtheDOMtreecontinuously.Unfortunately,programmersinalllanguagesandtargetplatformstendtofirstwritetheirimplementationlogicandfillsuchchecksatalatertime,oftenaftertheygetanerrorwhentestingtheirimplementation.
FollowingtheCompositePattern,evenanemptyjQuerycollectionobject(onethatcontainsnoretrievedelements)isstillavalidcollectionobject,wherewecansafelyapplyanymethodthatjQueryprovides.Asaresult,wedonotneedtheextraifstatementstocheckwhetheracollectionactuallycontainsanyelementbeforeapplyingamethodsuchas.css(),justforthesakeofavoidingaJavaScriptruntimeerror.
Overall,theabstractionsthatjQueryoffersbyusingtheCompositePatternleadtofewerlinesofcode,whichismorereadable,uniform,andwithfewertypo-pronelines(comparetyping$('#elementID')versusdocument.getElementById('elementID')).
UsingtheCompositePatterntodevelopapplicationsNowthatwehaveseenhowjQueryusestheCompositePatterninitsarchitectureandalsodidacomparisononthebenefitsitprovided,let’strytowriteanexampleusecaseofourown.Wewilltrytocoverallconceptsthatwehaveseenearlierinthischapter.WewillstructureourCompositetobeanArray-likeobject,operateontotallydifferentstructuredobjects,provideaFluentAPItoallowchaining,andhavemethodsthatapplyonalltheitemsofthecollection.
AsampleusecaseLet’ssaythatwehaveanapplicationthatatsomepointneedstoperformoperationsonnumbers.Ontheotherhand,theitemsthatitneedstooperateoncomefromdifferentsourcesandarenotuniformatall.Tomakethisexampleinteresting,let’ssupposethatonesourceofdataprovidesplainnumbersandanotheroneprovidesobjectswithaspecificpropertythatholdsthenumberweareinterestedin:
varnumberValues=[2,5,8];
varobjectsWithValues=[
{value:7},
{value:4},
{value:6},
{value:9}
];
Theobjectsreturnedbythesecondsourceofourusecasecouldhaveamorecomplexstructureandprobablysomeextraproperties.Suchchangeswouldn’tdifferentiateourexampleimplementationinanyway,sincewhendevelopingaCompositeweareonlyinterestedinprovidingauniformhandlingoverthecommonpartsbetweenthetargeteditems.
TheCompositeCollectionImplementationLet’sproceedanddefinetheConstructorFunctionandtheprototypethatwilldescribeourCompositeCollectionObject:
functionValuesComposite(){
this.length=0;
}
ValuesComposite.prototype.append=function(item){
if((typeofitem==='object'&&'value'initem)||
typeofitem==='number'){
this[this.length]=item;
this.length++;
}
returnthis;
};
ValuesComposite.prototype.increment=function(number){
for(vari=0;i<this.length;i++){
varitem=this[i];
if(typeofitem==='object'&&'value'initem){
item.value+=number;
}elseif(typeofitem==='number'){
this[i]+=number;
}
}
returnthis;
};
ValuesComposite.prototype.getValues=function(){
varresult=[];
for(vari=0;i<this.length;i++){
varitem=this[i];
if(typeofitem==='object'&&'value'initem){
result.push(item.value);
}elseif(typeofitem==='number'){
result.push(item);
}
}
returnresult;
};
TheValuesComposite()constructorfunctioninourexampleisquitesimple.Wheninvokedwiththenewoperator,itreturnsanemptyobjectwithalengthpropertyequaltozero,representingthatthecollectionitwrapsisempty.
NoteFormoreinformationonthePrototype-basedprogrammingmodelofJavaScript,visithttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript.
Wefirstneedtodefineawaythatwillenableustopopulateourcompositecollectionobjects.Wedefinedtheappendmethodthatcheckswhethertheprovidedparameterisoneofthetypesthatitcanhandle;inthiscase,itappendstheparameterontheCompositeObjectonthenextavailablenumericpropertyandincrementsthelengthpropertyvalue.Forexample,thefirstappendeditem,whetheritisanobjectwithavaluepropertyoraplainnumber,willbeexposedtothe“0”propertyoftheCompositeObjectandwillbeaccessiblewiththeBracketPropertyAccessor’ssyntaxasmyValuesComposition[0].
Theincrementmethodispresentedasasimpleexamplemethodthatcanmanipulatesuchcollectionsbyoperatingoverallthecollectionitems.Itacceptsanumericvalueasaparameterandthenappropriatelyhandlesitbyaddingittoeachitemofourcollection,basedontheirtype.SinceourcompositeisanArray-likeobject,incrementusesaforlooptoiterateoverallthecollectionitemsandeitherincreasestheitem.value(incasetheitemisanobject)ortheactualnumericvaluestored(whenthecollectionitemstoredisanumber).Inthesamemanner,wecancontinueandimplementothermethodsthatwill,
forexample,enableustomultiplythecollectionitemswithaspecificnumber.
InordertoallowchainingthemethodsofourCompositeObject,allthemethodsoftheprototypeneedtoreturnareferencetotheinstanceoftheobject.Weachievethisgoalbysimplyaddingareturnthis;statementasthelastlineforallthemethodsthatmanipulatethecollection,suchasappendandincrement.KeepinmindthatmethodssuchasgetValuesthatdonotmanipulatethecollectionbutareusedtoreturnaresult,bydefinition,can’tbechainedtorelaythecollectionobjectinstancetosubsequentmethodcalls.
Finally,weimplementthegetValuesmethodasaconvenientwaytoretrievetheactualnumericvaluesofalltheitemsinourcollection.Similartotheincrementmethod,thegetValuesmethodabstractsawaythehandlingbetweenthedifferentitemtypesofourcollection.Ititeratesoverthecollectionitems,extractseachnumericvalue,andappendsthemtoaresultarraythatitreturnstoitscaller.
AnexampleexecutionLet’snowseeanactualexamplethatwillusetheCompositeObjectwejustimplemented:
varvaluesComposition=newValuesComposite();
for(vari=0;i<numberValues.length;i++){
valuesComposition.append(numberValues[i]);
}
for(vari=0;i<objectsWithValues.length;i++){
valuesComposition.append(objectsWithValues[i]);
}
valuesComposition.increment(2)
.append(1)
.append(2)
.append({value:3});
console.log(valuesComposition.getValues());
Whentheprecedingcodeisexecutedinabrowser,bywritingthecodeeitherinanexistingpageordirectlyinthebrowser’sconsole,itwilllogaresultthatlooksasfollows:
►Array[4,7,10,9,6,8,11,1,2,3]
WeareusingourdatasourcessuchasthenumberValuesandobjectsWithValuesvariablesthatwereshownearlier.TheprecedingcodeiteratesoverbothofthemandappendstheiritemstoanewlycreatedCompositeObjectinstance.Wethenproceedbyincrementingthevaluesofourcompositecollectionby2.Rightafterthis,wechainthethreeiteminsertionsusingappend,withthefirsttwoappendingnumericvaluesandthethirdappendinganobjectwithavalueproperty.Finally,weusethegetValuesmethodinordertogetanarraywithallthenumericvaluesofourcollectionandlogitinourbrowser’sconsole.
Alternativeimplementations
KeepinmindthataCompositedoesnotneedtobeanArray-likeobject,butiscommonlypreferredsinceJavaScriptmakesiteasytocreatesuchanimplementation.Additionally,Array-likeimplementationsalsohavethebenefitofallowingustoiterateoverthecollectionitemsusingasimpleforloop.
Ontheotherhand,incaseanArray-likeobjectisnotpreferred,wecaneasilyuseapropertyontheCompositeObjecttoholdourcollectionitems.Forexample,thispropertycanbenamedasitemsandbeusedtostoreandaccesstheitemsofthecollectioninsideourmethodsusingthis.items.push(item)andthis.items[i],respectively.
TheIteratorPatternThekeyconceptoftheIteratorPatternistheuseofafunctionwiththesingleresponsibilitytotraverseacollectionandprovideaccesstoitsitems.Thisfunctionisknownastheiteratorandprovidesawaytoaccesstheitemsofthecollection,withoutexposingimplementationspecificsandtheunderlyingdatastructureusedbythecollectionobject.
Iteratorsprovidealevelofencapsulationregardingthewaytheiterationoccurs,decouplingtheiterationovertheitemsofacollectionfromtheimplementationlogicoftheirconsumers.
NoteFormoreinformationontheSingleResponsibilityprinciple,youcanvisithttp://www.oodesign.com/single-responsibility-principle.html.
HowtheIteratorPatternisusedbyjQueryAswesawearlierinthischapter,thejQuerycore$()functionreturnsanArray-likeobjectthatwrapsacollectionofpageelementsanditalsoprovidesaniteratorfunctiontotraverseitandaccesseachelementindividually.ItactuallygoesonestepfurtherandprovidesagenerichelpermethodjQuery.each()thatcaniterateoverarrays,Array-likeobjects,andalsoobjectproperties.
AmoretechnicaldescriptioncanbefoundinjQueryAPIdocumentationpageathttp://api.jquery.com/jQuery.each/,wherethedescriptionofjQuery.each()readsasfollows:
Agenericiteratorfunction,whichcanbeusedtoseamlesslyiterateoverbothobjectsandarrays.ArraysandArray-likeobjectswithalengthproperty(suchasafunction’sargumentsobject)areiteratedbynumericindex,from0tolength-1.Otherobjectsareiteratedviatheirnamedproperties.
ThejQuery.each()helperfunctionisusedinternallyinseveralplacesofthejQuerysourcecode.OneofitsusesisiteratingovertheitemsofajQueryobjectandapplyingmanipulationsoneachofthem,astheCompositePatternsuggests.Asimplesearchforthekeyword.each(reveals56matches.
NoteAsofwritingthisbook,thelateststableversionisv2.2.0andthiswasusedfortheabovestatistics.
WecaneasilytraceitsimplementationinjQuery’ssource,eitherbysearchingfor"each:"(notethattherearetwooccurrences)orusingthejQuerySourceViewerandsearchingfor"jQuery.each()"(likewedidearlierinthischapter):
each:function(obj,callback){
varlength,i=0;
if(isArrayLike(obj)){
length=obj.length;
for(;i<length;i++){
if(callback.call(obj[i],i,obj[i])===false){
break;
}
}
}else{
for(iinobj){
if(callback.call(obj[i],i,obj[i])===false){
break;
}
}
}
returnobj;
}
ThishelperfunctionisalsoaccessibleonanyjQueryobjectbyusingthesameprototypicalinheritancethatwesawearlierformethodssuchas.append().Youcaneasilyfindthecodethatdoesexactlythis,bysearchingfor"jQuery.fn.each()"injQuerySourceViewerordirectlysearchingjQuerysourcecodeforeach:(notethattherearetwooccurrences):
each:function(callback){
returnjQuery.each(this,callback);
}
Usingthemethodversionof".each()"enablesustodirectlyiterateovertheelementsofajQuerycollectionobjectwithamoreconvenientsyntax.
Theexamplecodethatfollowsshowcaseshowthetwoflavorsof.each()canbeusedinourcode:
//usingthehelperfunctiononanarray
$.each([3,5,7],function(index){
console.log(this+1);
});
//usingthemethodonajQueryobject
$('.boxContainer.box').each(function(index){
console.log('I\'mbox#'+(index+1));//indexiszero-based
});
Whenexecuted,theprecedingcodewilllogthefollowingonthebrowser’sconsole:
HowitpairswiththeCompositePatternSincetheCompositePatternencapsulatesacollectionofitemsintoasingleobjectandtheIteratorPatterncanbeusedtoiterateoveranabstracteddatastructure,wecaneasilycharacterizethesetwopatternsascomplementary.
WherecanitbeusedTheIteratorPatterncanbeusedinourapplicationstoabstractthewayweaccessitemsfromadatastructure.Forexample,let’ssupposeweneedtoretrievealltheitemsthataregreaterthan4fromthefollowingtreestructure:
varcollection={
nodeValue:7,
left:{
nodeValue:4,
left:2,
right:{
nodeValue:6,
left:5,
right:9
}
},
right:{
nodeValue:9,
left:8
}
};
Let’snowimplementouriteratorfunction.Sincetreedatastructurescanhavenesting,weendupwiththefollowingrecursiveimplementation:
functioniterateTreeValues(node,callback){
if(node===null||node===undefined){
return;
}
if(typeofnode==='object'){
if('left'innode){
iterateTreeValues(node.left,callback);
}
if('nodeValue'innode){
callback(node.nodeValue);
}
if('right'innode){
iterateTreeValues(node.right,callback);
}
}else{
//itsaleaf,sothenodeisthevalue
callback(node);
}
}
Finally,weendupwithanimplementationthatlooksasfollows:
varvaluesArray=[];
iterateTreeValues(collection,function(value){
if(value>4){
valuesArray.push(value);
}
});
console.log(valuesArray);
Whenexecuted,theprecedingcodewilllogthefollowingonthebrowser’sconsole:
►Array[5,6,9,7,8,9]
Wecanclearlyseethattheiteratorsimplifiedourcode.Wenolongerbotherwiththeimplementationspecificsofthedatastructureusedeverytimeweneedtoaccesssomeitemsthatfulfillcertaincriteria.OurimplementationworksontopofthegenericAPIthattheiteratorexposes,andourimplementationlogicappearsinthecallbackthatweprovidetotheiterator.
Thisencapsulationallowsustodecoupleourimplementationfromthedatastructureused,giventhataniteratorwiththesameAPIwillbeavailable.Forinstance,inthisexample,wecaneasilychangethedatastructureusedtoasortedbinarytreeorasimplearrayandpreserveourimplementationlogicthesame.
SummaryInthischapter,wehadarefresheronJavaScript’sDOMScriptingAPIandjQuery.WewereintroducedtotheCompositePatternandsawhowitisusedbythejQuerylibrary.WesawhowtheCompositePatternsimplifiesourworkflowafterwerewroteourexamplepagewithoutusingjQuery,andlatershowcasedanexampleofusingtheCompositePatterninourapplications.Finally,wewereintroducedtotheIteratorPatternandsawhowwellitpairswhenusedalongwiththeCompositePattern.
NowthatwehavecompletedourintroductiononhowtheCompositePatternplaysanimportantroleinthewayweusejQuerymethodseveryday,wecanmoveontothenextchapterwherewewillshowcasetheObserverPatternandtheconvenientwaytoutilizeitinourpagesusingjQuery.
Chapter2.TheObserverPatternInthischapter,wewillshowcasetheObserverPatternandtheconvenientwayinwhichwecanutilizeitinourpagesusingjQuery.Lateron,wewillalsoexplaintheDelegatedEventObserverPatternvariant,whichwhenproperlyappliedtowebpagescanleadtocodesimplificationsandalsolessenthememoryconsumptionthatapagerequires.
Inthischapter,wewill:
IntroducetheObserverPatternSeehowtheObserverPatternisusedbyjQueryComparetheObserverPatternwithusingtheeventattributesLearnhowtoavoidmemoryleaksfromobserversIntroducetheDelegatedEventObserverPatternandshowcasingitsbenefits
IntroducingtheObserverPatternThekeyconceptoftheObserverPatternisthatthereisanobject,oftenreferredtoastheobservableorthesubject,whoseinternalstatechangesduringitslifetime.Therearealsoseveralotherobjects,referredastheobservers,thatwanttobenotifiedintheeventthatthestateoftheobservable/subjectchanges,inordertoexecutesomeoperations.
Theobserversmayneedtobenotifiedaboutanykindofstatechangeoftheobservableoronlyspecifictypesofchanges.Inthemostcommonimplementation,theobservablemaintainsalistwithitsobserversandnotifiesthemwhenanappropriatestatechangeoccurs.Incaseastatechangeoccurstotheobservable,ititeratesthroughthelistofobserversthatareinterestedforthattypeofstatechangeandexecutesaspecificmethodthattheyhavedefined.
AccordingtothedefinitionoftheObserverPatternandthereferenceimplementationinComputerSciencebooks,theobserversaredescribedasobjectsthatimplementawell-knownprogramminginterface,inmostcases,specifictoeachobservabletheyareinterestedin.Inthecaseofastatechange,theobservablewillexecutethewell-knownmethodofeachobserverasitisdefinedintheprogramminginterface.
NoteFormoreinformationonhowtheObserverPatternisusedintraditional,object-orientedprogramming,youcanvisithttp://www.oodesign.com/observer-pattern.html.
Inthewebstack,theObserverPatternoftenusesplainanonymouscallbackfunctionsasobserversinsteadofobjectswithwell-knownmethods.Anequivalentresult,asdefinedby
theObserverPattern,canbeachievedsincethecallbackfunctionkeepsreferencestothevariablesoftheenvironmentthatitwasdefinedin—apatterncommonlyreferencedasaClosure.ThemainbenefitofusingtheObserverPatternovercallbacksasinvocationorinitializationparametersisthattheObserverPatterncansupportseveralindependenthandlersonasingletarget.
NoteFormoreinformationonclosures,youcanvisithttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures.
TipDefiningasimplecallback
Acallbackcanbedefinedasafunctionthatispassedasanargumenttoanotherfunction/methodorisassignedtoapropertyofanobjectandexpectedtobeexecutedatsomelaterpointoftime.Inthisway,thepieceofcodethatwashandedourcallbackwillinvokeorcallit,propagatingtheresultsofanoperationoreventbacktothecontextwherethecallbackwasdefined.
Sincethepatternofregisteringfunctionsasobservershasproventobemoreflexibleandstraightforwardtoprogram,itcanbefoundinprogramminglanguagesoutsidethewebstackaswell.Otherprogramminglanguagesprovideanequivalentfunctionalitythroughlanguagefeaturesorspecialobjectssuchassubroutines,lambdaexpressions,blocks,andfunctionpointers.Forexample,Pythonalsodefinesfunctionsasfirst-classobjectssuchasJavaScript,enablingthemtobeusedascallbacks,whileC#definesDelegatesasaspecialobjecttypeinordertoachievethesameresult.
TheObserverPatternisanintegralpartofdevelopingwebinterfacesthatrespondtouseractions,andeverywebdeveloperhasusedittosomedegree,evenwithoutnoticingit.Thisisbecausethefirstthingthatawebdeveloperneedstodowhilecreatingarichuserinterfaceistoaddeventlistenerstopageelementsanddefinehowthebrowsershouldrespondtothem.
ThisistraditionallyachievedbyusingtheEventTarget.addEventListener()methodonthepageelementsthatweneedtolistentoforeventssuchasa“click”,andprovidingacallbackfunctionwiththecodethatneedstobeexecutedwhenthateventoccurs.ItisworthmentioningthatinordertosupportolderversionsofInternetExplorer,testingfortheexistenceofEventTarget.attachEvent(),andusingthatinstead,isrequired.
NoteFormoreinformationontheaddEventListener()andattachEvent()methods,youcanvisithttps://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListenerandhttps://developer.mozilla.org/en-US/docs/Web/API/EventTarget/attachEvent.
HowitisusedbyjQueryThejQuerylibraryheavilyusestheObserverPatterninseveralpartsofitsimplementation,eitherdirectlybyusingtheaddEventListenermethodorcreatingitsownabstractionoverit.Moreover,jQueryoffersaseriesofabstractionsandconvenientmethodstomakeworkingwiththeObserverPatterneasieronthewebandalsousessomeoftheminternallytoimplementothermethodsaswell.
ThejQueryonmethodThejQuery.fn.on()methodisthecentraljQuerymethodforattachingeventhandlerstoelements,providinganeasywaytoadopttheObserverPattern,whilekeepingourcodeeasytoreadandreason.ItattachestherequestedeventhandleroveralltheelementsofacompositejQuerycollectionobjectreturnedbythe$()function.
SearchingforjQuery.fn.oninjQuery’sSourceViewer(whichisavailableathttp://james.padolsey.com/jquery),ordirectlysearchingjQuery’ssourcecodeforon:function(thefirstcharacterisatab),willleadustothemethod’sdefinition,whichcounts67linesofcode.Actually,thefirst55linesoftheinternalonfunctionarejusthandlingallthedifferentwaysthatthejQuery.fn.on()methodcanbeinvoked;nearitsend,wecanseethatitactuallyusestheinternalmethodjQuery.event.add():
jQuery.fn.extend({
on:function(types,selector,data,fn){
returnon(this,types,selector,data,fn);
}
});
functionon(elem,types,selector,data,fn,one){
/*55linesofcodehandlingthemethodoverloads*/
returnelem.each(function(){
jQuery.event.add(this,types,fn,data,selector);
});
}
ThejQuery.eventobjectistheone-placestopforeventhandlinginjQueryanditsimplementationcountsaround443linesofcode.Itholdsseveralhelperfunctionsformanagingeventssuchasadd,dispatch,fix,handlers,remove,simulate,andtrigger.AllthesefunctionsareusedinternallybyjQueryitselfwherevertheObserverPatternappearsormanagingeventsisrequired.
SearchingforjQuery.event.addinjQuery’sSourceViewerorjQuery.event=directlyinjQuery’ssourcecode,willleadustotherelativelylongimplementationofthehelperfunctionthatcountsaround107linesofcodeinjQueryv2.2.0.Thefollowingcodesnippetshowsatrimmeddownversionofthatmethod,wheresomecoderelatedtothetechnicalimplementationofjQueryandnotrelatedtotheObserverPatternhasbeenremovedforclarity:
add:function(elem,types,handler,data,selector){
/*...4linesofcode…*/
elemData=dataPriv.get(elem);
/*...13linesofcode…*/
//MakesurethatthehandlerhasauniqueID,
//usedtofind/removeitlater
if(!handler.guid){
handler.guid=jQuery.guid++;
}
//Inittheelement'seventstructureandmainhandler,
//ifthisisthefirst
if(!(events=elemData.events)){
events=elemData.events={};
}
/*...9linesofcode…*/
//Handlemultipleeventsseparatedbyaspace
types=(types||"").match(rnotwhite)||[""];
t=types.length;
while(t--){
/*...30linesofcode…*/
//Inittheeventhandlerqueueifwe'rethefirst
if(!(handlers=events[type])){
handlers=events[type]=[];
handlers.delegateCount=0;
//OnlyuseaddEventListenerifthespecialeventshandler
//returnsfalse
if(!special.setup||special.setup.call(elem,data,
namespaces,eventHandle)===false){
if(elem.addEventListener){
elem.addEventListener(type,eventHandle);
}
}
}
/*...9linesofcode…*/
//Addtotheelement'shandlerlist,delegatesinfront
if(selector){
handlers.splice(handlers.delegateCount++,0,handleObj);
}else{
handlers.push(handleObj);
}
/*...3linesofcode…*/
}
}
Now,let’sseehowtheObserverPatternisimplementedbyjQuery.event.add(),byreferringtotheprecedinghighlightedcode.
ThehandlervariableintheargumentsofthejQuery.event.add()methodstoresthefunctionthatwasoriginallypassedasanargumenttothejQuery.fn.on()method.Wecanrefertothisfunctionasourobserverfunction,sinceitisexecutedwhenthe
appropriateeventfiresontheelementthatitwasattachedto.
Inthefirsthighlightedcodearea,jQuerycreatesandassignsaguidpropertytotheobserverfunctionthatisstoredinthehandlervariable.KeepinmindthatassigningpropertiestofunctionsispossibleinJavaScript,sincefunctionsarefirst-classobjects.ThejQuery.guid++statementisexecutedrightaftertheassignmentoftheoldvalueandisrequiredsincejQuery.guidisapage-widecounterusedbyjQueryandjQuerypluginsinternally.TheguidpropertyontheobserverfunctionisusedasawaytoidentifyandlocatetheobserverfunctioninsidetheobserverlistthatjQueryhasforeachelement.Forexample,itisusedbythejQuery.fn.off()methodtolocateandremoveanobserverfunctionfromtheobserverlistassociatedwithanelement.
TipjQuery.guidisapage-widecounterthatisusedbythepluginsandjQueryitselfasacentralizedwaytoretrieveuniqueintegerIDs.ItisoftenusedtoassignuniqueIDstoelements,objects,andfunctions,inordertomakeiteasiertolocatethemincollections.ItistheresponsibilityofeachimplementerthatretrievesandusesthecurrentvalueofjQuery.guidtoalsoincreasethepropertyvalue(byone)aftereachuse.Otherwise,andsincethisisapage-widecounterthatisusedbybothjQuerypluginsandjQuerythemselvesforidentification,thepagewillprobablyfacemalfunctionsthatarehardtodebug.
Inthesecondandthirdhighlightedcodeareas,jQueryinitializesanarraytoholdtheobserverlistsforeachindividualeventthatmayfireonthatelement.OnethingtonoteinthesecondhighlightedcodeareaisthattheobserverlistsfoundintheelemDatavariablearenotapropertyontheactualDOMelement.AsshowninthedataPriv.get(elem)statement,nearthestartofthejQuery.event.add()method,jQueryusesseparatemappingobjectstoholdtheassociationsbetweenDOMelementsandtheirobserverlists.Byusingthisdatacachemechanism,jQueryisabletoavoidpollutingtheDOMelementswiththeextrapropertiesthatareneededbyitsimplementation.
NoteYoucaneasilylocatethedatacachemechanismimplementationinthesourcecodeofjQuerybysearchingforfunctionData().ThiswillbringyoutotheconstructorfunctionoftheDataclassthatisalsofollowedbytheimplementationoftheclassmethodsthataredefinedintheData.prototypeobject.Formoreinformation,youcanvisithttp://api.jquery.com/data.
ThenexthighlightedcodeareaiswherejQuerycheckswhethertheEventTarget.addEventListener()methodisactuallyavailableforthatelementandthenusesittoaddtheeventlistenertotheelement.Inthefinalhighlightedcodearea,jQueryaddstheobserverfunctiontoitsinternallist,whichholdsalltheobserversofthesameeventtypethatareattachedtothatspecificelement.
NoteDependingontheversionyouareusing,youmightgetdifferentresultstosomedegree.
ThemostrecentstablejQueryversionreleasedandusedasreferencewhilewritingthisbookwasv2.2.0.
Incaseyouneedtoprovidesupportforolderbrowsers,forexample,InternetExplorerlowerthanversion9,thenyoushouldusethev1.xversionsofjQuery.Thelatestversionasofthewritingofthisbookwasv1.12.0,whichofferstheexactsameAPIasthev2.2.xversions,butalsohastherequiredcodetoworkonolderbrowsers.
Inordertocovertheimplementationinconsistenciesofolderbrowsers,theimplementationofjQuery.event.add()injQueryv1.xisabitlongerandmorecomplex.OneofthereasonsforthisisbecausejQueryalsoneedstotestwhetherEventTarget.addEventListener()isactuallyavailableinthebrowserthatitisrunningandtrytouseEventTarget.attachEvent()ifthisisnotthecase.
Aswesawintheprecedingcode,thejQueryimplementationfollowstheoperationmodelthattheObserverPatterndescribes,butitalsoincorporatessomeimplementationtricksinordertomakeitworkmoreefficientlywiththeAPIsavailabletowebbrowsers.
Thedocument-readyobserverAnotherconvenientmethodthatjQueryoffers,whichiswidelyusedbydevelopers,isthe$.fn.ready()method.ThismethodacceptsafunctionparameterandexecutesitonlyaftertheDOMtreeofthepagehasbeenfullyloaded.Suchathingcanbeusefulincaseyourcodeisnotloadedlastinthepageandyoudon’twanttoblocktheinitialpagerender,ortheelementsthatitneedstomanipulatearedefinedlaterthanitsown<script>tag.
NoteKeepinmindthatthe$.fn.ready()methodworksslightlydifferentlythanthewindow.onloadcallbackandthe“load”eventofthepage,whichwaituntilalltheresourcesofthepageareloaded.Formoreinformation,youcanvisithttp://api.jquery.com/ready.
Thefollowingcodedemonstratesthemostcommonwaytousethe$.fn.ready()method:
$(document).ready(function(){
/*thiscodewillexecuteonlyafterthepagehasbeenfullyloaded*/
})
IfwetrytolocatetheimplementationofjQuery.fn.ready,wewillseethatitactuallyusesjQuery.ready.promiseinternallytowork:
jQuery.fn.ready=function(fn){
//Addthecallback
jQuery.ready.promise().done(fn);
returnthis;
};
/*…alotlinesofcodeinbetween*/
jQuery.ready.promise=function(obj){
if(!readyList){
readyList=jQuery.Deferred();
//Catchcaseswhere$(document).ready()iscalled
//afterthebrowsereventhasalreadyoccurred.
//Support:IE9-10only
//OlderIEsometimessignals"interactive"toosoon
if(document.readyState==="complete"||(document.readyState!==
"loading"&&!document.documentElement.doScroll)){
//Handleitasynchronouslytoallow…todelayready
window.setTimeout(jQuery.ready);
}else{
//Usethehandyeventcallback
document.addEventListener("DOMContentLoaded",completed);
//Afallbacktowindow.onload,thatwillalwayswork
window.addEventListener("load",completed);
}
}
returnreadyList.promise(obj);
};
Asyoucanseeintheprecedinghighlightedcodeareasoftheimplementation,jQueryusesaddEventListenertoobservewhentheDOMContentLoadedeventisfiredonthedocumentobject.Moreover,toensurethatitwillworkacrossawiderangeofbrowsers,italsoobservesfortheloadeventtobefiredonthewindowobject.
ThejQuerylibraryalsoprovidesshortermethodstoaddtheabovefunctionalityinyourcode.Sincetheaforementionedimplementationdoesnotactuallyneedareferencetothedocument,wecaninsteadjustwrite$().ready(function(){/*...*/}).Therealsoexistsanoverloadofthe$()functionthatachievesthesameresult,whichisusedlike$(function(){/*...*/}).ThesetwoalternativewaystousejQuery.fn.readyhavebeenheavilycriticizedamongdevelopers,sincetheycommonlyleadtomisunderstandings.Thesecond,shorterversioninparticularcanleadtoconfusion,sinceitlookslikeanImmediatelyInvokedFunctionExpression(IIFE),apatternthatJavaScriptdevelopersuseheavilyandhavelearnedtorecognize.Infact,itonlydiffersbyonecharacter($)andasaresult,itsuseisnotsuggestedbeforeadiscussionwiththerestofyourdeveloperteam.
NoteThe$.fn.ready()methodisalsocharacterizedasamethodthatprovidesaneasywaytoimplementtheLazyInitialization/ExecutionPatterninourcode.Thecoreconceptofthispatternistopostponetheexecutionofapieceofcodeorloadaremoteresourceatalaterpointoftime.Forexample,wecanwaitforthepagetobefullyloadeduntilweaddourobserversorwaitforacertaineventtohappenbeforedownloadingawebresource.
DemonstrateasampleusecaseInordertoseetheObserverPatterninaction,wewillcreateanexampleshowcasingaskeletonimplementationofadashboard.Inourexample,theuserwillbeabletoaddinformationboxestohisdashboardrelatedtosomesampleitemsandcategoriesthatareavailableforselectionontheheader.
Ourexamplewillhavethreepredefinedcategoriesforouritems:Products,Sales,andAdvertisements.Eachofthesecategorieswillhaveaseriesofrelateditemsthatwillappearinthearearightbelowthecategoryselector.Theuserwillbeabletoselectthedesiredcategorybyusingadrop-downselectorandthiswillchangethevisibleselectionitemsofthedashboard.
Ourdashboardwillinitiallycontainahintinformationboxaboutthedashboardusage.Wheneverauserclicksononeofthecategoryitems,anewinformationboxwillappearinourthree-columnlayoutdashboard.Intheprecedingimage,theuserhasaddedtwonewinformationboxesforProductBandProductDbyclickingontheassociatedbuttons.
Theuserwillalsobeabletodismissanyoftheseinformationboxesbyclickingonaredclosebuttononthetop-rightofeachinformationbox.Intheprecedingimage,theuserdismissedtheProductDinformationbox,thenaddedinformationboxesfortheAdvertisement3andlaterthe1st,2nd,and3rdweekitemsoftheSalescategory.
Byjustreadingtheabovedescription,wecaneasilyisolatealltheuserinteractionsthatarerequiredfortheimplementationofourdashboard.WewillneedtoaddobserversforeachoneoftheseuserinteractionsandwritecodeinsidethecallbackfunctionsthatexecutetheappropriateDOMmanipulations.
Indetail,ourcodewillneedto:
ObservechangesdonetothecurrentlyselectedelementandrespondtosucheventbyhidingorrevealingtheappropriateitemsObservetheclicksoneachitembuttonandrespondbyaddinganewinformationboxObservetheclicksontheclosebuttonofeachinformationboxandrespondbyremovingitfromthepage
Nowlet’sproceedandreviewtheHTML,CSS,andJavaScriptcoderequiredfortheprecedingexample.Let’sstartwiththeHTMLcodeandforreference,let’ssaythatwesaveditinafilenamedDashboardExample.html,asfollows:
<!DOCTYPEhtml>
<html>
<head>
<title>DashboardExample</title>
<linkrel="stylesheet"type="text/css"href="dashboard-example.css">
</head>
<body>
<h1id="pageHeader">DashboardExample</h1>
<divclass="dashboardContainer">
<sectionclass="dashboardCategories">
<selectid="categoriesSelector">
<optionvalue="0"selected>Products</option>
<optionvalue="1">Sales</option>
<optionvalue="2">Advertisements</option>
</select>
<sectionclass="dashboardCategory">
<button>ProductA</button>
<button>ProductB</button>
<button>ProductC</button>
<button>ProductD</button>
<button>ProductE</button>
</section>
<sectionclass="dashboardCategoryhidden">
<button>1stweek</button>
<button>2ndweek</button>
<button>3rdweek</button>
<button>4thweek</button>
</section>
<sectionclass="dashboardCategoryhidden">
<button>Advertisement1</button>
<button>Advertisement2</button>
<button>Advertisement3</button>
</section>
<divclass="clear"></div>
</section>
<sectionclass="boxContainer">
<divclass="boxsizer">
<articleclass="box">
<headerclass="boxHeader">
Hint!
<buttonclass="boxCloseButton">✖</button>
</header>
Pressthebuttonsabovetoaddinformationboxes…
</article>
</div>
</section>
<divclass="clear"></div>
</div>
<scripttype="text/javascript"src="jquery.js"></script>
<scripttype="text/javascript"src="dashboard-example.js">
</script>
</body>
</html>
IntheprecedingHTML,weplacedallourdashboard-relatedelementsinsidea<div>elementwiththedashboardContainerCSSclass.Thiswillenableustohaveacentricstartingpointtosearchforourdashboard’selementsandalsoscopeourCSS.Insideit,wedefinetwo<section>elementsinordertodividethedashboardintologicalareasusingsomeHTML5semanticelements.
Thefirst<section>withthedashboardCategoriesclassisusedtoholdthecategoriesselectorofourdashboard.Insideit,wehavea<select>elementwiththeIDcategoriesSelectorthatisusedtofilterthevisiblecategoryitemsandthreesubsectionswiththedashboardCategoryclassthatareusedtowrapthe<button>elementsthatwillpopulatethedashboardwithinformationboxeswhenclicked.Twoofthemalsohavethehiddenclasssothatonlythefirstoneisvisiblewhenthepageloadsbymatchingtheinitiallyselectedoption(<option>)ofthecategoryselector.Also,attheendofthefirstsection,wealsoaddeda<div>withtheclearclassthat,aswesawinthefirstchapter,willbeusedtoclearthefloated<button>elements.
Thesecond<section>withtheboxContainerclassisusedtoholdtheinformationboxesofourdashboard.Initially,itcontainsonlyonewithahintabouthowtousethedashboard.Weusea<div>elementwiththeboxsizerclasstosettheboxdimensionsandanHTML5<article>elementwiththeboxclasstoaddtherequiredborderpaddingandshadow,similartotheboxelementsfromthefirstchapter.
Eachinformationbox,besidesitscontent,alsocontainsa<header>elementwiththeboxHeaderclassanda<button>elementwiththeboxCloseButtonclassthat,whenclicked,removestheinformationboxthatcontainsit.Wealsousedthe✖HTMLcharactercodeasthebutton’scontentinordertogetabetter-looking“x”markandavoid
usingaseparateimageforthatpurpose.
Lastly,sincetheinformationboxesarealsofloated,wealsoneeda<div>withtheclearclassattheendoftheboxContainer.
Inthe<head>oftheprecedingHTML,wealsoreferenceaCSSfilenamedasdashboard-example.csswiththefollowingcontent:
.dashboardCategories{
margin-bottom:10px;
}
.dashboardCategoriesselect,
.dashboardCategoriesbutton{
display:block;
width:200px;
padding:5px3px;
border:1pxsolid#333;
margin:3px5px;
border-radius:3px;
background-color:#FFF;
text-align:center;
box-shadow:01px1px#777;
cursor:pointer;
}
.dashboardCategoriesselect:hover,
.dashboardCategoriesbutton:hover{
background-color:#DDD;
}
.dashboardCategoriesbutton{
float:left;
}
.box{
padding:7px10px;
border:solid1px#333;
margin:5px3px;
box-shadow:01px2px#777;
}
.boxsizer{
float:left;
width:33.33%;
}
.boxHeader{
padding:3px10px;
margin:-7px-10px7px;
background-color:#AAA;
box-shadow:01px1px#999;
}
.boxCloseButton{
float:right;
height:20px;
width:20px;
padding:0;
border:1pxsolid#000;
border-radius:3px;
background-color:red;
font-weight:bold;
text-align:center;
color:#FFF;
cursor:pointer;
}
.clear{clear:both;}
.hidden{display:none;}
AsyoucanseeinourCSSfile,firstofallweaddsomespacebelowtheelementwiththedashboardCategoriesclassandalsodefinethesamestylingforthe<select>elementandthebuttonsinsideit.Inordertodifferentiateitfromthedefaultbrowserstyling,weaddsomepadding,aborderwithroundedcorners,adifferentbackgroundcolorwhenhoveringthemousepointer,andsomespaceinbetweenthem.Wealsodefinethatour<select>elementshouldbedisplayedaloneinitsrowasablockandthatthecategoryitembuttonsshouldfloatnexttoeachother.WeagainusetheboxsizerandboxCSSclasses,aswedidinChapter1,ARefresheronjQueryandtheCompositePattern;thefirstonetocreateathree-columnlayoutandthesecondonetoactuallyprovidethestylingofaninformationbox.WecontinuebydefiningtheboxHeaderclassthatisappliedtothe<header>elementsofourinformationboxes,anddefinesomepadding,agreybackgroundcolor,alightshadow,andalsosomenegativemarginssothatitcounterbalancestheeffectofthebox’spaddingsandplacesitselfnexttoitsborder.
Tocompletethestylingoftheinformationboxes,wealsodefinetheboxCloseButtonCSSclassthat(i)floatsthebox’sclosebuttonstotheupper-rightcornerinsidethebox<header>,(ii)definesa20pxwidthandheight,(iii)overridesthedefaultbrowser’s<button>stylingtozeropadding,and(iv)addsasingle-pixelblackborderwithroundedcornersandaredbackgroundcolor.Lastly,likeinChapter1,ARefresheronjQueryandtheCompositePatternwedefinetheclearutilityCSSclasstopreventtheelementfrombeingplacednexttothepreviousfloatingelementsandalsodefinethehiddenclassasaconvenientwayofhidingelementsofthepage.
InourHTMLfile,wereferencethejQuerylibraryitselfandalsoaJavaScriptfilenamedasdashboard-example.jsthatcontainsourdashboardimplementation.Followingthebestpracticesofcreatingperformantwebpages,wehaveplacedthemrightbeforethe</body>tag,inordertoavoiddelayingtheinitialpagerendering:
$(document).ready(function(){
$('#categoriesSelector').change(function(){
var$selector=$(this);
varselectedIndex=+$selector.val();
var$dashboardCategories=$('.dashboardCategory');
var$selectedItem=$dashboardCategories.eq(selectedIndex).show();
$dashboardCategories.not($selectedItem).hide();
});
functionsetupBoxCloseButton($box){
$box.find('.boxCloseButton').click(function(){
$(this).closest('.boxsizer').remove();
});
}
//maketheclosebuttonofthehintboxwork
setupBoxCloseButton($('.box'));
$('.dashboardCategorybutton').on('click',function(){
var$button=$(this);
varboxHtml='<divclass="boxsizer"><articleclass="box">'+
'<headerclass="boxHeader">'+
$button.text()+
'<buttonclass="boxCloseButton">✖'+
'</button>'+
'</header>'+
'Informationboxregarding'+$button.text()+
'</article></div>';
$('.boxContainer').append(boxHtml);
setupBoxCloseButton($('.box:last-child'));
});
});
Wehaveplacedallourcodeinsidea$(document).ready()call,inordertodelayitsexecutionuntiltheDOMtreeofthepageisfullyloaded.Thiswouldbeabsolutelyrequiredifweplacedourcodeinthe<head>element,butitisalsoabestpracticethatisgoodtofollowinanycase.
WefirstaddanobserverforthechangeeventonthecategoriesSelectorelementusingthe`$.fn.change()`method,whichisactuallyashorthandmethodforthe$.fn.on('change',/*…*/)method.InjQuery,thevalueofthethiskeywordinsideafunctionthatisusedasanobserverholdsareferencetotheDOMelementthattheeventwasfired.ThisappliestoalljQuerymethodsthatregisterobservers,fromthecore$.fn.on()tothe$.fn.change()and$.fn.click()convenientmethods.Soweusethe$()functiontomakeajQueryobjectwiththe<select>elementandstoreitinthe$selectorvariable.Then,weuse$selector.val()toretrievethevalueoftheselected<option>andcastittoanumericvaluebyusingthe+operator.Rightafterthis,weretrievethe<section>elementsofdashboardCategoryandcachetheresulttothe$dashboardCategoriesvariable.Then,weproceedbyfindingandrevealingthecategorywhosepositionisequaltothevalueoftheselectedIndexvariableandalsostoretheresultingjQueryobjecttothe$selectedItemvariable.Finally,weareusingthe$selectedItemvariablewiththe$.fn.not()methodtoretrieveandhideallthecategoryelements,exceptfromtheonewejustrevealed.
Inthenextcodesection,wedefinethesetupBoxCloseButtonfunctionthatwillbeusedtoinitializethefunctionalityoftheclosebutton.ItexpectsajQueryobjectwiththeboxelementsasaparameter,andforeachofthem,searchestheirdescendantsforthe
boxCloseButtonCSSclassthatweuseontheclosebuttons.Using$.fn.click(),whichisaconvenientmethodfor$.fn.on('click',/*fn*/),weregisterananonymousfunctiontobeexecutedwheneveraclickeventisfiredthatusesthe$.fn.closest()methodtofindthefirstancestorelementwiththeboxsizerclassandremovesitfromthepage.Rightafterthis,wecallthisfunctiononcefortheboxelementsthatalreadyexistedinthepageatthetimewhenthepagewasloaded.Inthiscase,theboxelementwiththeusagehint.
NoteAnextrathingtokeepinmindwhenusingthe$.fn.closest()methodisthatitbeginstestingthegivenselectorfromthecurrentelementofthejQuerycollectionbeforeproceedingwithitsancestorelements.Formoreinformation,youcanvisititsdocumentationathttp://api.jquery.com/closest.
Inthefinalcodesection,weusethe$.fn.on()methodtoaddanobserverfortheclickeventoneachofthecategorybuttons.Inthiscase,insidetheanonymousobserverfunction,weusethethiskeyword,whichholdstheDOMelementofthe<button>thatwasclicked,andusethe$()methodtocreateajQueryobjectandcacheitsreferenceinthe$buttonvariable.Rightafterthis,weretrievethebutton’stextcontentusingthe$.fn.text()methodandalongwithit,constructtheHTMLcodefortheinformationbox.Fortheclosebutton,weusethe✖HTMLcharactercodethatwillberenderedasaprettier“X”icon.ThetemplatewecreatedisbasedontheHTMLcodeoftheinitiallyvisiblehintbox;fortheneedsofthischapter’sexample,weuseplainstringconcatenation.Lastly,weappendthegeneratedHTMLcodeforourboxtotheboxContainer,andsinceweexpectittobethelastelement,weusethe$()functiontofinditandprovideitasaparametertothesetupBoxCloseButton.
HowitiscomparedwitheventattributesBeforetheEventTarget.addEventListener()wasdefinedintheDOMLevel2Eventsspecification,theeventlistenerswereregisteredeitherbyusingtheeventattributesthatareavailableforHTMLelementsortheelementeventpropertiesthatareavailableforDOMnodes.
NoteFormoreinformationontheDOMLevel2Eventspecificationandeventattributes,youcanvisithttp://www.w3.org/TR/DOM-Level-2-Eventsandhttps://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Event_attributes,respectively.
TheeventattributesareasetofattributesthatareavailabletoHTMLelementsandprovideadeclarativewayofdefiningpiecesofJavaScriptcode(preferablyfunctioncalls)thatshouldbeexecutedwhenaspecificeventistriggeredonthatelement.Becauseoftheirdeclarativenatureandhowsimplytheycanbeused,thisisoftenthefirstwaythatnewdevelopersgetintroducedtoeventsinwebdevelopment.
Ifweusedeventattributesintheaboveexample,thentheHTMLcodefortheclosebuttonsintheinformationboxeswilllookasfollows:
<articleclass="box">
<headerclass="boxHeader">
Hint!
<buttononclick="closeInfoBox();"
class="boxCloseButton">✖</button>
</header>
Pressthebuttonsabovetoaddinformationboxes…
</article>
Also,weshouldchangethetemplatethatisusedtocreatenewinformationboxesandexposethecloseInfoBoxfunctiononthewindowobject,inorderforittobeaccessiblefromtheHTMLeventattribute:
window.closeInfoBox=function(){
$(this).closest('.boxsizer').remove();
};
SomeofthedisadvantagesofusingeventattributesovertheObserverPatternare:
ItmakesithardertodefinemultipleseparateactionsthathavetobeexecutedwhenaneventfiresonanelementItmakestheHTMLcodeofthepagebiggerandlessreadableItisagainsttheseparationofconcernsprinciple,sinceitaddsJavaScriptcodeinsideourHTML,possiblymakingabughardertotrackandfixMostofthetime,itleadstothefunctionsbeingcalledintheeventattributegettingexposedtotheglobalwindowobject,thereby“polluting”theglobalnamespace
UsingtheelementeventpropertieswouldnotrequireanychangestoourHTML,keepingalltheimplementationinourJavaScriptfiles.Thechangesrequiredinour
setupBoxCloseButtonfunctionwillmakeitlookasfollows:
functionsetupBoxCloseButton($box){
var$closeButtons=$box.find('.boxCloseButton');
for(vari=0;i<$closeButtons.length;i++){
$closeButtons[i].onclick=function(){
this.onclick=null;
$(this).closest('.boxsizer').remove();
};
}
}
Notethat,forconvenience,wearestillusingjQueryforDOMmanipulations,buttheresultingcodestillhassomeoftheaforementioneddisadvantages.Moreimportantly,inordertoavoidmemoryleaks,wearealsorequiredtoremovethefunctionassignedtotheonclickpropertybeforeremovingtheelementfromthepage,ifitcontainsreferencestotheDOMelementthatitisappliedon.
Usingthetoolsthattoday’sbrowsersoffer,wecanevenmatchtheconveniencethatthedeclarativenatureofeventattributesoffers.Inthefollowingimage,youcanseehowtheFirefoxdevelopertoolsprovideuswithhelpfulfeedbackwhenweusethemtoinspectapageelementthathasaneventlistenerattached:
Asyoucanseeintheprecedingimage,alltheelementsthathaveobserversattachedalsohaveanevsignrightnexttothem,whichwhenclicked,displaysadialogshowingalltheeventlistenersthatarecurrentlyattached.Tomakeourdevelopingexperienceevenbetter,wecandirectlyseethefileandthelinethatthesehandlersweredefinedin.Moreover,wecanclickontheminordertoexpandandrevealtheircode,orclickonthesigninfrontofthemtonavigatetotheirsourceandaddbreakpoints.
OneofthebiggestbenefitsofusingtheObserverPatternovereventattributesisclearlyvisibleinthecasewhereweneedtotakemorethanoneactionwhenacertaineventhappens.Supposethatwealsoneedtoaddanewfeatureinourexampledashboard,whichwouldpreventauserfromaccidentallydouble-clickingacategoryitembuttonandaddingthesameinformationboxtwicetothedashboard.Thenewimplementationshouldideallybecompletelyindependentfromtheexistingone.UsingtheObserverPattern,allweneedtodoisaddthefollowingcodethatobservesforbuttonclicksanddisablesthatbuttonfor700milliseconds:
$(document).ready(function(){
$('.dashboardCategorybutton').on('click',function(){
var$button=$(this);
$button.prop('disabled',true);
setTimeout(function(){
$button.prop('disabled',false);
},700);
});
});
TheprecedingcodeisindeedcompletelyindependentfromthebasicimplementationandwecouldplaceitinsidethesameoradifferentJSfileandloadittoourpage.Thiswouldbemoredifficultwhenusingeventattributes,sinceitwouldrequireustodefinebothactionsatthesametimeinsidethesameeventhandlerfunction;asaresult,itwouldstronglycouplethetwoindependentactions.
AvoidmemoryleaksAswesawearlier,therearesomestrongadvantagesofusingtheObserverPatterntohandleeventsonawebpage.WhenusingtheEventTarget.addEventListener()methodtoaddanobservertoanelement,wealsoneedtokeepinmindthatinordertoavoidmemoryleaks,wealsohavetocalltheEventTarget.removeEventListener()methodbeforeremovingsuchelementsfromthepagesothattheobserversarealsoremoved.
NoteFormoreinformationonremovingeventlistenersfromelements,youcanvisithttps://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener,orforthejQueryequivalentmethod,visithttp://api.jquery.com/off/.
ThejQuerylibrarydevelopersunderstoodthatsuchanimplementationconcerncouldeasilybeforgottenornothandledproperly,therebymakingtheadoptionoftheObserverPatternlookmorecomplex,sotheydecidedtoencapsulatetheappropriatehandlinginsidethejQuery.eventimplementation.Asaresult,whenusinganyeventhandlingjQuerymethod,suchasthecore$.fn.on()oranyoftheconvenientmethodssuchas$.fn.click()or$.fn.change(),theobserverfunctionsaretrackedbyjQueryitselfandareproperlyunregisteredifwelaterdecidetoremovetheelementfromthepage.AswesawearlierintheimplementationofjQuery.event,jQuerystoresareferencetotheobserversofeachelementinaseparatemappingobject.EverytimeweauseajQuerymethodthatremovesDOMelementsfromthepage,itfirstmakessuretoremoveanyobserversattachedtothoseelementsoranyofthedescendantelements,bycheckingthemappingobject.Asaresult,theexamplecodeweusedearlierisnotcausingmemoryleakseventhoughwearenotusinganymethodthatexplicitlyremovestheobserversweaddtothecreatedelements.
TipBecarefulwhenmixingjQueryandplainDOMmanipulations
EventhoughalljQuerymethodskeepyousafefrommemoryleakscausedfromobserversthatareneverunregistered,keepinminditcan’tprotectyouifyouremoveelementsusingplainmethodsfromtheDOMAPI.IfmethodssuchasElement.remove()andElement.removeChild()areusedandtheremovedelementsortheirdescendantshaveobserversattached,thentheyarenotgoingtobeunregisteredautomatically.ThesameapplieswhenassigningtotheElement.innerHTMLproperty.
IntroducingtheDelegatedEventObserverPatternNowthatwehavelearnedsomeadvanceddetailsabouthowtousetheObserverPatternusingjQuery,wewillgetintroducedtoaspecialvariationofitthatfitsperfectlytothewebplatformandprovidessomeextrabenefits.TheDelegatedEventObserverPattern(orsimplyDelegateObserverPattern)isoftenusedinwebdevelopmentanditutilizesthebubblingfeaturethatmosteventsthatarefiredonDOMelementshave.Forexample,whenweclickonapageelement,theclickeventisimmediatelyfiredonit,andrightafterthisitalsofiresonallitsparentelementsuntilitreachestherootofourHTMLdocument.UsingaslightlydifferentoverloadedversionofthejQuery’s$.fn.onmethod,wecaneasilycreateandattachobserversonpageelementsfordelegatedeventsthatarefiredonspecificchildelements.
NoteTheterm“EventDelegation”describestheprogrammingpatternwherethehandlerofaneventisnotattacheddirectlytotheelementofinterest,butisinsteadattachedtooneofitsancestorelements.
HowitsimplifiesourcodeReimplementingourdashboardexampleusingtheDelegatedEventObserverPatternwillrequireustochangeonlythecodeoftheincludedJavaScriptfiletothefollowing:
$(document).ready(function(){
$('#categoriesSelector').change(function(){
var$selector=$(this);
varselectedIndex=+$selector.val();
var$dashboardCategories=$('.dashboardCategory');
var$selectedItem=$dashboardCategories.eq(selectedIndex).show();
$dashboardCategories.not($selectedItem).hide();
});
$('.dashboardCategories').on('click','button',function(){
var$button=$(this);
varboxHtml='<divclass="boxsizer"><articleclass="box">'+
'<headerclass="boxHeader">'+
$button.text()+
'<buttonclass="boxCloseButton">✖'+
'</button>'+
'</header>'+
'Informationboxregarding'+$button.text()+
'</article></div>';
$('.boxContainer').append(boxHtml);
});
$('.boxContainer').on('click','.boxCloseButton',function(){
$(this).closest('.boxsizer').remove();
});
});
Themostobviousdifferenceisthatthenewimplementationisshorter.Thebenefitscomebydefiningjustoneobservertoacommonancestorelement,foreachactionthatappliestomorethanonepageelement.Forthisreason,weusethe$.fn.on(events,selector,handler)overloadvariationofthe$.fn.on()method.
Specifically,weaddanobservertothepageelementwiththedashboardCategoriesCSSclassandlistenfortheclickeventsthatoriginatefromanyofits<button>descendants.Similarly,weaddasingleobservertotheboxContainerelementthatwillbeexecutedwheneveraclickeventfiresonanyofitsdescendantsthatmatchthe.boxCloseButtonCSSselector.
Sincetheaboveobserversapplynotonlytotheelementsthatexistedinthepageatthemomenttheywereregistered,butalsotoanyelementthatisaddedatanylaterpointoftimeandmatchesthespecifiedCSSselector;weareabletodecouplethecodethathandlestheclicksontheclosebuttonsandplaceitinaseparateobserver,insteadofregisteringanewoneeverytimeanewinformationboxisadded.Asaresult,theobserverthataddsthenewinformationboxesinthedashboardissimplerandonlyhastodealwithcreatingtheHTMLoftheboxandinsertitintothedashboard,leadingtoagreaterseparationof
concerns.Moreover,wenolongerneedtohandletheregistrationoftheobserverfortheclosebuttonofthehintboxinaseparatepieceofcode.
ComparethememoryusagebenefitsWewillnowcomparethedifferenceinmemoryusagewhenusingthe$.fn.on()methodwiththesimpleandDelegatedEventObserverPatternvariation.ToachievethiswewillopenthetwoimplementationsofourdashboardexampleandcomparetheirmemoryusageonChrome.ToopenChrome’sdevelopertools,justpressF12andthennavigatetotheTimelinetab.Wepressthe“record”buttonintheChrome’sTimelinetabandthenpresseachcategoryitembutton10times,resultingintheadditionof120informationboxestoourdashboard.Afteraddingalltheboxes,weendupwith121openboxesintotal,sincethehintboxwillstillbeopenandthenstopthetimelinerecording.
TheresultsinthetimelineforourinitialObserverPatternimplementationwilllookasfollows:
RepeatingthesameprocessfortheDelegatedEventObserverPatternimplementationwillgiveasmoothertimeline,revealinglessobjectallocationsandGarbageCollections,asfollows:
Asyoucanseeintheprecedingimages,weendupwith1192pageelementsinbothcases,butinthefirstimplementationweareusing134eventlisteners,ascomparedtotheimplementationwitheventdelegationwhereweinitiallycreatedthreeeventlistenersandneveractuallyaddedanother.
Finally,asyoucanseefromthebluelineinthegraph,thememoryconsumptionofthedelegateversionstayedrelativelythesame,addinguptojustaround200KB.Ontheotherhand,intheoriginalimplementation,theheapsizeincreasedmorethanfivetimes,gainingmorethan1MBofincrease.
Addingsomanyelementsmaynotbeanactualusecase,butthedashboardwillprobablynotbetheonlydynamicpartofyourpage.Asaresult,inarelativelycomplexwebpage,wecouldgetsimilarimprovementsifwereimplementedeveryapplicablepartofitusing
theDelegatedEventObserverPatternvariant.
SummaryInthischapter,welearnedabouttheObserverPattern,howitcanmaketheHTMLcodeofourwebpagescleaner,andthewaythatdecouplesitfromourapplication’scode.WelearnedhowjQueryaddsaprotectionlayertoitsmethodsinordertoprotectusfromundetectedmemoryleaks,whichmayoccurbyaddingobserverstoelements,whennotusingthejQueryDOMmanipulationmethods.
WealsotriedtheDelegatedEventObserverPatternvariantandusedittorewriteourinitialexample.Wecomparedthetwoimplementationsandsawhowitsimplifieswritingcodethatappliestomanypageelementswhentheyaregeneratedafterthepagehasbeenloaded.Finally,wehadacomparisonregardingthememoryconsumptionoftheplainObserverPatternwithitsdelegatevariantandhighlightedhowitalsolessensthememoryconsumptionofourpagebyreducingtherequirednumberofattachedobservers.
NowthatwehavecompletedourintroductiononhowtheObserverPatternisusedtolistentouseractions,wecanmoveontothenextchapterwherewewilllearnaboutcustomeventsandthePublish/SubscribePatternandthewaytheycanleadtoamoredecoupledimplementation.
Chapter3.ThePublish/SubscribePatternInthischapter,wewillshowcasethePublish/SubscribePattern,adesignpatternquitesimilartotheObserverPatternbutwithamoredistinctrolethatisabetterfitformorecomplexusecases.WewillseehowitdiffersfromtheObserverPatternandhowjQueryadoptedsomeofitsconceptsandbroughtthemtoitsObserverPatternimplementation.
Later,wewillproceedandrewriteourpreviouschapter’sexampleusingthispattern.Wewillusethispattern’sbenefitstoaddsomeextrafeaturesandalsoreducethecouplingofourcodewiththeelementsofthewebpage.
Inthischapter,wewill:
IntroducethePublish/SubscribePatternLearnhowitdiffersandwhatadvantagesithasovertheObserverPatternLearnhowjQuerybringssomeofitsfeaturestoitsmethodsLearnhowtoemitcustomeventswithjQueryRewriteandextendtheexamplefromChapter2,TheObserverPattern,usingthispattern
IntroducingthePublish/SubscribePatternThePublish/SubscribePatternisaMessagingPatternwheretheemittersofthemessages,calledthepublishers,multicastmessagestoanumberofrecipients,calledthesubscribers,thathaveexpressedtheirinterestinreceivingsuchmessages.Thekeyconceptofthispattern,whichisalsocommonlyreferredtoasthePub/SubPatterninshort,istoprovideawaytoavoiddependenciesbetweenthepublishersandtheirsubscribers.
Anextraconceptofthispatternistheuseoftopicsthatareusedbythesubscribersinordertoexpressthattheyareonlyinterestedinmessagesofaspecifictype.Thisway,publishersfiltersubscribersbeforesendingamessageanddistributethatmessageonlytotheappropriateones,therebyreducingtheamountoftrafficandworkrequiredonbothsides.
Anothercommonvariantistouseacentral,application-wideobject,knownasthebroker,thatrelaysmessagesproducedbythepublisherstotherelevantsubscribers.Thebroker,inthiscase,actsasawell-knownmessagehandlertosendandsubscribetomessagetopics.Thisenablesus,insteadofcouplingdifferentapplicationpartstogether,toonlyreferencethebrokeritselfandalsothetopicthatourcomponentsareinterestedin.Eventhoughtopicsmightnotbeanabsoluterequirementinthefirstvariantofthispattern,thisvariantplaysanessentialroleinscalabilitysincetherewillcommonlyexistwaylessbrokers(ifnotjustone)thanpublishersandsubscribers.
Byfollowingasubscriptionscheme,thecodeofthepublisheriscompletelydecoupledfromthesubscribers,meaningthatthepublisherdoesnothavetoknowtheobjectsdependonthem.Asaresult,wedonotneedtohardcodetothepublishereachseparateactionthatshouldbeexecutedonthedifferentpartsofourapplication.Instead,thecomponentsofanapplication,andpossiblythird-partyextensions,subscribetobenotifiedonlyabouttopics/eventsthattheyneedtoknow.Insuchdistributedarchitecture,addinganewfeaturetoanexistingapplicationrequiresminimaltonochangestotheapplicationcomponentsitdependson.
HowitdiffersfromtheObserverPatternThemostbasicdifferenceisthat,bydefinition,thePub/SubPatternisaone-way-MessagingPatternthatcanalsopassamessage,unliketheObserverPatternthatjustdescribeshowtonotifytheobserversaboutaspecificstatechangeonthesubject.
Moreover,unliketheObserverPattern,thePub/SubPatternwithabrokerresultsinmorelooselycoupledcodeforthedifferentpartsofanimplementation.Thisisbecausetheobserversneedtoknowtheirsubjectthatisemittingtheevents;however,ontheotherhand,thepublishersandtheirsubscribersonlyneedtoknowthebrokerthatisused.
HowitisadoptedbyjQueryOnceagain,thejQuerylibraryprovidesuswithaconvenientwaytotakeadvantageofthePub/SubPatterninourcode.InsteadofextendingitsAPIbyaddingnewmethodsspecificallynamed“publish”and“subscribe”andintroducingnewconcepts,thedevelopersdecidedtoextendthejQuery.fn.on()andjQuery.fn.trigger()methodswiththeabilitytohandleandemitcustomevents.Thisway,jQuerycanbeusedtoimplementapublisher/subscribercommunicationschemeusingthealreadyknownconvenientmethodsitprovides.
CustomeventsinjQueryCustomeventsallowustousealmostanyuser-definedstringvalueasacommoneventthatwecanaddlistenersfor,andalsomanuallyfireitonpageelements.Asanextrabutapreciousfeature,customeventscanalsocarrysomeextradatatobedeliveredtothelistenersoftheevent.
ThejQuerylibraryaddeditsowncustomeventsimplementation,beforeitwasactuallyaddedtoanywebspecification.Thisway,itwasprovedhowusefultheycanbewhenusedinwebdevelopment.Aswesawinthepreviouschapter,injQuery,thereisaspecificpartoftheimplementationthathandlesboththecommonelementeventandalsocustomevents.ThejQuery.eventobjectholdsalltheinternalimplementationsrelatedtofiringandlisteningtoevents.Also,thejQuery.EventclassisadedicatedwrapperthatjQueryusesfortheneedsofboththecommonelementeventsanditscustomeventsimplementation.
ImplementingaPub/SubschemeusingcustomeventsInthepreviouschapter,wesawhowthejQuery.fn.on()methodcanbeusedtoaddeventlistenersonelements.Wealsosawthatitsimplementationismaintaininglistswiththeaddedhandlersandnotifyingthemwhenrequired.Moreover,theeventnameseemstohavethesamecoordinationpurpose,justlikethetopic.ThisimplementationsemanticsseemtomatchexactlywiththePub/SubPatternaswell.
ThejQuery.fn.trigger()methodactuallyusestheinternaljQuery.event.trigger()methodthatisusedtofireeventsinjQuery.Ititeratesovertheinternalhandlerslistandexecutesthemwiththerequestedeventalongwithanyextraparametersthatthecustomeventdefines.Onceagain,thisalsomatchestheoperationrequirementsofthePub/SubPattern.
Asaresult,jQuery.fn.trigger()andjQuery.fn.on()seemtomatchtheneedsofthePub/SubPatternandcanbeusedinsteadofseparate“publish”and“subscribe”methods,respectively.SincetheyarebothavailableonthejQuery.fnobject,wecanusethesemethodsonanyjQueryobject.ThisjQueryobjectwillactasanintermediateentitybetweenthepublishersandthesubscribers,inawaythatperfectlyalignswiththedefinitionofthebroker.
Agoodcommonpractice,whichisalsousedbyalotofjQueryplugins,istousetheoutermostpageelementthatholdstheimplementationoftheapplicationorthepluginasthebroker.Ontheotherhand,jQueryactuallyallowsustouseanyobjectasabroker,sinceallthatitactuallyneedsisatargettoemitanobserveforourcustomevents.Asaresult,wecouldevenuseanemptyobjectasourbrokersuchas$({}),incaseusingapageelementseemstoorestrictingornotcleanenoughaccordingtothePub/SubPattern.ThisisactuallywhatthejQueryTinyPub/Sublibrarydoes,alongwithsomemethodaliasing,sothatweactuallyusemethodsnamed“publish”and“subscribe”insteadofjQuery’s“on”and“trigger”.FormoreinformationonTiny,youcanvisititsrepositorypageathttps://github.com/cowboy/jquery-tiny-pubsub.
DemonstratingasampleusecaseInordertoseehowthePub/SubPatternisused,andmakeiteasytocompareitwiththeObserverPattern,wearegoingtorewritethedashboardexamplefromChapter2,TheObserverPattern,usingthispattern.Thiswillalsoclearlydemonstratehowthispatterncanhelpusdecoupletheindividualpartsofanimplementationandmakeitmoreextendableandscalable.
UsingPub/SubonthedashboardexampleFortheneedsofthisdemonstration,wewillusetheHTMLandCSSfilesexactlyaswesawtheminChapter2,TheObserverPattern.
Toapplythispattern,wewillonlyneedtochangethecodeintheJavaScriptfilewithournewimplementation.Inthefollowingcodesnippet,wecanseehowthecodewaschangedinordertoadapttothePublisher/SubscriberPattern:
$(document).ready(function(){
window.broker=$('.dashboardContainer');
$('#categoriesSelector').change(function(){
var$selector=$(this);
varmessage={categoryID:$selector.val()};
broker.trigger('dashboardCategorySelect',[message]);
});
broker.on('dashboardCategorySelect',function(event,message){
var$dashboardCategories=$('.dashboardCategory');
varselectedIndex=+message.categoryID;
var$selectedItem=$dashboardCategories.eq(selectedIndex).show();
$dashboardCategories.not($selectedItem).hide();
});
$('.dashboardCategory').on('click','button',function(){
var$button=$(this);
varmessage={categoryName:$button.text()};
broker.trigger('categoryItemOpen',[message]);
});
broker.on('categoryItemOpen',function(event,message){
varboxHtml='<divclass="boxsizer"><articleclass="box">'+
'<headerclass="boxHeader">'+
message.categoryName+
'<buttonclass="boxCloseButton">✖'+
'</button>'+
'</header>'+
'Informationboxregarding'+message.categoryName+
'</article></div>';
$('.boxContainer').append(boxHtml);
});
$('.boxContainer').on('click','.boxCloseButton',function(){
varboxIndex=$(this).closest('.boxsizer').index();
varmessage={boxIndex:boxIndex};
broker.trigger('categoryItemClose',[message]);
});
broker.on('categoryItemClose',function(event,message){
$('.boxContainer.boxsizer').eq(message.boxIndex).remove();
});
});
Justlikeinourpreviousimplementation,weuse$(document).ready()inordertodelaytheexecutionofourcodeuntilthepagehasbeenfullyloaded.Firstofall,wedeclareourbrokerandassignittoanewvariableonthewindowobjectsothatitisgloballyavailableonthepage.Forourapplication’sbroker,weareusingajQueryobjectwiththeoutermostcontainerofourimplementation,whichinourcaseisthe<div>elementwiththedashboardContainerclass.
TipEventhoughusingglobalvariablesisgenerallyananti-pattern,westorethebrokerintoaglobalvariablesinceitisanimportantsynchronizationpointofthewholeapplicationandmustbeavailableforeverypieceofourimplementation,eventothosethatarestoredinseparate.jsfiles.AswewilldiscussinthenextchapterabouttheModulePattern,theprecedingcodecouldbeimprovedbystoringthebrokerasapropertyoftheapplication’snamespace.
Inordertoimplementthecategoryselector,wearefirstobservingthe<select>elementforthechangeevent.Whentheselectedcategorychanges,wecreateourmessageusingaplainJavaScriptobjectwiththevalueoftheselected<option>storedinthecategoryIDproperty.Then,wepublishitinthedashboardCategorySelecttopicusingthejQueryjQuery.fn.trigger()methodonourbroker.Thisway,wemovefromaUIelementeventtoamessagewithapplicationsemanticsthatcontainsalltherequiredinformation.Rightbelow,inoursubscriber’scode,weareusingthejQuery.fn.on()methodonourbrokerwiththedashboardCategorySelecttopicasaparameter(ourcustomevent),justlikewewoulddotolistenforasimpleDOMevent.ThesubscriberthenusesthecategoryIDfromthereceivedmessage,justlikewedidintheimplementationofthepreviouschapter,todisplaytheappropriatecategoryitems.
Followingthesameapproach,wesplitthecodethathandlesaddingandclosinginformationboxesinourdashboardinpublishersandsubscribers.Fortheneedsofthisdemonstration,themessageofthecategoryItemOpentopiccontainsjustthenameofthecategorywewanttoopen.However,inanapplicationwheretheboxcontentisretrievedfromaserver,wewouldprobablyuseacategoryitemIDinstead.Thesubscriberthenusesthecategoryitemnamefromthemessagetocreateandinserttherequestedinformation
box.
Similarly,themessageforthecategoryItemClosetopiccontainstheindexoftheboxthatwewantremoved.OurpublisherusesthejQuery.fn.closest()methodtotraversetheDOMandreachthechildelementsofourboxContainerelementandthenusesthejQuery.fn.index()methodtofinditspositionamongitssiblings.ThesubscriberthenusesjQuery.fn.eq()andtheboxIndexpropertyfromthereceivedmessagetofilterandremoveonlytherequestedinformationboxfromthedashboard.
TipInamorecomplexapplication,insteadoftheboxindex,wecanassociateeachinformationboxelementwithanewlyretrievedjQuery.guidusingamappingobject.Thiswillallowourpublishertousethatguidinthemessageinsteadofthe(DOM-related)elementindex.Thesubscriberwillthensearchthemappingobjectforthatguidinordertolocateandremovetheappropriatebox.
SincewearetryingtodemonstratetheadvantagesofthePub/SubPattern,thisimplementationchangewasnotintroducedinordertoeasethecomparisonwiththeObserverPatternandisinsteadleftasarecommendedexerciseforthereader.
Tosummarizetheabove,weusedthedashboardCategorySelect,categoryItemOpen,andcategoryItemClosetopicsasourapplication-leveleventsinordertodecouplethehandlingoftheuseractionsfromtheirorigin(theUIelement).Asaresult,wenowhavededicatedreusablepiecesofcodethatmanipulateourdashboard’scontent,whichisequivalenttoabstractingthemintoseparatefunctions.Thisallowsustoprogrammaticallypublishaseriesofmessagessothatwecan,forexample,removealltheexistinginformationboxesandaddallthecategoryitemsofthecurrentlyselectedcategory.Alternatively,evenbetter,makethedashboardshowalltheitemsofeachcategoryfor10secondsandthenmovetothenextone.
ExtendingtheimplementationInordertodemonstratethescalabilitythatthePub/SubPatternbringswithit,wewillextendourcurrentexamplebyaddingacounterwiththenumberofboxesthatarecurrentlyopeninthedashboard.
Forthecounterimplementation,wewillneedtoaddsomeextraHTMLtoourpageandalsocreateandreferenceanewJavaScriptfiletoholdthecounterimplementation:
...
</section>
<divstyle="margin-left:5px;">
Openboxes:
<outputid="dashboardItemCounter">1</output>
</div>
<sectionclass="boxContainer">
...
IntheHTMLpageoftheexample,wewillneedtoaddanextra<div>elementtoholdourcounterandsomedescriptiontext.Forourcounter,weareusingan<output>element,whichisasemanticHTML5elementidealtopresentresultsofuseractions.Thebrowserwilluseitjustlikeanormal<span>element,soitwillappearrightnexttoitsdescription.Also,sincethereisinitiallyahintboxopeninourdashboard,weusea1foritsinitialcontent:
$(document).ready(function(){
broker.on('categoryItemOpencategoryItemClose',function(event,
message){
var$counter=$('#dashboardItemCounter');
varcount=parseInt($counter.text());
if(event.type==='categoryItemOpen'){
$counter.text(count+1);
}elseif(event.type==='categoryItemClose'&&count>0){
$counter.text(count-1);
}
});
});
Forthecounterimplementationitself,allweneedtodoisaddanextrasubscribertothedashboard’sbroker,whichisgloballyavailabletootherJavaScriptfilesloadedinthepage,sincewehaveattachedittothewindowobject.Wearesimultaneouslysubscribingtotwotopics,bypassingthemspacedelimitedtothejQuery.fn.on()method.Rightafterthis,welocatethecounter<output>elementthathastheIDdashboardItemCounterandparseitstextcontentasanumber.Inordertodifferentiateouraction,basedonthetopicthatthemessagehasreceived,weusetheeventobjectthatjQuerypassesasthefirstparametertoouranonymousfunction,whichisoursubscriber.Specifically,weusethetypepropertyoftheeventobjectthatholdsthetopicnameofthemessagethatwasreceivedandbasedonitsvalue,wechangethecontentofthecounter.
NoteFormoreinformationontheeventobjectthatjQueryprovides,youcanvisithttp://api.jquery.com/category/events/event-object/.
Similarly,wecouldalsorewritethecodethatpreventsaccidentaldouble-clicksonthecategoryitembuttons.AllthatisneededistoaddanextrasubscriberforthecategoryItemOpentopicandusethecategoryNamepropertyofthemessagetolocatethepressedbutton.
UsinganyobjectasabrokerWhileinourexampleweusedtheoutermostcontainerelementofourdashboardforourbroker,itisalsocommontousethe$(document)objectasabroker.Usingtheapplication’scontainerelementisconsideredagoodsemanticpractice,whichalsoscopestheemittedevents.
Aswedescribedearlierinthischapter,jQueryactuallyallowsustouseanyobjectasabroker,evenanemptyone.Asaresult,wecouldinsteadusesomethingsuchaswindow.broker=$({});forourbroker,incasewepreferitoverusingapageelement.
Byusingnewlyconstructedemptyobjects,wecanalsoeasilycreateseveralbrokers,incasesuchathingwouldbepreferredforaspecificimplementation.Moreover,incaseacentralizedbrokerisnotpreferred,wecouldjustmakeeachpublisherthebrokerofitself,leadingtoanimplementationmorelikethefirst/basicvariantofthePub/SubPattern.
Sinceinmostcases,adeclaredvariableisusedtoaccesstheapplication’sbrokerwithinapage,thereislittledifferencebetweentheaboveapproaches.Justchoosetheonethatbettermatchesyourteam’staste,andincaseyouchangeyourmindatalaterpoint,allyouhavetodoisuseadifferentassignmentonyourbrokervariable.
UsingcustomeventnamespacingAsaclosingnoteforthischapter,wewillpresent,inshort,themechanismthatjQueryprovidesfornamespacingcustomevents.Themainbenefitofeventnamespacingisthatitallowsustousemorespecificeventnamesthatbetterdescribetheirpurpose,whilealsohelpingustoavoidconflictsbetweendifferentimplementationpartsandplugins.Italsoprovidesaconvenientwaytounbindalltheeventsofagivennamespacefromanytarget(elementorbroker).
Asimpleexampleimplementationwilllookasfollows:
varbroker=$({});
broker.on('close.dialog',function(event,message){
console.log(event.type,event.namespace);
});
broker.trigger('close.dialog',['messageEmitted']);
broker.off('.dialog');
//removesalleventhandlersofthe"dialog"namespace
Formoreinformation,youcanvisitthedocumentationpageathttp://docs.jquery.com/Namespaced_Eventsandthearticleathttps://css-tricks.com/namespaced-events-jquery/fromtheCSS-Trickswebsite.
SummaryInthischapter,wewereintroducedtothePublish/SubscribePattern.WesawitssimilaritieswiththeObserverPatternandalsolearneditsbenefitsbydoingacomparisonofthetwo.WeanalyzedhowthemoredistinctrolesandtheextrafeaturesthatthePublish/SubscribePatternoffersmakeitanidealpatternformorecomplexusecases.WesawhowjQuerydevelopersadoptedsomeofitsconceptsandbroughtthemtotheirObserverPatternimplementationascustomevents.Finally,werewrotetheexamplefromthepreviouschapterusingthePublish/SubscribePattern,addingsomeextrafeaturesandalsoachievinggreaterdecouplingbetweenthedifferentpartsandpageelementsofourapplication.
NowthatwehavecompletedourintroductiontohowthePublish/SubscribePatterncanbeusedasafirststeptodecouplethedifferentpartsofanimplementation,wecanmoveontothenextchapterwherewewillbeintroducedtotheModulePattern.Inthenextchapter,wewilllearnhowtoseparatethedifferentpartsofanimplementationintoindependentmodulesandhowtousenamespacingtoachievebettercodeorganizationanddefineastrictAPItoachievecommunicationbetweenthedifferentmodules.
Chapter4.DivideandConquerwiththeModulePatternInthischapter,wewillbeintroducedtotheconceptsofModulesandNamespacingandseehowtheycanleadtomorerobustimplementations.Wewillshowcasehowthesedesignprinciplescanbeusedinapplications,bydemonstratingsomeofthemostcommonlyuseddevelopmentpatternstocreateModulesinJavaScript.
Inthischapter,wewill:
ReviewtheconceptofModulesandNamespacingIntroducetheObjectLiteralPatternIntroducetheModulePatternanditsvariantsIntroducetheRevealingModulePatternanditsvariantsHaveasmalldiveintoES5StrictModeandES6ModulesExplainhowModulescanbeusedandbenefitjQueryapplications
ModulesandNamespacesThetwomainpracticesofthischapterareModulesandNamespaces,whichareusedtogetherinordertostructureandorganizeourcode.WewillfirstanalyzethemainconceptofModulesthatiscodeencapsulationandrightafterthis,wewillproceedtoNamespacing,whichisusedtologicallyorganizeanimplementation.
EncapsulatinginternalpartsofanimplementationWhiledevelopingalarge-scaleandcomplexwebapplication,theneedforawell-defined,structuredarchitecturebecomesclearfromthebeginning.Inordertoavoidcreatingaspaghetticodeimplementation,wheredifferentpartsofourcodecalleachotherinachaoticway,wehavetosplitourapplicationintosmall,self-containedparts.
Theseself-containedpiecesofcodecanbedefinedasModules.Todocumentthisarchitectureprinciple,ComputerSciencehasdefinedconceptssuchasSeparationofConcerns,wheretherole,operation,andtheexposedAPIofeachModuleshouldbestrictlydefinedandfocusedonprovidingagenericsolutiontoaspecificproblem.
NoteFormoreinformationonEncapsulationandSeparationofConcerns,youcanvisithttps://developer.mozilla.org/en-US/docs/Glossary/Encapsulationandhttp://aspiringcraftsman.com/2008/01/03/art-of-separation-of-concerns/.
AvoidingglobalvariableswithNamespacesInJavaScript,thewindowobjectisalsoknownastheGlobalNamespace,whereeachdeclaredvariableandfunctionidentifierisattachedbydefault.ANamespacecanbedefinedasanamingcontextwhereeachidentifierhastobeunique.ThemainconceptofNamespacingistoprovideawaytologicallygroupalltherelatedpiecesofadistinctandself-containedpartofanapplication.Inotherwords,itsuggeststhatwecreategroupswithrelatedfunctionsandvariablesandmakethemaccessibleunderthesameumbrellaidentifier.ThishelpstoavoidnamingcollisionsbetweendifferentpartsofanapplicationandotherJavaScriptlibrariesthatareused,sinceweonlyneedtokeepalltheidentifiersuniqueundereachdifferentNamespace.
AgoodexampleofNamespacingisthemathematicalfunctionsandconstantsthatJavaScriptprovides,whicharegroupedunderthebuilt-inJavaScriptobjectcalledMath.SinceJavaScriptprovidesmorethan40short-namedmathematicalidentifiers,suchasE,PI,andfloor(),inordertoavoidnamingconflictsandgroupingthemtogether,itwasdesignedtomakethemaccessibleaspropertiesoftheMathobjectthatactsastheNamespaceofthisbuilt-inlibrary.
WithoutproperNamespacing,eachfunctionandvariableneedstobeuniquelynamedthroughtheentireapplication,andcollisionscouldhappenbetweentheidentifiersofdifferentapplicationpartsorevenwiththoseofathird-partylibrarythatanapplicationuses.Finally,whileModulesprovideawaytoisolateeachindependentpartofyourapplication,NamespacingprovidesawaytostructureyourdifferentModulestowhatbecomesthearchitectureoftheapplication.
ThebenefitsofthesepatternsDesigninganapplicationarchitecturebasedonModulesandnamespacingleadstobettercodeorganizationandclearlyseparatedparts.Insucharchitectures,Modulesareusedtogrouptogetherpartsoftheimplementationthatarerelated,whileNamespacesconnectthemtoeachothertocreatetheapplicationstructure.
Thisarchitecturehelpstocoordinatelargedeveloperteams,enablingtheimplementationofindependentpartstotakeplaceinparallel.Itcanalsoshortenthedevelopmenttimeneededtoaddanewfunctionalitytotheexistingimplementation.Thisisbecausetheexistingpiecesthatareusedcanbelocatedeasilyandtheaddedimplementationhaslesschanceofconflictingwiththeexistingcode.
Theresultingcodestructuresarenotonlycleanlyseparated,butsinceeachModuleisdesignedtoachieveasinglegoal,thereisagoodchancethatitcanalsobeusedinothersimilarapplications.Asanaddedbenefit,sincetheroleofeachModuleisstrictlydefined,italsomakestracingtheoriginofabugaloteasierinalargecodebase.
ThewideacceptanceBoththecommunityandtheenterpriseworldrealizedthat,inordertohavemaintainable,largefrontendapplicationswritteninJavaScript,theyshouldendupwithasetofbestpracticesthatshouldbeincorporatedineverypartoftheirimplementations.
TheacceptanceandadoptionofModulesandNamespacinginJavaScriptimplementationsisclearlyvisibleinthebestpracticesandcodingstyleguidesthatthecommunityandenterpriseshavereleased.
Forexample,Google’sJavaScriptStyleGuide(availableathttps://google.github.io/styleguide/javascriptguide.xml#Naming)describesandsuggestsadoptingnamespacinginourimplementations:
ALWAYSprefixidentifiersintheglobalscopewithauniquepseudonamespacerelatedtotheprojectorlibrary.
Moreover,thejQueryJavaScriptStyleGuide(availableathttps://contribute.jquery.org/style-guide/js/#global-variables)suggestsusingglobalvariablessothat:
Eachprojectmayexposeatmostoneglobalvariable.
Anotherexampleofacceptanceamongthedevelopercommunity,comesfromtheMozillaDeveloperNetwork.Itsguideforobject-orientedJavaScript(availableathttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript#Namespace)alsosuggestsusingNamespaces,towraptheimplementationofourapplicationunderasingleexposedvariable,usingsomethingassimpleasfollows:
//globalnamespace
varMYAPP=MYAPP||{};
TheObjectLiteralPatternTheObjectLiteralPatternisprobablythesimplestwaytowrapalltherelatedpartsofanimplementationunderanumbrellaobjectthatworksasaModule.Thenameofthispatternaccuratelydescribesthewayitisused.ThedeveloperjustneedstodeclareavariableandassignanobjectwithalltherelatedpartsthatneedtobeencapsulatedintothisModule.
Let’sseehowwecancreateaModulethatprovidesuniqueintegerstoapage,inasimilarwayhowjquery.guiddoesit:
varsimpleguid={
guid:1,
init:function(){
this.guid=1;
},
increaseCounter:function(){
this.guid++;
//orsimpleguid.guid++;
},
getNext:function(){
varnextGuid=this.guid;
this.increaseCounter();
returnnextGuid;
}
};
Asseenabove,asimplerulethatyoucanfollowinordertoadoptthispatternistodefineallthevariablesandfunctionsthateachimplementationneedsaspropertiesofanobject.OurcodeisreusableanddoesnotpollutetheGlobalNamespace,otherthanjustdefiningasinglevariablenameforourModule,simpleguidinthiscase.
WecanaccesstheModulepropertiesinternally,eitherbyusingthethiskeyword,suchasthis.guid,orusingthefullnameoftheModulesuchassimpleguid.guid.InordertousetheaboveModuleinourcode,wejustneedtoaccessitspropertybyusingitsname.Forexample,callingthesimpleguid.getNext()methodwillreturntoourcodethenext-in-ordernumericguidandalsochangetheModule’sstatebyincreasingtheinternalcounter.
OneofthenegativesofthispatternisthatitdoesnotprovideanyprivacytotheinternalpartsoftheModule.AlltheinternalpartsoftheModulecanbeaccessedandbeoverriddenbyexternalcode,eventhoughweideallyprefertoonlyexposethesimpleguid.init()andsimpleguid.getNext()methods.Thereareseveralnamingconventionsthatdescribeprependingorappendinganunderscore(_)tothenamesofpropertiesthatareintendedonlyforinternaluse,butthistechnicallydoesn’tfixthisdisadvantage.
AnotherdisadvantageisthatwritingabigModuleusinganobjectliteralcaneasilygettiring.It’struethatJavaScriptdevelopersareusedtoendtheirvariablesandfunctiondefinitionswithsemicolons(;),andtryingtowriteabigModuleusingcommas(,)aftereachpropertycaneasilyleadtosyntacticerrors.
EventhoughthispatternmakesiteasytodeclarenestedNamespacesforaModule,itcanalsoleadtobigcodestructureswithbadreadabilityincaseweneedseverallevelsofnesting.Forexample,let’stakealookatthefollowingskeletonofaTodoapplication:
varmyTodoApp={
todos:[],
addTodo:function(todo){this.todos.push(todo);},
getTodos:function(){returnthis.todos;},
updateTodo:function(todo){/*...*/},
imports:{
fromGDrive:function(){/*...*/},
fromUrl:function(){/*...*/},
fromText:function(){/*...*/}
},
exports:{
gDrivePublicKey:'#wnanqAASnsmkkw',
toGDrive:function(){/*...*/},
toFile:function(){/*...*/},
},
share:{
toTwitter:function(todo){/*...*/}
}
};
Fortunately,thiscanbeeasilyfixedbysplittingtheobjectliteraltomultipleassignmentsforeachsubmodule(andpreferablytodifferentfiles)asfollows:
varmyTodoApp={
todos:[],
addTodo:function(todo){this.todos.push(todo);},
getTodos:function(){returnthis.todos;},
updateTodo:function(todo){/*...*/},
};
/*…*/
myTodoApp.exports={
gDrivePublicKey:'#wnanqAASnsmkkw',
toGDrive:function(){/*...*/},
toFile:function(){/*...*/},
};
/*...*/
TheModulePatternThekeyconceptofthebasicModulePatternistoprovideasimplefunction,class,orobjectthattherestoftheapplicationcanuse,throughawell-knownvariablename.ItenablesustoprovideaminimalAPIforaModule,byhidingthepartsoftheimplementationthatdonotneedtobeexposed.Thisway,wealsoavoidpollutingtheGlobalNamespacewithvariablesandutilityfunctionsthatareneededforinternalusebyourModule.
TheIIFEbuildingblockInthissubsection,wewillgetasmallintroductiontotheIIFEDesignPatternsinceit’sanintegralpartforallthevariantsoftheModulePatternthatwewillseeinthischapter.TheImmediatelyInvokedFunctionExpression(IIFE)isaverycommonlyusedDesignPatternamongJavaScriptdevelopersbecauseofthecleanwayinwhichitisolatesblocksofcode.IntheModulePattern,anIIFEisusedtowrapalltheimplementationinordertoavoidpollutingtheGlobalNamespaceandprovideprivacytothedeclarationstotheModuleitself.
EachIIFEcreatesaClosurewiththevariablesandfunctionsdeclaredinsideit.TheClosurethatiscreatedenablestheexposedfunctionoftheIIFEtokeepreferencestotherestofthedeclarationsoftheirenvironmentandaccessthemnormallywhenexecutedfromotherpartsofanimplementation.Asaresult,thenon-exposeddeclarationsoftheIIFEdonotleakoutsideit,butarekeptprivateandareaccessibleonlybythefunctionsthatarepartofthecreatedClosure.
NoteFormoreinformationonIIFEsandClosures,youcanvisithttps://developer.mozilla.org/en-US/docs/Glossary/IIFEandhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures.
AnIIFEismostcommonlyusedasfollows:
(function(){
varx=7;
console.log(x);
//prints7
})();
Sincetheprecedingcodeconstructmightlookbizarreonfirstsight,let’sseethepiecesthatitiscomposedfrom.AnIIFEisalmostequivalenttodeclaringananonymousfunction,assigningittoavariable,andthenexecutingit,asshowninthefollowingcode:
vartmp=function(){
varx=7;
console.log(x);
};
tmp();
//or
(tmp)();
Intheprecedingcode,wedefineafunctionexpressionandexecuteitusingtmp().Since,inJavaScript,wecanuseparenthesesaroundanidentifierwithoutchangingitsmeaning,wecanalsoexecutethestoredfunctionwith(tmp)();.Thefinalstep,inordertoturntheprecedingcodeintoanIIFE,istoreplacethetmpvariablewiththeactualanonymousfunctiondeclaration.
Aswesawearlier,theonlydifferenceisthat,withanIIFE,wedoneedtodeclarea
variablejusttoholdthefunctionitself.Weonlycreateananonymousfunctionandinvokeitimmediatelyrightafterdefiningit.
SincethecreationofanIIFEcanbeachievedinseveralways,whichmightlooklikeanexerciseofJavaScript’srules,thecommunityofJavaScriptdevelopershasconcludedtotheabovecodestructureasapointofreferenceforthispattern.ThiswayofcreatinganIIFEisconsideredtohavebetterreadabilityandisusedbylargelibrariesandasaresultofitsadoption,developerscaneasilyrecognizeitinsidelargeJavaScriptimplementations.
Anexampleoftheless-widely-usedwaystocreateanIIFEisthefollowingcodestructure:
(function(){
//code
}());
ThesimpleIIFEModulePatternSincethereisnoactualnameforthispattern,itisrecognizedbythefactthatthedefinedModulereturnsasingleentity.Forreferenceonhowtocreateareusablelibraryusingthispattern,wewillrewritethesimpleguidModulethatwesawearlier.Theresultingimplementationwilllookasfollows:
varsimpleguid=(function(){
varsimpleguid={};
varguid;
simpleguid.init=function(){
guid=1;
};
simpleguid.increaseCounter=function(){
guid++;
};
simpleguid.getNext=function(){
varnextGuid=guid;
this.increaseCounter();
returnnextGuid;
};
simpleguid.init();
returnsimpleguid;
})();
ThispatternusesanIIFEtodefineanobjectthatactsastheModulecontainer,attachespropertiestoit,andlaterreturnsit.ThevariablesimpleguidinthefirstlineoftheprecedingcodeisusedastheNamespaceoftheModuleandisassignedwiththevaluethatisreturnedbytheIIFE.ThemethodsandpropertiesthataredefinedonthereturnedobjectaretheonlyexposedpartsoftheModulesandconstituteitspublicAPI.
Onceagain,thispatternallowsustousethethiskeyword,inordertoaccesstheexposedmethodsandpropertiesofourModule.Furthermore,italsoprovidestheflexibilitytoexecuteanyrequiredinitializationcodebeforecompletingtheModule’sdefinition.
UnliketheObjectLiteralPattern,theModulePatternenablesustocreateactualprivatemembersinourModules.VariablesdeclaredinsidetheIIFE,thatarenotattachedtothereturnvalue,suchastheguidvariable,actasprivatemembersandareonlyaccessibleinsidetheModulebyrestmembersofthecreatedClosure.
Lastly,incaseweneedtodefineanestedNamespace,allwehavetodoischangetheassignmentofthevaluereturnedbytheIIFE.Asanexampleofanapplicationstructuredwithsubmodules,let’sseehowwewilldefinetheexportingsubmodulefortheTodoapplicationskeletonthatwesawearlier:
varmyTodoApp=(function(){
varmyTodoApp={};
vartodos=[];
myTodoApp.addTodo=function(todo){
todos.push(todo);
};
myTodoApp.getTodos=function(){
returntodos;
};
returnmyTodoApp;
})();
myTodoApp.exports=(function(){
varexports={};
vargDrivePublicKey='#wnanqAASnsmkkw';
exports.toGDrive=function(){/*...*/};
exports.toFile=function(){/*...*/};
returnexports;
})();
Giventhatourapplication’sNamespacemyTodoApphasalreadybeendefinedearlier,theexportssubmodulecanbedefinedasasimplepropertyonit.AgoodpracticetofollowwillbetocreateonefileforeachoneoftheaboveModules,usingtheIIFEsasthelandmarkstosplityourcode.Awidelyusednamingconvention,whichisalsosuggestedbyGoogle’sJavaScriptStyleGuide,istouselowercasenamingforyourfilesandadddashestoseparatesubmodules.Forexample,byfollowingthisnamingconvention,theprecedingcodeshouldbedefinedintwofilesnamedasmytodoapp.jsandmytodoapp-exports.jsforeachModule,respectively.
HowitisusedbyjQueryTheModulePatternisusedwithinjQueryitself,inordertoisolatethesourcecodeoftheCSSselectorengine(Sizzle),whichpowersthe$()function,fromtherestofthejQuerysource.Fromthebeginning,SizzlewasabigpartofthejQuerysource,whichiscurrentlycountingabout2135linesofcode;since2009,ithasbeensplitintoaseparateprojectnamedSizzle,soitcanbemoreeasilymaintained,bedevelopedindependently,andbereusablebyotherlibraries:
varSizzle=(function(window){
/*179linesofcode*/
functionSizzle(selector,context,results,seed){
/*131linesofcode*/
}
/*
1804linesofcode,definingmethodslike:
Sizzle.attr
Sizzle.compile
Sizzle.contains
Sizzle.getText
Sizzle.matches
Sizzle.matchesSelector
Sizzle.select
*/
returnSizzle;
})(window);
jQuery.find=Sizzle;
SizzleisaddedtothejQuery’ssourceinsideanIIFE,whileitsmainfunctionisreturnedandassignedtojQuery.findforuse.
NoteFormoreinformationonSizzle,youcanvisithttps://github.com/jquery/sizzle.
TheNamespaceParameterModulevariantInthisvariant,insteadofreturninganobjectfromourIIFEandthenassigningittothevariablethatactsastheNamespaceoftheModule,wecreatetheNamespaceandpassitasaparametertotheIIFEitself:
(function(simpleguid){
varguid;
simpleguid.init=function(){
guid=1;
};
simpleguid.increaseCounter=function(){
guid++;
};
simpleguid.getNext=function(){
varnextGuid=guid;
this.increaseCounter();
returnnextGuid;
};
simpleguid.init();
})(window.simpleguid=window.simpleguid||{});
ThelastlineoftheModuledefinitiontestswhethertheModuleisalreadydefined;incaseitisnot,itinitializesittoanemptyobjectliteralandassignsittotheglobalobject(window).Inanycase,thesimpleguidparameterinthefirstlineoftheIIFEwillholdtheModule’sNamespace.
NoteTheaboveexpressionisalmostequivalenttowriting:
window.simpleguid=window.simpleguid!==undefined?window.simpleguid:
{};
UsingthelogicalORoperator(||)makestheexpressionbothshorterandmorereadable.Moreover,thisisapatternthatmostwebdevelopershavelearnedtoeasilyrecognize,anditappearsinalotofdevelopmentpatternsandbestpractices.
Onceagain,thispatternallowsustousethethiskeywordtoaccesspublicmembersfromwithintheexportedmethodsoftheModule.Atthesametime,itallowsustokeepsomefunctionsandvariablesprivate,whichwillbeaccessibleonlybyotherfunctionsoftheModule.
Eventhoughit’sconsideredagoodpracticetodefineeachModuletoitsownJSfile,thisvariantalsoallowsustosplittheimplementationoflargeModulestomorethanonefile.ThisbenefitcomesasaresultofcheckingwhethertheModuleisalreadydefined,beforeinitializingittoanemptyobject.Thismightbeusefulinsomecases,withtheonlylimitationbeingthateachpartialfileofaModulecanaccesstheprivatemembersdefinedinitsownIIFE.
Moreover,inordertoavoidrepetition,wecanuseasimpleridentifierfortheparameteroftheIIFEandwriteourModuleasfollows:
(function(namespace){
/*…*/
namespace.getNext=function(){
varnextGuid=guid;
this.increaseCounter();
returnnextGuid;
};
namespace.init();
})(window.simpleguid=window.simpleguid||{});
WhenitcomestoapplicationswithnestedNamespaces,thispatternmightstartfeelingalittleuncomfortabletoread.ThelastlineoftheModuledefinitionwillstarttogetlongerforeveryextralevelofnestednamespacingthatwedefine.Forexample,let’sseehowtheexportssubmoduleofourTodoapplicationwouldlook:
(function(exports){
vargDrivePublicKey='#wnanqAASnsmkkw';
exports.toGDrive=function(){/*...*/};
exports.toFile=function(){/*...*/};
})(myTodoApp.exports=myTodoApp.exports||{});
Asyoucansee,eachextralevelofthenestedNamespaceneedstobeaddedonbothsidesoftheassignmentthatispassedasaparametertotheIIFE.ForapplicationswithcomplexfeaturesthatleadtomultiplelevelsofnestedNamespaces,thiscouldleadtoModuledefinitionslookingsomethinglikethis:
(function(smallModule){
smallModule.method=function(){/*...*/};
returnsmallModule;
})(myApp.bigFeature.featurePart.smallModule=
myApp.bigFeature.featurePart.smallModule||{});
Moreover,ifwewanttoprovidethesamesafetyguaranties,asintheoriginalcodesample,thenwewouldneedtoaddsimilarsafechecksforeachNamespacelevel.Withthisinmind,theexportsModuleofourTodoapplicationthatwesawearlierwouldneedtohavethefollowingform:
(function(exports){
vargDrivePublicKey='#wnanqAASnsmkkw';
exports.toGDrive=function(){/*...*/};
exports.toFile=function(){/*...*/};
})((window.myTodoApp=window.myTodoApp||{},myTodoApp.exports=
myTodoApp.exports||{}));
Asseenintheprecedingcode,weusedthecommaoperator(,)toseparateeachnamespaceexistencecheckandwrappedthewholeexpressioninanextrapairofparenthesissothatthewholeexpressionisusedasthefirstparameteroftheIIFE.Usingthecommaoperator(,)tojoinexpressionswillleadthemtobeevaluatedinorderandpasstheresultofthelastevaluatedexpressionastheparameteroftheIIFE,andthatresultwillbeusedastheNamespaceoftheModule.Keepinmindthat,foreachextranestedNamespacelevel,weneedtoaddanextraexistencecheckexpressionusingthecommaoperator(,).
Adisadvantageofthispattern,especiallywhenusedfornestednamespacing,isthattheNamespacedefinitionoftheModuleisattheendofthefile.EventhoughitishighlyrecommendedtonameyourJSfilessothattheyproperlyrepresenttheModulesthattheycontain,forexample,mytodoapp.exports.js;nothavingtheNamespacenearthetopofthefilecansometimesbecounterproductiveormisleading.Aneasywork-aroundforthisproblemwouldbetodefinetheNamespacebeforetheIIFEandthenpassitasaparameter.Forexample,theprecedingcodeusingthistechniquewouldbetransformedtosomethingasfollows:
window.myTodoApp=window.myTodoApp||{};
myTodoApp.exports=myTodoApp.exports||{};
(function(exports){
vargDrivePublicKey='#wnanqAASnsmkkw';
exports.toGDrive=function(){/*...*/};
exports.toFile=function(){/*...*/};
})(myTodoApp.exports);
TheIIFE-containedModulevariantLikeinthepreviousvariantsoftheModulePattern,thisvariantdoesnotactuallyhaveaspecificvariantname,butisrecognizedbythewaythecodeisstructured.ThekeyconceptofthisvariantistomovealltheModule’scodeinsidetheIIFE:
(function(){
window.simpleguid=window.simpleguid||{};
varguid;
simpleguid.init=function(){
guid=1;
};
simpleguid.increaseCounter=function(){
guid++;
};
simpleguid.getNext=function(){
varnextGuid=guid;
this.increaseCounter();
returnnextGuid;
};
simpleguid.init();
})();
ThisvariantlooksverysimilartothepreviousoneandmainlydiffersinthewaythattheNamespaceiscreated.Firstofall,itkeepstheNamespacecheckandinitializationnearthetopoftheModule,likeaheading,makingourcodemorereadableregardlessofwhetherweuseaseparatefilefortheModuleornot.LikeothervariantsoftheModulePattern,itsupportsprivatemembersforourModulesandalsoallowsustousethethiskeywordtoaccesspublicmethodsandproperties,makingourcodelookmoreobject-oriented.
RegardingimplementationswithnestedNamespaces,thecodestructureoftheexportssubmoduleofourTodoapplicationskeletonwilllookasfollows:
(function(){
window.myTodoApp=window.myTodoApp||{};
myTodoApp.exports=myTodoApp.exports||{};
vargDrivePublicKey='#wnanqAASnsmkkw';
myTodoApp.exports.toGDrive=function(){/*...*/};
myTodoApp.exports.toFile=function(){/*...*/};
})();
Asseenintheprecedingcode,wealsoborrowedtheNamespacedefinitionchecksfromthepreviousvariantand,likewise,appliedittoeverylevelofnestednamespacing.Even
thoughthisisnotabsolutelynecessary,itbringsthebenefitsthatwediscussedearliersuchasenablingustosplitaModuledefinitionintoseveralfilesandevenresultsinamoreerror-tolerantimplementationregardingtheimportorderoftheapplication’sModules.
TheRevealingModulePatternTheRevealingModulePatternisavariantoftheModulePatternwithaknownandwidelyrecognizedname.WhatmakesthispatternspecialisthatitcombinesthebestpartsoftheObjectLiteralPatternandtheModulePattern.AllthemembersoftheModulearedeclaredinsideanIIFE,whichattheend,returnsanObjectLiteralcontainingonlythepublicmembersoftheModuleandisassignedtothevariablethatactsasourNamespace:
varsimpleguid=(function(){
varguid=1;
functioninit(){
guid=1;
}
functionincreaseCounter(){
guid++;
}
functiongetNext(){
varnextGuid=guid;
increaseCounter();
returnnextGuid;
}
return{
init:init,
getNext:getNext
};
})();
OneofthemainbenefitsofthispatternthatdifferentiatesitfromothervariantsisthatitallowsustowriteallthecodeofourModuleinsidetheIIFE,justlikewewouldiftheywouldbedeclaredontheGlobalNamespace.Moreover,thispatterndoesnotrequireanyvariationonthewaythatthepublicandprivatemembersaredeclared,makingthecodeoftheModulelookuniform.
SincethereturnedObjectLiteraldefinesthepubliclyavailablemembersoftheModule,itisalsoaconvenienteasywaytoinspectitspublicAPI,evenifitiswrittenbysomeoneelse.Moreover,incaseweneedtoexposeaprivatemethodonourModule’sAPI,allweneedtodoisaddanextrapropertytothereturnedObjectLiteralwithoutchanginganypartofitsdefinition.Additionally,theuseofanObjectLiteralenablesustochangetheexposedidentifiersfortheModule’sAPI,withoutchangingthenamesusedbytheModule’simplementationinternally.
Evenifthisisnotclearlyvisible,thethiskeywordcanbeusedforcallsbetweenthepublicmembersoftheModule.Unfortunately,usingthethiskeywordisdiscouragedforthispattern,sinceitbreakstheuniformityofthefunctiondeclarationsandcaneasilyleadtoerrors,especiallywhenchangingthevisibilityofapublicmethodtoprivate.
SincetheNamespacedefinitioniskeptoutsidethebodyoftheIIFE,thispatternclearlyseparatestheNamespacedefinitionfromtheactualimplementationoftheModule.UsingthispatterntodefineaModuleinanestedNamespacedoesnotaffecttheModule’simplementation,whichwillnotlookdifferentatanypointfromatop-levelNamespaceModule.RewritingtheexportssubmoduleofourTodoskeletonapplicationusingthispatternwillmakeitlooklikethis:
myTodoApp.exports=(function(){
vargDrivePublicKey='#wnanqAASnsmkkw';
functiontoGDrive(){/*...*/}
functiontoFile(){/*...*/}
return{
toGDrive:toGDrive,
toFile:toFile
};
})();
Asaresultofthisseparation,wehavelesscoderepetitionandwecaneasilychangetheNamespaceofaModulewithoutaffectingitsimplementationatall.
UsingES5StrictModeAsmallbutpreciousadditiontoalltheModulePatternsthatuseIIFEsastheirbasicbuildingblocks,istheuseofStrictModeforJavaScriptexecution.ThiswasstandardizedinthefiftheditionofJavaScript,andisanopt-inexecutionmodewithslightlydifferentsemantics,inordertopreventsomeofthecommonpitfallsofJavaScript,butalsohavingbackwardscompatibilityinmind.
Underthismode,theJavaScriptruntimeenginewillpreventyoufromaccidentallycreatingaglobalvariableandpollutingtheGlobalNamespace.Eveninnot-so-largeapplications,itisquitepossiblethatavardeclarationbeforetheinitialassignmentofavariablecanbemissing,automaticallypromotingthattoaglobalvariable.Topreventthiscase,strictmodethrowsanerrorincaseanassignmentisissuedtoanundeclaredvariable.ThefollowingimageshowtheerrorthatisthrownbyFirefoxandChromewhenaStrictModeviolationhappens.
Thismodecanbeenabledbyaddingthe"usestrict";or'usestrict';statementbeforeanyotherstatements.Eventhoughthiscanbeenabledontheglobalscope,itishighlyrecommendedthatyouenableitonlyinsidethescopeofafunction.Enablingitontheglobalscopemightmakethird-partylibrariesthatarenon-strict-modecompliantstopworkingormisbehave.Ontheotherhand,thebestplacetoenableStrictModeisinsidetheIIFEofaModule.TheStrictModewillberecursivelyappliedtoallnestedNamespaces,methods,andfunctionsofthatIIFE.
NoteFormoreinformationonJavaScript’sstrictexecutionmode,youcanvisithttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode.
IntroducingES6ModulesEventhoughJavaScriptinitiallyhadnobuilt-inpackagingandnamespacingsupportlikeotherprogramminglanguages,webdevelopersfilledthegapsbydefiningandadoptingsomedesignpatternsforthispurpose.ThesesoftwaredevelopmentpracticesworkedaroundthemissingfeaturesofJavaScriptandallowedlargeandscalableimplementationsofcomplexapplicationsonaprogramminglanguagethatsomeyearsagowasmostlyusedforformvalidation.
Thiswasuntilthe6thversionofJavaScript,commonlyreferredtoasES6,wasreleasedasastandardonJune2015andintroducedtheconceptofModulesaspartofthelanguage.
NoteES6isanabbreviationofECMAScript6thedition,whichisalsoreferredtoasHarmonyorECMAScript2015,whereECMAScriptisthetermthatisusedforthestandardizationprocessofJavaScript.Thespecificationcanbefoundathttp://www.ecma-international.org/ecma-262/6.0/index.html#sec-modules.
AsanexampleofES6Modules,wewillseeoneofthemanywaysinwhichthesimpleguidModulecanbewritten:
vares6simpleguid={};
exportdefaultes6simpleguid;
varguid;
es6simpleguid.init=function(){
guid=1;
};
es6simpleguid.increaseCounter=function(){
guid++;
};
es6simpleguid.getNext=function(){
varnextGuid=guid;
this.increaseCounter();
returnnextGuid;
};
es6simpleguid.init();
Ifwesavethisasafilenamedes6simpleguid.js,thenwecanimportanduseitinadifferentfilebysimplywritingthefollowingcode:
importes6simpleguidfrom'es6simpleguid';
console.log(es6simpleguid.getNext());
SinceES6ModulesarebydefaultinStrictMode,writingyourModulestodayusingyourpreferredModulePatternvariantwithStrictModeenabledwillmakeyourtransitiontoES6Moduleseasier.Someoftheabovepatternsrequireveryfewchangestoachievethis.
Forexample,intheIIFE-containedModulePatternvariant,allthatisneededisremovetheIIFEandthe"usestrict";statement,replacethecreationoftheModule’sNamespacewithavariable,andusetheexportkeywordonit.
Unfortunately,atthetimeofwritingthisbook,nobrowserhas100%supportforES6Modules.Asaresult,specialloadersortoolsthattranspileES6toES5arerequiredsothatwecanstartwritingourcodeusingthenewfeaturesofES6.
NoteFormoreinformation,youcanvisitES6Moduleloader’sdocumentationpageathttps://github.com/ModuleLoader/es6-module-loader,andBabeltranspiler(earlierknownasES6toES5)athttp://babeljs.io/.
UsingModulesinjQueryapplicationsInordertodemonstratehowtheModulePatterncanleadtoabetterapplicationstructure,wewillreimplementthedashboardexamplethatwesawinthepreviouschapters.Wewillincludeallthefunctionalitiesthatwehaveseenuntilnow,includingthecounteroftheopeninformationboxes.TheHTMLandCSScodeusedisexactlythesameasinthepreviouschapterand,asaresult,ourdashboardlooksexactlythesameasbefore:
Forthisdemonstration,wewillrefactorourJavaScriptcodeintofoursmallModulesusingthesimpleIIFE-containedModulevariant.ThedashboardModulewillactasthemainentryofcodeexecutionandalsoasthecentralcoordinationpointofthedashboardapplication.Thecategoriessubmodulewillberesponsiblefortheimplementationoftheupper-toppartofourdashboard.Thisincludescategoryselection,thepresentationofappropriatebuttons,andthehandlingofbuttonclicks.TheinformationBoxsubmodulewillberesponsibleforthemainpartofourdashboard.Itwillprovidemethodstocreateandremoveinformationboxesfromthedashboard.Finally,thecountersubmodulewillberesponsibleforkeepingthefieldwiththenumberofthecurrentlyopeninformationboxesup-to-date,respondingtotheuseractions.
AsinglechangethatweneedtomaketotheHTMLofthepageinordertosupportthismultimodulearchitectureislimitedtothewayinwhichtheJavaScriptfilesareincluded:
<scripttype="text/javascript"src="jquery.js"></script>
<scripttype="text/javascript"src="dashboard.js"></script>
<scripttype="text/javascript"src="dashboard.categories.js"></script>
<scripttype="text/javascript"src="dashboard.informationbox.js">
</script>
<scripttype="text/javascript"src="dashboard.counter.js"></script>
TipEvenifthismultifilestructuremakesthedevelopmentanddebuggingprocessesaloteasier,itisrecommendedthatwecombineallthesefilesbeforemovingourapplicationtoaproductionenvironment.Severaltoolsspecializedforthisjobexist;forexample,theverysimpleandeffectivegrunt-contrib-concatprojectthatisavailableathttps://github.com/gruntjs/grunt-contrib-concat.
ThemaindashboardmoduleTheresultingcodeforthedashboardmodulewilllookasfollows:
(function(){
'usestrict';
window.dashboard=window.dashboard||{};
dashboard.$container=null;
dashboard.init=function(){
dashboard.$container=$('.dashboardContainer');
dashboard.categories.init();
dashboard.informationBox.init();
dashboard.counter.init();
};
$(document).ready(dashboard.init);
})();
Aswealreadymentioned,thedashboardmodulewillbethecentralpointofourapplication.Sincethisisthestartingpointofexecutionforourapplication,itsmaindutyistodoalltherequiredinitializationsforitselfandeachsubmodule.Theinvocationoftheinit()methodiswrappedinsideacalltothe$(document).ready()methodsothatitsexecutionisdelayeduntiltheDOMtreeofthepageisfullyloaded.
Oneimportantthingtonoteisthat,duringtheinitialization,wedoaDOMtraversalinordertofindthecontainerelementofthedashboardandstoreittoapublicpropertyoftheModulenamed$container.ThiselementwillbeusedbyallthemethodsofthedashboardthatneedtoaccesstheDOMtree,inordertoscopetheircodeinsidethatcontainerelement,removingtheneedtoconstantlytraversethewholeDOMtreeusingcomplexselectors.KeepingreferencestokeyDOMelementsandreusingtheminthedifferentsubmodules,canmaketheapplicationsnappierandalsolessenthechanceofaccidentallyinterferingwiththerestofthepage;thus,leadingtolessbugsthatarealsoeasiertoresolve.
TipCacheelementsbutavoidmemoryleaks.
KeepinmindthatmaintainingreferencestoDOMelementsthatareconstantlyaddedandremovedfromthepageaddsextracomplexitytoourapplication.Thiscanevenleadtomemoryleaksincaseweareaccidentallykeepingareferencetoanelementthathasalreadybeenremovedfromthepage.Forsuchelements,suchastheinformationboxes,itmightbesaferandmoreeffectivetohavedelegatedhandlingfortheeventstriggeredonthemandtodoascopedDOMtraversalwhenneeded,inordertoretrieveajQueryobjectwithfreshreferencesoftheelements.
ThecategoriesmoduleLet’sproceedwiththecategoriessubmodule:
(function(){
'usestrict';
dashboard.categories=dashboard.categories||{};
dashboard.categories.init=function(){
dashboard.$container.find('#categoriesSelector').change(function()
{
var$selector=$(this);
varcategoryIndex=+$selector.val();
dashboard.categories.selectCategory(categoryIndex);
});
dashboard.$container.find('.dashboardCategories').on('click',
'button',function(){
var$button=$(this);
varitemName=$button.text();
dashboard.informationBox.openNew(itemName);
});
};
dashboard.categories.selectCategory=function(categoryIndex){
var$dashboardCategories=
dashboard.$container.find('.dashboardCategory');
var$selectedItem=$dashboardCategories.eq(categoryIndex).show();
$dashboardCategories.not($selectedItem).hide();
};
})();
Thissubmodule’sinitializationmethodusesthereferencetothe$containerelementthatthemainModuleprovidesandaddstwoobserverstothepage.Thefirsthandlesthechangeeventonthe<select>categoryandcallstheselectCategory()methodwiththenumericvalueoftheselectedcategory.TheselectCategory()methodofthissubmodulewillthenhandlerevealingtheappropriatecategoryitems,decouplingitfromtheeventhandlingcodeandmakingitareusablefunctionalityavailabletotheentireapplication.
Rightafterthis,wecreateasingleDelegatedEventObserverthathandlestheclickeventonthe<button>categoryitem.Itextractsthetextofthe<button>pressedandcallstheopenNew()methodoftheinformationBoxsubmodulethatcontainsalltheimplementationrelatedtoinformationboxes.Inanon-demogradeapplication,aparametertosuchamethodwouldprobablybeanidentifierinsteadofatextvaluethatwouldbeusedtoretrievemoredetailsfromaremoteserver.
TheinformationBoxmoduleTheinformationBoxsubmodulethatcontainstheimplementationpartsrelatedtothemainareaofourdashboardhasthefollowingform:
(function(){
'usestrict';
dashboard.informationBox=dashboard.informationBox||{};
var$boxContainer=null;
dashboard.informationBox.init=function(){
$boxContainer=dashboard.$container.find('.boxContainer');
$boxContainer.on('click','.boxCloseButton',function(){
var$button=$(this);
dashboard.informationBox.close($button);
});
};
dashboard.informationBox.openNew=function(itemName){
varboxHtml='<divclass="boxsizer"><articleclass="box">'+
'<headerclass="boxHeader">'+
itemName+
'<buttonclass="boxCloseButton">✖'+
'</button>'+
'</header>'+
'Informationboxregarding'+itemName+
'</article></div>';
$boxContainer.append(boxHtml);
};
dashboard.informationBox.close=function($boxElement){
$boxElement.closest('.boxsizer').remove();
};
})();
Thefirstthingthatthissubmodule’sinitializationcodedoesisretrieveandstoreareferenceofthecontainerthatholdstheinformationboxestothe$boxContainervariable,usingthe$containerpropertyofthedashboardforscoping.
TheopenNew()methodisresponsibleforcreatingtheHTMLrequiredforanewinformationboxandaddingittothedashboardusingthe$boxContainervariable,whichactslikeaprivatememberoftheModule,andisusedforcachingthereferenceofthepreviouslyassignedDOMelement.Thisisagoodpracticethatcanimprovetheapplication’sperformance,sincethestoredelementisneverremovedfromthepageandisusedduringtheinitializationandtheopenNew()methodsoftheModule.Thisway,wenolongerneedtoexecuteslowDOMtraversalseverytimetheopenNew()methodiscalled.
Theclose()method,ontheotherhand,isresponsibleforremovinganexistinginformationboxfromthedashboard.ItreceivesajQuerycompositecollectionobjectasa
parameterrelatedtothetargetinformationbox,whichisbasedonthewaythatthe$.fn.closest()methodworks,andcaneitherbetheboxelementcontaineroranyofitsdescendants.
TipImplementationsofmethodsthatprovideflexibilityregardingthewaythattheycanbecalledcanmakethemusablebymorepartsofalargeapplication.Thenextlogicalstepforthismethod,whichisleftasanexercisetothereader,wouldbetomakeitacceptasaparameter,theindex,oranidentifieroftheinformationboxthatneedstobeclosed.
ThecountermoduleLastly,hereishowwerewrotethecounterimplementation,whichwesawinthepreviouschapter,asanindependentsubmodule:
(function(){
'usestrict';
dashboard.counter=dashboard.counter||{};
vardashboardItemCounter;
var$counter;
dashboard.counter.init=function(){
$counter=$('#dashboardItemCounter');
var$boxContainer=dashboard.$container.find('.boxContainer');
varinitialCount=$boxContainer.find('.boxsizer').length;
dashboard.counter.setValue(initialCount);
dashboard.$container.find('.dashboardCategories').on('click',
'button',function(){
dashboard.counter.setValue(dashboardItemCounter+1);
});
$boxContainer.on('click','.boxCloseButton',function(){
dashboard.counter.setValue(dashboardItemCounter-1);
});
};
dashboard.counter.setValue=function(value){
dashboardItemCounter=value;
$counter.text(dashboardItemCounter);
};
})();
Forthissubmodule,weareusingthe$countervariableasaprivatemembertocacheareferencetotheelementthatdisplaysthecount.AnotherprivatememberoftheModuleisthedashboardItemCountervariable,whichatanypointoftimewillholdthenumberofvisibleinformationboxesinthedashboard.KeepingsuchinformationonthemembersofourModulesreducesthetimesweneedtoreachtheDOMtreetoextractinformationonthestateoftheapplication,makingtheimplementationmoreefficient.
TipPreservingthestateoftheapplicationinthepropertiesofJavaScriptobjectsorModulesinsteadofreachingtheDOMtoextractthem,isaverygoodpracticethatmakestheapplication’sarchitecturemoreobject-oriented,andisalsoadoptedbymostofthemodernwebdevelopmentframeworks.
DuringtheinitializationoftheModule,wearegivinganinitialvaluetoourcountervariablesothatwearenolongerdependentontheinitialHTMLofthepageandhavea
morerobustimplementation.Moreover,weareattachingtwoDelegatedEventObservers,oneforclicksthatwillleadtothecreationofnewinformationboxesandanotheroneforclicksthatwillclosethem.
OverviewoftheimplementationWiththeabove,wecompletedtherewriteofthedashboardskeletonapplicationtoamodulararchitecture.Alltheavailableactionsareexposedaspublicmethodsofeachofoursubmodulesthatcanbeinvokedprogrammaticallyandthiswaytheyaredecoupledfromtheeventsthattriggerthem.
Agoodexerciseforthereaderwouldbetopromotethedecouplingevenfurther,byalsoadoptingthePublisher/SubscriberPatternintheaboveimplementation.ThefactthatthecodeisalreadystructuredintoModuleswillmakesuchchangealoteasiertoimplement.
Anotherpartthatcanbeimplementedinadifferentwayisthewayinwhichthesubmodulesareinitialized.InsteadofexplicitlyorchestratingtheinitializationofeachModuleinourmaindashboardModule,wecouldinsteadinitializeeachsubmoduleonitsownbywrappingtheinvocationoftheinit()methodina$(document).ready()callandissuingitsinitializationrightafteritsdeclaration.Ontheotherhand,nothavingacentralpointtocoordinatetheinitializationsandrelyingonpageeventscanfeellessdeterministic.AnotherwaytoimplementitwouldbelikethePublisher/SubscriberPattern,byexposingaregisterForInit()methodonourmainModule,whichwouldkeeptrackoftheModulesthathavebeenrequestedtobeinitializedusinganarray.
NoteFormorejQuerycodeorganizationtips,youcanvisithttp://learn.jquery.com/code-organization/concepts/.
SummaryInthischapter,welearnedtheconceptsofModulesandNamespacesandalsothebenefitsthatcomefromtheiradoptioninlargeapplications.Wehadanin-depthanalysisofthemostwidelyadoptedpatternsandcomparedtheirbenefitsandlimitations.WelearnedbyexamplehowtodevelopModulesusingtheObjectLiteralPattern,thevariantsoftheModulePattern,andtheRevealingModulePattern.
WecontinuedwithasmallintroductiontoES5’sStrictModeandsawhowitcanbenefittoday’sModules.ThenweproceededbylearningsomedetailsaboutthestandardizedbutnotyetwidelysupportedES6Modules.Lastly,wesawhowthearchitectureofthedashboardapplicationcanchangedramaticallyafterusingtheModulePatterninitsimplementation.
NowthatwehavecompletedourintroductiononhowtouseModulesandNamespaces,wecanmoveontothenextchapterwherewewillbeintroducedtothefacadepattern.Inthenextchapter,wewilllearnaboutthephilosophyoffacadesandtheuniformwaythattheydefinehowcodeabstractionsshouldbecreatedsothattheyareeasilyunderstandableandreusablebyotherdevelopers.
Chapter5.TheFacadePatternInthischapter,wewillshowcasetheFacadePattern,astructuraldesignpatternthattriestodefineauniformwayregardinghowdevelopersshouldcreateabstractionsintheircode.Initially,wewillusethispatterntowrapcomplexAPIsandexposesimpleronesthatfocusontheneedsofourapplication.WewillseehowjQueryembracestheconceptsofthispatterninitsimplementation,howitachievesencapsulatingcompleximplementationsthatareintegralpartsofthewebdeveloper’stool-beltintoeasy-to-useAPI’s,andhowthisplaysacriticalroleforitswideadoption.
Inthischapter,wewill:
IntroducetheFacadePatternDocumentitskeyconceptsandbenefitsSeehowjQueryusesitinitsimplementationWriteanexampleimplementationwhereFacadesareusedtocompletelyabstractanddecoupleathird-partylibrary
IntroducingtheFacadePatternTheFacadeisastructuralsoftwaredesignpatternthatdealswithhowabstractionsofthevariouspartsofanimplementationshouldbecreated.ThekeyconceptoftheFacadePatternistoabstractanexistingimplementationandprovideasimplifiedAPIthatbettermatchestheusecasesofthedevelopedapplication.AccordingtomostComputerSciencebibliographiesdescribingthispattern,aFacadeismostcommonlyimplementedasaspecializedclassthatisusedtosegmenttheimplementationofanapplicationintosmallerpiecesofcode,whileprovidinganinterfacethatcompletelyhidestheencapsulatedcomplexity.Inthewebdevelopmentworld,itisalsocommontouseplainobjectsorfunctionsfortheimplementationofaFacade,takingadvantageofthewayinwhichJavaScripttreatsfunctionsasobjects.
Inapplicationsthathaveamodularstructure,liketheexamplesofthepreviouschapter,itisalsocommontoimplementFacadesasseparatemoduleswiththeirownnamespace.Moreover,foralargerimplementationwithverycomplexparts,anapproachwithmultiplelevelsofFacadescanalsobefollowed.Onceagain,theFacadeswillbeimplementedasmodulesandsubmodules,havingthetop-levelFacadeorchestratingthemethodsofitssubmodules,whileprovidinganAPIthatcompletelyhidesthecomplexityoftheentiresubsystem.
ThebenefitsofthispatternMostofthetime,theFacadePatternisadoptedforimplementationpartsthathavearelativelyhighdegreeofcomplexityandareusedinseveralplacesofanapplication,whereinlargepiecesofcodecanbereplacedwithasimplecalltothecreatedFacade,leadingnotonlytolesscoderepetition,butalsohelpingustoincreasethereadabilityoftheimplementation.SincetheFacademethodsareusuallynamedbythehigher-levelapplicationconceptsthattheyencapsulate,theresultingcodeisalsoeasiertounderstand.ThesimplifiedAPIthataFacadeprovidesthroughitsconvenientmethods,leadstoanimplementationthatiseasiertouse,understand,andalsowriteunittestsfor.
Moreover,havingFacadestoabstractcompleximplementationsprovesitsusefulnessincaseswherethereisaneedtointroduceachangetothebusinesslogicoftheimplementation.IncaseaFacadehasawell-designedAPIwithapredictionforfuturerequirements,suchchangescanoftenrequiremodificationsjusttotheFacade’scode,leavingtherestoftheapplication’simplementationuntouchedandfollowingtheSeparationofConcernsprinciple.
Inthesamemanner,usingFacadestoabstracttheAPIofathird-partylibrarytobettermatchtheneedsofeachapplication,providesadegreeofdecouplingbetweenourcodeandtheusedlibrary.Incasethethird-partylibrarychangesitsAPIorneedstobereplacedwithanotherone,thedifferentmodulesoftheapplicationwillnotneedtoberewritten,sincetheimplementationchangeswouldbelimitedtothewrapperFacade.Inthiscase,allthatisneededistoprovideanequivalentimplementationusingthenewlibraryAPIwhilekeepingtheFacade’sAPIintact.
Asanexampleoforchestratingmethodcallsandusingsensibledefaultsforspecificusecases,takealookatthefollowingsampleimplementation:
functiondo(x,y){
varz=y-x/2;
varyy=Math.pow(y,2);
varb=3*Math.random();//addsomerandomnesstotheresult
vari=0;//forthiscase
returnLibraryA.doingMethod(x,z,i,yy,b);
}
HowitisadoptedbyjQueryAverylargepartofthejQueryimplementationisdedicatedtoprovidingsimpler,shorter,andmoreconvenient-to-usemethodsforthingsthatthedifferentJavaScriptAPIsalreadyallowustoachieve,butwithmorelinesofcodeandeffort.BytakingalookattheprovidedAPIsofjQuery,wecandistinguishsomegroupsofrelatedmethods.Thisgroupingcanalsobeseeninthewayinwhichthesourcecodeisstructured,placingmethodsforrelatedAPIsneartoeachother.
EvenifthewordFacadedoesnotappearinjQuery’ssourcecode,theuseofthispatterncanbewitnessedbythewayinwhichtherelatedmethodsaredefinedontheexposedjQueryobject.Mostofthetime,therelatedmethodsthatformagroupareimplementedanddefinedaspropertiesonanObjectLiteralandthenattachedtothejQueryobjectwithasinglecalltothe$.extend()orthe$.fn.extend()method.Asyoumightremember,fromthebeginningofthischapter,thismatchesalmostexactlywiththeimplementationthatComputerSciencecommonlyusestodescribehowaFacadeisimplemented,withtheexceptionthat,inJavaScript,wecancreateaplainobjectwithoutneedingtofirstdefineaclass.Asaresult,jQueryitselfcanbeseenasacollectionofFacades,whereeachoneindependentlyaddsgreatvaluetothelibrarywiththeAPIofconvenientmethodsthatitprovides.
NoteFormoreinformationon$.extend()and$.fn.extend(),youcanvisithttp://api.jquery.com/jQuery.extend/andhttp://api.jquery.com/jQuery.fn.extend/.
SomeoftheabstractedAPIgroupsthatarebigpartsofthejQueryimplementationandplayacriticalroletoitsadoptionareasfollows:
TheDOMTraversalAPITheAJAXAPITheDOMManipulationAPITheEffectsAPI
Also,agreatexampleofhowthispatterncanbeusedtoprovidesimplifiedAPIsisjQuery’sEventsAPI,whichprovidesavarietyofconvenientmethodsforthemostcommonusecasesthatareeasiertousethantherespectiveplainJavaScriptAPIs.
ThejQueryDOMTraversalAPIAtthetimethatjQuerywasreleased,webdeveloperscouldlocatespecificDOMelementsofapageonlybyusingtheverylimitedgetElementById()andgetElementsByTagName()methods,sinceothermethods,suchasgetElementsByClassName(),werenotwidelysupportedbytheexistingbrowsers.ThejQueryteamrealizedhowthewebdevelopmentcouldbeleveragediftherewasasimpleAPIthatwouldeasesuchDOMtraversals,whichwouldworkthesamewayacrossallbrowsers,beaseffectiveasthefamiliarCSSSelectors,anddidtheirbesttomakesuchanimplementationareality.
TheresultofthiseffortisthenowfamousjQueryDOMTraversalAPIthatisexposedthroughthe$()function,whichplayedaseriousroleinthestandardizationofthequerySelectorAll()methodaspartoftheLevel2SelectorAPI.TheimplementationunderthehoodusesthemethodsprovidedbytheDOMAPIandcountsabout2,135linesofcodeinjQueryv2.2.0,whileitisevenbiggerinthev1.xversionsthatneededtosupportolderbrowsersaswell.Aswesawinthischapter,becauseofitscomplexitythisimplementationisnowpartofaseparatestand-aloneprojectthatisnamedSizzle.
NoteFormoreinformationonSizzleandthequerySelectorAll()method,youcanvisithttps://github.com/jquery/sizzleandhttps://developer.mozilla.org/en-US/docs/Web/API/document/querySelectorAll.
Regardlessofitscompleximplementation,theexposedAPIsarequiteeasytouse,mostlyusingsimpleCSSSelectorsasstringparameters,makingitanexcellentexampleofhowaFacadecanbeusedtocompletelyhidethecomplexityofitsinnerworkingsandexposeaconvenientAPI.SinceSizzle’sAPIisstillquitecomplex,thejQuerylibraryactuallywrapsitwithitsownAPIactingasanextraFacadelevel:
//Line733
functionSizzle(selector,context,results,seed){/*...*/}
//Line2678
jQuery.find=Sizzle;
ThejQuerylibraryfirstkeepsareferenceofSizzletotheinternaljQuery.find()methodandthenusesittoimplementallitsexposedDOMTraversalmethods,whichworkonCompositeObjectssuchas$.fn.find():
//Line2769
jQuery.fn.extend({
find:function(selector){
/*15linesofcode*/
for(i=0;i<len;i++){
jQuery.find(selector,self[i],ret);
}
/*3linesofcode*/
returnret;
}
});
Finally,thefamous$()functioncanactuallybeinvokedinseveralways,butevenwhenitisinvokedwithaCSSSelectorasastringparameter,itactuallyhasanextralevelofhiddencomplexity:
//Line71
jQuery=function(selector,context){
returnnewjQuery.fn.init(selector,context);
};
//Line2825
rquickExpr=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
//Line2735
init=jQuery.fn.init=function(selector,context,root){
/*12linesofcode*/
if(typeofselector==="string"){
if(/*...*/){
/*3linesofcode*/
}else{
match=rquickExpr.exec(selector);
}
//Matchhtmlormakesurenocontextisspecifiedfor#id
if(match&&(match[1]||!context)){
if(match[1]){
/*27linesofcode*/
//HANDLE:$(#id)
}else{
elem=document.getElementById(match[2]);
//Support:Blackberry4.6
//gEBIDreturnsnodesnolongerinthedocument(#6963)
if(elem&&elem.parentNode){
//InjecttheelementdirectlyintothejQueryobject
this.length=1;
this[0]=elem;
}
this.context=document;
this.selector=selector;
returnthis;
}
//HANDLE:$(expr,$(...))
}elseif(!context||context.jquery){
return(context||root).find(selector);
//HANDLE:$(expr,context)
//(whichisjustequivalentto:$(context).find(expr)
}else{
returnthis.constructor(context).find(selector);
}
}/*else…21linesofcode*/
};
Asyoucansee,intheprecedingcode,the$()isactuallycreatinganewobjectwith$.fn.init().Insteadofbeingjustanentrypointto$.fn.find()orjQuery.find(),itisactuallyaFacadethathidesalevelofoptimization.Specifically,itmakesjQueryfasterbyavoidinginvoking$.fn.find()andSizzle,whensimpleIDselectorsareusedbydirectlyinvokingthegetElementById()method.
ThepropertyaccessandmanipulationAPIAnotherveryinterestingabstractionthatfollowstheprinciplesoftheFacadePatternandcanbefoundinjQuery’ssource,isthe$.fn.prop()method.Likethe$.fn.attr(),$.fn.val(),$.fn.text(),and$.fn.html(),itbelongstoafamilyofmethodsthatischaracterizedbythefactthateachmethodisbothagetterandasetteroftherelatedsubject.Thedistinctionofthemethod’sexecutionmodeisdonebyinspectingthenumberofparametersthatarepassedduringitsinvocation.ThisconvenientAPIallowsustohavetorememberlessmethodsignaturesandmakethesettersdifferonlybyoneextraparameter.Forexample,$('#myCheckBox').prop('checked')willreturntrueorfalse,basedonthestateoftheselectedcheckbox.Ontheotherhand,$('#myCheckBox').prop('checked',true);willprogrammaticallycheckthatcheckboxforus.Inthesameconcept,$('button').prop('disabled',true);willdisableallthe<button>elementsonapage.
The$.fn.prop()methoddoesthejQueryCompositeObjecthandling,buttheactualimplementationoftheFacadeistheinternaljQuery.prop()method.AnextraconcernthataddscomplexitytotheFacade’simplementationisthefactthattherearesomeHTMLattributesthathavedifferentidentifiersforthecorrespondingpropertiesontheDOMelements:
jQuery.extend({
prop:function(elem,name,value){
/*8liesofcode*/
if(nType!==1||!jQuery.isXMLDoc(elem)){
//Fixnameandattachhooks
name=jQuery.propFix[name]||name;
hooks=jQuery.propHooks[name];
}
if(value!==undefined){
if(hooks&&"set"inhooks&&
(ret=hooks.set(elem,value,name))!==undefined){
returnret;
}
return(elem[name]=value);
}
if(hooks&&"get"inhooks&&(ret=hooks.get(elem,name))!==
null){
returnret;
}
returnelem[name];
},
propHooks:{
tabIndex:{
get:function(elem){
vartabindex=jQuery.find.attr(elem,"tabindex");
returntabindex?parseInt(tabindex,10):/*...*/;
}
}
},
propFix:{
"for":"htmlFor",
"class":"className"
}
});
ThefirsthighlightedcodeareaefficientlyresolvesthepropertytoattributeidentifiermismatchbyusingthepropFixandpropHooksobjectstodothematching.ThepropFixobjectactslikeasimpledictionarytomatchtheidentifiers,whilethepropHooksobjectholdsafunctionthatdoesthematchinginaless-hard-codedway,withprogrammatictesting.Thisisagenericimplementationthatcaneasilybeextendedbyaddingextrapropertiestothosetwoobjects.
Therestofthehighlightedareasareresponsibleforthegetter/settermodeofthemethod.Theoverallimplementationistoperformthefollowingtasks:
Checkwhetheravalueispassedasanargumentand,ifthepropertyfindsthattheassignmentissuccessful,dotheassignmentandreturnthevalue.Alternatively,iftherewasnovaluepassed,returnthevalueoftherequestedpropertyifitisretrievable.
UsingFacadesinourapplicationsInordertodemonstratehowfacadescanbeusedbothtoencapsulatecomplexity,helpingusenforcetheSeparationofConcernsprinciple,andalsoabstractthird-partylibraryAPIsintomoreconvenientmethodsthatareapplicationcentric,wearegoingtodemonstrateaverysimplelotteryapplication.Our“ElementLottery”applicationwillpopulateitscontainerwithsomeLotteryTicketelementsthatwillhaveauniqueIDandcontainarandomnumber.
Thewinningticketwillbepickedbyrandomlyselectingoneofthelotteryelements,basedonarandomindexamongthecreateduniqueIDs.Thewinningnumberwillthenbeannouncedtobethenumericcontentofthepickedelement.Let’sseethemodulesofourapplication:
(function(){
window.elementLottery=window.elementLottery||{};
varelementIDs;
var$lottery;
varticketCount=30;
elementLottery.init=function(){
elementIDs=[];
$lottery=$('#lottery').empty();
elementLottery.add(ticketCount);
$('#lotteryTicketButton').on('click',elementLottery.pick);
};
elementLottery.add=function(n){
for(vari=0;i<n;i++){
varid=this.uidProvider.get();
elementIDs.push(id);
$lottery.append(this.ticket.createHtml(id));
}
};
elementLottery.pick=function(){
varindex=Math.floor(Math.random()*elementIDs.length);
varresult=$lottery.find('#'+elementIDs[index]).text();
alert(result);
returnresult;
};
$(document).ready(elementLottery.init);
})();
ThemainelementLotterymoduleofourapplicationinitializeditselfrightafterthepagewasfullyloaded.Theaddmethodisusedtopopulatethelotterycontainerelementwithtickets.ItusestheuidProvidersubmoduletogenerateuniqueidentifiersfortheticketelements,keepstrackofthemontheelementIDsarray,usestheticketsubmoduletoconstructtheappropriateHTMLcode,andfinallyappendstheelementtothelottery.Thepickmethodisusedtorandomlyselectthewinningticketbyrandomlyselectingoneofthegeneratedidentifiers,retrievingthepageelementwiththatID,anddisplayingitscontentinsideanalertboxasthewinningresult.ThepickmethodistriggeredbyclickingonthebuttonthatwehaveaddedanObserverduringtheinitializationphase:
(function(){
elementLottery.ticket=elementLottery.ticket||{};
elementLottery.ticket.createHtml=function(id){
varticketNumber=Math.floor(Math.random()*1000*10);
return'<divid="'+id+'"class="ticket">'+ticketNumber+
'</div>';
};
})();
(function(){
elementLottery.uidProvider=elementLottery.uidProvider||{};
elementLottery.uidProvider.get=function(){
return'Lot'+simpleguid.getNext();
};
})();
TheticketsubmobuleactsasaFacadewithasinglemethodthatisusedtoencapsulatethegenerationofarandomnumberandthecreationoftheHTMLcodethatwillbeusedastheticket.Ontheotherhand,theuidProvidesubmoduleisaFacadethatprovidesasinglegetmethodthatencapsulatesthewayweusethesimpleguidmodulethatwesawinthepreviouschapters.Asaresult,wecaneasilychangethelibrarythatisusedtogenerateuniqueidentifiersandtheonlyplacethatwewillhavetomodifytheexistingimplementationwillbetheuidProvidesubmodule.Forexample,let’sseehowitwilllookifwedecidedtousethegreatnode-uuidlibrarythatgenerates128-bituniqueidentifiersasstringsofhexadecimalcharacters:
(function(){
elementLottery.uidProvider=elementLottery.uidProvider||{};
elementLottery.uidProvider.get=function(){
returnuuid.v4();
};
})();
NoteFormoreinformationonthenode-uuilibrary,youcanvisithttps://github.com/broofa/node-uuid.
SummaryInthischapter,welearnedwhataFacadeactuallyis.Welearneditsphilosophyandtheuniformwayinwhichitdefineshowcodeabstractionsshouldbecreatedsothattheyareeasilyunderstandableandreusablebyotherdevelopers.
Startingfromthesimplestusecasesofthispattern,welearnedhowtowrapacomplexAPIwithaFacadeandexposeasimpleronethatisfocusedontheneedsofourapplicationandisabettermatchtoitsspecificusecases.WealsosawhowjQueryembracestheconceptsofthispatterninitsimplementationandhowprovidingsimpleAPIsformorebasicweb-developingtechniques,suchasDOMTraversals,playedacriticalroleforitswideadoption.
NowthatwehavecompletedourintroductiontohowtheFacadePatterncanbeusedtodecoupleandabstractpartsofanimplementation,wecanmoveontothenextchapterwherewewillbeintroducedtotheBuilderandFactoryPatterns.Inthenextchapter,wewilllearnhowtousethesetwoCreationalDesignPatternstoabstracttheprocessofgeneratingandinitializingnewobjectsforspecificusecasesandanalyzehowtheiradoptioncanbenefitourimplementations.
Chapter6.TheBuilderandFactoryPatternsInthischapter,wewillshowcasetheBuilderandFactoryPatterns,twoofthemostcommonlyusedCreationalDesignPatterns.Thesetwodesignpatternshavesomesimilaritieswitheachother,sharesomecommongoals,andarededicatedtoeasingthecreationofcomplexresults.Wewillanalyzethebenefitsthattheiradoptioncanbringtoourimplementationsandalsothewaysinwhichtheydiffer.Finally,wewilllearnhowtousethemproperlyandchoosethemostappropriateoneforthedifferentusecasesofourimplementations.
Inthischapter,wewill:
IntroducetheFactoryPatternSeehowtheFactoryPatternisusedbyjQueryHaveanexampleoftheFactoryPatteninajQueryapplicationIntroducetheBuilderPatternComparetheBuilderandFactoryPatternsSeehowtheBuilderPatternisusedbyjQueryHaveanexampleoftheBuilderPatteninajQueryapplication
IntroducingtheFactoryPatternTheFactoryPatternispartofthegroupofCreationalPatternsandoverallitdescribesagenericwayforobjectcreationandinitialization.Itiscommonlyimplementedasanobjectorfunctionthatisusedtogenerateotherobjects.AccordingtothemajorityofComputerScienceresources,thereferenceimplementationoftheFactoryPatternisdescribedasaclassthatprovidesamethodthatreturnsnewlycreatedobjects.Thereturnedobjectsarecommonlytheinstancesofaspecificclassorsubclass,ortheyexposeasetofspecificcharacteristics.
ThekeyconceptoftheFactorypatternistoabstractthewayanobjectoragroupofrelatedobjectsarecreatedandinitializedforaspecificpurpose.Thepointofthisabstractionistoavoidcouplinganimplementationwithspecificclassesorthewaythateachobjectinstanceneedstobecreatedandconfigured.Theresultisanimplementationthatworksasanabstractwayforobjectcreationandinitialization,whichfollowstheconceptofSeparationofConcerns.
Theresultingimplementationsareonlybasedontheobjectmethodsandpropertiesthatarerequiredbytheiralgorithmorbusinesslogic.Suchanapproachcanbenefitthemodularityandextensibilityofanimplementation,byfollowingtheconceptofprogrammingoverObjectFeaturesandFunctionalityinsteadofObjectClasses.Thisgivesustheflexibilitytochangetheusedclasseswithanyotherobjectthatexposesthesamefunctionality.
HowitisadoptedbyjQueryAswehavealreadynotedintheearlierchapters,oneoftheearlygoalsofjQuerywastoprovideasolutionthatworkedthesameacrossallbrowsers.The1.12.xversionseriesofjQueryarefocusedonprovidingsupportforbrowsersasoldasInternetExplorer6(IE6),whilemaintainingthesameAPIwiththenewerv2.2.xversionsthatonlyfocusonmodernbrowsers.
Inordertohaveasimilarstructureandmaximizethecommoncodebetweenthetwoversions,thejQueryteamtriedtoabstractmostcompatibilitymechanismsinadifferentimplementationlayer.Suchadevelopmentpracticegreatlyimprovesthereadabilityofthecodeandreducesthecomplexityofthemainimplementation,encapsulatingitintodifferentsmallerpieces.
AgreatexampleofthisistheimplementationoftheAJAX-relatedmethodsthatjQueryprovides.Specifically,inthefollowingcode,youcanfindapartofit,asfoundinversion1.12.0ofjQuery:
//Createtherequestobject
//(ThisisstillattachedtoajaxSettingsforbackwardcompatibility)
jQuery.ajaxSettings.xhr=window.ActiveXObject!==undefined?
//Support:IE6-IE8
function(){
//XHRcannotaccesslocalfiles,alwaysuseActiveXforthatcase
if(this.isLocal){
returncreateActiveXHR();
}
//Support:IE9-11
if(document.documentMode>8){
returncreateStandardXHR();
}
//Support:IE<9
return/^(get|post|head|put|delete|options)$/i.test(this.type)&&
createStandardXHR()||createActiveXHR();
}:
//Forallotherbrowsers,usethestandardXMLHttpRequestobject
createStandardXHR;
//Functionstocreatexhrs
functioncreateStandardXHR(){
try{
returnnewwindow.XMLHttpRequest();
}catch(e){}
}
functioncreateActiveXHR(){
try{
returnnewwindow.ActiveXObject("Microsoft.XMLHTTP");
}catch(e){}
}
EverytimeanewAJAXrequestisissuedonjQuery,thejQuery.ajaxSettings.xhrmethodisusedasaFactorythatcreatesanewinstanceoftheappropriateXHRobjectbasedonthesupportofthecurrentbrowser.Lookinginmoredetail,wecanseethatthejQuery.ajaxSettings.xhrmethodorchestratestheuseoftwosmallerFactoryfunctions,witheachresponsibleforaspecificimplementationofAJAX.Moreover,wecanseethatitactuallytriestoavoidrunningthecompatibilitytestsoneverycallbydirectlywiringupitsreferencetothesmallercreateStandardXHRFactoryfunctionwhenappropriate.
UsingFactoriesinourapplicationsAsanexampleusecaseofFactories,wewillcreateadata-drivenformwhereouruserswillbeabletofillsomefieldsthataredynamicallycreatedandinsertedintothepage.Wewillassumetheexistenceofanarraycontainingobjectsthatdescribeeachformfieldthatneedstobepresented.OurFactorymethodwillencapsulatethewayinwhicheachformfieldneedstobeconstructed,andproperlyhandleeachspecificcase,basedonthecharacteristicsdefinedontherelatedobjects.
TheHTMLcodeforthispageisquitesimple:
<h1>DataDrivenForm</h1>
<form></form>
<scripttype="text/javascript"src="jquery.js"></script>
<scripttype="text/javascript"src="datadrivenform.js"></script>
Itonlycontainsan<h1>elementwiththepageheadingandanempty<form>elementthatwillhostthegeneratedfields.AsfortheCSSused,weonlystylethe<button>elementsinthesamewayaswedidinthepreviouschapters.
AsfortheJavaScriptimplementationoftheapplication,wecreateamoduleanddeclare
dataDrivenFormasthenamespaceofthisexample.Thismodulewillcontainthedatathatdescribesourform,theFactorymethodthatwillgeneratetheHTMLofeachformelementand,ofcourse,theinitializationcodethatwillcombinetheaforementionedpartstocreatetheresultingform:
(function(){
'usestrict';
window.dataDrivenForm=window.dataDrivenForm||{};
dataDrivenForm.formElementHTMLFactory=function(type,name,title){
if(!title||!title.length){
title=name;
}
vartopPart='<div><label><span>'+title+':</span><br/>';
varbottomPart='</label></div>';
if(type==='text'){
returntopPart+
'<inputtype="text"maxlength="200"name="'+name+'"/>'+
bottomPart;
}elseif(type==='email'){
returntopPart+
'<inputtype="email"requiredname="'+name+'"/>'+
bottomPart;
}elseif(type==='number'){
returntopPart+
'<inputtype="number"min="0"max="2147483647"'+'name="'+name+
'"/>'+
bottomPart;
}elseif(type==='date'){
returntopPart+
'<inputtype="date"min="1900-01-01"name="'+
name+'"/>'+
bottomPart;
}elseif(type==='textarea'){
returntopPart+
'<textareacols="30"rows="3"maxlength="800"name="'+name+'"
/>'+
bottomPart;
}elseif(type==='checkbox'){
return'<div><label><span>'+title+':</span>'+
'<inputtype="checkbox"name="'+name+'"/>'+
'</label></div>';
}elseif(type==='notice'){
return'<p>'+name+'</p>';
}elseif(type==='button'){
return'<buttonname="'+name+'">'+title+'!</button>';
}
};
})();
OurFactorymethodwillbeinvokedwiththreeparameters.Startingfromthemostimportantone,itacceptsthetypeandthenameoftheformfieldandalsothetitlethatwillbeusedasitsdescription.Sincemostformfieldssharesomecommoncharacteristics,
liketheirtitle,theFactorymethodtriestoabstracttheminordertohavelesscoderepetition.Asyoucansee,theFactorymethodalsocontainssomesensibleextraconfigurationforeachfieldtype,likethemaxlengthattributeofthetextfields,thatisspecificforthisusecase.
TheobjectstructurethatwillbeusedtorepresenteachformelementwillbeaplainJavaScriptobjectthathasatype,name,andtitleproperty.ThecollectionofobjectsthatdescribetheformfieldswillbegroupedinanarrayandbeavailableonthedataDrivenForm.partspropertyofourmodule.Inareal-worldapplication,thesefieldswouldcommonlyeitherberetrievedwithanAJAXrequestorbeinjectedintosomepartoftheHTMLofthepage.Inthefollowingcodesnippet,wecanseethedatathatwillbeusedtodrivethecreationofourform:
dataDrivenForm.parts=[{
type:'text',
name:'firstname',
title:'FirstName'
},{
type:'text',
name:'lastname',
title:'LastName'
},{
type:'email',
name:'email',
title:'e-mailaddress'
},{
type:'date',
name:'birthdate',
title:'Dateofbirth'
},{
type:'number',
name:'experience',
title:'Yearsofexperience'
},{
type:'textarea',
name:'summary',
title:'Summary'
},{
type:'checkbox',
name:'receivenotifications',
title:'Receivenotificatione-mails'
},{
type:'notice',
name:'Byusingthisformyouacceptthetermsofuse'
},{
type:'button',
name:'save'
},{
type:'button',
name:'submit'
}];
Finally,wedefineandimmediatelyinvokeaninitmethodforourmodule:
dataDrivenForm.init=function(){
for(vari=0;i<dataDrivenForm.parts.length;i++){
varpart=dataDrivenForm.parts[i];
varelementHTML=dataDrivenForm.formElementHTMLFactory(part.type,
part.name,part.title);
//checkiftheresultisnull,undefinedoremptystring
if(elementHTML&&elementHTML.length){
$('form').append(elementHTML);
}
}
};
$(document).ready(dataDrivenForm.init);
TheinitializationcodewaitsuntiltheDOMofthepageisfullyloadedandthenusestheFactorymethodtocreatetheformelementsandattachthemtothe<form>elementofourpage.AnextraconcernoftheprecedingcodeistochecktheresultoftheFactorymethodinvocationbeforeactuallystartingtouseit.
MostFactories,wheninvokedwithparametersforacasetheycan’thandle,returnnulloremptyobjects.Asaresult,it’sagoodcommonpractice,whenusingFactories,tocheckwhethertheresultofeachinvocationisactuallyvalid.
Asyoucansee,havingFactoriesthatacceptonlysimpleparameters(forexample,stringsandnumbers),inmanycases,leadstoanincreasednumberofparameters.Eventhoughtheseparametersmayonlybeusedinspecificcases,theAPIofourFactorystartstobeawkwardlylongandneedsproperdocumentationforeachspecialcaseinordertobeusable.
Ideally,aFactorymethodshouldacceptasfewargumentsaspossible,otherwiseitwillstartlookinglikeaFacadethatonlyprovidesadifferentAPI.Since,insomecases,usingasinglestringornumericargumentdoesnotsuffice,inordertoavoidusingahugenumberofparameters,wecanfollowapracticewheretheFactoryisdesignedtoacceptasingleobjectasitsparameter.
Forexample,inourcase,wecanjustpassthewholeobjectthatdescribestheformfieldasaparametertotheFactorymethod:
dataDrivenForm.formElementHTMLFactory=function(formElementDefinition){
vartopPart='<div><label><span>'+formElementDefinition.title+':
</span><br/>';
varbottomPart='</label></div>';
if(formElementDefinition.type==='text'){
returntopPart+
'<inputtype="text"maxlength="200"name="'
+formElementDefinition.name+'"/>'+
bottomPart;
}/*...*/
};
Thispracticeissuggestedforthefollowingcases:
WhenwecreategenericFactoriesthatarenotfocusedonspecificusecasesandweneedtoconfiguretheirresultsdifferentlyforeachspecificusecase.
Whentheconstructedobjectshavemanyoptionalconfigurationparametersthatlargelydiffer.Inthiscase,addingthemasseparateparameterstotheFactorymethodwouldleadtoinvocationsthathaveanumberofnullarguments,dependingonwhichexactargumentweareinterestedindefining.
Anotherpractice,especiallyinJavaScriptprogramming,istocreateaFactorymethodthatacceptsasimplestringornumericvalueasitsfirstargumentandoptionallyprovideacomplementaryobjectasasecondparameter.ThisenablesustohaveasimplegenericAPIthatcanbeuse-case-specificandalsogivesussomeextrapointsoffreedomtoconfiguresomespecialcases.Thisapproachisusedbythe$.ajax(url[,settings])methodthatallowsustogeneratesimpleGETrequestsbyjustprovidingaURLandalsoacceptsanoptionalsettingsparameterthatallowsustoconfigureanyaspectoftherequest.Changingtheaboveimplementationtousethisvariationisleftasanexerciseforthereader,inordertoexperimentandgetfamiliarwiththeuseofFactorymethods.
IntroducingtheBuilderPatternTheBuilderPatternispartofthegroupofCreationalPatternsandprovidesusawaytocreateobjectsthatrequirealotofconfigurationbeforetheyreachthepointwheretheycanbeused.TheBuilderPatternisoftenusedforobjectsthatacceptmanyoptionalparametersinordertodefinetheiroperation.Anothermatchingcaseisforthecreationofobjectswheretheirconfigurationneedstobedoneinseveralstepsorinaspecificorder.
ThecommonparadigmfortheBuilderPatternaccordingtoComputerScienceisthatthereisaBuilderObjectthatprovidesoneormoresettermethods(setA(...),setB(...))andasinglegenerationmethodthatconstructsandreturnsthenewlycreatedresultobject(getResult()).
Thispatternhastwoimportantconcepts.ThefirstoneisthattheBuilderObjectexposesanumberofmethodsasawaytoconfigurethedifferentpartsoftheobjectthatisunderconstruction.Duringtheconfigurationphase,theBuilderObjectpreservesaninternalstatethatreflectstheeffectsoftheinvocationsoftheprovidedsettermethods.Thiscanbebeneficialwhenusedtocreateobjectsthatacceptalargenumberofconfigurationparameters,solvingtheproblemofTelescopicConstructors.
NoteTelescopicConstructorsisananti-patternofobject-orientedprogrammingthatdescribesthesituationwhereaclassprovidesseveralconstructorsthattendtodifferonthenumber,thetype,andthecombinationoftheargumentsthattheyrequire.Objectclasseswithseveralparametersthatcanbeusedinmanydifferentcombinationscanoftenleadtoimplementationsfallingintothisanti-pattern.
Thesecondimportantconceptisthatitalsoprovidesagenerationmethodthatreturnstheactualconstructedobjectbasedontheprecedingconfiguration.Mostofthetime,theinstantiationoftherequestedobjectisdonelazilyandactuallytakesplaceatthemomentthatthismethodisinvoked.Insomecases,theBuilderObjectallowsustoinvokethegenerationmethodmorethanonce,allowingustogenerateseveralobjectswiththesameconfiguration.
HowitisadoptedbyjQuery’sAPITheBuilderPatterncanalsobefoundaspartoftheAPIthatjQueryexposes.Specifically,thejQuery$()functioncanalsobeusedtocreatenewDOMelementsbyinvokingitwithanHTMLstringasanargument.Asaresult,wecancreatenewDOMelementsandsettheirdifferentpartsasweneedthem,insteadofhavingtocreatetheexactHTMLstringthatisneededforthefinalresult:
var$input=$('<input/>');
$input.attr('type','number');
$input.attr('min','0');
$input.attr('max','100');
$input.prop('required',true);
$input.val(4);
$input.appendTo('form');
The$('<input/>')callreturnsaCompositeObjectcontaininganelementthatisnotattachedtotheDOMtreeofthepage.Thisunattachedelementisonlyanin-memoryobjectthatisneitherfullyconstructednorfullyfunctionaluntilweattachittothepage.Inthiscase,thisCompositeObjectactslikeaBuilderObjectInstancehavinganinternalstateofobjectsthatarenotyetfinalized.Rightafterthis,wedoaseriesofmanipulationsonitusingsomejQuerymethodsthatactlikethesettermethodsdescribedbytheBuilderPattern.
Finally,afterweapplyalltherequiredconfigurations,sothattheresultingobjectbehavesinthedesiredway,weinvokethe$.fn.appendTo()method.The$.fn.appendTo()methodworksasthegenerationmethodoftheBuilderPattern,byattachingthein-memoryelementofthe$inputvariabletotheDOMtreeofthepage,transformingitintoanactualattachedDOMelement.
Ofcourse,theaboveexamplecangetmorereadableandlessrepetitivebyutilizingtheFluentAPIthatjQueryprovidesforitsmethods,andalsocombinethe$.fn.attr()methodinvocations.Moreover,jQueryallowsustousealmostallitsmethodstodotraversalsandmanipulationsontheelementsthatareunderconstruction,justaswecanonnormalDOMelementCompositeObjects.Asaresult,theaboveexamplecangetalittlemorecompleteasfollows:
$('<input/>').attr({
'type':'number',
'min':'0',
'max':'100'
})
.prop('required',true)
.val(4)
.css('display','block')
.wrap('<label>')//wraptheinputwitha<label>
.parent()//traverseonelevelup,tothe<label>
.prepend('<span>Qty:#</span')
.appendTo('form');
Theresultwilllookasfollows:
Thecriteriathatallowustocategorizethisoverloadedwayofinvokingthe$()functionasanimplementationthatadoptstheBuilderPattern,isthefactthat:
Itreturnsanobjectwithaninternalstatecontainingpartiallyconstructedelements.Thecontainedelementsareonlyin-memoryobjectsthatarenotpartofthepage’sDOMtree.Itprovidesusmethodstomanipulateitsinternalstate.MostjQuerymethodscanbeusedforthispurpose.Itprovidesusmethod(s)togeneratethefinalresult.WecanusejQuerymethodssuchas$.fn.appendTo()and$.fn.insertAfter(),asawaytocompletetheconstructionoftheinternalelementsandmakethempartoftheDOMtreewithpropertiesthatreflecttheirearlierin-memoryrepresentation.
AswehavealreadyseeninChapter1,ARefresheronjQueryandtheCompositePattern,theprimarywaytousethe$()functionistoinvokeitwithaCSSselectorasastringparameterandinturnitwillretrievethematchingpageelementsandreturntheminaCompositeObject.Ontheotherhand,whenthe$()functiondetectsthatithasbeeninvokedwithastringparameterthatlookslikeapieceofHTML,itworksasaDOMelementBuilder.Thisoverloadedwayofinvokingthe$()functionbasesitsdetectionontheassumptionthattheprovidedHTMLcodestartsandendswiththeinequalitysymbols<and>:
init=jQuery.fn.init=function(selector,context){
/*11linesofcode*/
//HandleHTMLstrings
if(typeofselector==="string"){
if(selector[0]==="<"&&selector[selector.length-1]===">"
&&selector.length>=3){
//Assumethatstringsthatstartandendwith<>areHTML//and
skiptheregexcheck
match=[null,selector,null];
}/*...*/
//Matchhtmlormakesurenocontextisspecifiedfor#id
if(match&&(match[1]||!context)){
//HANDLE:$(html)->$(array)
if(match[1]){
/*4linesofcode*/
jQuery.merge(this,jQuery.parseHTML(match[1],/*...*/));
/*16linesofcode*/
returnthis;
}/*...*/
}/*...*/
}/*...*/
};
Aswecanseeintheprecedingcode,thisoverloadusesthejQuery.parseHTML()helpermethodthatultimatelyleadstoacallofthecreateDocumentFragment()method.ThecreatedDocumentFragmentisthenusedasahostoftheunderconstructiontreestructureofelements.AfterjQueryfinishesconvertingtheHTMLintoelements,theDocumentFragmentisdiscardedandonlyit’shostedelementsarereturned:
jQuery.parseHTML=function(data,context,keepScripts){
/*17linesofcode*/
//Singletag
if(parsed){
return[context.createElement(parsed[1])];
}
parsed=buildFragment([data],context,scripts);
/*5linesofcode*/
returnjQuery.merge([],parsed.childNodes);
};
ThisresultsinthecreationofanewjQueryCompositeObjectcontaininganin-memorytreestructureofelements.EventhoughtheseelementsarenotattachedtotheactualDOMtreeofthepage,wecanstilldotraversalsandmanipulationsonthemlikeanyotherjQueryCompositeObject.
NoteFormoreinformationonDocumentFragments,youcanvisit:https://developer.mozilla.org/en-US/docs/Web/API/Document/createDocumentFragment.
HowitisusedbyjQueryinternallyAnundoubtedlybigpartofjQueryisitsAJAX-relatedimplementation,whichaimstoprovideasimpleAPIforasynchronouscallsthatisalsoconfigurabletoalargedegree.UsingthejQuerySourceViewerandsearchingforjQuery.ajax,ordirectlysearchingjQuery’ssourcecodefor"ajax:",willbringustheaforementionedimplementation.Inordertomakeitsimplementationmorestraightforwardandalsoallowittobeconfigurable,jQueryinternallyusesaspecialobjectstructurethatactsasaBuilderObjectforthecreationandhandlingofeachAJAXrequest.Aswewillsee,thisisnotthemostcommonwayofusingaBuilderObject,butitisactuallyaspecialvariantwithsomemodificationsinordertofittherequirementsofthiscompleximplementation:
jqXHR={
readyState:0,
//Buildsheadershashtableifneeded
getResponseHeader:function(key){/*...*/},
//Rawstring
getAllResponseHeaders:function(){/*...*/},
//Cachestheheader
setRequestHeader:function(name,value){/*...*/},
//Overridesresponsecontent-typeheader
overrideMimeType:function(type){/*...*/},
//Status-dependentcallbacks
statusCode:function(map){/*...*/},
//Canceltherequest
abort:function(statusText){/*...*/}
};
ThemainmethodthatthejqXHRobjectexposestoconfigurethegeneratedasynchronousrequestisthesetRequestHeader()method.Theimplementationofthismethodisquitegeneric,enablingjQuerytosetallthedifferentHTTPheadersfortherequest,usingonlyonemethod.
Inordertoprovideanevengreaterdegreeofflexibilityandabstraction,jQueryinternallyusesaseparatetransportobjectasawrapperofthejqXHRobject.ThistransportobjecthandlesthepartofactuallysendingtheAJAXrequesttotheserver,workinglikeapartnerbuilderobjectthatcooperateswiththejqXHRobjectforthecreationofthefinalresult.Thisway,jQuerycanfetchScripts,XML,JSON,andJSONPresponsesfromthesameorcross-originservers,usingthesameAPIandoverallimplementation:
transport=inspectPrefiltersOrTransports(transports,s,options,jqXHR);
//Ifnotransport,weauto-abort
if(!transport){
done(-1,"NoTransport");
}else{
jqXHR.readyState=1;
/*12linesofcode*/
try{
state=1;
transport.send(requestHeaders,done);
}catch(e){/*7linesofcode*/}
}
AnotherspecialthingaboutthisimplementationoftheBuilderPatternisthatitshouldbeabletooperateinbothsynchronousandasynchronousmanner.Asaresult,thesend()methodofthetransportobjectthatactsastheresultgeneratormethodofthewrappedjqXHRobjectcan’tjustreturnaresultobject,butitisinsteadinvokedwithacallback.
Finally,aftertherequestiscomplete,jQueryusesthegetResponseHeader()methodtoretrievealltherequiredresponseheaders.Rightafterthis,theheadersareusedtoproperlyconvertthereceivedresponsethatisstoredintheresponseTextpropertyofthejqXHRobject.
HowtouseitinourapplicationsAsanexampleusecaseoftheBuilderPatterninaclient-sideapplicationthatusesjQuery,wewillcreateasimpledata-drivenmultiple-choicequiz.ThemainreasonthattheBuilderPatternisabettermatchforthiscase,ascomparedtotheFactoryPatternexamplethatwesawearlier,isthattheresultismorecomplexandhasmoredegreesofconfiguration.Eachquestionwillbegeneratedbasedonamodelobjectthatwillrepresentitsdesiredproperties.
Onceagain,therequiredHTMLisverysimple,containingjustan<h1>elementwiththeheaderofthepage,anempty<form>tag,andsomereferencestoourCSSandJavaScriptresources:
<h1>DataDrivenQuiz</h1>
<form></form>
<scripttype="text/javascript"src="jquery.js"></script>
<scripttype="text/javascript"src="datadrivenquiz.js"></script>
Besidesthecommon,simplestylesthatwehaveseeninthepreviouschapters,theCSSof
thisexampleadditionallydefines:
ul.unstyled>li{
margin:0;
padding:0;
list-style:none;
}
Fortheneedsofthisexample,wewillcreateamodulewithanewnamespacenameddataDrivenQuiz.Aswesawearlierinthischapter,wewillassumetheexistenceofanarraycontainingthemodelobjectsthatdescribeeachmultiple-choicequestionthatneedstobepresented.Eachofthesemodelobjectswillhave:
AtitlepropertythatwillholdthequestionAnoptionspropertythatwillbeanarraywiththeavailableanswerstochoosefromAnoptionalacceptsMultiplepropertytosignifywhetherweshoulduseradioorcheckboxes
ThearraywiththemodelobjectsthatdescribetheformquestionswillbeavailableatthedataDrivenQuiz.partspropertyofourmodule,whilekeepinginmindthatourimplementationcouldeasilybemodifiedtofetchthemodelswithanAJAXrequest:
dataDrivenQuiz.questions=[{
title:'WhichisthemostpreferredwaytowriteourJavaScriptcode?',
options:[
'inlinealongwithourHTML',
'flatinside*.jsfiles',
'insmallModules,oneper*.jsfile'
]
},{
title:'Whatdoesthe$()functionreturnswheninvokedwithaCSS
selector?',
options:[
'asingleelement',
'anarrayofelements',
'theHTMLoftheselectedelement',
'aCompositeObject'
]
},{
title:'WhichofthefollowingareDesignPatterns',
acceptsMultiple:true,
options:[
'GarbageCollector',
'Class',
'ObjectLiteral',
'Observer'
]
},{
title:'Howcangetaholdtothe<body>elementofapage?',
acceptsMultiple:true,
options:[
'document.body',
'document.getElementsByTagName(\'body\')[0]',
'$(\'body\')[0]',
'document.querySelector(\'body\')'
]
}];
TipDefiningthedatastructuresthatarerequiredtodescribeaproblem,beforestartingtheactualimplementation,allowsustofocusontheneedsoftheapplicationandgetanestimateofitsoverallcomplexity.
Giventheprecedingsampledata,let’snowproceedtotheimplementationofourBuilder:
functionMultipleChoiceBuilder(){
this.title='Untitled';
this.options=[];
}
dataDrivenQuiz.MultipleChoiceBuilder=MultipleChoiceBuilder;
MultipleChoiceBuilder.prototype.setTitle=function(title){
this.title=title;
returnthis;
};
MultipleChoiceBuilder.prototype.setAcceptsMultiple=
function(acceptsMultiple){
this.acceptsMultiple=acceptsMultiple;
returnthis;
};
MultipleChoiceBuilder.prototype.addOption=function(title){
this.options.push(title);
returnthis;
};
MultipleChoiceBuilder.prototype.getResult=function(){
var$header=$('<header>').text(this.title||'Untitled');
varquestionGuid='quizQuestion'+(jQuery.guid++);
var$optionsList=$('<ulclass="unstyled">');
for(vari=0;i<this.options.length;i++){
var$input=$('<input/>').attr({
'type':this.acceptsMultiple?'checkbox':'radio',
'value':i,
'name':questionGuid,
});
var$option=$('<li>');
$('<label>').append($input,$('<span>').text(this.options[i]))
.appendTo($option);
$optionsList.append($option);
}
return$('<article>').append($header,$optionsList);
};
UsingthePrototypicalObject-OrientedapproachofJavaScript,wefirstlydefinetheConstructorFunctionforourMultipleChoiceBuilderclass.WhentheConstructor
Functionisinvokedusingthenewoperator,itwillcreateanewinstanceoftheBuilderandinitializeitstitlepropertyto"Untitled"andtheoptionspropertytoanemptyarray.
Rightafterthis,wecompletethedefinitionoftheConstructorFunctionofourBuilder,weattachitasamemberofourmodule,andcontinuewiththedefinitionofitssettermethods.FollowingthePrototypicalClassparadigm,thesetTitle(),setAcceptsMultiple(),andaddOption()methodsaredefinedaspropertiesofourBuilder’sPrototypeandareusedtomodifytheinternalstateoftheunderconstructionelement.Additionally,inordertoenableustochainseveralinvocationsofthesemethods,whichresultsinamorereadableimplementation,allofthemendwiththereturnthis;statement.
WecompletetheimplementationoftheBuilderwiththegetResult()methodthathasthedutyofgatheringalltheparametersthatareappliedontheBuilderobjectinstanceandgeneratingtheresultingelementwrappedinsideajQueryCompositeObject.Initsfirstline,itcreatesaheaderofthequestion.Rightafterthis,itcreatesa<ul>elementwiththeunstyledCSSclasstoholdthepossibleanswerstothequestionandauniqueidentifierthatwillbeusedasthenameofthegenerated<input>ofthequestion.
Intheforloopthatfollows,wewill:
Createan<input/>elementforeachoptionofthequestionProperlysetitstypeasacheckboxoraradiobutton,basedonthevalueoftheacceptsMultiplepropertyUsetheforloop’siterationnumberasitsvalueSettheuniqueidentifierthatwegeneratedearlierforthequestionastheinput’snameinordertogrouptheanswersFinally,adda<label>withtheoption’stext,whichwrapsalloftheminsidean<li>,andappendittothequestion’s<ul>.
Lastly,theheaderandthelistofoptionsarewrappedinan<article>element,whichisthenreturnedasthefinalresultoftheBuilder.
Intheaboveimplementation,weusethe$.fn.text()methodtoassignthecontentofthequestion’sheaderanditsavailablechoicesinsteadofstringconcatenation,inordertoproperlyescapethe<and>charactersthatarefoundintheirdescriptions.Asanextranote,sincesomeoftheanswersalsocontainsinglequotes,weneedtoescapetheminthemodelobjectsusingabackslash(\').
Finally,inourmodule’simplementation,wedefineandimmediatelyinvoketheinitmethod:
dataDrivenQuiz.init=function(){
for(vari=0;i<dataDrivenQuiz.questions.length;i++){
varquestion=dataDrivenQuiz.questions[i];
varbuilder=newdataDrivenQuiz.MultipleChoiceBuilder();
builder.setTitle(question.title)
.setAcceptsMultiple(question.acceptsMultiple);
for(varj=0;j<question.options.length;j++){
builder.addOption(question.options[j]);
}
$('form').append(builder.getResult());
}
};
$(document).ready(dataDrivenQuiz.init);
TheexecutionoftheinitializationcodeisdelayeduntiltheDOMtreeofthepageisfullyloaded.Thentheinit()methoditeratesoverthemodelobjectsarrayandusestheBuildertocreateeachquestionandpopulatethe<form>elementofourpage.
Agoodexerciseforthereaderwouldbetoextendtheaboveimplementationinordertosupporttheclient-sideevaluationofthequiz.Firstly,thiswouldrequireyoutoextendthequestionobjectstocontaininformationaboutthevalidityofeachchoice.Then,itwouldbesuggestedthatyoucreateaBuilderthatwouldretrievetheanswersfromtheform,evaluatethem,andcreatearesultobjectwiththeuserchoicesandtheoverallsuccessonthequiz.
SummaryInthischapter,welearnedtheconceptsoftheBuilderandFactoryPatterns,twoofthemostcommonlyusedCreationalDesignPatterns.Weanalyzedtheircommongoals,theirdifferentapproachesonabstractingtheprocessofgeneratingandinitializingnewobjectsforspecificusecases,andhowtheiradoptioncanbenefitourimplementations.Finally,welearnedhowtousethemproperlyandhowtochoosethemostappropriateoneforthedifferentusecasesofanygivenimplementations.
NowthatwehavecompletedourintroductiontothemostimportantCreationalDesignPatterns,wecanmoveontothenextchapterwherewewillbeintroducedtothedevelopmentpatternsthatareusedtoprogramasynchronousandconcurrentprocedures.Inmoredetail,wewilllearnhowtoorchestratetheexecutionofasynchronousproceduresthatruneitherinorderorparalleltoeachother,byusingcallbacksandjQueryDeferredandPromisesAPIs.
Chapter7.AsynchronousControlFlowPatternsThischapterisdedicatedtodevelopmentpatternsthatareusedtoeasetheprogrammingofasynchronousandconcurrentprocedures.
Atfirst,wewillhavearefresheronhowCallbacksareusedinJavaScriptprogrammingandhowtheyareanintegralpartofwebdevelopment.Wewillthenproceedandidentifytheirbenefitsandlimitationswhenusedinlargeandcompleximplementations.
Rightafterthis,wewillbeintroducedtotheconceptofPromises.WewilllearnhowjQuery’sDeferredandPromiseAPIsworkandhowtheydifferfromES6Promises.WewillseewhereandhowtheyareusedinternallybyjQuerytosimplifyitsimplementationandleadtomorereadablecode.Wewillanalyzetheirbenefits,classifythebestmatchingusecases,andcomparethemwiththeclassicCallbackPattern.
Bytheendofthischapter,wewillbeabletousejQueryDeferredandPromisestoefficientlyorchestratetheexecutionofasynchronousproceduresthatruneitherinorderorparalleltoeachother.
Inthischapter,wewill:
HavearefresheronhowCallbacksareusedinJavaScriptprogrammingGetintroducedtotheconceptofPromisesLearnhowtousejQuery’sDeferredandPromiseAPIsComparejQueryPromiseswithES6PromisesLearnhowtoorchestrateasynchronoustasksusingPromises.
ProgrammingwithcallbacksACallbackcanbedefinedasafunctionthatispassedasaninvocationargumenttoanotherfunctionormethod(whichisreferredtoasaHigher-OrderFunction)andisexpectedtobeexecutedatsomelaterpointoftime.Inthisway,thepieceofcodethatwashandedourCallbackwilleventuallyinvokeit,propagatingtheresultsofanoperationoreventbacktothecontextthattheCallbackwasdefined.
Callbackscanbecharacterizedassynchronousorasynchronous,basedonthewaythattheinvokedmethodoperates.ACallbackischaracterizedassynchronouswhenitisexecutedbyablockingmethod.Ontheotherhand,JavaScriptdevelopersaremorefamiliarwithasynchronouscallbacks,alsocalleddeferredcallbacks,whicharesettobeexecutedafteranasynchronousprocedurefinishesorwhenaspecificeventoccurs(pageload,click,AJAXresponsearrival,andsoon).
CallbacksarewidelyusedinJavaScriptapplicationssincetheyareanintegralpartofmanycoreJavaScriptAPIssuchasAJAX.Moreover,JavaScriptimplementationsofthispatternarealmostwordforwordasdescribedbytheabovesimpledefinition.ThisisaresultofthewaythatJavaScripttreatsfunctionsasobjectsandallowsustostoreandpassmethodreferencesassimplevariables.
UsingsimplecallbacksinJavaScriptPerhapsoneofthesimplestexamplesofasynchronouscallbacksinJavaScriptisthesetTimeout()function.Thefollowingcodedemonstratesasimpleuseofit,whereweinvokesetTimeout()withthedoLater()functionasacallbackparameterand,after1000millisecondsofwaiting,thedoLater()callbackisinvoked:
varalertMessage='Onesecondpassed!';
functiondoLater(){
alert(alertMessage);
}
setTimeout(doLater,1000);
Asseeninthesimpleprecedingexample,thecallbackisexecutedinthecontextthatitwasdefined.Thecallbackstillhasaccesstothevariablesofthecontextthatitwasdefinedbycreatingaclosure.Eventhoughtheprecedingexampleusesanamedfunctiondefinedearlier,thesameappliesforanonymouscallbacks:
varalertMessage='Onesecondpassed!';
setTimeout(function(){
alert(alertMessage);
},1000);
Inmanycases,usinganonymouscallbacksisamoreconvenientwayofprogramming,sinceitresultsinshortercodeandalsoreducesthereadabilitynoise,whichisaresultofdefiningseveraldifferentnamedfunctionsthatareusedonlyonce.
SettingcallbacksasobjectpropertiesAsmallvariationoftheabovedefinitionalsoexists,wherethecallbackfunctionisassignedtoapropertyofanobjectinsteadofbeingpassedasanargumentofamethodinvocation.Thisiscommonlyusedincaseswherethereareseveraldifferentactionsthatneedtotakeplaceduringorafteramethodinvocationiscompleted:
varc=newCountdown();
c.onProgress=function(progressStatus){/*...*/};
c.onDone=function(result){/*...*/};
c.onError=function(error){/*...*/};
c.start();
Anotherusecaseoftheabovevariantistoaddhandlersonobjectsthathavealreadybeeninstantiatedandinitialized.Agoodexampleofthiscaseisthewaywesetuparesulthandlerforsimple(non-jQuery)AJAXcalls:
varr=newXMLHttpRequest();
r.open('GET','data.json',true);
r.onreadystatechange=function(){
if(r.readyState!=4||r.status!=200){
return;
}
alert(r.responseText);
};
r.send();
Intheprecedingcode,wesetananonymousfunctionontheonreadystatechangepropertyoftheXMLHttpRequestobject.Thisfunctionactsasacallbackandisinvokedeverytimethereisastatechangeontheongoingrequest.Insideourcallback,wecheckwhethertherequesthascompletedwithasuccessfulHTTPstatuscodeanddisplayanalertwiththeresponsebody.Likeinthisexample,whereweinitiatetheAJAXcallbyinvokingthesend()methodwithoutpassinganyarguments,itiscommonforAPIsthatusethisvarianttoleadtominimalwaysofinvokingtheirmethods.
UsingcallbacksinjQueryapplicationsPerhapsthemostcommonwayinwhichcallbacksareusedinjQueryapplicationsisforeventhandling.Thisislogicalsincethefirstthingthateveryinteractiveapplicationshoulddoishandleandrespondtouseractions.Aswesawinearlierchapters,oneofthemostconvenientwaystoattacheventhandlerstoelementsisbyusingjQuery’s$.fn.on()method.
AnothercommonplacewherecallbacksareusedinjQueryisforAJAXrequests,wherethe$.ajax()methodhasthecentralrole.Moreover,thejQuerylibraryalsoprovidesseveralotherconvenientmethodstomakeAJAXrequeststhatarefocusedonthemostcommonusecases.Sinceallthesemethodsareexecutedasynchronously,theyalsoacceptacallbackasaparameter,asawaytomaketheretrieveddataavailablebacktothecontextthatinitiatedtheAJAXrequest.Oneoftheseconvenientmethodsis$.getJSON(),whichisawrapperaround$.ajax(),andisusedasabettermatchingAPItoexecuteAJAXrequeststhatintendtoretrieveJSONresponses.
OtherwidelyusedjQueryAPIsacceptingcallbacksareasfollows:
Theeffects-relatedjQuerymethodssuchas$.animate()The$(document).ready()method
Let’snowcontinuebydemonstratingacodeexamplewherealltheabovemethodsareused.
$(document).ready(function(){
$('#fetchButton').on('click',function(){
$.getJSON('AjaxContent.json',function(json){
console.log('doneloadingnewcontent');
$('#newContent').css({'display':'none'})
.text(json.data)
.slideDown(function(){
console.log('donedisplayingnewcontent');
});
});
});
});
TheprecedingcodefirstlydelaysitsexecutionuntiltheDOMtreeofthepagehasbeenfullyloadedandthenaddsanObserverforclicksonthe<button>withIDfetchButtonbyusingthejQuery’s$.fn.on()method.Whenevertheclickeventisfired,theprovidedcallbackwillbeinvokedandinitiateanAJAXcalltofetchtheAjaxContent.jsonfile.Fortheneedsofthisexample,weareusingasimpleJSONfile,likethefollowing:
{"data":"I'mthetextcontentfetchedbyanAJAXcall!"}
WhentheresponseisreceivedandtheJSONisparsedsuccessfully,thecallbackisinvokedwiththeparsedobjectasaparameter.Finally,thecallbackitselflocatesthepageelementwiththeIDnewContentinthepage,hidesit,andthensetsthedatafieldoftheretrievedJSONasitstextcontent.Rightafterthis,weusethejQuery$.fn.slideDown()
methodthatmakesthenewlysetpagecontentappear,byprogressivelyincreasingitsheight.Finally,aftertheanimationiscomplete,wewritealogmessagetothebrowserconsole.
NoteFurtherdocumentationregardingjQuery’s$.ajax(),$.getJSON(),and$.fn.slideDown()methodscanbefoundathttp://api.jquery.com/jQuery.ajax/,http://api.jquery.com/jQuery.getJSON/,andhttp://api.jquery.com/slideDown/.
Keepinmindthatthe$.getJSON()methodmightnotworkinsomebrowserswhenthepageisloadedthroughthefilesystem,butworksasintendedwhenservedusinganywebserversuchasApache,IIS,ornginx.
WritingmethodsthatacceptcallbacksWhenwritingafunctionthatutilizesoneormoreasynchronousAPIs,thatalsodictatesthattheresultingfunctionwillbeasynchronousbydefinition.Inthatcase,itisobviousthatsimplyreturningaresultvalueisnotanoption,sincetheresultwillprobablybeavailableafterthefunctioninvocationhasalreadyfinished.
Theeasiestsolutionforasynchronousimplementationsistouseacallbackasaparameterofyourfunction,which,aswediscussedearlier,ishassle-freeinJavaScript.Asanexample,wewillcreateanasynchronousfunctionthatgeneratesarandomnumberofaspecifiedrange:
functiongetRandomNumberAsync(max,callbackFn){
varrunFor=1000+Math.random()*1000;
setTimeout(function(){
varresult=Math.random()*max;
callbackFn(result);
},runFor);
}
ThegetRandomNumberAsync()functionacceptsitsmaxargumentasthenumericupperboundforthegeneratedrandomnumberandalsoacallbackfunctionthatitwillinvokewiththegeneratedresult.ItusessetTimeout()toemulateanasynchronouscalculationthatrangesfrom1000to2000milliseconds.Forthegenerationoftheresult,itusestheMath.random()method,multiplyingitwiththemaximumallowedvalue,andfinallyinvokestheprovidedcallbackwithit.Asimplewaytoinvokethisfunctionwilllookasfollows:
getRandomNumberAsync(10,function(number){
console.log(number);//returnsanumberbetween0and10
});
EventhoughtheaboveexampleusessetTimeout()toemulateasynchronousprocessing,theimplementationprinciplesremainthesameregardlessoftheasynchronousAPI(s)thatisused.Forexample,wecanrewritetheabovefunctiontoretrieveitsresultthroughanAJAXcall:
functiongetRandomNumberWS(max,callbackFn,errorFn){
$.ajax({
url:'https://qrng.anu.edu.au/API/jsonI.php?length=1&type=uint16',
dataType:'json',
success:function(json){
varresult=json.data[0]/65535*max;
callbackFn(result);
},
error:errorFn
});
}
Theprecedingimplementationusesthe$.ajax()methodthatisinvokedwithanobjectparameter,enclosingalltheoptionsoftherequest.ExceptfortheURLfortherequest,theobjectalsodefinestheexpecteddataTypeoftheresultandthesuccessanderror
callbacks,whicharewiredwiththerespectiveparametersofourfunction.
Perhapstheonlyextraconcernthattheprecedingcodehastoresolveishowtohandleerrorsinsidethesuccesscallbacksothatthecallerofthefunctioncanbenotifiedincasesomethinggoeswrongduringthecreationoftheresult.Forexample,theAJAXrequestmightreturnanemptyobject.Addingproperhandlingforsuchcasesisleftasanexerciseforthereader,afterreadingtherestofthischapter.
NoteTheAustralianNationalUniversity(ANU)providesfree,trulyrandom,numberstothepublic,throughtheirRESTWebService.Formoreinformation,youcanvisithttp://qrng.anu.edu.au/API/api-demo.php.
OrchestratingcallbacksWewillnowcontinuebyanalyzingsomepatternsthatarecommonlyusedtocontroltheexecutionflowwhendealingwithasynchronousmethodsthatacceptcallbacks.
QueuinginorderexecutionAsourfirstexample,wewillcreateafunctionthatdemonstrateshowwecanqueuetheexecutionofseveralasynchronoustasks:
functiongetThreeRandomNumbers(callbackFn,errorFn){
varresults=[];
getRandomNumberAsync(10,function(number){
results.push(number);
getRandomNumberAsync(10,function(number){
results.push(number);
getRandomNumberWS(10,function(number){
results.push(number);
callbackFn(results);
},function(error){
errorFn(error);
});
});
});
}
Intheprecedingimplementation,ourfunctioncreatesaqueueofthreerandomnumbergenerations.ThefirsttworandomnumbersaregeneratedfromoursamplesetTimeout()implementationandthethirdisretrievedfromtheaforementionedwebservicethoughanAJAXcall.Inthisexample,allthenumbersaregatheredintheresultarray,whichispassedasaninvocationparametertothecallbackFnafteralltheasynchronoustaskshavecompleted.
TheprecedingimplementationisquitestraightforwardandjustappliesthesimpleprinciplesoftheCallbackPatternrepeatedly.Foreveryextraorqueuedasynchronoustask,wejustneedtonestitsinvocationinsidethecallbackofthetaskthatitdependson.Keepinmindthat,indifferentusecases,wemightonlycaretoreturntheresultofthefinaltaskandhavetheresultsoftheintermediatestepsbepropagatedasargumentsforeachsubsequentasynchronouscall.
AvoidingtheCallbackHellanti-pattern
Eventhoughwritingcodeasshownintheaboveexampleiseasy,whenappliedtolargeandcompleximplementations,itcanleadtobadreadability.Thetriangularshapethatiscreatedbythewhite-spacesinfrontofourcodeandthestackingofseveral});nearitsend,arethetwosignsthatourcodemightleadtoananti-patternknownasCallbackHell.
NoteFormoreinformation,youcanvisithttp://callbackhell.com/.
Awaytoavoidthisanti-patternistounfoldthenestedcallbacks,bycreatingseparatenamedfunctionsatthesamelevelwiththeasynchronoustaskthattheyareused.Afterapplyingthissimpletiptotheaboveexample,theresultingcodelooksalotcleaner:
functiongetThreeRandomNumbers(callbackFn,errorFn){
varresults=[];
getRandomNumberAsync(10,function(number){//task1
results.push(number);
task2();
});
functiontask2(){
getRandomNumberAsync(10,function(number){
results.push(number);
task3();
});
}
functiontask3(){
getRandomNumberWS(10,function(number){
results.push(number);
callbackFn(results);
},errorFn);
}
}
Asyoucansee,theresultingcodesurelydoesnotremindusofthecharacteristicsoftheCallbackHellanti-pattern.Ontheotherhand,itnowneedsmorelinesofcodeforitsimplementation,mostlyusedfortheadditionalfunctiondeclarationsfunctiontaskX(){}thatarenowrequired.
TipAmiddlegroundsolutionbetweentheabovetwoapproachesistoorganizetherelatedpartsofsuchasynchronousexecutionqueuesinsmallandmanageablefunctions.
RunningconcurrentlyEventhoughJavaScriptinwebbrowsersissingle-threaded,makingindependentasynchronoustasksrunconcurrentlycanmakeourapplicationsworkfaster.Asanexample,wewillrewritetheprecedingimplementationtofetchallthreerandomnumbersinparallel,whichcanmaketheresulttoberetrievedalotfasterthanbefore:
functiongetRandomNumbersConcurent(callbackFn,errorFn){
varresults=[];
varresultCount=0;
varn=3;
functiongatherResult(resultPos){
returnfunction(result){
results[resultPos]=result;
resultCount++;
if(resultCount===n){
callbackFn(results);
}
};
}
getRandomNumberAsync(10,gatherResult(0));
getRandomNumberAsync(10,gatherResult(1));
getRandomNumberWS(10,gatherResult(2),errorFn);
}
Intheprecedingcode,wedefinedthegatherResult()helperfunction,whichreturnsananonymousfunctionthatisusedasacallbackforourrandomnumbergenerators.ThereturnedcallbackfunctionusestheresultPosparameterastheindexofthearraywhereitwillstorethegeneratedorretrievedrandomnumber.Additionally,ittrackshowmanytimesithasbeeninvoked,asawaytoknowwhetherallthreeconcurrenttaskshaveended.Finally,rightafterthethirdandfinalinvocationofthecallback,thecallbackFnfunctionisinvokedwiththeresultsarrayasaparameter.
Anothergreatapplicationofthistechnique,otherthanAJAXcalls,istoaccessdatastoredinIndexedDB.Retrievingmanyvaluesfromthedatabaseconcurrentlycanleadtoperformancegains,sincethedataretrievalscanexecuteinparallelwithoutblockingeachother.
NoteFormoreinformationonIndexedDB,youcanvisithttps://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB.
IntroducingtheconceptofPromisesPromises,alsoknownasFutures,aredescribedbyComputerScienceasspecializedobjectsthatareusedforsynchronizationofasynchronous,concurrent,orparallelprocedures.Theyarealsousedasproxiestopropagatetheresultofataskwhenitsgenerationcompletes.Thisway,aPromiseobjectislikeacontractwhereanoperationwilleventuallycompleteitsexecution,andanyonehavingareferencetothiscontractcandeclaretheirinteresttobenotifiedabouttheresult.
SincetheywereintroducedtoJavaScriptdevelopers,aspartofseverallibraries,theyrevolutionizedthewayweuseasynchronousfunctionsandcomposetheminimplementationwithcomplexsynchronizationschemes.Thisway,webdeveloperscancreatemoreflexible,scalable,andreadableimplementations,makingmethodinvocationswithcallbackslooklikeaprimitivepatternandeffectivelyeliminatingtheCallbackHellsituations.
OneofthekeyconceptsofPromisesisthatasynchronousmethodsreturnanobjectthatrepresentstheireventualresult.EveryPromisehasaninternalstatethatinitiallystartsasPending.Thisinternalstatecanchangeonlyonce,fromPendingtoeitherResolvedorRejected,byusingoneoftheresolve()orreject()methodsthateveryimplementationprovides.ThesemethodscanbeinvokedonlytochangethestateofaPendingPromise;inmostcases,theyareintendedtobeusedonlybytheoriginalcreatorofthePromiseobjectandnotbeavailabletoitsconsumers.Theresolve()methodcanbeinvokedwiththe
resultoftheoperationasasingleparameter,whilethereject()methodisusuallyinvokedwiththeErrorthatcausedthePromiseobjecttogetRejected.
AnotherkeyconceptofPromisesistheexistenceofathen()method,givingthemthecharacterizationofthe“thenable”,asageneraltermtodescribepromisesamongallthedifferentimplementations.EveryPromiseobjectexposesathen()methodthatisusedbyacallerinordertoprovidethefunction(s)thatwillbeinvokedwhenthePromiseissettled(ResolvedorRejected).Thethen()methodcanbeinvokedwithtwofunctionsasparameters,wherethefirstisinvokedincasethePromisegetsResolved,whilethesecondisinvokedwhenitisRejected.ThefirstargumentiscommonlyreferredtoastheonFulfilled()callback,whilethesecondisreferredtoastheonRejected().
EveryPromisepreservestwointernallistcontainingalltheonFulfilled()andonRejected()callbackfunctionsthatarepassedasargumentstothethen()method.Thethen()methodcanbeinvokedseveraltimesforeachPromise,addingnewentriestotheappropriateinternallist,asfarastherespectiveparameterisactuallyafunction.WhenaPromiseeventuallygetsResolvedorRejected,ititeratesovertheappropriatelistofcallbacksandinvokestheminorder.Moreover,fromthepointthataPromisegetssettledandafter,everyfurtherusageofthethen()methodhas,asaresult,theimmediateinvocationoftheappropriateprovidedcallback.
NoteBasedonitscharacteristics,aPromisecanbelikenedtoaBrokerfromthe
Publish/SubscribePatterntosomedegree.TheirkeydifferencesincludethefactsthatitcanonlybeusedforasinglePublishandthattheSubscribersgetnotifiedoftheresulteveniftheyexpressedtheirinterestafterthePublishtookplace.
UsingPromisesAswesaidearlier,theconceptofPromisesrevolutionizedprogrammingofasynchronoustasksinJavaScriptand,foralongtime,theywerethenewbigthingthateveryonewasenthusiasticabout.Atthattime,manyspecializedlibrariesappearedwhereeachoneprovidedanimplementationofPromiseswithslightdifferencestoeachother.Moreover,PromiseimplementationsbecameavailableaspartofutilitylibrariessuchasjQueryandwebframeworkssuchasAngularJSandEmberJS.Atthattime,the“CommonJSPromises/A”specificationmadeitsappearanceasareferencepointandwasthefirstattempttodefinehowPromisesshouldactuallyworkacrossallimplementations.
NoteFormoreinformationonthe“CommonJSPromises/A”specification,youcanvisithttp://wiki.commonjs.org/wiki/Promises/A.
UsingthejQueryPromiseAPIAPromise-basedAPIfirstappearedinthejQuerylibraryinv1.5,basedonthe“CommonJSPromises/A”design.ThisimplementationintroducedtheadditionalconceptoftheDeferredobject,whichworkslikeaPromiseFactory.TheDeferredobjectsexposeasupersetofthemethodsthatPromisesprovide,wheretheadditionalmethodscanbeusedtodomanipulationstothestateofitsinternalPromise.Additionally,theDeferredobjectexposesapromise()methodandreturnstheactualPromiseobject,whichdoesnotexposeanywaytomanipulateitsinternalstateandjustexposesobservationmethodssuchasthen().
Inotherwords:
OnlycodethathasareferencetoaDeferredobjectcanactuallychangetheinternalstateofitsPromise,byeitherresolvingorrejectingit.AnypieceofcodethathasareferencetoaPromiseobjectcan’tchangeitsstatebutjustobserveforitsstatetochange.
NoteFormoreinformationonjQuery’sDeferredobject,youcanvisithttp://api.jquery.com/jQuery.Deferred/.
AsasimpleexampleofjQuery’sDeferredobject,let’sseehowwecanrewritethegetRandomNumberAsync()functionthatwesawearlierinthischapter,tousePromisesinsteadofCallbacks:
functiongetRandomNumberAsync(max){
vard=$.Deferred();
varrunFor=1000+Math.random()*1000;
setTimeout(function(){
varresult=Math.random()*max;
d.resolve(result);
},runFor);
returnd.promise();
}
getRandomNumberAsync(10).then(function(number){
console.log(number);//returnsanumberbetween0and10
});
OurtargetistomakeanasynchronousfunctionthatreturnsaPromisethatiseventuallyresolvedtotheresultingrandomnumber.Atfirst,anewDeferredobjectiscreatedandthentherespectivePromiseobjectisreturned,byusingthepromise()methodoftheDeferred.Whentheasynchronousgenerationoftheresultiscomplete,ourmethodusestheresolve()methodoftheDeferredobjecttosetthefinalstateofthePromisethatwasreturnedearlier.
Thecallerofourfunctionusesthethen()methodofthereturnedPromise,toattachacallbackthatwillbeinvokedwiththeresultasaparameterassoonasthePromisegetsResolved.Moreover,asecondcallbackcanalsobepassedinordertogetnotifiedincasethePromisegetsRejected.Animportantthingtonoticeisthat,byfollowingtheabovepatternwherefunctionsalwaysreturnPromisesandnevertheactualDeferredobjects,wecanbesurethatonlythecreatoroftheDeferredobjectcanchangethestateofthePromise.
UsingPromises/A+Aftersometimeofhands-onexperimentationwithCommonJSPromises/A,thecommunityidentifiedsomeoftheirlimitationsandalsorecommendedsomewaystoimprovethem.TheresultwasthecreationofthePromises/A+specification,asawaytoimprovetheexistingspecificationandalsoasasecondattempttounifythevariousavailableimplementations.ThemostimportantpartsofthenewspecificationfocusedonhowchainingPromisesshouldwork,makingthemevenmoreusefulandconvenienttoworkwith.
NoteFormoreinformationonthePromises/A+specification,youcanvisithttps://promisesaplus.com/.
Finally,thePromises/A+specificationwaspublishedaspartofthe6thversionofJavaScript,commonlyreferredasES6,thatwasreleasedasastandardonJune,2015.Asaresult,Promises/A+startedtobeimplementednativelyinbrowsers,removingtheneedtousecustomthird-partylibrariesandpushingmostoftheexistinglibrariestoupgradetheirsemantics.Asofwritingofthisbook,nativePromises/A+compliantimplementationshavebeenavailableinmostmodernbrowsers,exceptforIE11,makingthemavailableout-of-the-boxtomorethan65%ofwebusers.
NoteFormoreinformationontheadoptionofA+Promisesinbrowsers,youcanvisithttp://caniuse.com/#feat=promises.
ArewriteofthegetRandomNumberAsync()functionusingthenownativelyimplementedES6A+Promiseswilllookasfollows:
functiongetRandomNumberAsync(max){
returnnewPromise(function(resolve,reject){
varrunFor=1000+Math.random()*1000;
setTimeout(function(){
varresult=Math.random()*max;
resolve(result);
},runFor);
});
}
getRandomNumberAsync(10).then(function(number){
console.log(number);//returnsanumberbetween0and10
});
Asyoucansee,ES6/A+PromisesarecreatedbyusingthePromiseconstructorfunctionwiththenewkeyword.Theconstructorisinvokedwithafunctionasaparameter,whichmakesaclosurethathasaccesstoboththevariablesofthecontextthatthePromiseiscreated,butalsogetsaccesstotheresolve()andreject()functionsasparameters,whichistheonlywaytochangethestateofthenewlycreatedPromise.AfterthesetTimeout()functionfiresitscallback,theresolve()functionisinvokedwiththegeneratedrandomnumberasaparameter,changingthestateofthePromiseobjecttoFulfilled.Finally,thecallerofourfunctionusesthethen()methodofthereturnedPromiseinexactlythesamewayaswesawintheearlierimplementationthatwasusingjQuery.
ComparingjQueryandA+PromisesWewillnowhaveanin-depthstep-by-stepanalysisofthecoreconceptsofthejQueryandA+PromiseAPIs,byalsodoingaside-by-sidecodecomparisonofthetwo.Thiscanbeagreatassettohave,sinceyouwillalsobeabletouseitasareferencewhiletheimplementationsofPromisesaregraduallyadaptingtotheES6A+specification.
Theneedtounderstandfromthebeginninghowthetwovariantsdifferseemsevengreater,sincethejQueryteamhasalreadyannouncedthatVersion3.0ofthelibrarywillhavePromises/A+compliantimplementation.Specifically,asofwritingthisbook,thefirstbetaversionisalreadyout,makingthetimethatthemigrationwillhappentoappearevencloser.
NoteFormoreinformationonjQueryv3.0A+Promisesimplementation,youcanvisithttp://blog.jquery.com/2016/01/14/jquery-3-0-beta-released/.
OneofthemostobviousdifferencesbetweenthetwoimplementationsisthewaythatnewPromisesarecreated.Aswesaw,jQueryusesthe$.Deferred()functionlikeafactoryofamorecomplexobjectthatprovidesdirectaccesstothestateofthePromiseandeventuallyextractstheactualPromiseusingaseparatemethod.Ontheotherhand,A+Promisesusethenewkeywordandafunctionasaparameter,whichwillbeinvokedbytheruntimewiththeresolve()andreject()functionsasparameters:
vard=$.Deferred();
setTimeout(function(){
d.resolve(7);
},2000);
varp=d.promise();//jQueryPromise
varp=newPromise(function(resolve,reject){//Promises/A+
setTimeout(function(){
resolve(7);
},2000);
});
Moreover,jQueryalsoprovidesanotherwaytocreatePromisesthatlookmorelikethewaythatA+Promiseswork.Inthiscase,$.Deferred()canbeinvokedwithafunctionasanargumentthatreceivestheDeferredobjectasaparameter:
vard=$.Deferred(function(deferred){
setTimeout(function(){
deferred.resolve(7);
},2000);
});
varp=d.promise();
Aswediscussedearlier,thesecondpossibleoutcomeofaPromiseistobeRejected,afeaturethatnicelypairswiththeclassicalexceptionsofJavaScriptinsynchronousprogramming.RejectingaPromiseiscommonlyusedforcaseswhereanerroroccursduringtheprocessingoftheresult,orinsituationswheretheresultisnotvalid.WhileES6Promisesprovideareject()functionasanargumenttothefunctionpassedtoitsconstructor,injQuery’simplementationareject()methodissimplyexposedontheDeferredobjectitself.
varp=$.Deferred(function(deferred){
deferred.reject(newError('Somethinghappened!'));
}).promise();
varp=newPromise(function(resolve,reject){
reject(newError('Somethinghappened!'));
});
Inboththeimplementations,theresultofaPromisecanberetrievedusingthethen()method,whichcanbeinvokedwithtwofunctionsasarguments,onetohandlethecasethatthePromisegetsFulfilledandoneforthecasewhereitisRejected:
p.then(function(result){//worksthesameinjQuery&ES6
console.log(result);
},function(error){
console.error('Anerroroccurred:',error);
});
BothimplementationsalsoprovideconvenientmethodstohandlethecasewherethePromisegetsRejected,butwithdifferentmethodnames.Insteadofusingp.then(null,fn),ES6Promisesprovidethecatch()methodthatnicelypairswiththetry…catchJavaScriptexpression,whilejQuery’simplementationprovides,forthesamepurpose,thefail()method:
p.fail(function(error){//jQuery
console.error(error);
});
p.catch(function(error){//ES6
console.error(error);
});
Moreover,asajQueryexclusivefeature,jQueryPromisesalsoexposeadone()andanalways()method.Thecallbacksprovidedtodone()areinvokedwhenthePromisegetsFulfilledandisequivalenttousingthethen()methodwithasingleparameter,whilethecallbacksofthealways()methodareinvokedwhenthepromisegetssettledinbothpossibleoutcomes.
NoteFormoreinformationondone()andalways(),youcanvisithttp://api.jquery.com/deferred.doneandhttp://api.jquery.com/deferred.always.
Finally,bothimplementationsprovideaneasywaytodirectlycreatePromisesthatarealreadyResolvedorRejected.Thiscanbeusefulasastartingvaluetoimplementcomplexsynchronizationschemesorasaneasywaytomakesynchronousfunctionstooperatelikeasynchronousones:
varpResolved=$.Deferred().resolve(7).promise();//jQuery
varpRejected=$.Deferred().reject(newError('Something
happened!')).promise();
varpResolved=Promise.resolve(7);//ES6
varpRejected=Promise.reject(newError('Somethinghappened!'));
AdvancedconceptsAnotherkeyconceptofPromisesthatmakesthemuniqueandgreatlyincreasestheirusefulnessistheabilitytoeasilycreatecompositionsofseveralPromisesthatinturnarePromisesthemselves.Compositionisavailableintwoforms,serialcompositionthatchainsPromisestogetherandparallelcompositionthatusesspecialmethodstojointheresolutionofconcurrentPromisesintoanewone.Aswesawearlierinthischapter,implementingsuchsynchronizationschemescanbehardtoimplementwiththetraditionalcallbackapproach.Promises,ontheotherhand,trytosolvethisprobleminamoreconvenientandreadableway.
ChainingPromisesEveryinvocationofthethen()methodreturnsanewPromise,whosebothfinalstatusandresultdependsonthePromisethatthethen()methodwascalledon,butisalsosubjecttothevaluereturnedbytheattachedcallbacks.Thisallowsustochaincallsofthethen()method,enablingustocomposePromisesbyseriallyjoiningthem.Thisway,wecaneasilyorchestratebothasynchronousandsynchronouscode,whereeachchainingsteppropagatesitsresulttothenextoneandallowsustoconstructthefinalresultinareadableanddeclarativeway.
Let’snowproceedtoanalyzingallthedifferentwaysthatchainingofcallstothethen()methodworks.SincewewillbefocusingontheconceptsofPromisecompositionbychaining,whichworksthesameasjQueryandES6Promises,let’ssupposethatthereisapvariablethatisholdingaPromiseobjectcreatedbyeitherofthefollowinglinesofcode:
varp=$.Deferred().resolve(7).promise();
//or
varp=Promise.resolve(7);
Thesimplestusecasethatdemonstratesthepowerofchainingiswhentheinvokedcallbackreturnsa(non-promise)value.ThenewlycreatedPromiseusesthereturnedvalueasitsresult,whilepreservingthesamestateasthePromisethatthethen()methodwascalledon:
p.then(function(x){//worksthesameinjQuery&ES6
console.log(x);//logs7
returnx*3;
}).then(function(x){
console.log(x);//logs21
});
Aspecialcasetohaveinmindisthatfunctionsthatdonotreturnanythingasaresultarehandledlikereturningundefined.ThisessentiallyremovestheresultvaluefromthenewlyreturnedPromise,whichnowonlypreservestheparentsettlementstatus:
p.then(function(x){//worksthesameinjQuery&ES6
console.log(x);//logs7
}).then(function(x){
console.log(x);//logsundefined
});
InthecasewheretheinvokedcallbackreturnsanotherPromise,itsstateandresultareusedforthePromisereturnedbythethen()method:
p.then(function(x){//forjQueryPromises
console.log(x);//logs7
vard2=$.Deferred();
setTimeout(function(){
d2.resolve(x*3);
},2000);
returnd2.promise();
}).then(function(x){
console.log(x);//logs21
});
p.then(function(x){//fortheA+Promises
console.log(x);//logs7
returnnewPromise(function(resolve){
setTimeout(function(){
resolve(x*3);
},2000);
});
}).then(function(x){
console.log(x);//logs21
});
TheprecedingcodesamplesdemonstratetheimplementationsforboththejQueryandA+Promises,andbothhaveequivalentresults.Inbothcases,7isloggedintotheconsolefromthefirstthen()methodinvocationandanewPromiseisthenreturnedthatwillbeResolvedatalatertimeusingsetTimeout().After2000milliseconds,thatsetTimeout()willfireitscallback,thereturnedPromisewillbeResolvedwith21asavalueand,atthatpoint,21willalsobeloggedintotheconsole.
OneextrathingtonoteisthecasewheretheoriginalPromisegetssettledandthereisnoappropriatecallbackprovidedtothechainedthen()method.Inthiscase,thenewlycreatedPromisesettlestothesamestateandresult,asthePromisewherethethen()methodwascalledon:
p.then(null,function(error){//worksthesameinjQuery&ES6
console.error('Anerrorhappened!');//doesnotrun,sincethepromise
isresolved
}).then(function(x){
console.log(x);//logs7
});
Intheprecedingexample,thecallbackwiththeconsole.errorstatementthatispassedasthesecondargumentofthethen()method,doesnotgetinvokedsincethePromiseisresolvedwith7asitsvalue.Asaresult,thecallbackofthechaineventuallyreceivesanewPromise,whichisalsoresolvedwith7asitsvalueandlogsthatintheconsole.SomethingtohaveinmindinordertodeeplyunderstandhowchainingofPromisesworks,isthatp!=p.then()inallcases.
HandlingthrownerrorsThefinalconceptofchainingdefinesthecasewhereexceptionsarethrownduringtheinvocationofathen()callback.ThePromise/A+specificationdefinedthatthenewlycreatedPromiseisRejectedandthatitsresultistheErrorthatwasthrown.Moreover,theRejectionwillbubblethroughtheentirechainofPromises,enablingustobenotifiedaboutanyerrorinthechainonlydefiningtheerrorhandlingonce,neartotheendofthechain.
Unfortunately,thisisnotconsistentintheimplementationofthelateststableversionofjQuery,whichasofthewritingofthisbookisv2.2.0:
$.Deferred().resolve().promise().then(function(){
thrownewError('Somethinghappened!');
//theexecutionstopshere
}).then(null,function(x){
console.log(x);//nothinggetsprinted
});
$.Deferred().resolve().promise().then(function(){
try{//thisisaworkaround
thrownewError('Somethinghappened!');
}catch(e){
return$.Deferred().reject(e).promise();
}
}).then(function(){
console.log('Success');//notprinted
}).then(null,function(x){//almostequivalentto.fail()
console.log(x);//logs'Somethinghappened!''
});
Promise.resolve().then(function(){
thrownewError('Somethinghappened!');
}).then(function(){
console.log('Success');//notprinted
}).then(null,function(x){//equivalentto.catch()
console.log(x);//logs'Somethinghappened!''
});
Inthefirstcase,theexceptionthatisthrownstopstheexecutionofthePromisechain.Theonlywayarounditisprobablyexplicitlyaddingatry…catchstatementinsidethecallbackthatispassedtothethen()method,asshowninthesecondcasethatisdemonstrated.
JoiningPromisesTheotherwayoforchestratingPromisesthatrunconcurrentlyisbycomposingthemtogether.Asanexample,let’ssupposetheexistenceoftwoPromises,p1andp2,thatgetresolvedwith7and11astheirvalues,after2000and3000milliseconds,respectively.SincethesetwoPromisesareexecutedconcurrently,thecomposedPromisewillonlyneed3000millisecondstogetResolved,asitisthegreaterofthetwodurations:
//jQuery
$.when(p1,p2).then(function(result1,result2){
console.log('p1',result1);//logs7
console.log('p2',result2);//logs11
//thiscanbeusedtomakeourcodelooklikeA+
varresults=arguments;
});
//A+
Promise.all([p1,p2]).then(function(results){
console.log('p1',results[0]);//logs7
console.log('p2',results[1]);//logs11
});
BothPromiseAPIsprovideaspecializedfunctionthatallowsustoeasilycreatePromisecompositionsandalsoretrievetheindividualresultsofthecomposition.AcomposedPromisegetsResolvedwhenallitspartsgetResolved,whileitgetsRejectedwhenanyoneofitspartsgetsRejected.Unfortunately,thetwoPromiseAPIsdiffer,notonlybythenameofthefunctions,butalsobythewaytheyareinvokedandthewaytheyprovidetheirresults.
ThejQueryimplementationprovidesthe$.when()methodthatcanbeinvokedwithanynumberofargumentsthatwewanttobecomposed.Byusingthethen()methodonacomposedjQueryPromise,wecangetnotifiedwhenthecompositiongetssettledasawholeandalsoaccesseachindividualresultasargumentsofourcallback.
Ontheotherhand,theA+PromisesspecificationprovidesusthePromise.all()methodthatisinvokedwithanarrayasitssingleparameterthatcontainsallthePromisesthatwewanttogetcomposed.ThereturnedcomposedPromisedoesnotdifferatallfromthePromisesthatwehaveseensofarandthecallbackofthethen()methodisinvokedwithanarrayasitsparameter,whichcontainsalltheresultsofthePromisesthatarepartofthecomposition.
HowjQueryusesPromisesAtthetimethatjQueryaddedanimplementationofPromisestoitsAPI,italsostartedtoexposeitthroughotherasynchronousmethodsofitsAPI.Perhapsthemostwell-knownexampleofthiskindisthemethodofthe$.ajax()familythatreturnsajqXHRobject,whichisaspecializedPromiseobjectthatalsoprovidessomeextramethodsrelatedtotheAJAXrequest.
NoteFormoreinformationonthejQuery’s$.ajax()methodandthejqXHRobject,youcanvisithttp://api.jquery.com/jQuery.ajax/#jqXHR.ThejQueryteamalsodecidedtochangetheimplementationofseveralinternalpartsofthelibrarytousePromises,inordertoimprovetheirimplementations.Firstofall,the$.ready()methodisimplementedusingPromisessothattheprovidedcallbacksfireevenifthepagehasalreadybeenloadedalongtimebeforeitsinvocation.Also,someofthecomplexanimationsthatjQueryprovidesusePromisesinternallyasthepreferredwaytosynchronizetheexecutionofthesequentialpartsoftheanimationqueue.
TransformingPromisestoothertypesDevelopingbyusingseveraldifferentJavaScriptlibrariesoftenmakesmanyPromiseimplementationsavailabletoourprojectsthatunfortunatelytendtohavedifferentlevelsofcompliancetothereferencePromisesspecification.ComposingPromisesreturnedbythemethodsofdifferentlibrariescanoftenleadtoproblemsthatarehardtotrackandresolve,asaresultoftheirimplementationinconsistencies.
Inordertoavoidconfusionsinsuchsituations,itisn’tconsideredagoodpracticetotransformallthePromisestoasingletypebeforeattemptingtocomposethem.ThesuggestedtypeforsuchsituationsisthePromises/A+specification,sincenotonlyisitwidelyacceptedbythecommunitybutitisalsopartofthenewlyreleasedversionofJavaScript(theES6languagespecification)thatisalreadynativelyimplementedinmanybrowsers.
TransformingtoPromises/A+Forexample,let’sseehowajQueryPromisecanbetransformedtoanA+Promisethatisavailableinmostrecentbrowsers:
varjqueryPromise=$.Deferred().resolve('IwillbeA+
compliant').promise();
varp=Promise.resolve(jqueryPromise);
p.then(function(result){
console.log(result);
});
Intheprecedingexample,thePromise.resolve()methoddetectsthatithasbeeninvokedwitha“thenable”andthatthenewlycreatedA+PromisethatisreturnedbindsitsstatusandresulttothoseoftheprovidedjQueryPromise.Thisisessentiallyequivalenttodoingsomethingasfollows:
varp=newPromise(function(resolve,reject){
jqueryPromise.then(resolve,reject);
});
Ofcourse,thisisnotlimitedtoPromisesthatarecreatedbydirectinvocationsofthe$.Deferred()method.TheabovetechniquecanalsobeusedtotransformPromisesthatarereturnedbyanyjQuerymethod.Forexample,thisishowitcanbeusedwiththe$.getJSON()method:
varaPlusAjaxPromise=Promise.resolve($.getJSON('AjaxContent.json'));
aPlusAjaxPromise.then(function(result){
console.log(result);
});
TransformingtojQueryPromisesEventhoughIwouldgenerallynotrecommendthis,itisalsopossibletotransformanyPromisetoajQueryvariant.ThenewlycreatedjQueryPromisereceivesalltheextrafunctionalitiesthatjQueryprovides,butthetransformationisnotasstraightforwardasthepreviousone:
varaPromise=Promise.resolve('IwillbeajQueryPromise');
varp=$.Deferred(function(deferred){
aPromise.then(function(result){
returndeferred.resolve(result);
},function(error){
returndeferred.reject(error);
});
}).promise();
p.then(function(result){
console.log(result);
});
YoushouldonlyusetheprecedingtechniqueincaseswhereyouneedtoextendabigwebapplicationthatisalreadyimplementedusingjQueryPromises.Ontheotherhand,youshouldalsoconsiderupgradingsuchimplementations,sincethejQueryteamhasalreadyannouncedthatVersion3.0ofthelibrarywillhavePromises/A+compliantimplementation.
NoteFormoreinformationonjQueryv3.0A+Promisesimplementation,youcanvisithttp://blog.jquery.com/2016/01/14/jquery-3-0-beta-released/.
SummarizingthebenefitsofPromisesOverall,thebenefitsofusingPromisesoverplainCallbacksinclude:
HavingaunifiedwaytohandletheresultofasynchronousinvocationsHavingpredictableinvocationparametersfortheusedcallbacksTheabilitytoattachmultiplehandlersforeachoutcomeofthePromiseTheguaranteethattheappropriateattachedhandlerswillexecuteevenifthePromisehasalreadybeenResolved(orRejected)Theabilitytochainasynchronousoperations,makingthemruninorderTheabilitytoeasilycreatecompositionsofasynchronousoperations,makingthemrunconcurrentlyTheconvenientwayofhandlingerrorsinPromisechains
UsingamethodthatreturnsaPromiseremovestheneedtodirectlypassfunctionsofonecontexttoanotherasaninvocationargumentandthequestionregardingwhichparametersareusedasthesuccessandtheerrorCallbacks.Moreover,wealreadyknowtosomedegreehowtoretrievetheresultofanyoperationthatreturnsaPromise,byusingthethen()method,evenbeforereadingthedocumentationaboutthemethod’sinvocationparameters.
Lessparametersoftenmeanslesscomplexity,smallerdocumentation,andlesssearchingeverytimewewanttodoamethodinvocation.Evenbetter,thereisagoodchancethattherewillonlybeasingleorafewparameters,makingtheinvocationmoresensibleandreadable.Theimplementationofasynchronousmethodsalsobecomeslesscomplex,sincethereisnolongertheneedtoacceptcallbackfunctionsasanextraargumentorhavingtoproperlyinvokethemwiththeresult.
SummaryInthischapter,weanalyzedthedevelopmentpatternsthatareusedtoprogramasynchronousandconcurrentprocedures.Wealsolearnedhowtousethemtoefficientlyorchestratetheexecutionofasynchronousproceduresthatruneitherinorderorparalleltoeachother.
Atfirst,wehadarefresheronhowCallbacksareusedinJavaScriptprogrammingandhowtheyareanintegralpartofwebdevelopment.Weanalyzedtheirbenefitsandlimitationswhenusedinlargeandcompleximplementations.
Rightafterthis,wewereintroducedtotheconceptsofPromises.WelearnedhowjQuery’sDeferredandPromiseAPIsworkandhowtheydifferfromES6Promises.WealsosawwhereandhowtheyareusedinternallybyjQueryitself,asanexampleofhowtheycanleadtomorereadablecodeandsimplifysuchcompleximplementations.
Inthenextchapter,wewillproceedtolearninghowtodesign,create,anduseMockObjectsandMockServicesinourapplications.WewillanalyzethecharacteristicsthataproperMockObjectshouldhaveandunderstandhowtheycanbeusedasrepresentativeusecasesandevenastestcasesforourcode.
Chapter8.MockObjectPatternInthischapterwewillshowcasetheMockObjectPattern,apatterntofacilitatethedevelopmentofapplicationswithoutactuallybeingpartofthefinalimplementation.Wewilllearnhowtodesign,createandusethisindustry-standarddesignpatterninordertocoordinateandcompletethedevelopmentofmulti-partjQueryapplicationsfaster.WewillanalyzethecharacteristicsthataproperMockObjectshouldhaveandunderstandhowtheycanbeusedasrepresentativeusecasesandevenastestcasesforourcode.
WewillseehowgoodapplicationarchitecturemakesiteasierforustouseMockObjects&Servicesbymatchingindividualpartsoftheapplication,andalsorealizethebenefitsofusingthemduringdevelopment.Bytheendofthischapter,wewillbeabletocreateMockObjects&Servicestoacceleratetheimplementationofourapplicationandalsotogetasenseoftheoverallfunctionalitylongbeforeallofitspartsarecompleted.
Inthischapter,weshall:
IntroducetheMockObjectandMockServicePatternsAnalyzethecharacteristicsthatMockObjects&ServicesshouldhaveUnderstandwhytheyfitbetterwithapplicationswithgoodarchitectureLearnhowtousetheminjQueryapplicationsasawaytodrivethedevelopmentandaccelerateit
IntroducingtheMockObjectPatternThekeyconceptoftheMockObjectPatternisincreatingandusingadummyobjectthatsimulatesthebehaviorofamorecomplexobjectthatis(orwillbe)partofanimplementation.TheMockObjectshouldhavethesameAPIastheactual(orreal)object,returnsimilarresultsusingthesamedatastructures,andalsooperateinasimilarmannerwithregardstohowitsmethodsalteritsexposedstate(theproperties).
MockObjectsareusuallycreatedduringtheearlydevelopmentphasesofanapplication.TheirprimaryusecaseistoenableustoproceedwiththedevelopmentofaModule,evenifitdependsonothersthathavenotyetbeenimplemented.MockObjectscanalsobedescribedasprototypesofthedataexchangedbetweenthedifferentpartsoftheimplementation,actinglikecontractsbetweenthedevelopersandeasingtheparalleldevelopmentofinterdependentmodules.
TipInthesamewaythattheprinciplesoftheModulePatterndecoupletheimplementationsofthedifferentpartsofanapplication,creatingandusingMockObjectsandMockServicesdecouplestheirdevelopment.
CreatingMockObjectsforeveryModulebeforestartingtheirimplementationclearlydefinesthedatastructuresandAPIsthatwillbeusedbytheapplication,removinganymisconceptionsandenablingustodetectinsufficienciesintheproposedAPIs.
TipDefiningthedatastructuresthatarerequiredtodescribeaproblembeforestartingtheactualimplementationallowsustofocusontheneedsoftheapplicationandgetanideaofitsoverallcomplexityandstructure.
YoucanalwaystestanypartofyourimplementationafteranycodechangebyusingtheMockObjectsthatwerecreatedfortheoriginalimplementation.YoucanbesurethattheoriginalusecasestillworksbyusingtheMockObjectsonthemodifiedmethods.Thisisveryusefulwhenthemodifiedimplementationisapartofausecaseinvolvingseveralstages.
MockObjectsareespeciallyusefulfortracingerrorsiftheimplementationofaModulehaschangedandcausedtherestoftheapplicationtomisbehave.ByusingtheexistingMockObjects,wecaneasilyidentifytheModulethatdivergedfromtheoriginalspecification.Moreover,thesameMockObjectscanbeusedasthebasisforhighqualitytestcasessincetheyoftencontainmorerealisticsampledata,somethingespeciallyusefulifyourteamisfollowingaTestDrivenDevelopment(TDD)paradigm.
NoteInTestDrivenDevelopment(TDD),thedeveloperfirstlydefinesatestcaseforausecaseoranewfeaturethatneedstobeaddedandthenproceedswithitsimplementationbytryingtosatisfythecreatedtestcase.Formoreinformation,youcanvisit:
https://www.packtpub.com/books/content/overview-tdd.
TheMockObjectPatterniscommonlyusedamongfrontendwebdeveloperstodecoupletheclient-sidedevelopmentfromthewebservicesthatthebackendwillexpose.Thathasledtowittycommentssuchas:
“Thewebservicewillalwaysbelate&changesuddenly,souseaMockinstead.”
Summarizingallofthis,themainreasonstocreateMockObjectsandServicesinclude:
Theactualobjectorserviceisnotyetimplemented.Theactualobjectisdifficulttosetupforaspecificusecase.Weneedtoemulatearareornon-deterministicbehavior.Theactualobjectbehavesinawaythatishardtoreproduce,suchasnetworkerrorsorUIevents.
UsingMockObjectsinjQueryapplicationsInordertodemonstratehowtheMockObjectPatterncanbeusedduringthedevelopmentofamulti-partapplication,wewillextendthedashboardexample,aswesawinChapter4,DivideandConquerwiththeModulePattern,inordertopresentthumbnailsofYouTubevideosfromwebdevelopingconferences.Thevideoreferencesaregroupedintofourpredefinedcategoriesandtherelatedbuttonswillbedisplayedbasedonthecurrentcategoryselection,asillustratedbelow:
ThechangesthatneedtobeintroducedtotheHTMLandtheCSSareminimal.TheonlyextraCSSthatisneededfortheaboveimplementation,whencomparedtotheexistingimplementationfromChapter4,DivideandConquerwiththeModulePattern,isrelatedtothewidthofthethumbnails:
.boximg{
width:100%;
}
ThechangeintheHTMLisintendedtoorganizethe<button>elementsofeachcategory.ThischangewillmakeourimplementationmorestraightforwardsincethecategoriesandtheiritemsarenolongerstaticallydefinedintheHTMLbutareinsteadcreateddynamically,drivenbytheavailabledata.
<!--…-->
<sectionclass="dashboardCategories">
<selectid="categoriesSelector"></select>
<divclass="dashboardCategoriesList"></div>
<divclass="clear"></div>
</section>
<!--…-->
IntheabovepieceofHTML,the<div>elementwiththedashboardCategoriesListCSSclass,willbeusedasacontainerforthegroupedbuttonsofthedifferentvideocategories.AftercoveringtheUIelements,let’snowmoveontotheanalysisoftheJavaScriptimplementation.
DefiningtheactualservicerequirementsThevideoreferencestobedisplayedinourdashboardcouldberetrievedfromvarioussources.Forexample,youcouldmakeadirectcalltoYouTube’sclient-sideAPIoranAJAXcalltoabackendwebservice.Inalloftheabovecases,itisconsideredagoodpracticetoabstractthisdataretrievalmechanismintoaseparatemodule,followingthecodestructuringrecommendationsofthepreviouschapters.
Forthisreason,weneedtoaddanextramoduletotheexistingimplementation.Thiswillbeaservice,responsibleforprovidingthemethodsthatwillallowustoretrievethemostrelevantvideosfromeachcategoryandloadinformationforeachvideoindividually.ThiswillbeachievedbyusingthesearchVideos()andgetVideo()methodsrespectively.
Aswehavealreadysaid,oneofthemostimportantphasesofeachimplementation,especiallyincaseofparalleldevelopment,istheanalysisanddefinitionofthedatastructurestobeused.SinceourdashboardwillbeusingtheYouTubeAPI,weneedtocreatesomesampledatawhichfollowitsdatastructurerules.AfterinspectingtheAPI,weendupwithasub-setofthefieldsthatarerequiredforourdashboard,andcanproceedtocreateaJSONobjectwithmockdatatodemonstratetheuseddatastructure:
{
"items":[{
"id":{"videoId":"UdQbBq3APAQ"},
"snippet":{
"title":"jQueryUIDevelopmentTutorial:jQueryUITooltip|
packtpub.com",
"thumbnails":{
"default":{"url":
"https://i.ytimg.com/vi/UdQbBq3APAQ/default.jpg"},
"medium":{"url":
"https://i.ytimg.com/vi/UdQbBq3APAQ/mqdefault.jpg"},
"high":{"url":"https://i.ytimg.com/vi/UdQbBq3APAQ/hqdefault.jpg"
}
}
}
}/*,...*/]
}
NoteFormoreinformationabouttheYouTubeAPI,youcanvisit:https://developers.google.com/youtube/v3/getting-started.
Ourserviceprovidestwocoremethods,oneforsearchingforvideosinaspecifiedcategoryandoneforretrievinginformationaboutaspecificvideo.Thestructureofthesampleobjectisusedforthesearchmethodtoretrieveasetofrelevantitems,whilethemethodforretrievinginformationforasinglevideousesthedatastructureofeachindividualitem.TheresultingimplementationforthevideoinformationretrievalisinaseparatemodulenamedvideoService,whichwillbeavailableonthedashboard.videoServicenamespace,andourHTMLwouldcontaina<script>referencelikethefollowing:
<scripttype="text/javascript"src="dashboard.videoservice.js"></script>
ImplementingaMockServiceChangingthe<script>referencesoftheserviceimplementationwiththeMockServiceandviceversashouldleaveuswithaworkingapplication,helpingusprogressandtesttherestoftheimplementationbeforetheactualimplementationofthevideoserviceisfinished.Asaresult,theMockServiceneedstousethesamedashboard.videoServicenamespace,butitsimplementationshouldbeinadifferentlynamedfilesuchasdashboard.videoservicemock.jsthatsimplyaddsthe“mock”suffix.
Aswehavealreadymentioned,itisagoodpracticetoplaceallourmockdataunderasinglevariable.Moreover,iftherearealotofMockedObjects,itiscommontoplacetheminadifferentfilealtogether,withanestednamespace.Inourcase,thefilewiththemockdataisnameddashboard.videoservicemock.mockdata.jsanditsnamespaceisdashboard.videoService.mockData,whileexposingthesearchesandvideospropertiesthatwillbeusedbythetwocoremethodsofourMockService.
EventhoughtheimplementationsofMockServicesshouldbesimple,theyhavetheirowncomplexitysincetheyneedtoprovidethesamemethodsasthetargetimplementations,acceptthesamearguments,andlookasiftheyareoperatingintheexactsameway.Forexample,inourcase,thevideoretrievalserviceneedstobeasynchronousanditsimplementationneedstoreturnPromises:
(function(){//dashboard.videoservicemock.js
'usestrict';
dashboard.videoService=dashboard.videoService||{};
dashboard.videoService.searchVideos=function(searchKeywords){
return$.Deferred(function(deferred){
varsearches=dashboard.videoService.mockData.searches;
for(vari=0;i<searches.length;i++){
if(searches[i].keywords===searchKeywords){
//returnthefirstmatchingsearchresults
deferred.resolve(searches[i].data);
return;
}
}
deferred.reject('Notfound!');
}).promise();
};
dashboard.videoService.getVideo=function(videoTitle){
return$.Deferred(function(deferred){
varvideos=dashboard.videoService.mockData.allVideos;
for(vari=0;i<videos.length;i++){
if(videos[i].snippet.title===videoTitle){
//returnthefirstmatchingitem
deferred.resolve(videos[i]);
return;
}
}
deferred.reject('Notfound!');
}).promise();
};
varvideoBaseUrl='https://www.youtube.com/watch?v=';
dashboard.videoService.getVideoUrl=function(videoId){
returnvideoBaseUrl+videoId;
};
})();
AsshownintheMockServiceimplementationabove,thesearchVideos()andgetVideo()methods,areiteratingoverthearrayswiththemockdataandreturnaPromisethatiseitherResolvedwithanappropriateMockObjectorRejectedwhensuchanobjectisnotfound.Finally,youcanseebelowthecodeforthesub-modulecontainingtheMockObjects,followingthedatastructurethatwedescribedearlier.NotethatwestoretheMockObjectsofallcategoriesintheallVideospropertyinordertomakesearchingwiththemockgetVideo()methodsimpler.
(function(){//dashboard.videoservicemock.mockdata.js
'usestrict';
dashboard.videoService.mockData=dashboard.videoService.mockData||
{};
dashboard.videoService.mockData.searches=[{
keywords:'jQueryconference',
data:{
"items":[/*...*/]
}
}/*,...*/];
varallVideos=[];
varsearches=dashboard.videoService.mockData.searches;
for(vari=0;i<searches.length;i++){
allVideos=allVideos.concat(searches[i].data.items);
}
dashboard.videoService.mockData.allVideos=allVideos;
})();
ExperimentingwiththeimplementationofsomeMockServiceswillgetyoufamiliarwiththeircommonimplementationpatternsinaveryshortperiodoftime.Beyondthat,youwillbeabletoeasilycreateMockObjectsandServices,helpingyoudesigntheAPIsofyourapplications,trythemoutbyusingthemocksandfinallysettleonthebestmatchingmethodsanddatastructuresforeachusecase.
TipUsingthejQueryMockjaxlibrary
TheMockjaxjQueryPluginlibrary(availableathttps://github.com/jakerella/jquery-mockjax)focusesonprovidingasimplewayofmockingorsimulatingAJAXrequestsandresponses.ThisreducesthecodeneededtofullyimplementyourownMockServices,ifallthatyouneedistointerceptanAJAXrequesttoawebserviceandreturnaMock
Objectinstead.
UsingtheMockServiceInordertoaddthefunctionalitythatwedescribedearliertotheexistingdashboardimplementation,weneedtointroducesomechangestothecategoriesandtheinformationBoxmodules,addingthecodethatwillconsumethemethodsofourservice.AsarepresentativeexampleofusingthenewlycreatedMockService,let’stakealookattheimplementationoftheopenNew()method,intheinformationBoxmodule:
dashboard.informationBox.openNew=function(itemName){
var$box=$('<divclass="boxsizer"><articleclass="box">'+
'<headerclass="boxHeader">'+
'<buttonclass="boxCloseButton">✖</button>'+
itemName+
'</header>'+
'<divclass="boxContent">Loading…</div>'+
'</article></div>');
$boxContainer.append($box);
dashboard.videoService.getVideo(itemName).then(function(result){
var$a=$('<a>').attr('href',
dashboard.videoService.getVideoUrl(result.id.videoId));
$a.append($('<img/>').attr('src',
result.snippet.thumbnails.medium.url));
$box.find('.boxContent').empty().append($a);
}).fail(function(){
$buttonContainer.html('Anerroroccurred!');
});
};
ThismethodinitiallyopensanewinformationboxwithaLoading…labelasitscontentandusesthedashboard.videoService.getVideo()methodtoretrievethedetailsoftherequestedvideoasynchronously.Finally,whenthereturnedPromisegetsresolved,itreplacestheLoading…labelwithananchorcontainingthethumbnailofthevideo.
SummaryInthischapter,welearnedhowtodesign,createanduseMockObjectsandMockServicesinourapplications.WeanalyzedthecharacteristicsthatMockObjectsshouldhaveandunderstoodhowtheycanbeusedasrepresentativeusecases.WearenowabletouseMockObjects&Servicestoacceleratetheimplementationofourapplicationsandgetabettersenseofitsoverallfunctionality,longbeforeallofitsindividualpartsarecompleted.
Inthenextchapter,wewillbeintroducedtoclient-sidetemplatingandlearnhowtogeneratecomplexHTMLstructuresinthebrowserfromreadabletemplatesefficiently.WewillgetanintroductiontoUnderscore.jsandHandlebars.js,analyzetheirconventions,evaluatetheirfeaturesandfindwhichonebettersuitsourtaste.
Chapter9.Client-sideTemplatingThischapterwilldemonstratesomeofthemostwidelyusedlibrariestocreatecomplexHTMLtemplatesfaster,whilemakingourimplementationeasiertoreadandunderstandwhencomparedtotraditionalstringconcatenationtechniques.WewilllearninmoredetailhowtousetheUnderscore.jsandHandlebars.jstemplatinglibraries,getatasteoftheirconventions,evaluatetheirfeaturesandfindtheonethatbestsuitsourtaste.
Bytheendofthischapter,wewillbeabletogeneratecomplexHTMLstructuresinthebrowserefficientlybyusingreadabletemplatesandutilizingtheuniquecharacteristicsofeachtemplatinglibrary.
Inthischapter,wewill:
DiscussthebenefitsofusingaspecializedtemplatinglibraryIntroducethecurrenttrendsinclient-sidetemplating,specificallythetoprepresentativeofthefamiliesthatuse<%%>and{{}}astheirplaceholdersIntroduceUnderscore.jsasanexampleofthefamilyoftemplatingenginesthatuse<%%>placeholdersIntroduceHandlebars.jsasanexampleofthefamilyoftemplatingenginesthatusecurlybraces{{}}placeholders
IntroducingUnderscore.jsUnderscore.jsisaJavaScriptlibrarythatprovidesacollectionofutilitymethodsthathelpwebdevelopersworkmoreefficientlyandfocusontheactualimplementationoftheirapplicationratherthanbotheringwithrepetitivealgorithmicproblems.Underscore.jsis,bydefault,accessiblethroughthe“_”identifieroftheglobalnamespaceandthat’sexactlywhereitsnamecomesfrom.
NoteAswiththe$identifierinjQuery,theunderscore“_”identifiercanalsobeusedasavariablenameinJavaScript.
Oneoftheutilityfunctionsthatitprovidesisthe_.template()method,whichprovidesuswithaconvenientwayofinterpolatingspecificvaluesintoexistingtemplatestringsthatfollowaspecificformat.The_.template()methodrecognizesthreespecialplaceholdernotationsinsidetemplates,whichareusedtoadddynamiccharacteristics:
The<%=%>notationisusedasthesimplestwaytointerpolateavalueofavariableoranexpressioninatemplate.The<%-%>notationperformsHTMLescapingonavariableorexpressionandtheninterpolatesitinatemplate.The<%%>notationisusedtoexecuteanyvalidJavaScriptstatementaspartofthetemplategeneration.
The_.template()methodacceptsatemplatestringthatfollowsthesecharacteristicsandreturnsaplainJavaScriptfunction,commonlyreferredtoasthetemplatefunction,whichcanbeinvokedwithanobjectcontainingthevaluesthataregoingtobeinterpolatedinthetemplate.Theresultoftheinvocationofthetemplatefunctionisastringvalue,whichistheresultoftheinterpolationoftheprovidedvaluesinsidethetemplate:
vartemplateFn=_.template('<h1><%=title%></h1>');
varresultHtml=templateFn({
title:'Underscore.jsexample'
});
Asanexample,theabovecodereturns<h1>Underscore.jsexample</h1>andisequivalenttothefollowingshorthandinvocation:
varresultHtml=_.template('<h1><%=title%></h1>')({
title:'Underscore.jsexample'
});
NoteFormoreinformationaboutthe_.templatemethod,youcanreadthedocumentationat:http://underscorejs.org/#template.
WhatmakesUnderscore.jstemplatesveryflexibleisthe<%%>notation,whichallowsustoperformanymethodinvocationandis,forexample,usedastherecommendedwaytocreateloopsinatemplate.Ontheotherhand,overusingthisfeaturemayaddtoomuch
logictoyourtemplates,whichisaknownanti-patternfoundinmanyotherframeworks,violatingtheprincipleofSeparationofConcerns.
UsingUnderscore.jstemplatesinourapplicationsAsanexampleofusingUnderscore.jsfortemplating,wewillnowuseittorefactortheHTMLcodegenerationwhichtakesplaceinsomemodulesofthedashboardexample,aswesawinpreviouschapters.ThemodificationsrequiredtotheexistingimplementationarelimitedtothecategoriesandtheinformationBoxmodules,whichmanipulatetheDOMtreeofthepagebyaddingnewelements.
Thefirstplacethatsucharefactorcanbeappliedisintheinit()methodofthecategoriesmodule.Wecanmodifythecodethatcreatestheavailable<option>softhe<select>categorytolooklikethis:
varoptionTemplate=_.template('<optionvalue="<%=value%>"><%-title%>
</option>');
varoptionsHtmlArray=[];
for(vari=0;i<dashboard.categories.data.length;i++){
varcategoryInfo=dashboard.categories.data[i];
optionsHtmlArray.push(optionTemplate({
value:i,
title:categoryInfo.title
}));
}
$categoriesSelector.append(optionsHtmlArray.join(''));
Asyoucansee,weiterateoverthecategoriesofthedashboardinordertocreateandappendtheappropriate<option>elementstothe<select>categoryelement.Inourtemplate,weareusingthe<%=%>notationforthevalueattributeofthe<option>sinceweknowthatitwillholdanintegervaluethatdoesnotneedescaping.Ontheotherhand,weareusingthe<%-%>notationforthecontentpartofeach<option>inordertoescapethetitleofeachcategoryforthecaseitsvalueisnotanHTML-safestring.
Weareusingthe_.template()methodoutsidetheforloopinordertocreateasinglecompiledtemplatefunctionthatwillbereusedoneachiterationoftheforloop.Inthisway,thebrowsernotonlyexecutesthe_.template()methodjustonce,butalsooptimizesthegeneratedtemplatefunctionandmakesitrunfasteroneachsubsequentexecutioninsidetheforloop.Lastly,weareusingthejoin('')methodtocombinealltheHTMLstringsoftheoptionsHtmlArrayvariableandappend()theresulttotheDOMwithasingleoperation.
Analternativeandpossiblysimplerwaytoachievethesameresultisbycombiningthe<%%>notationandthe_.each()methodthatUnderscore.jsprovides,enablingustoimplementaloopinsidethetemplateitself.Inthisway,thetemplatewillberesponsiblefortheiterationovertheprovidedarrayofcategories,movingthecomplexityfromtheimplementationofthemoduleintothetemplate.
vartemplateSource=''.concat(
'<%_.each(categoryInfos,function(categoryInfo,i){%>',
'<optionvalue="<%=i%>"><%-categoryInfo.title%></option>',
'<%});%>');
varoptionsHtml=_.template(templateSource)({
categoryInfos:dashboard.categories.data
});
$categoriesSelector.append(optionsHtml);
Asyoucanseeintheabovecode,ourJavaScriptimplementationnolongercontainsaforloop,reducingitscomplexityandtherequirednesting.Thereisonlyasinglecalltothe_.template()method,whichnicelyabstractstheimplementationtoanoperationthatgeneratestheHTMLandrendersthe<option>elementsforallthecategories.YoucanalsoseehownicelythistechniquefitsinwiththeCompositelogicthatjQueryitselffollows,inwhichthemethodsaredesignedtooperateovercollectionsofelementsinsteadofsingleitems.
SeparatingHTMLtemplatesfromJavaScriptcodeEvenafterintroducingalloftheaboveimprovements,itsoonstartstobecomeobviousthatwritingtemplatesinbetweenyourapplicationlogicmightnotbethebestapproachtofollow.Assoonasyourapplicationbecomescomplexenough,orwhenyouneedtousetemplatesthataremorethanafewlineslong,theimplementationstartstofeelfragmentedbythemixoftheapplication’slogicandtheHTMLtemplates.
AcleanerapproachtothisproblemistostoreyourtemplatesalongsidetherestoftheHTMLcodeofyourpage.ThisisagoodsteptowardsbetterSeparationofConcernssinceitproperlyisolatesthepresentationfromtheapplicationlogic.
InordertoincludeHTMLtemplatesaspartofwebpagesinaninactiveform,weneedtouseahosttagthatwillpreventthemfrombeingrendered,butalsoallowustoretrieveitscontentprogrammaticallywhenneeded.Forthispurpose,wecanuse<script>tagsinsidethe<head>orthe<body>ofourpageandspecifyanytypeotherthanthecommontext/javascriptthatwenormallyuseforourJavaScriptcode.Theoperationprinciplebehindthisisthatbrowsersdonottrytoparse,executeorrenderthecontentof<script>tags,incasetheirtypeattributeisn’trecognized.Aftersomeexperimentation,thecommunityofUnderscore.jsusershaslargelyadoptedthispracticeandagreedtospecifytext/templateasthepreferredtypeforthese<script>tags,inanattempttomaketheseimplementationsmoreuniformamongdevelopers.
TipEventhoughUnderscore.jsisneitheropinionatednorcontainsanyimplementationspecifictothewaythatthetemplatesbecomeavailable,usingtext/template<script>tagsand/orAJAXrequestshavebeenvaluabletechniquesthatarewidelyusedandareconsideredbestpractices.
Asanexampleofacomplextemplatethatwouldbebeneficialtomoveintoa<script>tag,wewillrefactortotheopenNew()methodoftheinformationBoxmodule.Asyoucanseeinthecodebelow,theresulting<script>tagiscleanlyformattedandwenolongerneedtousestringconcatenationforthedefinitionofthemulti-linetemplate:
<scriptid="box-template"type="text/template">
<divclass="boxsizer">
<articleclass="box">
<headerclass="boxHeader">
<buttonclass="boxCloseButton">✖</button>
<%-itemName%>
</header>
<divclass="boxContent">Loading…</div>
</article>
</div>
</script>
AgoodpracticewhenmovingHTMLtemplatesoutofourcodeistowriteanabstractedmechanismtoberesponsibleforretrievingthemandprovidingthecompiledtemplatefunction.Thisapproachnotonlydecouplestherestoftheimplementationfromthetemplateretrievalmechanismbutalsomakesitlessrepetitiveandcreatesacentralizedmethoddesignedtoprovidetemplatesfortherestoftheapplication.Moreover,aswecanseebelow,thisapproachalsoallowsustooptimizethewaythattemplatesareretrieved,propagatingthebenefitstoalltheplacesthattheyareused.
vartemplateCache={};
functiongetEmbeddedTemplate(templateName){
varcompiledTemplate=templateCache[templateName];
if(!compiledTemplate){
vartemplate=$('#'+templateName).html();
compiledTemplate=_.template(template);
templateCache[templateName]=compiledTemplate;
}
returncompiledTemplate;
}
dashboard.informationBox.openNew=function(itemName){
varboxCompiledTemplate=getEmbeddedTemplate('box-template');
varboxHtml=boxCompiledTemplate({
itemName:itemName
});
var$box=$(boxHtml).appendTo($boxContainer);
/*...*/
};
Asshownintheaboveimplementation,theopenNew()methodoftheinformationBoxmodulesimplyinvokesthegetEmbeddedTemplate()functionbypassingauniqueidentifierthatisassociatedwiththerequestedtemplateandusesthereturnedtemplatefunctiontogeneratethenewbox’sHTMLandfinallyappendittothepage.ThemostinterestingpartoftheimplementationisthegetEmbeddedTemplate()method,whichusesthetemplateCachevariableasadictionarytoholdallthepreviouslycompiledtemplatefunctions.
Thefirststepisalwaystocheckwhethertherequestedtemplateidentifierexistsinourtemplatecache.Ifnot,thentheDOMtreeofthepageissearchedforthe<script>tagwiththerelatedIDanditsHTMLcontentisusedtocreatethetemplatefunction,whichisthenstoredinthecacheandreturnedtothecaller.
KeepinmindthatitisagoodpracticetouseaspecificprefixorsuffixforalltheidentifiersofyourHTMLtemplatesinordertoavoidconflictswiththeIDsofotherpage
elements.Forthispurpose,intheaboveexampleweusedthe-templateasasuffixoftheidentifierofourboxtemplate.
Ideally,theimplementationofthetemplateprovidermethodshouldbeinaseparatemodulethatwillbeusedbyallthepartsofanapplicationbut,sinceinourdashboardthisisusedinonlyoneplace,wemettheneedsofourdemonstrationbysimplyusingafunction.
IntroducingHandlebars.jsHandlebars.js,orsimplyHandlebars,isaspecializedclient-sidetemplatinglibrarythatenableswebdeveloperstocreatesemantictemplateseffectively.UsingHandlebarsfortemplatingleadstothecreationoflogic-freetemplateswhichensuresthattheviewandthecodeareisolated,helpingpreservetheSeparationofConcernsprinciple.ItislargelycompatiblewithMustachetemplates,whichareatemplatinglanguagespecificationthathaveproventheireffectivenessovertimeandhavemanyimplementationsforallthemajorprogramminglanguages.Additionally,HandlebarsprovidesasetofextensionsontopoftheMustachetemplatespecification,suchashelpermethodsandpartials,asameansofextendingthetemplatingengineandcreatingmoreeffectivetemplates.
NoteYoucanseeallthedocumentationforHandlebarsat:http://handlebarsjs.com/.YoucangetmoreinformationaboutMustacheinJavaScriptat:https://github.com/janl/mustache.js/.
ThemaintemplatenotationthatHandlebarsprovidesisthedoublecurlybracessyntax{{}}.AsHandlebarswasdesignedtobeusedforHTMLtemplatesfromthebeginning,thisnotationalsoappliesHTMLescapingbydefault,loweringthechancesthatanon-escapedvaluecouldreachthetemplatecausingpotentialsecurityproblems.Ifanon-escapedinterpolationisrequiredforaspecificpartofatemplate,wecanusethetriplecurlybracesnotation{{{}}}.
Moreover,sinceHandlebarspreventsusfrominvokingmethodsdirectlyfromwithinatemplate,itprovidesuswiththeabilitytodefineandusehelpermethodsandblockexpressionsasawaytocovermorecomplexusecaseswhilealsohelpingtomaintainourtemplatesascleanandreadableaspossible.Thesetofbuilt-inhelpersincludesthe{{#if}}and{{#each}}helperswhichallowustoperformiterationsoverarraysandchangetheoutcomesofatemplatebasedonconditionsveryeasily.
ThecentralmethodoftheHandlebarslibraryistheHandlebars.compile()method,whichacceptsatemplatestringasaparameterandreturnsafunctionthatcanbeusedtogeneratestringvaluesthatfollowtheformoftheprovidedtemplate.Thisfunctioncanthenbeinvoked(asinUnderscore.js)withanobjectasaparameter,thepropertiesofwhichwillbeusedasacontextfortheevaluationofalltheHandlebarsexpressions(thecurlybracesnotations)thatweredefinedintheoriginaltemplate:
vartemplateFn=Handlebars.compile('<h1>!!!{{title}}!!!</h1>');
varresultHtml=templateFn({
title:'>Handlebarsexample<'
});
Asanexample,theabovecodereturns"<h1>!!!>Handlebarsexample<!!!</h1>",turningtheinterpolatedtitleintoasafeHTMLstring,butonewhichwouldotherwiserenderproperlywhenattachedtotheDOMtreeofapage.Ofcourse,thesameresultcanbeachievedwiththefollowingshorthandinvocation,ifwedon’tneedtokeepareferencetothecompiledtemplatefunctionforfutureuse:
varresultHtml=Handlebars.compile('<h1>!!!{{title}}!!!</h1>')({
title:'>Handlebarsexample<'
});
UsingHandlebars.jsinourapplicationsAsanexampleofusingHandlebars.jsfortemplatingandinordertodemonstrateitsdifferencesfromUnderscore.jstemplates,wewillnowuseittorefactorourdashboardexample,likewedidintheprevioussection.Likebefore,therefactoringislimitedtothecategoriesandtheinformationBoxmodules,whichmanipulatetheDOMtreeofthepagebyaddingnewelements.
Therefactoredimplementationoftheinit()methodofthecategoriesmoduleshouldlooklikethis:
varoptionTemplate=Handlebars.compile('<optionvalue="{{value}}">{{
title}}</option>');
varoptionsHtmlArray=[];
for(vari=0;i<dashboard.categories.data.length;i++){
varcategoryInfo=dashboard.categories.data[i];
optionsHtmlArray.push(optionTemplate({
value:i,
title:categoryInfo.title
}));
}
$categoriesSelector.append(optionsHtmlArray.join(''));
Firstofall,wehaveusedtheHandlebars.compile()methodwhichgeneratesandreturnsatemplatefunctionbasedontheprovidedtemplatestring.ThemaindifferencewiththeUnderscore.jsimplementationwesawintheprevioussection,isthatwenowusethedoublecurlybracesnotation{{}}tointerpolatevaluesinourtemplate.Apartfromthedifferentappearance,Handlebars.jsalsodoesHTMLstringescapingbydefaultinanattempttoeliminateHTMLinjectionsecurityholesbymakingescapingpartofitsprimaryusecase.
Aswedidearlierinthischapter,wewillcreatethetemplatefunctionoutsidetheforloopanduseittogeneratetheHTMLforeach<option>element.AllthegeneratedHTMLstringsaregatheredinanarrayandarefinallycombinedandattachedtotheDOMtreewithasingleoperation,usingthe$.append()method.
ThenextincrementalsteptoreducethecomplexityofourimplementationistoabstracttheiterationsawayfromourJavaScriptcodeusingtheloopingcapabilitiesofthetemplatingengineitself:
vartemplateSource=''.concat(
'{{#eachcategoryInfos}}',
'<optionvalue="{{@index}}">{{title}}</option>',
'{{/each}}');
varoptionsHtml=Handlebars.compile(templateSource)({
categoryInfos:dashboard.categories.data
});
$categoriesSelector.append(optionsHtml);
TheHandlebars.jslibraryallowsustoachievethatbyusingthespecial{{#each}}notation.Inbetweenthe{{#each}}and{{/each}},thecontextofthetemplateischangedtomatcheachindividualobjectoftheiteration,allowingtodirectlyaccessand
interpolatethe{{title}}ofeachobjectinthecategoryInfosarray.Moreover,inordertoaccesstheloopcounter,Handlebarsprovidesuswiththespecial@indexvariableaspartofthecontextoftheloop.
NoteForafulllistofallthespecialnotationsthatHandlebarsprovides,youcanreadthedocumentationat:http://handlebarsjs.com/reference.html
SeparatingHTMLtemplatesfromJavaScriptcodeLikemosttemplatingengines,HandlebarsalsoleadsustoisolateourtemplatesfromtheJavaScriptimplementationofourapplicationanddeliverthemtothebrowserbyincludingthemin<script>tags,insidetheHTMLofourpages.Moreover,Handlebarsisopinionatedandprefersthespecialtext/x-handlebars-templateasthetypeattributeforall<script>tagsthatcontainHandlebarstemplates.Forexample,hereishowthetemplateforthedashboard’sboxesshouldbedefinedaccordingtothelibraryrecommendations:
<scriptid="box-template"type="text/x-handlebars-template">
<divclass="boxsizer">
<articleclass="box">
<headerclass="boxHeader">
<buttonclass="boxCloseButton">✖</button>
{{itemName}}
</header>
<divclass="boxContent">Loading…</div>
</article>
</div>
</script>
TipEventhoughourimplementationwouldstillworkifadifferenttypewasspecifiedforthe<script>tag,followingthelibrary’sguidelinescanobviouslymakeimplementationsmoreuniformamongdevelopers.
Aswedidearlierinthischapter,wewillfollowthebestpracticeofcreatingaseparatefunctiontoberesponsibleforprovidingthetemplateswherevertheyareneededintheapplication:
vartemplateCache={};
functiongetEmbeddedTemplate(templateName){
varcompiledTemplate=templateCache[templateName];
if(!compiledTemplate){
vartemplate=$('#'+templateName).html();
compiledTemplate=Handlebars.compile(template);
templateCache[templateName]=compiledTemplate;
}
returncompiledTemplate;
}
dashboard.informationBox.openNew=function(itemName){
varboxCompiledTemplate=getEmbeddedTemplate('box-template');
varboxHtml=boxCompiledTemplate({
itemName:itemName
});
var$box=$(boxHtml).appendTo($boxContainer);
/*...*/
};
Asyoucansee,theimplementationismostlythesameastheUndescore.jsexamplethatwesawearlierinthischapter.TheonlydifferenceisthatwearenowusingtheHandlebars.compile()methodtogeneratethecompiledtemplatefunctionsfromtheretrievedtemplates.
Pre-compilingtemplatesAnextrafeatureoftheHandlebarslibraryisthesupportfortemplatepre-compilation.Thisallowsustopre-generateallthetemplatefunctionswithasimpleterminalcommandandthenhaveourserverdelivertothemtothebrowser,insteadoftheactualtemplates.Inthisway,thebrowserwillbeabletousethepre-compiledtemplatesdirectly,removingtheneedforthecompilationofeachindividualtemplateandmakingtheexecutionofthelibraryandourapplicationfaster.
Inordertopre-compileourtemplates,wefirstneedtoplacetheminseparatefiles.TheHandlebarsdocumentationsuggestsusingthe.handlebarsextensionforourfilesbutwecanstillusethe.htmlextensionifitispreferred.Afterinstallingthecompilationtoolonourdevelopmentmachine(withnpminstallhandlebars-g),wecanissuethefollowingcommandinourterminaltocompileatemplate:
handlebarsbox-template.handlebars-fbox-template.js
Thiswillgeneratethebox-template.jsfilethatisactuallyamini-moduledefinitionthataddsthetemplatetoHandlebars.templates.ThegeneratedfilecanthenbecombinedandminifiedlikeregularJavaScriptfilesand,whenloadedbyabrowser,thetemplatefunctionwillbecomeavailablethroughtheHandlebars.templates['box-template']property.
NoteKeepinmindthatifthe.htmlextensionisbeingusedforthetemplates,thenthepre-compiledtemplatefunctionwillbeavailablethroughtheHandlebars.templates['box-template.html']property.
Asyoucansee,usingatemplateproviderfunctionassistswiththemigrationofanexistingapplicationtopre-compiledtemplatessinceitallowsustoencapsulatethewaythatthetemplatesareretrieved.Movingtopre-compiledtemplatesonlyrequireschangingthegetEmbeddedTemplate()tosomethinglikethis:
functiongetEmbeddedTemplate(templateName){
returnHandlebars.templates[templateName];
}
Note
Formoreinformationabouttemplatepre-compilationinHandlebars,readthedocumentationat:http://handlebarsjs.com/precompilation.html.
RetrievingHTMLtemplatesasynchronouslyThefinalsteptomasteringclient-sidetemplatingisadevelopmentpracticethatallowsustoloadtemplatesdynamicallyandusetheminawebpagethathasalreadybeenloaded.Thisapproachcanleadtomorescalableimplementationsthantheapproachofembeddingalltheavailabletemplatesas<script>tagsinsidetheHTMLsourceofeachpage.
Thekeyelementofthistechniqueistoloadeachtemplateonlywhenitisrequiredforthepresentationofawebpage,commonlyafterauseraction.Themainbenefitsofthisapproacharethat:
TheinitialpageloadtimeisreducedsincetheHTMLofthepageissmaller.Thegainsfromthereductionofthepagesizebecomeevengreaterifourapplicationhasalotoftemplatesthatareusedonlyundercertaincircumstances,forexample,afterspecificuserinteractions.Theuseronlydownloadsatemplateifitisactuallygoingtobeused.Inthisway,thesizeofthetotaldownloadedresourcesforeachpageloadcanbereduced.Subsequentrequestsforanalreadyloadedtemplatewillnotleadtoanextradownload,sincethebrowser’sHTTPcachingmechanismwillreturnthecachedresource.Additionally,sincethebrowsercacheisusedforallHTTPrequestsregardlessofthepagefromwhichtheyoriginate,usersonlyhavetodownloadtherequiredtemplateoncewhileusingourwebapplication.
Becauseofitsbenefitstouserexperienceanditsscalability,thistechniqueiswidelyusedbythemostpopularwebmailandsocialnetworkingwebsites,wherevariousHTMLtemplatesandJavaScriptmodulesareloadeddynamically,basedonuseractions.
NoteFormoreinformationonhowjQuerycanbeusedtoloadJavaScriptmodulesonapagedynamically,readthedocumentationforthe$.getScript()methodat:https://api.jquery.com/jQuery.getScript/.
AdoptingitinanexistingimplementationToillustratethistechnique,wewillchangetheUnderscore.jsandHandlebars.jsimplementationsoftheinformationBoxmodulesothatitfetchestheboxtemplateforourdashboardusinganAJAXrequest.
Let’sproceedbyanalyzingthenecessarychangesforourUnderscore.jsimplementation:
vartemplateCache={};
functiongetAjaxTemplate(templateName){
varcompiledTemplate=templateCache[templateName];
if(compiledTemplate){
return$.Deferred().resolve(compiledTemplate);
}
return$.ajax({
mimeType:'text/html',
url:templateName+'.html'
}).then(function(template){
templateCache[templateName]=_.template(template);
returntemplateCache[templateName];
});
}
Asyoucanseeintheabovecode,wehaveimplementedthegetAjaxTemplate()functionasawayofdecouplingthemechanismthatisresponsibleforfetchingthetemplatefromtheimplementationthatusesit.ThisimplementationhasalotincommonwiththegetEmbeddedTemplate()functionthatweusedearlier,themaindifferencebeingthatthegetAjaxTemplate()functionisasynchronousand,asaresult,returnsaPromise.
ThegetAjaxTemplate()functionfirstlycheckswhetherornottherequestedtemplatealreadyexistsinitscache,asanextraattempttoreduceHTTPrequeststotheserver.Ifthetemplateisfoundinthecache,thenitisreturnedaspartofaResolvedPromise,otherwiseweinitiateanAJAXrequestusingthe$.ajax()methodtoretrieveitfromtheserver.Likebefore,weneedtohaveaconventionregardingthenamingofthetemplateHTMLfilesandthepathusedtostorethemintheserver.Inourexample,wearelookinginthesamedirectoryasthewebpageitselfandjustappendingthe.htmlfileextension.Anextraconcerninsomecases,dependingonthewebserverused,isthedefinitionofthemimeTypeoftheresourceastext/html.
WhentheAJAXrequestcompletes,thethen()methodisexecutedwiththecontentofthetemplateasastringparameter,whichisusedtogeneratethecompiledtemplatefunction.OurimplementationfinallyreturnsthecompiledtemplatefunctionastheresultofthechainedPromise,rightafteraddingittoitscache.SincethegetAjaxTemplate()functionisasynchronous,wealsohadtochangetheimplementationoftheopenNew()methodandmoveallthecodeusingthereturnedtemplatefunctioninsideathen()callback.Apartfromthis,theimplementationhasremainedthesameandusesthetemplatefunctioninexactlythesamewayasbefore.
dashboard.informationBox.openNew=function(itemName){
vartemplatePromise=getAjaxTemplate('box-template');
templatePromise.then(function(boxCompiledTemplate){
varboxHtml=boxCompiledTemplate({
itemName:itemName
});
var$box=$(boxHtml).appendTo($boxContainer);box);
/*...*/
});
};
Whenre-implementingthegetAjaxTemplate()functiontouseHandlebars.js,theresultingcodeismostlythesameasbefore.TheonlydifferenceisintheinvocationoftheHandlebars.compile()methodinsteadoftheUndescore.jsequivalent.Thisisanaddedbenefitasmanyclient-sidetemplatingenginesinfluencedeachotherandhaveconvergedintoaverysimilarAPIregardingthewaythattheirtemplatefunctionsareused,largelybecauseofthepositiveuserfeedbackontheexistingimplementations.
functiongetAjaxTemplate(templateName){
/*…sameasbefore…*/
return$.ajax({/*…sameasbefore…*/}).then(function(template){
templateCache[templateName]=Handlebars.compile(template);
returntemplateCache[templateName];
});
}
NoteKeepinmindthatthe$.ajax()methodmightnotworkinsomebrowserswhenthepageisloadedthroughthefilesystem,butworksasintendedwhenservedusingawebserverlikeApache,IIS,ornginx.
ModerationisbestinallthingsEventhoughthistechniquereducestheoveralldownloadfootprintofeachwebpage,italsoinevitablyincreasesthenumberofHTTPrequestsmade.Moreover,thepracticeofloadingeverytemplatelazilycansometimesincreasethetimethattheuserwillhavetowaitifthetemplatesarerequiredfortheinitialrenderingofthepage.
Balancingthewaythatweloadourtemplatesbetweenlazyloadingandembeddingthemin<script>tagsusuallybringsthebestofbothworlds.Thishybridapproachisconsideredabestpracticebytheindustrysinceitallowsustomicromanageandfinetuneeachimplementationbasedonitsneeds.Accordingtothispractice,thetemplatesthatarerequiredforthepresentationofthemaincontentofapageareembeddedinitsHTML,whiletherestofthemaredeliveredlazilywhenneeded,takingadvantageofbrowsercaching.
Theimplementationofsuchatemplateproviderfunctionisleftasanexerciseforthereader.Asahint,suchmethodshavetobeasynchronoussince,whentherequestedtemplateisnotfoundembeddedinthe<script>tagofthepage,itwillhavetoproceedandmakeanAJAXrequesttoretrieveitfromtheserver.
TipKeepinmindthatitisgenerallypreferabletogeneratethecompleteinitialHTMLcontentofthepageontheserversideinsteadofusingclient-sidetemplating.ThisnotonlyleadstoasmallerloadingtimeoftheinitialpagecontentbutitalsopreventssituationsinwhichtheuserispresentedwithanemptypagewhenJavaScriptisunavailableoranerrorhasoccurred.
SummaryInthischapter,welearnedhowtousetwoofthemostcommonclient-sidetemplatinglibraries:Underscore.jsandHandlebars.js.WealsolearnedhowtheyallowustocreatecomplexHTMLtemplatesfasterwhilemakingourimplementationseasiertoreadandunderstand.Wethenwentontoanalyzetheirconventionsandevaluatetheirfeaturesandlearnedbyexamplehowtheycanbeeffectivelyandefficientlyusedinourimplementations.
Aftercompletingthischapter,wearenowabletogeneratecomplexHTMLstructuresinabrowserefficientlybyusingreadabletemplatesandutilizingtheuniquecharacteristicsofthetemplatinglibraries.
Inthenextchapter,wewilllearnhowtocreatejQueryPluginsasawaytoabstractpartsofourapplicationsintoreusableandextensibleimplementations.WewillintroducethemostwidelyusedpatternsfordevelopingjQueryPluginsandanalyzetheimplementationproblemsthateachofthemhelpstosolve.
Chapter10.PluginandWidgetDevelopmentPatternsThischapterfocusesonthedesignpatternsandbestpracticesusedwhenimplementingjQueryPlugins.WewilllearnherehowtoabstractpartsofanapplicationintoseparatejQueryPlugins,promotingtheSeparationofConcernsprincipleandcodereusability.
WewillfirstlyanalyzethesimplestwaysthatajQueryPlugincanbeimplemented,learnthevariousconventionsofjQueryPlugindevelopmentandthebasiccharacteristicsthateverypluginshouldsatisfyinordertofollowjQueryprinciples.Wewillthenproceedwithanintroductiontothemostwidelyuseddesignpatternsandanalyzethecharacteristicsandbenefitsofeachofthem.Bytheendofthischapter,wewillbeabletoimplementextensiblejQueryPluginsusingthedevelopmentpatternthatbestsuitseachusecase.
Inthischapterwewill:
IntroducethejQueryPluginAPIanditsconventionsAnalyzethecharacteristicsthatmakeanexcellentpluginLearnhowtocreateapluginbyextendingthe$.fnobjectLearnhowtoimplementgenericpluginsthatareextensibleinordertomakethemreusableinmoreusecasesLearnhowtoprovideoptionsandmethodstoyourpluginsIntroducethemostcommondesignpatternsforjQueryplugindevelopmentandanalyzethecommonimplementationproblemsthateachofthemhelpstosolve
IntroducingjQueryPluginsThekeyconceptofjQuerypluginsliesinextendingthejQueryAPIbymakingtheirfunctionalityaccessibleasamethodonjQueryCompositeCollectionObjects.AjQuerypluginissimplyafunctionthatisdefinedasanewmethodonthe$.fnobject,whichisthePrototypeObjectthateveryjQueryCollectionObjectinheritsfrom.
$.fn.simplePlugin101=function(arg1,arg2/*,...*/){
//Plugin'simplementation…
};
Bydefiningamethodonthe$.fnobject,weareactuallyextendingthecorejQueryAPIitself,sincethismakesthemethodavailableonallcreatedjQueryCollectionObjectsfromthatpointonwards.Asaresult,afterapluginhasbeenloadedinawebpage,itsfunctionalityisavailableasamethodoneveryobjectreturnedbythe$()function:
$('h1').simplePlugin101('test',1);
ThemainconventionofthejQuerypluginAPIisthatthejQueryCollectionObjectthatthepluginwasinvokedonismadeavailabletotheplugin’smethodasitsexecutioncontext.Inotherwords,wecanusethethisidentifierinthepluginmethod,asshownbelow:
$.fn.simplePlugin101=function(){
this.slideToggle();
//"this"isajQueryobjectwhereall
//jQuerymethodsareavailable
};
FollowingjQueryprinciplesOneofthegoalswhencreatingapluginistomakeitfeellikeapartofjQueryitself.Afterreadingthepreviouschapters,youshouldbefamiliarwithsomeoftheprinciplesthatalljQuerymethodsfollowandthecharacteristicsthatmakeitsapproachspecial.ImplementingapluginthatfollowstheseprinciplesmakesusersfeelmorecomfortablewithitsAPI,bemoreproductive,andmakefewerimplementationerrors,whichleadstoanincreaseintheplugin’spopularityandadoption.
TwoofthemostimportantcharacteristicsthatagreatjQuerypluginshouldhaveareasfollows:
ItshouldapplyonalltheelementsofthejQueryCollectionObjectitisinvokedonwheneverapplicableItshouldallowfurtherchainingofotherjQuerymethods
Let’snowmoveonandanalyzeeachoftheseprinciples.
WorkingonCompositeCollectionObjectsOneofthemostimportantfeaturesofjQuerymethodsisthattheyareappliedoneveryitemoftheCompositeCollectionObjectthattheyareinvokedon.Asanexample,the$.fn.addClass()methodaddsoneormoreCSSclassestoeveryitemofthecollectionafterindividuallycheckingwhethereachclasshasalreadybeendefinedoneachindividualelement.
Asaresult,ourjQuerypluginsshouldalsofollowthisprinciplebyoperatingoneveryelementofacollection,whensuchathingseemslogical.IfyouareusingonlyjQuerymethodsinyourplugin’simplementation,mostofthetime,yougetthisforfree.Ontheotherhand,animportantconsiderationtobearinmindisthatnotalljQuerymethodsoperateoneveryelementofacollectionobject.Methodslike$.fn.html(),$.fn.css()and$.fn.data()applyonalltheitemsofthecollectionwhenusedassettermethods,butoperateonlyonthefirstelementwhenusedasgetters.
Let’sseeanexampleimplementationofapluginthatuses$.fn.animate()tocreateashakeeffectonallitemsofajQueryobject:
$.fn.vibrate=function(){
this.each(function(i,element){
//specificallyhandleeveryelement
var$element=$(element);
if($element.css('position')==='static'){
$element.css({position:'relative'});
}
});
this.animate({left:'+=3'},30)
.animate({left:'-=6'},60)
.animate({left:'+=6'},60)
.animate({left:'-=3'},30);
returnthis;//allowfurtherchaining
};
Invokingthispluginwith$('button').vibrate();appliestheshakinganimationoneverymatchedelementofthepage.Toachievethat,thepluginchangestheleftCSSpropertyofallmatchedelementsusingthe$.fn.animate()method,whichconvenientlyoperatesoneveryelement.Ontheotherhand,sincethe$.fn.css()methodappliesonlyonthefirstelementofthecollectionwhenusedasagetter,wehadtoiterateoveralltheelementsusingthe$.fn.each()methodandensurethateachofthemwasnotstaticallypositioned,inwhichcasetheleftCSSpropertywouldnotaffectitsappearance.
Obviously,usingonlyjQuerymethodsisnotalwayssufficientfortheimplementationofaplugin.Inmostcases,anewpluginwillhavetouseatleastonenon-jQueryAPIforitsimplementation,requiringustoiterateovertheitemsofthecollectionandapplythelogicoftheplugintoeachofthemindividually.Thesameapproachshouldbeusedwheneachelementofthecollectionhastobehandledslightlydifferentlybasedonitsstate.
Asaresult,itisquitecommonforpluginstowrapalmostalloftheirimplementationsinsidea$.fn.each()invocation.Byrecognizingthecommonneedsthatarecoveredbyexplicititeration,thejQueryteamandmostjQuerypluginboilerplatesnowmakeitpartoftheirstandardpractice.
AllowingfurtherchainingIngeneral,whenyourplugin’scodedoesnotneedtoreturnanything,allthatyouhavetodotoenablefurtherchainingistoaddareturnthis;statementtoitslastline,aswesawinthepreviousexample.Makesurethatallthecodepathsreturnareferenceoftheinvocationcontext(this)oranotherrelevantjQuerycollectionobject,inthesamewaythat$.fn.parent()and$.fn.find()do.Alternatively,whenallyourcodeiswrappedinsideanotherjQuerymethod,suchas$.fn.each(),itiscommonpracticetosimplyreturntheresultofthatinvocation,asdemonstratedbelow:
$.fn.myLogPlugin=function(){
returnthis.each(function(i,element){
console.log($(element).text());
});
};
Keepinmindthat,ifyourcodemanipulatesthecollectionobjectthatitwasinvokedon,insteadofreturningthethisreference,youmightneedtoreturnthenewcollectionthatwastheresultofyourplugin’smanipulations.
NoteYoushouldavoidbasingyourplugin’simplementationonareturnvalueinordertoallowfurtherchaining.Insteadofdoingthat,itispreferabletoinitializethepluginonitsfirstinvocationandthenprovidesomeoverloadedwaystoinvokeit,asawayofreturningvalues.
Workingwith$.noConflict()Thefirststeptoimproveaplugin’simplementationistomakeitworkinenvironmentsthatdonothaveaccesstothe$identifier.AnexampleofthisiswhenawebpageusesthejQuery.noConflict()method,whichpreventsjQueryfromassigningitselftothe$globalidentifier(orwindow.$)andkeepsitavailableonlyonthejQuerynamespace(window.jQuery).
NoteThejQuery.noConflict()methodallowsustopreventjQueryfromconflictingwithotherlibrariesandimplementationsthatalsohappentousethe$variable.Formoreinformation,youcanvisitthejQuerydocumentationpageat:http://api.jquery.com/jQuery.noConflict/
Insuchcases,theplugindefinitionwouldthrowan$isnotdefinederrororevenworse;itmighttrytousethe$variablethatthedeveloperhasreservedtouseinanimplementation,leadingtoerrorsthatarehardtodebug.
Fortunately,thechangesrequiredtofixthisproblemareeasytoimplementanddonotaffectthefunctionalityoftheplugin.Allthatwehavetodoisrenamealloftheoccurrencesofthe$identifierinourpluginwithjQuery,asshownbelow:
jQuery.fn.simplePlugin101=function(arg1,arg2/*,...*/){
var$buttons=jQuery('button');
//...
};
WrappingwithanIIFEThenextbestpracticetofollowistowrapthedefinitionandimplementationofourpluginwithanIIFE.ThisnotonlymakesourpluginlookliketheModulePatternbutalsomakesourimplementationmorerobustbyaddingseveralotherbenefitstoit.
Firstofall,theIIFEpatternallowsustocreateanduseprivatevariablesandfunctionsinthecontextoftheplugin’sdefinition.Thesevariablesaresharedacrossalltheinstancesoftheplugininasimilarwaytohowstaticvariablesworkinotherprogramminglanguages,enablingustousethemassynchronizationpointsbetweentheplugininstances:
(function($){
varcallCounter=0;
functionutilityLogMethod(message){
if(window.console&&console.log){
console.log(message);
}
}
$.fn.simplePlugin101=function(arg1,arg2/*,...*/){
callCounter++;
utilityLogMethod(callCounter);
returnthis;
};
})(jQuery);
Otherwise,wewouldhavetousesomethinglike$.simplePlugin101._callCounteror$.simplePlugin101._utilityLogMethod()toemulateprivacy,whichisjustanamingconventionanddoesnotprovideanyactualprivacy.
Thesecondbenefit,asdemonstratedintheaboveexample,isthatitallowsustousethe$identifieragaintoaccessjQuerywithnoconcernsaboutconflicts.Inordertoachievethis,wearepassingthejQuerynamespacevariableasaninvocationparametertoourIIFEandusethe$identifiertonametherespectiveparameter.Inthisway,weeffectivelyaliasthejQuerynamespaceto$inthecontextcreatedbytheIIFE,enablingustousetheminimal$identifierinourimplementationtokeepourcodeslimandreadable,evenifjQuery.noConflict()isused.
Additionally,addingtheusestrict;statementonthetopofourIIFEhelpsustoeliminateanyleakingofvariablesintotheglobalnamespace.Forexample,thefollowingcodewouldthrowaReferenceError:assignmenttoundeclaredvariablexerrorduringtheinvocationoftheplugin’smethod,enablingustocatchthoseerrorsduringthedevelopmentphaseofthepluginhelpingproduceamorerobustfinalimplementation:
(function($){
'usestrict';
$.fn.leakingPlugin=function(){
x=0;//thereisno"varx"declaration,
//soanerroristhrownwhenexecuted
};
})(jQuery);
$('div').leakingPlugin();
NoteFormoreinformationaboutJavaScript’sstrictexecutionmode,youcanvisit:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode
Finally,thispattern,aswithallthenamespacealiasingpracticesthatuseIIFEs,canalsohelpincreasethegainswhenminifyingyourplugin’ssourcecode,whencomparedtoanimplementationthatreferencesthejQuerynamespacevariabledirectly.Inanattempttomaximizethebenefitsofthistechnique,it’salsocommontoaliasalltheglobalnamespacevariablesthatourpluginaccesses,asdemonstratedbelow:
(function($,window,document,undefined){
//Plugin'simplementation…
})(jQuery,window,document);
CreatingreusablepluginsAfteranalyzingthemostimportantaspectsofthedevelopmentofjQueryplugins,wearenowreadytoanalyzeanimplementationthatisusedforsomethingmorethanasimpledemonstration.Inordertocreateareallyusefulandreusableplugin,itmustbedesignedsuchthatitsoperationsarenotrestrictedbythedemandsofitsoriginalusecase.
Themostpopularplugins,likethemostusefuljQuerymethods,arethosethatprovideahighdegreeofconfigurationoftheirfunctionality.Creatingapluginthatisconfigurableaddsadegreeofflexibilitytoitsimplementation,whichenablesustomatchtheneedsofseveralotherusecasesthataregovernedbythesameoperationprinciples.
Aswesaidearlier,ajQuerypluginisjustafunctionattachedtothe$.fnobjectand,asaresult,wecanmakeitsimplementationmoreabstractandgenericinthesamewayaswithplainfunctionsofourmodules.Asinsimplefunctions,theeasiestwaytodifferentiatetheoperationofajQuerypluginisbyusinginvocationparameters.Apluginthatexposesalotofconfigurationparametershasgreatpotentialofbeingabletobematchtherequirementsofseveraldifferentusecases.
AcceptingconfigurationparametersIncontrasttohowweimplementfunctionsthatusuallyacceptuptofiveargumentsandstillhaveamanageableandrelativelycleanAPI,thispracticedoesnotworksowellwithjQueryplugins.InordertoexposeaclearAPIandmaintainahighlevelofusability,regardlessofthevariousconfigurationoptionsthatareexposed,mostjQuerypluginsprovideaminimalAPIthatacceptsuptothreeinvocationarguments.Thisisachievedbyusingdedicatedsettingobjectswithaspecificformat,asawayofencapsulatingmultipleoptionsandpassingthemasasingleargument.AnotherapproachistoexposeanAPIwithtwoparameters,wherethefirstoneisaregularvaluethatdefinestheoperationofthepluginandthesecondoneisusedtowrapthelessimportantconfigurationoptions.
Agreatexampleofbothofthesepracticesisthe$.ajax(settings)method,whichisinvokedwithasinglesettingsobjectasaparametertodefinehowitshouldoperate,butalsoexposesanotheroverloadedwaytobeinvokedwithtwoarguments.Thetwoargumentoverloadisinvokedwith$.ajax(url,settings),wherethefirstisthetargetURLfortheHTTPrequestandthesecondisanobjectwiththerestconfigurationoptions.Whatappliestobothofthemisthatthemethoditselfcontainsasetofsensibledefaultsthatareusedinsteadofanyconfigurationparameterthattheuserhasnotdefined.Moreover,thesecondoverloadalsodefinesthesecondparameterasoptionaland,ifthatwasnotprovidedduringitsinvocation,itbasesitsoperationonthedefaultsettings.
Adoptingthesettingsobjectpracticeinourpluginsnotonlybringsalltheaforementionedbenefits,butalsoallowsustoextendtheimplementationinamorescalableway,sincetheadditionofanextraconfigurationparameterhaslittleeffectontherestofitsAPI.Asanexampleofthis,wewillreimplementthe$.fn.vibratepluginthatwesawearlierinthischapterinamoregenericway,sothatasettingobjectwithdefaultvaluesisusedforitsconfiguration:
(function($){
$.fn.vibrate=function(options){
varopts=$.extend({},$.fn.vibrate.defaultOptions,options);
this.each(function(i,element){
var$element=$(element);
if($element.css('position')==='static'){
$element.css({position:'relative'});
}
});
for(vari=0,len=opts.loops*4;i<len;i++){
varanimationProperties={};
varmovement=(i%2)?'+=':'-=';
movement+=(i===0||i===len-1)?
opts.amplitude/2:
opts.amplitude;
vart=(i===0||i===len-1)?
opts.period/4:
opts.period/2;
animationProperties[opts.direction]=movement;
this.animate(animationProperties,t);
}
returnthis;
};
$.fn.vibrate.defaultOptions={
loops:2,
amplitude:8,
period:100,
direction:'left'
};
})(jQuery);
Incontrasttotheoriginalfixedimplementation,thisoneacceptsasingleobjectasaninvocationparameterwhichwrapsfourdifferentoptionsthatcanbeusedtodiversifytheoperationoftheplugin.Theoptionsobjectallowsustodiversifytheoperationofthepluginbyexposingfourcustomizationpoints:
ThenumberofloopsthattheshakeeffectshouldrunTheamplitudeoftheanimation,asameansofcontrollinghowmuchanelementshouldmoveawayfromitsoriginalpositionTheperiodofeachloop,asameansofcontrollinghowfastthemovementwillbeThedirectionoftheanimation,whichishorizontalwhenleftisusedorverticalwhentopisused
Byfollowingawidelyacceptedbestpractice,wehavedefinedallthedefaultvaluesfortheconfigurationoptionsasaseparateobject.Thispatternnotonlyallowsustogatheralltherelatedvaluesunderasingleobject,butalsoenablesustousethe$.extend()methodasaneffectivewayofcomposingallthedefinedoptionswiththedefaultvaluesoftheundefinedones.Wecanthusavoidcheckingexplicitlyfortheexistenceofeachindividualproperty,reducingthecomplexityandthesizeofourcode.
Inbrief,the$.extend()methodreturnstheobjectpassedasitsfirstargumentaftermergingthepropertiesofthesubsequentobjectstogetherintothefirstobject.Asaresult,thereturnedobjectwillcontainallthedefaultvaluesexceptthosethatweredefinedintheoptionsobjectthatwaspassedasaninvocationparameter.
NoteFormoreinformationaboutthe$.extend()helpermethod,youcanvisitthedocumentationpageat:http://api.jquery.com/jQuery.extend/
Moreover,insteadofusingasimplevariable,weareexposingthedefaultoptionsobjectasapropertyoftheplugin’sfunction,enablinguserstochangethemtobettersuittheirneeds.Asanexample,consideracaseinwhichasmoothanimationisrequiredfortheneedsofaspecificapplication.Bysetting$.fn.vibrate.defaultOptions.period=250,thedeveloperwouldcompletelyremovetheneedtospecifytheperiodoptionin
everyinvocationoftheplugin,whichwouldleadtoanimplementationwithlessrepetitivecode.
NoteThejQuerylibraryitselfadoptsthispracticefordefiningthedefaultconfigurationparametersofthe$.ajax()method.Becauseoftheincreasedcomplexityofthismethod,jQueryprovidesuswiththejQuery.ajaxSetup()methodasawayofsettingupthedefaultparametersforeveryAJAXrequest.
Finally,inordertocreateagenericvariantoftheoriginalimplementationandutilizetheaforementionedconfigurationoptions,wereplacedthefourfixedinvocationsofthe$.fn.animate()methodoftheoriginalimplementationwithaforloopthatutilizedtheloopsoption.Insidetheforloopitself,weconstructtheparametersforeachcallofthe$.fn.animate()methodandbrieflyalternatethedirectionoftheanimatedmovementoneachsubsequentexecutionoftheloop,andalsoensurethatthefirstandlastmovementshavehalfofthetimedurationandhalfoftheshiftofalloftheothersteps.
Thefinalimplementationcanbeconfiguredtoproducedifferentanimations,basedontheneedsofeachspecificusecase,rangingfromshorthorizontalanimationsthatareidealfornotifyingauseraboutaninvalidaction,toverticallonganimationsthatlooklikealevitationeffect.Theplugincanbeinvokedwithanycombinationoftheaforementionedoptions,usethedefaultvaluesformissingoptionsandevenoperatewithnoinvocationargument,asshownbelow:
//dothedefaultintenseanimationonabutton
//thatappearsdisabled,todesignateaninvalidaction
$('button.disabled').on('click',function(){
$(this).vibrate();
});
//doasmothershakeanimationtocatchtheuser's
//attentiononanimportantpartofthepage
$('.save-button').vibrate({loops:3,period:250});
//startalongrunninglevitationeffectontheheaderofthepage
$('h1').vibrate({direction:'top',loops:1000,period:5000});
WritingstatefuljQuerypluginsThepluginimplementationsthatwehavelookedatsofarwerestatelesssince,aftercompletingtheirexecution,theyreverttheirmanipulationsontheDOM’sstateanddon’tleaveallocatedobjectsinthebrowser’smemory.Asaresult,subsequentinvocationsofstatelesspluginsalwaysproducethesameresults.
Asyoucanprobablyguess,suchpluginshavelimitedapplicationssincetheycan’tbeusedtocreateaseriesofcomplexinteractionswiththeuserofthewebpage.Inordertoorchestratecomplexuserinteractions,apluginneedstopreserveaninternalstatewiththeactionstakenuptothatpointinordertochangeitsoperationmodeappropriatelyandhandlesubsequentinteractions.Comparingthecharacteristicofstatefulandstatelesspluginscouldbedefinedastheequivalenttocomparingplain(static)functionswithmethodsthatarepartofanobjectandcanoperateonitsstate.
Anotherpopularcategoryofplugins,inwhichhavinganinternalstateisessential,isthefamilyofpluginsthatmanipulatetheDOMtree.Thesepluginsusuallycreatecomplexelementstructuressuchasarichtexteditors,datepickersandcalendars,commonlybybuildingonauser-definedempty<div>element.
ImplementingastatefuljQueryPluginAsanexampleofthepatternsusedfortheimplementationofpluginsofthisfamily,wewillwriteagenericElementMutationObserverplugin.ThispluginwillprovideuswithaconvenientwayofaddingeventlistenersforchangestotheDOMtreethatoriginatefromanyoftheelementsthatthispluginwasinvokedon.Asawayofachievingthat,thefollowingimplementationusestheMutationObserverAPI,which,atthetimeofwriting,isimplementedbyallmodernbrowsersandisavailabletomorethan86%ofwebusers.
NoteFormoreinformationontheMutationObserver,youcanvisit:https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
Let’snowproceedwiththeimplementationandanalyzethepracticesthatwereused:
(function($){
$.fn.mutationObserver=function(action){
returnthis.each(function(i,element){
var$element=$(element);
varinstance=$element.data('plugin_mutationObserver');
if(!instance){
varobserver=newMutationObserver(function(mutations){
mutations.forEach(function(mutation){
instance.callbacks.forEach(function(callbackFn){
callbackFn(mutation);
});
});
});
observer.observe(element,{
attributes:true,
childList:true,
characterData:true
});
instance={
observer:observer,
callbacks:[]
};
$element.data('plugin_mutationObserver',instance);
}
if(typeofaction==='function'){
instance.callbacks.push(action);
}
});
};
})(jQuery);
Firstly,wedefineourplugininsideanIIFE,asrecommendedearlierinthischapter.Rightafterthedeclarationofthepluginonthe$.fnobject,weusethe$.fn.each()methodasa
directapproachtoensurethatthefunctionalityofourpluginisappliedtoeveryitemofthejQueryCollectionObjectthatitwasinvokedon.
Twoofthemainissuesthatstatefulpluginimplementationshaveisthelackofamechanismtopreservetheinternalstateofeachinstantiationofthepluginandawayofavoidingbeinginitializedmanytimesonthesamepageelement.Inordertosolvebothoftheseproblems,weneedtousesomethinglikeahashtableinwhichthekeyistheelementitselfandthevalueisanobjectwiththestateoftheplugin’sinstance.
Fortunately,thisismoreorlesshowthe$.fn.data()methodworksbyassociatingDOMelementsandJavaScriptobjectvaluesusingspecificstringkeys.Byusingthe$.fn.data()methodandtheplugin’snameasanassociationkey,weareabletostoreandretrievethestateobjectofourpluginveryeasily.
TipUsingthe$.fn.data()methodforthisusecaseisconsideredabestpracticeandisusedbymoststatefulpluginimplementationsandboilerplatessinceitisarobustpartofjQuerythatenablesustoreducethesizeofourplugin’simplementation.
Ifanexistingstateobjectisnotfoundthenwecanassumethatthepluginisnotyetinitializedonthatspecificelementandstartitsinitializationrightaway.ThestateobjectofthispluginwillcontaintheinstanceoftheactiveMutationObserverresponsiblefortrackingthechangesthathappenontheobservedDOMelement,andanarraywithallthecallbacksthathavesubscribedtoittogetnotificationsaboutchanges.
AftercreatinganewMutationObserverinstance,weconfigureittolookforthreespecifictypesofDOMchangesandinstructittoinvokeallthecallbacksoftheplugin’sstateobjectwheneversuchDOMchangesoccur.Finally,wecreatethestateobjectitselftoholdtheobserverandtheassociatedcallbacksandusethe$.fn.data()methodasasetterandassociateitwiththepageelement.
Afterensuringthatthepluginisinstantiatedandinitializedontheprovidedelement,wecheckwhetherthepluginisinvokedwithafunctionasaparameterand,ifso,weaddittothelistoftheplugin’scallbacks.
TipKeepinmindthatusingasingleMutationObserverinstanceperelementandhavingitnotifyaboutDOMchangesbyiteratingoveranarrayofcallbacksgreatlyreducesthememoryrequirementsoftheimplementation,justlikewhenweareusingasingledelegateobserver.
AnexampleofusingournewlyimplementedplugintoobserveforchangesofaspecificDOMelementwouldlooklikethis:
$('.container').mutationObserver(function(mutation){
console.log('SomethingchangedontheDOMtree!');
});
DestroyingaplugininstanceAnextraconsiderationthatastatefulpluginhastotakeintoaccountisofferingthedeveloperawaytoreversethechangesthatitintroducedtothestateofthepage.ThemostcommonandsimpleAPIforachievingthisistoinvokethepluginwiththedestroyliteralasitsfirstparameter.Let’sproceedwiththerequiredimplementationchanges:
(function($){
$.fn.mutationObserver=function(action){
returnthis.each(function(i,element){
var$element=$(element);
varinstance=$element.data('plugin_mutationObserver');
if(action==='destroy'&&instance){
instance.observer.disconnect();
instance.observer=null;
$element.removeData('plugin_mutationObserver');
return;
}
if(!instance){
/*...*/
}
});
};
})(jQuery);
Inordertoadaptourimplementationtotheaboverequirement,allwehadtodowastocheckwhetherthepluginwasinvokedwiththedestroystringvalueasitsfirstparameter,rightafterretrievingtheplugin’sstateobject.Ifwefindthatthepluginhasalreadybeeninstantiatedonthespecifiedelementandthatthedestroystringvaluehasbeenused,wecanproceedtostoptheMutationObserveritselfandcleartheassociationthat$.fn.data()createdbyusingthe$.fn.removeData()method.Finally,attheendoftheifstatementweaddedareturnstatementsince,aftercompletingthedestructionoftheplugininstance,wenolongerneedtoexecuteanyothercode.Anexampleofdestroyingaplugininstancewiththisimplementationwouldlooklikethis:
$('.container').mutationObserver('destroy');
ImplementinggetterandsettermethodsByusingthesametechniquethatwedemonstratedearlierfortheimplementationofthedestroymethodofourplugin,wecanprovideseveralotheroverloadedwaystoinvokeourpluginthatworklikenormalmethods.ThispatternisnotonlyusedbyplainjQueryplugins,butisalsoadoptedbymorecomplexpluginarchitectures,aswithjQuery-UI.
Ontheotherhand,wemightendupwithapluginimplementationthatresultsinalargenumberofinvocationoverloads,whichissomethingthatwouldmakeitdifficulttouseanddocument.AwaytoworkaroundthisistocombinethegetterandsettermethodsofyourAPIintomulti-purposemethods.ThisnotonlyreducestheAPIsurfaceofyourpluginsothatadeveloperhastorememberfewermethodnamesbutitalsoincreasestheproductivitysincethesamepatternisusedinmanyjQuerymethodslike$.fn.html(),$.fn.css(),$.fn.prop(),$.fn.val(),and$.fn.data().
Asademonstrationofthis,let’sseehowwecanaddanewmethodtoourMutationObserverpluginthatworksbothasagetterandasetterfortheregisteredcallbacks:
(function($){
$.fn.mutationObserver=function(action,callbackFn){
varresult=this;
this.each(function(i,element){
var$element=$(element);
varinstance=$element.data('plugin_mutationObserver');
/*...*/
if(typeofaction==='function'){
instance.callbacks.push(action);
}elseif(action==='callbacks'){
if(callbackFn&&callbackFn.length>=0){
//usedasasetter
instance.callbacks=callbackFn;
}else{
//usedasagetterforthefirstelement
result=instance.callbacks;
returnfalse;//breakthe$.fn.each()iteration
}
}
});
returnresult;
};
})(jQuery);
Asshownintheabovecode,wehavecreatedanoverloadedinvocationmethodwhichusesthecallbacksstringvalueasthefirstargumentoftheplugininvocation.ThisgetterandsettermethodallowsustoretrieveoroverwriteallofthecallbacksthatareregisteredontheMutationObserverandworksinadditiontothepre-existingmethodsforinvokingtheplugin,byusingafunctionparameterandthedestroymethod.
Thegetterandsetterimplementationisbasedontheassumptionthat,whentryingtousethecallbacksmethodasagetter,youdon’tneedtopassanyextraparametersand,whentryingtouseitasasetter,youwillpassanextraarrayasaninvocationparameter.Inordertosupportthegettervariant,whichpreventsfurtherchainingandonlyoperatesonthefirstelementofthecompositecollection,wehadtodeclareandusetheresultvariablewhichisinitializedtothevalueofthethisidentifier.Ifthecallbacksgetterisused,weassignthecallbacksofthefirstelementofthecollectiontotheresultvariableandbreakoutofthe$.fn.each()iterationbyreturningfalsetofinishtheexecutionoftheplugin’smethod.
Hereisanexampleusecaseforournewlyimplementedgetterandsettermethod:
//retrievethecallbacks
varoldCallbacks=$('.container').mutationObserver('callbacks');
//clearthem
$('.container').mutationObserver('callbacks',[]);
//addanewone
$('.container').mutationObserver(function(){
console.log('Printedonlyonce');
//restoretheoldcallbacks
$('.container').mutationObserver('callbacks',oldCallbacks);
});
TipKeepinmindthatinvocationoverloadsthatpreventfurtherchainingbyreturningnon-jQueryobjectresultsshouldbewelldocumentedsincethistechniqueconflictswiththechainingprinciplethateveryoneexpectstowork.
UsingourplugininourDashboardapplicationAftercompletingourmutationObserverplugin,letsnowseehowwecanuseitfortheimplementationofthecountersub-modulethatweusedinourDashboard’simplementationinpreviouschapters:
(function(){
'usestrict';
dashboard.counter=dashboard.counter||{};
var$counter;
dashboard.counter.init=function(){
$counter=$('#dashboardItemCounter');
var$boxContainer=dashboard.$container
.find('.boxContainer');
$boxContainer.mutationObserver(function(mutation){
dashboard.counter.setValue($boxContainer.children().length);
});
};
dashboard.counter.setValue=function(value){
$counter.text(value);
};
})();
Asyoucanseeintheaboveimplementation,ourpluginabstractsnicelyandreplacestheoldimplementationbyprovidingageneric,flexibleandreusableAPI.Insteadoflisteningforclickeventsonthedifferentbuttonsofthepage,theimplementationisnowusingthemutationObserverpluginandobservestheboxContainerelementfortheadditionsorremovalsofchildelements.Moreover,thisimplementationchangedoesnotaffectthefunctionalityofthecountermodulewhichappearstoworkinthesamewaysinceallthechangesareencapsulatedinthemodule.
UsingthejQueryPluginBoilerplateThejQueryBoilerplateproject,whichisavailableathttps://github.com/jquery-boilerplate/jquery-patterns,offersseveraltemplatesthatcanbeusedasstartingpointsfortheimplementationofrobustandextensibleplugins.Thesetemplatesincorporatealotofbestpracticesanddesignpatternssuchasthoseanalyzedearlierinthischapter.Eachofthetemplatespacksanumberofbestpracticesthatworkwelltogether,inanattempttoprovidegoodstartingpointsthatbettermatchthevarioususecases.
Perhapsthemostwidelyusedtemplateisjquery.basic.plugin-boilerplatefromAdamSontagandAddyOsmani,whicheventhoughitischaracterizedasagenerictemplateforbeginnersandabove,successfullycoversmostaspectsofjQueryplugindevelopment.WhatmakesthistemplateuniqueistheObject-Orientedapproachthatitfollowswhichispresentedinsuchawaythatithelpsyouwritebetterstructuredcode,withoutmakingithardertointroducecustomizationsontheimplementation.Let’sproceedandanalyzeitssourcecode:
/*!
*jQuerylightweightpluginboilerplate
*Originalauthor:@ajpiano
*Furtherchanges,comments:@addyosmani
*LicensedundertheMITlicense
*/
;(function($,window,document,undefined){
varpluginName="defaultPluginName",
defaults={
propertyName:"value"
};
functionPlugin(element,options){
this.element=element;
this.options=$.extend({},defaults,options);
this._defaults=defaults;
this._name=pluginName;
this.init();
}
Plugin.prototype={
init:function(){/*Placeinitializationlogichere*/},
yourOtherFunction:function(options){/*somelogic*/}
};
//Areallylightweightpluginwrapperaroundtheconstructor,
//preventingagainstmultipleinstantiations
$.fn[pluginName]=function(options){
returnthis.each(function(){
if(!$.data(this,"plugin_"+pluginName)){
$.data(this,"plugin_"+pluginName,
newPlugin(this,options));
}
});
};
})(jQuery,window,document);
Thesemi-colonrightbeforetheIIFEistheretoavoiderrorsincaseofunfortunatescriptconcatenation(andpossiblyminification)withafilethatmightbemissinganendingsemi-colon.Rightbelow,theboilerplateusesthepluginNamevariableasaDRYwayofnamingourpluginandusingitsnameforanyothercase.Asanaddedbenefit,allthatwehavetodoifweneedtorenameourpluginischangethevalueofthisvariableandrenamethe.jsfileofourpluginaccordingly.
Followingthebestpracticesthatwesawearlier,avariableisusedtoholdthedefaultoptionsofthepluginand,aswecanseeafewlineslater,itismergedwiththeuser-providedoptionsusingthe$.extend()method.Keepinmindthat,ifwewanttoexposethedefaultoptions,allthatwehavetodoisdefineitaspartoftheplugin’snamespace:$.fn[pluginName].defaultOptions=defaults;
Theactualplugindefinitioncanbefoundneartheendofthisboilerplatecode.Followingthealreadydiscussedbestpractices,ititeratesovertheitemsofthecollectionusing$.fn.each()andreturnsitsresult,whichisequivalenttoreturningthis.Itthenensuresthatapluginstateinstanceexistsforeachitemofthecollectionbyusingthe$.data()methodandtheprefixedpluginnameasanassociationkey.
ThePluginconstructorfunctionisusedforthecreationoftheplugin’sstateobjectwhich,afterstoringtheDOMelementandthefinalpluginoptionsaspropertiesoftheobject,invokestheinit()methodofitsprototype.Theinit()methodisthesuggestedplacetodefineourinitializationcode,forexample,itcouldinstantiateanewMutationObserveraswedidearlierinthischapter.
AddingmethodstoyourpluginBydefault,everymethodthatisdefinedaspartoftheprototypeisonlyavailableforinternaluse.Ontheotherhand,wecaneasilyextendtheaboveimplementationtomakeamethodavailabletoallourusers,asshownbelow:
$.fn[pluginName]=function(options,extraParam){
returnthis.each(function(){
varinstance=$.data(this,"plugin_"+pluginName);
if(!instance){
instance=newPlugin(this,options);
$.data(this,"plugin_"+pluginName,instance);
}elseif(options==='yourOtherFunction'){
instance.yourOtherFunction(this,extraParam);
}
});
};
OneguidelinetofollowwhenworkingwiththisboilerplateistoextendyourpluginbyaddingextramethodstothePlugin‘sprototype.Additionally,trytokeepanymodificationstotheplugin’sdefinitionassmallaspossible,ideallysinglelinemethodinvocations.
Inordertomaketheimplementationmorescalable,withregardstohowthepluginmethodsareinvokedandifwewanttoaddanabstractapproachformethodsthatareintendedforinternalorprivateusebytheplugin,wecanintroducethefollowingchanges:
$.fn[pluginName]=function(options){
varrestArgs=Array.prototype.slice.call(arguments,1);
returnthis.each(function(){
varinstance=$.data(this,"plugin_"+pluginName);
if(!instance){
instance=newPlugin(this,options);
$.data(this,"plugin_"+pluginName,instance);
}elseif(typeofoptions==='string'&&//methodname
options[0]!=='_'&&//protectprivatemethods
typeofinstance[options]==='function'){
instance[options].apply(instance,restArgs);
}
});
};
Intheaboveimplementation,weusedthefirstargumenttoidentifythemethodthatneedstobeinvokedandtheninvokeditwiththerestarguments.Wealsoaddedachecktopreventtheinvocationofmethodsthatstartwithanunderscorewhich,accordingtocommonconventions,areintendedtobeforinternalorprivateuse.Asaresult,inordertoaddanextramethodtoyourplugin’spublicAPI,wejustneedtodeclareitinthePlugin.prototypethatwesawearlier.
NoteAnothergreatwaytoimplementyourpluginwhenyouarealreadyusingjQuery-UIinyourapplicationistousethe$.widget()methodwhichisalsoknownasjQuery-UI
WidgetFactory.Itsimplementationabstractsseveralpartsoftheboilerplatecodethatwesawinthischapterandhelpscreatecomplexandrobustplugins.Formoreinformation,youcanreadthedocumentationat:http://api.jqueryui.com/jQuery.widget/
ChoosinganameLastly,afterlearningthebestpracticesthatweneedtocreateajQueryplugin,let’ssaysomethingaboutthenamingconventionsandwheretopublishyournewandshinyplugin.
Asyouhaveprobablyalreadyseen,mostjQuerypluginsusethefollowingnamingconvention:jQuery-myPluginNamefortheirprojectsitesandrepositoriesandstoretheirimplementationsinafilenamedjquery.mypluginname.js.Aftersettlingonsomeprospectivenamesforyourplugin,takeamomentandsearchthewebtoverifythatthereisnooneelsewiththesameprojectname.ThejQuerydocumentationsuggestssearchingforpluginsonNPMandrefiningyourresultsbyusingthejquery-pluginkeyword.Thisisobviouslythebestwaytopublishyourpluginsothatitcanbeeasilyfoundbyothers.
NoteFormoreinformationaboutNPM,youcanvisit:https://www.npmjs.com/
AnotherpopularplaceforsearchingandhostingJavaScriptlibrariesisGitHub.Youcanfinditsrepositorysearchpageathttps://github.com/search?l=JavaScript,whereitfiltersthesearchresultstoincludeonlyJavaScriptprojectsandsearchesforexistingpluginsandalreadyusedprojectnames.SinceinourcasewearefocusingonjQueryplugins,youwillgetbetterresultsbysearchingforprojectnamesthatfollowtheaforementionednamingconvention,jQuery-myPluginName.
NoteUntilrecently,developerscouldsearchforexistingpluginsandregisteranewoneattheofficialjQueryPluginRegistry(http://plugins.jquery.com/).Unfortunately,ithasbeendiscontinuedandnowonlyallowssearchingforolderpluginswithnonewsubmissions.
SummaryInthischapterwelearnedhowjQuerycanbeextendedbyimplementingandusingplugins.WefirstsawanexampleofthesimplestwaythatajQueryplugincanbeimplementedandanalyzedthecharacteristicsthatmakeagreatplugin,andonewhichfollowstheprinciplesofthejQuerylibrary.
WewerethenintroducedtothemostcommondevelopmentpatternsinthedevelopercommunityforcreatingjQueryPlugins.Weanalyzedtheimplementationproblemsthateachofthemsolvesandtheusecasesthatareabettermatchforthem.
Aftercompletingthischapter,wearenowabletoabstractpartsofourapplicationsintoreusableandextensiblejQuerypluginsthatarestructuredusingthedevelopmentpatternthatbestmatcheseachusecase.
Inthenextchapter,wewillpresentseveraloptimizationtechniquesthatcanbeusedtoimprovetheperformanceofourjQueryapplications,especiallywhentheybecomelargeandcomplex.WewilldiscusssimplepracticessuchasusingCDNstoloadthird-partylibrariesandcontinuewithmoreadvancedsubjectssuchaslazyloadingthemodulesofanimplementation.
Chapter11.OptimizationPatternsThischapterpresentsseveraloptimizationtechniquesthatcanbeusedtoimprovetheperformanceofjQueryapplications,especiallywhentheybecomelargeandcomplex.
WewillstartwithsimplepracticeslikebundlingandminifyingourJavaScriptfilesanddiscussthebenefitsofusingCDNstoloadthird-partylibraries.WewillthenmoveontoanalyzesomesimplepatternsforwritingefficientJavaScriptcodeandlearnhowtowriteefficientCSSselectorsinordertoimprovethepage’srenderingspeedandDOMtraversalsusingjQuery.
WewillthenstudyjQuery-specificpracticessuchasthecachingofjQueryCompositeCollectionObjects,howtominimizeDOMmanipulations,andhaveareminderoftheDelegateObserverPatternasagoodexampleoftheFlyweightPattern.Lastly,wewillgetanintroductiontotheadvancedtechniqueofLazyLoadingandhaveademonstrationofhowtoloadthedifferentmodulesofanimplementationprogressively,basedonuseractions.
Bytheendofthischapter,wewillbeabletoapplythemostcommonoptimizationpatternstoourimplementationsandusethischapterasachecklistofbestpracticesandperformancetipsbeforemovingtheapplicationtoaproductionenvironment.
Inthischapter,weshall:
LearnthebenefitsofbundlingandminifyingourJavaScriptfilesLearnhowtoloadthird-partylibrariesthroughtheCDNserverLearnsomesimpleJavaScriptperformancetipsLearnhowtooptimizeourjQuerycodeIntroducetheFlyweightpatternandshowcasesomeexamplesofitLearnhowtolazyloadpartsofourapplicationwhenrequiredbyauseraction
PlacingscriptsneartheendofthepageThefirsttipformakingyourpage’sinitialrenderingfasteristogatheralltherequiredJavaScriptfilesandplacetheir<script>tagsneartheendofthepage,preferablyjustbeforetheclosing</body>tag.Thischangewillhaveagreatimpactonthetimeneededfortheinitialrenderingofthepage,especiallyforuserswithlowspeedconnectionssuchasmobileusers.Ifyouarealreadyusingthe$(document).ready()methodforallinitializationpurposesthatrelatetotheDOM,movingthe<script>tagsaroundshouldnotaffectthefunctionalityofyourimplementationatall.
Themainreasonforthisisthat,eventhoughbrowsersdownloadthepage’sHTMLandotherresources(CSS,images,andsoon)inparallel,whena<script>tagisencountered,thebrowserpauseseverythingelseuntilitisdownloadedandexecuted.Inordertoworkaroundthislimitationofthespecification,attributeslikedeferandasyncfromHTLM5havebeenintroducedaspartsofthe<script>tagspecificationbutunfortunatelyhaveonlystartedtobeadoptedbysomebrowsersrecently.Asaresult,thispracticeisstillwidelyusedtoobtaingoodpageloadingspeedsevenonolderbrowsers.
NoteFormoreinformationaboutthe<script>tagyoucanvisit:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
BundlingandminifyingresourcesThefirstplacetolookwhentryingtomakeapageloadfasterisforwaystoreducethenumberandtotalsizeofHTTPrequests.Thebenefitscomefromthefactthatthebrowserdownloadsthecontentinlargerchunksinsteadofspendingtimewaitingforalotofsmallround-tripstotheservertocomplete.Thisisespeciallybeneficialforuserswithlowspeedconnectionssuchasmobileusers.
Resourceconcatenationisasimpleconceptthatdoesnotneedanyintroduction.Thiscanbedonemanuallybutitispreferabletoautomatethistaskwithabundlingscriptorintroduceabuildstepforyourproject.Dependingonyourdevelopmentenvironment,therearedifferentbundlingsolutionstochoosefrom.Ifyouareusinggruntorgulpaspartofyourdevelopmentstack,youcanusesolutionslikegrunt-contrib-concat(https://github.com/gruntjs/grunt-contrib-concat)andgulp-concat(https://github.com/contra/gulp-concat)respectively.
MinifyingJavaScriptfilesisamorecomplexprocedurewhichincludesaseriesofcodetransformationsthatareappliedtothetargetsourcecode,rangingfromsomethingassimpleaswhitespaceremovaltomorecomplextaskslikevariablerenaming.PopularsolutionsforminifyingJavaScriptinclude:
YUICompressoravailableathttp://yui.github.io/yuicompressor/Google’sClosureCompileravailableathttps://developers.google.com/closure/compiler/UglifyJSavailableathttps://github.com/mishoo/UglifyJS2
Onceagain,varioussolutionsexistthatintegratetheabovelibrariesnicelywithyourpreferreddevelopmentenvironmentandmakeminificationasimpletask.Examplesofintegrationsforgruntandgulpincludegrunt-contrib-uglify(https://github.com/gruntjs/grunt-contrib-uglify)andgulp-uglify(https://github.com/terinjokes/gulp-uglify)respectively.
Asafinalword,keepinmindthatyourcodeshouldbeasreadableandaslogicallystructuredaspossible.BundlingandminifyingyourJavaScriptandCSSfilesismosteffectivelydoneasabuildstepofyourdevelopmentanddeploymentprocedures.
UsingIIFEparametersApartfromhelpingtoavoidpollutingtheglobalnamespace,usingIIFEstowrapyourimplementationcanalsobebeneficialforthesizeofyourminifiedJavaScriptfiles.Let’stakealookatthefollowingcodeinwhichthejQuery,thewindow,andthedocumentvariablesarepassedasinvocationparameterstothemodule’sIIFE.
(function($,window,document,undefined){
if(window.myModule===undefined){
window.myModule={};
}
myModule.init=function(){/*...*/};
$(document).ready(myModule.init);
})(jQuery,window,document);
Wesawasimilarpatterninthepreviouschapter,aspartofthesuggestedtemplateforcreatingjQueryplugins.Eventhoughthevariablealiasingdoesnotaffectthefunctionalityoftheimplementation,itallowsthecodeminifierstoapplyvariablerenaminginmoreplacesthanbefore,resultingincodelikethefollowing:
(function(b,a,c,d){
a.myModule===d&&(a.myModule={});
myModule.init=function(){/*...*/};
b(c).ready(myModule.init);
})(jQuery,window,document);
Asyoucanseeintheabovecode,alltheinvocationparametersoftheIIFEwererenamedbytheminifiertosingleletteridentifiers,whichincreasesthegainsoftheminificationespeciallyiftheoriginalidentifiersareusedinseveralplaces.
TipAsanaddedbenefit,aliasingalsoprotectsourmodulesfromthecasethattheoriginalvariablesgetaccidentallyassignedadifferentvalue.Forexample,whenIIFEparametersarenotused,anassignmentlike$={}orundefined=7fromwithinadifferentmodulewouldbreakalltheimplementation.
UsingCDNsInsteadofservingalloftheJavaScriptandCSSfilesofthethird-partylibrariesfromyourwebserver,youshouldconsiderusingaContentDeliveryNetwork(CDN).UsingaCDNtoservethestaticfilesofthelibrariesthatareusedbyyourwebsitecanmakeitloadfastersince:
CDNshavehighspeedconnectionsandseveralcachinglevels.CDNshavemanygeographicallydistributedserversthatcandelivertherequestedfilesfastersincetheyareclosertotheenduser.CDNshelpparallelizeresourcerequests,sincemostbrowserscanonlydownloaduptofourresourcesconcurrentlyfromanyspecificdomain.
Moreover,ifauserhasstaticresourcescachedfromapreviousvisittoanotherwebsitethatusesthesameCDN,heorshewillnothavetodownloadthemagain,reducingthetimethatyoursiteneedstoload.
BelowisalistwiththemostwidelyusedCDNsforJavaScriptlibrarieswhichyoucanuseinyourimplementations:
https://code.jquery.com/https://developers.google.com/speed/libraries/https://cdnjs.com/http://www.jsdelivr.com/
UsingJSDelivrAPIAnewcomertotheCDNworldisJSDelivr,whichisgainingpopularitybecauseofitsuniquefeatures.Beyondsimplyservingexistingstaticfiles,JSDelivrprovidesanAPI(https://github.com/jsdelivr/api)thatallowsustocreateandusecustombundleswiththeresourcesthatweneedtoload,helpingustominimizetheHTTPrequeststhatoursiteneeds.Moreover,itsAPIallowsustotargetlibrarieswithdifferentlevelsofspecificity(major,minor,orbugfixreleases)andevenallowsustoloadonlyspecificpartsofalibrary.
Asanexample,takealookatthefollowingURL,whichallowsustoloadthemostrecentbugfixreleasesofjQueryv1.11.xwithasinglerequestaswellassomepartsofjQuery-UIv1.10.xandBootstrapv3.3.x:http://cdn.jsdelivr.net/g/jquery@1.11,jquery.ui@1.10(jquery.ui.core.min.js+jquery.ui.widget.min.js+jquery.ui.mouse.min.js+jquery.ui.sortable.min.js),bootstrap@3.3
OptimizingcommonJavaScriptcodeInthissection,wewillanalyzesomeperformancetipsthatarenotjQuery-specificandcanbeappliedtomostJavaScriptimplementations.
WritingbetterforloopsWheniteratingovertheitemsofanarrayoranarray-likecollectionwithaforloop,asimplewaytoimprovetheperformanceoftheiterationistoavoidaccessingthelengthpropertyoneveryloop.Thiscaneasilybedonebystoringtheiterationlengthtoaseparatevariable,declaredjustbeforethelooporevenalongwithit,asshownbelow:
for(vari=0,len=myArray.length;i<len;i++){
varitem=myArray[i];
/*...*/
}
Moreover,ifweneedtoiterateovertheitemsofanarraythatdoesnotcontainfalsyvalues,wecanuseanevenbetterpatternwhichiscommonlyappliedforiteratingoverarraysthatcontainobjects:
varobjects=[{},{},{}];
for(vari=0,item;item=objects[i];i++){
console.log(item);
}
Inthiscase,insteadofrelyingonthelengthpropertyofthearray,weexploitthefactthataccesstoanout-of-boundspositionofthearrayreturnsundefinedwhichisfalsyandstopstheiteration.AnothersamplecasethatthistrickcanbeusediniswheniteratingoverNodeListsorjQueryCompositeCollectionObjectsasshownbelow:
varanchors=$('a');//ordocument.getElementsByTagName('a');
for(vari=0,anchor;anchor=anchors[i];i++){
console.log(anchor.href);
}
NoteFormoreinformationaboutthetruthyandfalsyJavaScriptvalues,visit:https://developer.mozilla.org/en-US/docs/Glossary/Truthyandhttps://developer.mozilla.org/en-US/docs/Glossary/Falsy
WritingperformantCSSselectorsEventhoughSizzle(jQuery’sselectorengine)hidesthecomplexityofDOMtraversalsbasedoncomplexCSSselectors,weshouldhaveanideaofhowourselectorsareperforming.UnderstandinghowCSSselectorsarematchedagainsttheelementsoftheDOMhelpsuswritemoreefficientselectorswhichperformbetterwhenusedwithjQuery.
ThekeycharacteristicofefficientCSSselectorsisspecificity.Accordingtothis,IDandClassselectorsarealwaysmoreefficientthanselectorswithmanyresultslikedivand*.WhenwritingcomplexCSSselectors,keepinmindthattheyareevaluatedfromtherighttotheleftandthataselectorgetsrejectedafterrecursivelytestingitagainsteveryparentelementuntiltherootoftheDOM.
Asaresult,trytobeasspecificaspossiblewiththerightmostselectorinordertocutdownthematchedelementsasquicklyaspossibleduringtheexecutionoftheselector.
//initiallymatchesalltheanchorsofthepage
//andthenremovesthosethatarenotchildrenofthecontainer
$('.containera');
//performsbetter,sinceitmatchesfewerelements
//inthefirststepoftheselector'sevaluation
$('.container.mySpecialLinks');
TheotherperformancetipisusingtheChildSelector(“parent>child”)whereverapplicable,inanefforttoeliminatetherecursionoverallthehierarchyoftheDOMtree.Agreatexamplewherethiscanbeappliedisincaseswherethetargetelementscanbefoundataspecificdescendantlevelofacommonancestorelement:
//initiallymatchesallthediv'softhepage,whichisbad
$('.containerdiv');
//alotfasterthanthepreviousone,
//sinceitavoidstherecursiveclasschecks
//untilreachingtherootoftheDOMtree
$('.container>div');
//bestofall,butcan'tbeusedalways
$('.container>.specialDivs');
TipThesametipscanalsobeappliedtoCSSselectorsthatareusedforstylingpages.EventhoughbrowsershavebeentryingtooptimizeanygivenCSSselector,thetipsdescribedabovecangreatlyreducethetimethatisrequiredtorenderawebpage.
NoteFormoreinformationonjQueryCSSselectorperformance,youcanvisit:http://learn.jquery.com/performance/optimize-selectors/
WritingefficientjQuerycodeLet’snowproceedandanalyzethemostimportantjQuery-specificperformancetips.Formoreinformationaboutthemostup-to-dateperformancetipsonjQuery,keepaneyeontherelevantpageforjQuery’sLearningCenter:http://learn.jquery.com/performance
MinimizingDOMtraversalsSincejQuerymadeDOMtraversalssosimple,manywebdevelopersoverusedthe$()functioneverywhere,eveninsubsequentlinesofcode,makingtheirimplementationsslowerbyexecutingunnecessarycode.OneofthemainreasonsthatthecomplexityoftheoperationissooftenoverlookedistheelegantandminimalisticsyntaxthatjQueryuses.DespitethefactthatJavaScriptbrowserenginesbecamemanytimesfasterinthelastfewyears,withperformancecomparabletomanycompiledlanguages,theDOMAPIisstilloneoftheirslowestcomponentsand,asaresult,developershavetominimizetheirinteractionswithit.
CachingjQueryobjectsStoringtheresultofthe$()functiontoalocalvariableandsubsequentlyusingittooperateontheretrievedelementsisthesimplestwayofeliminatingunnecessaryexecutionsofthesameDOMtraversals.
var$element=$('.boxHeader');
if($element.css('position')==='static'){
$element.css({position:'relative'});
}
$element.height('40px');
$element.wrapInner('<b>');
Inthepreviouschapters,weevensuggestedstoringCompositeCollectionObjectsofimportantpageelementsaspropertiesofourmodulesandreusingthemeverywhereinourapplication:
dashboard.$container=null;
dashboard.init=function(){
dashboard.$container=$('.dashboardContainer');
};
TipCachingretrievedelementsonmodulesisaverygoodpracticewhentheelementsarenotgoingtoberemovedfromthepage.Keepinmindthat,whendealingwithelementswithshorterlifespans,inordertoavoidmemoryleaks,youhavetoeitherensurethatyouclearalltheirreferenceswhentheyareremovedfromthepageorhaveafreshreferenceretrievedwhenrequiredandcacheitonlyinsideyourfunctions.
ScopingelementtraversalsInsteadofwritingcomplexCSSselectorsforyourtraversalslike:
$('.dashboardContainer.dashboardCategories');
YoucaninsteadhavethesameresultinamoreefficientwaybyusinganalreadyretriedancestorelementtoscopetheDOMtraversal.Thisway,youarenotonlyusingsimplerCSSselectorsthatarefastertomatchagainstpageelements,butyouarealsoreducingthenumberofelementsthathavetobechecked.Moreover,theresultingimplementationshavelesscoderepetitions(areDRYer)andtheCSSselectorsusedaresimpleandasa
resultmorereadable.
var$container=$('.dashboardContainer');
$container.find('.dashboardCategories');
Additionally,thispracticeworksevenbetterwithmodule-widecachedelementslikethoseweusedinthepreviouschapters:
$boxContainer=dashboard.$container.find('.boxContainer');
ChainingjQuerymethodsOneofthecharacteristicsofalljQueryAPIsisthattheyareFluentinterfaceimplementationsthatenableustochainseveralmethodinvocationsonasingleCompositeCollectionObject.
$('.boxContent').html('')
.append('<ahref="#">')
.height('40px')
.wrapInner('<b>');
Aswediscussedinpreviouschapters,chainingallowsustoreducethenumberofusedvariablesandleadstomorereadableimplementationswithfewercoderepetitions.
Don’toverdoitKeepinmindthatjQueryalsoprovidesthe$.fn.end()method(http://api.jquery.com/end/)asawayofmovingbackfromachainedtraversal.
$('.box')
.filter(':even')
.find('.boxHeader')
.css('background-color','#0F0')
.end()
.end()//undothefilterandfindtraversals
.filter(':odd')//appliedontheinitial.boxresults
.find('.boxHeader')
.css('background-color','#F00');
Eventhoughthisisahandymethodinmanycases,youshouldavoidoverusingitsinceitcandamagethereadabilityandperformanceofyourcode.Inmanycases,usingcachedelementcollectionsinsteadof$.fn.end()resultsinfasterandmorereadableimplementations.
ImprovingDOMmanipulationsAswesaidearlier,theextensiveuseoftheDOMAPIisoneofthemostcommonthingsthatmakesanapplicationslower,especiallywhenusedtomanipulatethestateoftheDOMtree.Inthissection,wewillshowcasesometipstoimproveperformancewhenmanipulatingtheDOMtree.
CreatingDOMelementsThemostefficientwaytocreateDOMelementsistoconstructaHTMLstringandappendittotheDOMtreeusingthe$.fn.html()method.Additionally,sincethisistoolimitinginsomeusecases,youcanalsousethe$.fn.append()and$.fn.prepend()methods,whichareslightlyslowerbutmaybeabettermatchforyourimplementation.Ideally,ifmultipleelementsneedtobecreated,youshouldtrytominimizetheinvocationofthesemethodsbycreatingaHTMLstringthatdefinesalltheelementsandtheninsertingitintotheDOMtree,asshownbelow:
varfinalHtml='';
for(vari=0,len=questions.length;i<len;i++){
varquestion=questions[i];
finalHtml+='<div><label><span>'+question.title+':</span>'+
'<inputtype="checkbox"name="'+question.name+'"/>'+
'</label></div>';
}
$('form').html(finalHtml);
Anotherwaytoachievethesameresult,isbyusinganarraytostoretheHTMLforeachintermediateelementandthenjointhemrightbeforetheinsertiontotheDOMtree:
varparts=[];
for(vari=0,len=questions.length;i<len;i++){
varquestion=questions[i];
parts.push('<div><label><span>'+question.title+':</span>'+
'<inputtype="checkbox"name="'+question.name+'"/>'+
'</label></div>');
}
$('form').html(parts.join(''));
NoteThisisacommonlyusedpatternsince,untilrecently,itperformedbetterthanconcatenatingtheintermediateresultswith“+=”.
StylingandanimatingWheneverpossible,useCSSclassesforyourstylingmanipulationsbyutilizingthe$.fn.addClass()and$.fn.removeClass()methodsinsteadofmanuallymanipulatingthestyleoftheelementswiththe$.fn.css()method.That’sespeciallyusefulwhenyouneedtostylealargenumberofelementssincethisisthemainpurposeofCSSclassesandbrowsershavealreadyspentyearsoptimizingit.
Tip
Asanextraoptimizationsteptominimizethenumberofmanipulatedelements,youcanapplyCSSclassesonasinglecommonancestorelementanduseadescendantCSSselectortoapplyyourstyling,asdemonstratedhere:https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_selectors
Whenyoustillneedtousethe$.fn.css()method,forexample,whenyourimplementationneedstobeimperative,usetheinvocationoverloadthatacceptsobjectparameters:http://api.jquery.com/css/#css-properties.Inthisway,therequiredmethodinvocationsareminimizedwhenapplyingmultiplestylesonelementsandyourcodeisbetterorganized.
Moreover,avoidmixingmethodsthatmanipulatetheDOMwithmethodsthatreadfromtheDOMsincethiswillforceareflowofthepagesothatthebrowsercancalculatethenewpositionsofthepageelements.
Insteadofdoingsomethinglikethis:
$('h1').css('padding-left','2%');
$('h1').css('padding-right','2%');
$('h1').append('<b>!!</b>');
varh1OuterWidth=$('h1').outerWidth();
$('h1').css('margin-top','5%');
$('body').prepend('<b>--!!--</b>');
varh1Offset=$('h1').offset();
Prefergroupingthenon-conflictingmanipulationstogetherlikethis:
$('h1').css({
'padding-left':'2%',
'padding-right':'2%',
'margin-top':'5%'
}).append('<b>!!</b>');
$('body').prepend('<b>--!!--</b>');
varh1OuterWidth=$('h1').outerWidth();
varh1Offset=$('h1').offset();
Thebrowsercanthusskipsomere-renderingsofthepage,resultinginfewerpausesoftheexecutionofyourcode.
NoteFormoreinformationaboutreflows,visitthefollowingpage:https://developers.google.com/speed/articles/reflow
Lastly,notethatalljQuery-generatedanimationsinv1.xandv2.xareimplementedusingthesetTimeout()function.Thisisgoingtochangeinv3.xofjQuerywhichplanstousetherequestAnimationFrame()function,whichisabettermatchforcreatingimperativeanimations.Untilthen,youcanusethejQuery-requestAnimationFrameplugin(https://github.com/gnarf/jquery-requestAnimationFrame)whichmonkey-patchesjQuerytousetherequestAnimationFrame()functionforitsanimationswhenitisavailable.
ManipulatingdetachedelementsAnotherwaytoavoidunnecessaryrepaintsofthepagewhilemanipulatingDOMelementsistodetachtheelementfromthepageandre-attachitaftercompletingyourmanipulations.Workingwithadetachedin-memoryelementismuchfasteranddoesnotcausereflowsonthepage.
Inordertoachievethat,weusethe$.fn.detach()methodwhich,incontrastto$.fn.remove(),preservesalleventhandlersandjQuerydataonthedetachedelement.
var$h1=$('#pageHeader');
var$h1Cont=$h1.parent();
$h1.detach();
$h1.css({
'padding-left':'2%',
'padding-right':'2%',
'margin-top':'5%'
}).append('<b>!!</b>');
$h1Cont.append($h1);
Additionally,tobeabletoplacethemanipulatedelementbackintoitsoriginalposition,wecancreateandinsertahiddenplaceholderelementintotheDOM.Thisemptyandhiddenelementdoesnotaffecttherenderingofthepageandisremovedaftertheoriginalitemisplacedbackintoitsoriginalposition.
var$h1PlaceHolder=$('<divstyle="display:none;"></div>');
var$h1=$('#pageHeader');
$h1PlaceHolder.insertAfter($h1);
$h1.detach();
$h1.css({
'padding-left':'2%',
'padding-right':'2%',
'margin-top':'5%'
}).append('<b>!!</b>');
$h1.insertAfter($h1PlaceHolder);
$h1PlaceHolder.remove();
$h1PlaceHolder=null;
NoteFormoreinformationaboutthe$.fn.detach()method,youcanreadthedocumentationat:http://api.jquery.com/detach/
IntroducingtheFlyweightPatternAccordingtoComputerScience,aFlyweightisanobjectthatisusedasameansofreducingthememoryconsumptionofanimplementationbyprovidingfunctionalityand/ordatathataresharedwithotherobjectinstances.ThePrototypesofJavaScriptconstructorfunctionscanbecharacterizedasFlyweightssinceeveryobjectinstancecanuseallofthe
methodsandpropertiesthataredefinedinitsprototypeuntilitoverwritesthem.Ontheotherhand,classicalFlyweightsareseparateobjectsfromtheobjectfamilythattheyareusedwithandoftenholdtheshareddataandfunctionalityinspecialdatastructures.
UsingDelegateObserversAgreatexampleofFlyweightsinjQueryapplicationsisDelegateObserverswhich,aswesawintheDashboardexampleinChapter2,TheObserverPattern,cangreatlyreducethememorydemandsofanimplementationbyworkingasacentralizedeventhandlerforalargegroupofelements.Inthisway,wecanavoidthecostofsettingupseparateobserversandeventhandlersforeveryelementandusethebrowser’seventbubblingmechanismtoobserveforthemonasinglecommonancestorelementandfiltertheirorigin.
$boxContainer.on('click','.boxCloseButton',function(){
var$button=$(this);
dashboard.informationBox.close($button);
});
NoteTheactualFlyweightobjectistheeventhandleralongwiththecallbackthatisattachedtotheancestorelement.
Using$.noop()ThejQuerylibraryoffersthe$.noop()methodwhichisactuallyanemptyfunctionthatcanbesharedamongimplementations.Usingemptyfunctionsasdefaultcallbackvaluessimplifiesandimprovesthereadabilityofanimplementationbyreducingthenumberofifstatements.ThisishandyforjQuerypluginsthatalreadyencapsulatecomplexfunctionality.
functiondoLater(callbackFn){
setTimeout(function(){
if(callbackFn){
callbackFn();
}
},500);
}
//with$.noop()
functiondoLater(callbackFn){
callbackFn=callbackFn||$.noop();
setTimeout(function(){
callbackFn();
},500);
}
Insuchsituations,wheretheimplementationrequirementsorthepersonaltasteofthedeveloperhasledtousingemptyfunctions,the$.noop()methodisusefulasawaytolowermemoryconsumptionbysharingasingleemptyfunctioninstanceamongallthedifferentpartsofanimplementation.Anaddedbenefitofusingthe$.noop()methodforeverypartofanimplementationisthatwecanalsocheckwhetherapassedfunctionreferenceistheemptyfunctionbysimplycheckingcallbackFn===$.noop().
NoteFormoreinformation,youcanfindthedocumentationat:http://api.jquery.com/jQuery.noop/
Usingthe$.singlepluginAnothersimpleexampleoftheFlyweightpatterninjQueryapplicationsisthejQuery.singlepluginasdescribedbyJamesPadolseyinhisarticle,76bytesforfasterjQuery,whichtriestoeliminatethecreationofnewjQueryobjectswheneverweneedtoapplyjQuerymethodsonasinglepageelement.TheimplementationisquitesmallandcreatesasinglejQuerycompositecollectionobjectthatisreturnedoneveryinvocationofthejQuery.single()method,containingthepageelementthatwasusedasanargument.
jQuery.single=(function(){
varcollection=jQuery([1]);
//Fillwith1item,tomakesurelength===1
returnfunction(element){
collection[0]=element;//Givecollectiontheelement:
returncollection;//Returnthecollection:
};
}());
ThejQuery.singlepluginisusefulwhenusedinobserverslike$.fn.on()anditerationswithmethodslike$.each().
$boxContainer.on('click','.boxCloseButton',function(){
//var$button=$(this);
var$button=$.single(this);
//thisisnotcreatinganynewobject
dashboard.informationBox.close($button);
});
ThebenefitsofusingthejQuery.singleplugincomefromthefactthatwearecreatingfewerobjectsand,asaresult,thebrowser’sGarbageCollectorwillalsohavelessworktodowhenfreeingupthememoryofshortlivedobjects.
Asasidenote,keepinmindthesideeffectsofhavingasinglejQueryobjectreturnedbyeveryinvocationofthe$.single()methodandthefactthatthelastinvocationargumentwillbestoreduntilthenextinvocationofthemethod:
varbuttons=document.getElementsByTagName('button');
var$btn0=$.single(buttons[0]);
var$btn1=$.single(buttons[1]);
$btn0===$btn1//thisistrue
Additionally,incasethatyouusesomethinglike$btn1.remove()thentheelementwillnotbefreeduntilthenextinvocationofthe$.single()methodwhichwillremoveitfromtheplugin’sinternalcollectionobject.
AnothersimilarbutmoreextensivepluginisthejQuery.flypluginwhichcanbeinvokedwitharraysandjQueryobjectsasparameters.
NoteFormoreinformationaboutjQuery.singleandjQuery.fly,youcanvisitthefollowingURLs:http://james.padolsey.com/javascript/76-bytes-for-faster-jquery/andhttps://github.com/matjaz/jquery.fly.
Ontheotherhand,thejQueryimplementationthathandlestheinvocationofthe$()methodwithasinglepageelementisnotcomplexatallandonlycreatesasinglesimpleobject.
jQuery=function(selector,context){
returnnewjQuery.fn.init(selector,context);
};
/*...*/init=jQuery.fn.init=function(selector,context,root){
/*...else*/
if(selector.nodeType){
this.context=this[0]=selector;
this.length=1;
returnthis;
}/*...*/
};
Moreover,theJavaScriptenginesofmodernbrowsershavealreadybecomequiteefficientwhendealingwithshort-livedobjectssincesuchobjectsarecommonlypassedaroundanapplicationasmethodinvocationparameters.
LazyLoadingModulesFinally,wewillgetanintroductiontotheadvancedtechniqueofLazyLoadingModules.Thekeyconceptofthispracticeisthat,duringthepageload,thebrowserwillonlydownloadandexecutethosemodulesthatarerequiredfortheinitialrenderingofthepagewhiletherestoftheapplicationmodulesarerequestedafterthepageisfullyloadedandisrequiredtorespondtoauseraction.RequireJS(http://requirejs.org/)isapopularJavaScriptlibrarythatisusedasamoduleloaderbut,forsimplecases,wecanachievethesameresultwithjQuery.
Asanexampleofthis,wewilluseittolazyloadtheinformationBoxmoduleoftheDashboardexamplethatwesawinpreviouschapters,afterthefirstclickoftheuserontheDashboard’s<button>.WewillabstracttheimplementationthatisresponsiblefordownloadingandexecutingJavaScriptfilesintoagenericandreusablemodulenamedmoduleUtils:
(function(){
'usestrict';
dashboard.moduleUtils=dashboard.moduleUtils||{};
dashboard.moduleUtils.getModule=function(namespaceString){
varparts=namespaceString.split('.');
varresult=parts.reduce(function(crnt,next){
returncrnt&&crnt[next];
},window);
returnresult;
};
varongoingModuleRequests={};
dashboard.moduleUtils.ensureLoaded=function(namespaceString){
varexistingNamespace=this.getModule(namespaceString);
if(existingNamespace){
return$.Deferred().resolve(existingNamespace);
}
if(ongoingModuleRequests[namespaceString]){
returnongoingModuleRequests[namespaceString];
}
varmodulePromise=$.getScript(namespaceString.toLowerCase()+
'.js')
.always(function(){
ongoingModuleRequests[namespaceString]=null;
}).then(function(){
returndashboard.moduleUtils.getModule(namespaceString);
});
ongoingModuleRequests[namespaceString]=modulePromise;
returnmodulePromise;
};
})();
ThegetModule()methodacceptsthemodule’snamespaceasastringparameterandreturnseithertheModule’sSingletonObjectitselforafalsyvalueifthemoduleisnotalreadyloaded.ThisisdonewiththeArray.reduce()methodwhichisusedtoiterateoverthedifferentpartsofthenamespacestring,usingthedot(.)asadelimiterandevaluatingeachpartonthepreviousobjectcontext,startingwithwindow.
NoteFormoreinformationabouttheArray.reduce()method,youcanvisit:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
ensureLoaded()istheprimarymethodofthemoduleUtilsmoduleandisresponsibleforretrievingandexecutingmodulesthatarenotalreadyloaded.ItfirstusesthegetModule()methodtocheckwhethertherequestedmodulehasalreadybeenloadedand,ifso,returnsitsnamespaceobjectasaResolvedPromise.
Thenextstep,ifamodulehasnotyetbeenloaded,istochecktheongoingModuleRequestsobjecttoverifywhethertherequestedmoduleisnotalreadybeingdownloaded.Inordertodothat,theongoingModuleRequestsobjectusesthemodule’snamespacestringasapropertyandstoresthePromisesoftheAJAXrequeststhatareusedtoretrievethe.jsfilesfromtheserver.IfaPromiseobjectisavailablethenwecaninferthattheAJAXrequestisstillongoingand,insteadofstartinganewone,wereturntheexistingPromise.
Finally,whennoneoftheabovereturnsaresult,weusethelowercasemodulefilenamingconventionthatwediscussedinpreviouschaptersandusejQuery’s$.getScript()methodtoinitiateanAJAXrequesttoretrievetherequestedmodulefile.ThePromisecreatedfortheAJAXrequestisassignedastotheappropriatepropertyoftheongoingModuleRequestsobjectandisthenreturnedtothecallerofthemethod.When,atalaterpointintime,thePromiseisFulfilled,were-evaluatethemoduleandreturnitasthefinalresultofthereturnedPromise.Moreover,regardlessoftheresultoftheAJAXrequest,thePromiseisalsoremovedfromtheongoingModuleRequestsobjectinordertokeeptheimplementationreusableincaseofanetworkfailureandalsofreeupthememorythatwasallocatedfortherequest.
NoteKeepinmindthatthe$.getScript()methodmightnotworkinsomebrowserswhenthepageisloadedthroughthefilesystem,butdoesworkasintendedwhenservedusingawebserverlikeApache,IISornginx.Formoreinformationabout$.getScript(),youcanvisit:http://api.jquery.com/jQuery.getScript/
TheonlychangethatweintroducedtotheexistingimplementationoftheinformationBoxmoduleforthisdemonstrationwastomakeitself-initializableinanattempttoreducethecomplexityoftheensureLoaded()method.
(function(){
'usestrict';
dashboard.informationBox=dashboard.informationBox||{};
var$boxContainer=null;
dashboard.informationBox.init=function(){/*…*/};
$(document).ready(dashboard.informationBox.init);
/*...*/
})();
Finally,wealsohadtochangetheimplementationofthecategoriesmodulesothatitwouldusetheensureLoaded()methodbeforeusingtheinformationBoxmodule.Asyoucanseebelow,wehadtorefactorthecodehandlingtheclickeventonthedashboard’s<button>sincetheensureLoaded()methodreturnsaPromiseasaresult:
//indashboard.categories.init
dashboard.$container.find('.dashboardCategories').on('click','button',
function(){
var$button=$(this);
varitemName=$button.text();
varp=dashboard.moduleUtils.ensureLoaded('dashboard.informationBox');
p.then(function(){
dashboard.informationBox.openNew(itemName);
});
});
SummaryInthischapter,welearnedseveraloptimizationtechniquesthatcanbeusedtoimprovetheperformanceofjQueryapplications,especiallywhentheybecomelargeandcomplex.
WestartedwithsimplepracticeslikebundlingandminifyingourJavaScriptfilesanddiscussedthebenefitsofusingCDNstoloadthird-partylibraries.WethenwentontoanalyzesomesimplepatternstowritingefficientJavaScriptcodeandlearnedhowtowriteefficientCSSselectorstoimprovethepage’srenderingspeedandDOMtraversalsusingjQuery.
WecontinuedwithjQuery-specificpracticessuchascachingofjQueryCompositeCollectionObjects,howtominimizeDOMmanipulations,andhadareminderoftheDelegateObserverpattern,asagoodexampleoftheFlyweightPattern.Lastly,wegotanintroductiontotheadvancedtechniqueofLazyLoadingandsawademonstrationofhowtoloadthevariousmodulesofanimplementationprogressively,basedonuseractions.
Aftercompletingthischapter,wearenowabletoapplythemostcommonoptimizationpatternstoourimplementationsandusethischapterasachecklistofbestpracticesandperformancetipsbeforemovinganapplicationtoaproductionenvironment.
IndexA
$.ajax()method/AcceptingconfigurationparametersaddEventListener()methods
URL/IntroducingtheObserverPatternapplications
developing,withCompositePattern/UsingtheCompositePatterntodevelopapplicationsUnderscore.jstemplates,using/UsingUnderscore.jstemplatesinourapplicationsHandlebars.jstemplates,using/UsingHandlebars.jsinourapplications
Array.reduce()methodreference/LazyLoadingModules
attachEvent()methodURL/IntroducingtheObserverPattern
AustralianNationalUniversity(ANU)/Writingmethodsthatacceptcallbacks
BBabeltranspiler
URL/IntroducingES6Modulesbroker/IntroducingthePublish/SubscribePatternBuilderPattern
about/IntroducingtheBuilderPatternadopting,byjQuery/HowitisadoptedbyjQuery’sAPIusing,byjQueryinternally/HowitisusedbyjQueryinternallyusing,inapplications/Howtouseitinourapplications
CCallbackHell
URL/AvoidingtheCallbackHellanti-patterncallbacks
about/Programmingwithcallbacksprogrammingwith/Programmingwithcallbackssimplecallbacks,usinginJavaScript/UsingsimplecallbacksinJavaScriptsetting,asobjectproperties/Settingcallbacksasobjectpropertiesusing,injQueryapplications/UsingcallbacksinjQueryapplicationsmethods,writing/Writingmethodsthatacceptcallbacks
callbacks,orchestratingabout/Orchestratingcallbacksqueuing,inorderexecution/QueuinginorderexecutionCallbackHellanti-pattern,avoiding/AvoidingtheCallbackHellanti-patternrunningconcurrently/Runningconcurrently
categoriesmodule/ThecategoriesmoduleCDNs
about/UsingCDNsusing/UsingCDNsJSDelivrAPI,using/UsingJSDelivrAPI
closureabout/IntroducingtheObserverPatternURL/IntroducingtheObserverPattern
ClosureCompilerreference/Bundlingandminifyingresources
closuresURL/TheIIFEbuildingblock
commonJavaScriptcodeoptimizing/OptimizingcommonJavaScriptcodeforloops,writingfor/Writingbetterforloops
CompositePatternabout/TheCompositePatternusing,byjQuery/HowtheCompositePatternisusedbyjQuerycomparing,withplainDOMAPIbenefits/ComparingthebenefitsovertheplainDOMAPIused,fordevelopingapplications/UsingtheCompositePatterntodevelopapplicationssampleusecase/AsampleusecaseCollectionImplementation/TheCompositeCollectionImplementationexampleexecution/Anexampleexecutionalternativeimplementations/Alternativeimplementationspairing,withIteratorPattern/HowitpairswiththeCompositePattern
configurationparameters
accepting/AcceptingconfigurationparametersContentDeliveryNetwork(CDN)/UsingCDNscountermodule/ThecountermoduleCSSSelectors/ThejQueryDOMTraversalAPIcustomeventnamespacing
using/UsingcustomeventnamespacingURL/Usingcustomeventnamespacing
customeventsinjQuery/CustomeventsinjQueryused,forimplementingPublish/SubscribePattern/ImplementingaPub/Subschemeusingcustomevents
DDashboardapplication
reusableplugins,using/UsingourplugininourDashboardapplicationdashboardexample
Publish/SubscribePattern,using/UsingPub/Subonthedashboardexampledashboardmodule/Themaindashboardmoduledeferredobserver/Thecategoriesmoduledeferredobservers/ThecountermoduleDelegatedEventObserverPattern
about/IntroducingtheDelegatedEventObserverPatternused,forsimplifyingcode/Howitsimplifiesourcodememoryusagebenefits,comparing/Comparethememoryusagebenefits
DelegateObserversusing/UsingDelegateObservers
descendantCSSselectorreference/Stylingandanimating
DocumentFragmentabout/HowitisadoptedbyjQuery’sAPI
DocumentFragmentsreference/HowitisadoptedbyjQuery’sAPI
DocumentObjectMode(DOM)URL/jQueryandDOMscriptingmanipulating,withjQuery/ManipulatingtheDOMusingjQuery
DOMAPI/ThejQueryDOMTraversalAPIDOMLevel2Eventspecification
URL/HowitiscomparedwitheventattributesDOMmanipulations,improving
about/ImprovingDOMmanipulationsDOMelements,creating/CreatingDOMelementsanimating/Stylingandanimatingstyling/Stylingandanimatingdetachedelements,manipulating/ManipulatingdetachedelementsFlyweightPattern/IntroducingtheFlyweightPattern
DOMscriptingandjQuery/jQueryandDOMscripting
DOMtraversals,minimizingabout/MinimizingDOMtraversalsjQueryobjects,caching/CachingjQueryobjectselementtraversals,scoping/ScopingelementtraversalsjQuerymethods,chaining/ChainingjQuerymethods
E$.extend()helpermethod
URL/AcceptingconfigurationparametersefficientjQuerycode
writing/WritingefficientjQuerycodeDOMtraversals,minimizing/MinimizingDOMtraversalsoveruse,avoiding/Don’toverdoitDOMmanipulations,improving/ImprovingDOMmanipulationsDelegateObservers,using/UsingDelegateObservers$.noop(),using/Using$.noop()$.singleplugin,using/Usingthe$.singleplugin
EncapsulationURL/Encapsulatinginternalpartsofanimplementation
ES5StrictModeusing/UsingES5StrictMode
ES6modulesabout/IntroducingES6ModulesURL/IntroducingES6Modules
eventattributesURL/Howitiscomparedwitheventattributes
eventlistenersremoving,URL/Avoidmemoryleaks
eventobjectURL/Extendingtheimplementation
F$.fn.addClass()method/WorkingonCompositeCollectionObjects$.fn.closest()method
URL/Demonstrateasampleusecase$.fn.data()method/ImplementingastatefuljQueryPlugin$.fn.end()method
reference/Don’toverdoit$.fn.ready()method/Thedocument-readyobserver
URL/Thedocument-readyobserverFacadePattern
about/IntroducingtheFacadePatternbenefits/Thebenefitsofthispatternadopting,byjQuery/HowitisadoptedbyjQuery
Facadesusing,inapplications/UsingFacadesinourapplications
Factoriesusing,inapplications/UsingFactoriesinourapplications
FactoryPatternabout/IntroducingtheFactoryPatternkeyconcept/IntroducingtheFactoryPatternadopting,byjQuery/HowitisadoptedbyjQuery
falsyJavaScriptvaluereference/Writingbetterforloops
FlyweightPatternabout/IntroducingtheFlyweightPattern
functionData()URL/ThejQueryonmethod
G$.getScript()method
URL/RetrievingHTMLtemplatesasynchronouslyreference/LazyLoadingModules
genericiteratorfunction/HowtheIteratorPatternisusedbyjQuerygettermethod
implementing/ImplementinggetterandsettermethodsgetValuesmethod/TheCompositeCollectionImplementationglobalnamespace/AvoidingglobalvariableswithNamespaces,TheRevealingModulePatternGoogle’sJavaScriptStyleGuide
URL/Thewideacceptancegrunt-contrib-concatproject
URL/UsingModulesinjQueryapplications
HHandlebars.js
about/IntroducingHandlebars.jsURL/IntroducingHandlebars.js,UsingHandlebars.jsinourapplications,Pre-compilingtemplatesusing,inapplications/UsingHandlebars.jsinourapplicationstemplatepre-compilation,URL/Pre-compilingtemplates
HTMLtemplatesseparating,fromJavaScriptcode/SeparatingHTMLtemplatesfromJavaScriptcode,SeparatingHTMLtemplatesfromJavaScriptcoderetrievingasynchronously/RetrievingHTMLtemplatesasynchronouslyadopting,inexistingimplementation/Adoptingitinanexistingimplementationmoderation/Moderationisbestinallthings
IIIFE
used,forwrappingjQueryPlugin/WrappingwithanIIFEabout/WrappingwithanIIFE
IIFE-containedmodulevariant/TheIIFE-containedModulevariantIIFEparameters
using/UsingIIFEparametersImmediatelyInvokedFunctionExpression(IIFE)/Thedocument-readyobserver
about/TheIIFEbuildingblockURL/TheIIFEbuildingblock
incrementmethod/TheCompositeCollectionImplementationIndexedDB
URL/RunningconcurrentlyinformationBoxmodule/TheinformationBoxmoduleIteratorPattern
about/TheIteratorPatternusing,byjQuery/HowtheIteratorPatternisusedbyjQuerypairing,withCompositePattern/HowitpairswiththeCompositePatternusing/Wherecanitbeused
JJavaScript
Prototype-basedprogrammingmodel,URL/TheCompositeCollectionImplementation
JavaScriptcodeHTMLtemplates,separating/SeparatingHTMLtemplatesfromJavaScriptcode,SeparatingHTMLtemplatesfromJavaScriptcode
JavaScriptlibrariesURL/Choosinganame
jQueryandDOMscripting/jQueryandDOMscriptingURL/jQueryandDOMscripting,ManipulatingtheDOMusingjQuery,HowtheIteratorPatternisusedbyjQueryused,formanipulatingDOM/ManipulatingtheDOMusingjQueryMethodChaining/MethodChainingandFluentInterfacesFluentInterfaces/MethodChainingandFluentInterfacesCompositePattern,using/HowtheCompositePatternisusedbyjQueryGitHubpage,URL/HowtheCompositePatternisusedbyjQueryIteratorPattern,using/HowtheIteratorPatternisusedbyjQueryObserverPattern,using/HowitisusedbyjQueryPublish/SubscribePattern/HowitisadoptedbyjQuerycustomevents/CustomeventsinjQuerycodeorganization,URL/Overviewoftheimplementation
jQuery-requestAnimationFramepluginreference/Stylingandanimating
jQuery-UIWidgetFactoryURL/Addingmethodstoyourplugin
jQuery.ajaxSetup()method/AcceptingconfigurationparametersjQuery.guid/ThejQueryonmethodjQuery.noConflict()method
URL/Workingwith$.noConflict()jQueryapplications
modules,using/UsingModulesinjQueryapplicationsMockObjectPattern,using/UsingMockObjectsinjQueryapplications
jQueryequivalentmethodURL/Avoidmemoryleaks
jQueryimplementationabout/HowitisadoptedbyjQueryjQueryDOMTraversalAPI/ThejQueryDOMTraversalAPIpropertyaccessandmanipulationAPI/ThepropertyaccessandmanipulationAPI
jQueryJavaScriptStyleGuideURL/Thewideacceptance
jQueryonmethod/ThejQueryonmethodjQueryPlugin
about/IntroducingjQueryPluginsprinciples,following/FollowingjQueryprinciplescharacteristics/FollowingjQueryprinciples$.noConflict(),workingwith/Workingwith$.noConflict()wrapping,withIIFE/WrappingwithanIIFEnamingconventions/Choosinganame
jQueryPluginBoilerplateURL/UsingthejQueryPluginBoilerplateusing/UsingthejQueryPluginBoilerplatemethods,adding/Addingmethodstoyourplugin
jQueryPluginRegistryURL/Choosinganame
jQueryprinciplesfollowing/FollowingjQueryprinciplesCompositeCollectionObjects,workingon/WorkingonCompositeCollectionObjectsfurtherchaining/Allowingfurtherchaining
jQueryPromisestransforming,to/TransformingtojQueryPromises
jQuerysourceviewerURL/ThejQueryonmethod
jQuerySourceViewerURL/HowtheCompositePatternisusedbyjQuery
jQueryv3.0A+Promisesimplementationreference/ComparingjQueryandA+Promises
JSDelivrAPIusing/UsingJSDelivrAPIreference/UsingJSDelivrAPI
LLazyLoadingModules
about/LazyLoadingModulesLevel2SelectorAPI/ThejQueryDOMTraversalAPI
Mmemoryusagebenefits
comparing/ComparethememoryusagebenefitsMethodChaining/MethodChainingandFluentInterfacesMockjaxjQueryPluginlibrary
reference/ImplementingaMockServiceusing/ImplementingaMockService
MockObjectPatternabout/IntroducingtheMockObjectPatternusing,injQueryapplications/UsingMockObjectsinjQueryapplicationsactualservicerequirements,defining/DefiningtheactualservicerequirementsMockService,implementing/ImplementingaMockServiceMockService,using/UsingtheMockService
ModulePatternabout/TheModulePatternImmediatelyInvokedFunctionExpression(IIFE)buildingblock/TheIIFEbuildingblocksimpleIIFEModulePattern/ThesimpleIIFEModulePatternusing,injQuery/HowitisusedbyjQuerynamespaceparametermodulevariant/TheNamespaceParameterModulevariantIIFE-containedmodulevariant/TheIIFE-containedModulevariant
modulesabout/ModulesandNamespaces,Encapsulatinginternalpartsofanimplementationinternalpartimplementation,encapsulating/Encapsulatinginternalpartsofanimplementationacceptance/Thewideacceptanceusing,injQueryapplications/UsingModulesinjQueryapplications
modules,jQueryapplicationsusing/UsingModulesinjQueryapplicationsdashboardmodule/Themaindashboardmodulecategoriesmodule/ThecategoriesmoduleinformationBoxmodule/TheinformationBoxmodulecountermodule/Thecountermoduleimplementation,overview/Overviewoftheimplementation
MustacheURL/IntroducingHandlebars.js
MutationObserverURL/ImplementingastatefuljQueryPlugin
N$.noConflict()
working/Workingwith$.noConflict()$.noop()
using/Using$.noop()reference/Using$.noop()
namespacedeventsURL/Usingcustomeventnamespacing
namespaceparametermodulevariant/TheNamespaceParameterModulevariantnamespaces
about/ModulesandNamespaces,AvoidingglobalvariableswithNamespacesinternalpartimplementation,encapsulating/Encapsulatinginternalpartsofanimplementationglobalvariables,avoiding/AvoidingglobalvariableswithNamespacesbenefits/Thebenefitsofthesepatternsacceptance/Thewideacceptance
namespacing/AvoidingglobalvariableswithNamespacesnamingconventions,jQueryPlugin
selecting/ChoosinganameNPM
URL/Choosinganame
Oobject-orientedJavaScript
URL/ThewideacceptanceObjectLiteral/HowitisadoptedbyjQueryObjectLiteralPattern/TheObjectLiteralPattern,ThesimpleIIFEModulePattern,TheRevealingModulePatternObserverPattern
about/IntroducingtheObserverPatternURL/IntroducingtheObserverPatternusing,injQuery/HowitisusedbyjQueryjQuery.fn.on()method/ThejQueryonmethoddocument-readyobserver/Thedocument-readyobserversampleusecase,demonstrating/Demonstrateasampleusecasecomparing,witheventattributes/Howitiscomparedwitheventattributeseventattributes,comparingwith/Howitiscomparedwitheventattributesmemoryleaks,avoiding/AvoidmemoryleaksandPublish/SubscribePattern,differentiatingbetween/HowitdiffersfromtheObserverPattern
Ppatterns
benefits/ThebenefitsofthesepatternsperformancetipsonjQuery
reference/WritingefficientjQuerycodeperformantCSSselectors
writing/WritingperformantCSSselectorsplaceholdernotations,_.tempate()method
<%=%>notation/IntroducingUnderscore.js<%-%>/IntroducingUnderscore.js<%%>notation/IntroducingUnderscore.js
Promisesabout/IntroducingtheconceptofPromisesusing/UsingPromisesjQueryPromiseAPI,using/UsingthejQueryPromiseAPIadvancedconcepts/Advancedconceptsjoining/JoiningPromisesusing,byjQuery/HowjQueryusesPromisestransforming,toothertypes/TransformingPromisestoothertypesbenefits/SummarizingthebenefitsofPromises
Promises,chainingabout/ChainingPromisesthrownerrors,handling/Handlingthrownerrors
Promises/A+using/UsingPromises/A+reference/UsingPromises/A+comparing,withjQuery/ComparingjQueryandA+Promisestransforming,to/TransformingtoPromises/A+
Publish/SubscribePatternabout/IntroducingthePublish/SubscribePatternandObserverPattern,differentiatingbetween/HowitdiffersfromtheObserverPatternusing,byjQuery/HowitisadoptedbyjQueryimplementing,withcustomevents/ImplementingaPub/Subschemeusingcustomeventssampleusecase/Demonstratingasampleusecaseusing,ondashboardexample/UsingPub/Subonthedashboardexampleimplementation,extending/Extendingtheimplementation
publishers/IntroducingthePublish/SubscribePattern
RRequireJS
URL/LazyLoadingModulesresources
bundling/Bundlingandminifyingresourcesminifying/Bundlingandminifyingresources
reusablepluginscreating/Creatingreusablepluginsconfigurationparameters,accepting/AcceptingconfigurationparametersstatefuljQueryPlugins,writing/WritingstatefuljQuerypluginsstatefuljQueryPlugin,implementing/ImplementingastatefuljQueryPlugininstance,destroying/Destroyingaplugininstancegettermethod,implementing/Implementinggetterandsettermethodssettermethods,implementing/Implementinggetterandsettermethodsusing,inDashboardapplication/UsingourplugininourDashboardapplication
RevealingModulePatternabout/TheRevealingModulePattern
S$.singleplugin
using/Usingthe$.singleplugin<script>tag/Placingscriptsneartheendofthepagesampleusecase,ObserverPattern
demonstrating/Demonstrateasampleusecasesampleusecase,Publish/SubscribePattern
demonstrating/Demonstratingasampleusecaseobject,usingasbroker/Usinganyobjectasabroker
scriptsplacing,nearendofpage/Placingscriptsneartheendofthepage
SeparationofConcernsabout/EncapsulatinginternalpartsofanimplementationURL/Encapsulatinginternalpartsofanimplementation
SeparationofConcernsprinciple/Thebenefitsofthispatternsettermethod
implementing/Implementinggetterandsettermethodssimplecallback
defining/IntroducingtheObserverPatternsimpleIIFEModulePattern
about/ThesimpleIIFEModulePatternSingleResponsibilityprinciple
URL/TheIteratorPatternSizzle/ThejQueryDOMTraversalAPI,WritingperformantCSSselectors
URL/HowitisusedbyjQueryreference/ThejQueryDOMTraversalAPI
specifiedNodeTypesURL/HowtheCompositePatternisusedbyjQuery
statefuljQueryPluginswriting/WritingstatefuljQuerypluginsimplementing/ImplementingastatefuljQueryPlugininstance,destroying/Destroyingaplugininstance
strictexecutionmodeURL/WrappingwithanIIFE
strictmode,JavaScriptURL/UsingES5StrictMode
subscribers/IntroducingthePublish/SubscribePattern
TTiny
URL/ImplementingaPub/SubschemeusingcustomeventstruthyJavaScriptvalue
reference/Writingbetterforloops
UUglifyJS
reference/BundlingandminifyingresourcesUnderscore.js
about/IntroducingUnderscore.jsusing,inapplications/UsingUnderscore.jstemplatesinourapplicationsHTMLtemplates,separatingfromJavaScriptcode/SeparatingHTMLtemplatesfromJavaScriptcode,SeparatingHTMLtemplatesfromJavaScriptcodetemplates,pre-compiling/Pre-compilingtemplates
Vvariable
namingconventions/ManipulatingtheDOMusingjQuery
YYUICompressor
reference/Bundlingandminifyingresources
Recommended