Vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为Object.defineProperty不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是Vue内部通过重写函数的方式解决了这个问题。
在Vue3.0中已经不使用这种方式了,而是通过使用Proxy对对象进行代理,从而实现数据劫持。使用Proxy的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为Proxy是ES6的语法。
在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗长、混乱,这样对项目开发和后期的项目维护是非常不利的。
(1)MVC
(2)MVVM
MVVM分为Model、View、ViewModel:
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。
(3)MVP
MVP模式与MVC唯一不同的在于Presenter和Controller。在MVC模式中使用观察者模式,来实现当Model层数据发生变化的时候,通知View层的更新。这样View层和Model层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP的模式通过使用Presenter来实现对View层和Model层的解耦。MVC中的Controller只知道Model的接口,因此它没有办法控制View层的更新,MVP模式中,View层的接口暴露给了Presenter因此可以在Presenter中将Model的变化和View的变化绑定在一起,以此来实现View和Model的同步更新。这样就实现了对View和Model的解耦,Presenter还包含了其他的响应逻辑。
对于Computed:
对于Watch:
它不支持缓存,数据变化时,它就会触发相应的操作
支持异步监听
监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
当一个属性发生变化时,就需要执行相应的操作
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。
总结:
运用场景:
可以将同一函数定义为一个method或者一个计算属性。对于最终的结果,两种方式是相同的
不同点:
slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
根据过滤器的名称,过滤器是用来过滤数据的,在Vue中使用filters来过滤数据,filters不会修改数据,而是过滤数据,改变用户看到的输出(计算属性computed,方法methods都是通过修改数据来处理数据格式的输出显示)。
使用场景:
过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在插值表达式{{}}和v-bind表达式中,然后放在操作符“|”后面进行指示。
例如,在显示金额,给商品价格添加单位:
那么可以按照这两种情况分别得到以下方法:
组件会被卸载:
(1)将状态存储在LocalStorage/SessionStorage
只需要在组件即将被销毁的生命周期componentWillUnmount(react)中在LocalStorage/SessionStorage中把当前组件的state通过JSON.stringify()储存下来就可以了。在这里面需要注意的是组件更新状态的时机。
比如从B组件跳转到A组件的时候,A组件需要更新自身的状态。但是如果从别的组件跳转到B组件的时候,实际上是希望B组件重新渲染的,也就是不要从Storage中读取信息。所以需要在Storage中的状态加入一个flag属性,用来控制A组件是否读取Storage中的状态。
优点:
缺点:
(2)路由传值
通过react-router的Link组件的prop——to可以实现路由间传递参数的效果。
在这里需要用到state参数,在B组件中通过history.location.state就可以拿到state值,保存它。返回A组件时再次携带state达到路由状态保持的效果。
组件不会被卸载:
(1)单页面渲染
要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。
除此之外,在Vue中,还可以是用keep-alive来缓存页面,当组件在keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行被包裹在keep-alive中的组件的状态将会被保留:
本质是一个父子组件通信的语法糖,通过prop和$.emit实现。因此父组件v-model语法糖本质上可以修改为:
可以。v-model实际上是一个语法糖,如:
所以,custom-input组件的实现应该类似于这样:
而在Vue中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。
所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。
如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用keep-alive组件包裹需要保存的组件。
(1)keep-alive
keep-alive有以下三个属性:
注意:keep-alive包裹动态组件时,会缓存不活动的组件实例。
主要流程
(2)keep-alive的实现
实现步骤:
(3)keep-alive本身的创建过程和patch过程
缓存渲染的时候,会根据vnode.componentInstance(首次渲染vnode.componentInstance为undefined)和keepAlive属性判断不会执行组件的created、mounted等钩子函数,而是对缓存的组件执行patch过程∶直接把缓存的DOM对象直接插入到目标元素中,完成了数据更新的情况下的渲染过程。
首次渲染
(4)LRU(leastrecentlyused)缓存策略
LRU缓存策略∶从内存中找出最久未使用的数据并置换新的数据。LRU(Leastrencentlyused)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是"如果数据最近被访问过,那么将来被访问的几率也更高"。最常见的实现是使用一个链表保存缓存数据,详细算法实现如下∶
Vue的nextTick其本质是对JavaScript执行原理EventLoop的一种应用。
nextTick的核心是利用了如Promise、MutationObserver、setImmediate、setTimeout的原生JavaScript方法来模拟对应的微/宏任务的实现,本质是为了利用JavaScript的这些异步回调任务队列来实现Vue框架中自己的异步回调队列。
nextTick不仅是Vue内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对DOM更新数据时机的后续逻辑处理
nextTick是典型的将底层JavaScript执行原理应用到具体案例中的示例,引入异步更新队列机制的原因∶
Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick了。
由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick中。
javascript复制代码this.$nextTick(()=>{//获取数据的操作...})所以,在以下情况下,会用到nextTick:
因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。
javascript复制代码addObjB()(this.$set(this.obj,'b','obj.b')console.log(this.obj)}$set()方法相当于手动的去把obj.b处理成一个响应式的属性,此时视图也会跟着改变了。
概念:
vue的模版编译过程主要如下:template->ast->render函数
vue在模版编译版本的码中会执行compileToFunctions将template转化为render函数:
javascript复制代码//将模板编译为render函数const{render,staticRenderFns}=compileToFunctions(template,options//省略},this)CompileToFunctions中的主要逻辑如下∶(1)调用parse方法将template转化为ast(抽象语法树)
javascript复制代码constast=parse(template.trim(),options)parse的目标:把tamplate转换为AST树,它是一种用JavaScript对象的形式来描述整个模板。解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造AST树的目的。AST元素节点总共三种类型:type为1表示普通元素、2为表达式、3为纯文本
(2)对静态节点做优化
javascript复制代码optimize(ast,options)这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化
深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的DOM永远不会改变,这对运行时模板更新起到了极大的优化作用。
(3)生成代码
javascript复制代码constcode=generate(ast,options)generate将ast抽象语法树编译成render字符串并将静态部分放到staticRenderFns中,最后通过newFunction(``render``)生成render函数。
不会立即同步执行重新渲染。Vue实现响应式并不是数据发生变化之后DOM立即变化,而是按一定的策略进行DOM的更新。Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。然后,在下一个的事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。
(1)mixin和extendsmixin和extends均是用于合并、拓展组件的,两者均通过mergeOptions方法实现合并。
(1)自定义指令基本内容
全局定义:Vue.directive("focus",{})
局部定义:directives:{focus:{}}
钩子函数:指令定义对象提供钩子函数
obind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
oinSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
oupdate:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。
oComponentUpdate:指令所在组件的VNode及其子VNode全部更新后调用。
ounbind:只调用一次,指令与元素解绑时调用。
钩子函数参数oel:绑定元素
obing:指令核心对象,描述指令全部信息属性
oname
ovalue
ooldValue
oexpression
oarg
omodifers
ovnode虚拟节点
ooldVnode:上一个虚拟节点(更新钩子函数中才有用)
(2)使用场景
(3)使用案例
初级应用:
高级应用:
子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的prop都将会刷新为最新的值。如果这样做了,Vue会在浏览器的控制台中发出警告。
Vue提倡单向数据流,即父级props的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug的成本会非常高。
只能通过$emit派发一个自定义事件,父组件接收到后,由父组件修改。
javascript复制代码functiondefieneReactive(obj,key,val){constdep=newDep();...Object.defineProperty(obj,key,{...get:functionreactiveGetter(){if(Dep.target){dep.depend();...}returnval}...})}以上只保留了关键代码,主要就是constdep=newDep()实例化一个Dep的实例,然后在get函数中通过dep.depend()进行依赖收集。(1)DepDep是整个依赖收集的核心,其关键代码如下:
(2)Watcher
(3)过程
javascript复制代码updateComponent=()=>{vm._update(vm._render())}newWatcher(vm,updateComponent)get方法中的pushTarget实际上就是把Dep.target赋值为当前的watcher。
this.getter.call(vm,vm),这里的getter会执行vm._render()方法,在这个过程中便会触发数据对象的getter。那么每个对象值的getter都持有一个dep,在触发getter的时候会调用dep.depend()方法,也就会执行Dep.target.addDep(this)。刚才Dep.target已经被赋值为watcher,于是便会执行addDep方法,然后走到dep.addSub()方法,便将当前的watcher订阅到这个数据持有的dep的subs中,这个目的是为后续数据变化时候能通知到哪些subs做准备。所以在vm._render()过程中,会触发所有数据的getter,这样便已经完成了一个依赖收集的过程。
相似之处:
不同之处:
1)数据流
Vue默认支持数据双向绑定,而React一直提倡单向数据流
2)虚拟DOM
Vue2.x开始引入"VirtualDOM",消除了和React在这方面的差异,但是在具体的细节还是有各自的特点。
3)组件化
React与Vue最大的不同是模板的编写。
5)高阶组件
react可以通过高阶组件(HOC)来扩展,而Vue需要通过mixins来扩展。
高阶组件就是高阶函数,而React的组件本身就是纯粹的函数,所以高阶函数对React来说易如反掌。相反Vue.js使用HTML模板创建视图组件,这时模板无法有效的编译,因此Vue不能采用HOC来实现。
6)构建工具
两者都有自己的构建工具:
7)跨平台
相同点:assets和static两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点
不相同点:assets中存放的静态资源文件在项目打包时,也就是运行npmrunbuild时会将assets中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在static文件中跟着index.html一同上传至服务器。static中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是static中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于assets中打包后的文件提交较大点。在服务器中就会占据更大的空间。
建议:将项目中template需要的样式文件js文件等都可以放置在assets中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css等文件可以放置在static中,因为这些引入的第三方文件已经经过处理,不再需要处理,直接上传。
当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为Object.defineProperty()限制,监听不到变化。
解决方式:
javascript复制代码this.$set(this.arr,0,"OBKoro1");//改变数组this.$set(this.obj,"c","OBKoro1");//改变对象调用以下几个数组的方法javascript复制代码splice()、push()、pop()、shift()、unshift()、sort()、reverse()vue源码里缓存了array的原型链,然后重写了这几个方法,触发这几个方法的时候会observer数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。推荐使用splice方法会比较好自定义,因为splice可以在数组的任何位置进行删除/添加操作
vm.$set的实现原理是:
vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端
SSR的优势:
SSR的缺点:
(1)编码阶段
(2)SEO优化
(3)打包优化
(4)用户体验
SPA(single-pageapplication)仅在Web页面初始化时加载相应的HTML、JavaScript和CSS。一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现HTML内容的变换,UI与用户的交互,避免页面的重新加载。
对于runtime来说,只需要保证组件存在render函数即可,而有了预编译之后,只需要保证构建过程中生成render函数就可以。在webpack中,使用vue-loader编译.vue文件,内部依赖的vue-template-compiler模块,在webpack构建过程中,将template预编译成render函数。与react类似,在添加了jsx的语法糖解析器babel-plugin-transform-vue-jsx之后,就可以直接手写render函数。
所以,template和jsx的都是render的一种表现形式,不同的是:JSX相对于template而言,具有更高的灵活性,在复杂的组件中,更具有优势,而template虽然显得有些呆滞。但是template在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。
首先:在css里加上以下代码:
javascript复制代码[v-cloak]{display:none;}如果没有彻底解决问题,则在根元素加上style="display:none;":style="{display:'block'}"
这个API很少用到,作用是扩展组件生成一个构造器,通常会与$mount一起使用。
缺点:
Vue实例有个完整的命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom->渲染、更新->渲染、卸载等系列过程,称这是Vue的命周期。
另外还有keep-alive独有的生命周期,分别为activated和deactivated。用keep-alive包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactivated钩子函数,命中缓存渲染后会执行activated钩子函数。
加载渲染过程:
更新过程:
销毁过程:
我们可以在钩子函数created、beforeMount、mounted中进行调用,因为在这三个钩子函数中,data已经创建,可以将服务端端返回的数据进行赋值。
推荐在created钩子函数中调用异步请求,因为在created钩子函数中调用异步请求有以下优点:
keep-alive是Vue提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
如果为一个组件包裹了keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy和destroyed就不会再被触发了,因为组件不会被真正销毁。
当组件被换掉时,会被缓存到内存中、触发deactivated生命周期;当组件被切回来时,再去缓存里找这个组件、触发activated钩子函数。
组件通信的方式如下:
父组件通过props向子组件传递数据,子组件通过$emit和父组件通信
javascript复制代码//event-bus.jsimportVuefrom'vue'exportconstEventBus=newVue()(2)发送事件假设有两个兄弟组件firstCom和secondCom:
虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。
provide/inject是Vue提供的两个钩子,和data、methods是同级的。并且provide的书写形式和data一样。
在父组件中:
javascript复制代码provide(){return{num:this.num};}在子组件中:
javascript复制代码inject:['num']还可以这样写,这样写就可以访问父组件中的所有属性:
javascript复制代码provide(){return{app:this};}data(){return{num:1};}inject:['app']console.log(this.app.num)注意:依赖注入所提供的属性是非响应式的。
这种方式也是实现父子组件之间的通信。
ref:这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。
在子组件中:
javascript复制代码exportdefault{data(){return{name:'JavaScript'}},methods:{sayHello(){console.log('hello')}}}在父组件中:
考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?
如果是用props/$emit来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。
针对上述情况,Vue引入了$attrs/$listeners,实现组件之间的跨代通信。
先来看一下inheritAttrs,它的默认值true,继承所有的父组件属性除props之外的所有属性;inheritAttrs:false只继承class属性。
A组件(APP.vue):
(1)父子组件间通信
(2)兄弟组件间通信
(3)任意组件之间
如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用vuex,vuex的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。