uccs

uccs 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

一只刚刚接触前端的小鸟

个人动态

uccs 发布了文章 · 2月1日

SVG 入门

核心优势

  • XML的格式,容量较小
  • 比较灵活,可以修改样式

自适应缩放

  • viewport:是 SVG 的可视区域,就是 SVG 的属性 widthheight 的范围,离开可视区域外的内容是不会被渲染的。
  • viewBox:用于画布上制作 SVG图形的坐标系统( viewBox="x, y, width, height" )默认和 viewBox 一致。

    <svg width="20px" height="20px" viewBox="0 0 20 20">xxx</svg> 
    • 如果进行手动指定, SVG 会进行自动换算缩放比: width/viewBox.widthheight/viewBox.height

      <svg width="500" height="200" viewBox="0 0 50 20" style="border: 1px solid #000;">
        <rect x="20" y="10" width="10" height="5" style="stroke: #000; fill: none;"/>
      </svg> 
      Tip: viewBox 一般作为静态值需要固定下来, widthheight 是可以动态修改的
  • preserveAspectRatio:保留宽高比,当 viewportviewBox 宽高比不相同时,指定在 viewport 中的绘制区域,默认值是 xMidYMid meet

    • meet:表示固定宽高比,并将 viewBox 缩放为 viewport 的大小

      meet 模式下,最终压缩比优先采纳压缩比较小的。(设置 yMid/yMin/yMax 都一样)
    • slice:保持宽高比并将所有不在 viewport 中的 viewBox 裁掉

      slice 模式下,最终压缩比优先采纳压缩比较大的。(设置 xMid/xMin/xMax 都一样)
    • xMidyMid:( x 轴和 y 轴一样)

      • xMidviewBoxx 轴的中点在 viewportx 轴中点
      • xMinviewBoxx 轴的起点(最小边)在 viewportx 轴起点(最小边)
      • xMaxviewBoxx 轴的终点(最大边)在 viewportx 轴终点(最大边)
    • none:不保持宽高比,缩放图像适合整个 viewBox,可能会发生图像变形。

      none 模式下,将分别计算 x 和 y 轴的压缩比。
查看原文

赞 0 收藏 0 评论 0

uccs 发布了文章 · 2020-08-20

常用的数组去重方式

数组去重通用做法一般有两种方法:
1、两层循环
2、利用对象属性的不可重复性
还有一种是语言本身 api 特性, js 利用 indexOfincludesSet 等方法

将下面合过属性进行去重操作

let arr = [ 1, 1, "true", "true", true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, "NaN", 0, 0, "a", "a", {}, {}, 1, "true", true, 15, false, 'false', undefined, null, NaN, "NaN", 0, {}];

用 Set 去重

无法对对象去重

const newArr = Array.from(new Set(arr));
// 等价于
const newArr = [...new Set(arr)];

console.log(newArr)

for 循环嵌套 for 循环,用 splice 去重

第一个和第二个相同,用 splice 删掉第二个

对象和 NaN 无法去重

Tips:会改变原数组

for 循环可以用 forEachmap 等代替,逻辑都差不多

for (let i = 0; i < arr.length; i++) {
  for (let j = i + 1; j < arr.length; j++) {
    if (arr[i] === arr[j]) {
      arr.splice(j, 1);
    }
  }
}
console.log(arr);

indexOf 去重

新建一个数组, for 循环原数组,判断新数组中有没有当前元素,有就跳过,没有就放进去。

对象和 NaN 无法去重

let newArr = [];
for (let i = 0; i < arr.length; i++) {
  if (newArr.indexOf(arr[i]) === -1) {
    newArr.push(arr[i]);
  }
}
console.log(newArr);

includes 去重

使用方法和 indexOf 差不多,但它可以对 NaN 去重

对象无法去重

let newArr = [];
for (let i = 0; i < arr.length; i++) {
  if (!newArr.includes(arr[i])) {
    newArr.push(arr[i]);
  }
}
console.log(newArr);

利用 sort 排序

利用 sortfor 循环 sort 排序后的数组,比较当前数组和当前数组的前一项,如果相等就跳过

对象和 NaN 无法去重

Tips:如果排序出来相同的有间隔,则间隔的无法去重,比如: [true, 'true', true]

arr = arr.sort();
let newArr = [];
for (let i = 0; i < arr.length; i++) {
  if (arr[i] !== arr[i - 1]) {
    newArr.push(arr[i]);
  }
}
console.log(newArr)

利用 filter 配合 indexOf

和遍历差不多,使用 indexOf 的话 NaN 全部被去掉了(不包括 'NaN' ),因为用 indexOf 找不到 NaN

对象不会被去重

const newArr = arr.filter(
  (item, index, array) => array.indexOf(item) === index
);
console.log(newArr);

利用 reduce 配合 includes

const newArr = arr.reduce(
  (prev, cur) => (prev.includes(cur) ? [...prev] : [...prev, cur]),
  []
);
console.log(newArr);

利用 filter 和 hasOwnProperty

定义一个空对象

filter 遍历数组,使用 hasOwnProperty 判断 obj 上有没有这个属性,如果有,就返回 false 如果没有返回 true ,并将这个值以属性的方式保存在 obj

Tips:对象无法去重,最后只会保留一个对象

var obj = {};
let newArr = arr.filter(function (item, index, arr) {
  return obj.hasOwnProperty(typeof item + item)
    ? false
    : (obj[typeof item + item] = true);
});
console.log(newArr);

以上数组去重的方式,都有一点有缺陷,要写出完美的去重方式需要考虑多种数据类型,这里不做过多讨论,上面几种方式的去重可基本满足日常工作的需要。

查看原文

赞 0 收藏 0 评论 0

uccs 发布了文章 · 2020-08-14

ToyReact 项目总结

webpack 配置

  • optimization: { minimize: false} :打包后的会把每一个文件放在 eval 中执行,通过 sourceURL 的方式在浏览器中打开它的时候变成一个单独的文件

    eval('console.log("1");\n\n//# sourceURL=webpack:///./main.js?');
  • @babel/preset-env 把高版本的 es 语法翻译成低版本的 es 语法
  • @babel/plugin-transform-react-jsxjs 中可以使用 jsx 语法

    let a = <MyComponent name="a" />;
    // 被翻译成
    var a = createElement(MyComponent, {
      name: "a",
    });
  • pragma :文本替换,如果不加的话默认是 React.crateElement ,这里要自己实现一个 React 所以这里要替换成 ToyReact.createElement
module.exports = {
  entry: {
    main: "./main.js",
  },
  mode: "development",
  optimization: {
    minimize: false,
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
            plugins: [
              [
                "@babel/plugin-transform-react-jsx",
                { pragma: "ToyReact.createElement" },
              ],
            ],
          },
        },
      },
    ],
  },
};

ToyReact 基本使用方法

源码

main.js 是项目的入口文件,引入 ToyReact ,声明一个组件 MyComponent

// main.js
import { ToyReact } from "ToyReact";

class MyComponent {}

let a = <MyComponent name="a" />;

ToyReact.js 是核心代码,这样就形成了一个最简单的 react

// ToyReact.js
export let ToyReact = {
  createElement() {
    console.log(arguments);
  },
};

createElement 有两个参数:

  • type:组件名 MyComponent (上图)

    • 如果 jsx 标签是小写的,那么这里的第一个参数是个字符串不是变量(下图图)
  • attribute :组件上的属性

Snipaste_2020-08-11_21-42-26.png

Snipaste_2020-08-11_21-43-51.png

用 ToyReact 实现 dom

创建 dom 节点

// main.js
import { ToyReact } from "ToyReact";

let a = (
  <div name="a" id="ids">
    <span></span>
    <span></span>
    <span></span>
  </div>
);

createElement 需要返回一个 dom 元素,否则最后的 div 接收到的 children 都是 null

// ToyReact.js
export let ToyReact = {
  createElement(type, attributes, ...children) {
    console.log(arguments);
    return document.createElement(type);
  },
};

前三次创建了三个 span ,第四次就把这三个 span 作为 children 传递进来。

所以这时候

  • type 就是 div
  • attribute 就是 nameid
  • children 就是三个 span

Snipaste_2020-08-11_22-04-23.png

挂载 dom 节点

// main.js
import { ToyReact } from "ToyReact";

let a = (
  <div name="a" id="ids">
    <span>Hello </span>
    <span>ToyReact </span>
    <span>!!!</span>
  </div>
);

document.body.appendChild(a);

span 里面的文本节点,传递进来后变成了一个字符串,所以在 appendChild 时需要创建一个文本节点。(下图)

// ToyReact.js
export let ToyReact = {
  createElement(type, attributes, ...children) {
    let element = document.createElement(type);

    for (let name in attributes) {
      element.setAttribute(name, attributes[name]);
    }

    for (let child of children) {
      if (typeof child === "string") child = document.createTextNode(child);
      element.appendChild(child);
    }
    return element;
  },
};

Snipaste_2020-08-11_22-23-22.png

实现 ToyReact.render

react 中,挂载节点不是直接使用 document.body.appendChild 而是使用了 render 的方法,它为什么要这样做呢?主要原因是 jsx 是一个混合内容,组件和 HTML 是并存的,如果不是 HTML 的元素的话,无法使用 document.body.appendChild

// main.js
import { ToyReact, Component } from "ToyReact";

class MyComponent extends Component {
  render() {
    return <div>cool</div>;
  }
}

let a = <MyComponent name="a" id="ids"></MyComponent>;

ToyReact.render(a, document.body);

这里面给 document.createElement 包裹一层 wrapper ,把创建 dom 的过程统一操作。这里的 Wrapper 分为 ElementWrapperTextWrapperElementWrapper 用于创建元素节点, TextWrapper 用于创建文本节点。

ElementWrapperappendChild 接收的参数是虚拟 dom

同时抽象出 Component ,实现 mountTosetAttribute 方法,让 MyComponent 继承 。

class ElementWrapper {
  constructor(type) {
    this.root = document.createElement(type);
  }

  setAttribute(name, value) {
    this.root.setAttribute(name, value);
  }

  appendChild(vchild) {
    vchild.mountTo(this.root);
  }

  mountTo(parent) {
    parent.appendChild(this.root);
  }
}

class TextWrapper {
  constructor(content) {
    this.root = document.createTextNode(content);
  }

  mountTo(parent) {
    parent.appendChild(this.root);
  }
}

export class Component {
  constructor() {
    this.children = [];
  }

  setAttribute(name, value) {
    this[name] = value;
  }

  mountTo(parent) {
    let vdom = this.render();
    vdom.mountTo(parent);
  }

  appendChild(vchild) {
    this.children.push(vchild);
  }
}

export let ToyReact = {
  createElement(type, attributes, ...children) {
    let element;
    if (typeof type === "string") element = new ElementWrapper(type);
    else element = new type();

    for (let name in attributes) {
      element.setAttribute(name, attributes[name]);
    }

    if (typeof child === "string") child = new TextWrapper(child);
    element.appendChild(child);

    return element;
  },
  render(vdom, element) {
    vdom.mountTo(element);
  },
};

自动展开 children

写到这里的时候,会有一个问题,这里面的 this.children 不会自动展开,而不是把这东西当成一个 child 来看

class MyComponent extends Component {
  render() {
    return (
      <div>
        <span>Hello</span>
        <span>Hello</span>
        <div>{this.children}</div>
      </div>
    );
  }
}

mountTo 时接收到的是一个数组,我们是希望把数组展开,实现一个 inseartChildren

createElement(type, attributes, ...children) {
  let element
  if (typeof type === 'string')
    element = new ElementWrapper(type)
  else
    element = new type

  for (let name in attributes) {
    element.setAttribute(name, attributes[name])
  }

  let insertChildren = (children) => {
    for (let child of children) {
      if (typeof child === 'object' && child instanceof Array) {
        insertChildren(child)
      } else {
        if (!(child instanceof Component) &&
          !(child instanceof ElementWrapper) &&
          !(child instanceof TextWrapper))
          child = String(child)

        if (typeof child === 'string')
          child = new TextWrapper(child)
        element.appendChild(child)
      }
    }
  }

  insertChildren(children)

  return element
}

实现 setState,props,事件

源码

单次渲染,已经完成了,但肯定不能一次渲染,后面再也不去动它了,可能需要通过某种方式去重新渲染。

这里的 main.js 就相对复杂了,本来这个例子是教我们如何把 react 跑起来,现在我们反过来,用这个例子去验证框架支不支持

// mian.js
import { ToyReact, Component } from "./ToyReact";

class Square extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button className="square" onClick={() => this.setState({ value: "X" })}>
        {this.state.value ? this.state.value : ""}
      </button>
    );
  }
}

class Board extends Component {
  renderSquare(i) {
    return <Square value={i}></Square>;
  }

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

let a = <Board />;
ToyReact.render(a, document.body);

用上面写好的框架跑这个程序的话,会发现报了一个错误, props 不存在

实现 props

class Component {
  constructor() {
    this.props = Object.create(null);
  }
  setAttribute(name, value) {
    this.props[name] = value;
  }
}

实现 onClick

用正则去匹配,绑定到元素上。

react 上,基本没有 remove 的情况,因为 remove 的话, reactrender 的合法性就会被破坏掉。 react 只能把整个 dom 树销毁重新绑定一个事件。

class ElementWrapper {
  setAttribute(name, value) {
    if (name.match(/^on([\s\S]+)$/)) {
      const eventName = RegExp.$1.replace(/^[\s\S]/, (s) => s.toLowerCase());
      this.root.addEventListener(eventName, value);
    }
    if (name === "className") name = "class";
    this.root.setAttribute(name, value);
  }
}

实现 setState

stateprops 没啥区别就是一个普通变量,但 setState 是由 component 提供的一个神奇的函数。

setState 实现的大概是一个 merge 操作, setState 会替换掉 state 中的内容,替换完之后 react 会做一个重新的 render 的操作。

this.state = { value: null };

this.setState({ value: "x" });

setState 大概流程是先 mergeupdateDom

merge 实现:对传进来的 state 遍历,如果是一个对象就递归调用,如果是基本类型就直接更新 oldState 对应的值。

class Component {
  setState(state) {
    let merge = (oldState, newState) => {
      for (let p in newState) {
        if (typeof newState[p] === "object") {
          if (typeof oldState[p] !== "object") {
            oldState[p] = {};
          }
          merge(oldState[p], newState[p]);
        } else {
          oldState[p] = newState[p];
        }
      }
    };
    if (!this.state && state) this.state = {};
    merge(this.state, state);
    this.update();
  }
}

更新 dom 这里使用 range 来操作。

这里使用 rangeapi 有:

  • setStartsetEnd 接收两个参数: nodeoffset

    • node 如果是文本节点, offset 就是里面的文字; node 如果是一个普通的 elementoffset 就是它内部子节点的个数
  • setStartBeforesetStartAfter 接收一个节点
  • deleteContents 清空 range 内容
  • insertNoderange 的其实位置插入节点

运行流程: dom 节点不直接挂载,而是用 range 去挂载

  1. 元素节点创建 range 然后调用 mountTo 去挂载,文本节点直接调用 mountTo 去挂载
  2. ToyReact.render 将被调用,创建 range ,调用 ComponentmountTo 去挂载
  3. ComponentmountTo 调用会将会调用自身的 update 更新 dom
class ElementWrapper {
  appendChild(vchild) {
    let range = document.createRange();
    if (this.root.children.length) {
      range.setStartAfter(this.root.lastChild);
      range.setEndAfter(this.root.lastChild);
    } else {
      range.setStart(this.root, 0);
      range.setEnd(this.root, 0);
    }
    vchild.mountTo(range);
  }

  mountTo(range) {
    range.deleteContents();
    range.insertNode(this.root);
  }
}

class TextWrapper {
  mountTo(range) {
    range.deleteContents();
    range.insertNode(this.root);
  }
}

export class Component {
  mountTo(range) {
    this.range = range;
    this.update();
  }

  update() {
    let placeholder = document.createComment("placeholder");
    let range = document.createRange();
    range.setStart(this.range.endContainer, this.range.endOffset);
    range.setEnd(this.range.endContainer, this.range.endOffset);
    range.insertNode(placeholder);
    this.range.deleteContents();
    let vdom = this.render();
    vdom.mountTo(this.range);
  }

  appendChild(vchild) {
    this.children.push(vchild);
  }
}

export let ToyReact = {
  render(vdom, element) {
    let range = document.createRange();
    if (element.children.length) {
      range.setStartAfter(element.lastChild);
      range.setEndAfter(element.lastChild);
    } else {
      range.setStart(element, 0);
      range.setEnd(element, 0);
    }
    vdom.mountTo(range);
  },
};

update 中的核心是把 range 中的核心全部删了,获取新的 vdom ,然后在调用 mountTo

update 中创建了一个 placeholder 作用是用来占位。因为在使用 deleteContents 后, offset 会发生变化,导致出错。

mountTo 就可以加 willMountdidMount

update 就可以加 willMountdidMount

写到这里面,实 dom 的工作已经基本完成了,下面就是把实 dom 变成虚拟 dom

实现 ToyReact 虚拟 Dom

源码

用 ToyReact 实现这个例子

// main.js
import { ToyReact, Component } from "./ToyReact";

class Square extends Component {
  render() {
    return (
      <button className="square" onClick={this.props.onClick}>
        {this.props.value}
      </button>
    );
  }
}

class Board extends Component {
  renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null),
        },
      ],
      stepNumber: 0,
      xIsNext: true,
    };
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([
        {
          squares: squares,
        },
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: step % 2 === 0,
    });
  }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ? "Go to move #" + move : "Go to game start";
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    let status;
    if (winner) {
      status = "Winner: " + winner;
    } else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        {/*<div className="game-info">*/}
        {/*  <div>{status}</div>*/}
        {/*  <ol>{moves}</ol>*/}
        {/*</div>*/}
      </div>
    );
  }
}

// ========================================

ToyReact.render(<Game />, document.body);

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

将 dom 操作放到 mountTo 中去完成

ElementWrapperappendChildsetAttribute 的逻辑都放在 mountTo 中去做, update 时就只操作虚拟 dom

class ElementWrapper {
  constructor(type) {
    this.type = type;
    this.props = Object.create(null);
    this.children = [];
  }

  setAttribute(name, value) {
    this.props[name] = value;
  }

  appendChild(vchild) {
    this[childrenSymbol].push(vchild);
    this.children.push(vchild.vdom);
  }

  get vdom() {
    return this;
  }

  mountTo(range) {
    this.range = range;
    let placeholder = document.createComment("placeholder");
    let endRange = document.createRange();
    endRange.setStart(range.endContainer, range.endOffset);
    endRange.setEnd(range.endContainer, range.endOffset);
    endRange.insertNode(placeholder);

    range.deleteContents();

    let element = document.createElement(this.type);

    for (let name in this.props) {
      let value = this.props[name];
      if (name.match(/^on([\s\S]+)$/)) {
        const eventName = RegExp.$1.replace(/^[\s\S]/, (s) => s.toLowerCase());
        element.addEventListener(eventName, value);
      }

      if (name === "className") element.setAttribute("class", value);
      element.setAttribute(name, value);
    }

    for (let child of this.children) {
      let range = document.createRange();
      if (element.children.length) {
        range.setStartAfter(element.lastChild);
        range.setEndAfter(element.lastChild);
      } else {
        range.setStart(element, 0);
        range.setEnd(element, 0);
      }
      child.mountTo(range);
    }

    range.insertNode(element);
  }
}

然后 update 中就可以精简代码

update() {
  let vdom = this.render()
  vdom.mountTo(this.range)
}

实现 update

update 中可以获取到新旧两颗树,diff 算法就是比对这两颗树的差异。

那么我们在比对这两个树的差异时,应该比对:

  • type
  • props
  • children

typeprops 比对都很有好理解, children 就有点麻烦,因为 children 有删除和增加不同的操作

Tips

  1. 这个算法不适合处理有 history 的情况
  2. 函数这里没有处理,因为每次返回的都是一个新函数,造成了新旧 dom 肯定不一样,按照 react 的实现,应该要有一个全局事件代理。
  3. children 没有处理新增、删除、移位等情况

实现, update 有三个函数:

  1. isSameNode 比对新旧节点是否相同,如果相同返回 true 否则返回 false

    1. type 是否相同
    2. props 是否相同
    3. props 个数是否相同
  2. isSameTree 比对新旧 dom 是否相同,如果相同返回 true 否则返回 false

    1. 节点是否相同
    2. 子节点数量是否相同
    3. 每个子节点是否相同
  3. replace 更新 dom 树的差异

    1. 根节点是否相同
    2. 节点不相同,调用 mountTo 更新 dom
    3. 如果节点相同,就比对子节点(递归调用)
update() {
  let vdom = this.vdom
  if (this.oldVdom) {
    let isSameNode = (node1, node2) => {
      if (node1.type !== node2.type)
        return false
      for (let name in node1.props) {

        if (typeof node1.props[name] === "object" && typeof node2.props[name] === 'object' &&
          JSON.stringify(node1.props[name]) === JSON.stringify(node2.props[name]))
          continue
        if (node1.props[name] !== node2.props[name])
          return false
      }
      if (Object.keys(node1.props).length !== Object.keys(node2.props).length)
        return false

      return true
    }

    let isSameTree = (node1, node2) => {
      if (!isSameNode(node1, node2))
        return false
      if (node1.children.length !== node2.children.length)
        return false
      for (let i = 0; i < node1.children.length; i++) {
        if (!isSameTree(node1.children[i], node2.children[i]))
          return false
      }
      return true
    }
    let replace = (newTree, oldTree) => {
      if (isSameTree(newTree, oldTree)) return
      if (!isSameNode(newTree, oldTree)) {
        newTree.mountTo(oldTree.range)
      } else {
        for (let i = 0; i < newTree.children.length; i++) {
          replace(newTree.children[i], oldTree.children[i])
        }
      }
    }
    replace(vdom, this.oldVdom)
  } else {
    vdom.mountTo(this.range)
  }
  this.oldVdom = vdom
}

要实现局部跟进,需要把 update 的逻辑放进 ElementWrapper 当中

项目教程:ToyReact

查看原文

赞 4 收藏 1 评论 1

uccs 发布了文章 · 2020-08-14

react 高阶使用

非受控组件

无法使用 setState 获得想要的结果时使用非受控组件,比如文件上传

Portal

作用:让组件渲染到父组件以外

使用场景:

  • overflow: hidden
  • 父组件 z-index 值太小
  • fixed 放在 body 的第一层级
ReactDOM.createPortal(
  <div className="modal">{this.props.children}</div>,
  document.body
);

Context

const ThemeContext = React.createContext('light')

<ThemeContext.Provider value={this.state.theme}>
    <A />
</ThemeContext.Provider>

// class组件:A 组件
class A extends React.Component {
    // static contextType = ThemeContext   等价于下面的 A.contextType = ThemeContext
    render() {
        const theme = this.context  // React 会往上找最近的 theme Provider
        return <div>{theme}</div>
    }
}
A.contextType = ThemeContext  // 指定 contextType 读取当前的 ThemeContext

// 函数组件:B 组件
function B = () => {
    // 函数组件没有 this.context
    // 函数组件通过可以使用 ThemeContext.Consumer
    return <ThemeContext.Consumer>
        {value => {value}}
    </ThemeContext.Consumer>
}

异步组件

const A = React.lazy(() => import("./A"))

// 使用异步组件时,可能会有等待的一些情况,就可以使用 React.Suspense, fallback 可以传入 loading 组件
<React.Suspense fallback={<div>loading...</div>}>
    <A />
</React.Suspense>

性能优化

react 默认父组件有更新,子组件无条件更新

shouldComponentUpdate 默认返回 true

shouldComponentUpdate(nextProps, nextState) {
    if(nextState.count !== this.setState.count) {
        return true  // 可以渲染
    }
    return false   // 不重复渲染
}

PureComponentmemo 实现了 shouldComponentUpdate 浅比较

class 组件使用 PureComponent ,函数组件使用 memo

memo 使用

function A(props) {
  // 使用 props 渲染
}

function areEqual(prevProps, nextProps) {
  /*
   * 如果把 nextProps 传入 render 方法的返回结果与将 prevProps 传入 render 方法的返回结果一致则返回 true,否则返回 false
   */
}

React.memo(a, areEqual);

高阶组件

// 高阶组件不是一种功能,而是一种模式
const HOCFactory = (Component) => {
  class HOC extends React.Component {
    // 在此定义多个组件的公共逻辑
    render() {
      return <Component {...this.props} />;
    }
  }
  return HOC;
};
const EnhancedComponent1 = HOCFactory(WrapperComponent);

Render Props

// Render Props 的核心思想
// 通过一个函数将 class 组件的 state 作为 props 传递给纯函数组件
class Factory extends React.Component {
  constructor() {
    this.state = {
      // state 即多个组件的公共逻辑数据
    };
  }

  // 修改 state
  render() {
    return <div>{this.props.render(this.state)}</div>;
  }
}

const App = () => {
  <Factory
    render={
      /* render 是个函数组件 */
      (props) => (
        <p>
          {props.a}
          {props.b}
          ...
        </p>
      )
    }
  />;
};
查看原文

赞 1 收藏 0 评论 0

uccs 发布了文章 · 2020-08-14

react setState 机制

setState特点

  • 不可变值
  • 可能是异步更新
  • 可能会被合并

state 要在构造函数中定义

不可变值

函数式编程思想,纯函数,不能有副作用

什么时候修改,什么时候对 state 操作,不要提前操作

// 操作数组
this.setState({
  list1: this.state.list1.concat(100),
  list2: [...this.state.list2, 100],
  list3: this.state.list3.slice(0, 3),
  list4: this.state.list4.filter((item) => item > 100),
  list5: list5Copy, // 其他操作
});

不能直接对数组进行 poppushsplice 等操作

// 操作对象
this.setState({
    obj1: Object.assign({}, this.state.obj1, {a: 100}),
    obj2: {...this.state.obj2, {a: 100}}
})

不能直接对 this.state.obj1 操作

为什么要强调不可变值,因为在性能优化部分 shouldComponentUpdate 是比较两个值 oldStatenewState 值是否一样, setState 会触发 shouldComponentUpdate 此时 oldStatenewState 的值就一样了。

可能是异步更新

在异步函数中使用 setState 是一个同步操作,否则为异步操作

// 异步
this.state = {
  count: 0,
};

this.setState({
  count: this.state.count + 1,
});

console.log(this.state.count); // 0
// 同步
this.state = {
  count: 0,
};

setTimeout(() => {
  this.setState({
    count: this.state.count + 1,
  });
  console.log(this.state.count); // 1
}, 0);
// 同步
this.state = {
  count: 0,
};

document.body.addEventListener("click", () => {
  this.setState({
    count: this.state.count + 1,
  });
  console.log(this.state.count); // 1
});

可能会被合并

setState 传入的是对象就会被合并,传入函数就不会被合并

// 会被合并
this.setState({
  count: this.state.count + 1,
});
this.setState({
  count: this.state.count + 1,
});
this.setState({
  count: this.state.count + 1,
});

count; // 1
// 不会被合并
this.setState((prevState, props) => {
  return {
    count: prevState.count + 1,
  };
});
this.setState((prevState, props) => {
  return {
    count: prevState.count + 1,
  };
});
this.setState((prevState, props) => {
  return {
    count: prevState.count + 1,
  };
});
count; // 3

setState 主流程

this.setState(newState)newState 存入 padding 队列 → 是否处于 batchUpdate 机制中

→ 是 → 保持组件与 dirtyComponents

→ 否 → 遍历所有的 dirtyComponents → 调用 updateComponent → 更新 padding state or props

函数在开始执行之前它会先设置一个变量 isBatchingUpdates = true , 这是 react 的一个机制,然后这函数执行完后在将 isBatchingUpdates 改为 false

这个变量的设置不是在函数里面的,可以认为先设置把这个变量设为 true 然后执行这个函数,函数执行完后把这个变量在改为 false

increate = () => {
  // 开始:处于 batchUpdate
  // isBatchingUpdates = true
  this.setState({
    count: 1,
  });
  // 结束
  // isBatchingUpdate = false
};
increate = () => {
  // 开始:处于 batchUpdate
  // isBatchingUpdates = true
  setTimeout(() => {
    // 此时 isBatchingUpdates 为 false
    this.setState({
      count: 1,
    });
  }, 0);
  // 结束
  // isBatchingUpdates = false
};

setState 异步还是同步?

  • setState 无所谓异步还是同步
  • 看是否能命中 batchUpdate 机制
  • 依据是判断: isBatchingUpdates

batchUpdate 机制

React 可以“管理”的入口

  • 生命周期(和它调用的函数)
  • React 中注册的事件(和它调用的函数)

React “管不到”的入口

  • setTimeoutsetInterval 等(和它调用的函数)
  • 自定义的 dom 事件(和它调用的函数)

transaction 事务机制

定义一个开始的逻辑,一个结束的逻辑。

在执行这个的时候,先执行开始的逻辑,再执行当前的函数,最后在执行结束的逻辑,这种机制就是 transaction 事务机制

这个开始和结束的逻辑不一定在函数中定义

increate = () => {
  // 开始:处于 batchUpdate
  // isBatchingUpdates = true
  // 其他任何操作
  // 结束:isBatchingUpdates = false
};

事务机制执行过程

perform(anyMethod)initializeanyMethodclosewrapper invariants miantained

perform 这个 apitransaction 提供的

查看原文

赞 0 收藏 0 评论 0

uccs 发布了文章 · 2020-08-13

redux 详解

redux1.png

React Components 指的就是 component 组件

Store 指的就是存储数据的公共区域

这个过程就像在图书馆借书的一个过程

React Components 代表的是一个借书的用户,当我在图书馆借书的时候,我要跟图书馆管理员说我要借什么书,这个语境表达就是 Action Creators,可以理解为你说的那句话:你要借什么书,图书馆的管理员就是 Store,负责整个图书馆的图书管理,图书馆的管理员是没办法记住图书馆所有图书的存放,一般都有一个系统,你要借什么书,都会先查一下书有没有,这个系统就是 Reducers

Redux 使用

  1. store 是唯一的
  2. 只有 store 能够改变自己的内容
  3. Reducer 必须是纯函数

    • 纯函数指的是,给定固定的输入,就一定会有固定的输出,而且不会有任何副作用

安装

yarn add redux

新建 store/index.js

import { createStore } from "redux";
import reducer from "./reducer";

const store = createStore(reducer);
export default store;

新建 store/reducer.js

// 根据业务设置默认数据
const defaultState = {
  inputValue: "",
  list: [],
};
/**
 *
 * state 整个 store 的数据,修改前的 store
 * action 传递过来的 action
 */
export default (state = defaultState, action) => {
  if (action.type === "change_input_value") {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputValue = action.value;
    return newState;
  }
  return state;
};
// Tip: reducer 可以接受 state,但是绝不能修改 state

使用

store.getState() // 读取

const action = {
    type: 'change_input_value',
    value: e.target.value
}
store.dispatch(action)

store.subscribe(this.handleChange) // 组件订阅 store,传递一个函数,只要 store 数据改变,这个函数就会被执行
handleChange(){
    this.setState(store.getState())
}

拆分 actionTypes

新建 store/actionTypes

export const CHANGE_INPUT_VALUE = change_input_value;

拆分 actionCreators

新建 store/actionCreators

import {CHANGE_INPUT_VALUE} from './actionTypes'
export const getInputChangeAction = (value) => ({
    type: CHANGE_INPUT_VALUE
    value
})

使用 React-thunk 中间件发送 ajax 请求

// actionCreators
export const initListAction = (data) => ({
    type: INIT_LIST_ACTION,
    data
})

export const getTodoList = () => {
    return (dispatch) => {
        axios.get('/list.json').then(res => {
            const data = res.data
            const action = initListAction(data)
            dispatch(action)
        })
    }
}

// 组件
componentDidMount() {
    const action = getTodoList()
    store.dispatch(action)
}

什么是 Redux 中间件

redux2.png

View 在 Redux 中会派发一个 Action,Action 通过 Store 的 Dispatch 方法传递给 Store,Store 接收到 Action,连同之前的 State 一起传给 Reducer,Reducer 返回一个新的数据给 Store,Store 去改变自己的 State,这是 Redux 的一个标准流程。

Redux 的中间件在这个流程里面指的是 Action 和 Store 之间,在 Redux 中,Action 只能是一个对象,所以 Action 是一个对象直接派发给了 Store,当使用了 Redux-thunk 之后,Action 可以是函数,Action 通过 Dispatch 方法传递给 Store,那么 Action 和 Store 之间是谁?是不是就是 Dispatch 这个方法,实际上我们说的中间件指的是对 Dispatch 方法的封装。

当我们对 Dispatch 方法封装之后,比如说使用 redux-thunk 这个中间件,这个时候当你调用 Dispatch 方法,给 Dispatch 方法传递的参数是对象的话,那么 Dispatch 方法就会把这个对象直接传给 Store;这时候,假如你传给 Dispatch 方法是一个函数时,就不会把这个函数直接传递给 Store,它会让这个函数先执行,执行完之后需要调用 Store 时,这个函数在去调用 Store

Redux-saga 中间件

新建 store/sagas.js

// actionCreators.js
export const initListAction = (data) => ({
  type: INIT_LIST_ACTION,
  data,
});

// store/sagas.js
import { takeEvery, put } from "redux-saga/effects";
import { GET_INIT_LIST } from "./actionTypes";

function* getInitList() {
  const res = yield axios.get("/list.json");
  const action = initListAction(data);
  yield put(action);
}

function* todoSagas() {
  yield takeEvery("GET_INIT_LIST", getInitList);
}

export default todoSagas;
// store/index.js
import createSageMiddleware from "redux-sage";
import todoSagas from "./sagas.js";

const sageMiddleware = createSagaMiddleware();
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const enhancer = composeEnhancers(applyMiddleware(sageMiddleware));

const store = createStore(reducer, enhancer);
sagaMiddleware.run(todoSagas);

export default store;
// 组件
componentDidMount() {
    const action = getTodoList()
    store.dispatch(action)
}

Redux 实际应用

// index.js
import store from "./store";
const App = () => {
  <Provider store={store}>
    <TodoList />
  </Provider>;
};

// 组件
import React, { Component } from "react";
import { connect } from "react-redux";

class TodoList extends Component {
  render() {
    <div>
      <div>
        <input
          value={this.props.inputValue}
          onChange={this.props.changeInputValue}
        />
        <button>提交</button>
      </div>
    </div>;
  }
}

const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    changeInputValue(e) {
      const action = {
        type: "change_input_value",
        value: e.target.value,
      };
      dispatch(action);
    },
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

// reducers.js
export default (state = defaultState, action) => {
  if (action.type === "change_input_value") {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputValue = action.value;
    return newState;
  }
  return state;
};
查看原文

赞 0 收藏 0 评论 0

uccs 发布了文章 · 2020-08-13

react 基础知识

React.FunctionComponent

React提供了一个组件类型React.FunctionComponent,可简写React.FC

  • 可以接收一个泛型p,默认是{}
  • children,返回一个React.ReactNode,这个children是任何component都拥有的
  • 静态属性defaultProps,组件的默认属性,外部可以不传这个属性。
interface IHelloProps {
  message?: string;
}

const Hello: React.FunctionComponent<IHelloProps> = (props) => {
  return <h2>{props.message}</h2>;
};

Hello.defaultProps = {
  message: "Hello  world",
};

React Hook

  • 完全可选
  • 百分百向后兼容
  • 没有计划从React移除class

Hook是一个特殊的函数,它可以让你勾入React特性,例如useState就允许在React函数组件添加state Hook

在编写函数组件时,意识到要向里面添加一些State时,以前的做法是必须转换成Class类型的组件,现在可以在现有的函数组件中使用Hook

useState

分开使用

import React, { useState } from "react";

const LikeButton: React.FC = () => {
  const [like, setLike] = useState(0);
  const [on, setOn] = useState(true);
  return (
    <>
      <button
        onClick={() => {
          setLike(like + 1);
        }}
      >
        {like}?
      </button>
      <button
        onClick={() => {
          setOn(!on);
        }}
      >
        {on ? "ON" : "OFF"}
      </button>
    </>
  );
};

export default LikeButton;

合在一起使用

import React, { useState } from "react";

const LikeButton: React.FC = () => {
  const [obj, setObj] = useState({ like: 1, on: true });
  return (
    <>
      <button
        onClick={() => {
          setObj({ like: obj.like + 1, on: obj.on });
        }}
      >
        {obj.like}?
      </button>
      <button
        onClick={() => {
          setObj({ like: obj.like, on: !obj.on });
        }}
      >
        {obj.on ? "ON" : "OFF"}
      </button>
    </>
  );
};

export default LikeButton;

删除数据

react 中有一个概念 immutable ,意思是不允许 state 有任何的改变。

一旦修改 state ,后面做 react 性能做优化的时候会有问题。

const { list } = [...this.state.list];
list.splice(1, 1);
this.setState({ list });

// 不推荐
this.state.list.splice(1, 1);
this.setState({ list: this.state.list });

属性名

labelfor 属性,在 jsx 中会和 for 循环冲突, reacthtmlFor 来代替 label 标签的 for

<label htmlFor='insertArea'>姓名</label>
<input type='text' id='insertArea'/>

父组件和子组件通信

子组件操作父组件的数据:

  1. 父组件向子组件传递一个方法
  2. 子组件调用这个方法,间接的操作父组件的数据
class TodoList extends Component {
  deleteItem = (number) => {
    alert(number);
  };
  render() {
    return <TodoItem deleteItem={this.deleteItem} />;
  }
}

class TodoItem extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return <div onClick={this.props.deleteItem(1)}>按钮</div>;
  }
}

setState 接收函数

onClick(e) {
    const value = e.target.value
    this.setState(() => ({
        value
    }))
}

onClick() {
    // prevState 是修改前的数据状态
    this.setState((prevState) => ({
        list: [...prevState.list]
    }))
}

PropTypes

限制父组件向子组件传值的类型

import PropTypes from "prop-types";
TodoItem.propTypes = {
  test: PropTypes.string.isRequired,
  content: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  deleteItem: PropTypes.func,
  index: PropTypes.number,
};

defaultProps

属性默认值

TodoItem.defaultProps = {
  test: "hello world",
};

虚拟 DOM

当父组件的 render 函数被运行时,它的子组件的 render 也会被重新执行

  1. state 数数据
  2. JSX 模版
  3. 数据 + 模版 生成虚拟 DOM(虚拟 DOM 就是一个 js 对象,用它来描述正式的 DOM),(损耗了性能)。

    ["div", { id: "name" }, ["span", {}, "hello world"]];
  4. 用虚拟 DOM 的结构生成真实的 DOM,来显示

    <div id="abc"><span>abc</span></div>
  5. state 发生变化(setState 时,数据会发生变化)
  6. 数据 + 模版 生成新的虚拟 DOM(极大提升了性能),diff 算法,react 中的 diff 是同层比较,如果第一层节点就发生了变化,下面就不在比较了,直接全部替换

    ["div", { id: "name" }, ["span", {}, "bye bye"]];
  7. 比较原始虚拟 DOM 和新的虚拟 DOM 的区别,找到区别的是 span 中的内容(极大提升了性能)
  8. 直接操作 DOM,改变 span 中的内容

Ref

react 建议用数据驱动,尽量不要去操作 DOM

<input onChange={this.onChange} ref={(input) => (this.input = input)} />;

onChange = (e) => {
  const value = this.input.value;
  // 等价于
  const value = e.target.value;
};

声明周期函数

  • componentWillMount:组件在即将被挂载到页面时执行(第一次挂载的时候被执行)
  • render
  • componentDidMount:组件在被挂载到页面后执行(第一次挂载的时候被执行),ajax 请求最好发在这里
  • shouldComponentUpdate:组件被更新之前执行(需要在函数内部返回 true,返回 false 不会执行)
  • componentWillUpdate:组件被更新之前执行,但在 shouldComponentUpdate 之后(shouldComponentUpdate 返回 true 才会被执行)
  • componentDidUpdate:组件更新完之后执行
  • componentWillReceiveProps:没有 props 参数,不会被执行

    1. 一个组件要从父组件接收参数
    2. 只要父组件的 render 函数被重新执行了,子组件的 componentWillReceiveProps 就会被执行(如果子组件第一次出现在页面中,不会执行)
  • componentWillUnmount:组件被移除前执行

shouldComponentUpdate

/**
 * nextProps 一个组件更新时 props 会变化成什么样
 * nextState 一个组件更新时 state 会变化成什么样
 */
shouldComponentUpdate(nextProps, nextState) {
    if(nextProps.content !== this.props.content) {
        return true
    } else {
        return false
    }
}

动画

安装 react-transition-group

import { CSSTransition } from "react-transtion-group";

<CSSTransition
  in={this.stata.show} // 告诉 CSSTransition 组件什么时候有动画
  timeout={1000} // 动画时长 1s
  className="fade"
  unmountOnExit // 元素消失后会被移除
  appear={true} // 页面一刷新,也会有动画效果
>
  <div>hello</div>
</CSSTransition>;
  • fade-enter(动画进入前一瞬间)
  • fade-enter-active(动画进入前第二瞬间,一直到动画完成)
  • fade-enter-done(进入动画完成时)
  • fade-exit(动画消失前一瞬间)
  • fade-exit-active(动画消失前第二瞬间,一直到动画消失完成)
  • fade-exit-done(消失动画完成时)
  • fade-appear(动画进入前一瞬间)
  • fade-appear-active(动画进入前第二瞬间,一直到动画完成)
查看原文

赞 0 收藏 0 评论 0

uccs 发布了文章 · 2020-08-13

React 事件和 Dom 事件的区别

function App() {
  const onClickButton = (event) => {
        console.log('合成事件:', e)
    console.log('currentTarget:', e.currentTarget)
    console.log('target:', e.target)
    console.log('原生事件:', e.nativeEvent)
    console.log('currentTarget:', e.nativeEvent.currentTarget)
    console.log('target:', e.nativeEvent.target)
  }
  return (
    <div className="App">
      <button onClick={onClickButton}>点我</button>
    </div>
  );
}

这里面的 event 不是原生事件,而是合成事件 SyntheticEvent

1.png

原生事件要使用 event.nativeEvent

原生事件是被挂在到 document 上的,所以 currentTarget 获取到的是 document

2.png

虽然 SyntheticEventreact 模拟的,但 Dom 具有的能力,它都有,只是这些能力不受 Dom 控制。比如:阻止默认行为 preventDefault ,阻止冒泡 stopPropagation

查看原文

赞 0 收藏 0 评论 0

uccs 发布了文章 · 2020-08-01

git 常用指令

更新本地代码

git pull --rebase origin origin_branch

新建分支

git checkout -b new_branch  # 新建并切换分支

切换分支

git checkout branch   # 切换分支
git checkout -        # 切换到上一次分支

查看本地文件状态

git status

暂存代码

git add .

提交代码

git commit -m 'message'

查看提交信息

git log             # 当前分支
git log --all       # 所有分支
git log --oneline

代码推到远端

git push origin origin_branch

合并分支

git merge --no-ff branch

清除本地文件修改

git checkout -- xxx   # 某个文件
git checkout -f       # 所有

临时保存最近修改

git add .     # 没有被 git 管理的文件,需要使用
git stash

查看stash

git stash list

恢复临时保存

git stash pop stash@{stash_id}

git stash冲突

git stash pop 冲突,不会自动删除git stash中的记录需手动清理

git stash drop stash@{stash_id}

删除本地分支

git branch -d local_branch    # 普通删除
git branch -D local_branch    # 强制删除

删除远端分支

git push --delete origin origin_branch

Tip:无法重命名远端分支,需要现删除远端分支,再将本地分支推到远端。

将本地分支推送到远端

git push origin local_branch:origin_branch

拉远端分支

git fetch
git checkout origin_branch

重命名本地分支

git branch -m old_branch new_branch   # 不在 old_branch
git branch -m new_branch      # 在 old_branch

修改最近一次提交信息

git commit --amend -m 'message'

修改git仓库开发者信息

git config --global user.name
git config --global user.email

撤销上一次提交

git reset HEAD^1

回滚

git reset --hard commit_id

取消合并

git merge --abort

查看所有操作记录

git reflog

Tip:搭配回滚可御剑飞行

打标签

git tag v1.2.3    # 当前版本打标签
git push origin --tags    # 标签推到远端

查看git远程关联

git remote -v

关联远程仓库

git remote set-url --add origin origin_url

添加远程仓库

git remote add origin_name origin_url
查看原文

赞 0 收藏 0 评论 0

uccs 发布了文章 · 2020-07-29

React 仿简书项目实战

项目源码

https://github.com/astak16/ac...

项目结构

src/common; // 公共组件
src/pages; // 页面
src/static; // 静态资源
src/store; // 主 store
App.js; // 根组件
index.css; // 样式
index.js; // 入口文件

重置 css

搜索reset.css,复制过来

styled-components

css文件一旦在一个文件中被引入,会在全局中生效

这样写css会有些问题,当页面中有多个组件时,样式存在被覆盖的风险

我们希望在写样式的时候,每个组件的样式是独立的,不会互相的影响

使用

  1. index.css重命名为style.js
  2. index.js引入style.js

    import "style.js";
  3. style.js

    import { injectGlobal } from "styled-components";
    
    injectGlobal`
      body{
        padding: 10px
      }
    `;

在组件中使用

组件声明

// Header/style.js
import styled from "styled-components";

export const HeaderWrapper = styled.div`
  height: 56px;
`;

// Header/index.js
import { HeaderWrapper } from "./style";

class Header extends Component {
  render() {
    return <HeaderWrapper></HeaderWrapper>;
  }
}

图片使用

style.js中使用背景图片的话,直接使用background: url('/images/logo.png')是没有效果的,因为webpack在打包的时候,不知道工程目录是啥样的,它会把路径当成字符串

需要这样写

import logoPic from "/images/logo.png";

export const Logo = style.a`
  background: url(${logoPic})
`;

设置属性

import logoPic from "/images/logo.png";

export const Logo = style.a.attr({
  href: "/",
})`
  background: url(${logoPic})
`;

参数传递

组件中要传递不同的参数进来,styled提供了函数功能

<RecommendItem key='1' imgURL='/images/1.png'>1</RecommendItem>
<RecommendItem key='2' imgURL='/images/2.png'>1</RecommendItem>

// style.js
export const RecommendItem = styled.div`
  width: 280px;
  height: 50px;
  background: url(${props => props.imgURL});
  background-size: contain;
`

immutable.js

reducer中,state的数据是不希望被改变的,在写项目的时候,很容易在不知不觉中把state给改了,从而造成异常。

immutable.js可以帮助解决问题,它会生成一个immutable对象,这个对象是不可被改变的。

immutable提供了fromJS的方法,可以把js对象转换成immutable对象

const a = fromJS({ age: 18 });

immutable提供了getgetIn两种方法来读取数据

const a = fromJS({
  age: 18
  feature: {
    height: 180
  }
})

const age = a.get('age')

const height = a.getIn(['feature', 'height'])
// 或者
const height = a.get('feature').get('height')

immutable提供了setmerge两种方法来设置数据。

immutable对象的set方法,会结合之前的immutable对象的值,和设置的值,返回一个全新的对象。它并不会去改之前的immutable数据。

const a = fromJS({
  age: 18,
  name: "uccs",
});
a.set("age", 19).set("name", "tiantain");
// 或者
a.merge({
  age: 1,
  name: "tiantian",
});

当最外面使用immutable对象时,内部的Object类型的数据也会变成immutable类型,所以在使用set设置时,先要把普通对象变成immutable对象才行。

const a = fromJS({
  list: [], // 这个 list 也是 immutable 数组
});

const data = [1, 2, 3];
a.set("list", fromJS(data));

immutable.js提供了toJS方法,是为了将immutable对象转换成js对象

const a = fromJS({
  list: [1, 2, 3],
});

const b = a.toJS();
b[1];

redux-thunk

redux-thunk的作用是可以在action中写函数

redux

首页需要安装reduxreact-redux

react-redux是方便在react中使用redux

redux的使用可以看:redux 学习笔记

store/index.js

// store/index.js
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import reducer from "./reducer";

const store = createStore(reducer, compose(applyMiddleware(thunk))); // 在 store 中使用 redux-thunk

export default store;

store/reducer.js

// 主 store/reducer.js
import { combineReducers } from "redux-immutable";
import { headerReducer } from "../common/header/store";

export default combineReducers({
  header: headerReducer,
});

// 分 header/store/reducer.js
import { Search_Focus } from "./actionType";
import { fromJS } from "immutable";
const defaultState = fromJS({
  focused: false, // 默认数据
});
export default (state = defaultState, action) => {
  switch (action.type) {
    case Search_Focus:
      return state.set("focused", action.data);
    default:
      return state;
  }
};

redux-immutable提供了一个combineReducers方法,这个方法用来组合各个拆分出来的reducer

header/store/actionCreators.js

// header/store/actionCreators.js
import {Search_Focus} from './actionType'

const searchFocus = (data) => {
  type: Search_Focus,
  data
}

export const getList = async () => {
  return (dispatch) => {
    const res = await axios.get('api/header.json')
    dispatch(searchFocus(res.data))
  }
}

header/store/actionType.js

// header/store/actionType.js
export const Search_Focus = "header/search_focus";

App.js

// App.js
import { Provider } from "react-redux";
import store from "./store";

<Provider store={store}>
  <BrowserRouter>
    <div>
      <Header />
    </div>
  </BrowserRouter>
</Provider>;

react-redux有一个核心组件Provider,这个组件有一个store属性,将我们定义的store传给它,这样Provider里面所有组件都有能力去使用store中的数据了

组件连接 store

Providerstore提供给了各组件,但是组件想使用store中的数据,还要做连接

import { connect } from "react-redux";

class Header extends React.Component {}

export default connect()(Header);

react-redux提供了connect方法,connect就是帮助组件和store建立连接的。
connect接收两个参数:mapStateToPropsmapDispatchToProps

import { actionCreators } from "./store/actionCreators";

const mapStateToProps = (state) => {
  return {
    focused: state.getIn(["header", "focused"]),
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    handleInputFocus() {
      dispatch(actionCreators.getList());
    },
  };
};
export default connect(mapStateToProps, mapDispatchToProps)(Header);

组件中使用this.props.focused

react-router-dom

Route

  • render方法可以渲染一个东西
  • exact完全匹配路径

    • exactfalse时,//detail都能匹配上
  • component渲染组件
import { BrowserRouter, Route } from "react-router-dom";
import Home from "./pages/home";
import Detail from "./pages/detail/loadable";

function App() {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <div>
          <Route path="/" exact component={Home} />
          <Route path="/detail/:id" exact component={Detail} />
        </div>
      </BrowserRouter>
    </Provider>
  );
}

export default App;

动画

换一换旁边有个icon,每次点击的时候,这个icon需要转动起来。

每次点击的时候只要改变icontrannform: rotate() 的值就可以了。每次增加 360°

通过ref可以获取到react渲染出来的真实节点。

<SearchInfoSwitch onClick={() => handleChangePage(this.spinIcon)}>
  <i className='iconfont spin'
     ref={icon => this.spinIcon = icon}
  >&#xe852;</i>
换一批</SearchInfoSwitch>

handleChangePage(spin) {
  let originAngle = spin.style.transform.replace(/[^0-9]/ig, '')
  if (originAngle)
    originAngle = parseInt(originAngle, 10)
  else
    originAngle = 0

  spin.style.transform = `rotate(${originAngle + 360}deg)`
}

其他

  1. Link代替a做跳转,可以做到按需加载
  2. PureComponent组件等价于Component+shouldComponentUpdate
  3. dangerouslySetInnerHTML={{__html: '<div>文本</div>'}}可以渲染HTML内容
  4. <Route path='/detail/:id'/>路由参数获取this.props.match.params.id
  5. 异步组件redux-immutable,使用异步组件时需要用withRouterexport default connect(mapState, mapDispatch)(withRouter(Detail));

    // detail/loadable.js
    import React from "react";
    import Loadable from "react-loadable";
    
    const LoadableComponent = Loadable({
      loader: () => import("./"),
      loading() {
        return <div>正在加载</div>;
      },
    });
    
    export default () => <LoadableComponent />;
    
    // App.js
    import Detail from "./pages/detail/loadable";

总结

路由

router.png

项目技术栈:

  • react-router:路由
  • react-redux:数据挂你工具
  • react-loadable: 按需加载
  • react-transition-group:动画
  • redux-immutable:将state变成immutable对象
  • redux-thunk:可以在action中使用函数
  • styled-components:局部样式
  • axiosajax请求
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 163 次点赞
  • 获得 9 枚徽章 获得 0 枚金徽章, 获得 3 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-04-21
个人主页被 1.6k 人浏览