preface
As the core of Vue, responsive principle uses data hijacking to realize data-driven view. In the interview is often examined knowledge points, but also interview bonus items.
This paper will analyze the workflow of response principle step by step, mainly in the following structure:
- Analyze key members and understand them to help understand the process
- Split the process and understand its role
- Combined with the above points, understand the overall process
The article is a little long, but most of it is code implementation, please watch patiently. In order to facilitate the understanding of the principle, the code in this article will be simplified. If you can, please refer to the source code for learning.
leading member
In the responsive principle, the three classes of Observe, Watcher and Dep are the main members of the complete principle.
- Observe, the entrance of the responsive principle, processes the observation logic according to the data type
- Watcher, which is used to perform update rendering. The component will have a rendering watcher. We often say that collection dependency is to collect watcher
- Dep, dependency collector and attribute all have a Dep, which is convenient to find the corresponding dependency trigger update in case of change
Let's take a look at the implementation of these classes, including the main properties and methods.
Observe: I will observe the data
Warm tip: the serial number in the code corresponds to the explanation of the serial number below the code block
// Source location / src/core/observer/index.js class Observe { constructor(data) { this.dep = new Dep() // 1 def(data, '__ob__', this) if (Array.isArray(data)) { // 2 protoAugment(data, arrayMethods) // 3 this.observeArray(data) } else { // 4 this.walk(data) } } walk(data) { Object.keys(data).forEach(key => { defineReactive(data, key, data[key]) }) } observeArray(data) { data.forEach(item => { observe(item) }) } }
- Add for observed properties__ ob__ Property, whose value is equal to this, that is, the current Observe instance
- Add overridden array methods to the array, such as push, unshift, splice and so on. The purpose of overriding is to update and render when these methods are called
- Observe the data in the array. new Observe will be called inside the observe to form a recursive observation
- For observation object data, defineReactive defines get and set for data, i.e. data hijacking
Dep: I will rely on for data collection
// Source location / src/core/observer/dep.js let id = 0 class Dep{ constructor() { this.id = ++id // dep unique identification this.subs = [] // Store Watcher } // 1 depend() { Dep.target.addDep(this) } // 2 addSub(watcher) { this.subs.push(watcher) } // 3 notify() { this.subs.forEach(watcher => watcher.update()) } } // 4 Dep.target = null export function pushTarget(watcher) { Dep.target = watcher } export function popTarget(){ Dep.target = null } export default Dep
- The main methods relied on for data collection, Dep.target Is a watcher instance
- Add watcher to array, that is, add dependency
- When the property changes, the notify method will be called to notify each dependency to update
- Dep.target It is used to record the watcher instance, which is globally unique. Its main purpose is to find the corresponding Watcher in the process of collecting dependencies
pushTarget and popTarget are two obvious methods for setting Dep.target Of. Dep.target It is also a key point. It may be difficult to understand this concept when viewing the source code for the first time. In the later process, we will explain its function in detail. We need to pay attention to this part of the content.
Watcher: I'll trigger the view update
// Source location / src/core/observer/watcher.js let id = 0 export class Watcher { constructor(vm, exprOrFn, cb, options){ this.id = ++id // watcher unique ID this.vm = vm this.cb = cb this.options = options // 1 this.getter = exprOrFn this.deps = [] this.depIds = new Set() this.get() } run() { this.get() } get() { pushTarget(this) this.getter() popTarget(this) } // 2 addDep(dep) { // Prevent adding dep repeatedly if (!this.depIds.has(dep.id)) { this.depIds.add(dep.id) this.deps.push(dep) dep.addSub(this) } } // 3 update() { queueWatcher(this) } }
- this.getter Stored are functions that update the view
- The watcher stores the DEP, and the dep also stores the watcher for bidirectional recording
- Trigger the update. queueWatcher is for asynchronous update. Asynchronous update will call run method to update the page
Responsive principle process
We have a general understanding of the functions of the above members. Let's combine them to see how these functions work in a responsive principle process.
Data observation
The data will be initialized to create the observe class through the observe method
// Source location / src/core/observer/index.js export function observe(data) { // 1 if (!isObject(data)) { return } let ob; // 2 if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observe) { ob = data.__ob__ } else { // 3 ob = new Observe(data) } return ob }
During initialization, the data obtained by observe is the object returned in the data function.
- observe function only observes data of object type
- The observed data will be added__ ob__ Attribute. It can prevent repeated observation by judging whether the attribute exists
- Create the Observe class and start processing the observation logic
Object observation
Enter the Observe internal, because the initialized data is an object, the walk method will be called:
walk(data) { Object.keys(data).forEach(key => { defineReactive(data, key, data[key]) }) }
Internal use of defineReactive method Object.defineProperty Hijacking data is the core of the responsive principle.
function defineReactive(obj, key, value) { // 1 let childOb = observe(value) // 2 const dep = new Dep() Object.defineProperty(obj, key, { get() { if (Dep.target) { // 3 dep.depend() if (childOb) { childOb.dep.depend() } } return value }, set(newVal) { if (newVal === value) { return } value = newVal // 4 childOb = observe(newVal) // 5 dep.notify() return value } }) }
- Because the value may be of object type, observe needs to be called for recursive observation
- The dep here is that every attribute mentioned above will have a DEP, which exists as a closure and is responsible for collecting dependency and notification updates
- At initialization, Dep.target Is the component's render watcher, here dep.depend This watcher is the dependence of collection, childOb.dep.depend Mainly collect dependencies for arrays
- The new value set may be of the object type and needs to be observed
- Value changes, dep.notify Notify the watcher to update, which is the trigger point for us to update the page in real time after changing the data
Through Object.defineProperty After the property is defined, the get callback is triggered when the property is acquired, and the set callback is triggered when the property is set, so as to realize the responsive update.
Through the above logic, we can also find out why Vue3.0 uses proxy instead of Object.defineProperty Yes. Object.defineProperty You can only define a single attribute. If the attribute is an object type, you need to recursively observe it, which will consume performance. Proxy is the proxy for the whole object, and the callback will be triggered whenever the property changes.
Array observation
For array type observations, the observeArray method is called:
observeArray(data) { data.forEach(item => { observe(item) }) }
Unlike objects, it performs observe to observe the types of objects in the array, not every item in the array Object.defineProperty In other words, there is no dep for the items in the array.
Therefore, when we modify an item through an array index, the update will not be triggered. But you can modify the trigger update through this.$set. So the question is, why does Vue design this way?
Combined with the actual scenario, the array usually holds multiple data, such as list data. This observation will consume performance. Another reason is that generally, modifying array elements rarely replaces the whole element directly through index. For example:
export default { data() { return { list: [ {id: 1, name: 'Jack'}, {id: 2, name: 'Mike'} ] } }, cretaed() { // If you want to change the value of name, you usually use this.list[0].name = 'JOJO' // Not the following // this.list[0] = {id:1, name: 'JOJO'} // Of course you can update it like this // this.$set(this.list, '0', {id:1, name: 'JOJO'}) } }
Array method override
When an array element is added or deleted, the view is updated. This is not a matter of course, but Vue internally rewrites the array methods. When these methods are called, the array will update and detect, triggering the view update. These methods include:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
Back in the Observe class, when the observed data type is an array, the protoaugust method is called.
if (Array.isArray(data)) { protoAugment(data, arrayMethods) // Watch array this.observeArray(data) } else { // Observation object this.walk(data) }
In this method, the array prototype is replaced by arrayMethods. When calling the array changing method, the overridden method is preferred.
function protoAugment(data, arrayMethods) { data.__proto__ = arrayMethods }
Next let's see how arrayMethods are implemented:
// Source location / src/core/observer/array.js // 1 let arrayProto = Array.prototype // 2 export let arrayMethods = Object.create(arrayProto) let methods = [ 'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice' ] methods.forEach(method => { arrayMethods[method] = function(...args) { // 3 let res = arrayProto[method].apply(this, args) let ob = this.__ob__ let inserted = '' switch(method){ case 'push': case 'unshift': inserted = args break; case 'splice': inserted = args.slice(2) break; } // 4 inserted && ob.observeArray(inserted) // 5 ob.dep.notify() return res } })
- Save the prototype of the array, because in the rewritten array method, you still need to call the native array method
- arrayMethods is an object for saving overridden methods, which are used here Object.create(arrayProto) objects are created so that users can inherit and use native methods when calling non overriding methods
- Call the native method to store the return value, which is used to set the return value of the rewriting function
- Inserted stores the new value. If inserted exists, observe the new value
- ob.dep.notify Trigger view update
Dependency collection
Dependency collection is not only the premise of view updating, but also the crucial part of responsive principle.
Pseudo code flow
To facilitate understanding, write a piece of pseudo code here, about the process of relying on Collection:
// Data data let data = { name: 'joe' } // Render watcher let watcher = { run() { dep.tagret = watcher document.write(data.name) } } // dep let dep = [] // Storage dependency dep.tagret = null // Record watcher // Data hijacking Object.defineProperty(data, 'name', { get(){ // Collect dependencies dep.push(dep.tagret) }, set(newVal){ data.name = newVal dep.forEach(watcher => { watcher.run() }) } })
initialization:
- First define get and set for the name property
- Then initialization is performed once watcher.run Render Page
- At this time, get data.name , trigger the get function to collect dependencies.
to update:
Modification data.name , trigger the set function and call run to update the view.
Real process
Let's see how the real dependency collection process works.
function defineReactive(obj, key, value) { let childOb = observe(value) const dep = new Dep() Object.defineProperty(obj, key, { get() { if (Dep.target) { dep.depend() // Collect dependencies if (childOb) { childOb.dep.depend() } } return value }, set(newVal) { if (newVal === value) { return } value = newVal childOb = observe(newVal) dep.notify() return value } }) }
First, initialize the data and call the defineReactive function to hijack the data.
export class Watcher { constructor(vm, exprOrFn, cb, options){ this.getter = exprOrFn this.get() } get() { pushTarget(this) this.getter() popTarget(this) } }
Initialize to mount the watcher to Dep.target , this.getter Start rendering the page. When rendering a page, you need to take a value for the data and trigger the get callback, dep.depend Collect dependencies.
class Dep{ constructor() { this.id = id++ this.subs = [] } depend() { Dep.target.addDep(this) } }
Dep.target For the watcher, call the addDep method and pass in the dep instance.
export class Watcher { constructor(vm, exprOrFn, cb, options){ this.deps = [] this.depIds = new Set() } addDep(dep) { if (!this.depIds.has(dep.id)) { this.depIds.add(dep.id) this.deps.push(dep) dep.addSub(this) } } }
After adding DEP in addDep, call dep.addSub And pass in the current watcher instance.
class Dep{ constructor() { this.id = id++ this.subs = [] } addSub(watcher) { this.subs.push(watcher) } }
Collect the incoming watcher, and the dependency collection process is completed.
To add, usually there are many attribute variables bound to the page, and the rendering will take values for the attributes. At this time, each attribute collection depends on the same watcher, that is, the component's rendering watcher.
Dependency collection of arrays
methods.forEach(method => { arrayMethods[method] = function(...args) { let res = arrayProto[method].apply(this, args) let ob = this.__ob__ let inserted = '' switch(method){ case 'push': case 'unshift': inserted = args break; case 'splice': inserted = args.slice(2) break; } // Observation of new values inserted && ob.observeArray(inserted) // update the view ob.dep.notify() return res } })
Remember that in the overridden method, the ob.dep.notify Update view__ ob__ It is the identification defined for observation data in Observe. The value is the Observe instance. So ob.dep Where are the dependencies collected?
function defineReactive(obj, key, value) { // 1 let childOb = observe(value) const dep = new Dep() Object.defineProperty(obj, key, { get() { if (Dep.target) { dep.depend() // 2 if (childOb) { childOb.dep.depend() } } return value }, set(newVal) { if (newVal === value) { return } value = newVal childOb = observe(newVal) dep.notify() return value } }) }
- The return value of the Observe function is the Observe instance
- childOb.dep.depend Execute, add dependency for dep of Observe instance
So when the array is updated, ob.dep Dependency has been collected in.
Overall process
Next, go through the initialization process and update process. If you are looking at the source code for the first time, and don't know where to start, you can also refer to the following order. Because there are many source code implementations, the source code shown below will be slightly reduced
Initialization process
Entry file:
// Source location / src/core/instance/index.js import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index' function Vue (options) { this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue
_init:
// Source location / src/core/instance/init.js export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { // mergeOptions merges the mixin options and the options passed in // The $options here can be understood as the object passed in when new Vue vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props // Initialization data initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') if (vm.$options.el) { // Initialize the render page mount component vm.$mount(vm.$options.el) } } }
The above focuses on two functions: initState initialization data, VM. $mount (VM$ options.el )Initializes the rendered page.
Enter initState first:
// Source location / src/core/instance/state.js 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) { // data initialization 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) } } function initData (vm: Component) { let data = vm.$options.data // When data is a function, execute the data function and take out the return value data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} // 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 (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) } } // observe data // Here we go to the logic of observation data observe(data, true /* asRootData */) }
The internal process of observe has been mentioned above, and it's simple here:
- new Observe data
- defineReactive hijacking data
After the initState logic is executed, go back to the beginning, and then execute VM. $mount (VM$ options.el )Render page:
$mount:
// Source location / src/platforms/web/runtime/index.js Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
mountComponent:
// Source location / src/core/instance/lifecycle.js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}` mark(startTag) const vnode = vm._render() mark(endTag) measure(`vue ${name} render`, startTag, endTag) mark(startTag) vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } else { // This method is called when the data changes updateComponent = () => { // vm._render() returns vnode, which will take the value of data data // vm._update turns vnode into real dom and renders it to the page vm._update(vm._render(), hydrating) } } // Execute Watcher, which is the rendering wacther mentioned above new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
Watcher:
// Source location / src/core/observer/watcher.js let uid = 0 export default class Watcher { constructor(vm, exprOrFn, cb, options){ this.id = ++id this.vm = vm this.cb = cb this.options = options // exprOrFn is the updateComponent passed in above this.getter = exprOrFn this.deps = [] this.depIds = new Set() this.get() } get() { // 1. pushTarget records the current watcher to Dep.target , Dep.target It's unique pushTarget(this) let value const vm = this.vm try { // 2. Call this.getter It is equivalent to executing VM_ Render function, which takes the value of the attribute on the instance, //This triggers Object.defineProperty In the get method of the( dep.depend ), which depends on collection Dep.target value = this.getter.call(vm, 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) } // 3. popTarget will Dep.target Empty popTarget() this.cleanupDeps() } return value } }
At this point, the initialization process is completed. The main tasks of the initialization process are data hijacking, page rendering and collection dependency.
Update process
Data changes, trigger set, execute dep.notify
// Source location / src/core/observer/dep.js let uid = 0 /** * A dep is an observable that can have multiple * directives subscribing to it. */ 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) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !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((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { // Execute the update method of the watcher subs[i].update() } } }
wathcer.update:
// Source location / src/core/observer/watcher.js /** * Subscriber interface. * Will be called when a dependency changes. */ update () { /* istanbul ignore else */ if (this.lazy) { // Calculation property update this.dirty = true } else if (this.sync) { // Synchronize updates this.run() } else { // General data will be updated asynchronously queueWatcher(this) } }
queueWatcher:
// Source location / src/core/observer/scheduler.js // Used to store watcher const queue: Array<Watcher> = [] // For watcher de duplication let has: { [key: number]: ?true } = {} /** * Flush both queues and run the watchers. */ function flushSchedulerQueue () { let watcher, id // Sort watcher s queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed // as we run existing watchers for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id has[id] = null // run method update view watcher.run() } } /** * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the queue is being flushed. */ export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // watcher adding array queue.push(watcher) // Asynchronous update nextTick(flushSchedulerQueue) } }
nextTick:
// Source location / src/core/util/next-tick.js const callbacks = [] let pending = false function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 // Traversal callback function execution for (let i = 0; i < copies.length; i++) { copies[i]() } } let timerFunc if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) } } export function nextTick (cb?: Function, ctx?: Object) { let _resolve // Add callback function to array callbacks.push(() => { if (cb) { cb.call(ctx) } }) if (!pending) { pending = true // Traversal callback function execution timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
This step is to use the micro task to execute the callback function asynchronously, which is p.then above. Finally, the watcher.run Update the page.
This completes the update process.
Write at the end
If I haven't contacted the source code students, I believe I will be a little confused after reading it, which is very normal. It is recommended that you read the source code several times to know the process. For the students who have a foundation, it's like reviewing.
Want to become strong, learn to see the source code is the only way. In this process, we can not only learn the design idea of the framework, but also cultivate our own logical thinking. It is difficult to start everything. Sooner or later, we should take this step. It is better to start from today.
I have put the simplified code in github , you can have a look if you need.