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

vue2源码分析(重构版)(一)

时间:2023-03-31 22:20:14 vue.js

vue设计模式之前写过三篇vue分析,一篇按照源码的顺序,看起来有点乱,在这里看了很多资料,自己编译厌恶。首先说下源码结构,源码都在src下src├──compiler#编译相关├──core#核心代码├──platforms#支持不同平台├──server#服务器渲染├──证监会#。Vue文件解析├──shared#共享代码编译器编译器目录包含了Vue.js所有编译相关的代码。包括将模板解析成ast语法树、ast语法树优化、代码生成等功能。可以在构建时进行编译(借助webpack、vue-loader等辅助插件);它也可以在运行时完成,使用包含构建函数的Vue.js。显然,编译是一个性能密集型的工作,所以推荐前者——离线编译。core目录包含Vue.js的核心代码,包括内置组件、全局API封装、Vue实例化、观察者、虚拟DOM、实用函数等。这里的代码是Vue.js的灵魂,也是后面需要重点分析的地方。platformVue.js是一个跨平台的MVVM框架,可以运行在web端,也可以配合weex运行在原生客户端。platform是Vue.js的入口,两个目录代表两个主要入口,分别打包成运行在web端的Vue.js和weex。我们的主要目标是网络端。weex有兴趣的可以看看serverVue.js2.0,支持服务端渲染。所有服务器端渲染相关的逻辑都在这个目录中。注意:这部分代码是运行在服务器端的Node.js,不要与运行在浏览器端的Vue.js混淆。服务器端渲染的主要工作是将组件渲染成服务器端的HTML字符串,直接发送给浏览器,最后将静态标记“混合”成客户端上的完全交互的应用程序。sfc通常我们开发Vue.js都是借助webpack来构建,然后通过.vue单文件来编写组件。该目录中的代码逻辑会将.vue文件的内容解析为JavaScript对象。sharedVue.js会定义一些工具方法,这里定义的工具方法会被浏览器端的Vue.js和服务端的Vue.js共享。从Vue.js的目录设计可以看出,作者将功能模块拆分的非常清晰,将相关逻辑维护在一个独立的目录中,将复用的代码抽取出来放到一个独立的目录中。这样的目录设计使得代码的可读性和可维护性都更高,非常值得学习。调试准备首先,这里有一个vue2.6的注释源码,大家可以下载下来照着做。打开vscode中的setting,设置javascript。流报错。Flow类似于TypeScript,用于类型检测。源代码中有部分代码没有高亮显示。vscode下载一个插件,babeljavascript打开其他的,然后就会高亮显示。然后你可以安装一个全局包,服务,然后在示例目录中运行服务。把目录启动为静态服务器,然后把所有的vue测试例子放在文件里,然后把src的vue地址改成../../dist/vue.js”,这就是我们搭建的vue更改如下命令后,可以随意打断调试,下面我们更改一下vue源码package.json的scripts配置,方便后续调试,dev命令改为"dev":"rollup-w-cscripts/config.js--sourcemap--environmentTARGET:web-full-dev》启用热加载-w-c设置配置文件开启sourcemap设置环境变量找到scripts/configs.js搜索对应的环境变量值web-full-dev找到对应的入口src/platforms/web/entry-runtime-with-compiler.js我们这里调试的是vue的全量,vue-cli是运行时版本,完整版可以更好的调试所有的代码vue.因为vue的源码,里面是跳转的,很难把逻辑串联起来第一次来,这是一张通用的思维导图。上图有3个watcher创建的关键点,这里也有对应的思维导图watcher图注:从watcher被dep.notify()通知,然后执行watcher.update(),可以看出计算观察者。update只是dirty=true将缓存标记为无效,并且依赖值已更新。它不会执行queueWatcher(this)将当前计算观察者放入队列更新观察者队列中。用户watcher和渲染watcher在执行update的时候,基本都会执行queueWatcher(this),将当前watcher放入队列中,因为计算watcher是有缓存的。注意这里。未缩小版本的地图太大,步骤太多。这是思维导图的简化版本。![Uploading...]()从入口我们按照我的思维导图找到了vue的最终定义,发现vue是一个构造函数,其实就是一个用Function实现的类。我们只能通过newVue来实例化它。有同学看到这里不禁疑惑,为什么Vue不使用ES6Class来实现呢?我们回过头来看,这里调用了很多xxxMixin函数,并且传入了Vue作为参数。它们的作用是在Vue的原型上扩展一些方法(这里的具体细节会在后面的文章中介绍,这里不再展开)。Vue将这些扩展分散到多个模块中去实现功能,而不是全部在一个模块中实现,而Class很难实现。这样做的好处是非常方便代码的维护和管理,而且这种编程技巧也非常值得学习。数据驱动,我们在实际使用vue的时候,会这样调用newVue()。其实我们会看到根据源码,this._init()初始化函数其实是被调用的。大家可以根据我的思维导图和源码找出来。vue初始化init主要做了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化数据,props,computed,watcher等等。Vue的初始化逻辑写的很清楚。将不同的功能逻辑拆分成一些单独的函数执行,让主要逻辑一目了然。这种编程思想非常值得学习和借鉴。Vue实例挂载在Vue中,我们使用$mount实例方法来挂载vm。$mount方法定义在多个文件中,如src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js、src/platform/weex/runtime/index。js。因为$mount方法的实现与平台和构建方法有关。接下来重点分析$mount的编译版本的实现,因为除了webpack的vue-loader之外,我们在纯前端浏览器环境下分析Vue的工作原理,有助于我们深入理解原理。compiler版本的$mount实践非常有意义,先来看一下src/platform/web/entry-runtime-with-compiler.js文件中定义:constmount=Vue.prototype.$mountVue.prototype.$mount=function(el?:string|Element,hydrating?:boolean):Component{el=el&&query(el)/*伊斯坦布尔忽略if*/if(el===document.body||el===document.documentElement){process.env.NODE_ENV!=='production'&&warn(`不要将Vue挂载到或-而是挂载到普通元素。`)returnthis}constoptions=this.$options//resolvetemplate/el并转换为渲染函数'){template=idToTemplate(template)/*istanbulignoreif*/if(process.env.NODE_ENV!=='production'&&!template){warn(`模板元素不是found或为空:${options.template}`,this)}}elseif(template.nodeType){template=template.innerHTML}else{if(process.env.NODE_ENV!=='production'){warn('无效的模板选项:'+template,this)}returnthis}}elseif(el){template=getOuterHTML(el)}if(template){/*istanbulignoreif*/if(process.env.NODE_ENV!=='production'&&config.performance&&mark){mark('compile')}const{render,staticRenderFns}=compileToFunctions(template,{shouldDecodeNewlines,shouldDecodeNewlinesForHref,delimiters:options.delimiters,comments:options.comments},这个)options.render=renderoptions.staticRenderFns=staticRenderFns/*istanbulignoreif*/if(process.env.NODE_ENV!=='production'&&config.performance&&mark){mark('compileend')measure(`vue${this._name}compile`,'compile','compileend')}}}returnmount.call(this,el,hydrating)}this代码首先缓存了原型上的$mount方法,然后重新定义了该方法。我们先来分析一下这段代码。首先,它限制了el。vue不能挂载在body、html等根节点上。接下来是关键逻辑——如果没有定义render方法,el或模板字符串将被转换为render方法。这里我们要记住,在Vue2.0中,所有Vue组件的渲染最终都需要render方法。不管我们是用single-file.vue的方式开发组件,还是写el或者template属性,最终都会转化为render方法。那么这个过程就是Vue的一个“在线编译”过程,是通过调用compileToFunctions方法来实现的。后面我们会介绍编译过程。最后调用原原型上的$mount方法进行挂载。原始原型上的$mount方法定义在src/platform/web/runtime/index.js中。之所以这样设计完全是为了复用,因为它可以直接被只有运行时(runtime)版本的Vue使用。//publicmountmethodVue.prototype.$mount=function(el?:string|Element,hydrating?:boolean):Component{el=el&&inBrowser?query(el):undefinedreturnmountComponent(this,el,hydrating)}最后会调用mountComponent,src/core/instance/lifecycle.jsexportfunctionmountComponent(vm:Component,el:?Element,hydrating?:boolean):Component{vm.$el=elif(!vm.$options.render){vm.$options.render=createEmptyVNodeif(process.env.NODE_ENV!=='production'){/*伊斯坦布尔忽略if*/if((vm.$options.template&&vm.$options.template.charAt(0)!=='#')||vm.$options.el||el){warn('你正在使用Vue其中模板'+'编译器不可用。要么将模板预编译为'+'渲染函数,要么使用编译器包含的构建。',vm)}else{warn('无法安装组件:templateorrenderfunctionnotdefined.',vm)}}}callHook(vm,'beforeMount')letupdateComponent/*istanbulignoreif*/if(process.env.NODE_ENV!=='production'&&config.performance&&标记){updateComponent=()=>{constname=vm._nameconstid=vm._uidconststartTag=`vue-perf-start:${id}`constendTag=`vue-perf-end:${id}`mark(startTag)constvnode=vm._render()mark(endTag)measure(`vue${name}render`,startTag,endTag)mark(startTag)vm._update(vnode,hydrating)mark(endTag)measure(`vue${name}patch`,startTag,endTag)}}else{updateComponent=()=>{vm._update(vm._render(),hydrating)}}//我们将其设置为watcher中的vm._watcherconstructor//因为watcher的初始补丁可能会调用$forceUpdate(例如在child//组件的挂载钩子中),它依赖于vm._watcherbeingalreadydefinednewWatcher(vm,updateComponent,noop,{before(){if(vm._isMounted){callHook(vm,'beforeUpdate')}}},true/*isRenderWatcher*/)hydrating=false//手动挂载instance,callmountedonself//mounted会在其插入的钩子中为渲染创建的子组件调用if(vm.$vnode==null){vm._isMounted=truecallHook(vm,'mounted')}returnvm}from从上面代码可以看出,mountComponent的核心是实例化一个渲染Watcher(思维导图中),并在其回调函数中调用updateComponent方法。在该方法中调用vm._render方法生成虚拟Node,最后调用vm._update更新DOMWatcher,这里起到两个作用,一是执行初始化时的回调函数,二是执行回调函数当vm实例中的监控数据发生变化时。当函数最终确定为根节点时,设置vm._isMounted为true,表示实例已经挂载,同时执行挂载的钩子函数。这里注意,vm.$vnode代表的是Vue实例的父虚拟Node,所以如果为Null,则表示当前是根Vue实例。mountComponent方法的逻辑也很清晰。它将完成整个渲染工作。接下来重点分析细节,就是两个核心方法:vm._render和vm._update。