获取Vue
项目地址:https://github.com.vuejs/vue
迁出项目:git clone https://github.com.vuejs/vue.git
当前版本号:2.6.11
文件结构:
源码目录
调试环境搭建
-
安装依赖:npm i 或者yarn
安装到phantom.js即可终止
-
安装rollup:npm i -g rollup
-
修改dev脚本,添加–sourcemap,在package.js文件里
-
运行开发命令:npm run dev 或 yarn dev会在dist目录下生成vue.js文件和vue.js.map(其主要作用就是调试过程中文件映射更好的查找源文件,即加上–sourcemap的作用)
-
引入前面创建的vue.js,在samples/commits/index.html
<script src="../../dist/vue.js"></script>
术语解释:
- runtime:仅包含运行时,不包含编译器
- common:cjs规范,用于webpack1
- esm:ES模块,用于webpack2+
- umd: universal module definition,兼容cjs和amd,用于浏览器
注:平时项目开发用webpack编译使用的版本是vue-runtime-esm.js
文件解析
src\platforms\web\entry-runtime-with-compiler.js
- 入口文件
- 解析模板相关选项
$mount()
函数里面,先判断render函数=>再判断template=>最后el选项
mount的作用最终都是把el和template编译为render函数,所以我们在项目后面没有el和template选项时,要在app后加上$mount(’#app’)函数
src\platforms\web\runtime\index.js
- 安装平台patch,实现跨平台操作
- 实现$mount(’#app’)=>mountComponent:render()=>vdom=>patch()=>dom
注:mountComponent()方法在lifecylce.js文件里
src\core\index.js
- 初始化全局API
src\core\instance\index.js
- Vue的构造函数
- 声明实例属性和方法
src\core\instance\init.js
- 初始化
创建组件实例、初始化其数据、属性、事件等
src\core\instance\lifecycle.js
- mountComponent()=>updateComponent=>new Watcher()=>updateComponent()=>render()=>update()=>patch()主要作用是把虚拟dom编译成真实dom
几个生命周期钩子函数都在这里除了beforeCreate和created
$mount
- mountComponent()-执行挂载,获取dom并转换为dom
- new Watcher()-创建组件渲染watcher
- updateComponent()-执行初始化或更新
- update()-初始化或更新,将传入的vdom转换为dom,初始化时执行的是dom创建操作
- render()- src/core/instance/render.js,渲染组件,获取vdom
整体流程捋一捋
new Vue()=>_init()=>$mount()=>mountComponent()=>new Watcher()=>updateComponent()=>render()=>_update()
一道相关面试题:谈谈vue生命周期
- 概念:组件创建、更新和销毁过程
- 用途:生命周期钩子使我们可以在合适的时间做合适的事情
- 分类列举:
- 初始化阶段:beforeCreate、created、beforeMount、mounted
- 更新阶段:beforeUpdate、updated
- 销毁阶段:beforeDestroy、destroyed- 应用:
- created时,所有数据准备就绪,适合做数据获取、赋值等数据操作
- mounted时,$el已生成,可以获取dom;子组件也已挂载,可以访 问它们
- updated时,数值变化已作用于dom,可以获取dom最新状态
- destroyed时,组件实例已销毁,适合取消定时器等操作
数据响应式
数据响应式是MVVM框架的一大特点,通过某种策略可以感知数据的变化。Vue中利用了JS语言特性Object.defineProperty()
,通过定义对象属性getter/setter拦截对属性的访问
具体实现是在Vue初始化时,会调用initState,它会初始化data,props等
整体流程:
initState(vm:Component) src/core/instance/state.js
初始化数据,包括props,methods,data,computed和watch
initData核心代码是将data数据响应化
function initData (vm: Component) {
// 执行数据响应化 observe(data, true /* asRootData */)
}
core/observer/index.js
observe方法返回一个Observer实例
Observer对象根据数据类型执行相应的响应化操作
defineReactive定义对象属性的getter/setter,getter负责添加依赖,setter负责通知更新
core/observer/dep.js
Dep负责管理一组Watcher,包括watcher实例的增删及通知更新
Watcher解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API和指令中
每个组件也会有对象的Watcher,数值变化会触发其update函数导致重新渲染
export default class Watcher {
constructor(){}
get(){}
addDep(){}
update(){}
}
数组响应化
数组数据变化的侦测跟对象不同,我们操作数组通常使用push、pop、splice等方法,此时没有办法得知数据变化。所以vue中采取的策略是拦截这些方法并通知dep
src/core/observer/array.js
为数组原型中的7个可以改变内容的方法定义拦截器
Observer中覆盖数组原型
if (Array.isArray(value)) {
// 替换数组原型
protoAugment(value, arrayMethods) // value.__proto__ = arrayMethods
this.observeArray(value)
}
Vue异步更新策略
Vue高效的秘诀是一套批量、异步的更新策略
事件循环Event Loop:浏览器为了协调事件处理、脚本执行、网络请求和渲染等任务而制定的工作机制
宏任务Task:代表一个个离散的、独立的工作单元。浏览器完成一个宏任务,在下一个宏任务执行开始前,会对页面进行重新渲染。主要包括创建文档对象、解析HTML、执行主线JS代码以及各种页面加载、输入、网络事件和定时器等
微任务:微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务。如果存在微任务,浏览器会清空微任务之后在重新渲染。微任务的例子有Promise回调函数、DOM变化等
浏览器在一次事件循环中,只做一个宏任务,执行所有同步代码,异步代码放到异步队列中(宏任务如setTimeout定时器等放到宏任务队列中,微任务如promise.then等放到微任务队列中),在下一次事件循环前,执行微任务队列并清空。
Vue中的具体实现
nextTick(flushSchedulerQueue)
- 异步:只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更
- 批量:如果同一个watcher被触发多次,只会被推入到队列中一次。去重对于避免不必要的计算和DOM操作是非常重要的。然后,在下一个事件循环"tick"中,Vue刷新队列执行实际工作。
- 异步策略:Vue在内部对异步队列尝试使用原生的
Promise.then
、MutationObserver
或setImmediate
,如果执行环境都不支持,则会采用setTimeout
代替
首先当前data属性值发生改变时,会触发set赋值操作,在src\core\observer\index.js
里的set中会调用dep.notify()
方法,通知更新,执行入队操作
src\core\observer\dep.js
中的subs是watcher数组,会调用update()方法src\core\observer\watcher.js
,执行watcher入队操作
queueWatcher(watcher) src\core\observer\scheduler.js
执行watcher操作
nextTick(flushSchedulerQueue) src\core\util\next-tick.js
nextTick按照特定异步策略执行队列操作
注意:
flushSchedulerQueue:刷新watchers队列
flushCallbacks:刷新callbacks数组中的回调
callbacks:[flushSchedulerQueue]
nextTick:其实做的事情就是往callbacks里放函数
虚拟DOM
虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用的各种状态变化会作用于虚拟DOM,最终映射到DOM上
优点:
- 虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM比对可以得到最小的DOM操作量,配合异步更新策略减少刷新频率,从而提升性能
- 跨平台:将虚拟DOM更新转换为不同运行时特殊操作实现跨平台
- 兼容性:还可以加入兼容性代码增强操作的兼容性
patch实现
patch src\core\vdom\patch.js
首先进行树级别的比较,可能有三种情况:增删改
- new VNode不存在就删
- old VNode不存在就增
- 都存在就执行diff执行更新
diff是先同层在深度递归
patchVNode
比较两个VNode,包括三种类型操作:属性更新、文本更新、子节点更新
具体规则如下:
- 新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren
- 如果新节点有子节点而老节点没有子节点,先清空老节点的文本内容,然后为其新增子节点。
- 当新节点没有子节点而老节点有子节点的时候,则移除该节点的所有子节点。
- 当新老节点都无子节点的时候,只是文本的替换
updateChildren
updateChildren主要作用是用一种较高效的方式比对新旧两个VNode的children得出最小操作补丁。执行一个双循环是传统方式,vue中针对web场景特点做了特别的算法优化
key的作用:
- 判断两个VNode是否是相同节点,会进行节点的复用操作
- 不添加会更新额外的操作,会始终认为两个节点相同
组件化机制
组件声明:Vue.component()
initAssetRegisters(Vue) src\core\global-api\assets.js
Vue.component(name, options)=>默认选项中加入组件配置=>mergeOptions=>局部组件内部就同时包含了已有的组件声明和全局组件声明
组件注册使用extend方法将配置转换为构造函数并添加到components选项,然后获取组件实例=>$mount()=>render()=>update()=>patch()
- 全局声明:Vue.component()
- 局部声明-:components
注:如果有父子组件
parent create
child create
parent mount
child mount
创建的过程是自上而下,挂载的过程是自下而上
组件实例创建及挂载
整体流程:
new Vue()=>$mount()=>vm._render()=>createElement()=>createComponent()=>vm._update()=>patch()=>createElm=>createComponent()
首先自定义组件
<comp foo="abc" @myClick="..."></comp>
经过编译_c('comp')
生成虚拟DOMvnode
{tag:'vue-component-1-comp', data:{...}, children:{...}, componentConstructor}
得到componentInstance
组件实例
在执行instance.$mount挂载
生成DOM节点