Javascript经典系列
事件循环机制(Event Loop)
前言
事件循环(Event Loop) 是 JavaScript 运行时环境(如浏览器或 Node.js)处理异步操作的核心机制。它使得 JavaScript 能够在单线程中实现非阻塞的异步行为,同时处理用户交互、网络请求、定时器等任务。
为什么需要事件循环?
JavaScript 是单线程的,意味着它一次只能执行一个任务。如果所有任务都是同步的,那么长时间的任务(如网络请求或复杂计算)会阻塞主线程,导致页面卡顿或无响应。
为了解决这个问题,JavaScript 引入了 异步编程模型,而事件循环就是实现这一模型的核心机制。
Event Loop(事件循环)
Event Loop(事件循环)中,每一次循环称为tick,每一次tick任务如下:
-
同步和异步任务分别进入不同的执行场所,同步的进入主线程,异步的进入
Event Table并注册函数 -
当指定的事情完成时(重点) ,
Event Table会将这个函数移入Event Queue中 -
主线程内的任务执行完毕为空,会去
Event Queue读取对应的函数,进入主线程执行 -
上述的过程会不断的重复,也就是常常说的
Event Loop(事件循环)。
任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
宏任务(Macrotask)和微任务(Microtask)
宏任务
- 包括:
setTimeout、setInterval、setImmediate(Node.js)、I/O操作、UI 渲染(浏览器)。 - 每次事件循环只会执行一个宏任务。
微任务
- 包括:
Promise的then和catch、MutationObserver、process.nextTick(Node.js)。 - 微任务会在当前宏任务执行完毕后立即执行,且会清空整个微任务队列。
事件循环的示例
console.log("Start"); // 同步任务
setTimeout(() => {
console.log("Timeout"); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log("Promise"); // 微任务
});
console.log("End"); // 同步任务
事件循环的重要性
- 非阻塞:事件循环使得 JavaScript 能够处理异步操作,避免阻塞主线程。
- 高效:通过任务队列和微任务队列,事件循环能够高效地管理任务的执行顺序。
- 用户体验:在浏览器中,事件循环确保了页面的流畅交互和渲染。
总结
- 事件循环是 JavaScript 处理异步操作的核心机制。
- 它通过调用栈、任务队列和微任务队列管理任务的执行顺序。
- 宏任务和微任务的区别在于执行优先级:微任务优先于宏任务。
- 浏览器和 Node.js 的事件循环实现略有不同,但核心原理一致。
Promise 和 async/await 的区别是什么
Promise 和 async/await 都是 JavaScript 中处理异步操作的机制,但它们在语法和使用方式上有显著区别。以下是它们的对比:
Promise
-
Promise 是 ES6 引入的一种异步编程解决方案。
-
它表示一个异步操作的最终完成(或失败)及其结果值。
-
Promise 有三种状态:
- Pending:初始状态,既不是成功也不是失败。
- Fulfilled:操作成功完成。
- Rejected:操作失败。
async/await
- async/await 是 ES8(ES2017)引入的语法糖,基于 Promise。
async用于声明一个异步函数,await用于等待一个 Promise 的完成。- 它使异步代码看起来像同步代码,更易于阅读和维护。
语法对比
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched!");
}, 1000);
});
}
fetchData()
.then((data) => {
console.log(data); // 输出: Data fetched!
})
.catch((error) => {
console.error(error);
});
async/await 示例
async function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched!");
}, 1000);
});
}
async function main() {
try {
const data = await fetchData();
console.log(data); // 输出: Data fetched!
} catch (error) {
console.error(error);
}
}
main();
Promise 示例
主要区别
| 特性 | Promise | async/await |
|---|---|---|
| 语法 | 链式调用(.then 和 .catch) | 同步风格的异步代码 |
| 可读性 | 链式调用可能导致代码嵌套,可读性较差 | 代码更简洁,易于阅读和维护 |
| 错误处理 | 使用 .catch 捕获错误 | 使用 try/catch 捕获错误 |
| 执行顺序 | 需要手动管理 Promise 链 | 自动按顺序执行异步操作 |
| 兼容性 | ES6 引入,兼容性较好 | ES8 引入,需要较新的 JavaScript 环境 |
| 适用场景 | 简单的异步操作 | 复杂的异步逻辑 |
详细对比
Promise:当有多个异步操作时,链式调用可能导致代码嵌套(回调地狱)。
fetchData()
.then((data) => {
return processData(data);
})
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error);
});
async/await:代码更直观,类似于同步代码。
async function main() {
try {
const data = await fetchData();
const result = await processData(data);
console.log(result);
} catch (error) {
console.error(error);
}
}
执行顺序
Promise:需要手动管理 Promise 链。
fetchData()
.then((data) => {
return processData(data);
})
.then((result) => {
console.log(result);
});
async/await:自动按顺序执行异步操作。
async function main() {
const data = await fetchData();
const result = await processData(data);
console.log(result);
}
兼容性
- Promise:ES6 引入,现代浏览器和 Node.js 环境广泛支持。
- async/await:ES8 引入,需要较新的 JavaScript 环境(如 Node.js 7.6+ 或现代浏览器)。
使用场景
Promise
- 适合简单的异步操作。
- 需要兼容较旧的 JavaScript 环境时。
async/await
- 适合复杂的异步逻辑。
- 需要提高代码可读性和维护性时。
在实际开发中,async/await 通常是更好的选择,因为它使代码更简洁、更易读。但在某些场景下(如需要兼容旧环境),Promise 仍然是必不可少的工具。两者可以结合使用,充分发挥各自的优势。
Js作用域(Scope)
1. 全局作用域(Global Scope)
- 在全局作用域中声明的变量和函数可以在代码的任何地方访问。
- 全局作用域是默认的作用域,不属于任何函数或块。
示例:
var globalVar = "I'm global"; // 全局变量
function foo() {
console.log(globalVar); // 可以访问全局变量
}
foo();
console.log(globalVar); // 输出: I'm global
2. 函数作用域(Function Scope)
- 在函数内部声明的变量和函数只能在该函数内部访问。
- 函数作用域是 JavaScript 中实现变量隔离和封装的重要机制。
示例:
function bar() {
var localVar = "I'm local"; // 函数作用域内的变量
console.log(localVar); // 输出: I'm local
}
bar();
console.log(localVar); // 报错: localVar is not defined
3. 块级作用域(Block Scope)
- 在块(如
{}、if、for、while等)内部声明的变量只能在该块内部访问。 - 块级作用域是 ES6 引入的特性,使用
let和const声明的变量具有块级作用域。
示例:
if (true) {
let blockVar = "I'm block scoped"; // 块级作用域内的变量
console.log(blockVar); // 输出: I'm block scoped
}
console.log(blockVar); // 报错: blockVar is not defined
4. 词法作用域(Lexical Scope)
- 词法作用域是指函数在定义时确定的作用域,而不是在调用时确定。
- JavaScript 采用词法作用域,也称为静态作用域。
示例:
var x = 10; // 全局变量
function outer() {
var x = 20; // 函数作用域内的变量
function inner() {
console.log(x); // 访问 outer 函数作用域内的 x
}
return inner;
}
const innerFn = outer();
innerFn(); // 输出: 20
✅ 闭包机制(Closure)
• inner 函数在 outer 函数内部定义,并返回;
• 返回的 innerFn 依旧持有对 outer 函数作用域的引用(闭包形成);
• 执行 innerFn() 时,inner() 会先在自己作用域查找 x,找不到,再往上找,找到了 outer 作用域中的 var x = 20; ;
• 全局的 var x = 10; 完全被 outer 作用域的 x 遮蔽(shadow)了;
5. 作用域链(Scope Chain)
- 作用域链是指在当前作用域中查找变量时,会沿着作用域链逐级向上查找,直到找到变量或到达全局作用域。
- 作用域链是基于词法作用域的。
示例:
var a = 1; // 全局变量
function outer() {
var b = 2; // outer 函数作用域内的变量
function inner() {
var c = 3; // inner 函数作用域内的变量
console.log(a + b + c); // 输出: 6
}
inner();
}
outer();
6. 作用域的类型对比
| 作用域类型 | 描述 | 声明方式 |
|---|---|---|
| 全局作用域 | 在全局作用域中声明的变量和函数可以在代码的任何地方访问。 | var, let, const |
| 函数作用域 | 在函数内部声明的变量和函数只能在该函数内部访问。 | var |
| 块级作用域 | 在块内部声明的变量只能在该块内部访问。 | let, const |
| 词法作用域 | 函数在定义时确定的作用域,而不是在调用时确定。 | - |
7. 作用域的应用
7.1 避免变量污染
-
使用函数作用域或块级作用域可以避免变量污染全局作用域。
javascript
复制
(function() { var localVar = "I'm local"; console.log(localVar); // 输出: I'm local })(); console.log(localVar); // 报错: localVar is not defined7.2 实现封装
-
使用函数作用域可以隐藏内部实现细节,只暴露必要的接口。
javascript
复制
function createCounter() { let count = 0; // 私有变量 return function() { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 输出: 1 console.log(counter()); // 输出: 27.3 闭包
-
闭包是函数作用域的一个重要应用,函数可以访问其词法作用域中的变量,即使函数在其词法作用域外执行。
javascript
复制
function outer() { var x = 10; function inner() { console.log(x); // 访问外部函数的变量 } return inner; } const closureFn = outer(); closureFn(); // 输出: 108. 总结
-
全局作用域:变量和函数可以在代码的任何地方访问。
-
函数作用域:变量和函数只能在函数内部访问。
-
块级作用域:变量只能在块内部访问(使用
let和const)。 -
词法作用域:函数在定义时确定的作用域。
-
作用域链:查找变量时沿着作用域链逐级向上查找。
理解作用域是掌握 JavaScript 的基础,它直接影响变量的生命周期、可访问性以及代码的组织方式。
this指向
在 JavaScript 中,this 是一个特殊的关键字,它的值取决于函数的调用方式。this 的指向在函数执行时确定,而不是在定义时确定。以下是 this 的几种常见指向:
1. 全局上下文
在全局作用域中,this 指向全局对象。在浏览器中,全局对象是 window,在 Node.js 中,全局对象是 global。
javascript
复制
console.log(this); // 在浏览器中输出: Window {...}
2. 函数上下文
在普通函数中,this 的指向取决于函数的调用方式:
- 默认绑定:在非严格模式下,普通函数中的
this指向全局对象(浏览器中是window,Node.js 中是global)。在严格模式下,this为undefined。
javascript
复制
function foo() {
console.log(this);
}
foo(); // 在浏览器中输出: Window {...} (非严格模式)
// 在严格模式下输出: undefined
- 方法调用:当函数作为对象的方法调用时,
this指向调用该方法的对象。
const obj = {
name: 'Alice',
greet: function() {
console.log(this.name);
}
};
obj.greet(); // 输出: Alice
3. 构造函数
当函数作为构造函数(使用 new 关键字调用)时,this 指向新创建的对象实例。
javascript
复制
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
console.log(alice.name); // 输出: Alice
4. 显式绑定
可以使用 call、apply 或 bind 方法显式地设置 this 的值。
call和apply:立即调用函数,并指定this的值。
function greet() {
console.log(this.name);
}
const obj = { name: 'Alice' };
greet.call(obj); // 输出: Alice
greet.apply(obj); // 输出: Alice
bind:返回一个新函数,并将this绑定到指定的对象。
const boundGreet = greet.bind(obj);
boundGreet(); // 输出: Alice
5. 箭头函数
箭头函数没有自己的 this,它的 this 继承自外层作用域。
const obj = {
name: 'Alice',
greet: function() {
setTimeout(() => {
console.log(this.name);
}, 100);
}
};
obj.greet(); // 输出: Alice
6. 事件处理函数
在 DOM 事件处理函数中,this 通常指向触发事件的元素。
document.querySelector('button').addEventListener('click', function() {
console.log(this); // 输出: <button>...</button>
});
总结
this的指向取决于函数的调用方式。- 普通函数中,
this可以是全局对象、undefined或调用对象。 - 箭头函数中的
this继承自外层作用域。 - 可以使用
call、apply或bind显式设置this。
理解 this 的指向是掌握 JavaScript 的关键之一。
ES6系列
var、let、const区别
| 区别 | var | let或const |
|---|---|---|
| 变量提升 | 存在 | 不存在 |
| 重复声明 | 允许 | 不允许 |
| 块级作用域 | 存在 | 不存在 |
| 顶层window属性 | 属于 | 不属于 |
const常用于声明常量,不能改变;且声明同时必须赋值;
var
在ES5中,顶层对象的属性和全局变量是等价的,用var声明的变量既是全局变量也是顶层变量;
注意:顶层对象,在浏览器中指的是window对象,在node中指的是global对象。
var a = 10;
console.log(window.a); // 10
使用var声明的变量存在变量提升的情况
console.log(a); // undefined
var a = 10;
在编译阶段,编译器会将其变成以下执行
var a;
console.log(a);
a = 10;
使用var,我们可以对一个变量重复声明,后面声明的变量会覆盖前面声明的变量
var a = 10;
var a = 20;
console.log(a); // 20
在函数中使用var声明变量时,该变量是局部的
var a = 10;
function fn() {
console.log(a) // undefined
var a = 20;
console.log(a); // 20
a = 30;
console.log(a); // 30
}
fn();
console.log(a); // 10
而如果在函数内不使用var,该变量是全局的
只有使用了var才会变量提升
var a = 10;
function fn() {
console.log(a); // 10
// console.log(b); //ReferenceError: b is not undefined
console.log(window.b); // undefined
a = 20;
console.log(a); // 20
b = 100;
console.log(b); // 100
console.log(window.b); // 100
}
fn();
console.log(a); // 20
console.log(b); // 100
console.log(window.a); // 20
console.log(window.b); // 100
let
let是ES6新增的命令,用来声明变量
用法类似于var,但是所有声明的变量,只在let所命令的代码块内有效
{
let b = 10;
console.log(b); // 10;
}
console.log(b); // ReferenceError: b is not undefined
let不存在变量提升
console.log(b); // ReferenceError: b is not undefined
let b = 10;
let不允许在相同作用域中重复声明
let b = 10;
{
let b = 20;
// let b = 30; // SyntaxError: Identifier 'b' has already been declared
}
console.log(b); // 10
let定义的变量不属于顶层window对象属性
let b = 20;
console.log(window.b); // undefined
只要块级作用域内存在let命令,这个区域就不再受外部影响
使用let变量声明前,该变量都不可以用,也就是大家常说的“暂时性死区”
var b = 10;
{
// console.log(b); // ReferenceError: Cannot access 'b' before initialization
// b = 20; // ReferenceError: Cannot access 'b' before initialization
//console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 30;
console.log(b); // 30
}
console.log(b); // 10
const
const声明一个只读的常量,一旦声明,常量的值则不能改变
const c = 10;
c = 20; // TypeError: Assignment to constant variable.
这意味着,const一旦声明,就必须立即初始化,不能留到以后赋值
const c; // SyntaxError: Missing initializer in const declaration
如果之前用var或let声明过变量,再用const声明同样会报错
let a = 1;
// var a = 2; // SyntaxError: Identifier 'a' has already been declared
const a = 3; // SyntaxError: Identifier 'a' has already been declared
const实际保证的并不是变量的值不得改动,而是变量所指向的那个内存地址所保存的数据不得改动
对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量
对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的,并不能确保变量的结构不变
延伸:栈堆内存
const obj = {}
// 为obj添加一个属性可以成功
obj.name = 'wxx'
console.log(obj.name); // wxx
// 将obj指向另一个对象就会报错
obj = {} // TypeError: Assignment to constant variable.
var、let、const共同点:
全局作用域中定义的变量,可以在函数中使用;
函数中声明的变量,只能在函数及其子函数中使用,外部无法使用
延伸:闭包
Vue系列
vue是什么
Vue.js(/vjuː/,或简称为Vue)
是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用(SPA)用框架。
Vue所关注的核心是MVC模式中的视图层,同时,它也能方便地获取数据更新,并通过组件内部特定的方法实现视图与模型的交互。
说说你对SPA(单页应用)的理解?
什么是SPA
SPA(single-page application),翻译过来就是单页应用。
SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验;
在单页应用中,所有必要的代码(HTML、JavaScript和CSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面;
页面在任何时间点都不会重新加载,也不会将控制转移到其他页面。
举个例子来讲就是一个杯子,早上装的牛奶,中午装的是开水,晚上装的是茶,我们发现,变的始终是杯子里的内容,而杯子始终是那个杯子;
我们熟知的JS框架如react,vue,angular,ember都属于SPA
SPA和MPA的区别
MPA(MultiPage-page application),翻译过来就是多页应用。
在MPA中,每个页面都是一个主页面,都是独立的;
当我们在访问另一个页面的时候,都需要重新加载html、css、js文件,公共文件则根据需求按需加载。
单页应用与多页应用的区别
| 单页面应用(SPA) | 多页面应用(MPA) | |
|---|---|---|
| 组成 | 一个主页面和多个页面片段 | 多个主页面 |
| 刷新方式 | 局部刷新 | 整页刷新 |
| url模式 | 哈希模式 | 历史模式 |
| SEO搜索引擎优化 | 难实现,可使用SSR方式改善 | 容易实现 |
| 数据传递 | 容易 | 通过url、cookie、localStorage等传递 |
| 页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
| 维护成本 | 相对容易 | 相对复杂 |
单页应用优缺点
优点:
- 具有桌面应用的即时性、网站的可移植性和可访问性
- 用户体验好、快,内容的改变不需要重新加载整个页面
- 良好的前后端分离,分工更明确
缺点:
- 不利于搜索引擎的抓取
- 首次渲染速度相对较慢
实现一个SPA
原理
- 监听地址栏中
hash变化驱动界面变化 - 用
pushsate记录浏览器的历史,驱动界面发送变化
实现
hash模式
核心通过监听url中的hash来进行路由跳转
// 定义 Router
class Router {
constructor () {
this.routes = {}; // 存放路由path及callback
this.currentUrl = '';
// 监听路由change调用相对应的路由回调
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback){
this.routes[path] = callback;
}
push(path) {
this.routes[path] && this.routes[path]()
}
}
// 使用 router
window.miniRouter = new Router();
miniRouter.route('/', () => console.log('page1'))
miniRouter.route('/page2', () => console.log('page2'))
miniRouter.push('/') // page1
miniRouter.push('/page2') // page2
history模式
history 模式核心借用 HTML5 history api,api 提供了丰富的 router 相关属性
先了解几个相关的api
history.pushState浏览器历史纪录添加记录history.replaceState修改浏览器历史纪录中当前纪录history.popState当history发生变化时触发
// 定义 Router
class Router {
constructor () {
this.routes = {};
this.listerPopState()
}
init(path) {
history.replaceState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
route(path, callback){
this.routes[path] = callback;
}push(path) {
history.pushState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
listerPopState () {
window.addEventListener('popstate' , e => {
const path = e.state && e.state.path;
this.routers[path] && this.routers[path]()
})
}
}
// 使用 Router
window.miniRouter = new Router();
miniRouter.route('/', ()=> console.log('page1'))
miniRouter.route('/page2', ()=> console.log('page2'))
// 跳转
miniRouter.push('/page2') // page2
vue核心特性
数据驱动(MVVM)
MVVM表示的是Model-View-ViewMadel;
Model:数据模型层,负责处理业务逻辑以及和服务器进行交互。
View:视图层,负责把数据模型转化为UI展示,可以简单的理解为HTML。
ViewModel:视图模型层,用于连接Model和View,是View和Model之间通信的桥梁。
在MVVM架构中,是不允许数据和视图直接通信的,只能通过ViewModel来通信,而ViewModel就是定义了一个Observer观察者。ViewModel是连接View和Model的中间件。
ViewModel能够观察到数据的变化,并对视图对应的内容进行更新。
ViewModel能够监听到视图的变化,并能够通知数据发生变化。
总结:View Model是通信桥梁,View与Model是通过View Model实现数据双向绑定。
组件化
1.什么是组件化
- 来说就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在
Vue中每一个.vue文件都可以视为一个组件。
2.组件化的优势
- 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现。
- 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单。
- 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级。
指令系统
解释:指令 (Directives) 是带有 v- 前缀的特殊属性
作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM
- 常用的指令
-
- 条件渲染指令
v-if - 列表渲染指令
v-for - 属性绑定指令
v-bind - 事件绑定指令
v-on - 双向数据绑定指令
v-model
- 条件渲染指令
说说你对双向绑定的理解?
计算属性VS方法VS侦听器
计算属性(computed)
计算属性是基于它们的反应依赖关系缓存的,计算属性只在相关响应式依赖发生改变时它们才会重新求值。 其依赖值未发生改变时,取的则是之前计算的缓存中的值。
方法(methods)
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
侦听器(watch)
每当监听的值发生改变时,就会调用函数。
一些实用属性:
// 初始化的时候调用函数
immdiate: true/false
// 深度监听对象属性
deep: true/false
// 监听函数
handle:function(newVal,oldVal){}
Vue中v-show和v-if的区别?
控制手段不同
v-if显示隐藏是将dom元素整个添加或删除;v-show隐藏则是为该元素添加css--display:none,dom元素依旧还在。
编译过程不同
v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;v-show只是简单的基于css切换。
编译条件不同
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块;
相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
v-if由false变为true的时候,触发组件的beforeCreate、create、beforeMount、mounted钩子,由true变为false的时候触发组件的beforeDestroy、destroyed方法。v-show不会触发组件的生命周期。
性能消耗不同
一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
为什么Vue中的v-if和v-for不建议一起用?
v-for优先级比v-if高
解决办法
1、如果v-if 和 v-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
2、如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
3、如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项
为什么使用v-for的时候必须要添加唯一的key?
为了给Vue一个提示,以便于它能跟踪每个节点的身份,从而重用和重新排序;
使用v-for更新已渲染的元素列表时,默认用就地复用策略;列表数据修改的时候,他会根据key值去判断某个值是否修改,如果修改,则重新渲染这一项,否则复用之前的元素。\
key的作用主要是为了高效的更新虚拟DOM。 另外vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。
vue传值的几种方式
props
父组件向子组件传值,单向数据流(父组件数据发生改变,子组件也受到影响,反之不行)。
Parent.vue 传送:
<template>
<child :msg="msg"></child>
</template>
Child.vue 接收:
export default {
// 写法一 用数组接收
props:['msg'],
// 写法二 用对象接收,可以限定接收的数据类型、设置默认值、验证等
props:{
msg:{
type: String,
default: '默认数据'
}
},
mounted() {
console.log(this.msg)
},
}
$emit / v-on
子组件通过派发事件的方式给父组件数据,或者触发父组件更新等操作。
// Child.vue 派发
export default {
data(){
return { msg: "这是发给父组件的信息" }
},
methods: {
handleClick(){
this.$emit("sendMsg",this.msg)
}
},
}
// Parent.vue 响应
<template>
<child v-on:sendMsg="getChildMsg"></child>
// 或 简写
<child @sendMsg="getChildMsg"></child>
</template>
export default {
methods:{
getChildMsg(msg){
console.log(msg) // 这是父组件接收到的消息
}
}
}
$refs
ref 如果在普通的DOM元素上,引用指向的就是该DOM元素;
如果在子组件上,引用的指向就是子组件实例;
父组件可以通过 ref 主动获取子组件的属性或者调用子组件的方法。
// Child.vue
export default {
data(){
return {
name:"oldCode"
}
},
methods:{
someMethod(msg){
console.log(msg)
}
}
}
// Parent.vue
<template>
<child ref="child"></child>
</template>
<script>
export default {
mounted(){
const child = this.$refs.child
console.log(child.name)
child.someMethod("调用了子组件的方法")
}
}
</script>
$children / $parent
$children:获取到一个包含所有子组件(不包含孙子组件)的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法等。
$parent:获取到一个父节点的 VueComponent 对象,同样包含父节点中所有数据和方法等。
有多个父/子组件引用到则不推荐
Parent.vue:
export default{
mounted(){
this.$children[0].someMethod() // 调用第一个子组件的方法
this.$children[0].name // 获取第一个子组件中的属性
}
}
Child.vue:
export default{
mounted(){
this.$parent.someMethod() // 调用父组件的方法
this.$parent.name // 获取父组件中的属性
}
}
$root
1、 作用:访问根组件中的属性或方法
2、 注意:是根组件,不是父组件。$root只对根组件有用
provid/inject
provide / inject 是依赖注入,在一些插件或组件库里被常用
provide:可以让我们指定想要提供给后代组件的数据或方法
inject:在任何后代组件中接收想要添加在这个组件上的数据或方法,不管组件嵌套多深都可以直接拿来用
要注意的是 provide 和 inject 传递的数据不是响应式的,也就是说用 inject 接收来数据后,provide 里的数据改变了,后代组件中的数据不会改变,除非传入的就是一个可监听的对象 所以建议还是传递一些常量或者方法
// 父组件
export default{
// 方法一 不能获取 methods 中的方法
provide:{
name:"oldCode",
age: this.data中的属性
},
// 方法二 不能获取 data 中的属性
provide(){
return {
name:"oldCode",
someMethod:this.someMethod // methods 中的方法
}
},
methods:{
someMethod(){
console.log("这是注入的方法")
}
}
}
// 后代组件
export default{
inject:["name","someMethod"],
mounted(){
console.log(this.name)
this.someMethod()
}
}