前言
后序我会更新一系列的前端面试题,感兴趣的可以关注一手
vue2和vue3的响应式有什么区别?
vue2
Vue 2 的响应式原理是通过使用 Object.defineProperty() 方法来劫持对象的属性,从而实现对数据的监听和响应。
分为以下几点
1.数据劫持
- 在 Vue 初始化过程中,会遍历 data 中的所有属性,并使用 Object.defineProperty() 方法将它们转换为 getter 和 setter。通过这种方式,Vue 实现了对数据属性的劫持。
var data = { msg: 'Hello, Vue!' }; Object.defineProperty(data, 'msg', { get() { console.log('获取数据'); return val; }, set(newVal) { console.log('设置数据'); val = newVal; }, });
在上述示例中,我们通过 Object.defineProperty() 方法定义了一个名为 ‘msg’ 的属性,并为它提供了 getter 和 setter。当我们读取 ‘msg’ 属性时,会执行 get() 函数;当我们修改 ‘msg’ 属性时,会执行 set() 函数。
2.依赖收集
- 每个属性的 getter 都会在初始化时被调用,当获取到该属性的值时,会触发对应的 getter 函数。在 getter 函数中,Vue 会将当前正在获取的属性与 Watcher 进行关联,将 Watcher 添加到依赖列表中。
var dep = new Dep(); var watcher = new Watcher(); Object.defineProperty(data, 'msg', { get() { if (Dep.target) { dep.addSub(Dep.target); } return val; }, set(newVal) { val = newVal; dep.notify(); }, });
在上述示例中,我们定义了一个名为 ‘dep’ 的依赖管理器,以及一个名为 ‘watcher’ 的监听器。当 ‘msg’ 属性被获取时,会判断当前是否存在正在监听的 Watcher 对象,如果存在则将该 Watcher 添加到 ‘dep’ 的订阅列表中。
3.派发更新
- 当修改了被劫持的属性值时,会触发对应的 setter 函数。在 setter 函数中,Vue 会通知依赖列表中的所有 Watcher 对象进行更新操作。这样,所有依赖该属性的 Watcher 都会得到通知并执行相应的更新操作。
var Dep = function() { this.subs = []; }; Dep.prototype.addSub = function(sub) { this.subs.push(sub); }; Dep.prototype.notify = function() { for (var i = 0; i < this.subs.length; i++) { this.subs[i].update(); } };
在上述示例中,我们定义了一个名为 ‘Dep’ 的依赖管理器,并添加了添加订阅者和通知订阅者更新的方法。当属性值发生改变时,会调用 ‘Dep’ 的 notify() 方法,遍历依赖列表并触发每个 Watcher 对象的 update() 方法。
!!!值得注意的是:Vue 2 的响应式原理仅适用于初始化过程中已经存在的属性,无法监听新添加的属性或删除的属性。如果需要监听动态添加或删除的属性,可以使用 Vue.set() 或 Vue.delete() 进行操作。
vue3
Vue 3 的响应式原理是使用 Proxy 对象实现的。Proxy 是 ES6 引入的一个功能,它可以拦截并自定义对象的操作。
在 Vue 3 中,当你创建一个响应式对象时,Vue 内部会使用 Proxy 对象来包装这个对象。Proxy 对象通过拦截对响应式对象的访问,使得 Vue 可以追踪到对响应式对象的修改,并触发相应的更新。
在 Vue 3 中,使用 reactive() 函数和 Proxy 对象来创建响应式对象。具体而言,当你通过 reactive() 函数将一个普通对象转换为响应式对象时,Vue 会使用 Proxy 对象对这个对象进行包装。
响应式对象的创建:
import { reactive } from 'vue'; const state = reactive({ count: 0, name: 'John', });
上述代码中,以 reactive() 函数将普通对象 { count: 0, name: ‘John’ } 转换为一个响应式对象 state。现在,当你修改 state 中的属性时,Vue 会捕获到这个操作,并触发相应的更新。
如何触发更新?
当你修改了响应式对象的属性时,Vue 会根据依赖收集器(Dependency Tracking)中的关联关系,将相关的代码块进行重新执行,进而更新视图。
依赖收集器的建立:
在 Vue 3 中,可以使用 effect() 函数来建立依赖收集器。effect() 函数接收一个函数作为参数,这个函数内部可能会访问响应式对象的属性。当响应式对象的属性发生变化时,与其相关联的 effect() 函数会重新执行。
示例:
import { reactive, effect } from 'vue'; const state = reactive({ count: 0, }); effect(() => { console.log('Count changed:', state.count); });
上述代码中,我们通过 effect() 函数创建了一个依赖收集器。在 effect() 函数内部,我们访问了 state.count 属性。当 state.count 属性发生变化时,在控制台输出相应的消息。
响应式对象的属性访问和更新:
使用响应式对象的属性与普通对象类似,可以通过点符号或方括号来访问和更新属性的值。
示例:
console.log(state.count); // 访问 count 属性的值 state.count = 1; // 修改 count 属性的值
上述代码中,我们访问了响应式对象 state 的 count 属性,并将其修改为新的值。
!!!需要注意的是,响应式系统只能追踪到在初始化过程中访问过的属性。如果后续新增属性需要响应式,可以使用 Vue.set() 方法或者直接赋值一个新的响应式对象。
vue2和vue3的响应式的区别?
1.Proxy vs Object.defineProperty:
- Vue 2 使用了 Object.defineProperty 来实现响应式系统,它可以拦截对属性的读取和修改操作。
- Vue 3 使用了更强大的原生 JavaScript API Proxy 来实现响应式系统,它可以拦截更多类型的操作,包括新增和删除属性。
2.性能优化:
- Vue 2 每个组件实例都有独立的观察者(Watcher),当数据变化时,会遍历所有观察者进行更新,这在大规模数据变动时可能导致性能问题。
- Vue 3 使用基于 Proxy 的跟踪机制,可以精确追踪响应式对象的依赖,只更新与依赖相关的部分,提高了性能。
3.依赖收集:
- Vue 2 通过在 getter 中收集依赖,在 setter 中触发依赖更新。它使用了一个全局的依赖管理器 Dep 来维护依赖关系。
- Vue 3 使用了一个更轻量级的依赖收集器,它是基于 Proxy 和 Map 数据结构来构建的。每个响应式对象都有一个关联的 Map,用于存储属性和对应的依赖。
4.生命周期钩子:
- Vue 2 中,可以在 beforeUpdate 生命周期钩子中执行一些副作用操作。但在 setup() 函数中无法执行副作用操作,需要使用 created 钩子。
- Vue 3 中,setup() 函数是组件的入口点,可以在其中执行副作用操作,而且没有 beforeUpdate 钩子。
5.Computed 和 Watch:
- Vue 2 中的计算属性(computed)和侦听器(watch)是通过定义在 computed 和 watch 选项中的函数来实现的。
- Vue 3 中的计算属性(computed)和侦听器(watch)可以直接在 setup() 函数中使用,而不需要特定的选项。
6.总结
总的来说,Vue 3 的响应式系统经过重新设计和优化,使用了 Proxy 来取代 Object.defineProperty,提供了更好的性能和更广泛的拦截能力。它还引入了更轻量级的依赖收集器,并对生命周期钩子和计算属性、侦听器做了一些改进。这些变化使得 Vue 3 更加高效、灵活和易用。
作用域,作用域链?
作用域
- 作用域(Scope)是指在程序中定义变量的区域,这些变量在该区域内可被访问。作用域规定了变量的可见性和生命周期。
作用域链
- 作用域链(Scope Chain)是指在一个嵌套的函数中,每个函数都有自己的作用域,并且可以访问上层函数中定义的变量。当访问一个变量时,解释器会先在当前函数的作用域中查找该变量,如果找不到,则继续向上一层作用域查找,直至找到变量或抵达全局作用域。
作用域链的执行机制
- 具体来说,当一个函数被创建时,会创建一个保存变量和函数声明的内部对象,称为“活动对象”(Activation Object)。函数执行时,会创建一个称为“执行环境”(Execution Context)的内部对象,它包含了当前执行中的代码所需的全部信息,包括变量、函数等。
- 在作用域链中,每个执行环境都有一个关联的“变量对象”(Variable Object),它用于存储该执行环境中定义的变量和函数。执行环境的变量对象作为作用域链的一部分,形成了一个链式结构。
- 当访问一个变量时,解释器首先在当前执行环境的变量对象中查找,如果找不到,则沿着作用域链向上一级执行环境的变量对象中查找,直到找到变量或抵达全局执行环境。如果在最顶层的全局执行环境中也找不到该变量,则视为未定义
作用域链的底层原理是什么?
1.词法环境(Lexical Environment):
- 词法环境是一个存储变量和函数声明的数据结构,每个词法环境关联着一个特定的执行上下文(Execution Context)。
每次函数创建时,都会生成一个新的词法环境,并将其绑定到这个函数的执行上下文中。
词法环境以嵌套的方式组成了作用域链,形成了一个链式结构。每个词法环境都有一个指向父级词法环境的引用,这样就形成了一条从内到外的链接关系。
2.标识符解析(Identifier Resolution):
- 当访问一个变量时,解释器会在当前词法环境的变量记录中搜索该变量。如果找不到该变量,则会沿着作用域链向上一级词法环境查找,直到找到变量或抵达全局词法环境。如果在最顶层的全局词法环境中也找不到该变量,则视为未定义。
具体流程
在实际的查找过程中,当解释器遇到一个变量引用时,会根据当前的词法环境和作用域链进行标识符解析。如果在当前词法环境中找到了对应的变量,则返回该变量的值;否则,解释器会按照作用域链的顺序依次查找,直到找到变量或抵达全局词法环境。
需要注意的是,一旦变量被找到后,解释器会停止进一步的查找,这就是标识符解析的过程。通过作用域链的层层嵌套和标识符解析的机制,JavaScript 实现了变量在不同作用域中的访问和使用。
原型,原型链?
原型(Prototype):
- 在 JavaScript 中,每个对象都有一个原型,它是一个对象或者 null。
对象可以通过原型来共享属性和方法,当访问对象的属性或方法时,如果该对象自身没有找到对应的属性或方法,就会去原型中查找。
通过原型,可以实现对象之间的属性和方法的共享,减少内存占用并提高代码的可维护性。
原型链(Prototype Chain):
- 原型链是一种由原型对象组成的层级结构,用于查找对象的属性和方法。
- 当访问对象的属性或方法时,如果该对象自身没有找到对应的属性或方法,就会沿着原型链向上查找,直到找到该属性或方法或者抵达原型链的顶端(即 Object.prototype)。
- 原型链的形成是通过每个对象的 proto 属性来实现的,该属性指向该对象的原型。
后言
创作不易,要是本文章对广大读者有那么一点点帮助 不妨三连支持一下,您的鼓励就是博主创作的动力