五月花开

五月花开 查看完整档案

天津编辑天津工业大学  |  光电信息科学与工程 编辑  |  填写所在公司/组织 baijiawei.top 编辑
编辑

仰望星空,也要脚踏实地。

个人动态

五月花开 赞了文章 · 6月27日

性能优化小册 - 渲染十万条数据:基于 IntersectionObserver 的虚拟化长列表

技术不局限于框架,相同的原理只是实现方式略有不同。

前置

1. 什么是虚拟列表?

首先,虚拟列表只是一个概念,本人对虚拟列表这个表述不置可否。

虚拟列表是对于列表形态数据展示的一种按需渲染,是对长列表渲染的一种优化。

虚拟列表不会一次性完整地渲染长列表,而是按需显示的一种方案,以提高无限滚动的性能。

2. 虚拟列表的实现原理?

根据容器元素的高度 clientHeight 以及列表项元素的高度 offsetHeight 来显示长列表数据中的某一个部分,而不是去完整地渲染整个长列表。

实现一个虚拟列表需要:

  • 得知容器元素的高度 clientHeight
  • 得知列表项元素的高度 offsetHeight
  • 计算可视区域应该渲染的列表项的个数 count = clientHeight / offsetHeight
  • 计算可视区域数据渲染的起始位置 start
  • 计算可视区域数据渲染的结束位置 end
  • 对完整长列表数据进行截断 sliceList = dataList.slice(start, end)
  • 渲染截断后的列表数据,进而实现无限加载

3. 虚拟列表与懒加载有何不同?

懒加载与虚拟列表其实都是延时加载的一种实现,原理相同但场景略有不同。

  • 懒加载的应用场景偏向于网络资源请求,解决网络资源请求过多时,造成的网站响应时间过长的问题。
  • 虚拟列表是对长列表渲染的一种优化,解决大量数据渲染时,造成的渲染性能瓶颈的问题。

4. IntersectionObserver 介绍

IntersectionObserver 提供了一种异步观察目标元素与视口的交叉状态,简单地说就是能监听到某个元素是否会被我们看到,当我们看到这个元素时,可以执行一些回调函数来处理某些事务。

let io = new IntersectionObserver(callback, option);

callback 会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。

更多介绍 Intersection Observer

实现

1. 模拟十万条数据:

function getDataList() {
  let data = []
  for(let i = 0; i < 100000; i++) {
    data.push({id: "item" + i, value: Math.random() * i})
  }
  return data;
}

2. Dom 创建及列表渲染:

不依赖框架的情况下,需要命令性的去创建 DOM 以及操作 DOM

<ul class="container">
  <span class="sentinels">....</span>
</ul>
function $(selector) {
  return document.querySelector(selector)
}

function loadData(start, end) {
  // 截取数据
  let sliceData = getDataList().slice(start, end)
  // 现代浏览器下,createDocumentFragment 和 createElement 的区别其实没有那么大
  let fragment = document.createDocumentFragment(); 
  for(let i = 0; i < sliceData.length; i++) {
    let li = document.createElement('li');
    li.innerText = JSON.stringify(sliceData[i])
    fragment.appendChild(li);
  }
  $('.container').insertBefore(fragment, $('.sentinels'));
}

如果是基于 Virtual DOM 的框架,直接操作数据即可(伪代码):

// 父组件
<virtual-list :listData="listData"></virtual-list>

// 子组件
<ul class='container'>
  <li
    v-for="item in sliceData" 
    :key="item.id"
  >{{ item }}</li>
</ul>
...

// js
this.sliceData = this.data.slice(start, index)

3. 使用 IntersectionObserver API 创建监听器:

let count = Math.ceil(document.body.clientHeight / 120);
let startIndex = 0;
let endIndex = 0;
...
let io = new IntersectionObserver(function(entries) {
    loadData(startIndex, count)
    // 标志位元素进入视口
    if(entries[0].isIntersecting) {
      // 更新列表数据起始和结束位置
      startIndex = startIndex += count;
      endIndex = startIndex + count;
      if(endIndex >= getDataList().length) {
        // 数据加载完取消观察
        io.unobserve(entries[0].target)
      }
      // requestAnimationFrame 由系统决定回调函数的执行时机
      requestAnimationFrame(() => {
        loadData(startIndex, endIndex)
        let num = Number(getDataList().length - startIndex)
        let info = ['还有', num , '条数据']
        $('.top').innerText = info.join(' ')
        if(num - count <= 0) {
           $('.top').classList.add('out')
         }
      })
    }
  });
  // 开始观察“标志位”元素
  io.observe($('.sentinels'));
})

由于 IntersectionObserver 无法监听动态创建的 dom,所以我们设置一个「标志位」元素 span.sentinels 作为监听的目标对象。

<ul class="container">
  <span class="sentinels">....</span>
</ul>

如果目标元素正处于交叉状态 entries[0].isIntersecting == true,则代表 .sentinels 进入了可视区域,从而加载新的列表数据。

if(entries[0].isIntersecting) {
  ...
  requestAnimationFrame(() => {
    loadData(startIndex, endIndex)
  })
  ...
}

最后将新的列表 insertBefore 到其前面,进而实现无限加载。

$('.container').insertBefore(fragment, $('.sentinels'));

同系列文章:

查看原文

赞 29 收藏 22 评论 8

五月花开 赞了文章 · 1月1日

实习好忙|前端埋点之曝光实现

最近有一个工作需求是曝光埋点,让我得以有机会接触相关的东西。之前实习时没有做过这方面的需求,个人项目更是和埋点扯不上关系。以至于上周开会讨论时听到“埋点”这个词就怂了。

不过后面听大佬分析了下后才意识到,原来“埋点”是这个意思。曝光埋点的思路也是很简单:无非是判断某个DOM是否出现在视窗中,出现了就收集数据上报给服务端。

所谓“埋点”,是数据采集领域(尤其是用户行为数据采集领域)的术语,指的是针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。比如用户某个文章点击次数、观看某个视频的时长等等。

再说「曝光埋点」,它与「图片懒加载」「计算广告浏览量」这些需求一样,本质就是让你计算某一元素和另一元素(视窗)的相对可视状态/相对位置,然后进行一些操作(一般是上报给服务端)。

思考如何实现

最先出现在脑海里的方法是利用getBoundingClientRect / offset类 + onscroll。即:注册滚动事件,然后在滚动的回调函数中利用getBoundingClientRect / offset类拿到每个元素的位置信息,然后经过判断确定是否元素处于曝光状态/视窗中。

但这种方式有很大的缺陷。如果你熟悉浏览器的渲染过程的话,就会知道调用getBoundingClientRect / offset类会引起浏览器的回流重绘,影响网页表现/性能。频繁、大量调用更不是一个妥当的选择。

我开始尝试在社区找找看有没有其他更妥当的方法,还真被我找到了:Intersection Observer

Intersection Observer

它提供了一种异步观察目标元素与祖先元素或顶级文档Viewport的交集变化的方法。也就是说,不仅可以用来获得相对于视窗的曝光,可以做得更多,这取决于“另一个元素”是什么。

Intersection Observer将本来是开发者做的:监听滚动、遍历获取元素与另一个元素(或视窗)相对位置的工作给做了。这两块工作是页面性能损耗大户,现在交给浏览器来实现,会比我们开发者来做要妥当的多。开发者现在只需要关心其他业务逻辑即可 😁

那这么好用的API,它的兼容性状况如何呢?

兼容性

还不错,但兼容性方面要求高的话还是不能让人放心使用。

Polyfill

但不用担心,我们有polyfill。W3C提供了一个polyfill,当浏览器不支持时使用常规解决方案替代。它的思路就是在检测到当前浏览器不支持Intersection Observer API时,使用getBoundingClientRect去重新实现一遍Intersection Observer API

那么使用了该Polyfill后,浏览器兼容性状况如何呢?

非常棒! 😎(IE7都支持了,还想啥呢,大兄弟。)

Polyfill兼容性

曝光实现步骤

思路就像上面一再提到的,很简单:

  1. new IntersectionObserver() 实例化一个全局observer,(结合Vue指令)让每个DOM自行把自己加入到observer的观察列表。
  2. 当某个DOM进入视窗,收集对应的信息,上报。
  3. 取消对该DOM的观察。

代码实现

Exposure.ts 封装成类

import 'intersection-observer';

export default class Exposure {
    private observer: IntersectionObserver | undefined;

    constructor() {
        this.init();
    }

    private init() {
        const self = this;

        this.observer = new IntersectionObserver(
            (entries, observer) => {
                entries.forEach(item => {
                    if (item.isIntersecting) {
                        const data = item.target.getAttribute('data-article');
                        self.upload(data);
                        observer!.unobserve(item.target);
                    }
                });
            },
            {
                root: null,
                rootMargin: '0',
                threshold: 0.1,
            }
        );
    }
  
    public add(el: Element) {
        this.observer && this.observer.observe(el);
    }

    private upload(data: string | null) {
        if (data) {
            // ajax上报数据
        }
    }
}

directive/exposure.ts 封装Vue指令

import Exposure from '@/lib/Exposure';
import Vue from 'vue';

const exposure = new Exposure();

Vue.directive('exposure', {
    bind(el) {
        exposure.add(el);
    },
});

*.vue 使用指令

<div v-exposure :data-article='article'>
...
</div>
查看原文

赞 11 收藏 7 评论 0

五月花开 赞了文章 · 2019-12-18

Mac osx 下安装iTerm2,并使用rz sz上传下载(附homebrew配置)

从window上转换到Mac还是有一些不习惯的地方,今天就一直在弄这个远程服务器的操作的软件,先是费了好大劲下载了SecureCRT,又是破解弄了半天发现上传下载用不了,很是伤心。
一直想找一个类似xShell的软件,我放弃了,最后定为到iTerm配合rz和sz来实现上传下载。
先说一下为什么写这个文章,因为在网络上真的是看了好多的文章,良莠不齐,有的好使,有的根本就是*,所以打算记录一篇文章,不能说很好,但是是我亲测可行的,所以记录下来,希望给同行人一些帮助。

环境

  • Mac ox 10.12.3
  • iTerm2-3_1_5

安装iTerm2

安装方式和苹果的安装方式是一样的,解压双击 把他拖进application中就可以了。

安装rz sz

吐槽一下 好多的博客写的是很简单,一个命令 brew install lrzsz。。。可是也许有很多人没有brew这个命令 比如我 总是not found command,伤心。。

安装rz sz 前的准备 安装brew

简单介绍一下,一般我们在unix或者类unix的linux系统上,使用yum或者apt-get,不过抱歉,在mac osx上好像不好使,所以几经周折找到homebrew简称brew,它的功能就类似yum或者apt-get。它是max osx上的软件包管理工具。

  • 安装homebrew 只需要下面一条语句即可
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

注意: 我在安装过程中看了很多博客,也尝试过很多链接,甚至打算翻墙来着,不过上面这条语句我一次执行就通过了(没翻墙),并看到了成功的标记 successful。

注意 在安装过会有一次卡顿,建入回车 接着我们会看到让我们输入密码,输入密码后等待安装就可以了。

好了,安装成功后输入 brew 查看是否有提示信息

harryliangdeMacBook-Pro:~ harryliang$ brew
Example usage:
  brew search [TEXT|/REGEX/]
  brew (info|home|options) [FORMULA...]
  brew install FORMULA...
  brew update
  brew upgrade [FORMULA...]
  brew uninstall FORMULA...
  brew list [FORMULA...]

Troubleshooting:
  brew config
  brew doctor
  brew install -vd FORMULA

Developers:
  brew create [URL [--no-fetch]]
  brew edit [FORMULA...]
  https://docs.brew.sh/Formula-Cookbook.html

Further help:
  man brew
  brew help [COMMAND]
  brew home
  
  

如果看到如上信息,则表示brew安装成功了。

安装lrzsz

  • 这里安装才可以说简单了,使用如下命令即可
brew install lrzsz  

配置rz sz 进行上传下载

要想使用rz 和sz进行上传下载我们要尽兴一些配置

  • 进入到/usr/local/bin 目录下
cd /usr/local/bin


  • 这个目录下需要两个配置文件,这个配置文件可以自己粘贴,也可以去github网站下载。

    1. github网址: https://github.com/mmastrac/i...
    2. 如果不会下载就在github上打开文件 自己创建两个文件 并把内容粘贴进去
  • 这里把我的配置过程和配置文件内容粘贴出来给大家,方便大家操作。
  1. 在/usr/loal/bin 目录下创建两个文件

图片描述

命令:


vi iterm2-recv-zmodem.sh
vi iterm2-send-zmodem.sh

创建好两个文件后分别添加内容:

  1. iterm2-recv-zmodem.sh
#!/bin/bash
# Author: Matt Mastracci (matthew@mastracci.com)
# AppleScript from http://stackoverflow.com/questions/4309087/cancel-button-on-osascript-in-a-bash-script
# licensed under cc-wiki with attribution required 
# Remainder of script public domain

osascript -e 'tell application "iTerm2" to version' > /dev/null 2>&1 && NAME=iTerm2 || NAME=iTerm
if [[ $NAME = "iTerm" ]]; then
    FILE=`osascript -e 'tell application "iTerm" to activate' -e 'tell application "iTerm" to set thefile to choose folder with prompt "Choose a folder to place received files in"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")"`
else
    FILE=`osascript -e 'tell application "iTerm2" to activate' -e 'tell application "iTerm2" to set thefile to choose folder with prompt "Choose a folder to place received files in"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")"`
fi

if [[ $FILE = "" ]]; then
    echo Cancelled.
    # Send ZModem cancel
    echo -e \\x18\\x18\\x18\\x18\\x18
    sleep 1
    echo
    echo \# Cancelled transfer
else
    cd "$FILE"
    /usr/local/bin/rz -E -e -b
    sleep 1
    echo
    echo
    echo \# Sent \-\> $FILE
fi

  1. iterm2-send-zmodem.sh
#!/bin/bash
# Author: Matt Mastracci (matthew@mastracci.com)
# AppleScript from http://stackoverflow.com/questions/4309087/cancel-button-on-osascript-in-a-bash-script
# licensed under cc-wiki with attribution required 
# Remainder of script public domain

osascript -e 'tell application "iTerm2" to version' > /dev/null 2>&1 && NAME=iTerm2 || NAME=iTerm
if [[ $NAME = "iTerm" ]]; then
    FILE=`osascript -e 'tell application "iTerm" to activate' -e 'tell application "iTerm" to set thefile to choose file with prompt "Choose a file to send"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")"`
else
    FILE=`osascript -e 'tell application "iTerm2" to activate' -e 'tell application "iTerm2" to set thefile to choose file with prompt "Choose a file to send"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")"`
fi
if [[ $FILE = "" ]]; then
    echo Cancelled.
    # Send ZModem cancel
    echo -e \\x18\\x18\\x18\\x18\\x18
    sleep 1
    echo
    echo \# Cancelled transfer
else
    /usr/local/bin/sz "$FILE" -e -b
    sleep 1
    echo
    echo \# Received $FILE
fi 

  1. 将文件写好后保存好,使用如下命令添加权限

    chmod +777 iterm2-*

  • 配置好配置文件之后,开始对iTerm2进行配置

iTerm2 配置添加rz sz 功能

  • 点击 iTerm2 的设置界面 Perference-> Profiles -> Default -> Advanced -> Triggers 的 Edit 按钮

图片描述


图片描述


图片描述

--- 在最后一个图片的位置进行配置,上图是配置好的样子,配置文件如下:

Regular expression: rz waiting to receive.\*\*B0100
Action: Run Silent Coprocess
Parameters: /usr/local/bin/iterm2-send-zmodem.sh

Regular expression: \*\*B00000000000000
Action: Run Silent Coprocess
Parameters: /usr/local/bin/iterm2-recv-zmodem.sh

按照上面的配置文件信息的格式填写即可。

ok!到此为止我们的rz sz安装配置就可以了,接着我们介绍一下rz 和 sz 的使用

rz 上传功能

  • 在bash中,也就是iTerm2终端输入rz 就会弹出文件选择框,选择文件 choose 就开始上传,会上传到当前目录

sz 下载功能

  • sz fileName(你要下载的文件的名字) 回车
  • 会弹出窗体 我们选择要保存的地方即可。

小记

最后呢本来还想写点更多的用法,无奈在写文档的时候非常卡,下次再说吧。
这里呢想呼吁一下大家多些一些完整的博客,不要去直接复制粘贴转载。

查看原文

赞 31 收藏 21 评论 18

五月花开 发布了文章 · 2019-10-30

Electron+Reat Hooks 开发一个ToDoList桌面端程序

开始

安装react脚手架并初始化项目

cnpm install -g create-react-app
create-react-app electron-react-today
cd electron-react-today
npm start

此时项目已经运行在 :localhost:3000

安装 electron

electron 7.0.0 实在太坑爹了

使用6.1.2没有问题。

cnpm install --save-dev electron@6.1.2 --verbose

新建main.js

// 引入electron并创建一个Browserwindow
const { app, BrowserWindow } = require('electron')
const path = require('path')
const url = require('url')

// 保持window对象的全局引用,避免JavaScript对象被垃圾回收时,窗口被自动关闭.
let mainWindow

function createWindow () {
//创建浏览器窗口,宽高自定义具体大小你开心就好
mainWindow = new BrowserWindow({width: 800, height: 600})

  /* 
   * 加载应用-----  electron-quick-start中默认的加载入口
    mainWindow.loadURL(url.format({
      pathname: path.join(__dirname, 'index.html'),
      protocol: 'file:',
      slashes: true
    }))
  */
  // 加载应用----适用于 react 项目
  mainWindow.loadURL('http://localhost:3000/')
  
 // 加载本地html
 // mainWindow.loadFile('./index.html')   
    
  // 打开开发者工具,默认不打开
  mainWindow.webContents.openDevTools()

  // 关闭window时触发下列事件.
  mainWindow.on('closed', function () {
    mainWindow = null
  })
}

// 当 Electron 完成初始化并准备创建浏览器窗口时调用此方法
app.on('ready', createWindow)

// 所有窗口关闭时退出应用.
app.on('window-all-closed', function () {
  // macOS中除非用户按下 `Cmd + Q` 显式退出,否则应用与菜单栏始终处于活动状态.
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', function () {
   // macOS中点击Dock图标时没有已打开的其余应用窗口时,则通常在应用中重建一个窗口
  if (mainWindow === null) {
    createWindow()
  }
})

启动项目

package.json文件中添加:

{
    "main": "main.js",
    "scripts": {
      "electron-start": "electron ."
  },
}

然后执行:

npm run electron-start

看到如下页面,终于第一步完成了。

K4qYc9.md.png

React Hooks

  • useState声明状态变量

    const [ count , setCount ] = useState(0);
  • useEffect代替常用生命周期函数

    // 第一次渲染和每次更新都会被执行 并且是异步执行的
    useEffect(()=>{
      console.log(`useEffect=>You clicked ${count} times`)
    })
    
    // 当传空数组[]时,就是当组件将被销毁时才进行解绑,
    // 这也就实现了componentWillUnmount的生命周期函数
    useEffect(()=>{
        console.log('useEffect=>老弟你来了!Index页面')
        return ()=>{
            console.log('老弟,你走了!Index页面')
        }
    },[])
  • useContext 实现数据共享(父子组件传值)

    可以通过这个hook传递useReducer产生的dispatch函数。

    也就是不直接传递数据,而是传递修改数据的方法,在根组件中通过reducer修改状态。

    父组件

    import React, { useState, createContext } from 'react';
    import { Button } from '@material-ui/core';
    // 引入子组件
    import Num from './Num';
    
    // 创建上下文对象
    const CounterContext = createContext();
    
    export default function Counter() {
        
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>点击了 {count} 次</p>
          <Button
            variant="contained"
            color="primary"
            onClick={() => setCount(count + 1)}
          >
            点我加一
          </Button>
          <CounterContext.Provider value={count}>
            <Num />
          </CounterContext.Provider>
        </div>
    );
    }
    
    // 导出上下文对象
    export { CounterContext };

    子组件

    import React, { useContext } from 'react';
    
    // 引入父组件的上下文
    import { CounterContext } from './Counter';
    
    export default function Count() {
      const count = useContext(CounterContext);
      return <h2>{count}</h2>
    }
  • useReducer 实现对复杂状态对象的管理

    使用场景

    对某个state有很多种操作

    子组件需要修改上层组件的值,可以传递一个dispatch函数

    const reducer = (state, action) => {
        switch(action.type) {
           case: "xx":
              ....
        }
    }
    
    const [state, dispatch] = useReducer(/*reducer函数*/ reducer, /*初始值*/ initialVal);
    
    dispatch({ type: 'xx', val: '' });

当然还有其他的Hooks,例如用于性能优化的useMemo就不说了。

ToDoList 应用

有了以上内容就可以开发一个简单的TODoList应用了。

K4qJ1J.png

使用到的内容:material-ui 和 上文中的React Hooks 。

至于如何安装组件库可以自行查看:https://material-ui.com/zh/ge...

整体目录结构如下图:

K4qa0x.png

App.js 文件: 承载状态数据和组件。

import React, { useReducer, createContext } from 'react';
import { initialTodos, filterReducer, todosReducer } from './reducer/index';

import Filter from './components/Filter';
import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';

// 导出共享对象
export const AppContext = createContext();

function App() {
  const [todos, dispatchTodos] = useReducer(todosReducer, initialTodos);
  const [filterVal, dispatchFilter] = useReducer(filterReducer, 'ALL');

  return (
    <div style={{ margin: '20px 30px 0', maxWidth: 450 }}>
      <AppContext.Provider value={dispatchTodos}>
        <AddTodo />
        <Filter dispatch={dispatchFilter} />
        <TodoList filterVal={filterVal} todos={todos} />
      </AppContext.Provider>
    </div>
  );
}

export default App;

/reducer/index.js 文件: 完成对数据的修改。

import uuid from 'uuid';

export const initialTodos = [
  {
    id: uuid(),
    label: '学习React Hooks',
    complete: false,
  }, {
    id: uuid(),
    label: '吃饭睡觉',
    complete: true,
  }
];

export const filterReducer = (state, action) => {
  switch (action.type) {
    case 'SHOW_ALL':
      return 'ALL';
    case 'SHOW_COMPLETE':
      return 'COMPLETE';
    case 'SHOW_INCOMPLETE':
      return 'INCOMPLETE';
    default:
      throw Error();
  }
}

export const todosReducer = (state, action) => {
  switch (action.type) {
    case 'CHECK_TODO':
      return state.map(todo => {
        if (todo.id === action.id) {
          todo.complete = !todo.complete
        }
        return todo;
      });
    case 'DELETE_TODO':
      console.log(state, action.id)
      return state.filter(todo => todo.id !== action.id);
    case 'ADD_TODO':
      return state.concat(action.todo);
    default:
      throw Error();
  }
}

/components/AddTodo.js: 输入框,添加任务。

import React, { useState, useContext } from 'react';
import { Input, Button } from '@material-ui/core';
import uuid from 'uuid';
import { AppContext } from '../App';

export default function AddTodo() {

  const dispatch = useContext(AppContext);

  const handleSubmit = () => {
    if (!task) return;
    setTask('');
    dispatch({ type: 'ADD_TODO', todo: { id: uuid(), label: task, complete: false } });
  }

  const [task, setTask] = useState('');

  return (
    <div style={{ display: 'flex' }}>
      <Input
        value={task}
        style={{ flex: 1 }}
        onChange={(e) => setTask(e.target.value)}
        inputProps={{ 'aria-label': 'description' }}
      />
      <Button color="primary" onClick={handleSubmit}>添加</Button>
    </div>
  )
}

/components/Filter.js: 筛选任务。

import React from 'react';
import { Button } from '@material-ui/core';

export default function Filter({ dispatch }) {

  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', margin: '20px 0' }}>
      <Button color="primary" onClick={() => dispatch({ type: 'SHOW_ALL' })}>全部</Button>
      <Button color="primary" onClick={() => dispatch({ type: 'SHOW_COMPLETE' })}>已完成</Button>
      <Button color="primary" onClick={() => dispatch({ type: 'SHOW_INCOMPLETE' })}>未完成</Button>
    </div>
  )
}

/components/TodoList.js: 展示所有的任务。

import React, { useContext } from 'react';
import { List, ListItem, ListItemText, ListItemSecondaryAction, IconButton, Checkbox, ListItemIcon } from '@material-ui/core';
import { Delete as DeleteIcon } from '@material-ui/icons';
import { AppContext } from '../App'

export default function TodoList({ todos, filterVal }) {

  const dispatch = useContext(AppContext);

  const deleteTodo = (item) => {
    dispatch({ type: 'DELETE_TODO', id: item.id });
  }

  const checkTodo = (item) => {
    dispatch({ type: 'CHECK_TODO', id: item.id });
  }

  // 过滤 todos
  const filteredTodos = () => {
    if (filterVal === 'ALL') return todos;
    if (filterVal === 'COMPLETE') {
      return todos.filter(todo => todo.complete);
    }
    if (filterVal === 'INCOMPLETE') {
      return todos.filter(todo => !todo.complete);
    }
    return [];
  }

  return (
    <List component="nav" aria-label="secondary mailbox folders">
      {
        filteredTodos().map(item => (
          <ListItem key={item.id} button>
            <ListItemIcon>
              <Checkbox
                edge="start"
                checked={item.complete}
                onChange={() => checkTodo(item)}
                disableRipple
              />
            </ListItemIcon>
            <ListItemText primary={item.label} />
            <ListItemSecondaryAction>
              <IconButton onClick={() => deleteTodo(item)} edge="end" aria-label="delete">
                <DeleteIcon />
              </IconButton>
            </ListItemSecondaryAction>
          </ListItem>
        ))
      }
    </List>
  )
}

至此,一个简单的ToDoList就完成了。

查看原文

赞 7 收藏 5 评论 1

五月花开 收藏了文章 · 2019-09-12

学习 lodash 源码整体架构,打造属于自己的函数式编程类库

前言

你好,我是若川。这是学习源码整体架构系列第三篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。

要是有人说到怎么读源码,正在读文章的你能推荐我的源码系列文章,那真是太好了

学习源码整体架构系列文章如下:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
5.学习 vuex 源码整体架构,打造属于自己的状态管理库
6.学习 axios 源码整体架构,打造属于自己的请求库
7.学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理
8.学习 redux 源码整体架构,深入理解 redux 及其中间件原理
感兴趣的读者可以点击阅读。

其他源码计划中的有:expressvue-rotuerreact-redux 等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。

源码类文章,一般阅读量不高。已经有能力看懂的,自己就看了。不想看,不敢看的就不会去看源码。

所以我的文章,尽量写得让想看源码又不知道怎么看的读者能看懂。

underscore源码分析的文章比较多,而lodash源码分析的文章比较少。原因之一可能是由于lodash源码行数太多。注释加起来一万多行。

分析lodash整体代码结构的文章比较少,笔者利用谷歌、必应、github等搜索都没有找到,可能是找的方式不对。于是打算自己写一篇。平常开发大多数人都会使用lodash,而且都或多或少知道,lodashunderscore性能好,性能好的主要原因是使用了惰性求值这一特性。

本文章学习的lodash的版本是:v4.17.15unpkg.com地址 https://unpkg.com/lodash@4.17...

文章篇幅可能比较长,可以先收藏再看,所以笔者使用了展开收缩的形式。

导读:

文章主要学习了runInContext() 导出_lodash函数使用baseCreate方法原型继承LodashWrapperLazyWrappermixin挂载方法到lodash.prototype、后文用结合例子解释lodash.prototype.value(wrapperValue)Lazy.prototype.value(lazyValue)惰性求值的源码具体实现。

匿名函数执行

;(function() {

}.call(this));

暴露 lodash

var _ = runInContext();

runInContext 函数

这里的简版源码,只关注函数入口和返回值。

var runInContext = (function runInContext(context) {
    // 浏览器中处理context为window
    // ...
    function lodash(value) {}{
        // ...
        return new LodashWrapper(value);
    }
    // ...
    return lodash;
});

可以看到申明了一个runInContext函数。里面有一个lodash函数,最后处理返回这个lodash函数。

再看lodash函数中的返回值 new LodashWrapper(value)

LodashWrapper 函数

function LodashWrapper(value, chainAll) {
    this.__wrapped__ = value;
    this.__actions__ = [];
    this.__chain__ = !!chainAll;
    this.__index__ = 0;
    this.__values__ = undefined;
}

设置了这些属性:

__wrapped__:存放参数value

__actions__:存放待执行的函数体func, 函数参数 args,函数执行的this 指向 thisArg

__chain__undefined两次取反转成布尔值false,不支持链式调用。和underscore一样,默认是不支持链式调用的。

__index__:索引值 默认 0。

__values__:主要clone时使用。

接着往下搜索源码,LodashWrapper
会发现这两行代码。

LodashWrapper.prototype = baseCreate(baseLodash.prototype);
LodashWrapper.prototype.constructor = LodashWrapper;

接着往上找baseCreate、baseLodash这两个函数。

baseCreate 原型继承

//  立即执行匿名函数
// 返回一个函数,用于设置原型 可以理解为是 __proto__
var baseCreate = (function() {
    // 这句放在函数外,是为了不用每次调用baseCreate都重复申明 object
    // underscore 源码中,把这句放在开头就申明了一个空函数 `Ctor`
    function object() {}
    return function(proto) {
        // 如果传入的参数不是object也不是function 是null
        // 则返回空对象。
        if (!isObject(proto)) {
            return {};
        }
        // 如果支持Object.create方法,则返回 Object.create
        if (objectCreate) {
            // Object.create
            return objectCreate(proto);
        }
        // 如果不支持Object.create 用 ployfill new
        object.prototype = proto;
        var result = new object;
        // 还原 prototype
        object.prototype = undefined;
        return result;
    };
}());

// 空函数
function baseLodash() {
    // No operation performed.
}

// Ensure wrappers are instances of `baseLodash`.
lodash.prototype = baseLodash.prototype;
// 为什么会有这一句?因为上一句把lodash.prototype.construtor 设置为Object了。这一句修正constructor
lodash.prototype.constructor = lodash;

LodashWrapper.prototype = baseCreate(baseLodash.prototype);
LodashWrapper.prototype.constructor = LodashWrapper;

笔者画了一张图,表示这个关系。

lodash 原型关系图

衍生的 isObject 函数

判断typeof value不等于null,并且是object或者function

function isObject(value) {
    var type = typeof value;
    return value != null && (type == 'object' || type == 'function');
}

Object.create() 用法举例

面试官问:能否模拟实现JS的new操作符 之前这篇文章写过的一段,所以这里收缩起来了。
笔者之前整理的一篇文章中也有讲过,可以翻看JavaScript 对象所有API解析

MDN Object.create()

Object.create(proto, [propertiesObject])
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
它接收两个参数,不过第二个可选参数是属性描述符(不常用,默认是undefined)。

var anotherObject = {
    name: '若川'
};
var myObject = Object.create(anotherObject, {
    age: {
        value:18,
    },
});
// 获得它的原型
Object.getPrototypeOf(anotherObject) === Object.prototype; // true 说明anotherObject的原型是Object.prototype
Object.getPrototypeOf(myObject); // {name: "若川"} // 说明myObject的原型是{name: "若川"}
myObject.hasOwnProperty('name'); // false; 说明name是原型上的。
myObject.hasOwnProperty('age'); // true 说明age是自身的
myObject.name; // '若川'
myObject.age; // 18;

对于不支持ES5的浏览器,MDN上提供了ployfill方案。

if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' && typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;
        return new F();
    };
}

lodash上有很多方法和属性,但在lodash.prototype也有很多与lodash上相同的方法。肯定不是在lodash.prototype上重新写一遍。而是通过mixin挂载的。

mixin

mixin 具体用法

_.mixin([object=lodash], source, [options={}])
添加来源对象自身的所有可枚举函数属性到目标对象。 如果 object 是个函数,那么函数方法将被添加到原型链上。

注意: 使用 _.runInContext 来创建原始的 lodash 函数来避免修改造成的冲突。

添加版本

0.1.0

参数

[object=lodash] (Function|Object): 目标对象。

source (Object): 来源对象。

[options={}] (Object): 选项对象。

[options.chain=true] (boolean): 是否开启链式操作。

返回

(*): 返回 object.

mixin 源码

function mixin(object, source, options) {
    var props = keys(source),
        methodNames = baseFunctions(source, props);

    if (options == null &&
        !(isObject(source) && (methodNames.length || !props.length))) {
        options = source;
        source = object;
        object = this;
        methodNames = baseFunctions(source, keys(source));
    }
    var chain = !(isObject(options) && 'chain' in options) || !!options.chain,
        isFunc = isFunction(object);

    arrayEach(methodNames, function(methodName) {
        var func = source[methodName];
        object[methodName] = func;
        if (isFunc) {
            object.prototype[methodName] = function() {
                var chainAll = this.__chain__;
                if (chain || chainAll) {
                    var result = object(this.__wrapped__),
                        actions = result.__actions__ = copyArray(this.__actions__);

                    actions.push({ 'func': func, 'args': arguments, 'thisArg': object });
                    result.__chain__ = chainAll;
                    return result;
                }
                return func.apply(object, arrayPush([this.value()], arguments));
            };
        }
    });

    return object;
}

接下来先看衍生的函数。

其实看到具体定义的函数代码就大概知道这个函数的功能。为了不影响主线,导致文章篇幅过长。具体源码在这里就不展开。

感兴趣的读者可以自行看这些函数衍生的其他函数的源码。

mixin 衍生的函数 keys

mixin 函数中 其实最终调用的就是 Object.keys

function keys(object) {
    return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
}

mixin 衍生的函数 baseFunctions

返回函数数组集合

function baseFunctions(object, props) {
    return arrayFilter(props, function(key) {
        return isFunction(object[key]);
    });
}

mixin 衍生的函数 isFunction

判断参数是否是函数

function isFunction(value) {
    if (!isObject(value)) {
        return false;
    }
    // The use of `Object#toString` avoids issues with the `typeof` operator
    // in Safari 9 which returns 'object' for typed arrays and other constructors.
    var tag = baseGetTag(value);
    return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}

mixin 衍生的函数 arrayEach

类似 [].forEarch

function arrayEach(array, iteratee) {
    var index = -1,
        length = array == null ? 0 : array.length;

    while (++index < length) {
        if (iteratee(array[index], index, array) === false) {
            break;
        }
    }
    return array;
}

mixin 衍生的函数 arrayPush

类似 [].push

function arrayPush(array, values) {
    var index = -1,
        length = values.length,
        offset = array.length;

    while (++index < length) {
    array[offset + index] = values[index];
    }
    return array;
}

mixin 衍生的函数 copyArray

拷贝数组

function copyArray(source, array) {
    var index = -1,
        length = source.length;

    array || (array = Array(length));
    while (++index < length) {
        array[index] = source[index];
    }
    return array;
}

mixin 源码解析

lodash 源码中两次调用 mixin

// Add methods that return wrapped values in chain sequences.
lodash.after = after;
// code ... 等 153 个支持链式调用的方法

// Add methods to `lodash.prototype`.
// 把lodash上的静态方法赋值到 lodash.prototype 上
mixin(lodash, lodash);

// Add methods that return unwrapped values in chain sequences.
lodash.add = add;
// code ... 等 152 个不支持链式调用的方法


// 这里其实就是过滤 after 等支持链式调用的方法,获取到 lodash 上的 add 等 添加到lodash.prototype 上。
mixin(lodash, (function() {
    var source = {};
    // baseForOwn 这里其实就是遍历lodash上的静态方法,执行回调函数
    baseForOwn(lodash, function(func, methodName) {
        // 第一次 mixin 调用了所以赋值到了lodash.prototype
        // 所以这里用 Object.hasOwnProperty 排除不在lodash.prototype 上的方法。也就是 add 等 152 个不支持链式调用的方法。
        if (!hasOwnProperty.call(lodash.prototype, methodName)) {
            source[methodName] = func;
        }
    });
    return source;
// 最后一个参数options 特意注明不支持链式调用
}()), { 'chain': false });

结合两次调用mixin 代入到源码解析如下

function mixin(object, source, options) {
    // source 对象中可以枚举的属性
    var props = keys(source),
        // source 对象中的方法名称数组
        methodNames = baseFunctions(source, props);

    if (options == null &&
        !(isObject(source) && (methodNames.length || !props.length))) {
        // 如果 options 没传为 undefined  undefined == null 为true
        // 且 如果source 不为 对象或者不是函数
        // 且 source对象的函数函数长度 或者 source 对象的属性长度不为0
        // 把 options 赋值为 source
        options = source;
        // 把 source 赋值为 object
        source = object;
        // 把 object 赋值为 this 也就是 _ (lodash)
        object = this;
        // 获取到所有的方法名称数组
        methodNames = baseFunctions(source, keys(source));
    }
    // 是否支持 链式调用
    // options  不是对象或者不是函数,是null或者其他值
    // 判断options是否是对象或者函数,如果不是或者函数则不会执行 'chain' in options 也就不会报错
    //  且 chain 在 options的对象或者原型链中
    // 知识点 in [MDN in :  https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/in
    // 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true。

    // 或者 options.chain 转布尔值
    var chain = !(isObject(options) && 'chain' in options) || !!options.chain,
        // object 是函数
        isFunc = isFunction(object);

    // 循环 方法名称数组
    arrayEach(methodNames, function(methodName) {
        // 函数本身
        var func = source[methodName];
        // object 通常是 lodash  也赋值这个函数。
        object[methodName] = func;
        if (isFunc) {
            // 如果object是函数 赋值到  object prototype  上,通常是lodash
            object.prototype[methodName] = function() {
                // 实例上的__chain__ 属性 是否支持链式调用
                // 这里的 this 是 new LodashWrapper 实例 类似如下
                /**
                 {
                    __actions__: [],
                    __chain__: true
                    __index__: 0
                    __values__: undefined
                    __wrapped__: []
                 }
                 **/

                var chainAll = this.__chain__;
                // options 中的 chain 属性 是否支持链式调用
                // 两者有一个符合链式调用  执行下面的代码
                if (chain || chainAll) {
                    // 通常是 lodash
                    var result = object(this.__wrapped__),
                    // 复制 实例上的 __action__ 到 result.__action__ 和 action 上
                    actions = result.__actions__ = copyArray(this.__actions__);

                    // action 添加 函数 和 args 和 this 指向,延迟计算调用。
                    actions.push({ 'func': func, 'args': arguments, 'thisArg': object });
                    //实例上的__chain__ 属性  赋值给 result 的 属性 __chain__
                    result.__chain__ = chainAll;
                    // 最后返回这个实例
                    return result;
                }

                // 都不支持链式调用。直接调用
                // 把当前实例的 value 和 arguments 对象 传递给 func 函数作为参数调用。返回调用结果。
                return func.apply(object, arrayPush([this.value()], arguments));
            };
        }
    });

    // 最后返回对象 object
    return object;
}

小结:简单说就是把lodash上的静态方法赋值到lodash.prototype上。分两次第一次是支持链式调用(lodash.after153 个支持链式调用的方法),第二次是不支持链式调用的方法(lodash.add152个不支持链式调用的方法)。

lodash 究竟在_和_.prototype挂载了多少方法和属性

再来看下lodash究竟挂载在_函数对象上有多少静态方法和属性,和挂载_.prototype上有多少方法和属性。

使用for in循环一试便知。看如下代码:

var staticMethods = [];
var staticProperty = [];
for(var name in _){
    if(typeof _[name] === 'function'){
        staticMethods.push(name);
    }
    else{
        staticProperty.push(name);
    }
}
console.log(staticProperty); // ["templateSettings", "VERSION"] 2个
console.log(staticMethods); // ["after", "ary", "assign", "assignIn", "assignInWith", ...] 305个

其实就是上文提及的 lodash.after153个支持链式调用的函数 、lodash.add152不支持链式调用的函数赋值而来。

var prototypeMethods = [];
var prototypeProperty = [];
for(var name in _.prototype){
    if(typeof _.prototype[name] === 'function'){
        prototypeMethods.push(name);
    }
    else{
        prototypeProperty.push(name);
    }
}
console.log(prototypeProperty); // []
console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 317个

相比lodash上的静态方法多了12个,说明除了 mixin 外,还有12个其他形式赋值而来。

支持链式调用的方法最后返回是实例对象,获取最后的处理的结果值,最后需要调用value方法。

笔者画了一张表示lodash的方法和属性挂载关系图。

codelodash/code的方法和属性挂载关系图

请出贯穿下文的简单的例子

var result = _.chain([1, 2, 3, 4, 5])
.map(el => {
    console.log(el); // 1, 2, 3
    return el + 1;
})
.take(3)
.value();
// lodash中这里的`map`仅执行了`3`次。
// 具体功能也很简单 数组 1-5 加一,最后获取其中三个值。
console.log('result:', result);

也就是说这里lodash聪明的知道了最后需要几个值,就执行几次map循环,对于很大的数组,提升性能很有帮助。

underscore执行这段代码其中map执行了5次。
如果是平常实现该功能也简单。

var result = [1, 2, 3, 4, 5].map(el => el + 1).slice(0, 3);
console.log('result:', result);

而相比lodash这里的map执行了5次。

// 不使用 map、slice
var result = [];
var arr = [1, 2, 3, 4, 5];
for (var i = 0; i < 3; i++){
    result[i] = arr[i] + 1;
}
console.log(result, 'result');

简单说这里的map方法,添加 LazyWrapper 的方法到 lodash.prototype存储下来,最后调用 value时再调用。
具体看下文源码实现。

添加 LazyWrapper 的方法到 lodash.prototype

主要是如下方法添加到到 lodash.prototype 原型上。

// "constructor"
["drop", "dropRight", "take", "takeRight", "filter", "map", "takeWhile", "head", "last", "initial", "tail", "compact", "find", "findLast", "invokeMap", "reject", "slice", "takeRightWhile", "toArray", "clone", "reverse", "value"]
// Add `LazyWrapper` methods to `lodash.prototype`.
// baseForOwn 这里其实就是遍历LazyWrapper.prototype上的方法,执行回调函数
baseForOwn(LazyWrapper.prototype, function(func, methodName) {
    // 检测函数名称是否是迭代器也就是循环
    var checkIteratee = /^(?:filter|find|map|reject)|While$/.test(methodName),
        // 检测函数名称是否head和last
        // 顺便提一下 ()这个是捕获分组 而加上 ?:  则是非捕获分组 也就是说不用于其他操作
        isTaker = /^(?:head|last)$/.test(methodName),
        // lodashFunc 是 根据 isTaker 组合 takeRight take methodName
        lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName],
        // 根据isTaker 和 是 find 判断结果是否 包装
        retUnwrapped = isTaker || /^find/.test(methodName);

    // 如果不存在这个函数,就不往下执行
    if (!lodashFunc) {
        return;
    }
    // 把 lodash.prototype 方法赋值到lodash.prototype
    lodash.prototype[methodName] = function() {
        // 取实例中的__wrapped__ 值 例子中则是 [1,2,3,4,5]
        var value = this.__wrapped__,
            // 如果是head和last 方法 isTaker 返回 [1], 否则是arguments对象
            args = isTaker ? [1] : arguments,
            // 如果value 是LayeWrapper的实例
            isLazy = value instanceof LazyWrapper,
            // 迭代器 循环
            iteratee = args[0],
            // 使用useLazy isLazy value或者是数组
            useLazy = isLazy || isArray(value);

        var interceptor = function(value) {
            // 函数执行 value args 组合成数组参数
            var result = lodashFunc.apply(lodash, arrayPush([value], args));
            // 如果是 head 和 last (isTaker) 支持链式调用 返回结果的第一个参数 否则 返回result
            return (isTaker && chainAll) ? result[0] : result;
        };

        // useLazy true 并且 函数checkIteratee 且迭代器是函数,且迭代器参数个数不等于1
        if (useLazy && checkIteratee && typeof iteratee == 'function' && iteratee.length != 1) {
            // Avoid lazy use if the iteratee has a "length" value other than `1`.
            // useLazy 赋值为 false
            // isLazy 赋值为 false
            isLazy = useLazy = false;
        }
        // 取实例上的 __chain__
        var chainAll = this.__chain__,
            // 存储的待执行的函数 __actions__ 二次取反是布尔值 也就是等于0或者大于0两种结果
            isHybrid = !!this.__actions__.length,
            // 是否不包装 用结果是否不包装 且 不支持链式调用
            isUnwrapped = retUnwrapped && !chainAll,
            // 是否仅Lazy 用isLazy 和 存储的函数
            onlyLazy = isLazy && !isHybrid;

        // 结果不包装 且 useLazy 为 true
        if (!retUnwrapped && useLazy) {
            // 实例 new LazyWrapper 这里的this 是 new LodashWrapper()
            value = onlyLazy ? value : new LazyWrapper(this);
            // result 执行函数结果
            var result = func.apply(value, args);

            /*
            *
            // _.thru(value, interceptor)
            // 这个方法类似 _.tap, 除了它返回 interceptor 的返回结果。该方法的目的是"传递" 值到一个方法链序列以取代中间结果。
            _([1, 2, 3])
            .tap(function(array) {
                // 改变传入的数组
                array.pop();
            })
            .reverse()
            .value();
            // => [2, 1]
            */

            // thisArg 指向undefined 或者null 非严格模式下是指向window,严格模式是undefined 或者nll
            result.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined });
            // 返回实例 lodashWrapper
            return new LodashWrapper(result, chainAll);
        }
        // 不包装 且 onlyLazy 为 true
        if (isUnwrapped && onlyLazy) {
            // 执行函数
            return func.apply(this, args);
        }
        // 上面都没有执行,执行到这里了
        // 执行 thru 函数,回调函数 是 interceptor
        result = this.thru(interceptor);
        return isUnwrapped ? (isTaker ? result.value()[0] : result.value()) : result;
    };
});

小结一下,写了这么多注释,简单说:其实就是用LazyWrapper.prototype 改写原先在lodash.prototype的函数,判断函数是否需要使用惰性求值,需要时再调用。

读者可以断点调试一下,善用断点进入函数功能,对着注释看,可能会更加清晰。
例子的chain和map执行后的debugger截图

例子的chain和map执行后的debugger截图

链式调用最后都是返回实例对象,实际的处理数据的函数都没有调用,而是被存储存储下来了,最后调用value方法,才执行这些函数。

lodash.prototype.value 即 wrapperValue

function baseWrapperValue(value, actions) {
    var result = value;
    // 如果是lazyWrapper的实例,则调用LazyWrapper.prototype.value 方法,也就是 lazyValue 方法
    if (result instanceof LazyWrapper) {
        result = result.value();
    }
    // 类似 [].reduce(),把上一个函数返回结果作为参数传递给下一个函数
    return arrayReduce(actions, function(result, action) {
        return action.func.apply(action.thisArg, arrayPush([result], action.args));
    }, result);
}
function wrapperValue() {
    return baseWrapperValue(this.__wrapped__, this.__actions__);
}
lodash.prototype.toJSON = lodash.prototype.valueOf = lodash.prototype.value = wrapperValue;

如果是惰性求值,则调用的是 LazyWrapper.prototype.valuelazyValue

LazyWrapper.prototype.value 即 lazyValue 惰性求值

function LazyWrapper(value) {
    // 参数 value
    this.__wrapped__ = value;
    // 执行的函数
    this.__actions__ = [];
    this.__dir__ = 1;
    // 过滤
    this.__filtered__ = false;
    // 存储迭代器函数
    this.__iteratees__ = [];
    // 默认最大取值个数
    this.__takeCount__ = MAX_ARRAY_LENGTH;
    // 具体取值多少个,存储函数和类型
    this.__views__ = [];
}
/**
* Extracts the unwrapped value from its lazy wrapper.
*
* @private
* @name value
* @memberOf LazyWrapper
* @returns {*} Returns the unwrapped value.
*/
function lazyValue() {
    // this.__wrapped__ 是 new LodashWrapper 实例 所以执行.value 获取原始值
    var array = this.__wrapped__.value(),
        //
        dir = this.__dir__,
        // 是否是函数
        isArr = isArray(array),
        // 是否从右边开始
        isRight = dir < 0,
        // 数组的长度。如果不是数组,则是0
        arrLength = isArr ? array.length : 0,
        // 获取 take(3) 上述例子中 则是 start: 0,end: 3
        view = getView(0, arrLength, this.__views__),
        start = view.start,
        end = view.end,
        // 长度 3
        length = end - start,
        // 如果是是从右开始
        index = isRight ? end : (start - 1),
        // 存储的迭代器数组
        iteratees = this.__iteratees__,
        // 迭代器数组长度
        iterLength = iteratees.length,
        // 结果resIndex
        resIndex = 0,
        // 最后获取几个值,也就是 3
        takeCount = nativeMin(length, this.__takeCount__);

    // 如果不是数组,或者 不是从右开始 并且 参数数组长度等于take的长度 takeCount等于长度
    // 则直接调用 baseWrapperValue 不需要
    if (!isArr || (!isRight && arrLength == length && takeCount == length)) {
        return baseWrapperValue(array, this.__actions__);
    }
    var result = [];

    // 标签语句 label
    // MDN label 链接
    // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/label
    // 标记语句可以和 break 或 continue 语句一起使用。标记就是在一条语句前面加个可以引用的标识符(identifier)。
    outer:
    while (length-- && resIndex < takeCount) {
        index += dir;

        var iterIndex = -1,
            // 数组第一项
            value = array[index];

        while (++iterIndex < iterLength) {
            // 迭代器数组 {iteratee: function{}, typy: 2}
            var data = iteratees[iterIndex],
                iteratee = data.iteratee,
                type = data.type,
                // 结果 迭代器执行结果
                computed = iteratee(value);

            if (type == LAZY_MAP_FLAG) {
                // 如果 type 是 map 类型,结果 computed 赋值给value
                value = computed;
            } else if (!computed) {
                if (type == LAZY_FILTER_FLAG) {
                    // 退出当前这次循环,进行下一次循环
                    continue outer;
                } else {
                    // 退出整个循环
                    break outer;
                }
            }
        }
        // 最终数组
        result[resIndex++] = value;
    }
    // 返回数组 例子中则是 [2, 3, 4]
    return result;
}
// Ensure `LazyWrapper` is an instance of `baseLodash`.
LazyWrapper.prototype = baseCreate(baseLodash.prototype);
LazyWrapper.prototype.constructor = LazyWrapper;

LazyWrapper.prototype.value = lazyValue;

笔者画了一张 lodashLazyWrapper的关系图来表示。

codelodash/code和codeLazyWrapper/code的关系图

小结:lazyValue简单说实现的功能就是把之前记录的需要执行几次,把记录存储的函数执行几次,不会有多少项数据就执行多少次,而是根据需要几项,执行几项。
也就是说以下这个例子中,map函数只会执行3次。如果没有用惰性求值,那么map函数会执行5次。

var result = _.chain([1, 2, 3, 4, 5])
.map(el => el + 1)
.take(3)
.value();

总结

行文至此,基本接近尾声,最后总结一下。

文章主要学习了runInContext() 导出_lodash函数使用baseCreate方法原型继承LodashWrapperLazyWrappermixin挂载方法到lodash.prototype、后文用结合例子解释lodash.prototype.value(wrapperValue)Lazy.prototype.value(lazyValue)惰性求值的源码具体实现。

分享一个只知道函数名找源码定位函数申明位置的VSCode 技巧Ctrl + p。输入 @functionName 定位函数functionName在源码文件中的具体位置。如果知道调用位置,那直接按alt+鼠标左键即可跳转到函数申明的位置。

如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持。万分感谢。

推荐阅读

lodash github仓库
lodash 官方文档
lodash 中文文档
打造一个类似于lodash的前端工具库
惰性求值——lodash源码解读
luobo tang:lazy.js 惰性求值实现分析
lazy.js github 仓库
本文章学习的lodash的版本v4.17.15unpkg.com链接

笔者往期文章

学习 underscore 源码整体架构,打造属于自己的函数式编程类库
学习 jQuery 源码整体架构,打造属于自己的 js 类库
面试官问:JS的继承
面试官问:JS的this指向
面试官问:能否模拟实现JS的call和apply方法
面试官问:能否模拟实现JS的bind方法
面试官问:能否模拟实现JS的new操作符
前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客-若川-本文链接地址,使用vuepress重构了,阅读体验可能更好些
掘金专栏,欢迎关注~
segmentfault前端视野专栏,欢迎关注~
知乎前端视野专栏,欢迎关注~
github blog,相关源码和资源都放在这里,求个star^_^~

欢迎加微信交流 微信公众号

可能比较有趣的微信公众号,长按扫码关注。也可以加微信 ruochuan12,注明来源,拉您进【前端视野交流群】。

若川视野

查看原文

五月花开 赞了文章 · 2019-08-08

React源码剖析系列 - 玩转 React Transition

过去一年,React 给整个前端界带来了一种新的开发方式,我们抛弃了无所不能的 DOM 操作。对于 React 实现动画这个命题,DOM 操作已经是一条死路,而 CSS3 动画又只能实现一些最简单的功能。这时候 ReactCSSTransitionGroup Addon,无疑是一枚强心剂,能够帮助我们以最低的成本实现例如节点初次渲染、节点被删除时添加动效的需求。本文将会深入实现原理来玩转 ReactCSSTransitionGroup。

初窥 ReactCSSTransitionGroup

在介绍 ReactCSSTransitionGroup 的用法前,先来实现一个常规 transition 动画,要实现的是删除某个节点的时候,让该节点的透明度不断的变大。

handleRemove(item) {
  const { items } = this.state;
  const len = items.length;

  this.setState({
    items: items.reduce((result, entry) => {
      return entry.id === item.id ? [...result, { ...item, isRemoving: true }] : [...result, item];
    }, [])
  }, () => {
    setTimeout(() => {
      this.setState({
        items: items.reduce((result, entry) => {
          return entry.id === item.id ? result : [...result, item];
        }, [])
      });
    }, 500);
  });
},

render() {
  const items = this.state.items.map((item, i) => {
    return (
      <div
        key={item.id} onClick={this.handleRemove.bind(this, item)}
        className={item.isRemoving ? 'removing-item' : ''}>
        {item.name}
      </div>
    );
  });

  return (
    <div>
      <button onClick={this.handleAdd}>Add Item</button>
      <div>
        {items}
      </div>
    </div>
  );
}

同时我们在 CSS 中需要提供如下的样式

.removing-item {
  opacity: 0.01;
  transition: opacity .5s ease-in;
}

相同的需求,使用 ReactCSSTransitionGroup 创建动画会是怎么的呢?

handleRemove(i) {
  const { items } = this.state;
  const len = items.length;

  this.setState({
    items: [...items.slice(0, i),  ...item.slice(i + 1, len - 1)]
  });
},

render() {
  const items = this.state.items.map((item, i) => {
    return (
      <div key={item} onClick={this.handleRemove.bind(this, i)}>
        {item}
      </div>
    );
  });

  return (
    <div>
      <button onClick={this.handleAdd}>Add Item</button>
      <ReactCSSTransitionGroup transitionName="example">
        {items}
      </ReactCSSTransitionGroup>
    </div>
  );
}

在这个例子中,当新的节点从 ReactCSSTransitionGroup 中删除时,这个节点会被加上 example-leave 的 class,在下一帧中这个节点还会被加上 example-leave-active 的 class,通过添加以下 CSS 代码,被删除的节点就会有动画的效果。

.example-leave {
  opacity: 1;
  transition: opacity .5s ease-in;
}

.example-leave.example-leave-active {
  opacity: 0.01;
}

从这个例子,我们可以看到 ReactCSSTransition 可以把开发者从一大堆动画相关的 state 中解放出来,只需要关心数据的变化,以及 CSS 的 transition 动画逻辑。

后面将会仔细分析 ReactCSSTransitionGroup 的源码实现。在看代码之前,大家可以先看 官网的文档,对 ReactCSSTransitionGroup 的用法进一步了解。看完之中,可以想想两个问题:

  • appear 动画和 enter 动画有什么区别?

  • ReactCSSTransitionGroup 子元素的生命周期是怎样的?

ReactCSSTransitionGroup 模块关系

ReactCSSTransitionGroup 的源码分为5个模块,我们先看看这5个模块之间的关系:

Relation

我们来整理一下这几个模块的分工与职责:

  • ReactTransitionEvents 提供了对各种前缀的 transitionend、animationend 事件的绑定和解绑工具

  • ReactTransitionChildMapping 提供了对 ReactTransitionGroup 这个 component 的 children 进行格式化的工具

  • ReactCSSTransitionGroup 会调用 ReactCSSTransitionGroupChild 对 children 中的每个元素进行包装,然后将包装后的 children 作为 ReactTransitionGroup 的 children 。

从这个关系图里面可以看到,ReactTransitionGroupReactCSSTransitionGroupChild 才是实现动画的关键部分,因此,本文会从 ReactTransitionGroup 开始解读,然后从 ReactCSSTransitionGroupChild 中解读怎么实现具体的动画逻辑。

ReactTransitionGroup 源码解读

下面我们按照 React 生命周期来解读 ReactTransitionGroup

初次 Mount

  • 在初始化 state 的时候,将 this.props.children 转化为对象,其中对象的 key 就是 component key,这个 key 与 children 中的元素一一对应,然后将该对象设置为 this.state.children

getChildMapping

  • 在初次 render 的时候,将 this.state.children 中每一个普通的 child component 通过指定的 childFactory 包裹成一个新的 component,并渲染成指定类型的 component 的子元素。在下面的源码中也可以看到,我们在创建过程中给每个 child 设置的 key 也会作为 ref,方便后续索引。

render: function() {
  var childrenToRender = [];
  for (var key in this.state.children) {
    var child = this.state.children[key];

    if (child) {
      childrenToRender.push(React.cloneElement(
        this.props.childFactory(child),
        {ref: key, key: key}
      ));
    }
  }
  return React.createElement(
    this.props.component,
    this.props,
    childrenToRender
  );
}
  • 初次 mount 后,遍历 this.state.children 中的每个元素,依次执行 appear 动画的逻辑。

Appear

更新 component

当接收到新的 props 后,先将 nextProps.childrenthis.props.children 合并,然后转化为对象,并更新到 this.state.children。计算在 nextProps 中即将 leave 的 child,如果该元素当前没有正在运行的动画,将该元素的 key 保存在 keysToLeave。

对于 nextProps 中新的 child,如果该元素没有正在运行的动画的话(也许会疑惑,一个刚进入的元素怎么会有动画正在运行呢?下文将会解释),将该元素的 key 保存在 keysToEnter。从这里也能看出来,本来在 nextProps 中即将 leave 的 child 会被保留下来以达到动画效果,等动画效果结束后才会被 remove。

componentWillReceiveProps

component 更新完成后,对 keysToEnter 中的每个元素执行 enter 动画的逻辑,对 keysToLeave 中的每个元素执行 leave 动画的逻辑。由于 enter 动画的逻辑和 appear 动画的逻辑几乎一模一样,无非是变成执行 child 的componentWillEntercomponentDidEnter 方法。

leave 动画稍有不同,看下面源码可以看到,在 leave 动画结束后,如果发现该元素重新 enter,这里会再次执行 enter 动画,否则的话通过更新 state 中的 children 来删除相应的节点。这里也可以回答,为什么对刚 enter 的元素,也要判断该元素是否正在进行动画,因为如果该元素上一次 leave 的动画还没有结束,那么这个节点还一直保留在页面中运行动画。

另外,大家有没有注意到一个问题,如果 leave 动画的回调函数没有被调用,那么这个节点将永远不会被移除。

if (currentChildMapping && currentChildMapping.hasOwnProperty(key)) {
  // This entered again before it fully left. Add it again.
  this.performEnter(key);
} else {
  this.setState(function(state) {
    var newChildren = assign({}, state.children);
    delete newChildren[key];
    return {children: newChildren};
  });
}

至此,我们看到 ReactTransitionGroup 没有实现任何具体的动画逻辑。

ReactCSSTransitionGroup

搞清楚 ReactTransitionGroup 的原理以后,ReactCSSTransitionGroup 做的事情就很简单了。简单地说, ReactCSSTransitionGroup 调用了 ReactTransitionGroup ,提供了自己的 childFactory 方法,而这个 childFactory 则是调用了 ReactCSSTRansitionGroupChild 。

_wrapChild: function(child) {
  // We need to provide this childFactory so that
  // ReactCSSTransitionGroupChild can receive updates to name, enter, and
  // leave while it is leaving.
  return React.createElement(
    ReactCSSTransitionGroupChild,
    {
      name: this.props.transitionName,
      appear: this.props.transitionAppear,
      enter: this.props.transitionEnter,
      leave: this.props.transitionLeave,
      appearTimeout: this.props.transitionAppearTimeout,
      enterTimeout: this.props.transitionEnterTimeout,
      leaveTimeout: this.props.transitionLeaveTimeout,
    },
    child
  );
}

下面来看 ReactCSSTransitionGroupChild 是怎么实现节点的动画的。以 appear 动画为例,在 child.componentWillAppear 被调用的时候,给该节点加上 xxx-appear 的 className ,并且在一帧(React 里是写死的17ms)后,给该节点加上 xxx-appear-active 的 className ,最后在动画结束后删除 xxx-appear 以及 xxx-appear-active 的 className。

componentWillAppear

enter、leave 动画的实现类似。到这里源码就解读完了,其中,还有一些细节要去注意的。

隐藏在 key 里的秘密

在源码解读的过程中,我们发现 ReactTransitionGroup 会将 children 转化为对象,然后通过 for...in... 遍历。对于这一过程,会不会感到有所疑虑,ReactTransitionGroup 怎么保证子节点渲染的顺序。

对于这个问题,React 的处理过程可以简化为下面的代码,测试结果显示,当 key 为字符串类型时,for...in... 遍历的顺序和 children 的顺序能够保持一致;但是当 key 为数值类型时,for...in... 遍历的顺序和 children 的顺序就不一定能够保持一致,大家可以用下面这段简单的代码测试一下。

function test (o) {
  var result = {};
  for (var i = 0, len = o.length; i < len; i++) {
    result[o[i].key] = o[i];
  }
  for (var key in result) {
    if (result[key]) {
      console.log(key, result[key]);
    }
  }
}

因此,我们知道 ReactCSSTransitionGroup 所有子 component 的 key 千万不要设置成纯数字,一定要是字符串类型的。

transitionend 之殇

在 React 0.14 版本中,React 已经表示将在未来的版本中废弃监听 transitionend、 animationend 事件,而是通过设置动画的 timeout 来达到结束动画的目的,有没有想过 React 为什么要放弃原生事件,而改用 setTimeout

事实上,原因很简单,transitontend 事件在某些情况是不会被触发。在 transitionend 的 MDN文档 中有这么几行文字:

In the case where a transition is removed before completion, such as if the transition-property is removed, then the event will not fire. The event will also not fire if the animated element becomes display: none before the transition fully completes.

  • 当动画元素的 transition 属性在动画完成前被移除了,transitionend 事件不会被触发

  • 当动画元素在动画完成前,display 样式被设置成 "none",这种情况 transitionend 事件不会被触发

  • 当动画还没完成,当前浏览器标签页失焦很长的时间(大于动画时间),transitionend 事件不会被触发,直到该标签页重新聚焦后 transitionend 事件才会触发

正是由于 transitionend 不会触发,会导致隐形 bug,可以看其中一个 bug

总结

  • appear 动画是 ReactCSSTransitionGroup 组件初次 mount 后,才会被添加到 ReactCSSTransitionGroup 的所有子元素上。

  • enter 动画是 ReactCSSTransitionGroup 组件更新后,被添加到新增的子元素上。

  • ReactCSSTransitionGroup 提供创建 CSS 动画最简单的方法,对于更加个性化的动画,大家可以通过调用 ReactTransitionGroup 自定义动画。

参考资料

查看原文

赞 8 收藏 50 评论 3

五月花开 赞了文章 · 2019-07-20

我喜欢的5个编程技巧

在这篇文章中,我介绍了一些编程时尝试使用的模式。这些模式是多年来我自己在工作中实践的结果,也有是从同事那里偷偷学到的。

这些模式没有特定的顺序,只是一个简单的集合。

提前退出(early exits)

function transformData(rawData) {
  // check if no data
  if (!rawData) {
    return [];
  }

  // check for specific case
  if (rawData.length == 0) {
    return [];
  }

  // actual function code goes here
  return rawData.map((item) => item);
}

我将这种模式称为“提前退出(early exits)”,但有些人也将此称为“保镖模式(the Bouncer Pattern)”或“保护条款('guard clauses)”。撇开命名不谈,该模式采用的方法是首先检查无效的情况,然后从该函数返回,否则它将继续使用该函数的预期情况并执行。

对我来说,这种方法有一些我非常喜欢的优点:

  1. 有助于思考无效和边界情况,以及在这种情况下该如何处理。
  2. 避免对意外情况进行意外和不必要的代码处理
  3. 这样使我更能清楚地处理每种情况
  4. 一旦使用这种方式,您就可以快速地浏览函数并理解流程和执行,这通常遵循自顶向下的方法,即从无效的情况—>小情况—>预期情况。

更多信息:保镖模式(the Bouncer Pattern)

2. 用对象字面量替代 Switch

// Switch
let createType = null;
switch (contentType) {
  case "post":
    createType = () => console.log("creating a post...");
    break;
  case "video":
    createType = () => console.log("creating a video...");
    break;
  default:
    createType = () => console.log('unrecognized content type');
}

createType();

// Object literal
const contentTypes = {
  post: () => console.log("creating a post..."),
  video: () => console.log("creatinga  video..."),
  default: () => console.log('unrecognized content type')
};

const createType = contentTypes[contentType] || contentTypes['default'];
createType();

接下来就是要移除 Switch。在写 case 的时候我经常会犯错误,也会忘记写 break,这会引起各种有趣的问题。当我编写代码时,switch语句并没有体现太多的价值。

我更喜欢使用对象字面量,原因如下:

  1. 不用担心 cacebreak
  2. 更容易阅读并快速了解正在发生的事情
  3. 对象字面量很容易写
  4. 代码量少

3. 用一次循环处理两个数组

const exampleValues = [2, 15, 8, 23, 1, 32];
const [truthyValues, falseyValues] = exampleValues.reduce((arrays, exampleValue) => {
  if (exampleValue > 10) {
    arrays[0].push(exampleValue);
    return arrays;
  }

  arrays[1].push(exampleValue);
  return arrays;
}, [[], []]);

这种模式没什么特别的,我应该早点意识到,但我发现自己过滤一组元素,以获得所有匹配特定条件的元素,然后在另一种情况下要再做一次。这意味着对一个数组进行两次循环,但我可以只做一次。

原来它有一个名字(bifurcate),我从 30secondsofcode.org借鉴过来的。如果你从未去过那个网站,我建议你去那里。有很多有用的信息和代码。

我知道 reduce 可能会让人望而生畏,也不太清楚会发生什么,但如果你能适应它,在遍历集合时,您可以真正利用它来构建所需的任何数据结构。他们应该叫它 builder 而不是 reduce

4. 不要用 foo 做变量

// bad
const foo = y && z;

// good
const isPostEnabled = isPost && postDateValid;

这看起来很明显,但我相信我们都见过这样做的代码。花点时间,尽你最大的努力取个合适的名字。

这对于在职的专业人士或处于教育他人位置的人来说尤其重要。应该使用变量命名来帮助解释,在代码的上下文中发生了什么事情。

别人能够在阅读您的代码时,并大致可以理解要解决的问题。

更多信息:The art of naming variables

5. 嵌套三元运算符

// 之前
let result = null;
if (conditionA) {
  if (conditionB) {
    result = "A & B";
  } else {
    result = "A";
  }
} else {
  result = "Not A";
}
// 改造后
const result = !conditionA
  ? "Not A"
  : conditionB
  ? "A & B"
  : "A";

我承认,一开始,使用嵌套三元运算符的想法的确令人倒胃口。它看起来是一种编写条件的巧妙方式。

然后我开始编写业务逻辑,发现自己使用了嵌套的 if else 语句和一些非常可疑的条件逻辑。

我认为使用 ifelse更容易阅读,因为它们是实际的单词,有语义化,但当它们嵌套后,我开始很难理解发生了什么,并在心里默默跟踪所有情况。

我认为这种模式取决于你、你的团队的偏好。我在代码库中也很好的使用这种方式。当然使用三元运算符具有两面性,但就我个人而言,嵌套三元运算符真的越来越吸引我了。

更多信息:Nested Ternaries are Great by Eric Elliot

如果对你有帮助,请关注【前端技能解锁】:
qrcode_for_gh_d0af9f92df46_258.jpg

查看原文

赞 30 收藏 16 评论 12

五月花开 关注了标签 · 2019-07-20

react.js

React (sometimes styled React.js or ReactJS) is an open-source JavaScript library for creating user interfaces that aims to address challenges encountered in developing single-page applications. It is maintained by Facebook, Instagram and a community of individual developers and corporations.

关注 36808

五月花开 赞了文章 · 2019-07-09

redux入门指南(一)

作者:Jogis
原文链接:https://github.com/yesvods/Blog/issues/6
转载请注明原文链接以及作者信息

简介

redux是可预测的JS应用状态容器。

状态

现在有一个开关按钮组件ToggleButton,而按钮的on/off就是这个组件的状态。
一个应用里面会有大量的组件,而管理以及传递这些组件的状态,就成了非常繁琐的工作。redux使用一个非常简洁的思路,方便地提供不同层级组件的解耦式状态操作。

管理状态

reudx本身是一个非常简单的流程概念,它仅仅是一个状态容器。如图:
redux-flow

APP或者说是Web/SPA,在组件化的应用中,会有着大量的组件层级关系,深嵌套的组件与浅层父组件进行数据交互,变得十分繁琐困难。而redux,站在一个服务级别的角度,可以毫无阻碍地(这个得益于react的context机制,后面会讲解)将应用的状态传递到每一个层级的组件中。一整个应用,得一管家,足矣。

redux三大准则

使用redux过程中,只需要知晓这几个准则,就可以非常便捷地进行开发,少踩坑才是硬道理。

单一数据源

整个应用状态,都应该被存储在单一store的对象树中。

只读状态

唯一可以修改状态的方式,就是发送(dispatch)一个动作(Action),这个动作以最小的数据,去描述发生了什么。

比如,用户想要从状态中删除一条TODO信息:

//Action
{
    type: constant.REMOVE_TODO,
    id: 'TODO_ID'
}

非常简单,动作(Action)只需要一个type来描述这个动作类型,以及一个简单的id去表明要删那条信息。并不需要提供包含timestampdata在内的一大堆信息。

使用纯函数去修改状态

纯函数(pure function)可被看成是一个状态机,在任何时候,只要有相同的输入,就会得到相同的输出:

//pure function
//add(1, 2) ===> 3
//add(1, 2) ===> 3
function add(a, b){
    return a + b;
}

//impure function
//不纯函数,多次使用相同参数调用,可能会得出不同结果
//add(1) ===> 1
//add(1) ===> 2
var count = 0;
function add(n){
    return count + n;
}

这种修改状态的纯函数在redux里面被称作reducer,纯函数保障了状态的稳定性,不会因不同环境导致应用程序出现不同情况。而这也是redux内核的精髓(核心源码分析章节会讲解)。

redux几个概念

以上是redux的一些介绍,下面就开始入门redux。实际上,redux使用起来非常简单,就以上面提到的图片进行介绍。

Action

第一步,当然就是Action这个动作了,它可是唯一可以改变状态的途径,包括服务器来的推送以及用户操作,最终都需要转化为一个个Action。这种简单的方式解决了传统应用因为多种方式修改状态,而导致应用出现各种问题。

通过集中化对状态的修改,Action会按着严格的顺序一个个执行,因此应用并不会出现竞态的情况;Action仅仅是一个纯的JS对象,因此这个动作,可以被记录、序列化、存储以及以后通过一系列的“回放”来调试应用。

一个简单的添加TODO信息的动作:

{
    type: constant.ADD_TODO,
    msg: "下班回家收衣服"
}

Store

第二步,store可以被看作redux的最核心部分,一个小小的store,管理着整个应用的状态。其中包括:应用状态初始化、状态修改、注册状态变化监听器、替换应用状态。

当然,现在并不会涉及到这么多内容。store提供了一个方法#dispatch,这个就是用来发送一个动作,去修改store里面的状态,然后可以通过#getState方法来重新获得最新的状态:

//dispath above action
store.getState() // ===> {todo: []}
store.dispatch({
    type: constant.ADD_TODO,
    msg: "下班回家收衣服"
});
store.getState() // ===> {todo: ['下班回家收衣服']}

然后返回的新状态,即将作为应用的状态通过props(在react应用里面)发送回APP。APP接收到新的props会对应用进行实时更新。这样就完成一个动作触发到应用更新的过程了。

Reducer

刚有耳闻,这个reducer是用来修改状态的。再回顾上面的代码,在store.dispatch后发现store.getState调用返回的对象变了。

那应该是reducer做了手脚,顺带一提,reducer会接收两个参数:现在的状态state,被发送的动作Action

function todo(state = [], action){
    if(action.type === constant.ADD_TODO){
        //如果是添加TODO的动作,那就赶紧把TODO存起来
        state.push(action.msg);
        return state;
    }else {
        //其他动作的话,就不改状态啦,原样返回
        return state;
    }
}

store通过读取reducer返回的内容,来作为新的状态,因此应用状态就更新了!

查看原文

赞 3 收藏 16 评论 0

五月花开 赞了文章 · 2019-07-08

redux 资料合集

redux 资料合集

学习redux有段时间了,相关不错的资料整理下,希望能帮到有缘人

五颗星推荐

  • 中文文档 通读不下3边,翻译的很好,想理解清楚redux,定下心来,认真读,必有收获

  • awesome-redux 官方推荐资料合集,没有啥说的

  • Redux中间件深入浅出 就因为看了这篇文章才更加深入学习redux,同时对js有了别样的看法

三颗星推荐

一颗星推荐

查看原文

赞 59 收藏 213 评论 3

认证与成就

  • 获得 431 次点赞
  • 获得 8 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-11-02
个人主页被 2.4k 人浏览