概述最近在管理一个基于Vue项目的SentryIssue时,发现大量Issue是由Vue内部逻辑引起的,为了更好的解决问题,所以也复习了一下Vue2的原理。相比于Vue3更加清晰的项目结构和实现,Vue2中各个部分的实现耦合度更高,这也使得其逻辑更加复杂。其中,“响应式”部分是最复杂和重要的部分,实际项目中的大部分问题也都与它有关,正如Vue2官网所说:“Vue最独特的特性之一是它的非侵入式响应系统。数据模型只是普通的JavaScript对象。当您修改它们时,视图会更新。这使得状态管理非常简单,但了解它的工作原理同样重要,这样您就可以避免一些常见的问题”。在系统梳理“响应式”工作原理的过程中,我也参考了很多已有的文章,其中大部分都是与“依赖收集”、“派发更新”或“Watcher”、“Dep”相关的。当然,这些概念和逻辑对于要描述的内容来说是必不可少的,但是如果你只是简单地围绕这些内容写一篇文章,可能有助于理解“响应式”在整个Vue中的工作过程。因此,本文将换个角度,从Vue的使用过程来讲解“响应式”的工作原理,即从“实例化”、“渲染”、“数据”三行来描述“响应式”的工作过程更新”。对应的是如何定义响应式数据,如何触发响应式逻辑执行,如何触发响应式数据更新。介绍完“响应式”的工作原理后,会根据工作原理解决一些数据更新相关的常见问题。从实例化到渲染importVuefrom'vue'importAppfrom'./App.vue'Vue.config.productionTip=falsenewVue({render:h=>h(App),}).$mount('#app')以上是一段大家应该很熟悉的代码,也就是VueCli创建的示例工程中实例化Vue的代码。虽然是实例化代码,但是这里其实做了两件事:newVue,也就是创建了一个Vue实例。调用实例的$mount方法,即将Vue的渲染结果挂载到#app节点。这是Vue中两条重要的工作线。接下来我们看看这两个操作在Vue内部做了什么。当然,我们会重点关注“响应式”相关的部分。实例化过程//精简非生产逻辑函数Vue(options){this._init(options)}initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue)exportdefaultVue先定义后定义Vue构造函数,_init方法将在构造函数中调用。定义构造函数后,会调用initMixin、stateMixin等方法。其中构造函数中的_init方法会在initMixin中定义,所以首先要注意initMixin。//精简非生产逻辑导出函数initMixin(Vue:Class){Vue.prototype._init=function(options?:Object){constvm:Component=thisvm._uid=uid++vm._isVue=truevm._self=vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm,'beforeCreate')initInjections(vm)//解决数据/props前的注入initState(vm)initProvide(vm)//解决数据后提供/propscallHook(vm,'created')if(vm.$options.el){vm.$mount(vm.$options.el)}}}initMixin方法会扩展很多方法到Vue的原型上,最初的它是_init方法,包括生命周期、渲染函数(模板构造为render函数,render函数负责输出虚拟节点)、data/props、调用createdhook等响应式封装数据的逻辑也从这里开始。exportfunctioninitState(vm:Component){vm._watchers=[]constopts=vm.$optionsif(opts.props)initProps(vm,opts.props)if(opts.methods)initMethods(vm,opts.methods)复制代码if(opts.data){initData(vm)}else{observe(vm._data={},true/*asRootData*/)}if(opts.computed)initComputed(vm,opts.computed)if(opts.watch&&opts.watch!==nativeWatch){initWatch(vm,opts.watch)}}initState是核心负责处理数据,props,methods,computed,watch这些常见的Vue选项也在这里处理,主要的处理内容包括做一些检查,比如名字冲突,比如比较常见的warning:“Methodxxxhasalreadybeendefinedasaprop.”,就是这个阶段做的检查,最重要的是对的进行响应式封装数据,接下来以最常用和最直观的数据为例。//精简非生产逻辑functioninitData(vm:Component){letdata=vm.$options.datadata=vm._data=typeofdata==='function'?getData(数据,虚拟机):数据||{}constkeys=Object.keys(data)constprops=vm.$options.props让i=keys.lengthwhile(i--){constkey=keys[i]if(props&&hasOwn(props,key)){}elseif(!isReserved(key)){proxy(vm,`_data`,key)}}observe(data,true/*asRootData*/)}上图是initData的主要逻辑,main函数就是要检查内容的格式,比如必须是一个isPlainObject(至于这是什么,后面会详细说明),还有上面说的检查名称,防止冲突。比如有一个datakey和props冲突,就会报出大家应该不陌生的warning:“datapropertyxxxisalreadydeclaredasaprop.Usepropdefaultvalueinstead.”最后才是真正的响应式逻辑观察方法。至此,实例化的主线已经梳理完毕。可以看到newVue之后Vue的处理步骤,数据等options是如何去响应数据处理的。调用$mount挂载实例。在Vue例子中,实例化后,会调用$mount将渲染后的DOM挂载到页面上。$mount实际上是触发渲染的入口点。//精简的非生产逻辑导出函数render=createEmptyVNode}callHook(vm,'beforeMount')letupdateComponentupdateComponent=()=>{vm._update(vm._render(),hydrating)}newWatcher(vm,updateComponent,noop,{before(){if(vm._isMounted&&!vm._isDestroyed){callHook(vm,'beforeUpdate')}}},true/*isRenderWatcher*/)hydrating=falseif(vm.$vnode==null){vm._isMounted=truecallHook(vm,'mounted')}returnvm}$mount首先会调用渲染的核心逻辑mountComponent方法,依次做以下事情:判断是否有传入的render方法,render方法是converttheVuetemplateVNode的方法,在Vue内部,如果newVue中有render,会优先使用。上面newVue的例子,传入了render方法,这也是大家熟悉的传入App.vue的逻辑。如果没有传入render,render会被分配一个方法来创建一个空的VNode节点。调用beforeMount的钩子。定义updateComponent方法,负责渲染和更新实例。内部会调用Vue实例的_update,_update会传入render的调用结果,也就是计算出来的VNode。_update方法中最重要的是调用了patch,即把VNode转换成真实DOM的方法。转换过程与“响应性”关系不大,这里不在patch上过多展开。创建一个Watcher实例,传入当前Vue实例vm,updateComponent,以及一些选项,比如before参数。调用挂载的钩子。梳理完$mount流程,就可以梳理出一条清晰的Vue实例渲染主线,调用newVue实例化Vue,然后检查并“响应式”封装数据、props等选项,然后调用$mount开始渲染,首先创建一个与Vue实例相关联的Watcher对象,并传入updateComponent方法维护实例的渲染和更新,render为updateComponent,负责将模板转换为虚拟节点VNode,patch方法后期转换将VNode转化为真正的DOM,最后挂载到页面上。在这个过程中,在实例化时定义响应数据,在渲染时调用响应数据的更新逻辑,最终实现整个更新逻辑。订阅者模式在上面的整个更新逻辑中,核心的“响应式”逻辑应用了订阅者模式的设计模式。在讲解Vue如何基于订阅者模式实现“响应式”之前,我先介绍一下。订户模式。什么是订阅者模式?“一个目标对象管理所有依赖的观察者对象,并在自身状态发生变化时主动发送通知。”这是订阅者的简单描述。在JavaScript中,订阅者模式是最常用的模式之一。比如经常使用的DOM事件监听也是订阅者模式,如:document.body.addEventListener('click',()=>{console.log('clicked1');});document.body.addEventListener('click',()=>{console.log('clicked2');});body作为观察目标,订阅点击事件后,点击body时会向订阅者发送通知,订阅者依次输出clicked1和clicked2,完成一个订阅-通知-响应的过程。订阅者模式的基本实现根据上面的例子,可以总结出订阅者模式的基本特点:一个观察目标对象通常有一个观察者管理类,包括添加、删除、通知观察者更新三个主要操作.一个或多个观察者接收并处理来自被观察对象的通知。也就是说,观察目标类、观察者管理类、观察者是订阅者模式中的三个基本元素。基于以上特点,这里给出一个订阅者模式实现的简单例子,其中使用观察者集合类ObserverList作为管理观察者的工具类,观察者目标类Subject调用ObserverList进行实际的观察者(Observer)管理,并在需要时向观察者发送更新通知。示例中的更新通知是更新随机数。观察者接受通知并输出最新的随机数。至此,Vue实例化和渲染的基本逻辑已经梳理完毕,下一篇文章将详细介绍Vue“响应式”的具体实现。