vue源码分析和对原理的理解

# 读源码分享

# 先整体 - 后细节

  • 先弄清楚源码分为哪几个模块,整套流程是怎么将各个模块串起来的。
  • 然后细化了解每个模块的核心原理。

# 站在别人的肩膀上

  • 不用一股脑扎进源码仓库里一点点啃,这样很低效,适用于对源码较为了解的人
  • 推荐先读别人的源码简介,源码分析(推荐:Vue.js 技术揭秘 (opens new window)),弄清楚脉络,还有每个部分的大概功能和核心流程。心里带着思路去看源码实现。
  • 多数情况不需要逐行代码的细究,但针对某些核心功能的实现需要细究,例如:虚拟dom、diff算法、数据驱动、响应式实现以及组件化等核心的功能的核心实现推荐细看。

# 读多遍

  • 一次粗读:看整体流程,看每个模块的核心功能和职责,体会平时写的代码在源码里面经过了什么步骤体现到了页面上。
  • 二次精读:看细节实现,弄清楚核心模块的实现方式(如弄懂diff算法思路,最好自己动手实现)。
  • 三次领悟:领悟源码整体架构和设计思想,体会每个模块之间如何配合协作,架构如何组织。

# vue简介

# 几个核心思想

  • 数据驱动
  • 组件化
  • 虚拟dom、diff局部最优更新

# 核心知识点

  • 数据驱动分析
    • Vue 实例化
    • Vue 实例挂载
    • render
    • Virtual DOM
    • createElement
    • update
  • 组件化分析
    • createComponent
    • patch
    • 合并配置
    • 生命周期
    • 组件注册
    • 异步组件
  • 响应式原理分析
    • 响应式对象 / 依赖收集
    • 派发更新 / nextTick
    • 检测变化的注意事项
    • 计算属性 VS 侦听属性
    • 组件更新

# 总体把握

  • template风格、对象配置风格
  • 虚拟dom思想(js对象操作代替dom操作)
  • diff算法思想(同层比较,添加、移动、删除)
  • 组件化思想(组件编译、组件通信)
  • 数据响应式(依赖收集、派发更新,发布订阅)

# 目录介绍

Vue.js 的源码在 src 目录下,其目录结构如下。

src
├── compiler        # 编译相关 
├── core            # 核心代码 
├── platforms       # 不同平台的支持
├── server          # 服务端渲染
├── sfc             # .vue 文件解析
├── shared          # 共享代码
1
2
3
4
5
6
7
  • compiler:编译相关的代码。它包括把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能。
  • core:核心代码,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等。
  • platform:不同平台的支持,是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。
  • server:服务端渲染,把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。
  • sfc: .vue 文件内容解析成一个 JavaScript 的对象。
  • shared:浏览器端和服务端所共享的工具方法。

# 源码构建

基于 Rollup (opens new window) 构建,相关配置在 scripts 目录下。

构建时通过不同的命令执行不同的脚本,去读取不同用处的配置,然后生成适合各种场景的Vue源码。 vue2.0有以下几种场景:

  • 浏览器端
  • 服务端渲染
  • 配合weex平台在客户端使用

# 类型检查

在vue2.x版本中使用 Flow (opens new window) 作为js静态类型检查工具,3.x版本使用typescript实现,自带类型检查。

# 数据驱动

vue核心思想之一就是数据驱动,指数据驱动生成视图,通过修改数据自动实现对视图的修改。这里主要分析模板和数据是如何渲染成最终的DOM的。

# new Vue的过程

Vue 初始化主要就干了几件事情,

  • 合并配置
  • 初始化生命周期
  • 初始化事件中心
  • 初始化渲染
  • 初始化 data、props、computed、watcher 等等。

# 实例挂载

$mount方法

  • Vue 不能挂载在 body、html 这样的根节点上;
  • 如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法
  • 在 Vue 2.0 版本中所有 Vue 的组件的渲染最终都需要 render 方法,是一个“在线编译”的过程;

挂载组件: mountComponent核心就是先实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。

Watcher在这里起到两个作用:

  • 初始化的时候会执行回调函数;
  • 当 vm 实例中的监测的数据发生变化的时候执行回调函数

# render渲染

  • 把 template 编译成 render 方法【编译过程后面专门介绍】
  • createElement创建dom节点

# 虚拟dom

  • 由于dom操作耗时十分长,且dom对象的体积很大,单个div的dom属性就有294个之多;
  • Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。
  • VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。
  • Virtual DOM到真实的dom需要经过以下过程:VNode 的 create、diff、patch

# createElement

判断第一个参数tag的类型,分为普通html标签、组件和其他类型,将子节点规范成 VNode 类型,递归整个树完成虚拟dom树的构建。

此方法是render函数的参数。

const app = new Vue({
    el: '#app',
    render: createElement => createElement(App)
})
1
2
3
4

# update

调用的时机:一个是首次渲染,一个是数据更新的时候;

首次渲染会将虚拟dom树整个渲染为dom节点,数据更新的时候会经过diff过程,只选取修改的虚拟dom节点进行局部更新。

update 的核心就是调用 vm.__patch__ 方法,不同的平台实现不一样,web平台生成dom节点,ssr服务端渲染生成html字符串。

dom树节点的插入顺序是先子后父

  • vue初始渲染的工作流程:
new Vue` ➜ `init` ➜ `$mount` ➜ `compile` ➜ `render` ➜ `vnode` ➜ `patch` ➜ `dom
1

# 组件化

  • 组件化,就是把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。
  • 组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。

# createComponent

在createElement里面调用,判断tag类型为组件时调用,用来将组件转换成虚拟dom

核心步骤:

  • 构造子类构造函数
  • 安装组件钩子函数
  • 实例化 vnode

# Vue.extend

  • 作用就是构造一个 Vue 的子类,这个子类就是组件本身,使用原型继承把纯对象转换一个继承于 Vue 的构造器 Sub 并返回
  • Sub 扩展了属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作;
  • 最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。

# patch

patch主要完成组件的渲染工作。

createComponent过程把组件转换成了VNode,patch过程会调用createElm把 VNode 转换成真正的 DOM 节点。

  • createComponent:递归实现深度遍历整个VNode树,用先子后父的方式插入dom树
  • 最终根节点VNode转化为dom后挂载到#app的节点上,且挂载元素不能是htmlbody
  • patch整体流程:createComponent ➜ 自组件初始化 ➜ 子组件render ➜ 自组件patch

# 合并配置

vue自身定义了一些默认配置,同时又可以在初始化阶段传入一些定义配置,然后去 merge 默认配置,来达到定制化不同需求的目的。

vue组件其实是一个js对象,我们写组件其实就是在写各种配置,这个配置在构建组件的时候会调用Vue.extent方法构建成一个组件类(因此我们组件内部访问到的this才是Vue的实例),那么在组件类实例化 new Vue() 的过程中,就会做合并配置这件事。

合并配置分为两种方式:

  • 外部初始化调用new Vue(例如挂载#app的时候)
  • 组件场景

主要合并以下几方面的配置:

  • mixin的配置
  • extends继承的配置
  • 编写的对象组件配置

# 生命周期

生命周期是vue在运行期间的各个关键节点运行的钩子函数,以便可以在特定场景做特定的事。

  • 生命周期依次有: beforeCreate ➜ created ➜ beforeMount ➜ mounted ➜ beforeUpdate ➜ updated ➜ beforeDestroy ➜ destroyed
  • 除此之外还有两个keep-alive中使用的生命周期: activated ➜ deactivated
  • 生命周期是一个数组,可能有多个钩子函数(合并配置中自带的和用户写的?)
  • 父子组件创建挂载执行顺序 父beforeCreate ➜ 父create ➜ 父beforeMount ➜ 子beforeCreate ➜ 子created ➜ 子mounted ➜ 父mounted
  • 更新 父beforeUpdate ➜ 子beforeUpdate ➜ 子updated ➜ 父updated
  • 销毁 父beforeDestroy ➜ 子beforeDestroy ➜ 子destroyed ➜ 父destroyed

beforeCreate & created

  • 调用在实例化&初始化state(props、data、methods、watch、computed 等属性)前后
  • 故在beforeCreate方法中无法使用this访问当前实例,也无法访问data、props等;
  • 使用场景:这两个方法可用来和后端交互,created做渲染前的数据初始化操作;不可访问组件dom;
  • 父子组建执行顺序先父后子

beforeMount & mounted

  • 调用在 DOM 挂载前后
  • beforeMount钩子函数执行顺序先父后子,mounted钩子函数执行顺序先子后父
  • 使用场景:这两个方法可用来和后端交互;mounted中可操作dom;访问ref

beforeUpdate & updated

  • beforeUpdate数据更新后更新dom前,updated调用在更新dom前后

beforeDestroy & destroyed

  • 调用在实例销毁前后
  • 销毁操作:parent 的 $children 中删掉自身,删除 watcher
  • 使用场景:beforeDestroy可解除自己绑定的事件监听等

activated & deactivated

  • 调用在keep-alive激活后和即将离开激活后

生命周期示意图

img

# 组件注册

  • 全局注册Vue.component(tagName, options), 挂载到Vue.options.components上,所有组件均可访问;
  • 局部注册components:{componentName: component}, 挂载到vm.$options.components上,仅父组件可访问;
  • 解析template模版遇到组件时,优先寻找vm.$options.components上的局部组件,找不着再寻找Vue.options.components上的全局组件;
  • 注册组件的时候,id 可以是连字符、驼峰或首字母大写的形式

# 异步组件

Vue有 3 种异步组件,实现了 loading、resolve、reject、timeout 4 种状态。异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender 强制重新渲染,这样就能正确渲染出我们异步加载的组件了。

  1. 普通函数异步组件: 通过resove回调处理
  2. Promise 异步组件:promise.then处理
  3. 高级异步组件: 增加loading、error、delay、timeout等,处理了加载占位、错误处理、超时时间等;

# 深入响应式原理

img

参考ustbhuangyi.github.io/vue-analysi… (opens new window)

# 响应式对象

响应式对象,核心就是利用 Object.defineProperty 给数据递归添加了 gettersetter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter 做的事情是派发更新。本质上是发布订阅模式(观察者模式)

# 依赖收集

所以在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了一个依赖收集的过程。

在定义相应式对象的的getter函数里,触发dep.depend做依赖收集,将获取属性的地方全部加入订阅者列表中,当数据发生变化时,通过遍历订阅者列表实现变更发布。

再次render时会先做依赖清除,再次进行新的依赖收集,这样做是为了处理v-if条件渲染的数据不用再派发更新了。

# 派发更新

实际上就是当数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcher 的 run,最后执行它们的回调函数。

通过setter来触发变量的更新,这里引入了一个队列的概念,这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发更新,而是先添加到一个队列里,然后在 nextTick 后执行更新,可以理解为等一段时间一起更新。

队列排序 queue.sort((a, b) => a.id - b.id) 对队列做了从小到大的排序,这么做主要有以下要确保以下几点:

  1. 组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。
  2. 用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。
  3. 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。

# nextTick

此方法可以在数据修改触发dom更新完成之后调用。 在浏览器环境中,常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;常见的 micro task 有 MutationObsever 和 Promise.then。

# 检测变化的注意事项

  1. 给对象添加属性不能被Object.defineProperty监测到,需要通过Vue.set方法手动告诉vue收集这个依赖并且派发更新。

  2. vue不能直接检测到数组项的赋值直接修改长度的,但是可以监测到splice等方法的修改,原因在于

    • 赋值:无法给数组设置defineProperty,可通过Vue.set实现对数组项的修改;
    • 通过数组方法修改:vue在Observer类中单独对数组做了处理,对数组对能增加数组长度的 3 个方法重写pushunshiftsueplice,现将方法原有逻辑执行完,再手动把新添加的值变成一个响应式对象,并且派发更新。
  3. 对象属性的删除可通过Vue.del方法,确保触发更新视图。

# 计算属性 VS 侦听属性

# computed和watch区别

# 计算属性

计算属性的触发有以下两种情况:

  • 主动访问:当计算属性被访问时触发getter函数,执行用户返回的计算结果,如果返回值发生变化才触发渲染更新(有缓存,依赖发生变化才执行)。
  • 依赖属性更新:计算属性会成为依赖变量的订阅者,依赖变量发生改变则触发计算属性重新计算。
  • 计算属性有 lazyactive两种模式,active模式依赖更新立即计算,lazy模式依赖变化仅设置this.dirty = true,等访问计算属性时再重新计算,并加入缓存。
  1. 计算属性的和侦听属性初始化都发生在 Vue 实例初始化阶段的 initState 函数中,先初始化计算属性后监听属性。
  2. 延时计算计算属性不会立刻求值(除非设置immediate: true
  3. 计算属性可以通过闭包来实现传参:
computed: {
    value () {
        return function (a, b, c) {
            /** do something */
            return data
        }
    }
}
1
2
3
4
5
6
7
8

# 监听属性

  • 监听属性相当于主动订阅了属性的变化,属性发生变化时执行回调函数
  • 监听属性的watcher执行优先级高于渲染watcher;
  • deep 设置为 true 用于监听对象内部值的变化
  • immediate 设置为 true 将立即以表达式的当前值触发回调

本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。

计算属性 vs 监听属性 从应用场景看

  • 计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;
  • 侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑(例如执行异步或开销较大的操作)。

watcher的 4 种类型deepusercomputedsync

  • deep:可以监听到对象属性的属性的修改变化,递归遍历对象所有属性,订阅所有子孙属性的变化(设置方法deep: true);
  • user:watch里面添加的侦听属性
  • computed:computed里面添加的监听
  • sync:属性变化后同步执行更新,不会加入缓冲队列在 nextTick 后执行。(设置方法immediate: true

计算属性 vs 方法

  • 缓存: computed 是可以缓的,methods 不能缓存;只要相关依赖没有改变,多次访问计算属性得到的值是之前缓存的计算结果,不会多次执行;调用方法将总会再次执行函数;
  • 延时计算: 计算属性主要对响应式属性的依赖,没有响应式依赖不会自动更新:如这样只会计算一次computed: {now: ()=>Date.now()}
  • 值得注意的是计算属性可以通过返回一个函数形成闭包来实现传参

# 组件更新(diff)

组件更新核心是响应式数据监控到数据的改变,重新生成了虚拟dom树,然后通过diff算法计算出前后虚拟dom树的差异点,更新dom时只更新变化的部分。

快问快答:

  • 为什么要diff?

答: O(n^3) 意味着如果要展示1000个节点,就要依次执行上十亿次的比较,无法承受大数据量的对比。

  • 直接比较和修改两个树的复杂度为什么是n^3

答: 老树的每一个节点都去遍历新树的节点,直到找到新树对应的节点。那么这个流程就是 O(n^2),再紧接着找到不同之后,再计算最短修改距离然后修改节点,这里是 O(n^3)。

  • diff的策略是什么?有什么根据?

答: 1、Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计,因此仅进行同层比较。 2、如果父节点不同,放弃对子节点的比较,直接删除旧节点然后添加新的节点重新渲染; 3、如果子节点有变化,Virtual DOM不会计算变化的是什么,而是重新渲染。 4、同级多个节点可通过唯一的key对比异同;

  • diff流程是什么?

答: 新旧节点不同:创建新节点 ➜ 更新父占位符节点 ➜ 删除旧节点; 新旧节点相同且没有子节点:不变; 新旧节点相同且都有子节点:遍历子节点同级比较,做移动、添加、删除三个操作,具体见下图; img

# Props属性

用作父组件给自组件传参,

  1. 规范化:将props属性都变成对象(原来可能是数组);
  2. 初始化:校验数据类型、响应式处理、代理(this.name 变成 this._props.name);
  3. 更新:一种是prop值被修改触发this._prop的setter;另一种是对象属性修改走对象响应式;

# 编译

编译的核心是把 template 模板编译成 render 函数。

vue有两种编译模式:

  • 运行时编译:在运行时才做模版编译( Runtime + Compiler )
  • 打包编译:借助 webpack 的 vue-loader 事先把模板编译成 render函数(Runtime only )

# 入口

运行时编译:入口compileToFunctions

// 解析模板字符串生成 AST
const ast = parse(template.trim(), options)
// 优化语法树
optimize(ast, options)
// 生成代码
const code = generate(ast, options)
1
2
3
4
5
6

# parse

AST:种抽象语法树,是对源代码的抽象语法结构的树状表现形式。

主要采用标记化算法的思路,解析器内部维护一个状态机

  • 解析流程会遍历html字符串,随着索引index的后移,状态机status会更新现在所处的解析状态;
  • 根据不同的解析状态使用不同的解析方法,当前解析状态完成后,再进入下一个解析状态;
  • 如此循环往复解析完整个树。

parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。【换成常量更好】

  • 解析template模版用到的正则
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*' 
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`) // 开始标签打开
const startTagClose = /^\s*(\/?)>/ // 开始标签关闭
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) // 标签结束
const doctype = /^<!DOCTYPE [^>]+>/i // 文档类型节点
const comment = /^<!\--/ // 注释节点
const conditionalComment = /^<!\[/
1
2
3
4
5
6
7
8
9
  • 解析模版流程图

img

# optimize

优化AST树的原因:处理响应式、标记静态节点、处理指令等

静态节点的判断方法:

  • 如果是表达式,就是非静态;如果是纯文本,就是静态;
  • 普通元素用了 v-pre 指令,是静态;
  • 普通元素满足下面所有条件则是静态:
    • 没有使用 v-if、v-for
    • 没有使用其它指令(不包括 v-once)
    • 非内置组件,是平台保留的标签
    • 非带有 v-for 的 template 标签的直接子节点,节点的所有属性的 key 都满足静态 key;

如果是普通元素非静态节点,则遍历它的所有 children,递归执行静态节点的标记,子节点有不是静态的情况,则它的父节点也为非静态。

标记静态根:缓存节点,优化diff过程,来减少操作dom

# codegen

把AST语法树转换成可执行的render函数,

主要处理AST的以下属性,将其变成render函数的写法:

  • static静态节点
  • once只渲染一次的节点
  • for处理循环
  • if处理条件渲染
  • children递归处理树状结构
  • slot处理插槽
  • 处理组件或者原生dom标签

# 扩展

主要介绍event、v-model、slot、keep-alive、transition等。

# event事件

主要从下面三个角度分析:

  • 编译解析
  • dom原生事件
  • 组件自定义事件

编译解析

在编译过程中解析template模版,识别其中v-on@等指令,记录下事件的名称回调函数,其中回调函数可能使函数名称或者一个函数。

dom原生事件

绑定方法:在组件上使用原生事件需要加.native 修饰符(例如@click.native) 添加移除:DOM事件调用原生 addEventListenerremoveEventListener

组件自定义事件

通过事件中心实现,思想类似发布订阅模式:

  • 把所有的事件用 vm._events 存储起来,当执行 vm.$on(event,fn) 的时候,按事件的名称 event 把回调函数 fn 存储起来 vm._events[event].push(fn)。
  • 当执行 vm.$emit(event) 的时候,根据事件名 event 找到所有的回调函数 let cbs = vm._events[event],然后遍历执行所有的回调函数。
  • 当执行 vm.$off(event,fn) 的时候会移除指定事件名 event 和指定的 fn
  • 当执行 vm.once(event,fn)的时候,内部就是执行vm.once(event,fn) 的时候,内部就是执行 vm.once(event,fn)的时候,内部就是执行vm.on,并且当回调函数执行一次后再通过 vm.$off 移除事件的回调,这样就确保了回调函数只执行一次。

注意

  • 原生DOM事件和自定义事件主要的区别:添加和删除事件的方式不一样;DOM事件调用原生 addEventListenerremoveEventListener 添加和删除;自定义事件调用vm.$off方法删除回调函数即可;
  • 只有组件节点才可以添加自定义事件,并且添加原生 DOM 事件需要使用 native 修饰符;而普通元素使用 .native 修饰符是没有作用的,也只能添加原生 DOM 事件。

# v-model双向数据绑定

数据响应:data ➜ view v-model双向数据绑定: data view

v-model 是一种语法糖,即可以作用在普通表单元素上,又可以作用在组件上。

  • 表单元素实现 v-model 的方法:

通过修改 AST 元素,给 el 添加一个 prop,相当于我们在 input 上动态绑定了 value,又给 el 添加了事件处理,相当于在 input 上绑定了 input 事件。<input v-model="message" /> 相当于: <input v-bind:value="message" v-on:input="message=$event.target.value" />

  • 对组件来说就是:

<child :value="message" @input="e => message = e"/> 组件可以配置子组件接收的 prop 名称和派发的事件名称

{
  props: ['msg'],
  model: {
    prop: 'msg',
    event: 'change'
  },
  methods: {
    updateValue(e) {
      this.$emit('change', e.target.value)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# slot插槽

插槽就像是子组件中的一个个空抽屉,父组件可以在调用子组件的时候自己决定放什么内容到不同的抽屉里。

编译

  • 编译父组件时,当解析到标签上有 slot 属性的时候,将元素节点上标记为data.slot = slotName || 'default'
  • 编译自组件时,当解析到 slot 标签的时候,在此AST元素节点上标记 slotName ,然后在渲染阶段从父组件的 children 中遍历匹配data.slot 获取对应名称渲染好的插槽vnode

作用域插槽 作用域插槽作用:子组件给父组件传递数据。

读取 scoped-slot 属性并赋值给当前元素节点的 slotScope 属性,接下来在构造 AST树的时候,不会作为 children 添加到当前 AST 树中,而是存到父 AST 元素节点的 scopedSlots 属性上,它是一个对象,以插槽名称 name 为 key,以渲染函数为value。

然后在子组件渲染的时候,取到父组件的scopedSlots 里面的渲染函数,执行生成vnode。

普通插槽和作用域插槽的区别:

  • 普通插槽是在父组件编译和渲染阶段生成 vnodes,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的 vnodes
  • 对于作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes,而是在父节点 vnode 的 data 中保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 vnodes,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例
  • 简单地说,两种插槽的目的都是让子组件 slot 占位符生成的内容由父组件来决定,但数据的作用域会根据它们 vnodes 渲染时机不同而不同。

# keep-alive

  • keep-alive是一个内置抽象组件,在组件实例建立父子关系的时候会被忽略;
  • created 钩子里定义了 this.cache 和 this.keys用来缓存已经创建过的 vnode;在组件重新被激活时把缓存的domvnode.elm插入dom树即可;
  • keep-alive组件只处理第一个子元素(将子节点当成树的根节点故只有一个),一般和它搭配使用的有 component 动态组件或者是 router-view组件;
  • include 和 exclude做匹配过滤,匹配成功的加入缓存,参数支持数组、字符串、正则表达式等;max 控制缓存的组件个数
  • 生命周期:activated(激活)、deacitvated(冻结)

# transition、transition-group

  • keep-alive也是一个内置抽象组件,是 web平台独有的,同样也只处理一个子节点(多了会警告);
  • 延迟dom操作的执行时机,用来执行动画,执行完毕进行dom操作。
  • 通过监听dom上css3过渡动画的执行结束的事件得知执行完毕

在下列情形中添加过渡效果

  • 条件渲染 (使用 v-if)
  • 条件展示 (使用 v-show)
  • 动态组件
  • 组件根节点

Vue 的过渡实现分为以下几个步骤:

  • 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
  • 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
  • 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧(requestAnimationFrame实现)中立即执行。

真正执行动画的是我们写的 CSS 或者是 JavaScript 钩子函数,而 Vue 的transition组件只是帮我们很好地管理了这些 CSS 的添加/删除,以及钩子函数的执行时机。

transition-group

  • transiiton只能针对单一元素实现过渡效果,transition-group对列表实现过度效果,当修改列表的数据的时候,如果是添加或者删除数据,则会触发相应元素本身的过渡动画;

# vue3比较

# vue3新特性了解

# vue2.x的痛点

  • 源码自身的维护性;
  • 数据量大后带来的渲染和更新的性能问题;
  • 一些想舍弃但为了兼容一直保留的鸡肋 API 等;
  • TypeScript 支持;

# vue3.0优化点

  • 一、使用 monorepo管理源码
  • 二、使用 TypeScript 开发源码
  • 三、性能优化 1.源码体积优化 2.数据劫持优化Proxy 3.编译优化 4.diff算法优化
  • 四、语法 API 优化:Composition API

# 细究一下diff算法

# vue2的diff

组件更新核心是响应式数据监控到数据的改变,重新生成了虚拟dom树,然后通过diff算法计算出前后虚拟dom树的差异点,更新dom时只更新变化的部分。 快问快答:

1. 为什么要diff?

答: O(n^3) 意味着如果要展示1000个节点,就要依次执行上十亿次的比较,无法承受大数据量的对比。

直接比较和修改两个树的复杂度为什么是n^3?

答: 老树的每一个节点都去遍历新树的节点,直到找到新树对应的节点。那么这个流程就是 O(n^2),再紧接着找到不同之后,再计算最短修改距离然后修改节点,这里是 O(n^3)。

2. diff的策略是什么?有什么根据?

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计,因此仅进行同层比较。
  2. 如果父节点不同,放弃对子节点的比较,直接删除旧节点然后添加新的节点重新渲染;
  3. 如果子节点有变化,Virtual DOM不会计算变化的是什么,而是重新渲染。
  4. 同级多个节点可通过唯一的key对比异同;

3. diff流程是什么?

答: 新旧节点不同:创建新节点 ➜ 更新父占位符节点 ➜ 删除旧节点; 新旧节点相同且没有子节点:不变; 新旧节点相同且都有子节点:遍历子节点同级比较,做移动、添加、删除三个操作;

# vue3.0的diff

深度递归遍历vnode树,节点的标签和key相同认为是同一个节点则更新,不同则删除,然后处理子节点。 子节点分这几种情况处理:纯文本、vnode 数组和空

空往往意味着添加或删除; 纯文本相同直接更新innerText,不同则删除; 新旧子节点都是vnode数组则diff算法来处理;

vue3.0 diff算法思想

  • 编译模版时进行静态分析,标记动态节点,diff对比差异时仅对比动态节点(性能提升明显);
  • diff算法先去头去尾,借此缩短遍历对比数组长度(对数组插入和删除操作性能优化明显);
  • 通过对更新前后子节点数组建立映射表的方式,将O(n^2)复杂度的遍历降低到O(n);
  • 通过最长递增子序列方法了来diff前后的子节点数组,减少移动操作的次数;

最长递增子序列算法实现

leetcode最长递增子序列算法题 (opens new window)

/*
 * 寻找最长递增子序列
 * 使用动态规划思想,a -> c = a -> b + b -> c
 * 其中p数组存储的是从p[p[i]] 到 p[i] 的最长递增子序列索引,也就是前一个b的索引;
 * r数组存储最后一个元素也就是c的索引
 */
 function getSequenceOfLIS(arr) {
    const p = [0];
    const result = [0];
    for (let i = 0; i < arr.length; i ++) {
        const val = arr[i];
        const top = result[result.length - 1];
        if (arr[top] < val) {
            p[i] = top;
            result.push(i);
            continue;
        }
        // 二分法搜索
        let l = 0, r = result.length - 1;
        while(l < r) {
            const c = (l + r) >> 1;
            if (arr[result[c]] < val) {
                l = c + 1;
            } else {
                r = c;
            }
        }
        if (val < arr[result[l]]) {
            if (l > 0) {
                p[i] = result[l - 1]
            }
            result[l] = i;
        }
    }
    // 回朔p数组,找出最长递增子序列
    let preIndex = result[result.length - 1];
    for (let i = result.length - 1; i > 0; i --) {
        result[i] = preIndex;
        preIndex = p[preIndex]
    }
    return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# vue-router

路由的功能是统筹分发,告诉什么人应该干什么事情,对前端来说就是将不同的路径映射到不同的功能(视图)上去。

vue-router支持 hashhistoryabstract 3 种路由方式,提供了 <router-link><router-view> 2 种组件

# 路由注册

  • Vue 从它的设计上就是一个渐进式 JavaScript 框架,它本身的核心是解决视图渲染的问题,其它的能力就通过插件的方式来解决。Vue-Router 就是官方维护的路由插件。

  • 插件通过Vue.use方法来实现注册,实际上是运行插件的install方法

  • Vue-Router安装最重要的一步就是利用 Vue.mixin 去把 beforeCreate 和 destroyed 钩子函数注入到每一个组件中

  • 通过在Vue.prototype原型上添加方法的方式来让用户访问到方法,使用defineProperty设置只读可避免被用户手动篡改。

    Object.defineProperty(Vue.prototype, '$router', {
        get () { return this._routerRoot._router }
    })
    
    1
    2
    3
  • 注册全局组件<router-link><router-view>

# VueRouter 对象

包含以下方法:

  • init
  • beforeEach
  • beforeResolve
  • afterEach
  • onReady
  • onError
  • push
  • replace
  • go
  • back
  • forward
  • getMatchedComponents
  • resolve
  • addRoutes

# matcher

匹配过程主要做的事情:

  • 初始化根据用户传递的路由配置,生成映射表
  • 解析url参数,url与用户设置的路由路径匹配

# 路径切换

路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据。

  • router-link点击相当于调用$router.push方法去修改url

<router-link> 比起写死的 <a href="..."> 会好一些,理由如下:

  • 无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当你要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动。
  • 在 HTML5 history 模式下,router-link 会守卫点击事件,让浏览器不再重新加载页面。
  • 当你在 HTML5 history 模式下使用 base 选项之后,所有的 to 属性都不需要写(基路径)了。

# 相关链接

Vue.js 技术揭秘 (opens new window)

https://juejin.cn/post/6886266739378618382

上次更新: 2022/04/15, 05:41:29
×