一、Vue源码目录结构
1、目录practice\vue-2.6.14\src\compiler(与平台无关的)
compiler负责将模板转化为render函数,而render函数负责创建虚拟DOM2、目录practice\vue-2.6.14\src\core(与平台无关的)core是vue的核心。core\components中定义了组件keep-alive;core\global-api中定义了vue的静态方法(Vue.component、Vue.filter、Vue.mixin、Vue.use等)core\instance是创建vue实例的位置,定义了vue的构造函数、初始化以及生命周期钩子函数core\observer是响应式机制实现的位置(重点)core\util是vue项目源码依赖的功能方法core\vdom是vue项目的虚拟DOM实现的方法,其中重写了snabbdom,增加了组件的机制 3、目录practice\vue-2.6.14\src\platforms(与平台相关的)web平台下的相关代码,其中包含了一些入口文件weex(基于vue的移动端开发框架)平台下的相关代码 4、目录practice\vue-2.6.14\src\server- 服务端渲染的相关代码
5、目录
practice\vue-2.6.14\src\sfc SFC(Single File Components)会将单文件组件转化为JS对象 6、目录practice\vue-2.6.14\src\shared- 公共的常量和方法
二、Vue源码的打包与输出
1、打包
- 打包工具
Rollup,比webpack更轻量,只处理JS文件更适合像vue这样的库使用,打包不会产生冗余代码。 - 安装依赖
npm i
- 设置
sourcemap。在package.json文件中的dev脚本中添加参数--sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
- 执行
npm run dev,其中-w是监听文件变化,-c是指定配置文件 2、输出文件
vue不同构建版本文件的区别Full(完整版):同时包含了编译器和运行时的版本Compiler(编译器):用来将模板字符串编译成为 JavaScript 渲染函数的代码,体积大、效率低Runtime-only(运行时):用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码,体积小、效率高。基本上就是除 去编译器的代码UMD (Universal Module Definition)(通用模块定义),支持AMD与CommonJS模块方式。vue.js 默认文件就是运行时 + 编译器的UMD版本CommonJS(cjs): CommonJS 版本用来配合老的打包工具比如Browserify或webpack1ES Module: 从 2.6 开始 Vue 会提供两个 ES Modules (ESM) 构建文件,为现代打包工具提供的版本。SM格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行tree-shaking并将用不到的代码排除出最终的包。
三、Vue初始化过程
1、确定入口文件practice\vue-2.6.14\src\platforms\web\entry-runtime-with-compiler.js
2、确定Vue的构造函数
==> vue-2.6.14\src\platforms\web\entry-runtime-with-compiler.js(重写Vue.prototype.$mount)
==> vue-2.6.14\src\platforms\web\runtime\index.js(注册平台相关的指令与组件:v-model、v-transtion)
==> vue-2.6.14\src\core\index.js(调用initGlobalAPI初始化Vue的静态方法)
==> vue-2.6.10\src\core\instance\index.js(初始化Vue实例的方法和属性)
3、Vue初始化静态成员(initGlobalAPI)
初始化全局APIVue.extend Vue.nextTick Vue.set Vue.delete Vue.directive Vue.filter Vue.component Vue.use Vue.mixin``Vue.compile Vue.observable Vue.version
4、Vue初始化实例成员
// 此处不用 class 的原因是因为方便后续给 Vue 实例混入实例成员
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// 调用 _init() 方法
this._init(options)
}
// 注册 vm 的 _init() 方法,初始化 vm
initMixin(Vue)
// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)
Vue构造函数定义文件中调用initMixin方法,在initMixin方法中调用initState
// vm 的生命周期相关变量初始化
// $children/$parent/$root/$refs
initLifecycle(vm)
// vm 的事件监听初始化, 父组件绑定在当前组件上的事件
initEvents(vm)
// vm 的编译render初始化
// $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
initRender(vm)
// beforeCreate 生命钩子的回调
callHook(vm, 'beforeCreate')
// 把 inject 的成员注入到 vm 上
initInjections(vm) // resolve injections before data/props
// 初始化 vm 的 _props/methods/_data/computed/watch
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// created 生命钩子的回调
callHook(vm, 'created')
...
// 调用 $mount() 挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
四、Vue首次渲染的过程
图解首次渲染
- 在首次渲染之前,首先进行
Vue初始化,初始化实例成员和静态成员 - 当初始化结束之后,要调用
Vue的构造函数new Vue(),在构造函数中调用了_init()方法,这个方法相当于我们整个Vue的入口 - 在
_init方法中,最终调用了$mount,一共有两个$mount,- 第一个定义在
entry-runtime-with-compiler.js文件中,也就是我们的入口文件$mount,这个$mount()的核心作用是帮我们把模板编译成render函数,但它首先会判断一下当前是否传入了render选项,如果没有传入的话,它会去获取我们的template选项,如果template选项也没有的话,他会把el中的内容作为我们的模板,然后把模板编译成render函数,它是通过compileToFunctions()函数,帮我们把模板编译成render函数的,当把render函数编译好之后,它会把render函数存在我们的options.render中。 - 接着会调用
src/platforms/web/runtime/index.js文件中的$mount方法,在这个中首先会重新获取el,因为如果是运行时版本的话,是不会走entry-runtime-with-compiler.js这个入口中获取el,所以如果是运行时版本的话,我们会在runtime/index.js的$mount()中重新获取el`。
- 第一个定义在
- 接下来调用
mountComponent(),这个方法在src/core/instance/lifecycle.js中定义的,在mountComponent()中,首先会判断render选项,如果没有render选项,但是我们传入了模板,并且当前是开发环境的话会发送一个警告,目的是如果我们当前使用运行时版本的Vue,而且我们没有传入render,但是传入了模版,告诉我们运行时版本不支持编译器。接下来会触发beforeMount这个生命周期中的钩子函数,也就是开始挂载之前。 - 然后定义了
updateComponent(),在这个函数中,调用vm._render和vm._update,vm._render的作用是生成虚拟DOM,vm._update的作用是将虚拟DOM转换成真实DOM,并且挂载到页面上 - 创建
Watcher对象,在创建Watcher时,传递了updateComponent这个函数,这个函数最终是在Watcher内部调用的。在Watcher内部会用了get方法,当Watcher创建完成之后,会触发生命周期中的mounted钩子函数,在get方法中,会调用updateComponent() - 挂载结束,最终返回
Vue实例。
五、数据响应式原理
1、Watcher分为3种,计算属性Watcher、用户Watcher(侦听器)、渲染Watcher。
2、响应式原理过程
Vue的响应式是从Vue的实例init()方法中开始的- 在
init()方法中先调用initState()初始化Vue实例的状态,在initState方法中调用了initData(),initData()是把data属性注入到Vue实例上,并且调用observe(data)将data对象转化成响应式的对象 observe是响应式的入口- 在
observe(value)中,首先判断传入的参数value是否是对象,如果不是对象直接返回 - 再判断
value对象是否有__ob__这个属性,如果有说明做过了响应式处理,则直接返回 - 如果没有,创建
observer对象,然后返回observer对象
- 在
- 在创建
observer对象时,给当前的value对象定义不可枚举的__ob__属性,记录当前的observer对象,然后再进行数组的响应式处理和对象的响应式处理- 数组的响应式处理,就是设置数组的几个特殊的方法,
push、pop、sort等,这些方法会改变原数组,所以这些方法被调用的时候需要发送通知- 找到数组对象中的
__ob__对象中的dep,调用dep的notify()方法 - 再遍历数组中每一个成员,对每个成员调用
observe(),如果这个成员是对象的话,也会转换成响应式对象
- 找到数组对象中的
- 对象的响应式处理,就是调用
walk方法,walk方法就是遍历对象的每一个属性,对每个属性调用defineReactive方法
- 数组的响应式处理,就是设置数组的几个特殊的方法,
defineReactive会为每一个属性创建对应的dep对象,让dep去收集依赖,如果当前属性的值是对象,会调用observe,defineReactive中最核心的方法是getter和settergetter的作用是收集依赖,收集依赖时,为每一个属性收集依赖,如果这个属性的值是对象,那也要为子对象收集依赖,最后返回属性的值- 在
setter中,先保存新值,如果新值是对象,也要调用observe,把新设置的对象也转换成响应式的对象,然后派发更新(发送通知),调用dep.notify()
- 收集依赖时
- 在
watcher对象的get方法中调用pushTarget, 记录Dep.target属性 - 访问
data中的成员的时候收集依赖,defineReactive的getter中收集依赖 - 把属性对应的
watcher对象添加到dep的subs数组中,也就是为属性收集依赖 - 如果属性的值也是对象,给
childOb收集依赖,目的是子对象添加和删除成员时发送通知
- 在
- 在数据发生变化的时候
- 调用
dep.notify()发送通知,dep.notify()会调用watcher对象的update()方法 update()中的调用queueWatcher(),会去判断watcher是否被处理,如果这个watcher对象没有被处理的话,添加到queue队列中,并调用flushScheduleQueue()- 在
flushScheduleQueue()中触发beforeUpdate钩子函数 - 调用
watcher.run():run()-->get()-->getter()-->updateComponent() - 然后清空上一次的依赖
- 触发
actived的钩子函数 - 触发
updated钩子函数 3、图解数据响应式原理
- 调用
4、图解Vue响应式的原理
六、添加、删除、监听响应式属性
1、set方法
set方法分为两种:Vue.set()在core\global-api\index.js中定义和vm.$set()在core\instance\index.js中定义。set方法在修改数组元素时,是调用splice(key, 1, val)方法,最终在core\observer\array.js通过ob.dep.notify()发送通知。set方法在修改响应式对象属性时,是调用defineReactive(obj.value, key, val)方法,最后通过ob.dep.notify()发送通知。
2、delete方法
delete方法的功能是删除对象属性时,如果对象时响应式对象,确保删除能触发更新视图。该方法主要用于避开Vue不能检测到属性被删除的限制,但应该很少使用它。delete方法分为两种:Vue.delete()在core\global-api\index.js中定义和vm.$delete()在core\instance\index.js中定义。delete方法在删除数组元素时,是调用splice(key, 1)方法,最终在core\observer\array.js通过ob.dep.notify()发送通知。set方法在删除响应式对象属性时,是调用delete target[key]方法,最后通过ob.dep.notify()发送通知。
3、watch方法
Watcher创建顺序:计算属性Watcher、用户Watcher(侦听器)、渲染Watcher。前两者在initState中创建,后者在重写$mount方法的mountComponent执行。vm.$watch()在core\instance\state.js中定义。watch方法没有静态方法,因为$watch方法中要使用到Vue的实例- 计算属性
Watcher会配置this.lazy = true, 因此在实例化之后不会立即获取值,而是在渲染模板阶段获取值 - 用户
Watcher(侦听器)会配置this.user = true, 表示在run ()中会给用户Watcher的回调的执行添加try...catch...
4、nextTick方法
nextTick方法的功能是在下次DOM更新循环结束之后执行延迟回调。在修改数据之后使用这个方法,获取更新后的DOM。nextTick方法分为两种:Vue.nextTick()在core\global-api\index.js中定义和vm.$nextTick()在core\instance\index.js中定义。nextTick方法的核心是timerFunc的处理,nextTick在执行回调函数时,会先将回调函数放到一个callback的数组中,紧接着会优先以微任务的方式处理回调函数,若浏览器不支持微任务会降级为宏任务。- 在
watch类的queueWatcher方法中会调用nextTick(flushSchedulerQueue)将观察者队列作为参数传入,异步执行所有的watch对象的更新DOM操作。 setImmediate的性能优于setTimeout,即使将setTimeout的时间设置为0,也会等待4ms再执行。nextTick用来异步获取DOM的最新数据,本身仅仅是用来开启一个微任务或者宏任务,所以它不会造成页面的卡顿,除非回调函数中的代码过于耗时会导致页面卡顿
七、Vue虚拟DOM
1、为什么使用虚拟DOM?
- 避免直接操作
DOM,提高开发效率 - 作为一个中间层可以跨平台
- 虚拟
DOM不一定可以提高性能- 首次渲染的时候会增加开销
- 复杂的视图情况下提升渲染性能(避免多次重排和重绘)
2、
h函数
- 调用
vm.$createElement(tag, data, children, normalizeChildren)返回VNode对象 tag(标签名或组件对象)、data(描述tag,可以设置DOM的属性或标签的属性)、children(tag中的文本内容或者子节点)VNode对象的核心属性tag、data、children、text、el(真实DOM)、key(用于遍历时的复用)
3、图解整体过程
4、模板转换成视图的过程
5、当v-for遍历创建VNode时,设置key可以减少DOM操作的次数。
八、模板编译和组件化
1、梳理模板编译的入口
2、模板编译的过程
- 模版编译入口函数
compileToFunctions- 内部首先从缓存加载编译好的
render函数,如果缓存中没有,调用compile开始编译
- 内部首先从缓存加载编译好的
- 在
compile函数中,首先合并选项options,调用baseCompile编译模版 compile的核心是合并选项options, 真正处理是在basCompile中完成的,把模版和合并好的选项传递给baseCompile, 这里面完成了模版编译的核心三件事情parse(): 把模版字符串转化为AST对象,也就是抽象语法树optimize(): 对抽象语法树进行优化,标记静态语法树中的静态根节点(静态根节点是标签中除了文本内容以外,还需要包含其它标签)。检测到静态子树,设置为静态,不需要在每次重新渲染的时候重新生成节点。patch的过程中会跳过静态根节点generator(): 把优化过的AST对象,转化为字符串形式的代码
- 执行完成之后,会回到入口函数
complieToFunctionscompileToFunction会继续把字符串代码转化为函数- 调用
createFunction - 当
render和staticRenderFns初始化完毕,最终会挂在到Vue实例的options对应的属性中
3、组件化回顾
4、要点
- 当调用
$mount时仅仅是创建真实DOM,但并没有将组件添加到DOM结构中,真正的挂载是在patch.js中的createComponent函数的insert方法把组件对应的DOM插入到父元素中。 - 组件的创建过程是先创建父组件后创建子组件,组件的挂载过程是先挂载子组件后挂载父组件。
5、虚拟 DOM 中 Key 的作用和好处
- 作用:追踪列表中哪些元素被添加、被修改、被移除的辅助标志。可以快速对比两个虚拟
DOM对象,找到虚拟DOM对象被修改的元素,然后仅仅替换掉被修改的元素,然后再生成新的真实DOM - 好处:可以优化
DOM的操作,减少Diff算法和渲染所需要的时间,提升性能。