concent v2
版本的發佈了,在保留了和v1
如出一轍的api使用方式上,內置了依賴收集系統,支持同時從狀態、計算結果和反作用3個維度收集依賴,創建其精確更新路徑,固然也保留了v1
的依賴標記特性,支持用戶按需選擇是讓系統自動收集依賴仍是人工管理依賴,大多數場景,推薦使用自動收集依賴,除非很是在乎渲染期間自動收集和更新依賴的那一點微弱的額外計算以及很是清楚本身組件對狀態的依賴關係,那麼能夠降級爲人工標記依賴,固然了,若是是v1
版本,那就沒得選了,只能是人工標記了。html
在正式瞭解依賴收集以前,咱們先會細聊一下組件編程體驗統一這個話題,本質來講concent
並無刻意的要統一類組件和函數組件的編碼方式,只是基於爲組件實例注入標記了元數據的實例上下文ref ctx
的核心運行機制,隨着迭代的進行,發現了組件的形態已再也不那麼重要,它們表達的都是react vdom
,並最終會被react-dom
轉換成的真實的html dom
渲染到瀏覽器窗口裏,react開發者針對hook
也說過,hook
並無改變react的本質,只是換了一種編碼方式書寫組件而已,包括狀態的定義和生命週期的定義,均可以在類組件和函數組件的不一樣表達代碼裏一一映射。vue
class ClassComp extends React.Component{
constructor(props, context){
super(props, context);
this.state = {tag:props.tag, name:''}
}
}
function FnComp(props){
const propsTag = props.tag;
const [tag, setTag] = useState(propsTag);
const [name, setName] = useState('');
}
複製代碼
class ClassComp extends React.Component{
componentDidMount(){
//組件初次掛載觸發
}
}
function FnComp(){
React.useEffect(()=>{
//組件初次掛載觸發
}, [])
}
複製代碼
class ClassComp extends React.Component{
componentDidUpdate(){
//組件存在期渲染完畢觸發
}
}
function FnComp(){
const efFlag = React.useRef(0);
React.useEffect(()=>{
efFlag.current++;
if(efFlag.current>1){
//組件存在期渲染完畢觸發
}
})
}
複製代碼
class ClassComp extends React.Component{
componentWillUnmount(){
//組件卸載前觸發
}
}
function FnComp(){
React.useEffect(()=>{
return ()=>{
//組件卸載前觸發
}
}, [])
}
複製代碼
class SomePage extends Component{
static getDerivedStateFromProps (props, state) {
if (props.tag !== state.tag) return {tag: props.tag}
return null
}
}
function FnComp(props){
const propsTag = props.tag;
const [tag, setTag] = useState(propsTag);
React.useEffect(()=>{
// 首次渲染時,此反作用仍是會執行的,在內部巧妙的再比較一次,避免一次多餘的ui更新
// 等價於上面組件類裏getDerivedStateFromProps裏的邏輯
if(tag !== propsTag)setTag(tag);
}, [propsTag, tag]);
}
複製代碼
既然他們本質上只是表達方式的不一樣,concent
經過setup
只在組件初次渲染前執行一次的特性,開闢另外一個空間,完美和諧的統一他們的表達方式,而且還順帶額外提供其餘可選的特性給開發者使用。react
這裏提早申明一下,下面的代碼演示setup特性以及相關生命週期統一的函數是都是可選的,並不是必定要這樣編碼才能接入concent,你依然能夠按照最傳統的方式組織代碼,使用setState就能夠了git
如下舉一個實戰例子:es6
const api = {
async fetchProducts() {
return {
products: [
{name:'name_'+Math.random(), author:'zzk_invoke'},
{name:'name_'+Math.random(), author:'concent_invoke'},
]
};
}
};
export const setup = ctx => {
//初始化props.tag到state裏,initState會自動作合併
ctx.initState({ tag: ctx.props.tag });
const fetchProducts = () => {
const { type, sex, addr, keyword } = ctx.state;
api.fetchProducts({ type, sex, addr, keyword })
.then(({products}) => ctx.setState({ products }))
.catch(err => alert(err.message));
};
ctx.effect(() => {
fetchProducts();
}, ["type", "sex", "addr", "keyword"]);
/** 原函數組件內寫法: useEffect(() => { fetchProducts(type, sex, addr, keyword); }, [type, sex, addr, keyword]); */
ctx.effect(() => {
return () => {
// 返回一個清理函數
// 等價於componentWillUnmout, 這裏搞清理事情
};
}, []);
/** 原函數組件內寫法: useEffect(()=>{ return ()=>{// 返回一個清理函數 // 等價於componentWillUnmout, 這裏搞清理事情 } }, []);//第二位參數傳空數組,次反作用只在初次渲染完畢後執行一次 */
ctx.effectProps(() => {
// 對props上的變動書寫反作用
const curTag = ctx.props.tag;
if (curTag !== ctx.prevProps.tag) ctx.setState({ tag: curTag });
}, ["tag"]);
/** 原函數組件內寫法: useEffect(()=>{ // 首次渲染時,此反作用仍是會執行的,在內部巧妙的再比較一次,避免一次多餘的ui更新 // 等價於上面組件類裏getDerivedStateFromProps裏的邏輯 if(tag !== propTag)setTag(tag); }, [propTag, tag]); */
return {
// 返回結果收集在ctx.settings裏
fetchProducts,
fetchByInfoke: () => ctx.invoke(api.fetchProducts),
//推薦使用此方式,把方法定義在settings裏
changeType: ctx.sync("type")
};
};
複製代碼
定義一個初始化狀態函數github
export const iState = () => ({
products: [],
type: "",
sex: "",
addr: "",
keyword: "",
tag: ""
});
複製代碼
如今咱們來看看組件長什麼樣子吧編程
import { useConcent } from 'concent';
const ConcentFnPage = React.memo(function(props) {
// useConcent返回ctx,這裏直接解構ctx,拿想用的對象或方法
const { state, settings, sync } = useConcent({ setup, state: iState, props });
// 渲染須要的數據
const { products, type, sex, addr, keyword, tag } = state;
// 裝配好的方法
const { fetchProducts, fetchByInfoke } = settings;
return <div>... your ui ... </div>
});
複製代碼
import { register } from 'concent';
@register({ setup, module:'product' })
class ConcentFnModuleClass extends React.Component{
render(){
const { state, settings, sync } = this.ctx;
const { products, type, sex, addr, keyword, tag } = state;
const { fetchProducts, fetchByInfoke } = settings;
return <div>... your ui ... </div>
}
}
複製代碼
點我查看上述示例api
經過觀察發現,是否是長得如出一轍呢?惟一不一樣的是實例上下文在類組件裏經過this.ctx
得到,在函數組件裏經過useConcent
返回,並且setup
相比傳統的函數組件帶來了幾大優點數組
settings
裏返回給用戶使用,沒有了每一輪渲染都生成臨時閉包函數的多餘消耗以及其餘值捕獲陷阱、useCallback
進一步封裝等問題。concent
自動維護着一個上一刻狀態和當前狀態的引用,同構淺比較直接決定要不要觸發反作用函數下面一個示例演示閉包陷阱和使用setup
後如何避免此問題,且複用在類與函數組件之間瀏覽器
// 這是一個普通的函數組件
function NormalDemo() {
const [count, setCount] = useState(0);
const dom = useRef(null);
useEffect(() => {
const cur = dom.current;
const add = () => setCount(count + 1);
cur.addEventListener("click", add);
return () => cur.removeEventListener("click", add);
}, [count]);//須要顯示傳遞count值做爲依賴
return <div ref={dom}>normal {count}</div>;
}
//定義一個setup函數
const setup = ctx => {
const addCount = () => {
const count = ctx.state.count;
ctx.setState({ count: count + 1 });
};
// 由於鎖住了count在ctx.state裏,這裏不須要重複綁定和去綁定click事件了
ctx.effect(() => {
const cur = ctx.refs.dom.current;
cur.addEventListener("click", addCount);
return () => cur.removeEventListener("click", addCount);
}, []);
return { addCount };
};
function ConcentFnDemo() {
const { useRef, state, settings } = useConcent({
setup,
state: { count: 0 }
});
return (
<div> <div ref={useRef("dom")}>click me fn {state.count}</div> <button onClick={settings.addCount}>add</button> </div>
);
}
// or @register({setup, state:{count:0}})
@register({ setup })
class ConcentClassDemo extends React.Component {
state = { count: 0 };
render() {
const { useRef, state, settings } = this.ctx;
// this.ctx.state === this.state
return (
<div> <div ref={useRef("dom")}>click me class {state.count}</div> <button onClick={settings.addCount}>add</button> </div>
);
}
}
複製代碼
經過上面的示例代碼咱們發現,協調類組件和函數組件的共享和複用業務邏輯的方式是如此的簡單與輕鬆,但這並非必需的,你依然能夠像傳統方式同樣爲類組件和函數組件組織代碼,不過僅僅是多了一種更棒的方式提供給你罷了。
咱們已提到依賴收集,首先會想到vue
框架,依賴收集做爲其核心驅動視圖精確的原理,讓很多react
須要人工維護shouldComponentUpdate
,useCallback
等額外api才能寫出性能更好的react代碼眼饞,不論是vue2
的defineProperty
和vue3
的proxy
,本質上都能隱式的收集視圖對數據的依賴關係來作到精確更新。
那麼concent
又怎樣來實現依賴收集呢?仍是離不開咱們提到的實例上下文,它將做爲咱們收集到依賴的重要媒介,來幫助咱們毫無違和感的書寫具備依賴收集的react代碼。
爲何說毫無違和感?由於你書寫的代碼和原始react代碼並無區別,依然保持react的味道。
咱們定義一個普通的Concent組件
run();
const iState = ()=>({firstName:'Jim', lastName:'Green'});
function NormalPerson(){
const { state, sync } = useConcent({state:iState});
return (
<div className="box">
<input value={state.firstName} onChange={sync('firstName')} />
<input value={state.lastName} onChange={sync('lastName')} />
</div>
);
}
複製代碼
若是咱們渲染這兩個組件的話,它們的狀態是各自獨立的
export default function App() {
return (
<div className="App"> <NormalPerson /> <NormalPerson /> </div>
);
}
複製代碼
咱們提高一下狀態,讓全部示例共享 定義一個模塊名爲login
const iState = ()=>({firstName:'Jim', lastName:'Green'});
run(
{
login: {// 定義login模塊
state: iState, // 傳遞狀態初始化函數,固然了這裏也能夠傳對象
}
}
);
複製代碼
而後指定組件屬於login
模塊
function SharedPerson(){
const { state, sync } = useConcent('login');
return (
<div className="box">
<input value={state.firstName} onChange={sync('firstName')} />
<input value={state.lastName} onChange={sync('lastName')} />
</div>
);
}
複製代碼
渲染它們看看效果吧
點我查看此在線示例是否是提高狀態是從沒有感受過如此輕鬆愜意,無Provider
包裹根組件,僅僅只是標記模塊,就完成了狀態提高和共享,示例是爲了方便使用sync
,若是咱們更傳統一點,應該是這樣的
const { state, setState } = useConcent('login');
const changeFirstName = (e)=> setState({firstName: e.target.value})
<input value={state.firstName} onChange={changeFirstName} />
複製代碼
固然對於類組件也是同樣的,並無任何改變你認知的react組件形態
@register('login')
class SharedPersonC extends React.Component{
changeFirstName = (e)=> this.setState({firstName: e.target.value})
render(){
const { state, sync } = this.ctx;
return (
<div className="box">
<input value={state.firstName} onChange={this.changeFirstName} />
<input value={state.lastName} onChange={sync('lastName')} />
</div>
);
}
}
複製代碼
事實上this.state
上能夠定義額外的key做爲私有狀態
@register('login')
class SharedPersonC extends React.Component{
// 由於privKey並非模塊裏的key,因此這個key的狀態變動僅影響當前實例,
// 並不會派發到其餘同屬於login模塊的實例
state = {privKey:'key1'}
render(){
// this.state
// {firstName:'', lastName:'', privKey:'key1'}
}
}
複製代碼
若是咱們不喜歡共享狀態狀態合併到this.state
,那就使用connect
就行了,connect
支持傳遞數組意味着能夠跨多個模塊消費共享數據。
function SharedPerson(){
const { connectedState, sync } = useConcent({connect:['login']});
// connectedState.login.firstName
}
@register({connect:['login']})
class SharedPersonC extends React.Component{
render(){
const { connectedState, sync } = this.ctx;
// connectedState.login.firstName
}
}
複製代碼
鋪墊了這麼久,咱們說的依賴收集在哪裏,體如今何處,不要慌,咱們給組件加個開關,控制firstName
和lastName
是否顯示
const spState = () => ({ showF: true, showL: true });
function SharedPerson() {
const { state, sync, syncBool } = useConcent({
module: "login",
state: spState
});
return (
<div className="box">
{state.showF ? (
<input value={state.firstName} onChange={sync("firstName")} />
) : (
""
)}
{state.showL ? (
<input value={state.lastName} onChange={sync("lastName")} />
) : (
""
)}
<br />
<button onClick={syncBool('showF')}>toggle showF</button>
<button onClick={syncBool('showL')}>toggle showL</button>
</div>
);
}
複製代碼
若是咱們實例化2個實例,將第一個showF
值置爲false,意味着視圖裏再也不有讀取state.firstName
的行爲,那麼當前組件的依賴列表裏僅有lastName
一個字段了,咱們在另外一個組件實例裏對lastName
輸入新內容時,會觸發第一個實例渲染,可是對firstName
輸入新內容時不該該觸發第一個實例渲染,如今咱們看看效果吧。
固然了用戶必定會有一個疑問,實例1不觸發更新,那麼當我須要用這個firstName
時,是否是已通過期了,的確,若是你切換實例1的showF
爲true,stata.firstName
會拿到最新的值渲染,可是若是你不切換,而是直接點擊實例1的某個按鈕直接用firstName
做業務邏輯處理的話,從state.firstName
取到的的確是舊值,你只需從ctx.moduleState
上去取就解決了,取到的值必定是最新值,由於全部屬於login
模塊的實例的moduleState
指向的是同一個對象,固然就不存在值過時的問題,固然你能夠一開始在視圖裏使用模塊數據時,就從moduleState
裏取(同樣能收集到依賴),而不是從合併後的state
上取,就不會形成渲染邏輯從state
取而業務邏輯從moduleState
裏取同一個值的違和感了。
咱們知道concent是支持定義計算函數的,分爲實例級別的計算和模塊級別的計算,咱們一個個來講
首先咱們經過setup
一次性定義好實例計算函數,而後交給useConcent
const setup = ctx=>{
ctx.computed('fullName', (newState, oldState)=>{
return `${newState.firstName}_${newState.lastName}`;
})
}
const spState = () => ({ showF: true, showL: true });
function SharedPerson() {
// 從refComputed取實例計算結果
const { state, sync, ccUniqueKey, syncBool, refComputed } = useConcent({
module: "login",
state: spState,
setup,
});
console.log(`%c${ccUniqueKey}`, "color:green");
return (
<div className="box">
{state.showF ? (
<input value={state.firstName} onChange={sync("firstName")} />
) : (
""
)}
{state.showL ? (
<input value={state.lastName} onChange={sync("lastName")} />
) : (
""
)}
<br />
{/** 此處渲染實例計算結果 */}
fullName: {refComputed.fullName}
<br />
<button onClick={syncBool('showF')}>toggle showF</button>
<button onClick={syncBool('showL')}>toggle showL</button>
</div>
);
}
複製代碼
接下來咱們要說此處有趣的事了,咱們依然渲染兩個實例,當咱們點擊第一個實例toggle showF
按鈕設置showF
爲false,可是注意哦,實例1的讀取了refComputed.fullName
,而這個值是經過${newState.firstName}_${newState.lastName}
計算出來的數據,因此儘管視圖不顯示firstName
了,可是當前實例的依賴列表依然爲firstName, lastName
,因此咱們在實例2裏輸入firstName
,依然能觸發實例1渲染
run({
login: {
// 定義login模塊
state: iState, // 傳遞狀態初始化函數,固然了這裏也能夠傳對象
computed:{
fullName(newState, oldState){
return `${newState.firstName}_${newState.lastName}`;
}
}
}
});
複製代碼
如今咱們的組件代碼僅需將refComputed.fullName
改成moduleComputed.fullName
便可
// const { state, sync, ccUniqueKey, syncBool, refComputed } = useConcent({
// 改成從moduleComputed取實例計算結果
const { state, sync, ccUniqueKey, syncBool, moduleComputed } = useConcent({
module: "login",
state: spState,
setup,
});
複製代碼
讓咱們看看效果吧
點我查看此在線示例還記得開文裏咱們說組件編程體驗統一里提到的ctx.effect
嗎,埋了這麼久的伏筆,在這裏終於要排上用場了,ctx.effect
的執行時機是組件渲染完畢,檢查依賴列表裏是否有變化從而決定是否要觸發反作用函數。
在這裏咱們簡單定義一個反作用即firstName
發生變化時打印一句話。
const setup2 = ctx=>{
ctx.effect(()=>{
console.log('firstName changed');
}, ['firstName']);
}
複製代碼
嘿嘿,接下來咱們聲明一個空組件並將其傳給它
function EmptyPerson() {
console.log('render EmptyPerson');
useConcent({module:'login', setup:setup2});
return <h1>EmptyPerson</h1>
}
複製代碼
這個組件僅僅是標記它屬於login
模塊,可是咱們並無讀取任何模塊狀態用於渲染,只不過在setup
裏定義了一個反作用,依賴列表裏有firstName
,因此當咱們把EmptyPerson
和SharedPerson
放一塊兒實例化後,當咱們在SharedPerson
實例裏輸入firstName
新內容時,會觸發EmptyPerson
渲染並觸發它的反作用函數。
隨着再也不考慮古老的瀏覽器支持,擁抱es6新特性後,v2的concent
已攜帶一整套完整的方案,可以漸進式的開發react組件,即不干擾react自己的開發哲學和組件形態,同時也可以得到巨大的性能收益,這意味着咱們能夠至下而上的增量式的迭代,狀態模塊的劃分,派生數據的管理,事件模型的分類,業務代碼的分隔均可以逐步在開發過程勾勒和剝離出來,其過程是絲滑柔順的,也容許咱們至上而下統籌式的開發,一開始吧全部的領域模型和業務模塊抽象的清清楚楚,同時在迭代過程當中也能很是快速的靈活調整而影響整個項目架構,指望讀到此文的你可以瞭解到concent
在依賴收集到所作的努力並有興趣開始瞭解和試用。
某一個夜晚,我作了個夢,發現基於現有的concent
運行機制,加以適當的約束,好像可讓react
和vue
之間相互轉譯彷佛有那麼一點點可能.....
❤ star me if you like concent ^_^
若是有關於concent的疑問,能夠掃碼加羣諮詢,會盡力答疑解惑,幫助你瞭解更多。