当前位置: 首页 > Web前端 > vue.js

vue源码学习(一)入门与响应式原理

时间:2023-04-01 01:48:52 vue.js

vue版本为2.6.11。博客有点大。如果觉得繁琐,可以跳过阅读。没有很多源代码卡在里面。我将git源码的链接贴出来,大家可以查看源码链接或者下载源码看vue的源码半天,一是感觉写的不够清晰,二是感觉vue3出来了。vue2的源码我写的有点晚,所以一直没有发表。后面觉得可以自己写点东西,也给大家提供一个阅读源码的思路,希望写一篇对自己和大家都有好处的帖子。首先,设定目标。我们不可能把vue的所有源码都看一遍而不漏一行(如果每一行都看一遍,很快就会失去方向和兴趣),所以我们需要知道我们看源码的目标是什么,以及到什么程度才能认为我们已经学会了vue真正的核心代码和思想。我认为,如果你对以下几点有了透彻的了解,你就真正了解了vue的精髓。vdomcompiler响应式原理(watch、computed、collectiondependencies、gettersetter)指令原理filter原理vue2和vue3的核心区别以上几点就是我们学习的目标。我们一定要先定好目标,否则就不知道我们学习源码的意义,学完就放弃了。我们必须学会以目标为导向。首先我们找到vue项目的入口,然后开始借鉴vue中最重要最核心的内容响应式原理找到入口入口文件。如果想知道怎么找词条,请看找词条的思路。如果觉得没必要看入口文件platforms/web/entry-runtime.js或者platforms/web/entry-runtime-with-compiler.js可以跳过,前者是withoutcompiler,后者是withcompiler,因为在我们的目标设定中有编译器的内容,所以我们选择后者作为我们源码学习的切入点,分析文件依赖,在core/instance/index.js文件函数Vue中找到VUe构造函数文件(options){if(process.env.NODE_ENV!=='production'&&!(thisinstanceofVue)){warn('Vue是一个构造函数,应该使用`new`关键字调用')}this._init(options)}initMixin(Vue)//添加_init方法stateMixin(Vue)//$set$delete$watch$data$propseventsMixin(Vue)//$on$once$off$emitlifecycleMixin(Vue)//_update$forceUpdate$destroyrenderMixin(Vue)//$nextTick_render文件的主要作用是定义Vue并丰富其原型上的方法(我在上面的注释中标记了每个方法对应的thoseprototypemethodsadded),看一下vue的构造函数,发现只调用了_init方法,整个Vue入口就是_init方法。_init方法我们分析一下_init方法的内容。Vue.prototype._init=function(options?:Object){...如果(选项&&options._isComponent){initInternalComponent(vm,options)}else{vm.$options=mergeOptions(resolveConstructorOptions(vm.constructor),options||{},vm)}initLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm,'beforeCreate')initInjections(vm)//在数据/props之前解决注入initState(vm)initProvide(vm)//在数据/props之后解决提供callHook(vm,'created')if(vm.$options.el){vm.$mount(vm.$options.el)}...}我在这里省略了部分源代码。您可以检查您下载的源代码。这个_init方法做了很多,我们不可能在这里说清楚一次,需要知道我们真正想看到的是什么。找到这个文件的目的是为了帮助我们更好的实现我们的目标(当然也不是说不用看,只是不要太深入,因为每个方法都有特别深的调用chain,再看肯定会迷失方向。)从语义上看,我们要看的响应式原理应该从initState方法开始,深入学习(vue的命名规范很好,通过命名很容易看出每个方法的作用,也是值得学习的地方)找到入口的方法就是如何找到入口。其实写框架和我们平时写代码是一样的。想一想,如果我们接触到一个陌生的项目,我们应该如何找到项目的入口呢?我们首先要看package.json,然后再分析。脚本,根据经验和语义判断(也可以看vue的开发者文档找一些介绍,我觉得如果不想花太多时间找词条的话看也不是那么严谨),vue的打包命令应该是"build":"nodescripts/build.js",我们现在分析这个文件。你不需要太仔细地查看这个文件。如果你真的想学习rollup打包,可以深入了解一下。我们做项目的目的是为了找到项目真正的入口,在这里深入研究只会让我们误入歧途。文件脚本/build.jsletbuilds=require('./config').getAllBuilds()文件脚本/config.jsconstbuilds={...'web-runtime-esm':{entry:resolve('web/entry-runtime.js'),dest:resolve('dist/vue.runtime.esm.js'),...},'web-full-dev':{条目:resolve('web/entry-runtime-with-compiler.js'),dest:resolve('dist/vue.js'),},...}我上面粘贴的代码就是我们要找的入口文件。我如何找到这两个?通过查看package.json中的main、module、unpkg,这三个配置就是我们使用vue库时cjs、esm、cdn的导入文件,它们对应的入口就是入口文件。响应式原理首先我们看一下vue的官方文档。responsiveprinciple介绍的很详细(一定要看,一定要看,一定要记在心里,深入理解,面试的时候问),这里我就不细说了.官方文档首先用一句话解释了响应性原则。当组件的数据发生变化时,会触发组件的重新渲染。响应原则的核心要点是什么?就是我们细分的targetwatcherobservergetter,setter依赖集合(dependencycollect)dependency和watcher之间的关系我想如果你理解了以上几点,你就会真正理解响应式的原理了。下面我们从源码入手,分析一下响应式的原理。本来打算从_init中调用的_initState开始深入分析的,但实际上,看完_initState方法后,你会发现watcher观察者和dep类有很多依赖,所以我们先把这几个放在后面一个类就完全清楚了,看_initState方法,对我们阅读有帮助。阅读源码不一定非得按照调用栈的层层代码学习。当你发现这部分代码强依赖了另一部分代码时,我们可以先了解他所依赖的部分,这样更方便我们理解。我们先来思考下Vue实现响应式原理的逻辑,再来看这三个类。如果我们忘记了核心思想,去看代码,我们会很迷茫,就像没有需求写代码,自己写代码的过程中,首先要思考这个类应该实现什么功能,然后开始写这个班。回过头来看思路,大致的流程就是为数据设置getters和setters,getters时收集依赖,setters时调用依赖的回调。源码一般比较大,调用栈比较深。遇到不懂的问题,复习一下基本原则和目标,就不会迷失方向。大致关系如果不想看代码细节,可以跳过下面的代码细节。这里总结一下Observerwatcher和dep的主要功能以及它们之间的关系。observer的主要功能是将传入的数据转换为Observer数据,就是设置新的get和set方法。dep对象主要是data和watcher之间的桥梁。它存储了数据更新时需要调用的watcher,当数据更新时触发所有的watcher。updatewatcher对象监听数据变化并调用回调和dep的关系是dep触发watcher的更新,一个watcher可以有多个depObserver文件:core/observer/index.js我们大致浏览一下这个内容,以及export一个Observer类,定义Reactive并observe这个方法,其他的方法先不要看,等用到再看。如果传入对象不是对象或Vnode,则observe方法返回。如果传入对象包含__ob__,则ob=value.__ob__否则,如果shouldObserve==true加上一些其他检查通过,则ob=newObserver(value)ifasRootData&&obthenvmCount++如果是组件,则记录vmCount与数据对象。一般来说,这个方法就是newObserver。接下来我们看Observer类Observer类的构造函数constructor(value:any)this。value的值为传入的值this.dep=newDep()this.vmCount=0记录依赖组件的个数将此对象添加到value对象的__ob__属性中如果value是数组调用observeArray否则调用walkwalk(obj:Object)方法循环对象的key,调用defineReactive重置属性get和setobserveArray(items:Array)方法循环数组对象,调用observe(item[i]),设置每个对象数组中要响应的数据defineReactive方法的主要参数:obj对象,key修饰的字段主要功能:1.获取当前传入key对应值的Observer实例2.为key属性设置get和set传入的对象。我们知道Vue的响应是在get的时候收集依赖,setting的时候调用回调,我们看看这里是如何收集和调用回调的?constdep=newDep()...letchildOb=!shallow&&observe(val)...get:functionreactiveGetter(){constvalue=getter?getter.call(obj):valif(Dep.target){dep.依赖()如果(childOb){childOb。部门depend()if(Array.isArray(value)){dependArray(value)}}}返回值},set:functionreactiveSetter(newVal){constvalue=getter?getter.call(obj):val/*eslint-disableno-self-compare*/if(newVal===value||(newVal!==newVal&&value!==value)){return}/*eslint-复制代码启用无自我比较*/if(process.env.NODE_ENV!=='production'&&customSetter){customSetter()}//#7981:对于没有setter的访问器属性if(getter&&!setter)returnif(setter){setter.call(obj,newVal)}else{val=newVal}childOb=!shallow&&observe(newVal)dep.notify()}get方法调用dep的depend方法收集依赖set方法调用dep的notify发送通知变化总结:Observer的任务是将数据转化为response类型的数据,添加__ob__和getset方法收集依赖并通知更新dep依赖文件core/observer/dep.js属性介绍1.id从0开始计数,每次添加一个新的实例1.subs:指所有订阅了dep的watcher对象。方法介绍addSub添加订阅者removeSub删除指定订阅者depend集合依赖notify通知更新,遍历所有订阅者,调用订阅者(watcher)的update方法。当前系统中watcher对象的静态变量target一次只能有一个。通过调用pushTarget和popTarget这两个方法来设置Watcher文件core/observer/watcher.js的构造函数逻辑,如果是RenderWatcher,则赋值vm._watcher=this(渲染时会用到,可以忽略))并将当前实例放入vm._watchers对象中。vm._watchers包含了当前vm的所有watcher对象,并将options的值赋给这个对象。需要特别注意的几个属性在deeplazy为真时不调用get方法。initComputed时,lazy为truesync,即同步更新。如果sync为false,watcher对象将被放入一个队列中等待执行。将cb分配给this.cb。cb是观察者对象的回调。每次更新数据都会调用添加watcher对象的id属性,从0开始计数,每new一个instance+1给当前watcher对象的state添加active属性。如果active为false,则当前观察者对象将不再有效。添加脏属性。lazy为true时dirty也为true,lazy为false时不会直接调用get方法,dirty用于标识是否调用了get,调用后变为false。将deps属性添加到观察者当前订阅的所有dep对象。为当前watcher订阅的所有dep对象重新计算后添加newDeps属性(主要用于新旧依赖对比,清除dep中的Uselesssubs)为watcher当前订阅的所有dep对象的idnewDepIds属性添加depIds属性到。重新计算后,使用当前watcher订阅的所有dep对象的idexpression进行异常处理。增加一个getter属性,由expOrFn函数转换而来,如果expOrFn的值为函数则getter是方法,否则调用parsePath方法将表达式转换为方法。此时getter方法的返回值是给监控数据加上value属性(79~91行)。value属性是watcher对象监听的数据。当lazy为true时(computed配置中watcher对象的lazy为true)为undefined,当lazy为false时,调用get方法收集依赖,获取valueget方法的逻辑。调用dep中的pushTarget方法,将dep中的Dep.target设置为当前的watcher对象,也就是说后续所有的依赖收集都收集在当前watcher对象上,调用this.getter方法实现依赖的收集。在this.getter方法执行过程中,所有被观察者观察到的对象的依赖都会被收集到当前观察者对象中。在对象上,finally方法中,如果deep为true,则调用遍历方法递归最终值,实现所有子属性的依赖收集,最后调用dep中的popTarget方法,关闭收集,调用cleanupDeps方法addDep方法逻辑是给当前watcher对象添加newDepIds,判断当前watcher中的newDepIds对象是否包含dep的id,如果不存在则在当前watcher对象的newDepIds和newDeps中添加新的依赖,以及将当前watcher对象添加到dep对象的subs属性中,如果this.depIds中没有depid,则将当前watcher对象放入dep对象的subs中。cleanupDeps方法逻辑get方法调用deps(旧依赖)进行循环,如果newDepIds不包含循环项,说明当前watcher对象不依赖旧dep对象,调用dep.removeSub(this)清空depsubs中的currentwatcher,updatedepIdstonewDepIds,clearnewDepIds,updatedepstonewDeps,update方法中clearnewDeps逻辑dep的notify方法中的调用,当监听到的数据发生变化时更新,如果lazy==true,则this.dirty=true不更新??,如果sync为true则同步更新,否则调用queueWatcher(this)方法放入队列执行run方法。run方法主要是重新计算值,执行回调evaluate方法。该方法只有在lazy为true时,调用计算value的值,并将dirty设置为false。eardown方法清除watcher对象的依赖,清除dep上subs的watcher对象,并设置this.active为false。再来看initState方法入口方法_init中调用的initState方法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方法定义_watchers变量,调用initProps、initMethods、initData、initComputed,initWatcher分别为props,methods,data,computed,watcher初始化initPropsdefinevm._props遍历propsOptions调用defineReactive将propsData中的数据转换为响应式数据,然后然后调用proxy(vm,'_props',key)将propsData中的数据通过proxy挂到vm中。initData为vm._data赋值。如果options.data是方法,调用getData(data,vm)否则直接返回options.data,getData方法主要是执行options.data方法循环数据的key,调用proxy(vm,"_data",key)方法,将_data上的属性通过proxy挂到vm中调用observe(data,true);该方法将数据对象转换为观察后的对象。initComputed定义了vm._computedWatchers循环选项。computed将每个新方法放入computed并将一个Watcher对象放入vm._computedWatchers。需要注意的是,这里的watcher对象没有回调,设置了options的{lazy:true},也就是不会立即调用computed方法。调用defineComputed方法将compute上的每个key都挂在vm上作为一个变量,变量的get方法是通过调用createComputedGetter,这个方法返回的一个方法createComputedGetter方法:根据调用_computedWatchers[key]的get方法传入key,最后返回watcher的值initWatch循环options.watcher并调用createWatcher,createWatcher再次调用vm.$watch,这个方法new了一个Watcher对象initMethods,内容比较简单。主要是把options.methods中的方法挂载到vm对象上,不要把这个指向vm和Vue3的区别。vue3的源码我没看过,看完再补充。