React是Facebook开发的一款JS库,那么Facebook为什么要建造React呢,主要为了解决什么问题,通过这个又是如何解决的?
从这几个问题出发我就在网上搜查了一下,有这样的解释。
Facebook认为MVC无法满足他们的扩展需求,由于他们非常巨大的代码库和庞大的组织,使得MVC很快变得非常复复杂,每当需要添加一项新的功能或特性时,系统的复杂度就成级数增长,致使代码变得脆弱和不可预测,结果导致他们的MVC正在土崩瓦解。认为MVC不适合大规模应用,当系统中有很多的模型和相应的视图时,其复杂度就会迅速扩大,非常难以理解和调试,特别是模型和视图间可能存在的双向数据流动。
解决这个问题需要“以某种方式组织代码,使其更加可预测”,这通过他们(Facebook)提出的Flux和React已经完成。
Flux
是一个系统架构,用于推进应用中的数据单向流动。React
是一个JavaScript框架,用于构建“可预期的”和“声明式的”Web用户界面,它已经使Facebook更快地开发Web应用
对于Flux,目前还没怎么研究,不怎么懂,这里就先把Flux的图放上来,有兴趣或者了解的可以再分享下,这里主要说下React。
那么React是解决什么问题的,在官网可以找到这样一句话:
We built React to solve one problem: building large applications with data that changes over time.
构建那些数据会随时间改变的大型应用,做这些,React有两个主要的特点:
另外在React官网上,通过《Why did we build React?》为什么我们要建造React的文档中还可以了解到以下四点:
Virtual DOM 虚拟DOM
传统的web应用,操作DOM一般是直接更新操作的,但是我们知道DOM更新通常是比较昂贵的。而React为了尽可能减少对DOM的操作,提供了一种不同的而又强大的方式来更新DOM,代替直接的DOM操作。就是Virtual DOM
,一个轻量级的虚拟的DOM,就是React抽象出来的一个对象,描述dom应该什么样子的,应该如何呈现。通过这个Virtual DOM去更新真实的DOM,由这个Virtual DOM管理真实DOM的更新。
为什么通过这多一层的Virtual DOM操作就能更快呢? 这是因为React有个diff算法,更新Virtual DOM并不保证马上影响真实的DOM,React会等到事件循环结束,然后利用这个diff算法,通过当前新的dom表述与之前的作比较,计算出最小的步骤更新真实的DOM。
component 的使用在 React 里极为重要, 因为 components 的存在让计算 DOM diff 更。
State 和 Render
React是如何呈现真实的DOM,如何渲染组件,什么时候渲染,怎么同步更新的,这就需要简单了解下State和Render了。state属性包含定义组件所需要的一些数据,当数据发生变化时,将会调用Render重现渲染,这里只能通过提供的setState方法更新数据。
好了,说了这么多,下面看写代码吧,先看一个官网上提供的Hello World
的示例:
<!DOCTYPE html> <html> <head> <script src="http://fb.me/react-0.12.1.js"></script> <script src="http://fb.me/JSXTransformer-0.12.1.js"></script> </head> <body> <div id="example"></div> <script type="text/jsx"> React.render( <h1>Hello, world!</h1>,
document.getElementById('example')
); </script> </body> </html>
这个很简单,浏览器访问,可以看到Hello, world!
字样。JSXTransformer.js
是支持解析JSX语法的,JSX是可以在Javascript中写html代码的一种语法。如果不喜欢,React也提供原生Javascript的方法。
再来看下另外一个例子:
<html> <head> <title>Hello React</title> <script src="http://fb.me/react-0.12.1.js"></script> <script src="http://fb.me/JSXTransformer-0.12.1.js"></script> <script src="http://code.jquery.com/jquery-1.10.0.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script> <style> #content{ width: 800px; margin: 0 auto; padding: 5px 10px; background-color:#eee; } .commentBox h1{ background-color: #bbb; } .commentList{ border: 1px solid yellow; padding:10px; } .commentList .comment{ border: 1px solid #bbb; padding-left: 10px; margin-bottom:10px; } .commentList .commentAuthor{ font-size: 20px; } .commentForm{ margin-top: 20px; border: 1px solid red; padding:10px; } .commentForm textarea{ width:100%; height:50px; margin:10px 0 10px 2px; } </style> </head> <body> <div id="content"></div> <script type="text/jsx"> var staticData = [ {author: "张飞", text: "我在写一条评论~!"}, {author: "关羽", text: "2货,都知道你在写的是一条评论。。"}, {author: "刘备", text: "哎,咋跟这俩逗逼结拜了!"} ]; var converter = new Showdown.converter();//markdown /** 组件结构: <CommentBox> <CommentList> <Comment /> </CommentList> <CommentForm /> </CommentBox> */ //评论内容组件 var Comment = React.createClass({ render: function (){ var rawMarkup = converter.makeHtml(this.props.children.toString()); return ( <div className="comment"> <h2 className="commentAuthor"> {this.props.author}: </h2> <span dangerouslySetInnerHTML={{__html: rawMarkup}} /> </div> ); } }); //评论列表组件 var CommentList = React.createClass({ render: function (){ var commentNodes = this.props.data.map(function (comment){ return ( <Comment author={comment.author}> {comment.text} </Comment> ); }); return ( <div className="commentList"> {commentNodes} </div> ); } }); //评论表单组件 var CommentForm = React.createClass({ handleSubmit: function (e){ e.preventDefault(); var author = this.refs.author.getDOMNode().value.trim(); var text = this.refs.text.getDOMNode().value.trim(); if(!author || !text){ return; } this.props.onCommentSubmit({author: author, text: text}); this.refs.author.getDOMNode().value = ''; this.refs.text.getDOMNode().value = ''; return; }, render: function (){ return ( <form className="commentForm" onSubmit={this.handleSubmit}> <input type="text" placeholder="Your name" ref="author" /><br/> <textarea type="text" placeholder="Say something..." ref="text" ></textarea><br/> <input type="submit" value="Post" /> </form> ); } }); //评论块组件 var CommentBox = React.createClass({ loadCommentsFromServer: function (){ this.setState({data: staticData}); /* 方便起见,这里就不走服务端了,可以自己尝试 $.ajax({ url: this.props.url + "?_t=" + new Date().valueOf(), dataType: 'json', success: function (data){ this.setState({data: data}); }.bind(this), error: function (xhr, status, err){ console.error(this.props.url, status, err.toString()); }.bind(this) }); */ }, handleCommentSubmit: function (comment){ //TODO: submit to the server and refresh the list var comments = this.state.data; var newComments = comments.concat([comment]); //这里也不向后端提交了 staticData = newComments; this.setState({data: newComments}); }, //初始化 相当于构造函数 getInitialState: function (){ return {data: []}; }, //组件添加的时候运行 componentDidMount: function (){ this.loadCommentsFromServer(); this.interval = setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, //组件删除的时候运行 componentWillUnmount: function() { clearInterval(this.interval); }, //调用setState或者父级组件重新渲染不同的props时才会重新调用 render: function (){ return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data}/> <CommentForm onCommentSubmit={this.handleCommentSubmit} /> </div> ); } }); //当前目录需要有comments.json文件 //这里定义属性,如url、pollInterval,包含在props属性中 React.render( <CommentBox url="comments.json" pollInterval="2000" />, document.getElementById("content") ); </script> </body> </html>
乍一看挺多,主要看脚本部分就可以了。方便起见,这里都没有走后端。定义了一个全局的变量staticData
,可权当是走服务端,通过浏览器的控制台改变staticData
的值,查看下效果,提交一条评论,查看下staticData的值的变化。
国外应用的较多,facebook、Yahoo、Reddit等。在github可以看到一个列表Sites-Using-React,国内的话,查了查,貌似比较少,目前知道的有一个杭州大搜车。大多技术要在国内应用起来一般是较慢的,不过React确实感觉比较特殊,特别是UI的组件化和Virtual DOM的思想,我个人比较看好,有兴趣继续研究研究。
和其他一些js框架相比,React怎样,比如Backbone、Angular等。
闭包是一个让初级JavaScript
使用者既熟悉又陌生的一个概念。因为闭包在我们书写JavaScript
代码时,随处可见,但是我们又不知道哪里用了闭包。
关于闭包的定义,网上(书上)的解释总是千奇百怪,我们也只能“取其精华去其糟粕”去总结一下。
ECMAScript中,闭包指的是:
从实践角度:一下才算是闭包:
闭包跟词法作用域,作用域链,执行上下文这几个JavaScript
中重要的概念都有关系,因此要想真的理解闭包,至少要对那几个概念不陌生。
闭包的优点:
闭包的缺点:
我们来一步一步引出闭包。
自执行函数也叫立即调用函数(IIFE),是一个在定义时就执行的函数。
var a=1;
(function() { console.log(a)
})()
上述代码是一个最简单的自执行函数。
在ES6之前,是没有块级作用域的,只有全局作用域和函数作用域,因此自执行函数还能在ES6之前实现块级作用域。
// ES6 块级作用域 var a = 1; if(true) { let a=111; console.log(a); // 111 } console.log(a); // 1
这里 if{} 中用let声明了一个 a。这个 a 就具有块级作用域,在这个 {} 中访问 a ,永远访问的都是 let 声明的a,跟全局作用域中的a没有关系。如果我们把 let 换成 var ,就会污染全局变量 a 。
如果用自执行函数来实现:
var a = 1;
(function() { if(true) { var a=111; console.log(a); // 111 }
})() console.log(a); // 1
为什么要在这里要引入自执行函数的概念呢?因为通常我们会用自执行函数来创建闭包,实现一定的效果。
来看一个基本上面试提问题:
for(var i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
在理想状态下我们期望输出的是 0 ,1 ,2 ,3 ,4。但是实际上输出的是5 ,5 ,5 ,5 ,5。为什么是这样呢?其实这里不仅仅涉及到作用域,作用域链还涉及到Event Loop、微任务、宏任务。但是在这里不讲这些。
下面我们先解释它为什么会输出 5个5,然后再用自执行函数来修改它,以达到我们预期的结果。
提示:for 循环中,每一次的都声明一个同名变量,下一个变量的值为上一次循环执行完同名变量的值。
首先用var声明变量 for 是不会产生块级作用域的,所以在 () 中声明的 i 为全局变量。相当于:
// 伪代码 var i; for(i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
setTimeout中的第一个参数为一个全局的匿名函数。相当于:
// 伪代码 var i; var f = function() { console.log(i);
} for(i=0;i<5;i++) {
setTimeout(f,1000)
}
由于setTimeout是在1秒之后执行的,这个时候for循环已经执行完毕,此时的全局变量 i 已经变成了 5 。1秒后5个setTimeout中的匿名函数会同时执行,也就是5个 f 函数执行。这个时候 f 函数使用的变量 i 根据作用域链的查找规则找到了全局作用域中的 i 。因此会输出 5 个5。
那我们怎样来修改它呢?
for(var i=0;i<5;i++) {
(function (){ setTimeout(function() { console.log(i);
},1000)
})();
}
上述例子会输出我们期望的值吗?答案是否。为什么呢?我们虽然把 setTimeout 包裹在一个匿名函数中了,但是当setTimeout中匿名函数执行时,首先去匿名函数中查找 i 的值,找不到还是会找到全局作用域中,最终 i 的值仍然是全局变量中的 i ,仍然为 5个5.
那我们把外层的匿名函数中声明一个变量 j 让setTimeout中的匿名函数访问这个 j 不就找不到全局变量中的变量了吗。
for(var i=0;i<5;i++) {
(function (){ var j = i;
setTimeout(function() { console.log(j);
},1000)
})();
}
这个时候才达到了我们预期的结果:0 1 2 3 4。
我们来优化一下:
for(var i=0;i<5;i++) {
(function (i){ setTimeout(function() { console.log(i);
},1000)
})(i);
}
*思路2:用 let 声明变量,产生块级作用域。
for(let i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
这时for循环5次,产生 5 个块级作用域,也会声明 5 个具有块级作用域的变量 i ,因此setTimeout中的匿名函数每次执行时,访问的 i 都是当前块级作用域中的变量 i 。
什么是理论中的闭包?就是看似像闭包,其实并不是闭包。它只是类似于闭包。
function foo() { var a=2; function bar() { console.log(a); // 2 }
bar();
}
foo();
上述代码根据最上面我们对闭包的定义,它并不完全是闭包,虽然是一个函数可以访问另一个函数中的变量,但是被嵌套的函数是在当前词法作用域中被调用的。
我们怎样把上述代码foo 函数中的bar函数,在它所在的词法作用域外执行呢?
下面的代码就清晰的展示了闭包:
function foo() { var a=2; function bar() { console.log(a);
} return bar;
} var baz=foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
上述代码中 bar 被当做 foo函数返回值。foo函数执行后把返回值也就是 bar函数 赋值给了全局变量 baz。当 baz 执行时,实际上也就是 bar 函数的执行。我们知道 foo 函数在执行后,foo 的内部作用域会被销毁,因为引擎有垃圾回收期来释放不再使用的内存空间。所以在bar函数执行时,实际上foo函数内部的作用域已经不存在了,理应来说 bar函数 内部再访问 a 变量时是找不到的。但是闭包的神奇之处就在这里。由于 bar 是在 foo 作用域中被声明的,所以 bar函数 会一直保存着对 foo 作用域的引用。这时就形成了闭包。
我们先看个例子:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope;
} return f;
} var foo = checkscope();
foo();
我们用伪代码来解释JavaScript
引擎在执行上述代码时的步骤:
JavaScript
引擎遇到可执行代码时,就会进入一个执行上下文(环境)
但是我们想一个问题,checkscope函数执行完毕,它的执行上下文从栈中弹出,也就是销毁了不存在了,f 函数还能访问包裹函数的作用域中的变量(scope)吗?答案是可以。
理由是在第6步,我们说过当checkscope 执行函数执行完毕时,它的执行上下文会从栈中弹出,此时活动对象也会被回收,按理说当 f 在访问checkscope的活动对象时是访问不到的。
其实这里还有个概念,叫做作用域链:当 checkscope 函数被创建时,会创建对应的作用域链,里面值存放着包裹它的作用域对应执行上下文的变量对象,在这里只是全局执行上下文的变量对象,当checkscope执行时,此时的作用域链变化了 ,里面存放的是变量对象(活动对象)的集合,最顶端是当前函数的执行上下文的活动对象。端是全局执行上下文的变量对象。类似于:
checkscope.scopeChain = [
checkscope.AO
global.VO
]
当checkscope执行碰到了 f 函数的创建,因此 f 函数也会创建对应的作用域链,默认以包裹它的函数执行时对应的作用域链为基础。因此此时 f 函数创建时的作用域链如下:
checkscope.scopeChain = [
checkscope.AO
global.VO
]
当 f 函数执行时,此时的作用域链变化如下:
checkscope.scopeChain = [
f.AO
checkscope.AO
global.VO
]
当checkscope函数执行完毕,内部作用域会被回收,但是 f函数 的作用域链还是存在的,里面存放着 checkscope函数的活动对象,因此在f函数执行时会从作用域链中查找内部使用的 scope 标识符,从而在作用域链的第二位找到了,也就是在 checkscope.AO 找到了变量scope的值。
正是因为JavaScript
做到了这一点,因此才会有闭包的概念。还有人说闭包并不是为了拥有它采取设计它的,而是设计作用域链时的副作用产物。
闭包是JavaScript
中最难的点,也是平常面试中常问的问题,我们必须要真正的去理解它,如果只靠死记硬背是经不起考验的。
this
this是我们在书写代码时最常用的关键词之一,即使如此,它也是JavaScript最容易被最头疼的关键词。那么this到底是什么呢?
如果你了解执行上下文,那么你就会知道,其实this是执行上下文对象的一个属性:
executionContext = {
scopeChain:[ ... ],
VO:{
...
},
this: ?
}
执行上下文中有三个重要的属性,作用域链(scopeChain)、变量对象(VO)和this。
this是在进入执行上下文时确定的,也就是在函数执行时才确定,并且在运行期间不允许修改并且是永久不变的
在全局代码中的this
在全局代码中this 是不变的,this始终是全局对象本身。
var a = 10;
this.b = 20;
window.c = 30;
console.log(this.a);
console.log(b);
console.log(this.c);
console.log(this === window) // true
// 由于this就是全局对象window,所以上述 a ,b ,c 都相当于在全局对象上添加相应的属性
如果我们在代码运行期尝试修改this的值,就会抛出错误:
this = { a : 1 } ; // Uncaught SyntaxError: Invalid left-hand side in assignment
console.log(this === window) // true
函数代码中的this
在函数代码中使用this,才是令我们最容易困惑的,这里我们主要是对函数代码中的this进行分析。
我们在上面说过this的值是,进入当前执行上下文时确定的,也就是在函数执行时并且是执行前确定的。但是同一个函数,作用域中的this指向可能完全不同,但是不管怎样,函数在运行时的this的指向是不变的,而且不能被赋值。
function foo() {
console.log(this);
}
foo(); // window
var obj={
a: 1,
bar: foo,
}
obj.bar(); // obj
函数中this的指向丰富的多,它可以是全局对象、当前对象、或者是任意对象,当然这取决于函数的调用方式。在JavaScript中函数的调用方式有一下几种方式:作为函数调用、作为对象属性调用、作为构造函数调用、使用apply或call调用。下面我们将按照这几种调用方式一一讨论this的含义。
作为函数调用
什么是作为函数调用:就是独立的函数调用,不加任何修饰符。
function foo(){
console.log(this === window); // true
this.a = 1;
console.log(b); // 2
}
var b = 2;
foo();
console.log(a); // 1
上述代码中this绑定到了全局对象window。this.a相当于在全局对象上添加一个属性 a 。
在严格模式下,独立函数调用,this的绑定不再是window,而是undefined。
function foo() {
"use strict";
console.log(this===window); // false
console.log(this===undefined); // true
}
foo();
这里要注意,如果函数调用在严格模式下,而内部代码执行在非严格模式下,this 还是会默认绑定为 window。
function foo() {
console.log(this===window); // true
}
(function() {
"use strict";
foo();
})()
对于在函数内部的函数独立调用 this 又指向了谁呢?
function foo() {
function bar() {
this.a=1;
console.log(this===window); // true
}
bar()
}
foo();
console.log(a); // 1
上述代码中,在函数内部的函数独立调用,此时this还是被绑定到了window。
总结:当函数作为独立函数被调用时,内部this被默认绑定为(指向)全局对象window,但是在严格模式下会有区别,在严格模式下this被绑定为undefined。
作为对象属性调用
var a=1;
var obj={
a: 2,
foo: function() {
console.log(this===obj); // true
console.log(this.a); // 2
}
}
obj.foo();
上述代码中 foo属性的值为一个函数。这里称 foo 为 对象obj 的方法。foo的调用方式为 对象 . 方法 调用。此时 this 被绑定到当前调用方法的对象。在这里为 obj 对象。
再看一个例子:
var a=1;
var obj={
a: 2,
bar: {
a: 3,
foo: function() {
console.log(this===bar); // true
console.log(this.a); // 3
}
}
}
obj.bar.foo();
遵循上面说的规则 对象 . 属性 。这里的对象为 obj.bar 。此时 foo 内部this被绑定到了 obj.bar 。 因此 this.a 即为 obj.bar.a 。
再来看一个例子:
var a=1;
var obj={
a: 2,
foo: function() {
console.log(this===obj); // false
console.log(this===window); // true
console.log(this.a); // 1
}
}
var baz=obj.foo;
baz();
这里 foo 函数虽然作为对象obj 的方法。但是它被赋值给变量 baz 。当baz调用时,相当于 foo 函数独立调用,因此内部 this被绑定到 window。
使用apply或call调用
apply和call为函数原型上的方法。它可以更改函数内部this的指向。
var a=1;
function foo() {
console.log(this.a);
}
var obj1={
a: 2
}
var obj2={
a: 3
}
var obj3={
a: 4
}
var bar=foo.bind(obj1);
bar();// 2 this => obj1
foo(); // 1 this => window
foo.call(obj2); // 3 this => obj2
foo.call(obj3); // 4 this => obj3
当函数foo 作为独立函数调用时,this被绑定到了全局对象window,当使用bind、call或者apply方法调用时,this 被分别绑定到了不同的对象。
作为构造函数调用
var a=1;
function Person() {
this.a=2; // this => p;
}
var p=new Person();
console.log(p.a); // 2
上述代码中,构造函数 Person 内部的 this 被绑定为 Person的一个实例。
总结:
当我们要判断当前函数内部的this绑定,可以依照下面的原则:
函数是否在是通过 new 操作符调用?如果是,this 绑定为新创建的对象
var bar = new foo(); // this => bar;
函数是否通过call或者apply调用?如果是,this 绑定为指定的对象
foo.call(obj1); // this => obj1;
foo.apply(obj2); // this => obj2;
函数是否通过 对象 . 方法调用?如果是,this 绑定为当前对象
obj.foo(); // this => obj;
函数是否独立调用?如果是,this 绑定为全局对象。
foo(); // this => window
DOM事件处理函数中的this
1). 事件绑定
<button id="btn">点击我</button>
// 事件绑定
function handleClick(e) {
console.log(this); // <button id="btn">点击我</button>
}
document.getElementById('btn').addEventListener('click',handleClick,false); // <button id="btn">点击我</button>
document.getElementById('btn').onclick= handleClick; // <button id="btn">点击我</button>
根据上述代码我们可以得出:当通过事件绑定来给DOM元素添加事件,事件将被绑定为当前DOM对象。
2).内联事件
<button onclick="handleClick()" id="btn1">点击我</button>
<button onclick="console.log(this)" id="btn2">点击我</button>
function handleClick(e) {
console.log(this); // window
}
//第二个 button 打印的是 <button id="btn">点击我</button>
我认为内联事件可以这样理解:
//伪代码
<button onclick=function(){ handleClick() } id="btn1">点击我</button>
<button onclick=function() { console.log(this) } id="btn2">点击我</button>
这样我们就能理解上述代码中为什么内联事件一个指向window,一个指向当前DOM元素。(当然浏览器处理内联事件时并不是这样的)
定时器中的this
定时器中的 this 指向哪里呢?
function foo() {
setTimeout(function() {
console.log(this); // window
},1000)
}
foo();
再来看一个例子
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
console.log(this.name); // chen
},1000)
}
}
obj.foo();
到这里我们可以看到,函数 foo 内部this指向为调用它的对象,即:obj 。定时器中的this指向为 window。那么有什么办法让定时器中的this跟包裹它的函数绑定为同一个对象呢?
1). 利用闭包:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name) // erdong
var that=this;
setTimeout(function() {
// that => obj
console.log(that.name); // erdong
},1000)
}
}
obj.foo();
利用闭包的特性,函数内部的函数可以访问含义访问当前词法作用域中的变量,此时定时器中的 that 即为包裹它的函数中的 this 绑定的对象。在下面我们会介绍利用 ES6的箭头函数实现这一功能。
当然这里也可以适用bind来实现:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
// this => obj
console.log(this.name); // erdong
}.bind(this),1000)
}
}
obj.foo();
被忽略的this
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call 、apply或者bind,这些值在调用时会被忽略,实例 this 被绑定为对应上述规则。
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
foo.call(null);
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
foo.apply(null);
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
var bar = foo.bind(null);
bar();
bind 也可以实现函数柯里化:
function foo(a,b) {
console.log(a,b); // 2 3
}
var bar=foo.bind(null,2);
bar(3);
更复杂的例子:
var foo={
bar: function() {
console.log(this);
}
};
foo.bar(); // foo
(foo.bar)(); // foo
(foo.bar=foo.bar)(); // window
(false||foo.bar)(); // window
(foo.bar,foo.bar)(); // window
上述代码中:
foo.bar()为对象的方法调用,因此 this 绑定为 foo 对象。
(foo.bar)() 前一个() 中的内容不计算,因此还是 foo.bar()
(foo.bar=foo.bar)() 前一个 () 中的内容计算后为 function() { console.log(this); } 所以这里为匿名函数自执行,因此 this 绑定为 全局对象 window
后面两个实例同上。
这样理解会比较好:
(foo.bar=foo.bar) 括号中的表达式执行为 先计算,再赋值,再返回值。
(false||foo.bar)() 括号中的表达式执行为 判断前者是否为 true ,若为true,不计算后者,若为false,计算后者并返回后者的值。
(foo.bar,foo.bar) 括号中的表达式之行为分别计算 “,” 操作符两边,然后返回 “,” 操作符后面的值。
箭头函数中的this
箭头函数时ES6新增的语法。
有两个作用:
更简洁的函数
本身不绑定this
代码格式为:
// 普通函数
function foo(a){
// ......
}
//箭头函数
var foo = a => {
// ......
}
//如果没有参数或者参数为多个
var foo = (a,b,c,d) => {
// ......
}
我们在使用普通函数之前对于函数的this绑定,需要根据这个函数如何被调用来确定其内部this的绑定对象。而且常常因为调用链的数量或者是找不到其真正的调用者对 this 的指向模糊不清。在箭头函数出现后其内部的 this 指向不需要再依靠调用的方式来确定。
箭头函数有几个特点(与普通函数的区别)
箭头函数不绑定 this 。它只会从作用域链的上一层继承 this。
箭头函数不绑定arguments,使用reset参数来获取实参的数量。
箭头函数是匿名函数,不能作为构造函数。
箭头函数没有prototype属性。
不能使用 yield 关键字,因此箭头函数不能作为函数生成器。
这里我们只讨论箭头函数中的this绑定。
用一个例子来对比普通函数与箭头函数中的this绑定:
var obj={
foo: function() {
console.log(this); // obj
},
bar: () => {
console.log(this); // window
}
}
obj.foo();
obj.bar();
上述代码中,同样是通过对象 . 方法调用一个函数,但是函数内部this绑定确是不同,只因一个数普通函数一个是箭头函数。
用一句话来总结箭头函数中的this绑定:
个人上面说的它会从作用域链的上一层继承 this ,说法并不是很正确。作用域中存放的是这个函数当前执行上下文与所有父级执行上下文的变量对象的集合。因此在作用域链中并不存在 this 。应该说是作用域链上一层对应的执行上下文中继承 this 。
箭头函数中的this继承于作用域链上一层对应的执行上下文中的this
var obj={
foo: function() {
console.log(this); // obj
},
bar: () => {
console.log(this); // window
}
}
obj.bar();
上述代码中obj.bar执行时的作用域链为:
scopeChain = [
obj.bar.AO,
global.VO
]
根据上面的规则,此时bar函数中的this指向为全局执行上下文中的this,即:window。
再来看一个例子:
var obj={
foo: function() {
console.log(this); // obj
var bar=() => {
console.log(this); // obj
}
bar();
}
}
obj.foo();
在普通函数中,bar 执行时内部this被绑定为全局对象,因为它是作为独立函数调用。但是在箭头函数中呢,它却绑定为 obj 。跟父级函数中的 this 绑定为同一对象。
此时它的作用域链为:
scopeChain = [
bar.AO,
obj.foo.AO,
global.VO
]
这个时候我们就差不多知道了箭头函数中的this绑定。
继续看例子:
var obj={
foo: () => {
console.log(this); // window
var bar=() => {
console.log(this); // window
}
bar();
}
}
obj.foo();
这个时候怎么又指向了window了呢?
我们还看当 bar 执行时的作用域链:
scopeChain = [
bar.AO,
obj.foo.AO,
global.VO
]
当我们找bar函数中的this绑定时,就会去找foo函数中的this绑定。因为它是继承于它的。这时 foo 函数也是箭头函数,此时foo中的this绑定为window而不是调用它的obj对象。因此 bar函数中的this绑定也为全局对象window。
我们在回头看上面关于定时器中的this的例子:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
console.log(this); // chen
},1000)
}
}
obj.foo();
这时我们就可以很简单的让定时器中的this与foo中的this绑定为同一对象:
var name="chen";
var obj={
name: "erdong",
foo: function() {
// this => obj
console.log(this.name); // erdong
setTimeout(() => {
// this => foo中的this => obj
console.log(this.name); // erdong
},1000)
}
}
obj.foo();
课程介绍
近些年,浏览器的功能越来越强大,渐渐得成为了复杂应用和图形的平台。同时,现有大多数浏览器实现了对 WebGL 的支持,但要直接使用 WebGL 相关接口进行开发,则需要学习复杂的着色器语言,且开发周期长,不利于项目的快速开发。
面对这种情况,Three.js 应运而生,它不但对 WebGL 进行了封装,将复杂的接口简单化,而且基于面向对象思维,将数据结构对象化,非常方便我们开发。Three.js 的发展十分迅速,然而配套的学习材料却比较匮乏,于是便有了当前的这个课程。
本课程作为入门课程,不会深入做源码解析,主要协助初学者了解 Three.js 的数据结构,基础 API 以及相关辅助插件的使用。帮助初学者达到快速入门的目的。
本课程共包含四大部分。
第一部分(第01-02课),入门前概述,带你初步认识 Three.js、框架选择标准、开发工具,源码获取,实现一个“Hello World”辅助工具。
第二部分(第03-08课),基础功能篇,主要包括 Object3D、Scene、Mesh、Group、Geometry、Materials、Lights、Cameras、粒子等相关功能的介绍。
第三部分(第09-15课),进阶篇,主要包括 Controls、Loaders、Animation、Tween、核心对象,与场景之间的交互以及性能优化介绍。
第四部分(第16课),实战篇,带大家利用所学知识实现一个 3D 小案例。
郑世强,现就职于上海某网络公司担任前端工程师,CSDN 博客作者,长期活跃于各大论坛,擅长前端开发、WEBGL 开发。
WebGL(Web 图形库)是一种 JavaScript API,用于在任何兼容的 Web 浏览器中呈现交互式 3D 和 2D 图形,而无需使用插件。WebGL 通过引入一个与 OpenGL ES 2.0 紧密相符合的 API,可以在 HTML5 <canvas> 元素中使用(简介引自 MDN)。
以我的理解,WebGL 给我们提供了一系列的图形接口,能够让我们通过 JavaScript 去使用 GPU 来进行浏览器图形渲染的工具。
Three.js 是一款 webGL 框架,由于其易用性被广泛应用。Three.js 在 WebGL 的 API 接口基础上,又进行的一层封装。它是由居住在西班牙巴塞罗那的程序员 Ricardo Cabbello Miguel 所开发,他更为人知的网名是 Mr.doob。
Three.js 以简单、直观的方式封装了 3D 图形编程中常用的对象。Three.js 在开发中使用了很多图形引擎的高级技巧,极大地提高了性能。另外,由于内置了很多常用对象和极易上手的工具,Three.js 的功能也非常强大。最后,Three.js 还是完全开源的,你可以在 GitHub 上找到它的源代码,并且有很多人贡献代码,帮助 Mr.doob 一起维护这个框架。
WebGL 原生 API 是一种非常低级的接口,而且还需要一些数学和图形学的相关技术。对于没有相关基础的人来说,入门真的很难,Three.js 将入门的门槛降低了一大截,对 WebGL 进行封装,简化我们创建三维动画场景的过程。只要你有一定的 JavaScript 基础,有一定的前端经验,我坚信,用不了多长时间,三维制作会变得很简单。
用最简单的一句话概括:WebGL 和 Three.js 的关系,相当于 JavaScript 和 jQuery 的关系。
Three.js 作为 WebGL 框架中的佼佼者,由于它的易用性和扩展性,使得它能够满足大部分的开发需求,Three.js 的具体功能如下:
Three.js 掩盖了 3D 渲染的细节:Three.js 将 WebGL 原生 API 的细节抽象化,将 3D 场景拆解为网格、材质和光源(即它内置了图形编程常用的一些对象种类)。
面向对象:开发者可以使用上层的 JavaScript 对象,而不是仅仅调用 JavaScript 函数。
功能非常丰富:Three.js 除封装了 WebGL 原始 API 之外,Three.js 还包含了许多实用的内置对象,可以方便地应用于游戏开发、动画制作、幻灯片制作、髙分辨率模型和一些特殊的视觉效果制作。
速度很快:Three.js 采用了 3D 图形最佳实践来保证在不失可用性的前提下,保持极高的性能。
支持交互:WebGL 本身并不提供拾取(Picking)功能(即是否知道鼠标正处于某个物体上)。而 Three.js 则固化了拾取支持,这就使得你可以轻松为你的应用添加交互功能。
包含数学库:Three.js 拥有一个强大易用的数学库,你可以在其中进行矩阵、投影和矢量运算。
内置文件格式支持:你可以使用流行的 3D 建模软件导出文本格式的文件,然后使用 Three.js 加载,也可以使用 Three.js 自己的 JSON 格式或二进制格式。
扩展性很强:为 Three.js 添加新的特性或进行自定义优化是很容易的事情。如果你需要某个特殊的数据结构,那么只需要封装到 Three.js 即可。
支持HTML5 Canvas:Three.js 不但支持 WebGL,而且还支持使用 Canvas2D、Css3D 和 SVG 进行渲染。在未兼容 WebGL 的环境中可以回退到其它的解决方案。
虽然 Three.js 的优势很大,但是它也有它的不足之处:
官网文档非常粗糙,对于新手极度不友好。
国内的相关资源匮乏。
Three.js 所有的资料都是以英文格式存在,对国内的朋友来说又提高了门槛。
Three.js 不是游戏引擎,一些游戏相关的功能没有封装在里面,如果需要相关的功能需要进行二次开发。
随着 WebGL 的迅速发展,相关的 WebGL 库也丰富起来,接下来介绍几个比较火的 WebGL 库。
Babylon.JS 是最好的 JavaScript 3D 游戏引擎,它能创建专业级三维游戏。主要以游戏开发和易用性为主。与 Three.js 之间的对比:
Three.js 比较全面,而 Babylon.js 专注于游戏方面。
Babylon.js 提供了对碰撞检测、场景重力、面向游戏的照相机,Three.js 本身不自带,需要依靠引入插件实现。
对于 WebGL 的封装,双方做得各有千秋,Three.js 浅一些,好处是易于扩展,易于向更底层学习;Babylon.js 深一些,好处是易用扩展难度大一些。
Three.js 的发展依靠社区推动,出来的比较早,发展比较成熟,Babylon.js 由微软公司在2013推出,文档和社区都比较健全,国内还不怎么火。
PlayCanvas 是一个基于 WebGL 游戏引擎的企业级开源 JavaScript 框架,它有许多的开发工具能帮你快速创建 3D 游戏。与 Three.js 之间的对比:
PlayCanvas 的优势在于它有云端的在线可视化编辑工具。
PlayCanvas 的扩展性不如 Three.js。
最主要是 PlayCanvas 不完全开源,还商业付费。
Cesium 是国外一个基于 JavaScript 编写的使用 WebGL 的地图引擎,支持 3D、2D、2.5D 形式的地图展示,可以自行绘制图形,高亮区域。与 Three.js 对比:
Cesium 是一个地图引擎,专注于 Gis,相关项目推荐使用它,其它项目还是算了。
至于库的扩展,其它的配套插件,以及周边的资源都不及Three.js。
通过以上信息我们发现,Three.js 在其库的扩展性,易用性以及功能方面有很好的优势。学习 Three.js 入门 3D 开发不但门槛低,而且学习曲线不会太陡,即使以后转向 WebGL 原生开发,也能通过 Three.js 学习到很多有用的知识。
现在最火的微信小游戏跳一跳也是在 Three.js 的基础上开发出来的。所以,Three.js 是我们必须要学的 WebGL 框架。
Three.js 可以使用 WebGL 在所有现代浏览器上渲染场景。对于旧版浏览器,尤其是 Internet Explorer 10 及更低版本,您可能需要回退到其他渲染器(CSS2DRenderer、CSS3DRenderer、SVGRenderer、CanvasRenderer)。
注意:如果您不需要支持这些旧版浏览器,则不推荐使用其他渲染器,因为它们速度较慢并且支持的功能比 WebGLRenderer 更少。
即可下载当前版本的代码及相关案例,文件下载解压后是这样的:
其中相关文件夹的内容是:
build:里面含有 Three.js 构建出来的 JavaScript 文件,可以直接引入使用,并有压缩版;
docs:Three.js 的官方文档;
editor:Three.js 的一个网页版的模型编辑器;
examples:Three.js 的官方案例,如果全都学会,必将成为大神;
src:这里面放置的全是编译 Three.js 的源文件;
test:一些官方测试代码,我们一般用不到;
utils:一些相关插件;
其他:开发环境搭建、开发所需要的文件,如果不对 Three.js 进行二次开发,用不到。
还有第三种,就是直接去 GitHub 上下载源码,和在官网上下载的代码一样。
<!DOCTYPE html><html><head> <meta charset=utf-8> <title>我的第一个Three.js案例</title> <style> body { margin: 0; } canvas { width: 100%; height: 100%; display: block; } </style></head><body onload="init()"><script src="https://cdn.bootcss.com/three.js/92/three.js"></script><script> //声明一些全局变量 var renderer, camera, scene, geometry, material, mesh; //初始化渲染器 function initRenderer() { renderer = new THREE.WebGLRenderer(); //实例化渲染器 renderer.setSize(window.innerWidth, window.innerHeight); //设置宽和高 document.body.appendChild(renderer.domElement); //添加到dom } //初始化场景 function initScene() { scene = new THREE.Scene(); //实例化场景 } //初始化相机 function initCamera() { camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); //实例化相机 camera.position.set(0, 0, 15); } //创建模型 function initMesh() { geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体 material = new THREE.MeshNormalMaterial(); //创建材质 mesh = new THREE.Mesh( geometry, material ); //创建网格 scene.add( mesh ); //将网格添加到场景 } //运行动画 function animate() { requestAnimationFrame(animate); //循环调用函数 mesh.rotation.x += 0.01; //每帧网格模型的沿x轴旋转0.01弧度 mesh.rotation.y += 0.02; //每帧网格模型的沿y轴旋转0.02弧度 renderer.render( scene, camera ); //渲染界面 } //初始化函数,页面加载完成是调用 function init() { initRenderer(); initScene(); initCamera(); initMesh(); animate(); }</script></body></html>
请将上面的代码复制到 HTML 文件中,然后使用浏览器打开,我们就会发现下面的效果:
————————————————
版权声明:本文为CSDN博主「GitChat的博客」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/valada/java/article/details/80871701
用例
让我们从介绍几种不同的填充用例开始。
标签和值
假设你在同一行上有标签和值,例如 name:zhangsan 和 Phone Number:(555)-555-1234。如果把他们放在一起看起来会有点奇怪,会是这样:
Name: zhangsan
Phone Number: (555)-555-1234
你可能想要这个。
Name: zhangsan
Phone Number: (555)555-1234
或这个...
Name: zhangsan
Phone Number: (555)555-1234
金额
在中国,显示价格时通常显示两位数的角、分。所以代替这个...
¥10.1
你会想要这个。
¥10.01
日期
对于日期,日期和月份都需要2位数字。所以代替这个...
2020-5-4
你会想要这个。
2020-05-04
时间
与上面的日期类似,对于计时器,你需要2位数字表示秒,3位数字表示毫秒。所以代替这个...
1:1
你会想要这个。
01:001
padstart()
让我们从 padStart() 以及标签和值示例开始。假设我们希望标签彼此正确对齐,以使值在同一位置开始。
Name: zhangsan
Phone Number: (555)555-1234
由于 Phone Number 是两个标签中较长的一个,因此我们要在 Name 标签的开头加上空格。为了将来的需要,我们不要把它专门填充到电话号码的长度,我们把它填充到长一点,比如说20个字符。这样一来,如果你在未来使用较长的标签,这一招仍然有效。
在填充之前,这是用于显示此信息的入门代码。
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log(label1 + ": " + name);
console.log(label2 + ": " + phoneNumber);
//Name: zhangsan
//Phone Number: (555)-555-1234
现在,让我们填充第一个标签。要调用 padStart(),你需要传递两个参数:一个用于填充字符串的目标长度,另一个用于你希望填充的字符。在这种情况下,我们希望长度为20,而填充字符为空格。
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log(label1.padStart(20, " ") + ": " + name);
console.log(label2 + ": " + phoneNumber);
// Name: zhangsan
////Phone Number: (555)-555-1234
现在填充第二行。
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log(label1.padStart(20, " ") + ": " + name);
console.log(label2.padStart(20, " ") + ": " + phoneNumber);
// Name: zhangsan
//// Phone Number: (555)-555-1234
padEnd()
对于相同的标签和值示例,让我们更改填充标签的方式。让我们将标签向左对齐,以便在末尾添加填充。
初始代码
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log(label1 + ": " + name);
console.log(label2 + ": " + phoneNumber);
//Name: zhangsan
//Phone Number: (555)-555-1234
现在,让我们填充第一个标签,与我们之前所做的类似,但有两个小区别。现在,我们使用 padEnd() 而不是padStart(),并且需要在填充之前将冒号与标签连接起来,这样我们就能确保冒号在正确的位置。
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log((label1 + ': ').padEnd(20, ' ') + name);
console.log(label2 + ": " + phoneNumber);
//Name: zhangsan
//Phone Number: (555)-555-1234
现在两行都已填充。
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log((label1 + ': ').padEnd(20, ' ') + name);
console.log((label2 + ': ').padEnd(20, ' ') + phoneNumber);
//Name: zhangsan
//Phone Number: (555)-555-1234
数字(价格、日期、计时器等)呢?
padding函数是专门针对字符串而不是数字的,所以,我们需要先将数字转换为字符串。
价格
让我们看一下显示价格的初始代码。
const rmb = 10;
const cents = 1;
console.log("¥" + rmb + "." + cents); //¥10.1
要填充分,我们需要先将其转换为字符串,然后调用 padStart() 函数,指定长度为1且填充字符为'0';
const rmb = 10;
const cents = 1;
console.log("¥" + rmb + "." + cents.toString().padStart(2,0)); //¥10.01
日期
这是显示日期的初始代码。
const month = 2;
const year = 2020;
console.log(year + "-" + month); //2020-2
现在,让我们填充月份以确保它是两位数。
const month = 2;
const year = 2020;
console.log(year + "-" + month.toString().padStart(2,"0")); // 2020-02
计时器
最后是我们的计时器,我们要格式化两个不同的数字,即秒和毫秒。尽管有相同的原则。这是初始代码。
const seconds = 1;
const ms = 1;
console.log(seconds + ":" + ms); //1:1
现在要填充,我将在单独的行上进行填充,以便于阅读。
const seconds = 1;
const formattedSeconds = seconds.toString().padStart(2,0);
const ms = 1;
const formattedMs = ms.toString().padStart(3,0);
console.log(formattedSeconds + ":" + formattedMs); // 01:001
最后
虽然编写自己的padding函数并不难,但既然已经内置在JavaScript中,为什么还要自己去做呢?有很多有趣的函数已经内置了。在你自己构建一些东西之前,可能值得先快速搜索一下。
数组——最简单的内存数据结构
数组存储一系列同一种数据类型的值。( Javascript 中不存在这种限制)
对数据的随机访问,数组是更好的选择,否则几乎可以完全用 「链表」 来代替
在很多编程语言中,数组的长度是固定的,当数组被填满时,再要加入新元素就很困难。Javascript 中数组不存在这个问题。
但是 Javascript 中的数组被实现成了对象,与其他语言相比,效率低下。
数组的一些核心方法
方法 描述
push 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。(改变原数组)
pop 方法从数组中删除最后一个元素,并返回该元素的值。(改变原数组)
shift 方法从数组中删除第一个元素,并返回该元素的值,如果数组为空则返回 undefined 。(改变原数组)
unshift 将一个或多个元素添加到数组的开头,并返回该数组的新长度(改变原数组)
concat 连接两个或多个数组,并返回结果(返回一个新数组,不影响原有的数组。)
every 对数组中的每个元素运行给定函数,如果该函数对每个元素都返回 true,则返回 true。若为一个空数组,,始终返回 true。 (不会改变原数组,[].every(callback)始终返回 true)
some 对数组中的每个元素运行给定函数,如果任一元素返回 true,则返回 true。若为一个空数组,,始终返回 false。(不会改变原数组,)
forEach 对数组中的每个元素运行给定函数。这个方法没有返回值,没有办法中止或者跳出 forEach() 循环,除了抛出一个异常(foreach不直接改变原数组,但原数组可能会被 callback 函数该改变。)
map 对数组中的每个元素运行给定函数,返回每次函数调用的结果组成的数组(map不直接改变原数组,但原数组可能会被 callback 函数该改变。)
sort 按照Unicode位点对数组排序,支持传入指定排序方法的函数作为参数(改变原数组)
reverse 方法将数组中元素的位置颠倒,并返回该数组(改变原数组)
join 将所有的数组元素连接成一个字符串
indexOf 返回第一个与给定参数相等的数组元素的索引,没有找到则返回 -1
lastIndexOf 返回在数组中搜索到的与给定参数相等的元素的索引里最大的值,没有找到则返回 -1
slice 传入索引值,将数组里对应索引范围内的元素(浅复制原数组中的元素)作为新数组返回(原始数组不会被改变)
splice 删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容(改变原数组)
toString 将数组作为字符串返回
valueOf 和 toString 类似,将数组作为字符串返回
栈是一种遵循后进先出(LIFO)原则的有序集合,新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。
通俗来讲,就是你向一个桶里放书本或者盘子,你要想取出最下面的书或者盘子,你必须要先把上面的都先取出来。
栈也被用在编程语言的编译器和内存中保存变量、方法调用等,也被用于浏览器历史记录 (浏览器的返回按钮)。
代码实现
// 封装栈队列是遵循先进先出(FIFO,也称为先来先服务)原则的一组有序的项。队列在尾部添加新
元素,并从顶部移除元素。添加的元素必须排在队列的末尾。
生活中常见的就是排队
代码实现
function Queue() {代码实现
链表——存储有序的元素集合
,但在内存中不是连续放置
的。
链表(单向链表)中的元素
由存放元素本身「data」
的节点和一个指向下一个「next」
元素的指针组成。牢记这个特点
相比数组,链表添加或者移除元素不需要移动其他元素,但是需要使用指针
。访问元素每次都需要从表头
开始查找。
代码实现:
单向链表
表头
、表尾
和 存储数据的 节点
,其中节点
包含三部分:一个链向下一个元素的next
, 另一个链向前一个元素的prev
和存储数据的 data
。牢记这个特点
function doublyLinkedList() {
this.head = null // 表头:始终指向第一个节点,默认为 null
this.tail = null // 表尾:始终指向最后一个节点,默认为 null
this.length = 0 // 链表长度
function Node(data) {
this.data = data
this.prev = null
this.next = null
}
doublyLinkedList.prototype.append = function (data) {
let newNode = new Node(data)
if (this.length === 0) {
// 当插入的节点为链表的第一个节点时
// 表头和表尾都指向这个节点
this.head = newNode
this.tail = newNode
} else {
// 当链表中已经有节点存在时
// 注意tail指向的始终是最后一个节点
// 注意head指向的始终是第一个节点
// 因为是双向链表,可以从头部插入新节点,也可以从尾部插入
// 这里以从尾部插入为例,将新节点插入到链表最后
// 首先将新节点的 prev 指向上一个节点,即之前tail指向的位置
newNode.prev = this.tail
// 然后前一个节点的next(及之前tail指向的节点)指向新的节点
// 此时新的节点变成了链表的最后一个节点
this.tail.next = newNode
// 因为 tail 始终指向的是最后一个节点,所以最后修改tail的指向
this.tail = newNode
}
this.length++
}
doublyLinkedList.prototype.toString = function () {
return this.backwardString()
}
doublyLinkedList.prototype.forwardString = function () {
let current = this.tail
let str = ''
while (current) {
str += current.data + ''
current = current.prev
}
return str
}
doublyLinkedList.prototype.backwardString = function () {
let current = this.head
let str = ''
while (current) {
str += current.data + ''
current = current.next
}
return str
}
doublyLinkedList.prototype.insert = function (position, data) {
if (position < 0 || position > this.length) return false
let newNode = new Node(data)
if (this.length === 0) {
this.head = newNode
this.tail = newNode
} else {
if (position == 0) {
this.head.prev = newNode
newNode.next = this.head
this.head = newNode
} else if (position == this.length) {
newNode.prev = this.tail
this.tail.next = newNode
this.tail = newNode
} else {
let current = this.head
let index = 0
while( index++ < position){
current = current.next
}
newNode.next = current
newNode.prev = current.prev
current.prev.next = newNode
current.prev = newNode
}
}
this.length++
return true
}
doublyLinkedList.prototype.get = function (position) {
if (position < 0 || position >= this.length) return null
let current = this.head
let index = 0
while (index++) {
current = current.next
}
return current.data
}
doublyLinkedList.prototype.indexOf = function (data) {
let current = this.head
let index = 0
while (current) {
if (current.data === data) {
return index
}
current = current.next
index++
}
return -1
}
doublyLinkedList.prototype.update = function (position, newData) {
if (position < 0 || position >= this.length) return false
let current = this.head
let index = 0
while(index++ < position){
current = current.next
}
current.data = newData
return true
}
doublyLinkedList.prototype.removeAt = function (position) {
if (position < 0 || position >= this.length) return null
let current = this.head
if (this.length === 1) {
this.head = null
this.tail = null
} else {
if (position === 0) { // 删除第一个节点
this.head.next.prev = null
this.head = this.head.next
} else if (position === this.length - 1) { // 删除最后一个节点
this.tail.prev.next = null
this.tail = this.tail.prev
} else {
let index = 0
while (index++ < position) {
current = current.next
}
current.prev.next = current.next
current.next.prev = current.prev
}
}
this.length--
return current.data
}
doublyLinkedList.prototype.remove = function (data) {
let index = this.indexOf(data)
return this.removeAt(index)
}
}
感谢你的阅读~
————————————————
版权声明:本文为CSDN博主「重庆崽儿Brand」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/brand2014/java/article/details/106134844
ESLint
先说是什么:ESLint 是一个检查代码质量与风格的工具,配置一套规则,他就能检查出你代码中不符合规则的地方,部分问题支持自动修复。
使用这么一套规则有什么用呢?如果单人开发的话倒是没什么了,但是一个团队若是存在两种风格,那格式化之后处理代码冲突就真的要命了,统一的代码风格真的很重要!
(其实以前自己做一个项目的时候,公司电脑和家庭电脑的代码风格配置不一样,在家加班的时候也经常顺手格式化了,这么循环了几次不同的风格,导致 diff 极其混乱
事件循环
JavaScript是单线程,非阻塞的
浏览器的事件循环
执行栈和事件队列
宏任务和微任务
node环境下的事件循环
和浏览器环境有何不同
事件循环模型
宏任务和微任务
经典题目分析
1. JavaScript是单线程,非阻塞的
单线程:
JavaScript的主要用途是与用户互动,以及操作DOM。如果它是多线程的会有很多复杂的问题要处理,比如有两个线程同时操作DOM,一个线程删除了当前的DOM节点,一个线程是要操作当前的DOM阶段,最后以哪个线程的操作为准?为了避免这种,所以JS是单线程的。即使H5提出了web worker标准,它有很多限制,受主线程控制,是主线程的子线程。
非阻塞:通过 event loop 实现。
2. 浏览器的事件循环
执行栈和事件队列
为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲 《Help, I'm stuck in an event-loop》)
Help, I'm stuck in an event-loop
执行栈: 同步代码的执行,按照顺序添加到执行栈中
function a() {
b();
console.log('a');
}
function b() {
console.log('b')
}
a();
我们可以通过使用 Loupe(Loupe是一种可视化工具,可以帮助您了解JavaScript的调用堆栈/事件循环/回调队列如何相互影响)工具来了解上面代码的执行情况。
调用情况
执行函数 a()先入栈
a()中先执行函数 b() 函数b() 入栈
执行函数b(), console.log('b') 入栈
输出 b, console.log('b')出栈
函数b() 执行完成,出栈
console.log('a') 入栈,执行,输出 a, 出栈
函数a 执行完成,出栈。
事件队列: 异步代码的执行,遇到异步事件不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务。当异步事件返回结果,将它放到事件队列中,被放入事件队列不会立刻执行起回调,而是等待当前执行栈中所有任务都执行完毕,主线程空闲状态,主线程会去查找事件队列中是否有任务,如果有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,然后执行其中的同步代码。
我们再上面代码的基础上添加异步事件,
function a() {
b();
console.log('a');
}
function b() {
console.log('b')
setTimeout(function() {
console.log('c');
}, 2000)
}
a();
此时的执行过程如下
img
我们同时再加上点击事件看一下运行的过程
$.on('button', 'click', function onClick() {
setTimeout(function timer() {
console.log('You clicked the button!');
}, 2000);
});
console.log("Hi!");
setTimeout(function timeout() {
console.log("Click the button!");
}, 5000);
console.log("Welcome to loupe.");
img
简单用下面的图进行一下总结
执行栈和事件队列
宏任务和微任务
为什么要引入微任务,只有一种类型的任务不行么?
页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。
不同的异步任务被分为:宏任务和微任务
宏任务:
script(整体代码)
setTimeout()
setInterval()
postMessage
I/O
UI交互事件
微任务:
new Promise().then(回调)
MutationObserver(html5 新特性)
运行机制
异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中去。
在当前执行栈为空时,主线程会查看微任务队列是否有事件存在
存在,依次执行队列中的事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的事件,把当前的回调加到当前指向栈。
如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;
当前执行栈执行完毕后时会立刻处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
执行一个宏任务(栈中没有就从事件队列中获取)
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
简单总结一下执行的顺序:
执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。
宏任务和微任务
深入理解js事件循环机制(浏览器篇) 这边文章中有个特别形象的动画,大家可以看着理解一下。
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('end')
浏览器事件循环
全局代码压入执行栈执行,输出 start
setTimeout压入 macrotask队列,promise.then 回调放入 microtask队列,最后执行 console.log('end'),输出 end
调用栈中的代码执行完成(全局代码属于宏任务),接下来开始执行微任务队列中的代码,执行promise回调,输出 promise1, promise回调函数默认返回 undefined, promise状态变成 fulfilled ,触发接下来的 then回调,继续压入 microtask队列,此时产生了新的微任务,会接着把当前的微任务队列执行完,此时执行第二个 promise.then回调,输出 promise2
此时,microtask队列 已清空,接下来会会执行 UI渲染工作(如果有的话),然后开始下一轮 event loop, 执行 setTimeout的回调,输出 setTimeout
最后的执行结果如下
start
end
promise1
promise2
setTimeout
node环境下的事件循环
和浏览器环境有何不同
表现出的状态与浏览器大致相同。不同的是 node 中有一套自己的模型。node 中事件循环的实现依赖 libuv 引擎。Node的事件循环存在几个阶段。
如果是node10及其之前版本,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask队列中的任务。
node版本更新到11之后,Event Loop运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列,跟浏览器趋于一致。下面例子中的代码是按照的去进行分析的。
事件循环模型
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
事件循环各阶段详解
node中事件循环的顺序
外部输入数据 --> 轮询阶段(poll) --> 检查阶段(check) --> 关闭事件回调阶段(close callback) --> 定时器检查阶段(timer) --> I/O 事件回调阶段(I/O callbacks) --> 闲置阶段(idle, prepare) --> 轮询阶段...
这些阶段大致的功能如下:
定时器检测阶段(timers): 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。
I/O事件回调阶段(I/O callbacks): 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
闲置阶段(idle, prepare): 这个阶段仅在内部使用,可以不必理会
轮询阶段(poll): 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
检查阶段(check): setImmediate()的回调会在这个阶段执行。
关闭事件回调阶段(close callbacks): 例如socket.on('close', ...)这种close事件的回调
poll:
这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。
这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。
check:
该阶段执行setImmediate()的回调函数。
close:
该阶段执行关闭请求的回调函数,比如socket.on('close', ...)。
timer阶段:
这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。
I/O callback阶段:
除了以下的回调函数,其他都在这个阶段执行:
setTimeout()和setInterval()的回调函数
setImmediate()的回调函数
用于关闭请求的回调函数,比如socket.on('close', ...)
宏任务和微任务
宏任务:
setImmediate
setTimeout
setInterval
script(整体代码)
I/O 操作等。
微任务:
process.nextTick
new Promise().then(回调)
Promise.nextTick, setTimeout, setImmediate的使用场景和区别
Promise.nextTick
process.nextTick 是一个独立于 eventLoop 的任务队列。
在每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。
是所有异步任务中最快执行的。
setTimeout:
setTimeout()方法是定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行。
setImmediate:
setImmediate()方法从意义上将是立刻执行的意思,但是实际上它却是在一个固定的阶段才会执行回调,即poll阶段之后。
经典题目分析
一. 下面代码输出什么
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务
从上往下执行代码,先执行同步代码,输出 script start
遇到setTimeout,现把 setTimeout 的代码放到宏任务队列中
执行 async1(),输出 async1 start, 然后执行 async2(), 输出 async2,把 async2() 后面的代码 console.log('async1 end')放到微任务队列中
接着往下执行,输出 promise1,把 .then()放到微任务队列中;注意Promise本身是同步的立即执行函数,.then是异步执行函数
接着往下执行, 输出 script end。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码
依次执行微任务中的代码,依次输出 async1 end、 promise2, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出 setTimeout
最后的执行结果如下
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
二. 下面代码输出什么
console.log('start');
setTimeout(() => {
console.log('children2');
Promise.resolve().then(() => {
console.log('children3');
})
}, 0);
new Promise(function(resolve, reject) {
console.log('children4');
setTimeout(function() {
console.log('children5');
resolve('children6')
}, 0)
}).then((res) => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0)
})
这道题跟上面题目不同之处在于,执行代码会产生很多个宏任务,每个宏任务中又会产生微任务
从上往下执行代码,先执行同步代码,输出 start
遇到setTimeout,先把 setTimeout 的代码放到宏任务队列①中
接着往下执行,输出 children4, 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列②中,此时.then并不会被放到微任务队列中,因为 resolve是放到 setTimeout中执行的
代码执行完成之后,会查找微任务队列中的事件,发现并没有,于是开始执行宏任务①,即第一个 setTimeout, 输出 children2,此时,会把 Promise.resolve().then放到微任务队列中。
宏任务①中的代码执行完成后,会查找微任务队列,于是输出 children3;然后开始执行宏任务②,即第二个 setTimeout,输出 children5,此时将.then放到微任务队列中。
宏任务②中的代码执行完成后,会查找微任务队列,于是输出 children7,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出 children6;
最后的执行结果如下
start
children4
children2
children3
children5
children7
children6
三. 下面代码输出什么
const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
resolve(2)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}
p().then((res) => {
console.log(res);
})
console.log('end');
执行代码,Promise本身是同步的立即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到p1.then会先放到微任务队列中,接着往下执行,输出 3
遇到 p().then 会先放到微任务队列中,接着往下执行,输出 end
同步代码块执行完成后,开始执行微任务队列中的任务,首先执行 p1.then,输出 2, 接着执行p().then, 输出 4
微任务执行完成后,开始执行宏任务,setTimeout, resolve(1),但是此时 p1.then已经执行完成,此时 1不会输出。
最后的执行结果如下
3
end
2
4
你可以将上述代码中的 resolve(2)注释掉, 此时 1才会输出,输出结果为 3 end 4 1。
const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}
p().then((res) => {
console.log(res);
})
console.log('end');
3
end
4
1
最后强烈推荐几个非常好的讲解 event loop 的视频:
What the heck is the event loop anyway? | Philip Roberts | JSConf EU
Jake Archibald: In The Loop - JSConf.Asia
注意:为了方便开发者减少配置项,描述页面的四个文件必须具有相同的路径与文件名。
WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构。WXML
充当的就是类似 HTML
的角色
要完整了解 WXML 语法,请参考WXML 语法参考。
WXSS (WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式。
WXSS 用来决定 WXML 的组件应该怎么显示。
为了适应广大的前端开发者,WXSS 具有 CSS 大部分特性。同时为了更适合开发微信小程序,WXSS 对 CSS 进行了扩充以及修改。
与 CSS 相比,WXSS 扩展的特性有:
尺寸单位
样式导入
JSON 是一种数据格式,并不是编程语言,在小程序中,JSON扮演的静态配置的角色。
全局配置
小程序根目录下的 app.json 文件用来对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。
页面配置
每一个小程序页面也可以使用同名 .json 文件来对本页面的窗口表现进行配置,页面中配置项会覆盖 app.json 的 window 中相同的配置项。
工具配置 project.config.json
通常大家在使用一个工具的时候,都会针对各自喜好做一些个性化配置,例如界面颜色、编译配置等等,当你换了另外一台电脑重新安装工具的时候,你还要重新配置。
考虑到这点,小程序开发者工具在每个项目的根目录都会生成一个 project.config.json,你在工具上做的任何配置都会写入到这个文件,当你重新安装工具或者换电脑工作时,你只要载入同一个项目的代码包,开发者工具就自动
注意:
JSON文件都是被包裹在一个大括号中 {},通过key-value的方式来表达数据。JSON的Key必须包裹在一个双引号中,在实践中,编写 JSON 的时候,忘了给 Key 值加双引号或者是把双引号写成单引号是常见错误。
JSON的值只能是以下几种数据格式,其他任何格式都会触发报错,例如 JavaScript 中的 undefined。
数字,包含浮点数和整数
字符串,需要包裹在双引号中
Bool值,true 或者 false
数组,需要包裹在方括号中 []
对象,需要包裹在大括号中 {}
Null
还需要注意的是 JSON 文件中无法使用注释,试图添加注释将会引发报错。
一个服务仅仅只有界面展示是不够的,还需要和用户做交互:响应用户的点击、获取用户的位置等等。在小程序里边,我们就通过编写 JS
脚本文件来处理用户的操作。
注册页面
对于小程序中的每个页面,都需要在页面对应的 js 文件中进行注册,指定页面的初始数据、生命周期回调、事件处理函数等
使用 Page 构造器注册页面
简单的页面可以使用 Page() 进行构造。
使用 Component 构造器构造页面
Page 构造器适用于简单的页面。但对于复杂的页面, Page 构造器可能并不好用。
此时,可以使用 Component 构造器来构造页面。 Component 构造器的主要区别是:方法需要放在 methods: { } 里面。
————————————————
版权声明:本文为CSDN博主「前端岚枫」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yilanyoumeng3/java/article/details/106292742
应对现在数据可视化的趋势,越来越多企业需要在很多场景(营销数据,生产数据,用户数据)下使用,可视化图表来展示体现数据,让数据更加直观,数据特点更加突出。
常见的数据可视化库:
D3.js 目前 Web 端评价最高的 Javascript 可视化工具库(入手难)
ECharts.js 百度出品的一个开源 Javascript 数据可视化库
Highcharts.js 国外的前端数据可视化库,非商用免费,被许多国外大公司所使用
AntV 蚂蚁金服全新一代数据可视化解决方案 等等
Highcharts 和 Echarts 就像是 Office 和 WPS 的关系
ECharts,一个使用 JavaScript 实现的开源可视化库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Firefox,Safari等),底层依赖矢量图形库 ZRender,提供直观,交互丰富,可高度个性化定制的数据可视化图表。
官网地址:https://www.echartsjs.com/zh/index.html
echarts体验
下载echarts https://github.com/apache/incubator-echarts/tree/4.5.0
使用步骤(5大步骤):
1.引入echarts 插件文件到html页面中
2.准备一个具备大小的DOM容器
<div id="main" style="width: 600px;height:400px;"></div>
3.初始化echarts实例对象
var myChart = echarts.init(document.getElementById('main'));
4.指定配置项和数据(option)
var option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line'
}]
};
5.将配置项设置给echarts实例对象
myChart.setOption(option);
这是要求同学们知道以下配置每个模块的主要作用干什么的就可以了
series
系列列表。每个系列通过 type 决定自己的图表类型
大白话:图标数据,指定什么类型的图标,可以多个图表重叠。
xAxis:直角坐标系 grid 中的 x 轴
boundaryGap: 坐标轴两边留白策略 true,这时候刻度只是作为分隔线,标签和数据点都会在两个刻度之间的带(band)中间。
yAxis:直角坐标系 grid 中的 y 轴
grid:直角坐标系内绘图网格。
title:标题组件
tooltip:提示框组件
legend:图例组件
color:调色盘颜色列表
数据堆叠,同个类目轴上系列配置相同的stack值后 后一个系列的值会在前一个系列的值上相加。
option = {
// color设置我们线条的颜色 注意后面是个数组
color: ['pink', 'red', 'green', 'skyblue'],
// 设置图表的标题
title: {
text: '折线图堆叠123'
},
// 图表的提示框组件
tooltip: {
// 触发方式
trigger: 'axis'
},
// 图例组件
legend: {
// series里面有了 name值则 legend里面的data可以删掉
},
// 网格配置 grid可以控制线形图 柱状图 图表大小
grid: {
left: '3%',
right: '4%',
bottom: '3%',
// 是否显示刻度标签 如果是true 就显示 否则反之
containLabel: true
},
// 工具箱组件 可以另存为图片等功能
toolbox: {
feature: {
saveAsImage: {}
}
},
// 设置x轴的相关配置
xAxis: {
type: 'category',
// 是否让我们的线条和坐标轴有缝隙
boundaryGap: false,
data: ['星期一', '周二', '周三', '周四', '周五', '周六', '周日']
},
// 设置y轴的相关配置
yAxis: {
type: 'value'
},
// 系列图表配置 它决定着显示那种类型的图表
series: [
{
name: '邮件营销',
type: 'line',
data: [120, 132, 101, 134, 90, 230, 210]
},
{
name: '联盟广告',
type: 'line',
data: [220, 182, 191, 234, 290, 330, 310]
},
{
name: '视频广告',
type: 'line',
data: [150, 232, 201, 154, 190, 330, 410]
},
{
name: '直接访问',
type: 'line',
data: [320, 332, 301, 334, 390, 330, 320]
}
]
};
1.官网实例
官网默认为我们提供了大量的案例,我们需要使用那一种只需要直接点击打开后复制他们的相关配置,然后按照我上面说的5大步骤进行使用即可。
2.社区Gallery
官方自带的图例,可能在很多时候并不能满足我们的需要,在社区这里可以找到一些基于echart的高度定制好的图表,相当于基于jquery开发的插件,这里是基于echarts开发的第三方的图表。
本案例中使用了地图模拟飞行的案例就是从社区中进行引用的,
参考社区的例子:https://gallery.echartsjs.com/editor.html?c=x0-ExSkZDM (模拟飞机航线)
实现步骤:
第一需要下载china.js提供中国地图的js文件
第二个因为里面代码比较多,我们新建一个新的js文件 myMap.js 引入
使用社区提供的配置即可。
代码已经上传至我的码云如有需要的小伙伴可自行下载:
https://gitee.com/jiuyueqi/echarts
ps:最后呢,如果大家看完楼主的文章觉得对echarts的学习和了解有所帮助,麻烦大家路过点个赞点个关注呗!楼主后续还会继续更新有关前端方面的面试题资料或者其他方面的知识。
————————————————
版权声明:本文为CSDN博主「程序猿玖月柒」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_45257157/java/article/details/106300587
蓝蓝设计的小编 http://www.lanlanwork.com