基础概念

  • 渐进式框架:将框架分层,可以先使用其基础功能,然后在此基础上逐渐增加使用所需要的其他的功能。
    • 由最内层到最外层:声明式渲染->组件机制->路由机制->状态管理->构建工具
  • 响应式系统:Vue中数据模型仅仅是普通的JavaScript对象。而它们被修改时,视图会进行更新。
  • 渲染:从状态生成DOM,再输出到用户界面显示的一整套流程。
  • 声明式渲染:Vue使用 "模板语法" 来将数据渲染进DOM系统。
  • 事件循环:JavaScript是一门单线程且非阻塞的脚本语言,这意味着JavaScript代码在执行的任何时候都只有一个主线程来处理所有任务。而非阻塞是指当代码需要处理异步任务时,主线程会挂起(pending)这个任务,当异步任务处理完毕后,JavaScript会将这个事件加入一个队列(事件队列,微任务和宏任务在不同的队列中)中,被放入事件队列中的事件不会立刻执行回调,而是等待执行栈中的所有任务执行完毕后,主线程会去检查微任务队列中是否有事件存在,如果存在则会依次执行微任务队列中事件对应的回调,直到为空,然后去宏任务队列中取出一个事件,把对应的回调加入当前执行栈,当执行栈中的所有任务都执行完毕后,再重复检查微任务队列中是否有任务存在。
  • 执行栈:

变化侦测

  • 作用:侦测数据的变化,当数据变化时,会通知视图进行相应的更新。
  • 方式:
    • 推(Push):当状态发生改变,通过变化侦测技术,框架就立即知道了,可以进行细粒度的更新。
      • Vue实现了推。
    • 拉(Pull):当状态发生变化时,不知道哪个状态变了,但是知道状态发生了变化,就发送一个信号告诉框架,框架内部收到信号后,会进行暴力比对找出哪些DOM节点需要重新渲染。
      • Angular、React实现了拉。

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/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进行修改需要做三件事:
    • 创建新增的节点。
    • 删除已经废弃的节点。
    • 修改需要更新的节点。
      patch运行流程
  • 新增节点:
    • 只有元素节点(有效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进行渲染。
      模板编译在整个渲染过程中的位置
  • 模板编译的步骤:
    1. 解析器模块:将模板解析成抽象语法树(Abstract Syntax Tree, AST)。
    2. 优化器模块:遍历AST标记静态节点,在虚拟DOM中更新节点时,如果发现节点有这个标记,就不会重新渲染它。
    3. 代码生成器:使用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源码目录结构:
    1. scripts:与构建相关的脚本和配置文件
    2. dist:构建后的文件
    3. flow:Flow的类型声明
    4. packages:包含vue-server-renderer和vue-template-compiler,它们作为单独的NPM包发布,自动从源码中生成,且始终与Vue.js具有相同的版本。
    5. test:所有的测试代码
    6. src:源代码
      1. compiler:与模板编译相关的代码。
      2. core:通用的、与平台无关的运行时代码。
        1. observer:实现变化侦测的代码。
        2. vdom:实现虚拟DOM的代码。
        3. instance:Vue.js实例的构造函数和原型方法
        4. global-api:全局API的代码。
        5. components:通用的抽象组件。
      3. server:与服务端渲染相关的代码。
      4. platforms:特定平台代码。
      5. sfc:单文件组件(.vue文件)解析逻辑
      6. shared:整个项目的公用工具代码。
    7. types:TypeScript类型定义
      1. 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.$setvm.$deletevm.$watch三个数据相关的实例方法。
    • eventsMixin:被调用时,会向Vue构造函数的prototype中挂载vm.$onvm.$offvm.$oncevm.$emit四个事件相关的实例方法。
    • lifecycleMixin:被调用时,会向Vue构造函数的prototype中挂载vm.$forceUpdatevm.$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构造器创建一个“子类”,其参数是一个包含“组件选项”的对象。
    • 参数:
      • {Object} options:组件选项。
  • 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实例。插件作者可以使用混入向组件注入自定义行为,不推荐在代码中使用。
    • 参数:
      • {Object} mixin
  • Vue.compile(template):编译模板字符串并返回包含渲染函数的对象,只在完整版中才有效。
    • 参数:
      • {string} 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。
  • 指令原理概述:
    1. 在模板解析阶段,会将节点上的指令解析出来并添加到AST的directives属性中。
    2. 随后directives数据会传递到VNode中
    3. 接着就可以通过vnode.data.directives获取一个节点所绑定的指令。
    4. 最后当虚拟DOM进行修补时,会根据节点的对比结果触发一些钩子函数。(更新指令的程序会监听create/update/destroy钩子函数,并在这三个钩子函数触发时对VNode和OldVNode进行对比,最终根据对比结果触发指令的钩子函数)
    5. 指令的钩子函数被出发后,就说明指令生效了。
  • 常见指令:
    • v-if/v-else
    • v-for
    • v-on:绑定事件监听器,事件类型由参数指定。

过滤器

  • Vue.js允许自定义过滤器来格式化文本。它可以用在两个地方:双花括号插值和v-bind表达式。可以在组件的选项中定义本地过滤器,或者在创建Vue.js实例之前全局定义过滤器。
  • 过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。