I. Preface
Today, let's step by step realize the two-way binding of Vue. First, introduce a picture, this picture source , he wrote this article very well. You can go and have a look. The significance of my article lies in the analysis of source code and some understanding of myself. Please point out the shortcomings. Thank you. Maybe it's not easy to understand just by looking at the diagram. Then analyze the diagram from the perspective of source code. Looking back at this diagram, you'll understand a lot. You can't live much. Let's start.
2, Source code analysis
Take me first One line analysis of Vue source code (I) overall analysis , if you haven't read my first article, you can see that data monitoring binding is performed in initState method.
function initState (vm) { vm._watchers = []; var opts = vm.$options; if (opts.props) { initProps(vm, opts.props); } // First, check whether there is props initialization in options //First, check whether there are methods related to initialization methods in options, because we are talking about two-way binding now. We will not talk about it here, but will add later if (opts.methods) { initMethods(vm, opts.methods); } if (opts.data) { initData(vm); // Initialization data } else { observe(vm._data = {}, true /* asRootData */); } if (opts.computed) { initComputed(vm, opts.computed); } // Initialize computed if (opts.watch && opts.watch !== nativeWatch) { // Initialize listening initWatch(vm, opts.watch); } }
function initData (vm) { var data = vm.$options.data; // Get data from vm.$options data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};// If data is method execution GetData, the internal part of GetData method is to execute data.call to get the return value of method if (!isPlainObject(data)) { //To determine whether data is a pure object or not, the following warning requires that the data function return an object data = {}; warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ); } // proxy data on instance var keys = Object.keys(data); // Get the key of the data object var props = vm.$options.props; // Get props in options var methods = vm.$options.methods;// Get methods in options var i = keys.length; while (i--) { // Cyclic traversal var key = keys[i]; { if (methods && hasOwn(methods, key)) { // Use Object.prototype.hasOwnProperty to determine whether the key defined in data is also defined in methods // That's why we define a variable in data and a variable with the same name in methods, // The following props are the same, so data and methods props cannot have variables with the same name, because no matter the method // Either props or data will be placed on the instance of Vue. If the name is the same, I don't know if you want to take data // methods or porps warn( ("Method \"" + key + "\" has already been defined as a data property."), vm ); } } if (props && hasOwn(props, key)) { warn( "The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.", vm ); } else if (!isReserved(key)) { //Intercept the agent if the data key does not start with $or \ proxy(vm, "_data", key); // because } } // observe data observe(data, true /* asRootData */); }
Because the value in data is placed in_data, you can only get the value of MSG through this._data.msg. Isn't this what I need to get every value? It's hard for programmers to accept this. What we want to get from this.xxx is what I want, how simple it is, so there's a proxy method. Use Object.defineProperty
var sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }; function proxy (target, sourceKey, key) { // target represents vm, sourceKey represents_data, and key represents the value you want to take // This means that if you want to use this.msg to get the MSG, in fact, you need to point to this.u data.msg to get the same effect, // Therefore, this method is called agent, which is simple and convenient sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }
Next, the last observe method
function observe (value, asRootData) {// Pass in data, and whether the second parameter is the root node data if (!isObject(value) || value instanceof VNode) { // If the incoming data is not an object or the VNdode virtual node is not input, the judgment is returned for the end of recursion judgment, which will be discussed later return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // A series of judgments start from the Observer in the top figure ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob }
var Observer = function Observer (value) { this.value = value; // Put data on the value of Observer this this.dep = new Dep(); // Top picture of Dep message management center this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { // If the data passed in is an array if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { // Passed in is not an array this.walk(value); } };
var Dep = function Dep () { this.id = uid++; this.subs = []; // Define a list of subscribers };
Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); // Get the key of data and start to listen circularly for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } };
function defineReactive$$1 ( obj, // data key, // A key in data val, customSetter, shallow ) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); // Get the modifier of key in data if (property && property.configurable === false) { //If the key cannot be written back return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { // If there are only two parameters passed in, it means data listening val = obj[key]; } var childOb = !shallow && observe(val); // Recursion. The above observe first determines the correspondence. If val is still an object //Then recursion, if it is not an object, the end recursion // Below is Vue's two-way binding core using Object.defineProperty data hijacking Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // When the value is taken, for example, to get MSG, use this.msg to enter this method var value = getter ? getter.call(obj) : val; //If the property itself has a getter to use itself, otherwise get the original value if (Dep.target) { //It's more complicated here. It's about Watcher // If there is a new watcher, the Watcher will tell the message manager to add subscribers dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { // If the new value is equal to the old value, return directly without changing return } /* eslint-enable no-self-compare */ if (customSetter) { customSetter(); } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); // observe hijack the set value dep.notify(); // Message manager to notify } }); }
The following three codes are to add subscribers when taking values
Dep.prototype.depend = function depend () { if (Dep.target) { // Here, Dep.target is the Watcher. When there is a new Watcher, it will be put into Dep.target Dep.target.addDep(this); } };
Watcher.prototype.addDep = function addDep (dep) { var id = dep.id; // The id of Dep is used to determine whether there are already the same subscribers among subscribers. If not, add or not if (!this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) { dep.addSub(this); } } };
Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); // Add to Dep's sub array list };
The following code snippet is to notify Watcher of an update when the value is set
Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); if (!config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } };
Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); } };
Watcher.prototype.run = function run () { if (this.active) { var value = this.get(); if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value var 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); } } } };
I'm sure you are a little dizzy now. Maybe what I said is not very clear. Because there are many ways to encapsulate and always jump, it's very complicated. If you don't see from the whole, there are too many ways to jump, it's a little difficult. Now I'm going to draw a flow chart to smooth the thinking.
In fact, the two-way binding principle is mainly divided into four modules: Observer, DEP, Watcher, and Compile. First, the Observer uses Object.defineProperty to hijack data. When using the publish and subscribe design mode, the Observer is monitored. When taking a value, the message management center is informed to add a listener When setting the value, the Watcher notifies you to change, and the Compile resolves and compiles again, and updates the corresponding view. If you want to convert the vue template, such as the v-model, {}} into html, you need to Compile and parse it through Compile, so this is the basic principle of two-way binding. There is not too much talk about Compile here, because it is still learning. The Compile is converted into the AST virtual tree through the template, and it is changing There are many difficulties in the process of converting VNode to html. Please update later