作者简介:
???乐观正能量。
目标
环境搭建
掌握源码学习方法
vue初始化过程剖析
深入理解数据响应式
资源
vue源码地址 :https://github.com/vuejs/vue
知识点
获取vue
项目地址:https://github.com/vuejs/vue
迁出项目:git clone https://github.com/vuejs/vue.git
当前版本号:2.6.11
文件结构
flow是为ts写的
源码目录
调试环境搭建
前提条件:在整个目录下不要出现中文路径
安装依赖:npm i(yarn应该也是可以的)
安装rollup:npm i -g rollup
当npm i安装到phantomjs时,下载会非常慢,这时候就没必要去等待了。终止即可。(这个依赖是做什么的?主要是用在端到端测试的,安装过程中很容易出错,因为我们暂时不用,所以安装过程中跳过。)
修改dev脚本,添加sourcemap,package.json
(有什么用呢??生成的代码和源码之间有一定的映射关系)
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull-dev"
运行开发命令:npm run dev
引入前面创建的vue.js,samples/commits/index.html
映射文件的作用是让我们在调试的时候看到源码的基本结构。
比如说:我们在example中随便打开一个
打开浏览器控制台,打开sources,标记断点:页面刷新下,触发断点:
右键选择“reveal in sidebar”
这时候就可以看到它在整个源码中的路径,具体的所在目录,于是大家就可以愉快的学习源码。
术语解释:
runtime:仅包含运行时,不包含编译器
common:cjs规范,用于webpack1
esm:ES模块,用于webpack2+
常见于用webpack2.0以上的版本去打包的时候
umd: universal module definition,兼容cjs和amd,用于浏览器
但是今天的测试都是包含编译器版本的,所以我们用的是umd。常见于用script的方式直接引入,我们引用的是vue.js中间什么都没有。说明我们引用的是umd版本。
入口
dev脚本中 -c scripts/config.js 指明配置文件所在 参数 TARGET:web-full-dev 指明输出文件配置项,line:123
// Runtime+compiler development build (Browser)
{
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'), // 入口
dest: resolve('dist/vue.js'),// 目标文件
format: 'umd', // 输出规范
env: 'development',
alias: { he: './entity-decoder' },
banner,
},}
查找方式有两种:
第一种:堆栈
如果说想找一个vue在初始化的时候都经过了哪些流程,我们可以在不同的地方打断点
当把整个流程都跑的差不多了
我们可以在浏览器中看到它的流程堆栈,我们可以把这个图记录下来,方便今后的学习中使用。
第二种:通过对打包工具的了解。
rollup是什么意思呢?rollup和webpack很相似,都是打包工具,只不过,rollup以前特别流行在js库中,它的打包,不涉及其他东西,像素材之类的,但并不代表他不可以做,rollup里面跟了一个config.js,我们的配置文件就可以在里面去查找。
你就会发现它在打包的时候,会涉及到各种各样的插件,完成不同的任务。这一点和webpack是一致的,同时它也需要接收一下各种各样的参数,来完成我们最终打包的任务,打包配置从builds开始了
怎么看呢?package.json文件中,配置中有target+“关键字”
然后在config中,搜一下这个关键字
entry:入口文件。但是它在哪个目录呢?显然“web/XXX”不足以告诉我们它的具体目录,所以我们可以看一下resolve,点进去:针对刚才的例子,我们在split拿到的是“/web”
这个封装的函数本质的作用是从我们定义的别名的映射对象里头,找到刚才那个文件的前缀地址。所以我们去alias里面找一个web对应的前缀,
于是我们就定位到了整个程序的入口文件了,打包的入口文件就在这里了,找到前缀后,再把刚才的名字加起来,
这个就是携带编译器的比较完整的入口文件了。
初始化流程
整体流程
new Vue()
_init()
$mount()
mountComponent()
updateComponent()
render()
update()
new Watcher()
new Vue都发生了哪些事情?使用断点调试,先进去它的构造函数,在构造函数里面有个_init(),init方法里面有一些用于初始化的方法,下面具体介绍。
入口 platforms/web/entry-runtime-with-compiler.js
扩展默认$mount方法:处理解析template、el等选项
写法为flow写法,有些类似于ts,优先级顺序:el
web平台常见的初始化的方式是,直接在上面初始化模版,当设置el的时候,这个东西就作为templent选项。这东西如果想成为模版,它需要一个编译器,将它编译成render。这也就是说大家平时用webpack,vue-cli创建的项目,我们只能写render不能使用el呢,因为我们使用的vue的版本不带编译器,所以我们不能声明成字符串模版。
为什么要编译?template.charat(0)==='#'中可以看到我们设置编译器的方式有可能是selector;"template.innerHtml",也有可能是设置的字符串模版;template.nodeType,可以是dom元素;如果设置了el,就将getOuterHTML(el)作为模版。获取模版之后编译它,编译的目标:获取渲染函数。(render方法)所以我们平时应用render方法,而不是用el或template,就是为了省去了编译过程。因为我们运行的vue没有编译器。
compileToFunctions他把这个模版最终渲染成一个render和stateRenderFns.
options.render=render这个渲染函数还会重新赋值给options.
platforms/web/runtime/index.js
安装web平台特有指令和组件
定义__patch__:补丁函数,执行patching算法进行更新 (diff过程)
定义$mount:挂载vue实例到指定宿主元素(获得dom并替换宿主元素)
mountComponent(this,el.hydrating)挂载执行
el:要挂载的宿主元素,使用三木运算符,证明它是可选参数。
core/index.js
初始化全局api
直接暴露给外面去用,defineReactive:响应函数。
具体如下:
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
initUse(Vue) // 实现Vue.use函数
initMixin(Vue) // 实现Vue.mixin函数
initExtend(Vue) // 实现Vue.extend函数
initAssetRegisters(Vue) // 注册实现Vue.component/directive/filter
如果想要看vue.use,这里有个initUse
Vue是个构造函数,我们在调用vue.use的时候,是静态方法,直接在vue的上面去挂,是个函数,接收一个插件,插件可能是函数还可能是对象,平时的用法是Vue.use(MyPlugin),vueRouter本身是个class,class本身是函数。也就是说我们传递的是个函数进来的,后续还有可能还会有其他可能的参数arg1,arg2等。它接收到参数从第一位开始将它转换成数组,就是说将除了插件之外的参数全部拿出来,unshift(this),插件必须实现一个install方法,install方法为什么里面是vue的构造函数?因为unshift的传的this就是vue。如果传进来的插件是个函数并且有install方法,则会执行它;如果plugin直接就是一个函数,则它就把这个函数当成install方法去执行。所以我们平时写的时候传递的并不是一个对象,我们传递的是个class,class本身是函数。我们在这个函数的上面又附加了一个install方法。这个方法会被vue去调用,
core/instance/index.js
Vue构造函数定义
定义Vue实例API
function Vue (options) {
// 构造函数仅执行了_init
this._init(options)问题这个init方法哪来的?
}
initMixin(Vue) // 实现init函数,混入了_init()
stateMixin(Vue) // 状态相关api $data,$props,$set,$delete,$watch
eventsMixin(Vue)// 事件相关api $on,$once,$off,$emit
lifecycleMixin(Vue) // 生命周期api _update,$forceUpdate,$destroy
renderMixin(Vue)// 渲染api _render,$nextTick
我们先把Vue构造函数作为参数传进去了,该方法为给vue的原型实现一个init方法,可以理解为实例方法。
合并哪些选项:用户设置的选项和系统默认的一些选项(components、dereactives、filters)这些东西会和用户传递过来的一些额外的事件、回调函数、data做合并。
core/instance/init.js
创建组件实例,初始化其数据、属性、事件等
初始化过程:组件属性、事件等初始化、两个生命周期、数据响应式
initLifecycle(vm) // $parent,$root,$children,$refs
initEvents(vm) // 处理父组件传递的事件和回调 事件监听
initRender(vm) // $slots,$scopedSlots,_c,$createElement
callHook(vm, 'beforeCreate')//组件创建之前的钩子
initInjections(vm) // 获取注入数据
initState(vm) // 初始化props,methods,data,computed,watch
initProvide(vm) // 提供数据注入
callHook(vm, 'created')
callHook(vm,'beforeCreact')//组件创建之前钩子;
initLifecycle//$parent/$root/$children;这时候跟组件能找到,但是还没有孩子。都为空
initEvents:事件监听,有时候在当前事件中写一些事件绑定,其实真正这个事件监听和派发者应该是它的子元素。应该是放这个事件的子组件上头。它会把父级监听器里面存放的监听器,直接处理一下。这里面初始化的事件,其实是在父级做的监听,声明的地方在父级,监听的地方在子级。
initRender:h函数,就是vm.$createElement,所以虚拟dom的真正创造者就是vm.$createElement。slots、$createElement()都是在这文件中声明的。
initInjections:注入祖辈传递的数据。resolve provide after data/props
initState:重要:组件数据初始化,包括props/data/methods/computed/watch,所以我们每次写数据都要在created或者mount里面去写,因为这时候数据都已经非常安全的初始化了。所以我们会在created钩子函数中去访问数据。不会在beforeCreate里面去访问。
问题:为什么先injections(注入)再provide呢?先从祖辈中继承过来,再传递给后代。
$mount
- mountComponent
执行挂载,获取vdom并转换为dom
- new Watcher()
创建组件渲染watcher
- updateComponent()
执行初始化或更新
- update()
初始化或更新,将传入vdom转换为dom,初始化时执行的是dom创建操作
- render() src\core\instance\render.js
渲染组件,获取vdom