ken1992code

ken1992code 查看完整档案

深圳编辑东莞理工学院  |  本科_电子信息工程 编辑平安HRX  |  前端工程师 编辑 github.com/makeng 编辑
编辑

手绘文档,从小算法到源码都能画出简笔画
笔记狂人,1/7/30/60 天间隔复习改进
舞蹈领队,年会团体表演包教包会
DOTA 辅助,队伍粘合剂
拳击自学者,模拟战中所向披靡

个人动态

ken1992code 赞了文章 · 11月27日

通过编写简易虚拟DOM,来学习虚拟DOM 的原理

作者:deathmood
译者:前端小智
来源:medium

1024程序员节,160就能买到400的书,红宝书 5 折

为了保证的可读性,本文采用意译而非直译。

要构建自己的虚拟DOM,需要知道两件事。你甚至不需要深入 React 的源代码或者深入任何其他虚拟DOM实现的源代码,因为它们是如此庞大和复杂——但实际上,虚拟DOM的主要部分只需不到50行代码。

有两个概念:

  • Virtual DOM 是真实DOM的映射
  • 当虚拟 DOM 树中的某些节点改变时,会得到一个新的虚拟树。算法对这两棵树(新树和旧树)进行比较,找出差异,然后只需要在真实的 DOM 上做出相应的改变。

用JS对象模拟DOM树

首先,我们需要以某种方式将 DOM 树存储在内存中。可以使用普通的 JS 对象来做。假设我们有这样一棵树:

<ul class=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

看起来很简单,对吧? 如何用JS对象来表示呢?

{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [
  { type: ‘li’, props: {}, children: [‘item 1’] },
  { type: ‘li’, props: {}, children: [‘item 2’] }
] }

这里有两件事需要注意:

  • 用如下对象表示DOM元素
{ type: ‘…’, props: { … }, children: [ … ] }

  • 用普通 JS 字符串表示 DOM 文本节点

但是用这种方式表示内容很多的 Dom 树是相当困难的。这里来写一个辅助函数,这样更容易理解:

function h(type, props, …children) {
  return { type, props, children };
}

用这个方法重新整理一开始代码:

h(‘ul’, { ‘class’: ‘list’ },
  h(‘li’, {}, ‘item 1’),
  h(‘li’, {}, ‘item 2’),
);

这样看起来简洁多了,还可以更进一步。这里使用 JSX,如下:

<ul className=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

编译成:

React.createElement(‘ul’, { className: ‘list’ },
  React.createElement(‘li’, {}, ‘item 1’),
  React.createElement(‘li’, {}, ‘item 2’),
);

是不是看起来有点熟悉?如果能够用我们刚定义的 h(...) 函数代替 React.createElement(…),那么我们也能使用JSX 语法。其实,只需要在源文件头部加上这么一句注释:

/** @jsx h */
<ul className=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

它实际上告诉 Babel ' 嘿,小老弟帮我编译 JSX 语法,用 h(...) 函数代替 React.createElement(…),然后 Babel 就开始编译。'

综上所述,我们将DOM写成这样:

/** @jsx h */
const a = (
  <ul className=”list”>
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

Babel 会帮我们编译成这样的代码:

const a = (
  h(‘ul’, { className: ‘list’ },
    h(‘li’, {}, ‘item 1’),
    h(‘li’, {}, ‘item 2’),
  );
);

当函数 “h” 执行时,它将返回普通JS对象-即我们的虚拟DOM:

const a = (
  { type: ‘ul’, props: { className: ‘list’ }, children: [
    { type: ‘li’, props: {}, children: [‘item 1’] },
    { type: ‘li’, props: {}, children: [‘item 2’] }
  ] }
);

从Virtual DOM 映射到真实 DOM

好了,现在我们有了 DOM 树,用普通的 JS 对象表示,还有我们自己的结构。这很酷,但我们需要从它创建一个真正的DOM。

首先让我们做一些假设并声明一些术语:

  • 使用以' $ '开头的变量表示真正的DOM节点(元素,文本节点),因此 $parent 将会是一个真实的DOM元素
  • 虚拟 DOM 使用名为 node 的变量表示

* 就像在 React 中一样,只能有一个根节点——所有其他节点都在其中

那么,来编写一个函数 createElement(…),它将获取一个虚拟 DOM 节点并返回一个真实的 DOM 节点。这里先不考虑 propschildren 属性:

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  return document.createElement(node.type);
}

上述方法我也可以创建有两种节点分别是文本节点和 Dom 元素节点,它们是类型为的 JS 对象:

{ type: ‘…’, props: { … }, children: [ … ] }

因此,可以在函数 createElement 传入虚拟文本节点和虚拟元素节点——这是可行的。

现在让我们考虑子节点——它们中的每一个都是文本节点或元素。所以它们也可以用 createElement(…) 函数创建。是的,这就像递归一样,所以我们可以为每个元素的子元素调用 createElement(…),然后使用 appendChild() 添加到我们的元素中:

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

哇,看起来不错。先把节点 props 属性放到一边。待会再谈。我们不需要它们来理解虚拟DOM的基本概念,因为它们会增加复杂性。

完整代码如下:

/** @jsx h */

function h(type, props, ...children) {
  return { type, props, children };
}

function createElement(node) {
  if (typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

const a = (
  <ul class="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

const $root = document.getElementById('root');
$root.appendChild(createElement(a));

比较两棵虚拟DOM树的差异

现在我们可以将虚拟 DOM 转换为真实的 DOM,这就需要考虑比较两棵 DOM 树的差异。基本的,我们需要一个算法来比较新的树和旧的树,它能够让我们知道什么地方改变了,然后相应的去改变真实的 DOM。

怎么比较 DOM 树?需要处理下面的情况:

  • 添加新节点,使用 appendChild(…) 方法添加节点

图片描述

  • 移除老节点,使用 removeChild(…) 方法移除老的节点

图片描述

  • 节点的替换,使用 replaceChild(…) 方法

图片描述

如果节点相同的——就需要需要深度比较子节点

图片描述

编写一个名为 updateElement(…) 的函数,它接受三个参数—— $parentnewNodeoldNode,其中 $parent 是虚拟节点的一个实际 DOM 元素的父元素。现在来看看如何处理上面描述的所有情况。

添加新节点

function updateElement($parent, newNode, oldNode) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  }
}

移除老节点

这里遇到了一个问题——如果在新虚拟树的当前位置没有节点——我们应该从实际的 DOM 中删除它—— 这要如何做呢?

如果我们已知父元素(通过参数传递),我们就能调用 $parent.removeChild(…) 方法把变化映射到真实的 DOM 上。但前提是我们得知道我们的节点在父元素上的索引,我们才能通过 $parent.childNodes[index] 得到该节点的引用。

好的,让我们假设这个索引将被传递给 updateElement 函数(它确实会被传递——稍后将看到)。代码如下:

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  }
}

节点的替换

首先,需要编写一个函数来比较两个节点(旧节点和新节点),并告诉节点是否真的发生了变化。还有需要考虑这个节点可以是元素或是文本节点:

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === ‘string’ && node1 !== node2 ||
         node1.type !== node2.type
}

现在,当前的节点有了 index 属性,就可以很简单的用新节点替换它:

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  }
}

比较子节点

最后,但并非最不重要的是——我们应该遍历这两个节点的每一个子节点并比较它们——实际上为每个节点调用updateElement(…)方法,同样需要用到递归。

  • 当节点是 DOM 元素时我们才需要比较( 文本节点没有子节点 )
  • 我们需要传递当前的节点的引用作为父节点
  • 我们应该一个一个的比较所有的子节点,即使它是 undefined 也没有关系,我们的函数也会正确处理它。
  • 最后是 index,它是子数组中子节点的 index
function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  } else if (newNode.type) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i < newLength || i < oldLength; i++) {
      updateElement(
        $parent.childNodes[index],
        newNode.children[i],
        oldNode.children[i],
        i
      );
    }
  }
}

完整的代码

Babel+JSX
/* @jsx h /

function h(type, props, ...children) {
  return { type, props, children };
}

function createElement(node) {
  if (typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === 'string' && node1 !== node2 ||
         node1.type !== node2.type
}

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  } else if (newNode.type) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i < newLength || i < oldLength; i++) {
      updateElement(
        $parent.childNodes[index],
        newNode.children[i],
        oldNode.children[i],
        i
      );
    }
  }
}

// ---------------------------------------------------------------------

const a = (
  <ul>
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

const b = (
  <ul>
    <li>item 1</li>
    <li>hello!</li>
  </ul>
);

const $root = document.getElementById('root');
const $reload = document.getElementById('reload');

updateElement($root, a);
$reload.addEventListener('click', () => {
  updateElement($root, b, a);
});

HTML

<button id="reload">RELOAD</button>
<div id="root"></div>

CSS

#root {
  border: 1px solid black;
  padding: 10px;
  margin: 30px 0 0 0;
}

打开开发者工具,并观察当按下“Reload”按钮时应用的更改。

图片描述

总结

现在我们已经编写了虚拟 DOM 实现及了解它的工作原理。作者希望,在阅读了本文之后,对理解虚拟 DOM 如何工作的基本概念以及在幕后如何进行响应有一定的了解。

然而,这里有一些东西没有突出显示(将在以后的文章中介绍它们):

  • 设置元素属性(props)并进行 diffing/updating
  • 处理事件——向元素中添加事件监听
  • 让虚拟 DOM 与组件一起工作,比如React
  • 获取对实际DOM节点的引用
  • 使用带有库的虚拟 DOM,这些库可以直接改变真实的 DOM,比如 jQuery 及其插件

原文:
https://medium.com/@deathmood...

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug


交流

文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

查看原文

赞 17 收藏 11 评论 1

ken1992code 赞了文章 · 11月26日

jenkins 备份和回滚(by:自己查资料学习)

jenkins备份和回滚
首先在项目配置中的General中:
1.勾选Discard old builds(丢弃旧的构建)
Days to keep builds 代表保存构建的天数,Max# of builds to keep 代表保存构建的最大个数。如图
clipboard.png

2.勾选This project is parameterized(参数化构建过程)
添加Choice Parameter(选择参数)和String parameter(文本参数),设置两个参数,status-构建动作,默认值有deploy-发布,rollback-回滚;version-构建记录id值,默认值为0如图:
clipboard.png

在Source Code Management(原码)
选好git链接,并填写好分支

clipboard.png

在Build(构建)里,添加shell脚本。

clipboard.png

脚本如下:
case $status in

deploy)
    echo "status:$status"
    backup_path="${JENKINS_HOME}/jobs/${JOB_NAME}/builds/${BUILD_NUMBER}/target"
    deploy_path="/data/wwwroot/www.test.com/"
    if [ -d $backup_path ];
    then
        echo "$backup_path is exists!"
    else
        mkdir -p $backup_path
    fi
    sudo rsync -r -z -P -p -t --delete $deploy_path $backup_path/
    sudo rsync -r -z -P -p -t --delete ${WORKSPACE}/ $deploy_path
    echo "deploy done!"
    ;;
rollback)
    echo "status:$status"
    echo "build_id:$version"
    backup_path="${JENKINS_HOME}/jobs/${JOB_NAME}/builds/$version/target"
    deploy_path="/data/wwwroot/www.test.com/"
    rm -rf $deploy_path
    cp -r $backup_path/ $deploy_path
    echo "roll_back done!"
    ;;
*)
exit
    ;;

esac

然后点击保存。

点击Build with Parameters,选择构建动作,deploy部署或rollback回滚,选择rollback则需要输入version构建记录ID,version构建记录ID对应的是构建历史的ID,例如#20的20

clipboard.png

然后点击build就可以了。

=====================分割线==========================

上面的方法用的是项目和jenkins系统在同一个服务器的脚本,
如果使用远程部署,就是项目和jenkins系统不在一个服务器下,要使用远程的方式,如下:
1.生成秘钥对:
在jenkins所在的服务器生成密钥对,首先要查看jenkins的配置文件,看一下jenkins的权限是在jenkins下,还是root下。然后在对应的权限文件夹内,生成密钥对。
(1)jenkins下:
在jenkins所在的服务器切换到 jenkins账户:su - jenkins
创建密钥:ssh-keygen
生成后得到密钥对位置:
Your identification has been saved in /var/lib/jenkins/.ssh/id_rsa
Your public key has been saved in /var/lib/jenkins/.ssh/id_rsa.pub
将公钥内容 id_rsa.pub 拷贝到客户端 /root/.ssh/authorized_keys 文件内
(2)root下:
在jenkins所在的服务器切换到 root账户:su - root
创建密钥:ssh-keygen
生成后得到密钥对位置:
Your identification has been saved in /root/.ssh/id_rsa
Your public key has been saved in /root/.ssh/id_rsa.pub
将公钥内容 id_rsa.pub 拷贝到客户端 /root/.ssh/authorized_keys 文件内。

查看原文

赞 3 收藏 1 评论 1

ken1992code 赞了回答 · 11月25日

解决为什么在HTTP头里要设置同时设置Expires和Cache-control:max-age

因为 ExpiresHTTP 1.0 定义的字段,而 Cache-ControlHTTP 1.1 的字段,万一客户端只支持 HTTP 1.0,那么 Cache-Control 有可能就会不工作,所以一般为了兼容会都写上。

关注 0 回答 1

ken1992code 赞了问题 · 11月25日

解决为什么在HTTP头里要设置同时设置Expires和Cache-control:max-age

以前很少关心这些内容,搜了下,说Cache-control里的max-age会覆盖Expires,但是为什么还要两个都设置呢?有没有别的意义?还是就是图省事就都设置了。
请输入图片描述

关注 0 回答 1

ken1992code 发布了文章 · 11月21日

【代码规范】一个好函数看起来应该是怎样的

image

好的模板能带来好结果,一般的模板则带来坏结果。

📚 代码规范之痛

好的代码是怎样的?答案是模块清晰、逻辑连贯。
好书《Clean Code》有阐述,还有市面上纷繁的概念(比如 Google 的 HTML 规范、CSS 的 BEM、Vue 的风格指南)也都明确地指向这个理念。
可是这些书籍或者文档,都有一个共同的缺点——太长了。

🛋️ 懒规范

为了推广这些规范,我用文档把林林总总的软件工程书籍、规范解释文档链接整理出来,满心欢喜地推广给其他程序员,一般也只能得到“有空会看”的回应。
“他们太懒了”,我心想。
对付懒人,只能用“懒办法”——归纳出一个简单易懂的、清晰可执行概念。

🔘 好函数

函数是我们编程中见得最多、写的最多的东西(搬砖工人的砖头)。
先看一个普通的函数,比如我们要把某个 id 加工一下,然后用它获取一个列表,对其所有元素加工后渲染。我见过最多的写法长这样:

function queryList(id){
    let list = foo.get('/path/list', 'boo' + id)
    for (let i =0; i < list.length; i ++){
        list[i].name = 'name:' + list[i].name
        list[i].desc = 'desc:' + list[i].desc
    }
    function render(list){
        for (let i =0; i < list.length; i ++){
            $('.list').find('li').text(list[i].name + ',' + list[i].desc)
        }
    }
    render(list)
}

这是典型的“想到哪写到哪”。
我们再来看看大牛 LucasHC 写的一个滚动渲染代码的设计思路:(这里简化了一些代码)

    (function() {
        var fetching = false; 
        var page = 1;
        var slideCache = [];

        function isVisible (id) {
            // ...判断元素是否在可见区域
        }
        function updateItemCache (node) {
            // ....更新DOM缓存
        }
        function handleScroll (e, force) {
            // ...滚动处理程序
        } 

        window.setTimeout(handleScroll, 100);
        fetchContent();
    }());

这是标准的“先设计再填充”。
分析一下,方法就是,把函数分割成 3 部分:变量、子函数、执行,视觉上就清晰明了(图片在文章头部)。就像建房子一样,准备好物料,规划好工序,最后动工。不信你拿出乐高积木玩具的说明书,也是这种构建方法(逻辑简洁明了到小孩子都能懂 :D)。
好,我们来把例子函数按照这个方法改造一下:

function queryList(id){
    const mergedId = 'boo' + id
    const url = '/path/list'
    
    function getListFromServer(url, id){} // 细节不展示
    function listItemHandle(list){} // 细节不展示
    function render(list){} // 细节不展示
    
    // 我们要关注的,不是细节,而是这个函数做了3件事:1.获取,2.加工,3.渲染
    let list = getListFromServer(url, mergedId)
    list = listItemHandle(list)
    render(list)
}

这是美好的“代码即逻辑”。
动用代码编辑器的“折叠”功能,就可以把所有子函数的细节收起来。等到需要维护&扩展的时候,我们可以迅速地找到并改造我们的代码。

☕️ 终归是懒

我保证,“懒规范”可以让你的编码速度至少提升 20%,可维护性提升 80%。
什么?你不信?
不信就去看看《Clean Code》和各种规范解释文档吧 :D。

查看原文

赞 1 收藏 0 评论 0

ken1992code 回答了问题 · 11月18日

解决“cp不是内部或外部命令”怎么解决

用 GitBash 也可以执行 Linux 的命令。

关注 2 回答 2

ken1992code 赞了文章 · 11月15日

前端面试题-BFC(块格式化上下文)

一、BFC 的概念

1.规范解释

块格式化上下文(Block Formatting Context,BFC)是Web页面的可视化CSS渲染的一部分,是布局过程中生成块级盒子的区域,也是浮动元素与其他元素的交互限定区域。

2.通俗理解

  • BFC 是一个独立的布局环境,可以理解为一个容器,在这个容器中按照一定规则进行物品摆放,并且不会影响其它环境中的物品
  • 如果一个元素符合触发 BFC 的条件,则 BFC 中的元素布局不受外部影响。
  • 浮动元素会创建 BFC,则浮动元素内部子元素主要受该浮动元素影响,所以两个浮动元素之间是互不影响的

二、创建 BFC

  1. 根元素或包含根元素的元素
  2. 浮动元素 float = left | right 或 inherit(≠ none)
  3. 绝对定位元素 position = absolute 或 fixed
  4. display = inline-block | flex | inline-flex | table-cell 或 table-caption
  5. overflow = hidden | auto 或 scroll (≠ visible)

三、BFC 的特性

  1. BFC 是一个独立的容器,容器内子元素不会影响容器外的元素。反之亦如此。
  2. 盒子从顶端开始垂直地一个接一个地排列,盒子之间垂直的间距是由 margin 决定的。
  3. 在同一个 BFC 中,两个相邻的块级盒子的垂直外边距会发生重叠。
  4. BFC 区域不会和 float box 发生重叠。
  5. BFC 能够识别并包含浮动元素,当计算其区域的高度时,浮动元素也可以参与计算了。

四、BFC 的作用

1.包含浮动元素(清除浮动)

  • 浮动元素会脱离文档流(绝对定位元素也会脱离文档流),导致无法计算准确的高度,这种问题称为高度塌陷
  • 解决高度塌陷问题的前提是能够识别并包含浮动元素,也就是清除浮动

清除浮动

问题举例:如上左图所示,容器(container)没有高度或者 height = auto ,并且其子元素(sibling)是浮动元素,所以该容器的高度是不会被撑开的,即高度塌陷。

解决方法:在容器(container)中创建 BFC。

HTML

<div class="container">
        <div class="Sibling"></div>
        <div class="Sibling"></div>
</div>

CSS

.container { 
        overflow: hidden; /* creates block formatting context */ 
        background-color: green; 
} 
.container .Sibling { 
        float: left; 
        margin: 10px;
        background-color: lightgreen;  
}

特别提示:

  • 通过 overflow:hidden 创建 BFC,固然可以解决高度塌陷的问题,但是大范围应用在布局上肯定不是最合适的,毕竟 overflow:hidden 会造成溢出隐藏的问题,尤其是与 JS 的交互效果会有影响。
  • 我们可以使用 clearfix 实现清除浮动,这里就不多介绍了,想要了解的可以阅读前端面试题-clearfix(清除浮动)

2.导致外边距折叠

相邻的两个盒子(可能是兄弟关系也可能是祖先关系)的垂直边距相遇时, 它们将形成一个外边距。这个外边距的高度等于两个发生折叠的外边距的高度中的较大者

导致外边距折叠

HTML

<div class="Container"> 
    <p>Sibling 1</p> 
    <p>Sibling 2</p> 
</div>

CSS

.Container { 
    background-color: red; 
    overflow: hidden; /* creates a block formatting context */ 
} 
p { 
    background-color: lightgreen; 
    margin: 10px 0; 
}

如上图所示:红色盒子(Container)中包含两个绿色的兄弟元素(P),并且红色盒子设置 overflow: hidden; 则一个BFC 已经被创建,即导致外边距折叠。

理论上两个兄弟元素之间的边距应该是两个元素的边距之和(20px),但实际是 10px。这就是外边距折叠导致的。

2.1 折叠外边距的值

  • 两个相邻的外边距都是 正数 时,折叠外边距是两者中较大的值。
  • 两个相邻的外边距都是 负数 时,折叠外边距是两者中绝对值较大的值。
  • 两个相邻的外边距是 一正一负 时,折叠外边距是两者相加的和。

2.2 外边距折叠的条件是 margin 必须相邻!

3.避免外边距折叠

这一听起来可能有些困惑,因为我们在前面讨论了 BFC 导致外边距折叠的问题。但我们必须记住的是外边距折叠(Margin collapsing)只会发生在属于同一BFC的块级元素之间。如果它们属于不同的 BFC,它们之间的外边距则不会折叠。所以通过创建一个不同的 BFC ,就可以避免外边距折叠。

修改前面的例子并添加第三个兄弟元素,CSS不变。

HTML

<div class="Container"> 
        <p>Sibling 1</p> 
        <p>Sibling 2</p> 
        <p>Sibling 3</p> 
</div>

结果不会改变,还会折叠外边距,三个兄弟元素(P)将会以垂直距离为 10px 的距离分开。原因是三个兄弟元素都属于同一个 BFC。

创建一个不同的 BFC ,就可以避免外边距折叠。

HTML

<div class="Container"> 
        <p>Sibling 1</p> 
        <p>Sibling 2</p> 
        <div class="newBFC"> 
            <p>Sibling 3</p> 
        </div> 
</div>

CSS

.Container { 
            background-color: red; 
            overflow: hidden; /* creates a block formatting context */ 
} 
p { 
            background-color: lightgreen; 
            margin: 10px 0; 
}
.newBFC { 
            overflow: hidden; /* creates new block formatting context */ 
}

避免外边距折叠

当第二和第三个兄弟元素属于不同的 BFC 时,它们之间就没有外边距折叠。

阅读更多

查看原文

赞 34 收藏 29 评论 2

ken1992code 赞了文章 · 11月9日

【图论】广度/深度优先搜索算法

前言

我们首次接触广度优先搜索深度优先搜索时,应该是在数据结构课上讲的 “图的遍历”。还有就是刷题的时候,遍历二叉树/拓扑排序我们会经常用到这两种遍历方法。

广度优先搜索算法(Breadth-First-Search,缩写为 BFS),是一种利用队列实现的搜索算法。简单来说,其搜索过程和 “湖面丢进一块石头激起层层涟漪” 类似。

深度优先搜索算法(Depth-First-Search,缩写为 DFS),是一种利用递归实现的搜索算法。简单来说,其搜索过程和 “不撞南墙不回头” 类似。

BFS 的重点在于队列,而 DFS 的重点在于递归。这是它们的本质区别。

1. 广度优先搜索法

广度优先搜索,也叫做广度优先遍历,其主要思想类似于树的层序遍历。

  1. 从任意一个节点A开始,遍历它的全部的邻接点B,C
  2. 然后再以它其中一个邻接点B为起点,遍历B的所有的邻接点D,F。
  3. 然后再以它另外一个邻接点C为起点,遍历C的所有的邻接点G,H。
  4. 然后再以它其中一个邻接点D为起点,遍历D的所有的邻接点...。
  5. 以此类推....

伪代码如下:

public void bfsSearch(){
    //定义一个结果集,queue队列
    List<Node> queue = new LinkedList<Node>();
    //先选择一个出发点,加入队列。
    queue.add(nodeA);
    int size = queue.getSize();
    for(int index=0;index<size;index++){
        //BFS的重点在于队列,它的思路就是沿着queue的添加顺序,依次遍历他们的邻接点。
        for(Node node : queue.get(index).getNeighbor()){
            //没被搜索过,那么加入结果集
            if(!queue.contain(node)){
                queue.add(node);
                size = queue.getSize();
            }
        }
    }
}

BFS比较适合判断二分图,以及用于实现寻找最小生成树(MST),如在BFS基础上的Kruskal算法。还有寻找最短路径问题(如Dijkstra算法)。

image

1.1 无向图的广度优先遍历

我们先给出一个图:

image

  1. 先找到A,这是第一层。
  2. 再找到A的邻接点,遍历到B,C,D,F。
  3. 再找到B,C,D,F的邻接点,遍历到G,E,H

image

1.2 有向图的广度优先遍历

image

思路和与无向图类似,只不过需要考虑边的走向问题。

  1. 先找到A,这是第一层。
  2. 再找到A的邻接点,遍历到B,C,F。
  3. 再找到B,C,F的邻接点,遍历到D,H
  4. 再找到D,H的邻接点,遍历到E,G

image

2. 深度优先搜索法

深度优先搜索,也叫做深度优先遍历,其主要思想是回溯法,它的核心是使用递归。

例如这张图,从1开始到2,之后到5,5不能再走了,退回2,到6,退回2退回1,到3,以此类推;

image

伪代码如下:

//stack:定义的结果集stack栈
//currentNode:本次递归搜索的当前node
public void dfsSearch(Stack<Node> stack,Node currentNode){
    if(currentNode没有邻接点 && !stack.contain(currentNode)){
        //压入结果栈
        stack.push(currentNode);
        return;
    }
    //node有邻接点,那么遍历邻接点,依次深搜
    for(Node node : currentNode.getNeighbor()){
        if(node.isVisited() || stack.contain(currentNode)){
            continue;
        }
        node.setVisited(true);
        dfsSearch(stack,node);
        node.setVisited(false);
    }
    //currentNode的邻接点都已经遍历过了,现在逻辑回溯回currentNode
    //那么需要将currentNode压入结果栈
    stack.push(currentNode);
}

image

2.1 无向图的深度优先遍历

我们先给出一个图:

image

对上无向图进行深度优先遍历,从A开始:

第1步:访问A。

第2步:访问B(A的邻接点)。 在第1步访问A之后,接下来应该访问的是A的邻接点,即"B,D,F"中的一个。但在本文的实现中,顶点ABCDEFGH是按照顺序存储,B在"D和F"的前面,因此,先访问B。

第3步:访问G(B的邻接点)。 和B相连只有"G"(A已经访问过了)  

第4步:访问E(G的邻接点)。 在第3步访问了B的邻接点G之后,接下来应该访问G的邻接点,即"E和H"中一个(B已经被访问过,就不算在内)。而由于E在H之前,先访问E。

第5步:访问C(E的邻接点)。 和E相连只有"C"(G已经访问过了)。

第6步:访问D(C的邻接点)。 

第7步:访问H。因为D没有未被访问的邻接点;因此,一直回溯到访问G的另一个邻接点H。

第8步:访问(H的邻接点)F。

因此访问顺序是:A -> B -> G -> E -> C -> D -> H->F

2.2 有向图的深度优先遍历

image

对上有向图进行深度优先遍历,从A开始:

第1步:访问A。

第2步:访问(A的出度对应的字母)B。 在第1步访问A之后,接下来应该访问的是A的出度对应字母,即"B,C,F"中的一个。但在本文的实现中,顶点ABCDEFGH是按照顺序存储,B在"C和F"的前面,因此,先访问B。

第3步:访问(B的出度对应的字母)F。 B的出度对应字母只有F。 

第4步:访问H(F的出度对应的字母)。 F的出度对应字母只有H。 

第5步:访问(H的出度对应的字母)G。

第6步:访问(G的出度对应字母)E。 在第5步访问G之后,接下来应该访问的是G的出度对应字母,即"B,C,E"中的一个。但在本文的实现中,顶点B已经访问了,由于C在E前面,所以先访问C。

第7步:访问(C的出度对应的字母)D。

第8步:访问(C的出度对应字母)D。 在第7步访问C之后,接下来应该访问的是C的出度对应字母,即"B,D"中的一个。但在本文的实现中,顶点B已经访问了,所以访问D。

第9步:访问E。D无出度,所以一直回溯到G对应的另一个出度E。

因此访问顺序是:A -> B -> F -> H -> G -> C -> D-> E

查看原文

赞 1 收藏 0 评论 0

ken1992code 赞了文章 · 11月9日

二叉树的遍历

二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。

结构

        1
       / \
      2   3
     / \   \
    4   5   6

二叉树的遍历分为深度优先遍历(DFS)和广度优先遍历(BFS),深度遍历有前(先)序、中序以及后序三种遍历方法,广度遍历即我们寻常所说的层次遍历

深度优先遍历(DFS)

深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次.

思路

深度优先遍历图的方法是,从图中某顶点v出发:
(1)访问顶点v;
(2)依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
(3)若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

前序遍历

首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树,js代码:

var postorder = function (root) {
    let res = []
    if (root == null) {
        return res
    }
    let dlr = (root) => {
        if (!root) {
            return null
        }
        res.push(root.val)
        dlr(root.left)
        dlr(root.right)
    }
    dlr(root)
    return res   
};

示例结果:[1, 2, 4, 5, 3, 6]

中序遍历

首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树,js代码:

var postorder = function (root) {
    let res = []
    if (root == null) {
        return res
    }
    let ldr = (root) => {
        if (!root) {
            return
        }
        ldr(root.left)
        res.push(root.val)
        ldr(root.right)
    }
    ldr(root)
    return res
};

示例结果:[4, 2, 5, 1, 3, 6]

后序遍历

首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根,js代码:

var postorder = function (root) {
    let res = []
    if (root == null) {
        return res
    }
    let ldr = (root) => {
        if (!root) {
            return
        }
        ldr(root.left)
        ldr(root.right)
        res.push(root.val)
    }
    ldr(root)
    return res
};

示例结果:[4, 5, 2, 6, 3, 1]

广度优先遍历(BFS)

是最简便的图的搜索算法之一,英文全称是Breadth First Search。 BFS并不使用经验法则算法。从算法的观点,所有因为展开节点而得到的子节点都会被加进一个先进先出的队列中。一般的实验里,其邻居节点尚未被检验过的节点会被放置在一个被称为 open 的容器中(例如队列或是链表),而被检验过的节点则被放置在被称为 closed 的容器中。

思路

给定图G=<V,E>和一个可以识别的源结点s,广度优先搜索对图G中的边进行系统性的探索来发现可以从源结点到达所有节点的路径。该算法能够计算出从源结点s到每个可到达的结点的距离,同时生成一颗广度优先搜索树。该数已源结点s为根节点,包含所有的可能从s到达的点。对于每一个从源结点s可以达到的jiedianv,在广度优先搜索树里面从结点s到达结点v的简单路径对应的就是s到v的最短路径。

层次遍历

即按照层次访问,通常用队列来做。访问根,访问子女,再访问子女的子女(越往后的层次越低)(两个子女的级别相同),js代码:

var postorder = function (root) {
    let res = []
    if (root == null) {
        return res
    }
    let bfs = (root, i) => {
        if (!root) {
            return
        }
        if (!res[i]) {
            res[i] = []
        }
        res[i].push(root.val)
        bfs(root.left, i + 1)
        bfs(root.right, i + 1)
    }
    bfs(root, 0)
    return res
};

示例结果:[[1], [2, 3], [4, 5, 6]]

永久链接: https://blog.qianxiaoduan.com/archives/1421

查看原文

赞 2 收藏 1 评论 0

ken1992code 发布了文章 · 11月5日

【前端面试】2020面试的一些感想

花一秒钟就看透事物本质的人,和花半辈子都看不清事物本质的人,注定是截然不同的命运。——《教父》

离职到现在为止大大小小面试了十多家,大的小的公司都有。经验积累了不少,记录下心得。

技术面

  • 80% 公司都有笔试题,所以需要大量刷题。你知识量多大都好,不刷题就是不行。临时的推理很难在 30 分钟内完成 5 个选择题 + 2 个简答题 + 1 到 2 个编程题。
  • 简历上要写项目难点,什么后台管理、国际化、看源码已经不吃香,我说的是没个几天几夜,或者基础不好就搞不出来的那种。
  • 话不要说的太死,或者太强硬。一些技术点、观点可以让面试官补充,让他有点话说,达到交流的目的。记住他在挑一个友善的同事。
  • 适当的称赞下面试官表达清晰、有气场,有奇效。
  • 电话面试时候,要学会“作弊”,用本子列一下自我介绍重点和项目重点,力求逻辑清晰。
  • 紧张在所难免,所以至少每天练习一次自我介绍和项目描述。

管理面 & HR 面

  • 把公司的技术架构和人员组织理顺一下,避免说的时候卡顿。
  • 老东家多不好都好,坏话也是半点说不得。离职的理由是考察你的性格的,尽量积极正面些(比如寻求更好的机遇,想要奋斗)。
  • 对一般的公司,报期待薪资时候不要太离谱,比之前的高 30-40% 就好。听猎头说,很多公司都在控制工资涨幅,JD 上写的 20-40K 看看就好。
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 25 次点赞
  • 获得 10 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2016-11-02
个人主页被 811 人浏览