本文最后更新于:8 个月前
Vue探究 一、Vue3新特性 1、源码优化
基于TypeScript
编写,提供了更好的类型检查
仓库使用了monorepo
模式(多包单仓库),将不同的模块拆分到packages
目录下不同的子目录中。好处是:更易阅读、理解源码模块;一些库(如reactivity
响应式库)可以独立于vue
单独使用
移除了一些不常用的API
,如:
2、编译优化
引入tree-shaking
,将无用模块删除,仅打包需要的模块,减小了打包体积(得益于ES6的import
、export
语法,可以在静态编译阶段获取到模块的依赖关系,删去无用模块)
diff算法优化,增加了静态标记,只标记动态节点,diff比较的时候只比较动态节点
静态提升,对不参与更新的元素做静态提升,保存只渲染一次,后面直接复用,避免重复创建节点
事件监听缓存
SSR优化
3、性能优化
数据劫持优化,使用Proxy
代替Object.defineProperty
,后者具有多个缺陷:不能检测对象属性的添加和删除、不能监听数组对象、对象层级太深时占用大量内存(Vue3的优化方式是在getter
中递归响应式,真正访问到的内部对象才做响应式,而不是所有内部对象都做)
4、语法API优化
Composition(组合式)API,克服了 Options(选项式)API 碎片化的缺点(同一个功能逻辑散乱分布在多个选项中,不利于阅读和维护),优化了逻辑组织。
优化了逻辑复用
二、Tree Shaking 是什么
Tree shaking
是一种通过清除多余代码方式来优化项目打包体积的技术。简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码
在Vue2
中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue
实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到
1 2 3 import Vue from 'vue' Vue .nextTick (() => {})
而Vue3
源码引入tree shaking
特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中
1 2 3 import { nextTick, observable } from 'vue' nextTick (() => {})
怎么做
Tree shaking
是基于ES6
模板语法(import
与exports
),主要是借助ES6
模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量
Tree shaking
无非就是做了两件事:
编译阶段利用ES6 Module
判断哪些模块已经加载
判断那些模块和变量未被使用或者引用,进而删除对应代码
三、响应式原理 整体思路是数据劫持+观察者模式。什么是“观察者模式”?
1、观察者模式 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。先看一段代码:
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 class Observerd { constructor (name ) { this .name = name this .state = '走路' this .observers = [] } setObserver (observer ) { this .observers .push (observer) } setState (state ) { this .state = state this .observers .forEach (observer => observer.update (this )) } }class Observer { constructor ( ) { } update (observerd ) { console .log (observerd.name + '正在' + observerd.state ) } }var client1 = new Observer ()var client2 = new Observer ()var server = new Observerd ('小明' ) server.setObserver (client1) server.setObserver (client2)
【优点】 :降低耦合、目标和观察者之间建立了一套触发机制
【缺点】 :目标和观察者之间如果有循环依赖,会循环调用导致程序崩溃;当观察者很多时,通知发布会占用很多时间,降低程序效率
参考文章:一文彻底搞懂观察者模式(Observer Pattern) - SegmentFault 思否
2、发布订阅模式(拓展) Publisher-Subscriber-Mode
Vue 中的 $on
和 $emit
方法。使用的就是发布-订阅模式。Node.js EventEmitter 中的 on 和 emit 方法也是发布-订阅者模式。那么什么是发布-订阅者模式?
先看一段简单代码:
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 43 44 45 46 47 48 49 class EventEmitter { constructor ( ){ this .events = {} } on (event,fn ){ (this .events [event] || this .events [event] = []).push (fn) } once (event,fn ){ let that = this function once_fn ( ){ that.off (event,fn) fn.apply (that,arguments ) } once_fn.fn = fn that.on (event,once_fn) } off (event,fn ){ var fns = this .events [event] if (!fns){ return } if (!fn){ fns = null }else { var len = fns.length while (len--){ if (fns[len]===fn || fns[len].fn ===fn){ fns.splice (len,1 ) break } } } } emit (event ){ if (this .events [event]){ this .events [event].forEach (cb =>cb ()) } } }var EE = new EventEmitter EE .on ('click' ,()=> {console .log ('订阅1,onClick' )})EE .once ('click' ,()=> {console .log ('订阅2,onceClick' )})EE .on ('hover' ,()=> {console .log ('订阅3,onHover' )})
【定义】
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。
订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
【优缺点】
优点:解耦合
缺点:消耗额外的内存
【观察者模式 vs 发布订阅模式】
在观察者模式中,观察者是知道 Subject 的,Subject 一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
观察者模式大多数时候是同步的,比如当事件触发,Subject 就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。
3、Vue2响应式模拟 Vue2响应式使用的是Object.defineProperty,给对象的每个key值设置get和set方法
模拟监听对象原理 ,还有set的原理没有探究,即对为新增加的对象属性设置监听的原理
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 43 44 45 46 47 48 const DEEP = true function updateView ( ) { console .log ('视图更新' ) }function defineReactive (target, key, value ) { if (DEEP ) { observe (value) } Object .defineProperty (target, key, { get ( ) { return value }, set (newVal ) { if (value !== newVal) { value = newVal updateView () } } }) }function observe (target ) { if (typeof target !== 'object' || target === null ) { return target } for (let key in target) { defineReactive (target, key, target[key]) } }const data = { id : 001 , name : 'Q' , info : { tel : '123456' , email : '123@qq.com' } }observe (data)
模拟监听数组 ,将为对象属性设置监听的方式运用到数组上是不实际的,因为如果数组有非常多的元素,那么为每一个元素单独设置一个get和set将让程序变得非常臃肿,降低程序性能。所以对数组的监听采取改写数组原型的方式。
具体操作:1、创建一个新对象,继承数组原型(让新对象的原型指向数组原型);2、修改新对象的原型方法(在原来的原型基础上为指定方法增加响应触发机制);3、让需要被监听的数组的原型指向新对象
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 43 44 45 46 47 48 function updateView ( ) { console .log ('视图更新' ) }const arrProperty = Object .create (Array .prototype ) const methods = ['push' ,'pop' ,'unshift' ,'shift' ,'splice' ] methods.forEach (method => { arrProperty[method] = function ( ){ updateView () Array .prototype [method].call (this ,...arguments ) } })function observe (target ){ if (typeof target !== 'object' || target === null ) { return target } if (Array .isArray (target)){ target.__proto__ = arrProperty return } for (let key in target){ defineProperty (target,key,target[key]) } }function defineProperty (target,key,value ){ observe (value) Object .defineProperty (target,key,{ get ( ){ return value }, set (newVal ){ value = newVal updateView () } }) }const data = { Cars :['BMW' ,'Audi' ] }observe (data)
4、Vue3响应式模拟 Vue3响应式弃用了Object.defineProperty,改用Proxy,改善之处:
在于可以直接监听对象的所有key,而不用一个个去给每个key设置
等到触发了对象之后,才进行监听,优化性能(Object.defineProperty在开始时就递归完成所有getter、setter的绑定)
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 function updateView ( ) { console .log ('视图更新' ) }function observe_proxy (data ){ const res = new Proxy (data,{ get (target,key,receiver ){ if (typeof target[key]==='object' ){ return observe_proxy (target[key]) } return Reflect .get (target,key,receiver) }, set (target,key,value,receiver ){ updateView () return Reflect .set (target,key,value,receiver) } }) return res }const data = { id : 001 , name : 'Q' , info : { tel : '123456' , email : '123@qq.com' }, cars :['BMW' ,'Audi' ] }const dataProxy = observe_proxy (data)
对于原对象data的所有操作需要通过Proxy代理dataProxy进行,才能够得到响应式的效果。
【依赖收集】 ,通过Proxy API对数据变化进行特定响应只是响应式系统的一部分,另外一个重要的步骤是依赖收集 ,即收集数据变化之后要对哪些地方进行通知。实现依赖收集的方式是绑定副作用函数,具体流程就是,在get中设置key值对应的请求来源,调用set的时候去通知key值对应的所有请求来源。
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 const fnList = new WeakMap ()let activeEffect = null function track (target, key ) { if (!activeEffect) return let dataMap = fnList.get (target) if (!dataMap) { dataMap = new Map () fnList.set (target, dataMap) } let fnSet = dataMap.get (key) if (!fnSet) { fnSet = new Set () dataMap.set (key, fnSet) } fnSet.add (activeEffect) }function trigger (target, key ) { const dataMap = fnList.get (target) if (!dataMap) return const effectFns = dataMap.get (key) effectFns && effectFns.forEach (fn => fn ()) }function observe_proxy (data ) { const res = new Proxy (data, { get (target, key, receiver ) { if (typeof target[key] === 'object' ) { return observe_proxy (target[key]) } const res = Reflect .get (target, key, receiver) track (data, key) return res }, set (target, key, value, receiver ) { let res = Reflect .set (target, key, value, receiver) trigger (data, key) return res } }) return res }function effect (effectFn ) { activeEffect = effectFn effectFn () activeEffect = null }const data = { id : 001 , name :'Q' , info : { tel : '123456' , email : '123@qq.com' }, cars : ['BMW' , 'Audi' ] }const dataProxy = observe_proxy (data)const fn1 = ( ) => { console .log ('data.id' , dataProxy.id ) }
以上代码简单模拟了依赖收集 的方式,实际场景中远比这复杂,需要考虑嵌套绑定的情况、需要考虑多个key值对应同一个依赖的情况。为了节省时间、加快进度,这些情况留作后续探究
四、Vue生命周期
可以把Vue的生命周期分为3大阶段
1、创建阶段 包括初始化事件和生命周期,初始化data、methods等属性,渲染模板
从new Vue()
到beforeMount
前,即new Vue()
的过程中依次发生了什么(具体分析阅读源码【Git/vue2源码解读】,已添加解析注释)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ...initLifecycle (vm) initEvents (vm) initRender (vm) callHook (vm, 'beforeCreate' , undefined , false ) initInjections (vm) initState (vm) initProvide (vm) callHook (vm, 'created' ) ...if (vm.$options .el ) { vm.$mount(vm.$options .el ) } ...
首先找到Vue的构造函数,/src/core/instance/index.ts
根据上一个文件,找到 /src/core/instance/init.ts,从这里可以获得的信息
beforeCreate时,还未开始数据初始化,无法访问data等属性
在beforeCreate -> created期间,依次完成 inject依赖注入、数据(props、data等)初始化、provide注入
为了探究数据初始化的顺序,找到 /src/core/instance/state.ts,从这里可以获得的信息
数据初始化的顺序依次为:props -> [ setup (组合式API)-> ] methods -> data -> computed -> watch
至此,触发了created钩子,下面为created之后的过程
挂载实例,$mount方法 是在 /src/platforms/web/runtime-with-compiler.ts 重写的
在该文件中,扩展$mount的逻辑,调用compileToFunctions方法,
1).在compileToFunctions方法中,调用parse方法,将将temmplate解析ast tree,
2).在compileToFunctions方法中,调用generate方法,将上一步获得的ast转换成render函数
然后调用原来的$mount方法,转到 src\platforms\web\runtime\index.ts,调用 mountComponent 方法渲染组件
mountComponent 方法在 src\core\instance\lifecycle.ts ,beforeMount钩子是在这里抛出的
参考文章:面试官:Vue实例挂载的过程 | web前端面试 - 面试官系列 (vue3js.cn)
2、运行阶段 从mounted
到beforeDestory
前,包括beforeUpdate
和updated
3、销毁阶段 从beforeDestory
到destoryed
五、双向绑定 什么是双向绑定
把 Model 层绑定到 View 层,数据驱动视图
把 View 层绑定到 Model 层,视图驱动数据
双向绑定的原理 Model -> View ,利用Observer观察者模式
数据监听,Vue2用Object.defineProperty 劫持数据对象,Vue3用Proxy代理监听数据对象
依赖收集,观察者模式,在getter中收集依赖它的对象,在setter中通知所有依赖它的对象
View -> Model ,监听视图事件,修改Model值
在处理v-model指令时,为当前dom节点添加input(或change)事件,修改Model值