From 3f8b04484b4fef1d2fdf6199a761b5770a652896 Mon Sep 17 00:00:00 2001 From: Vincent Schippefilt Date: Thu, 7 Mar 2019 10:24:54 +0000 Subject: [PATCH] [REF] doc: add async javascript code section in javascript Cheatsheet, add a bunch of example about asynchronous code closes odoo/odoo#31716 Signed-off-by: Aaron Bohy (aab) --- doc/reference/javascript_cheatsheet.rst | 266 +++++++++++++++++++++++- doc/reference/javascript_reference.rst | 67 ++++-- 2 files changed, 317 insertions(+), 16 deletions(-) diff --git a/doc/reference/javascript_cheatsheet.rst b/doc/reference/javascript_cheatsheet.rst index 50c93c9febb..97f7ba5cb5c 100644 --- a/doc/reference/javascript_cheatsheet.rst +++ b/doc/reference/javascript_cheatsheet.rst @@ -276,4 +276,268 @@ using the view in the kanban arch (a specific example is the helpdesk dashboard) Note: you can change the way the view interprets the arch structure. However, from the server point of view, this is still a view of the same base type, subjected to the same rules (rng validation, for example). So, your views still - need to have a valid arch field. \ No newline at end of file + need to have a valid arch field. + +Promises and asynchronous code +=============================== +For a very good and complete introduction to promises, please read this excellent article https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md + +Creating new Promises +----------------------- +- turn a constant into a promise + There are 2 static functions on Promise that create a resolved or rejected promise based on a constant: + + .. code-block:: javascript + + var p = Promise.resolve({blabla: '1'}); // creates a resolved promise + p.then(function (result) { + console.log(result); // --> {blabla: '1'}; + }); + + + var p2 = Promise.reject({error: 'error message'}); // creates a rejected promise + p2.catch(function (reason) { + console.log(reason); // --> {error: 'error message'); + }); + + + .. note:: Note that even if the promises are created already resolved or rejected, the `then` or `catch` handlers will still be called asynchronously. + + +- based on an already asynchronous code + Suppose that in a function you must do a rpc, and when it is completed set the result on this. + The `this._rpc` is a function that returns a `Promise`. + + .. code-block:: javascript + + function callRpc() { + var self = this; + return this._rpc(...).then(function (result) { + self.myValueFromRpc = result; + }); + } + +- for callback based function + Suppose that you were using a function `this.close` that takes as parameter a callback that is called when the closing is finished. + Now suppose that you are doing that in a method that must send a promise that is resolved when the closing is finished. + + .. code-block:: javascript + :linenos: + + function waitForClose() { + var self = this; + return new Promise (function(resolve, reject) { + self.close(resolve); + }); + } + + * line 2: we save the `this` into a variable so that in an inner function, we can access the scope of our component + * line 3: we create and return a new promise. The constructor of a promise takes a function as parameter. This function itself has 2 parameters that we called here `resolve` and `reject` + - `resolve` is a function that, when called, puts the promise in the resolved state. + - `reject` is a function that, when called, puts the promise in the rejected state. We do not use reject here and it can be omitted. + * line 4: we are calling the function close on our object. It takes a function as parameter (the callback) and it happens that resolve is already a function, so we can pass it directly. To be clearer, we could have written: + + .. code-block:: javascript + + return new Promise (function (resolve) { + self.close(function () { + resolve(); + }); + }); + + + +- creating a promise generator (calling one promise after the other *in sequence* and waiting for the last one) + Suppose that you need to loop over an array, do an operation *in sequence* and resolve a promise when the last operation is done. + + .. code-block:: javascript + + function doStuffOnArray(arr) { + var done = Promise.resolve(); + arr.forEach(function (item) { + done = done.then(function () { + return item.doSomethingAsynchronous(); + }); + }); + return done; + } + + This way, the promise you return is effectively the last promise. +- creating a promise, then resolving it outside the scope of its definition (anti-pattern) + .. note:: we do not recommend using this, but sometimes it is useful. Think carefully for alternatives first... + + .. code-block:: javascript + + ... + var resolver, rejecter; + var prom = new Promise(function (resolve, reject){ + resolver = resolve; + rejecter = reject; + }); + ... + + resolver("done"); // will resolve the promise prom with the result "done" + rejecter("error"); // will reject the promise prom with the reason "error" + +Waiting for Promises +-------------------- +- waiting for a number of Promises + if you have multiple promises that all need to be waited, you can convert them into a single promise that will be resolved when all the promises are resolved using Promise.all(arrayOfPromises). + + .. code-block:: javascript + + var prom1 = doSomethingThatReturnsAPromise(); + var prom2 = Promise.resolve(true); + var constant = true; + + var all = Promise.all([prom1, prom2, constant]); // all is a promise + // results is an array, the individual results correspond to the index of their + // promise as called in Promise.all() + all.then(function (results) { + var prom1Result = results[0]; + var prom2Result = results[1]; + var constantResult = results[2]; + }); + return all; + + +- waiting for a part of a promise chain, but not another part + If you have an asynchronous process that you want to wait to do something, but you also want to return to the caller before that something is done. + + .. code-block:: javascript + + function returnAsSoonAsAsyncProcessIsDone() { + var prom = AsyncProcess(); + prom.then(function (resultOfAsyncProcess) { + return doSomething(); + }); + /* returns prom which will only wait for AsyncProcess(), + and when it will be resolved, the result will be the one of AsyncProcess */ + return prom; + } + +Error handling +-------------- +- in general in promises + The general idea is that a promise should not be rejected for control flow, but should only be rejected for errors. + When that is the case, you would have multiple resolutions of your promise with, for instance status codes that you would have to check in the `then` handlers and a single `catch` handler at the end of the promise chain. + + .. code-block:: javascript + + function a() { + x.y(); // <-- this is an error: x is undefined + return Promise.resolve(1); + } + function b() { + return Promise.reject(2); + } + + a().catch(console.log); // will log the error in a + a().then(b).catch(console.log); // will log the error in a, the then is not executed + b().catch(console.log); // will log the rejected reason of b (2) + Promise.resolve(1) + .then(b) // the then is executed, it executes b + .then(...) // this then is not executed + .catch(console.log); // will log the rejected reason of b (2) + + + +- in Odoo specifically + In Odoo, it happens that we use promise rejection for control flow, like in mutexes and other concurrency primitives defined in module `web.concurrency` + We also want to execute the catch for *business* reasons, but not when there is a coding error in the definition of the promise or of the handlers. + For this, we have introduced the concept of `guardedCatch`. It is called like `catch` but not when the rejected reason is an error + + .. code-block:: javascript + + function blabla() { + if (someCondition) { + return Promise.reject("someCondition is truthy"); + } + return Promise.resolve(); + } + + // ... + + var promise = blabla(); + promise.then(function (result) { console.log("everything went fine"); }) + // this will be called if blabla returns a rejected promise, but not if it has an error + promise.guardedCatch(function (reason) { console.log(reason); }); + + // ... + + var anotherPromise = + blabla().then(function () { console.log("everything went fine"); }) + // this will be called if blabla returns a rejected promise, + // but not if it has an error + .guardedCatch(console.log); + + + .. code-block:: javascript + + var promiseWithError = Promise.resolve().then(function () { + x.y(); // <-- this is an error: x is undefined + }); + promiseWithError.guardedCatch(function (reason) {console.log(reason);}); // will not be called + promiseWithError.catch(function (reason) {console.log(reason);}); // will be called + + + +Testing asynchronous code +-------------------------- + +- using promises in tests + In the tests code, we support the latest version of Javascript, including primitives like `async` and `await`. This makes using and waiting for promises very easy. + Most helper methods also return a promise (either by being marked `async` or by returning a promise directly. + + .. note:: Version 12 and 13 of Odoo supports IE11, for which it is not possible to polyfill async and await. We do not consider that it is important that the tests runs on these browser though. + + .. code-block:: javascript + + var testUtils = require('web.test_utils'); + QUnit.test("My test", async function (assert) { + // making the function async has 2 advantages: + // 1) it always returns a promise so you don't need to define `var done = assert.async()` + // 2) it allows you to use the `await` + assert.expect(1); + + var form = await testUtils.createView({ ... }); + await testUtils.form.clickEdit(form); + await testUtils.form.click('jquery selector'); + assert.containsOnce('jquery selector'); + form.destroy(); + }); + + QUnit.test("My test - no async - no done", function (assert) { + // this function is not async, but it returns a promise. + // QUnit will wait for for this promise to be resolved. + assert.expect(1); + + return testUtils.createView({ ... }).then(function (form) { + return testUtils.form.clickEdit(form).then(function () { + return testUtils.form.click('jquery selector').then(function () { + assert.containsOnce('jquery selector'); + form.destroy(); + }); + }); + }); + }); + + + QUnit.test("My test - no async", function (assert) { + // this function is not async and does not return a promise. + // we have to use the done function to signal QUnit that the test is async and will be finished inside an async callback + assert.expect(1); + var done = assert.async(); + + testUtils.createView({ ... }).then(function (form) { + testUtils.form.clickEdit(form).then(function () { + testUtils.form.click('jquery selector').then(function () { + assert.containsOnce('jquery selector'); + form.destroy(); + done(); + }); + }); + }); + }); + + as you can see, the nicer form is to use `async/await` as it is clearer and shorter to write. \ No newline at end of file diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst index 02c477905d5..05e546dfec6 100644 --- a/doc/reference/javascript_reference.rst +++ b/doc/reference/javascript_reference.rst @@ -299,6 +299,17 @@ The *odoo.define* method is given three arguments: extract them from the function by calling toString on it, then using a regexp to find all *require* statements. +.. code-block:: javascript + + odoo.define('module.Something', ['web.ajax'], function (require) { + "use strict"; + + var ajax = require('web.ajax'); + + // some code here + return something; + }); + - finally, the last argument is a function which defines the module. Its return value is the value of the module, which may be passed to other modules requiring it. Note that there is a small exception for asynchronous modules, see the @@ -312,7 +323,7 @@ If an error happens, it will be logged (in debug mode) in the console: * ``Failed modules``: A javascript error is detected * ``Rejected modules``: - The module returns a rejected deferred. It (and its dependent modules) is not + The module returns a rejected Promise. It (and its dependent modules) is not loaded. * ``Rejected linked modules``: Modules who depend on a rejected module @@ -326,12 +337,12 @@ Asynchronous modules It can happen that a module needs to perform some work before it is ready. For example, it could do a rpc to load some data. In that case, the module can -simply return a deferred (promise). In that case, the module system will simply -wait for the deferred to complete before registering the module. +simply return a promise. In that case, the module system will simply +wait for the promise to complete before registering the module. .. code-block:: javascript - odoo.define('module.Something', ['web.ajax'], function (require) { + odoo.define('module.Something', function (require) { "use strict"; var ajax = require('web.ajax'); @@ -351,11 +362,11 @@ Best practices - declare all your dependencies at the top of the module. Also, they should be sorted alphabetically by module name. This makes it easier to understand your module. - declare all exported values at the end -- try to avoid exporting too much things from one module. It is usually better +- try to avoid exporting too many things from one module. It is usually better to simply export one thing in one (small/smallish) module. -- asynchronous modules can be used to simplify some use cases. For example, - the *web.dom_ready* module returns a deferred which will be resolved when the - dom is actually ready. So, another module that needs the DOM could simply have +- asynchronous modules can be used to simplify some use cases. For example, + the *web.dom_ready* module returns a promise which will be resolved when the + dom is actually ready. So, another module that needs the DOM could simply have a *require('web.dom_ready')* statement somewhere, and the code will only be executed when the DOM is ready. - try to avoid defining more than one module in one file. It may be convenient @@ -562,7 +573,7 @@ rendering takes place, then *start* and finally *destroy*. this method will be called once by the framework when a widget is created and in the process of being appended to the DOM. The *willStart* method is a - hook that should return a deferred. The JS framework will wait for this deferred + hook that should return a promise. The JS framework will wait for this promise to complete before moving on to the rendering step. Note that at this point, the widget does not have a DOM root element. The *willStart* hook is mostly useful to perform some asynchronous work, such as fetching data from the server @@ -585,9 +596,9 @@ rendering takes place, then *start* and finally *destroy*. the *start* method. This is useful to perform some specialized post-rendering work. For example, setting up a library. - Must return a deferred to indicate when its work is done. + Must return a promise to indicate when its work is done. - :returns: deferred object + :returns: promise .. function:: Widget.destroy() @@ -755,7 +766,7 @@ Inserting a widget in the DOM uses `.insertBefore()`_ All of these methods accept whatever the corresponding jQuery method accepts -(CSS selectors, DOM nodes or jQuery objects). They all return a deferred_ +(CSS selectors, DOM nodes or jQuery objects). They all return a promise and are charged with three tasks: * rendering the widget's root element via @@ -805,6 +816,34 @@ Widget Guidelines or intercepting DOM events) must inherit from :class:`~Widget` and correctly implement and use its API and life cycle. +* Make sure to wait for start to be finished before using $el e.g.: + + .. code-block:: javascript + + var Widget = require('web.Widget'); + + var AlmostCorrectWidget = Widget.extend({ + start: function () { + this.$el.hasClass(....) // in theory, $el is already set, but you don't know what the parent will do with it, better call super first + return this._super.apply(arguments); + }, + }); + + var IncorrectWidget = Widget.extend({ + start: function () { + this._super.apply(arguments); // the parent promise is lost, nobody will wait for the start of this widget + this.$el.hasClass(....) + }, + }); + + var CorrectWidget = Widget.extend({ + start: function () { + var self = this; + return this._super.apply(arguments).then(function() { + self.$el.hasClass(....) // this works, no promise is lost and the code executes in a controlled order: first super, then our code. + }); + }, + }); .. _reference/javascript_reference/qweb: @@ -926,7 +965,7 @@ The previous example can be updated to use the custom event system: start: function () { this.counter = new Counter(this); var def = this.counter.appendTo(this.$el); - return $.when(def, this._super.apply(this, arguments); + return Promise.all([def, this._super.apply(this, arguments)]); }, _onValueChange: function(event) { // do something with event.data.val @@ -2240,5 +2279,3 @@ For more information, look into the *control_panel.js* file. http://api.jquery.com/delegate/ .. _datepicker: https://github.com/Eonasdan/bootstrap-datetimepicker - -.. _deferred: http://api.jquery.com/category/deferred-object/