前言
通过debug源码的方式学习 Vue2.6
的整体构建流程,建议先不要关注具体的实现细节。
start
在vue源码中新建 examoples/demo/index.html
文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
{{name}}
<input type="text" v-model="name">
</div>
</body>
</html>
<script>
new Vue({
el: '#app',
data() {
return {
name: 'test'
}
}
})
</script>
修改 package.json
中的脚本命令,添加--sourcemap
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap",
关键节点的断点位置
加载完vuejs资源,根据平台重写$mount
给Vue的构造函数挂载方法并根据传入的options初始化
this._init(options)
initLifecycle(vm) // 给实例挂载$parent、$root、$children等属性
initEvents(vm) // 挂载_events
initRender(vm) // 最主要的是挂载$createElement,$createElement可以创建 VNode
callHook(vm, 'beforeCreate')
initInjections(vm) // 处理inject
initState(vm) // 初始化props、methods、data
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
initData(vm),拦截数据设置get、set,对象递归监听、数组改写原型链
如果有el参数,就直接执行mount()
调用改写后的$mount(),先处理el获取template
,通过compileToFunctions()获取render
挂载到options上
将模版template转换成了render函数,接下来看下具体细节
加载Vue的时候引入 compileToFunctions
方法,依次会触发 createCompiler()、createCompilerCreator()
,返回函数真正的createCompiler
函数
此时才开始调用compile()编译模版
parse() 解析template模版,最终生成ast。继续看这一步的实现
parseHTML() 正则匹配解析模版字符串,并且对应各种vue指令的逻辑,最终处理完的就是ast
得到ast,是否需要优化,再转成render
"with(this){return _c('div',{attrs:{"id":"app"}},[_v("\n "+_s(name)+"\n "),_c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})])}"
将render转换成render函数
最终的render函数
开始mountComponent()
将组件作为一个watcher实例
new Watcher()的时候,构造函数中调用了原型方法get
,触发劫持数据中的get,收集依赖(也就是watcher实例)
value = this.getter.call(vm, vm),有两个作用
作用一:先调用 updateComponent 方法,执行vm._render()
vm._render()
最终调用挂载在vm.$options上的render
函数,最终生成Vnode(过程见下)
作用二:触发侦测数据的getter,自动收集依赖(也就是本wathcer实例)
执行vm.$options上的render
函数,render函数内部调用了_c
方法
(function anonymous() {
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_v("\n " + _s(name) + "\n "), _c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (name),
expression: "name"
}],
attrs: {
"type": "text"
},
domProps: {
"value": (name)
},
on: {
"input": function($event) {
if ($event.target.composing)
return;
name = $event.target.value
}
}
})])
}
}
)
_c
在initRender()的时候就挂载在了Vue原型上
执行_c()
其实就是执行_createElement()
根据不同的节点,实例花Vnode创建不同类型的虚拟dom
然后再执行vm._update()
, 进行虚拟Dom的patch比较
Vue.prototype.__patch__
createPatchFunction()并在函数内部声明createElm() 函数, 返回path方法
patch阶段
开始执行patch(), patch()内部调用createElm()函数
createElm()函数内部调用createChildren(),也会调用invokeCreateHooks()
createChildren()内部再调用createElm()函数,形成递归
invokeCreateHooks()循环执行对应的更新方法,并通过浏览器的原生方法操作dom完成挂载
patch()结束后并将返回真实dom节点返回
双向绑定
v-model指令别解析识别,会执行updateDirectives(),调用指令的 inserted 方法
指令的 inserted 方法内监听事件
修改input的值,触发侦测数据的setter,通知依赖watcher
怎么触发setter的呢?
执行 watcher 的update()
queueWatcher() 队列缓存,调用nextTick(flushSchedulerQueue)
nextTick 用微任务实现,避免频繁更新,修改完成以后统一更新
执行watcher.run(),内部再次调用this.get()
,触发render()