左手原生右手框架
乍一看,vue 和 react 明显的区别可能是:
vue 是自动挡,而 react 是手动挡。
想要在这两框架间分出高下的人不在少数,且根据观察,开手动挡的鄙视开自动挡的大有人在,不信你可以去问问驾校教练。
再聊一下前端原生三剑客 html + js + css,原生和这两个框架相比应该有两个最基本的短板:
-
第一点发生在渲染列表的时候,需要复用条目 dom ,以及更新 innerText 还有事件的挂载卸载等,原生得自己解决这个问题。框架不知是否有解决这个问题,但用 v-for 或者 for 循环的时候框架编译器会提示少了 key,不过这个解决的也可能只是虚拟 dom 树这一层的复用。
当有海量条目的时候,还需要不断剥离滚动区域外的 dom ,插入到当前视图滚动方向的末尾,以做性能优化,这个原生还得自己解决,框架则可以方便地利用一些第三方 ui 库。
-
第二点是在补足上面短板问题的前提下,在数据变化后,最小化地更新 dom ,这一点也是框架要解决的主要问题,为此不惜在内存创建了虚拟 dom 树,以及运用相应的算法策略。如果用原生的话,在具体的项目中,解决此类问题,可以做到精准而高效,但要构建成通用的优化策略,仍需做很多额外的工作,因此也算是一个短板。
精准而优雅
上面这一段看似想要抛砖引玉,实则是为了引出前端开发中的两个进阶问题,优雅的复用 dom 和数据的精准化更新。
首先说如何优雅的复用 dom, 优雅这个词还是有点抽象,其实可以先用一个具象化一点的词代替,简洁。
因为简洁和优雅总是被一块提及,简洁应算优雅的第一大特征。
对框架来说,天然就自带复用 dom 机制,一个组件被引入一次都只会产生一个 dom 实例。
简洁复用 dom 的前提在于,深刻理解组件使用的业务场景,比如某些弹窗,如果很确定不会产生同时出现两个弹窗的场景,则可将其做成全局单例的模式。
很多前端项目,存在大量的代码堆砌,有点像上学应付老师写的口水文,洋洋洒洒一大段,字数很多,但就是阅后无感,甚至有想骂街的冲动。
归根结底,一方面是写得少,另一方面是没有自己的思考,或借鉴别人的思考。
只有不断的提高练习时长,才能练就优雅的转身,动作一气呵成,在删减代码量的同时,提高可读性。
ok,练习时长够了,接下来离实现优雅还有另一个重要特征,灵活。
就像运球一样,肩膀的灵活性也必不可少。
代码上来讲,就是完成功能抽象的同时,还能和组件使用场景高度解耦。
比如之前说的全局单例组件,在封装完毕后,这时突然又出现了需要同时产生两个实例的场景,那么原有组件应该改造量极小,或者可以直接复用。
如果用原生来写组件,就像捏橡皮泥,可以捏成自己想要的形状,灵活性拉满了。
但工作中,作为团队一份子,我们还得基于框架去做灵活性实现。
那在使用框架的前提下,既要简洁,又要灵活,还能实现数据的精准化更新,看哪方面呢?
比较看框架的使用年限,即架龄。
排水渠过弯
下面仅限表达个人观点。
vue 和 react 经过多年内卷,形成了从类组件到函数组件,从面向对象到到组合式设计的转变过程,不断给社会的底层牛马输送着新鲜的养分。
年轻的牛马都是负重前行,老牛马就应该学点弯道超车的技术。
下面教大家如何在实践中,利用排水渠过弯,在秋名山一骑绝尘。
首先要做的,是尽量避免对模板语法的依赖,不再通过模板语法进行 props 传值,实践证明这种使用方式,看似合理,实际呆板低效。
其次,要尽量减少甚至杜绝传统 store 机制的使用,这种机制一般用来存储和更新全局响应状态,在项目上,我们就要放弃 pinia,redux,mobx 这类插件的引入。
那如何补充缺失的 store 机制呢?
vue3示例
对 vue3 来说,因为直接利用了现代浏览器提供的 Proxy 这一天然的排水渠,设计上不仅相比上代版本更加简洁,使用上也更加灵活了,下面是展示一个示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { createApp, ref, h } from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js';
const ViewData = ref([1,2,3]);
setTimeout(() => {
ViewData.value = [4,5,6];
}, 2000);
const ViewToBeUpdatedForSure = {
render(){
// 非数组演示
return h("div",{ innerHTML: ViewData.value.join("<br/>") })
}
}
createApp({
render:() => h("div",[
h("div","无聊的不会变化的title"),
ViewToBeUpdatedForSure.render()
])
}).mount(app);
</script>
</body>
</html>
通过使用 getter 来监听数据的访问,将数据和组件形成一对多的订阅定关系。
当一个数据的 setter 被触发,所有组件订阅的视图更新操作也会触发。
同时,其他未订阅相关数据的组件则完全不受影响。
项目中,ViewData 这些需要产生视图响应的数据可以放在一个单独的文件中保存,以代替原来的 store 机制,同时对外导出这些属性的 setter 方法。
react示例一
React 因为是手动挡框架,实现同样的的 store 机制,离不开离合器。
因为前文说了要尽量避免引入 redux 或者 mobx 这种预制离合器。
放弃 props 的前提下,意味着要使用 context,而精准更新,意味着要写一个订阅器,用来实现数据修改时,所有订阅的组件会自动触发更新。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script>
const h = React.createElement;
const ctx = React.createContext(null);
function useDataProxyToBindCom(data){
let snapshotData = data;
let updaters = [];
function subscribe(updater){
updaters.push(updater);
return () => updaters.splice(updaters.indexOf(updater),1);
}
function getSnapshot(){
return snapshotData;
}
function trigger(newData){
snapshotData = newData;
for(let updater of updaters){
updater();
}
}
function dataProxyBinder(childs){
const dataRefs = React.useSyncExternalStore(subscribe,getSnapshot);
return h(ctx.Provider,{value:snapshotData},childs);
}
return [trigger, dataProxyBinder]
}
const listData = [1,2,3];
const [setListData,dataProxyBinder] = useDataProxyToBindCom(listData);
const TitleNoNeedToUpdate = h("div", null, "无聊的不会变化的title");
const ComBindListData = h(function (){
const list = React.useContext(ctx);
return h("div", null,!list ? "" : list.map(e => h("div",null,e)) );
});
const ViewToBeUpdatedForSure = h(function(){
return dataProxyBinder(ComBindListData);
})
setTimeout(() => {
setListData([4,5,6]);
}, 2000);
const App = h("div", null, [
TitleNoNeedToUpdate,
ViewToBeUpdatedForSure,
// 内嵌
h("div",null,ViewToBeUpdatedForSure)
])
ReactDOM.render(App, app);
</script>
</body>
</html>
这里使用了 React.useSyncExternalStore 来实现状态管理,当然也可以用官方提供的其他 hook 比如 useReducer 实现同样的效果。
显然,和 Vue3 相比还是不够简洁,这离合器还得改造。
那么可以效仿 Vue3 借助浏览器自带的 Proxy 来让使用变得更简洁。
react示例二
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script>
// 组件注册器,仅单一场景演示
const data2ComRegister = {
set(rEle){
if(!this._proxyMap){
this._proxyMap = new Map();
}
this._proxyMap.set(rEle,this._tmpProxy);
},
get(rEle){
return this._proxyMap.get(rEle);
}
};
const h = function(...args) {
const reactEle = React.createElement(...args);
if(typeof args[0] === "function"){
args[0]();
data2ComRegister.set(reactEle);
return React.createElement(function(){
return data2ComRegister.get(reactEle).dataProxyBinder(args[0]());
})
}
return reactEle;
}
function ref(data){
// 仅单一场景演示
let snapshotData = data;
const handler = {
set:function(obj,prop,value){
trigger(value)
},
get: function (obj, prop) {
if(prop === "value"){
// 仅单一场景演示
data2ComRegister._tmpProxy = dataProxy;
return snapshotData;
}else if(prop === "dataProxyBinder"){
return dataProxyBinder;
}
}
}
const dataProxy = new Proxy({},handler)
data2ComRegister._tmpProxy = dataProxy;
let updaters = [];
function subscribe(updater){
updaters.push(updater);
return () => updaters.splice(updaters.indexOf(updater),1);
}
function getSnapshot(){
return snapshotData;
}
function trigger(newData){
snapshotData = newData;
for(let updater of updaters){
updater();
}
}
function dataProxyBinder(childs){
const dataRefs = React.useSyncExternalStore(subscribe,getSnapshot);
return h("div",null,childs);
}
return dataProxy;
}
const listData = ref([1,2,3]);
const TitleNoNeedToUpdate = h("div", null, "无聊的不会变化的title");
const ViewToBeUpdatedForSure = h(function (){
const list = listData.value;
return h("div", null,!list ? "" : list.map(e => h("div",null,e)) );
});
setTimeout(() => {
listData.value = [4,5,6];
}, 2000);
const App = h("div", null, [
TitleNoNeedToUpdate,
ViewToBeUpdatedForSure,
// 内嵌
h("div",null,ViewToBeUpdatedForSure)
])
ReactDOM.render(App, app);
</script>
</body>
</html>
上面只是单一场景的简单示例,实际要管理的场景要更复杂。
但基本体现了函数式组件的核心绑定机制,即在 render 函数中实现数据到视图的绑定。
最后,道路千万条,优雅第一条,希望各位都能在前端领域找到自己的专属银弹。