从零开始学虚拟DOM
此文主要翻译自:Building a Simple Virtual DOM from Scratch,看原文的同学请直达!
此文是作者在一次现场编程演讲时现场所做的,有关演讲的相关资料我们也可以在原英文链接找到。
背景:什么是虚拟DOM
虚拟DOM指的是用于展现真实DOM的普通JS对象。简单说就是JS的普通对象,通过这个对象可以创建真实的DOM,它保存了创建真实DOM所需的所有东西。
Virtual DOMs usually refer to plain objects representing the actual DOMs.
The Document Object Model (DOM) is a programming interface for HTML documents.
比如,我们这样写:
const $app = document.getElementById('app');
浏览器会创建DOM:
, DOM也是一种对象,它提供了自己的接口,我们可以通过JS控制DOM,如:
$app.innerHTML = 'Hello world';
如果用一个对象来描述上面创建的真实DOM 我们可以像下面这样:
const vApp = {
tagName: 'div',
attrs: {
id: 'app',
}
};
对于虚拟DOM,还没有任何严格的规则要求要怎么创建,或者说创建的对象要遵守什么编程接口,规则等。例如上面的例子,你可以用tagLabel替代tagName,或者props替代attrs。只要它能创建一个真实的DOM,那它可以认为是一个虚拟DOM。
译者注:vue.js与react.js都使用了虚拟DOM,但他们的实现不一样
虚拟DOM不像真实的DOM,它没有提供编程接口,就是普通对象。所以跟真实的DOM相比,它更加轻量。
虽然虚拟DOM更加轻量,但真实DOM才是浏览器最基础的元素,大部分浏览器都对DOM的相关操作都做了大量的优化,所以真实的DOM操作可能不是像很多人说的那样慢的。
安装
我们先用mkdir创建一个项目目录,然后用cd进入刚创建的目录,如下:
$ mkdir /tmp/vdommm
$ cd /tmp/vdommm
然后初始化一个git仓库,用gitignorer创建.gitignore文件,然后用npm初始化项目,如:
$ git init
$ gitignore init node
$ npm init -y
译者注:gitignorer需要npm全局安装,没有安装的可以通过
npm instrall gitignorer -g
先安装。
现在我们可以先进行初次提交,将现有代码提交到git仓库,如:
$ git add -A
$ git commit -am ':tada: initial commit'
然后我们可以安装 Parcel Bundler(一个正在的零配置打包工具),它支持各种格式的开箱即用。在我做现场编码演讲时会经常用到它。
$ npm install parcel-bundler
译者注:Parcel跟Webpack功能类似,上手也快。
(有趣的是:安装的时候你不在需要 --save 参数), 趁安装parcel的时候,我们创建其他的文件,如下:
src/index.html
<html>
<head>
<title>hello world</title>
</head>
<body>
Hello world
<script src="./main.js"></script>
</body>
</html>
src/main.js
const vApp = {
tagName: 'div',
attrs: {
id: 'app',
},
};
console.log(vApp);
package.json
{
...
"scripts": {
"dev": "parcel src/index.html", // add this script
}
...
}
创建完上面的文件,我们可以直接执行下面的命令,如:
$ npm run dev
## 如果成功会输出下面
Server running at http://localhost:1234
√ Built in 1.26s.
打开浏览器访问: http://localhost:1234/ 你应该会看到 hello world字样,还有控制台会输出我们定义的虚拟DOM对象。如果一切如上,那么你的环境准备完毕。
createElement
大多的虚拟DOM实现都会有个叫createElement
的方法,通常简称为 h 。这个函数只是简单地返回一个“虚拟元素”,下面让我们看看它的实现。
src/vdom/createElement.js
export default (tagName, opts) => {
return {
tagName,
attrs: opts.attrs,
children: opts.children
};
};
利用对象的析构功能,我们可以改写上面的代码,如:
export default (tagName, {attrs, children}) => {
return {
tagName,
attrs,
children
};
};
当opts为空时,也应该可以创建虚拟元素,所以继续修改代码,如:
export default (tagName, {attrs={}, children=[]}={}) => {
return {
tagName,
attrs,
children
};
};
译者注:opts默认值为{},attrs默认值为{},children默认值为[]
定义了createElement方法,我们现在可以改写main.js,让main.js调用createElement方法创建虚拟DOM,
src/main.js
import createElement from "./vdom/createElement";
const vApp = createElement('div', {
attrs: {
id: 'app'
},
children: [
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif'
}
})
]
});
console.log(vApp);
修改后的main.js, 我们添加了一张来自giphy的图片。回到浏览器,刷新刚才的页面,你会看到控制台输出了新的虚拟DOM。
字面量对象(如:{a: 3})会自动继承自Object。就是说我们的虚拟DOM对象会自动包含hasOwnProperty, toString等这些方法。我们可以用Object.create(null)创建对象,这样不会继承只Object,也可以让我们的虚拟DOM更加的“纯”。所以修改createElement方法如下:
src/vdom/createElement.js
export default (tagName, {attrs={}, children=[]}={}) => {
const vElem = Object.create(null);
Object.assign(vElem, {
tagName,
attrs,
children
});
return vElem;
};
render(vNode)
渲染虚拟元素
现在我们已经有了一个函数能创建虚拟DOM,下面一个方法将虚拟DOM翻译成真实的DOM。定义render(vNode)
方法,如下:
src/vdom/render.js
const render = vNode => {
const $el = document.createElement(vNode.tagName);
for(const [k,v] of Object.entries(vNode.attrs)){
$el.setAttribute(k, v);
}
for(const child of vNode.children){
$el.appendChild(render(child));
}
return $el;
};
export default render;
上面的代码很简单,就不做过多解释了。
ElementNode与TextNode
对于真实的DOM,实际有8种类型的节点,这里我们只看2种类型:
- ElementNode, 比如:
,
- TextNode, 纯文本
看我们上面创建的虚拟DOM对象
{tagName, attrs, children}
,它只能创建真实DOM中的 ElementNode。因此,我们还需要增加能创建TextNode
的功能。我们将用String来创建TextNode为了好展示,让我们添加一些文本到虚拟DOM,如下:
src/main.js
import createElement from "./vdom/createElement"; const vApp = createElement('div', {
attrs: {
id: 'app'
},
children: [
'Hello world',
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif'
}
})
]
}); console.log(vApp);
扩展render函数以便支持文本节点(TextNode)
想我上面提到的,我们需要考虑两种类型的DOM节点。当前的render方法只能渲染ElementNode。所以让我们扩展render方法以便支持渲染文本节点。
我们将原来的render方法改名为renderElem,同时修改一下参数,将原来的vNode析构为
{tagName, attrs, children}
,如下:const renderElem = ({tagName, attrs, children}) => {
const $el = document.createElement(tagName);
for(const [k,v] of Object.entries(attrs)){
$el.setAttribute(k, v);
}
for(const child of children){
$el.appendChild(render(child));
}
return $el;
}; export default render;
接下来,我们重新定义一个render函数。新的render函数只需要检查vNode是否是string类型,如果是就调用document.createTextNode(string)来渲染文本节点,否则调用上面定义的renderElem即可,如下:
src/vdom/render.js
const renderElem = ({tagName, attrs, children}) => {
const $el = document.createElement(tagName);
for(const [k,v] of Object.entries(attrs)){
$el.setAttribute(k, v);
}
for(const child of children){
$el.appendChild(render(child));
}
return $el;
}; const render = vNode => {
if(typeof vNode === 'string'){
return document.createTextNode(vNode);
}
return renderElem(vNode);
}; export default render;
渲染我们的vApp
现在让我们来渲染我们的vApp,并输出它!
src/main.js
import createElement from "./vdom/createElement";
import render from "./vdom/render"; const vApp = createElement('div', {
attrs: {
id: 'app'
},
children: [
'Hello world',
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif'
}
})
]
});
const $app = render(vApp);
console.log($app);
控制台输出:
<div id="app">
Hello world
<img src="https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif">
</div>
mount($node, $target)
现在我们已经可以创建虚拟DOM,并且能将它渲染成真实的DOM。接下来我们需要把真实的DOM显示在页面上。
首先,我们需要一个挂载点,我们将原页面上的hello world替换为一个div元素,如下:
src/index.html
<html>
<head>
<title>hello world</title>
</head>
<body>
<div id="app"></div>
<script src="./main.js"></script>
</body>
</html>
接下来我们要做的是用$app替换掉空的div元素。如果不考虑IE与Safari这将非常容易,我们只要用ChildNode.replaceWith方法即可。
让我们定义render($node, $target)方法,它会将$target用$node替换掉,然后返回$node,如下:
src/vdom/mount.js
export default ($node, $target) => {
$target.replaceWith($node);
return $node;
};
现在修改main.js,调用mount方法,如下:
src/main.js
import createElement from "./vdom/createElement";
import render from "./vdom/render";
import mount from "./vdom/mount"; const vApp = createElement('div', {
attrs: {
id: 'app'
},
children: [
'Hello world',
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif'
}
})
]
});
const $app = render(vApp);
const $target = document.getElementById('app');
mount($app, $target);
让我们的app再有趣一点
接下来让我们的app变得有趣一点。我们用函数
createVApp
来创建vApp,createVApp接收一个count
参数,用于创建vApp。如下:src/main.js
import createElement from "./vdom/createElement";
import render from "./vdom/render";
import mount from "./vdom/mount"; const createVApp = count => createElement('div', {
attrs: {
id: 'app',
dataCount: count
},
children: [
'The current count is: ',
String(count),
createElement('img', {
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif'
}
})
]
}) let count = 0;
const vApp = createVApp(count);
const $app = render(vApp);
const $target = document.getElementById('app');
let $rootEl = mount($app, $target); setInterval(() => {
count++;
$rootEl = mount(render(createVApp(count)), $rootEl);
},1000) // function start(count){
// let vApp = createVApp(count);
// const $app = render(vApp);
// const $target = document.getElementById('app');
// return mount($app, $target);
// } // let count = 0;
// let $rootEl = start(count); // setInterval(() => {
// $rootEl = start(++count);
// },4000)我们用$rootEle保存每次挂载后的根元素,mount函数每次都挂载到新的rootEl元素。现在我们回到浏览器界面,你应该会看到计数每隔1秒增加一次,完美!
到现在我们已经可以以声明的方式创建应用了。通过上面的几行代码,应用能按照我们预期的渲染,这其中的秘密就是这么简单。如果你知道JQuery是怎么做渲染的,对比我们现在的方法,你将会感叹这是多么简洁的做法。
然而,上面的做法是每次每隔1秒会重新渲染整个节点,这将会有以下一些问题:
- 真实DOM比虚拟DOM笨重,每次都将整个节点重新渲染为真实DOM可能比较耗时。
- 元素会丢失状态。比如input元素会丢失焦点。
下面我们看看如何解决上面的问题。
diff(oldVTree, newVTree)
想像我们有一个diff(oldVTree, newVTree)函数,它用来比较旧的虚拟DOM与新的虚拟DOM之间的不同,为后面渲染做准备。diff函数返回一个patch函数,patch函数以旧的真实DOM为参数,对真实的DOM执行一些必要的操作,使其看上去像是从新的虚拟DOM创建出来的一样。
下面我们尝试实现diff函数,我们从简单的情形开始:
newVTree是undefined
因为新的虚拟DOM已经不存在了,所以在patch函数里面可以将传入的旧的真实DOM直接删除。
newVTree与oldVTree都是文本节点(TextNode)
当两者都是文本时,如果两者相等,则无需处理;如果不相等,则用新虚拟DOM渲染出来的元素替换掉旧的真实DOM,即:用render(newTree)替换$node.
一个是文本节点一个元素节点
这种情况,很显然两者不同,直接用新虚拟DOM渲染出来的元素替换掉旧的真实DOM。
新旧虚拟DOM的tagName不一样
tagName不一样,即为不同的元素,直接用新虚拟DOM渲染出来的元素替换掉旧的真实DOM。react的算法中也是这么做的。
src/vdom/diff.js
import render from './render'; const diff = (oldVTree, newVTree) => { // 如果新的虚拟dom是undefined
if(newVTree === undefined){
// 返回patch函数,$node为传入的旧的真实DOM元素
return $node => {
// 删除旧的元素
$node.remove();
// patch函数必须返回一个根元素,这种情况没有元素,所以返回undefined。
return undefined;
};
} // 如果两者都是文本节点
if(typeof oldVTree === 'string' || typeof newVTree === 'string'){
// 文本内容不等
if(oldVTree !== newVTree){
return $node => {
// 通过新的虚拟DOM渲染得到新的真实DOM
const $newNode = render(newVTree);
// 将新的DOM替换旧的
$node.replaceWith($newNode);
return $node;
}
}else{
// 文本相等,无需处理。
return $node => $node
}
} // 如果两者的tagName不同
if(oldVTree.tagName !== newVTree.tagName){
return $node => {
// 通过新的虚拟DOM渲染得到新的真实DOM
const $newNode = render(newVTree);
// 将新的DOM替换旧的
$node.replaceWith($newNode);
return $node;
}
} // (A) --- }; export default diff;
上面的代码完全根据我们比较算法实现,我们只考虑了三种大的情况(元素被删,文本类型,不同元素类型),如果代码执行到(A)的位置,那又如何处理?如果执行到(A)处,情况比较复杂,至少有以下几点我们知道:
- oldVTree与newVTree都是虚拟DOM
- 它们有相同的tagName
- 它们可能有不同的attrs和children
我们将实现两个独立的方法来处理attrs与children的比较。暂时分别命名为
diffAttrs(oldAttrs, newAttrs)
,diffChildren(oldVChildren, newVChildren)
,它们都会返回各自的patch函数。src/vdom/diff.js
// 此处省略上面的相同代码
... const patchAttrs = diffAttrs(oldVTree.attrs, newVTree.attrs);
const patchChildren = diffChildren(oldVTree.children, newVTree.children); return $node => {
patchAttrs($node);
patchChildren($node);
return $node;
} ....
diffAttrs(oldAttrs, newAttrs)
让我聚焦到diffAttrs函数。实际它非常简单。首先我们要将所有新的属性设置到dom上,然后将那些存在于旧的属性而不存在新的属性的属性全部删除。代码如下:
const diffAttrs = (oldAttrs, newAttrs) => {
const patches = []; // patch函数的数组
// 将新的属性设置进去
for(const [k, v] of Object.entries(newAttrs)){
patches.push($node => {
$node.setAttribute(k, v);
return $node;
});
}
// 删除不存在于新属性集而存在于旧属性集的属性
for(const k in oldAttrs){
if(!(k in newAttrs)){
patches.push($node => {
$node.removeAttribute(k);
return $node;
});
}
} return $node => {
for(const patch of patches){
patch($node);
}
return $node;
} };
diffChildren(oldVChildren, newVChildren)
Children的比较会有点复杂,我们需要考虑下面三种情况:
oldVChildren.length === newVChildren.length
说明子元素个数一样,此时我们必须对子元素逐个比较,即:diff(oldVChildren[i], newVChildren[i]),i从0到oldVChildren.length
oldVChildren.length > newVChildren.length
还是需要逐个比较子元素,直到newVChildren变为undefined,因为我们在diff里考虑了newVTree为undefined的情况,所以这种情况的处理方法跟第一种一样,即:diff(oldVChildren[i], newVChildren[i]),i从0到oldVChildren.length
oldVChildren.length < newVChildren.length
先遍历oldVChildren,执行diff(oldVChildren[i], newVChildren[i]),然后将newVChildren中没有遍历到的虚拟dom,渲染为真实dom,手动加入到旧dom的子元素中。
const diffChildren = (oldVChildren, newVChildren) => {
const childrenPatches = [];
// 1. 当oldVChildren.length === newVChildren.length
// 2. 当oldVChildren.length > newVChildren.length
// 3. 当oldVChildren.length < newVChildren.length
oldVChildren.forEach((oldVChild, i)=>{
childrenPatches.push(diff(oldVChild, newVChildren[i]));
}); // 上面的执行完毕后,只有第三种情况中newVChildren中的部分节点没有处理到
// 处理余下的节点
const additionalPatches = [];
for(const additionalVChild of newVChildren.slice(oldVChildren.length)){
additionalPatches.push($node => {
$node.appendChild(render(additionalVChild));
return $node;
});
} return $parent => {
$parent.childNodes.forEach(($child, i)=>{
childrenPatches[i]($child);
});
for(const patch of additionalPatches){
patch($parent);
}
return $parent;
}
};
译者注:作者原文用了一个zip函数来同时处理两个数组的循环,这里没有放出来。
最终的diff.js
import render from './render'; const diffAttrs = (oldAttrs, newAttrs) => {
const patches = []; // patch函数的数组
// 将新的属性设置进去
for(const [k, v] of Object.entries(newAttrs)){
patches.push($node => {
$node.setAttribute(k, v);
return $node;
});
}
// 删除不存在于新属性集而存在于旧属性集的属性
for(const k in oldAttrs){
if(!(k in newAttrs)){
patches.push($node => {
$node.removeAttribute(k);
return $node;
});
}
} return $node => {
for(const patch of patches){
patch($node);
}
return $node;
} }; const diffChildren = (oldVChildren, newVChildren) => {
const childrenPatches = [];
// 1. 当oldVChildren.length === newVChildren.length
// 2. 当oldVChildren.length > newVChildren.length
// 3. 当oldVChildren.length < newVChildren.length
oldVChildren.forEach((oldVChild, i)=>{
childrenPatches.push(diff(oldVChild, newVChildren[i]));
}); // 上面的执行完毕后,只有第三种情况中newVChildren中的部分节点没有处理到
// 处理余下的节点
const additionalPatches = [];
for(const additionalVChild of newVChildren.slice(oldVChildren.length)){
additionalPatches.push($node => {
$node.appendChild(render(additionalVChild));
return $node;
});
} return $parent => {
$parent.childNodes.forEach(($child, i)=>{
// childrenPatches[i]($child); -- 原文有错
// 因为diff可能返回undefined,所以需要判断patch是否存在。
childrenPatches[i]&&childrenPatches[i]($child);
});
for(const patch of additionalPatches){
patch($parent);
}
return $parent;
}
}; const diff = (oldVTree, newVTree) => { // 如果新的虚拟dom是undefined
if(newVTree === undefined){
// 返回patch函数,$node为传入的旧的真实DOM元素
return $node => {
// 删除旧的元素
$node.remove();
// patch函数必须返回一个根元素,这种情况没有元素,所以返回undefined。
return undefined;
};
} // 如果两者都是文本节点
if(typeof oldVTree === 'string' || typeof newVTree === 'string'){
// 文本内容不等
if(oldVTree !== newVTree){
return $node => {
// 通过新的虚拟DOM渲染得到新的真实DOM
const $newNode = render(newVTree);
// 将新的DOM替换旧的
$node.replaceWith($newNode);
return $node;
}
}else{
// 文本相等,无需处理。
return $node => $node
}
} // 如果两者的tagName不同
if(oldVTree.tagName !== newVTree.tagName){
return $node => {
// 通过新的虚拟DOM渲染得到新的真实DOM
const $newNode = render(newVTree);
// 将新的DOM替换旧的
$node.replaceWith($newNode);
return $node;
}
} const patchAttrs = diffAttrs(oldVTree.attrs, newVTree.attrs);
const patchChidlren = diffChildren(oldVTree.children, newVTree.children); return $node => {
patchAttrs($node);
patchChidlren($node);
return $node;
}; }; export default diff;
让我们的app再复杂一点
我们当前的应用实际是没有用到虚拟DOM的全部功能的。为了展示虚拟DOM的强大功能,让我们再次修改我们应用,让它变得再复杂一点。
src/main.js
import createElement from "./vdom/createElement";
import render from "./vdom/render";
import mount from "./vdom/mount";
import diff from './vdom/diff'; const createVApp = count => createElement('div', {
attrs: {
id: 'app',
dataCount: count
},
children: [
'The current count is: ',
String(count),
...Array.from({length: count}, ()=> createElement('img',{
attrs: {
src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif'
}
}))
]
}) let vApp = createVApp(0);
const $app = render(vApp);
const $target = document.getElementById('app');
let $rootEl = mount($app, $target); setInterval(() => {
const n = Math.floor(Math.random()*10);
let newVApp = createVApp(n);
const patch = diff(vApp, newVApp);
$rootEl = patch($rootEl);
vApp = newVApp;
},1000) // function start(count){
// let vApp = createVApp(count);
// const $app = render(vApp);
// const $target = document.getElementById('app');
// return mount($app, $target);
// } // let count = 0;
// let $rootEl = start(count); // setInterval(() => {
// $rootEl = start(++count);
// },4000)感谢
感谢您花时间一直阅读到这里,这篇文章确实有点长。读完还请留言!~
原文中已经提供了文章中所有代码的链接,还有git仓库。这里我也按照作者的代码自己实现了一遍,需要的请链接:
从零开始学虚拟DOM的更多相关文章
- 从零开始学 Web 之 DOM(一)DOM的概念,对标签操作
大家好,这里是「 Daotin的梦呓 」从零开始学 Web 系列教程.此文首发于「 Daotin的梦呓 」公众号,欢迎大家订阅关注.在这里我会从 Web 前端零基础开始,一步步学习 Web 相关的知识 ...
- 从零开始学 Web 之 DOM(三)innerText与innerHTML、自定义属性
大家好,这里是「 Daotin的梦呓 」从零开始学 Web 系列教程.此文首发于「 Daotin的梦呓 」公众号,欢迎大家订阅关注.在这里我会从 Web 前端零基础开始,一步步学习 Web 相关的知识 ...
- 从零开始学 Web 之 DOM(二)对样式的操作,获取元素的方式
大家好,这里是「 Daotin的梦呓 」从零开始学 Web 系列教程.此文首发于「 Daotin的梦呓 」公众号,欢迎大家订阅关注.在这里我会从 Web 前端零基础开始,一步步学习 Web 相关的知识 ...
- 从零开始学 Web 之 DOM(四)节点
大家好,这里是「 Daotin的梦呓 」从零开始学 Web 系列教程.此文首发于「 Daotin的梦呓 」公众号,欢迎大家订阅关注.在这里我会从 Web 前端零基础开始,一步步学习 Web 相关的知识 ...
- 从零开始学 Web 之 DOM(五)元素的创建
大家好,这里是「 从零开始学 Web 系列教程 」,并在下列地址同步更新...... +-------------------------------------------------------- ...
- 从零开始学 Web 之 DOM(六)为元素绑定与解绑事件
大家好,这里是「 从零开始学 Web 系列教程 」,并在下列地址同步更新...... +-------------------------------------------------------- ...
- 从零开始学 Web 之 DOM(七)事件冒泡
大家好,这里是「 从零开始学 Web 系列教程 」,并在下列地址同步更新...... +-------------------------------------------------------- ...
- 【高德地图API】从零开始学高德JS API(二)地图控件与插件——测距、圆形编辑器、鼠标工具、地图类型切换、鹰眼鱼骨
原文:[高德地图API]从零开始学高德JS API(二)地图控件与插件——测距.圆形编辑器.鼠标工具.地图类型切换.鹰眼鱼骨 摘要:无论是控件还是插件,都是在一级API接口的基础上,进行二次开发,封装 ...
- 从零开始学 Web 系列教程
大家好,这里是「 从零开始学 Web 系列教程 」,并在下列地址同步更新…… github:https://github.com/Daotin/Web 微信公众号:Web前端之巅 博客园:http:/ ...
随机推荐
- 太厉害了,终于有人能把TCP/IP协议讲的明明白白了!
从字面意义上讲,有人可能会认为 TCP/IP 是指 TCP 和 IP 两种协议.实际生活当中有时也确实就是指这两种协议.然而在很多情况下,它只是利用 IP 进行通信时所必须用到的协议群的统称.具体来说 ...
- Java基础 throw 抛出异常后,用try...catch捕获
JDK :OpenJDK-11 OS :CentOS 7.6.1810 IDE :Eclipse 2019‑03 typesetting :Markdown code ...
- NazoHell 攻略
http://hell.one-story.cn/hell-start.html Level 0: http://nazohell.one-story.cn/nazohell-start.html 跳 ...
- 简易商城 [ html + css ] 练习
1. 前言 通过使用 HTML + CSS 编写一个简易商城首页. 如图: 2. 布局思路 通过页面分析,大致可以决定页面的布局分为 5 大板块. 接下来,可以先定义页面的布局: <!DOCTY ...
- js set集合转数组 Array.from的使用方法
1.set集合转化Array数组 注意:这个可以使用过滤数组中的重复的元素 你可以先把数组转化为set集合 然后在把这个集合通过Array.from这个方法把集合在转化为数组 var set = n ...
- [LeetCode] 278. First Bad Version 第一个坏版本
You are a product manager and currently leading a team to develop a new product. Unfortunately, the ...
- 在ensp上利用单臂路由实验VLAN间路由
我们为什么要设置单臂路由? 因为我们要解决不同vlan,不同网络的PC机间的通信问题~ 那它为啥叫单臂路由嘞? 单臂路由的原理时通过一台路由器,使vlan间互通数据通过路由器进行三层转发,如果在路由器 ...
- 通过元类创建一个Python类
通过元类创建一个Python类 最开始学pytohn的时候我们这样定义类 class ClassName: pass 当熟悉了元类的概念之后我们还可以这样创建 ClassName = type(&qu ...
- 数据分析-numpy的用法
一.jupyter notebook 两种安装和启动的方式: 第一种方式: 命令行安装:pip install jupyter 启动:cmd 中输入 jupyter notebook 缺点:必须手动去 ...
- TypeScript之接口
1.写法 // 属性 interface Person { name:string; age:number; hobby: string; } // 函数 interface { todo(para ...