课程效果

项目简介

本课程将以 evernote 云笔记 mac 客户端版的 ui 做原型,做一款线上的云笔记 webapp 产品。产品包括登录、注册、笔记本的创建、修改、删除,笔记的markdown编辑、删除到回收站、markdown 预览、回收站管理等功能。采用前后端分离的开发方式,本课程只负责前端部分的开发。

前置知识

var-let-const

  1. var 可声明前置
  • 声明前置:变量可以在声明之前使用,值为 undefined

在JavaScript中,变量声明前置是指在代码执行过程中,JavaScript解析器会在代码执行之前将变量声明提升到作用域的顶部。这意味着你可以在变量声明之前使用变量,而不会引发错误。
例子:

1
2
console.log(x); // undefined
var x = 5;

在上面的例子中,尽管变量x在打印语句之前被声明,但由于变量声明前置的特性,代码不会引发错误,而是打印出undefined。这是因为在执行代码之前,var x会被提升到作用域的顶部,但是变量的赋值操作仍然是按照代码的实际顺序执行的。

需要注意的是,只有变量声明会被提升,而变量赋值并不会被提升。因此,在使用变量之前进行赋值操作仍会导致变量值为undefined

1
2
3
console.log(x); // undefined
var x = 5;
console.log(x); // 5
1
2
3
4
a = 3;
var a; // undefined
console.log(a); // 3
var a = 3;
  1. let 不可声明前置
1
2
3
a = 3; // ReferenceError: a is not defined
let a;
let a = 3; // SyntaxError: Identifier 'a' has already been declared
  1. let 不可重复声明
1
2
3
let a = 3;
let a = 4; // SyntaxError: Identifier 'a' has already been declared
let a = 5; // SyntaxError: Identifier 'a' has already been declared
  1. let 有块级作用域
1
2
3
4
if (true) {
let a = 3;
}
console.log(a); // ReferenceError: a is not defined
1
2
3
4
for (let i = 0; i < 3; i++) {
console.log(i);
}
console.log(i); // ReferenceError: i is not defined
  1. IIFE 的替换
  • IIFE
1
2
3
4
5
(function () {
var a = 3;
})();

console.log(a); // ReferenceError: a is not defined
1
2
3
4
5
{
let a = 3;
}

console.log(a); // ReferenceError: a is not defined

暂时性死区

  • 在let声明变量之前都是该变量的死区, 在死区内该变量是不能被访问的
1
2
3
4
5
var a = 3;
if (true) {
console.log(a); // ReferenceError: a is not defined
let a = 4;
}

IIFE指的是Immediately Invoked Function Expression(立即调用的函数表达式)。它是一个在定义后立即执行的 JavaScript 函数。

IIFE的一般语法如下:

1
2
3
(function () {
// 函数体
})();

上述语法中,在函数定义的末尾加上一对圆括号 (),表示立即调用该函数。

IIFE 的主要作用有两个:

  1. 创建一个独立的作用域:在 IIFE 内部定义的变量和函数在外部是不可访问的,从而避免变量冲突和污染全局命名空间。
  2. 执行一些初始化的操作:可以在 IIFE 内部执行某些操作,并且不会暴露在全局作用域中。

示例:

1
2
3
4
5
6
(function () {
var x = 10;
console.log(x); // 10
})();

console.log(x); // 报错:x is not defined

在上述示例中,IIFE 内部定义了变量 x,并且可以在函数内部访问该变量。而在外部,由于 x 是在 IIFE 内部声明的,因此无法访问到该变量,会报错。

IIFE 还可以接收参数,并在调用时传递参数进去,以便在函数内部使用。这样可以进一步扩展 IIFE 的功能。

1
2
3
(function (name) {
console.log("Hello, " + name);
})("Alice"); // Hello, Alice

在上述示例中,IIFE 接收一个参数 name,并在调用时传递了参数值 "Alice"。在函数内部,通过使用参数 name,输出了相应的消息。

  1. const 声明的常量不可改变
1
2
const a = 1;
a = 2; // TypeError: Assignment to constant variable.
1
2
3
const obj = {a: 1}
obj.a = 2 // 可以
obj = {a: 2} // TypeError: Assignment to constant variable.
  1. 适用于let的同样适用于const

解构赋值

  1. 数组的解构
1
2
3
4
5
6
7
let [a,b,c] = [1,2,3]
console.log(a,b,c) // 1 2 3

let [a,[b],c] = [2,[4],6]
console.log(a,b,c) // 2 4 6

let [a] = 1 // TypeError: 1 is not iterable
  1. 默认值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let [a,b,c=3] = [1,2]
console.log(a,b,c) // 1 2 3

let [a,b,c=3] = [1,2,undefined]
console.log(a,b,c) // 1 2 3

let [a,b,c=3] = [1,2,null]
console.log(a,b,c) // 1 2 null

let [a,b=2] = [3,4]
console.log(a,b) // 3 4

let [a,b=2] = [3,undefined]
console.log(a,b) // 3 2

let [a,b=2] = [3,null]
console.log(a,b) // 3 null

数组对应对值有没有? 如果没有(数组对没有指的是undefined), 就用默认值 如果有, 就用对应的值

1
2
let [a=2,b=3] = [undefined,null]
console.log(a,b) // 2 null
1
2
let [a=1, b=a] = [2]
console.log(a,b) // 2 2
  1. 对象的解构赋值

前置知识

1
2
3
4
let [name, age] = ["hunger", 3]
let p1 = {name, age} // 对象的简写
// 等同与
let p2 = {name: name, age: age}

解构范例

1
2
let {name, age} = {name: "hunger", age: 3}
console.log(name, age) // hunger 3

以上代码等同于

1
2
let {name: name, age: age} = {name: "hunger", age: 3}
console.log(name, age) // hunger 3
  1. 默认值
1
2
let {name, age=3} = {name: "hunger"}
console.log(name, age) // hunger 3
  1. 函数结构
1
2
3
4
5
function add([x,y]=[1,2]){
return x + y
}
add() // 3
add([3,4]) // 7
1
2
3
4
function sum ({x, y} = {x: 0, y: 0}, {a = 1, b = 2}){ // {x, y} = {x: 0, y: 0} 是默认值 // {a = 1, b = 2} 是解构赋值
return [x + a, y + b];
}
sum({x:1, y:2}, {a:2}) // [3, 4]
  1. 作用
1
2
3
let [x, y] = [1, 2];
[x, y] = [y, x];
console.log(x, y); // 2 1
1
2
3
4
function ajax({url, type="GET"}){

}
ajax({url: "xxx"}) // 设置了默认值: type="GET"

字符串-函数-数组-对象

字符串

  1. 多行字符串
1
2
let str = `hello
world`
  1. 字符串模板
1
2
3
let name = "hunger"
let age = 3
let str = `hello, ${name}, age is ${age}`
  1. 字符串查找
1
2
3
4
let str = "hello world"
str.includes("hello") // true
str.startsWith("hello") // true
str.endsWith("world") // true

数组

  1. 扩展运算符
1
2
let arr = [1,2,3]
console.log(...arr) // 1 2 3
  1. 合并数组
1
2
3
4
let arr1 = [1,2,3]
let arr2 = [4,5,6]
let arr3 = [...arr1, ...arr2]
console.log(arr3) // [1,2,3,4,5,6]
  1. 数组克隆
1
2
3
let arr1 = [1,2,3]
let arr2 = [...arr1]
console.log(arr2) // [1,2,3]
  1. 函数参数的扩展
1
2
3
4
5
6
7
8
9
function sort(...arr){ // ...arr 是扩展运算符 js会把传进来的参数转成数组
console.log(arr.sort())
}
sort(3,2,1) // [1,2,3]

function max(arr){
return Math.max(...arr)
}
max([3,4,1]) // 4
  1. 类数组对象转数组
1
2
3
4
5
6
7
let ps = document.querySelectorAll("p")
Array.from(ps).forEach(function(p){
console.log(p.innerText)
})
[...ps].forEach(function(p){
console.log(p.innerText)
})

函数

  1. 默认值
1
2
3
4
5
function sayHi(name='jirengu'){
console.log(`hi, ${name}`)
}
sayHi() // hi, jirengu
sayHi("Zkeq") // hi, Zkeq
1
2
3
4
function fetch (url, {body="", method="GET", headers={}}){
console.log(method)
}
fetch("http://www.baidu.com", {}) // GET

以下两种写法的区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ex1
function m1({x=0, y=0} = {}) {
return [x, y];
}
// ex2
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}

// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]

// x 和 y 都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x 有值,y 无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// x 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

ex1: 调用函数需要传入一个对象, 如果不传, 就用默认值 `{}`, 默认值对象里面都是 undefined, 所以属性使用初始值
ex2: 调用函数需要传入一个对象, 如果不传, 就用默认值 `{x: 0, y: 0}`, 如果传了对象, 就用传入的对象
  1. 箭头函数
1
2
3
4
5
6
7
let f = v => v + 1
f(2) // 3
// 等价于
var f = function(v){
return v + 1
}
f(2) // 3
1
2
3
4
5
6
7
var f = () => 5
f() // 5
// 等价于
var f = function(){
return 5
}
f() // 5
1
2
3
4
5
6
7
var sum = (num1, num2) => num1 + num2
sum(1,2) // 3
// 等价于
var sum = function(num1, num2){
return num1 + num2
}
sum(1,2) // 3
1
2
3
4
5
6
7
8
9
var arr = [1,2,3]
var arr2 = arr.map(v=>v*v)
arr2 // [1,4,9]
// 等价于
var arr = [1,2,3]
var arr2 = arr.map(function(v){
return v*v
})
arr2 // [1,4,9]

箭头函数里的 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
// 解释: ES6 的箭头函数没有自己的 this,内部的 this 就是外层代码块的 this。

对象

1
2
3
var name = 'jirengu'
var age = 3
var people = {name, age} // {name: "jirengu", age: 3}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let app = {
init(){
console.log("init")
},
start(){
console.log("start")
}
}
app.init() // init

// 等价于
let app = {
init: function(){
console.log("init")
},
start: function(){
console.log("start")
}
}

模块化

  1. export

写法1

1
2
3
4
5
6
7
8
9
10
// a.js
export var a = 1
export function add(x,y){
return x + y
}
export class Person{
constructor(name){
this.name = name
}
}
1
2
3
4
5
// b.js
import {a, add, Person} from "./a.js"
console.log(a) // 1
console.log(add(1,2)) // 3
console.log(new Person("hunger")) // Person {name: "hunger"}
1
2
3
4
5
// b.js
import * as a from "./a.js"
console.log(a.a) // 1
console.log(a.add(1,2)) // 3
console.log(new a.Person("hunger")) // Person {name: "hunger"}
1
2
3
// b.js
import {a as b} from "./a.js"
console.log(b) // 1

写法2

1
2
3
4
5
6
7
8
9
10
11
// a.js
var a = 1
function add(x,y){
return x + y
}
class Person{
constructor(name){
this.name = name
}
}
export {a, add, Person}

使用

1
2
3
4
5
// b.js
import {a, add, Person} from "./a.js"
console.log(a) // 1
console.log(add(1,2)) // 3
console.log(new Person("hunger")) // Person {name: "hunger"}
1
2
3
4
5
// b.js
import * as a from "./a.js"
console.log(a.a) // 1
console.log(a.add(1,2)) // 3
console.log(new a.Person("hunger")) // Person {name: "hunger"}
1
2
3
// b.js
import {a as b} from "./a.js"
console.log(b) // 1

写法3

1
2
3
4
// a.js
export function getName(){}
export function getAge(){}
// 注意的是, 导出函数的时候不可以赋值给变量
1
2
3
// b.js
import {getName, getAge} from "./a.js"
getName()

写法4

1
2
3
4
// a.js
function getName(){}
function getAge(){}
export {getName, getAge}
1
2
3
// b.js
import {getName, getAge} from "./a.js"
getName()

写法5

1
2
3
export default function(){
console.log("hello")
}
1
2
3
// b.js
import foo from "./a.js" // 注意的是, 导入的时候可以赋值给变量, 重新命名
foo()

类和继承

  1. 构造函数
1
2
3
4
5
6
7
8
9
10
11
class Person{
constructor(name){
this.name = name
}
sayHi(){
console.log(`hi, ${this.name}`)
}
}

let p = new Person("hunger")
p.sayHi() // hi, hunger

等价于

1
2
3
4
5
6
7
8
function Person(name){
this.name = name
}
Person.prototype.sayHi = function(){
console.log(`hi, ${this.name}`)
}

let p = new Person("hunger")
  1. 静态方法
1
2
3
4
5
6
7
8
class EventCenter {
static fire() {
return "fire";
}
static on() {
return "on";
}
}

等价于

1
2
3
4
5
6
7
function EventCenter(){};
EventCenter.fire = function(){
return "fire";
}
EventCenter.on = function(){
return "on";
}
  1. 继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person{
constructor(name){
this.name = name;
}
sayHi(){
console.log(`hi, ${this.name}`);
}
}

class Student extends Person{
constructor(name, number){
super(name);
this.number = number;
}
sayHi(){
console.log(`姓名 ${this.name} 学号 ${this.number}`);
}
}

Vue基础知识

阅读以下 vue 教程,跟随教程手写并运行代码。

  1. 介绍
  2. Vue 实例
  3. 模板语法
  4. 计算属性和观察者
  5. class 和 style 绑定
  6. 条件渲染
  7. 列表渲染
  8. 表单输入绑定
  9. 组件

Vue-router 初体验 && Vue-router 基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)

// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]

// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
router
}).$mount('#app')

// 现在,应用已经启动了!

动态路由匹配

1
2
3
4
5
6
7
8
9
10
const User = {
template: '<div>User</div>'
}

const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: '/user/:id', component: User }
]
}
模式 匹配路径 $route.params
/user/:username /user/evan { username: 'evan' }
/user/:username/post/:post_id /user/evan/post/123 { username: 'evan', post_id: '123' }

捕获所有路由或 404 Not found 路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
// 会匹配所有路径
path: '*'
}
{
// 会匹配以 `/user-` 开头的任意路径
path: '/user-*'
}

// 给出一个路由 { path: '/user-*' }
this.$router.push('/user-admin')
this.$route.params.pathMatch // 'admin'
// 给出一个路由 { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'

匹配优先级

有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:路由定义得越早,优先级就越高。

嵌套路由

Vue.js 添加 进入/离开 & 列表过渡

Vue生命周期图示

Vue 实例生命周期

选项 / 生命周期钩子

实例生命周期钩子

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

比如 created 钩子可以用来在一个实例被创建之后执行代码:

1
2
3
4
5
6
7
8
9
10
new Vue({
data: {
a: 1
},
created: function () {
// `this` 指向 vm 实例
console.log('a is: ' + this.a)
}
})
// => "a is: 1"

也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 mountedupdateddestroyed。生命周期钩子的 this 上下文指向调用它的 Vue 实例。

不要在选项 property 或回调上使用箭头函数,比如 created: () => console.log(this.a)vm.$watch('a', newValue => this.myMethod())。因为箭头函数并没有 thisthis 会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefinedUncaught TypeError: this.myMethod is not a function 之类的错误。

vm.$nextTick

vm.$nextTick 是 Vue 提供的实例方法之一,它的作用是在下次 DOM 更新循环结束之后执行延迟回调。

Vue 在更新 DOM 时是异步执行的,当你修改了 Vue 实例的数据后,Vue 不会立即更新 DOM,而是将这个更新放入一个队列中,等到下一个事件循环时进行批量更新。

而 vm.$nextTick 方法就是用来在 DOM 更新完毕后执行一些回调函数的。这经常用于以下情况:

  1. 当需要操作 DOM 元素的时候,因为通过 vm.$nextTick 方法可以确保我们操作的是最新的 DOM。

  2. 当需要等待 DOM 更新的时候执行一些其他逻辑,例如获取元素宽高、计算元素位置等。

下面是一个示例,展示了使用 vm.$nextTick 的常见用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在数据改变后立即获取更新后的 DOM
Vue.component('example-component', {
template: '<div>{{ message }}</div>',
data() {
return {
message: 'Hello Vue.js'
};
},
mounted() {
this.message = 'Hello World';
this.$nextTick(function() {
console.log(this.$el.textContent); // 'Hello World'
});
}
});

在上面的示例中,当数据 message 被更新为 ‘Hello World’ 后,我们使用了 vm.$nextTick 方法来确保获取更新后的 DOM 内容。

总结一下,vm.$nextTick 方法允许我们在 DOM 更新之后执行一些回调,以确保我们操作最新的 DOM 或执行一些其他逻辑。

聊一聊Vue 的生命周期?beforeCreate、created、beforeMount、mounted 分别有什么区别?

Vue的生命周期钩子函数指的是在Vue实例从创建、运行到销毁的过程中,会触发的一系列函数。这些函数可以让开发者在不同的阶段添加自己的逻辑代码,以实现更灵活的控制。

在Vue的生命周期中,包括了以下四个阶段:

  1. beforeCreate(创建前):在实例初始化之前被调用。此时,Vue实例的数据观测、事件和生命周期钩子都尚未初始化。
  2. created(创建后):在实例创建完成之后被调用。此时,Vue实例已完成数据观测、属性和方法的设置,但是$el属性尚未被创建,因此无法访问到DOM元素。
  3. beforeMount(挂载前):在挂载元素之前被调用,此时,模板编译已完成,但是还没有将编译后的模板替换到页面上的DOM中。
  4. mounted(挂载后):在挂载元素之后被调用,此时,Vue实例已经完成了DOM的挂载,可以访问到通过$el属性获取到的DOM元素。

这四个生命周期钩子函数的区别如下:

  • beforeCreate:在实例初始化之前被调用,此时Vue实例还没有初始化完成,无法访问到实例的数据和方法。
  • created:在实例创建之后被调用,此时Vue实例已经完成了数据观测和属性、方法的设置,但是DOM还没有挂载,无法访问到$el。
  • beforeMount:在挂载元素之前被调用,此时编译已完成,但还没有将模板替换到DOM中,可以在此阶段进行一些DOM操作。
  • mounted:在挂载元素之后被调用,Vue实例已经完成了DOM的挂载,可以访问到通过$el属性获取到的DOM元素,常用于初始化插件、获取服务器数据等操作。

总的来说,beforeCreate和created主要用于初始化Vue实例的数据和方法,beforeMount和mounted主要用于DOM操作和其他Vue实例的初始化操作。

如何进行不同组件间事件传递?

在 Vue 中,组件之间的事件传递可以通过以下几种方式来实现:

  1. 父子组件通信:父组件可以通过 props 向子组件传递数据,并在子组件中使用 $emit 触发自定义事件来通知父组件。父组件可以在子组件上监听这些自定义事件,并通过回调函数获取传递的数据。

  2. 子组件向父组件通信:子组件可以通过 $emit 触发自定义事件并传递数据到父组件。父组件可以在子组件上使用 v-on 监听这些自定义事件,并通过回调函数获取传递的数据。

  3. 兄弟组件通信:可以使用一个共享的 Vue 实例或者其他的中央事件总线(例如 Vue 的事件系统或者一个全局的事件总线库,比如 EventBus)来实现兄弟组件之间的通信。兄弟组件通过共享的实例或事件总线来触发和监听事件,从而传递信息。

  4. 跨级组件通信:如果组件处于不同的层级,可以使用 provide 和 inject 来进行跨级组件通信。父级组件通过 provide 来提供数据,而子孙级组件通过 inject 来注入数据。

这些是 Vue 中常用的几种组件间事件传递方式。具体使用哪种方式取决于你的应用场景和组件的关系。