Object. DefinePrototype sets getters and setters for object properties to listen for data changes. Today we will explore the responsivity principle of vue.
object.definePrototype
Syntax: Object.defineProperty(obj, prop, Descriptor)
The base application
/* * Params obj listener object prop Listener attribute Descriptor */
let _obj = {
name: 'Rookie'
},
tag = 'Old dog';
Object.defineProperty(_obj, 'name', {
get: () = > {
console.log('trigger getter');
return tag; // Return the tag variable
},
set: newVal= > {
console.log("Trigger setter", newVal);
returnnewVal; }})console.log(_obj.name); // Trigger the getter
_obj.name = 'Touch the fish' // Trigger the setter to touch the fish
Copy the code
From the above code, you can see that we use object.defineProperty to listen on the name attribute of _obj, trigger GET, return the tag variable (old grease), trigger SET, return newVal (touch the fish). Vue2. X uses object.deineProperty API to intercept get and set of objects to realize object data responsiveness.
The Vue2. X response is done in the Observer. Let’s explore the Observer
The Observer responsive
Responsive process
- From new Vue() we know that the _init() method executes the initState() method;
- The initData() method is executed in the initState() method;
- Call the observe method in initData;
- Observe instantiate new Observer in observe;
- Observer instantiates a reactive object that calls the defineReactive method;
// 1, initState * SRC \core\instance\state
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // initProps
if (opts.methods) initMethods(vm, opts.methods) // initMethods
if (opts.data) { // The component has data
initData(vm) // initData
} else {
observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch)// initWatch
}
// Here we can see the props, methods, data, and watch priorities
}
// 2, initData * SRC \core\instance\state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// *** *
observe(data, true /* asRootData */) // observe data
}
// 3, observe * SRC \core\observer\index.js
export function observe (value: any, asRootData: ? boolean) :Observer | void {
// Return if it is not an object or virtual DOM
if(! isObject(value) || valueinstanceof VNode) {
return
}
let ob: Observer | void
// Check if there is __ob__ if there is, the data has been observed and assigned to the return value
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve && // can be observed! isServerRendering() &&// Not server rendering
(Array.isArray(value) || isPlainObject(value)) && // Is an array or object
Object.isExtensible(value) && // Is an extensible property! value._isVue// Not a VUE instance
) {
ob = new Observer(value) // Instance observer
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
// 4, Observer * SRC \core\ Observer \index.js
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 // The value to be observed
this.dep = new Dep() // Create a dependency collector
this.vmCount = 0
// Assign the Observer instance to the __ob__ property of value.
def(value, '__ob__'.this)
// Determine the data type
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value) // Array observation method
} else {
this.walk(value) // The object performs the walk method}}/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */
walk (obj: Object) { // Get an array of the object's key values to traverse the monitor key
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // Responsive object core}}/** * Observe a list of Array items. */
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
DefineReactive * SRC \core\observer\index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
const dep = new Dep()
// Get the object property descriptor
const property = Object.getOwnPropertyDescriptor(obj, key)
// The property descriptor exists and the 64x property cannot be configured
if (property && property.configurable === false) {
return
}
// Property descriptors exist to get getters, setters
const getter = property && property.get
const setter = property && property.set
// Return obj[key] with no getter, setter and length of 2
if((! getter || setter) &&arguments.length === 2) {
val = obj[key]
}
Shallow indicates whether the value is deeply processed.
// this means that if obj's key, corresponding to val, is still an object, it becomes an observable
letchildOb = ! shallow && observe(val)// Listen to the key of the object
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// Dependencies are collected at specific times. By Dep. Target
if (Dep.target) {
dep.depend() // Rely on collection
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()
}
// #7981: for accessor properties without setter
if(getter && ! setter)return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// Determine whether the set value is an object recursive observationchildOb = ! shallow && observe(newVal) dep.notify()// Trigger the update}})}Copy the code
The paper process
The initState() method is executed in the _init() method;
initState()
// the initState() method executes the initData() method;
function initState(vm){
let data = vn.$options.data;
initData(data)
}
// Call observe in initData
function initData(data) {
observe(data, true)}// Observe instantiates the Observer
function observe() {
new Observer(value)
}
// An Observer object executes the walk method
class Observer{
this.walk(obj);
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
The defineReactive method is used to set the property descriptor of the object
function defineReactive(obj, key) {
const dep = new Dep()
// Omit some code
Object.defineProperty(obj, key, {
// Attribute descriptor
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend() // Collect dependencies
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
dep.notify() // Trigger the update}})}// Object type data response process
// Finally, by triggering the object data get collection dependency (dep.depend), triggering the set call dep.notify() to achieve data view responsiveness
Copy the code
At this point, reactive binding of objects is implemented
Dep dependency management
I believe that many people do not understand the concept of Dep. What is Dep used for? Now that we’ve implemented responsive binding of objects, we can listen for changes in the data, so how do we update the view, and where exactly?
A Dep can be thought of as a container for collecting the specifics of where to update to, collecting dependencies (watcher) when data gets triggered, setting when data changes, and updating views.
// *src\core\observer\dep.js
// Used as a Dep identifier
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
statictarget: ? Watcher; id: number; subs:Array<Watcher>;
// Define a subs array to store the watcher instance
constructor () {
this.id = uid++
this.subs = []
}
// Add the watcher instance to subs
addSub (sub: Watcher) {
this.subs.push(sub)
}
// Remove the corresponding watcher instance from subs.
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// Dependency collection, which is the dep.dpend method seen above
depend () {
// dep. target is the watcher instance
if (Dep.target) {
// Call the addDep method of the watcher instance with the current DEP instance
Dep.target.addDep(this)}}// Dispatch updates, which is the dep.notify method we saw earlier
notify () {
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)
}
// Iterate through the subs array, triggering the Update of the Watcher instance in turn
for (let i = 0, l = subs.length; i < l; i++) {
// Call the watcher instance update method
subs[i].update()
}
}
}
// Attach a static attribute target to the Dep,
// The dep. target value is assigned to the current Watcher instance object when pushTarget and popTarget are called.
Dep.target = null
// Maintain a stack structure for storing and deleting dep.target
const targetStack = []
// pushTarget will be called on new Watcher
export function pushTarget (_target: ? Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
// popTarget is also called on new Watcher
export function popTarget () {
Dep.target = targetStack.pop()
}
Copy the code
Dep is a class that relies on collecting and distributing updates, that is, storing the Watcher instance and triggering the Update method on the Watcher instance.
Reactive summary
At this point initData is done. The getter and setter Settings for Define Active above are not triggered at the beginning because dependencies have not been collected yet. Here we just define the rules and use them in conjunction with the template. Next, let’s look at where the template is mounted.
// post the _init method to consolidate the initial procedure * SRC \core\instance\init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options? :Object) {
const vm: Component = this
// Omit some code
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm) // *** completes the responsivity to the data but at this point no dependencies have been collected
initProvide(vm)
callHook(vm, 'created')
// Omit some code
// Mount phase
if (vm.$options.el) {
vm.$mount(vm.$options.el) // *** The dependencies are actually collected during the mount phase}}}Copy the code
$mount
The Vue prototype defines $mount in two places, but ultimately calls the mountComponent method defined in the first place
// the first definition
// 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)
}
Copy the code
// The second definition
// src\platforms\web\entry-runtime-with-compiler.js
var mount = Vue.prototype.$mount; // The $mount method defined in the first section is saved
Vue.prototype.$mount = function (el, hydrating) {
el = el && query(el); // Query is used to find dom elements
// el cannot be body, HTML
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}
var options = this.$options;
// We need to check whether the render function is initialized
// The render function returns vNode
if(! options.render) {var template = options.template;
// There is no template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) = = =The '#') {
template = idToTemplate(template);
/* istanbul ignore if */
if(! template) { warn( ("Template element not found or is empty: " + (options.template)),
this); }}}else if (template.nodeType) {
template = template.innerHTML;
} else {
{
warn('invalid template option:' + template, this);
}
return this}}else if (el) {
// 有el
template = getOuterHTML(el);
}
// Generate the render function
if (template) {
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile');
}
var ref = compileToFunctions(template, {
outputSourceRange: "development"! = ='production'.shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render;
options.staticRenderFns = staticRenderFns;
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile end');
measure(("vue " + (this._name) + " compile"), 'compile'.'compile end'); }}}/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Call the variable mount (actually calling the first defined $mount method to call the mountComponent method)
return mount.call(this, el, hydrating)
};
Copy the code
mountComponent
// src\core\instance\lifecycle.js
export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
vm.$el = el
// Omit some code
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 {
updateComponent = () = > {
vm._update(vm._render(), hydrating) // *** Updates the view core ***
// updateComponent calls vm._render()
Vnode = render. Call (vm, vm.$createElement)
// _update is responsible for rendering VNode into a real DOM and rendering it
// The render function takes the value of vm.a. Or read the value of vm.b.
// When vm. A or B is read, the getter for the corresponding property is triggered
// The current Watcher is then added to the deP corresponding to the property.
// When the setter for the property is triggered, dep.notify() is executed to update each watcher collected by the DEP
// update calls the run method.
// The run method triggers the current get method, and the interface is updated when getter.call is executed.}}// The core new Watcher is finally instantiated
new Watcher(vm, updateComponent, noop, { // The updateComponent method is the focus
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
}
Copy the code
After all the hardships, FINALLY waiting for you, fortunately I did not give up
Watcher
Watcher is also a class that initializes Watcher instances of data. It has an update method on its prototype to distribute updates.
SRC \core\observer\ Watcher.js SRC \core\observer\ Watcher
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function.// The callback function
options?: ?Object, isRenderWatcher? : booleanWhen Vue is initialized, this parameter is set to true
) {
// omit some code... What this code does is initialize some variables
// expOrFn can be a string or a function
// When is the key 'x' converted to a string, such as watch: {x: fn}
// New Watcher(vm, updateComponent, noop...); // New Watcher(vm, updateComponent, noop...); // New Watcher(vm, updateComponent, noop...); ;
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// When the watch form 'a.b' is used, the parsePath method returns a function
// The function internally retrieves the value of the 'a.b' attribute
this.getter = parsePath(expOrFn)
// omit some code...
}
// If it is an attribute, return undefined, otherwise get value
// this.get() will be called on new Watcher
this.value = this.lazy
? undefined
: this.get()
}
get () {
// Assign the current watcher instance to the dep. target static property
// If this line of code is executed, the dep. target is the current watcher instance
// Add dep. target to the targetStack array
pushTarget(this)
let value
const vm = this.vm
try {
// *** This. Getter executes the updateComponent method
// Let's go back to the comments for the updateComponent method
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget() / / Dep. Target out of the stack
this.cleanupDeps()
}
return value
}
// Here is a review
AddDep (dep) // Dep. depend, which executes dep.target.adddep (dep)
// addDep(dep) will execute dep.addSub(watcher)
// Add the current Watcher instance to the SUBs array of the DEP, that is, collect dependencies
// Dep. depend and the addDep method, with several this, may be a bit convoluted.
addDep (dep: Dep) {
const id = dep.id
// The following two if conditions are both deduplicated, so we can ignore them for the moment
// Just know that this method executes dep.addSub(this).
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// Add the current watcher instance to deP's subs array
dep.addSub(this)}}}// Send updates
update () {
if (this.lazy) {
this.dirty = true
// if this.sync is true, this.run is immediately executed
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
const value = this.get() // Execute the get method
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
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "The ${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// Omit some code
}
Copy the code
conclusion
Object. DefinePrototype intercepts get of object attributes to add dependencies, and triggers set of attributes to update dependencies to trigger view updates. At the heart of the update view is vm._update(vm._render(), hydrating). In the next article we’ll look at vm._update and vm._render principles.
If there are any mistakes please point out, must be the first time to learn and update.
So far we still leave a problem, that is the array data response formula, here is not repeated, hope interested JYM can go to understand.