Mobx 4结合jQuery使用时this指向及计算属性异常求助
问题
首次使用Mobx,在非React项目(仅用jQuery更新DOM)中使用Mobx 4(兼容旧浏览器),参考Mobx 2的示例时出现错误。
错误代码
function ShoppingCart() { mobx.extendObservable(this, { entries: [], total: function() { return this.entries.reduce(function (sum, entry) { return sum + entry.price; }, 0); } }); }
控制台报错
[mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: 'Reaction[Autorun@19]' TypeError: Cannot read properties of undefined (reading 'reduce')
尝试箭头函数后的问题
改用箭头函数后:
function ShoppingCart() { mobx.extendObservable(this, { entries: [], total: () => { return this.entries.reduce(function (sum, entry) { return sum + entry.price; }, 0); } }); }
返回了函数声明内容:0function () { return this.article ? this.article.price * this.amount : 0; }
需解答以下问题:
- 普通函数与箭头函数的差异
- Mobx在此场景下如何处理
this(.entries) - 为何计算属性无法获取
entries的值
完整源码
/** Data model */ function Article(name, price) { mobx.extendObservable(this, { name: name, price: price }); } function ShoppingCartEntry(article) { mobx.extendObservable(this, { article: article, amount: 1, price: function () { return this.article ? this.article.price * this.amount : 0; } }); } function ShoppingCart() { mobx.extendObservable(this, { entries: [], total: function () { return this.entries.reduce(function (sum, entry) { return sum + entry.price; }, 0); } }); } $(function () { // Some available articles var articles = mobx.observable([ ["Funny Bunnies", 17.63], ["Awesome React", 23.95], ["Second hand netbook", 50.00] ].map(function (e) { return new Article(e[0], e[1]); })); // Our shopping cart var shoppingCart = new ShoppingCart(); // With a demo item inside shoppingCart.entries.push(new ShoppingCartEntry(articles[0])); $.fn.insertAt = function (index, $parent) { return this.each(function () { if (index === 0) { $parent.prepend(this); } else { $parent.children().eq(index - 1).after(this); } }); }; /** UI Logic */ var $articles = $("#articles"); // Make the articles list follow the array articles.observe(function (change) { console.log(change); // items where added or removed if (change.type === "splice") { $articles.children().slice(change.index, change.index + change.removed.length).remove(); for (var i = 0; i < change.addedCount; i++) { renderArticle(articles[change.index + i]) .insertAt(change.index + i, $articles); } } }, true); // true makes sure the observe function is invoked immediately // Render an article in the articles overview, and watch or changes function renderArticle(article) { var $name = $("<span>").text(article.name); var $price = $("<span>").addClass("price").text(article.price); mobx.autorun(function () { $name.text(article.name); }); mobx.autorun(function () { $price.text(article.price); }); return $("<li>").append($name, $("<button>").text("+").addClass("add-article"), $("<button>").text("edit").addClass("edit-article"), $price); } // ShoppingCart ui var $cart = $("#cart"); var $total = $("#total"); mobx.autorun(function () { // console.log(shoppingCart, shoppingCart.total, shoppingCart.entries); $total.text(shoppingCart.total); }, true); shoppingCart.entries.observe(function (change) { // items where added or removed if (change.type === "splice") { $cart.children().slice(change.index, change.index + change.removed.length).remove(); for (var i = 0; i < change.addedCount; i++) { renderCartEntry(shoppingCart.entries[change.index + i]) .insertAt(change.index + i, $cart); } } }, true); function renderCartEntry(entry) { var $name = $("<span>").text(entry.article.name); var $amount = $("<span>").addClass("price").text(entry.amount); mobx.autorun(function () { $name.text(entry.article.name); }); mobx.autorun(function () { $amount.text(entry.amount); }); return $("<li>").append( $("<button>").text("-").addClass("remove-article"), $name, $amount); } // Events $("#new-article").on("click", function () { articles.push(new Article(prompt("Article name"), prompt("Price"))); }); $(document).on("click", ".edit-article", function (event) { var idx = $(event.target).parent().index(); articles[idx].name = prompt("New name"); articles[idx].price = prompt("New price"); }); $(document).on("click", ".add-article", function (event) { var article = articles[$(event.target).parent().index()]; var existingEntry = shoppingCart.entries.find(function (entry) { return entry.article === article; }); if (existingEntry) existingEntry.amount += 1; else shoppingCart.entries.unshift(new ShoppingCartEntry(article)); }); $(document).on("click", ".remove-article", function (event) { var entryIndex = $(event.target).parent().index(); if ((shoppingCart.entries[entryIndex].amount -= 1) < 1) shoppingCart.entries.splice(entryIndex, 1); }); })
解答
1. 普通函数与箭头函数的核心差异
this指向:普通函数的this由调用方式决定;箭头函数没有自身的this,它的this继承自定义时所在的词法作用域。- 构造函数兼容性:箭头函数不能作为构造函数使用,无法通过
new关键字调用。 - 原型对象:箭头函数没有原型属性,无法为其添加原型方法。
2. Mobx在extendObservable中对this的处理
在Mobx 4中,直接在extendObservable里定义普通函数作为计算属性时,Mobx会修改函数的this指向——它不会绑定到当前的实例对象,而是指向Mobx内部的观察代理对象,导致this.entries返回undefined,这就是报错的直接原因。
而箭头函数因为自身没有this,它的this指向ShoppingCart构造函数执行时的实例对象,但Mobx 4不会将箭头函数识别为计算属性,而是直接把函数本身当作普通值保存,所以会出现返回函数声明内容的问题。
3. 解决方法:显式使用mobx.computed定义计算属性
Mobx 4调整了计算属性的定义规则,必须显式使用mobx.computed来声明,而不是直接写函数。修改后的ShoppingCart代码如下:
function ShoppingCart() { mobx.extendObservable(this, { entries: [], total: mobx.computed(function() { return this.entries.reduce(function (sum, entry) { return sum + entry.price; }, 0); }) }); }
同时,ShoppingCartEntry中的price属性也需要做同样修改,否则计算总价时会继续出错:
function ShoppingCartEntry(article) { mobx.extendObservable(this, { article: article, amount: 1, price: mobx.computed(function () { return this.article ? this.article.price * this.amount : 0; }) }); }
4. 迁移说明
Mobx 2中允许直接在extendObservable里用普通函数定义计算属性,但Mobx 4及之后版本要求显式使用computed,这是你从Mobx 2示例迁移过来出现问题的核心原因。
另外,访问computed属性时,Mobx 4会自动调用其内部的getter函数,所以$total.text(shoppingCart.total)可以正常工作,无需手动调用。
内容的提问来源于stack exchange,提问作者Canh

