Vue-2.6.10源码解析

480 阅读9分钟

一、Vue源码目录结构

image.png
1、目录practice\vue-2.6.14\src\compiler(与平台无关的)

  • compiler负责将模板转化为render函数,而render函数负责创建虚拟DOM 2、目录practice\vue-2.6.14\src\core(与平台无关的)
  • corevue的核心。
  • core\components中定义了组件keep-alive
  • core\global-api中定义了vue的静态方法(Vue.componentVue.filterVue.mixinVue.use等)
  • core\instance是创建vue实例的位置,定义了vue的构造函数、初始化以及生命周期钩子函数
  • core\observer是响应式机制实现的位置(重点)
  • core\utilvue项目源码依赖的功能方法
  • core\vdomvue项目的虚拟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、输出文件
    image.png
  • vue不同构建版本文件的区别 image.png
  • Full(完整版):同时包含了编译器和运行时的版本
  • Compiler(编译器):用来将模板字符串编译成为 JavaScript 渲染函数的代码,体积大、效率低
  • Runtime-only(运行时):用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码,体积小、效率高。基本上就是除 去编译器的代码
  • UMD (Universal Module Definition)(通用模块定义),支持AMD与CommonJS模块方式。vue.js 默认文件就是运行时 + 编译器的UMD 版本
  • CommonJS(cjs): CommonJS 版本用来配合老的打包工具比如Browserifywebpack1
  • ES 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-modelv-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首次渲染的过程

图解首次渲染 first-render.png

  • 在首次渲染之前,首先进行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._rendervm._updatevm._render的作用是生成虚拟DOMvm._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对象,然后再进行数组的响应式处理和对象的响应式处理
    • 数组的响应式处理,就是设置数组的几个特殊的方法,pushpopsort等,这些方法会改变原数组,所以这些方法被调用的时候需要发送通知
      • 找到数组对象中的__ob__对象中的dep,调用depnotify()方法
      • 再遍历数组中每一个成员,对每个成员调用observe(),如果这个成员是对象的话,也会转换成响应式对象
    • 对象的响应式处理,就是调用walk方法,walk方法就是遍历对象的每一个属性,对每个属性调用defineReactive方法
  • defineReactive会为每一个属性创建对应的dep对象,让dep去收集依赖,如果当前属性的值是对象,会调用observedefineReactive中最核心的方法是gettersetter
    • getter 的作用是收集依赖,收集依赖时,为每一个属性收集依赖,如果这个属性的值是对象,那也要为子对象收集依赖,最后返回属性的值
    • setter 中,先保存新值,如果新值是对象,也要调用 observe ,把新设置的对象也转换成响应式的对象,然后派发更新(发送通知),调用dep.notify()
  • 收集依赖时
    • watcher对象的get方法中调用pushTarget, 记录Dep.target属性
    • 访问data中的成员的时候收集依赖,defineReactivegetter中收集依赖
    • 把属性对应的 watcher 对象添加到depsubs数组中,也就是为属性收集依赖
    • 如果属性的值也是对象,给childOb收集依赖,目的是子对象添加和删除成员时发送通知
  • 在数据发生变化的时候
    • 调用dep.notify()发送通知,dep.notify()会调用watcher对象的update()方法
    • update()中的调用queueWatcher(),会去判断watcher是否被处理,如果这个watcher对象没有被处理的话,添加到queue队列中,并调用flushScheduleQueue()
    • flushScheduleQueue()中触发beforeUpdate钩子函数
    • 调用watcher.run() : run()-->get() --> getter() --> updateComponent()
    • 然后清空上一次的依赖
    • 触发actived的钩子函数
    • 触发updated钩子函数 3、图解数据响应式原理 0107b7701b00497d8ac26f003ed1a6be_tplv-k3u1fbpfcp-watermark.webp

4、图解Vue响应式的原理 19827184-99ebf78cf52f0d6b.webp

六、添加、删除、监听响应式属性

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对象的核心属性tagdatachildrentextel(真实DOM)、key(用于遍历时的复用)

3、图解整体过程 image.png

4、模板转换成视图的过程 image.png

image.png

5、当v-for遍历创建VNode时,设置key可以减少DOM操作的次数。

八、模板编译和组件化

1、梳理模板编译的入口 image.png

2、模板编译的过程

  • 模版编译入口函数compileToFunctions
    • 内部首先从缓存加载编译好的render函数,如果缓存中没有,调用compile开始编译
  • compile 函数中,首先合并选项options,调用 baseCompile 编译模版
  • compile的核心是合并选项options, 真正处理是在basCompile中完成的,把模版和合并好的选项传递给baseCompile, 这里面完成了模版编译的核心三件事情
    • parse(): 把模版字符串转化为AST 对象,也就是抽象语法树
    • optimize(): 对抽象语法树进行优化,标记静态语法树中的静态根节点(静态根节点是标签中除了文本内容以外,还需要包含其它标签)。检测到静态子树,设置为静态,不需要在每次重新渲染的时候重新生成节点。patch的过程中会跳过静态根节点
    • generator(): 把优化过的AST对象,转化为字符串形式的代码
  • 执行完成之后,会回到入口函数complieToFunctions
    • compileToFunction会继续把字符串代码转化为函数
    • 调用createFunction
    • renderstaticRenderFns初始化完毕,最终会挂在到Vue实例的options对应的属性中

image.png

3、组件化回顾 image.png

4、要点

  • 当调用$mount时仅仅是创建真实DOM,但并没有将组件添加到DOM结构中,真正的挂载是在patch.js中的createComponent函数的insert方法把组件对应的 DOM 插入到父元素中。
  • 组件的创建过程是先创建父组件后创建子组件,组件的挂载过程是先挂载子组件后挂载父组件。

5、虚拟 DOM 中 Key 的作用和好处

  • 作用:追踪列表中哪些元素被添加、被修改、被移除的辅助标志。可以快速对比两个虚拟DOM对象,找到虚拟DOM对象被修改的元素,然后仅仅替换掉被修改的元素,然后再生成新的真实DOM
  • 好处:可以优化 DOM 的操作,减少Diff算法和渲染所需要的时间,提升性能。