Vue源码解析-初始化流程

652 阅读5分钟

前言

想学习一个框架的源码最好从初始化流程开始逐步深入,本篇幅主要介绍vue的初始化流程Trust me 绝对完整

建议

本篇幅可能需要阅读大概半个小时左右可以泡上一杯雀巢或者一杯花茶慢慢看,但是我觉得花上一些时间弄懂这些还是值得的

开始我们的操作,接下来会有大量的源码图片

1.github.com/vuejs/vue 可以去这个地址把源码克隆下来

区分目录结构

阅读源码之前我们需要先把工具准备好

  源码下载下来之后需要生成sourcemap映射源码文件 方便我们在浏览器断点调试
  1.npm i //下载包的时候如果看到phantom.js终止就可以这个下载特别慢重点是咱们也用不到
  2.npm i -g rollup
  3.修改dev脚本 "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull-dev"
  4.npm run dev 
  5.之后会看到dist目录下生成了vue.js.map文件

现在就可以开始调试了

  • scripts 文件内有很多命令我们重点关注TARGET的值
  • dev命令指向的是 web-full-dev 我们可以全局搜一下
  • 这时候应该来到了 scripts/config.js 里面可以从这里开始看

这里就是根据环境来做区分该引用那些js,比如我们在vue-cli脚手架中用到的后缀就是esm的js,通常这类js是不会携带编译器因为在webpack中vue-loader会在打包的时候帮我们编译,我们现在调试的vue.js是带编译器的版本

我们可以看到web-full-dev引用的entry是web/entry-runtime-with-compiler.js需要进入到这个文件 我用的是vs code command+p全局搜文件名字就可以找到

  • 扩展$mount
  • 内部判断了vue挂载的优先级 大家都知道 vue挂载有几种方式 render > template > el 就是在这里区分的优先级
  • 接下来调用compileToFunctions函数将vue模板转化成render渲染函数 (内部大概流程是将模板转化成抽象语法树ast之后将ast代码生成为可执行的代码具体细节以后会讲 本次只关心渲染的流程)
  • 将render函数挂载到options上方便后续调用

这里只看到了vue的$mount扩展我们继续往后找Vue函数 查看runtime/index

  • patch函数挂载到Vue的原型上后面的流程diff计算的时候会用到该函数

  • $mount函数内部执行 mountComponent挂载函数

  • new Watcher 将更新函数传到watcher中等后续依赖注入完成调用_render渲染函数渲染虚拟dom

  • 我们继续看 core/index创建完成之后会回来继续看mountComponent挂载的过程

  • 初始化全局api 例如use、componet、delete、set、nexttick

  • 继续往后查找 instance/index

  • 在这个函数中我们就看到了Vue函数的定义还有其他初始化方法

  • 接下来看initMixin方法内部干了什么

  • 首先初始化_init方法也就是外部Vue函数里面调用的

  • 函数内部会进行选项合并主要是用户在new Vue传进来的选项和Vue本身的选项做个合并

  • 在这里我们看到执行了$mount并且是有个判断的vm.$options.el存在会执行这个挂载,这就证明了在外部new Vue的时候只要传了el后面不跟mount也能挂载因为在这个位置还会隐式挂载一次

  • 之后我们继续看下面几个初始化的方法

initLifecycle

  • 挂载$paren、$root、$children、$refs 等属性

initEvents

  • 挂载events事件

initRender

  • 挂载$slots、$createElement等方法 (我们在使用render挂载的时候 参数里面的h调用的就是此方法)

allHook(vm, 'beforeCreate') 执行beforeCreate生命周期 在这里我们就能知道 上面执行的在当前生命周期都能获取到

initInjections 依赖注入props、data

initState 这里就是将传进来的数据变为响应式的过程

initProvide 依赖注入完成

initState

  • 区分props> methods> data 设置属性的优先级
  • 将数据变为响应式
  • 之后调用initData

继续看一下initData做了什么事

  • 判断date是否是函数还是对象执行不同的操作
  • 判断propsmethods保证没有重复性
  • 调用proxy做代理使this可以直接访问到Vue实例上的属性
  • 调用observe将数据变为响应式

observe内部实现

  • 首先会判断当前传进来的变量是否加过响应式加过就返回否则继续执行
  • 调用Observer类将数据变为响应式并且创建依赖

Observer类

  • 创建一个Dep管理当前依赖 (这里我们可以叫大管家dep后续会讲到为什么是大管家)
  • 区分数组还是对象做不同的响应式处理
  • 对象的情况直接调用walk方法
  • 数组的情况需要判断是否有原型 (老的ie是没有的)
  • 在数组有原型的情况直接调protoAugment进行原型覆盖(这里主要是为了数组的增删改因为Object.defineProperty在语言层面检测不到数组的变化所以需要自己覆盖下数组的原型使用自己的方法检测
  • 在数组没有原型的情况调copyAugment复制一份

我们先看对象的情况是怎么处理的

  • 直接调用walk将数据循环遍历调用defineReactive
  • 我们看defineReactive做了些什么操作
  • 每创建一个响应式对象,这里就会创建一个dep管理当前自己的依赖 这里的dep我们可以看成 小管家dep只管理自己当前的依赖
  • 这里就是通过Object.defineProperty将数据变成响应式的过程并且和watcher创建依赖关系
  • 到这里依赖就已经注入完成创建完毕了,接下来就会执行$mount中的 mountComponent方法
  • 我们继续接着上面的mountComponent挂载开始讲

看下 mountComponent方法内部做了啥

  • 将更新函数赋值给updateComponent
  • new Watcher 将更新函数传到watcher中等后续依赖注入完成调用_render渲染函数渲染vdom

Watcher类内部

  • 调用this.get方法
  • get方法内部会执行getter方法这里的getter方法也就是Watcher类接收的第二个参数_render渲染函数如果是在浏览器打断点调试的话到这里就可以看到页面初始化完成了

接下来在看一下依赖收集过程(但是注意这段是触发get的时候才会执行的)

  • dep内部有几个方法addSub是将依赖放到当前数组中方便之后notify进行更新

  • defineReactive中触发的方法的depend方法就是dep中的depend

  • notify方法就是更新的时候调的批量更新subs中的依赖就可以(subs中大家看到的update方法是在watcher中关联的时候创建的这里涉及到更新过程之后会讲解)

  • depend方法的内部Dep.target指向的就是Watcher类我们在来看一下watcher类里面的方法

  • addDep方法里面我们可以看到和外面的dep做了关联保证了以后的更新流程这里就是依赖收集的过程(但是注意依赖收集初始化的时候是不会做的当你用的时候才会触发get做依赖收集这里只是介绍)

  • update方法同步的时候会走run函数
  • run函数内部调用的也是get方法

我们在来看一下get方法内部

  • get方法内部主要调用的是getter方法
  • getter是在new Watcher的时候传进来的是上面提到的_render渲染函数

我们可以再来看一下watcher类的第二个参数就是我们说的getter,而这里接收的参数就是上面mountComponent里面执行的挂载的时候传进来的_render

所以最后run函数内部调用的是get执行的是render渲染函数

有个问题说明一下 上面提到的大管家dep和小管家dep

  • 大管家dep的出现主要是为了弥补Object.defineProperty语言层面的不足vue里面有两个方法 setdelete是用来更新data属性的 这两个方法在内部用的时候更新完成之后就会调用一下大管家dep进行数据更新
  • 小管家dep就比较简单只是为了管理当前的依赖 和watcher之间产生对应依赖关系方便以后更新操作

vue的初始渲染流程到这里就讲完了我们在串一遍

1.从Vue函数内部的this._init开始看看this._init具体做了啥

  • 合并选项
  • 初始化全局api和方法
  • 挂载$mount

2.之后看initState方法

  • props>methods>data优先级判断
  • 调用initData方法

3.initData方法

  • 判断date是否是函数还是对象执行不同的操作
  • 判断prose和methods保证没有重复性
  • 调用proxy做代理使this可以直接访问到实例上的属性
  • 调用observe将数据变为响应式

4.observe方法

  • 首先判断数据是否是响应式的如果是直接返回否则继续执行
  • 调用Observer类将数据变为响应式

5.Observer类

  • 创建dep管理当前依赖
  • 区分数组还是对象做不同的响应式处理
  • 数组的情况需要判断是否有原型 (老的ie是没有的)
  • 有原型的情况直接调protoAugment进行原型覆盖
  • 没有原型的情况调copyAugment复制一份

6.defineReactive方法

  • 创建一个dep这里是小管家dep
  • 这里就是通过Object.defineProperty将数据变成响应式的过程也是依赖收集的过程只是在get被触发的时候才会做
  • 以上步骤完成之后就是创建完成了,之后会调用 最开始$mount 内部的 mountComponent执行挂载过程

7.mountComponent方法

  • 将更新函数赋值给updateComponent
  • new Watcher 将更新函数传到watcher中等后续依赖注入完成调用_render渲染函数渲染虚拟dom

8.Watcher类内部

  • 1.调用get
  • 2.get内部调用getter方法(getter就是new Watcher传进来的更新函数到此更新完成)

到这里本篇幅就算结束了之后会有nexttickrender函数渲染原理包括全局组件注册过程来袭

创作不易如果觉得有帮助的话点个赞吧😁