共计 3021 个字符,预计需要花费 8 分钟才能阅读完成。
场景分析
Vue 的模板语法适用于绝大部分的需求场景(模板最终会被编译为渲染函数),在绝大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力,举例如下:
1. 不确定层级的菜单
假设设计一个开源的后台管理系统,侧边栏菜单需要根据路由自动生成菜单,由于系统可能会被用于不同的功能需求。所以路由的层级、数量都是不确定的。
如果通过模板语法来写,假设路由最多只有三层,我们当然可以在模板内通过 if 加循环来适配所有需求场景,但是实际场景并非如此。
2. 组织架构
组织架构的常见实现就是 Tree 组件,Tree 组件的特点之一就是没有确定数量的数据、没有确定数量的层级。此处可以思考一下,如果使用模板语法该如何去实现这样的一个功能组件?
3. 总结分析
通过渲染函数,对于以上的例子我们完全可以通过递归满足生成任意层级、数量的菜单栏、Tree 分支。(此处不作具体展开)。
我们可以先推出结论:模板适用于“组件结构是确定的”这种需求场景,此处的确定可以简单理解为:“嵌套的层级是确定的”,在这种情况下模板语法比渲染函数更加简单易用。但是当组件结构层级不确定时,渲染函数显然更加合适。
使用渲染函数
1. 选项式 API
// 选项式 API
export default {props: ['message'],
render() {
return [//
h('div', this.$slots.default()),
//
h(
'div',
this.$slots.footer({text: this.message})
)
]
}
}
2. 组合式 API
export default {props: ['message'],
setup(props, { slots}) {return () => [
// 默认插槽://
h('div', slots.default()),
// 具名插槽://
h(
'div',
slots.footer({text: props.message})
)
]
}
}
使用总结
1.vNode 必须唯一
同一个 vNode 对象,不能被多次用于渲染函数,必须保证 vNode 的唯一性;
2.v-model 需要自己实现
v-model 语法糖会被拆分为 modelValue 和 onUpdate:modelValue 事件,在渲染函数中需要我们自己实现双向绑定的逻辑处理;
3. 传递插槽
// 单个默认插槽
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
h(FormItem,null,()=>{default:h("div")}) // 对象
h(FormItem,null,()=>h("div")) // 单个 VNode
h(FormItem,null,()=>[h("div")]) // 数组
需要注意的是如果渲染普通的 html 标签时,不能返回对象格式(会导致无法渲染,并且不报错);
// 这样子不会被渲染,估计是普通的 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 对象不会转换成原数组,然后保持响应式传递给被渲染的组件。
这个过程需要我们自己完成(触发渲染函数的依赖收集机制)。测试如下:
//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. 这样也会收集依赖
() => h(components[item.type],
Object.assign({value: props.data[item.key] },
item.attr,
options.data.length == 0 ? {} : {options: options.data}))
)));
7. 依赖收集
/* 这样会收集,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 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失:
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 对象被替换了,变成没有代理的对象了。
2. 绑定事件
事件绑定和属性是一样的,只不过事件属性需要以 on 开始,例如 onUpdate:value,监听的就是 update:value 事件。
3. 渲染的时机
每次依赖更新的时候,都会重新调用渲染函数然后刷新 DOM,简单说就是 setup 只会运行一次,渲染函数每次刷新的时候都会调用。