从源码层面解读16道Vue常考面试题,2024年最新面试心得体会怎么写

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Web前端全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024c (备注前端)
img

正文

this.deps = [];

this.newDeps = [];

this.depIds = new Set();

this.newDepIds = new Set();

this.expression =

process.env.NODE_ENV !== “production” ? expOrFn.toString() : “”;

// parse expression for getter

if (typeof expOrFn === “function”) {

this.getter = expOrFn;

} else {

this.getter = parsePath(expOrFn);

if (!this.getter) {

this.getter = noop;

process.env.NODE_ENV !== “production” &&

warn(

Failed watching path: "${expOrFn}"  +

"Watcher only accepts simple dot-delimited paths. " +

“For full control, use a function instead.”,

vm

);

}

}

this.value = this.lazy ? undefined : this.get();

}

// 评估getter,并重新收集依赖项。

get() {

// 实际上就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复用)。

pushTarget(this);

let value;

const vm = this.vm;

try {

// this.getter 对应就是 updateComponent 函数,这实际上就是在执行:

// 这里需要追溯 new Watcher 执行的地方,是在

value = this.getter.call(vm, vm);

} catch (e) {

if (this.user) {

handleError(e, vm, getter for watcher "${this.expression}");

} else {

throw e;

}

} finally {

// “touch” every property so they are all tracked as

// dependencies for deep watching

// 递归深度遍历每一个属性,使其都可以被依赖收集

if (this.deep) {

traverse(value);

}

// 出栈

popTarget();

// 清理依赖收集

this.cleanupDeps();

}

return value;

}

// 添加依赖

// 在 Dep 中会调用

addDep(dep: Dep) {

const id = dep.id;

// 避免重复收集

if (!this.newDepIds.has(id)) {

this.newDepIds.add(id);

this.newDeps.push(dep);

if (!this.depIds.has(id)) {

// 把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中

// 目的是为后续数据变化时候能通知到哪些 subs 做准备

dep.addSub(this);

}

}

}

// 清理依赖

// 每次添加完新的订阅,会移除掉旧的订阅,所以不会有任何浪费

cleanupDeps() {

let i = this.deps.length;

// 首先遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅

while (i–) {

const dep = this.deps[i];

if (!this.newDepIds.has(dep.id)) {

dep.removeSub(this);

}

}

let tmp = this.depIds;

this.depIds = this.newDepIds;

this.newDepIds = tmp;

this.newDepIds.clear();

tmp = this.deps;

this.deps = this.newDeps;

this.newDeps = tmp;

this.newDeps.length = 0;

}

// 发布接口

// 依赖更新的时候触发

update() {

/* istanbul ignore else */

if (this.lazy) {

// computed 数据

this.dirty = true;

} else if (this.sync) {

// 同步数据更新

this.run();

} else {

// 正常数据会经过这里

// 派发更新

queueWatcher(this);

}

}

// 调度接口,用于执行更新

run() {

if (this.active) {

const value = this.get();

if (

value !== this.value ||

// Deep watchers and watchers on Object/Arrays should fire even

// when the value is the same, because the value may

// have mutated.

isObject(value) ||

this.deep

) {

// 设置新的值

const oldValue = this.value;

this.value = value;

if (this.user) {

try {

this.cb.call(this.vm, value, oldValue);

} catch (e) {

handleError(

e,

this.vm,

callback for watcher "${this.expression}"

);

}

} else {

this.cb.call(this.vm, value, oldValue);

}

}

}

}

/**

* Evaluate the value of the watcher.

* This only gets called for lazy watchers.

*/

evaluate() {

this.value = this.get();

this.dirty = false;

}

/**

* Depend on all deps collected by this watcher.

*/

depend() {

let i = this.deps.length;

while (i–) {

this.deps[i].depend();

}

}

/**

* Remove self from all dependencies’ subscriber list.

*/

teardown() {

if (this.active) {

// remove self from vm’s watcher list

// this is a somewhat expensive operation so we skip it

// if the vm is being destroyed.

if (!this.vm._isBeingDestroyed) {

remove(this.vm._watchers, this);

}

let i = this.deps.length;

while (i–) {

this.deps[i].removeSub(this);

}

this.active = false;

}

}

}

小结

综上响应式核心代码,我们可以描述响应式的执行过程:

  1. 根据数据类型来做不同处理,如果是对象则 Object.defineProperty() 监听数据属性的 get 来进行数据依赖收集,再通过 get 来完成数据更新的派发;如果是数组如果是数组则通过覆盖 该数组原型的⽅法,扩展它的 7 个变更⽅法(push/pop/shift/unshift/splice/reverse/sort),通过监听这些方法可以做到依赖收集和派发更新;
  1. Dep 是主要做依赖收集,收集的是当前上下文作为 Watcher,全局有且仅有一个 Dep.target,通过 Dep 可以做到控制当前上下文的依赖收集和通知 Watcher 派发更新;
  1. Watcher 连接表达式和值,说白了就是 watcher 连接视图层的依赖,并可以触发视图层的更新,与 Dep 紧密结合,通过 Dep 来控制其对视图层的监听

4. vue3 为何用 proxy 替代了 Object.defineProperty?
traverse

截取上面 Watcher 中部分代码

if (this.deep) {

// 这里其实递归遍历属性用作依赖收集

traverse(value);

}

再查看 src/core/observer/traverse.jstraverse 的实现,如下:

const seenObjects = new Set();

// 递归遍历对象,将所有属性转换为 getter

// 使每个对象内嵌套属性作为依赖收集项

export function traverse(val: any) {

_traverse(val, seenObjects);

seenObjects.clear();

}

function _traverse(val: any, seen: SimpleSet) {

let i, keys;

const isA = Array.isArray(val);

if (

(!isA && !isObject(val)) ||

Object.isFrozen(val) ||

val instanceof VNode

) {

return;

}

if (val.ob) {

const depId = val.ob.dep.id;

if (seen.has(depId)) {

return;

}

seen.add(depId);

}

if (isA) {

i = val.length;

while (i–) _traverse(val[i], seen);

} else {

keys = Object.keys(val);

i = keys.length;

while (i–) _traverse(val[keys[i]], seen);

}

}

小结

再综上一题代码实际了解,其实我们看到一些弊端:

  1. Watcher 监听 对属性做了递归遍历,这里可能会造成性能损失;
  1. defineReactive 遍历属性对当前存在的属性 Object.defineProperty() 作依赖收集,但是对于不存在,或者删除属性,则监听不到;从而会造成 对新增或者删除的属性无法做到响应式,只能通过 Vue.set/delete 这类 api 才可以做到;
  1. 对于 es6 中新产⽣的 MapSet 这些数据结构不⽀持

5. vue 双向绑定,Model 怎么改变 ViewView 怎么改变 Model

其实这个问题需要承接上述第三题,再结合下图

响应式原理

Model 改变 View:

  1. defineReactive 中通过 Object.defineProperty 使 data 可响应;
  1. Dep 在 getter 中作依赖收集,在 setter 中作派发更新;
  1. dep.notify() 通知 Watcher 更新,最终调用 vm._render() 更新 UI;

View 改变 Model: 其实同上理,View 与 data 的数据关联在了一起,View 通过事件触发 data 的变化,从而触发了 setter,这就构成了一个双向循环绑定了;

6. vue 如何对数组方法进行变异?例如 pushpopslice 等;

这个问题,我们直接从源码找答案,这里我们截取上面 Observer 部分源码,先来追溯一下,Vue 怎么实现数组的响应:

constructor(value: any) {

this.value = value;

this.dep = new Dep();

this.vmCount = 0;

def(value, “ob”, this);

if (Array.isArray(value)) {

// 数组则通过扩展原生方法形式使其可响应

if (hasProto) {

protoAugment(value, arrayMethods);

} else {

copyAugment(value, arrayMethods, arrayKeys);

}

this.observeArray(value);

} else {

this.walk(value);

}

}

arrayMethods

这里需要查看一下 arrayMethods 这个对象,在 src/core/observer/array.js

import { def } from “…/util/index”;

const arrayProto = Array.prototype;

// 复制数组原型链,并创建一个空对象

// 这里使用 Object.create 是为了不污染 Array 的原型

export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = [

“push”,

“pop”,

“shift”,

“unshift”,

“splice”,

“sort”,

“reverse”,

];

// 拦截突变方法并发出事件

// 拦截了数组的 7 个方法

methodsToPatch.forEach(function (method) {

// cache original method

const original = arrayProto[method];

// 使其可响应

def(arrayMethods, method, function mutator(…args) {

const result = original.apply(this, args);

const ob = this.ob;

let inserted;

switch (method) {

case “push”:

case “unshift”:

inserted = args;

break;

case “splice”:

inserted = args.slice(2);

break;

}

if (inserted) ob.observeArray(inserted);

// notify change

// 派发更新

ob.dep.notify();

return result;

});

});

def

def 使对象可响应,在 src/core/util/lang.js

export function def(obj: Object, key: string, val: any, enumerable?: boolean) {

Object.defineProperty(obj, key, {

value: val,

enumerable: !!enumerable,

writable: true,

configurable: true,

});

}

小结

  1. Object.create(Array.prototype) 复制 Array 原型链为新的对象;
  1. 拦截了数组的 7 个方法的执行,并使其可响应,7 个方法分别为:push, pop, shift, unshift, splice, sort, reverse
  1. 当数组调用到这 7 个方法的时候,执行 ob.dep.notify() 进行派发通知 Watcher 更新;

附加思考

不过,vue 对数组的监听还是有限制的,如下:

  1. 数组通过索引改变值的时候监听不到,比如:array[2] = newObj
  1. 数组长度变化无法监听

这些操作都需要通过 Vue.set/del 去操作才行;

7. computed 如何实现?
initComputed

这个方法用于初始化 options.computed 对象, 这里还是上源码,在 src/core/instance/state.js 中,这个方法是在 initState 中调用的

const computedWatcherOptions = { lazy: true };

function initComputed(vm: Component, computed: Object) {

// $flow-disable-line

// 创建一个空对象

const watchers = (vm._computedWatchers = Object.create(null));

// computed properties are just getters during SSR

const isSSR = isServerRendering();

for (const key in computed) {

// 遍历拿到每个定义的 userDef

const userDef = computed[key];

const getter = typeof userDef === “function” ? userDef : userDef.get;

// 没有 getter 则 warn

if (process.env.NODE_ENV !== “production” && getter == null) {

warn(Getter is missing for computed property "${key}"., vm);

}

if (!isSSR) {

// 为每个 computed 属性创建 watcher

// create internal watcher for the computed property.

watchers[key] = new Watcher(

vm,

getter || noop,

noop,

computedWatcherOptions // {lazy: true}

);

}

// component-defined computed properties are already defined on the

// component prototype. We only need to define computed properties defined

// at instantiation here.

if (!(key in vm)) {

// 定义 vm 中未定义的计算属性

defineComputed(vm, key, userDef);

} else if (process.env.NODE_ENV !== “production”) {

if (key in vm.$data) {

// 判断 key 是不是在 data

warn(The computed property "${key}" is already defined in data., vm);

} else if (vm.KaTeX parse error: Expected 'EOF', got '&' at position 15: options.props &̲& key in vm.options.props) {

// 判断 key 是不是在 props 中

warn(

The computed property "${key}" is already defined as a prop.,

vm

);

}

}

}

}

defineComputed

这个方法用作定义 computed 中的属性,继续看代码:

export function defineComputed(

target: any,

key: string,

userDef: Object | Function

) {

const shouldCache = !isServerRendering();

if (typeof userDef === “function”) {

sharedPropertyDefinition.get = shouldCache

? createComputedGetter(key)

: createGetterInvoker(userDef);

sharedPropertyDefinition.set = noop;

} else {

sharedPropertyDefinition.get = userDef.get

? shouldCache && userDef.cache !== false

? createComputedGetter(key)

: createGetterInvoker(userDef.get)

: noop;

sharedPropertyDefinition.set = userDef.set || noop;

}

if (

process.env.NODE_ENV !== “production” &&

sharedPropertyDefinition.set === noop

) {

sharedPropertyDefinition.set = function () {

warn(

Computed property "${key}" was assigned to but it has no setter.,

this

);

};

}

// 定义计算属性的 get / set

Object.defineProperty(target, key, sharedPropertyDefinition);

}

// 返回计算属性对应的 getter

function createComputedGetter(key) {

return function computedGetter() {

const watcher = this._computedWatchers && this._computedWatchers[key];

if (watcher) {

if (watcher.dirty) {

// watcher 检查是 computed 属性的时候 会标记 dirty 为 true

// 这里是 computed 的取值逻辑, 执行 evaluate 之后 则 dirty false,直至下次触发

// 其实这里就可以说明 computed 属性其实是触发了 getter 属性之后才进行计算的,而触发的媒介便是 computed 引用的其他属性触发 getter,再触发 dep.update(), 继而 触发 watcher 的 update

watcher.evaluate();

// --------------------------- Watcher --------------------------------

// 这里截取部分 Watcher 的定义

// update 定义

// update () {

//   /* istanbul ignore else */

//   if (this.lazy) {

//     // 触发更新的时候标记计算属性

//     this.dirty = true

//   } else if (this.sync) {

//     this.run()

//   } else {

//     queueWatcher(this)

//   }

// }

// evaluate 定义

// evaluate () {

//   this.value = this.get()

//   // 取值后标记 取消

//   this.dirty = false

// }

// ------------------------- Watcher ----------------------------------

}

if (Dep.target) {

// 收集依赖

watcher.depend();

}

return watcher.value;

}

};

}

function createGetterInvoker(fn) {

return function computedGetter() {

return fn.call(this, this);

};

}

小结

综上代码分析过程,总结 computed 属性的实现过程如下(以下分析过程均忽略了 ssr 情况):

  1. Object.create(null) 创建一个空对象用作缓存 computed 属性的 watchers,并缓存在 vm._computedWatchers 中;
  1. 遍历计算属性,拿到用户定义的 userDef,为每个属性定义 Watcher,标记 Watcher 属性 lazy: true;
  1. 定义 vm 中未定义过的 computed 属性,defineComputed(vm, key, userDef),已存在则判断是在 data 或者 props 中已定义并相应警告;
  1. 接下来就是定义 computed 属性的 gettersetter,这里主要是看 createComputedGetter 里面的定义:当触发更新则检测 watcher 的 dirty 标记,则执行 watcher.evaluate() 方法执行计算,然后依赖收集;
  1. 这里再追溯 watcher.dirty 属性逻辑,在 watcher.update 中 当遇到 computed 属性时候被标记为 dirty:false,这里其实可以看出 computed 属性的计算前提必须是引用的正常属性的更新触发了 Dep.update(),继而触发对应 watcher.update 进行标记 dirty:true,继而在计算属性 getter 的时候才会触发更新,否则不更新;

以上便是计算属性的实现逻辑,部分代码逻辑需要追溯上面第三题响应式的部分 Dep/Watcher 的触发逻辑;

8. computedwatch 的区别在哪里?
initWatch

这里还是老样子,上代码,在 src/core/instance/state.js 中:

function initWatch(vm: Component, watch: Object) {

// 遍历 watch 对象属性

for (const key in watch) {

const handler = watch[key];

//  数组则进行遍历创建 watcher

if (Array.isArray(handler)) {

for (let i = 0; i < handler.length; i++) {

createWatcher(vm, key, handler[i]);

}

} else {

createWatcher(vm, key, handler);

}

}

}

// 创建 watcher 监听

function createWatcher(

vm: Component,

expOrFn: string | Function,

handler: any,

options?: Object

) {

if (isPlainObject(handler)) {

options = handler;

handler = handler.handler;

}

// handler 传入字符串,则直接从 vm 中获取函数方法

if (typeof handler === “string”) {

handler = vm[handler];

}

// 创建 watcher 监听

return vm.$watch(expOrFn, handler, options);

}

$watch

我们还需要看一下 $watch 的逻辑,在 src/core/instance/state.js 中:

Vue.prototype.$watch = function (

expOrFn: string | Function,

cb: any,

options?: Object

): Function {

const vm: Component = this

if (isPlainObject(cb)) {

return createWatcher(vm, expOrFn, cb, options)

}

options = options || {}

options.user = true

// 创建 watch 属性的 Watcher 实例

const watcher = new Watcher(vm, expOrFn, cb, options)

if (options.immediate) {

try {

cb.call(vm, watcher.value)

} catch (error) {

handleError(error, vm, callback for immediate watcher "${watcher.expression}")

}

}

// 用作销毁

return function unwatchFn () {

// 移除 watcher 的依赖

watcher.teardown()

}

}

}

小结

综上代码分析,先看来看一下 watch 属性的实现逻辑:

  1. 遍历 watch 属性分别创建属性的 Watcher 监听,这里可以看出其实该属性并未被 Dep 收集依赖;
  1. 可以分析 watch 监听的属性 必然是已经被 Dep 收集依赖的属性了(data/props 中的属性),进行对应属性触发更新的时候才会触发 watch 属性的监听回调;

这里就可以分析 computed 与 watch 的异同:

  1. computed 属性的更新需要依赖于其引用属性的更新触发标记 dirty: true,进而触发 computed 属性 getter 的时候才会触发其本身的更新,否则其不更新;
  1. watch 属性则是依赖于本身已被 Dep 收集依赖的部分属性,即作为 data/props 中的某个属性的尾随 watcher,在监听属性更新时触发 watcher 的回调;否则监听则无意义;

这里再引申一下使用场景:

  1. 如果一个数据依赖于其他数据,那么就使用 computed 属性;
  1. 如果你需要在某个数据变化时做一些事情,使用 watch 来观察这个数据变化;

9. 计算属性和普通属性的区别?

这个题目跟上题类似,区别如下:

  1. 普通属性都是基于 gettersetter 的正常取值和更新;
  1. computed 属性是依赖于内部引用普通属性的 setter 变更从而标记 watcherdirty 标记为 true,此时才会触发更新;

10. v-if/v-show/v-html 的原理是什么,它是如何封装的?
v-if

先来看一下 v-if 的实现,首先 vue 编译 template 模板的时候会先生成 ast 静态语法树,然后进行标记静态节点,再之后生成对应的 render 函数,这里就直接看下 genIf 的代码,在src/compiler/codegen/index.js中:

export function genIf(

el: any,

state: CodegenState,

altGen?: Function,

altEmpty?: string

): string {

el.ifProcessed = true; // 标记避免递归,标记已经处理过

return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty);

}

function genIfConditions(

conditions: ASTIfConditions,

state: CodegenState,

altGen?: Function,

altEmpty?: string

): string {

if (!conditions.length) {

return altEmpty || “_e()”;

}

const condition = conditions.shift();

// 这里返回的是一个三元表达式

if (condition.exp) {

return `( c o n d i t i o n . e x p ) ? {condition.exp})? condition.exp)?{genTernaryExp(

condition.block

)}😒{genIfConditions(conditions, state, altGen, altEmpty)}`;

} else {

return ${genTernaryExp(condition.block)};

}

// v-if with v-once should generate code like (a)?_m(0):_m(1)

function genTernaryExp(el) {

return altGen

? altGen(el, state)

: el.once

? genOnce(el, state)

: genElement(el, state);

}

}

v-if 在 template 生成 ast 之后 genIf 返回三元表达式,在渲染的时候仅渲染表达式生效部分;

v-show

这里截取 v-show 指令的实现逻辑,在 src/platforms/web/runtime/directives/show.js 中:

export default {

bind(el: any, { value }: VNodeDirective, vnode: VNodeWithData) {

vnode = locateNode(vnode);

const transition = vnode.data && vnode.data.transition;

const originalDisplay = (el.__vOriginalDisplay =

el.style.display === “none” ? “” : el.style.display);

if (value && transition) {

vnode.data.show = true;

enter(vnode, () => {

el.style.display = originalDisplay;

});

} else {

el.style.display = value ? originalDisplay : “none”;

}

},

update(el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) {

/* istanbul ignore if */

if (!value === !oldValue) return;

vnode = locateNode(vnode);

const transition = vnode.data && vnode.data.transition;

if (transition) {

vnode.data.show = true;

if (value) {

enter(vnode, () => {

el.style.display = el.__vOriginalDisplay;

});

} else {

leave(vnode, () => {

el.style.display = “none”;

});

}

} else {

el.style.display = value ? el.__vOriginalDisplay : “none”;

}

},

unbind(

el: any,

binding: VNodeDirective,

vnode: VNodeWithData,

oldVnode: VNodeWithData,

isDestroy: boolean

) {

if (!isDestroy) {

el.style.display = el.__vOriginalDisplay;

}

},

};

这里其实比较明显了,v-show 根据表达式的值最终操作的是 style.display

v-html

v-html 比较简单,最终操作的是 innerHTML,我们还是看代码,在 src/platforms/compiler/directives/html.js 中:

import { addProp } from “compiler/helpers”;

export default function html(el: ASTElement, dir: ASTDirective) {

if (dir.value) {

addProp(el, “innerHTML”, _s(${dir.value}), dir);

}

}

小结

综上代码证明:

  1. v-iftemplate 生成 ast 之后 genIf 返回三元表达式,在渲染的时候仅渲染表达式生效部分;
  1. v-show 根据表达式的值最终操作的是 style.display,并标记当前 vnode.data.show 属性;
  1. v-html 最终操作的是 innerHTML,将当前值 innerHTML 到当前标签;

11. v-for 给每个元素绑定事件需要事件代理吗?

首先,我们先来看一下 v-for 的实现,同上面 v-if,在模板渲染过程中由genFor 处理,在 src/compiler/codegen/index.js 中:

export function genFor(

el: any,

state: CodegenState,

altGen?: Function,

altHelper?: string

): string {

const exp = el.for;

const alias = el.alias;

const iterator1 = el.iterator1 ? ,${el.iterator1} : “”;

const iterator2 = el.iterator2 ? ,${el.iterator2} : “”;

if (

process.env.NODE_ENV !== “production” &&

state.maybeComponent(el) &&

el.tag !== “slot” &&

el.tag !== “template” &&

!el.key

) {

state.warn(

<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with  +

v-for should have explicit keys.  +

See https://vuejs.org/guide/list.html#key for more info.,

el.rawAttrsMap[“v-for”],

true /* tip */

);

}

el.forProcessed = true; // 标记避免递归,标记已经处理过

return (

${altHelper || "_l"}((${exp}), +

function(${alias}${iterator1}${iterator2}){ +

return ${(altGen || genElement)(el, state)} +

“})”

);

// 伪代码解析后大致如下

// _l(data, function (item, index) {

//   return genElement(el, state);

// });

}

这里其实可以看出,genFor 最终返回了一串伪代码(见注释)最终每个循环返回 genElement(el, state),其实这里可以大胆推测,vue 并没有单独在 v-for 对事件做委托处理,只是单独处理了每次循环的处理;

可以确认的是,vue 在 v-for 中并没有处理事件委托,处于性能考虑,最好自己加上事件委托,这里有个帖子有分析对比,第 94 题:vue 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?

12. 你知道 key 的作⽤吗?

key 可预想的是 vue 拿来给 vnode 作唯一标识的,下面我们先来看下 key 到底被拿来做啥事,在 src/core/vdom/patch.js 中:

updateChildren

function updateChildren(

parentElm,

oldCh,

newCh,

insertedVnodeQueue,

removeOnly

) {

let oldStartIdx = 0;

let newStartIdx = 0;

let oldEndIdx = oldCh.length - 1;

let oldStartVnode = oldCh[0];

let oldEndVnode = oldCh[oldEndIdx];

let newEndIdx = newCh.length - 1;

let newStartVnode = newCh[0];

let newEndVnode = newCh[newEndIdx];

let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

// removeOnly is a special flag used only by 

// to ensure removed elements stay in correct relative positions

// during leaving transitions

const canMove = !removeOnly;

if (process.env.NODE_ENV !== “production”) {

checkDuplicateKeys(newCh);

}

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

if (isUndef(oldStartVnode)) {

oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left

} else if (isUndef(oldEndVnode)) {

oldEndVnode = oldCh[–oldEndIdx];

} else if (sameVnode(oldStartVnode, newStartVnode)) {

patchVnode(

oldStartVnode,

newStartVnode,

insertedVnodeQueue,

newCh,

newStartIdx

);

oldStartVnode = oldCh[++oldStartIdx];

newStartVnode = newCh[++newStartIdx];

} else if (sameVnode(oldEndVnode, newEndVnode)) {

patchVnode(

oldEndVnode,

newEndVnode,

insertedVnodeQueue,

newCh,

newEndIdx

);

oldEndVnode = oldCh[–oldEndIdx];

newEndVnode = newCh[–newEndIdx];

} else if (sameVnode(oldStartVnode, newEndVnode)) {

// Vnode moved right

patchVnode(

oldStartVnode,

newEndVnode,

insertedVnodeQueue,

newCh,

newEndIdx

);

canMove &&

nodeOps.insertBefore(

parentElm,

oldStartVnode.elm,

nodeOps.nextSibling(oldEndVnode.elm)

);

oldStartVnode = oldCh[++oldStartIdx];

newEndVnode = newCh[–newEndIdx];

} else if (sameVnode(oldEndVnode, newStartVnode)) {

// Vnode moved left

patchVnode(

oldEndVnode,

newStartVnode,

insertedVnodeQueue,

newCh,

newStartIdx

);

canMove &&

nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);

oldEndVnode = oldCh[–oldEndIdx];

newStartVnode = newCh[++newStartIdx];

} else {

if (isUndef(oldKeyToIdx))

oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);

idxInOld = isDef(newStartVnode.key)

? oldKeyToIdx[newStartVnode.key]

: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);

if (isUndef(idxInOld)) {

// New element

createElm(

newStartVnode,

insertedVnodeQueue,

parentElm,

oldStartVnode.elm,

false,

newCh,

newStartIdx

);

} else {

vnodeToMove = oldCh[idxInOld];

if (sameVnode(vnodeToMove, newStartVnode)) {

patchVnode(

vnodeToMove,

newStartVnode,

insertedVnodeQueue,

newCh,

newStartIdx

);

oldCh[idxInOld] = undefined;

canMove &&

nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);

} else {

// same key but different element. treat as new element

createElm(

newStartVnode,

insertedVnodeQueue,

parentElm,

oldStartVnode.elm,

false,

newCh,

newStartIdx

);

}

}

newStartVnode = newCh[++newStartIdx];

}

}

if (oldStartIdx > oldEndIdx) {

refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;

addVnodes(

parentElm,

refElm,

newCh,

newStartIdx,

newEndIdx,

insertedVnodeQueue

);

} else if (newStartIdx > newEndIdx) {

removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);

}

}

这段代码是 vue diff 算法的核心代码了,用作比较同级节点是否相同,批量更新的,可谓是性能核心了,以上可以看下 sameVnode 比较节点被用了多次,下面我们来看下是怎么比较两个相同节点的

sameVnode

function sameVnode(a, b) {

return (

// 首先就是比较 key,key 相同是必要条件

a.key === b.key &&

((a.tag === b.tag &&

a.isComment === b.isComment &&

isDef(a.data) === isDef(b.data) &&

sameInputType(a, b)) ||

(isTrue(a.isAsyncPlaceholder) &&

a.asyncFactory === b.asyncFactory &&

isUndef(b.asyncFactory.error)))

);

}

可以看到 key 是 diff 算法用来比较节点的必要条件,可想而知 key 的重要性;

小结

以上,我们了解到 key 的关键性,这里可以总结下:

key 在 diff 算法比较中用作比较两个节点是否相同的重要标识,相同则复用,不相同则删除旧的创建新的;

  1. 相同上下文的 key 最好是唯一的;
  1. 别用 index 来作为 key,index 相对于列表元素来说是可变的,无法标记原有节点,比如我新增和插入一个元素,index 对于原来节点就发生了位移,就无法 diff 了;

13. 说一下 vue 中所有带$的方法?
实例 property
  • vm.$data: Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象 property 的访问。

  • vm.$props: 当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象 property 的访问。

  • vm.$el: Vue 实例使用的根 DOM 元素。

  • vm.$options: 用于当前 Vue 实例的初始化选项。

  • vm.$parent: 父实例,如果当前实例有的话。

  • vm.$root: 当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。

  • vm.$children: 当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。

  • vm.$slots: 用来访问被插槽分发的内容。每个具名插槽有其相应的 property (例如:v-slot:foo 中的内容将会在 vm.$slots.foo 中被找到)。default property 包括了所有没有被包含在具名插槽中的节点,或 v-slot:default 的内容。

  • vm.$scopedSlots: 用来访问作用域插槽。对于包括 默认 slot 在内的每一个插槽,该对象都包含一个返回相应 VNode 的函数。

  • vm.$refs: 一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。

  • vm.$isServer: 当前 Vue 实例是否运行于服务器。

  • vm.$attrs: 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

  • vm.$listeners: 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

实例方法 / 数据
  • vm.$watch( expOrFn, callback, [options] ): 观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。

  • vm.$set( target, propertyName/index, value ): 这是全局 Vue.set 的别名。

  • vm.$delete( target, propertyName/index ): 这是全局 Vue.delete 的别名。

实例方法 / 事件
  • vm.$on( event, callback ): 监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。

  • vm.$once( event, callback ): 监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。

  • vm.$off( [event, callback] ): 移除自定义事件监听器。

    • 如果没有提供参数,则移除所有的事件监听器;
  • 如果只提供了事件,则移除该事件所有的监听器;

  • 如果同时提供了事件与回调,则只移除这个回调的监听器。

  • vm.$emit( eventName, […args] ): 触发当前实例上的事件。附加参数都会传给监听器回调。

实例方法 / 生命周期
  • vm.$mount( [elementOrSelector] )

    • 如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例。
  • 如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中。

  • 这个方法返回实例自身,因而可以链式调用其它实例方法。

  • vm.$forceUpdate(): 迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

  • vm.$nextTick( [callback] ): 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。

  • vm.$destroy(): 完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。

    • 触发 beforeDestroy 和 destroyed 的钩子。
14. 你知道 nextTick 吗?

最后

给大家分享一些关于HTML的面试题。


网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
组件,并且使用 Array 作为真正的来源。

  • vm.$slots: 用来访问被插槽分发的内容。每个具名插槽有其相应的 property (例如:v-slot:foo 中的内容将会在 vm.$slots.foo 中被找到)。default property 包括了所有没有被包含在具名插槽中的节点,或 v-slot:default 的内容。

  • vm.$scopedSlots: 用来访问作用域插槽。对于包括 默认 slot 在内的每一个插槽,该对象都包含一个返回相应 VNode 的函数。

  • vm.$refs: 一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。

  • vm.$isServer: 当前 Vue 实例是否运行于服务器。

  • vm.$attrs: 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

  • vm.$listeners: 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

实例方法 / 数据
  • vm.$watch( expOrFn, callback, [options] ): 观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。

  • vm.$set( target, propertyName/index, value ): 这是全局 Vue.set 的别名。

  • vm.$delete( target, propertyName/index ): 这是全局 Vue.delete 的别名。

实例方法 / 事件
  • vm.$on( event, callback ): 监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。

  • vm.$once( event, callback ): 监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。

  • vm.$off( [event, callback] ): 移除自定义事件监听器。

    • 如果没有提供参数,则移除所有的事件监听器;
  • 如果只提供了事件,则移除该事件所有的监听器;

  • 如果同时提供了事件与回调,则只移除这个回调的监听器。

  • vm.$emit( eventName, […args] ): 触发当前实例上的事件。附加参数都会传给监听器回调。

实例方法 / 生命周期
  • vm.$mount( [elementOrSelector] )

    • 如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例。
  • 如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中。

  • 这个方法返回实例自身,因而可以链式调用其它实例方法。

  • vm.$forceUpdate(): 迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

  • vm.$nextTick( [callback] ): 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。

  • vm.$destroy(): 完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。

    • 触发 beforeDestroy 和 destroyed 的钩子。
14. 你知道 nextTick 吗?

最后

给大家分享一些关于HTML的面试题。

[外链图片转存中…(img-bYP9t4Om-1713229543962)]
[外链图片转存中…(img-TdNPS8BW-1713229543962)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-0zlWfApV-1713229543962)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值