单页应用spa/多页应用
SPA(single-page application),翻译过来就是单页应用SPA是一种网络应用程序或网站的模型。
它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验。
在单页应用中,所有必要的代码(HTML、JavaScript和CSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面页面在任何时间点都不会重新加载,也不会将控制转移到其他页面。
我们所熟悉的js框架react,vue等等都SPA页面。
多页应用MPA(MultiPage-page application),翻译过来就是多页应用在MPA中,每个页面都是一个主页面,都是独立的当我们在访问另一个页面的时候,都需要重新加载html、css、js文件,公共文件则根据需求按需加载。
那么如何实现SPA页面:
监听地址栏中hash变化驱动界面变化
用pushsate记录浏览器的历史,驱动界面发送变化
hash 模式
核心通过监听url中的hash来进行路由跳转
// 定义 Router
class Router {
constructor () {
this.routes = {}; // 存放路由path及callback
this.currentUrl = '';
// 监听路由change调用相对应的路由回调
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback){
this.routes[path] = callback;
}
push(path) {
this.routes[path] && this.routes[path]()
}
}
// 使用 router
window.miniRouter = new Router();
miniRouter.route('/', () => console.log('page1'))
miniRouter.route('/page2', () => console.log('page2'))
miniRouter.push('/') // page1
miniRouter.push('/page2') // page2
history模式
history 模式核心借用 HTML5 history api,api 提供了丰富的 router 相关属性先了解一个几个相关的api
history.pushState 浏览器历史纪录添加记录
history.replaceState修改浏览器历史纪录中当前纪录
history.popState 当 history 发生变化时触发
// 定义 Router
class Router {
constructor () {
this.routes = {};
this.listerPopState()
}
init(path) {
history.replaceState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
route(path, callback){
this.routes[path] = callback;
}
push(path) {
history.pushState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
listerPopState () {
window.addEventListener('popstate' , e => {
const path = e.state && e.state.path;
this.routers[path] && this.routers[path]()
})
}
}
// 使用 Router
window.miniRouter = new Router();
miniRouter.route('/', ()=> console.log('page1'))
miniRouter.route('/page2', ()=> console.log('page2'))
// 跳转
miniRouter.push('/page2') // page2
下面给出基于Vue的SPA如何实现SEO的三种方式
SSR服务端渲染
将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js
静态化
目前主流的静态化主要有两种:(1)一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中(2)另外一种是通过WEB服务器的 URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果
使用Phantomjs针对爬虫处理
原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。
MVVM(render函数 响应式 如何解析模板)
MVVM -Model View ViewModel,
最核心的就是 ViewModel 。ViewModel 包含 DOM Listeners 和 Data。
MVVM -Model View ViewModel
,它包括 DOM Listenters 和 Data bindings,前者实现了页面与数据的绑定,当页面操作数据的时候 DOM 和 Model 也会发生相应的变化。后者实现了数据与页面的绑定,当数据发生变化的时候会自动渲染页面。
MVVM 实现了数据与页面的双向绑定,MVC 只实现了 Model 和 View 的单向绑定。
MVVM 实现了页面业务逻辑和渲染之间的解耦,也实现了数据与视图的解耦,并且可以组件化开发。
VUE是如何体现MVVM思想的?
胡子语法,实现了数据与视图的绑定。
v-on 事件绑定,通过事件操作数据时,v-model 会发生相应的变化。
在vue中我们使用模板HTML语法组建页面的,使用render函数我们可以用js语言来构建DOM
因为vue是虚拟DOM,所以在拿到template模板时也要转译成VNode的函数,而用render函数构建DOM,vue就免去了转译的过程。
当使用render函数描述虚拟DOM时,vue提供一个函数,这个函数是就构建虚拟DOM所需要的工具。官网上给他起了个名字叫createElement。
render:(h) => {
return h('div',{
//给div绑定value属性
props: {
value:''
},
//给div绑定样式
style:{
width:'30px'
},
//给div绑定点击事件
on: {
click: () => {
console.log('点击事件')
}
},
})
}
有一件事要注意:正如在模板语法中,v-bind:class 和 v-bind:style ,会被特别对待一样,在 VNode 数据对象中,下列属性名是级别最高的字段。该对象也允许你绑定普通的 HTML 特性,就像 DOM 属性一样,比如 innerHTML (这会取代 v-html 指令)。
{
// 和`v-bind:class`一样的 API
'class': {
foo: true,
bar: false
},
// 和`v-bind:style`一样的 API
style: {
color: 'red',
fontSize: '14px'
},
// 正常的 HTML 特性
attrs: {
id: 'foo'
},
// 组件 props
props: {
myProp: 'bar'
},
// DOM 属性
domProps: {
innerHTML: 'baz'
},
// 事件监听器基于 `on`
// 所以不再支持如 `v-on:keyup.enter` 修饰器
// 需要手动匹配 keyCode。
on: {
click: this.clickHandler
},
// 仅对于组件,用于监听原生事件,而不是组件内部使用
// `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
// 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// Scoped slots in the form of
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其他组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其他特殊顶层属性
key: 'myKey',
ref: 'myRef'
}
响应式指的是组件 data 的数据一旦变化,立刻触发视图的更新。它是实现数据驱动视图的第一步。
Vue 实现响应式的一个核心 API 是 Object.defineProperty。该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
监听 data 变化的核心 API
Vue 实现响应式的一个核心 API 是 Object.defineProperty。
该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
基本用法:
const data = {}
const name = 'zhangsan'
Object.defineProperty(data, 'name', {
get: function() {
console.log('get')
return name
},
set: function(newVal) {
console.log('set')
name = newVal
}
})
// 测试
console.log(data.name) // get zhangsan
data.name = 'lisi' // set
利用 Object.defineProperty 重写 get 和 set,将对象属性的赋值和获取变成函数,我们可以实现一个简单的双向绑定。
如何监听 data 变化
共定义了三个函数:
updateView:模拟 Vue 更新视图的入口函数。
defineReactive:对数据进行监听的具体实现。
observer:调用该函数后,可对目标对象进行监听,将目标对象编程响应式的。
执行逻辑为:
定义一个对象 data => 调用 observer(data) 将对象变成响应式的 => 修改对象内的属性 => 更新视图
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 监听的不是对象或数组时,直接返回
return target
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
测试一下,会打印出两个 “视图更新” 字符串。
// 准备数据
const data = {
name: 'zhangsan',
age: 20
}
// 监听数据
observer(data)
// 测试
data.name = 'lisi'
data.age = 21
vue的响应式原理(数据双向绑定)
- 在init数据初始化的时候,对象内部通过 defineReactive 方法,使用 Object.defineProperty,将属性进行劫持(这时候只会劫持已经存在的属性)。如果数据是数组类型,Vue2中是通过重写数组方法来实现。多层对象是通过递归来实现劫持的。
- 在初始化流程中的编译阶段,当render function被渲染的时候,会读取Vue实例中和视图相关的响应式数据,此时会触发 getter函数进行依赖收集(将观察者Watcher对象存放到当前闭包的订阅者Dep的subs中),此时的数据劫持功能和观察者模式就实现了一个MVVM模式中的Binder,之后就是正常的渲染和更新流程。
- 当数据发生变化或者视图导致的数据发生变化时,会触发数据劫持的setter函数,setter会通知初始化依赖收集中的Dep中和视图相应Watcher,告知需要重新渲染视图,Watcher 就会再次通过 update 方法来更新视图。
VUE实现双向数据绑定的原理就是利用了 Object.defineProperty() 这个方法重新定义了对象获取属性值(get)和设置属性值(set)的操作来实现的。
双向数据绑定, 数据层和视图层中的数据同步, 在写入数据时视图层实时的跟着更新。
-
数据变化会同步更新视图 (热更新),我们使用Vue的数据双向绑定一般是操纵data来更新view
-
视图变化也可以更新数据,通过事件监听实现
-
不用再去操作DOM对象,更多精力投入到业务逻辑上
data为什么是函数(什么时候可以使用对象)
vue实例的时候定义data属性既可以是一个对象,也可以是一个函数
const app = new Vue({
el:"#app",
// 对象格式
data:{
foo:"foo"
},
// 函数格式
data(){
return {
foo:"foo"
}
}
})
组件中定义data属性,只能是一个函数
如果为组件data直接定义为一个对象
Vue.component('component1',{
template:`<div>组件</div>`,
data:{
foo:"foo"
}
})
则会得到警告信息::返回的data应该是一个函数在每一个组件实例中
为什么呢?
首先可以看看vue初始化data的代码,data的定义可以是函数也可以是对象
源码位置:/vue-dev/src/core/instance/state.js
定义data会进行数据校验
源码位置:/vue-dev/src/core/instance/init.js
这时候vm实例为undefined,进入if判断,若data类型不是function,则出现警告提示。
主要还是:
根实例对象data可以是对象也可以是函数(根实例是单例),
不会产生数据污染情况
组件实例对象data必须为函数,
目的是为了防止多个组件实例对象之间共用一个data,
产生数据污染。
采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象
v-model的原理
v-model 本质上不过是语法糖。(语法糖简单来说就是『便捷写法』)
它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。 – 官方文档
<!-- 在大部分情况下,以下两种写法是等价的 -->
<el-input v-model="foo" />
<el-input :value="foo" @input="foo = $event" />
v-model 不仅仅是语法糖,它还有副作用。
副作用如下:
如果 v-model 绑定的是响应式对象上某个不存在的属性,那么 vue 会悄悄地增加这个属性,并让它响应式。
那我们会想,vmodel是单向还是双向?
对于这个问题:enmm,我们可以想一想。
你可以用 v-model 指令在表单 、 及 元素上创建双向数据绑定。』 —— vue2官方文档
那 v-model 是单向数据流吗?
是的,它甚至是单向数据流的典型范式。
什么是单项数据流?
子组件不能改变父组件传递给它的 prop 属性,推荐的做法是它抛出事件,通知父组件自行改变绑定的值。
v-model 的做法是怎样的?
v-model 做法完全符合单项数据流。甚至于,它给出了一种在命名和事件定义上的规范。
众所周知 .sync 修饰符是单向数据流的另一个典型范式。
简单来说:单向数据流总结起来其实也就8个字:数据向下,事件向上。
v-if和v-show
我们都知道在 vue 中 v-show 与 v-if 的作用效果是相同的(不含v-else),都能控制元素在页面是否显示
在用法上也是相同的
<Model v-show="isShow" />
<Model v-if="isShow" />
当表达式为true的时候,都会占据页面的位置
当表达式都为false时,都不会占据页面位置
那么区别在哪里?
- 控制手段不同
- 编译过程不同
- 编译条件不同
控制手段:
v-show隐藏则是为该元素添加css–display:none,dom元素依旧还在。
v-if显示隐藏是将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有更高的初始渲染消耗;
原理:
将模板template转为ast结构的JS对象
用ast得到的JS对象拼装render和staticRenderFns函数
render和staticRenderFns函数被调用后生成虚拟VNODE节点,该节点包含创建DOM节点所需信息
vm.patch函数通过虚拟DOM算法利用VNODE节点创建真实DOM节点
#v-show原理
不管初始条件是什么,元素总是会被渲染
我们看一下在vue中是如何实现的
代码很好理解,有transition就执行transition,没有就直接设置display属性
export const vShow: ObjectDirective<VShowElement> = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display
if (transition && value) {
transition.beforeEnter(el)
} else {
setDisplay(el, value)
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el)
}
},
updated(el, { value, oldValue }, { transition }) {
// ...
},
beforeUnmount(el, { value }) {
setDisplay(el, value)
}
}
v-if在实现上比v-show要复杂的多,因为还有else else-if 等条件需要处理,这里我们也只摘抄源码中处理 v-if 的一小部分
返回一个node节点,render函数通过表达式的值来决定是否生成DOM。
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// ...
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
})
}
)
总接一下:
v-if 与 v-show 都能控制dom元素在页面的显示
v-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)
如果需要非常频繁地切换,则使用 v-show 较好
如果在运行时条件很少改变,则使用 v-if 较好
computed,watch,method
计算属性是基于他们的响应式依赖进行缓存的,只有在依赖发生变化时,才会计算求值,而使用 methods,每次都会执行相应的方法。
首先是computed和watch的区别:
watch中的函数是不需要调用的
computed内部的函数调用的时候不需要加()
watch属性监听 :监听属性的变化
computed:计算属性通过属性计算而得来的属性
watch需要在数据变化时执行异步或开销较大的操作时使用
对于任何复杂逻辑或一个数据属性在它所依赖的属性发生变化时,也要发生变化,这种情况下,我们最好使用计算属性computed。
computed 属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。主要当作属性来使用;
computed中的函数必须用return返回最终的结果当computed中的函数所依赖的属性如果没有发生改变的时候,那么调用当前函数的时候结果会从缓存中读取
watch 一个对象,键是需要观察的表达式,值是对应回调函数。主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作;
2、computed和watch的使用场景:
computed 当一个属性受多个属性影响的时候就需要用到computed 最典型的例子: 购物车商品结算的时候
watch 当一条数据影响多条数据的时候就需要用watch 比如: 搜索数据
而methods调用方法一定要有()。methods方法页面刚加载时调用一次,但是结果不会缓存。methods里面是用来定义函数的,很显然,它需要手动调用才能执行。而不像watch和computed那样,“自动执行”预先定义的函数
vue生命周期
这个,我之前总结过了,感兴趣的小伙伴·可以看看。
vue生命周期-cxuyuanLch
父子组件生命周期执行顺序
父子组件的生命周期中,首先走父组件的生命周期,当父组件的生命周期走完beforeMount(挂载前)这个函数时就会走子组件的生命周期,当子组件挂载完成后(执行完mounted)父组件再挂载dom节点。
vue组件之间通信方式
组件间通信的分类可以分成以下
父子组件之间的通信
兄弟组件之间的通信
祖孙与后代组件之间的通信
非关系组件间之间的通信
整理vue中8种常规的通信方案:
通过 props 传递
适用场景:父组件传递数据给子组件
子组件设置props属性,定义接收父组件传递过来的参数
父组件在使用子组件标签中通过字面量来传递值
Children.vue
props:{
// 字符串形式
name:String // 接收的类型参数
// 对象形式
age:{
type:Number, // 接收的类型为数值
defaule:18, // 默认值为18
require:true // age属性必须传递
}
}
Father.vue组件
<Children name="jack" age=18 />
通过 $emit 触发自定义事件
适用场景:子组件传递数据给父组件
子组件通过$emit触发自定义事件,$emit第二个参数为传递的数值
父组件绑定监听器获取到子组件传递过来的参数
Chilfen.vue
this.$emit('add', good)
Father.vue
<Children @add="cartAdd($event)" />
使用 ref
父组件在使用子组件的时候设置ref
父组件通过设置子组件ref来获取数据
父组件
<Children ref="foo" />
this.$refs.foo // 获取子组件实例,通过子组件实例我们就能拿到对应的数据
EventBus
使用场景:兄弟组件传值
创建一个中央事件总线EventBus
兄弟组件通过$emit触发自定义事件,$emit第二个参数为传递的数值
另一个兄弟组件通过$on监听自定义事件
Bus.js
// 创建一个中央时间总线类
class Bus {
constructor() {
this.callbacks = {}; // 存放事件的名字
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || [];
this.callbacks[name].push(fn);
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach((cb) => cb(args));
}
}
}
// main.js
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上
// 另一种方式
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能
Children1.vue
this.$bus.$emit('foo')
Children2.vue
this.$bus.$on('foo', this.handle)
parent和root:
$parent 或$root
通过共同祖辈$parent或者$root搭建通信桥连
兄弟组件
this.$parent.on('add',this.add)
另一个兄弟组件
this.$parent.emit('add')
attrs 与 listeners
适用场景:祖先传递数据给子孙
设置批量向下传属性$attrs和 $listeners
包含了父级作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。
可以通过 v-bind="$attrs" 传⼊内部组件
// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>
// parent
<HelloWorld foo="foo"/>
// 给Grandson隔代传值,communication/index.vue
<Child2 msg="lalala" @some-event="onSomeEvent"></Child2>
// Child2做展开
<Grandson v-bind="$attrs" v-on="$listeners"></Grandson>
// Grandson使⽤
<div @click="$emit('some-event', 'msg from grandson')">
{{msg}}
</div>
Provide 与 Inject
在祖先组件定义provide属性,返回传递的值
在后代组件通过inject接收组件传递过来的值
祖先组件
provide(){
return {
foo:'foo'
}
}
后代组件
inject:['foo'] // 获取到祖先组件传递过来的值
Vuex
适用场景: 复杂关系的组件数据传递
Vuex作用相当于一个用来存储共享变量的容器 。
其中:state用来存放共享变量的地方
getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值
mutations用来存放修改state的方法。
actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。常用来做一些异步操作
总结:父子关系的组件数据传递选择 props 与 $emit
进行传递,也可选择ref
兄弟关系的组件数据传递可选择$bus
,其次可以选择$parent
进行传递
祖先与后代组件数据传递可选择attrs与listeners或者 Provide与 Inject
复杂关系的组件数据传递可以通过vuex
存放共享的变量
vue的单向数据流
我们设想这样的情景:
父组件的数据通过props传递给子组件,而子组件里更新了props,导致父组件和其他关联组件的数据更新,UI 渲染也会随数据而更新,毫无疑问,这是会导致严重的数据紊乱和不可控。
因此绝大多数框架在这方面做了处理。而 React 在这方面的处理,就是直接规定了 Props 为只读的,而不是可更改的。这也就是我们前面看到的数据更新不能直接通过 this.state 操作,想要更新,就需要通过 React 提供的专门的 this.setState() 方法来做。
单向数据流 其实就是一种框架本身对数据流向的限制。
暂时先说这些吧,等我们学的越多,经验越丰富,对它的理解也就会越深刻,看待它的角度也就越全面。
在我上面的v-model已经说了单向的原理了。
keep-alive组件(利用keep-alive实现快速页面缓存)
keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
keep-alive可以设置以下props属性:
include - 字符串或正则表达式。只有名称匹配的组件会被缓存
exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
max - 数字。最多可以缓存多少组件实例
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activated与deactivated):
首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > … … > beforeRouteLeave > deactivated
再次进入组件时:beforeRouteEnter >activated > … … > beforeRouteLeave > deactivated
那么我们什么时候使用呢?
使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive
举个例子:
当我们从首页–>列表页–>商详页–>再返回,这时候列表页应该是需要keep-alive
从首页–>列表页–>商详页–>返回到列表页(需要缓存)–>返回到首页(需要缓存)–>再次进入列表页(不需要缓存),这时候可以按需来控制页面的keep-alive
在路由中设置keepAlive属性判断是否需要缓存
{
path: 'list',
name: 'itemList', // 列表页
component (resolve) {
require(['@/pages/item/list'], resolve)
},
meta: {
keepAlive: true,
title: '列表页'
}
}
<div id="app" class='wrapper'>
<keep-alive>
<!-- 需要缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
源码位置:src/core/components/keep-alive.js
可以看到该组件没有template,而是用了render,在组件渲染的时候会自动执行render函数
slot卡槽
在HTML中 slot 元素 ,作为 Web Components 技术套件的一部分,是Web组件内的一个占位符
该占位符可以在后期使用自己的标记语言填充,
通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理
如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情
通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用。比如布局组件、表格列、下拉选、弹框显示内容等。
面试官:说说你对slot的理解?slot使用场景有哪些?_动感超人,的博客-程序员ITS401
技术标签: css web vue js html
一、slot是什么
在HTML中 slot 元素 ,作为 Web Components 技术套件的一部分,是Web组件内的一个占位符
该占位符可以在后期使用自己的标记语言填充
举个栗子
Slot template 1 2 template不会展示到页面中,需要用先获取它的引用,然后添加到DOM中,customElements.define(‘element-details’,
class extends HTMLElement {
constructor() {
super();
const template = document
.getElementById(‘element-details-template’)
.content;
const shadowRoot = this.attachShadow({mode: ‘open’})
.appendChild(template.cloneNode(true));
}
})
在Vue中的概念也是如此
Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口
可以将其类比为插卡式的FC游戏机,游戏机暴露卡槽(插槽)让用户插入不同的游戏磁条(自定义内容)
放张图感受一下
二、使用场景
通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理
如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情
通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用
比如布局组件、表格列、下拉选、弹框显示内容等
slot可以分来以下三种:
默认插槽
子组件用标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面
父组件在使用的时候,直接在子组件的标签内写入内容即可
子组件Child.vue
<template>
<slot>
<p>插槽后备的内容</p>
</slot>
</template>
父组件
<Child>
<div>默认插槽</div>
</Child>
具名插槽
子组件用name属性来表示插槽的名字,不传为默认插槽
父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值
子组件Child.vue
<template>
<slot>插槽后备的内容</slot>
<slot name="content">插槽后备的内容</slot>
</template>
父组件
<child>
<template v-slot:default>具名插槽</template>
<!-- 具名插槽⽤插槽名做参数 -->
<template v-slot:content>内容...</template>
</child>
作用域插槽
子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上
父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用
子组件Child.vue
<template>
<slot name="footer" testProps="子组件的值">
<h3>没传footer插槽</h3>
</slot>
</template>
父组件
<child>
<!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
<template v-slot:default="slotProps">
来⾃⼦组件数据:{
{slotProps.testProps}}
</template>
<template #default="slotProps">
来⾃⼦组件数据:{
{slotProps.testProps}}
</template>
</child>
总结一下:
v-slot属性只能在上使用,但在只有默认插槽时可以在组件标签上使用
默认插槽名为default,可以省略default直接写v-slot
缩写为#时不能不写参数,写成#default
可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}“和定义默认值v-slot=”{user = ‘默认值’}"
vue检测数组和对象(vue.set())
对于对象, Vue 无法检测 property 的添加或移除,由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的.
追问:那如何解决?
使用this.$set(this.someObject,'b',2) 添加新的属性
使用this.$delete(this.someObject,'b') 删除旧属性
。
Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。
然而它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上:Vue.set(vm.obj, ‘e’, 0)
您还可以使用 vm.$set
实例方法,这也是全局 Vue.set 方法的别名:this.$set(this.obj,'e',02)
有时你想向已有对象上添加一些属性,例如使用 Object.assign() 或 _.extend() 方法来添加属性。但是,添加到对象上的新属性不会触发更新。在这种情况下可以创建一个新的对象,让它包含原对象的属性和新的属性:
// 代替 Object.assign(this.obj, { a: 1, e: 2 })
this.obj= Object.assign({}, this.obj, { a: 1, e: 2 })
Vue文档中明确指出的注意事项,由于 JavaScript 的限制,Vue 不能检测出数据的改变,所以当我们需要动态改变数据的时候,Vue.set()完全可以满足我们的需求
Vue.set()不光能修改数据,还能添加数据,弥补了Vue数组变异方法的不足
调用方法:Vue.set( target, key, value )
target参数1: 要修改的对象(或数组,数组里面的值是对象)
key参数2: 属性
value参数3: 属性的值是啥
返回值:已经修改好的值
vue里key的作用
开始之前,我们先还原两个实际工作场景
当我们在使用v-for时,需要给单元加上key。
如果不用key,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse。
如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed
<ul>
<li v-for="item in items" :key="item.id">...</li>
</ul>
用+new Date()生成的时间戳作为key,手动强制触发重新渲染。
当拥有新值的rerender作为key时,拥有了新key的Comp出现了,那么旧key Comp会被移除,新key Comp触发渲染。
<Comp :key="+new Date()" />
key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点、
所以设置key能够大大减少对页面的DOM操作,提高了diff效率
设置key值一定能提高diff效率吗?
其实不然,文档中也明确表示
当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出
建议尽可能在使用 v-for 时提供 key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理插件。它采用集中式存储管理应用的所有组件的状态,而更改状态的唯一方法是提交mutation。
详细见:vuex
nextTick的原理
熟悉 vue 的前端,想必对 vue 里的 nextTick 也很熟悉了,用的时候就知道他是延迟回调,有时候用起来甚至和setTimeout 看起来是同样的效果。但他和setTimeout到底有什么区别?
作用:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
比如:
Html结构
<div id="app"> {{ message }} </div>
构建一个vue实例
const vm = new Vue({
el: '#app',
data: {
message: '原始值'
}
})
修改message
this.message = '修改后的值1'
this.message = '修改后的值2'
this.message = '修改后的值3'
这时候想获取页面最新的DOM节点,却发现获取到的是旧值
console.log(vm.$el.textContent) // 原始值
这是因为message数据在发现变化的时候,vue并不会立刻去更新Dom,而是将修改数据的操作放在了一个异步操作队列中
如果我们一直修改相同数据,异步操作队列还会进行去重
等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行DOM的更新!
nextTick本质是一种优化策略。
如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()
第一个参数为:回调函数(可以获取最近的DOM结构)
第二个参数为:执行函数上下文
// 修改数据
vm.message = '修改后的值'
// DOM 还没有更新
console.log(vm.$el.textContent) // 原始的值
Vue.nextTick(function () {
// DOM 更新了
console.log(vm.$el.textContent) // 修改后的值
})
组件内使用 vm. n e x t T i c k ( ) 实 例 方 法 只 需 要 通 过 t h i s . nextTick() 实例方法只需要通过this. nextTick()实例方法只需要通过this.nextTick(),并且回调函数中的 this 将自动绑定到当前的 Vue 实例上
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '修改后的值'
})
$nextTick() 会返回一个 Promise 对象,可以是用async/await完成相同作用的事情
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
await this.$nextTick()
console.log(this.$el.textContent) // => '修改后的值'
vue-router的两种模式
Vue Router 是Vue官方的路由管理器。
它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。
vue-router 默认 hash 模式, history 模式。
hash 模式的工作原理是 hashchange 事件,可以在window监听 hash 的变化。我们在url后面随便添加一个#xxxx就可以触发这个事件。
比如:
window.onhashchange = function(event){
console.log(event);
// {
// ...
// oldUrl: 'http://xxxx#web',
// newUrl: 'http://xxxx#webxiu',
// }
}
打印对象里面包含两个字段,oldUrl 和 newUrl,这样我们就能根据对应的hash值,做出相应操作。
尽管改变hash,浏览器本身是没有任何请求服务器动作的,但是页面状态和url已经关联起来了,这就是所谓的前端路由,单页应用的标配。
而history模式
是HTML5新增的API
1、history.pushState(data, title [, url]):向历史记录堆栈顶部添加一条记录; data会在onpopstate事件触发时作为参数传递过去;title为页面标题,当前所有浏览器都会 忽略此参数;url为页面地址,可选,缺省为当前页地址;
2、history.replaceState(data, title [, url]) :更改当前的历史记录,参数同上;
3、history.state:用于存储以上方法的data数据,不同浏览器的读写权限不一样;
4、window.onpopstate:响应pushState或replaceState的调用;有了这几个新的API,针对支持的浏览器,我们可以构建用户体验更好的应用了。就像刚提到的Facebook相册,虽然是AJAX的方式,但用户可以直接复 制页面地址分享给好友,
如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。
当你使用 history 模式时,URL 就像正常的 url,例如 http://xxx/webxiu。
不过使用 history 模式时也需要注意,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://xxx/webxiu 就会返回 404,这就是非常不好的。
所以呢,我们需要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。
vue-router有几种导航钩子(路由守卫)
导航”表示路由正在发生改变。
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。
下面介绍一下:
router.beforeEach 注册一个全局前置守卫:
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// to:要去哪个页面
// from:从哪里来
// next:它是一个函数。
// 如果直接放行 next()
// 如果要跳到其它页 next(其它页)
})
router.beforeEach(回调(三个参数))
导航守卫函数中,一定要调用next( )
to.path: to是一个路由对象,path表示路径,是它的一个属性。
比如:
router.beforeEach(async(to, from, next) => {
NProgress.start() // 开启进度条
const token = store.state.user.token
const userId = store.state.user.userInfo.userId
console.log(token, to.path, from.path)
if (token) {
if (to.path === '/login') { // 有 token还要去login
next('/')
NProgress.done() // 关闭进度条
} else { // 有 token,去其他页面,放行
if (!userId) {
await store.dispatch('user/getUserInfo')
}
next()
}
} else {
if (to.path === '/login') { // 没有token,去login,放行
next()
} else {
next('/login') // 没有token,去其他页面
NProgress.done()
}
}
})
router.afterEach 注册一个全局后置钩子:
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
router.afterEach((to, from) => {
// ...
})
在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
路由独享守卫:
你可以在路由配置上直接定义 beforeEnter 守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
组件内的守卫
beforeRouteEnter
beforeRouteUpdate (2.2 新增)
beforeRouteLeave
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
其主要流程;
完整的导航解析流程
导航被触发。
在失活的组件里调用 beforeRouteLeave 守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve 守卫 (2.5+)。
导航被确认。
调用全局的 afterEach 钩子。
触发 DOM 更新。
调用 beforeRouteEnter 守卫中传给 next 的回调函数,
创建好的组件实例会作为回调函数的参数传入。
mixin和mixins的区别
Mixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类。
Mixin类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承。
mixin(混入),提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如data、components、methods、created、computed等等
我们只要将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来
在Vue中我们可以局部混入跟全局混入。
定义一个mixin对象,有组件options的data、methods属性
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
组件通过mixins属性调用mixin对象
Vue.component('componentA',{
mixins: [myMixin]
})
该组件在使用的时候,混合了mixin里面的方法,在自动执行create生命钩子,执行hello方法。
通过Vue.mixin()进行全局的混入
Vue.mixin({
created: function () {
console.log("全局混入")
}
})
使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)
PS:全局混入常用于插件的编写
vue动态组件
1.分析需求:
确定业务需求,把页面中可以复用的结构,样式以及功能,单独
抽离成一个文件,实现复用
2.新建vue文件
如:
3.Vue.component注册组件
main.js中:
// 全局组件
import myVue from './components/MyVue.vue'
.Vue.component('myVue',myVue)
// 局部组件:在要引入的组件中导入封装的组件:
import myVue from './components/MyVue.vue'
// 注册局部组件:
...省略其他
export default {
components:{
myVue
}
}
<style>
</style>
1.子组件需要数据,可以在 props 中接受定义,而子组件修改好数据后,想把数据传递给父组件。可以采用$emit 方
法向外抛数据
2.如果需要给组件传入模板,则定义为插槽 slot
3.如果需要 父组件主动调用子组件的方法 可以在 methods 选项中开放方法
vue中局部作用域样式
vue中哪一个阶段可以发送请求
看实际情况,一般在 created(或beforeRouter) 里面就可以,如果涉及到需要页面加载完成之后的话就用 mounted
vue3.0双向绑定原理
vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,其中比较关键的是数据劫持。
主要是通过Object对象的defineProperty属性,重写data的set和get函数来实现的。
我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成
数据层(Model):应用的数据及业务逻辑
视图层(View):应用的展示效果,各类UI组件
业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
理解vm:
它的主要职责就是:
数据变化后更新视图
视图变化后更新数据
当然,它还有两个主要部分组成
监听器(Observer):对所有数据的属性进行监听
解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数。
主流程:
new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe中
同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数。