这篇文章讲述了从响应式数据被赋值到操作dom去更新视图的整个过程。vue提供了声明式的方式,我们只需关注状态的变化,而不必手动操作dom。vue通过数据绑定和虚拟dom封装了操作dom的过程,但最终更新视图还是要操作dom,我在操作dom更新文本处打个断点,看下整个调用的过程。
<div id="app">
<h1>{{num}}</h1>
<button @click="add">add</button>
</div>
<script>
new Vue({
el: '#app',
data: {
num: 0
},
methods: {
add() {
this.num++
}
}
})
</script>
点击add按钮后,打断点看调用堆栈,分析响应式数据是如何更新视图的。
- 点击add按钮执行了this.num++,修改该属性值时触发了object.defineProperty的set proxySetter函数。
add() {
this.num++
}
- proxySetter函数,这个函数用到了代理模式,同学们有没有想过,当我们在方法中通过this.num来拿到_data的数据是如何做到的。其实就是遍历_data的数据,使用Object.defineProperty的get自定义属性的读取行为,返回this._data.num来做的。当我们使用vm.a时,通过get代理,返回的是this.data.a。除此之外,vue初始化方法时,使用bind函数改变this的指向为vm实例。所以我们定义在methods中的函数可以通过this.a使用this.data.a,函数体中的this就是vm实例。
// 代理模式设置属性读取和被赋值时的自动行为
export function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
}
// 当修改this.num时,通过代理修改this._data.num
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
// 下列可以直接在控制台上输出,看下通过代理实现了访问vm.a得到了vm
const vm = {
data: {
a: 0
}
}
function proxy(target, sourceKey, key) {
Object.defineProperty(target, key, {
// 自定义属性的读取行为,当读取this.num时返回this._data.num
get: function proxyGetter() {
return this[sourceKey][key]
},
// 自定义属性的写入操作,当设置this.num时,就是设置this._data.num
set: function proxySetter(val) {
this[sourceKey][key] = val
},
enumerable: true,
configurable: true
})
}
const keys = Object.keys(vm.data)
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, 'data', key)
}
console.log(vm.a)
- reactiveSetter 是定义响应数据的defineProperty的set,当属性被赋值时就会触发ractiveSetter, 这个函数的作用是当响应式数据被赋值时执行dep.notify。通知依赖该响应式数据的所有组件执行视图更新
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
// 如果值没有发生改变,直接return
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
// 处理这个对象原来的自定义读取行为
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
val = newVal
}
childOb = !shallow && observe(newVal, false, mock)
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
// 通知依赖这个属性的所有watcher
dep.notify()
}
}
- dep.notify 遍历dep实例上收集的所有watcher,调用update函数
notify(info?: DebuggerEventExtraInfo) {
// stabilize the subscriber list first
const subs = this.subs.filter(s => s) as DepTarget[]
if (__DEV__ && !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++) {
const sub = subs[i]
if (__DEV__ && info) {
sub.onTrigger &&
sub.onTrigger({
effect: subs[i],
...info
})
}
sub.update()
}
}
- Wathcer.update 就是将该组件的watcher推入到视图更新的queue队列中
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
- queueWatcher 当我们触发了多次依赖,在queueWatcher时就会将已经放入到queue队列的过滤掉, 保证queue队列中每个都是id不重复的watcher。
function queueWatcher(watcher: Watcher) {
const id = watcher.id
if (has[id] != null) {
return
}
if (watcher === Dep.target && watcher.noRecurse) {
return
}
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 (__DEV__ && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
- nextTick,将该flushSchedulerQueue更新视图的队列函数推入到微任务队列中
function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
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
})
}
}
- timerFunc 使用Promise.resolve.then,将flushCallbacks函数推入到微任务队列中,
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
- flushCallbacks 当这个函数被执行时就会遍历执行cb(也就是flushSchedulerQueue) 更新响应式数据发生变化所推入的watcher组件视图
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
- flushSchedulerQueue 触发组件beforeUpdate钩子,并执行watcher.run
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort(sortCompareFn)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
cleanupDeps()
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
- watcher.run 主要就是执行this.get。就是watcher实例化参数的getters就是执行 虚拟dom diff更新视图 即updateComponent
run() {
if (this.active) {
const value = this.get()
}
}
- updateComponet vm._render() 返回虚拟dom,执行虚拟dom diff,
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
- vm._update 执行diff 并将实例化完成的dom绑定到vm.$el上
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
let wrapper: Component | undefined = vm
while (
wrapper &&
wrapper.$vnode &&
wrapper.$parent &&
wrapper.$vnode === wrapper.$parent._vnode
) {
wrapper.$parent.$el = wrapper.$el
wrapper = wrapper.$parent
}
}
- patch diff算法,最后 通过diff算法找出需要更新的地方,操作dom。 作者会不断的完善这篇文章。如果觉得有用,请动动发财的小手点点赞吧。