vue源码分析【6】-vue 指令

740 阅读6分钟

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方法

image.png

image.png

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属性的节点进行替换操作。(插槽的作用域为父组件)

image.png

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 把它插入文档中。
    • 这个方法返回实例自身,因而可以链式调用其它实例方法。
  • vm.$forceUpdate(): 迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

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

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

    • 触发 beforeDestroy 和 destroyed 的钩子。