One feature of Vue is its responsiveness to data, where changes to data are applied to the view without DOM manipulation. In principle, the Object. DefifineProperty () is used, and the setter method for Object attributes is defined to intercept the changes of Object attributes, so the changes in attribute values are converted to changes in the view. When Vue initializes, initState is called, which initializes props, Methods, data, computed, watch, and so on.
Responsive object
initState
// 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) {
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
initProps
// src/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// Cache each key of props, performance optimization
const keys = vm.$options._propKeys = []
constisRoot = ! vm.$parent// root instance props should be converted
// The case of non-root instances
if(! isRoot) {// Reactive optimization mainly optimizes the recursive process of reactive processing
toggleObserving(false)}for (const key in propsOptions) {
/ / the cache key
keys.push(key)
// Check whether the data passed conforms to the specification defined in prop
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if(process.env.NODE_ENV ! = ='production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
// Set responsiveness for each key of props
defineReactive(props, key, value, () = > {
if(! isRoot && ! isUpdatingChildComponent) { warn(`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
// Set responsiveness for each key of props
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if(! (keyin vm)) {
proxy(vm, `_props`, key)
}
}
// Reactive optimization mainly optimizes the recursive process of reactive processing
toggleObserving(true)}Copy the code
The initialization of the props is to traverse it, and the traverse does two things:
- call
defineReactive
Reactive processing is done for each value. - through
proxy
把vm._props.xxx
Access proxy tovm.xxx
On.
initData
// src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
// Determine whether data is a function or an object
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 to the VM instance.
// The properties on data cannot be the same as those on props and methods.
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 operation
proxy(vm, `_data`, key)
}
}
// Reactive operations
observe(data, true /* asRootData */)}export function getData (data: Function, vm: Component) :any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return{}}finally {
popTarget()
}
}
Copy the code
Initialization of data is similar to props, and there are three main things that are done here:
- check
data
The attributes of theprops
、methods
Have the same properties on. - through
proxy
把vm._data.xxx
Access proxy tovm.xxx
On. - call
observe
把data
The data on the.
proxy
// src/core/instance/state.js
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
The purpose of the proxy is to delegate properties on props and data to the VM instance, which is why we defined props. XXX, which can be accessed through this. XXX.
observe
// src/core/observer/index.js
export function observe (value: any, asRootData: ? boolean) :Observer | void {
// Non-objects and VNode instances do not respond
if(! isObject(value) || valueinstanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// If an __ob__ attribute exists on the value object, then the observation has been made and the __ob__ attribute is returned
ob = value.__ob__
} else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && ! value._isVue ) {// Create an observer instance
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Copy the code
Observe: Create an Observer instance Observer for a non-VNode object. If the Observer is observed successfully, return the existing Observer. Otherwise, create a new instance.
Observer
// src/core/observer/index.js
/** * Observer class that is attached to each observed * nce attached, the observer converts the target * object's property keys into getter/setters that * collect dependencies and dispatch updates. */
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
// Why declare Dep in Observer?
this.dep = new Dep()
this.vmCount = 0
// Set the __ob__ attribute on the value object, referring to the current Observer instance
def(value, '__ob__'.this)
// Determine the type
if (Array.isArray(value)) {
// Override the default seven prototype methods for arrays to implement array responsiveness
// hasProto = '__proto__' in {}
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/** * iterates over each key on the pair, setting the response * only goes here if type Object */
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/** * if the value in the array is still an object, then you need to do a response */
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Copy the code
The Observer class is attached to the object being observed, that is, every responsive object has an __ob__; Then a judgment is made on the data type; __proto__ is not a standard attribute, so some browsers do not support it, such as IE6-10. Opera10.1.
Why declare Dep in the Observer? For those of you who are familiar with reactive behavior, we should have a DEP for each key to manage dependencies and trigger setters to notify updates when the key value changes. Dep here is mainly used to add and delete Object properties, Array change methods. {a: {b: 1}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, C: 2}} What changes need to be truly updated, hand over to Brother Diff. If you don’t know the tao here, you can review the whole response.
defineReactive
// src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
// instantiate dep, one key at a time
const dep = new Dep()
// Get obj[key] property descriptor, find it is not configurable object directly return
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// Record getters and setters to get val
const getter = property && property.get
const setter = property && property.set
if((! getter || setter) &&arguments.length === 2) {
val = obj[key]
}
// A recursive call to handle the case where val is an object
letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
enumerable: true.configurable: true.// Hijack the read operation
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// Dep.target is a static attribute of Dep that holds the current Watcher instance.
// When the new Watcher is instantiated (except for computed, because it executes lazily), read artifacts are triggered and hijacked to run the get function for dependency collection.
// At the end of instantiating Watcher, dep. target is set to null to avoid double collection.
if (Dep.target) {
// Depending on the collection, add watcher to the DEP and add deP to the watcher
dep.depend()
// childOb indicates that val is still a complex type, object, or array.
if (childOb) {
// This DEP was created in the Observer, as mentioned earlier.
childOb.dep.depend()
if (Array.isArray(value)) {
// The array is still an object
dependArray(value)
}
}
}
return value
},
// Hijack the modify operation
set: function reactiveSetter (newVal) {
[key] / / the old obj
const value = getter ? getter.call(obj) : val
// If the old and new values are the same, return without updating
if(newVal === value || (newVal ! == newVal && value ! == value)) {return
}
/* eslint-enable no-self-compare */
if(process.env.NODE_ENV ! = ='production' && customSetter) {
customSetter()
}
// If the setter does not exist, the property is read-only and returns directly
if(getter && ! setter)return
// Set the new value
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// Observe the new value so that it is also responsivechildOb = ! shallow && observe(newVal)// Rely on notification updates
dep.notify()
}
})
}
Copy the code
// src/core/observer/index.js
/** * iterates through each element of the array, recursively handling the case that the array element is an object, adding dependencies to it
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
Copy the code
DefineReactive is used to hijack reading and writing of data using Object.defineProperty, adding getters and setters to the property key for dependency collection and notification of updates. If the value passed in is still an object, recursively call the Observe method to ensure that all the child properties become responsive.
Depend on the collection
Dep
// src/core/observer/dep.js
/* @flow */
import type Watcher from './watcher'
import { remove } from '.. /util/index'
import config from '.. /config'
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>;
constructor() {
this.id = uid++
this.subs = []
}
// Add a subscription and save the Watcher instance to subs
addSub (sub: Watcher) {
this.subs.push(sub)
}
// Remove the subscription to remove the Watcher instance from subs
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// Add deP to Watcher
depend () {
if (Dep.target) {
Dep.target.addDep(this)}}// Notification update
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)
}
// Go through the watcher stored in the DEP and execute watcher.update()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
/** * Only one watcher is executing at a time * dep. target = the current executing watcher, and the assignment is completed by calling pushTarget. Call the popTarget method to complete the reset
Dep.target = null
const targetStack = []
export function pushTarget (target: ? Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]}Copy the code
Watcher
// src/core/observer/watcher.js
/* @flow */
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError,
invokeWithErrorHandling,
noop
} from '.. /util/index'
import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'
import type { SimpleSet } from '.. /util/index'
let uid = 0
/** * One watcher per component (render watcher) or one watcher per expression (user watcher) * Watcher will be triggered when data is updated, Watcher */ will also be triggered when visiting this.computedProperty
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, isRenderWatcher? : boolean) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.syncthis.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
? expOrFn.toString()
: ' '
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// this.getter = function() { return this.xx }
// Dependency collection is triggered when this.getter is executed in this.get
// Reactive will be triggered when this.xx is updated later
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop process.env.NODE_ENV ! = ='production' && warn(
`Failed watching path: "${expOrFn}"` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
Getter is the second argument passed when watcher is instantiated, a function or string, such as: The function returned by updateComponent or parsePath that reads the value of this.xx * why collect dependencies again? * Dependencies are not collected */ because the observe-observed dependencies are not collected */ because the render function is re-executed when the page is updated and the read operation is triggered
get () {
// called when dependency collection is needed
// Set targetstack.push (target) and dep.target = watcher
pushTarget(this)
let value
const vm = this.vm
try {
// Execute the callback function, such as updateComponent, to enter the patch phase
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
// Rely on the collection to end the call
// Set targetstack.pop () and targetStack[targetstack.length-1]
popTarget()
// Clear dependencies
this.cleanupDeps()
}
return value
}
/** * add dep to watcher */
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// Save the ID for deduplicating
this.newDepIds.add(id)
// Add dep to current watcher
this.newDeps.push(dep)
// Avoid adding watcher repeatedly in deP
if (!this.depIds.has(id)) {
// Add the current watcher to dep
dep.addSub(this)}}}/** * Clear dependencies, each data change is rerendered, * then the vm._render() method is executed again, and the data getters are triggered again, So Watcher initializes two arrays of Dep instances in the constructor: * this.deps represents the last array of Dep instances, and this.newDeps represents the newly added array of Dep instances. * /
cleanupDeps () {
let i = this.deps.length
// Iterate through deps to remove the subscription to the Wathcer in dep.subs array
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)}}// newDepIds swaps with depIds and empties newDepIds
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
// newDeps swaps with deps, then empties newDeps
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/ /... Here are a few more ways to look at it
}
Copy the code
Dependency Collection process
Recall that there is logic in the mountComponent for performing a mount.
// src/core/instance/lifecycle.js
updateComponent = () = > {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
Copy the code
Collection process:
- When instantiated
Watcher
Is executedWatcher
Constructorthis.get()
. get
Is called first inpushTarget(this)
In fact, it isDep.target = The Watcher currently being executed
And push it onto the stack.- Then perform
value = this.getter.call(vm, vm)
。 this.getter
And that corresponds toupdateComponent
What is actually executed isvm._update(vm._render(), hydrating)
; It calls firstvm._render()
.vm._render()
That generates a VNode, which triggers access to the data, which triggers the getter.- Per object
key
There will always be a correspondingdep
That’s called in the getterdep.depend()
, will callDep.target.addDep(this)
; Because at step 2Dep.target = The Watcher currently being executed
, so the call should beThe currently executing watcher.adddep (this)
. addDep
In the case of not repeated calldep.addSub(this)
, will be executedthis.subs.push(sub)
, that is, theWatcher
Save the instance todep
的subs
In the.- in
vm._render()
This triggers the getter for all the data, which actually completes the dependency collection process, butWatcher
Constructorthis.get()
There are some more operations to follow. if (this.deep) { traverse(value) }
We’re going to recursevalue
That triggers all of its childrengetter
。- Then perform
popTarget()
, dependency collection ends, resetDep.target
. - And then finally, call
this.cleanupDeps()
To clear dependencies.
Array response
// src/core/observer/index.js
export class Observer {
// ...
constructor(value: any) {
// ...
if (Array.isArray(value)) {
// Override the default seven prototype methods for arrays to implement array responsiveness
// hasProto = '__proto__' in {}
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
// ...}}}Copy the code
Remember the code mentioned above in the Observer? Special treatment is made for arrays of type. Determine that __proto__ is a compatible notation because some browsers do not support it.
protoAugment
// src/core/observer/index.js
/** * Set the target.__proto__ prototype object to SRC * such as the array object, arr.__proto__ = arrayMethods */
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
Copy the code
copyAugment
// src/core/observer/index.js
/** * uses def, object.defineProperty, to define its own property values * such as array: define the seven methods */ for array objects
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
Copy the code
def
// src/core/util/lang.js
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
Seven methods
// src/core/observer/array.js
Prototype * When accessing the seven methods on the arrayMethods object, it will be hijacked to implement array-responsive */
import { def } from '.. /util/index'
// Back up the array prototype object
const arrayProto = Array.prototype
// Create a new arrayMethods by inheritance
export const arrayMethods = Object.create(arrayProto)
// Operate on an array of seven methods
const methodsToPatch = [
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
/** * intercepts the method and fires the event */
methodsToPatch.forEach(function (method) {
// Cache native methods such as push
const original = arrayProto[method]
// def is object.defineProperty, which hijacks access to ArrayMethods. method
def(arrayMethods, method, function mutator (. args) {
// Implement native methods like push.apply(this, args)
const result = original.apply(this, args)
const ob = this.__ob__
// If method is one of the following, a new value is inserted
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// Respond to the newly inserted value
if (inserted) ob.observeArray(inserted)
// Notification update
ob.dep.notify()
return result
})
})
Copy the code
The array method rewrite does a few things:
arrayMethods
inheritedArray
.- Seven methods in an array that can change the array itself are hijacked and overwritten so that when they are called, the overwritten method is called.
- The rewritten method first invokes the logic from the original prototype.
- Identify three ways you can add values
push
,unshift
,splice
, gets the newly inserted value for response processing. - The last call
ob.dep.notify()
Notification update.
$set and $delete
In the application, the data is set to be responsive during initialization. But the data for the response is defined and declared at initialization. If you add attributes to data, it does not exist at initialization. For example: {a: {b: 1}} adds an attribute c -> {a: {b: 1, c: 2}}, attribute C does not exist in the initialization phase, so how does it respond? Vue provides the global API Vue. Set, Vue. Delete and instance methods vm.$set, vm.$delete to handle the addition and removal of object attributes, ensuring that updated views are triggered. Let’s look at some definitions of this method.
// src/core/global-api/index.js
import { set, del } from '.. /observer/index'
export function initGlobalAPI (Vue: GlobalAPI) {
// ...
Vue.set = set
Vue.delete = del
}
Copy the code
// src/core/instance/state.js
import {
set,
del,
observe,
defineReactive,
toggleObserving
} from '.. /observer/index'
export function stateMixin (Vue: Class<Component>) {
// ...
Vue.prototype.$set = set
Vue.prototype.$delete = del
}
Copy the code
Set and vue. delete are the same as the instance methods vm.$set and vm.$delete.
set
// src/core/observer/index.js
Val * If target is an object and the key does not already exist, set the response for the new key, and then execute dependency notification */
export function set (target: Array<any> | Object, key: any, val: any) :any {
if(process.env.NODE_ENV ! = ='production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}Vue. Set (array, IDx, val)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
Vue. Set (obj, key, val)
if (key intarget && ! (keyin Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// Can't add dynamic add response attributes to Vue instances or $data, one of vmCount's uses,
// Ob. vmCount = 1 for this.$data
if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// Target is not a reactive object. The new property will be set, but it will not be reactive
if(! ob) { target[key] = valreturn val
}
// Define new attributes for the object, set responsiveness via defineReactive method, and trigger dependency updates
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
Copy the code
del
// src/core/observer/index.js
/** * deletes the specified key * array of the target object via vue. delete or vm.$delete using the splice method. The object deletes the specified key using the delete operator and performs dependency notification */
export function del (target: Array<any> | Object, key: any) {
if(process.env.NODE_ENV ! = ='production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)}// If target is an array, use the splice method to delete the element with the specified subscript
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
// Avoid deleting Vue instance attributes or $data
if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
// If the attribute does not exist, end it
if(! hasOwn(target, key)) {return
}
// Delete the attributes of an object with the delete operator
delete target[key]
if(! ob) {return
}
// Perform dependency notification
ob.dep.notify()
}
Copy the code
methods
initMethods
// src/core/instance/state.js
function initMethods (vm: Component, methods: Object) {
// Get the props configuration item
const props = vm.$options.props
// Iterate over the methods object
for (const key in methods) {
if(process.env.NODE_ENV ! = ='production') {
if (typeofmethods[key] ! = ='function') {
warn(
`Method "${key}" has type "The ${typeof methods[key]}" in the component definition. ` +
`Did you reference the function correctly? `,
vm
)
}
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
vm[key] = typeofmethods[key] ! = ='function' ? noop : bind(methods[key], vm)
}
}
Copy the code
Iterating through a Methods object and processing each one does several things:
- check
methoss[key]
It has to be a function. - check
methoss[key]
Can’t withprops
In the same. - check
methoss[key]
Cannot be the same as the method on the instance, usually some built-in method, such as to$
和_
The way to start. - will
methods[key]
In thevm
Instance.
Compute properties vs. listen properties
computed
// src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
// Iterate over a computed object
for (const key in computed) {
/** * computed = { * key1: function() { return xx }, * } */
const userDef = computed[key]
// getter = function() { return xx }
const getter = typeof userDef === 'function' ? userDef : userDef.get
if(process.env.NODE_ENV ! = ='production' && getter == null) {
warn(
`Getter is missing for computed property "${key}". `,
vm
)
}
if(! isSSR) {// Create an instance of Watcher for the computed property, which is computed Watcher differently than rendered Watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
// Config item, computed is lazy by default
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if(! (keyin vm)) {
// Proxy attributes in computed objects to VM instances
defineComputed(vm, key, userDef)
} else if(process.env.NODE_ENV ! = ='production') {
// Attributes of computed cannot be the same as those of data, props, and methods.
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
/** * Proxies key in computed objects to target (VM) */
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
constshouldCache = ! isServerRendering()if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else{ sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if(process.env.NODE_ENV ! = ='production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`.this)}}// The proxy accesses computed[key] and sets get and set for computed[key]
Object.defineProperty(target, key, sharedPropertyDefinition)
}
/** * returns a function as the getter for computed[key] */
function createComputedGetter (key) {
// The principle that computed attribute values are cached is also implemented here in combination with watcher.dirty, watcher.evalaute, and watcher.update
return function computedGetter () {
// Get computed Watcher for computed[key]
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
/ / cache
if (watcher.dirty) {
watcher.evaluate()
}
// Add a subscription
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
/** * functions as createComputedGetter */
function createGetterInvoker (fn) {
return function computedGetter () {
return fn.call(this.this)}}Copy the code
As you can see from the above code, the initialization of the computed property does several things:
- right
computed[key]
createWatcher
Instance. - check
computed
The property of thedata
,props
,methods
Property with the same name. - The agent
computed[key]
到vm
Is on the instance, and sets the getter and setter.
Process analysis
As mentioned earlier, during the initialization of computed properties, an instance of Watcher is created for computed[key], which is not quite the same as rendered Watcher. In Watcher’s constructor there is this logic:
// src/core/observer/watcher.js
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, isRenderWatcher? : boolean) {
if (options) {
this.lazy = !! options.lazy }this.dirty = this.lazy // for lazy watchers
this.value = this.lazy
? undefined
: this.get()
}
Copy the code
Default lazy execution, not immediately evaluated. When the render function executes a call to the calculated property, it fires the getter for the calculated property, also known as the computedGetter:
// src/core/instance/state.js
return function computedGetter () {
// Get computed Watcher for computed[key]
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
/ / cache
if (watcher.dirty) {
watcher.evaluate()
}
// Add a subscription
if (Dep.target) { // This time the render watcher, depend call will change
watcher.depend()
}
return watcher.value
}
}
Copy the code
Get computed[Key] corresponding to computed Watcher; Execute watcher.evaluate() if watcher.dirty is true, and then execute watcher.depend().
// src/core/observer/watcher.js
evaluate () {
this.value = this.get()
this.dirty = false
}
get () {
// called when dependency collection is needed
// Set targetstack.push (target) and dep.target = watcher
pushTarget(this)
let value
try {
// This triggers a dependency property read
value = this.getter.call(vm, vm)
} catch (e) {
/ /...
} finally {
// Rely on the collection to end the call
// Set targetstack.pop () and targetStack[targetstack.length-1]
popTarget()
}
return value
}
Copy the code
Evaluate is implemented by evaluating this.get(); Then set this.dirty to false. During evaluation, because the values used for evaluation are also reactive, their getters are also fired. As described earlier, they collect the current WATcher, which in this case DP. Target is computed Watcher, as a dependency in their Dep. This is equivalent to adding computed Watcher to the attribute-dependent DEP; It also adds its OWN DEP to Computed Watcher. Then proceed:
// src/core/observer/watcher.js
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend() // Actually call dep's Depend}}// src/core/observer/dep.js
depend () {
if (Dep.target) {
Dep.target.addDep(this)}}Copy the code
Watcher.depend () executes the above code, and watcher.evaluate() resets dep.target to render watcher. This.deps [I] is the DEP in computed Watcher, that is, attribute-dependent DEP. So this.deps[I].depend() is equivalent to adding the render Watcher to the attribute-dependent DEP. That is, rendered and computed Watcher are collected in the DEP corresponding to the dependency attribute.
When the dependent property of the calculated property changes, the setter is triggered to notify Watcher of the update, calling the watcher.update method.
// src/core/observer/watcher.js
export default class Watcher {
// When set is triggered by responsive data in computed
update () {
if (this.lazy) {
// Notification computed needs to be recalculated
this.dirty = true}}}Copy the code
First, notify computed Watcher that it needs to be recalculated, then notify the view to perform rendering, which accesses computed values, and finally render to the page.
watch
// src/core/instance/state.js
function initWatch (vm: Component, watch: Object) {
// Iterate over the watch object
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
// If handler is an array, each item is iterated
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
Copy the code
// src/core/instance/state.js
function createWatcher (
vm: Component,
expOrFn: string | Function, handler: any, options? :Object
) {
// If handler is an object, get the value of the handler option in it
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// If hander is a string, it is a method to obtain vm[handler].
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
Copy the code
$watch = vm.$watch = vm.$watch Vm.$watch is defined in stateMixin:
// src/core/instance/state.js
export function stateMixin (Vue: Class<Component>) {
// ...
Vue.prototype.$watch = function (
expOrFn: string | Function, cb: any, options? :Object
) :Function {
const vm: Component = this
// Processing cb may be an object
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
// options.user stands for user watcher, as well as render watcher, which is instantiated in the updateComponent method
options = options || {}
options.user = true
/ / create a Watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
// If the user sets immediate to true, the callback function is executed immediately
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
// Returns a function to remove the watcher
return function unwatchFn () {
watcher.teardown()
}
}
}
Copy the code
Vm.$watch does a few things:
- To deal with
cb
Is the case of the object. - through
options.user = true
logouser watcher
. - create
Watcher
Instance. - If the user sets it
immediate
为true
, the callback function is executed immediately. - Returns a function to remove this
watcher
.
This is where we go when we set deep: True with watch.
// src/core/observer/watcher.js
export default class Watcher {
get () {
/ /...
if (this.deep) {
traverse(value)
}
// ...}}Copy the code
Remember, this is going to recursively access the value, trigger the getters for all of its children, and you get deep listening.
Through the analysis of the implementation of computed and watch attributes, computed is mainly used for applications that depend on the calculation of other attributes, and watch is used for operations that we need to do when a certain attribute changes. Computed and Watch are essentially the same things that Watcher did. One is computed Watcher and the other is User Watcher.
A link to the
Vue (V2.6.14) source code detoxification (pre) : handwritten a simple version of Vue
Vue (V2.6.14) source code detoxification (a) : preparation
Vue (V2.6.14) source code detoxification (two) : initialization and mount
Vue (V2.6.14) source code detoxification (three) : response type principle
Vue (V2.6.14) source code detoxification (four) : update strategy (to be continued)
Vue (v2.6.14) source code detoid (v) : Render and VNode (to be continued)
Vue (v2.6.14) source code: Update and patch (to be continued)
Vue (v2.6.14) source code detoxification (seven) : template compilation (to be continued)
If you think it’s ok, give it a thumbs up!! You can also visit my personal blog at www.mingme.net/