Vue进阶之源码阅读

Vue进阶之源码阅读

阅读方式

  • 先宏观了解,看大致目录和结构;
  • 带着目标看源码,明确核心功能,要意识到我是谁、我在哪儿、我要做什么;
  • 看源码的过程要不求甚解,跳过分支功能;
  • 调试其运行过程;

Vue 的不同构建版本

官方文档 - 对不同构建版本的解释 dist\README.md https://github.com/vuejs/vue/tree/dev/dist

Vue进阶之源码阅读_第1张图片

  • 完整版:同时包含编译器和运行时的版本。
  • 编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码,体积大、效率低。
  • 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码,体积小、效率高。基本上就是除 去编译器的代码。
  • UMD:UMD 版本通用的模块版本,支持多种模块方式。 默认文件就是运行时 + 编译器的 UMD 版本
  • CommonJS(cjs):CommonJS 版本用来配合老的打包工具比如 Browserify 或 webpack 1
  • ES Module:从 2.6 开始 Vue 会提供两个 ES Modules (ESM) 构建文件,为现代打包工具提供的
    版本。
    • ESM 格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行“tree-shaking”并 将用不到的代码排除出最终的包。
    • ES6 模块与 CommonJS 模块的差异

从入口文件开始

提示:阅读源码时vscode默认会自动检测js语法,在vscode设置中改为false,不校验"javascript.validate.enable": false,

从package.json文件的启动命令可以找到配置文件,script/config.js,继续变可找到入口配置

// 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
},

则dev模式的入口文件为 src/platform/web/entry-runtime-with-compiler.js

阅读记录

入口文件

src/platforms/web/entry-runtime-with-compiler.js

  1. el 不能是 body 或者 html 标签;
  2. 如果没有 render,把 template 转换成 render 函数
  3. 如果有 render 方法,直接调用 mount 挂载 DOM,不再处理template模板
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
     
  // 获取document元素
  el = el && query(el)

  /* istanbul ignore if */ // el元素不能是body和html
  if (el === document.body || el === document.documentElement) {
     
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to  or  - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  // 如果没有render函数就处理template模板,将template编译为render函数
  if (!options.render) {
     
    ...
  }
  return mount.call(this, el, hydrating)
}

这里根据import 引入一直查找,可找到四个导出Vue相关的模块儿

  • src/platforms/web/entry-runtime-with-compiler.js
    • web 平台相关的入口
    • 重写了平台相关的 $mount() 方法
    • 注册了 Vue.compile() 方法,传递一个 HTML 字符串返回 render 函数
  • src/platforms/web/runtime/index.js
    • web 平台相关
    • 注册和平台相关的全局指令:v-model、v-show
    • 注册和平台相关的全局组件: v-transition、v-transition-group
    • 全局方法:patch:把虚拟 DOM 转换成真实 DOM,$mount:挂载方法
  • src/core/index.js
    • 与平台无关
    • 设置了 Vue 的静态方法,initGlobalAPI(Vue)
  • src/core/instance/index.js
    • 与平台无关
    • 定义了构造函数,调用了 this._init(options) 方法
    • 给 Vue 中混入了常用的实例成员

Vue 的初始化

初始化 Vue 的静态方法 src/core/global-api/index.js

// 注册 Vue 的静态属性/方法 
initGlobalAPI(Vue)
// src/core/global-api/index.js
// 初始化 Vue.config 对象 
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on // them unless you are aware of the risk.
// 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们 
Vue.util = {
     
  warn,
  extend,
  mergeOptions,
  defineReactive
}
// 静态方法 set/delete/nextTick 
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
// 让一个对象可响应
Vue.observable = <T>(obj: T): T => {
     
  observe(obj)
  return obj 
}
// 初始化 Vue.options 对象,并给其扩展
// components/directives/filters/_base 
Vue.options = Object.create(null) 
ASSET_TYPES.forEach(type => {
     
Vue.options[type + 's'] = Object.create(null) })
// this is used to identify the "base" constructor to extend all plain- object
// components with in Weex's multi-instance scenarios. 
Vue.options._base = Vue
// 设置 keep-alive 组件 
extend(Vue.options.components, builtInComponents)
// 注册 Vue.use() 用来注册插件 
initUse(Vue)
// 注册 Vue.mixin() 实现混入 
initMixin(Vue)
// 注册 Vue.extend() 基于传入的 options 返回一个组件的构造函数 
initExtend(Vue)
// 注册 Vue.directive()、 Vue.component()、Vue.filter() 
initAssetRegisters(Vue)
初始化 Vue 的实例成员

定义 Vue 的构造函数 src/core/instance/index.js

// 此处不用class的原因是因为之后方便给Vue实例混入实例成员
function Vue (options) {
     
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
    // 判断是否使用new 方法来创建实例 instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。
  ) {
     
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 调用 _init()方法
  this._init(options)
}
// 注册 vm 实例的init方法初始化; 为实例注册方法
initMixin(Vue)
// 注册 vm 实例的状态方法,$data/$props/$set/$delete/$watch
stateMixin(Vue)
// 注册 vm 实例的事件方法,观察者模式,$on/$once/$off/$emit
eventsMixin(Vue)
// 注册 vm 实例的部分生命周期方法, _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 注册 vm 实例的render函数和$nextTick函数
renderMixin(Vue)
initMixin(Vue)

初始化 _init() 方法 src/core/instance/init.js

// src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
      // 给 Vue 实例增加 _init() 方法
// 合并 options / 初始化操作
Vue.prototype._init = function (options?: Object) {
     
// a flag to avoid this being observed // 如果是 Vue 实例不需要被 observe vm._isVue = true
// merge options
// 合并 options
if (options && options._isComponent) {
     
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options)
} else {
     
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), options || {
     },
vm
) }
      /* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
} else {
      vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 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')
      /* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
     
vm._name = formatComponentName(vm, false) mark(endTag)
measure(`vue ${
       vm._name} init`, startTag, endTag)
}
// 如果没有提供 el,调用 $mount() 挂载 if (vm.$options.el) {
     
vm.$mount(vm.$options.el) }
} }

首次渲染过程

  • Vue 初始化完毕,开始真正的执行
  • 调用 new Vue() 之前,已经初始化完毕


Vue进阶之源码阅读_第2张图片

数据响应式原理

一些总结
1、Vue的observe方法,将数据变为响应式,就是给对象添加get、set方法,set方法中触发此属性的发布者后通知观察者,观察者再去做相应变化,get方法中,判断此属性是否有观察者添加,有就添加。

2、在Vue中当数据被调用,或者被改变时都会调用 watcher 的 get 方法,在get方法中就会触发Dep的target属性去将watcher添加到次数据的Dep的观察者列表中,使得dep可以在数据改变时触发watcher变化。如何添加观察者的核心。

3、这里有两个get,watcher的get去触发触发数据的get,在数据的get中添加观察者。

4、watcher的get是怎么被调用的呢,实例化时在watcher的构造函数中就会调用get方法;也就是new watcher的地方就会调用。

关于渲染 Watcher

渲染Watcher,将渲染的函数独立放出 在 Watcher 的get中去调用函数,其中渲染函数会调用用到变量的get,此时就可以给对应的属性添加Watcher,到对应的Dep中,切注册updata为该渲染函数,则当其中任意一个变量变化都会执行该渲染函数去渲染。

new Watcher(vm, updateComponent, noop, {
     
    before () {
     
      if (vm._isMounted && !vm._isDestroyed) {
     
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
关于Vue中的响应式数组

Vue中修补了,原生数组中可以修改数组的方法,为其增加了响应式监听,所以在使用这些方法改变数组的方法时,就可更新视图
方法有

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

所以在vue中

var vm = new Vue({
     
	el:'#app',
	data: {
     
		arr:[1,2,3]
	}
})

vm.arr.push(8);  //可以更新视图
vm.arr[0] = 100; //不会更新视图
vm.arr.length = 0; //不会更新视图
Dep 类 (发布者)

位置 src\core\observer\dep.js

  • 依赖对象
  • 记录 watcher 对象
  • depend() – watcher 记录对应的 dep
  • 发布通知
  1. 在 defineReactive() 的 getter 中创建 dep 对象,并判断 Dep.target 是否有值(一会 再来看有什么
    时候有值得), 调用 dep.depend()
  2. dep.depend() 内部调用 Dep.target.addDep(this),也就是 watcher 的 addDep() 方 法,它内部最
    调用 dep.addSub(this),把 watcher 对象,添加到 dep.subs.push(watcher) 中,也 就是把订阅者
    添加到 dep 的 subs 数组中,当数据变化的时候调用 watcher 对象的 update() 方法
  3. 什么时候设置的 Dep.target? 通过简单的案例调试观察。调用 mountComponent() 方法的时 候,创建了
    渲染 watcher 对象,执行 watcher 中的 get() 方法
  4. get() 方法内部调用 pushTarget(this),把当前 Dep.target = watcher,同时把当前 watcher 入栈,
    因为有父子组件嵌套的时候先把父组件对应的 watcher 入栈,再去处理子组件的 watcher,子 组件的处理完毕
    后,再把父组件对应的 watcher 出栈,继续操作
  5. Dep.target 用来存放目前正在使用的watcher。全局唯一,并且一次也只能有一个 watcher 被使用
Watcher 类
  • Watcher 分为三种,Computed Watcher、用户 Watcher (侦听器)、渲染 Watcher 渲染
  • Watcher 的创建时机 位置/src/core/instance/lifecycle.js
  • 渲染 wacher 创建的位置 lifecycle.js 的 mountComponent 函数中
  • Wacher 的构造函数初始化,处理 expOrFn (渲染 watcher 和侦听器处理不同)
  • 调用 this.get() ,它里面调用 pushTarget() 然后 this.getter.call(vm, vm) (对于渲染 wacher 调 用 updateComponent),如果是用户 wacher 会获取属性的值(触发get操作)
  • 当数据更新的时候,dep 中调用 notify() 方法,notify() 中调用 wacher 的 update() 方法
  • update() 中调用 queueWatcher()
  • queueWatcher() 是一个核心方法,去除重复操作,调用 flushSchedulerQueue() 刷新队列并执行 watcher
  • flushSchedulerQueue() 中对 wacher 排序,遍历所有 wacher ,如果有 before,触发生命周期 的钩子函数 beforeUpdate,执行 wacher.run(),它内部调用 this.get(),然后调用 this.cb() (渲染 wacher 的 cb 是 noop)
  • 整个流程结束
    Vue进阶之源码阅读_第3张图片
    Vue进阶之源码阅读_第4张图片
    Vue进阶之源码阅读_第5张图片
    Vue进阶之源码阅读_第6张图片

疑问记录,给子对象收集依赖过程; 已解决

实例方法/数据
  • vm.$set
    功能:
    向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于 向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = ‘hi’)

注意: 对象不能是 Vue 实例,或者 Vue 实例的根数据对象。 示例

  • vm.$delete
    功能:
    删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。

注意: 目标对象不能是一个 Vue 实例或 Vue 实例的根数据对象。

  • vm.$watch

vm.$watch( expOrFn, callback, [options] )
功能:
观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只 接受监督的键路径。对于更复杂的表达式,用一个函数取代。
参数:

  • expOrFn:要监视的 $data 中的属性,可以是表达式或函数
  • callback:数据变化后执行的函数
    函数:回调函数
    对象:具有 handler 属性(字符串或者函数),如果该属性为字符串则 methods 中相应 的定义
  • options:可选的选项
    deep:布尔类型,深度监听
    immediate:布尔类型,是否立即执行一次回调函数
三种类型的 Watcher 对象
  • 没有静态方法,因为 $watch 方法中要使用 Vue 的实例
  • Watcher 分三种:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher
  • 创建顺序:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher

查看渲染 watcher 的执行过程

  • 当数据更新,defineReactive 的 set 方法中调用 dep.notify()
  • 调用 watcher 的 update()
  • 调用 queueWatcher(),把 wacher 存入队列,如果已经存入,不重复添加
  • 循环调用 flushSchedulerQueue()
  • 通过 nextTick(),在消息循环结束之前时候调用 flushSchedulerQueue()
  • 调用 wacher.run()
  • 调用 wacher.get() 获取最新值
  • 如果是渲染 wacher 结束
  • 如果是用户 watcher,调用 this.cb()
nextTick方法

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
利用了Promise在微任务中的立即执行,既此时正好是DOM更新完成,还没有渲染到浏览器上,执行回调,回调完成后,去将DOM渲染到浏览器。

Promise的兼容问题,Vue是优先使用Promise,然后是MutationObserver,都是微任务,如果都不支持,最后就是setTimeout,添加宏任务。

你可能感兴趣的:(Vue,笔记)