谈谈你对 MVVM 的理解
mvc
比较早期的是mvc针对后端来说的,大概流程如下。
用在前端比较典型的是 backbone,它有控制器的概念,配合上 underscore 的模板引擎,再加上 jquery 去做视图(view)和控制器(controller)的关联,其中 jq 是被用于 backbone 的 controller 层的(模板组装)。
缺点:如果使用传统的 mvc 大量的逻辑会耦合在c一层(不仅有数据处理,还要处理模板拼接,页面更新), 不仅有数据处理,还有(导致维护困难),所有就有了后面几种模式,比如 mvp, mv*,但是目前比较主流的就是 mvvm。
mvvm
mvvm 也分为三层,分别是 model,view 和 viewModal(viewModel 简化controller这一层),传统的 mvvm 要求不能手动操作视图更新(Vue 暴露了 ref 属性,这就不完全遵循 mvvm 了),可以说,mvvm 的核心就是为了隐藏 controller,因为这层太过复杂,通过封装的方法,使页面更新和数据自动更新(v-model)。
虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例。
mvvm 角度对比 vue 和 react
提到这里就不得不说一句,vue 和 react 的区别之一就是,react 专注 view 层,但是通过它们都能实现 mvvm,react 本身是库,vue 则是框架,库和框架的区别在于,我们使用库时,会主动调用库中的方法,比如 react 库想更新页面,需要调用它提供的 api(数据改变后我们需要手动调用 setState 更新页面),而框架会帮我们调用,只要我们按照它的套路来写代码~
请说一下 Vue2 和 Vue3 响应式数据的理解
响应式数据的核心就是,数据变化我能知道,而不是说数据驱动视图这一层。
对象在 vue2 中采用了 defineProperty(Vue.util.defineReactive)将数据定义成了响应式的数据(拦截所有的属性增加了 getter 和 setter),缺陷就是要递归构建,且每层递归都是一个闭包函数,缓存了当前属性的 val 和 watcher 收集器 dep,且不存在的属性无法被监控到。
Vue2 对数组并没有采用 defineProperty,因为数组中可能元素很多(劫持索引性能差),而且用户不会操作索引更改数据(劫持索引也无效),不过数组内元素会被递归检测,如果是对象,会被继续劫持对象的属性,所以数组不要钱陶过深,Vue2 通常采用以下方法进行优化~
- Vue2中减少层级数据嵌套不要过深
- 如果不需要响应式的数据就不要放在 data 中, 合理使用 Object.freeze
- 我们尽量缓存使用过的变量,不要循环取值或 set 值。
vue3 采用了 proxy 直接可以对对象拦截,不用重写 get 和 set 方法,性能高,不需要直接递归(懒劫持)。
Vue2 中如何检测数组变化
vue2 中并没有使用 defineProperty 来检测我们的数组,因为数组中可能元素很多(劫持索引性能差),而且用户不会操作索引更改数据(劫持索引也无效),vue2 里面就采用重写数组的方法来实现 (7个变异的方法,能改变原数组的方法),通过 Object.create 一层对象做拦截的方式实现的,数组中的对象类型的元素也会被递归观测。缺陷是不能检测到索引更改和数组长度的更改,
Vue中如何进行依赖收集?(观察者模式)
依赖收集的目的是,等会数据变化了可以更新视图。
如何收集的呢?每个属性都有一个 dep 属性、每个对象也都有一个 dep 属性(obj.ob.dep)。 每个组件在渲染的过程中都会创建一个渲染 watcher(watcher 有三种,渲染 watcher,计算属性 watcher,用户 watcher), 一个属性可能会有多个 watcher(多个组件中使用), 反过来一个 watcher 有多个 dep(一个组件使用了多个变量)。
当调用取值方法的时候(比如渲染模板中使用到的变量)如果有 watcher 就会将 watcher 收集到属性的 dep 上,等会数据变化后会通知自己对应的 dep 触发更新调用 watcher.update 方法,当然这里会异步触发更新(最后调用 watcher.get,也就是 vm.update(vm._render()) 去做页面更新,当然这里不用回答)。
如何理解 vue2 中的模板编译原理
模板编译的原理核心就是 模板 -> ast -> render 函数。
- 会将模板通过正则分词,变为 ast 语法树
- 对 ast 语法树进行优化,标记静态节点,静态节点 patch 时会跳过比对,(vue3 中模板编译的优化: patchFlag,blockTree,事件缓存,节点缓存等优化)
- 代码生成,拼接 render 函数内部字符串,比如 _c('div', ...)
- 通过 new Function(字符串变函数) + with(保证 this 指向 vm) 将其转为 render 函数
let template = `<div id="app">{{ msg }}</div>`;
// 模板 -> ast
const ast = parserHTML(template);
// ast -> render 函数内代码字符串,结果如下
const code = generate(ast);
// @1 _c 是创建 dom 节点的方法
// @2 _v 是创建文本的方法
// @3 _s 是转字符串的方法,如果 msg 是个对象,则会转成字符串输出
code = ```_c('div', {
attrs: {
"id": "app"
}
}, [_v(_s(msg))])```
// 生成 render 函数
render = new Function(`with(this){return ${code}}`);
Vue2 的生命周期是怎么实现的
- 生命周期钩子在内部会被vue维护成一个数组(vue 内部有一个方法 mergeOptions)和全局的生命周期(比如 mixin 的生命周期会挂载全局 vue.options 上)合并最终转换成数组,当执行到具体流程时会通过 callHook 来实现调用钩子函数,其实就是一个发布订阅模式(先收集,再遍历调用)
Vue2 的生命周期方法有哪些?一般在哪一步发送请求及原因?
首先,神图奉上。

本题是个误导题目,其实在哪儿发请求都一样。
不过如果你想操作 dom,created 中必须异步的去获取 dom 才行,因为 ajax 请求本身是个异步,所以 created 中发送请求操作 dom 也没毛病!而 mounted 中 dom 才生成,自然也没毛病,至于有说法 created 中组件会更早的渲染更是无稽之谈,created 的请求会更早的发出,但是 ajax 回调是异步的,会等同步代码执行完毕后(mounted 也执行完毕)才能处理请求回调,所以一定不会影响到页面的初次渲染,然后执行 ajax 回调,请求回来再次渲染。
综上所述,created 和 mounted 中请求数据,都可以在回调中获取到 dom,且都不影响组件的初次渲染,只是可能 created 中发送请求,请求会提前被发送出去,请求结果可以更快的拿到,等初次渲染完成后可以更快的去更新。
不过,在服务端渲染的时候,我们无法使用浏览器的钩子(服务端渲染是把结果渲染成一个字符串,返回给浏览器,挂载过程是在服务器做的,那么在浏览器写的 mounted 方法自然是不能指向的),所以我们会把功能写在 created 方法中。
具体罗列如下:
- beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。
- created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算,watch/event 事件回调。这里没有$el
- beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
- mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
- beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
- updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
- beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
- destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
Vue 组件 data 为什么必须是一个函数
我们知道,Vue.component 注册一个组件,会调用 Vue.extend,并将 Vue.compoent 组件配置 options 传入,而 Vue.extend 返回一个子组件类的实例 Sub,如果实例化 2 次子组件时,data 是函数可以保证返回的不是同一个对象,防止 data 数据被多个子组件共享,否则,Vue 源码内部会抛出一个警告。
// 伪代码
Vue.extend = function(options) {
function Sub(options) {
// 合并初始化 options 中 data 和 实例化时传入的 options
this.data = mergeOptions(this.constructor.options.data(), options.props);
}
// 子类记录 extend 传入的 options
Sub.options = options;
return Sub;
}
const Sub = Vue.extend({
data() {
return { name: 'ys' }
}
})
let childComp1 = new Sub({ props, slot });
let childComp2 = new Sub({ props, slot });
Vue.mixin 的使用场景和原理
Vue.mixin 的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准。
mixin 优点:
- 无侵入式增加组件的公共能力。
mixin 缺点:
- 命名冲突,混入的属性或者方法名称可能被组件内的同名变量替换掉
- 数据来源不清晰,莫名其妙应用 2 中多了一个 a 属性。
nextTick在哪里使用?原理是?
- nextTick 功能是批处理,多次调用默认会将逻辑暂存到队列 callbacks 中,稍后同步代码执行完毕后会调用,依次执行队列任务
- 内部实现原理,经典的异步更新流程,收集 -> 加锁 -> 异步更新 -> 继续执行同步代码收集任务 -> 同步执行完毕后开始整微任务队列 -> 微任务队列就一个任务!nextTick -> 同步方式清空收集到的任务 callbacks 中列表 -> 开锁,注意,不管 callbacks 收集了多少任务,也只是在外面包了一个异步任务,该任务执行后去同步的方式依次清空 callbacks。
- 异步的实现原理 先采用promise.then 不行在采用 mutationObserver 不行在采用 setImmediate 不行在采用 setTimeout 优雅降级
let vm = new Vue({
el: '#app',
data() {
a: 1
},
mounted() {
Vue.nextTick(() => {
console.log(app.innerHtml);
});
this.a = 100;
}
});
// nextTick 输出的是 1,因为 this.a = 100,虽然看起来是同步的,实际上是往 nextTick 的 callbacks 中塞了一个更新任务 watcher.run,此时微队列为 [nextTick],同步代码执行完毕后,nextTick 执行,去同步清空 callbacks,也就是 [userNextTickCb, watcher.run],所以先打印,再更新~
// 如果二者反过来,则打印 100 哦
附 nextTick 源码
// ......
const callbacks = [];
let wating = false; // 防抖
// 依次执行 nextTick 队列中的 callback
function flushCallbacks() {
callbacks.forEach(cb => cb());
wating = false;
}
// 降级策略
function timer(cb) {
let timerFn = () => {};
if (Promise) {
timerFn = () => {
Promise.resolve().then(cb);
};
} else if (MutationObserver) { // 微任务 监听节点变化的 api
let textNode = document.createTextNode(1); // 随便创建个文本节点来监听
let observe = new MutationObserver(cb); // 注册个回调
observe.observe(textNode, { // 监控文本节点变化 characterData 代表文本内容
characterData: true
});
timerFn = () => {
textNode.textContent = 2;
}
} else if (setImmediate) { // ie 才认的 api,性能略高于 setTimeout
timerFn = () => {
setImmediate(cb);
}
} else {
// 再不支持 只能延时器了
timerFn = () => {
setTimeout(cb);
}
}
timerFn();
}
// 源码中的调度器会优先调用 nextTick 方法(批量更新就调用)
// 所以更新 dom 的操作会先入 callbacks 队列
export function nextTick(cb) {
callbacks.push(cb);
if (!wating) {
// vue3 不考虑兼容,这里直接 Promise.resolve.then(flushCallbacks)
// vue2 中考虑兼容性问题,有个降级策略
timer(flushCallbacks);
wating = true;
}
}
多次修改一个属性,会多次更新页面么
this.a = 100;
this.a = 200;
不会重复渲染,因为每个组件对应一个 watcher,属性改变时去调用 dep.notify,对自己收集到的 watcher 依次执行 update 操作,而 update 时调用 queueWatcher 进行了 watcher 的去重,最后调用 nextTick 完成异步更新,所以两次数据的更改不会重复渲染。
computed 和 watcher 的区别
这两个东西内部都是基于 watcher 的,区别是 computed 数据可以用于页面渲染,watch 不行,computed 只有在取值时才会执行对应的回调(lazy 为 true 所以不会立即执行),watch 默认会执行一次(拿到老的值)。
computedWatcher 用了一个 dirty 属性实现了缓存机制,多次取值如果依赖的值不发生变化不会更改 dirty 的结果,拿到的就是老的值。
Vue.set 方法是如何实现的?
为了实现给以前不存在的对象添加属性可以实现更新页面,对象采用的是defineProperty不存在的属性检测不到,数组没有检测索引所以也监控不到,所以 Vue 提供了一个 set api。
- 如果是数组,使用数组的 splice 方法触发更新
- 如果是对象,调用 defineReactive 对该属性进行劫持,并调用对象 ob.dep.notify() 去通知更新
export function set (target: Array<any> | Object, key: any, val: any): any {
// 是不是数组, 如果是数组而且是索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val) // splice内部会触发 notify
return val
}
// 如果对象本身就有这个属性 那就直接赋值就好了
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// 不能把响应式属性添加到 vue 实例上
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) { // 不是响应式的直接赋值
target[key] = val
return val
}
defineReactive(ob.value, key, val) // 把这个属性定义成响应式的
ob.dep.notify() // 主动通知更新
return val
}
Vue 为什么需要虚拟 DOM
- 最核心的是跨端,不同的平台实现方案不同。 内部实现可以不局限于针对浏览器平台,比如在服务端渲染不会生成真实 dom,可能就是根据 vnode 生成一个字符串。
- 如果开发者频繁操作 dom 可能会浪费性能,虚拟 dom 你可以认为增加了一层缓存,我们会先更新虚拟 dom,再更新到页面上。
- 因为 dom diff 比较的是前后的虚拟 dom,比较差异更新页面(也可以真实 dom diff 性能差)。
- 多次 dom 操作浏览器会进行合并的。
for (let i = 0; i < 10; i++) {
document.body.appendChild(xxx); // 只会渲染一次
}
Vue中 diff 算法原理
- diff 算法是 O(n) 级别的,采用的是同级比较,内部是深度优先遍历的方式遍历节点
- 节点判断是否是同一个元素,如果是同一个元素,则比对属性比对孩子,如果不是则直接删除老的换成新的
- Vue2 中采用了双指针对一些场景做了优化策略 (如果是静态节点可以跳过 diff 算法),头头,尾尾,尾头,头尾进行优化。
- 最后乱序比较就是根据老节点创造一个映射表,用新的去里边找能复用的就复用节点,dom 前移,列表原位补 null,不存在的节点就创建 dom,最好删除老节点列表没使用到的节点。
可以看到,顺序固定的元素,复用的时候也会做一次移动,而在 Vue3 中,针对相对顺序相同的节点采用了 O(logn) 最长递增子序列算法(贪心 + 二分 + 前驱节点消除贪心影响),使新旧子节点列表中相对顺序相同的元素无需移动。
Vue3 中还有一个 blockTree 概念,如果是通过模板编译的,会根据节点是否稳定拆分 Block,它是一个拥有 dynmaicChildren 的对象,dynmaicChildren 是一个数组,会收集动态节点(根据 patchFlag),我们最后使用 BlockTree 收集到的数组去做更新,可以认为是从虚拟 dom 树的全量 diff 转为靶向 diff。
请说明 key 的作用和原理
- Vue2 在 patch 过程中通过 vnode 的 tag 和 key 可以判断两个虚拟节点是否是相同节点(可以复用老节点)。
- 无 key 会导致更新的时候出问题。
- 动态列表中,不要使用索引作为 key,因为动态列表标签名是相同的,如果索引做 key,如果列表头新增数据,可能复用了不该复用的节点。
<body>
<div id="app">
<li v-for="(item, index) of arr" :key="index">
<input type="checkbox">
{{ item }}
</li>
<button @click="addFruit">添加</button>
</div>
<script src="../node_modules/vue/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
arr: ['香蕉🍌', '苹果', '橘子🍊']
},
methods: {
addFruit() {
this.arr.unshift('柠檬🍋'); // 实际上 dom 是在尾部追加了一下
}
}
});
</script>
</body>
此时勾选香蕉,点添加后,发现元素复用错了,多选框被复用到了新插入的下标 0 元素上。
解决方法是,将 key 换成唯一值~
谈一谈你对 vue2 组件化的理解
组件化最早出现在 webComponent,浏览器通过它可以实现自定义标签,不过兼容性比较差。
组件化的好处有几点:
- 组件级更新,特别是对于 Vue 组件,单独分一个 watcher 来处理组件更新,减小影响范围。
- 合理规划、拆分代码。
- 复用性强。
- 单向数据流,子组件不能直接修改父组件的属性,不然数据混乱了。
组件的三大特性:
- 属性,父组件可以给子组件传递属性。
- 自定义事件,子组件可以通过自定义事件调用父组件的方法。
- 插槽。
可以继续延伸一下,谈谈封装过什么组件,怎样封装组件的,如何基于原有组件进行二次开发。
Vue2 组件渲染流程
首先整体的页面模板(包含组件标签)会经历以下过程,页面 dom AST -> render -> vnode,在生成 vnode 前,我们在 createElment 方法中给组件的节点的 props 增加 props.hook.init 方法,并且执行 installComponentHooks 给组件创建 hooks(init,prepatch,insert,destroy),然后才生成组件的 vnode,在进行 patch 方法时,又会在创建真实节点的方法 createElm 中为组件节点的 vnode 调用其 vnode.props.hook.init,该方法会 new Sub 生成一个组件实例挂载到 vnode.componentInstance 上,实例化过程中调用了组件实例继承来的 _init 方法进行组件的数据劫持,生命周期,watcher 初始化等,最后调用组件继承来的 $mount 方法进行挂载(没有传递 el),返回组件实例上的真实 dom(vnode.componentInstance.el)。
上面标黑体的流程就是组件的渲染流程,vnode.componentInstance.el = patch(this.$el, vnode),到这里组件初始化和挂载流程结束,组件实例的 el 属性上保存着组件真实 dom,这行代码执行完毕后,上述标红的地方就能拿到组件真实 dom 返回插入到页面。

网上找到的图,但还是觉得我画的更好 =。=
Vue2 组件更新流程
首先,触发组件更新的两个因素是:
- 组件本身 data 发生改变,这个没什么要说的,触发组件自己的 watcher.update -> watcher.run -> watcher.get(compVm._patch(compVm.render()))
- 组件接收的 props 发生了改变(父组件传入的值更新了),我们来重点说下这个
<my :a= "a"></my>
<script>
Vue.component('my', {
props: {
a: {
type: Number
},
render(h) {
return h('div', this.a);
}
}
});
let vm = new Vue({
el: '#app',
data() {
return { a: 1 }
}
});
setTimeout(() => {
vm.a = 100;
}, 1000);
</script>
属性更新时会触发 patchVnode 方法 -> 组件虚拟节点会调用 prepatch 钩子 -> 更新属性 -> 组件更新,更具体流程如下。
至于为什么 vm.$options.propsData 是响应式数据,为什么它改变后组件会更新,我们接下来会说,往后看!
Vue 中异步组件原理
<my></my>
<script>
Vue.component('my', function(resolve) {
setTimeout(() => {
resolve({ // 这个对象会进行 Vue.extend() 转成子类 Sub
render(h) {
return h('div', 'my')
}
});
}, 1000)
});
let vm = new Vue({
el: '#app'
});
</script>
1s 后,页面会渲染出组件,异步组件的使用场景很多,比如路由异步加载组件(配合魔法注释使用更佳),loading 组件等~
实现原理:

可以说,异步组件的实现核心在于:
- 注册组件时 options 作为一个函数传入,创建虚拟 dom 时,走到 createComponent 方法,会先去解析该函数,并对组件 options(函数) 状态进行判断,比如 error,resolved,loading 等(遇到这些,直接返回对应的组件),然后收集组件实例到 owners 中,最后调用异步函数,等待函数完成。
- 给页面塞一个空标签作为占位符。
- 异步返回结果后,将返回结果对象使用 Vue.extend 生成子类 Sub,并挂在组件的 options.resolved 上,然后调用组件实例的 $forceUpdate 方法强行更新,再次走到 createComponent 方法,对 option(函数) 进行状态机校验,发现 options.resolved 后,取出其存储的中的子类 Sub,接下来走正常的组件创建流程,重新生成组件真实 dom 插入页面。
函数式组件的优势及原理
<my></my>
<script>
Vue.component('my', { // 不用去调用组件的 _init,只需要渲染流程即可
functional: true, // 函数式组件
render(h) {
return h('div', 'hello 函数式组件');
}
});
let vm = new Vue({
el: '#app'
});
原理:createComponent 方法执行的时候,会判断组件选项中 functional 字段,如果为 true,跳过初始化组件,生成 Sub 类,installComponents 等操作,包括 _init(所以函数式组件没有 watcher),直接进行初始化上下文,生成 Vnode,patch 操作,所以性能很高。
函数式组件没有自己的状态和 watcher,所以他不会主动更新,除非父级更新,重新生成函数式组件,比如父级传来一个 props 列表数据,函数式组件的方式去渲染列表页面。
优点:
- 性能好,没有自己的数据源,可以接收 props,无声明周期,无 this,无副作用,一般用于单纯的页面渲染(正常组件是一个类,但是函数组件就是一个普通的函数)。
缺点:
- 无状态,没有自己的数据源
组件间的传值方式及之间的区别
- props 和 emit 触发事件来做到的
- children 获取当前组件的父组件和当前组件的子组件
- listeners,Vue 2.4 开始提供了 listeners 来解决这个问题
- 父组件中通过 provide 来提供变量,然后在子孙组件中通过 inject 来注入变量(会导致数据不明确,不建议使用,一般开发组件库才用)。
- $refs 父拿子的实例
- envetBus 平级组件数据传递,这种情况下可以使用中央事件总线的方式
- vuex 状态管理
props 和 $emit 原理
<div id="app">
<my a=1 b=2 c=3 @toChildFn="fn"></my>
</div>
<script src="../dist/vue.js"></script>
<script>
Vue.component('my', { // 不用去调用组件的 _init,只需要渲染流程即可
props: {
a: {
type: String
},
b: {
type: String
}
},
render(h) {
return h('div', 'hello');
}
});
let vm = new Vue({
el: '#app',
methods: {
fn() {
}
},
});
</script>
@1 创建组件 vnode 时,调用了 createComponent,会抽离 props,将组件传入的属性(a、b、c)和组件接收的属性(a 和 b 两个)进行比对,其中组件接收的属性挂到 attrs 上,然后把组件的属性和事件挂载到 vnode.componentOptions 上。
// 抽离出的组件 props
const propsData = extractPropsFromVNodeData(data, Ctor, tag);
// 给组件传递的事件 { 'on': { tochildfn: ƒ () }}
const listeners = data.on
...
// 创建虚拟节点时,把组件属性挂到 vnode.componentOptions.propsData 上
// 组件接收的事件放到 vnode.componentOptions.listeners 上
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
@2 组件调用 Vue.prototype._init 时,会走到 initInternalComponent 方法进行组件的属性,事件,插槽,render 等属性的收集,这里我们只关心属性和事件,会将组件的属性挂载到组件实例的 vm.options._parentListeners 上,跳出该函数。
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
// 将组件属性挂载到组件实例 vm.$options.propsData 上
// 这里其实就是 { a: 1, b: 2 }
opts.propsData = vnodeComponentOptions.propsData
// 组件上绑定的事件,{ tochildfn: ƒ () }, 挂载到 m.$options._parentListeners 上
opts._parentListeners = vnodeComponentOptions.listeners
// 处理插槽
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
@3 开始执行 _init 中的 initEvents -> updateComponentListeners -> updateListeners,进行组件的事件绑定,会在子组件实例上收集一个 _events 事件列表(子组件的 _events 上收集的是父组件的方法),提供了 $emit 去触发组件上收集到的事件(触发父组件的方法执行),其实就是个发布订阅。
太长了被折起来了
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// 我们所以的事件选项,本例中只有一个事件 { tochildfn: ƒ () }
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
// 介不是发布订阅么
function add (event, fn) {
target.$on(event, fn)
}
// 介不是发布订阅么
function remove (event, fn) {
target.$off(event, fn)
}
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
// 把当前的事件挂载到组件实例上 vm._evnets = [tochildfn]
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
// 订阅
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// vm._events 挂载着注册的事件
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
// 发布
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
// $emit 触发事件执行
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
@4 开始执行 _init 中的 initState,然后调用了 initProps,该方法会判断 vm.$options.propsData 组件接受到的属性的值是否合法,如果合法,就将值定义到 vm._props 上,最后对 vm 取值做了一层代理,取的是 vm._props 上的值哟。
太长了被折起来了
export function initState (vm: Component) {
// 给实例上增加一个属性 _watchers 存放当前实例对应的 watcher 用于强制更新使用
vm._watchers = [];
const opts = vm.$options
// 初始化有顺序要求, 是vue自己定义的顺序
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm); // 数据的初始化
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
// 最后 props 上的属性会被定义在 vm._props 中
const props = vm._props = {}
...
// propsOptions 就是 { a: { type: String }, b: { type: String } }
for (const key in propsOptions) {
// 这部分代码过长,限于篇幅这里简要描述下,这里主要拿了组件注册的属性(a 和 b)和传入的属性做了比对
if (代码不符合规则) {
// balabala
} else {
// 符合规则的属性被定义在 vm._props 上
defineReactive(props, key, value)
}
if (!(key in vm)) {
// 如果在 vm 上取值,取的是 vm._props,做了层代理
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
parent & children
这个比较简单,就是说父亲记住儿子的实例,儿子记住父亲的实例而已,我们来看下组件初始化时候调用的 initLifecycle 方法。
export function initLifecycle (vm: Component) {
const options = vm.$options
// 组件初始化的时候会将父组件的实例传递过来,在 options 中哦
let parent = options.parent // 获取父组件实例
// 先不要在意抽象组件,后面 keep-alive 的时候再说~
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm) // 让父亲记录儿子
}
vm.$parent = parent // 让儿子记录父亲
}
inject provide 如何实现跨级通信
跨级通信指的是,父亲可以和孙子,曾孙,玄孙直接通信,其实 provide 就是把数据定义在一个变量上,子代实例可以一层层网上找.
Vue.component('my', {
inject: ['foo'],
render(h) {
return h('div', `hello 组件`);
},
created () {
console.log(this.foo) // => "foo 的值"
}
});
let vm = new Vue({
provide: {
foo: 'foo 的值'
},
el: '#app',
});
通过组件初始化中的 initProvide 方法,找到 inject.js 中的源码,可以看到,它的实现很简单:
- provide 会在当前实例上保存一个 _provided 方法
- 对组件进行 inject 时, 会调用 resolveInject 去解析值,解析的过程就是通过 vm.$parent 不停的往上层实例上寻找 _provide 属性且看寻找到的对象中有没有注入的属性,没有就一直往上找,找到之后,把 result 返回,子组件再将属性和值挂载到自己实例上。
注意,provide 提供的数据,默认不是响应式的,除非提供的就是响应式数据。
太长了被折起来了
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
// 获取用户定义的 provide,provide 可以是函数或对象
// 是函数会取返回值
// 最好挂载到 vm._provided 上
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
export function initInjections (vm: Component) {
// 去当前实例上解析 inject,获取到父亲定义的数据
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false) // 暂时关闭响应式
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
// 定义到自己身上,这里不会去代理对象,因为开关关啦
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
let source = vm
while (source) {
// 去当前组件上不停的去找父亲有没有 _provide 且有没有我需要的属性,没有就一直往上找
// 有点类似 instance 的实现哈哈
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
// 没找到
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}
listeners
获取所有的事件和组件未接收的属性,直接定义在实例上即可,注意,它和 provide、inject 不能一概而论,listeners 不能跨层通信。
组件 _init 时候调用了 initRender 方法,用来初始化 vm._c、listeners。
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
const parentData = parentVnode && parentVnode.data
if (process.env.NODE_ENV !== 'production') {
// 定义 vm.$attrs
defineReactive(
vm,
'$attrs',
parentData && parentData.attrs || emptyObject,
() => { !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)},
true
)
// 定义 vm.$listeners
defineReactive(
vm,
'$listeners',
options._parentListeners || emptyObject,
() => { !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)},
true
)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
refs
给组件添加 refs 之后,可以获取组件实例,怎么实现的呢。
我们看下注册 ref 时做了什么,ref.js 中 registerRef 方法,可以看到,对于数组,refs 收集到的实际上是数组的实例。
export function registerRef (vnode: VNodeWithData, isRemoval: ?boolean) {
const key = vnode.data.ref
if (!isDef(key)) return
const vm = vnode.context
// 如果有实例就是实例,没有实例就是 dom 节点
// 也就是说,如果为组件,refs 保存的 ref 就是组件的实例
const ref = vnode.componentInstance || vnode.elm
const refs = vm.$refs
if (isRemoval) {
if (Array.isArray(refs[key])) {
remove(refs[key], ref)
} else if (refs[key] === ref) {
refs[key] = undefined
}
} else {
if (vnode.data.refInFor) { // v-for 中的 ref,循环 push
if (!Array.isArray(refs[key])) {
refs[key] = [ref]
} else if (refs[key].indexOf(ref) < 0) {
// $flow-disable-line
refs[key].push(ref)
}
} else {
// refs[key] = 组件实例
refs[key] = ref
}
}
}
v-if 和 v-for 哪个优先级更高?
v-for 和 v-if 不要在同一个标签中使用(可以写但是有警告), 因为解析时先解析 v-for 再解析 v-if。
我们使用 vue-template-explore 来看下~
可以看出
<div>
<li v-for="item in list" v-if="true"></li>
</div>
编译出来是这样的
function render() {
with(this) {
// _l 即为 v-for
return _c('div', _l((list), function (item) {
// 会拿 list 数据循环,最后渲染的时候判断是否要展示 li 标签,v-for 先执行
return (true) ? _c('li') : _e()
}), 0)
}
}
所以,针对这样的场景,我们可以手动加一个 template 标签包裹
<div>
<template v-if="true">
<li v-for="item in list"></li>
</template>
</div>
源码在 compiler/codegen/index.js
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // 先处理的是 v-for
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // 再处理的是 v-if
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// ...
}
v-for,v-if,v-model 的实现原理
其实,v-medel 算是指令,而 v-for 和 v-if,并不会编译出 directive 来,在生成代码的时候就将这两个东西进行了转义。
v-for
v-for 实现原理其实就是拼接一个循环函数,内部用了一个方法 _l
src/compiler/codegen/index.js:187
export function genFor(
el: any,
state: CodegenState,
altGen ? : Function,
altHelper ? : string
): string {
const exp = el.for // 拿到表达式arr
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) && // slot 和 template不能进行v-for操作
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 // avoid recursion 生成循环函数
const r = `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
return r;
}
v-if
v-if 自动会被转义成三元表达式。
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 `(${condition.exp})?${ // 将表达式拼接起来
genTernaryExp(condition.block)
}:${ // v-else-if
genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
return `${genTernaryExp(condition.block)}` // 没有表达式直接生成元素 像v-else
}
// 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-model
组件上的 v-model
在以往,我们通常传给子组件一个数据,子组件如果想修改,还需要 $emit 调用父组件传递过来的方法修改数据,这样很麻烦。
<body>
<div id="app">
<my :name="name" @input="changeName"></my>
</div>
<script src="../dist/vue.js"></script>
<script>
Vue.component('my',{
props: {
name: String
},
template:`<div><button @click="$emit('input', 'ys222')">{{ name }}</button></div> `
})
new Vue({
el: '#app',
data:{
name: 'ys'
},
methods:{
changeName(newVal){
this.name = newVal
}
}
})
</script>
</body>
我们可以使用 v-model 来改写它
<body>
<div id="app">
<my v-model="name"></my>
{{ name }}
</div>
<script src="../dist/vue.js"></script>
<script>
Vue.component('my',{
props: {
value: String // 默认传递的属性名为 value
},
template:`<div>{{value}}<button @click="$emit('input', 'ys222')">更改姓名</button></div> `
})
new Vue({
el: '#app',
data:{
name: 'ys'
}
})
</script>
</body>
可以看出,实际上 v-model 只是一个 :value + @input 的语法糖,所以我们需要 $emit('input', 'xxx') 来去触发,真正的实现是这样的:
<my type="text" :value="name" @input="name=$event.target.value"></my>
可以通过 model 选项更改子组件接收的属性名和传递的事件名~
<body>
<div id="app">
<my v-model="name"></my>
{{ name }}
</div>
<script src="../dist/vue.js"></script>
<script>
Vue.component('my', {
model: {
prop: 'aaa', // 接收的属性名更改
event: 'myEvent' // 接收的事件名
},
props: {
aaa: String // 这里也要改哦
},
template: `<div>{{ aaa }}<button @click="$emit('myEvent', 'ys222')">更改姓名</button></div> `
})
new Vue({
el: '#app',
data: {
name: 'ys'
}
})
</script>
</body>
当模板解析到 v-model 时,会执行 transformModel 方法
function transformModel (options, data: any) {
// 是否子组件配置了 model.props 选项,默认接收属性名是 value
const prop = (options.model && options.model.prop) || 'value'
// 是否子组件配置了 model.event 选项,默认接收属性名是 input
const event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {
if (
Array.isArray(existing)
? existing.indexOf(callback) === -1
: existing !== callback
) {
on[event] = [callback].concat(existing)
}
} else {
on[event] = callback // 给元素绑定了个事件,事件名默认是 input
}
}
表单元素上的 v-model
表单元素上的 v-model 和组件是有一些差异的,如下所示,这是 v-model 在输入框绑定的效果
{{ name }}
<input type="text" v-model="name">
而对于 :value + @input 而言
{{ name }}
<input :value="name" @input="e => name=e.target.value" />
所以,可以看出,v-model 用在表单元素和用在组件上是不同的,一定是做过特殊处理的~
我们看下 v-model 编译后的结果
function render() {
with(this) {
return _c('input', {
directives: [{ // 这里多了个指令属性
name: "model",
rawName: "v-model",
value: (name),
expression: "name"
}],
attrs: {
"type": "text"
},
domProps: {
"value": (name)
},
on: {
"input": function ($event) {
if ($event.target.composing) return;
name = $event.target.value
}
}
})
}
}
所以 v-model 用在表单元素上,会被解析成一个指令(在编译的时候会将 v-model 解析成一个指令),默认会给 input 事件拼接一个处理中文输入法的问题,在运行的时候需要调用指令(会对不同的类型做不同的处理),指令执行的时候还会去处理修饰符 v-model.xxxx。
const directive = { // 运行时 会调用此方法
inserted (el, binding, vnode, oldVnode) {
if (vnode.tag === 'select') {
// #6903
if (oldVnode.elm && !oldVnode.elm._vOptions) {
mergeVNodeHook(vnode, 'postpatch', () => {
directive.componentUpdated(el, binding, vnode)
})
} else {
setSelected(el, binding, vnode.context)
}
el._vOptions = [].map.call(el.options, getValue)
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers
if (!binding.modifiers.lazy) {
el.addEventListener('compositionstart', onCompositionStart)
el.addEventListener('compositionend', onCompositionEnd)
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd)
/* istanbul ignore if */
if (isIE9) {
el.vmodel = true
}
}
}
}
Vue中slot是如何实现的?什么时候使用它?
slot 分为三种:
- 具名插槽
- 普通插槽(在父组件中渲染 vnode,只能用父组件的数据,渲染后传递给儿子)
- 作用域插槽(在子组件中渲染 vnode,可以使用子组件的数据来进行渲染,比如表格组件,slot-scope={ row })
具名插槽
// base-layout 组件
<div class="container">
<header>
<!-- 我这里接收 v-slot:header 的模板 -->
<slot name="header"></slot>
</header>
<main>
<!-- 我这里接收没有具名的模板 -->
<slot></slot>
</main>
<footer>
<!-- 我这里接收 v-slot:footer 的模板 -->
<slot name="footer"></slot>
</footer>
</div>
使用组件和插槽
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
普通插槽
<body>
<div id="app">
<my>
<div>{{ name }}</div>
<div slot="title">{{ name }}</div>
<div slot="content">{{ name }}</div>
<!-- 这里整个会被解析为插槽的属性 slots = { default: vNode1, title: vNode2, content: vNode3 } -->
</my>
</div>
<script src="../dist/vue.js"></script>
<script>
Vue.component('my', {
template: `<div><slot></slot></div>`
// _c('div', [_t("default"), _t("title"), _t("content")], 2)
// 可以看到,依次去渲染子节点
})
new Vue({
el: '#app',
data: {
name: 'ys'
}
})
</script>
</body>
core/instance/init.js
export function initInternalComponent (vm: Component, options) {
const opts = vm.$options = Object.create(vm.constructor.options)
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children // 插槽
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
继续执行 core/instance/render.js 中的 initRender 方法
// 解析插槽
vm.$slots = resolveSlots(options._renderChildren, renderContext)
看下 core/instance/render-helpers/resolve-slots.js 中的 resolveSlots 方法
// 东西很多,关注这一句即可,如果有 child,就给插槽的 default 属性构建为一个数组
// 比如 slots = { default: [子标签虚拟节点] }
(slots.default || (slots.default = [])).push(child)
作用域插槽
渲染的作用域取自组件内部属性~
<body>
<div id="app">
<my>
<div slot-scope="{ a }">{{ a }}</div>
<!--解析后为 function ({ a }) { return _c('div', {}, [_v(_s(a))])} -->
<!-- 没有去渲染,而是返回了一个函数,接收一个参数 a,return 一个 _c 函数 -->
</my>
</div>
<script src="../dist/vue.js"></script>
<script>
Vue.component('my', {
template: `<div><slot a=1></slot></div> `
// _c('div', [_t("default", null, {
// "a": "1"
// })], 2)
// 可以看到,在子组件中渲染的时候调用了上面那个函数
// 并将数据传递给刚才解析后的函数进行渲染
})
new Vue({
el: '#app',
data: {
name: 'ys'
}
})
</script>
</body>
Vue.use 是干什么的?原理是什么?
- Vue.use是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等。
- 维护了 installedPlugins 缓存加载过的插件列表,如果加载过,直接返回 Vue
- 会调用插件的 install 方法,将 Vue 的构造函数跟添加到 install 原有参数列表传入,并执行 install 方法。
- 如果 install 不是一个函数,看插件本身是不是一个函数,是的话,直接执行插件~
src/core/global-api/use.js
// 接收函数或者对象
Vue.use = function (plugin: Function | Object) {
// 插件缓存
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) { // 如果已经安装过插件,直接返回
return this
}
const args = toArray(arguments, 1) // 除了第一项其他的参数整合成数组
args.unshift(this) // 将 Vue 放入参数 arguments 首位
if (typeof plugin.install === 'function') {
// 如果插件的 install 为函数,调用 install 方法
// 传入 plugin 和 args「args第一项是 Vue」
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
// 插件就是函数,调用方法,传入 args「args第一项是 Vue」
plugin.apply(null, args)
}
installedPlugins.push(plugin) // 缓存插件
return this
}
那么为什么需要有 install 方法呢,提供插件直接用不爽么?其实这是为了解除插件本身和 Vue 版本的强依赖,插件不去规定死 Vue 版本,而采用用户的 Vue,去进行插件的初始化,挂载,防止了版本不同所造成的麻烦。
const customPlugin = {
install(this, a, b, c) {
// this 需要你传给我,我不约束 Vue 版本
}
}
Vue.use(customPlugin) => customPlugin.install(Vue, a, b, c)
组件中写name选项有哪些好处及作用?
- 增加 name 选项会在 components 属性中增加组件本身,实现组件的递归调用(递归组件,比如多层树型组件,核心思想是递归调用某个组件,而这个组件的作用就是解析出此层的数据。)。
- 可以标识组件的具体名称方便调试和查找对应组件,比如 keep-alive 可搭配组件 name 进行缓存过滤。
src/core/global-api/extend.js:67
Sub.options.components[name] = Sub; // 组件配置生成的组件构造函数是挂载 components.name 上的
Vue 事件修饰符有哪些?其实现原理是什么?
.stop、.prevent、.capture!、.self、.once~、.passive&
- 组件在编译的时候会对一些修饰符做处理(根据不同的修饰符,生成不同的代码)
- 真正运行的时候,也需要去处理一些修饰符
src\compiler\helpers.js:69
code
export function addHandler ( // 针对 AST 解析的时候,会调用此方法
el: ASTElement,
name: string,
value: string,
modifiers: ?ASTModifiers,
important?: boolean,
warn?: ?Function,
range?: Range,
dynamic?: boolean
) {
modifiers = modifiers || emptyObject
// warn prevent and passive modifier
/* istanbul ignore if */
if (
process.env.NODE_ENV !== 'production' && warn &&
modifiers.prevent && modifiers.passive
) {
warn(
'passive and prevent can\'t be used together. ' +
'Passive handler can\'t prevent default event.',
range
)
}
if (modifiers.right) { // 鼠标右键
if (dynamic) {
name = `(${name})==='click'?'contextmenu':(${name})`
} else if (name === 'click') {
name = 'contextmenu'
delete modifiers.right
}
} else if (modifiers.middle) { // 鼠标中间
if (dynamic) {
name = `(${name})==='click'?'mouseup':(${name})`
} else if (name === 'click') {
name = 'mouseup'
}
}
// 精彩来啦
if (modifiers.capture) { // 如果 capture 用 ! 标记 比如,@click.capture 解析为 !click
delete modifiers.capture
name = prependModifierMarker('!', name, dynamic)
}
if (modifiers.once) { // 如果是once 用~ 标记
delete modifiers.once
name = prependModifierMarker('~', name, dynamic)
}
/* istanbul ignore if */
if (modifiers.passive) { // 如果是passive 用 &标记
delete modifiers.passive
name = prependModifierMarker('&', name, dynamic)
}
let events
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}
const handlers = events[name]
/* istanbul ignore if */
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
src\compiler\codegen\events.js:42
code
function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
let code = ''
let genModifierCode = ''
const keys = []
for (const key in handler.modifiers) {
if (modifierCode[key]) {
genModifierCode += modifierCode[key]
// left/right
if (keyCodes[key]) {
keys.push(key)
}
} else if (key === 'exact') {
const modifiers: ASTModifiers = (handler.modifiers: any)
genModifierCode += genGuard(
['ctrl', 'shift', 'alt', 'meta']
.filter(keyModifier => !modifiers[keyModifier])
.map(keyModifier => `$event.${keyModifier}Key`)
.join('||')
)
} else {
keys.push(key) // modifiers中表达式存起来
}
}
if (keys.length) {
code += genKeyFilter(keys)
}
// Make sure modifiers like prevent and stop get executed after key filtering
if (genModifierCode) {
code += genModifierCode
}
const handlerCode = isMethodPath
? `return ${handler.value}.apply(null, arguments)`
: isFunctionExpression
? `return (${handler.value}).apply(null, arguments)`
: isFunctionInvocation
? `return ${handler.value}`
: handler.value
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
return genWeexHandler(handler.params, code + handlerCode)
}
return `function($event){${code}${handlerCode}}`
}
vue-dev\src\platforms\web\runtime\modules\events.js:105
code
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, def, cur, old, event
for (name in on) { // 循环on中的 即事件
def = cur = on[name]
old = oldOn[name]
event = normalizeEvent(name) // 事件修饰符
/* istanbul ignore if */
if (__WEEX__ && isPlainObject(def)) {
cur = def.handler
event.params = def.params
}
if (isUndef(cur)) {
process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm)
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
add(event.name, cur, event.capture, event.passive, event.params)
} else if (cur !== old) {
old.fns = cur
on[name] = old
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
Vue 中 .sync 修饰符的作用,用法
.sync 在 vue3 中被删除掉了,一个类似于 v-model 的用于组件传递 prop 的语法糖,可以实现"双向绑定",它比之组件 v-model 有以下优点:
- v-model 默认传递的属性名和事件名叫 value 和 input,除非用户改写,.sync 没有这个限制
- v-model 不能传递和更新多个属性,.sync 可以
<!-- 旧写法 -->
<text-document
v-bind:title="a"
v-on:update:title="a = $event"
></text-document>
<!-- .sync 方便写法 .sync 修饰符的 v-bind 不能和表达式一起使用 -->
<text-document v-bind:title.sync="a" v-bind:name.sync="b"></text-document>
<!-- 组件内触发更新 -->
this.$emit('update:title', newTitle)
this.$emit('update:name', newTitle)
那么为什么要废弃掉它呢?因为 vue3 中 v-model 也提供了同样的写法:
<text-document v-model:title="a" v-model:name="b"></text-document>
如何理解自定义指令
- 1.在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性 "{ directive: "v-for", name: for}"
- 2.通过 genDirectives 生成指令代码
directives: [{
name: "model",
rawName: "v-model",
value: (xxx),
expression: "xxx"
}]
- 3.在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子
- 4.当执行 cbs 对应的钩子时,调用对应指令定义的方法 (create/update/destroy) , 调用用户自定义指令的钩子函数 (inserted,bind,unbind, componentUpdate)
最后实际调用的方法,需要仔细调试下~
src/core/vdom/modules/directives.js
function _update (oldVnode, vnode) {
const isCreate = oldVnode === emptyNode
const isDestroy = vnode === emptyNode
// 拿到老的指令和新的指令
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)
const dirsWithInsert = []
const dirsWithPostpatch = []
let key, oldDir, dir
for (key in newDirs) {
oldDir = oldDirs[key]
dir = newDirs[key]
if (!oldDir) { // 如果没有老的指令 调用bind方法 Vue.directive({bind,inserte,compontentUpdate})
// new directive, bind
callHook(dir, 'bind', vnode, oldVnode)
if (dir.def && dir.def.inserted) { // 先存起来inserted 要等待组件被插入到页面后再 调用方法
dirsWithInsert.push(dir)
}
} else {
// existing directive, update
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg // 如果老的新的都有就调用更新的钩子
callHook(dir, 'update', vnode, oldVnode)
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir) // 将更新流程放到队列中
}
}
}
if (dirsWithInsert.length) {
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
if (isCreate) { // 将vnode插入的操作 和 insert操作合并在一起
mergeVNodeHook(vnode, 'insert', callInsert)
} else {
callInsert()
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch', () => {
for (let i = 0; i < dirsWithPostpatch.length; i++) {
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
}
})
}
if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
}
}
}
}
keep-alive平时在哪里使用?原理是?
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
<router-view></router-view>
</keep-alive>
keep-alive 缓存的是什么呢?它缓存了组件的实例,而组件的实例上就有组件的 dom(vm.el 结果~
keep-alive 不用做任何渲染操作,内部使用了一个 LRU 算法来管理缓存(抽象组件,不会产生父子关系,但是有缓存)。
LRU:最近最久未使用法(最近使用的要提前,最久未使用的优先丢弃)
let cachedList = [a, b, c];
maxCache = 3;
// 最久未使用丢弃
arr.push(d); // a 移除,d 插入
// 如果插入元素在缓存中存在,则把缓存中该元素移动到最新的位置
arr.push(b) // 第二项的 b,移动到数组末尾
keep-alive 实现
src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 说明是一个抽象组件,不会被记录在 $parent $children 中
props: {
include: patternTypes, // 白名单
exclude: patternTypes, // 黑名单 动态操作黑白名单可以切换缓存 v-if 来切换
max: [String, Number] // 最大的缓存个数
},
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this // 取出要缓存的节点
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
cache[keyToCache] = { // 缓存的就是实例
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache) // [1]
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
// 超过限制,把第 0 个删掉,最新的放进来,最近最久未使用法的思想
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
created () {
this.cache = Object.create(null) // 记录缓存的列表
this.keys = [] // 记录所有要缓存组件的名字
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () { // mounted要等待页面渲染完毕后调用,先往下看 render!!
this.cacheVNode()
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
updated () {
this.cacheVNode()
},
render () {
const slot = this.$slots.default // 拿到默认插槽
const vnode: VNode = getFirstComponentChild(slot) // 获取第一个组件的虚拟节点 keep-alive只能缓存一个组件
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) { // Ctor,props,children,slos,name 都在里面哦
// check pattern
const name: ?string = getComponentName(componentOptions) // componentOptions.name
const { include, exclude } = this
if ( // 获取组件名 看是否需要缓存,不需要缓存则直接返回
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
// 不缓存 直接返回 vnode,原样返回
return vnode
}
const { cache, keys } = this // cache 缓存列表,keys 缓存的 key 的数组
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key // 计算一个缓存的key值,组件有 key 直接用,组件没 key,使用 cid + 标签名作为 key
if (cache[key]) { // 缓存过,复用组件实例
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key) // 先把自己删掉
keys.push(key) // 插到最后面
} else { // 没有缓存过
// delay setting the cache until update
this.vnodeToCache = vnode // 记录需要缓存的组件 vnode
this.keyToCache = key // 记录需要缓存的 key
}
// 为了后续在组件创造的过程中 init 方法判断是不是 keep-alive 缓存过的组件
// 如果缓存过就不再次 new Ctor
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0]) // 返回虚拟节点
// a b页面都缓存了
// a->b 页面他是如何更新的
// <keep-alive><router-view></router-view></keep-alive>
}
}
切换路由,keep-alive 缓存的页面怎么更新
比如 a b 页面都被缓存了,那么当我从 a 切换到 b 页面时,为什么组件的更换会更新页面呢?页面明明都是一层 keep-alive 套着 router-view,原因是 router-view 里面有个 slot 标签。
keep-alive 中组件切换的时候,(router-view 内部的)插槽会触发更新,如果插槽的内容变化了,会强制重新渲染 $forceupdate, 重新进行渲染。
activated 和 deactiveted
还有个点就是,当 keep-alive 的组件被激活,会调用 activated 钩子,同样的还有 keep-alive 的组件被销毁时会调用 deactiveted 钩子。