课前预习

搭建调试环境

  • 获取地址: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中使用)

image.png

  • 单步执行 快捷键F10

image.png

  • 进入函数中 F11

image.png

  • 查看调用栈 (便于理顺思路)

image.png

  • 定位源文件所在位置

image.png

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平台的东西)
image.png

flow和types存放的是针对两个语言的类型提示文件 packages:中存放的是独立的库,独立在vue之外也打包发布出去的库(大部分在src中写好的,但是要将它提取出来,打包成独立的库给别人的第三方框架去使用,所以会放在packages中,你会看到很多服务端渲染的,包括weex平台的东西) scripts是所有的构建脚本 src:源码目录

src源码目录
core是运行时的核心代码,也是通用代码
instance:构造函数,实例方法,一些重要的初始化过程等
vdom:虚拟dom和diff算法等
global-api:存放Vue.xxx方法
components:放默认三个组件
platforms:放平台通用代码(浏览器平台和移动端平台,weex移动端平台很少有人使用)
image.png

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,包括编译器

  1. "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更改

    1. <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

  1. // Runtime+compiler development build (Browser)
  2. {
  3. 'web-full-dev': {
  4. // 入口文件
  5. entry: resolve('web/entry-runtime-with-compiler.js'), // 入口
  6. dest: resolve('dist/vue.js'),// 目标文件
  7. format: 'umd', // 输出规范
  8. env: 'development',
  9. alias: { he: './entity-decoder' },
  10. banner,
  11. },
  12. }

找源码的顺序 package.json文件中 image.png 找config.js文件中的配置 image.png 查找resolve的方法,找到基本路径 image.png 找aliases image.png 找到打包入口文件 image.png

初始化流程

整体流程

  • new Vue()
    • _init()
  • $mount()
    • mountComponent()
      • updateComponent()
        • render()
        • update()
      • new Watcher()

image.png

生命周期图示

测试用例: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
具体如下:

  1. Vue.set = set
  2. Vue.delete = del
  3. Vue.nextTick = nextTick
  4. initUse(Vue) // 实现Vue.use函数
  5. initMixin(Vue) // 实现Vue.mixin函数
  6. initExtend(Vue) // 实现Vue.extend函数
  7. initAssetRegisters(Vue) // 注册实现Vue.component/directive/filter

实现的事情:

  • 初始化全局api:Vue.use/component/filter/… 一系列静态的全局方法

core/instance/index.js
Vue构造函数定义
定义Vue实例API(声明实例属性和方法)

  1. function Vue (options) {
  2. // 构造函数仅执行了_init
  3. this._init(options)
  4. }
  5. initMixin(Vue) // 实现init函数,实现_init();
  6. stateMixin(Vue) // 状态相关api $data,$props,$set,$delete,$watch
  7. eventsMixin(Vue)// 事件相关api $on,$once,$off,$emit
  8. lifecycleMixin(Vue) // 生命周期api _update,$forceUpdate,$destroy
  9. 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
创建组件实例,初始化其数据、属性、事件等

  1. initLifecycle(vm) // $parent,$root,$children,$refs
  2. initEvents(vm) // 处理父组件传递的事件和回调
  3. initRender(vm) // $slots,$scopedSlots,_c,$createElement
  4. callHook(vm, 'beforeCreate')
  5. initInjections(vm) // 获取注入数据
  6. initState(vm) // 初始化props,methods,data,computed,watch
  7. initProvide(vm) // 提供数据注入
  8. callHook(vm, 'created')

// 2.初始化 vm._self = vm initLifecycle(vm) // $parent/$root/$refs/.. 初始化声明周期 //

  1. initEvents(vm) // event 初始化parent附加的事件
  2. // <comp>xxxxxx</comp>
  3. initRender(vm) // $slots/$scopSlots/$createElement() 初始化渲染函数
  4. callHook(vm, 'beforeCreate') // 生命周期钩子中,在写插件中使用
  5. // provide/inject
  6. initInjections(vm) // 注入祖辈传下来的数据
  7. initState(vm) // 组件数据初始化,包括了props/methods/data/computed/watch
  8. initProvide(vm) // 给后代传递数据
  9. callHook(vm, 'created') // 生命周期钩子
  10. // 挂载
  11. // 如果用户设置了el,则可以省略$mount
  12. if (vm.$options.el) {
  13. vm.$mount(vm.$options.el)
  14. }

$mount
- mountComponent
执行挂载,获取vdom并转换为dom

- new Watcher()
创建组件渲染watcher

- updateComponent()
执行初始化或更新

- update()
初始化或更新,将传入vdom转换为dom,初始化时执行的是dom创建操作

- render() src\core\instance\render.js
渲染组件,获取vdom

测试代码:examples\test\01-init.html

调试代码流程 image.png 合并选项 image.png 每一行代码执行之后,组件中添加的内容 image.png

image.png image.png image.png image.png image.png

整体流程捋一捋

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数据响应化

  1. function initData (vm: Component) {
  2. // 执行数据响应化
  3. observe(data, true /* asRootData */)
  4. }

core/observer/index.js
observe方法返回一个Observer实例

core/observer/index.js
Observer对象根据数据类型执行对应的响应化操作
defineReactive定义对象属性的getter/setter,getter负责添加依赖,setter负责通知更新

core/observer/dep.js
Dep负责管理一组Watcher,包括watcher实例的增删及通知更新
image.png

Watcher
Watcher解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API和指令中。
每个组件也会有对应的Watcher,数值变化会触发其update函数导致重新渲染

  1. export default class Watcher {
  2. constructor () { }
  3. get () { }
  4. addDep (dep: Dep) { }
  5. update () { }
  6. }

相关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,这个关系

image.png

测试代码examples\test\02-1-reactive.html

调试整体流程: image.png image.png image.png image.png 查看依赖收集的过程 image.png 当前的watcher实例创建一个映射关系,和dep的ID之间做映射.也就是当前的watcher和哪个dep有关,会保存dep的Id image.png 将watcher加入到dep中 image.png data中发生对象的嵌套之后,会出现子Observe诞生。这个关系也要和当前watcher建立更新关系。随后相互之间的变化就会有机会相互通知 image.png 页面发生变化,通知更新 image.png 遍历watcher执行更新函数,但是更新函数和以前不一样。更新函数中的操作不会立即执行,因为组件很多,希望所有组件都做完之后,统一批量进行更新。这个是下一节要讨论的异步更新 image.png

数组响应化

数组数据变化的侦测跟对象不同,我们操作数组通常使用push、pop、splice等方法,此时没有办法得
知数据变化。所以vue中采取的策略是拦截这些方法并通知dep。

src\core\observer\array.js
为数组原型中的 7 个可以改变内容的方法定义拦截器

Observer中覆盖数组原型

  1. if (Array.isArray(value)) {
  2. // 替换数组原型
  3. protoAugment(value, arrayMethods) // value.__proto__ = arrayMethods
  4. this.observeArray(value)
  5. }

测试代码examples\test\02-2-reactive-arr.html

相关API:Vue.set()/delete()

  1. data: {
  2. arr: []
  3. }
  4. arr.length = 0
  5. arr[index] = xxx
  6. Vue.set()
  7. Vue.del()

面试:new Vue()之后发生了什么事情
1.在构造函数中,_init()方法,即初始化。 =》 在初始化中,做了选项和并+实例相关的属性初始化,生命周期的钩子,数据响应式的操作,数据状态的初始化,得到渲染结果即虚拟dom =》 $mount()(vdom转成dom) =》 执行mountComponent() =》 vdom转成dom是通过 updateComponent() =》 上面那个方法是调用render(),得到vdom => _update()更新函数执行完成之后 =》 _update()是使用patch()函数(补丁函数)将vdom编变成真实dom

作业

  1. 整体流程思维导图

https://www.processon.com/view/5d1eae32e4b05dcb439787d5?fromnew=1#map

  1. 尝试编写测试案例调试
  2. 研究Vue.set/delete/$watch等API

    研究这两个的时候从哪里开始: 想办法找到这两个对应的是那两个文件,可能根本想不起来在哪里。这个时候就可以写上测试的页面,在页面中调用一下相关的接口,等到调试的时候就知道对应的页面在哪里展示了(这个是最简单的方式) 调试zuoye.html

    调试: image.png 先触发的是this.obj的代理,跳出函数,再次进入 image.png 和响应式相关的接口、类、实现基本都在这个文件夹observer 判断是否是数组(校验:是否是数组,是否是合理的index) image.png $watch:状态相关的实例上的方法 src\core\instance\state.js

    • watch选项 ```javascript // watch选项底层是使用$watch来实现的

watch: {‘$route’: function(){}} ```

  • Dep和Watcher是n对n的关系
  1. 尝试vue异步更新是如何实现的

    异步更新要涉及到队列Queue,将来vue会创建一个队列,这个和react是一致的

    • Queue队列(异步更新中vue会创建一个队列,这个和react是一样的。每次提交更新的时候,不会立即做这件事,而是尝试将watcher放入队列,如果watcher在队列中,则去重,不让watcher进入队列。一个watcher在一个队列中只可能出现一次。如果同时对一个组件的n个key做操作,最终进入到队列的只有一个,这就是原理。去重之后如何操作批量异步执行)
    • 批量异步执行:核心思想涉及到浏览器对于异步处理的概念(需要了解微任务和宏任务)Promise/MutationObserver/setImmediate/SetTimeOut
    • 微任务、宏任务在浏览器做刷新处理的时候,他们特点是什么
      • 微任务特点:在浏览器刷新之前做事情,这些事情做完之后,浏览器才会刷新
      • 宏任务特点:在浏览器刷新之后的下一帧再去做任务

(事情在刷新之前做完,一刷新就看到结果了。用户体验好,速度快。在选择批量异步处理的时候,首先先看运行环境,如果浏览器是谷歌,支持Promise,那么首选Promise。如果移动端浏览器中,不支持Promise,则降级,则使用MutationObserver。如果再不支持,则没有微任务可以使用了,则使用宏任务setImmediate;再不支持,使用SetTimeOut方法)
如何使用异步方式刷新队列