I recently contacted some interviewers. When I asked "how does Vue implement two-way data binding", I would blurt out "data hijacking", and then what? Then there is no ╮(╯_╰)╭. It is true that "data hijacking" is the foundation, but it is far from the answer that the interviewer wants to hear. It is better to spend ten minutes reading this article and answer it next time.
"Two-way binding" itself
To answer the question, we must first understand the problem: two-way data binding is a pattern, which generally refers to the automatic synchronization of data from dom to JS objects in the web context. DOM and JS are isolated on two different runtimes, and they need to communicate with each other through an imperative DOM interface : DOM needs to trigger events correctly and transmit information to JS programs; JS also needs to consciously change the state after Call the appropriate interface to change the DOM content. This approach causes two problems:
- Status management and presentation are two separate sets of logic that need to be kept in sync, which is a high development cost
- The DOM specification defines a lot of interfaces, and there are compatibility issues, which is a high cost of learning
Two-way binding encapsulates the synchronization process of data from DOM to JS or from JS to DOM through various designs, and encapsulates it in the framework itself. The upper-level code is separated from the dependence on the underlying interface, and only needs to focus on the state management logic.
Object.defineProperty
The first question we want to discuss is, how to detect changes to JS object properties? The simplest and crudest method is "dirty checking", which is used in older versions of Angular to initiate a dirty check after various events that may cause state changes. This method is intuitive, but there are many issues to consider in implementation:
- During the dirty check process, new data changes may be triggered, and an infinite loop is entered.
- The implementation of dirty checking must keep a copy of the old and new data
- Dirty checks must predict all possible timings that trigger state changes, which means that some native interfaces need to be wrapped (including
setTimeout
,requestNextAnimationFrame
etc.) - ...
Vue is Object.defineProperty
implemented . When the component is initialized, the interface will be called, the object properties will be wrapped as a get
function set
, and the code will be "buried" in the "get" and "modify" behaviors of the properties. Take a look at a simple example and feel it intuitively:
const person = {};
// 嘿嘿,谁都改不了我的名字
Object.defineProperty(person, 'name', {
get() {
return 'van';
},
set(v) {
console.log('they want change my name');
}
});
console.log(person.name);
// van
person.name = 'tec';
// they want change my name
console.log(person.name);
// van
Dependency Management Solution
Object.defineProperty
It just solves the problem of how to trigger the notification after the state changes. Who should be notified? Who cares that those properties have changed? In Vue, Dep is used to decouple the process of determining the relationship between the dependant and the dependant. simply put:
-
The first step is to traverse the state object through the interface provided by Observer , and bind a dedicated object to each property and sub-property of the
Dep
object . The state object here mainly refers to thedata
properties in the component. -
The second step is to create three types of watchers:
- Call initComputed to convert
computed
properties towatcher
instances - Call the initWatch method to convert the
watch
configuration into anwatcher
instance - Call the mountComponent method
render
to bind thewatcher
instance to the function
- Call initComputed to convert
-
The third step, after the state changes, trigger the
dep.notify()
function , which further triggers the Watcher objectupdate
function to perform the recalculation of the watcher.
Corresponding to the following figure:
Note that the render
function can simply be regarded as a special computed
function. When the corresponding Watcher
object changes, it will trigger the execution of render, generate a new virutal-dom structure, and then hand it over to Vue for diff. Update the view.
Epilogue
This article ends here. For more content, you can try to look at the source code. The design patterns in the code are very worth learning.
Vue uses data hijacking as the underlying support, and designs a sophisticated dependency management solution to decouple dependencies. However, the data hijacking scheme also has its own pain points that are difficult to solve:
- Can only be applied to simple objects
- Not valid for arrays, so you need to wrap the array method
- The starting point of attribute hijacking is "change", so vue cannot easily access the "immutable" mode