preface

Recently this period of time in learning Vue data driven the implementation of the principle of the Internet a lot and write articles about Vue data driven, found that’s all very abstract, not suitable for small white read, today I will from the perspective of the small white, detailed analyze the implementation of the Vue data-driven principle, in the hope that we can better understand the Vue read data driven.

Before we begin, let me tell you a little story. We all know that the scout is very important to monitor the changes of the target enemy, and to report the enemy’s situation in time so that we can make the right strategic response. So if scouts are so important, how do we become a good scout in the programming world? Vue’s data-driven implementation can tell you the answer. Here to reveal, Vue data-driven implementation is built on the basis of the observer pattern, after reading this article, you can not only learn the Vue data-driven implementation principle, and can be skilled to master programming is often used in the observer pattern, become qualified scouts, a programming field will not is a dream!

What will you learn from this passage?

1.Vue data driven implementation principle

2. Observer mode

3. Difference between computed and Watch in Vue

Responsive programming

One of the core ideas of the Vue framework is data-driven. The so-called data-driven view means that the view is generated by the data drive. We do not need to operate DOM directly to modify the view, but can modify the pre-set data to achieve the purpose of modifying the view. How to understand this sentence? Those of you who have used the Vue framework know that when we define a Vue component, we define the properties data, props, computed, and Watch, and when we need to change the page view, All we need to do is change the values of the corresponding variables in Data, props, computed, and Watch, and the Vue framework listens for variables and makes changes to the page view so that we can handle a lot of view updates easily. Today this article is to talk about the Vue implementation of data driven principle, we first look at a Vue official tutorial on data driven summary diagram.This diagram shows the central idea of Vue’s Data drive. The diagram can be divided into three parts — the rendering part (yellow), Watcher subscribers (blue), and Data publishers (purple). Let’s briefly describe what each module does

Render function

The yellow part of the image is the rendering method of Vue. For details about the rendering process of Vue, see juejin.cn/post/691679… For now, we just need to know that the rendering function can convert the Vue template into a DOM to display the page view.

Data publishers

The purple part of the figure is the Data publisher. During the execution of the Vue rendering function, it is inevitable to take values from the Data object, which will trigger the getter method of the current variable, which will collect the subscribers of the current variable, namely Watcher.

The Watcher observer

When a variable in a Data object changes, setter methods in the current object are fired, which notify subscribers collected by getter methods of the change in the current variable, refire the render function, and update the page view.

In short, the center of the Data driven, by rewriting the Data of the Data in the get and set properties method, let the Data be rendered when all use of subscribers in your subscriber list, when the Data changes will the change notification to all subscribed to their subscribers, achieve the purpose of rendering again.Here’s a summary of the data-driven process:

Step 1: Execute initData() to initialize variables in the Data object.

Step 2: Add getter and setter properties to the variable pairs in the Data object. The getter property is responsible for adding the subscribers to the variables, and the setter notifies the subscribers.

Step 3: Execute a render function to convert the Template and Data into real DOM elements.

Step 4: During the execution of the render function, the need to fetch Data from Data triggers the getter method of the current variable, which collects the subscribers of the current variable when it changes (i.e., the render function rerenders when the variable changes).

Step 5: Update the view to present the complete front end page to the user.

Step 6: If the value of a variable in the Data object is changed in response to user requirements, the setter method of the variable is triggered, which notifys the render function (observer) of the change and triggers the page rendering again.

Step 7: Update the view and present the updated front end page to the user.

Through the analysis of the whole process of Vue data-driven can be found that the whole Vue data-driven implementation is achieved through the observer mode, let’s briefly understand the observer mode, deepen the understanding of Vue data-driven implementation, but also convenient for subsequent source analysis.

Observer model

The principle of the observer model

The literal interpretation of the observer pattern is that object A (observer) needs to respond to every change in object B (observed). For example, people react differently according to the weather. On rainy days, people will open umbrellas and wear raincoats. Hurricane weather, people choose to stay inside. In this case it can be observed, and we human beings is the observer, according to their different response to the changes in the weather, you can see in the observer pattern, there are two important objects, one is observed (also referred to as the publisher), an observer (also known as a subscriber), is the observer pattern can also be called the publish-subscribe pattern.

From the example above we know that the observer pattern has two important elements — publisher and subscriber. Publishers (i.e., weather) are responsible for notifying humans of changes in the weather, and subscribers (humans, animals, etc.) respond to changes in the weather. Another important question in this process is, when the weather changes, who needs to be notified of the change? People and animals do, buildings and rivers do not. This requires pre-registration of good subscribers with weather (publishers) so that publishers know who to notify.

Programming implementation of observer mode

Now that we know how the observer pattern works, let’s look at how to implement the observer pattern in programming. Based on our principle analysis, we know that we need two interfaces, a Subject interface, to define the publisher, and the publisher needs to have three basic methods — register the subscriber, delete the subscriber, and notify the subscriber. One is the Observer interface to define the Observer. The Observer needs to have the ability to respond to changes. We can implement different observers according to our own needs to cope with the ever-changing needs.

To summarize the three elements of the observer model:

1. Publisher (Observed)

2. Subscribers (observers)

3. Publish and subscribe timing

After learning the observation mode, we can better understand the principle of Vue data drive, let’s uncover the mystery of Vue data drive, take a look!

How is VUE responsive

Through the analysis of Vue’s data-driven process in the above chapter, combined with the observer mode, we can roughly divide the whole data-driven implementation into three steps:

1. Generate responsive objects, creating a publisher (Dep) and subscriber (Watcher) for each variable in data.

2. Dependent collection, the process by which publishers collect subscribers.

3. Distribute updates. When the variable monitored by the publisher changes, the publisher will notify the subscriber to respond.

Generate reactive objects

A reactive object is a data object that adds getter and setter properties to every variable in the data object. Getter methods are fired when we access a variable, and setter methods are fired when we modify that property. This allows us to collect the observers that depend on the data object when we first access the variables in data, and notify the observers to respond in a timely manner when we access it again.

Vue uses ES5’s object.defineProperty () method to add getters and setters to variables.

Object.defineProperty(obj, prop, descriptor)
Copy the code

Obj is the object on which attributes are to be defined;

Prop is the name of the property to be defined or modified;

Descriptor is the property descriptor to be defined or modified.

The core descriptor is descriptor, which has a number of optional key values. See the documentation for Object.defineProperty at developer.mozilla.org/zh-CN/docs/… . Here we care most about get and set. Get is a getter method for a property that fires when it is accessed; Set is a setter method for a property that is triggered when we make changes to that property. Once an object has a getter and setter, we can simply call it a reactive object. So what objects Vue has turned into responsive objects? Let’s analyze them from the source level.

We know that the initState() function is executed when the Vue framework is initialized

export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

As you can see, during the initialization phase, Vue initializes properties such as props, methods, data, computed, and Wathcer. Therefore, props, Methods, Data, computed, and Wathcer are all responsive objects.

Vue generates a responsive object by initializing the data object to 🌰. InitData () is used to initialize the data object.

function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (! isPlainObject(data)) { data = {} process.env.NODE_ENV ! == 'production' && 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 const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] if (process.env.NODE_ENV ! == 'production') { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } if (props && hasOwn(props, key)) { process.env.NODE_ENV ! == 'production' && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (! IsReserved (key)) {proxy(vm, '_data', key) // Step 1}} observe(data, true /* asRootData */) // Step 2}Copy the code

As you can see, the entire process of generating reactive objects does two main things:

First, we use the proxy() method to proxy each value vm._data.xxx to vm.xxx, which is why we directly access the vm._data.xxx variable through vm.xxx.

Second, make variables in data reactive using the defineReactive() method. DefineReactive () is the method that actually adds get and set attribute methods to the data. It defines a reactive object for the data in the data and sets the get and set attribute methods to the object, where the get method collects dependencies, The set method notifies Watcher when data changes.

Let’s look at the implementation:

First, each value vm._data. XXX is proxy to vm. XXX, which is why we directly access the vM. _data. XXX variable through vM. XXX. DefineProperty is used to read and write target[sourceKey][key] into target[key]. When we call proxy, target is VM, that is, this object, sourceKey is _data, and key is the specific variable XXX. When we access this. XXX, we access this._data.xxx.

Const sharedPropertyDefinition = {enumerable: true, different: 64x;} // Proxy (vm, '_data', 64X); true, get: noop, set: noop } export function proxy (target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) }Copy the code

Second, make variables in data reactive using the defineReactive() method. As you can see, each variable in Data creates an Observer object for it (see note 1). Let’s look at the implementation of the Observer.

export function observe (value: any, asRootData: ? boolean): Observer | void { if (! isObject(value) || value instanceof VNode) { return } let ob: Observer | void 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) {ob = new Observer(value) } if (asRootData && ob) {ob.vmCount++} return ob}Copy the code

The Observer constructor logic is very simple and evaluates the type of value. As you can see, the final logic goes to the defineReactive() function whether the value is an array or not (see comment 1). Let’s look at defineReactive().

export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { const augment = hasProto ? protoAugment : CopyAugment (value, arrayMethods, arrayKeys) // This.observearray () this.observearray (value)} else {this.walk(value) // Walk ()}} /** * walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; I ++) {defineReactive(obj, keys[I]) DefineReactive ()}} /** * Observe a list of Array items. Array<any>) { for (let i = 0, l = items.length; i < l; Observe () observe(items[I])}} /** * Define a property. */ export function def (obj: Object, key: string, val: any, enumerable? : boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !! enumerable, writable: true, configurable: true }) }Copy the code

The defineReactive function starts by initializing an instance of the Dep object, first instantiating the Dep object (note 1). The Dep object is the subscriber of the variables in data, implementing registration and notification of changes to the observers of the variables in data. We’ll go through the implementation of the Dep object later, and then see that defineReactive adds getters and setters to all variables in data via defineProperty, turning variables into reactive objects, and variables in this data into reactive objects.

export function defineReactive ( obj: Object, key: string, val: any, customSetter? :? Function, shallow? : Boolean) {const dep = new dep () / / comment a const property = Object. GetOwnPropertyDescriptor (obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((! getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = ! DefineProperty (obj, key, {enumerable: true, different: true, get: different) function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal ! == newVal && value ! == value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV ! == 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = ! shallow && observe(newVal) dep.notify() } }) }Copy the code

Publisher-dep in VUE

Class Dep defines a subs array to store the observer of the current variable:

Dep class three important methods introduced:

1. RemoveSub () deletes the dependency

2. AddSub () adds dependencies

3. Notify () dependency

export default class Dep { static target: ? Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) {remove(this.subs, sub)} depend () {if (dep.target) {dep.target.adddep (this); This is because only one subscriber is allowed to be processed at a time. } } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null const targetStack = [] export function pushTarget (_target: ? Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { Dep.target  = targetStack.pop() }Copy the code

Watcher in VUE

Let’s look at the Watcher class, which is the observer of a variable:

The Watcher class has two important methods:

1. Update () triggers watcher’s callback

2. Get () executes watcher’s callback

3. There is some Watcher sorting and optimization logic between firing and executing callback functions

export default class Watcher { // ... get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, } catch (e) {if (this.user) {handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching 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) } } } update () { if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true } else { // In activated mode, we want to proactively perform the computation // but only notify our subscribers when the value has indeed changed. this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } } }Copy the code

Dependent collection – Release timing

There are two important pieces here — what exactly is a dependency? Where do dependencies reside?

The dependency is the Watcher class (observer), and the Dep class is our publisher (observed), storing the dependency.

The time to fire getter and setter methods on reactive objects is the time to publish and subscribe. This corresponds to dependency collection and update distribution

The principle of dependency collection is that when a view is rendered, the get property method of the data used in the rendering will be triggered, and the dependency collection will be carried out through the GET method.

The mountComponent() function is executed during Vue rendering, and as you can see, a watcher is created during updateComponent(). The updateComponent() callback passed to Watcher is -updatecomponent (). The updateComponent() callback performs the component’s rendering and generates the front end page. So we call this type of wather a rendering watcher, meaning that when this type of watcher is triggered, the rendering process of the page is re-executed (see note 2).

UpdateComponent = () => {vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, {if (vm._isMounted) {callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)Copy the code

In the process of rendering the page, it is inevitable to fetch a variable from data, which triggers the getter() function of the variable (which is already a responsive object). The getter() function is used to subscribe to the variable by putting the current watcher in the SUBs array of the Dep. The whole process depends on the collection.

get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val
  if (Dep.target) {
    dep.depend()
    if (childOb) {
      childOb.dep.depend()
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
  }
  return value
}
Copy the code

Distributed update

1. When we change the value of a variable in a data object, we fire the setter() function for that variable (which is now a reactive object). You can see that setter() first checks whether the new value is equal to the old value, and returns the old value, otherwise we execute dep.notify() for dependency notification (see note 1).

set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal ! == newVal && value ! == value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV ! == 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = ! Shallow && observe(newVal) dep.notify()Copy the code

2. Notify essentially iterates through the subs (or watcher queue) and calls each of the Watcher’s Update () methods.

class Dep { // ... notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; I ++) {subs[I].update()Copy the code

Update performs different logic for different states of the Watcher. For computed and sync states, the last queueWatcher(this) is executed (see note 1) in a normal component data update scenario.

class Watcher { // ... update () { /* istanbul ignore else */ if (this.computed) { // A computed property watcher has two modes: lazy and activated. // It initializes as lazy by default, and only becomes activated when // it is depended on by at least one subscriber, which is typically // another computed property or a component's render function. if (this.dep.subs.length === 0) { // In lazy mode, we don't want to perform computations until necessary, // so we simply mark the watcher as dirty. The actual computation is // performed just-in-time in this.evaluate() when the computed property // is accessed. this.dirty = true } else { // In activated mode, we want to proactively perform the computation // but only notify our subscribers when the value has indeed changed. GetAndInvoke (() => {this.dep.notify()})}} else if (this.sync) {this.run()} else {queueWatcher(this)} }}Copy the code

4. QueueWatcher first obtains the ID of the watcher, and then uses the ID to deduplicate the watcher, ensuring that the same watcher can be added only once. The initial value of flushing is false, so watcher is added to the queue. NextTick () is called only once in the next event loop by using the wating attribute to ensure that the nextTick() callback is executed in the next event loop. FlushSchedulerQueue () is asynchronously executed to ensure that no callback is performed on every update, optimizing performance.

export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (! Flushing) {queue. Push (watcher)} else {// // If already flushing, splice the watcher based on its id // if already flushing, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (! waiting) { waiting = true nextTick(flushSchedulerQueue) } } }Copy the code

FlushSchedulerQueue () first sets flushing to true. Secondly, sort watcher in queue for the following reasons:

1. Update components from parent to child; Because the parent component is created before the child, the Watcher should be created after the parent and executed in the same order.

2. User custom Watcher takes precedence over rendering Watcher; Because the user custom Watcher is created before rendering the Watcher.

3. If a component is destroyed during the parent component’s Watcher execution, its corresponding Watcher execution can be skipped, so the parent component’s Watcher should be executed first.

The next step is to traverse the queue and call Watcher’s run() function. Queue. Length is evaluated every time it is traversed, because at watcher.run() it is likely that the user will add a new watcher. This will do it again to queueWatcher() above, where flushing is true, and then go back in the queue and find the correct place to insert it according to watcher’s ID.

let flushing = false let index = 0 /** * Flush both queues and run the watchers. */ function flushSchedulerQueue () { flushing = true let watcher, id queue.sort((a, b) => a.id - b.id) for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() // In dev build, check and stop circular updates. If (process.env.node_env! == 'production' && has[id] ! = null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn( 'You may have an infinite update loop ' + ( watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.` ), watcher.vm ) break } } } const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() resetSchedulerState() callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) if (devtools && config.devtools)  { devtools.emit('flush') } }Copy the code

6. Watcher’s run() method calls this.get(), which triggers this.getter(). This.getter () finally executes vm._update(vm._render()) for re-rendering. At this point the whole process of update distribution is completed.

run () { if (this.active) { this.getAndInvoke(this.cb) } } getAndInvoke (cb: Function) {const value = this.get(); // Const value = this.get(); }Copy the code

New Watcher – Computed and Watch properties

We all know that in VUE, not only the variables in data objects are responsive, but also the variables in Watch and computed objects are responsive. In fact, the implementation principle of watch and computed is also based on the observer mode. The calculated watcher listens for changes in the properties of the data object, while the render Watcher listens for changes in the calculated properties themselves, so when the values of the calculated properties change, the calculated properties themselves will be recalculated and updated, and the calculated properties will be updated again to trigger the page rendering. It’s a double tap.

The watcher of the watch property (user Watcher for short) monitors the change of the property in data, and it itself is the listener of the property in data. When the change of the property in data is monitored, the callback function of the object will be triggered, which is level with the rendering Watcher. It can be understood that when the rendering Watcher listens to the property change in data, it triggers the page update, and when the user Watcher listens to the property change in data, it executes the callback function defined by the user itself.

Due to limited space, this article will not analyze the specific implementation of Watch and computed arithmetic, but those who are interested can visit caibaojian.com/vue-analysi… Find out for yourself.

conclusion

Vue responsive programming is based on the observer pattern implementation, understand this can easily understand its principle!

Reference documentation

1. caibaojian.com/vue-analysi…

2. www.infoq.cn/article/we3…

3. Flyyang. Me / 2019/03/15 /…