课前预习
搭建调试环境
- 获取地址:https://github.com/vuejs/vue (克隆项目)
- 在终端中输入
git clone 地址
- 在终端中输入
- 安装依赖:
npm i
- 安装rollup:
npm i -g rollup
(因为打包工具是rollup)全局安装 - 在克隆的项目中找到package.json,在dev上加上
--sourcemap
,其他都不需要修改- 修改dev脚本:添加—sourcemap配置项
- 执行dev脚本:npm run dev
npm run dev成功之后,在dist目录下的vue文件的颜色会变化,会变成黄色的,最后面还有M(也就是修改)的标识。还多了一个vue.map.js的文件,在调试的时候就可以和源码之间产生一个很好的映射。可以非常好的看源代码
调试技巧
- 打开指定文件 ctrl+p (可以在谷歌浏览器和vscode中使用)
- 打断点 (代码左侧打断点) (可以在谷歌浏览器和vscode中使用)
- 单步执行 快捷键F10
- 进入函数中 F11
- 查看调用栈 (便于理顺思路)
- 定位源文件所在位置
Vue源码剖析 01
目标
- 环境搭建
- 掌握源码学习方法
- vue初始化过程剖析
- 深入理解数据响应式
资源
- vue源码地址
知识点
获取vue
项目地址:https://github.com/vuejs/vue
迁出项目:git clone https://github.com/vuejs/vue.git
当前版本号: 2. 6. 11
安装依赖: e2e工具下载时间很长,可以直接终止(也就是下载fatemjs的时候可以直接终止,不影响后续的实验和学习)
文件结构
dist:文件打包之后的目录,也就是发布代码目录
examples:范例,里面是测试代码
flow和types:是分别针对两个语言的提示文件(类型声明提示文件)
packages:存放一下独立的库,独立在vue之外,也打包发布的库(多半是在src中写好的,要单独提取出来,打成一个独立的包,给别人的第三方框架使用。会看到一些服务端渲染的,包括wiks平台的东西)
flow和types存放的是针对两个语言的类型提示文件 packages:中存放的是独立的库,独立在vue之外也打包发布出去的库(大部分在src中写好的,但是要将它提取出来,打包成独立的库给别人的第三方框架去使用,所以会放在packages中,你会看到很多服务端渲染的,包括weex平台的东西) scripts是所有的构建脚本 src:源码目录
src源码目录
core是运行时的核心代码,也是通用代码
instance:构造函数,实例方法,一些重要的初始化过程等
vdom:虚拟dom和diff算法等
global-api:存放Vue.xxx方法
components:放默认三个组件
platforms:放平台通用代码(浏览器平台和移动端平台,weex移动端平台很少有人使用)
instance包:构造函数、实例方法等一系列重要的过程都会在instance中 vdom:是虚拟Dom相关和diff算法相关的 global-api:存放Vue.* platforms:平台特有的 web是浏览器平台 weex是移动端平台(weex已经很少有人使用)
调试环境搭建
安装依赖:
npm i
安装phantom.js时即可终止
安装rollup:
npm i -g rollup
(rollup是打包工具,不管是全局还是本地)- 修改dev脚本,添加sourcemap,package.json(添加sourcemap,帮助添加文件映射,更加便于读懂源码)
TARGET:web-full-dev表示打包的目标,这个表示打包的目标是完成的vue,包括编译器
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
- 运行开发命令:
npm run dev
- 打包出来两个文件一个是压缩的vue.js,一个是vue.js.map,将压缩的文件和源码进行映射,便于调试
在examples文件中添加测试文件。在examples/test中创建01-test.html文件,引入vue.js
引入前面创建的vue.js,在examples/commits/index.html更改
<script src="../../dist/vue.js"></script>
接下来就在页面中写Vue的代码进行调试
术语解释:(可以在vue官网查看)
- runtime:仅包含运行时,不包含编译器
- common:cjs(Node环境)规范,用于webpack1(或者服务端渲染的时候使用)
- esm:ES模块,用于webpack2+ ——-打包出来的vue.esm.xx.js
- umd: universal module definition(通用模块定义),兼容cjs(NodeJS环境)和amd(前端浏览器异步加载方式),用于浏览器———打包出来的vue.js的文件
浏览器调试技巧
- 打开指定文件:ctrl+p
- 断点
- 单步执行:f10或ctrl+’
- 进入函数中:f11或ctrl+;
- 查看调用栈
- 定位源文件所在位置:文件中鼠标右键
入口
dev脚本中 -c scripts/config.js
指明配置文件所在,在script(脚本)目录下
参数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,
},
}
找源码的顺序 package.json文件中 找config.js文件中的配置 查找resolve的方法,找到基本路径 找aliases 找到打包入口文件
初始化流程
整体流程
- new Vue()
- _init()
- $mount()
- mountComponent()
- updateComponent()
- render()
- update()
- new Watcher()
- updateComponent()
- mountComponent()
测试用例:01-init.html
入口 platforms/web/entry-runtime-with-compiler.js(打包入口文件)
扩展默认$mount方法:处理template或el选项(解析选项,获取render函数)
获取渲染函数的优先级:render > template > el
实现的事情:
- 扩展$mount:解析初始化选项(解析初始化的渲染函数的选项),获取render函数
platforms/web/runtime/index.js
安装web平台特有指令和组件
定义patch:补丁函数,执行patching算法进行更新
定义$mount:挂载vue实例到指定宿主元素(获得dom并替换宿主元素)
实现的事情
- 安装平台特有的patch函数,就是做diff算法的那个函数,也就是说是用来做更新的,它会接收虚拟DOM然后做比对(每个平台的patch函数不一样,所以需要合成patch)
- 实现mount方法:vdom =》 dom 添加到页面上dom树上
core/index.js
初始化全局api
具体如下:
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
实现的事情:
- 初始化全局api:Vue.use/component/filter/… 一系列静态的全局方法
core/instance/index.js
Vue构造函数定义
定义Vue实例API(声明实例属性和方法)
function Vue (options) {
// 构造函数仅执行了_init
this._init(options)
}
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
instance表示实例
定义Vue构造函数(接收根组件的选项)初始化中的 _init()在哪?
mixin做什么的?声明vue实例属性和方法
// mixin做什么的?声明vue实例属性和方法 initMixin(Vue) // _init() stateMixin(Vue) // $attrs/$listeners/$set / $delete/$watch 数据相关 eventsMixin(Vue) // $on()/$emit()/$once()/$off() 事件相关的方法 lifecycleMixin(Vue) // _update()/$forceUpdate()/$destroy() 生命周期相关的方法 renderMixin(Vue) // $nextTick()/_render() 和渲染函数相关的异步更新
实现的事情:
- 声明了构造函数
- 声明vue实例的属性和方法
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')
// 2.初始化 vm._self = vm initLifecycle(vm) // $parent/$root/$refs/.. 初始化声明周期 //
initEvents(vm) // event 初始化parent附加的事件
// <comp>xxxxxx</comp>
initRender(vm) // $slots/$scopSlots/$createElement() 初始化渲染函数
callHook(vm, 'beforeCreate') // 生命周期钩子中,在写插件中使用
// provide/inject
initInjections(vm) // 注入祖辈传下来的数据
initState(vm) // 组件数据初始化,包括了props/methods/data/computed/watch
initProvide(vm) // 给后代传递数据
callHook(vm, 'created') // 生命周期钩子
// 挂载
// 如果用户设置了el,则可以省略$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
$mount
- mountComponent
执行挂载,获取vdom并转换为dom
- new Watcher()
创建组件渲染watcher
- updateComponent()
执行初始化或更新
- update()
初始化或更新,将传入vdom转换为dom,初始化时执行的是dom创建操作
- render() src\core\instance\render.js
渲染组件,获取vdom
测试代码:examples\test\01-init.html
调试代码流程 合并选项 每一行代码执行之后,组件中添加的内容
整体流程捋一捋
new Vue() => _init() => $mount() => mountComponent() =>
new Watcher() => updateComponent() => render() => _update()
面试题: new Vue之后发生了什么 执行了初始化方法,传入相关配置 在构造函数中执行了_init()(即初始化),在初始化内部做了 =》 选项合并+实例相关属性初始化 + 生命周期的钩子(派发beforeCreated和created这样的钩子)+数据响应式的操作(data相关的数据的状态初始化,包括属性、方法)。上面都是属于_init内部的方法,在事情做完之后需要挂载。初始化完成之后,得到第一次的渲染结果,得到一次虚拟dom,虚拟dom如何变成真实dom,需要走下一个步骤 =》 $mount() 方法将vdom转换成真实的节点。在$mount()的内部如何将vdom转换为真实的dom。在$mount内部有updateComponet =》updateComponet()方法,这个方法会调用渲染函数render()(在入口文件做编译的时候,那个编译函数生成了渲染函数。如果用户手写渲染函数,则使用用户手写的;如果用户没有手写render则将template编译成为渲染函数)。渲染函数的目标是得到虚拟dom,虚拟dom需要执行update方法 =》 _update() 就是所谓的更新函数,更新函数执行完成之后猜得到了真实的dom。但是_update中是使用patch函数(补丁函数)做的 =》 patch() 这个函数是真正的将虚拟dom变更成真实dom。这是整个初始化的基本流程
思考一道相关面试题:谈谈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等,这里着重关注data初始
化,
整体流程
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实例
core/observer/index.js
Observer对象根据数据类型执行对应的响应化操作
defineReactive定义对象属性的getter/setter,getter负责添加依赖,setter负责通知更新
core/observer/dep.js
Dep负责管理一组Watcher,包括watcher实例的增删及通知更新
Watcher
Watcher解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API和指令中。
每个组件也会有对应的Watcher,数值变化会触发其update函数导致重新渲染
export default class Watcher {
constructor () { }
get () { }
addDep (dep: Dep) { }
update () { }
}
相关API:
$watcher
为什么是n:n的关系 因为有可能是使用$watcher的,所以会导致一个key和多个watcher之间会有关系,所以产生了n:n的关系 watcher是和组件之间相关联的。上一节课写的是最早版本的vue(性能问题:watcher太多了)。为了解决这个问题,vue2降低了维度,初始化的过程中new Vue的时候就会创建watcher,由于这个vue的构造函数会被组件继承,所以new一个组件的时候,也会伴生着唯一的Watcher,这个叫渲染watcher。还有一种watcher是user watcher(用户watcher),他是使用$watcher的时候产生的。记住在vue2中产生watcher的方式只有上面两种方式:一个是用户watcher;一个是渲染watcher;大概率watcher是和组件相关的,一个组件一个watcher,这个关系
测试代码examples\test\02-1-reactive.html
调试整体流程: 查看依赖收集的过程 当前的watcher实例创建一个映射关系,和dep的ID之间做映射.也就是当前的watcher和哪个dep有关,会保存dep的Id 将watcher加入到dep中 data中发生对象的嵌套之后,会出现子Observe诞生。这个关系也要和当前watcher建立更新关系。随后相互之间的变化就会有机会相互通知 页面发生变化,通知更新 遍历watcher执行更新函数,但是更新函数和以前不一样。更新函数中的操作不会立即执行,因为组件很多,希望所有组件都做完之后,统一批量进行更新。这个是下一节要讨论的异步更新
数组响应化
数组数据变化的侦测跟对象不同,我们操作数组通常使用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)
}
测试代码examples\test\02-2-reactive-arr.html
相关API:Vue.set()/delete()
data: {
arr: []
}
arr.length = 0
arr[index] = xxx
Vue.set()
Vue.del()
面试:new Vue()之后发生了什么事情
1.在构造函数中,_init()方法,即初始化。 =》 在初始化中,做了选项和并+实例相关的属性初始化,生命周期的钩子,数据响应式的操作,数据状态的初始化,得到渲染结果即虚拟dom =》 $mount()(vdom转成dom) =》 执行mountComponent() =》 vdom转成dom是通过 updateComponent() =》 上面那个方法是调用render(),得到vdom => _update()更新函数执行完成之后 =》 _update()是使用patch()函数(补丁函数)将vdom编变成真实dom
作业
- 整体流程思维导图
https://www.processon.com/view/5d1eae32e4b05dcb439787d5?fromnew=1#map
- 尝试编写测试案例调试
研究Vue.set/delete/$watch等API
研究这两个的时候从哪里开始: 想办法找到这两个对应的是那两个文件,可能根本想不起来在哪里。这个时候就可以写上测试的页面,在页面中调用一下相关的接口,等到调试的时候就知道对应的页面在哪里展示了(这个是最简单的方式) 调试zuoye.html
调试: 先触发的是this.obj的代理,跳出函数,再次进入 和响应式相关的接口、类、实现基本都在这个文件夹observer 判断是否是数组(校验:是否是数组,是否是合理的index) $watch:状态相关的实例上的方法 src\core\instance\state.js
- watch选项 ```javascript // watch选项底层是使用$watch来实现的
watch: {‘$route’: function(){}} ```
- Dep和Watcher是n对n的关系
尝试vue异步更新是如何实现的
异步更新要涉及到队列Queue,将来vue会创建一个队列,这个和react是一致的
- Queue队列(异步更新中vue会创建一个队列,这个和react是一样的。每次提交更新的时候,不会立即做这件事,而是尝试将watcher放入队列,如果watcher在队列中,则去重,不让watcher进入队列。一个watcher在一个队列中只可能出现一次。如果同时对一个组件的n个key做操作,最终进入到队列的只有一个,这就是原理。去重之后如何操作批量异步执行)
- 批量异步执行:核心思想涉及到浏览器对于异步处理的概念(需要了解微任务和宏任务)Promise/MutationObserver/setImmediate/SetTimeOut
- 微任务、宏任务在浏览器做刷新处理的时候,他们特点是什么
- 微任务特点:在浏览器刷新之前做事情,这些事情做完之后,浏览器才会刷新
- 宏任务特点:在浏览器刷新之后的下一帧再去做任务
(事情在刷新之前做完,一刷新就看到结果了。用户体验好,速度快。在选择批量异步处理的时候,首先先看运行环境,如果浏览器是谷歌,支持Promise,那么首选Promise。如果移动端浏览器中,不支持Promise,则降级,则使用MutationObserver。如果再不支持,则没有微任务可以使用了,则使用宏任务setImmediate;再不支持,使用SetTimeOut方法)如何使用异步方式刷新队列