Vue的双向数据绑定是通过数据劫持结合发布者订阅者模式来实现的
在new Vue的时候,在Oserver中通过Object.defineProperty()打到数据劫持,代理所有数据的getter和setter属性,在每次触发setter的时候,都会通过Dep来通知Watcher,Watcher作为Observer数据监听器与Compile模板解析器之间的桥梁,当O不server监听到数据发生改变的时候,通过Updateer来通知Compile更新视图
每个组件实例都有相应的watcher实例对象,他会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用的时候,会通知watcher重新计算,从而致使它关联的组件得到更新。
MVVM全程是Model-View-ViewModel
vue是以数据为驱动的,vue自身将DOM和数据进行绑定,一旦创建绑定,dom和数据将保持同步。每当数据发生变化,dom会跟着变化。ViewModel是Vue的核心,它是Vue的一个实例。
DomListeners和DataBindings是实现双向绑定的关键。DOMListenners监听页面所有View层DOM元素的变化,当发生变化,Model层的数据随之改变;DataBindings监听Model层的数据,当数据发生变化,View层的DOM元素随之变化。
“渐进式框架”就是用你想用或者能用的功能特性,你不想用的部分功能可以先不用。vue不强求你一次性接受并使用它的全部功能特性。
view更新data其实可以通过时间监听即可,比如input标签监听“input”事件就可以实现了
通过Object.defineProperty()对属性设置一个set函数,当数据改变了就会触发这个函数,所以我们只要将一些需要更新的方法放在里面就可以实现data更新view了。
virtual dom 其实就是一颗以js对象(VNode节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实DOM的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。
找出有必要更新的节点更新,没有更新的节点就不要动。这其中的核心就是如何找出哪些更新哪些不更新,这个过程就需要diff算法来完成
怎么做到复杂度从o(n^3)降低到o(n)
标准diff算法:o(n^3) 树的最小距离编辑算法(找到最少的改动路径)算出来的(待了解)考虑到前端操作很少跨级别修改节点,通常都是修改节点属性,及调整子节点,所以只比较同层级的节点。就将复杂度降到了n
patch过程:
1 对比新节点是否存在,不存在插入新节点
2 对比老节点是否存在,不存在删除老节点
3 对比新节点与老节点是否相同,如果不同则删除老节点,插入新节点
4 如果新老节点相同(tag,key等),则执行patchVNode,如果是文本的话就处理,然后对比children。
diff重点
对比children,若老的有新的没则删除,若新的有老的没,则新增。
若都有children,先头头比较,然后尾尾比较,头尾,尾头比较(主要是为了快速处理reverse的情况),
如果相同,则执行patchVNode进行children的对比,如果不同,则会对老节点进行map处理,在里面按照key来找新节点,如果找到了,则patchvnode,否则新建,没有key的话就新建
3.0中diff的差异
预处理及最长递增子序列
created
实例创建完成,可以访问data、computed、watch、methods上的方法和数据,未挂载到DOM,不能访问到$el属性, $ref属性内容为空数组。
mounted
实例挂载到DOM上,此时可以通过DOM API获取到DOM节点,$ref属性可以访问,常用与获取VNode信息和操作,Ajax请求。
computed
computed看上去是方法,但是实际上是计算属性,它会根据你所依赖的数据动态显示新的计算结果。计算结果会被缓存,computed的值在getter执行之后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值才会重新调用对应的getter来计算。
watch
watcher更像是一个data的数据监听回调,当依赖的data的数据变化,执行回调,在方法中会传入newVal和oldVal。可以提供输入值无效,中间值等场景。Vue实例将会在实例化的时候调用$watch(),遍历watch对象的每一个属性。如果你需要在某个数据变化时候做一些事情,使用watch
总结
如果一个数据需要经过复杂计算就用computed。
如果一个数据依赖于其他数据,那么把这个数据设计为computed的
如果一个数据需要被监听并且对数据做一些操作就用watch。
如果你需要在某个数据变化的时候做一些事情,使用watch来观察这个数据变化。
Object.defineProperty只能劫持对象的属性,而 Proxy 是直接代理对象。
由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,
如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作。
Object.defineProperty对新增属性需要手动进行 Observe。
由于 Object.defineProperty劫持的是对象的属性,所以新增属性时,需要重新遍历对象,
对其新增属性再使用 Object.defineProperty 进行劫持。
也正是因为这个原因,使用 Vue 给 data 中的数组或对象新增属性时,
需要使用 vm.$set 才能保证新增的属性也是响应式的。
通信
一.父组件向子组件传递值使用props;
二.子组件向父组件传递值用this. $emit()
三.组件与组件值引用可采用vuex.
四.vuex是一个专为vue. js应用程序开发状态管理模式。集中式管理所有组件的状态。
五.利用插槽子组件向父组件传值
如何实现一个vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。简单来说就是:应用遇到多个组件共享状态时,使用vuex。
场景:多个组件共享数据或者是跨组件传递数据时
vuex的流程
页面通过mapAction异步提交事件到action。action通过commit把对应参数同步提交到mutation,mutation会修改state中对应的值。最后通过getter把对应值跑出去,在页面的计算属性中,通过,mapGetter来动态获取state中的值
通过commit 提交 mutation 的方式来修改 state 时,vue的调试工具能够记录每一次state的变化,这样方便调试。但是如果是直接修改state,则没有这个记录。
1.query穿参,或者params传参
使用 this.KaTeX parse error: Expected '}', got 'EOF' at end of input: … '参数值'}) this.router.push({name: ‘/’, params: {参数名: ‘参数值’})
注意1: 使用params时不能使用path
接收: var a = this. r o u t e . q u e r y . 参数名 v a r b = t h i s . route.query.参数名 var b = this. route.query.参数名varb=this.route.params.参数名
注意2:实用params去传值的时候,在页面刷新时,参数会消失,用query则不会有这个问题。
生命周期
概念
Vue 实例从创建到销毁的过程,就是生命周期。也就是从开始创建、初始化数据、编译模板、挂载DOM-渲染、更新-渲染、卸载等一系列的过程,我们称这是 Vue 的生命周期。
初始化显示
beforeCreated
created
beforeMounted
mounted
更新
beforeUpdate
updated
销毁
beforeDestrory
destrory
key的作用是为了在diff算法执行时更快找到对应的节点,提高diff速。
key具有唯一性
vue中循环需加 :key = “唯一标识“,唯一表示可以是item里面id index等,因为vue组件高度复用增加key可以标识组件的唯一性,为了更好的区别各个组件key的作用主要是为了高校的更新虚拟DOM
编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换
编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
v-show 由false变为true的时候不会触发组件的生命周期
v-if由false变为true的时候,触发组件的beforeCreate、create、beforeMount、mounted钩子,由true变为false的时候触发组件的beforeDestory、destoryed方法
性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
使用场景
v-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)
如果需要非常频繁地切换,则使用 v-show 较好
如果在运行时条件很少改变,则使用 v-if 较好
#参考文献
window.location的hash方法是干嘛用的
改变location.hash会在浏览器的访问历史中增加一个记录,使用后退键时可以回到上一个浏览位置。利用这一点就可以解决ajax中无访问状态的问题,配合#和历史记录,就可以在无刷新的ajax中顺利往返于各个访问状态。
我们都知道,单页面应用(SPA)的核心之一是: 更新视图而不重新请求页面;vue-rouetr在实现单页面前端路由时,提供了两种方式:Hash模式和History模式;根据mode参数来决定采用哪一种方式。
1、Hash模式:
hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中也不会不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;
2、History模式:
HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面
通常情况下,我们会选择使用History模式,原因就是Hash模式下URL带着‘#’会显得不美观
但当用户直接在用户栏输入地址并带有参数时: Hash模式:xxx.com/#/id=5 请求地址为 xxx.com,没有问题; History模式: xxx.com/id=5 请求地址为 xxx.com/id=5,如果后端没有对应的路由处理,就会返回404错误;
在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。 给个警告,因为这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。或者,如果你使用 Node.js 服务器,你可以用服务端路由匹配到来的 URL,并在没有匹配到路由的时候返回 404,以实现回退。
1、流程顺序
“相应视图—>修改State”拆分成两部分,视图触发Action,Action再触发Mutation。
2、角色定位
基于流程顺序,二者扮演不同的角色。
Mutation:专注于修改State,理论上是修改State的唯一途径。
Action:业务代码、异步请求。
3、限制
角色不同,二者有不同的限制。
Mutation:必须同步执行。
Action:可以异步,但不能直接操作State。
简单理解Vue中的nextTick
什么时候需要用的Vue.nextTick()
Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。
原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载已完成。
v-for的优先级比v-if更高,这意味着v-if将分别重复运行与每个v-for循环中
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
当你设置 vm.someData = ‘new value’,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。
异步更新队列意思,就是当vue检测到data变化以后,由于watcher触发了,所以会被推入到队列中,这是在操作DOM的时候,是不会立即更新DOM的,所以用nextTrick(),可以立即更新DOM。
概述
keep-alive是Vue内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。也就是所谓的组件缓存
基本用法
//被keep-alive包含的组件会被缓存
<keep-alive>
<component/>
keep-alive>
被keep-alive包含的组件不会被再次初始化,也就意味着不会重走生命周期函数 但是有时候是希望我们缓存的组件可以能够再次进行渲染,这时Vue为我们解决了这个问题 被包含在 keep-alive 中创建的组件,会多出两个生命周期的钩子: activated 与 deactivated
keep-alive是一个抽象的组件,缓存的组件不会被mounted,为此提供activated和deactivated钩子函数
package.json
1、主要用来定义项目中需要依赖的包,在创建项目的时候会生成。
2、记录项目中所需要的所有模块。当你执行npm install的时候,node会先从package.json文件中读取所有dependencies信息,然后根据dependencies中的信息与node_modules中的模块进行对比,没有的直接下载,已有的检查更新
package-lock.json
1、在 npm install时候生成一份文件,用以记录当前状态下实际安装的各个npm package的具体来源和版本号。
2、package-lock.json文件锁定所有模块的版本号,包括主模块和所有依赖子模块。当你执行npm install的时候,node从package.json文件读取模块名称,从package-lock.json文件中获取版本号,然后进行下载或者更新。
侵入式和非侵入式
非侵入式
Vue数据变化
this.a++
侵入式
React数据变化
this.setState({
a: this.state.a+1
})
小程序数据变化
this.setData({
a: this.data.a + 1
})
Object.defineProperty()方法
Object.defineProperty()方法可以设置一些额外隐藏的属性
Object.defineProperty(obj, 'a', {
value: 3,
// 是否可写
writable: false,
// 是否可以被枚举
enumerable: true
});
getter/setter需要变量周转才能工作
var tmp
Object.defineProperty(obj, 'a', {
// getter
get() {
console.log('你试图访问obj的a属性')
return tmp
},
// setter
set(newValue) {
console.log('你试图改变obj的a属性', newValue)
tmp = newValue
}
});
defineReactive函数
var obj = {};
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可以被配置,比如可以被delete
configurable: true,
// getter
get() {
console.log('你试图访问obj的'+ key +'属性')
return val
},
// setter
set(newValue) {
console.log('你试图改变obj的' + key + '属性', newValue)
if(val === newValue) {
return;
}
val = newValue
}
});
}
defineReactive(obj, 'a', 10)
Observer类
将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
AMD规范则是非同步加载模块,允许指定回调函数。
由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。
如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
Node使用CommonJS模块规范,内置的require命令用于加载模块文件。
加载规则
(1)如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require(‘/home/marco/foo.js’)将加载/home/marco/foo.js。
(2)如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require(‘./circle’)将加载当前脚本同一目录的circle.js。
(3)如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
举例来说,脚本/home/user/projects/foo.js执行了require(‘bar.js’)命令,Node会依次搜索以下文件。
/usr/local/lib/node/bar.js
/home/user/projects/node_modules/bar.js
/home/user/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
(4)如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require(‘example-module/path/to/file’),则将先找到example-module的位置,然后再以它为参数,找到后续路径。
(5)如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
(6)如果想得到require命令加载的确切文件名,使用require.resolve()方法。
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):
const Foo = () =>
Promise.resolve({
/* 组件定义对象 */
})
第二,在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 (split point):
import('./Foo.vue') // 返回 Promise
1.vue中组件是用来复用的,为了防止data复用,将其定义为函数。
2.vue组件中的data数据都应该是相互隔离,互不影响的,所以就需要通过data函数返回一个对象作为组件的状态。
3.当我们将组件中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,拥有自己的作用域,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。
4.当我们组件的date单纯的写成对象形式,这些实例用的是同一个构造函数,由于JavaScript的特性所导致,所有的组件实例共用了一个data,就会造成一个变了全都会变的结果。
data和props
data是每个组件的私有内存,可以在其中存储需要的任何变量。props是将数据从父组件传递到子组件的方式。
在我用vue开发项目的时候,一般我都会用到组件封装,采用组件化的思想进行项目开发,我在搭建一个项目的时候,就会创建一个views目录和一个commen目录和一个feature目录,views目录中放页面级的组件,commen中放公共组件(如:head(公共头组件),foot(公共底部组件)等),feature目录内放功能组件(如:swiper(轮播功能组件),tabbar(切换功能组件)、list(上拉加载更多功能组件))
vue组件封装的优点
组件可以提升整个项目的开发效率。能够把页面抽象成多个相对独立的模块,解决了我们传统项目开发:效率低、难维护、复用性低等问题。
组件封装的大概过程
使用Vue.extend方法创建一个组件,然后使用Vue.component方法注册组件。但是我们一般用脚手架开发项目,每个 .vue单文件就是一个组件。在另一组件import 导入,并在components中注册,子组件需要数据,可以在props中接受定义。而子组件修改好数据后,想把数据传递给父组件。可以采用emit方法。
Slot
个人理解:
是对组件的扩展,通过slot插槽向组件内部指定位置传递内容,通过slot可以父子传参;
开发背景(slot出现时为了解决什么问题):
正常情况下,hello world在组件标签Child中的span标签会被组件模板template内容替换掉,当想让组件标签Child中内容传递给组件时需要使用slot插槽;
Slot的通俗理解
是“占坑”,在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中位置),当插槽也就是坑
Slot使用
Vue响应式原理-如何监听Array的变化?
Vue 不能检测以下变动的数组:因为vue的响应式是通过 Object.defineProperty 来实现的,但是数组的length属性是不能添加getter和setter,所有无法通过观察length来判断。
Vue监听Array三步曲
结论
vue对数组的length直接改变无法直接进行观察,提供了vue.$set 进行显式观察,并且重写了 push, pop, shift, unshift, splice, sort, reverse方法来进行隐式观察。
概述
埋点,主要记录 “谁 什么时候 做了什么事情”
埋点怎么做
import Vue from 'vue'
// 自定义埋点指令
Vue.directive('track', {
//钩子函数,只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
/*
* el:指令所绑定的元素,可以用来直接操作 DOM
* binding:一个对象,包含以下 property:
* name:指令名,不包括 v- 前缀。
* value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
* expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
* arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
* modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
* vnode:Vue 编译生成的虚拟节点
*/
bind: (el, binding, vnode) => {
if (binding.value) {
//这里参数是根据自己业务可以自己定义
let params = {
currentUrl:binding.value.currentUrl,
actionType:binding.value.actionType,
frontTriggerType:binding.value.triggerType,
businessCode:binding.value.businessCode,
behavior:binding.value.behavior,
service:'xxx',
}
//如果是浏览类型,直接保存
if (binding.value.triggerType == 'browse'){
//调用后台接口保存数据
api.eventTrack.saveEventTrack(params);
} else if (binding.value.triggerType == 'click'){
//如果是click类型,监听click事件
el.addEventListener('click', (event) => {
//调用后台接口保存数据
api.eventTrack.saveEventTrack(params);
}, false)
}
}
}
})
import './directive/track'
<div class="app-online-list" v-track="{triggerType:'browse',currentUrl: $route.path,behavior:'浏览xxx功能',businessCode: 19,actionType:'xxx-view'}">
</div>
如果是浏览类型记录,triggerType写为click
<div class="app-online-list" v-track="{triggerType:'click',currentUrl: $route.path,behavior:'点击xxx按钮',businessCode: 19,actionType:'xxx-click'}">
</div>
多页面应用
每一次页面跳转的时候,后台服务器都会给返回一个新的html文档,这种类型的网站也就是多页网站,也叫做多页应用。
单页面应用
第一次进入页面的时候会请求一个html文件,刷新清除一下。切换到其他组件,此时路径也相应变化,但是并没有新的html文件请求,页面内容也变化了。
原理是:JS会感知到url的变化,通过这一点,可以用js动态的将当前页面的内容清除掉,然后将下一个页面的内容挂载到当前页面上,这个时候的路由不是后端来做了,而是前端来做,判断页面到底是显示哪个组件,清除不需要的,显示需要的组件。这种过程就是单页应用,每次跳转的时候不需要再请求html文件了。
vue如果同一个数据,很短的时间内连续更新 会怎么样
原因,就是多次请求了异步接口,一个接口没有返回,另外一个接口就发出去了。因为,ajax是一个异步操作。导致,在回调的时候,两次请求成功后的回调都会执行。就导致数据重复发生错误了。譬如下拉滚动加载更多 或是 tab切换。
怎么解决呢?
面对这种多次触发异步请求的处理,常用的解决办法如下:
1、在请求发出后,处于loading状态时,禁用再次触发异步的操作按钮。
譬如,表单提交了,就把表单提交按钮disable禁用,不然再次提交。
2、请求发出后,处于loading状态是,显示mask遮罩,不然点击下面的其他的操作。
Vue2和Vue3的区别(面试高频)
对于生命周期来说,整体上变化不大,只是大部分生命周期钩子名称上 + “on”,功能上是类似的。不过有一点需要注意,Vue3 在组合式API(Composition API,下面展开)中使用生命周期钩子时需要先引入,而 Vue2 在选项API(Options API)中可以直接调用生命周期钩子,如下所示。
// vue3
<script setup>
import { onMounted } from 'vue'; // 使用前需引入生命周期钩子
onMounted(() => {
// ...
});
// 可将不同的逻辑拆开成多个onMounted,依然按顺序执行,不会被覆盖
onMounted(() => {
// ...
});
</script>
// vue2
<script>
export default {
mounted() { // 直接调用生命周期钩子
// ...
},
}
</script>
常用生命周期对比如下表所示。
Setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地去定义。
熟悉 Vue2 的朋友应该清楚,在模板中如果使用多个根节点时会报错,如下所示。
// vue2中在template里存在多个根节点会报错
<template>
<header></header>
<main></main>
<footer></footer>
</template>
// 只能存在一个根节点,需要用一个来包裹着
<template>
<div>
<header></header>
<main></main>
<footer></footer>
</div>
</template>
但是,Vue3 支持多个根节点,也就是 fragment。即以下多根节点的写法是被允许的。
<template>
<header></header>
<main></main>
<footer></footer>
</template>