React + ES6 = không còn autobind
React đã loại bỏ tính năng “autobinding” với các class component sử dụng cú pháp EcmaScript 2015 (ES6). Vì vậy, những cách truyền hàm như kiểu onClick={this.onClickHandler}
sẽ không hoạt động nữa vì hàm onClickHandler
không được gán cho đối tượng nào, this
trong hàm đó sẽ không thể xác định được. Trong bài viết này, chúng ta sẽ tìm hiểu nguyên nhân cũng như một số biện pháp khắc phục.
ES6 Class
React đã từng sử dụng một phương thức để tạo các component là React.createClass()
(tài liệu tham khảo). Hàm này là một tổ hợp các quá trình phức tạp để tạo ra một component mới, trong đó, nó có tính năng “autobinding”.
Tuy nhiên, sau khi ES6 được công bố, nó cung cấp cho lập trình viên cú pháp mới cho phép tạo class đúng kiểu lập trình hướng đối tượng hơn, đó là sử dụng keyword Class
. Và React cũng hỗ trợ điều đó. Điều này cho phép lập trình viên xây dựng ứng dụng dễ dàng hơn, nhưng kéo theo đó, là tính năng “autobinding” đã không còn nữa. Điều này có thể gây nhiều vấn đề với những người đã quen với autobinding rồi.
Phương thức và hàm
Một phương thức là một hàm được định nghĩa như một thuộc tính của đối tượng nào đó. Một hàm thì độc lập, không được gắn vào đối tượng nào cả. Đây cũng là sự khác biệt giữa phương thức và hàm ở phần lớn những ngôn ngữ lập trình hướng đối tượng. Ở đây, chúng ta chỉ quan tâm đến sự khác biệt đơn giản đó thôi. Hãy tạm thời bỏ qua những yếu tố kỹ thuật khác như trình dịch hiểu phương thức thế nào, hàm ra sao, hàm được lưu thế này, phương thức được lưu thế kia…
Với phương thức, this
trong phương thức đó được dùng để tham chiếu đến đối tượng mà phương thức đó thuộc về.
Để hiểu rõ hơn về từ khóa
this
, bạn có thể tham khảo bài viết trước của tôi. Trong bài viết này, chúng ta chỉ nhắc lại một số điểm cơ bản.
var obj = {
prop: 'Hello',
greet: function() {
console.log(this.prop);
}
};
obj.greet(); // 'Hello'
Một ví dụ rất cơ bản và dễ hiểu. Nhưng mọi việc sẽ phức tạp hơn nếu chúng ta thay đổi cách tham chiếu phương thức:
var ref = obj.greet;
ref(); // "undefined"
Tại sao lúc này kết quả lại là undefined
? Các hàm và cả phương thức trong JavaScript là những đối tượng first-class, có nghĩa là chúng ta có thể sử dụng chúng như những đối tượng thông thường, có thể gán chúng, truyền chúng cho các hàm khác như tham số. Khi chúng ta gán phương thức cho biến khác var ref = obj.greet
, chúng ta đã làm mất mối quan hệ của phương thức với đối tượng. Khi chúng ta gọi nó, nó trở thành một hàm, chứ không phải phương thức nữa.
Khi hàm đó gọi đến this
, nó không được gán cho đối tượng nào cả, nó sẽ trở nên rất khó để xác định và đó là thứ bạn không bao giờ muốn gặp phải.
obj.greet()
vẫn là một phương thức. Cú pháp này của JavaScript báo cho trình dịch biết rằng trong phương thứcgreet
,this
chính là obj. Chúng ta chỉ đánh mất quan hệ này khi truyền phương thức cho đối tượng khác.
Bind this
cho hàm
Một cách làm thông thường được sử dụng để gán this
cho các hàm được truyền đi, đó là sử dụng bind
như ví dụ sau:
var obj = {
prop: 'Hello',
greet: function() {
console.log(this.prop);
}
};
var newFunc = obj.greet.bind(obj);
newFunc(); // 'Hello'
Trong ví dụ trên, bind
đã giúp chúng ta định nghĩa lại this
trong hàm newFunc
, khiến nó vẫn tham chiếu đến đối tượng cũ chứ không bị mất đi mối quan hệ này. Một điểm lưu ý rằng method.bind(this)
chưa thực thi phương thức đó, mà nó chỉ tạo ra một hàm mới, trong đó this
được định nghĩa lại theo cách của chúng ta. Khi hàm được gọi, this
sẽ có giá trị của tham số trong bind
.
Những điều này liên quan gì đến React?
Rất nhiều phương thức trong React vẫn hoạt động tốt mà không cần đến những thức phức tạp ở trên. Nhưng rắc rối sẽ phát sinh nếu chúng ta truyền phương thức ra ngoài đối tượng (mà điều này thường xuyên xảy ra). Hãy xem xét một component ví dụ như sau:
import React from 'react';
export default class ExampleComponent extends Component {
onClickHandler() {
this.setState({clicked: true});
}
render() {
return (
<div onClick={this.onClickHandler} />
);
}
}
Đoạn code trên không hề có lỗi cú pháp, nhưng component sẽ không hoạt động đúng. Khi chúng ta click, chúng ta sẽ gặp lỗi
this.setState is not a function
Lý do rất đơn giản, khi this.onClickHandler
được gọi, nó được gọi như một hàm chứ không phải phương thức, mặc dù chúng ta đã sử dụng cú pháp rất giống với việc gọi một phương thức. Vì là một hàm, lúc này this
không còn là đối tượng mà phương thức được định nghĩa nữa, nó có thể là window hoặc undefined nếu sử dụng strict mode.
Một số giải pháp
Sử dụng bind
khi render
Đây là cách đơn giản nhất:
render() {
return (
<div onClick={this.onClickHandler.bind(this)} />
)
};
Ưu điểm:
- Rất dễ dùng
- Dễ dàng chuyển đối từ version cũ (sử dụng
createClass
) sang cú pháp mới
Nhược điểm:
- Mỗi khi
render
được gọi,bind
sẽ tạo ra một hàm mới (chiếm một vùng bộ nhớ mới). Sẽ có nhiều rác trong bộ nhớ nếurender
được gọi nhiều lần. Tất nhiên, điều này chưa phải vấn đề lớn lắm, performance chưa bị ảnh hưởng nhiều, trừ khi bạn lập trình game bằng React và update state liên tục. - Bạn sẽ phải lặp đi lặp lại
this.onClickHandler.bind(this)
mỗi khi bạn cần đếnonClickHandler
trongrender
.
Sử dụng arrow function trong JSX
Đây cũng là một cách làm tương đối đơn giản
render() {
return (
<div onClick={() => this.onClickHandler()} />
)
};
Sự khác biệt ở đây là gì? Đó là khi gọi onClickHandler
chúng ta gọi nó là phương thức của this
. Nhưng vì sử dụng arrow function, nên hàm này được định nghĩa ngữ cảnh sẵn, không phát sinh this
của riêng nó. Vì vậy this
vẫn có giá trị là đối tượng của component và mọi thứ sẽ chạy như chúng ta muốn
Ưu điểm
- Rất dễ dùng
- Dễ dàng chuyển đối từ version cũ (sử dụng
createClass
) sang cú pháp mới
Nhược điểm
- Tương tự như
bind
, mỗi khirender
được gọi, một hàm mới sẽ được tạo ra. - Bạn sẽ phải lặp đi lặp lại
() => this.onClickHandler()
mỗi khi gọi phương thức (dù không dài lắm)
Bind
ngay khi khởi tạo
Chúng ta có thể bind
sẵn các phương thức ngay từ khi khởi tạo component, ví dụ như sau:
import React from 'react';
export default class ExampleComponent extends Component {
constructor() {
super();
this.onClickHandler = this.onClickHandler.bind(this);
}
...
}
Với cách làm này, mỗi khi chúng ta gọi phương thức, nó đã được bind sẵn rồi. Chúng ta không cần lo đến this
của nó nữa.
Ưu điểm:
- Không cần thay đổi gì trong hàm
render
- Một số thư viện như underscore còn cho phép chúng ta bind nhanh hơn nữa, ví dụ:
_.bindAll(this, 'onClickHandler', ...)
Nhược điểm:
- Có thể bị quên
bind
cho một phương thức nào đó - Chúng ta cần suy xét cẩn thận phương thức nào cần bind, phương thức nào không
- Phương thức không thể “hot reload”, nghĩa là khi Component được load lại, các phương thức của nó không cần thiết phải thực thi. Chúng ta cần phải bỏ tính năng này đi vì không thực thi lại
constructor
có thể gây lỗi.
Sử dụng thư viện cho phép autobinding
Một số thư viện như react-autobind hoặc autobind-decorator cho phép chúng ta nhanh chóng bind các phương thức của component một cách tự động. Ví dụ autobind-decorator cho chúng ta cú pháp khá giống với decorator của Python
import React from 'react';
import autobind from 'autobind-decorator';
export default class ExampleComponent extends Component {
@autobind
update() {
...
}
...
}
Ưu điểm:
- Rất dễ đọc và dễ hiểu
- Có thể sử dụng hot reload
Nhược điểm:
- Cần một số cài đặt với babel
- Decorator không phải là cú pháp chuẩn của JavaScript, không ai biết tương lai của nó.
Sử dụng arrow function làm class method
Hiện nay ES6 vẫn chưa làm được, nhưng ES7 thì có thể. Chúng ta có thể định nghĩa các class method như là những arrow function. Hiện nay babel đã hỗ trợ cách làm này rồi. Chúng ta hãy xem qua ví dụ sau:
import React from 'react';
export default class ExampleComponent extends Component {
update = () => {
...
}
...
}
Ưu điểm:
- Phương thức tự động được bind mà không cần thư viện bên thứ 3
- Có thể hot reload
Nhược điểm:
- Phương pháp này vẫn chưa được công bố chính thức, thậm chí nó mới là proposal. Nó có thể không được chấp nhận.
Sử dụng phương pháp cũ (createClass
)
React vẫn hỗ trợ phương pháp React.createClass()
để tạo các component. Nên nếu cần thiết, chúng ta vẫn có thể tiếp tục sử dụng chúng (cho đến khi nào React bỏ nó đi).
Ưu điểm:
- Không thay đổi gì so với trước đây
Nhược điểm:
- Cú pháp không đúng chuẩn hướng đối tượng
- Có thể tương lai sẽ không dùng được nữa
Kết luận
Trong bài viết này, chúng ta đã tìm hiểu về một số khó khăn do cú pháp ES6 không còn autobinding. Nhưng chúng ta hoàn toàn có thể khắc phục được những vấn đề này.
Welcome
Đây là thế giới của manhhomienbienthuy (naa). Chào mừng đến với thế giới của tôi!
Bài viết liên quan
Bài viết mới
Chuyên mục
Lưu trữ theo năm
Thông tin liên hệ
Cảm ơn bạn đã quan tâm blog của tôi. Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.