场景分析
Vue的模板语法适用于绝大部分的需求场景(模板最终会被编译为渲染函数),在绝大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力,举例如下:
1.不确定层级的菜单
假设设计一个开源的后台管理系统,侧边栏菜单需要根据路由自动生成菜单,由于系统可能会被用于不同的功能需求。所以路由的层级、数量都是不确定的。
如果通过模板语法来写,假设路由最多只有三层,我们当然可以在模板内通过if加循环来适配所有需求场景,但是实际场景并非如此。
2.组织架构
组织架构的常见实现就是Tree组件,Tree组件的特点之一就是没有确定数量的数据、没有确定数量的层级。此处可以思考一下,如果使用模板语法该如何去实现这样的一个功能组件?
3.总结分析
通过渲染函数,对于以上的例子我们完全可以通过递归满足生成任意层级、数量的菜单栏、Tree分支。(此处不作具体展开)。
我们可以先推出结论:模板适用于“组件结构是确定的” 这种需求场景,此处的确定可以简单理解为:“嵌套的层级是确定的”,在这种情况下模板语法比渲染函数更加简单易用。但是当组件结构层级不确定时,渲染函数显然更加合适。
使用渲染函数
1.选项式API
代码语言:javascript复制//选项式API
export default {
props: ['message'],
render() {
return [
// <div><slot /></div>
h('div', this.$slots.default()),
// <div><slot name="footer" :text="message" /></div>
h(
'div',
this.$slots.footer({
text: this.message
})
)
]
}
}
2.组合式API
代码语言:javascript复制export default {
props: ['message'],
setup(props, { slots }) {
return () => [
// 默认插槽:
// <div><slot /></div>
h('div', slots.default()),
// 具名插槽:
// <div><slot name="footer" :text="message" /></div>
h(
'div',
slots.footer({
text: props.message
})
)
]
}
}
使用总结
1.vNode 必须唯一
同一个vNode对象,不能被多次用于渲染函数,必须保证vNode的唯一性;
2.v-model需要自己实现
v-model语法糖会被拆分为modelValue和onUpdate:modelValue事件,在渲染函数中需要我们自己实现双向绑定的逻辑处理;
3.传递插槽
代码语言:javascript复制// 单个默认插槽
h(MyComponent, () => 'hello')
// 具名插槽
// 注意 `null` 是必需的
// 以避免 slot 对象被当成 prop 处理
h(MyComponent, null, {
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
})
4.渲染子元素
对于组件的子元素,每一个非纯字符串的子元素都应该通过传递一个返回Vnode的函数来指定,函数返回值可以是vNode、Vnode数组、插槽对象表示的vNode
代码语言:javascript复制h(FormItem,null,()=>{default:h("div")}) //对象
h(FormItem,null,()=>h("div")) //单个VNode
h(FormItem,null,()=>[h("div")]) //数组
需要注意的是如果渲染普通的html标签时,不能返回对象格式(会导致无法渲染,并且不报错);
代码语言:javascript复制//这样子不会被渲染,估计是普通的html没有插槽的概念
return h("div",null,{default:()=>h(Item)}
//这样可以
return h("div",null,()=>[h(Item)])
return h("div",null,()=>h(Item))
5.渲染函数的依赖收集
假设组件某属性需要的是Array,通过Ref包装一个数组,直接把这个Ref传递给组件,组件会报错提示需要的是数组,得到的是对象,说明渲染函数中ref 对象不会转换成原数组,然后保持响应式传递给被渲染的组件。
这个过程需要我们自己完成(触发渲染函数的依赖收集机制)。测试如下:
代码语言:javascript复制//item是一个ref,这样会触发依赖收集保持响应式
h("input",{value:item.value});
//这样就不会
let attr={
value:item.value
}
h("input",attr);
//这样才可以
let attr={
value:item.value
}
h("input",Object.assign({},attr));
经过测试,在渲染函数内被调用的ref,reactive对象都会收集依赖保持响应式,在渲染函数调用前定义 let attr={ value:item.value },在这个过程没有依赖收集,value被赋值的是一个普通的值,所以不会具有响应性(直接传递ref对象,会导致类型错误)。
6.这样也会收集依赖
代码语言:javascript复制 () => h(
components[item.type],
Object.assign(
{value: props.data[item.key] },
item.attr,
options.data.length == 0 ? {} : {options: options.data}))
)));
7.依赖收集
代码语言:javascript复制/* 这样会收集,options改变会进行响应 */
Object.assign(
{
value: props.data[item.key]
}, item.attr,
isRef(item.attr.options) ? {options: item.attr.options.value} : {},
options.data.length == 0 ? {} : {options: options.data}))
/* 这样options改变不会进行响应 */
Object.assign(
{
value: props.data[item.key]
},
Object.assign(item.attr, isRef(item.attr.options) ? {options: item.attr.options.value} : {}),
options.data.length == 0 ? {} : {options: options.data}))
其它的知识
1.reactive
reactive() API 有两条限制:仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。
Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失:
代码语言:javascript复制let a=reactice({
b:{c:1}
})
a.b.c ; //响应性保持
let c=a.b.c;
c ; //c已经独立了,没有响应性
let c=a.b;
c.c ; //还保持着引用,响应性存在
let d=a.b;
d={c:1};
d.c ; //这就没了,因为d整个Proxy对象被替换了,变成没有代理的对象了。