I have been using VUE2 for more than two years, and I always want to know how its internal implementation principle is. I used to read some blogs and absorb some knowledge points in pieces, but when piecing them together, I always feel that I still haven't formed a systematic understanding of it, and MY heart is always uneasy. Just recently have some free time, coupled with the heart that want to systematically understand it because of excitement and trembling heart, went to see the "simple Vue. Js" this book, combined with the source code to more detailed exploration, finally feel in my mind formed a systematic cognition, finally is practical. This article mainly shares my understanding of the responsive principle in this part after exploring the source code, which is also a summary.

responsive

Typically, the state within the application changes constantly at run time, and when the state changes, it needs to be rerendered to get the latest view. The responsive system gives the view the ability to be re-rendered, and its core components are the observation of state changes and efficient DOM update rendering. Next, let’s understand the operation principle of the responsive system from the perspective of source code.

Change detection

The premise of rerendering is that the state has changed, so how do you determine that the state has changed to notify the change?

Change detection is used to solve this problem.

To detect changes to an Object in JS, you can use object.defineproperty and Proxy, and since ES6 is not well supported in browsers (vue2.0 was released on 1 October 2016), So Object. DefineProperty is still used in the VUe2 version.

The official documentation states that Vue cannot detect array and object changes due to JavaScript limitations. So, what is this limitation? How is array and object change detection implemented differently in VUe2? Based on these two questions, we combine the source code together to explore.

Object change detection

First of all, in this part of the source code that involves data responsiveness, we can see several important classes, which are Observer, Dep, and Watcher

Function Introduction:

  • How to collect dependencies: where do you do data hijacking to collect dependencies? ==> Observer

  • Where are dependencies collected: Every object, every key dependency needs to be centrally managed ==> Dep

  • Who is dependent on: In other words, who is notified when a property changes? This data can be used in a template, a user-defined Watch, or computed, so define a class that handles these cases centrally, notifyit of data changes, and then notifyit elsewhere. ==> Watcher

The functional relationship between Observer, Dep, and Watcher

  • Data is converted into getters/setters via observers to track changes.

  • When the outside world reads data through the Watcher, the getter is triggered to add the Watcher to dependency management (Dep).

  • When data changes, setters are triggered to send notifications to dependencies (Watcher) in the Dep.

  • When Watcher receives a notification, it sends a notification to the outside world. The change notification may trigger a view update or a callback function of the user.

Now, I need to explain how the outside world can read data through Watcher, and there are three scenarios, right

  1. When the component is mounted, the Watcher is created, passing in the component update function

  2. Create a watcher for computed property computed, passing in the getter for computed property

  3. $watch creates watcher, passing in a listening expression such as’ A.B.C ‘

Watcher internally reads these functions or expressions to trigger getters for responsive data. You can take a look at the source code:

Detection of Array changes

We know that vue2 does not have a responsive vm.items[index] = newValue when we change the index of the array directly. Is this because Object.defineProperty cannot detect the change?

To answer this question, let’s take a test:

function defineReactive(obj, key, value) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: Function () {the console. The log (key, 'trigger the get - - -, value); return value; }, set: function(newValue) {console.log(key,'-- trigger set-- ', newValue); value= newValue; }}); } function Observer(value) { const keys = Object.keys(value) keys.forEach(k => { defineReactive(value, k, value[k]); }); } const arr = [' a ', 'b'] Observer (arr) / / test subscript arr [0] arr [0] = 'aa'Copy the code

Object.defineproperty can detect array changes, so why not just use object.defineProperty to respond to array data?

HSS ~! That’s a good question! In the issue of Vue, someone also raised such a question, and the big answer is that the performance cost is not proportional to the user experience gain. Very concise answer, understood but not fully understood!

Think about it, where does the performance cost come from? If object.defineProperty is used to listen on array properties, why is there a performance problem?

In general, we use the seven methods of arrays (push, POP, Shift, unshift, splice, sort, reverse), in addition to subscripting. Let’s test what happens when we use these seven methods to change an array after we implement the getter/setter for each subscript key of the array using Object.defineProperty:

push

When you add an array item with push, the array subscript is treated as a key value for triggering getters/setters. This is the same as when the Object is represented. The new key needs to be intercepted again using Object.defineProperty

unshift

Adding an item to the beginning of the array with unshift triggers multiple getters/setters, reading each item in the array, and then resetting the value

pop

Use pop to delete the last item in the array, and get is triggered when the subscript key of the last item has been intercepted

shift

Use Shift to remove the first item of the array. When the subscript key of the last item has been intercepted, the getter is fired multiple times, and the setter is fired once

splice

Use splice to change a value and fire a getter/setter. Adding and deleting items triggers multiple getters/setters

sort|reverse

Sort using sort and reverse, triggering multiple getters/setters

Conclusion:

Object. DefineProperty intercepts each subscript of an array. When using an array method, all methods except push and POP will fire multiple getter/ setters. For each subscript key, a Dep is created to manage dependencies. When an array is changed using array methods, getter collection dependencies (watcher) are fired multiple times, and setters notify dependencies.

Is the overhead of creating the Dep, firing getter/setter multiple times, and watcher necessary? Obviously not, in our actual business scenario, the array update only needs to know that the array itself has changed to trigger the view update. So when we use the array method to change the array, we know that the array has changed, so we can do interception update, not getter/setter for the array subscript, to avoid the extra performance overhead.

The thing to consider here is how to trigger the getter/setter for an array if object.defineProperty is not used for intercepting.

Then, let’s look for the answer in the source code

  • For arrays that require reactive processing, override the seven methods on their array prototypes. ==> setter
import { def } from '.. /util/index' const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache Original method const original = arrayProto[method] def(arrayMethods, method, function mutator (... Const result = original. Apply (this, args) // get ob instance const ob = this.__ob__ // Let inserted switch (method) {case 'push': case 'unshift': inserted = args break case 'splice': Inserted = args.slice(2) break} // The newly inserted object still needs a response if (inserted) ob.observearray (inserted) // notify change // Ob.dep. notify() return result})})Copy the code
  • When the original behavior is performed, the array has changed, so how do you notify that the dependency has changed

When creating an Observer instance, create a dependency management Dep instance for the observed object, and create an attribute __ob__ for the observed object, pointing to the current Observer instance. Get an Observer instance by __ob__ to get the OBJECT’s DEP and notify the change

Take a look at the constructor implementation of the Observer

export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: Any) {this.value = value // Create a one-to-one dep this.dep = new dep () this.vmCount = 0 // bind the __ob__ attribute in the observation object to an Observer instance def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } }Copy the code
  • Now that YOU have Dep management for an object, where do you use dependency collection for objects? ==> getter

We can see that when detecting each key, the corresponding value will be recursively traversed. If the value is an object, the detection will be carried out, that is, we can get ob instance, and then call Dep on OB instance to collect dependencies

export function defineReactive ( obj: Object, key: string, val: any, customSetter? :? Function, shallow? Const dep = new dep ()... For this omission... // if value is an object, let childOb =! shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : If (dep.target) {dep.depend() if (childOb) {val // Dep.target is the current watcher if (dep.target) {dep.depend() if (childOb) {// Add the dependency childob.dep.depend (). If (array.isarray (value)) {dependArray(value)}} return value}, set: dependArray(value) {dependArray(value)}} return value}, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val ... For this omission... childOb = ! Shallow && observe(newVal) // Change notification dep.notify()}})}Copy the code

$set, $delete

For objects, object.defineProperty cannot detect the addition or deletion of an attribute on the Object, so responsively implemented for these two cases. Vue2 provides $set to support the addition of Object attributes, and $delete to support the deletion of attributes. By determining whether an object has been processed responsively, the __ob__ attribute on the object can be accessed directly, and if it exists, the object’s dependencies can be retrieved and notified.

Object defineProperty does getter/setter for array subscripts, so $set is also provided to support subscript setting of array items, which is different from the way objects are handled. Arrays call the splice method to add, subtract, or delete subscripts, because the reactive array splice method already has getters and setters.

$set part of the source code implementation as follows:

export function set (target: Array<any> | Object, key: any, val: any): Any {// array: Splice if (array.isarray (target) &&isValidArrayIndex (key)) {target.length = math.max (target.length, key) target.splice(key, 1, val) return val } if (key in target && ! (key in Object.prototype)) { target[key] = val return val } const ob = (target: Any).__ob__ // Observer instance // has not been processed in response, returns if (! ob) { target[key] = val return val } defineReactive(ob.value, key, Ob.dep.notify () return val}Copy the code

$delete ($set); $delete ($set); $delete ($set);

export function del (target: Array<any> | Object, key: Any) {// Array: If (array.isarray (target) && isValidArrayIndex(key)) {target.splice(key, 1) return} const ob = (target: any).__ob__ if (! HasOwn (target, key)) {return} delete target[key] Ob) {return} ob.dep.notify() // Notify dependent changes}Copy the code

Batch asynchronous update

The specific implementation

Batch asynchronous updates mainly take advantage of the browser’s event loop mechanism. As long as it listens for data changes, it will open a queue and buffer all data changes in the same event loop.

If the same Watcher is fired more than once, it is pushed to the queue only once, avoiding unnecessary calculations and DOM manipulation, and then in the next event loop, “Tick,” Vue flusher the queue and performs the actual (de-duplicated) work. Vue internally attempts to use native Promise.then, MutationObserver, and setImmediate for asynchronous queues, and setTimeout(fn, 0) instead if the execution environment does not support it.

$nextTick

When we use the $nextTick API, which is actually nextTick in an asynchronous update, we add the callback function to the callback’s task queue. When we use $nextTick(callback) immediately after the data changes, the callback will be called after the DOM update is complete.

NextTick source code section

Const callbacks = [] let pending = false function flushCallbacks () {pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } let timerFunc if (typeof Promise ! // timerFunc = () => {p.hen (flushCallbacks)} isUsingMicroTask = true } ... Other execution environment determination code is omitted here... export function nextTick (cb? : Function, ctx? : Callbacks. Push (() => {if (cb) {try {cb.call(CTX)} catch (e) {if (cb) {cb.call(CTX)} catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (! Pending) {// Start pending = true timerFunc()}}Copy the code

DOM updates

Render function

In the asynchronous update phase of the previous step, the final update function vm._update(vnode: vnode, hydrating?: Boolean) is called via watcher.run(), where vnode is returned by the render function.

UpdateComponent = () => {// Execute vm._render() and return VNode vm._render()}Copy the code

Vue.com ponent is used to create the component’s constructor and generate the component’s hook function (which is triggered by hooks during patch).

Take a look at the render function compiled from the template:

Execute render function to generate vNode

Virtual DOM update

Vm._update () performs the update and invokes the patch function to compare the old and new VNodes to obtain the minimum DOM operation. In combination with the asynchronous update strategy, the refresh frequency is reduced and the performance is improved.

Patch process (same layer comparison, depth first)


  1. Let’s start with a tree comparison. Three cases
  • NewVnode does not exist, oldVnode does exist, delete
  • NewVnode exists, oldVnode does not exist, new
  • Both the old and new Vnodes exist. Run patchVnode to compare and update
  1. PatchVnode comparison update, including: text update, attribute update, child node update

  2. The child node comparison is updated

In the actual business scenarios, does not occur in all child nodes position change, there are always some nodes is not move, for these positions unchanged or predictable, we can use to find faster way = = > whether the same position of nodes for the same node, if just to match, can directly update node operation, If the attempt fails, use a loop. This approach largely eliminates the need for loops to find nodes, thus increasing execution speed.

Actual traversal process:

  • End the loop when oldStartIdx > oldEndIdx or newStartIdx > newEndIdx, and cross the first and last nodes to find the same node. If not, use the oldVnode array to find the same node as newStartIdx

  • When oldStartVnode/newStartVnode or oldEndVnode/newEndVnode meet sameVnode, direct execution patchVnode updates

  • When the oldStartVnode/newEndVnode meet the conditions, the node location, after patchVnode updates, need to be oldStartVnode. Elm moves to oldEndVnode. Behind the elm

  • When the oldEndVnode/newStartVnode meet the conditions, the node location, after patchVnode updates, need to be oldEndVnode. Elm mobile to oldStartVnode. The front of the elm

  • If none of the above conditions are met, the same node is not found at the beginning and end
  • If newStartVnode has a key, the same node in the old Vnode is directly searched by the key value.

  • If no, search for the sameVnode node in old Vnode that meets the sameVnode requirement with newStartVnode.

  • If vnodeToMove exists, move vnodeToMove. Elm to the front of oldStartvNode. elm.

  • If not, create a new DOM node and insert it in front of oldStartvNode.elm

SameVnode:

Key // 2. Tag // 3. IsComment is a comment node // 4. Function sameVnode (a, b) { return ( a.key === b.key && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error) ) ) ) }Copy the code

End of the cycle

  • When oldStartIdx > oldEndIdx, it indicates that the old node has been traversed. Determine whether there are any new nodes in the array, create them in batches and insert them into the DOM.

  • When newStartIdx > newEndIdx, the new node has been traversed and the old node array remains, removed from the DOM.

conclusion

Vue2 describes the mapping between the state and the view through the template. The template is first compiled into a rendering function, and then the rendering function is executed to generate virtual nodes. In order to avoid unnecessary DOM operations, the virtual node is compared with the last virtual node to find out the node that really needs to be updated for DOM operations.

The reason for using the virtual DOM to update views is that vUE’s change detection can know to some extent what data has changed, which means that it can know to some extent which nodes are using these states. If each node is bound to a Watcher to observe state changes, There is some memory overhead, which increases as the state is used by more nodes. Introduce virtual DOM, each component corresponds to a Watcher, when the data changes, notify the component Watcher, and then the component through the virtual DOM comparison, complete view update, this is a compromise solution!