根据调试工具看源码之虚拟dom(一)

39次阅读

共计 5889 个字符,预计需要花费 15 分钟才能阅读完成。

初次探索
什么是虚拟 dom

Vue 通过建立一个虚拟 DOM 对真实 DOM 发生的变化保持追踪。请仔细看这行代码:
return createElement(‘h1’, this.blogTitle)
createElement 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,及其子节点。我们把这样的节点描述为“虚拟节点 (Virtual Node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
以上这段对虚拟 Dom 的简短介绍来自 Vue 的官网
第一个断点
我们一开始的断点先打在 app.vue 的两个 hook 上:
export default {
name: ‘app’,
created () {
debugger
},
mounted () {
debugger
}
}
刷新页面,此时调用栈中显示的函数跟预想中的不太一样:
在 created 这个 hook 执行之前,多出了一些比较奇怪的函数:

createComponentInstanceForVnode
Vue._update
mountComponent

???? 看完以后我心中出现了一个疑问:
为什么在 created 钩子执行之前就出现了 mountComponent 这个方法,到底是文档出问题了,还是文档出问题了呢?带着这个疑惑我们接着往下看

mountComponent 做了什么?
通过上面打第一个断点,其实不难看出这样的执行顺序(从上往下):

(annoymous)
Vue.$mount
mountComponent

(annoymous) 这步其实就是在执行我们的 main.js,代码很短:

new Vue({
render: h => h(App)
}).$mount(‘#app’)
Vue.$mount
Vue.prototype.$mount = function (
el,
hydrating
) {
// 判断是否处于浏览器的环境
el = el && inBrowser ? query(el) : undefined;
// 执行 mountComponent
return mountComponent(this, el, hydrating)
};
mountComponent
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
// 开发环境下给出警告提示
if (process.env.NODE_ENV !== ‘production’) {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== ‘#’) ||
vm.$options.el || el) {
warn(
‘You are using the runtime-only build of Vue where the template ‘ +
‘compiler is not available. Either pre-compile the templates into ‘ +
‘render functions, or use the compiler-included build.’,
vm
);
} else {
warn(
‘Failed to mount component: template or render function not defined.’,
vm
);
}
}
}
callHook(vm, ‘beforeMount’);

var updateComponent;
/* istanbul ignore if */
// 这里对测试环境跟正式环境的 updateComponent 做了实现上的一个区分
if (process.env.NODE_ENV !== ‘production’ && config.performance && mark) {
updateComponent = function () {
var name = vm._name;
var id = vm._uid;
var startTag = “vue-perf-start:” + id;
var endTag = “vue-perf-end:” + id;

mark(startTag);
var vnode = vm._render();
mark(endTag);
measure((“vue ” + name + ” render”), startTag, endTag);

mark(startTag);
vm._update(vnode, hydrating);
mark(endTag);
measure((“vue ” + name + ” patch”), startTag, endTag);
};
} else {
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
}

// 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: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, ‘beforeUpdate’);
}
}
}, true /* isRenderWatcher */);
hydrating = false;

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, ‘mounted’);
}
return vm
}
简单罗列下上面这两段代码的逻辑????:

调用 beforeMount 钩子函数
封装一个 updateComponent 函数
执行 new Watcher 并将 updateComponent 当做参数传入
调用 vm._update 方法

_update 方法是如何被触发的?
Watcher
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {

// 将函数赋值给 this.getter,这里是 updateComponent 函数
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.lazy 决定是否触发 get 方法
this.value = this.lazy
? undefined
: this.get();
};
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
// 这里调用 getter 方法,实际上也就是调用 updateComponent 方法并拿到返回值
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();
}
// 返回函数(updateComponent)执行结果
return value
};
简单梳理下上面这段代码的逻辑:

新建 Watcher 实例时,将 updateComponent 赋值给 getter 属性
通过 this.get 方法,触发 updateComponent 函数
最终拿到函数的执行结果

小结
通过上面的分析我们可以初步得出一个结论:
组件的渲染跟 Watcher 离不开关系,父组件在执行完 created 钩子函数之后,会调用 updateComponent 函数对子组件进行处理
深入研究
如果前面你动手跟着断点一直走,那么不难得知存在这样的调用关系(从上往下):


mountComponent
Watcher
get
updateComponent
Vue._update
patch
createElm
createComponent
init
createComponentInstanceForVnode
VueComponent
Vue._init
callHook
invokeWithErrorHandling
created

Vue.prototype._update
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
// 重存储当前父实例
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// 执行 patch 函数
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();

};
当然,我们通过全局检索可以得知_patch 函数相关的代码????:
// 只在浏览器环境下 patch 函数有效
Vue.prototype.__patch__ = inBrowser ? patch : noop;
var patch = createPatchFunction({nodeOps: nodeOps, modules: modules});
function createPatchFunction (backend) {

return function patch (oldVnode, vnode, hydrating, removeOnly) {

}
}
这里先不深究 patch 的实现,我们只要知道 patch 是使用 createPatchFunction 来生成的一个闭包函数即可。
子组件的渲染
我们注意到,在子组件 created 钩子执行之前存在一个 init 方法????:
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
// 创建子组件实例
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
// 对子组件执行 $mount 方法
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},

相关代码:
createComponentInstanceForVnode
function createComponentInstanceForVnode (
vnode, // we know it’s MountedComponentVNode but flow doesn’t
parent // activeInstance in lifecycle state
) {
// 初始化一个子组件的 vnode 配置
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
// 检查 render 函数内是否有 template 模板
var inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
// 返回子组件实例
return new vnode.componentOptions.Ctor(options)
}
总结

存在子组件时,先初始化父组件,在 created 钩子执行之后,生成子组件的 vnode 实例
子组件的 created 钩子执行完,检查子组件是否也有子组件
子组件也存在子组件时,则重复 1,否则直接执行 $mount 函数,渲染子组件

正文完
 0