Angular/Vue/React 联合教程(三)解耦视图逻辑

当逻辑不在只局限于单一组件的时候,事情变得复杂起来

我相信许多同学都会下意识写出以下代码:

//vue
watch(formModel, (reqData, _, invalidate) => {
  const handle = request(reqData).then(() => {
    // ... some logic
  })
  invalidate(() => {
    handle.cancel
  })
})
 

对啊,监听数据,执行副作用,然后在组件无效时,清除副作用

别说这个没问题,甚至有没有写这个代码的习惯,都会被用来鉴定开发水平,更有甚者,promise 取消回调的问题,都是一个面试考点

但是如果我说这个逻辑是错的呢?来看看下面一段加了点新逻辑的情况:

//vue
const parentData = inject('parentData')
watch(formModel, (reqData, _, invalidate) => {
  const handle = request(reqData).then(() => {
    // ... some local logic
    parentData.requestCount ++
  })
  invalidate(() => {
    handle.cancel()
  })
})
 

请求回调之后,将注入数据的requestCount+1

请问,逻辑上,请求完成没有?

完成了

但是为什么要取消请求的回调?上其他组件拿到错误的数据呢?

因为……组件没了?

组件销毁之后,取消请求回调,是产品经理告诉你的逻辑么?

不是……,是程序运行逼着你写的逻辑

那这就是 —— 废逻辑!

出现这些问题的原因是什么呢?

事件,只能在源头处取消,不能靠取消回调掩耳盗铃!

clearTimeout, removeEventlistener 正确,并且:

// vue
let end = false
watchEffect(()=>{
   // 闭包可以访问
   /*
   * if(end) clearTimeout(timer)
   * 错误,阻碍逻辑运行
   */
   // ...some logic
  if(end) clearTimeout(timer)
  // 正确,逻辑得以运行
})

onBeforeUnmount(()=>{
  end = true
})
 

当然,直接用框架提供的方式更好,这里只是说明一下逻辑:

// vue
watchEffect(invalidate=>{
   invalidate(()=>{
     clearTimeout(timer)
   })
})
 

一句话重申一下:

对于异步事件来说:泼出去的水,已经泼出去了!

不能因为组件销毁与否,阻碍逻辑正常运行

只能取消事件监听

不可取消事件回调

视图逻辑解耦

问题的根源:

image.png

那你说不对,如果回调不取消,产生一些意外变更,怎么办?

你的意外变更

只有可能是 生命周期相关的逻辑

而现在,需要你忘掉生命周期

不要利用生命周期触发业务逻辑

逻辑从业务出发而非技术实现,这是 DDD(领域驱动设计) 的核心思想,之后的文章会进行分享

不用生命周期?

是的,可以试试 React,完全没有生命周期,一样可以开发:

// react
function SomeCompo(){
  useEffect(()=>{
    // 这不是生命周期回调,只是一个当前组件自有事件的初始化发生器
    // 依赖数组为空,代表这个视图被创建,即会执行的逻辑
    // 虽然可以等价生命周期,但是不建议这么做,在这里引用任何状态都会报错(未加入依赖数组)
  },[])
  
  useEffect(()=>{
    // useEffect 只代表一系列值跟随另一一列值变化(变化的函数关系)
    setOtherDataChangeAfterData('dataChanged')
  },[data])
}
 

这个和上一代前端框架的开发模式差距太大(使用 ng+rx 的同学可能比较适应)

难道上一代前端框架利用生命周期无法实现某些逻辑么?

是的,很多逻辑还真的实现不了:

// vue
onMounted(()=>{
  request(someData).then((res)=>{
    localData.value = res
  })
})
 

这是一个简单的,发送请求的逻辑,咋一看好像没什么问题

我们来跨组件一下:

// vue

// parent
let base = ref(null)
requestBaseData((res)=>{
  base.value = res
})
provide('parentBase', {base})

// child
const {base} = inject('parentBase')
onMounted(()=>{
  request(base.value).then((res)=>{
    localData.value = res
  })
})
 

子组件请求时,base 是 null!

mounted 发请求?初始化?显然在跨组件逻辑下,是绝对会扑街的!

因为子组件的请求参数,对父组件的数据产生了依赖!

应该的写法是:

// vue
// child
const {base} = inject('parentBase')
watch(base, (res)=>{
  request(base.value).then((res)=>{
    localData.value = res
  }),
  {
    immediate: false // 默认 false 避开 base 默认值
  }
})
 

那用 mounted 绑定 ref 呢?

一个例子击败你:

// vue
<template>
  <div ref="someRef" v-if="base"/>
</template>
 

ref 一样会产生依赖!

不过,一样可以通过响应式回调解决问题:

// vue
const {base} = inject('parentBase')
const someRef = ref(null)
watch(base,(base)=>{
  if(!base) return
  console.log(someRef.value)
},{
  flush: 'post' // flush post 为视图变化回调
})

// react
const {base} = useContext(ParentBase)
const someRef = useRef(null)
// useLayoutEffect 而不是 useEffect
useLayoutEffect(()=>{
  if(!base) return
  console.log(someRef.current)
},[base, someRef])

// angular
@ViewChild('someRef')
someRef: EelementRef
constructor(parentBase){
  parentBase$
  .pipe(
    filter(res=>res),
    // ng 的变检由事件驱动(zone会代理所有浏览器事件,promise等),与流同步进行
    // 变更会在下个事件循环完成
    // delay(0)即可获取相应视图
    delay(0)
   ).subscribe(()=>{
    console.log(someRef.el)
  })
}
 

绕过所有生命周期,所有变更根据业务逻辑和状态触发

应用就会变成这个样子:

image.png

同时,这也是我说为什么 MVVM 已死的原因

不是说 MVVM 这个模式已死,而是说 ——

现在这个时代,再用生命周期,你就输了~

原创文章,作者:我心飞翔,如若转载,请注明出处:https://www.pipipi.net/12604.html

发表评论

登录后才能评论