为说明方便,我们采用如下案例:
<div id="app">
<my :data="abc"></my>
</div>
<script>
const vm = new Vue({
el: '#app',
data: {
abc: '123'
},
components: {
my: {
props: {
data: {type:String}
},
template: '<div>my-component {{data}}</div>'
}
}
});
setTimeout(() => {
vm.abc = 'world'
}, 1000)
</script>
可以看到在定时器里面,我们更新了根组件上的abc这个data数据
每一个组件中data数据的更新,都会触发这个data数据所在的组件的渲染watcher重新执行,我们这里的这个动作首先就会触发根组件的渲染watcher的get方法执行,其次会触发my组件的更新流程:
根组件watcher.run() -> 根组件watcher.get() -> vm._render() -> vm._update -> vm.patch(prevVnode, vnode)即patch
往后的流程就和创建节点不同了,这种组件更新的情况,在patch方法中,会走到if (!isRealElement && sameVnode(oldVnode, vnode))这个分支:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // 要做删除的时候 patch(oldVnode,null) destroy方法
if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
return
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) { // new Ctor.$mount();
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
var isRealElement = isDef(oldVnode.nodeType); // patch(el,vnode)
if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch(oldVnode,newVnode)
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
从而进入patchVnode方法,patchVnode方法里思路也非常清晰,先对当前入参传进来的节点进行对比更新,此处就是oldVnode, vnode,之后再对children进行对比更新:
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// ...
var i;
var data = vnode.data;
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}
var oldCh = oldVnode.children; // 老的儿子
var 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)) {// vnode不是文本
if (isDef(oldCh) && isDef(ch)) { // 两方都有儿子
if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
} else if (isDef(ch)) {
{
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); }
}
}
在本案例中,对children进行对比就是对新旧两个my节点进行对比,会进入updateChildren方法,该方法里面就会通过diff算法对子组件调用patchVnode挨个进行更新
由于上面分析的是根节点的更新过程,根节点是个普通的div,所以它没有prepatch、update这些hook,而对于组件节点来说,它在VNode初始化阶段就会把这些hook注册上去,组件就是在这个时机更新的属性,然后属性更新进一步触发它所关联的渲染Watcher,这里渲染Watcher就是my组件了,然后完成DOM更新过程,因此我们重点关注一下my节点进入patchVnode的过程
首先会执行prepatch这个hook:
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
...
},
prepatch: function prepatch (oldVnode, vnode) {
var options = vnode.componentOptions;
var child = vnode.componentInstance = oldVnode.componentInstance;
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
);
},
可以看到prepatch里面又执行了updateChildComponent
function updateChildComponent (
vm,
propsData,
listeners,
parentVnode,
renderChildren
) {
if (propsData && vm.$options.props) {
toggleObserving(false); // 不要在进行观测了
var props = vm._props;
var propKeys = vm.$options._propKeys || [];
for (var i = 0; i < propKeys.length; i++) {
var key = propKeys[i];
var propOptions = 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;
var 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();
}
{
isUpdatingChildComponent = false;
}
}
在该方法最前面有很多和插槽、forceUpdate相关的代码,并不是我们分析的重点,直接去掉了,我们重点关注属性更新的部分
可以看到,这个方法主要就是取到新的属性值,然后将其放到Vue实例对象的propsData属性上:vm.$options.propsData,并在放的过程中暂时关闭了组件的observe响应式注册(这里为什么要关掉,目前我能想到的是如果赋值对象类型的属性时不会触发更新,但还是搞不太明白更具体的原因是什么)
这其中需要关注的是这行代码:
props[key] = validateProp(key, propOptions, propsData, vm);
这行代码的作用是对属性进行类型校验,然后再给属性赋值,需要注意的是,Vue官网明确说明,在开发过程中禁止修改props属性,我个人认为事实上Vue是希望把对props的修改收敛到框架内部,从而更便于管理,才有了这个规定
修改属性的这行代码会触发my组件的data属性的set钩子,在set钩子里面,我们会调用该属性对应的依赖对象dep的notify,进而触发my组件的渲染watcher.run从而执行更新
set: function reactiveSetter (newVal) {
var 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 ( 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();
}
但这次更新操作可不是立即进行的,而是将更新操作放到了queueWatcher更新队列中去了,等本轮代码执行结束后,才会轮到更新操作:
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if ( !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(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
上面notify方法里subs[i].update()执行时就是将更新操作放到了Watcher更新队列中去:
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this); // 每次组件数据更新了 都会执行queueWatcher
}
};
function queueWatcher (watcher) { // 过滤同名的watcher
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher); // 将多个渲染watcher去重后放到队列中
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var 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 ( !config.async) {
flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue); // 这里会产生一个nextTick
}
}
}
执行到这里时,上面queueWatcher里flushing是true,代表之前已经注册过一个watcher队列,所以此时新产生的watcher加到该队列中即可,那之前什么时候新注册过一个watcher队列呢?
其实在文章开头的时候我们提到过,vm.abc = 'world'这行代码执行时,就触发了abc对应的渲染watcher的执行,也就是根组件的渲染watcher的执行,我们当时是直接从watcher.run()开始分析的,事实上在此之前,从定时器里变更abc属性到watcher.run()就包含了注册watcher队列这个工作:
vm.abc = 'world' ->
abc的dep.notify(); ->
queueWatcher(this); // 这里放入队列的watcher是根组件的watcher ->
nextTick(flushSchedulerQueue) ->
timerFunc(); ->
p.then(flushCallbacks);
接下来这个宏任务就结束了,就进入到微任务遍历flushCallbacks执行的过程了,flushCallbacks里有一个方法就是flushSchedulerQueue,即遍历此时watcher队列里的各项执行run,这个时候就会进入根组件的watcher.run里面去了,此时队列里也只有一个根组件的渲染watcher
但通过之前的分析,根组件渲染watcher在执行过程中又加入了my组件的渲染watcher,也就是在这行代码时:
props[key] = validateProp(key, propOptions, propsData, vm);
给子组件赋值时触发它的notify,再将my组件的渲染watcher放进队列里去的
updateChildComponent里面在执行了notify之后,又对listeners事件进行了更新,对$forceUpdate做了一些处理
随着updateChildComponent方法执行完毕,子组件的prepatch这个hook执行完毕,再返回patchVnode继续执行一系列update方法(cbs.update),用于更新一些class、attrs、style、listeners等等,再返回根节点的patch方法中,根节点子节点更新操作updateChildren执行完毕,patch方法随即执行完毕,根节点_update方法执行完毕
接下来会再回到flushSchedulerQueue方法里,继续取到下一个watcher执行,这里的下一个watcher,就是我们刚刚加进去的my组件的渲染watcher,该渲染watcher执行完后,使用了新的属性的DOM就被创建出来,并随之更新到页面上了