写在前面
-
前端时间给大家分享了一篇开源文档 《 Vue 源码分析与讲解 》,详情请戳:
-
这段时间我也是把文档和配套的课程学习了一遍,对Vue的原理有了更深层次的理解,由于原文档中涉及大量的源代码以及实现逻辑分享,不易于阅读和事后查阅,因此我在阅读的过程中对一些关键性的内容做了标记和整理,现将整理的笔记再次分享给大家。
-
由于文章是通过 wolai 导出为 md,导致部分格式和样式丢失,为了更好的阅读体验,可访问下方原文链接。
Flow
认识 Flow
-
Flow 是 facebook 出品的 JavaScript 静态类型检查⼯具。
-
Vue.js 的源码利⽤了 Flow 做了静态类型检查。
为什么⽤Flow
-
JavaScript 是动态类型语⾔,它的灵活性有⽬共睹,但是过于灵活的副作⽤是很容易就写出⾮常隐蔽的隐患代码,在编译期甚⾄看上去都不会报错,但在运⾏阶段就可能出现各种奇怪的 bug。
-
类型检查是当前动态类型语⾔的发展趋势,所谓类型检查,就是在编译期尽早发现(由类型错误引起的)bug,⼜不影响代码运⾏(不需要运⾏时动态检查类型),使编写 JavaScript 具有和编写 Java 等强类型语⾔相近的体验。
-
项⽬越复杂就越需要通过⼯具的⼿段来保证项⽬的维护性和增强代码的可读性。
-
Vue.js 在做 2.0 重构的时候,在 ES2015 的基础上,除了 ESLint 保证代码⻛格之外,也引⼊了 Flow 做静态类型检查。之所以选择 Flow,主要是因为 Babel 和 ESLint 都有对应的 Flow 插件以⽀持语法,可以完全沿⽤现有的构建配置⾮常⼩成本的改动就可以拥有静态类型检查的能⼒。
Vue.js 源码构建
-
Runtime Only
我们在使⽤ Runtime Only 版本的 Vue.js 的时候,通常需要借助如 webpack 的 vue-loader ⼯具把 .vue ⽂件编译成 JavaScript,因为是在编译阶段做的,所以它只包含运⾏时的 Vue.js 代码,因此代码体积也会更轻量。
-
Runtime + Compiler
我们如果没有对代码做预编译,但⼜使⽤了 Vue 的 template 属性并传⼊⼀个字符串,则需要在客户端编译模板。
编译是比较耗费性能的操作,推荐使用第一种方式
Vue 为什么不用ES6的Class 去实现?
- Vue有很多 xxxMixin 的函数调⽤,并把 Vue 当参数传⼊,它们的功能都是给 Vue 的 prototype 上扩展⼀些⽅法,Vue 按功能把这些扩展分散到多个模块中去实现,⽽不是在⼀个模块⾥实现所有,这种⽅式是⽤ Class 难以实现的。这么做的好处是⾮常⽅便代码的维护和管理,这种编程技巧也⾮常值得我们去学习。
Vue 的本质
- Vue 本质上就是⼀个⽤ Function 实现的 Class,然后它的原型 prototype 以及它本⾝都扩展了⼀系列的⽅法和属性。
数据驱动
Vue.js ⼀个核⼼思想是数据驱动。所谓数据驱动,是指视图是由数据驱动⽣成的,我们对视图的修改,
不会直接操作 DOM,⽽是通过修改数据。
New Vue( ) 初始化
- Vue 初始化主要就⼲了⼏件事情,合并配置,初始化⽣命周期,初始化事件中⼼,初始化渲染,初始
化 data、props、computed、watcher 等等。
Vue 的挂载过程
Vue 不能挂载在 body 、 html 这样的根节点上
-
$mount ⽅法⽀持传⼊ 2 个参数,第⼀个是 el ,它表⽰挂载的元素,可以是字符串,也可以是DOM 对象,如果是字符串在浏览器环境下会调⽤ query ⽅法转换成 DOM 对象的。
-
$mount 方法实际上调用的是 mountComponent :mountComponent 核⼼就是先调⽤ vm._render ⽅法先⽣成虚拟 Node,再实例化⼀个渲染 Watcher
** $mount → mountComponent → vm._render → 虚拟Node → 实例化渲染watcher**
Render
-
Vue 的render 是实例的一个私有化方法,用于将实例渲染成虚拟 Node :** vm.render → V Node **
-
vm._render 最终是通过执⾏ createElement ⽅法并返回的是 vnode ,它是⼀个虚拟 Node。Vue2.0 相⽐ Vue 1.0 最⼤的升级就是利⽤了 Virtual DOM
vm.render → createElement → VNode
Virtual Dom
真正的 DOM 元素是⾮常庞⼤的,因为浏览器的标准就把 DOM 设计的⾮常复杂。当我们频繁的去做 DOM 更新,会产⽣⼀定的性能问题。
———————————————————
Virtual DOM 就是⽤⼀个原⽣的 JS 对象去描述⼀个 DOM 节点,所以它⽐创建⼀个 DOM 的代价要⼩很多。在 Vue.js 中,Virtual DOM 是⽤ VNode 这么⼀个 Class 去描述
-
其实 VNode 是对真实 DOM 的⼀种抽象描述,它的核⼼定义⽆⾮就⼏个关键属性,标签名、数据、⼦
节点、键值等,其它属性都是都是⽤来扩展 VNode 的灵活性以及实现⼀些特殊 feature 的。由于VNode只是⽤来映射到真实 DOM 的渲染,不需要包含操作 DOM 的⽅法,因此它是⾮常轻量和简单的。 -
Vue.js 中,VNode 的 create 是通过之前提到的** createElement **⽅法创建的
createElement
createElement ⽅法实际上是对 _createElement ⽅法的封装,它允许传⼊的参数更加灵活,在处
理这些参数后,调⽤真正创建 VNode 的函数 _createElement
- _createElement ⽅法有 5 个参数:
-
context 表⽰ VNode 的上下⽂环境,它是 Component 类型;
-
tag 表⽰标签,它可以是⼀个字符串,也可以是⼀个 Component ;
-
data 表⽰ VNode 的数据,它是⼀个 VNodeData 类型,可以在 flow/vnode.js 中找到它的定义,这⾥先不展开说;
-
children 表⽰当前VNode 的⼦节点,它是任意类型的,它接下来需要被规范为标准的 VNode数组
-
normalizationType 表⽰⼦节点规范的类型,类型不同规范的⽅法也就不⼀样,它主要是参考 render 函数是编译⽣成的还是⽤户⼿写的。
children 的规范化
-
_createElement 接收的第 4 个参数 children 是任意类型的,因此我们需要把它们规范成 VNode 类型。
-
functional component 函数式组件返回的是⼀个数组⽽不是⼀个根节点,所以会通过 Array.prototype.concat ⽅法把整个children 数组打平,让它的深度只有⼀层。
VNode的创建
-
createElement 函数,规范化 children 后,接下来会去创建⼀个 VNode 的实例:
这⾥先对 tag 做判断,如果是 string 类型,则接着判断如果是内置的⼀些节点,则直接创建⼀个普通VNode,如果是为已注册的组件名,则通过 ** createComponent 创建⼀个组件类型的 VNode**,否则创建⼀个未知的标签的 VNode。如果是 tag ⼀个 Component 类型,则直接调⽤createComponent 创建⼀个组件类型的 VNode 节点。 -
内置节点→普通的VNode
-
已注册的组件→createComponent → 组件类型的Vnode
-
未知→未知标签的VNode
每个 VNode 有children , children 每个元素也是⼀个 VNode,这样就形成了⼀个 VNode Tree,它很好的描述了我们的 DOM Tree。
Update
**_update 方法的作用是将VNode 渲染为真实的 DOM **
- Vue 的 _update 是实例的⼀个私有⽅法,它被调⽤的时机有 2 个,⼀个是⾸次渲染,⼀个是数据更新的时候
update → vm.__patch_**** → createPatchFunction ( { nodeOps , modules } )
createPatchFunction 内部定义了⼀系列的辅助⽅法,最终返回了⼀个 patch ⽅法,这个⽅法就
赋值给了 vm._update 函数⾥调⽤的 vm.patch
————————————————————————————————————
Ops 封装了⼀系列 DOM 操作的⽅法
modules 定义了⼀些模块的钩⼦函数的实现
-
patch 方法接受4个参数
-
oldVNode → 旧的节点,它也可以不存在或者是⼀个 DOM 对象;
-
Vnode → 执行_render后返沪的VNode 节点
-
hydrating → 是否服务端渲染
-
removeOnly → 给 transition-group ⽤的
-
⾸次渲染在执⾏ patch 函数的时候,传⼊的 vm.$el 对应的是例⼦中 id 为 app 的 DOM 对象,这个也就是我们在 index.html 模板中写的
。 -
通过emptyNodeAt 方法把oldNode 转换成VNode 对象,然后再调用createElm 方法
createElm 的作⽤是通过虚拟节点创建真实的 DOM 并插⼊到它的⽗节点中 -
调⽤ insert ⽅法把 DOM 插⼊到⽗节点中,整个VNode 树节点的创建过程是先子后父
insert 使用原生的DOM API 进行DOM操作,实现元素的插入
patch ⽅法,⾸次渲染我们调⽤了 createElm ⽅法,这⾥传⼊的 parentElm 是oldVnode.elm 的⽗元素, 在我们的例⼦是 id 为 #app div 的⽗元素,也就是 Body;实际上整个过程就是递归创建了⼀个完整的 DOM 树并插⼊到 Body 上。
—————————————————————————
最后,我们根据之前递归 createElm ⽣成的 vnode 插⼊顺序队列,执⾏相关的 insert 钩⼦函数
oldNode → emptyNodeAt → VNode → createElm → insert
组件化
createComponent
- createElement 的实现的时候,它最终会调⽤ _createElement ⽅法,其中有⼀段逻辑是对参数 tag 的判断,如果是⼀个普通的 html 标签,像上⼀章的例⼦那样是⼀个普通的div,则会实例化⼀个普通 VNode 节点,否则通过 createComponent ⽅法创建⼀个组件 VNode。
createElement → _createElement → ?( tag 是普通的html标签 ) → 普通的VNode
createElement → _createElement → ?( tag 不是html ) → createComponent → 组件 VNode
- createComponent 针对组件渲染主要就 3 个关键步骤:
-
构造⼦类构造函数,
-
安装组件钩⼦函数
-
实例化 vnode
构造⼦类构造函数
-
Vue.extend 的作⽤就是构造⼀个 Vue 的⼦类,它使⽤⼀种⾮常经典的原型继承的⽅式把⼀个纯对象转换⼀个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本⾝扩展了⼀些属性,如扩
展 options 、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化⼯作;最后对于
这个 Sub 构造函数做了缓存,避免多次执⾏ Vue.extend 的时候对同⼀个⼦组件重复构造。 -
实例化Sub的时候执行this._init
安装组件钩子函数
-
Vue.js 在初始化⼀个 Component 类型的 VNode 的过程中执行了部分钩子函数
-
在 VNode 执⾏ patch 的过程中执⾏相关的钩⼦函数
如果某个时机的钩⼦已经存在 data.hook 中,那么通过执⾏ mergeHook 函数做合并,这个逻辑很简单,就是在最终执⾏的时候,依次执⾏这两个钩⼦函数即可。
实例化VNode
- 通过 new VNode 实例化⼀个 vnode 并返回。需要注意的是和普通元素节点的vnode 不同,组件的 vnode 是没有 children 的
patch
**createComponent →(创建) → VNode → vm._update → vm._patch → createElm → 真实DOM **
合并配置
new Vue 的过程通常有 2 种场景,⼀种是外部我们的代码主动调⽤ new Vue(options) 的⽅式实例化⼀个 Vue 对象;另⼀种是我们上⼀节分析的组件过程中内部通过 new Vue(options) 实例化⼦组件。
——————————————————————————————————————————
无论何种场景,都会执行 _init ( options )
**_init ( options ) → merge options 逻辑 **
外部调用场景
new Vue → this._init ( options ) → mergeOptions ******** → extend → 扩展****内置组件
mergeOptions ⽅法来合并,它实际上就是把resolveConstructorOptions(vm.constructor) 的返回值和 options 做合并
Vue 的内置组件⽬前有 、 和 组件
-
mergeOptions 主要功能就是把 parent 和 child 这两个对象根据⼀些合并策略,合并成⼀个新对象并返回。
-
钩子函数合并为数组,执行的时候依次调用
组件场景
组件的构造函数是通过 Vue.extend 继承⾃ Vue
-
⼦组件初始化过程通过 initInternalComponent 合并,合并完的结果保留在 vm.$options 中。
-
⼦组件初始化过程通过 initInternalComponent ⽅式要⽐外部初始化 Vue 通过mergeOptions 的过程要快
生命周期
每个 Vue 实例在被创建之前都要经过⼀系列的初始化过程。例如需要设置数据监听、编译模板、挂载
实例到 DOM、在数据变化时更新 DOM 等。同时在这个过程中也会运⾏⼀些叫做⽣命周期钩⼦的函
数,给予⽤户机会在⼀些特定的场景下添加他们⾃⼰的代码。
-
源码中最终执⾏⽣命周期的函数都是调⽤ callHook ⽅法
-
callHook 函数的逻辑很简单,根据传⼊的字符串 hook ,去拿到 vm.$options[hook] 对应的回调函数数组,然后遍历执⾏,执⾏的时候把 vm 作为函数执⾏的上下⽂。
-
Vue.js 合并 options 的过程,各个阶段的⽣命周期的函数也被合并到 vm.$options ⾥,并且是⼀个数组。因此 callhook 函数的功能就是调⽤某个⽣命周期钩⼦注册的所有回调函数。
beforeCreate & created
-
beforeCreate 和 created 函数都是在实例化 Vue 的阶段,在** _init**⽅法中执⾏的
-
beforeCreate 和 created 的钩⼦调⽤是在 initState 的前后,
-
initState 的作⽤是初始化 props 、 data 、 methods 、 watch 、computed 等属性。
-
beforeCreate 的钩⼦函数中就不能获取到 props 、 data 中定义的值,也不能调⽤methods 中定义的函数。
beforeCreate ( 无法获取props,data,methods ) → initState ( 初始化props,data,methods ) → creared ( 可以访问props,data,methods )
- beforeCreate 和 created这俩个钩⼦函数执⾏的时候,并没有渲染DOM,所以我们也不能够访问 DOM
⼀般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩⼦函数执⾏都可以,如果是需要访问props 、 data 等数据的话,就需要使⽤ created 钩⼦函数。
—————————————————————————————
** vue-router 和 vuex的时候会发现它们都混合了 beforeCreatd 钩⼦函数。**
beforeMount & mounted
-
beforeMount 钩⼦函数发⽣在 mount ,也就是 DOM 挂载之前,它的调⽤时机是在mountComponent 函数中
-
在执⾏ vm._render() 函数渲染 VNode 之前,执⾏了 beforeMount 钩⼦函数,在执⾏完vm._update() 把 VNode patch 到真实 DOM 后,执⾏ mouted 钩⼦。
** beforeMount → vm._render ( ) → VNode → vm._update() → patch → DOM → mounted ( 可以访问DOM了 )**
- mounted 钩⼦函数的执⾏顺序也是先⼦后⽗。
beforeUpdate & updated
-
beforeUpdate 和 updated 的钩⼦函数执⾏时机都应该是在数据更新的时候
-
beforeUpdate 的执⾏时机是在渲染 Watcher 的 before 函数中,在组件已经 mounted 之后,才会去调⽤这个钩⼦函数。
-
update 的执⾏时机是在 flushSchedulerQueue 函数调⽤的时候,只有 vm._watcher 的回调执⾏完毕后,才会执⾏ updated 钩⼦函数。
beforeDestroy & destroyed
-
beforeDestroy 和 destroyed 钩⼦函数的执⾏时机在组件销毁的阶段,最终会调⽤ $destroy ⽅法。
-
beforeDestroy 钩⼦函数的执⾏时机是在 destroy函数执⾏最开始的地⽅,接着执⾏了⼀系列的销毁动作,包括从parent的children 中删掉⾃⾝,删除 watcher ,当前渲染的 VNode 执⾏销毁钩⼦函数等,执⾏完毕后再调⽤ destroy 钩⼦函数。
**beforeDestroy → $destroy → destroy **
- 在 $destroy 的执⾏过程中,它⼜会执⾏ vm.patch(vm._vnode, null) 触发它⼦组件的销毁钩⼦函数,这样⼀层层的递归调⽤,所以 destroy 钩⼦函数执⾏顺序是先⼦后⽗,和 mounted 过程⼀样。
组件注册
全局注册
-
注册⼀个全局组件,可以使⽤ Vue.component ( tagName, options )
-
全局组件被挂载到 Vue.options.components 上
-
组件的创建都是通过 Vue.extend 继承⽽来,它会把 Vue.options 合并到 Sub.options ,也就是组件的 optinons 上, 然后在组件的实例化阶段,会执⾏ merge options 逻辑,把 Sub.options.components 合并到vm.options.components 上
先直接使⽤ id 拿,如果不存在,则把 id 变成驼峰的形式再拿,如果仍然不存在则在驼峰的基础上把⾸字⺟再变成⼤写的形式再拿,如果仍然拿不到则报错。这样说明了我们在使⽤ Vue.component(id, definition) 全局注册组件的时候,id 可以是连字符、驼峰或⾸字⺟⼤写的形式。
**id → 小驼峰 → 大驼峰 **
局部注册
- 局部注册是⾮常简单的。在组件的 Vue 的实例化阶段有⼀个合并option 的逻辑,之前我们也分析过,所以就把 components 合并到 vm.$options.components 上。
局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的⼦组件,⽽全局注册是扩展到 Vue.options 下,所以在所有组件创建的过程中,都会从全局的Vue.options.components 扩展到当前组件的 vm.$options.components 下,这就是全局注册的组件能被任意使⽤的原因。
异步组件
-
Vue 注册的组件不再是⼀个对象,⽽是⼀个⼯⼚函数,函数有两个参数 resolve 和 reject
-
异步组件有三种实现方式
-
普通异步组件
-
Promise 异步组件
-
高级异步组件
-
异步组件实现的本质是 2 次渲染,除了 0 delay 的⾼级异步组件第⼀次直接渲染成 loading 组件外,其它都是第⼀次渲染⽣成⼀个注释节点,当异步获取组件成功后,再通过 ** forceRender 强制重新渲染**,这样就能正确渲染出我们异步加载的组件了。
深⼊响应式原理
Vue 的数据驱动除了数据渲染DOM 之外,还有⼀个很重要的体现就是数据的变更会触发 DOM 的变化。
响应式对象
- Vue.js 实现响应式的核⼼是利⽤了 ES5 的 **** Object.defineProperty
这也是为什么 Vue.js 不能兼容 IE8 及以下浏览器的原因
Object.defineProperty
Object.defineProperty ⽅法会直接在⼀个对象上定义⼀个新属性,或者修改⼀个对象的现有属性, 并返回这个对象
Object.defineProperty(obj, prop, descriptor)
initState
-
Vue 的初始化阶段, _init ⽅法执⾏的时候,会执⾏ initState(vm) ⽅法
initState ⽅法主要是对 props 、 methods 、 data 、 computed 和 wathcer 等属性做了初始化操作 -
props 的初始化主要过程就是遍历定义的 props 配置。遍历的过程主要做两件事情:
-
⼀个是调⽤** defineReactive **⽅法把每个 prop 对应的值变成响应式,可以通过 vm._props.xxx 访问到定义 props 中对应的属性。
-
另⼀个是通过 proxy把 vm._props.xxx 的访问代理到 vm.xxx 上, 使得属性可以直接通过this访问
-
data 的初始化主要过程也是做两件事:
-
⼀个是对定义 data 函数返回对象的遍历,通过 proxy把每⼀个值 vm._data.xxx 都代理到 vm.xxx 上;
-
另⼀个是调⽤ observe ⽅法观测整个 data 的变化,把 data 也变成响应式,可以通过 vm._data.xxx 访问到定义 data 返回函数中对应的属性。
proxy
-
代理的作⽤是把 props 和 data 上的属性代理到 vm 实例上,这也就是为什么⽐如我们定义了如下 props,却可以通过 vm 实例访问到它。
-
proxy ⽅法的实现很简单,通过 Object.defineProperty 把 target[sourceKey][key] 的读写变成了对 target[key] 的读写。
observer
-
observe 的功能就是⽤来监测数据的变化,
-
observe ⽅法的作⽤就是给⾮ VNode 的对象类型数据添加⼀个 Observer ,如果已经添加过则直接返回,否则在满⾜⼀定条件下去实例化⼀个 Observer 对象实例。
Observer
-
Observer 是⼀个类,它的作⽤是给对象的属性添加** getter 和 setter**,⽤于**依赖收集和派发更新**
getter 做的事情是依赖收集,setter 做的事情是派发更新
-
对数据对象的访问会触发他们的 getter ⽅法
-
Observer 的构造函数:对于数组会调⽤ observeArray ⽅法,否则对纯对象调⽤** walk ** ⽅法
observeArray 是遍历数组再次调⽤ observe ⽅法,⽽walk ⽅法是遍历对象的 key 调⽤ defineReactive ⽅法
defineReactive
-
defineReactive 的功能就是定义⼀个响应式对象,给对象动态添加 getter 和 setter
-
defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对⼦对象递归调⽤ observe ⽅法,这样就保证了⽆论 obj 的结构多复杂,它的所有⼦属性也能变成响应式的对象
依赖收集
-
Dep 是整个 getter 依赖收集的核⼼
-
Dep 是⼀个 Class,它定义了⼀些属性和⽅法,这⾥需要特别注意的是它有⼀个静态属性 target ,这是⼀个全局唯⼀ Watcher ,这是⼀个⾮常巧妙的设计,因为在同⼀时间只能有⼀个全局的Watcher 被计算,另外它的⾃⾝属性 subs 也是 Watcher 的数组。
-
Dep 实际上就是对 Watcher 的⼀种管理, Dep 脱离 Watcher 单独存在是没有意义的
过程分析
-
对数据对象的访问会触发他们的 getter ⽅法
-
Vue 的 mount 过程是通过 mountComponent 函数,它会先执⾏ vm._render() ⽅法,因为之前分析过这个⽅法会⽣成 渲染 VNode,并且在这个过程中会对 vm 上的数据访问,这个时候就触发了数据对象的 getter
-
每个对象值的 getter 都持有⼀个 dep ,在触发 getter 的时候会调⽤ dep.depend() ⽅法,也就会执⾏ Dep.target.addDep(this) 。
-
vm._render() 过程中,会触发所有数据的 getter
-
Watcher 和 Dep 就是⼀个⾮常经典的观察者设计模式的实现
派发更新
-
当我们在组件中对响应的数据做了修改,就会**触发 setter **的逻辑,最后调⽤ dep.notify() ⽅法, 它是 Dep 的⼀个实例⽅法,通知依赖更新。
-
Vue 在做派发更新的时候的⼀个优化的点,它并不会每次数据改变都触发 watcher 的回调,⽽是把这些 watcher 先添加到⼀个队列⾥,然后在 nextTick 后执⾏ flushSchedulerQueue 。
-
组件的更新由⽗到⼦
-
⽤户的⾃定义 watcher 要优先于渲染 watcher 执⾏,因为⽤户⾃定义 watcher 是在渲染watcher 之前创建的。
-
如果⼀个组件在⽗组件的 watcher 执⾏期间被销毁,那么它对应的 watcher 执⾏都可以被跳过,所以⽗组件的 watcher 应该先执⾏。
修改派发更新的过程,实际上就是当数据发⽣变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher ,都触发它们的update 过程,这个过程⼜利⽤了队列做了进⼀步优化,在 nextTick 后执⾏所有 watcher 的run ,最后执⾏它们的回调函数。
nextTick
JS 运⾏机制
-
JS 执⾏是单线程的,它是基于事件循环的。事件循环⼤致分为以下⼏个步骤:
-
所有同步任务都在主线程上执⾏,形成⼀个执⾏栈
-
线程之外,还存在⼀个"任务队列"(task queue)。只要异步任务有了运⾏结果,就在"任务队列"之中放置⼀个事件。
-
⼀旦"执⾏栈"中的所有同步任务执⾏完毕,系统就会读取"任务队列",看看⾥⾯有哪些事件。那些对应的异步任务,于是结束等待状态,进⼊执⾏栈,开始执⾏。
-
主线程不断重复上⾯的第三步。
-
主线程的执⾏过程就是⼀个 tick ,⽽所有的异步结果都是通过 “任务队列” 来调度被调度。
-
规范中规定 task 分为两⼤类,分别是 macro task 和 micro task,并且
每个 macro task 结束后,都要清空所有的 micro task。 -
在浏览器环境中,常⻅的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;
-
常⻅的 micro task 有 MutationObsever 和 Promise.then。
Vue nextTick 的实现
- next-tick.js 申明了 microTimerFunc 和 macroTimerFunc 2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数。对于macro task 的实现,优先检测是否⽀持原⽣ setImmediate ,这是⼀个⾼版本 IE 和 Edge 才⽀持的特性,不⽀持的话再去检测是否⽀持原⽣的 MessageChannel ,如果也不⽀持的话就会降级为 setTimeout 0 ;⽽对于 micro task 的实现,则检测浏览器是否原⽣⽀持 Promise,不⽀持的话直接指向 macro task 的实现。
数据的变化到 DOM 的重新渲染是⼀个异步过程,发⽣在下⼀个 tick。这就是我们平时在开发的过程中,⽐如从服务端接⼝去获取数据的时候,数据做了修改,如果我们的某些⽅法去依赖了数据修改后的 DOM 变化,我们就必须在nextTick 后执⾏。
检测变化的注意事项
对象添加属性
-
对于使⽤ Object.defineProperty 实现响应式的对象,当我们去给这个对象添加⼀个新的属性的时候,是不能够触发它的 setter 的. → Vue.set
-
Vue 为了解决这个问题,定义了⼀个全局API Vue.set ⽅法
数组
- Vue 也是不能检测到以下变动的数组:
-
当你利⽤索引直接设置⼀个项时: →
** Vue.set( target , i ,item ) **
-
当你修改数组的⻓度时:
** vm.items.splice(newLength) **
arrayMethods ⾸先继承了 Array ,然后对数组中所有能改变数组⾃⾝的⽅法,如push、pop 等这些⽅法进⾏重写。
计算属性 VS 侦听属性
computed
-
计算属性的初始化是发⽣在 Vue 实例初始化阶段的 initState 函数中
-
利⽤ Object.defineProperty 给计算属性对应的 key 值添加 getter和setter,setter 通常是计算属性是⼀个对象,并且拥有 set ⽅法的时候才有,否则是⼀个空函数。
-
计算属性本质上就是⼀个 computed watcher
-
确保不仅仅是计算属性依赖的值发⽣变化,⽽是当计算属性最终计算的值发⽣变花才会触发渲染watcher 重新渲染
watch
-
侦听属性的初始化也是发⽣在 Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化
之后执行 initWatch -
initWatch 遍历watch对象,拿到每一个handler 执行createWatcher,如果handler是一个数组,则遍历这个数组,再执行createWatcher
-
侦听属性 watch 最终会调⽤ $watch ⽅法,接着执⾏ const watcher = new Watcher(vm,expOrFn, cb, options) 实例化了⼀个 watcher ,这是⼀个 user watcher
-
通过实例化 watcher 的⽅式,⼀旦我们 watch 的数据发送变化,它最终会执⾏ watcher 的run ⽅法,执⾏回调函数 cb
-
侦听属性也是基于 Watcher 实现的,这是⼀个 user watcher
Watcher options
deep watcher
-
设置 ** deep 为 true**后会执⾏traverse 函数
-
调用traverse,对⼀个对象做深层递归遍历,遍历过程中就是对⼀个⼦对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的watcher ,这个函数实现还有⼀个⼩的优化,遍历过程中会把⼦响应式对象通过它们的 dep id 记录到 seenObjects ,避免以后重复访问。
user watcher
- 通过 vm.$watch 创建的 watcher 属于 user watcher
computed watcher
- computed watcher ⼏乎就是为计算属性量⾝定制
sync watcher
-
当响应式数据发送变化后,触发了 watcher.update() ,只是把这个 watcher 推送到⼀个队列中,在 nextTick 后才会真正执⾏ watcher 的回调函数。⽽⼀旦我们设置了 sync ,就可以在当前 Tick 中同步执⾏ watcher 的回调函数。
-
只有当我们需要 watch 的值的变化到执⾏ watcher 的回调函数是⼀个同步过程的时候才会去设置该
属性为 true。
计算属性适合⽤在模板渲染中,某个值是依赖了其它的响应式对象甚⾄是计算属性计算⽽来;
——————————————————-
⽽侦听属性适⽤于观测某个值的变化去完成⼀段复杂的业务逻辑
组件更新
- 组件的更新还是调⽤了 vm._update 方法
新旧节点不同
- 新旧 vnode 不同,那么更新的逻辑⾮常简单,它本质上是要替换已存在的节点,大致分为3步
-
创建新节点
-
更新⽗的占位符节点
-
删除旧节点
新旧节点相同
-
新旧节点相同,它会调⽤ patchVNode ⽅法把新的 vnode patch 到旧的 vnode 上
-
vnode 是个⽂本节点且新旧⽂本不相同,则直接替换⽂本内容
-
如果不是⽂本节点,则判断它们的⼦节点,并分了⼏种情况处理:
-
oldCh 与 ch 都存在且不相时,使⽤ updateChildren 函数来更新⼦节点
-
如果只有 ch 存在,表⽰旧节点不需要了。如果旧的节点是⽂本节点则先将节点的⽂本清除,然后通过 addVnodes 将 ch 批量插⼊到新节点 elm 下。
-
如果只有 oldCh 存在,表⽰更新的是空节点,则需要将旧的节点通过 removeVnodes 全部清除。
-
当只有旧节点是⽂本节点的时候,则清除其节点⽂本内容。
-
执⾏ postpatch 钩⼦函数
执⾏完 patch 过程后,会执⾏ postpatch 钩⼦函数,它是组件⾃定义的钩⼦函数,有则执⾏。 -
当更新的 vnode 是⼀个组件 vnode 的时候执⾏ prepatch 钩⼦函数,拿到新的 vnode 的组件配置以及组件实例,去执⾏ updateChildComponent ⽅法
patchVNode → [ 是组件VNode ] → prepatch → updateChildComponent -
执⾏ update 钩⼦函数
patchVNode 函数,在执⾏完新的 vnode 的 prepatch 钩⼦函数,会执⾏所有 module 的update 钩⼦函数以及⽤户⾃定义 update 钩⼦函数 -
完成 patch 过程
updateChildren
编译
把模板编译成 render 函数
-
解析模板字符串⽣成 AST → parse
-
优化语法树 → optimize
-
⽣成代码 → codegen
parse
编译过程⾸先就是对模板做解析,⽣成 AST,它是⼀种抽象语法树,是对源代码的抽象语法结构的树状表现形式。
———————————————
这个过程是⽐较复杂的,它会⽤到⼤量正则表达式对字符串解析
-
parse 的⽬标是把 template 模板字符串转换成 AST 树,它是⼀种⽤ JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利⽤正则表达式顺序解析模板,当解析到开始标签、闭合标签、⽂本的时候都会分别执⾏对应的回调函数,来达到构造 AST 树的⽬的
-
AST 元素节点总共有 3 种类型, type 为 1 表⽰是普通元素,为 2 表⽰是表达式,为 3 表⽰是纯⽂
本。
optimize
- 为什么要有优化过程?
因为我们知道 Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是⾸次渲染后就永远不会变化的,那么这部分数据⽣成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的⽐对
标记静态节点
-
isStatic 是对⼀个 AST 元素节点是否是静态的判断,如果是表达式,就是⾮静态;如果是纯⽂本,就是静态;对于⼀个普通元素,如果有 pre 属性,那么它使⽤了 v-pre 指令,是静态
-
否则要同时满⾜以下条件:没有使⽤ v-if 、 v-for ,没有使⽤指令(不包括 v-once ),⾮内置组件,是平台保留的标签,⾮带有 v-for 的 template 标签的直接⼦节点,节点的所有属性的 key 都满⾜静态 key;这些都满⾜则这个 AST 节点是⼀个静态节点。
-
如果这个节点是⼀个普通元素,则遍历它的所有 children ,递归执⾏ markStatic 。因为所有的elseif 和 else 节点都不在 children 中, 如果节点的 ifConditions 不为空,则遍历ifConditions 拿到所有条件中的 block ,也就是它们对应的 AST 节点,递归执⾏markStatic 。在这些递归过程中,⼀旦⼦节点有不是 static 的情况,则它的⽗节点的 static 均变成 false。
标记静态根
optimize 的过程,就是深度遍历这个 AST 树,去检测它的每⼀颗⼦树是不是静态节点,如果是静态节点则它们⽣成 DOM 永远不需要改变,这对运⾏时对模板的更新起到极⼤的优化作⽤。
——————————————
通过 optimize 我们把整个 AST 树中的每⼀个 AST 元素节点标记了 static 和staticRoot
codegen
编译的最后⼀步就是把优化后的 AST 树转换成可执⾏的代码
- 编译后⽣成的代码就是在运⾏时执⾏的代码
扩展
event
-
经典的事件中⼼的实现,把所有的事件⽤ 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.on,并且当回调函数执⾏⼀次后再通过vm.off 移除事件的回调,这样就确保了回调函数只执⾏⼀次。
-
Vue ⽀持 2 种事件类型,原⽣ DOM 事件和⾃定义事件,它们主要的区别在于添加和删除事件的⽅式不⼀样,并且⾃定义事件的派发是往当前实例上派发,但是可以利⽤在⽗组件环境定义回调函数来实现⽗⼦组件的通讯。另外要注意⼀点,只有组件节点才可以添加⾃定义事件,并且添加原⽣ DOM 事件需要使⽤ native 修饰符;⽽普通元素使⽤
.native
修饰符是没有作⽤的,也只能添加原⽣ DOM 事件。
v-model
-
实现 v-model 的精髓,通过修改 AST 元素,给 el 添加⼀个 prop ,相当于我们在 input 上动态绑定了 value ,⼜给 el 添加了事件处理,相当于在 input 上绑定了input 事件
<input v-bind:value="message" v-on:input="message=$event.target.value" >
slot
-
当解析到标签上有 slot 属性的时候,会给对应的 AST 元素节点添加 slotTarget 属性
-
在普通插槽中,⽗组件应⽤到⼦组件插槽⾥的数据都是绑定到⽗组件的,因为它渲染成vnode 的时机的上下⽂是⽗组件的实例。⼦组件渲染的时候直接拿到这些渲染好的 vnodes
-
对于作⽤域插槽,⽗组件在编译和渲染阶段并不会直接⽣成 vnodes ,⽽是在⽗节点 vnode 的 data 中保留⼀个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染⼦组件阶段才会执⾏这个渲染函数⽣成vnodes ,由于是在⼦组件环境执⾏的,所以对应的数据作⽤域是⼦组件实例。
-
两种插槽的⽬的都是让⼦组件 slot 占位符⽣成的内容由⽗组件来决定,但数据的作⽤域会根据它们 vnodes 渲染时机不同⽽不同。
keep-alive
-
是一个内置组件
-
在 created 钩⼦⾥定义了 this.cache 和 this.keys ,本质上它就是去**缓存已
经创建过的 vnode ** -
props 定义了 include , exclude ,它们可以字符串或者表达式,;
-
include 表⽰只有匹配的组件会被缓存,
-
exclude 表⽰任何匹配的组件都不会被缓存,
-
props 还定义了 max ,它表⽰缓存的⼤⼩,因为我们是缓存的 vnode 对象,它也会持有DOM,当我们缓存很多的时候,会⽐较占⽤内存,所以该配置允许我们指定缓存⼤⼩。
组件渲染
-
首次渲染
⾸次渲染⽽⾔,除了在 中建⽴缓存,和普通组件渲染没什么区别。 -
缓存渲染
当我们从 B 组件再次点击 switch 切换到 A 组件,就会命中缓存渲染。
包裹的组件在有缓存的时候就不会在执⾏组件的 created 、 mounted 等钩⼦函数
生命周期
-
组件⼀旦被 缓存,那么再次渲染的时候就不会执⾏created 、 mounted 等钩⼦函数
-
Vue 提供了 activated 钩⼦函数
它的执⾏时机是 包裹的组件渲染的时候
transition
-
组件也是内置组件
-
组件,我们可以利⽤它配合⼀些 CSS3 样式很⽅便地实现过渡动画,也可以利⽤它配
合 JavaScript 的钩⼦函数实现过渡动画 -
下列情形中,可以给任何元素和组件添加 entering/leaving 过渡:
-
条件渲染 (使⽤ v-if )
-
条件展⽰ (使⽤ v-show )
-
动态组件
-
组件根节点
-
组件是只能包裹⼀个⼦节点的
-
过度实现的几个步骤
-
⾃动嗅探⽬标元素是否应⽤了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
-
如果过渡组件提供了 JavaScript 钩⼦函数,这些钩⼦函数将在恰当的时机被调⽤。
-
如果没有找到 JavaScript 钩⼦并且也没有检测到 CSS 过渡/动画,DOM 操作 (插⼊/删除) 在下⼀帧中⽴即执⾏。
真正执⾏动画的是我们写的 CSS 或者是 JavaScript 钩⼦函数,⽽ Vue 的 只是我
们很好地管理了这些 CSS 的添加/删除,以及钩⼦函数的执⾏时机。
transition-group
组件,很好地帮助我们实现了列表的过渡效果
-
不同于 组件, 组件⾮抽象组件,它会渲染成⼀个真实元素
-
当我们去修改列表的数据的时候,如果是添加或者删除数据,则会触发相应元素本⾝的过渡动画,这和 组件实现效果⼀样,
-
还实现了 move 的过渡效果,让我们的列表过渡动画更加丰富。
Vue Router
路由的作用就是根据不同的路径映射到不同的视图
-
Vue Router 支持 hash 、 history 、 abstract 3 种路由⽅式;
-
提供了 和 2 种组件
路由注册
Vue 提供了 Vue.use 的全局 API 来注册插件
路由安装
- Vue-Router 安装最重要的⼀步就是利⽤ Vue.mixin 去把 beforeCreate 和 destroyed 钩⼦函数注⼊到每⼀个组件中。
它的实现实际上⾮常简单,就是把要混⼊的对象通过 mergeOption 合并到 Vue 的 options 中,由于每个组件的构造函数都会在 extend 阶段合并 Vue.options 到⾃⾝的 options 中,所以也就相当于每个组件都定义了 mixin 定义的选项。
VueRouter 对象
-
每个组件在执⾏ beforeCreated 钩⼦函数的时候,都会执⾏ router.init ⽅法
-
组件的初始化阶段,执⾏到 beforeCreated 钩⼦函数的时候会执⾏ router.init ⽅法,然后⼜会执⾏ history.transitionTo ⽅法做路由过渡
matcher
-
在 Vue-Router 中,所有的 Route 最终都会通过 createRoute 函数创建,并且它最后是不可以被外部修改的。Route 对象中有⼀个⾮常重要属性是 matched ,它通过formatMatch(record) 计算⽽来
-
通过matcher 的 match ⽅法,我们会找到匹配的路径 Route
路径切换
- 当我们切换路由线路的时候,就会执⾏** history.transitionTo **⽅法
导航守卫
官⽅的说法叫导航守卫,实际上就是发⽣在路由路径切换的时候,执⾏的⼀系列钩⼦函数。
-
iterator 函数逻辑很简单,它就是去执⾏每⼀个 导航守卫 hook ,并传⼊ route 、 current 和匿名函数,这些参数对应⽂档中的 to 、 from 、 next ,当执⾏了匿名函数,会根据⼀些条件执⾏abort 或 next ,只有执⾏ next 的时候,才会前进到下⼀个导航守卫钩⼦函数中。
-
钩子函数的执行顺序
-
在失活的组件⾥调⽤离开守卫
-
调⽤全局的 beforeEach 守卫
-
在重⽤的组件⾥调⽤ beforeRouteUpdate 守卫
-
在激活的路由配置⾥调⽤ beforeEnter
-
解析异步路由组件
Vuex
Vuex 是⼀个专为 Vue.js 应⽤程序开发的状态管理模式。它采⽤集中式存储管理应⽤的所有组件的状态,并以相应的规则保证状态以⼀种可预测的⽅式发⽣变化。
Vuex 核⼼思想
-
Vuex 应⽤的核⼼就是 store(仓库)。“store”基本上就是⼀个容器,它包含着你的应⽤中⼤部分的状态
-
Vuex 和单纯的全局对象有以下两点不同?
-
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发⽣变化,那么相应的组件也会相应地得到⾼效更新。
-
不能直接改变 store 中的状态
-
action ⽐我们⾃⼰写⼀个函数执⾏异步操作然后提交 muataion 的好处是在于它可以在参数中获取到当前模块的⼀些⽅法和状态,Vuex 帮我们做好了这些。
-
在 Vuex 安装阶段,它会往每⼀个组件实例上混⼊ befeforeCreated 钩⼦函数,然后往组件实例上添加⼀个 $store 的实例,它指向的就是我们实例化的 store ,因此我们可以在组件中访问到 store 的任何属性和⽅法。
本文同步发布于个人 G 众号:【前端知识营地】点击下方链接关注我,获取更多优质有趣的内容!
🔴 点击关注前端知识营地
🔵 阅读原文