Explorando o desempenho do front-end no nível do código | Equipe técnica da JD Cloud

Prefácio

Tenho feito otimização de desempenho recentemente e os métodos específicos de otimização estão por toda a Internet, então não vou repeti-los aqui.

A otimização de desempenho pode ser dividida nas seguintes dimensões: nível de código, nível de construção e nível de rede.
Este artigo explora principalmente o desempenho do front-end no nível do código e está dividido principalmente nas quatro seções a seguir.

  • Use CSS em vez de JS

  • Análise aprofundada de JS

  • Algoritmo de front-end

  • Camada inferior do computador

Use CSS em vez de JS

Aqui o apresentamos principalmente a partir de dois aspectos: animação e componentes CSS.

Animação CSS

Antes do lançamento do CSS2, até mesmo uma animação simples precisava ser implementada por meio de JS. Por exemplo, o movimento horizontal do quadrado vermelho abaixo:

movimento horizontal

Código JS correspondente:

let redBox = document.getElementById('redBox')
let l = 10

setInterval(() => {
    l+=3
    redBox.style.left = `${l}px`
}, 50)


A especificação CSS2 de 1998 definiu algumas propriedades de animação, mas devido às limitações da tecnologia do navegador da época, esses recursos não eram amplamente suportados e aplicados.

Até a introdução do CSS3, as animações CSS eram mais totalmente suportadas. Ao mesmo tempo, CSS3 também introduz mais efeitos de animação, tornando a animação CSS amplamente utilizada no desenvolvimento Web atual.

Então, quais animações podem ser obtidas com CSS3, aqui estão alguns exemplos:

  • Transição - Transição é um dos efeitos de animação comumente usados ​​em CSS3. Ao transformar certos atributos de um elemento, o elemento pode fazer uma transição suave de um estado para outro dentro de um período de tempo.

  • Animação - Animação é outro efeito de animação comumente usado em CSS3. É usado para adicionar alguns efeitos de animação complexos a um elemento. Uma série de sequências de animação pode ser definida por meio de quadros-chave (@keyframes).

  • Transform - Transform é uma tecnologia usada em CSS3 para obter efeitos de transformação gráfica 2D/3D, incluindo rotação, dimensionamento, movimento, chanfro e outros efeitos.

Reescreva o exemplo acima em código CSS da seguinte forma:

#redBox {
    animation: mymove 5s infinite;
}

@keyframes mymove
{
    from {left: 0;}
    to {left: 200px;}
}


O mesmo efeito pode ser alcançado usando estilos, então por que não?

Deve-se ressaltar que as animações CSS ainda estão se desenvolvendo e melhorando. Com o surgimento de novos recursos de navegador e versões CSS, os recursos das animações CSS são constantemente adicionados e otimizados para atender às necessidades de animação cada vez mais complexas e melhor experiência do usuário.

Componentes CSS

Em algumas bibliotecas de componentes bem conhecidas, a maioria dos adereços de alguns componentes são implementados modificando estilos CSS, como o componente Space de Vant .

Adereços Função Estilos CSS
direção direção de espaçamento direção flexível: coluna;
alinhar Alinhamento alinhar itens: xxx;
preencher Se deve tornar o Espaço um elemento de nível de bloco e preencher todo o elemento pai exibição: flexível;
enrolar Se as linhas devem ser quebradas automaticamente flex-wrap: envoltório;

Outro exemplo é o componente Space do Ant Design .

Adereços Função Estilos CSS
alinhar Alinhamento alinhar itens: xxx;
direção direção de espaçamento direção flexível: coluna;
tamanho tamanho do espaçamento lacuna: xxx;
enrolar Se as linhas devem ser quebradas automaticamente flex-wrap: envoltório;

Este tipo de componente pode ser completamente encapsulado na implementação do mixin SCSS (o mesmo vale para LESS), o que pode não apenas reduzir o volume de construção do projeto (o tamanho do componente Space das duas bibliotecas após gzip é 5,4k e 22,9 k respectivamente), mas também melhoram o desempenho.

Visualize o volume de um componente na biblioteca de componentes e acesse a conexão .

Por exemplo, o seguinte mixin espacial:

/* 
* 间距
* size: 间距大小,默认是 8px
* align: 对齐方式,默认是 center,可选 start、end、baseline、center
* direction: 间距方向,默认是 horizontal,可选 horizontal、vertical
* wrap: 是否自动换行,仅在 horizontal 时有效,默认是 false
*/
@mixin space($size: 8px, $direction: horizontal, $align: center, $wrap: false) {
    display: inline-flex;
    gap: $size;

    @if ($direction == 'vertical') {
        flex-direction: column;
    }

    @if ($align == 'center') {
        align-items: center;
    }

    @if ($align == 'start') {
        align-items: flex-start;
    }

    @if ($align == 'end') {
        align-items: flex-end;
    }

    @if ($align == 'baseline') {
        align-items: baseline;
    }

    @if ($wrap == true) {
        @if $direction == 'horizontal' {
            flex-wrap: wrap;
        }
    }
}


Componentes semelhantes incluem Grade, Layout, etc.

Vamos falar sobre ícones. Abaixo está a primeira captura de tela do componente de ícone do Ant Design. Existem muitos que podem ser facilmente implementados usando apenas HTML + CSS.

Ícone de direção do design da formiga

Ideias de implementação:

  • Priorize a implementação usando apenas estilos

  • Se o estilo por si só não for suficiente, adicione uma tag primeiro e implemente-a através desta tag e seus dois pseudoelementos::before e::after

  • Se um rótulo não for suficiente, considere adicionar rótulos adicionais.

Por exemplo, para implementar um triângulo sólido que suporta quatro direções, você pode conseguir isso com apenas algumas linhas de estilos (a captura de tela acima mostra 4 ícones):

/* 三角形 */
@mixin triangle($borderWidth: 10, $shapeColor: #666, $direction: up) {
    width: 0;
    height: 0;
    border: if(type-of($borderWidth) == 'number', #{$borderWidth} + 'px', #{$borderWidth}) solid transparent;

    $doubleBorderWidth: 2 * $borderWidth;
    
    $borderStyle: if(type-of($doubleBorderWidth) == 'number', #{$doubleBorderWidth} + 'px', #{$doubleBorderWidth}) solid #{$shapeColor};

    @if($direction == 'up') {
        border-bottom: $borderStyle;
    }

    @if($direction == 'down') {
        border-top: $borderStyle;
    }

    @if($direction == 'left') {
        border-right: $borderStyle;
    }

    @if($direction == 'right') {
        border-left: $borderStyle;
    }
}


Resumindo, o que pode ser implementado com CSS não requer JS , além de ter bom desempenho, atravessa pilhas de tecnologia e até cross-terminais.

Análise aprofundada de JS

Depois de apresentar o CSS, vamos dar uma olhada no JS, principalmente sob dois aspectos: instruções básicas e código-fonte da estrutura.

Otimização de instruções if-else

Primeiro entenda como a CPU executa instruções condicionais. Consulte o seguinte código:

const a = 2
const b = 10
let c
if (a > 3) {
    c = a + b
} else {
    c = 2 * a
}


O fluxo de execução da CPU é o seguinte:

Declarações condicionais

Vemos que quando a instrução 0102 é executada, porque a condição a > 3 não é atendida, ele salta diretamente para a instrução 0104 para execução; além disso, o computador é muito inteligente, se durante a compilação descobrir que a nunca pode ser maior que 3, ele excluirá diretamente a instrução 0103, e então a instrução 0104 se tornará a próxima instrução, que será executada diretamente em sequência, que é a otimização do compilador.

Então, voltando ao tópico, se houver o seguinte código:

function check(age, sex) {
    let msg = ''
    if (age > 18) {
        if (sex === 1) {
            msg = '符合条件'
        } else {
            msg = ' 不符合条件'
        }
    } else {
        msg = '不符合条件'
    }
}


A lógica é muito simples. É filtrar pessoas com idade > 18 e sexo == 1. Não há nenhum problema com o código, mas é muito detalhado. Do ponto de vista da CPU, duas operações de salto precisam ser ser executado. Quando idade > 18 anos, entre no if-else interno para continuar julgando, o que significa pular novamente.

Na verdade, podemos otimizar diretamente essa lógica (geralmente fazemos isso, mas podemos saber, mas não sabemos por quê):

function check(age, sex){
    if (age > 18 && sex ==1) return '符合条件'
    return '不符合条件'
}


Portanto, se a lógica puder terminar mais cedo , ela terminará mais cedo para reduzir saltos de CPU.

Otimização da instrução Switch

Na verdade, não há muita diferença entre a instrução switch e a instrução if-else, exceto que elas são escritas de maneiras diferentes.No entanto, a instrução switch possui uma otimização especial, ou seja, matrizes.

Consulte o seguinte código:

function getPrice(level) {
    if (level > 10) return 100
    if (level > 9) return 80
    if (level > 6) return 50
    if (level > 1) return 20
    return 10
}


Nós mudamos para uma instrução switch:

function getPrice(level) {
    switch(level)
        case 10: return 100
        case 9: return 80
        case 8: 
        case 7: 
        case 6: return 50
        case 5:
        case 4: 
        case 3:
        case 2: 
        case 1: return 20
        default: return 10
}


Parece que não há diferença, mas na verdade o compilador irá otimizá-lo em um array, onde os subscritos do array vão de 0 a 10. O preço correspondente aos diferentes subscritos é o valor do retorno, ou seja:

Matriz de comutação

E sabemos que os arrays suportam acesso aleatório e são extremamente rápidos, portanto, a otimização do switch do compilador melhorará muito a eficiência de execução do programa, que é muito mais rápido do que executar comandos um por um.

Bem, ainda preciso escrever uma declaração if-else estúpida. Não posso simplesmente escrever todas as opções?

não! Como a otimização de switch do compilador é condicional, ela exige que seu código seja compacto, ou seja, contínuo.

Por que é isso? Porque quero usar um array para otimizar você. Se você não for compacto, por exemplo, seu código é 1, 50, 51, 101, 110, vou criar um array com comprimento de 110 para armazená-lo. Somente essas posições são úteis., não é uma perda de espaço!

Portanto, quando usamos switch, tentamos garantir que o código seja do tipo numérico compacto.

Otimização de instruções de loop

Na verdade, as instruções de loop são semelhantes às instruções condicionais, mas são escritas de maneiras diferentes.O ponto de otimização das instruções de loop é principalmente reduzir as instruções.

Vejamos primeiro como escrever a segunda série:

function findUserByName(users) {
   let user = null
   for (let i = 0; i < users.length; i++) {
       if (users[i].name === '张三') {
           user = users[i]
       }
   }
   return user
}


Se o comprimento da matriz for 10.086 e a primeira pessoa se chamar Zhang San, as próximas 10.085 travessias serão em vão e a CPU realmente não será usada como ser humano.

Você não poderia simplesmente escrever assim:

function findUserByName(users) {
    for (let i = 0; i < users.length; i++) {
        if (users[i].name === '章三') return users[i]
    }
}


Isto é altamente eficiente na escrita e altamente legível, e também está em linha com a nossa visão acima mencionada de que se a lógica puder terminar mais cedo, terminará mais cedo. CPU agradece a todos diretamente.

Na verdade, há algo que pode ser otimizado aqui, ou seja, o comprimento do nosso array pode ser extraído sem precisar acessá-lo todas as vezes, ou seja:

function findUserByName(users) {
    let length = users.length
    for (let i = 0; i < length; i++) {
        if (users[i].name === '章三') return users[i]
    }
}


Isso pode parecer um pouco complicado, e é, mas se você considerar o desempenho, ainda é útil. Por exemplo, a função size() de algumas coleções não é um simples acesso a atributos, mas precisa ser calculada uma vez a cada vez.Este cenário é uma grande otimização, pois salva o processo de muitas chamadas de função, o que significa salvar Existem muitos instruções de chamada e retorno, o que simplesmente melhora a eficiência do código. Especialmente no caso de declarações em loop, onde mudanças quantitativas levam facilmente a mudanças qualitativas, a lacuna é ampliada a partir deste detalhe.

Referência do processo de chamada de função:

chamada de função

O código correspondente é o seguinte:

let a = 10
let b = 11

function sum (a, b) {
    return a + b
}


Depois de falar sobre algumas declarações básicas, vamos dar uma olhada na estrutura que usamos com frequência. Vale a pena explorar o desempenho em muitos lugares.

algoritmo de comparação

Tanto o Vue quanto o React usam DOM virtual. Ao realizar atualizações, compare o DOM virtual antigo e o novo. Sem qualquer otimização, a complexidade de tempo de diferenciar estritamente duas árvores diretamente é O (n ^ 3), o que não é utilizável de forma alguma. Portanto, Vue e React devem usar o algoritmo diff para otimizar o DOM virtual:

Vue2 - Comparação dupla:

Vue2 - Comparação dupla

Semelhante à imagem acima:

  • Defina 4 variáveis: oldStartIdx, oldEndIdx, newStartIdx e newEndIdx

  • Determine se oldStartIdx e newStartIdx são iguais

  • Determine se oldEndIdx e newEndIdx são iguais

  • Determine se oldStartIdx e newEndIdx são iguais

  • Determine se oldEndIdx e newStartIdx são iguais

  • Ao mesmo tempo, oldStartIdx e newStartIdx movem-se para a direita; oldEndIdx e newEndIdx movem-se para a esquerda.

Vue3 - Subsequência crescente mais longa:

Vue3 - subsequência crescente mais longa

Todo o processo é otimizado novamente com base na comparação dupla do Vue2. Por exemplo, a captura de tela acima:

  • Primeiro, execute uma comparação dupla e descubra que os dois primeiros nós (A e B) e o último nó (G) são iguais e não precisam ser movidos.

  • Encontre a subsequência crescente mais longa C, D, E (um grupo de nós que inclui filhos antigos e novos, e a ordem mais longa não mudou)

  • Trate a subsequência como um todo, sem nenhuma operação interna. Você só precisa mover F para a frente dela e inserir H atrás dela.

Reagir - mudar apenas para a direita:

Reagir - mudar apenas para a direita

O processo de comparação da captura de tela acima é o seguinte:

  • Atravesse Old e salve o mapa subscrito correspondente

  • Atravessando New, o subscrito de b muda de 1 para 0 e não se move (é um deslocamento para a esquerda, não um deslocamento para a direita)

  • O subscrito de c muda de 2 para 1 e não se move (também se move para a esquerda, não para a direita)

  • O subscrito de a muda de 0 para 2, movendo-se para a direita, e os subscritos de b e c são reduzidos em 1.

  • As posições de d e e não mudaram e não precisam ser movidas.

Resumindo, não importa qual algoritmo seja utilizado, seus princípios são:

  • Compare apenas no mesmo nível, não entre níveis

  • Se a Tag for diferente, exclua-a e reconstrua-a (não compare mais detalhes internos)

  • Os nós filhos são diferenciados por chave (importância da chave)

No final, a complexidade do tempo foi reduzida com sucesso para O(n) antes que pudesse ser usada em nossos projetos reais.

setState é realmente assíncrono?

Muitas pessoas pensam que setState é assíncrono, mas veja o exemplo a seguir:

clickHandler = () => {
    console.log('--- start ---')

    Promise.resolve().then(() => console.log('promise then'))

    this.setState({val: 1}, () => {console.log('state...', this.state.val)})

    console.log('--- end ---')
}

render() {
    return <div onClick={this.clickHandler}>setState</div>
}


Resultado real da impressão:

resultados de impressão setState

Se for assíncrono, a impressão do estado deverá ser executada após a microtarefa Promise.

Para explicar esse motivo, devemos primeiro entender o mecanismo de eventos em JSX.

Eventos em JSX, como onClick={() => {}}, são na verdade chamados de eventos sintéticos, que são diferentes dos eventos personalizados que costumamos chamar:

// 自定义事件
document.getElementById('app').addEventListener('click', () => {})


Os eventos sintéticos estão vinculados ao nó raiz e têm pré e pós-operações. Veja o exemplo acima:

function fn() { // fn 是合成事件函数,内部事件同步执行
    // 前置
    clickHandler()
    
    // 后置,执行 setState 的 callback
}


Você pode imaginar que existe uma função fn e os eventos nela são executados de forma síncrona, incluindo setState. Após a execução de fn, o evento assíncrono começa a ser executado, ou seja, Promise.then, que é consistente com o resultado impresso.

Então, por que o React faz isso?
Por questões de desempenho, se o estado precisar ser modificado várias vezes, o React mesclará essas modificações primeiro e renderizará o DOM apenas uma vez após a mesclagem, para evitar renderizar o DOM toda vez que for modificado.

Portanto, setState é de natureza síncrona, e o "assíncrono" de que costumamos falar não é rigoroso.

Algoritmo de front-end

Depois de falar do nosso desenvolvimento diário, vamos falar da aplicação de algoritmos no front-end.

Lembrete amigável: Os algoritmos geralmente são projetados para grandes volumes de dados, que são diferentes do desenvolvimento diário.

Se você puder usar tipos de valor, não precisará de tipos de referência.

Vejamos primeiro uma questão.

Encontre todos os números simétricos entre 1-10000, por exemplo: 0, 1, 2, 11, 22, 101, 232, 1221...

Idéia 1 - Use reversão e comparação de arrays: converta números em strings e depois converta-os em arrays; inverta arrays e junte-os em strings; compare as strings antes e depois.

function findPalindromeNumbers1(max) {
    const res = []
    if (max <= 0) return res

    for (let i = 1; i <= max; i++) {
        // 转换为字符串,转换为数组,再反转,比较
        const s = i.toString()
        if (s === s.split('').reverse().join('')) {
            res.push(i)
        }
    }

    return res
}


Idéia 2 - Comparação de início e fim de string: Converta números em strings; compare caracteres de início e fim de string.

function findPalindromeNumbers2(max) {
    const res = []
    if (max <= 0) return res

    for (let i = 1; i <= max; i++) {
        const s = i.toString()
        const length = s.length

        // 字符串头尾比较
        let flag = true
        let startIndex = 0 // 字符串开始
        let endIndex = length - 1 // 字符串结束
        while (startIndex < endIndex) {
            if (s[startIndex] !== s[endIndex]) {
                flag = false
                break
            } else {
                // 继续比较
                startIndex++
                endIndex--
            }
        }

        if (flag) res.push(res)
    }

    return res
}


Idéia 3 - Gerar números invertidos: Use % e Math.floor para gerar números invertidos; compare os números antes e depois (números operacionais do começo ao fim, sem tipo de string).

function findPalindromeNumbers3(max) {
    const res = []
    if (max <= 0) return res

    for (let i = 1; i <= max; i++) {
        let n = i
        let rev = 0 // 存储翻转数

        // 生成翻转数
        while (n > 0) {
            rev = rev * 10 + n % 10
            n = Math.floor(n / 10)
        }

        if (i === rev) res.push(i)
    }

    return res
}


Análise de desempenho: ficando mais rápido

  • Idéia 1- Parece ser O(n), mas a conversão e operação do array levam tempo, por isso é lento

  • Ideia 2 VS Ideia 3 – Manipule números mais rápido (o protótipo do computador é uma calculadora)

Resumindo, tente não converter estruturas de dados, especialmente estruturas ordenadas, como arrays, e tente não usar APIs integradas, como reversas. É difícil identificar a complexidade. As operações numéricas são as mais rápidas, seguidas por strings.

Tente usar código de "baixo nível"

Vamos direto para a próxima pergunta.

Insira uma string e troque as letras maiúsculas e minúsculas nela.
Por exemplo, insira a string 12aBc34 e produza a string 12AbC34.

Ideia 1 – Use expressões regulares.

function switchLetterCase(s) {
    let res = ''

    const length = s.length
    if (length === 0) return res

    const reg1 = /[a-z]
    const reg2 = /[A-Z]

    for (let i = 0; i < length; i++) {
        const c = s[i]
        if (reg1.test(c)) {
            res += c.toUpperCase()
        } else if (reg2.test(c)) {
            res += c.toLowerCase()
        } else {
            res += c
        }
    }

    return res
}


Ideia 2 – Julgar pelo código ASCII.

function switchLetterCase2(s) {
    let res = ''

    const length = s.length
    if (length === 0) return res

    for (let i = 0; i < length; i++) {
        const c = s[i]
        const code = c.charCodeAt(0)

        if (code >= 65 && code <= 90) {
            res += c.toLowerCase()
        } else if (code >= 97 && code <= 122) {
            res += c.toUpperCase()
        } else {
            res += c
        }
    }

    return res
}


Análise de desempenho: a primeira usa regularização e é mais lenta que a segunda

Portanto, tente usar código de "baixo nível" e use açúcar sintático, APIs de alto nível ou expressões regulares com cautela.

Camada inferior do computador

Finalmente, vamos falar sobre alguns dos aspectos subjacentes do computador que o front-end precisa entender.

Ler dados da "memória"

O que costumamos dizer: ler dados da memória significa ler dados em registradores.No entanto, nossos dados não são lidos diretamente da memória para os registradores, mas são lidos primeiro em um cache e depois lidos nos registradores.

O registrador está dentro da CPU e também faz parte da CPU, então a CPU lê e grava dados do registrador muito rapidamente.

Por que é isso? Porque a leitura de dados da memória é muito lenta.

Você pode entender desta forma: a CPU primeiro lê os dados no cache para uso e, quando é realmente usado, lê o registrador do cache; quando o registrador é usado, ele grava os dados de volta no cache e então O cache grava os dados na memória no momento apropriado.

A velocidade de operação da CPU é muito rápida, mas a leitura de dados da memória é muito lenta. Se você ler e gravar dados da memória todas as vezes, isso inevitavelmente diminuirá a velocidade de operação da CPU. A execução pode levar 100 segundos e 99 segundos serão gastos lendo dados. Para resolver este problema, colocamos um cache entre a CPU e a memória, e a velocidade de leitura e gravação entre a CPU e o cache é muito rápida. A CPU apenas lê e grava dados no cache, independentemente do cache e o cache.Como sincronizar dados entre memórias. Isso resolve o problema de leitura e gravação lenta na memória.

Operações de bits binários

O uso flexível de operações de bits binários pode não apenas aumentar a velocidade, mas o uso proficiente de binários também pode economizar memória.

Se um número n for dado, como determinar se n é 2 elevado à enésima potência?

É muito simples, basta pedir o restante.

function isPowerOfTwo(n) {
    if (n <= 0) return false
    let temp = n
    while (temp > 1) {
        if (temp % 2 != 0) return false
        temp /= 2
    }
    return true
}


Bem, não há nada de errado com o código, mas não é bom o suficiente. Dê uma olhada no código abaixo:

function isPowerOfTwo(n) {
    return (n > 0) && ((n & (n - 1)) == 0)
}


Você pode usar console.time e console.timeEnd para comparar a velocidade de execução.

Também podemos ver que existem muitas variáveis ​​de sinalizadores em alguns códigos-fonte. Realizamos operações AND bit a bit ou OR bit a bit nesses sinalizadores para detectar os sinalizadores e determinar se uma determinada função está habilitada. Por que ele simplesmente não usa valores booleanos? É muito simples, é eficiente e economiza memória.

Por exemplo, este código no código-fonte Vue3 não usa apenas AND bit a bit e OR bit a bit, mas também usa deslocamento para a esquerda:

export const enum ShapeFlags {
  ELEMENT = 1,
  FUNCTIONAL_COMPONENT = 1 << 1,
  STATEFUL_COMPONENT = 1 << 2,
  TEXT_CHILDREN = 1 << 3,
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}


if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
  ...
}


if (hasDynamicKeys) {
      patchFlag |= PatchFlags.FULL_PROPS
    } else {
    if (hasClassBinding) {
      patchFlag |= PatchFlags.CLASS
    }
    if (hasStyleBinding) {
      patchFlag |= PatchFlags.STYLE
    }
    if (dynamicPropNames.length) {
      patchFlag |= PatchFlags.PROPS
    }
    if (hasHydrationEventBinding) {
      patchFlag |= PatchFlags.HYDRATE_EVENTS
    }
}


Conclusão

O artigo explica o desempenho do front-end no nível do código, com dimensões detalhadas:

  • Análise aprofundada do conhecimento básico de JS

  • Código-fonte da estrutura

Existem também dimensões de largura:

  • Animações CSS, componentes

  • algoritmo

  • Camada inferior do computador

Espero que possa ajudar todos a ampliar seus horizontes em termos de desempenho de front-end. Se você estiver interessado no artigo, deixe uma mensagem para discussão ~~~

Autor: JD Retail Yang Jinjun

Fonte: JD Cloud Developer Community Por favor, indique a fonte ao reimprimir

Multado em 200 yuans e mais de 1 milhão de yuans confiscados You Yuxi: A importância dos documentos chineses de alta qualidade A migração radical de servidores de Musk O controle de congestionamento TCP salvou a Internet Apache OpenOffice é um projeto de fato “sem manutenção” Google comemora seu 25º aniversário Microsoft windows-drivers-rs de código aberto, use Rust para desenvolver drivers do Windows Raspberry Pi 5 será lançado no final de outubro, ao preço de US$ 60 macOS Containers: use o Docker para executar imagens do macOS no macOS IntelliJ IDEA 2023.3 EAP lançado
{{o.nome}}
{{m.nome}}

Acho que você gosta

Origin my.oschina.net/u/4090830/blog/10114289
Recomendado
Clasificación