vue源码分析【6】-vue 指令
以下代码和分析过程需要结合vue.js源码查看,通过打断点逐一比对。
模板代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="./../../oldVue.js"></script>
</head>
<body>
<div id="app">
<h2>开始存钱</h2>
<div>每月存 :¥{{ money }}</div>
<div>存:{{ num }}个月</div>
<div>总共存款: ¥{{ total }}</div>
<button @click="getMoreMoney">{{arryList[0].name}}多存一点</button>
<msg-tip :msginfo='msgText' :totalnum='total'></msg-tip>
</div>
<script>
debugger;
// 定义一个新组件
var a = {
props:['msginfo', 'totalnum'],
data: function () {
return {
count: 0
}
},
template: '<div>{{ msginfo }}存了¥{{ totalnum }}</div>'
}
var app = new Vue({
el: '#app',
components: { msgTip: a},
beforeCreate() { },
created() { },
beforeMount() { },
mounted: () => { },
beforeUpdate() { },
updated() { },
beforeDestroy() { },
destroyed() { },
data: function () {
return {
money: 100,
num: 12,
arryList: [{name:'子树'}],
msgText: "优秀的乃古:"
}
},
computed: {
total() {
return this.money * this.num;
}
},
watch:{
money:{
handler(newVal, oldVal){
this.msgText = newVal+this.msgText
},
deep:true,
immediate:true
}
},
methods: {
getMoreMoney() {
this.money = this.money * 2
this.arryList.unshift({name: '大树'})
}
}
})
</script>
</body>
</html>
前言
本文的结构依据点,线,面来展开。
- 点即函数的作用
- 线即函数的执行流程
- 面即源码的详细解读
十分不建议直接看源码,很多函数非常长,并且链路很长,在没有对函数有大概的了解情况,大概率下,你读了一遍源码后会发现,wc 我刚看了源码了吗?可是咋记不清它们做了啥操作。因此,先看作用,再看流程,再展开看源码。
源码提问
1. vue中v-if和v-show的区别
v-if如果条件不成立,不会渲染当前指令所在节点的dom元素
满足条件时会创建DOM元素,不满足时会通过_e()创建空节点。
解析模板解析到<div v-if='ishow'></div>,这个DOM元素时会执行到下面这里:
// 如果有v-if指令属性,例:ishow
if (condition.exp) {
/**
* 最终表达式:"[(ishow)?_c('div'):_e()]"
*/
return ("(" + (condition.exp) + ")?" + (genTernaryExp(condition.block)) + ":" +
(genIfConditions(conditions, state, altGen, altEmpty)))
}
v-show切换当前dom的显示和隐藏,本质上display:none
var show = {
bind: function bind(el, ref, vnode) {
....
var value = ref.value;
// 节点上原始的display属性
var originalDisplay = el.__vOriginalDisplay =
el.style.display === 'none' ? '' : el.style.display;
/**
如果v-show的值为true,那么使用原来节点的display的值。
需要注意的是,如果原始display已经是none了,需要重置为空(也就是说
当同时使用display:none和v-show=true时,还是会显示节点。)
如果v-show的值为false,那么就设置display: 'none'
*/
el.style.display = value ? originalDisplay : 'none';
....
}
}
2. 为什么v-for和v-if不能连用?
v-for会比v-if的优先级高一些,如果连用的话,如果同时出现,每次渲染都会先执行循环再判断条件,会造成性能问题。
如果确实需要判断每一个,可以用计算属性来解决,先用计算属性将满足条件的过滤出来,然后再去循环。
// 从下面可以看出,v-for的优先于v-if执行
function genElement(el, state ) {
if (el.staticRoot && !el.staticProcessed) {
} else if (el.once && !el.onceProcessed) {
} else if (el.for && !el.forProcessed) {
// v-for
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { //判断标签是否有if属性
// v-if
return genIf(el, state)
}
}
3. Vue中事件绑定的原理
Vue的事件绑定分为两种:一种是原生的事件绑定,一种是组件的事件绑定
原生dom事件绑定采用的是addEventListener
组件的事件绑定采用的是$on方法
4. v-model的实现原理?
v-model可以看成是value+input方法的语法糖
<input type="text" v-model="username">
等价于:
<input type="text" :value="username" @input="username=$event.target.value">
源码:
function createComponent(
Ctor, //VueComponen函数
data, // 组件标签上面的属性数据
context, //vm Vue 实例化之后的对象上下文
children, //子节点
tag) {
...
if (isDef(data.model)) { //如果定义有 model 转义 model 并且绑定 v-model
transformModel(Ctor.options, data);
}
}
// 没有传递model会默认使用value和input代替,需要重新定义v-model,手动传入model
function transformModel(options, data) {
//获取prop 如果获取不到 则取值 value
var prop = (options.model && options.model.prop) || 'value';
//获取event如果获取不到 则取值 input
var event = (options.model && options.model.event) || 'input';
//把data.model.value的值赋值到data.props.value 中
(data.props || (data.props = {}))[prop] = data.model.value;
var on = data.on || (data.on = {});
if (isDef(on[event])) { //如果model 事件已经定义了则是和钩子函数合并
on[event] = [data.model.callback].concat(on[event]);
} else {
on[event] = data.model.callback; //只赋值钩子函数
}
}
另外,根据input的类型,它的转换也是不一样的
function model(
el, //虚拟dom
dir, // v-model 属性的key和值
_warn //警告日志函数
) {
if (el.component) {
genComponentModel(el, value, modifiers);
} else if (tag === 'select') {
genSelect(el, value, modifiers);
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers);
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers);
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers);
} else if (!config.isReservedTag(tag)) {
return false
}
return true
}
5. 如何自定义v-model?
父组件:
<template>
<div class="parent">
<p>son val: {{ChildVal}}</p>
<Child v-model="ChildVal"></Child>
</div>
</template>
<script>
import Child from './Child.vue';
export default {
data() {
return {
ChildVal: 'my son'
};
},
components: {
Child
}
}
</script>
子组件:
<template>
<div class="child">
<p>parent: {{give}}</p>
<a @click="returnBackFn">回应</a>
</div>
</template>
<script>
export default {
props: {
give: String // v-model传进来的值,跟下面保持一致
},
model: {
prop: 'give', // 接收父组件v-model的变量名
event: 'returnBack' // 定义v-model对应的事件
},
methods: {
returnBackFn() {
this.$emit('returnBack', 'l am son');
}
}
}
</script>
6.vue中的v-html会导致哪些问题
- 可能会导致XXS攻击
- v-html会替换掉标签内的子元素
function html (el, dir) {
if (dir.value) {
addProp(el, 'innerHTML', ("_s(" + (dir.value) + ")")); //给el.prop上增加一个innerHTML属性
}
}
function updateDOMProps (oldVnode, vnode) {// 更新DOM对象的props
...
for (key in props) { // key,例:innerHTML
cur = props[key]; // v-html的值
if (key === 'textContent' || key === 'innerHTML') { //这里是对指令v-html和v-text的支持
if (vnode.children) { vnode.children.length = 0; } //如果有子节点,则删除它们
}
if (key === 'value') { //如果key等于value
...
} else {
elm[key] = cur; //否则直接设置elm的key属性值为cur,也就是设置元素的innerHTML或textContent属性
}
}
}
7. Vue.mixin是怎么混入的
//初始化vue mixin 函数
function initMixin$1(Vue) {
Vue.mixin = function (mixin) {
// 合并 对象
this.options = mergeOptions(this.options, mixin);
return this
};
}
function mergeOptions(
parent,
child,
vm
) {
...
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}
8. 插槽和作用域插槽
渲染的作用域不同,普通插槽是父组件,作用域插槽是子组件
插槽
- 创建组件虚拟节点时,会将组件的儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类,{a:[vnode],b:[vnode]}
- 渲染组件时,会拿对应的slot属性的节点进行替换操作。(插槽的作用域为父组件)
9. 谈谈你对keep-alive的理解(一个组件)
keep-alive可以实现组件的缓存,当组件切换时,不会对当前组件卸载
常用的2个属性include、exclude
常用的2个生命周期activated、deactivated
export default {
name: 'keep-alive',
abstract: true,//抽象组件
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null)//创建缓存列表
this.keys = []//创建缓存组件的key列表
},
destroyed () {//keep-alive销毁时,会清空所有的缓存和key
for (const key in this.cache) {//循环销毁
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {//会监控include和exclude属性,进行组件的缓存处理
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
const slot = this.$slots.default//默认拿插槽
const vnode: VNode = getFirstComponentChild(slot)//只缓存第一个组件
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)//取出组件的名字
const { include, exclude } = this
if (//判断是否缓存
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key//如果组件没key,就自己通过组件的标签和key和cid拼接一个key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance//直接拿到组件实例
// make current key freshest
remove(keys, key)//删除当前的[b,c,d,e,a] //LRU最近最久未使用法
keys.push(key)//将key放到后面[b,a]
} else {
cache[key] = vnode//缓存vnode
keys.push(key)//将key存入
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {//缓存的太多,超过了max就需要删除掉
pruneCacheEntry(cache, keys[0], keys, this._vnode)//要删除第0个,但是渲染的就是第0个
}
}
vnode.data.keepAlive = true//标准keep-alive下的组件是一个缓存组件
}
return vnode || (slot && slot[0])//返回当前的虚拟节点
}
}
10. 说一下 vue 中所有带$的方法?
10-1. 实例 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 把它插入文档中。
- 这个方法返回实例自身,因而可以链式调用其它实例方法。
- 如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用
-
vm.$forceUpdate(): 迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。 -
vm.$nextTick( [callback] ): 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。 -
vm.$destroy(): 完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。- 触发 beforeDestroy 和 destroyed 的钩子。