Vue的響應(yīng)式系統(tǒng)將按照如下幾個部分進(jìn)行講解:
1. 響應(yīng)式對象
2. 依賴收集
3. 派發(fā)更新
4. nextTick
5. 檢測變化的注意事項
6. 計算屬性VS偵聽屬性
7. 組件更新
8. Props
9. 原理圖
[響應(yīng)式對象]
對于響應(yīng)式對象,拿props的初始作為一個例子,其他屬性都可以觸類旁通。
vue的初始化有這么一條路:
initMixin => instate => initProps
來看initProps:
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
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
)
}
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 {
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 (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
通過defineReactive(props, key, value)將props將props變成一個響應(yīng)式對象,通過proxy(vm, _props, key)使得下面這條訪問鏈路成立:
vm._props.xxx => vm.xxx =>$options.prop
來看下proxy的實現(xiàn)
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
就是通過sharedPropertyDefinition橋接了下,我們平常在組件使用的data也一樣。這也就我們在data中對應(yīng)的數(shù)據(jù)可以通過this.xxxx訪問的原因。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
來看下這個observe(src\core\observer\index.js)
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
這段邏輯就是說如果之前添加過observe,那么就用之前的,否則就new一個Observer。ps:大家在看響應(yīng)式代碼之前建議先去了解下屬性描述符的一系列概念。
來看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
this.dep = new Dep()
this.vmCount = 0
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)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
其中dep有關(guān)依賴收集,我們等會再講,def(value, 'ob', this),就是將這個Observer添加到被觀察對象的"ob"上,作用我們之后再說。
如果被觀察的對象是數(shù)組的話,調(diào)用protoAugment 或copyAugment ,這兩個函數(shù)主要是重寫了數(shù)組的一些方法,最后調(diào)用observeArray。如果不是數(shù)組的話則調(diào)用walk方法。
來看walk方法:
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
其實就是調(diào)用了defineReactive方法(敲黑板了?。。。。?/p>
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
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
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
這玩意賊有用,vue-router的響應(yīng)式也是靠他實現(xiàn)的。
首先它先獲取了目標(biāo)對象屬性的屬性描述符。如果不可配置,直接return。
然后他通過Object.defineProperty,定義了該屬性的get和set,get我們后面會講,是用來做依賴收集的,set是用來做派發(fā)更新的。
先不管具體的依賴收集和派發(fā)更新。
整個創(chuàng)建一個響應(yīng)式對象,是這么一個流程
observe(xxx) => xxx是數(shù)組 ?遞歸observe :walk(調(diào)用defineReactive), observe整個動作是vue核心庫需要的,最后變成響應(yīng)式對象是靠defineReactive實現(xiàn)的。
[依賴收集]
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
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
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
這段代碼中其實真正重要的就是兩部分:
const dep = new Dep()
childOb.dep.depend()
Dep 是整個 getter 依賴收集的核心,它的定義在 src/core/observer/dep.js 中:
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++) {
subs[i].update()
}
}
}
可以看到這個類實現(xiàn)了1個構(gòu)造器和4個方法:
- constructor
- addSub
- removeSub
- depend
- notify
Dep 實際上就是對 Watcher 的一種管理,Dep 脫離 Watcher 單獨存在是沒有意義的,為了完整地講清楚依賴收集過程,我們有必要看一下 Watcher 的一些相關(guān)實現(xiàn),它的定義在 src/core/observer/watcher.js 中:
/* @flow */
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError,
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
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
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.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.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()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
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()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
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)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
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) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
可以看到這個類實現(xiàn)了1個構(gòu)造器和8個方法:
- constructor
- get
- addDep
- cleanupDeps
- update
- run
- evaluate
- depend
- teardown
了解這些之后我們看下這個依賴收集的過程。
之前我們講過,在調(diào)用$mount方法的時候其實是調(diào)用了mountComponent方法(src\platforms\web\runtime\index.js)
而在mountComponent這個方法的定義的時候有這么一段邏輯:
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
我們來看下new watch干了啥:
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.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.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()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
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()
}
首先實例中的_watchers收集了這個新的watch
vm._watchers.push(this)
拋去一些我們暫時不關(guān)心的邏輯,看下最后訪問了get方法
this.value = this.lazy
? undefined
: this.get()
get () {
pushTarget(this)
let value
const vm = this.vm
try {
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)
}
popTarget()
this.cleanupDeps()
}
return value
}
第一步就是干了這么一件事:
pushTarget(this)
看下pushTarget的定義(src\core\observer\dep.js):
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
也就是Dep.target會變成這個新的watch,同時把這個watch壓入targetStack中(其實后面會講到是為了恢復(fù)用)。
回到之前,我們講的defineReactive:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
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
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
當(dāng)我們開始訪問一個屬性的時候,那么依賴收集的工作就開始了:
首先調(diào)用
dep.depend()
進(jìn)而調(diào)用了addDep
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
然后調(diào)用addSub
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
上面的流程也就是下面這樣是等價的:
dep.subs.push(Dep.target)
而這個Dep.target在new Watcher的時候已經(jīng)被這個new Watcher賦值了。
也就是說我這個watcher就是持有這個dep的數(shù)據(jù)的訂閱者了,這個目的是為后續(xù)數(shù)據(jù)變化時候能通知到哪些訂閱者做準(zhǔn)備。
讓我們再關(guān)注一些細(xì)節(jié),我們先回到Wathcer的get方法中:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
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)
}
popTarget()
this.cleanupDeps()
}
return value
}
首先是做了
traverse(value)
``
這個等會會細(xì)講。
然后是
```js
popTarget()
看其定義:
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
實際上就是把 Dep.target 恢復(fù)成上一個狀態(tài),因為當(dāng)前 vm 的數(shù)據(jù)依賴收集已經(jīng)完成,那么對應(yīng)的渲染Dep.target 也需要改變。最后執(zhí)行:
this.cleanupDeps()
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
可以看到這個函數(shù)就是將新的依賴ID組和新的依賴組賦值到實際用的依賴ID組合依賴組里,然后清空了新的依賴ID組合依賴組。
考慮到一種場景,我們的模板會根據(jù) v-if 去渲染不同子模板 a 和 b,當(dāng)我們滿足某種條件的時候渲染 a 的時候,會訪問到 a 中的數(shù)據(jù),這時候我們對 a 使用的數(shù)據(jù)添加了 getter,做了依賴收集,那么當(dāng)我們?nèi)バ薷?a 的數(shù)據(jù)的時候,理應(yīng)通知到這些訂閱者。那么如果我們一旦改變了條件渲染了 b 模板,又會對 b 使用的數(shù)據(jù)添加了 getter,如果我們沒有依賴移除的過程,那么這時候我去修改 a 模板的數(shù)據(jù),會通知 a 數(shù)據(jù)的訂閱的回調(diào),這顯然是有浪費(fèi)的。
因此 Vue 設(shè)計了在每次添加完新的訂閱,會移除掉舊的訂閱,這樣就保證了在我們剛才的場景中,如果渲染 b 模板的時候去修改 a 模板的數(shù)據(jù),a 數(shù)據(jù)訂閱回調(diào)已經(jīng)被移除了,所以不會有任何浪費(fèi)。
[派發(fā)更新]
之前我們說派發(fā)個更新是在set里面進(jìn)行的:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
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
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
可以看到有這么幾步:
- 判斷新值和舊值如果相等或者同為NaN,則直接返回。
- 如果有g(shù)etter沒有setter則直接返回
- 有setter調(diào)用setter,否則直接賦值。
- 將新值變?yōu)轫憫?yīng)式對象。
- 派發(fā)更新。
這里需要注意的是,這里對新值和舊值是否相等是通過“===”判斷的,因此如果值是引用類型是判斷不出來的,所以通過set方法最后還是會調(diào)用defineReactive的,所以只適用值是普通值的情況。
重點來看下用于派發(fā)更新的方法:
dep.notify()
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++) {
subs[i].update()
}
}
可以看到這里是遍歷所有訂閱的watcher,然后執(zhí)行watcher的update方法:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
這里的sync等會會講,先看queueWatcher,這里has[id]保證每個watcher的回調(diào)只觸發(fā)一次,else等會再講,看flushSchedulerQueue:
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
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]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
隊列排序
queue.sort((a, b) => a.id - b.id) 對隊列做了從小到大的排序,這么做主要有以下要確保以下幾點:
1.組件的更新由父到子;因為父組件的創(chuàng)建過程是先于子的,所以 watcher 的創(chuàng)建也是先父后子,執(zhí)行順序也應(yīng)該保持先父后子。
2.用戶的自定義 watcher 要優(yōu)先于渲染 watcher 執(zhí)行;因為用戶自定義 watcher 是在渲染 watcher 之前創(chuàng)建的。
3.如果一個組件在父組件的 watcher 執(zhí)行期間被銷毀,那么它對應(yīng)的 watcher 執(zhí)行都可以被跳過,所以父組件的 watcher 應(yīng)該先執(zhí)行。
隊列遍歷
在對 queue 排序后,接著就是要對它做遍歷,拿到對應(yīng)的 watcher,執(zhí)行 watcher.run()。這里需要注意一個細(xì)節(jié),在遍歷的時候每次都會對 queue.length 求值,因為在 watcher.run() 的時候,很可能用戶會再次添加新的 watcher,這樣會再次執(zhí)行到 queueWatcher,如下:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
這個時候flushing為true走到else分支里,這里其實就是將id按照從小到大插入queue中。run完,接著進(jìn)行了resetSchedulerState操作:
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
其實就是清空了 has,queue,然后將waiting,flushing重置為fasle.我們再來看看run:
run () {
if (this.active) {
const value = this.get()
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) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
先通過 this.get() 得到它當(dāng)前的值,然后做判斷,如果滿足新舊值不等、新值是對象類型、deep 模式任何一個條件,則執(zhí)行 watcher 的回調(diào),注意回調(diào)函數(shù)執(zhí)行的時候會把第一個和第二個參數(shù)傳入新值 value 和舊值 oldValue,這就是當(dāng)我們添加自定義 watcher 的時候能在回調(diào)函數(shù)的參數(shù)中拿到新舊值的原因。
梳理下流程:
值發(fā)生改變 => dep.notify()=> 遍歷觸發(fā)訂閱的wathcer的update=> 根據(jù)是否同步等條件最后觸發(fā)watcher.run方法(渲染函數(shù)觸發(fā)get執(zhí)行更新DOM)=> 觸發(fā)watcher的回調(diào)(用戶可以拿到新值和舊值)
那么對于渲染 watcher 而言(new的時候第5參數(shù)傳不傳true),它在執(zhí)行 this.get() 方法求值的時候,會執(zhí)行 getter 方法:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
接著講nextTick:
[nextTick]
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
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]()
}
}
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
可以看到這里用了依次降級(Promise.then => MutationObserver => setImmediate => setTimeout)的方式創(chuàng)建了異步函數(shù)timerFunc ,
然后異步函數(shù)里循環(huán)執(zhí)行之前添加過的函數(shù)。如果不傳回調(diào)函數(shù),那返回的就是一個promise,可以直接用then方法。這里callback用數(shù)組的原因是在一個循環(huán)中可以執(zhí)行多個傳入的函數(shù),不會開啟多個異步任務(wù),而把這些異步任務(wù)都壓成一個同步任務(wù),在下一個 tick 執(zhí)行完畢。
舉個例子:
nextTick(f1)
nextTick(f1)
nextTick(f1)
這個時候callbacks中已經(jīng)有f1,f2,f3了...然后一次性依次執(zhí)行。
[檢測變化的注意事項]
平常我們有三種情況不能被vue的響應(yīng)式系統(tǒng)直接監(jiān)聽到:
- 通過a.b = 1給對象新添加屬性。
- 通過arr[0] = 1直接給原數(shù)組的元素賦值。
- 通過vm.items.length = newLength修改數(shù)組長度。
1的原因是因為data在初始化的時候已經(jīng)完成了數(shù)據(jù)的響應(yīng)式化,所以新添加的屬性不是響應(yīng)式的。
2和3的原因是因為vue的響應(yīng)式只對數(shù)組本身做了響應(yīng)式監(jiān)聽,而其元素并沒有,length其實也算其一個屬性。
對于1和2,vue提供了set方法:
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)}`)
}
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__
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
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
- 對于target是一個數(shù)組,并且傳入的索引是有效的,那么直接通過splice返回(這里的splice被重寫了,等會講)
- 對于本來就在對象中的屬性,就不需要處理了,因為已經(jīng)是響應(yīng)式對象了
- 對于 Vue的實例和根data直接return
- 如果target不存在ob,說明這個對象不是一個響應(yīng)式對象,就不添加了了。
- 否則通過defineReactive轉(zhuǎn)為響應(yīng)式對象并通過ob.dep.notify()派發(fā)更新。
再來看看被重寫的數(shù)組方法:
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
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 (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
可以看到對于push,unshift,splice3種方法,最后都通過了observeArray將傳進(jìn)去的所有參數(shù)對象變成了響應(yīng)式的并派發(fā)了更新。PS:Vue.delete用于數(shù)組也是基于splice的,而對象無需操作,最后也是通過ob.dep.notify()派發(fā)更新。
計算屬性VS偵聽屬性
計算屬性
先看他初始化的地方(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()
for (const key in computed) {
const userDef = computed[key]
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 internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
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)
}
}
}
}
先創(chuàng)建了一個空對象watchers,然后把我們傳進(jìn)去的computed的key作為它的key,賦值一個new Watcher,這就是計算watcher,注意我們傳了一個computedWatcherOptions進(jìn)去,就是一個{lazy: true}.再回頭看我們的watcher類的構(gòu)造器:
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.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.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()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
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()
}
可以看到最后如果我們的lazy是true就是一個計算watcher的話,那么是先不調(diào)用watcher的。
接著上面的initComputed看,接下來是執(zhí)行了defineComputed(vm, key, userDef):
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !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
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
可以看到如果需要緩存,那么返回一個函數(shù),否則直接調(diào)用我們傳進(jìn)去的函數(shù)(就是computed的那些函數(shù)),最后通過Object.defineProperty將我們computed的同名key添加到是vue的實例上,所以我們可以通過this.xxxx直接訪問??聪耤reateComputedGetter:
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
console.log('computedGetter')
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
可以看到由于首次傳的是lazy為true,這個dirty就是lazy,所以會先計算下watcher的值。然后開始收集依賴。接著調(diào)用了depend:
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
即Dep.target將計算屬性的依賴變成了自己的依賴。
這里有個疑問,為什么每次獲取計算屬性的值時都要進(jìn)行依賴收集呢,而不是僅進(jìn)行一次性的依賴收集?原因是,計算屬性的依賴項可能會改變,這次有x個依賴項,下次可能有y個依賴項。比如三元表達(dá)式
那么每當(dāng)依賴發(fā)生變化就會產(chǎn)生這么一條路徑:
**dep.notify() => watcher.update(這個時候只是將lazy置位true) **
然后再次訪問計算屬性的值是會觸發(fā)computedGetter去重新計算wathcer的值。
這個時候如果有渲染watcher訂閱了這個計算watcher,那么會對比前后值是否一樣,不一樣才去重新渲染。
再來看偵聽屬性watch:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
可以看到無他,唯遍歷添加watcher爾。
整理下所有的watcher;
- deep watcher(遞歸添加watcher)
- user watcher(用戶的watcher)
- computed watcher
- sync watcher(可以在user watcher里配置)
- renderwatcher(渲染 watcher,一般來說處的位置靠上)
組件更新
之前我們降到如果依賴發(fā)生變化,會觸發(fā)組件更新,那么組件最后是怎么更新的,我們這邊來了解下:
依賴發(fā)生變化 => watcher.getter => vm._update => vm.patch
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// console.log(123)
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)//找了oldELm的父node(實際上的DOM元素)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
由于這次存在oldVnode,走else分支:
先通過sameVnode判斷是不是同一個vnode:
function sameVnode (a, b) {//判定是否是同一個vnode
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)
)
)
)
}
就是按照這么一段邏輯判斷:
key => tag => isComment => data => sameInputType
異步組件則通過asyncFactory(常用的就是() => import(xxxx))判斷.
如果新舊節(jié)點不同,那么主要份3步進(jìn)行:
- 創(chuàng)建新節(jié)點
- 更新父的占位節(jié)點
- 刪除舊節(jié)點
創(chuàng)建新節(jié)點
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)//找了oldELm的父node(實際上的DOM元素)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
更新父的占位節(jié)點
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
刪除舊節(jié)點
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
刪除節(jié)點邏輯很簡單,就是遍歷待刪除的 vnodes 做刪除,其中 removeAndInvokeRemoveHook 的作用是從 DOM 中移除節(jié)點并執(zhí)行 module 的 remove 鉤子函數(shù),并對它的子節(jié)點遞歸調(diào)用 removeAndInvokeRemoveHook 函數(shù);invokeDestroyHook 是執(zhí)行 module 的 destory 鉤子函數(shù)以及 vnode 的 destory 鉤子函數(shù),并對它的子 vnode 遞歸調(diào)用 invokeDestroyHook 函數(shù);removeNode 就是調(diào)用平臺的 DOM API 去把真正的 DOM 節(jié)點移除。
在之前介紹組件生命周期的時候提到 beforeDestroy & destroyed 這兩個生命周期鉤子函數(shù),它們就是在執(zhí)行 invokeDestroyHook 過程中,執(zhí)行了 vnode 的 destory 鉤子函數(shù),它的定義在 src/core/vdom/create-component.js 中:
const componentVNodeHooks = {
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
當(dāng)組件并不是 keepAlive 的時候,會執(zhí)行 componentInstance.$destroy() 方法,然后就會執(zhí)行 beforeDestroy & destroyed 兩個鉤子函數(shù)。
當(dāng)兩個vnode相同的時候開始執(zhí)行patchVnode:
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
這里也大致分為4個步驟:
1. 執(zhí)行prepatch鉤子函數(shù)
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
看下prepatch的定義:
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
prepatch 方法就是拿到新的 vnode 的組件配置以及組件實例,去執(zhí)行 updateChildComponent 方法,它的定義在 src/core/instance/lifecycle.js 中:
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = true
}
// determine whether component has slot children
// we need to do this before overwriting $options._renderChildren.
// check if there are dynamic scopedSlots (hand-written or compiled but with
// dynamic slot names). Static scoped slots compiled from template has the
// "$stable" marker.
const newScopedSlots = parentVnode.data.scopedSlots
const oldScopedSlots = vm.$scopedSlots
const hasDynamicScopedSlot = !!(
(newScopedSlots && !newScopedSlots.$stable) ||
(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
(newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
)
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
const needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
)
vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render
if (vm._vnode) { // update child tree's parent
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren
// update $attrs and $listeners hash
// these are also reactive so they may trigger child update if the child
// used them during render
vm.$attrs = parentVnode.data.attrs || emptyObject
vm.$listeners = listeners || emptyObject
// update props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// update listeners
listeners = listeners || emptyObject
const oldListeners = vm.$options._parentListeners
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = false
}
}
就是更新了vnode.
2. 執(zhí)行 update 鉤子函數(shù)
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
3. 完成 patch 過程
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
大致邏輯是這樣的:
- 新節(jié)點存在文本節(jié)點的情況下,當(dāng)舊節(jié)點的子節(jié)點和新節(jié)點的子節(jié)點都存在的情況下,開始diff算法,等會講。
- 當(dāng)舊節(jié)點沒有子節(jié)點的情況下,檢查新節(jié)點子節(jié)點的key的重復(fù)性.如果舊節(jié)點存在文本節(jié)點,則清空文本節(jié)點。然后批量將新節(jié)點的子節(jié)點添加到elem后面
- 如果舊節(jié)點存在子節(jié)點,則移除所以的子節(jié)點。
- 如果新節(jié)點沒有子節(jié)點但是舊節(jié)點有子節(jié)點,則清空舊節(jié)點的文本內(nèi)容
- 若新舊節(jié)點文本內(nèi)容不同則替換。
然來開看第一步的diff算法:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
debugger
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
diff算法其實遵循幾個原則:
- 能移動就移動
- 先比頭和尾
- 頭尾比不了,就找中間key或索引一樣的比,然后添加到舊頭
- 新vnode較長則在舊vnode上添加node,較短則刪除。
props
在Vue的初始化的時候有這么一個流程:
規(guī)范化:
initGlobalAPI(Vue) => initMixin(Vue) => mergeOptions => normalizeProps
當(dāng) props 是一個數(shù)組,每一個數(shù)組元素 prop 只能是一個 string,表示 prop 的 key,轉(zhuǎn)成駝峰格式,prop 的類型為空。
當(dāng) props 是一個對象,對于 props 中每個 prop 的 key,我們會轉(zhuǎn)駝峰格式,而它的 value,如果不是一個對象,我們就把它規(guī)范成一個對象。
如果 props 既不是數(shù)組也不是對象,就拋出一個警告。
初始化:
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
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
)
}
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 {
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 (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
initProps 主要做 3 件事情:校驗、響應(yīng)式和代理。
校驗
export function validateProp (
key: string,
propOptions: Object,
propsData: Object,
vm?: Component
): any {
const prop = propOptions[key]
const absent = !hasOwn(propsData, key)
let value = propsData[key]
// boolean casting
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (value === '' || value === hyphenate(key)) {
// only cast empty string / same name to boolean if
// boolean has higher priority
const stringIndex = getTypeIndex(String, prop.type)
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key)
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
if (
process.env.NODE_ENV !== 'production' &&
// skip validation for weex recycle-list child component props
!(__WEEX__ && isObject(value) && ('@binding' in value))
) {
assertProp(prop, key, value, vm, absent)
}
return value
}
validateProp 主要就做 3 件事情:處理 Boolean 類型的數(shù)據(jù),處理默認(rèn)數(shù)據(jù),prop 斷言,并最終返回 prop 的值。
Boolean 類型特殊處理
若是定義prop的type時,Boolean為其中之一,則可能有如下情況,則重新設(shè)置該prop的值。
無值且無默認(rèn)值的情況:調(diào)用組件時未傳入
prop的值 &&prop定義時未設(shè)置默認(rèn)值,則將prop的值置為false-
針對布爾特性的情況:調(diào)用組件時傳入的
prop的值為空字符串 ||prop的值為key的連字符形式,則可能出現(xiàn)如下情況:- 該
prop指定的類型里沒有String,則將prop的值置為true - 該
prop指定的類型里有String,但是Boolean類型在String之前,則將prop的值置為false
- 該
經(jīng)過以上Boolean類型的處理之后,若是prop的值仍為undefined,則將獲取prop的默認(rèn)值。
非Boolean類型的默認(rèn)值
- default是函數(shù) && prop配置的type里沒有Function,則返回該函數(shù)調(diào)用后的返回值作為默認(rèn)值
- default是函數(shù) && prop配置的type里有Function,則返回該函數(shù)作為默認(rèn)值
- default為非函數(shù)類型,則返回該default值作為默認(rèn)值
驗證
- 需要做以下三個驗證
- case 1: 驗證 required 屬性
- case 2: prop 定義時是 required,但是調(diào)用組件時沒有傳遞該值(警告)
- case 3: prop 定義時是非 required 的,且 value === null || value === undefined(符合要求,返回)
- case 4: 驗證 type 屬性-- value 的類型必須是 type 數(shù)組里的其中之一
- case 5: 驗證自定義驗證函數(shù)
響應(yīng)式
defineReactive(props, key, value)
代理
defineReactive(props, key, value)
proxy(vm, `_props`, key)
總結(jié):
1. 創(chuàng)建響應(yīng)式對象:
observe(xxx) => xxx是數(shù)組 ?遞歸observe :walk(調(diào)用defineReactive), observe整個動作是vue核心庫需要的,最后變成響應(yīng)式對象是靠defineReactive實現(xiàn)的。
依賴收集和派發(fā)更新有2個概念:
- dep ,這是一個包含 依賴id, 訂閱者(watcher), 依賴數(shù)組的類,這類主要是用來管理watcher的。
- watcher,這個類的作用主要是數(shù)據(jù)發(fā)生變化的時候,調(diào)用各個方法產(chǎn)生計算和更改視圖的,包含了各種屬性。
2. 依賴收集:
依賴收集發(fā)生在數(shù)據(jù)屬性的get階段。干了這么一件事:
將當(dāng)前wather變成持有這個dep的訂閱者。
依賴收集要清空原來依賴,使用的新的依賴。防止重復(fù)訂閱的浪費(fèi)。
3. 派發(fā)更新:
派發(fā)更新主要發(fā)生在數(shù)據(jù)屬性的set階段, 做了這么幾個步驟:
- 判斷新值和舊值如果相等或者同為NaN,則直接返回。
- 通過observe將新值變?yōu)轫憫?yīng)式對象
- 通過dep.notify()派發(fā)更新。
派發(fā)更新的流程:
值發(fā)生改變 => dep.notify()=> 遍歷觸發(fā)訂閱的wathcer的update=> 根據(jù)是否同步等條件最后觸發(fā)watcher.run方法(渲染函數(shù)觸發(fā)get執(zhí)行更新DOM)=> 觸發(fā)watcher的回調(diào)(用戶可以拿到新值和舊值)
4. [檢測變化的注意事項]
通過a.b = 1給對象新添加屬性。
通過arr[0] = 1直接給原數(shù)組的元素賦值。
通過vm.items.length = newLength修改數(shù)組長度。
上述3中可以通過set方法變成響應(yīng)式。核心還是通過defineReactive和dep.notify()
5. 計算屬性VS偵聽屬性
-
計算屬性在創(chuàng)建wathcer的時候會置上一個標(biāo)志位lazy,做了2層優(yōu)化;
- 在初始化的時候不會去計算
- 在更新時候比較前后值是否一樣否則不會渲染(這是所有相應(yīng)是數(shù)據(jù)都一樣)。
watch
就是通過traverse做了遞歸響應(yīng)式。
watcher的種類:
deep watcher(遞歸添加watcher)
user watcher(用戶的watcher)
computed watcher
sync watcher(可以在user watcher里配置)
renderwatcher(渲染 watcher,一般來說處的位置靠上)
6. 組件更新:
說下數(shù)據(jù)變化到更新整個流程:
依賴發(fā)生變化 => watcher.getter => vm._update => vm.patch
判斷是否已是同一個節(jié)點的邏輯:
key => tag => isComment => data => sameInputType
如果新舊節(jié)點不同,那么主要分3步進(jìn)行:
- 創(chuàng)建新節(jié)點
- 更新父的占位節(jié)點
- 刪除舊節(jié)點
如果新舊節(jié)點相同:
- 執(zhí)行prepatch鉤子函數(shù)(去拿新的 vnode 的組件配置以及組件實例,去執(zhí)行 updateChildComponent 方法)
- 執(zhí)行 update 鉤子函數(shù)
- 完成 patch 過程
patch大致邏輯是這樣的:
- 新節(jié)點存在文本節(jié)點的情況下,當(dāng)舊節(jié)點的子節(jié)點和新節(jié)點的子節(jié)點都存在的情況下,開始diff算法。
- 當(dāng)舊節(jié)點沒有子節(jié)點的情況下,檢查新節(jié)點子節(jié)點的key的重復(fù)性.如果舊節(jié)點存在文本節(jié)點,則清空文本節(jié)點。然后批量將新節(jié)點的子節(jié)點添加到elem后面
- 如果舊節(jié)點存在子節(jié)點,則移除所以的子節(jié)點。
- 如果新節(jié)點沒有子節(jié)點但是舊節(jié)點有子節(jié)點,則清空舊節(jié)點的文本內(nèi)容
- 若新舊節(jié)點文本內(nèi)容不同則替換。
diff算法其實遵循幾個原則:
能移動就移動
先比頭和尾
頭尾比不了,就找中間key或索引一樣的比,然后添加到舊頭
新vnode較長則在舊vnode上添加node,較短則刪除。
7. Props;
- 規(guī)范化
當(dāng) props 是一個數(shù)組,每一個數(shù)組元素 prop 只能是一個 string,表示 prop 的 key,轉(zhuǎn)成駝峰格式,prop 的類型為空。
當(dāng) props 是一個對象,對于 props 中每個 prop 的 key,我們會轉(zhuǎn)駝峰格式,而它的 value,如果不是一個對象,我們就把它規(guī)范成一個對象。
如果 props 既不是數(shù)組也不是對象,就拋出一個警告。 - 初始化(校驗、響應(yīng)式和代理)
釋疑:
1. 為什么說Vue是異步更新,因為dep.notify()調(diào)用了watcher的udpate方法,這個方法調(diào)用了queueWatcher方方法,最終調(diào)用了nextTick這個異步方法,所以是異步更新的。
2. 因為在依賴收集階段如果碰到對象里面的屬性是數(shù)組的,我們對該屬性是沒有做依賴收集的,我們對對象屬性的值判斷是通過===來看他是否發(fā)生變化的,因此數(shù)組元素的變化是無法觸發(fā)更新的
我們能在watch拿到新值和舊值的原因是因為在wather執(zhí)行run的時候,會將新舊值傳到回調(diào)里
3. push, unshift, splice是響應(yīng)式操作的原因是Vue重寫了這3個方法。
4. 為什么每次獲取計算屬性的值時都要進(jìn)行依賴收集呢,而不是僅進(jìn)行一次性的依賴收集?原因是,計算屬性的依賴項可能會改變,這次有x個依賴項,下次可能有y個依賴項。比如三元表達(dá)式