基础概念
- 渐进式框架:将框架分层,可以先使用其基础功能,然后在此基础上逐渐增加使用所需要的其他的功能。
- 由最内层到最外层:声明式渲染->组件机制->路由机制->状态管理->构建工具
- 响应式系统:Vue中数据模型仅仅是普通的JavaScript对象。而它们被修改时,视图会进行更新。
- 渲染:从状态生成DOM,再输出到用户界面显示的一整套流程。
- 声明式渲染:Vue使用 "模板语法" 来将数据渲染进DOM系统。
- 事件循环:JavaScript是一门单线程且非阻塞的脚本语言,这意味着JavaScript代码在执行的任何时候都只有一个主线程来处理所有任务。而非阻塞是指当代码需要处理异步任务时,主线程会挂起(pending)这个任务,当异步任务处理完毕后,JavaScript会将这个事件加入一个队列(事件队列,微任务和宏任务在不同的队列中)中,被放入事件队列中的事件不会立刻执行回调,而是等待执行栈中的所有任务执行完毕后,主线程会去检查微任务队列中是否有事件存在,如果存在则会依次执行微任务队列中事件对应的回调,直到为空,然后去宏任务队列中取出一个事件,把对应的回调加入当前执行栈,当执行栈中的所有任务都执行完毕后,再重复检查微任务队列中是否有任务存在。
- 执行栈:
变化侦测
- 作用:侦测数据的变化,当数据变化时,会通知视图进行相应的更新。
- 方式:
- 推(Push):当状态发生改变,通过变化侦测技术,框架就立即知道了,可以进行细粒度的更新。
- 拉(Pull):当状态发生变化时,不知道哪个状态变了,但是知道状态发生了变化,就发送一个信号告诉框架,框架内部收到信号后,会进行暴力比对找出哪些DOM节点需要重新渲染。
Object的变化侦测
- 实现:
- Object.defineProperty
- Proxy:ES6中实现。
- 追踪变化(以Object.defineProperty为例):在getter中收集依赖,即了解哪些地方用到了该属性;在setter中触发依赖,即属性发生变化就将已经收集好的依赖全部触发一遍使其更新。
- 收集什么:专门有一个对依赖进行抽象的类Watcher,在依赖收集阶段只收集这个类的实例进来,通知也只通知它一个,由它负责通知其他地方。
- 收集到哪:专门有一个管理依赖的类Dep,类中持有一个数组,将依赖收集到该类中。
- 怎么收集:Watcher的getter方法会将自身收集到Dep中。
- 怎么触发:Watcher中有update方法,每当属性变化,就调用该方法,它调用回调函数进行触发。
- Observer类:为了使数据中的所有属性及子属性都能被侦测到而设置的封装类。它将数据的所有属性及子属性都转换成getter/setter形式,然后追踪它们的变化。
- 这种方法无法追踪新增属性和删除属性。(通过vm.$set/vm.$deleteAPI解决此问题。)

- Data/Observer/Dep和Watcher之间的关系:
- Data通过Observer转换成了getter/setter的形式来追踪变化。
- 当外界通过Watcher 读取数据时,会触发getter从而将Watcher收集到依赖中。
- 当数据发生了变化时会触发setter,从而向Dep中的Watcher依赖发送通知。
- Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
Array的变化侦测
- 与Object的不同点:可以通过Array原型上的方法来访问和改变数组的内容,不会触发getter/setter。
- 思路:用一个拦截器覆盖Array.prototype。之后每当使用Array原型上的方法操作数组时,执行的都是拦截器中提供的方法。
- Array原型中可以改变自身内容的方法有7个,分别是push/pop/shift/unshift/splice/sort/reverse。
- 定义拦截器,在Observer中使用拦截器覆盖那些即将被转换成响应式Array类型的数据的原型(使用
__proto__
覆盖原型,如果不支持__proto
就直接将arrayMethods身上的这些方法设置到被侦测的数组上)。
- Array在getter中收集依赖,在拦截器中触发依赖,依赖列表dep存放在Observer中。通过Observer的实例的dep执行depend()方法收集依赖。使用dep.notify()去通知依赖(Watcher)数据发生了改变。
- 通过数组数据的
__ob__
属性可以获取Observer实例(该属性表示这个数据已经被侦测了,是响应式的),然后就可以获取Observer实例的dep。
- 使用observeArray方法循环Array中的每一项,执行observe()来递归地侦测所有子元素的变化。
变化侦测相关API
vm.$watch
vm.$watch
其实是对Watcher的一种封装,增加的deep和immediate的功能。
vm.$watch(expOrFn, callback, [options])
:
- 参数:
{string | Function} expOrFn
:被观察的表达式(只接受以点分隔的路径)或computed函数(computed:当其依赖的属性的值发生变化时,计算属性会重新计算,反之则使用缓存中的属性值。)
{Function | Object} callback
:回调函数,参数为新数据(newVal)和旧数据(oldVal)。
{Object} [options]
{boolean} deep
:为true时可以发现对象内部值的变化。(监听变动的数组时不需要这么做)
{boolean} immediate
:为true时立即以表达式的当前值触发回调。
- 返回值:
{Function} unwatch
:返回一个取消观察函数,用来停止触发回调。
vm.$set
vm.$set
主要用于避开Vue不能侦测属性被添加的限制。
vm.$set(target, key, value)
- 参数:
{Object | Array} target
:要添加属性的object。
{string | number} key
:要添加的属性名。
{any} value
:要添加的属性值。
- 返回值:
{Function} unwatch
:返回一个取消观察函数,用来停止触发回调。
vm.$delete
vm.$delete
主要用于避开Vue不能侦测属性被删除的限制。
vm.$delete(target ,key)
- 参数:
{Object | Array} target
:要删除属性的object。
{string | number} key/index
:要删除的属性名或下标。
虚拟DOM
概念
- 命令式操作DOM的问题:简单易用,但随着业务逻辑复杂,程序中的状态难以管理,代码逻辑也很混乱。
- Vue使用声明式操作DOM的方法:通过描述状态和DOM之间的映射关系是怎么样的,就可以将状态渲染成视图,关于状态到视图的转换过程,由框架负责实现,不需要写出如何操作DOM的过程。
- 如何确定状态中发生了什么变化以及需要在哪里更新DOM:虚拟DOM的解决方式是通过状态生成一个虚拟节点(Virtual Node, vnode)树,然后使用虚拟节点树进行渲染。在渲染之前,会使用新生成的虚拟节点树和上一次生成的虚拟节点树进行对比,只渲染不同的部分。
- Vue2.0开始使用一个中等粒度的方案:每个组件有一个watcher实例观察组件内节点的状态变化,当状态发生变化时,只能通知到组件,然后组件内部通过虚拟DOM进行比对与渲染。
- 模板转换成视图的过程:Vue通过编译将模板转换为渲染函数(render),执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点树就可以渲染页面。
- 在patch中将当前vnode与oldVnode进行比对,判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作。

VNode
- VNode可以理解为节点描述对象,它描述了应该怎样去创建真实的DOM节点,一个VNode表示一个真实的DOM元素,所有真实的DOM节点都使用VNode创建并插入到页面中。
- 渲染视图的过程:先创建vnode,再使用vnode去生成真实的DOM元素,最后插入到页面渲染视图。将上一次渲染视图时所创建的vnode缓存起来,之后每当需要重新渲染视图时,将新创建的vnode和上一次缓存的vnode进行对比,查看它们之间有哪些不一样的地方,找出这些不一样的地方并基于此去修改真实的DOM。
- vnode的类型:不同类型的vnode只是有效属性不同,无效属性会默认被赋值为undefined或false。
- 注释节点:text、isComment
- 文本节点:text
- 元素节点:tag(节点名称)、data(节点数据)、children(子节点列表)、context(当前组建的Vue.js实例)
- 组件节点:componentOptions(组件节点的选项参数,包含propsData/tag/children等)、componentInstance(组件的实例,也是Vue.js的实例)
- 函数式组件:functionalContext、functionalOptions
- 克隆节点:isCloned
patch
- patch用于将vnode渲染成真实的DOM。对现有DOM进行修改需要做三件事:
- 创建新增的节点。
- 删除已经废弃的节点。
- 修改需要更新的节点。

- 新增节点:
- 只有元素节点(有效tag属性)、注释节点(有效isComment属性)、文本节点(不是前两种)会被创建并插入到DOM中。

- 删除节点:
- 当一个节点只在oldVnode存在时,这时就需要从DOM中删除。
function removeVnodes (vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (isDef(ch)) {
removeNode(ch.elm);
}
}
}
- 过程就是删除vnodes数组中从startIdx指定的位置到endIdx指定位置的内容。
- 更新节点:
- 静态节点:指那些一旦渲染到界面上之后,无论状态如何变化都不会发生任何变化的节点。判断出静态节点后就可以跳过更新节点的操作过程。

- 更新子节点:
- 更新策略:大概可以分为四种操作,即更新节点、新增节点、删除节点、移动节点位置。
- 优化策略:尝试使用相同位置的两个节点来比对是否是同一节点,如果是,直接进入节点更新操作,如果不是,再用循环的方式查找节点。
模板编译原理
- 模板编译规定的是如何让虚拟DOM拿到vnode,之后才是上一节虚拟DOM拿到vnode后所做的事。
- 模板编译的主要目标就是生成渲染函数。
- 渲染函数的作用是每次执行它,它就会使用当前最新的状态生成一份新的vnode,然后使用这个vnode进行渲染。

- 模板编译的步骤:
- 解析器模块:将模板解析成抽象语法树(Abstract Syntax Tree, AST)。
- 优化器模块:遍历AST标记静态节点,在虚拟DOM中更新节点时,如果发现节点有这个标记,就不会重新渲染它。
- 代码生成器:使用AST生成渲染函数。
- 解析器:内部有很多小解析器,通过一条主线将这些解析器组装在一起。
- 主线的任务是监听HTML解析器。每当触发钩子函数(hook)时,就生成一个对应的AST节点。生成AST前,会根据类型使用不同的方式生成不同的AST。当HTML解析器把所有模板都解析完毕后,AST也就生成好了。
- 带变量的文本需要使用文本解析器二次加工。
// HTML解析器的整体逻辑伪代码
parseHTML(template, {
start (tag, attrs, unary){
// 每当解析到标签的开始位置时,触发该函数
},
end (){
// 每当解析到标签的结束位置时,触发该函数
},
chars (text){
// 每当解析到文本时,触发该函数
},
comment (text){
// 每当解析到注释时,触发该函数
}
})
- 优化器:避免一些无用功(标记静态节点使其不会被重新渲染)来提升性能。
- static属性:是否静态节点;staticRoot属性:是否静态根节点。
- 静态节点的特征是它的子节点必须是静态节点。
export function optimize (root) {
if (!root) return
// 第一步:标记所有静态节点
markStatic(root)
// 第二步:标记所有静态根节点
markStaticRoots(root)
}
- 代码生成器:将AST转换成渲染函数中的内容,称为“代码字符串”,这个字符串最终导出到外界使用时,会将代码字符串放到函数里,这个函数就叫做渲染函数。
- 渲染函数可以生成VNode的原因:渲染函数执行了createElement,而createElement可以创建一个VNode。
- 代码生成器其实就是字符串拼接的过程。通过递归AST来生成字符串,最先生成根节点,然后在子节点字符串生成后,将其拼接在根节点的参数中,子节点的子节点拼接在子节点的参数中,这样一层一层地拼接,直到最后拼接成完整的字符串,当字符串拼接好后,会将字符串拼在with中返回给调用者。
- AST:用JavaScript中的对象来描述一个节点,一个对象表示一个节点,对象中的属性用来保存节点所需的各种数据。
整体流程
- Vue.js源码目录结构:
- scripts:与构建相关的脚本和配置文件
- dist:构建后的文件
- flow:Flow的类型声明
- packages:包含vue-server-renderer和vue-template-compiler,它们作为单独的NPM包发布,自动从源码中生成,且始终与Vue.js具有相同的版本。
- test:所有的测试代码
- src:源代码
- compiler:与模板编译相关的代码。
- core:通用的、与平台无关的运行时代码。
- observer:实现变化侦测的代码。
- vdom:实现虚拟DOM的代码。
- instance:Vue.js实例的构造函数和原型方法
- global-api:全局API的代码。
- components:通用的抽象组件。
- server:与服务端渲染相关的代码。
- platforms:特定平台代码。
- sfc:单文件组件(.vue文件)解析逻辑
- shared:整个项目的公用工具代码。
- types:TypeScript类型定义
- test:类型定义测试
- Vue.js构建版本:
- 完整版:构建后的文件同时包含编译器和运行时。
- 如果需要在客户端编译模板 (比如传入一个字符串给 template 选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就将需要加上编译器,即完整版。
// 需要编译器
new Vue({
template: '<div>{{ hi }}</div>'
})
// 不需要编译器
new Vue({
render (h) {
return h('div', this.hi)
}
})
- 编译器:负责将模板字符串编译成JavaScript渲染函数。
- 运行时:负责创建Vue.js实例,渲染视图和使用虚拟DOM实现重新渲染。(体积较小,推荐使用)
- 当使用 vue-loader 或 vueify 的时候,*.vue 文件内部的模板会在构建时预编译成JavaScript。你在最终打好的包里实际上是不需要编译器的,所以只用运行时版本即可。
- UMD:可以通过`<script>`标签直接在浏览器中使用。
- CommonJS:用来配合比较旧的打包工具。
- ES Module:用来配合现代的打包工具。
实例方法
- 向Vue构造函数的prototype中挂载方法:将Vue构造函数当做参数传给了下面5个函数
- initMixin:被调用时,会向Vue构造函数的prototype中挂载
_init
方法,该方法实现了一系列初始化操作,包括整个生命周期的流程以及响应式系统流程的启动等。
- stateMixin:被调用时,会向Vue构造函数的prototype中挂载
vm.$set
、vm.$delete
、vm.$watch
三个数据相关的实例方法。
- eventsMixin:被调用时,会向Vue构造函数的prototype中挂载
vm.$on
、vm.$off
、vm.$once
、vm.$emit
四个事件相关的实例方法。
- lifecycleMixin:被调用时,会向Vue构造函数的prototype中挂载
vm.$forceUpdate
、vm.$destroy
两个生命周期相关的实例方法。
- renderMixin:被调用时,会向Vue构造函数的prototype中挂载
vm.$nextTick
这个与生命周期相关的实例方法。
vm.$on(event, callback)
:监听当前实例上的自定义事件,事件可以由vm.$emit
触发。回调函数会接受所有传入事件所触发的函数的额外参数。
- 参数:
{string | Array<string>} event
:监听事件。
{Function} callback
:回调函数。
vm.$off([event, callback])
:移除自定义事件监听器。如果没有参数就移除所有事件的监听器;如果只提供了事件就移除该事件的所有监听器;如果提供了事件与回调则只移除这个回调的监听器。
- 参数:
{string | Array<string>} event
:监听事件。
{Function} callback
:回调函数。
vm.$once(event, callback)
:监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。
- 参数:
{string | Array<string>} event
:监听事件。
{Function} callback
:回调函数。
vm.$emit(event, [..args])
:触发当前实例上的事件,附加参数都回传给监听器回调。
- 参数:
{string} event
:事件。
[...args]
:附加参数。
vm.$forceUpdate
:迫使Vue.js实例重新渲染,它只影响实力本身以及插入插槽内容的子组件。
vm.$destroy
:完全销毁一个实例,它会清理该实例与其他实例的连接,并解绑其全部指令及监听器,同时会触发beforeDestroy和destroyed钩子函数。
vm.$nextTick
:接收一个回调函数作为参数,它的作用是将回调延迟到下次DOM更新周期之后执行。
vm.$mount
:如果Vue.js实例在实例化时没有收到el选项,则它处于“未挂载”状态,没有关联的DOM元素。可以通过此方法手动挂载一个未挂载的实例。
- 参数:
{Element | string} [elementOrSelector]
:如果没有此参数,模板将被渲染为文档之外的元素,并且必须使用原生DOM的API把它插入文档中。
- 返回值:实例自身,因此可以链式调用其他实例方法。
全局API
Vue.extend(options)
:使用基础Vue构造器创建一个“子类”,其参数是一个包含“组件选项”的对象。
Vue.nextTick([callback, context])
:在下次DOM更新循环结束之后执行延迟回调,修改数据之后立即使用这个方法获取更新后的DOM。
- 参数:
{Function} [callback]
{Object} [context]
Vue.set(target, key, value)
:设置对象的属性,如果对象是响应式的,确保属性被创建后也是响应式的,同时触发视图更新,这个方法主要用于避开Vue不能检测属性被添加的限制。
- 参数:
{Object | Array} target
:目标对象。
{string | number} key
:属性名/属性下标。
{any} value
:属性值。
- 返回值:设置的值。
Vue.delete(target ,key)
:删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开Vue.js不能检测到属性被删除的限制。
- 参数:
{Object | Array} target
:目标对象。
{string | number} key/index
:属性名/属性下表
Vue.directive(id, [definition])
:注册或获取全局指令。
- 参数:
{string} id
:指令名
{Function | Object} [definition]
:指令函数
Vue.filter(id ,[definition])
:注册或获取全局过滤器。
- 参数:
{string} id
:过滤器名
{Function | Object} [definition]
:过滤器函数
Vue.component(id ,[definition])
:注册或获取全局组件。
- 参数:
{string} id
:组件名
{Function | Object} [definition]
:组件函数
Vue.use(plugin)
:安装Vue.js插件。如果插件是一个对象则必须提供install方法,如果插件是一个函数,它会被作为install方法。
- 参数:
{Function | Object} plugin
:插件
Vue.mixin(mixin)
:全局注册一个混入(mixin),影响注册之后创建的每个Vue.js实例。插件作者可以使用混入向组件注入自定义行为,不推荐在代码中使用。
Vue.compile(template)
:编译模板字符串并返回包含渲染函数的对象,只在完整版中才有效。
Vue.version
:提供字符串形式的Vue.js安装版本号。
生命周期
- Vue.js实例的生命周期可以分为4个阶段:初始化阶段、模板编译阶段、挂载阶段、卸载阶段。

- 初始化阶段:从new Vue()到created之间的阶段。这个阶段的主要目的是在Vue.js实例上初始化一些属性、事件以及响应式数据,如props/methods/data/computed/watch/provide/inject等。
- 模板编译阶段:从created到beforeMount之间的截阶段。这个阶段的主要目的是将模板编译为渲染函数,只存在于完整版中。
- 挂载阶段:从beforeMount到mounted之间的阶段。这个阶段的主要目的是将模板渲染到指定的DOM元素中。在运行时的大部分时间下,Vue.js处于已挂在状态,每当状态发生变化时,Vue.js都会通知组件使用虚拟DOM重新渲染,这个状态会持续到组件被销毁。
- 卸载阶段:调用
vm.$destroy
方法后。这个阶段的主要目的是将Vue.js实例自身从父组件中删除,取消实例上所有依赖的追踪并且移除所有的事件监听器。
指令
- 指令(Directive)是Vue.js提供的带有v-前缀的特殊特性。
- 指令属性的值预期是单个JavaScript表达式。
- 指令的职责是,当表达式的值改变时,将其产生的连带影响响应式地作用于DOM。
- 指令原理概述:
- 在模板解析阶段,会将节点上的指令解析出来并添加到AST的directives属性中。
- 随后directives数据会传递到VNode中
- 接着就可以通过vnode.data.directives获取一个节点所绑定的指令。
- 最后当虚拟DOM进行修补时,会根据节点的对比结果触发一些钩子函数。(更新指令的程序会监听create/update/destroy钩子函数,并在这三个钩子函数触发时对VNode和OldVNode进行对比,最终根据对比结果触发指令的钩子函数)
- 指令的钩子函数被出发后,就说明指令生效了。
- 常见指令:
v-if/v-else
v-for
v-on
:绑定事件监听器,事件类型由参数指定。
过滤器
- Vue.js允许自定义过滤器来格式化文本。它可以用在两个地方:双花括号插值和v-bind表达式。可以在组件的选项中定义本地过滤器,或者在创建Vue.js实例之前全局定义过滤器。
- 过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。