什么是递归组件?举个例子说明下?
分析
递归组件我们用的比较少,但是在Tree
、Menu
这类组件中会被用到。
体验
组件通过组件名称引用它自己,这种情况就是递归组件
<template>
<li>
<div> {
{ model.name }}</div>
<ul v-show="isOpen" v-if="isFolder">
<!-- 注意这里:组件递归渲染了它自己 -->
<TreeItem
class="item"
v-for="model in model.children"
:model="model">
</TreeItem>
</ul>
</li>
<script>
export default {
name: 'TreeItem',
// ...
}
</script>
回答范例
- 如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
- 实际开发中类似
Tree
、Menu
这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。 - 使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件
name
属性,用来查找组件定义,如果使用SFC
,则可以通过SFC
文件名推断。组件内部通常也要有递归结束条件,比如model.children
这样的判断。 - 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给
resolveComponent
,这样实际获取的组件就是当前组件本身
原理
递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)
const _component_Comp = _resolveComponent("Comp", true)
就是在传递maybeSelfReference
export function resolveComponent(
name: string,
maybeSelfReference?: boolean
): ConcreteComponent | string {
return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
}
resolveAsset
中最终返回的是组件自身:
if (!res && maybeSelfReference) {
// fallback to implicit self-reference
return Component
}
说说你对slot的理解?slot使用场景有哪些
一、slot是什么
在HTML中 slot
元素 ,作为 Web Components
技术套件的一部分,是Web组件内的一个占位符
该占位符可以在后期使用自己的标记语言填充
举个栗子
<template id="element-details-template">
<slot name="element-name">Slot template</slot>
</template>
<element-details>
<span slot="element-name">1</span>
</element-details>
<element-details>
<span slot="element-name">2</span>
</element-details>
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
位置),作为承载分发内容的出口
二、使用场景
通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理
如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情
通过slot
插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用
比如布局组件、表格列、下拉选、弹框显示内容等
ref和reactive异同
这是Vue3
数据响应式中非常重要的两个概念,跟我们写代码关系也很大
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
const obj = reactive({
count: 0 })
obj.count++
ref
接收内部值(inner value
)返回响应式Ref
对象,reactive
返回响应式代理对象- 从定义上看
ref
通常用于处理单值的响应式,reactive
用于处理对象类型的数据响应式 - 两者均是用于构造响应式数据,但是
ref
主要解决原始值的响应式问题 ref
返回的响应式数据在JS中使用需要加上.value
才能访问其值,在视图中使用会自动脱ref
,不需要.value
;ref
可以接收对象或数组等非原始值,但内部依然是reactive
实现响应式;reactive
内部如果接收Re
f对象会自动脱ref
;使用展开运算符(...
)展开reactive
返回的响应式对象会使其失去响应性,可以结合toRefs()
将值转换为Ref
对象之后再展开。reactive
内部使用Proxy
代理传入对象并拦截该对象各种操作,从而实现响应式。ref
内部封装一个RefImpl
类,并设置get value/set value
,拦截用户对值的访问,从而实现响应式
为什么要使用异步组件
- 节省打包出的结果,异步组件分开打包,采用
jsonp
的方式进行加载,有效解决文件过大的问题。 - 核心就是包组件定义变成一个函数,依赖
import()
语法,可以实现文件的分割加载。
components:{
AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([])
}
原理
export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend
// 第二次渲染时Ctor不为undefined
if (Ctor === undefined) {
return createAsyncPlaceholder( // 渲染占位符 空虚拟节点
asyncFactory,
data,
context,
children,
tag
)
}
}
}
function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void {
if (isDef(factory.resolved)) {
// 3.在次渲染时可以拿到获取的最新组件
return factory.resolved
}
const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true) //2. 强制更新视图重新渲染
} else {
owners.length = 0
}
})
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true forceRender(true)
}
})
const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后
sync = false
return factory.resolved
}
Watch中的deep:true是如何实现的
当用户指定了
watch
中的deep属性为true
时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前watcher
存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新
源码相关
get () {
pushTarget(this) // 先将当前依赖放到 Dep.target上
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${
this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
// 如果需要深度监控
traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法
}popTarget()
}
Vue-router 除了 router-link 怎么实现跳转
声明式导航
<router-link to="/about">Go to About</router-link>
编程式导航
// literal string path
router.push('/users/1')
// object with path
router.push({
path: '/users/1' })
// named route with params to let the router build the url
router.push({
name: 'user', params: {
username: 'test' } })
回答范例
vue-router
导航有两种方式:声明式导航和编程方式导航- 声明式导航方式使用
router-link
组件,添加to
属性导航;编程方式导航更加灵活,可传递调用router.push()
,并传递path
字符串或者RouteLocationRaw
对象,指定path
、name
、params
等信息 - 如果页面中简单表示跳转链接,使用
router-link
最快捷,会渲染一个a标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航 - 实际上内部两者调用的导航函数是一样的
参考 前端进阶面试题详细解答
怎么实现路由懒加载呢
这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效
// 将
// import UserDetails from './views/UserDetails'
// 替换为
const UserDetails = () => import('./views/UserDetails')
const router = createRouter({
// ...
routes: [{
path: '/users/:id', component: UserDetails }],
})
回答范例
- 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段
- 一般来说,对所有的路由都使用动态导入是个好主意
- 给
component
选项配置一个返回Promise
组件的函数就可以定义懒加载路由。例如:{ path: '/users/:id', component: () => import('./views/UserDetails') }
- 结合注释
() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
可以做webpack
代码分块
组件中写name属性的好处
可以标识组件的具体名称方便调试和查找对应属性
// 源码位置 src/core/global-api/extend.js
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub // 记录自己 在组件中递归自己 -> jsx
}
Vue组件为什么只能有一个根元素
vue3
中没有问题
Vue.createApp({
components: {
comp: {
template: `
<div>root1</div>
<div>root2</div>
`
}
}
}).mount('#app')
vue2
中组件确实只能有一个根,但vue3
中组件已经可以多根节点了。- 之所以需要这样是因为
vdom
是一颗单根树形结构,patch
方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
vue3
中之所以可以写多个根节点,是因为引入了Fragment
的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment
节点,把多个根节点作为它的children
。将来patch
的时候,如果发现是一个Fragment
节点,则直接遍历children
创建或更新
Vue 修饰符有哪些
vue中修饰符分为以下五种
- 表单修饰符
- 事件修饰符
- 鼠标按键修饰符
- 键值修饰符
v-bind
修饰符
1. 表单修饰符
在我们填写表单的时候用得最多的是input
标签,指令用得最多的是v-model
关于表单的修饰符有如下:
lazy
在我们填完信息,光标离开标签的时候,才会将值赋予给value
,也就是在change
事件之后再进行信息同步
<input type="text" v-model.lazy="value">
<p