摘要:首先,我們需要一個基本框架來處理表單域變化和表格提交。最起碼我們需要提供一個來告訴如果用戶還沒有對表單域進行改動,就不必展示錯誤。我們需要一個來標識用戶已嘗試提交表單,還需要來標識表單是否正在提交以及每個表單域是否正在進行異步校驗。
用 React Hooks 寫異步表單校驗
React Hooks vs HOC 性能對比探討
Flexbox vs Grid
git-history 把玩
一個 React 優化模式
用 React Hooks 寫異步表單校驗春節期間,React 發布了 16.8 的版本,正式支持了 React Hooks。本文將使用 React Hooks API 通過 100 行代碼來實現支持異步的表單校驗功能。當然,本文最終的例子接近 200 行代碼,這其中包含了使用 Hooks 的部分。
許多與表單相關的教程都不怎么涉及這三個問題:
異步校驗
其他字段發生變化時觸發的字段校驗
校驗頻率的優化
在實際的表單應用場景中,上述三點是非常普遍的。那么這里我們就以下面的若干點作為實現的目標:
同步校驗表單域及當表單域變化時的依賴域
異步校驗表單域及當表單域變化時的依賴域
提交前所有表單域的同步校驗
提交前所有表單域的異步步校驗
嘗試異步提交,如失敗,展示返回值中的錯誤信息
為開發者暴露校驗方法,開發者可在 onBlur 或其他時機進行校驗
允許一個域的多種校驗
如果表單有錯誤,攔截提交
直到表單修改后,或嘗試發起表單提交后才展示一個表單域的錯誤信息
下面的例子是一個帶有用戶名(username),密碼(password)和確認密碼(confirmPassword)的賬號注冊場景。我們先從簡單的開始:
import React, { Component, useState, useEffect, useRef } from "react"; import { useForm, useField } from "./formHooks"; const form = useForm({ onSubmit, }); const usernameField = useField("username", form, { defaultValue: "", validations: [ async formData => { await timeout(2000); return formData.username.length < 6 && "Username already exists"; } ], fieldsToValidateOnChange: [] }); const passwordField = useField("password", form, { defaultValue: "", validations: [ formData => formData.password.length < 6 && "Password must be at least 6 characters" ], fieldsToValidateOnChange: ["password", "confirmPassword"] }); const confirmPasswordField = useField("confirmPassword", form, { defaultValue: "", validations: [ formData => formData.password !== formData.confirmPassword && "Passwords do not match" ], fieldsToValidateOnChange: ["password", "confirmPassword"] }); // const { onSubmit, getFormData, addField, isValid, validateFields, submitted, submitting } = form // const { name, value, onChange, errors, setErrors, pristine, validate, validating } = usernameField
這是相對簡單的 API,但應該會給我們很大的靈活性。可以注意到,此接口包含兩個命名類似的函數,validation 和 validate。我們將定義 validation 為接收表單數據和字段名稱的函數,如果發現問題則返回錯誤消息,否則將返回 false。另一方面,函數 validate 將運行字段的所有校驗函數,并將更新字段的錯誤列表。
首先,我們需要一個基本框架來處理表單域變化和表格提交。我們第一次迭代不包括任何校驗,它只會處理表單狀態。
// Skipping some boilerplate: imports, ReactDOM, etc. export const useField = (name, form, { defaultValue } = {}) => { let [value, setValue] = useState(defaultValue); let field = { name, value, onChange: e => { setValue(e.target.value); } }; // Register field with the form form.addField(field); return field; }; export const useForm = ({ onSubmit }) => { let fields = []; const getFormData = () => { // Get an object containing raw form data return fields.reduce((formData, field) => { formData[field.name] = field.value; return formData; }, {}); }; return { onSubmit: async e => { e.preventDefault(); // Prevent default form submission return onSubmit(getFormData()); }, addField: field => fields.push(field), getFormData }; }; const Field = ({ label, name, value, onChange, ...other }) => { return (); }; const App = props => { const form = useForm({ onSubmit: async formData => { window.alert("Account created!"); } }); const usernameField = useField("username", form, { defaultValue: "" }); const passwordField = useField("password", form, { defaultValue: "" }); const confirmPasswordField = useField("confirmPassword", form, { defaultValue: "" }); return ( {label} ); };
我們只對字段值進行跟蹤。每個字段在使用 useField 初始化結束時使用 form 進行注冊。我們的 onChange 也很簡單。而 getFormData 也就是遍歷表單字段,生成一個表單鍵值對進行存儲。
現在讓我們添加對校驗的支持。我們還不會指定在字段值更改時應校驗哪些字段。相反,我們將在值發生更改時以及每次提交表單時校驗所有字段。
export const useField = ( name, form, { defaultValue, validations = [] } = {} ) => { let [value, setValue] = useState(defaultValue); let [errors, setErrors] = useState([]); const validate = async () => { let formData = form.getFormData(); let errorMessages = await Promise.all( validations.map(validation => validation(formData, name)) ); errorMessages = errorMessages.filter(errorMsg => !!errorMsg); setErrors(errorMessages); let fieldValid = errorMessages.length === 0; return fieldValid; }; useEffect( () => { form.validateFields(); // Validate fields when value changes }, [value] ); let field = { name, value, errors, validate, setErrors, onChange: e => { setValue(e.target.value); } }; // Register field with the form form.addField(field); return field; }; export const useForm = ({ onSubmit }) => { let fields = []; const getFormData = () => { // Get an object containing raw form data return fields.reduce((formData, field) => { formData[field.name] = field.value; return formData; }, {}); }; const validateFields = async () => { let fieldsToValidate = fields; let fieldsValid = await Promise.all( fieldsToValidate.map(field => field.validate()) ); let formValid = fieldsValid.every(isValid => isValid === true); return formValid; }; return { onSubmit: async e => { e.preventDefault(); // Prevent default form submission let formValid = await validateFields(); return onSubmit(getFormData(), formValid); }, addField: field => fields.push(field), getFormData, validateFields }; }; const Field = ({ label, name, value, onChange, errors, setErrors, validate, ...other }) => { let showErrors = !!errors.length; return (); }; const App = props => { const form = useForm({ onSubmit: async formData => { window.alert("Account created!"); } }); const usernameField = useField("username", form, { defaultValue: "", validations: [ async formData => { await timeout(2000); return formData.username.length < 6 && "Username already exists"; } ] }); const passwordField = useField("password", form, { defaultValue: "", validations: [ formData => formData.password.length < 6 && "Password must be at least 6 characters" ] }); const confirmPasswordField = useField("confirmPassword", form, { defaultValue: "", validations: [ formData => formData.password !== formData.confirmPassword && "Passwords do not match" ] }); return ( {label} {showErrors && errors.map(errorMsg => {errorMsg})}); };
上面的代碼是一個改進,乍一看,它似乎可以很好地工作,但實際上它還差很多。我們需要一些必要的 flag 來避免在不適當的時機出現錯誤信息,在用戶再次修改之前能夠立刻顯示出相應的錯誤。
最起碼我們需要提供一個 pristine flag 來告訴 UI 如果用戶還沒有對表單域進行改動,就不必展示錯誤。當然我們還可以更進一步,引入其他更多的 flag 來細化行為。
我們需要一個 flag 來標識用戶已嘗試提交表單,還需要 flag 來標識表單是否正在提交以及每個表單域是否正在進行異步校驗。
此外,為什么我們要在 useEffect中 調用 validateFields 而不是 onChange 中?我們需要 useEffect,因為 setValue 是異步發生的,而這兩者都既不返回 promise 也不提供回調。因此,我們可以確定 setValue 已經完成的唯一方法是通過 useEffect 監聽 value 的變化。
下面我們就來嘗試實現這些 flag 并借助他們來清理 UI 及處理邊界 case,注釋已經詳細的寫在代碼中:
export const useField = ( name, form, { defaultValue, validations = [], fieldsToValidateOnChange = [name] } = {} ) => { let [value, setValue] = useState(defaultValue); let [errors, setErrors] = useState([]); // 初始時沒有錯誤 let [pristine, setPristine] = useState(true); // 初始時表單域未修改 let [validating, setValidating] = useState(false); // 初始時未處于校驗狀態 let validateCounter = useRef(0); // 計數器 優化多次校驗的場景 const validate = async () => { let validateIteration = ++validateCounter.current; // 每次調用先 +1 setValidating(true); let formData = form.getFormData(); let errorMessages = await Promise.all( validations.map(validation => validation(formData, name)) ); // 所有的校驗遍歷 返回 false 或 錯誤提示字符串 errorMessages = errorMessages.filter(errorMsg => !!errorMsg); if (validateIteration === validateCounter.current) { // 只會使用最新的一次校驗 setErrors(errorMessages); setValidating(false); // 校驗狀態置回 } let fieldValid = errorMessages.length === 0; return fieldValid; // 表單域正確 flag }; useEffect( () => { if (pristine) return; // 避免剛掛載就開始校驗 form.validateFields(fieldsToValidateOnChange); // 注意修改狀態的位置 }, [value] ); let field = { name, value, errors, setErrors, pristine, onChange: e => { if (pristine) { setPristine(false); // 只在第一次修改時會觸發 } setValue(e.target.value); // 這里只修改表單 不修改 Hooks 的狀態! }, validate, validating }; form.addField(field); // 注冊表單域 return field; }; export const useForm = ({ onSubmit }) => { let [submitted, setSubmitted] = useState(false); // 已經提交 let [submitting, setSubmitting] = useState(false); // 正在提交 let fields = []; // 參數就是 fieldsToValidateOnChange,變動時需要校驗的表單域 const validateFields = async fieldNames => { let fieldsToValidate; // 存放 useField 生成的需要校驗的表單域 if (fieldNames instanceof Array) { fieldsToValidate = fields.filter(field => fieldNames.includes(field.name) ); } else { //if fieldNames not provided, validate all fields fieldsToValidate = fields; } let fieldsValid = await Promise.all( fieldsToValidate.map(field => field.validate()) ); let formValid = fieldsValid.every(isValid => isValid === true); return formValid; }; const getFormData = () => { return fields.reduce((formData, f) => { formData[f.name] = f.value; return formData; }, {}); }; return { onSubmit: async e => { e.preventDefault(); setSubmitting(true); setSubmitted(true); // User has attempted to submit form at least once let formValid = await validateFields(); // 全部校驗 let returnVal = await onSubmit(getFormData(), formValid); setSubmitting(false); return returnVal; }, isValid: () => fields.every(f => f.errors.length === 0), // 全部域沒錯誤 addField: field => fields.push(field), // 注冊表單域 getFormData, validateFields, submitted, submitting }; }; // 表單域 const Field = ({ label, name, value, onChange, errors, setErrors, pristine, validating, validate, formSubmitted, ...other }) => { // 如果 修改過或提交過 且存在校驗錯誤,展示錯誤 let showErrors = (!pristine || formSubmitted) && !!errors.length; return (); }; const App = props => { const form = useForm({ onSubmit: async (formData, valid) => { if (!valid) return; await timeout(2000); // Simulate network time if (formData.username.length < 10) { //Simulate 400 response from server usernameField.setErrors(["Make a longer username"]); } else { //Simulate 201 response from server window.alert( `form valid: ${valid}, form data: ${JSON.stringify(formData)}` ); } } }); const usernameField = useField("username", form, { defaultValue: "", validations: [ async formData => { await timeout(2000); return formData.username.length < 6 && "Username already exists"; } ], fieldsToValidateOnChange: [] }); const passwordField = useField("password", form, { defaultValue: "", validations: [ formData => formData.password.length < 6 && "Password must be at least 6 characters" ], fieldsToValidateOnChange: ["password", "confirmPassword"] }); const confirmPasswordField = useField("confirmPassword", form, { defaultValue: "", validations: [ formData => formData.password !== formData.confirmPassword && "Passwords do not match" ], fieldsToValidateOnChange: ["password", "confirmPassword"] }); let requiredFields = [usernameField, passwordField, confirmPasswordField]; return ( {label} !pristine && validate()} // 如果已經修改過 則觸發校驗 endAdornment={{ // 通過 validating flag 控制異步校驗的旋轉 icon validating && } {...other} />} {showErrors && errors.map(errorMsg => {errorMsg})}); };
這一版我們加了很多內容,首先包括 4 個 flag:
pristine
validating
submitted
submitting
當然,還增加了 fieldsToValidateOnChange 這一參數,傳入 validateFields 來表示當表單域變化時哪些域要觸發校驗。
這些 flag 的用處就是:控制 UI 中的小菊花和錯誤提示,以及在適當時刻禁用提交按鈕。
需要注意的是 validateCounter。我們需要跟蹤調用驗證函數的次數,因為在校驗函數完成時,可能會再次調用新的校驗。如果是這種情況,我們需要忽略此次調用的結果,并僅使用最近調用的結果來更新表單域的錯誤狀態。
這個版本還有很多不足,不過這個 Demo 本身還是值得學習的。
源地址:https://medium.freecodecamp.o...
React Hooks vs HOC 性能對比探討Medium 上一篇文章給出結論說 Hooks 性能相對差一些,Dan Abramov 對此予以回應。我們來看看具體是怎么討論的。
為什么說 React Hooks 性能稍遜?這篇文章中的結論是,盡管最近 HOC 因為 wrapper hell 而越來越多的被詬病,根據其評判標準,HOC 仍然比 Hooks 快。當然他也提出,如果評判標準的測試結果有誤,也請隨時指正。
那么在了解 Dan 的回應之前,我們先來看看他是如何對二者進行對比的。
測試 App作者設計了一個簡單的測試場景:簡而言之,就是使用 React Hooks 和 HOC 分別渲染一個具有 10,000 個子組件的根組件。其中,每個子組件包含三個狀態值,以及第一次 render 后設置三個狀態值的副作用。根組件負責記錄渲染完這 10,000 個子組件所耗費的時間,當然,這個記錄過程也是通過一個副作用來完成。
Hooks 版本import React, { useEffect, useState } from "react"; import { render } from "react-dom"; const array = []; for (let i = 0; i < 10000; i++) array[i] = true; const Component = () => { const [a, setA] = useState(""); const [b, setB] = useState(""); const [c, setC] = useState(""); useEffect(() => { setA("A"); setB("B"); setC("C"); }, []); returnHOC 版本{a + b + c}; }; const Benchmark = ({ start }) => { useEffect(() => { console.log(Date.now() - start); }); return array.map((item, index) =>); }; render( , document.getElementById("root"));
import React from "react"; import { render } from "react-dom"; import { compose, withState, withEffect } from "@reactorlib/core"; const array = []; for (let i = 0; i < 10000; i++) array[i] = true; const _Component = ({ a, b, c }) => { return{a + b + c}; }; const Component = compose( withState({ a: "", b: "", c: "" }), withEffect(({ setA, setB, setC }) => { setA("A"); setB("B"); setC("C"); }, true) )(_Component); const _Benchmark = () => { return array.map((item, index) =>); }; const Benchmark = compose( withEffect(({ start }) => { console.log(Date.now() - start); }) )(_Benchmark); render( , document.getElementById("root"));
上述的 reactorlib 是作者自己封的包,基本上代碼沒什么差別。
測試設備具體設備我們并不關心,兩者都是在 15 年的 MacBook 上跑的。
測試結果作者展示了 10 組結果,從數據上看,似乎是 Hooks 穩定的落后于 HOC。
Rendering Time in milliseconds Run# Hooks HOCs ----------------------------- 1 2197 1440 2 2302 1757 3 2749 1407 4 2243 1309 5 2167 1644 6 2219 1516 7 2322 1673 8 2268 1630 9 2164 1446 10 2071 1597
那么事實真的如此嗎?
評判標準存在什么問題?Dan 提出,首先應該統一在生產模式才有比較的意義,畢竟開發模式下會有大量的提示信息等拖慢速度,生產模式的性能才是現實中更需要考量的。
當統一成生產模式后,二者跑出的結果都顯著變快,不過 Hooks 仍略慢一籌。
Hooks HOCs (NOTE: these results are still wrong, see below) 175 156 171 164 169 154 181 138 167 159 153 194 151 152 155 152 147 163 160 162
當然結果還不對,原因在于這兩者并沒有在相同的評判標準下運行。
我們看到,Hooks 版本的測試是通過 useEffect() 來更新狀態和計算時間的。我們看下文檔中的說法:
與 componentDidMount 和 componentDidUpdate 不同,傳遞到 useEffect 的函數執行的時間是晚于布局和繪制的。這就使其非常適合一些常見的副作用:設置訂閱和事件等,因為大多數工作都不應阻塞瀏覽器更新視圖。
因此,useEffect() 可以讓瀏覽器在運行副作用之前就完成視圖的繪制,對于用戶來說,useEffect() 通常能提供更好的體驗,因為初始渲染不需要等待副作用的執行,而這一點正是 React class 存在的一個普遍問題。
因為需要等待初始渲染, useEffect() 版的結果自然會比 class 版本的要慢一些了。那么我們來看看 HOC 到底做了什么呢?
從源碼上看,HOC 是在 componentDidMount() 中設置狀態并 log 時間開銷。這就意味著這一過程執行的更早了一些,但同時用戶還沒有真實看到視圖。即使按照擬定的標準來評判,結果上更好看,但 app 本身的響應性并不強。
為了將二者放到統一評判標準上進行比較,Dan 使用了 useLayoutEffect(),這一副作用是在布局階段執行的,這與 componentDidMount() 相似。重新跑一邊程序就會發現,Hooks 明顯是要快了一些。
Hooks HOCs (These rows compare the same thing--although not a very useful one) 121 156 106 170 111 157 141 152 111 156 121 158 105 170 111 162 108 166 108 157
那會不會是因為 useLayoutEffect() 本身就比 useEffect() 執行的快嗎?并不,這只是意味著 useLayoutEffect() 或 componentDidMount() 執行的更早一些。除非我們的副作用場景與布局有關,否則我們通常都會希望先渲染出初始視圖,然后在執行副作用。而這正式 useEffect() 幫助我們做到的。
如果我們在 useEffect() 中更新狀態,但在 useLayoutEffect()(類似 componentDidMount)中計算時間,我們甚至能得到更好的結果。
Hooks HOCs (Don"t pass a screenshot like this around--read the text below. They"re not doing equivalent work!) 106 174 104 198 88 149 88 154 88 149 89 163 85 162 90 157 89 164 91 153
這個結果并不奇怪,因為 useEffect() 版本在渲染之前所做的工作一定是更少的:我們首先展示了初始渲染的結果,然后執行了改變狀態的副作用,這就意味著更新的時候會有一些閃爍。不過,這一評判標準其實并不能代表真實的 UI 場景,我們并不能斷定究竟哪種方法更好。Hooks 可以讓我們選擇是否阻塞渲染過程,從而根據自己的場景來決定什么是比較好的。
當然,渲染 10,000 個文字節點并且一次性更新它們并不能真實的還原現實應用中所會遇到的性能挑戰,同時,這一場景也不能為不同場景下的性能權衡提供充分的上下文。
總之,這二者的對比本身并沒那么重要,當發布一項評判標準時,知道瀏覽器中究竟發生了什么才更值得深究。
Benchmarking is hard.
源地址:
https://hackernoon.com/react-...
https://medium.com/@dan_abram...
Flexbox 和 grid 之間有很多相似的地方,它們可以拉伸,可以收縮,可以居中,可以重新排序,可以排列等等。很多布局場景下,這兩者我們都可以選來用,當然,有些場景也確實存在其中一個比另一個更合適的情況。那么我們就來關注下這二者的差異究竟在哪里:
Flexbox 可以選擇性的 wrap我們可以指定 Flexbox 在一行放不下后另一起行,當然,這樣就會產生參差不齊的感覺;而 Grid 則會充滿一行(如果我們允許自動充滿的話)。
Flexbox on top, Grid on bottomFlexbox 可看作「一維」,Grid 可看作「二維」
盡管 Flexbox 可以形成行列布局,并且允許元素 wrap,但我們無法指定一個元素作為一行的結束,而僅僅是沿著一個坐標軸推出去,然后按照能否 wrap 挨個排列而已。
.parent { display: flex; flex-flow: row wrap; /* 元素會沿著一行盡可能的排列 然后根據情況進行 wrap */ }
而 Grid 就更像是「二維」的東西,我們可以指定行和列的尺寸,顯式的指定東西在行列的位置:
.parent { display: grid; grid-template-columns: 3fr 1fr; /* Two columns, one three times as wide as the other */ grid-template-rows: 200px auto 100px; /* Three columns, two with explicit widths */ grid-template-areas: "header header header" "main . sidebar" "footer footer footer"; } /* Now, we can explicitly place items in the defined rows and columns. */ .child-1 { grid-area: header; } .child-2 { grid-area: main; } .child-3 { grid-area: sidebar; } .child-4 { grid-area: footer; }
Flexbox on top, Grid on bottomGrid 的定位基本在父元素上,而 Flexbox 基本在子元素上
/* Flex 子元素完成大部分定位工作 */ .flexbox { display: flex; > div { &:nth-child(1) { // logo flex: 0 0 100px; } &:nth-child(2) { // search flex: 1; max-width: 500px; } &:nth-child(3) { // avatar flex: 0 0 50px; margin-left: auto; } } } /* Grid 父元素完成大部分定位工作 */ .grid { display: grid; grid-template-columns: 1fr auto minmax(100px, 1fr) 1fr; grid-template-rows: 100px repeat(3, auto) 100px; grid-gap: 10px; }Grid 更擅長覆蓋
如果是 Flexbox 想做到元素重疊的話,就要尋求一些傳統方法,如負 margin、transform 或絕對定位。當時通過 Grid 我們可以講子元素放置的覆蓋網格線,甚至將元素完全覆蓋在同一個網格里。
Flexbox on top, Grid on bottomFlexbox 可以把東西「推」走
Flexbox 有個獨有的特性,就是可以通過 margin auto 的方式來把子元素「推到一邊」。這主要是因為 Flexbox 在 margin auto 這塊的一些有趣特性,具體略,下面附一張圖來展示這一特性:
源地址:https://css-tricks.com/quick-...
git-history 把玩git-history 可以快速的翻看 Github 倉庫中的文件、或者本地倉庫中的文件的提交歷史信息。是不是真的有用另說,效果還是蠻酷的,展示如下(動圖約 8M,略過也可):
具體用法在文檔中有寫,就不啰嗦了。
他們也提供了 Chrome 插件,其實就是個帶鏈接的按鈕:
那么我們就看看它具體是怎么做的吧(雖然大佬們一眼就知道這是怎么做的了)。
本地版 CLI 解析 邏輯毫無疑問的是,這個小應用的核心之一就是 git 命令,那么我們先來看看本地版都做了哪些工作:
首先從入口文件看起,然后會發現如下代碼:
const cli = window._CLI; export default function App() { if (cli) { return; } const [repo, sha, path] = getUrlParams(); if (!repo) { return ; } else { return ; } }
這個 _CLI 是哪里來的呢?可以看出,如果存在 cli,就會走 CliApp,也就是本地版的代碼。那我們先去找下 cli 的源頭看看,可以找到如下代碼:
const server = http.createServer((request, response) => { if (request.url === "/") { Promise.all([indexPromise, commitsPromise]).then(([index, commits]) => { const newIndex = index.replace( "", `` ); var headers = { "Content-Type": "text/html" }; response.writeHead(200, headers); response.write(newIndex); response.end(); }); } else { return handler(request, response, { public: sitePath }); } }); // ... }
而在 index.html 中,可以看到:
也就是說,本地跑了一個服務,并且在獲取到提交信息之后,會替換掉 index.html 中注入的 window._CLI,用來存放文件路徑和提交記錄,再回到上面的 CliApp 上,就可以拿到相應數據進行頁面的渲染了。
在研究提交記錄究竟是怎么獲取的之前(下一小節統一聊用到的 git 命令),我們先繼續看下 CliApp 的邏輯。
function CliApp({ data }) { let { commits, path } = data; const fileName = path.split("/").pop(); useDocumentTitle(`Git History - ${fileName}`); commits = commits.map(commit => ({ ...commit, date: new Date(commit.date) })); const [lang, loading, error] = useLanguageLoader(path); if (error) { return; } if (loading) { return ; } return ; }
CliApp 居然是一個 Hook!其中使用了 useDocumentTitle 和 useLanguageLoader 這兩個自定義 Hook。useDocumentTitle 本身就是個副作用,內部將傳入的拼接字符串替換為頁面的 title:
export function useDocumentTitle(title) { useEffect(() => { document.title = title; }, [title]); }
而 useLanguageLoader 做了什么呢?
export function useLanguageLoader(path) { return useLoader(async () => { const lang = getLanguage(path); await loadLanguage(lang); return lang; }, [path]); }
可以看到,本身也是調用了自定義 Hook useLoader,那么 useLoader 又做了什么呢 -。-?
function useLoader(promiseFactory, deps) { const [state, setState] = useState({ data: null, loading: true, error: null }); useEffect(() => { promiseFactory() .then(data => { setState({ data, loading: false, error: false }); }) .catch(error => { setState({ loading: false, error }); }); }, deps); return [state.data, state.loading, state.error]; }
原來,useLoader 就是會在 useEffect 中根據指定的依賴執行一個 promise,而這個 Hook 在內部維護這個 promise 的相應狀態:data、loading、error,說起來就是封裝了一個「帶狀態的異步邏輯」。OK,也就是說我們只要關心傳入的異步方法就對了。回到上面的 useLanguageLoader 繼續看:
export function getLanguage(filename) { return filenameRegex.find(x => x.regex.test(filename)).lang; }
filenameRegex 其實就是模塊內維護的針對不同文件后綴的識別列表,根據文件路徑返回該文件的文件類型。下面的我們忽略,即可知道 useLanguageLoader 就是用來返回文件類型的。暈,原來是干這的,罷了罷了。
回到 CliApp,我們可以看出,正常渲染的頁面是 History,那我們進去看下:
export default function History({ commits, language }) { const codes = commits.map(commit => commit.content); const slideLines = getSlides(codes, language); return; } function Slides({ commits, slideLines }) { const [current, target, setTarget] = useSliderSpring(commits.length - 1); const setClampedTarget = newTarget => setTarget(Math.min(commits.length - 0.75, Math.max(-0.25, newTarget))); const index = Math.round(current); const nextSlide = () => setClampedTarget(Math.round(target + 0.51)); const prevSlide = () => setClampedTarget(Math.round(target - 0.51)); useEffect(() => { document.body.onkeydown = function(e) { if (e.keyCode === 39) { nextSlide(); } else if (e.keyCode === 37) { prevSlide(); } else if (e.keyCode === 32) { setClampedTarget(current); } }; }); return ( ); } setClampedTarget(index)} />
至此我們可以看出,后面就是根據提交記錄的數據來進行幻燈片的展示了,over。
git 命令回到本地版最根本的功能上,搞了半天,根本的功能居然都不在 src 目錄下,而是在 cli 目錄下,而 cli 目錄的功能是通過起本地服務來進行的。
回到之前的 runServer,我們去找尋下 commitsPromise 是如何實現的:
const execa = require("execa"); async function getCommits(path) { const format = `{"hash":"%h","author":{"login":"%aN"},"date":"%ad"},`; const { stdout } = await execa("git", [ "log", "--follow", "--reverse", `--pretty=format:${format}`, "--date=iso", "--", path ]); const json = `[${stdout.slice(0, -1)}]`; const messagesOutput = await execa("git", [ "log", "--follow", "--reverse", `--pretty=format:%s`, "--", path ]); const messages = messagesOutput.stdout.replace(""", """).split(/ ? /); const result = JSON.parse(json) .map((commit, i) => ({ ...commit, date: new Date(commit.date), message: messages[i] })) .slice(-20); return result; } async function getContent(commit, path) { const { stdout } = await execa("git", ["show", `${commit.hash}:${path}`]); return stdout; } module.exports = async function(path) { const commits = await getCommits(path); await Promise.all( commits.map(async commit => { commit.content = await getContent(commit, path); }) ); return commits; };
核心的代碼都在 git.js 文件中了,而文件暴露的方法就是用來生成 commitsPromise 的。我們可以看到,本質上就是通過 getCommits 和 getContent 拿到提交數據,他們究竟做了什么呢?getCommits 主要執行了 兩個 git log 命令,而 getContent 則執行的是 git show 命令。我只會些最近本的 git,來復習下這倆吧:
git log該命令是為了在提交若干更新之后,又或者克隆了某個項目,想回顧下提交歷史時使用的。上面代碼中使用到的參數功能如下:
--follow
列舉單個文件的提交歷史列表(即使重命名)
-- reverse
逆序輸出選擇展示的提交記錄
----pretty=format:
按格式輸出
通過 git show commitHash:filePath 可得到這次提交的文件內容。
綜上,這個工具本地版 CLI 的實現原理,就是通過 git log 命令,找到指定文件的提交記錄,并遍歷 hash 或叫 tag 列表,利用 git show 拿到每次提交的時候文件的 content。
源地址:https://github.com/pomber/git...
一個 React 優化模式今天有個推主提了一個優化模式,我們來看推主給出的代碼:
function Foo({ children }) { const [x, updateX] = useState(0); return{ children }; }
在上面的代碼中,如果我們以某種方式調用了 updateX,也就是說更新了 Foo 內部的狀態,組件就會重新 render。但是,因為 children 并沒有更新(引用相等),Foo 就不會嘗試著去重新 render children。
這樣做有什么價值呢?這樣寫的話我們就可以不用 React.memo 這類方法來避免計算開銷。我們可以寫一個更容易看清的例子來展示這種模式的作用:
可以看到,左邊的寫法中,父組件本身依賴組件內部的一個計算,父子組件之間沒有關聯。當點擊從而狀態更新結果后,App 需要重新 render,并且連帶著子組件一起重新渲染,這顯然存在性能浪費。
而右邊的寫法中,計算邏輯被封裝在組件內部了,其 children 屬性的引用沒有發生變化,這樣一來重新渲染子組件的開銷就可以節省掉了,也即 CompA 和 COMPB 并沒有重新 render。
此外,對 class 而言這個模式也是成立的,只要傳入的 children 不發生變化(引用相等)就可以。不過,我們的代碼通常會使 props 發生變化,所以通常情況下引用也不會相等。
源地址:https://twitter.com/sebmarkba...
「每日一瞥」是團隊內部日常業界動態提煉,發布時效可能略有延后。文章可隨意轉載,但請保留此 原文鏈接。
非常歡迎有激情的你加入 ES2049 Studio,簡歷請發送至 caijun.hcj(at)alibaba-inc.com 。
若有收獲,就賞束稻谷吧
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/101782.html
摘要:目前前端主要有以下四種方法會觸發對應的回調方法方法客戶端回調客戶端回調參考地址每日一瞥是團隊內部日常業界動態提煉,發布時效可能略有延后。 showImg(https://segmentfault.com/img/remote/1460000017975436?w=1200&h=630); 「ES2015 - ES2018」Rest / Spread Properties 梳理 Thr...
showImg(https://segmentfault.com/img/remote/1460000018793640?w=900&h=500); 簡介 安全、注入攻擊、XSS 13歲女學生被捕:因發布 JavaScript 無限循環代碼。 這條新聞是來自 2019年3月10日 很多同學匆匆一瞥便滑動屏幕去看下一條消息了,并沒有去了解這段代碼是什么,怎么辦才能防止這個問題。事情發生后為了抗議日本...
摘要:配置在設置項中確認包含增加設置項,值為一個字符串路徑,必須以結尾在模板中這樣引用在的目錄存放靜態文件開發期間使用極度低效時有別的做法注意默認為,一個列表,表示獨立于的靜態文件存放位置。 配置 1.在INSTALLED_APPS設置項中確認包含django.contrib.staticfiles 2.增加STATIC_URL設置項,值為一個字符串(路徑),必須以‘/’結尾 3.在模板中...
閱讀 916·2021-10-27 14:14
閱讀 1740·2021-10-11 10:59
閱讀 1314·2019-08-30 13:13
閱讀 3151·2019-08-29 15:17
閱讀 2749·2019-08-29 13:48
閱讀 488·2019-08-26 13:36
閱讀 2080·2019-08-26 13:25
閱讀 857·2019-08-26 12:24