I haven’t updated my technical blog for a while, because I’ve been looking at the Vue source code. In this article, we study the principle of bidirectional binding to analyze Vue source code. It is expected that some articles will be organized around the Vue source code, as follows.
- Learn about Vue bidirectional binding principles – data hijacking and publish/subscribe
- Template generates an AST
- -AST generates the Render string
- Let’s learn Vue Virtual DOM parsing -Virtual DOM implementation and DOM -diff algorithm
These articles are in my git repository: github.com/yzsunlei/ja… . Remember star collection if useful.
Simple application
Let’s start with a simple application example:
<div id="app"> <input id="input" type="text" V-model ="text"> <div id="text"> {{text}}</div> </div> <script> var vm = new Vue({ el: '#app', data: { text: 'hello world' } }) </script>Copy the code
The function of the above example is that the ‘Hello World’ string is initially displayed in the input box and in the div text, and the value of the div text changes when the value is entered manually.
Let’s simplify the implementation idea:
- 1. Bind the input field and div text to the data in data
- 2. When the content of input box changes, the corresponding data in data changes synchronously, that is, view => model
- 3. When the data in data changes, the corresponding DIV text content changes synchronously, that is, model => view
The principle is introduced
Vue. Js implements bidirectional binding through data hijacking and the combination of publisher and subscriber. Data hijacking uses ES5 object.defineProperty (OBj, key, val) to hijack the setter and getter of each attribute and publish messages to subscribers when data changes. Triggers a callback to update the view.
Bidirectional data binding, in a nutshell, is divided into three parts:
- The main job here is to recursively listen for all the properties of the object and trigger the corresponding Watcher when the property value changes.
- Watcher: the subscriber that executes the callback function (update template content in Vue) in response to changes in the listening data value.
- 3. Dep: Subscription manager, a bridge between the Observer and Watcher. Each Observer has a Dep that maintains an array of watchers associated with the Observer.
DEMO implements bidirectional binding
Let’s implement two-way data binding step by step.
The first part is Observer:
function Observer(obj, key, value) {
var dep = new Dep();
if (Object.prototype.toString.call(value) == '[object Object]') {
Object.keys(value).forEach(function(key) {
new Observer(value, key, value[key])
})
};
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function() {
if (Dep.target) {
dep.addSub(Dep.target);
};
return value;
},
set: function(newVal) { value = newVal; dep.notify(); }})}Copy the code
Add getters and setters recursively for each property of object obj. In the getter, we add watcher to the DEP. In the setter, watcher is triggered to perform the callback.
The second part is Watcher:
function Watcher(fn) {
this.update = function() {
Dep.target = this;
fn();
Dep.target = null;
}
this.update();
}
Copy the code
Fn is the callback function to be executed after the data changes, usually to get the data render template. The update method is executed once by default to establish the relationship between the data object and the getter during template rendering. Because only one Watcher is active at a time, bind the current watcher to dep. target (easy to fetch in the Observer). After the callback is complete, destroy dep.target.
The third part is Dep:
function Dep() {
this.subs = [];
this.addSub = function (watcher) {
this.subs.push(watcher);
}
this.notify = function() {
this.subs.forEach(function(watcher) { watcher.update(); }); }}Copy the code
An array subs that holds the watcher. AddSub is used to add a watcher(when getter) to an array. Notify is used to trigger updates to watcher (when setter).
Now that we have the simple bidirectional binding function, let’s see if we can achieve the same effect as the simple application above.
<div id="app">
<input id="input" type="text" v-model="text">
<div id="text">The values entered are: {{text}}</div>
</div>
<script type="text/javascript">
var obj = {
text: 'hello world'
}
Object.keys(obj).forEach(function(key){
new Observer(obj, key, obj[key])
});
new Watcher(function(){
document.querySelector("#text").innerHTML = "The input value is:" + obj.text;
})
document.querySelector("#input").addEventListener('input'.function(e) {
obj.text = e.target.value;
})
</script>
Copy the code
Of course, the above is the simplest bidirectional binding function, Vue also implements the array, object bidirectional binding, let’s take a look at the implementation of Vue.
Bidirectional binding in Vue
Before we look at the source code of Vue implementation, we first look at the following figure, the classic Vue bidirectional binding principle schematic diagram (picture from the network) :
The simple explanation is as follows:
- 1. Implement a data listener, Obverser, to monitor the data in data, and notify the corresponding subscribers if there is any change.
- 2, implement an instruction parser Compile, parse the instructions on each element, replace the data according to the instructions, update the view.
- Implement a Watcher that connects Obverser with Compile, binds the corresponding subscriber for each attribute, and executes the corresponding callback function to update the view when the data changes.
Observer in Vue:
The first is the Observer object, source location SRC/core/Observer/index. Js
export class Observer { value: any; dep: Dep; vmCount: number; constructor (value: Any) {this.value = value this.dep = new dep () this.vmcount = 0 // Add __ob__ to indicate that value has corresponding Observer def(value, '__ob__', This) if (array.isarray (value)) {// Handle Array if (hasProto) {// Implement is '__proto__' in {} protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, ArrayKeys)} this.observearray (value)} else {// Process object this.walk(value)}} Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; I ++) {defineReactive(obj, keys[I])}} // loop through each item of the array. Array<any>) { for (let i = 0, l = items.length; i < l; I ++) {observe(items[I])}}Copy the code
In general, a value is treated as an object or an array. Let’s start with defineReactive and Observe, two of the more important functions.
export function defineReactive ( obj: Object, key: string, val: any, customSetter? :? Function, shallow? : boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, If (property && property.64x === false) {return} // Handle for pre-defined without any additional configured information Getter /setters // Saves the own getter and setter for an object property const getter = property && property Property.set // If there is no getter defined on the property before, and no initial val value is passed, the property's original value is assigned to val if ((! getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = ! shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: Function reactiveGetter () {// Set getter const value = getter? Getter. call(obj) : Val if (dep.target) {// Create a Dep for each attribute. Dep.depend () if (childOb) { DependArray (value) {dependArray(value)}} return value}, set: dependArray(value) {dependArray(value)}} return value}, set: Function reactiveSetter (newVal) {// Set setter const value = getter? Getter. call(obj) : Val / / value did not change, just skip the if (newVal = = = value | | (newVal! == newVal && value ! == value)) { return } if (process.env.NODE_ENV ! == 'production' && customSetter) {customSetter();} if (getter &&! setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = ! Shallow && observe(newVal) // Notify dep.notify()}})}Copy the code
DefineReactive is where you specify getters and setters for object properties. It creates a DEP for each value and saves it temporarily if the user passes in a getter and setter for that value. Then add the decorator again via Object.defineProperty. In the getter, dep. Depend does two things. It adds the DEP to the deps inside the DEP. target, and it adds the dep. target to the subs inside the DEP, that is, establishes the connection between them. In the setter, if the old and new values are the same, return them directly, if not, call dep.notify to update the watcher associated with them.
export function observe (value: any, asRootData: ? Boolean) : the Observer | void {/ / if not object is to skip the if (! isObject(value) || value instanceof VNode) { return } let ob: The Observer | void if (hasOwn (value, '__ob__) && value. __ob__ instanceof Observer) {/ / if the Observer, is returned directly, Ob = value.__ob__} else if (shouldObserve &&! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && ! Value._isvue) {// Create an ob = new Observer(value)} if (asRootData && ob) {ob.vmcount+ +} return ob}Copy the code
Observe This method is used to observe an object, return an Observer associated with the object, or create an Observer for value if none exists.
Ok, we go back to Observer, and if an object is passed in, we call Walk, which iterates through the object, performing defineReactive on each value.
In the case of an array, there is some special processing, because the array itself refers to only one address, so we cannot listen to the array push, splice, sort, etc. So, Vue overwrites value’s __proto__ (if any), or redefines these methods on value. Augment is protoAugment when the environment supports __proto__ and copyAugment when it does not.
// augment function protoAugment (target, SRC: Function copyAugment (target: Object, SRC: Object, keys: Object) {target.__proto__ = SRC} // Augment augment with the environment not supporting __proto__ function copyAugment (target: Object, SRC: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }Copy the code
Augment augment is simple when the environment supports __proto__, calling protoAugment actually implements value.__proto__ = arrayMethods. Augment While the environment supports __proto__, call the loop in copyAugment to add the arrayKeys method on arrayMethods to the value.
So we’re going to look at arrayMethods here. ArrayMethods are really new objects that rewrite arrayMethods. ArrayKeys are a list of methods in arrayMethods.
const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { const original = arrayProto[method] def(arrayMethods, method, function mutator (... args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': Inserted = args.slice(2) break} Insert (ob.observearray (inserted) // Notify change ob.dep.notify() return result})})Copy the code
We actually call the corresponding array method to operate on the value, but then we add an update to the related Watcher. Ob. observeArray is re-called when push, unshift, and splice are called with parameters greater than 2. Because these three cases are like adding new elements to the array, each child element needs to be re-observed. At last the notification changes.
So much for the Observer in Vue. In fact, there are two functions set, del not explained, in fact, when adding or deleting array elements, object attributes for getter, setter binding and notification changes, you can see the source code.
Dep in Vue:
Vue Dep: SRC /core/ Observer /dep.js
let uid = 0 export default class Dep { static target: ? Watcher; id: number; subs: Array<Watcher>; Constructor () {this.id = uid++ this.subs = []} Watcher) {this.subs.push(sub)} // removeSub (sub: Watcher) { remove(this.subs, Sub)} // Add to the subscription manager Depend () {if (dep.target) {dep.target.adddep (this)}} // Notify changes notify () {const subs = this.subs.slice() if (process.env.NODE_ENV ! == 'production' && ! Config.async) {subs.sort((a, b) => a.id - b.id)} for (let I = 0, l = subs.length; i < l; i++) { subs[i].update() } } }Copy the code
The Dep class is simpler, with an ID and a subs inside. The ID is used as a unique identifier for the Dep object, and the subs is the array that holds the Watcher. Compared to the demo application we implemented above, there are removeSub and Depend. RemoveSub removes a Watcher from an array. Depend calls addDep to the watcher.
Ok, so much for Dep in Vue.
Watcher in Vue:
Finally, we take a look at the Vue Watcher, source location: SRC/core/observer/Watcher. Js.
// Note that I have removed some of the less important or bidirectional binding logic from the source code, using //... Let uid = 0 export default class Watcher {vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ? Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // ... this.cb = cb this.id = ++uid // ... this.expression = process.env.NODE_ENV ! == 'production' ? expOrFn.toString() : '' if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (! this.getter) { this.getter = noop process.env.NODE_ENV ! == 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } this.value = this.lazy ? undefined : this.get() } get () { pushTarget(this) let value const vm = this.vm // ... if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() return value } addDep (dep: Dep) { const id = dep.id if (! this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (! this.depIds.has(id)) { dep.addSub(this) } } } cleanupDeps () { // ... } update () {// Update () { The default asynchronous update is added to the processing queue if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {queueWatcher(this)}} run If (this.active) {const value = this.get() if (value! == this.value || isObject(value) || this.deep ) { const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } } evaluate () { // ... } depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } teardown () { // ... }}Copy the code
Note That the url is expOrFn or CB, and that the url is expOrFn or CB.
When Watcher is created, this.get is called, which executes the expOrFn url’s getter. In this getter, we either render the page or get the value of some data. In summary, the getter for the associated data is called to establish a bidirectional binding of the data.
When the relevant data changes, Watcher’s Update method is called, which in turn calls the run method. As we can see, run also calls this.get to get the modified value.
Watcher has two main uses: updating templates and listening for changes in values.
What happens to template updates: When Vue declares periodic mount elements, we update the render template by creating a Watcher object and then calling updateComponent.
vm._watcher = new Watcher(vm, updateComponent, noop)
Copy the code
When you create Watcher, you call this.get, which is updateComponent. During render, data’s getter method is called to establish a bidirectional binding of the data, and updateComponent is refired when the data changes.
Data monitoring: Another use is computed, watch, etc., which monitors changes in data to perform responsive operations. This. get returns the value of the data to listen on. During initialization, a call to this.get will get the initial value and save it as this.value. After the listening data changes, a call to this.get will get the modified value, pass the old value and new value to cb, and execute the callback in response.
Ok, so much for Watcher in Vue. CleanupDeps removes dependency logic, teardown destroys Watcher logic, etc.
To summarize
In Vue, bidirectional binding is simply described as Observer, Watcher and Dep. Let’s go through the process again:
First we implement data hijacking with Object.defineProperty() for each vue attribute, assigning each attribute a management array DEP of the subscriber collection; Then, at compile time, a subscriber is added to the array dep of that attribute. V-model in Vue adds a subscriber, as does {{}}, and v-bind; When the value is finally changed, the property is assigned a value, which triggers the property’s set method, notifies the subscriber array DEP in the set method, and the subscriber array loops through each subscriber’s UPDATE method to update the view.
related
- Github.com/liutao/vue2…
- Juejin. Cn/post / 684490…
- www.cnblogs.com/zhenfei-jia…
- Segmentfault.com/a/119000001…
- Segmentfault.com/a/119000001…