Skip to content

Proxy 相比于 defineProperty 的优势

Object.defineProperty 的优势:兼容性好,支持 IE9+

Object.defineProperty 缺陷

  1. 对普通对象的监听需要遍历每一个属性
  2. 无法监听数组的变动
  3. 无法监听 Map/Set 数据结构的变动
  4. 无法对对象新增/删除的属性进行监听

Proxy 优势如下

  • Proxy 可以直接监听对象而非属性
  • Proxy 可以直接监听数组的变化
  • Proxy 有多达 13 中拦截方式,不限于 apply, ownKeys, deleteProperty, has 等等是 Object.defineProperty 不具备的
  • Proxy 返回的是一个新对象,可以只操作新对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改
  • Proxy 作为新标准将收到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利

Vue 的常用修饰符

事件修饰符:stop, prevent, capture, self, once, passive

按键修饰符:keyup, keydown, enter, tab, esc

系统修饰符:ctrl, alt, shift, meta, left, right, middle

keep-alive 组件的作用及原理

keep-alive 是 Vue 提供的一个内置组件,在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。

如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivatedactivated。同时,beforeDestroydestroyed 就 不会再被触发了,因为组件不会被真正销毁:

  • 组件被换掉时,会被缓存到内存中,触发 deactivated 生命周期
  • 当组件被切回来时,再去缓存里找这个组件,触发 activated 钩子 函数

原理:Vue 内部将 DOM 节点抽象成了一个个的 VNode 节点,keep-alive 组件的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构。它将满足条件(pruneCache 与 pruneCache)的组件在 cache 对象中缓存起来,在需要重新渲染的时候再将 VNode 节点从 cache 对象中取出并渲染。

watch, computed 区别

computed 是计算属性,有缓存,多用于计算比较耗性能的场景,内部不允许对其它属性/数据做任何修改

watch 更多的是监听观察的作用,无缓存

Vue 是如何实现数据双向绑定的

可以实现双向绑定的方法有很多,KnockoutJS 基于观察者模式的双向绑定,Ember 基于数据模型的双向绑定,Angular 基于脏检查的双向绑定,Vue 是基于数据劫持的双向绑定。

基于数据劫持的双向绑定离不开 ProxyObject.defineProperty 等方法对对象/对象属性的「劫持」,我们要实现一个完整的双向绑定需要以下几个要点:

  1. 利用 ProxyObject.defineProperty 生成的 Observer 针对对象/对象的属性进行「劫持」,在数据变更的时候通知 Watcher 因为属性可能是多个,所以会有多个订阅者,故我们需要一个消息订阅器 Dep 来专门收集这些订阅者,并在监听器 Observer 和订阅者 Watcher 之间进行统一的管理
  2. 解析器 Compile 解析模板中的 Directive(指令),收集指令所依赖的方法和数据,替换模板数据,将相关指令初始化成一个订阅者 Watcher,等待数据变化然后进行渲染
  3. Watcher 属于 Observer 和 Compile 桥梁,它将接收到的 Observer 产生的数据变化,并根据 Compile 提供的指令进行视图渲染,使得数据变化驱动视图变化

利用 Object.defineProperty 劫持对象的访问器,在属性值发生变化时我们可以获取变化,然后根据变化进行后续响应,在 Vue3.0 中通过 Proxy 对象进行类似的操作。

js
const data = { name: '' };
Object.defineProperty(data, "name", {
    value: 'abc',
    writable: true,     // 是否可被赋值运算符重写
    enumerable: true,   // 是否可枚举
    configurable: true, // 是否可重定义,是否可删除
    get: function () {
        console.log('get');
    },
    set: function (val) {
        console.log(val);
    }
});

React 与 Vue 的异同?

相似点:

  • 都只关注视图层,注意力集中保持再核心库,而将其它功能如路由管理、全局状态管理交给其它的库
  • 使用 Virtual Dom,Virtual Dom 厉害的地方并不是说它比 Dom 快(本身就不比 Dom 快),而是说不管数据怎么变化,都可以以最小的代价来更新 Dom。方法就是在内存里用新的数据刷新 Virtual Dom,然后新旧 Dom 进行比较,找出差异,再更新到真正的 Dom 树上
  • 提供响应式和组件化的视图组件
  • 都很轻量,在大部分场景下都能提供近似的性能(个人觉得 Vue 会有少量优势)

差异:

  • 设计思想:
    • Vue 是一款渐进式框架,支持双向数据绑定
    • React 主张函数式编程,单向数据流的思想
  • 编写语法:Vue 推荐单文件组件,同时支持 JSX;React 的思想是 All in JS
  • 性能优化:
    • Vue 会跟踪每一个组件的依赖关系,状态变更时,能精确知道哪个组件需要重渲染
    • React 在状态变更时所有组件都会重新渲染,所以需要 shouldComponentUpdate 这个生命周期函数来优化

性能:

在 React 应用中,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。

如要避免不必要的子组件的重渲染,你需要在所有可能的地方使用 PureComponent,或是手动实现 shouldComponentUpdate 方法。同时你可能会需要使用不可变的数据结构(Immutable.js)来使得你的组件更容易被优化。

然而,使用 PureComponent 和 shouldComponentUpdate 时,需要保证该组件的整个子树的渲染输出都是由该组件的 props 所决定的。如果不符合这个情况,那么此类优化就会导致难以察觉的渲染结果不一致。这使得 React 中的组件优化伴随着相当的心智负担。

在 Vue 应用中,组件的依赖是在渲染过程中自动追踪的,所以系统能精确知晓哪个组件确实需要被重渲染。你可以理解为每一个组件都已经自动获得了 shouldComponentUpdate,并且没有上述的子树问题限制。

Vue 的这个特点使得开发者不再需要考虑此类优化,从而能够更好地专注于应用本身。

HTML & CSS:

在 React 中,一切都是 JavaScript。不仅仅是 HTML 可以用 JSX 来表达,现在的潮流也越来越多地将 CSS 也纳入到 JavaScript 中来处理。这类方案有其优点,但也存在一些不是每个开发者都能接受的取舍。

Vue 的整体思想是拥抱经典的 Web 技术,并在其上进行扩展。

在 React 中,所有的组件的渲染功能都依靠 JSX。JSX 是使用 XML 语法编写 JavaScript 的一种语法糖。

事实上 Vue 也提供了渲染函数,甚至支持 JSX。

有些开发者认为模板意味着需要学习额外的 DSL (Domain-Specific Language, 领域特定语言)才能进行开发 —— 我们认为这种区别是比较肤浅的。首先,JSX 并不是免费的 —— 它是基于 JS 之上的一套额外语法,因此也有它自己的学习成本。同时,正如同熟悉 JS 的人学习 JSX 会很容易一样,熟悉 HTML 的人学习 Vue 的模板语法也是很容易的。最后,DSL 的存在使得我们可以让开发者用更少的代码做更多的事,比如 v-on 的各种修饰符,在 JSX 中实现对应的功能会需要多得多的代码。

更抽象一点来看,我们可以把组件区分为两类:一类是偏视图表现的 (presentational),一类则是偏逻辑的 (logical)。我们推荐在前者中使用模板,在后者中使用 JSX 或渲染函数。这两类组件的比例会根据应用类型的不同有所变化,但整体来说我们发现表现类的组件远远多于逻辑类组件。

参考资料:

什么是纯函数(Pure Function)?

纯函数的好处

纯函数非常「靠谱」,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。不管何时何地,你给它什么它就会乖乖地吐出什么。如果你的应用程序大多数函数都是由纯函数组成,那么你的程序测试、调试起来会非常方便。

纯函数(Pure Function)函数式编程 里面非常重要的概念 。如果一个函数是纯函数,它必须符合两个条件:

  • 返回结果只依赖于它的参数
  • 并且在执行过程里面没有副作用

函数的返回结果只依赖于它的参数

例 1:

js
const a = 1;
const foo = (b) => a + b;
foo(2); // => 3

虽然这里的 a 是常量,但函数 foo 不属于纯函数,因为它的返回结果并不只是依赖它的参数 b,还依赖了 a。也许在这里 a 是常量,但谁也不能保证在别的地方调用 foo 函数时,a 仍是常量,值仍为 2,所以返回结果是不可控的。

例 2:

js
const a = 1;
const foo = (x, y) => x + y;
foo(a, 2);

函数 foo 仅依赖它的参数 xy,返回结果也只由这两个参数决定,是可控的。

函数执行过程没有副作用

一个函数执行过程对产生了外部可观察的变化那么就说这个函数是有副作用的。

例 1:

js
const counter = { x: 1 };
const foo = (obj, b) => {
  obj.x = 2;
  return obj.x + b;
}

foo(counter, 2); // => 4
counter.x; // => 2

执行 foo 函数时,将作为参数传进来的外部变量 counter.x 的值修改为 2。无论函数在哪里执行,都会修改函数外部变量的值,所以它产生了 副作用,不是纯函数

例 2:

js
const counter = { x: 1 };
const foo = (obj, b) => {
  return obj.x + b;
}

foo(counter, 2); // => 3
counter.x; // => 1

可以看得出来,以 counter2 为参数执行了 foo 函数后,counter.x 的值并没有发生什么变化。所以 foo 函数执行过程中没有修改函数外部的值,没有产生副作用

例 3:

js
const foo = (b) => {
    const obj = { x: 1 };
    obj.x = 2;
    return obj.x + b;
}

执行函数时,虽然 obj.x 的值有变化,但 obj 是在函数内部声明的,不属于函数外部的值,外部程序根本观察不到,所以没有产生副作用

TIP

除了修改外部的变量,一个函数在执行过程中还有很多方式产生外部可观察的变化,比如说调用 DOM API 修改页面,或者你发送了 Ajax 请求,还有调用 window.reload 刷新浏览器,甚至是 console.log 往控制台打印数据也是 副作用

Vue 中的 key 到底有什么⽤?

key 是 Vue 中标记 vonde 的唯一 id,通过这个 key 我们的 Dom Diff 操作可以更准确、更快速。

  • 准确:: 如果不加 key,那么 Vue 会选择复⽤节点(Vue 的就地更新策略),导致之前节点的状态被保留下来,会产⽣⼀系列的 bug
  • 快速:key 的唯⼀性可以被 Map 数据结构充分利⽤,相⽐于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)

Vue 组件间通信的方式有哪些?

  • props
  • emit
  • vuex
  • 发布订阅模式
  • ...

Vue 的 v-if 和 v-for 哪个优先级更高?如果出现在同一标签上,应如何优化?

v-for 优先于 v-if 被解析,如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能。 要避免出现这种情况,则在外层嵌套 template,在这一层进行 v-if 判断,然后在内部进行 v-for 循环。如果条件出现在循环内部,可通过计算属性提前过滤掉那些不需要显示的项。

Virtual Dom 是什么?以及作用?

用 JS 对象结构表示 DOM 树的结构。

Virtual DOM 其实就是一棵以 JS 对象(VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。

简单来说,可以把 Virtual DOM 理解为一个简单的 JS 对象,并且最少包含标签名(tag)、属性(attrs)和子元素对象(children)三个属性。不同的框架对这三个属性的命名会有点差别。

虚拟 Dom 最终的目的是将虚拟节点渲染到视图上。但是如果直接用虚拟节点覆盖旧节点的话会产生很多不必要的 Dom 操作。

为了避免不必要的 Dom 操作,虚拟 Dom 在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图的旧虚拟节点进行对比,找出真正需要更新的节点来进行 Dom 操作,从而避免操作其它无需改动的 Dom。

虚拟 Dom 在 Vue 中主要做了两件事:

  1. 提供与真实 Dom 节点所对应的虚拟节点 vnode
  2. 将虚拟节点 vnode 与旧虚拟节点 oldVnode 进行对比,然后以最小代价更新视图

为什么需要 Virtual Dom?

  1. 具备跨平台优势(客户端&服务端同构)
  2. 因 Dom 操作执行速度远不如 JS 运算速度快,采用 Virtual Dom 能最大限度地减少 Dom 操作,可以避免更多的回流&重绘,从而显著提高性能

Virtual Dom 很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡。

Virtual Dom Diff 算法?

所谓 Virtual DOM Diff 算法,就是比较两个对象树的差异,也正是 Virtual DOM 的核心。

传统的比较两棵树差异的算法,时间复杂度是 O(n³),大量操作 DOM 的时候肯定是接受不了的。所以 React 做了妥协,React 结合 WEB 界面的特点,做了两个简单的假设,使得算法的复杂度降低到了 O(n):

  1. 相同的组件产生相似的 DOM 树,不同的组件产生不同的 DOM 树
  2. 对于同一层次的一组子节点,它们可以通过唯一的 ID 进行区分

Vue 中 Virtual Dom Diff 算法是基于 snabbdom 算法改进而来,仅在同级的 vnode 间做 diff,递归地进行同级 vnode 的 diff,最终实现整个 Dom 树的更新。因为跨层级操作是非常少的,忽略不计,这样时间复杂度就从 O(n³) 变成 O(n)。

diff 算法包括几个步骤:

  1. 用 JS 对象结构表示 DOM 树的结构,然后用这个树构建一个真正的 DOM 树,插到文档当中
  2. 当状态变更的时候,重新构造一棵新的对象树,然后用新的树和旧的树进行比较,记录两棵树差异
  3. 把所记录的差异应用到所构建的真正的 DOM 树上,视图就更新了

参考资料:

Vue 子组件和父组件的执行顺序

加载渲染过程

  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted

更新过程

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

销毁过程

  1. 父组件 beforeDestroy
  2. 子组件 beforeDestroy
  3. 子组件 destroyed
  4. 父组件 destoryed

Vuex 的工作原理是什么?

Vuex 是通过全局注入 store 对象来实现组件间的状态共享。Vuex state, getter 的响应式实现借助了 Vue 的 data, computed,依赖监听、依赖收集都基于 Vue 对对象属性 get, set 的劫持。可以理解为 Vuex 中的 store 本质就是没有 template 的 Vue 组件。

nextTick 的原理以及运行机制?

nextTick 的作用时在下一次事件循环更新 Dom 之后再执行回调。

Vue 在更新 DOM 时是异步执行的,只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环「tick」中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

  1. Vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行
  2. microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  3. 因为兼容性问题,Vue 不得不做了 microtaskmacrotask 的降级方案

Promise.then -> MutationObserver -> setImmediate -> setTimeout(fn, 0)

Vuex 和 Redux 比较,它们的相同点以及不同点?

相同点:

  1. 都是通过 store 来作为全局状态存储对象
  2. 改变 state 的直接方法(vuex 中的 mutation 和 redux 中的 reducer)只允许同步操作

不同点:

  1. Vuex 只能配合 Vue 而 Redux 可以配合任何框架
  2. Vuex 自带 module 化功能,而 Redux 是没有的
  3. Vuex 中的异步操作只能在 action 中进行,而 Redux 中没有特别的为异步操作创建一个方法

在 Vue 中如何通过 createElement 创建虚拟 Dom?

Virtual Dom 简单实现:

js
class VNode {
    constructor(tagName, props, children) {
        this.tagName = tagName;
        this.props = props;
        this.children = children;
    }

    render() {
        let el = document.createElement(this.tagName);
        let props = this.props;

        for (let propName in props) {
            el.setAttribute(propName, props[propName]);
        }

        this.children.forEach(child => {
            let childEle = child instanceof VNode
                ? child.render()
                : document.createTextNode(child);

            el.appendChild(childEle);
        });

        return el;
    }
}

// Example:
let ulVnode = new VNode('ul', { class: 'list' }, [
    new VNode('li', { class: 'item' }, ['item-1']),
    new VNode('li', { class: 'item' }, ['item-2']),
    new VNode('li', { class: 'item' }, ['item-3'])
]);

let ulEle = ulVnode.render();

document.appendChild(ulEle);

Vue-router 原理

Vue Route 在实现单页应用前端路由时,提供了三种模式,分别为 hash, history, abstract,根据 mode 参数来决定采用哪一种方式。

  • hash: 默认模式,使用 URL hash 值来作路由

  • history: 依赖 HTML5 History API 和服务器配置

  • abstract: 支持所有 JavaScript 运行环境,通常在移动端原生环境中使用

  • Vue 番外篇 —— vue-router 浅析原理

Vue3 对比 Vue2 有哪些变化

  • 组合式 API:Vue2 是「选项式 API」(Option API),Vue3 是「组合式API」(Composition API)
  • 多根节点:Vue3 支持了多根节点组件
  • 响应式原理:Vue2 响应式原理基础是 Object.defineProperty;Vue3 响应式原理基础是 Proxy
  • 生命周期的变化:Vue3 组合式 API 生命周期与 Vue2 选项 API 声明周期的差别(setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地去定义)
  • 虚拟 DOM 优化,DOM Diff 优化
  • Vue3 提供 Suspense 组件:允许程序在等待异步组件时渲染兜底的内容,如 loading ,使用户体验更平滑
  • Vue3 提供 Teleport 组件:可将部分 DOM 移动到 Vue app 之外的位置,比如项目中常见的 Dialog 组件

参考资料:Vue3 对比 Vue2 有哪些变化?