环境准备
git clone https://github.com/vuejs/vue.git
目前稳定版本是 2.6.14
修改 package.json 加上 --sourcemap
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap",
下载依赖,注自己设置淘宝镜像源
yarn
npm run dev
终止控制台
dist/vue.js 生成 sourcemap 文件,可以指向源码
vscode 添加 debugger 配置 launch.json 内容如下:
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
// "url": "http://localhost:8080",
"file": "${workspaceFolder}/vue/examples/grid/index.html" // 修改为自己调试路径
}
]
}
html script 直接引用 dist/vue.js 调试
调试 html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue.js grid component example</title>
<!-- <link rel="stylesheet" href="style.css"> -->
<!-- Delete ".min" for console warnings in development -->
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
<script>
let Child = {
template: `
<button @click="clickHandler($event)">
click me
</button>
`,
methods: {
clickHandler(e) {
console.log('Button clicked!', e)
this.$emit('select')
this.$emit('my')
}
}
}
let vm = new Vue({
el: '#app',
template: `
<div id="test">
<child @select="selectHandler" @my="selectHandler" @click.native.prevent="clickHandler"></child>
</div>
`,
methods: {
clickHandler() {
console.log('Child clicked!')
},
selectHandler() {
console.log('Child select!')
}
},
components: {
Child
}
})
</script>
</body>
</html>
初始化流程
简单的文本显示
let vm = new Vue({
el: '#app',
template: `
<div id="test">
{{msg}}
</div>
`,
data(){
return {
msg: 'dxx'
}
},
})
流程如下:
_init: 先简单理解是初始化准备,打印下实例:
重写的 $mount : 在 compileToFunctions (vue/src/compiler/to-function.js) 返回打个断点,可查看生成的 ast 和 render
执行 $mount:
流程上看:执行编译阶段生成的 render 函数,创建 vnode,然后将 vnode 生成 DOM ,渲染到页面中。
函数执行栈,先执行的函数进入栈底,先进后出:
就拿 mountComponent 和 Wathcer 来说:
mountComponent 函数入栈,里面调用 new Watcher,当 Watcher 执行完后,出栈,回到 mountComponent 环境中。
我将 mountComponent 中要等待 Wathcer 标记为等待,即:
function mountComponent (){
new Watcher();
等待 Watcher 执行完
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
其他的类推,需要栈顶完成出栈后再执行,整理了整个过程的等待函数位置:
render 的 Vnode 如下:
可以看到 外面是 div vnode,children 里面 是个文本 vnode
patch 函数逻辑
function patch(oldVnode, vnode, hydrating, removeOnly) {
// vnode 没有
if (isUndef(vnode)) {
// oldVnode 有 -> 销毁逻辑
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
// vnode 有
if (isUndef(oldVnode)) {
// 加载组件时,根元素
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 {
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
| 新节点 | 旧节点 | 处理 |
|---|---|---|
| 没有 | 有 | 销毁旧 invokeDestroyHook(oldVnode) |
| 没有 | 没有 | return |
| 有 | 有 | 非元素且一样 -> diff 算法;其他->创建新的 |
| 有 | 没有 | 创建新的 createElm(vnode, insertedVnodeQueue) |
createElm
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// 组件处理
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// 子 node 处理 还调 createElm
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
} else if (isTrue(vnode.isComment)) {
// 注释
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
// 文本
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
上例中,patch 时,oldVnode 是 div#test 元素,vnode 为 render 创建的,新旧都有且旧是元素,走 createElm(vnode,insertedVnodeQueue,oldElm._leaveCb ? null : parentElm,nodeOps.nextSibling(oldElm)) ,
创建元素,非组件,tag 为 div,vnode.elm 为空 div ,然后处理 children,child vnode.elm 为文本,将它插入到父 DOM(div) 中,子完成后,处理 div 的属性(invokeCreateHooks),将 div 放到 body 中。
createElm 执行完回到 patch,移除旧(div#app),执行(invokeInsertHook),返回 vnode.elm (div#test)
回到 _update,回到 Watcher,回到 mountComponent ,结束。
内容为组件
let App = {
render: h => h('div','dxx1')
}
new Vue({
render: h => h(App)
}).$mount('#app')
执行 _init:同上,因为 options 没有传 el,需要后面手动执行 $mount
$mount
区别:这里自定义 render 函数(而上例由 template 编译生成),省略编译步骤
生成组件 vnode:
patch 和原来一样,但 createElm 进入组件实例初始挂载(和 Vue 实例差不多),会等组件渲染完后,才执行根挂载。
组件 init vnode.data.hook.init :
1、new VueComponent -> _init
2、$mount
组件实例时注意:
1、vnode.componentOptions.Ctor 是什么:
组件 vnode 创建时 Ctor(VueComponent) = Vue.extend(组件对象)
Vue.extend 是将 VueComponent 原型指向 Vue.prototype,还有 Vue 的函数方法赋给 VueComponent
2、new VueComponent 的入参 options
{
_isComponent: true,
_parentVnode: vnode, // 父 vnode 即上图 vue-component-1
parent // activeInstance Vue 实例
}
组件 $mount
div Vnode
搞清楚 vm 中 _vnode, $vnode, $el 是什么
改变它们的地方:
_vnode
_update:
vm._vnode = vnode
$vnode
initRender:
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // 组件实例传来的
const renderContext = parentVnode && parentVnode.context
$el
mountComponent
vm.$el = el
_update
vm.$el = vm.__patch__()
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
声明:
vnode: <App /> vnode
渲染 vnode 页面真实渲染的: App 组件内容 即<div>dxx</div> 的 div vnode,
它们关系如下:
| 执行环境 | 方法 | $vnode 父 | _vnode 渲染 | $el |
|---|---|---|---|---|
| Vue | initRender | |||
| mountComponent | div#app | |||
| _update | App Vnode | |||
| VueComponent | initRender | App Vnode | ||
| mountComponent | 同上 | |||
| _update | 同上 | div Vnode | ||
__patch__ | 同上 | 同上 | div Vnode |
整个执行过程:
Vue 初始化,$mount('#app'),生成 App Vnode ;
update 中,创建 App DOM 是组件,进入组件初始化;
VueComponent 初始化,$mount(undefined),生成 div Vnode 渲染 vnode;
update 中,创建好 div DOM,将 Vue.$el 改为 创好的 div DOM,注意此时 DOM 还没有插入到 html 中;
组件 mountComponent 执行环境完成后,回到 Vue 上下文,修改初始 App Vnode.elm 为 div,执行 invokeCreateHooks
createComponent 中将组件 div 插入到 body 中;
执行 App Vnode, invokeInsertHook 组件 mounted 生命周期就在此时执行;
执行 Vue 的 mounted;
看一开始的例子
let Child = {
template: `
<button @click="clickHandler($event)">
click me
</button>
`,
methods: {
clickHandler(e) {
console.log('Button clicked!', e)
this.$emit('select')
this.$emit('my')
}
}
}
let vm = new Vue({
el: '#app',
template: `
<div id="test">
<child @select="selectHandler" @my="selectHandler" @click.native.prevent="clickHandler"></child>
</div>
`,
data(){
return {
msg: 'dxx'
}
},
methods: {
clickHandler() {
console.log('Child clicked!')
},
selectHandler() {
console.log('Child select!')
}
},
components: {
Child
}
})
这个相当于原来的 <App /> 包了一层 div,所以
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
这里不会执行
等父执行完 div#test 赋给 vm.$el
总结
本文用简单的例子调试分析 Vue 初始加载的整个流程。就是初始化, render 函数生成 vnode,vnode 生成 DOM,当 vnode 是组件节点,走组件流程,初始创建挂载组件;vnode 是元素、注释、文本,创建相应元素。整个是个深度遍历过程。所有子创建完后,最后挂到 body 中。