Vue2 题解QA
# 什么是 MVVM?
MVVM 是 model-view-ViewModel 的缩写,是一种基于前端开发的架构模式,核心是对 view 和 viewModel 的双向数据绑定。viewModel 是 view 和 model 层的桥梁,数据会绑定到 viewModel 层并自动将数据渲染到页面中。视图变化的时候会通知 viewModel 层更新数据. 在 vue 中,将视图层的 dom 元素做数据劫持。通过 gettter 和 setter 暴露循环作用域下的变量,产生闭包。他能在内部让 vue 追踪依赖,在被访问和修改时通知变更。实现双向数据绑定。
# 那么 vue 中怎么做到双向数据绑定的
所谓的双向数据绑定对于单向数据绑定来说,其实就是为元素的变更绑定了相应的变更事件,实现的方式大致有以下几种
- 发布订阅者模式
- 脏值检查-在指定的一些事件触发时,对比数据是否变更来决定是否更新视图
- 数据劫持
vue 采用数据劫持结合发布订阅者模式 通过 object. defineProperty()来劫持各个属性的 setget,数据变动时发布消息给订阅者,出发相应的监听回调(在 3. 0 中改用 proxy)
# VUE 的性能优化
- 编码阶段
- 尽量减少 data 中的数据,data 中的数据都会增加 getter 和 setter,会收集对应的 watcher,object 引入的类型可以使用 object.freeze 冻结减少数据监听。
- v-if 和 v-for 不能连用
- 如果需要使用 v-for 时,若是需要循环数量很大时,可以考虑使用事件代理(会提高一点性能),无论是监听器数量还是内存占比率都会比绑定在子元素上少。react 是将事件委托到了 document 上,然后自己生成了合成事件,冒泡到 document 的时候进入合成事件,然后通过 getParent()获取该事件源的所有合成事件,触发完毕后继续冒泡。
- SPA 页面采用 keep-alive 缓存组件。
- 根据需求情况选择 v-if 还是用 v-show
- 使用路由懒加载、异步组件
- 图片懒加载
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- SEO
- 预渲染
- 服务端渲染 SSR
- 打包优化
- 压缩代码 (css: MiniCssExtractPlugin)(js: terser-webpack-plugin)
- Tree shaking/ scope Hoisting
- 使用 CDN 加载第三方模块
- 多线程打包 happypack(happypack/loader?id=happy-eslint-js)
- splitChunk 抽离公共文件
- sourceMap 优化(dev:cheap-module-source-map, prod:source-map)
# css 样式穿透
由于 scoped 属性的样式隔离,修改不到第三方组件的样式,需要做样式穿透(在 css 预处理器中使用才生效)
- less 使用/deep/
<style scoped lang="less">
.content /deep/ .el-button {
height:20px;
}
</style>
2
3
4
5
- scss 使用 ::v-deep
<style scoped lang="scss">
.content ::v-deep .el-button {
height:20px;
}
</style>
2
3
4
5
- stylus 使用 >>>
<style scoped lang="stylus">
.content >>> .el-button {
height:20px;
}
</style>
2
3
4
5
# 请详细说下你对 vue 生命周期的理解?
总共分为 8 个阶段创建前/后,载入前/后,更新前/后,销毁前/后
beforeCreate 创建前执行(vue 实例的挂载元素$el 和数据对象 data 都为 undefined,还未初始化)
created 完成创建 (完成了 data 数据初始化,el 还未初始化)
beforeMount 载入前(vue 实例的$el 和 data 都初始化了,但还是挂载之前为虚拟的 dom 节点,data.message 还未替换。)
mounted 载入后 html 已经渲染(vue 实例挂载完成,data.message 成功渲染。)
beforeUpdate 更新前状态(view 层的数据变化前,不是 data 中的数据改变前)
updated 更新状态后
beforeDestroy 销毁前
destroyed 销毁后 (在执行 destroy 方法后,对 data 的改变不会再触发周期函数,说明此时 vue 实例已经解除了事件监听以及和 dom 的绑定,但是 dom 结构依然存在)
说一下每一个阶段可以做的事情
beforeCreate:可以在这里加一个 loading 事件,在加载实例时触发。
created:初始化完成时的事件写这里,如果这里结束了 loading 事件,异步请求也在这里调用。
mounted:挂在元素,获取到 DOM 节点
updated:对数据进行处理的函数写这里。
beforeDestroy:可以写一个确认停止事件的确认框。
# data 是如何被访问的
vue 在执行过程中,会将 data 和 prop 都挂载在 vm 实例上,这个时候源码做了一个判断,如果是相同的命名则会报错。在源码中,其对 data 中的元素进行了 proxy 代理:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};
function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
// proxy(_vm, _data, key) 所以我们在vm._data中也可以访问到data中的对象
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
render: 在 vue 中可以写 template 和 render 函数,最终都会被 vue 编译成 render function 的形式,render()函数里有一个 createElement 方法,我们可以调用这个方法传入我们所 需要渲染的 dom 元素,createElement 方法其实是对_createElement 函数的封装,他通过用户传入更多的参数,在处理完传入的参数后,调用真正创建 Vnode 的函数_createElement
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
)
2
3
4
5
6
7
8
_createElement 方法有 5 个参数,context 表示 VNode 的上下文环境,它是 Component 类型;tag 表示标签,它可以是一个字符串,也可以是一个 Component;data 表示 VNode 的数据,它是一个 VNodeData 类型,可以在 flow/vnode.js 中找到它的定义;children 表示当前 VNode 的子节点,它是任意类型的,它接下来需要被规范为标准的 VNode 数组;normalizationType 表示子节点规范的类型,类型不同规范的方法也就不一样,它主要是参考 render 函数是编译生成的还是用户手写的。 由于 Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型。_createElement 接收的第 4 个参数 children 是任意类型的,因此我们需要把它们规范成 VNode 类型。 其实源码是很复杂繁琐的,我们要从源码中学习到 函数式的写法,柯里化的运用
# virtual dom 的优势在哪里
重要的一点,虚拟 dom 赋予了在渲染到浏览器之前我们以编程方式构造、检查、克隆以及操作所需的 dom 结构的能力 Dom 引擎、js 引擎相互独立,但又工作在同一线程,js 代码调用 dom api 必须挂起 js 引擎,转换传入参数数据、激活 dom 引擎,dom 重绘后再转换可能有的返回值,最后激活 js 引擎并继续执行若有频繁的 dom api 调用,且浏览器厂商不做“批量处理”优化。 引擎间切换的单位代价将迅速积累若其中有强制重绘的 dom api 调用,重新计算布局、重新绘制图像会引起更大的性能消耗。
优势:
- 虚拟 dom 不会立马进行排版和重绘操作。
- 虚拟 dom 会在进行频繁修改过后一次性修改真实的 dom,减少操作真实 dom 频繁触发排版和重绘。
- 虚拟 dom 有效降低大面积真实 dom 的重绘和排版,因为最终与真实 dom 比较差异,可以只渲染局部。
# 你对 template 与 jsx 写法的看法
当我们在书写一些复杂的 ui 组件或是业务组件的时候,他们往往只含有少量的标签,却拥有大量的交互逻辑,在这种情况下,模版语法有时候会限制你更容易的表达潜在的逻辑,这个时候你会发现会有许多的逻辑你写在 template 中,还有许多的逻辑写在了 javascript 中。而 render 函数的写法允许我们把这些逻辑组合到一个地方。
# vue 核心源码简写
1.x 版本
utils = {
getValue(expr, vm) {
return vm.$data[expr.trim()];
},
setValue(expr, vm, newValue) {
vm.$data[expr.trim()] = newValue;
},
modelUpdater(node, value) {
node.value = value;
},
textUpdater(node, value) {
node.textContent = value;
},
model(node, value, vm) {
const initValue = this.getValue(value, vm);
new Wacher(value, vm, (newVal) => {
this.modelUpdater(node, newVal);
});
node.addEventListener("input", (e) => {
const newValue = e.target.value;
this.setValue(value, vm, newValue);
});
this.modelUpdater(node, initValue);
},
text(node, value, vm) {
let result;
if (value.includes("{{")) {
result = value.replace(/\{\{(.+?)\}\}/g, (...args) => {
const expr = args[1];
new Wacher(expr, vm, (newVal) => {
this.textUpdater(node, newVal);
});
return this.getValue(expr, vm);
});
} else {
result = this.getValue(value, vm);
}
this.textUpdater(node, result);
},
on(node, value, vm, eventName) {
console.log(node, value, vm, eventName);
const eventMethod = vm.$options.methods ? vm.$options.methods[value] : null;
eventMethod &&
node.addEventListener(eventName, eventMethod.bind(vm), false);
},
};
class Dep {
constructor() {
this.subs = [];
}
addSubs(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach((w) => w.updated());
}
}
class Wacher {
constructor(expr, vm, cb) {
this.expr = expr;
this.vm = vm;
this.cb = cb;
// 为了能够触发一次defineProperty,将相同变量的事件挂载在同一个对象上
this.oldValue = this.getOldValue();
}
getOldValue() {
// 这里的Dep也可以换成任意一个全局变量 做定义
Dep.target = this;
// 调用了getValue方法从而调用了$data.get() 做事件劫持
const oldValue = utils.getValue(this.expr, this.vm);
Dep.target = null;
return oldValue;
}
updated() {
const newValue = utils.getValue(this.expr, this.vm);
if (newValue !== this.oldValue) {
this.cb(newValue);
}
}
}
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
/**
* 1. 创建碎片元素
*/
const fragment = this.compileFragment(this.el);
this.compile(fragment);
this.el.appendChild(fragment);
}
isElementNode(el) {
return el.nodeType && el.nodeType === 1;
}
isTextNode(el) {
return el.nodeType && el.nodeType === 3;
}
isDirector(name) {
return name.startsWith("v-");
}
isEventName(name) {
return name.startsWith("@");
}
compileElement(node) {
// v-
const attributes = Array.from(node.attributes);
attributes.forEach((attr) => {
const { name, value } = attr;
if (this.isDirector(name)) {
const [, directive] = name.split("-");
const [compileKey, eventName] = directive.split(":");
console.log(compileKey, eventName, name);
utils[compileKey](node, value, this.vm, eventName);
} else if (this.isEventName(name)) {
const [, eventName] = name.split("@");
utils["on"](node, value, this.vm, eventName);
}
});
}
compileText(node) {
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
console.log("content:", content);
utils["text"](node, content, this.vm);
}
}
compile(node) {
const childNodes = Array.from(node.childNodes);
childNodes.forEach((childNode) => {
if (this.isElementNode(childNode)) {
this.compileElement(childNode);
} else if (this.isTextNode(childNode)) {
this.compileText(childNode);
}
if (childNode.childNodes && childNode.childNodes.length) {
this.compile(childNode);
}
});
}
compileFragment(el) {
const f = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
f.appendChild(firstChild);
}
return f;
}
}
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
}
defineReactive(data, key, value) {
this.observe(value);
const dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: () => {
const target = Dep.target;
target && dep.addSubs(target);
return value;
},
set: (newValue) => {
if (value === newValue) return;
this.observe(newValue);
value = newValue;
dep.notify();
},
});
}
}
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
this.$el = options.el;
new Observer(this.$data);
new Compiler(this.$el, this);
this.proxyData(this.$data);
}
proxyData(data) {
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get: () => {
return data[key];
},
set: (newValue) => {
data[key] = newValue;
},
});
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# 为什么 Vuex
的 mutation
和 Redux
的 reducer
中不能做异步操作?
# Mutation
必须是同步函数
一条重要的原则就是要记住 mutation
必须是同步函数。为什么?请参考下面的例子
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
2
3
4
5
6
7
观察 devtool
中的 mutation
日志。每一条 mutation
被记录,devtools
都需要捕捉到前一状态和后一状态的快照。
然而,在上面的例子中 mutation
中的异步函数中的回调让这不可能完成:因为当 mutation
触发的时候,回调函数还没有被调用,devtools
不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
# 区分 actions
和 mutations
并不是为了解决竞态问题,而是为了能用 devtools
追踪状态变化。
- 事实上在
vuex
里面actions
只是一个架构性的概念,并不是必须的,说到底只是一个函数,你在里面想干嘛都可以,只要最后触发mutation
就行。 - 异步竞态怎么处理那是用户自己的事情。
vuex
真正限制你的只有mutation
必须是同步的这一点(在redux
里面就好像reducer
必须同步返回下一个状态一样)。 - 同步的意义在于这样每一个
mutation
执行完成后都可以对应到一个新的状态(和reducer
一样),这样devtools
就可以打个snapshot
存下来,然后就可以随便time-travel
了。 - 如果你开着
devtool
调用一个异步的action
,你可以清楚地看到它所调用的mutation
是何时被记录下来的,并且可以立刻查看它们对应的状态。
在此前提下,开发者们总结出:
vuex
的处理方式是同步在mutation
里面,异步在actions
里面。
# 在 Vue
中,子组件为何不可以修改父组件传递的 Prop
,如果修改了,Vue
是如何监控到属性的修改并给出警告的。
# 子组件为何不可以修改父组件传递的 Prop
?
- 一个父组件不只有你一个子组件。同样,使用这份
prop
数据的也不只有你一个组件。如果每个子组件都能修改prop
的话,将会导致修改数据的源头不止一处。 - 单向数据流,易于监测数据的流动。出现了错误可以更加迅速的定位到错误的位置。
# 如果修改了,Vue
是如何监控到属性的修改并给出警告的
// 在initProps的时候,在defineReactive时通过判断是否在开发环境
// 如果是开发环境,会在触发set的时候判断是否此key是否处于updatingChildren中被修改
// 如果不是,说明此修改来自子组件,触发warning提示
if (process.env.NODE_ENV !== "production") {
var hyphenatedKey = hyphenate(key);
if (
isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)
) {
warn(
'"' +
hyphenatedKey +
'" is a reserved attribute and cannot be used as component prop.',
vm
);
}
defineReactive$$1(props, key, value, function () {
if (!isRoot && !isUpdatingChildComponent) {
warn(
"Avoid mutating a prop directly since the value will be " +
"overwritten whenever the parent component re-renders. " +
"Instead, use a data or computed property based on the prop's " +
'value. Prop being mutated: "' +
key +
'"',
vm
);
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
需要特别注意的是,当你从子组件修改的
prop
属于基础类型时会触发提示。这种情况下,你是无法修改父组件的数据源的,因为基础类型赋值时是值拷贝。
你直接将另一个非基础类型(
Object, array
)赋值到此key
时也会触发提示(但实际上不会影响父组件的数据源),当你修改object
的属性时不会出发提示,并且会修改父组件数据源的数据
所有的
prop
都使得其父子prop
之间形成了一个单向下行绑定:父级prop
的更新会向下流动到子组件中,但是反过来不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
同时,不仅仅是判断是否在updatingChildren
中修改,当其内部还会传入第四个参数,如果不是root
根组件,并且不是更新子组件,那么说明更新的是prop
,所以会警告。
# vue
相关源码
// src/core/instance/state.js 源码路径
function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {};
const props = (vm._props = {});
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = (vm.$options._propKeys = []);
const isRoot = !vm.$parent;
// root instance props should be converted
if (!isRoot) {
toggleObserving(false);
}
for (const key in propsOptions) {
keys.push(key);
const value = validateProp(key, propsOptions, propsData, vm);
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production") {
const hyphenatedKey = hyphenate(key);
if (
isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)
) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
);
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
);
}
});
} else {
defineReactive(props, key, value);
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key);
}
}
toggleObserving(true);
}
// src/core/observer/index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep();
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return;
}
// cater for pre-defined getter/setters
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115