一杯绿茶

一杯绿茶 查看完整档案

杭州编辑  |  填写毕业院校海康威视  |  前端开发 编辑 github.com/Jiacheng787 编辑
编辑

人在一起就是过节,心在一起就是团圆

个人动态

一杯绿茶 发布了文章 · 1月16日

Vue3.0 + Vite 初体验(一)项目配置

Vue3.0 已经发布一段时间了,特别是还发布了一个打包构建工具 Vite,据说是可以干掉 webpack 的,趁这段时间有空,本人也体验了一下。

想使用 Vue3.0 不一定要用 Vite,vue-cli 也可以构建。

$ npm install -g @vue/cli

确保已经升级到最新的版本:

$ npm update -g @vue/cli
$ vue --version

然后创建项目:

$ vue create vue3-demo

选择第二个 Vue 3 Preview,或者自定义安装

image.png

使用 CLI 构建的项目还是使用 webpack 进行打包的,这里就不展开了。这里介绍下用 vite 构建。首先安装 vite 构建工具:

$ npm install -g create-vite-app

创建项目:

# 下面两个命令都可以
$ create-vite-app vue3-demo
$ cva vue3-demo

跟 CLI 不一样,vite 目前没有提供命令行交互的选项,就是按照默认的模板进行创建的。CLI 创建项目会自动从 npm 拉取依赖,而 vite 创建项目没有拉取依赖,因此执行命令之后很快就创建完成了。

image.png

创建完成的项目结构是这样的:

image.png

package.json 中可以看到依赖非常少,只有 vite@vue/compiler-sfc ,没有 babel,因为 vite 是通过浏览器去解析模块。

image.png

接下来手动拉一下依赖:

$ npm install

然后启动项目:

$ npm run dev

简直就是几秒钟的时间,实在是太快了!

image.png

现在的项目只能用来写写 HelloWorld,还不能够进行实际的开发,下面来介绍一下项目配置。

1. 引入 vue-router 4.0.3

查看 vue-router 版本:

$ npm info vue-router versions

直接安装最新版 vue-router:

$ npm install vue-router@4.0.3

在 src 目录下创建以下目录:

- src
  |- router
  |  |- index.js
  |- views
     |- Home.vue
     |- Contact.vue

index.js 中添加如下代码:

import { createRouter, createWebHistory } from 'vue-router'

// 开启历史模式
// vue2中使用 mode: history 实现
const routerHistory = createWebHistory();

const router = createRouter({
    history: routerHistory,
    routes: [
        {
            path: '/',
            redirect: '/home'
        },
        {
            path: '/home',
            component: () => import('../views/Home.vue')
        },
        {
            path: '/contact',
            component: () => import('../views/Contact.vue')
        }
    ]
})

export default router

然后在 App.vue 中添加如下代码进行测试:

<template>
  <img alt="Vue logo" data-original="./assets/logo.png" />
  <div class="nav-btn">
    <router-link to='/'> Home</router-link>
    <router-link to='/contact'>Contact </router-link>
  </div>
  <router-view></router-view>
</template>

<script>
export default {

}
</script>

点击Home和Contact,路由发生变化,同时组件切换

2. 引入 less && less-loader

执行下面的命令,等待安装完成就可以使用了,不用在 main.js 中引入

$ npm install less less-loader --dev

注意这里有个坑,less 和 less-loader 需要写到 devDependencies 里面,下面的命令会把依赖写到 dependencies 里面,项目运行会报错

# 下面两个命令是一样的,都是写到 dependencies
$ npm install less less-loader
$ npm install less less-loader --save

3. 模块配置路径别名,浏览器打开和跨域

根目录下创建 vite.config.js ,添加配置:

const path = require('path')

module.exports = {
    alias: {
        '/@/': path.resolve(__dirname, './src')
    },
    hostname: '0.0.0.0', // 默认是 localhost
    port: '8000', // 默认是 3000 端口
    open: true, // 浏览器自动打开
    https: false, // 是否开启 https
    ssr: false, // 服务端渲染
    base: './', // 生产环境下的公共路径
    outDir: 'dist', // 打包构建输出路径,默认 dist ,如果路径存在,构建之前会被删除
    proxy: { // 本地开发环境通过代理实现跨域,生产环境使用 nginx 转发
    '/api': {
      target: 'https://blog.csdn.net/weixin_45292658',
      changeOrigin: true,
      rewrite: path => path.replace(/^\/api/, '')
    }
  }
}

这边也有一个坑,模块路径别名必须以 / 开头和结尾,否则 vite 无法识别。

4. 引入 element-plus 组件库

执行下面的命令进行安装:

$ npm install element-plus

这边采用全部引入的方式,在 main.js 中添加如下代码:

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import router from './router'
import ElementPlus from 'element-plus' // 这边引入 element-plus

const app = createApp(App)
app.use(ElementPlus) // 使用 element-plus
app.use(router)
app.mount('#app')

这样搞了之后发现样式没引进来,可能是因为 beta 版的原因,那就手动引入样式:

import 'element-plus/lib/theme-chalk/index.css'

结果 vite 在打包的时候报错了:

[vite] Failed to resolve module import "element-plus/lib/theme-chalk/index.cssoduleselement-pluslibheme-chalkindex.css". (imported by /src/main.js)

到 node_modules 里面找了下,模块路径没问题,为什么还是无法解析模块呢?试了下右键查看源代码:

<!DOCTYPE html>
<html lang="en">
<head>
<script type="module">import "/vite/client"</script>

  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vite App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" data-original="/src/main.js"></script>
</body>
</html>

这里可以看到引入了 main.js ,打开这个文件看下:

import { createApp } from '/@modules/vue.js'
import App from '/@/App.vue'
import '/@/index.css?import'
import router from '/@/router/index.js'
import ElementPlus from '/@modules/element-plus.js'
// import 'element-plus/lib/theme-chalk/index.css'

const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.mount('#app')

可以看到所有 js 文件经过 vite 处理后,模块路径都被修改了,从 node_modules 引入的模块前面都被加了 /@modules ,而 src 目录下引入的模块被加了 /@ ,由此得到启发:

import '/@modules/element-plus/lib/theme-chalk/index.css'

这边确实是个坑,总结一下:

// 模块路径前面有 ./ 的解析为从 ./src 目录下引入的模块
'./index.css' => '/@/index.css'

// 没有模块路径的解析为从 node_modules 引入的模块
'vue' => '/@modules/vue.js'
'element-plus' => '/@modules/element-plus.js'

// 下面两种可以在 webpack 构建的项目使用,但是 vite 是无法解析的
'index.css'
'element-plus/lib/theme-chalk/index.css'

Element plus文档国内镜像貌似挂掉了,只能看官方版的,也可以参考element-ui的文档,用法基本一样

Element plus 官方文档

5. 引入 vuex

查看 vuex 版本:

$ npm info vuex versions

直接安装最新版 vuex:

$ npm install vuex@4.0.0-rc.2

在 src 目录下创建 store 目录,在里面创建几个文件:

- store
  |- state.js
  |- getters.js
  |- mutations.js
  |- actions.js
  |- index.js

写入如下内容:

// state.js
let state = {}
export default state

// getters.js
let getters = {}
export default getters

// mutations.js
let mutations = {}
export default mutations

// actions.js
let actions = {}
export default actions

// index.js
import { createStore } from 'vuex'
import state from './state'
import getters from './getters'
import mutations from './mutations'
import actions from './actions'

export default createStore({
    state,
    getters,
    mutations,
    actions
})

然后在 main.js 中使用 vuex :

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import router from './router'
import store from './store' // 引入 vuex
import ElementPlus from 'element-plus'
import '/@modules/element-plus/lib/theme-chalk/index.css'

const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.use(store) // 使用 vuex
app.mount('#app')

测试一下,在 state.js 里面加一句:

let state = {
    username: 'admin';
}

页面组件里面写一个方法:

methods: {
    handleButtonEvent() {
        this.$message.success(this.$store.state.username)
    }
}

成功获取到 username 顺便还测了一下 element-plus 组件

6. 引入 axios

安装 axios:

$ npm install axios

在 src 目录下创建两个目录:

- src
  |- api
  |  |- sendMessage.js
  |- utils
     |- request.js

request.js 中添加以下代码:

import axios from 'axios'

const service = axios.create({
    baseURL: '/',
    timeout: 1000000, // 请求超时时间
})

export default service
关于 axios 的封装这里不详细展开

然后在 sendMessage.js 中添加以下代码:

import request from '/@/utils/request'

let shortMessages = {};
shortMessages.send = function(data) {
    return request({
        url: 'ec/api.php?mod=leads&act=getvalidatecode',
        data,
        method: 'post'
    })
}

export default shortMessages

在接口请求之前,确保 vite.config.js 中设置了代理,否则跨域请求会被浏览器同源策略阻止:

const path = require('path')

module.exports = {
    alias: {
        '/@/': path.resolve(__dirname, './src')
    },
    proxy: {
        '/ec': {
            target: 'https://e.qq.com', // 后端实际地址
            changeOrigin: true,
            // rewrite: path => path.replace(/^\/apiBase/, '')
        }
    }
}

经过这样配置之后,你会看到浏览器发送的地址是:

http://localhost:8000/ec/api.php?mod=leads&act=getvalidatecode

这个地址会被 vite 内置的 koa-proxies 转发给实际后端地址

可以参考:https://github.com/vitejs/vit...

未完待续

下一篇教程会分享如何使用 Vue3.0 风格的 api 进行开发,敬请期待!

查看原文

赞 0 收藏 0 评论 0

一杯绿茶 发布了文章 · 2020-12-04

Vue源码研究

写在前面

最近新部门的 React 项目要做多环境部署,参考了一下之前部门做的 Vue 项目,顺便把 vue-cli-service 的源码看了一下。看源码的时候还去看了下 Vue 源码,感觉挺有意思的,打算好好研究下,这里持续更新本人的心得体会~

vue-cli-service多环境

首先在 package.json 里面 script 下面添加如下内容:

"scripts": {
    "serve": "vue-cli-service serve",
    "serve:staging": "vue-cli-service serve --mode staging",
    "serve:prod": "vue-cli-service serve --mode production",
    "lint": "vue-cli-service lint",
    "format": "vue-cli-service lint --formatter",
    "inspect": "vue-cli-service inspect",
    "build": "vue-cli-service build",
    "build:staging": "vue-cli-service build --mode staging",
    "build:prod": "vue-cli-service build --mode production"
},

vue-cli-service 源码在 node_modules/@vue/cli-service/bin/vue-cli-service.js 目录下

vue-cli-service.js 在第 14 行加载了 ../lib/Service.js 模块,这个模块在 151 行加载了 serve.js 模块

serve.js 在 node_modules/@vue/cli-service/lib/commands/serve.js 目录下

// serve.js 第 50 行
// 检测环境,只在开发环境引入热更新插件
// webpack配置链式语法可以看一下
// configs that only matters for dev server
api.chainWebpack(webpackConfig => {
    if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
        webpackConfig
            .devtool('cheap-module-eval-source-map')

        webpackConfig
            .plugin('hmr')
            .use(require('webpack/lib/HotModuleReplacementPlugin'))

        // https://github.com/webpack/webpack/issues/6642
        // https://github.com/vuejs/vue-cli/issues/3539
        webpackConfig
            .output
            .globalObject(`(typeof self !== 'undefined' ? self : this)`)

        if (!process.env.VUE_CLI_TEST && options.devServer.progress !== false) {
            webpackConfig
                .plugin('progress')
                .use(require('webpack/lib/ProgressPlugin'))
        }
    }
})

vue-cli和creat-react-app打开浏览器的插件

Vue 中 openBrowser 的插件在 node_modules/@vue/cli-shared-utils 目录下。打开 index.js 可以看到如下内容:

34b3eaa1b92945b86554aa7f1a68db05.png

这边引入了 openBrowser.js 文件,这个文件在 lib 目录下,打开可以看到如下内容:

23a680a8d7ba9de85e999dfde2d1403b.png

这个插件跟 React 里面的 openBrowser.js 是一样的,React相关代码如下:

76eaa6f08e5f177fe5d2448d85e08758.png

Vue 检测数组变化

我们知道 Vue 2.x 版本的响应式机制是通过 Object.defineProperty 实现的,这种方式不能够检测数组长度的变化,只能将数组整个替换掉才会触发 setter。但是在实际开发中,调用数组的非变异方法(push,pop,shift,unshift,splice,sort,reverse),也能触发视图更新,这是因为 Vue 对这些方法进行了封装。

源码在 node_modules/vue/src/core/observer/array.js

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

其中 def() 的作用就是重新定义对象属性的value值,代码在 util/lang.js

/**
 * Define a property.
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

def(arrayMethods,method,function(){}) 这个函数可以看作 arrayMethods[method] = function mutator(){}。具体可以看下面的例子:

const push = Array.prototype.push;
 
Array.prototype.push = function mutator (...arg){
  const result = push.apply(this,arg);
  doSomething();
  return result
}
 
function doSomething(){
  console.log('do something');
}

首先第一步就是保存了原生的数组方法,先求出实际的值

注意:为触发视图更新,应该将数组整个替换/调用变异方法,或者使用非变异方法。直接用下标修改数组或者用 length 修改是无法被检测到的。

$nextTick原理

Vue 核心源码都在 node_modules/vue/src/core 目录下

这个目录下还包括 vdom ,observer ,global-api 的实现,都可以看一下

$nextTick 的源码在 node_modules/vue/src/core/util/next-tick.js 目录下

查看原文

赞 0 收藏 0 评论 0

一杯绿茶 发布了文章 · 2020-11-19

React调试工具安装

先去git上下载react-devtools文件到本地,直接下载zip就行https://github.com/facebook/r...

下载之后解压,进入下载的文件夹,用npm安装依赖。

安装完成后,打包一份扩展程序出来,npm run build:extension:chrome

打出来的扩展程序在react-devtools -> shells -> chrome -> build -> unpacked文件夹

把unpacked文件夹拖到Chrome扩展程序里面就可以了。

参考:
最简单的安装React Devtools调试工具

查看原文

赞 1 收藏 1 评论 0

一杯绿茶 关注了用户 · 2020-11-18

薄荷前端 @boohee

薄荷前端周刊,希望能够给一起走在前端路上的你带来一点小小的帮助。https://github.com/BooheeFE,欢迎Watch & Star ★

关注 1077

一杯绿茶 发布了文章 · 2020-11-16

React内容汇总

本人技术栈是Vue,最近调部门改成用React开发,从头开始踩坑,持续更新中~

1月18日内容汇总

记一次vue3.0技术分享会
Vue3.0 官方文档
备战2021:vite 工程化实践,建议收藏

1月14日内容汇总

如何实现一个react-router路由拦截(导航守卫)
React Router 4.0 实现路由守卫
https://segmentfault.com/a/11...
http://cache.baiducontent.com...
https://segmentfault.com/a/11...
https://segmentfault.com/a/11...
http://cache.baiducontent.com...
使用React.memo()来优化函数组件的性能

1月6日内容汇总

使用 npm install 拉取依赖的时候报错,可以试一下清空缓存,后面的参数 f 表示强制执行。

$ npm cache clean -f

1月1日内容汇总

Vue + Nuxt.js 项目推荐:
领课网络(RonCoo) / roncoo-education-web

React Redux 官方文档
React-Redux 文档 - Segmentfault

12月30日内容汇总

测试的同学提了一个老项目的缺陷,说是提交到后台的参数会把上一次操作的数据也给带上。我看了下代码,各种拼接字符串,而且他就是为了得到一个逗号分隔的字符串,还要手写 for 循环,直接调用数组的 join 或者 toString 不香吗?

修改方案:不管 nodesList 原来是不是空值,直接用一个新的值去替换;使用 map 方法生成一个节点ip的数组,调用 join 方法将数组转成字符串。

36c1c2534bc7937e2b5f0fd0990e0526.png

说明:Array.prototype.join() 或者 Array.prototype.toString() 都默认返回以逗号分隔的字符串,这里为了语义更好给 join 传了一个 "," 作为参数。如果不希望有分隔符可以使用 Array.prototype.join("")

12月29日内容汇总

注释// TODO的作用

在注释中写 // TODO,一些编辑器会进行代码高亮,例如 Android Studio,IDEA。在 vs code 中可以装一个插件 todo-tree,支持高亮显示 TODOFIXMETagDONE等注释,还可以自定义样式。
vscode 插件推荐 todo-tree

记一次vue3.0技术分享会
第二集: 从零开始实现一套pc端vue的ui组件库(icon组件)

12月28日内容汇总

今天同事问了一个问题,在前端项目中,public 和 src/asstes 都可用于存放静态资源,这两个有什么区别呢?

查了一下其实很简单,在 public 目录下的文件不会被 webpack 打包,而是直接复制一份到构建出来的文件夹中;在 src/asserts 下的文件会被打包编译(前提是在JS中有引入),从而在实际项目中减少 HTTP 请求。

在实际项目中,public还是有它的作用的。如果你希望你的文件不被编译,比如jquery.min.js,或者压缩好的js插件等,你就可以把文件放在public文件夹中,这样可以减少文件构建时间,还可以减少构建出来的文件大小。

React使用公共文件夹public

12月22日内容汇总

最近一个项目需要重构,但是那个项目不断还有新需求,给重构带来了一定难度。找了一些react后台模板,提升一下重构效率。

基于React+antd的后台管理模板(可预览)
react构建后台管理系统
React+AntD 后台管理系统解决方案 -- 终极版
使用React全家桶搭建一个后台管理系统
react中后台管理界面

12月2日内容汇总

JS 函数库

Vue 中主要使用 lodash ,React 中使用 Immutable.js
Immutable.js入门 - 知乎
为什么 react要使用immutable.js

Vue 和 React 项目多环境部署

开发环境:development
集成测试环境:staging
发布环境:production

vue-cli-service 机制

三分钟教你搞定 React 项目多环境配置 - 知乎
react----cross-env配置多环境变量
react使用docker时多环境支持
React 添加环境变量
基于create-react-app 的 webpack 配置多环境

11月30日内容汇总

React 中动态加载组件

React.lazy() 允许你定义一个动态加载的组件。这有助于缩减 bundle 的体积,并延迟加载在初次渲染时未用到的组件。

const SomeComponent = React.lazy(() => import('./SomeComponent'));
使用 React.lazy 的动态引入特性需要 JS 环境支持 Promise。在 IE11 及以下版本的浏览器中需要通过引入 polyfill 来使用该特性。

Vue 这个被称为异步组件,也是利用 Promise 来实现的。

Vue.component(
  'async-webpack-example',
  // 这个动态导入会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})

React 中的 style

React 中的 style 并不是 Vue 中的双大括号,最外面那个括号代表内部是 JS 表达式,里面的括号是 JS 对象。所以多个样式之间用 , 分隔,原生的 HTML 行内样式用 ; 分隔,样式属性命名采用小驼峰法。

style 通常用来绑定动态样式,元素的主要样式应该使用外部引入的 CSS 样式表
function HelloWorldComponent() {
  return <div style={{color: blue, fontSize: 14px}}>Hello World!</div>;
}

既然 style 其实是 JS 对象,那么就可以把对象抽出来,从而实现动态样式:

const divStyle = {
  color: 'blue',
  backgroundImage: 'url(' + imgUrl + ')',
};

function HelloWorldComponent() {
  return <div style={divStyle}>Hello World!</div>;
}
在 Vue 中可以通过计算属性实现动态样式

React 会自动添加 ”px” 后缀到内联样式为数字的属性后。如需使用 ”px” 以外的单位,请将此值设为数字与所需单位组成的字符串。

// Result style: '10px'
<div style={{ height: 10 }}> Hello World! </div>

// Result style: '10%'
<div style={{ height: '10%' }}> Hello World! </div>
但并非所有样式属性都转换为像素字符串。有些样式属性是没有单位的(例如 zoomorderflex)

className 和 htmlFor

由于 classfor 都是 JS 关键字,所以在 React 里面使用 classNamehtmlFor 代替。

dangerouslySetInnerHTML

React 中使用 dangerouslySetInnerHTML 渲染 HTML,尽量不要用,容易导致 XSS 攻击。

// 需要传一个 key 为 __html 的对象
function createMarkup() {
  return {__html: 'First &middot; Second'};
}

function MyComponent() {
  return <div dangerouslySetInnerHTML={createMarkup()} />;
}
Vue 中使用 v-html 指令渲染 HTML

11月19日内容汇总

JSX 的一些注意事项

使用 JSX 必须在 React 作用域内,这样才能编译。在如下代码中,虽然 ReactCustomButton 并没有被直接使用,但还是需要导入:

import React from 'react';
import CustomButton from './CustomButton';

function WarningButton() {
     return <CustomButton color="red" />;
}

JSX 中可以使用点语法,例如:

<React.Fragment></React.Fragment>
<Form.Item></Form.Item>

自定义组件必须大写字母开头,才能被识别为 React 组件。

React 组件名不能是 JS 表达式,如果需要动态渲染组件,需要先将 组件名赋值给一个大写字母开头的变量:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}

与 Vue 类似,React 的 props 花括号里面接受 JS 表达式:

// React 用法
<MyComponent foo={isShow ? "1" : "0"} />
// Vue 用法
<my-component :foo="isShow ? '1' : '0'" />

if 语句以及 for 循环不是 JavaScript 表达式,所以不能在 JSX 中直接使用。但是,你可以用在 JSX 以外的代码中。

React 组件通过 props 传字符串有两种做法,第一种直接通过字符串字面量:

// React 用法
<MyComponent message="hello world" />
// Vue 用法
<my-component message="hello world" />

另一种是通过表达式:

// React 用法
<MyComponent message={'hello world'} />
// Vue 用法
<my-component :message="`hello world`" /> // Vue的属性前面加上v-bind,后面的""里面就是JS表达式

React 如果没有给 props 赋值,它会有一个默认值 true

这点我觉得还是 Vue 做得好,可以手动指定 props 的默认值和类型,类型传错了还会给你警告,维护起来相当方便

React 中的 props 对象可以进行属性展开,下面两个组件是等价的:

function App1() {
    return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
    const props = {firstName: 'Ben', lastName: 'Hector'};
    return <Greeting {...props} />;
}

下面这个技巧非常有用,选择只保留当前组件需要接收的 props,并使用展开运算符将其他 props 传递下去:

const Button = props => {
    const { kind, ...other } = props;
    const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
    return <button className={className} {...other} />;
};

高阶组件

高阶组件在 React 官方文档的说法是用于替代 mixins 实现组件方法复用。

高阶组件是参数为组件,返回值为新组件的函数

也就是说在组件外面封装了一层,把可以复用的方法都抽出来。

我看了一下文档的示例代码,觉得高阶组件的场景和 mixins 其实还是有区别的,而且 mixins 是可以全局混入的。文档上的示例我觉得更接近于 Vue 中的动态组件。

function withSubscription(WrappedComponent, selectData) {
  // ...并返回另一个组件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }
    componentDidMount() {
      DataSource.addChangeListener(this.handleChange);
    }
    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }
    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }
    render() {
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

使用的时候把组件传递给 withSubscription() :

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

之前不理解React 路由为什么要这样写,现在明白了,这其实就是高阶组件:

const NavbarWithRouter = withRouter(Navbar);

Vue 动态组件的 is 属性也可以接收一个组件作为参数,同时也能传递 props

<component :is="currentTabComponent" :values="values"></component>

11月18日内容汇总

React 组件生命周期调用顺序

挂载

组件实例被创建并插入DOM
  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

更新

当组件的 props 或 state 发生变化时会触发更新
  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

卸载

当组件从 DOM 中移除时会进行调用
  • componentWillUnmount()

错误处理

当渲染过程,生命周期,或子组件的构造函数中抛出错误时会进行调用
  • static getDerivedStateFromError()
  • componentDidCatch()

更多内容参考:
常用生命周期方法

2.x 的生命周期函数与新版 Composition API 之间的映射关系:

  • beforeCreate -> 使用 setup()
  • created -> 使用 setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

注意:beforeCreatecreatedVue3 中已经由 setup 替代。

参考资料

Vue 3.0 有很多借鉴了 React 的地方,看了 React 文档之后 可以对比着看下 Vue 3.0 。

Vue 3.0 修炼手册
抄笔记:尤雨溪在Vue3.0 Beta直播里聊到了这些…
API 手册 - Vue 组合式 API

Koa 快速入门系列

【从前端到全栈】- koa快速入门指南
【进阶篇】koa+Mysql的全栈之旅
【全栈之旅】NodeJs登录流程以及Token身份验证

11月17日内容汇总

React Hooks的用法

函数组件有重大限制,必须是纯函数,不能包含状态,也不支持生命周期方法。React Hooks 的设计目的,就是加强版函数组件,完全不使用"类",就能写出一个全功能的组件。

四个常用Hooks

useState()
useContext()
useReducer()
useEffect()

useState() 用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。

import React, { useState } from "react";

export default function  Button()  {
  const [buttonText, setButtonText] = useState("Click me,   please");

  function handleClick()  {
    return setButtonText("Thanks, been clicked!");
  }

  return  <button  onClick={handleClick}>{buttonText}</button>;
}

useEffect()用来引入具有副作用的操作,最常见的就是向服务器请求数据。以前,放在componentDidMount里面的代码,现在可以放在useEffect()

useEffect(()  =>  {
 // 这里调接口
}, [dependencies])

useEffect() 接受两个参数。第一个参数是一个函数,异步操作的代码放在里面。第二个参数是一个数组,用于给出 Effect 的依赖项,可以省略。组件第一次渲染时, useEffect() 就会执行。当依赖项的数组发生变化, useEffect() 也会执行。

Vue 3.0 的 Composition API 借鉴了 React Hooks

轻松学会 React 钩子:以 useEffect() 为例 - 阮一峰
React Hooks 入门教程 - 阮一峰

React中Fragment的用法

与 Vue 类似, React 组件要求 return 的内容必须由唯一根元素包裹,否则就会报错。

如果希望在不额外创建DOM元素的情况下 return 多个元素,就可以用到Fragment。

render() {
  return (
    <React.Fragment>
      Some text.
      <h2>A heading</h2>
    </React.Fragment>
  );
}
Vue 中的 template 也是虚拟元素,但还是有区别
在 template 中的元素必须要唯一根元素包裹
template 仅仅只是在条件渲染的时候可以不用引入新的DOM元素
Vue 3.0 中也提供了类似 React 的 Fragment

Fragment还有一种简写语法 <></> 。<React.Fragment>可以接受键值或者属性,例如在列表渲染的时候绑定 key ,但是 <></> 不行。

render() {
  return (
    <>
      Some text.
      <h2>A heading</h2>
    </>
  );
}

另外react 16开始, render支持返回数组,可以减少不必要的节点嵌套。

export default function () {
    return [
        <div>1</div>,
        <div>2</div>,
        <div>3</div>
    ];
}
Vue 3.0 的 render 函数同样也支持返回数组

Fragment - React中文文档

组件继承的两种写法

以上面的Fragment为例,如果只引入 React ,那么使用就要像这样 <React.Fragment>

import React from "react";

function App() {
    return (
        <React.Fragment>
            {/* Some text here */}
        </React.Fragment>
    )
}

如果像下面这样引入,就可以像这样使用 <Fragment>

import React, { Fragment } from "react";

function App() {
    return (
        <Fragment>
            {/* Some text here */}
        </Fragment>
    )
}

严格模式

嵌套在 <StrictMode> 标签中的元素将开启严格模式,用来突出显示应用程序中潜在问题。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。

严格模式检查仅在开发模式下运行;它们不会影响生产构建
import React from 'react';

function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  );
}

React’s diff algorithm

11月16日内容汇总

React事件类型

输入框:onChange,onKeyUp
按钮:onClick

React监听回车事件

<Input placeholder="www.chao99.top" onKeyUp={this.onKeyup} />
onKeyup(e) {
    if(e.keyCode === 13) {
        this.handleBtnClick()
    }
}

报错信息

Cannot update during an existing state transition (such as within render)

这个一般是事件处理函数的 this 没有绑定导致的 。

报错信息

Failed prop type: You provided a value prop to a form field without an onChange handler. This will render a read-only field.

受控组件表单必须要绑定 onChange 事件,否则这个表单就是只读,无法修改。

不能修改的前提是 value 被赋了一个常量。如果将空值 null 赋给 value ,还是可以修改的。

受控组件:自身的value值由组件的state托管,这也是 React 官方推荐的。

<Input value={this.state.name} onChange={this.handleChange} />

上面的代码中, value 用来给表单赋值, onChange 事件用于触发事件处理函数修改 state 。在 Vue 中直接就是一个语法糖 v-model

Vue 中的 <input v-model="searchText"> 等价于
<input :value="searchText" @input="searchText = $event.target.value">

非受控组件:value由第三方库或者DOM进行托管。

<Input type="file" ref={this.input}/>

文件上传是典型的非受控组件。

React修改antd原生样式

在 Vue 中直接修改是不生效的,需要通过样式穿透 >>>::v-deep

在 React 里面,调出控制台找到组件对应的类名,然后在样式文件中直接修改就行。

React修改Antd组件样式的方法

查看原文

赞 0 收藏 0 评论 0

一杯绿茶 发布了文章 · 2020-11-15

React 核心概念整理

最近调部门,改用React开发了,整理一些常用知识点。

更多内容还请参考:
React 中文文档
Ant Design

1. 项目创建

# 创建项目
npx create-react-app your-project-name

# antd
npm i antd

# 路由
npm i react-router-dom

2. 启动服务

npm start
Vue 脚手架构建的项目使用 npm run serve

3. 安装插件

VS Code安装ES 7,非常方便生成组件代码

创建 Login.js 文件

输入 rfce 按回车,就可以初始化一个组件代码

import React from 'react'

function Login() {
    return (
        <div>
        
        </div>
    )
}

export default Login

4. 创建路由

1)简单实现

打开根目录中的 index.js 文件,引入路由模块,添加如下代码:

import React from 'react';
import ReactDOM from 'react-dom';
// 引入路由模块,使用哈希路由
import { HashRouter as Router, Switch, Route } from 'react-router-dom';
import './index.css';
// 引入组件
// 不用自己引入,当用到的时候,编辑器会自动添加
import Login from './pages.Login';
import List from './pages/admin/products/List';

ReactDOM,render(<Router>
    <Switch>
        <Route path="/login" component={Login} />
        <Route path="/admin/products" component={List} />
    </Switch>
</Router>, document.getElementById('root'));
React 里面的 index.js 相当于 Vue 的 main.js 文件

2)正常用法

在开发中一般不会这样写,而是在根目录下建一个 routes 目录,在 routes 目录下建 index.js 文件,添加如下代码:

import Login from "../pages/Login";
import Index from "../pages/admin/dashboard/Index";
import List from "../pages/admin/products/List";
import Edit from "../pages/admin/products/Edit";

// 普通页面路由
export const mainRoutes = [{
    path: '/login',
    component: Login
}, {
    path: '/404',
    component: PageNotFound
}]

// 需要鉴权的路由
export const adminRoutes = [{
    path: '/admin/dashboard',
    component: Index
}, {
    path: '/admin/products',
    component: List,
    exact: true // 路由path和下面重复了,需要设置一下,只匹配当前path
}, {
    path: '/admin/products/edit/:id',
    component: Edit
}]

然后 index.js 文件可以改成下面这样:

import React from 'react';
import ReactDOM from 'react-dom';
// 引入路由模块,使用哈希路由
import {
    HashRouter as Router,
    Switch,
    Route,
    Redirect
} from 'react-router-dom';
import './index.css';
import { mainRoutes } from "./routes";

ReactDOM,render(
    <Router>
        <Switch>
            { mainRoutes.map(route => {
                // return <Route key={route.path} path={route.path} component={route.component} />
                // 由于route里面的key也是path和component,所以可以直接用ES6延展运算符
                return <Route key={route.path} {...route} />
            }) }
            <!-- 当找不到页面的时候就重定向到404 -->
            <Redirect to="/404" />
        </Switch>
    </Router>,
    document.getElementById('root')
);

3)嵌套路由

当访问的地址以 /admin 开头的时候,都走 App 这个容器。

index.js 文件中再加一个 Route,代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import {
    HashRouter as Router,
    Switch,
    Route,
    Redirect
} from 'react-router-dom';
import './index.css';
import App from "./App";
import { mainRoutes } from "./routes";

ReactDOM,render(
    <Router>
        <!-- 这里再加一个Route,把路由属性传过去 -->
        <Route path="/admin" render={routeProps => <App {...routeProps} />} />
        <Switch>
            { mainRoutes.map(route => {
                return <Route key={route.path} {...route} />
            }) }
            <Redirect to="/404" />
        </Switch>
    </Router>,
    document.getElementById('root')
);

然后在 App.js 里面添加如下代码:

import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import { adminRoutes } from './routes';

function App() {
    return (
        <div className="App">
            <h1>这是App组件</h1>
            <Switch>
                {adminRoutes.map(route => {
                    return (
                        <Route
                            key={route.path}
                            path={route.path}
                            exact={route.exact}
                            render={routeProps => {
                                return <route.component {...routeProps} />
                            }}
                        />
                    )
                })}
                <Redirect to="/404" />
            </Switch>
        </div>
    )
}

export default App
React 里面的 <Switch> 相当于 Vue 的 <router-view>

问题:Vue 可以直接定义嵌套路由,React 可以吗

5. 搭建页面框架

1)组件中引入图片

import React from 'react';
import logo from "./logo.png";

function App() {
    return (
        <div className="logo">
            <img data-original={logo} alt="logo" />
        </div>
    )
}

export default App

2)定义行内样式

import React from 'react';

function App() {
    return (
        <div style={{ background: "#fff" }}>
        </div>
        <div style={{ height: "100%", borderRight: 0 }}>
        </div>
        <div style={{ padding: "0 24px 24px" }}>
        </div>
        <div style={{ margin: "16px 0" }}>
        </div>
    )
}

export default App
在 React 中 {} 内部其实就是 JS 表达式

样式其实是以 JS 对象形式定义的

style 看上去是双大括号,但其实两层括号是不一样的,外层括号代表里面定义的是 JS 表达式,内层括号代表 JS 对象

3)路由点击跳转

import React from 'react';
// 在组件中使用路由需要用到 withRouter 插件,否则会报错
import { withRouter } from "react-router-dom";
// 在组件中引入 antd
import { Layout, Menu, Breadcrumb, Icon } from "antd";
import { adminRoutes } from "../../routes/index";

const routes = adminRoutes.filter(route => route.isShow);
function Index() {
    return (
        <Menu
            mode="inline"
            defaultSelectedKeys={["1"]}
            defaultOpenKeys={["sub1"]}
            style={{ height: "100%", borderRight: 0 }}
        >
            {routes.map(route => {
                return (
                    <Menu.Item
                        key={route.path}
                        onClick={p => props.history.push(p.key)}
                    >
                        <Icon type={route.icon}
                        {route.title}
                    </Menu.Item>
                )
            })}
        </Menu>
    )
}

export default withRouter(Index); // 这里需要把组件套进去
问题:Vue 可以直接通过全局方法 this.$router.push() 实现路由跳转,React 没有吗

问题:React 的 antd 组件能否全局注册

4)列表页面搭建

import React from 'react';
import { Card, Table, Button, Popconfirm } from "antd";

// 数据源
const dataSource = [{
    id: 1,
    name: "Jack",
    price: 8888
}, {
    id: 2,
    name: "Pony",
    price: 2333
}, {
    id: 3,
    name: "Eric",
    price: 9999
}]

function List() {
    // 表头字段
    const columns = [{
        title: '序号',
        key: "id",
        width: 80,
        align: "center",
        render: (txt, record, index) => index + 1 // 自定义字段类型
    }, {
        title: "名字",
        dataIndex: "name" // 对应 dataSource 里面的 name 字段
    }, {
        title: "价格",
        dataIndex: "price" // 对应 dataSource 里面的 price 字段
    }, {
        title: "操作",
        render: (txt, record, index) => {
            return (
                <div>
                    <Button type="primary" size="small">修改</Button>
                    <Popconfirm
                        title="确定删除此项?"
                        onCancel={() => console.log("用户取消删除!")}
                        onConfirm={() => console.log("用户确认删除!")}
                    >
                        <Button style={{ margin: "0 1rem" }} size="small">删除</Button>
                    </Popconfirm>
                </div>
            )
        }
    }]
    
    return (
        <Card
            title="商品列表"
            extra={
                <!-- 这里是扩展项 -->
                <Button type="primary" size="small">
                    新增
                </Button>
            }
        >
            <Table columns={columns}
                   bordered
                   dataSource={dataSource}
            />
        </Card>
    )
}

export default List;
React 列表自定义字段类型通过 render 实现, 在 Vue 中则是通过 slot 实现

6. 函数组件和 Class 组件

函数组件适合一些比较简单的应用场景,只接受外面传进来的 props , 没有自己的私有数据 state 和生命周期:

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

如果需要用到私有数据 state 和生命周期就要创建 Class 组件,Class 需要继承 React.Component

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
Class 组件的 render() 方法返回的东西就是函数组件里面的内容

render() 方法里面需要通过 this 访问 propsstate

7. 事件处理

在 React 中事件的命名采用小驼峰法,而在传统 HTML 中是纯小写。

<button onClick={activateLasers}>
  Activate Lasers
</button>

在 React 中必须显式地使用 preventDefault 阻止默认事件,而在传统 HTML 中可以直接 return false ,代码如下:

function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}
在这里,e 是一个合成事件。React 事件与原生事件不完全相同。

1)事件处理函数 this 的绑定

a)在构造器中绑定 this

在 JavaScript 中,class 的方法默认不会绑定this。如果你忘记绑定 this.handleClick 并把它传入了 onClick,当你调用这个函数的时候 this 的值为 undefined

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // 为了在回调中使用 `this`,这个绑定是必不可少的
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(state => ({
      isToggleOn: !state.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

如果觉得使用 bind 很麻烦,这里有两种方式可以解决。

b)使用 class fields

一种是可以使用 class fields 正确的绑定回调函数,Create React App 默认启用此语法:

class LoggingButton extends React.Component {
  // 此语法确保 `handleClick` 内的 `this` 已被绑定。
  // 注意: 这是 *实验性* 语法。
  handleClick = () => {
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

c)使用箭头函数

另一种是可以在回调中使用箭头函数,此语法问题在于每次渲染 LoggingButton 时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染:

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    // 此语法确保 `handleClick` 内的 `this` 已被绑定。
    return (
      <button onClick={() => this.handleClick()}>
        Click me
      </button>
    );
  }
}

2)向事件处理函数传递参数

下面两种方式是等价的。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数会被隐式的进行传递。

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

8. 条件渲染

基础用法:

class LoginControl extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isLoggedIn: false};
  }

  render() {
    const isLoggedIn = this.state.isLoggedIn;
    let button;
    if (isLoggedIn) {
      button = <LogoutButton onClick={this.handleLogoutClick} />;
    } else {
      button = <LoginButton onClick={this.handleLoginClick} />;
    }

    return (
      <div>
        <Greeting isLoggedIn={isLoggedIn} />
        {button}
      </div>
    );
  }
}

与运算符 &&

render() {
  const count = 0;
  return (
    <div>
      { count && <h1>Messages: {count}</h1>}
    </div>
  );
}
之所以能这样做,是因为在 JavaScript 中,true && expression 总是会返回 expression, 而 false && expression 总是会返回 false。因此,如果条件是 true&& 右侧的元素就会被渲染,如果是 false,React 会忽略并跳过它。

在上面的代码中,如果 count 的值为0,还是会进行渲染。

三元运算符

render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      {isLoggedIn
        ? <LogoutButton onClick={this.handleLogoutClick} />
        : <LoginButton onClick={this.handleLoginClick} />
      }
    </div>
  );
}

9. 状态提升

实际上就是子组件向父组件传参

子组件代码:

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    // 向外传递自定义事件
    this.props.onTemperatureChange(e.target.value);
  }

  render() {
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

父组件代码:

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {temperature: '', scale: 'c'};
  }

  <!-- 自定义事件处理函数 -->
  handleCelsiusChange(temperature) {
    this.setState({scale: 'c', temperature});
  }

  render() {
    const scale = this.state.scale;
    const temperature = this.state.temperature;
    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;

    return (
      <div>
        <!-- 绑定自定义事件处理函数 -->
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange} />
        <BoilingVerdict
          celsius={parseFloat(celsius)} />
      </div>
    );
  }
}
在 React 中使用 this.props.onTemperatureChange(e.target.value); 向外传递事件同时携带参数

相当于 Vue 中的 this.$emit("onTemperatureChange", e.targetvalue)

10. react-redux

  • redux三个基本原则:
    ①:store必须是唯一的
    ②:只有store可以改变自己的内容
    ③:reducer 必须是纯函数
  • 只有store能改变自己内容说明在reducer里我们不能直接操作state,只能通过定义新变量copy state的值,然后对新变量进行操作并 return 出新变量,不允许直接改变state。
  • 什么是纯函数?
    给固定的输入,就一定会有固定的输出,并且不会有任何副作用。
    所以对于异步函数(定时器、ajax数据请求等)、动态时间都不适意在reducer里订阅。
  • store核心api:
    crerteStore(): 创建一个store
    store.dispatch(action): 派发action,传递store
    store.getState(): 获取store的所有数据
    store.subscribe(): 订阅store的变化,接收的回调函数在store改变时候就会自动执行
查看原文

赞 1 收藏 1 评论 0

一杯绿茶 发布了文章 · 2020-10-18

JS生成链表和二叉树

最近刷算法题,经常有链表和二叉树的结构,在线运行代码都是直接传入链表或者二叉树的头节点,但是JS中没有这样的结构,本地没办法进行调试。今天专门写了下JS生成链表和二叉树的方法,下次本地调试就可以直接生成测试用例了。

链表生成

代码如下:

// ListNode构造器,用于创建链表的每个节点
class ListNode {
    constructor(val, next) {
        this.val = (val == undefined ? 0 : val);
        this.next = (next == undefined ? null : next);
    }
}

// 生成链表
function generateLinklist(arr) {
    let head = new ListNode(arr[0]), // 初始化第一个节点作为头节点
        curr = head; // curr指针保存当前节点
    for(let i=1; i<arr.length; i++) {
        curr.next = new ListNode(arr[i]); // 创建next节点
        curr = curr.next; // curr后移一位
    }
    return head // 返回头节点
}

// 调用
const head = generateLinklist([1, 2, 3, 4]);
console.log(head); // 1 -> 2 -> 3 -> 4

实现思路还是比较简单的,但是有一个地方需要注意,curr只能保存当前节点,即下一节点还没创建的时候不能提前移动。生成链表的方法本人最开始是这样写的:

// 注意,下面这段代码是错误的
function generateLinklist(arr) {
    let obj = new Linklist(arr[0]),
        curr = obj.next;
    for(let i=1; i<arr.length; i++) {
        curr = new Linklist(arr[i]);
        curr = curr.next;
    }
    return obj
}

上面的代码咋一看好像也没问题,但是拿上面用例进行测试的时候发现返回的链表只有一个节点,后面都不见了:

ListNode { val: 1, next: null }

这段代码的问题就在于,curr提前进行移动了。上面的代码中,先初始化第一个节点obj作为头节点,然后curr = obj.next的本意是希望curr指向obj的下一节点。但是curr并没有指向“下一节点”,而是指向了null,因为下一节点根本还没创建。因为obj.next == null,所以curr = obj.next相当于curr = null,如下图所示:

image.png

这样一来,执行curr = new Linklist(arr[i]);的时候,链表的第二个节点的指针其实是赋给了curr,而不是obj.next

image.png

后面每一次for循环,都是先将新的节点指针赋给curr,然后执行curr = curr.next;curr的值改为null,而这些操作都与obj无关,obj还是最开始那个创建的头节点。

所以从这里得出一个结论,当下一节点还未创建的时候,指针不能提前移动。在第一段代码中,我们看到是先创建节点,再移动curr指针的。

二叉树生成

层序遍历的反向操作,也是利用队列实现,代码如下:

// treeNode构造器
class treeNode {
    constructor(val, left, right) {
        this.val = (val == undefined ? 0 : val);
        this.left = (left == undefined ? null : left);
        this.right = (right == undefined ? null : right);
    }
}

// 生成二叉树
function generateBinarySearchTree(arr) {
    let root = new treeNode(arr[0]),
        curr = root,
        queue = new Array(),
        n = 0;
    queue.push(curr);
    while(queue.length > 0) {
        let size = queue.length;
        for(let i=0; i<size; i++) {
            curr = queue.pop();
            curr.left = arr[n+1] ? new treeNode(arr[n+1]) : null;
            curr.left && queue.unshift(curr.left); // 如果是null就不入队
            n++;

            curr.right = arr[n+1] ? new treeNode(arr[n+1]) : null;
            curr.right && queue.unshift(curr.right); // 如果是null就不入队
            n++;
        }
    }
    return root
}

generateBinarySearchTree([1, 2, 3, 4, 5, 6, 7])

代码感觉有些地方可以优化,有空再改下。

查看原文

赞 0 收藏 0 评论 0

一杯绿茶 发布了文章 · 2020-10-13

二叉树遍历JS实现和LeetCode题解

基本概念

二叉树遍历主要为深度优先(DFS)和广度优先(BFS),其中深度优先遍历包括前序、中序、后序,广度优先遍历也叫层序遍历。
image.png
深度优先三种方法遍历顺序:
image.png

其实很好记,就是中间节点在最前面、中间和最后面输出,而左右的相对顺序是固定的。

例如下面这棵树:

    1
   / \
  2   3
 / \ / \
4  5 6  7

前序遍历:1, 2, 4, 5, 3, 6, 7
中序遍历:4, 2, 5, 1, 6, 3, 7
后序遍历:4, 5, 2, 6, 7, 3, 1

深度优先和广度优先都能通过递归实现,由于递归方法过于简单,面试考察的通常是非递归实现。深度优先的非递归方法通过堆栈实现,广度优先的非递归方法通过队列实现

递归实现

前序遍历:

function preOrder(root) {
    if (root == null)
        return;
    console.log(root.val); // 输出控制
    preOrder(root.left);
    preOrder(root.right);
}

而中序遍历和后序遍历则只需修改输出控制的位置。

中序遍历:

function inOrder(root) {
    if (root == null)
        return;
    inOrder(root.left);
    console.log(root.val); // 输出控制
    inOrder(root.right);
}

后序遍历:

function postOrder(root) {
    if (root == null)
        return;
    postOrder(root.left);
    postOrder(root.right);
    console.log(root.val); // 输出控制
}

非递归实现

非递归版本使用堆栈实现,三种遍历方法主要是压栈弹栈的顺序不同。

前序遍历

一开始先把根节点压栈,每次弹出栈顶元素的同时输出该元素,然后把栈顶元素的右节点、左节点分别入栈(如果有的话,为空则不用);直到栈为空则停止。

image.png

代码如下:

function preOrderByStack(root) {
    let res = new Array();
    if (root == null) // 边界判断
        return res;
    let stack = new Array();
    stack.push(root); // 先把根节点压栈
    while (stack.length > 0) {
        root = stack.pop(); // 弹出当前栈顶元素
        res.push(root.val); // 保存结果
        if (root.right != null) {
            stack.push(root.right); // 先压入右节点
        }
        if (root.left != null) {
            stack.push(root.left); // 再压入左节点
        }
    }
    return res;
}

上面的代码复用了root变量,好处就是不使用额外的变量,降低空间复杂度。

后序遍历

后序遍历有一种非常trick的做法。我们知道先序遍历为中左右,而后序遍历为左右中,我们把后序遍历反过来,就是中右左,是不是发现和先序遍历有点像了?我们先序遍历采用了先压入右节点再压入左节点的方式得到了中左右的顺序,那么我们只要先压入左节点,再压入右节点,就能得到中右左的顺序,这里只要保存结果的时候从前往后插入,就变成了我们想要的后序遍历了:左右中

function preOrderByStack(root) {
    let res = new Array();
    if (root == null)
        return res;
    let stack = new Array();
    stack.push(root);
    while (stack.length > 0) {
        root = stack.pop();
        res.unshift(root.val); // 从数组头部添加结果
        if (root.left != null) {
            stack.push(root.left); // 先压入左节点
        }
        if (root.right != null) {
            stack.push(root.right); // 再压入右节点
        }
    }
    return res;
}

后序遍历可以求二叉树的深度。

中序遍历

当前节点只要有左节点,就将其左节点压栈,并且当前节点向其左节点方向移动,直到当前节点为空,说明此时位于最左下方的节点的空左节点处,那么接下来我们就需要弹栈获取栈顶,输出元素,然后移动到栈顶节点的右节点处。

image.png

代码如下:

function inOrderByStack(root) {
    let res = new Array();
    let stack = new Array();
    while (stack.length > 0 || (root != null)) {
        if (root != null) { // 当前节点非空,压栈后向左移动
            stack.push(root);
            root = root.left;
        } else { // 当前节点为空,弹栈输出后向右移动
            root = stack.pop();
            res.push(root.val);
            root = root.right;
        }
    }
    return res;
}

上面介绍的三种方法可以用于遍历,但是打印路径就不太方便。如果需要打印出二叉树的所有路径,可以使用下面的代码:

function dfsToPath(root) {
    let res = new Array();
    if(!root) return res;
    let stack = new Array();
    stack.push(root);
    while(stack.length > 0) {
        root = stack[stack.length-1]; // 获取栈顶元素但不弹出该元素
        // 存在左节点且左节点未遍历
        if(root.left && !root.isLeft) {
            root.isLeft = true; // 标记为已遍历
            stack.push(root.left);
            continue;
        }
        // 存在右节点且右节点未遍历
        if(root.right && !root.isRight) {
            root.isRight = true; // 标记为已遍历
            stack.push(root.right);
            continue;
        }
        // 遍历到叶子节点时保存当前路径
        if(!root.left && !root.right) {
            let temp = stack.map(item => item.val);
            res.push(temp);
        }
        // 弹出叶子节点或者子树都已遍历的节点
        stack.pop();
    }
    return res
}

下面还有两个递归版本,也可以参考下。

下面的代码输出结果是形如[ '1->2->4', '1->2->5', '1->3->6', '1->3->7' ]的字符串数组:

function binaryTreePathsString(root) {
  const paths = [];
  const construct_paths = (root, path) => {
    if (root) {
      path += root.val.toString();
      if (root.left === null && root.right === null) { // 当前节点是叶子节点
        paths.push(path); // 把路径加入到答案中
      } else {
        path += "->"; // 当前节点不是叶子节点,继续递归遍历
        construct_paths(root.left, path);
        construct_paths(root.right, path);
      }
    }
  }
  construct_paths(root, "");
  return paths
}

下面代码输出的结果是形如[ [ 1, 2, 4 ], [ 1, 2, 5 ], [ 1, 3, 6 ], [ 1, 3, 7 ] ]的数组:

function binaryTreePathsArray(root) {
    let paths = [];
    const resc = (root, path) => {
        if(root) {
            // path.push(root.val); 这样的写法是错误的
            // 数组是引用类型,跟上面代码的字符串是不一样的
            // 每次递归的时候都要浅拷贝一下
            path = [...path, root.val]; 
            if(root.left == null && root.right == null) {
                paths.push(path);
            } else {
                resc(root.left, path);
                resc(root.right, path);
            }
        }
    }
    resc(root, []);
    console.log(paths);
}

掌握上面的代码,下面的问题应该都能解决了:

剑指 Offer 68 - II. 二叉树的最近公共祖先
剑指 Offer 34. 二叉树中和为某一值的路径
124. 二叉树中的最大路径和

难度中等101收藏分享切换为英文接收动态反馈

层序遍历

简单来说就是一行一行地遍历,基于队列来做,先把根节点入队列,只要队列非空,每次把队头结点弹出,然后把堆头的左右节点压入队列中,这样最终遍历出来的就是层序遍历的顺序。

function levelOrder(root) {
    if (root == null)
        return null;
    let res = new Array();
    let queue = new Array();
    queue.unshift(root); // 先把根节点入队列
    while (queue.length > 0) { // 队列非空
        root = queue.pop();
        res.push(root.val); // 弹出队头节点
        if (root.left != null) queue.unshift(root.left);
        if (root.right != null) queue.unshift(root.right);
    }
    return res;
}

上面是一种比较简单的实现,只能按顺序进行遍历,例如[1, 2, 3, 4, 5, 6, 7],无法获取每一层的节点。

如果需要单独打印出每一层的节点,可以使用下面的写法:

function levelOrder(root) {
  if (root == null)
    return null;
  let res = new Array();
  let queue = new Array();
  queue.unshift(root);
  while (queue.length > 0) {
    let size = queue.length, // 队列保存了当前层的节点,获取节点个数
        temp = []; // 临时保存当前层节点的值
    for(let i=0; i<size; i++) { // for循环将当前层节点全部出队,并将下一层的节点加入队列
      root = queue.pop();
      temp.push(root.val);
      if (root.left != null) queue.unshift(root.left);
      if (root.right != null) queue.unshift(root.right);
    }
    res.push(temp);
  }
  return res
}

在上面的代码中,while每循环一次,就会遍历二叉树的一层,输出的结果为[ [ 1 ], [ 2, 3 ], [ 4, 5, 6, 7 ] ],掌握上面这个代码,下面的问题就不在话下了:

102. 二叉树的层序遍历
104. 二叉树的最大深度
111. 二叉树的最小深度
199. 二叉树的右视图
637. 二叉树的层平均值

参考:
二叉树遍历
二叉树:层序遍历登场!

查看原文

赞 0 收藏 0 评论 0

一杯绿茶 关注了专栏 · 2020-10-09

Sown

学习笔记

关注 239

一杯绿茶 关注了专栏 · 2020-10-09

javascript-lNong

只此一生,何必从众

关注 1004

认证与成就

  • 获得 2 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-08-15
个人主页被 577 人浏览