课程效果
项目简介
本课程将以 evernote 云笔记 mac 客户端版的 ui 做原型,做一款线上的云笔记 webapp 产品。产品包括登录、注册、笔记本的创建、修改、删除,笔记的markdown编辑、删除到回收站、markdown 预览、回收站管理等功能。采用前后端分离的开发方式,本课程只负责前端部分的开发。
前置知识
var-let-const
- var 可声明前置
- 声明前置:变量可以在声明之前使用,值为 undefined
在JavaScript中,变量声明前置是指在代码执行过程中,JavaScript解析器会在代码执行之前将变量声明提升到作用域的顶部。这意味着你可以在变量声明之前使用变量,而不会引发错误。
例子:
| console.log(x); // undefined | |
| var x = 5; |
在上面的例子中,尽管变量
x在打印语句之前被声明,但由于变量声明前置的特性,代码不会引发错误,而是打印出undefined。这是因为在执行代码之前,var x会被提升到作用域的顶部,但是变量的赋值操作仍然是按照代码的实际顺序执行的。需要注意的是,只有变量声明会被提升,而变量赋值并不会被提升。因此,在使用变量之前进行赋值操作仍会导致变量值为
undefined。
| console.log(x); // undefined | |
| var x = 5; | |
| console.log(x); // 5 |
| a = 3; | |
| var a; // undefined | |
| console.log(a); // 3 | |
| var a = 3; |
- let 不可声明前置
| a = 3; // ReferenceError: a is not defined | |
| let a; | |
| let a = 3; // SyntaxError: Identifier 'a' has already been declared |
- let 不可重复声明
| let a = 3; | |
| let a = 4; // SyntaxError: Identifier 'a' has already been declared | |
| let a = 5; // SyntaxError: Identifier 'a' has already been declared |
- let 有块级作用域
| if (true) { | |
| let a = 3; | |
| } | |
| console.log(a); // ReferenceError: a is not defined |
| for (let i = 0; i < 3; i++) { | |
| console.log(i); | |
| } | |
| console.log(i); // ReferenceError: i is not defined |
- IIFE 的替换
- IIFE
| (function () { | |
| var a = 3; | |
| })(); | |
| console.log(a); // ReferenceError: a is not defined |
| { | |
| let a = 3; | |
| } | |
| console.log(a); // ReferenceError: a is not defined |
暂时性死区
- 在let声明变量之前都是该变量的死区, 在死区内该变量是不能被访问的
| var a = 3; | |
| if (true) { | |
| console.log(a); // ReferenceError: a is not defined | |
| let a = 4; | |
| } |
IIFE指的是Immediately Invoked Function Expression(立即调用的函数表达式)。它是一个在定义后立即执行的 JavaScript 函数。
IIFE的一般语法如下:
| (function () { | |
| // 函数体 | |
| })(); |
上述语法中,在函数定义的末尾加上一对圆括号
(),表示立即调用该函数。IIFE 的主要作用有两个:
- 创建一个独立的作用域:在 IIFE 内部定义的变量和函数在外部是不可访问的,从而避免变量冲突和污染全局命名空间。
- 执行一些初始化的操作:可以在 IIFE 内部执行某些操作,并且不会暴露在全局作用域中。
示例:
| (function () { | |
| var x = 10; | |
| console.log(x); // 10 | |
| })(); | |
| console.log(x); // 报错:x is not defined |
在上述示例中,IIFE 内部定义了变量
x,并且可以在函数内部访问该变量。而在外部,由于x是在 IIFE 内部声明的,因此无法访问到该变量,会报错。IIFE 还可以接收参数,并在调用时传递参数进去,以便在函数内部使用。这样可以进一步扩展 IIFE 的功能。
| (function (name) { | |
| console.log("Hello, " + name); | |
| })("Alice"); // Hello, Alice |
在上述示例中,IIFE 接收一个参数
name,并在调用时传递了参数值"Alice"。在函数内部,通过使用参数name,输出了相应的消息。
- const 声明的常量不可改变
| const a = 1; | |
| a = 2; // TypeError: Assignment to constant variable. |
| const obj = {a: 1} | |
| obj.a = 2 // 可以 | |
| obj = {a: 2} // TypeError: Assignment to constant variable. |
- 适用于let的同样适用于const
解构赋值
- 数组的解构
| 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 |
- 默认值
| 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), 就用默认值 如果有, 就用对应的值
| let [a=2,b=3] = [undefined,null] | |
| console.log(a,b) // 2 null |
| let [a=1, b=a] = [2] | |
| console.log(a,b) // 2 2 |
- 对象的解构赋值
前置知识
| let [name, age] = ["hunger", 3] | |
| let p1 = {name, age} // 对象的简写 | |
| // 等同与 | |
| let p2 = {name: name, age: age} |
解构范例
| let {name, age} = {name: "hunger", age: 3} | |
| console.log(name, age) // hunger 3 |
以上代码等同于
| let {name: name, age: age} = {name: "hunger", age: 3} | |
| console.log(name, age) // hunger 3 |
- 默认值
| let {name, age=3} = {name: "hunger"} | |
| console.log(name, age) // hunger 3 |
- 函数结构
| function add([x,y]=[1,2]){ | |
| return x + y | |
| } | |
| add() // 3 | |
| add([3,4]) // 7 |
| 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] |
- 作用
| let [x, y] = [1, 2]; | |
| [x, y] = [y, x]; | |
| console.log(x, y); // 2 1 |
| function ajax({url, type="GET"}){ | |
| } | |
| ajax({url: "xxx"}) // 设置了默认值: type="GET" |
字符串-函数-数组-对象
字符串
- 多行字符串
| let str = `hello | |
| world` |
- 字符串模板
| let name = "hunger" | |
| let age = 3 | |
| let str = `hello, ${name}, age is ${age}` |
- 字符串查找
| let str = "hello world" | |
| str.includes("hello") // true | |
| str.startsWith("hello") // true | |
| str.endsWith("world") // true |
数组
- 扩展运算符
| let arr = [1,2,3] | |
| console.log(...arr) // 1 2 3 |
- 合并数组
| let arr1 = [1,2,3] | |
| let arr2 = [4,5,6] | |
| let arr3 = [...arr1, ...arr2] | |
| console.log(arr3) // [1,2,3,4,5,6] |
- 数组克隆
| let arr1 = [1,2,3] | |
| let arr2 = [...arr1] | |
| console.log(arr2) // [1,2,3] |
- 函数参数的扩展
| 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 |
- 类数组对象转数组
| let ps = document.querySelectorAll("p") | |
| Array.from(ps).forEach(function(p){ | |
| console.log(p.innerText) | |
| }) | |
| [...ps].forEach(function(p){ | |
| console.log(p.innerText) | |
| }) |
函数
- 默认值
| function sayHi(name='jirengu'){ | |
| console.log(`hi, ${name}`) | |
| } | |
| sayHi() // hi, jirengu | |
| sayHi("Zkeq") // hi, Zkeq |
| function fetch (url, {body="", method="GET", headers={}}){ | |
| console.log(method) | |
| } | |
| fetch("http://www.baidu.com", {}) // GET |
以下两种写法的区别?
| // 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}`, 如果传了对象, 就用传入的对象 |
- 箭头函数
| let f = v => v + 1 | |
| f(2) // 3 | |
| // 等价于 | |
| var f = function(v){ | |
| return v + 1 | |
| } | |
| f(2) // 3 |
| var f = () => 5 | |
| f() // 5 | |
| // 等价于 | |
| var f = function(){ | |
| return 5 | |
| } | |
| f() // 5 |
| var sum = (num1, num2) => num1 + num2 | |
| sum(1,2) // 3 | |
| // 等价于 | |
| var sum = function(num1, num2){ | |
| return num1 + num2 | |
| } | |
| sum(1,2) // 3 |
| 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
| // 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。 |
对象
| var name = 'jirengu' | |
| var age = 3 | |
| var people = {name, age} // {name: "jirengu", age: 3} |
| 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") | |
| } | |
| } |
模块化
- export
写法1
| // a.js | |
| export var a = 1 | |
| export function add(x,y){ | |
| return x + y | |
| } | |
| export class Person{ | |
| constructor(name){ | |
| this.name = name | |
| } | |
| } |
| // 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"} |
| // 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"} |
| // b.js | |
| import {a as b} from "./a.js" | |
| console.log(b) // 1 |
写法2
| // a.js | |
| var a = 1 | |
| function add(x,y){ | |
| return x + y | |
| } | |
| class Person{ | |
| constructor(name){ | |
| this.name = name | |
| } | |
| } | |
| export {a, add, Person} |
使用
| // 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"} |
| // 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"} |
| // b.js | |
| import {a as b} from "./a.js" | |
| console.log(b) // 1 |
写法3
| // a.js | |
| export function getName(){} | |
| export function getAge(){} | |
| // 注意的是, 导出函数的时候不可以赋值给变量 |
| // b.js | |
| import {getName, getAge} from "./a.js" | |
| getName() |
写法4
| // a.js | |
| function getName(){} | |
| function getAge(){} | |
| export {getName, getAge} |
| // b.js | |
| import {getName, getAge} from "./a.js" | |
| getName() |
写法5
| export default function(){ | |
| console.log("hello") | |
| } |
| // b.js | |
| import foo from "./a.js" // 注意的是, 导入的时候可以赋值给变量, 重新命名 | |
| foo() |
类和继承
- 构造函数
| class Person{ | |
| constructor(name){ | |
| this.name = name | |
| } | |
| sayHi(){ | |
| console.log(`hi, ${this.name}`) | |
| } | |
| } | |
| let p = new Person("hunger") | |
| p.sayHi() // hi, hunger |
等价于
| function Person(name){ | |
| this.name = name | |
| } | |
| Person.prototype.sayHi = function(){ | |
| console.log(`hi, ${this.name}`) | |
| } | |
| let p = new Person("hunger") |
- 静态方法
| class EventCenter { | |
| static fire() { | |
| return "fire"; | |
| } | |
| static on() { | |
| return "on"; | |
| } | |
| } |
等价于
| function EventCenter(){}; | |
| EventCenter.fire = function(){ | |
| return "fire"; | |
| } | |
| EventCenter.on = function(){ | |
| return "on"; | |
| } |
- 继承
| 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 教程,跟随教程手写并运行代码。
Vue-router 初体验 && Vue-router 基础
| // 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') | |
| // 现在,应用已经启动了! |
动态路由匹配
| 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 路由
| { | |
| // 会匹配所有路径 | |
| 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 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
比如 created 钩子可以用来在一个实例被创建之后执行代码:
| new Vue({ | |
| data: { | |
| a: 1 | |
| }, | |
| created: function () { | |
| // `this` 指向 vm 实例 | |
| console.log('a is: ' + this.a) | |
| } | |
| }) | |
| // => "a is: 1" |
也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 mounted、updated 和 destroyed。生命周期钩子的 this 上下文指向调用它的 Vue 实例。
不要在选项 property 或回调上使用箭头函数,比如
created: () => console.log(this.a)或vm.$watch('a', newValue => this.myMethod())。因为箭头函数并没有this,this会作为变量一直向上级词法作用域查找,直至找到为止,经常导致Uncaught TypeError: Cannot read property of undefined或Uncaught TypeError: this.myMethod is not a function之类的错误。
vm.$nextTick
vm.$nextTick 是 Vue 提供的实例方法之一,它的作用是在下次 DOM 更新循环结束之后执行延迟回调。
Vue 在更新 DOM 时是异步执行的,当你修改了 Vue 实例的数据后,Vue 不会立即更新 DOM,而是将这个更新放入一个队列中,等到下一个事件循环时进行批量更新。
而 vm.$nextTick 方法就是用来在 DOM 更新完毕后执行一些回调函数的。这经常用于以下情况:
当需要操作 DOM 元素的时候,因为通过 vm.$nextTick 方法可以确保我们操作的是最新的 DOM。
当需要等待 DOM 更新的时候执行一些其他逻辑,例如获取元素宽高、计算元素位置等。
下面是一个示例,展示了使用 vm.$nextTick 的常见用法:
| // 在数据改变后立即获取更新后的 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的生命周期中,包括了以下四个阶段:
- beforeCreate(创建前):在实例初始化之前被调用。此时,Vue实例的数据观测、事件和生命周期钩子都尚未初始化。
- created(创建后):在实例创建完成之后被调用。此时,Vue实例已完成数据观测、属性和方法的设置,但是$el属性尚未被创建,因此无法访问到DOM元素。
- beforeMount(挂载前):在挂载元素之前被调用,此时,模板编译已完成,但是还没有将编译后的模板替换到页面上的DOM中。
- 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 中,组件之间的事件传递可以通过以下几种方式来实现:
父子组件通信:父组件可以通过 props 向子组件传递数据,并在子组件中使用
$emit触发自定义事件来通知父组件。父组件可以在子组件上监听这些自定义事件,并通过回调函数获取传递的数据。子组件向父组件通信:子组件可以通过
$emit触发自定义事件并传递数据到父组件。父组件可以在子组件上使用v-on监听这些自定义事件,并通过回调函数获取传递的数据。兄弟组件通信:可以使用一个共享的 Vue 实例或者其他的中央事件总线(例如 Vue 的事件系统或者一个全局的事件总线库,比如 EventBus)来实现兄弟组件之间的通信。兄弟组件通过共享的实例或事件总线来触发和监听事件,从而传递信息。
跨级组件通信:如果组件处于不同的层级,可以使用 provide 和 inject 来进行跨级组件通信。父级组件通过 provide 来提供数据,而子孙级组件通过 inject 来注入数据。
这些是 Vue 中常用的几种组件间事件传递方式。具体使用哪种方式取决于你的应用场景和组件的关系。





