先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Web前端全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024c (备注前端)
正文
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;
}
}
}
小结
❝
综上响应式核心代码,我们可以描述响应式的执行过程:
- 根据数据类型来做不同处理,如果是对象则
Object.defineProperty()
监听数据属性的get
来进行数据依赖收集,再通过get
来完成数据更新的派发;如果是数组如果是数组则通过覆盖 该数组原型的⽅法,扩展它的 7 个变更⽅法(push
/pop
/shift
/unshift
/splice
/reverse
/sort
),通过监听这些方法可以做到依赖收集和派发更新;
Dep
是主要做依赖收集,收集的是当前上下文作为Watcher
,全局有且仅有一个Dep.target
,通过Dep
可以做到控制当前上下文的依赖收集和通知Watcher
派发更新;
Watcher
连接表达式和值,说白了就是 watcher 连接视图层的依赖,并可以触发视图层的更新,与Dep
紧密结合,通过Dep
来控制其对视图层的监听
❞
4. vue3
为何用 proxy
替代了 Object.defineProperty
?
traverse
❝
截取上面 Watcher 中部分代码
❞
if (this.deep) {
// 这里其实递归遍历属性用作依赖收集
traverse(value);
}
❝
再查看
src/core/observer/traverse.js
中traverse
的实现,如下:
❞
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);
}
}
小结
❝
再综上一题代码实际了解,其实我们看到一些弊端:
- Watcher 监听 对属性做了递归遍历,这里可能会造成性能损失;
- defineReactive 遍历属性对当前存在的属性
Object.defineProperty()
作依赖收集,但是对于不存在,或者删除属性,则监听不到;从而会造成 对新增或者删除的属性无法做到响应式,只能通过 Vue.set/delete 这类 api 才可以做到;
- 对于 es6 中新产⽣的
Map
、Set
这些数据结构不⽀持
❞
5. vue
双向绑定,Model
怎么改变 View
,View
怎么改变 Model
?
❝
其实这个问题需要承接上述第三题,再结合下图
❞
响应式原理
❝
Model 改变 View:
- defineReactive 中通过 Object.defineProperty 使 data 可响应;
- Dep 在 getter 中作依赖收集,在 setter 中作派发更新;
- dep.notify() 通知 Watcher 更新,最终调用
vm._render()
更新 UI;
View 改变 Model: 其实同上理,View 与 data 的数据关联在了一起,View 通过事件触发 data 的变化,从而触发了 setter,这就构成了一个双向循环绑定了;
❞
6. vue
如何对数组方法进行变异?例如 push
、pop
、slice
等;
❝
这个问题,我们直接从源码找答案,这里我们截取上面 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,
});
}
小结
❝
Object.create(Array.prototype)
复制Array
原型链为新的对象;
- 拦截了数组的 7 个方法的执行,并使其可响应,7 个方法分别为:
push
,pop
,shift
,unshift
,splice
,sort
,reverse
;
- 当数组调用到这 7 个方法的时候,执行
ob.dep.notify()
进行派发通知Watcher
更新;
❞
附加思考
❝
不过,vue 对数组的监听还是有限制的,如下:
- 数组通过索引改变值的时候监听不到,比如:
array[2] = newObj
- 数组长度变化无法监听
这些操作都需要通过
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 情况):
Object.create(null)
创建一个空对象用作缓存computed
属性的watchers
,并缓存在vm._computedWatchers
中;
- 遍历计算属性,拿到用户定义的
userDef
,为每个属性定义Watcher
,标记Watcher
属性lazy: true
;
- 定义
vm
中未定义过的computed
属性,defineComputed(vm, key, userDef)
,已存在则判断是在data
或者props
中已定义并相应警告;
- 接下来就是定义
computed
属性的getter
和setter
,这里主要是看createComputedGetter
里面的定义:当触发更新则检测 watcher 的 dirty 标记,则执行watcher.evaluate()
方法执行计算,然后依赖收集;
- 这里再追溯
watcher.dirty
属性逻辑,在watcher.update
中 当遇到 computed 属性时候被标记为dirty:false
,这里其实可以看出computed
属性的计算前提必须是引用的正常属性的更新触发了Dep.update()
,继而触发对应watcher.update
进行标记dirty:true
,继而在计算属性getter
的时候才会触发更新,否则不更新;
以上便是计算属性的实现逻辑,部分代码逻辑需要追溯上面第三题响应式的部分
Dep/Watcher
的触发逻辑;
❞
8. computed
和 watch
的区别在哪里?
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
属性的实现逻辑:
- 遍历
watch
属性分别创建属性的Watcher
监听,这里可以看出其实该属性并未被Dep
收集依赖;
- 可以分析
watch
监听的属性 必然是已经被Dep
收集依赖的属性了(data/props
中的属性),进行对应属性触发更新的时候才会触发watch
属性的监听回调;
这里就可以分析 computed 与 watch 的异同:
computed
属性的更新需要依赖于其引用属性的更新触发标记dirty: true
,进而触发computed
属性getter
的时候才会触发其本身的更新,否则其不更新;
watch
属性则是依赖于本身已被Dep
收集依赖的部分属性,即作为data/props
中的某个属性的尾随watcher
,在监听属性更新时触发watcher
的回调;否则监听则无意义;
这里再引申一下使用场景:
- 如果一个数据依赖于其他数据,那么就使用
computed
属性;
- 如果你需要在某个数据变化时做一些事情,使用
watch
来观察这个数据变化;
❞
9. 计算属性和普通属性的区别?
❝
这个题目跟上题类似,区别如下:
- 普通属性都是基于
getter
和setter
的正常取值和更新;
computed
属性是依赖于内部引用普通属性的setter
变更从而标记watcher
中dirty
标记为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);
}
}
小结
❝
综上代码证明:
v-if
在template
生成ast
之后genIf
返回三元表达式,在渲染的时候仅渲染表达式生效部分;
v-show
根据表达式的值最终操作的是style.display
,并标记当前vnode.data.show
属性;
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 算法比较中用作比较两个节点是否相同的重要标识,相同则复用,不相同则删除旧的创建新的;
- 相同上下文的 key 最好是唯一的;
- 别用 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()
手动地挂载一个未挂载的实例。
- 如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用
-
如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中。
-
这个方法返回实例自身,因而可以链式调用其它实例方法。
-
vm.$forceUpdate()
: 迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。 -
vm.$nextTick( [callback] )
: 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。 -
vm.$destroy()
: 完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。 -
- 触发 beforeDestroy 和 destroyed 的钩子。
14. 你知道 nextTick
吗?
❝
最后
给大家分享一些关于HTML的面试题。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事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()
手动地挂载一个未挂载的实例。
- 如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用
-
如果没有提供 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行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!