渣正refn

渣正refn 查看完整档案

深圳编辑深圳大学  |  土木工程 编辑字节跳动  |  前端开发 编辑 github.com/ZhaZhengRefn 编辑
编辑

Talk is cheap.

个人动态

渣正refn 赞了文章 · 11月20日

PHP使用protobuf

服务器环境Ubuntu 18.04.5 LTS
PHP7.2.24

安装protoc

1.获取v3.13.0.1(截止2020.10.14)
wget https://codeload.github.com/protocolbuffers/protobuf/tar.gz/v3.13.0.1
2.解压
tar zxvf v3.13.0.1 
cd protobuf-3.13.0.1

1.生成 configure 脚本;
./autogen.sh
2.编译安装
./configure --prefix=/usr/local/protobuf
make && make install
3.设置全局
export PATH=/usr/local/protobuf/bin:$PATH
4.检查安装成功
protoc  --version
出现`libprotoc 3.13.0`即安装成功

安装php-protobuf拓展

pecl install protobuf

接下来,将
`extension=protobuf.so`添加到 `php.ini` 文件(例如 `/etc/php/7.2/fpm/php.ini`)中。

查看php.ini位置
1.cli命令行 php --ini
2.phpinfo();
3.ps -ef | grep php

在项目根目录

protoc --php_out="protobuf/compile" "protobuf/protos/DmpDataProto.proto"

生成的结构

├── compile
│   ├── GPBMetadata
│   │   └── Protobuf
│   │       └── Protos
│   │           └── DmpDataProto.php
│   └── Toutiao
│       └── Dmp
│           ├── DmpData.php
│           ├── IdItem_DataType.php
│           └── IdItem.php
└── protos
    └── DmpDataProto.proto

composer

composer require  google/protobuf:^3.3

composer.json配置

"autoload": {
        "classmap": [
            "database/seeds",
            "database/factories"
        ],
        "psr-4": {
            "App\\": "app/",
            +"Toutiao\\":"protobuf/compile/Toutiao",
            +"GPBMetadata\\":"protobuf/compile/GPBMetadata/Protobuf/Protos"
        }
    },

生成优化的自动加载文件

composer dump-autoload

使用

//获取序列化的一行
public function decodeOneLine($line)
{
    $idItem = new \Toutiao\Dmp\IdItem();
    $idItem->setDataType(\Toutiao\Dmp\IdItem_DataType::IMEI);
    $idItem->setId(strtolower($line));
    $idItem->setTags('IMEI');
    $binaryString = $idItem->serializeToString();
    return $binaryString;
}
//获取反序列化的一行
public function decodeOneLine($line)
{
    $item = new \Toutiao\Dmp\IdItem();
    $item->mergeFromString($line);
    return $item->getId();
}

主要为为了上传头条dmp包,但是头条是protobuf2,emmm明天再看吧
未完待续

ok
继续分享
我在百度Google搜了不少文章看,其中有篇博文写道:
博文地址

因为版本不兼容的问题,我们用version3 序列化的文件,version2 是解不出来的,通过实验发现每次都是少了一个前缀,因此我在下面的代码手动带上了这个前缀,头条才让我过。如果你不需要可以去掉。
// 手动拼前缀,不需要可以去掉
$prefix = "\n\x19\x10\x00";
$binaryString = $prefix . $binaryString;

我对比了下version3和2的差异
(重装protobuf2的过程与上方比较类似,此处不赘述了)
version3的结果:

root@ubuntu:# hexdump -C test.bin
00000000  1a 0f 38 36 30 37 31 34  30 34 31 39 31 35 36 30  |..86071404191560|
00000010  39 22 04 49 4d 45 49                              |9".IMEI|
00000017

version2的结果:

root@ubuntu:# hexdump -C testV2.bin
00000000  0a 19 10 00 1a 0f 38 36  30 37 31 34 30 34 31 39  |......8607140419|
00000010  31 35 36 30 39 22 04 49  4d 45 49                 |15609".IMEI|
0000001b

果然如博主所言,多了如下这四个字符

Hex(16进制)Bin(二进制)缩写/字符解释
0x0a0000 1010LF (NL line feed, new line)换行键
0x190001 1001EM (end of medium)媒介结束
0x100001 0000DLE (data link escape)数据链路转义
0x000000 0000NUL(null)空字符

~~至此,完成。
使用V3版本转化成V2的完整代码如下:~~

ok,实际使用过程中呢,发现了新的问题,就是IMEI类型这么搞是没有问题的,但是IDFA/IMEI_MD5,反正只要不是IMEI,都是有问题的,需要拼接不同的前缀,因此,我觉得这样子是不靠谱的,虽然PHP可以序列化,以及反序列化,但是V3和V2序列化后,头条那边都是不认的,是反序列化失败,然后提工单回复又很慢,最后,只好使用pytohon序列化,写个脚本,PHP去调用这个脚本,然后头条成功接受(因为头条后端这块就是py吧)
这里贴一下py脚本代码,hex打印的部分也留着了,这个是当时对比V3/V2 以及PHP生成的二进制做对比用的

from __future__ import print_function
import DmpDataProtoV2_pb2
import os,sys
import time
import base64

dmp_data  = DmpDataProtoV2_pb2.DmpData()
id_item1 = dmp_data.idList.add()
#dtype = 'IDFA'
dtype = sys.argv[1]
dev_id =  sys.argv[2]
id_item1.dataType = getattr(DmpDataProtoV2_pb2.IdItem,dtype)
#id_item1.dataType = DmpDataProtoV2_pb2.IdItem.IDFA
id_item1.id = str.lower(dev_id)
id_item1.tags.append(dtype)
# id_item1.timestamp = int(time.time())

binary_string  = dmp_data.SerializeToString()
#print(binary_string)
#print "nHex = %r" %(binary_string)
#t = bin(binary_string)
s = base64.b64encode(binary_string)

print (s,end='')
//获取序列化的一行
public function decodeOneLine($line, $v2 = false)
{
    $idItem = new \Toutiao\Dmp\IdItem();
    $idItem->setDataType(\Toutiao\Dmp\IdItem_DataType::IMEI);
    $idItem->setId(strtolower($line));
    $idItem->setTags('IMEI');
    $binaryString = $idItem->serializeToString();
    if ($v2) {
        // 手动拼前缀,转化为V2,不需要可以不传V2参数
        $prefix = "\n\x19\x10\x00";
        $binaryString = $prefix . $binaryString;
    }
    return $binaryString;
}

总结:个人理解还是很浅薄的,只管感觉protobuf像是将key-value格式的json,去掉了key,只传输数据,而key在数据收发端约定好(当然protobuf还有其他的优化)。

最后,原创不易,转载请注明出处啊~

查看原文

赞 2 收藏 0 评论 0

渣正refn 赞了文章 · 11月20日

数据结构与算法 :C 二叉搜索树的插入/查找/删除

C语言实现搜索二叉树

1结构体
typedef struct BSTreeNode {
    int data; //数据域
    struct BSTreeNode *left; //左子结点
    struct BSTreeNode *right; //右子结点
} BSTreeNode;
2插入
//1.第一种,传入指针,返回根节点
BSTreeNode *insert2(BSTreeNode *root, int data)
{
    if (NULL == root)
    {
        struct BSTreeNode *node;
        node = (struct BSTreeNode *)malloc(sizeof(struct BSTreeNode));
        if (node == NULL)
        {
            printf("malloc error \n");
            return NULL;
        }
        node->data = data;
        node->right = NULL;
        node->left = NULL;
        
        printf("null %d \n", data);
        return node;
    }
    printf("%d vs %d  \n", data,root->data);
    if (data >= root->data)
    {
        root->right = insert2(root->right, data);
    }else{
        root->left = insert2(root->left, data);
    }
    return root;
}
//第二种 传入二级指针,即指针的指针,无需返回
void insert(BSTreeNode **root, int data)
{
    if (NULL == *root)
    {
        printf("****ins isnull %d \n", data);
        *root = (struct BSTreeNode *)malloc(sizeof(struct BSTreeNode));
        (*root)->data = data;
        (*root)->left = NULL;
        (*root)->right = NULL;
    }else{
        if (data >= (*root)->data)
        {
            insert(&(*root)->right, data);
        }else{
            insert(&(*root)->left, data);
        }
    }
}
3查找
BSTreeNode *BSearch(BSTreeNode *root, int target)
{
    if (NULL == root)
    {
        return NULL;
    }
    if (root->data == target)
    {
        return root;
    }else if (target > root->data)
    {
        return BSearch(root->right, target);
    }else{
        return BSearch(root->left, target);
    }
}

栈和队列的构建 详见其他博文

4删除
BSTreeNode *FindMin(BSTreeNode *root)
{
    if (NULL == root)
    {
        return NULL;
    }
    if (root->left == NULL)
    {
        return root;
    }else{
        return FindMin(root->left);
    }
}

BSTreeNode *Delete(BSTreeNode *root, int target)
{
    if (NULL == root)
    {
        return NULL;
    }
    printf("%d vs %d  \n", target,root->data);
    if (target > root->data)
    {
        root->right = Delete(root->right, target);
    }else if(target < root->data){
        root->left  = Delete(root->left, target);
    }else{
        //相等的情况
        struct BSTreeNode *tmp;
        if (root->left && root->right)
        {
            tmp = FindMin(root->right);
            root->data = tmp->data;
            root->right = Delete(root->right, root->data);
        }else{
            tmp = root;
            if (root->left == NULL)
            {
                root = root->right;
            }else if(root->right == NULL){
                root = root->left;
            }else{
                root = NULL;//删除自身
            }
        }
    }
    return root;
}

测试:
image

查看原文

赞 3 收藏 0 评论 0

渣正refn 赞了文章 · 11月20日

数据结构与算法: C语言实现 前/中/后序的递归/非递归遍历

1.递归遍历

递归遍历非常简单,,,,,,

1.1前序遍历
void preOrderTraverse(BSTreeNode *root)
{
    if (root != NULL)
    {
        printf("%d \n", root->data);
        preOrderTraverse(root->left);
        preOrderTraverse(root->right);
    }
}
1.2中序遍历
void InOrderTraverse(BSTreeNode *root)
{
    if (root != NULL)
    {        
        InOrderTraverse(root->left);
        printf("%d \n", root->data);
        InOrderTraverse(root->right);
    }
}
1.3后序遍历
void PostOrderTraverse(BSTreeNode *root)
{
    if (root != NULL)
    {        
        PostOrderTraverse(root->left);
        PostOrderTraverse(root->right);
        printf("%d \n", root->data);
    }
}

2.非递归遍历

栈和队列的构建 详见其他博文

2.1前序遍历
void preOrderTraverseNR(BSTreeNode *root)
{
    //建立栈
    struct LinkedStack *Stack;
    Stack = initStack();
    while(NULL != root){
        printf("%d \n", root->data);
        if (NULL != root->right)
        {
            push(Stack, root->right);
        }
        if (NULL != root->left)
        {
            push(Stack, root->left);
        }
        root = pop(Stack);
    }
    free(Stack);
    Stack = NULL;
}

下周继续

ok,继续

2.2中序遍历
void InOrderTraverseNR(BSTreeNode *root)
{
    //建立栈
    struct LinkedStack *Stack;
    Stack = initStack();
    while(NULL != root || Stack->size > 0){
        if (root != NULL)
        {
            push(Stack, root);
            root = root->left;
        }else{
            root = pop(Stack);
            printf("%d \n", root->data);
            root = root->right;
        }
    }
    free(Stack);
    Stack = NULL;
}
2.3后序遍历
void PostOrderTraverseNR(BSTreeNode *root)
{
    //建立栈
    struct LinkedStack *Stack;
    struct LinkedStack *StackTmp;
    Stack    = initStack();
    StackTmp = initStack();
    while(NULL != root || StackTmp->size > 0){
        while (root != NULL)
        {
            push(Stack, root);
            push(StackTmp, root);
            root = root->right;
        }
        if (StackTmp->size > 0)
        {
            root = pop(StackTmp);
            root = root->left;
        }
    }
    while(Stack->size > 0){
        root = pop(Stack);
        printf("%d \n", root->data);
    }
    free(Stack);
    free(StackTmp);
    Stack = NULL;
    StackTmp = NULL;
}

测试:
image

查看原文

赞 2 收藏 0 评论 0

渣正refn 赞了文章 · 2019-07-17

浅谈正则表达式原理

原文:浅谈正则表达式原理 | AlloyTeam
作者:TAT.liberty

正则表达式可能大部分人都用过,但是大家在使用的时候,有没有想过正则表达式背后的原理,又或者当我告诉你正则表达式可能存在性能问题导致线上挂掉,你会不会觉得特别吃惊?

我们先来看看7月初,因为一个正则表达式,导致线上事故的例子。

https://blog.cloudflare.com/d...

简单来说就是一个有性能问题的正则表达式,引起了灾难性回溯,导致cpu满载。

性能问题的正则

先看看出问题的正则

引起性能问题的关键部分是 .*(?:.*=.*) ,这里我们先不管那个非捕获组,将性能问题的正则看做 .*.*=.*

其中 . 表示匹配除了换行以外的任意字符(很多人把这里搞错,容易出bug), .* 表示贪婪匹配任意字符任意次。

什么是回溯

在使用贪婪匹配或者惰性匹配或者或匹配进入到匹配路径选择的时候,遇到失败的匹配路径,尝试走另外一个匹配路径的这种行为,称作回溯。

可以理解为走迷宫,一条路走到底,发现无路可走就回到上一个三岔口选择另外的路。

回溯现象

// 性能问题正则
// 将下面代码粘贴到浏览器控制台运行试试
const regexp = `[A-Z]+\\d+(.*):(.*)+[A-Z]+\\d+`;
const str = `A1:B$1,C$1:D$1,E$1:F$1,G$1:H$1`
const reg = new RegExp(regexp);
start = Date.now();
const res = reg.test(str);
end = Date.now();
console.log('常规正则执行耗时:' + (end - start))

现在来看看回溯究竟是怎么一回事

假设我们有一段正则 (.*)+\d ,这个时候输入字符串为 abcd ,注意这个时候仅仅输入了一个长度为4的字符串,我们来分析一下匹配回溯的过程:

上面展示了一个回溯的匹配过程,大概描述一下前三轮匹配。

注意 (.*)+ 这里可以先暂且看成多次执行 .* (.*){1,}

第一次匹配,因为 .* 可以匹配任意个字符任意次,那么这里可以选择匹配空、a、ab、abc、abcd,因为 * 的贪婪特性,所以 .* 直接匹配了 abcd 4个字符, + 因为后面没有其他字符了,所以只看着 .* 吃掉 abcd 后就不匹配了,这里记录 + 的值为1,然后 \d 没有东西能够匹配,所以匹配失败,进行第一次回溯。

第二次匹配,因为进行了回溯,所以回到上一个匹配路径选择的时候,上次 .* 匹配的是 abcd ,并且路不通,那么这次只能尝试匹配 abc ,这个时候末尾还有一个 d ,那么可以理解为 .* 第一次匹配了 abc ,然后因为 (.*)+ 的原因, .* 可以进行第二次匹配,这里 .* 可以匹配 d ,这里记录 + 的值为2,然后 \d 没有东西能够匹配,所以匹配失败,进行第二次回溯。

第三次匹配,因为进行了回溯,所以回到上一个匹配路径选择的时候,上次第一个 .* 匹配的是 abc ,第二个 .* 匹配的是 d ,并且路不通,所以这里第二次的 .* 不进行匹配,这个时候末尾还有一个 d \d d 匹配失败,进行第三次回溯。

如何减少或避免回溯

  • 优化正则表达式:时刻注意回溯造成的性能影响。
  • 使用DFA正则引擎的正则表达式

什么是DFA正则引擎

传统正则引擎分为NFA(非确定性有限状态自动机),和DFA(确定性有限状态自动机)。

DFA

对于给定的任意一个状态和输入字符,DFA只会转移到一个确定的状态。并且DFA不允许出现没有输入字符的状态转移。

比如状态0,在输入字符A的时候,终点只有1个,只能到状态1。

NFA

对于任意一个状态和输入字符,NFA所能转移的状态是一个非空集合。

比如状态0,在输入字符A的时候,终点可以是多个,即能到状态1,也能到状态0。

DFA和NFA的正则引擎的区别

那么讲了这么多之后,DFA和NFA正则引擎究竟有什么区别呢?或者说DFA和NFA是如何实现正则引擎的呢?

DFA

正则里面的DFA引擎实际上就是把正则表达式转换成一个图的邻接表,然后通过跳表的形式判断一个字符串是否匹配该正则。

// 大概模拟一下
function machine(input) {
    if (typeof input !== 'string') {
        console.log('输入有误');
        return;
    }
    // 比如正则:/abc/ 转换成DFA之后
    // 这里我们定义了4种状态,分别是0,1,2,3,初始状态为0
    const reg = {
        0: {
            a: 1,
        },
        1: {
            b: 3,
        },
        2: {
            isEnd: true,
        },
        3: {
            c: 2,
        },
    };
    let status = 0;
    for (let i = 0; i < input.length; i++) {
        const inputChar = input[i];
        status = reg[status][inputChar];
        if (typeof status === 'undefined') {
            console.log('匹配失败');
            return false;
        }
    }
    const end = reg[status];
    if (end && end.isEnd === true) {
        console.log('匹配成功');
        return true;
    } else {
        console.log('匹配失败');
        return false;
    }
}

const input = 'abc';
machine(input);

优点:不管正则表达式写的再烂,匹配速度都很快

缺点:高级功能比如捕获组和断言都不支持

NFA

正则里面NFA引擎实际上就是在语法解析的时候,构造出的一个有向图。然后通过深搜的方式,去一条路径一条路径的递归尝试。

优点:功能强大,可以拿到匹配的上下文信息,支持各种断言捕获组环视之类的功能

缺点:对开发正则功底要求较高,需要注意回溯造成的性能问题

总结

现在回到问题的开头,我们再来看看为什么他的正则会有性能问题

  1. 首先他的正则使用的NFA的正则引擎(大部分语言的正则引擎都是NFA的,js也是,所以要注意性能问题产生的影响)
  2. 他写出了有性能问题的正则表达式,容易造成灾难性回溯。

如果要避免此类的问题,要么提高开发对正则的性能问题的意识,要么改用DFA的正则引擎(速度快,功能弱,没有捕获组断言等功能)。

注意事项

在平常写正则的时候,少写模糊匹配,越精确越好,模糊匹配、贪婪匹配、惰性匹配都会带来回溯问题,选一个影响尽可能小的方式就好。写正则的时候有一个性能问题的概念在脑子里就行。

tips:之前我用js写了一个dfa的正则引擎,感兴趣的同学可以看看:
https://github.com/libertyzha...


AlloyTeam 欢迎优秀的小伙伴加入。
简历投递: alloyteam@qq.com
详情可点击 腾讯AlloyTeam招募Web前端工程师(社招)

clipboard.png

查看原文

赞 84 收藏 54 评论 1

渣正refn 赞了回答 · 2019-05-21

能否从浏览器获取地区格式的时区?

Date对象的getTimezoneOffset方法

返回的是本地时间与 GMT 时间或 UTC 时间之间相差的分钟数。实际上,该函数告诉我们运行 JavaScript 代码的时区,以及指定的时间是否是夏令时。

有的时区相差不到一小时,所以单位是分钟。

为了便于理解,补充一个例子

console.log(new Date().getTimezoneOffset());
// => -480

480分钟为8小时,负值是东区,所以中国是东八区。

关注 0 回答 1

渣正refn 赞了文章 · 2019-02-25

React Router 中文文档(一)

官方英文文档 - https://reacttraining.com/rea...
版本 - v4.2.0

<BrowserRouter>

<BrowserRouter> 使用 HTML5 提供的 history API (pushState, replaceStatepopstate 事件) 来保持 UI 和 URL 的同步。

import { BrowserRouter } from 'react-router-dom';

<BrowserRouter
  basename={string}
  forceRefresh={bool}
  getUserConfirmation={func}
  keyLength={number}
>
  <App />
</BrowserRouter>

basename: string

所有位置的基准 URL。如果你的应用程序部署在服务器的子目录,则需要将其设置为子目录。basename 的正确格式是前面有一个前导斜杠,但不能有尾部斜杠。

<BrowserRouter basename="/calendar">
  <Link to="/today" />
</BrowserRouter>

上例中的 <Link> 最终将被呈现为:

<a href="/calendar/today" />

forceRefresh: bool

如果为 true ,在导航的过程中整个页面将会刷新。一般情况下,只有在不支持 HTML5 history API 的浏览器中使用此功能。

const supportsHistory = 'pushState' in window.history;

<BrowserRouter forceRefresh={!supportsHistory} />

getUserConfirmation: func

用于确认导航的函数,默认使用 window.confirm。例如,当从 /a 导航至 /b 时,会使用默认的 confirm 函数弹出一个提示,用户点击确定后才进行导航,否则不做任何处理。译注:需要配合 <Prompt> 一起使用。

// 这是默认的确认函数
const getConfirmation = (message, callback) => {
  const allowTransition = window.confirm(message);
  callback(allowTransition);
}

<BrowserRouter getUserConfirmation={getConfirmation} />

keyLength: number

location.key 的长度,默认为 6

<BrowserRouter keyLength={12} />

children: node

要呈现的单个子元素(组件)

<HashRouter>

<HashRouter> 使用 URL 的 hash 部分(即 window.location.hash)来保持 UI 和 URL 的同步。

import { HashRouter } from 'react-router-dom';

<HashRouter>
  <App />
</HashRouter>
注意: 使用 hash 记录导航历史不支持 location.keylocation.state。在以前的版本中,我们视图 shim 这种行为,但是仍有一些问题我们无法解决。任何依赖此行为的代码或插件都将无法正常使用。由于该技术仅用于支持旧式(低版本)浏览器,因此对于一些新式浏览器,我们鼓励你使用 <BrowserHistory> 代替。

basename: string

所有位置的基准 URL。basename 的正确格式是前面有一个前导斜杠,但不能有尾部斜杠。

<HashRouter basename="/calendar">
  <Link to="/today" />
</HashRouter>

上例中的 <Link> 最终将被呈现为:

<a href="#/calendar/today" />

getUserConfirmation: func

用于确认导航的函数,默认使用 window.confirm

// 这是默认的确认函数
const getConfirmation = (message, callback) => {
  const allowTransition = window.confirm(message);
  callback(allowTransition);
}

<HashRouter getUserConfirmation={getConfirmation} />

hashType: string

window.location.hash 使用的 hash 类型,有如下几种:

  • slash - 后面跟一个斜杠,例如 #/ 和 #/sunshine/lollipops
  • noslash - 后面没有斜杠,例如 # 和 #sunshine/lollipops
  • hashbang - Google 风格的 ajax crawlable,例如 #!/ 和 #!/sunshine/lollipops

默认为 slash

children: node

要呈现的单个子元素(组件)

<Link>

为你的应用提供声明式的、可访问的导航链接。

import { Link } from 'react-router-dom';

<Link to="/about">About</Link>

to: string

一个字符串形式的链接地址,通过 pathnamesearchhash 属性创建。

<Link to='/courses?sort=name' />

to: object

一个对象形式的链接地址,可以具有以下任何属性:

  • pathname - 要链接到的路径
  • search - 查询参数
  • hash - URL 中的 hash,例如 #the-hash
  • state - 存储到 location 中的额外状态数据
<Link to={{
  pathname: '/courses',
  search: '?sort=name',
  hash: '#the-hash',
  state: {
    fromDashboard: true
  }
}} />

replace: bool

当设置为 true 时,点击链接后将替换历史堆栈中的当前条目,而不是添加新条目。默认为 false

<Link to="/courses" replace />

innerRef: func

允许访问组件的底层引用。

const refCallback = node => {
  // node 指向最终挂载的 DOM 元素,在卸载时为 null
}

<Link to="/" innerRef={refCallback} />

others

你还可以传递一些其它属性,例如 titleidclassName 等。

<Link to="/" className="nav" title="a title">About</Link>

<NavLink>

一个特殊版本的 <Link>,它会在与当前 URL 匹配时为其呈现元素添加样式属性。

import { NavLink } from 'react-router-dom';

<NavLink to="/about">About</NavLink>

activeClassName: string

当元素处于激活状态时应用的类,默认为 active。它将与 className 属性一起使用。

<NavLink to="/faq" activeClassName="selected">FAQs</NavLink>

activeStyle: object

当元素处于激活状态时应用的样式。

const activeStyle = {
  fontWeight: 'bold',
  color: 'red'
};

<NavLink to="/faq" activeStyle={activeStyle}>FAQs</NavLink>

exact: bool

如果为 true,则只有在位置完全匹配时才应用激活类/样式。

<NavLink exact to="/profile">Profile</NavLink>

strict: bool

如果为 true,则在确定位置是否与当前 URL 匹配时,将考虑位置的路径名后面的斜杠。有关更多信息,请参阅 <Route strict> 文档。

<NavLink strict to="/events/">Events</NavLink>

isActive: func

添加额外逻辑以确定链接是否处于激活状态的函数。如果你要做的不仅仅是验证链接的路径名与当前 URL 的路径名相匹配,那么应该使用它。

// 只有当事件 id 为奇数时才考虑激活
const oddEvent = (match, location) => {
  if (!match) {
    return false;
  }
  const eventID = parseInt(match.params.eventID);
  return !isNaN(eventID) && eventID % 2 === 1;
}

<NavLink to="/events/123" isActive={oddEvent}>Event 123</NavLink>

location: object

isActive 默认比较当前历史位置(通常是当前的浏览器 URL)。你也可以传递一个不同的位置进行比较。

<Prompt>

用于在位置跳转之前给予用户一些确认信息。当你的应用程序进入一个应该阻止用户导航的状态时(比如表单只填写了一半),弹出一个提示。

import { Prompt } from 'react-router-dom';

<Prompt
  when={formIsHalfFilledOut}
  message="你确定要离开当前页面吗?"
/>

message: string

当用户试图离开某个位置时弹出的提示信息。

<Prompt message="你确定要离开当前页面吗?" />

message: func

将在用户试图导航到下一个位置时调用。需要返回一个字符串以向用户显示提示,或者返回 true 以允许直接跳转。

<Prompt message={location => {
  const isApp = location.pathname.startsWith('/app');

  return isApp ? `你确定要跳转到${location.pathname}吗?` : true;
}} />
译注:上例中的 location 对象指的是下一个位置(即用户想要跳转到的位置)。你可以基于它包含的一些信息,判断是否阻止导航,或者允许直接跳转。

when: bool

在应用程序中,你可以始终渲染 <Prompt> 组件,并通过设置 when={true}when={false} 以阻止或允许相应的导航,而不是根据某些条件来决定是否渲染 <Prompt> 组件。

译注:when 只有两种情况,当它的值为 true 时,会弹出提示信息。如果为 false 则不会弹出。见阻止导航示例。
<Prompt when={true} message="你确定要离开当前页面吗?" />

<MemoryRouter>

将 URL 的历史记录保存在内存中的 <Router>(不读取或写入地址栏)。在测试和非浏览器环境中很有用,例如 React Native

import { MemoryRouter } from 'react-router-dom';

<MemoryRouter>
  <App />
</MemoryRouter>

initialEntries: array

历史堆栈中的一系列位置信息。这些可能是带有 {pathname, search, hash, state} 的完整位置对象或简单的字符串 URL。

<MemoryRouter
  initialEntries={[ '/one', '/two', { pathname: '/three' } ]}
  initialIndex={1}
>
  <App/>
</MemoryRouter>

initialIndex: number

initialEntries 数组中的初始位置索引。

getUserConfirmation: func

用于确认导航的函数。当 <MemoryRouter> 直接与 <Prompt> 一起使用时,你必须使用此选项。

keyLength: number

location.key 的长度,默认为 6

children: node

要呈现的单个子元素(组件)

<Redirect>

使用 <Redirect> 会导航到一个新的位置。新的位置将覆盖历史堆栈中的当前条目,例如服务器端重定向(HTTP 3xx)。

import { Route, Redirect } from 'react-router-dom';

<Route exact path="/" render={() => (
  loggedIn ? (
    <Redirect to="/dashboard" />
  ) : (
    <PublicHomePage />
  )
)} />

to: string

要重定向到的 URL,可以是 path-to-regexp 能够理解的任何有效的 URL 路径。所有要使用的 URL 参数必须由 from 提供。

<Redirect to="/somewhere/else" />

to: object

要重定向到的位置,其中 pathname 可以是 path-to-regexp 能够理解的任何有效的 URL 路径。

<Redirect to={{
  pathname: '/login',
  search: '?utm=your+face',
  state: {
    referrer: currentLocation
  }
}} />

上例中的 state 对象可以在重定向到的组件中通过 this.props.location.state 进行访问。而 referrer 键(不是特殊名称)将通过路径名 /login 指向的登录组件中的 this.props.location.state.referrer 进行访问。

push: bool

如果为 true,重定向会将新的位置推入历史记录,而不是替换当前条目。

<Redirect push to="/somewhere/else" />

from: string

要从中进行重定向的路径名,可以是 path-to-regexp 能够理解的任何有效的 URL 路径。所有匹配的 URL 参数都会提供给 to,必须包含在 to 中用到的所有参数,to 未使用的其它参数将被忽略。

只能在 <Switch> 组件内使用 <Redirect from>,以匹配一个位置。有关更多细节,请参阅 <Switch children>

<Switch>
  <Redirect from='/old-path' to='/new-path' />
  <Route path='/new-path' component={Place} />
</Switch>
// 根据匹配参数进行重定向
<Switch>
  <Redirect from='/users/:id' to='/users/profile/:id' />
  <Route path='/users/profile/:id' component={Profile} />
</Switch>
译注:经过实践,发现以上“根据匹配参数进行重定向”的示例存在bug,没有效果。to 中的 :id 并不会继承 from 中的 :id 匹配的值,而是直接作为字符串显示到浏览器地址栏!!!

exact: bool

完全匹配,相当于 Route.exact

strict: bool

严格匹配,相当于 Route.strict

<Route>

<Route> 可能是 React Router 中最重要的组件,它可以帮助你理解和学习如何更好的使用 React Router。它最基本的职责是在其 path 属性与某个 location 匹配时呈现一些 UI。

请考虑以下代码:

import { BrowserRouter as Router, Route } from 'react-router-dom';

<Router>
  <div>
    <Route exact path="/" component={Home} />
    <Route path="/news" component={News} />
  </div>
</Router>

如果应用程序的位置是 /,那么 UI 的层次结构将会是:

<div>
  <Home />
  <!-- react-empty: 2 -->
</div>

或者,如果应用程序的位置是 /news,那么 UI 的层次结构将会是:

<div>
  <!-- react-empty: 1 -->
  <News />
</div>

其中 react-empty 注释只是 React 空渲染的实现细节。但对于我们的目的而言,它是有启发性的。路由始终在技术上被“渲染”,即使它的渲染为空。只要应用程序的位置匹配 <Route>path,你的组件就会被渲染。

Route render methods

使用 <Route> 渲染一些内容有以下三种方式:

在不同的情况下使用不同的方式。在指定的 <Route> 中,你应该只使用其中的一种。请参阅下面的解释,了解为什么有三个选项。大多数情况下你会使用 component

Route props

三种渲染方式都将提供相同的三个路由属性:

component

指定只有当位置匹配时才会渲染的 React 组件,该组件会接收 route props 作为属性。

const User = ({ match }) => {
  return <h1>Hello {match.params.username}!</h1>
}

<Route path="/user/:username" component={User} />

当你使用 component(而不是 renderchildren)时,Router 将根据指定的组件,使用 React.createElement 创建一个新的 React 元素。这意味着,如果你向 component 提供一个内联函数,那么每次渲染都会创建一个新组件。这将导致现有组件的卸载和新组件的安装,而不是仅仅更新现有组件。当使用内联函数进行内联渲染时,请使用 renderchildren(见下文)。

render: func

使用 render 可以方便地进行内联渲染和包装,而无需进行上文解释的不必要的组件重装。

你可以传入一个函数,以在位置匹配时调用,而不是使用 component 创建一个新的 React 元素。render 渲染方式接收所有与 component 方式相同的 route props

// 方便的内联渲染
<Route path="/home" render={() => <div>Home</div>} />

// 包装
const FadingRoute = ({ component: Component, ...rest }) => (
  <Route {...rest} render={props => (
    <FadeIn>
      <Component {...props} />
    </FadeIn>
  )} />
)

<FadingRoute path="/cool" component={Something} />
警告:<Route component> 优先于 <Route render>,因此不要在同一个 <Route> 中同时使用两者。

children: func

有时候不论 path 是否匹配位置,你都想渲染一些内容。在这种情况下,你可以使用 children 属性。除了不论是否匹配它都会被调用以外,它的工作原理与 render 完全一样。

children 渲染方式接收所有与 componentrender 方式相同的 route props,除非路由与 URL 不匹配,不匹配时 matchnull。这允许你可以根据路由是否匹配动态地调整用户界面。如下所示,如果路由匹配,我们将添加一个激活类:

const ListItemLink = ({ to, ...rest }) => (
  <Route path={to} children={({ match }) => (
    <li className={match ? 'active' : ''}>
      <Link to={to} {...rest} />
    </li>
  )} />
)

<ul>
  <ListItemLink to="/somewhere" />
  <ListItemLink to="/somewhere-else" />
</ul>

这对动画也很有用:

<Route children={({ match, ...rest }) => (
  {/* Animate 将始终渲染,因此你可以利用生命周期来为其子元素添加进出动画 */}
  <Animate>
    {match && <Something {...rest} />}
  </Animate>
)} />
警告:<Route component><Route render> 优先于 <Route children>,因此不要在同一个 <Route> 中同时使用多个。

path: string

可以是 path-to-regexp 能够理解的任何有效的 URL 路径。

<Route path="/users/:id" component={User} />

没有定义 path<Route> 总是会被匹配。

exact: bool

如果为 true,则只有在 path 完全匹配 location.pathname 时才匹配。

<Route exact path="/one" component={OneComponent} />

图片描述

strict: bool

如果为 true,则具有尾部斜杠的 path 仅与具有尾部斜杠的 location.pathname 匹配。当 location.pathname 中有附加的 URL 片段时,strict 就没有效果了。

<Route strict path="/one/" component={OneComponent} />

图片描述

警告:可以使用 strict 来强制规定 location.pathname 不能具有尾部斜杠,但是为了做到这一点,strictexact 必须都是 true

图片描述

location: object

一般情况下,<Route> 尝试将其 path 与当前历史位置(通常是当前的浏览器 URL)进行匹配。但是,也可以传递具有不同路径名的位置进行匹配。

当你需要将 <Route> 与当前历史位置以外的 location 进行匹配时,此功能非常有用。如过渡动画示例中所示。

如果一个 <Route> 被包含在一个 <Switch> 中,并且需要匹配的位置(或当前历史位置)传递给了 <Switch>,那么传递给 <Route>location 将被 <Switch> 所使用的 location 覆盖。

sensitive: bool

如果为 true,进行匹配时将区分大小写。

<Route sensitive path="/one" component={OneComponent} />

图片描述

<Router>

所有 Router 组件的通用低阶接口。通常情况下,应用程序只会使用其中一个高阶 Router:

使用低阶 <Router> 的最常见用例是同步一个自定义历史记录与一个状态管理库,比如 Redux 或 Mobx。请注意,将 React Router 和状态管理库一起使用并不是必需的,它仅用于深度集成。

import { Router } from 'react-router-dom';
import createBrowserHistory from 'history/createBrowserHistory';

const history = createBrowserHistory();

<Router history={history}>
  <App />
</Router>

history: object

用于导航的历史记录对象。

import createBrowserHistory from 'history/createBrowserHistory';

const customHistory = createBrowserHistory();

<Router history={customHistory} />

children: node

要呈现的单个子元素(组件)

<Router>
  <App />
</Router>

<StaticRouter>

一个永远不会改变位置的 <Router>

这在服务器端渲染场景中非常有用,因为用户实际上没有点击,所以位置实际上并未发生变化。因此,名称是 static(静态的)。当你只需要插入一个位置,并在渲染输出上作出断言以便进行简单测试时,它也很有用。

以下是一个示例,node server 为 <Redirect> 发送 302 状态码,并为其它请求发送常规 HTML:

import { createServer } from 'http';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router';

createServer((req, res) => {
  // 这个 context 对象包含了渲染的结果
  const context = {};

  const html = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );

  // 如果使用 <Redirect>,context.url 将包含要重定向到的 URL
  if (context.url) {
    res.writeHead(302, {
      Location: context.url
    });
    res.end();
  } else {
    res.write(html);
    res.end();
  }
}).listen(3000);

basename: string

所有位置的基准 URL。basename 的正确格式是前面有一个前导斜杠,但不能有尾部斜杠。

<StaticRouter basename="/calendar">
  <Link to="/today" />
</StaticRouter>

上例中的 <Link> 最终将被呈现为:

<a href="/calendar/today" />

location: string

服务器收到的 URL,可能是 node server 上的 req.url

<StaticRouter location={req.url}>
  <App />
</StaticRouter>

location: object

一个形如 {pathname, search, hash, state} 的位置对象。

<StaticRouter location={{ pathname: '/bubblegum' }}>
  <App />
</StaticRouter>

context: object

一个普通的 JavaScript 对象。在渲染过程中,组件可以向对象添加属性以存储有关渲染的信息。

const context = {};

<StaticRouter context={context}>
  <App />
</StaticRouter>

当一个 <Route> 匹配时,它将把 context 对象传递给呈现为 staticContext 的组件。查看服务器渲染指南以获取有关如何自行完成此操作的更多信息。

渲染之后,可以使用这些属性来配置服务器的响应。

if (context.status === '404') {
  // ...
}

children: node

要呈现的单个子元素(组件)

<Switch>

用于渲染与路径匹配的第一个子 <Route><Redirect>

这与仅仅使用一系列 <Route> 有何不同?

<Switch> 只会渲染一个路由。相反,仅仅定义一系列 <Route> 时,每一个与路径匹配的 <Route> 都将包含在渲染范围内。考虑如下代码:

<Route path="/about" component={About} />
<Route path="/:user" component={User} />
<Route component={NoMatch} />

如果 URL 是 /about,那么 <About><User><NoMatch> 将全部渲染,因为它们都与路径匹配。这是通过设计,允许我们以很多方式将 <Route> 组合成我们的应用程序,例如侧边栏和面包屑、引导标签等。

但是,有时候我们只想选择一个 <Route> 来呈现。比如我们在 URL 为 /about 时不想匹配 /:user(或者显示我们的 404 页面),这该怎么实现呢?以下就是如何使用 <Switch> 做到这一点:

import { Switch, Route } from 'react-router';

<Switch>
  <Route exact path="/" component={Home} />
  <Route path="/about" component={About} />
  <Route path="/:user" component={User} />
  <Route component={NoMatch} />
</Switch>

现在,当我们在 /about 路径时,<Switch> 将开始寻找匹配的 <Route>。我们知道,<Route path="/about" /> 将会被正确匹配,这时 <Switch> 会停止查找匹配项并立即呈现 <About>。同样,如果我们在 /michael 路径时,那么 <User> 会呈现。

这对于动画转换也很有用,因为匹配的 <Route> 与前一个渲染位置相同。

<Fade>
  <Switch>
    {/* 这里只会渲染一个子元素 */}
    <Route />
    <Route />
  </Switch>
</Fade>

<Fade>
  <Route />
  <Route />
  {/* 这里总是会渲染两个子元素,也有可能是空渲染,这使得转换更加麻烦 */}
</Fade>

location: object

用于匹配子元素而不是当前历史位置(通常是当前的浏览器 URL)的 location 对象。

children: node

所有 <Switch> 的子元素都应该是 <Route><Redirect>。只有第一个匹配当前路径的子元素将被呈现。

<Route> 组件使用 path 属性进行匹配,而 <Redirect> 组件使用它们的 from 属性进行匹配。没有 path 属性的 <Route> 或者没有 from 属性的 <Redirect> 将始终与当前路径匹配。

当在 <Switch> 中包含 <Redirect> 时,你可以使用任何 <Route> 拥有的路径匹配属性:pathexactstrictfrom 只是 path 的别名。

如果给 <Switch> 提供一个 location 属性,它将覆盖匹配的子元素上的 location 属性。

<Switch>
  <Route exact path="/" component={Home} />
  <Route path="/users" component={Users} />
  <Redirect from="/accounts" to="/users" />
  <Route component={NoMatch} />
</Switch>
查看原文

赞 164 收藏 137 评论 6

渣正refn 赞了文章 · 2019-02-21

React进阶——使用高阶组件(Higher-order Components)优化你的代码

什么是高阶组件

Higher-Order Components (HOCs) are JavaScript functions which add functionality to existing component classes.

通过函数向现有组件类添加逻辑,就是高阶组件。

让我们先来看一个可能是史上最无聊的高阶组件:

function noId() {
  return function(Comp) {
    return class NoID extends Component {
      render() {
        const {id, ...others} = this.props;
        return (
          <Comp {...others}/>
        )
      }
    }
  }
}

const WithoutID = noId()(Comp);

这个例子向我们展示了高阶组件的工作方式:通过函数和闭包,改变已有组件的行为——这里是忽略id属性——而完全不需要修改任何代码。

之所以称之为高阶,是因为在React中,这种嵌套关系会反映到组件树上,层层嵌套就好像高阶函数的function in function一样,如图:

HOC-img

从图上也可以看出,组件树虽然嵌套了多层,但是实际渲染的DOM结构并没有改变。
如果你对这点有疑问,不妨自己写写例子试下,加深对React的理解。现在可以先记下结论:我们可以放心的使用多层高阶组件,甚至重复地调用,而不必担心影响输出的DOM结构。

借助函数的逻辑表现力,高阶组件的用途几乎是无穷无尽的:

适配器

有的时候你需要替换一些已有组件,而新组件接收的参数和原组件并不完全一致。

你可以修改所有使用旧组件的代码来保证传入正确的参数——考虑改行吧如果你真这么想

也可以把新组件做一层封装:

class ListAdapter extends Component {
    mapProps(props) {
        return {/* new props */}
    }
    render() {
        return <NewList {...mapProps(this.props)} />
    }
}

如果有十个组件需要适配呢?如果你不想照着上面写十遍,或许高阶组件可以给你答案

function mapProps(mapFn) {
    return function(Comp) {
        return class extends Component {
            render() {
                return <Comp {...mapFn(this.props)}/>
            }
        }
    } 
}

const ListAdapter = mapProps(mapPropsForNewList)(NewList);

借助高阶组件,关注点被分离得更加干净:只需要关注真正重要的部分——属性的mapping。

这个例子有些价值,却仍然不够打动人,如果你也这么想,请往下看:

处理副作用

纯组件易写易测,越多越好,这是常识。然而在实际项目中,往往有许多的状态和副作用需要处理,最常见的情况就是异步了。

假设我们需要异步加载一个用户列表,通常的代码可能是这样的:

class UserList extends Component {
  constructor(props) {
    super();
    this.state = {
      list: []
    }
  }
  componentDidMount() {
    loadUsers()
      .then(data=> 
        this.setState({list: data.userList})
      )
  }
  render() {
    return (
      <List list={this.state.list} />
    )
  }
  /* other bussiness logics */
}

实际情况中,以上代码往往还会和其它一些业务函数混杂在一起——我们创建了一个业务副作用混杂的、有状态的组件。

如果再来一个书单列表呢?再写一个BookList然后把loadUsers改成loadBooks ?
不仅代码重复,大量有状态和副作用的组件,也使得应用更加难以测试。

也许你会考虑使用Flux。它确实能让你的代码更清晰,但是在有些场景下使用Flux就像大炮打蚊子。比如一个异步的下拉选择框,如果要考虑复用的话,传统的Flux/Reflux几乎无法优雅的处理,Redux稍好一些,但仍然很难做优雅。关于flux/redux的缺点不深入,有兴趣的可以参考Cycle.js作者的文章

回到问题的本源:其实我们只想要一个能复用的异步下拉列表而已啊!

高阶函数试试?

import React, { Component } from 'react';

const DEFAULT_OPTIONS = {
  mapStateToProps: undefined,
  mapLoadingToProps: loading => ({ loading }),
  mapDataToProps: data => ({ data }),
  mapErrorToProps: error => ({ error }),
};
export function connectPromise(options) {
  return (Comp) => {
    const finalOptions = {
      ...DEFAULT_OPTIONS,
      ...options,
    };
    const {
      promiseLoader,
      mapLoadingToProps,
      mapStateToProps,
      mapDataToProps,
      mapErrorToProps,
    } = finalOptions;

    class AsyncComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          loading: true,
          data: undefined,
          error: undefined,
        };
      }
      componentDidMount() {
        promiseLoader(this.props)
          .then(
            data => this.setState({ data, loading: false }),
            error => this.setState({ error, loading: false }),
          );
      }
      render() {
        const { data, error, loading } = this.state;

        const dataProps = data ? mapDataToProps(data) : undefined;
        const errorProps = error ? mapErrorToProps(error) : undefined;

        return (
          <Comp
            {...mapLoadingToProps(loading)}
            {...dataProps}
            {...errorProps}
            {...this.props}
          />
        );
      }
    }

    return AsyncComponent;
  };
}


const UserList = connectPromise({
    promiseLoader: loadUsers,
    mapDataToProps: result=> ({list: result.userList})
})(List); //List can be a pure component

const BookList = connectPromise({
    promiseLoader: loadBooks,
    mapDataToProps: result=> ({list: result.bookList})
})(List);

不仅大大减少了重复代码,还把散落各处的异步逻辑装进了可以单独管理和测试的笼子,在业务场景中,只需要纯组件 + 配置 就能实现相同的功能——而无论是纯组件还是配置,都是对单元测试友好的,至少比异步组件友好多了。

使用curry & compose

高阶组件的另一个亮点,就是对函数式编程的友好。你可能已经注意到,目前我写的所有高阶函数,都是形如:

config => {
    return Component=> {
        return HighOrderCompoent
    }
}

表示为config=> Component=> Component

写成嵌套的函数是为了手动curry化,而参数的顺序(为什么不是Component=> config=> Component),则是为了组合方便。关于curry与compose的使用,可以移步我的另一篇blog

举个栗子,前面讲了适配器和异步,我们可以很快就组合出两者的结合体:使用NewList的异步用户列表

UserList = compose(
  connectPromise({
    promiseLoader: loadUsers,
    mapResultToProps: result=> ({list: result.userList})
  }),
  mapProps(mapPropsForNewList)
)(NewList);

总结

在团队内部分享里,我的总结是三个词 Easy, Light-weight & Composable.

其实高阶组件并不是什么新东西,本质上就是Decorator模式在React的一种实现,但在相当一段时间内,这个优秀的模式都被人忽略。在我看来,大部分使用mixinclass extends的地方,高阶组件都是更好的方案——毕竟组合优于继承,而mixin——个人觉得没资格参与讨论。

使用高阶组件还有两个好处:

  1. 适用范围广,它不需要es6或者其它需要编译的特性,有函数的地方,就有HOC。

  2. Debug友好,它能够被React组件树显示,所以可以很清楚地知道有多少层,每层做了什么。相比之下无论是mixin还是继承,都显得非常隐晦。

值得庆幸的是,社区也明显注意到了高阶组件的价值,无论是大家非常熟悉的react-reduxconnect函数,还是redux-form,高阶组件的应用开始随处可见。

下次当你想写mixinclass extends的时候,不妨也考虑下高阶组件。

查看原文

赞 29 收藏 88 评论 18

渣正refn 赞了回答 · 2019-01-30

解决flex设置成1和auto有什么区别

这是一句和回答无关的话:我是来谢赞的 :)

以下个人测试,如有纰漏错误恳请指正:

首先明确一点是, flexflex-growflex-shrinkflex-basis的缩写。故其取值可以考虑以下情况:

flex 的默认值是以上三个属性值的组合。假设以上三个属性同样取默认值,则 flex 的默认值是 0 1 auto。同理,如下是等同的:

.item {flex: 2333 3222 234px;}
.item {
    flex-grow: 2333;
    flex-shrink: 3222;
    flex-basis: 234px;
}

flex 取值为 none,则计算值为 0 0 auto,如下是等同的:

.item {flex: none;}
.item {
    flex-grow: 0;
    flex-shrink: 0;
    flex-basis: auto;
}

flex 取值为 auto,则计算值为 1 1 auto,如下是等同的:

.item {flex: auto;}
.item {
    flex-grow: 1;
    flex-shrink: 1;
    flex-basis: auto;
}

flex 取值为一个非负数字,则该数字为 flex-grow 值,flex-shrink 取 1,flex-basis 取 0%,如下是等同的:

.item {flex: 1;}
.item {
    flex-grow: 1;
    flex-shrink: 1;
    flex-basis: 0%;
}

flex 取值为一个长度或百分比,则视为 flex-basis 值,flex-grow 取 1,flex-shrink 取 1,有如下等同情况(注意 0% 是一个百分比而不是一个非负数字):

.item-1 {flex: 0%;}
.item-1 {
    flex-grow: 1;
    flex-shrink: 1;
    flex-basis: 0%;
}
.item-2 {flex: 24px;}
.item-1 {
    flex-grow: 1;
    flex-shrink: 1;
    flex-basis: 24px;
}

flex 取值为两个非负数字,则分别视为 flex-growflex-shrink 的值,flex-basis 取 0%,如下是等同的:

.item {flex: 2 3;}
.item {
    flex-grow: 2;
    flex-shrink: 3;
    flex-basis: 0%;
}

flex 取值为一个非负数字和一个长度或百分比,则分别视为 flex-growflex-basis 的值,flex-shrink 取 1,如下是等同的:

.item {flex: 2333 3222px;}
.item {
    flex-grow: 2333;
    flex-shrink: 1;
    flex-basis: 3222px;
}

flex-basis 规定的是子元素的基准值。所以是否溢出的计算与此属性息息相关。flex-basis 规定的范围取决于 box-sizing。这里主要讨论以下 flex-basis 的取值情况:

  • auto:首先检索该子元素的主尺寸,如果主尺寸不为 auto,则使用值采取主尺寸之值;如果也是 auto,则使用值为 content

  • content:指根据该子元素的内容自动布局。有的用户代理没有实现取 content 值,等效的替代方案是 flex-basis 和主尺寸都取 auto

  • 百分比:根据其包含块(即伸缩父容器)的主尺寸计算。如果包含块的主尺寸未定义(即父容器的主尺寸取决于子元素),则计算结果和设为 auto 一样。

举一个不同的值之间的区别:

<div class="parent">
    <div class="item-1"></div>
    <div class="item-2"></div>
    <div class="item-3"></div>
</div>

<style type="text/css">
    .parent {
        display: flex;
        width: 600px;
    }
    .parent > div {
        height: 100px;
    }
    .item-1 {
        width: 140px;
        flex: 2 1 0%;
        background: blue;
    }
    .item-2 {
        width: 100px;
        flex: 2 1 auto;
        background: darkblue;
    }
    .item-3 {
        flex: 1 1 200px;
        background: lightblue;
    }
</style>
  • 主轴上父容器总尺寸为 600px

  • 子元素的总基准值是:0% + auto + 200px = 300px,其中

    - 0% 即 0 宽度
    - auto 对应取主尺寸即 100px
  • 故剩余空间为 600px - 300px = 300px

  • 伸缩放大系数之和为: 2 + 2 + 1 = 5

  • 剩余空间分配如下:

    - item-1 和 item-2 各分配 2/5,各得 120px
    - item-3 分配 1/5,得 60px
  • 各项目最终宽度为:

    - item-1 = 0% + 120px = 120px
    - item-2 = auto + 120px = 220px
    - item-3 = 200px + 60px = 260px
  • 当 item-1 基准值取 0% 的时候,是把该项目视为零尺寸的,故即便声明其尺寸为 140px,也并没有什么用,形同虚设

  • 而 item-2 基准值取 auto 的时候,根据规则基准值使用值是主尺寸值即 100px,故这 100px 不会纳入剩余空间

更多资料可以参考:

http://www.w3.org/html/ig/zh/css-flex-1/

关注 21 回答 2

渣正refn 赞了文章 · 2019-01-30

打包工具的配置教程见的多了,但它们的运行原理你知道吗?

clipboard.png

前端模块化成为了主流的今天,离不开各种打包工具的贡献。社区里面对于webpack,rollup以及后起之秀parcel的介绍层出不穷,对于它们各自的使用配置分析也是汗牛充栋。为了避免成为一位“配置工程师”,我们需要来了解一下打包工具的运行原理,只有把核心原理搞明白了,在工具的使用上才能更加得心应手。

本文基于parcel核心开发者@ronami的开源项目minipack而来,在其非常详尽的注释之上加入更多的理解和说明,方便读者更好地理解。

1、打包工具核心原理

顾名思义,打包工具就是负责把一些分散的小模块,按照一定的规则整合成一个大模块的工具。与此同时,打包工具也会处理好模块之间的依赖关系,最终这个大模块将可以被运行在合适的平台中。

打包工具会从一个入口文件开始,分析它里面的依赖,并且再进一步地分析依赖中的依赖,不断重复这个过程,直到把这些依赖关系理清挑明为止。

从上面的描述可以看到,打包工具最核心的部分,其实就是处理好模块之间的依赖关系,而minipack以及本文所要讨论的,也是集中在模块依赖关系的知识点当中。

为了简单起见,minipack项目直接使用ES modules规范,接下来我们新建三个文件,并且为它们之间建立依赖:

/* name.js */

export const name = 'World'
/* message.js */

import { name } from './name.js'

export default `Hello ${name}!`
/* entry.js */

import message from './message.js'

console.log(message)

它们的依赖关系非常简单:entry.jsmessage.jsname.js,其中entry.js将会成为打包工具的入口文件。

但是,这里面的依赖关系只是我们人类所理解的,如果要让机器也能够理解当中的依赖关系,就需要借助一定的手段了。

2、依赖关系解析

新建一个js文件,命名为minipack.js,首先引入必要的工具。

/* minipack.js */

const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')

接下来,我们会撰写一个函数,这个函数接收一个文件作为模块,然后读取它里面的内容,分析出其所有的依赖项。当然,我们可以通过正则匹配模块文件里面的import关键字,但这样做非常不优雅,所以我们可以使用babylon这个js解析器把文件内容转化成抽象语法树(AST),直接从AST里面获取我们需要的信息。

得到了AST之后,就可以使用babel-traverse去遍历这棵AST,获取当中关键的“依赖声明”,然后把这些依赖都保存在一个数组当中。

最后使用babel-coretransformFromAst方法搭配babel-preset-env插件,把ES6语法转化成浏览器可以识别的ES5语法,并且为该js模块分配一个ID。

let ID = 0

function createAsset (filename) {
  // 读取文件内容
  const content = fs.readFileSync(filename, 'utf-8')

  // 转化成AST
  const ast = babylon.parse(content, {
    sourceType: 'module',
  });

  // 该文件的所有依赖
  const dependencies = []

  // 获取依赖声明
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value);
    }
  })

  // 转化ES6语法到ES5
  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  })

  // 分配ID
  const id = ID++

  // 返回这个模块
  return {
    id,
    filename,
    dependencies,
    code,
  }
}

运行createAsset('./example/entry.js'),输出如下:

{ id: 0,
  filename: './example/entry.js',
  dependencies: [ './message.js' ],
  code: '"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);' }

可见entry.js文件已经变成了一个典型的模块,且依赖已经被分析出来了。接下来我们就要递归这个过程,把“依赖中的依赖”也都分析出来,也就是下一节要讨论的建立依赖关系图集。

3、建立依赖关系图集

新建一个名为createGragh()的函数,传入一个入口文件的路径作为参数,然后通过createAsset()解析这个文件使之定义成一个模块。

接下来,为了能够挨个挨个地对模块进行依赖分析,所以我们维护一个数组,首先把第一个模块传进去并进行分析。当这个模块被分析出还有其他依赖模块的时候,就把这些依赖模块也放进数组中,然后继续分析这些新加进去的模块,直到把所有的依赖以及“依赖中的依赖”都完全分析出来。

与此同时,我们有必要为模块新建一个mapping属性,用来储存模块、依赖、依赖ID之间的依赖关系,例如“ID为0的A模块依赖于ID为2的B模块和ID为3的C模块”就可以表示成下面这个样子:

{
  0: [function A () {}, { 'B.js': 2, 'C.js': 3 }]
}

搞清楚了个中道理,就可以开始编写函数了。

function createGragh (entry) {
  // 解析传入的文件为模块
  const mainAsset = createAsset(entry)
  
  // 维护一个数组,传入第一个模块
  const queue = [mainAsset]

  // 遍历数组,分析每一个模块是否还有其它依赖,若有则把依赖模块推进数组
  for (const asset of queue) {
    asset.mapping = {}
    // 由于依赖的路径是相对于当前模块,所以要把相对路径都处理为绝对路径
    const dirname = path.dirname(asset.filename)
    // 遍历当前模块的依赖项并继续分析
    asset.dependencies.forEach(relativePath => {
      // 构造绝对路径
      const absolutePath = path.join(dirname, relativePath)
      // 生成依赖模块
      const child = createAsset(absolutePath)
      // 把依赖关系写入模块的mapping当中
      asset.mapping[relativePath] = child.id
      // 把这个依赖模块也推入到queue数组中,以便继续对其进行以来分析
      queue.push(child)
    })
  }

  // 最后返回这个queue,也就是依赖关系图集
  return queue
}

可能有读者对其中的for...of ...循环当中的queue.push有点迷,但是只要尝试过下面这段代码就能搞明白了:

var numArr = ['1', '2', '3']

for (num of numArr) {
  console.log(num)
  if (num === '3') {
    arr.push('Done!')
  }
}

尝试运行一下createGraph('./example/entry.js'),就能够看到如下的输出:

[ { id: 0,
    filename: './example/entry.js',
    dependencies: [ './message.js' ],
    code: '"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);',
    mapping: { './message.js': 1 } },
  { id: 1,
    filename: 'example/message.js',
    dependencies: [ './name.js' ],
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\n\nvar _name = require("./name.js");\n\nexports.default = "Hello " + _name.name + "!";',
    mapping: { './name.js': 2 } },
  { id: 2,
    filename: 'example/name.js',
    dependencies: [],
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nvar name = exports.name = \'world\';',
    mapping: {} } ]

现在依赖关系图集已经构建完成了,接下来就是把它们打包成一个单独的,可直接运行的文件啦!

4、进行打包

上一步生成的依赖关系图集,接下来将通过CommomJS规范来实现加载。由于篇幅关系,本文不对CommomJS规范进行扩展,有兴趣的读者可以参考@阮一峰 老师的一篇文章《浏览器加载 CommonJS 模块的原理与实现》,说得非常清晰。简单来说,就是通过构造一个立即执行函数(function () {})(),手动定义moduleexportsrequire变量,最后实现代码在浏览器运行的目的。

接下来就是依据这个规范,通过字符串拼接去构建代码块。

function bundle (graph) {
  let modules = ''

  graph.forEach(mod => {
    modules += `${mod.id}: [
      function (require, module, exports) { ${mod.code} },
      ${JSON.stringify(mod.mapping)},
    ],`
  })

  const result = `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];

        function localRequire(name) {
          return require(mapping[name]);
        }

        const module = { exports : {} };

        fn(localRequire, module, module.exports);

        return module.exports;
      }

      require(0);
    })({${modules}})
  `
  return result
}

最后运行bundle(createGraph('./example/entry.js')),输出如下:

(function (modules) {
  function require(id) {
    const [fn, mapping] = modules[id];

    function localRequire(name) {
      return require(mapping[name]);
    }

    const module = { exports: {} };

    fn(localRequire, module, module.exports);

    return module.exports;
  }

  require(0);
})({
  0: [
    function (require, module, exports) {
      "use strict";

      var _message = require("./message.js");

      var _message2 = _interopRequireDefault(_message);

      function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

      console.log(_message2.default);
    },
    { "./message.js": 1 },
  ], 1: [
    function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      });

      var _name = require("./name.js");

      exports.default = "Hello " + _name.name + "!";
    },
    { "./name.js": 2 },
  ], 2: [
    function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      var name = exports.name = 'world';
    },
    {},
  ],
})

这段代码将能够直接在浏览器运行,输出“Hello world!”。

至此,整一个打包工具已经完成。

5、归纳总结

经过上面几个步骤,我们可以知道一个模块打包工具,第一步会从入口文件开始,对其进行依赖分析,第二步对其所有依赖再次递归进行依赖分析,第三步构建出模块的依赖图集,最后一步根据依赖图集使用CommonJS规范构建出最终的代码。明白了当中每一步的目的,便能够明白一个打包工具的运行原理。

最后再次感谢@ronami的开源项目minipack,其源码有着更为详细的注释,非常值得大家阅读。

查看原文

赞 100 收藏 128 评论 0

渣正refn 赞了文章 · 2019-01-26

深入浅出基于“依赖收集”的响应式原理

clipboard.png

每当问到VueJS响应式原理,大家可能都会脱口而出“Vue通过Object.defineProperty方法把data对象的全部属性转化成getter/setter,当属性被访问或修改时通知变化”。然而,其内部深层的响应式原理可能很多人都没有完全理解,网络上关于其响应式原理的文章质量也是参差不齐,大多是贴个代码加段注释了事。本文将会从一个非常简单的例子出发,一步一步分析响应式原理的具体实现思路。

一、使数据对象变得“可观测”

首先,我们定义一个数据对象,就以王者荣耀里面的其中一个英雄为例子:

const hero = {
  health: 3000,
  IQ: 150
}

我们定义了这个英雄的生命值为3000,IQ为150。但是现在还不知道他是谁,不过这不重要,只需要知道这个英雄将会贯穿我们整篇文章,而我们的目的就是通过这个英雄的属性,知道这个英雄是谁。

现在我们可以通过hero.healthhero.IQ直接读写这个英雄对应的属性值。但是,当这个英雄的属性被读取或修改时,我们并不知情。那么应该如何做才能够让英雄主动告诉我们,他的属性被修改了呢?这时候就需要借助Object.defineProperty的力量了。

关于Object.defineProperty的介绍,MDN上是这么说的:

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

在本文中,我们只使用这个方法使对象变得“可观测”,更多关于这个方法的具体内容,请参考https://developer.mozilla.org...,就不再赘述了。

那么如何让这个英雄主动通知我们其属性的读写情况呢?首先改写一下上面的例子:

let hero = {}
let val = 3000
Object.defineProperty(hero, 'health', {
  get () {
    console.log('我的health属性被读取了!')
    return val
  },
  set (newVal) {
    console.log('我的health属性被修改了!')
    val = newVal
  }
})

我们通过Object.defineProperty方法,给hero定义了一个health属性,这个属性在被读写的时候都会触发一段console.log。现在来尝试一下:

console.log(hero.health)

// -> 3000
// -> 我的health属性被读取了!

hero.health = 5000
// -> 我的health属性被修改了

可以看到,英雄已经可以主动告诉我们其属性的读写情况了,这也意味着,这个英雄的数据对象已经是“可观测”的了。为了把英雄的所有属性都变得可观测,我们可以想一个办法:

/**
 * 使一个对象转化成可观测对象
 * @param { Object } obj 对象
 * @param { String } key 对象的key
 * @param { Any } val 对象的某个key的值
 */
function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    get () {
      // 触发getter
      console.log(`我的${key}属性被读取了!`)
      return val
    },
    set (newVal) {
      // 触发setter
      console.log(`我的${key}属性被修改了!`)
      val = newVal
    }
  })
}

/**
 * 把一个对象的每一项都转化成可观测对象
 * @param { Object } obj 对象
 */
function observable (obj) {
  const keys = Object.keys(obj)
  keys.forEach((key) => {
    defineReactive(obj, key, obj[key])
  })
  return obj
}

现在我们可以把英雄这么定义:

const hero = observable({
  health: 3000,
  IQ: 150
})

读者们可以在控制台自行尝试读写英雄的属性,看看它是不是已经变得可观测的。

二、计算属性

现在,英雄已经变得可观测,任何的读写操作他都会主动告诉我们,但也仅此而已,我们仍然不知道他是谁。如果我们希望在修改英雄的生命值和IQ之后,他能够主动告诉他的其他信息,这应该怎样才能办到呢?假设可以这样:

watcher(hero, 'type', () => {
  return hero.health > 4000 ? '坦克' : '脆皮'
})

我们定义了一个watcher作为“监听器”,它监听了hero的type属性。这个type属性的值取决于hero.health,换句话来说,当hero.health发生变化时,hero.type也应该发生变化,前者是后者的依赖。我们可以把这个hero.type称为“计算属性”。

那么,我们应该怎样才能正确构造这个监听器呢?可以看到,在设想当中,监听器接收三个参数,分别是被监听的对象、被监听的属性以及回调函数,回调函数返回一个该被监听属性的值。顺着这个思路,我们尝试着编写一段代码:

/**
 * 当计算属性的值被更新时调用
 * @param { Any } val 计算属性的值
 */
function onComputedUpdate (val) {
  console.log(`我的类型是:${val}`);
}

/**
 * 观测者
 * @param { Object } obj 被观测对象
 * @param { String } key 被观测对象的key
 * @param { Function } cb 回调函数,返回“计算属性”的值
 */
function watcher (obj, key, cb) {
  Object.defineProperty(obj, key, {
    get () {
      const val = cb()
      onComputedUpdate(val)
      return val
    },
    set () {
      console.error('计算属性无法被赋值!')
    }
  })
}

现在我们可以把英雄放在监听器里面,尝试跑一下上面的代码:

watcher(hero, 'type', () => {
  return hero.health > 4000 ? '坦克' : '脆皮'
})

hero.type

hero.health = 5000

hero.type

// -> 我的health属性被读取了!
// -> 我的类型是:脆皮
// -> 我的health属性被修改了!
// -> 我的health属性被读取了!
// -> 我的类型是:坦克

现在看起来没毛病,一切都运行良好,是不是就这样结束了呢?别忘了,我们现在是通过手动读取hero.type来获取这个英雄的类型,并不是他主动告诉我们的。如果我们希望让英雄能够在health属性被修改后,第一时间主动发起通知,又该怎么做呢?这就涉及到本文的核心知识点——依赖收集。

三、依赖收集

我们知道,当一个可观测对象的属性被读写时,会触发它的getter/setter方法。换个思路,如果我们可以在可观测对象的getter/setter里面,去执行监听器里面的onComputedUpdate()方法,是不是就能够实现让对象主动发出通知的功能呢?

由于监听器内的onComputedUpdate()方法需要接收回调函数的值作为参数,而可观测对象内并没有这个回调函数,所以我们需要借助一个第三方来帮助我们把监听器和可观测对象连接起来。

这个第三方就做一件事情——收集监听器内的回调函数的值以及onComputedUpdate()方法。

现在我们把这个第三方命名为“依赖收集器”,一起来看看应该怎么写:

const Dep = {
  target: null
}

就是这么简单。依赖收集器的target就是用来存放监听器里面的onComputedUpdate()方法的。

定义完依赖收集器,我们回到监听器里,看看应该在什么地方把onComputedUpdate()方法赋值给Dep.target

function watcher (obj, key, cb) {
  // 定义一个被动触发函数,当这个“被观测对象”的依赖更新时调用
  const onDepUpdated = () => {
    const val = cb()
    onComputedUpdate(val)
  }

  Object.defineProperty(obj, key, {
    get () {
      Dep.target = onDepUpdated
      // 执行cb()的过程中会用到Dep.target,
      // 当cb()执行完了就重置Dep.target为null
      const val = cb()
      Dep.target = null
      return val
    },
    set () {
      console.error('计算属性无法被赋值!')
    }
  })
}

我们在监听器内部定义了一个新的onDepUpdated()方法,这个方法很简单,就是把监听器回调函数的值以及onComputedUpdate()打包到一块,然后赋值给Dep.target。这一步非常关键,通过这样的操作,依赖收集器就获得了监听器的回调值以及onComputedUpdate()方法。作为全局变量,Dep.target理所当然的能够被可观测对象的getter/setter所使用。

重新看一下我们的watcher实例:

watcher(hero, 'type', () => {
  return hero.health > 4000 ? '坦克' : '脆皮'
})

在它的回调函数中,调用了英雄的health属性,也就是触发了对应的getter函数。理清楚这一点很重要,因为接下来我们需要回到定义可观测对象的defineReactive()方法当中,对它进行改写:

function defineReactive (obj, key, val) {
  const deps = []
  Object.defineProperty(obj, key, {
    get () {
      if (Dep.target && deps.indexOf(Dep.target) === -1) {
        deps.push(Dep.target)
      }
      return val
    },
    set (newVal) {
      val = newVal
      deps.forEach((dep) => {
        dep()
      })
    }
  })
}

可以看到,在这个方法里面我们定义了一个空数组deps,当getter被触发的时候,就会往里面添加一个Dep.target。回到关键知识点Dep.target等于监听器的onComputedUpdate()方法,这个时候可观测对象已经和监听器捆绑到一块。任何时候当可观测对象的setter被触发时,就会调用数组中所保存的Dep.target方法,也就是自动触发监听器内部的onComputedUpdate()方法。

至于为什么这里的deps是一个数组而不是一个变量,是因为可能同一个属性会被多个计算属性所依赖,也就是存在多个Dep.target。定义deps为数组,若当前属性的setter被触发,就可以批量调用多个计算属性的onComputedUpdate()方法了。

完成了这些步骤,基本上我们整个响应式系统就已经搭建完成,下面贴上完整的代码:

/**
 * 定义一个“依赖收集器”
 */
const Dep = {
  target: null
}

/**
 * 使一个对象转化成可观测对象
 * @param { Object } obj 对象
 * @param { String } key 对象的key
 * @param { Any } val 对象的某个key的值
 */
function defineReactive (obj, key, val) {
  const deps = []
  Object.defineProperty(obj, key, {
    get () {
      console.log(`我的${key}属性被读取了!`)
      if (Dep.target && deps.indexOf(Dep.target) === -1) {
        deps.push(Dep.target)
      }
      return val
    },
    set (newVal) {
      console.log(`我的${key}属性被修改了!`)
      val = newVal
      deps.forEach((dep) => {
        dep()
      })
    }
  })
}

/**
 * 把一个对象的每一项都转化成可观测对象
 * @param { Object } obj 对象
 */
function observable (obj) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i], obj[keys[i]])
  }
  return obj
}

/**
 * 当计算属性的值被更新时调用
 * @param { Any } val 计算属性的值
 */
function onComputedUpdate (val) {
  console.log(`我的类型是:${val}`)
}

/**
 * 观测者
 * @param { Object } obj 被观测对象
 * @param { String } key 被观测对象的key
 * @param { Function } cb 回调函数,返回“计算属性”的值
 */
function watcher (obj, key, cb) {
  // 定义一个被动触发函数,当这个“被观测对象”的依赖更新时调用
  const onDepUpdated = () => {
    const val = cb()
    onComputedUpdate(val)
  }

  Object.defineProperty(obj, key, {
    get () {
      Dep.target = onDepUpdated
      // 执行cb()的过程中会用到Dep.target,
      // 当cb()执行完了就重置Dep.target为null
      const val = cb()
      Dep.target = null
      return val
    },
    set () {
      console.error('计算属性无法被赋值!')
    }
  })
}

const hero = observable({
  health: 3000,
  IQ: 150
})

watcher(hero, 'type', () => {
  return hero.health > 4000 ? '坦克' : '脆皮'
})

console.log(`英雄初始类型:${hero.type}`)

hero.health = 5000

// -> 我的health属性被读取了!
// -> 英雄初始类型:脆皮
// -> 我的health属性被修改了!
// -> 我的health属性被读取了!
// -> 我的类型是:坦克

上述代码可以直接在code pen或者浏览器控制台上执行。

四、代码优化

在上面的例子中,依赖收集器只是一个简单的对象,其实在defineReactive()内部的deps数组等和依赖收集有关的功能,都应该集成在Dep实例当中,所以我们可以把依赖收集器改写一下:

class Dep {
  constructor () {
    this.deps = []
  }

  depend () {
    if (Dep.target && this.deps.indexOf(Dep.target) === -1) {
      this.deps.push(Dep.target)
    }
  }

  notify () {
    this.deps.forEach((dep) => {
      dep()
    })
  }
}

Dep.target = null

同样的道理,我们对observable和watcher都进行一定的封装与优化,使这个响应式系统变得模块化:

class Observable {
  constructor (obj) {
    return this.walk(obj)
  }

  walk (obj) {
    const keys = Object.keys(obj)
    keys.forEach((key) => {
      this.defineReactive(obj, key, obj[key])
    })
    return obj
  }

  defineReactive (obj, key, val) {
    const dep = new Dep()
    Object.defineProperty(obj, key, {
      get () {
        dep.depend()
        return val
      },
      set (newVal) {
        val = newVal
        dep.notify()
      }
    })
  }
}
class Watcher {
  constructor (obj, key, cb, onComputedUpdate) {
    this.obj = obj
    this.key = key
    this.cb = cb
    this.onComputedUpdate = onComputedUpdate
    return this.defineComputed()
  }

  defineComputed () {
    const self = this
    const onDepUpdated = () => {
      const val = self.cb()
      this.onComputedUpdate(val)
    }

    Object.defineProperty(self.obj, self.key, {
      get () {
        Dep.target = onDepUpdated
        const val = self.cb()
        Dep.target = null
        return val
      },
      set () {
        console.error('计算属性无法被赋值!')
      }
    })
  }
}

然后我们来跑一下:

const hero = new Observable({
  health: 3000,
  IQ: 150
})

new Watcher(hero, 'type', () => {
  return hero.health > 4000 ? '坦克' : '脆皮'
}, (val) => {
  console.log(`我的类型是:${val}`)
})

console.log(`英雄初始类型:${hero.type}`)

hero.health = 5000

// -> 英雄初始类型:脆皮
// -> 我的类型是:坦克

代码已经放在code pen,浏览器控制台也是可以运行的~

五、尾声

看到上述的代码,是不是发现和VueJS源码里面的很像?其实VueJS的思路和原理也是类似的,只不过它做了更多的事情,但核心还是在这里边。

在学习VueJS源码的时候,曾经被响应式原理弄得头昏脑涨,并非一下子就看懂了。后在不断的思考与尝试下,同时参考了许多其他人的思路,才总算把这一块的知识点完全掌握。希望这篇文章对大家有帮助,如果发现有任何错漏的地方,也欢迎向我指出,谢谢大家~

查看原文

赞 70 收藏 159 评论 15

认证与成就

  • 获得 26 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2016-01-24
个人主页被 668 人浏览