vue中的数据传递

168 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

高频面试题:vue数据传递方式有哪些?那么什么是数据,什么是传递。数据,1{age:30}methods: {fun () {...}}都可以说是数据,传递,可以理解当前组件有调用或获取其他组件中数据的能力,其他组件可以是父子、兄弟、祖孙或者亲戚。

一、prop

使用场景:父子传参

const A = {
  template: `<div @click='emitData'>{{childData}}</div>`,
  props: ['childData'],
  methods: {
    emitData() {
      this.$emit('emitChildData', 'data from child')
    }
  },
}

new Vue({
  el: '#app',
  components: {
    A
  },
  template: `<A :childData='parentData' @emitChildData='getChildData'></A>`,
  data() {
    return {
      parentData: 'data from parent'
    }
  },
  methods: {
    getChildData(v) {
      console.log(v);
    }
  }
})

从当前例子中可以看出,数据父传子是通过:childData='parentData'的方式,数据子传父是通过this.$emit('emitChildData', 'data from child')的方式,然后,父组件通过@emitChildData='getChildData'的方式进行获取。

1、父组件render函数

new Vue中传入的模板template经过遍历生成的render函数如下:

with(this) {
    return _c('A', {
        attrs: {
            "childData": parentData
        },
        on: {
            "emitChildData": getChildData
        }
    })
}

其中data部分有attrson来描述属性和方法。

在通过createComponent创建组件vnode的过程中,会通过const propsData = extractPropsFromVNodeData(data, Ctor, tag)的方式获取props,通过const listeners = data.on的方式获取listeners,最后将其作为参数通过new VNode(options)的方式实例化组件vnode

2、子组件渲染

在通过const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance )创建组件实例的过程中,会执行到组件继承自Vue._init方法,通过initEvents将事件处理后存储到vm._events中,通过initPropschildData赋值到子组件Avm实例上,并进行响应式处理,让其可以通过vm.childData的方式访问,并且数据发生变化时视图也可以发生改变。

组件模板编译后对应的render函数是:

with(this) {
    return _c('div', {
        on: {
            "click": emitData
        }
    }, [_v(_s(childData))])
}

createElm完成节点的创建后,在invokeCreateHooks(vnode, insertedVnodeQueue)阶段,给DOM原生节点节点绑定emitData

3、this.$emit

在点击执行this.$emit时,会通过var cbs = vm._events[event]取出_events中的事件进行执行。

至此,父组件中的传递的数据就在子组件中可以通过this.xxx的方式获得,也可以通过this.$emit的方式将子组件中的数据传递给父组件。

二、injectprovide

使用场景:嵌套组件多层级传参

const B = {
  template: `<div>{{parentData1}}{{parentData2}}</div>`,
  inject: ['parentData1', 'parentData2'],
}

const A = {
  template: `<B></B>`,
  components: {
    B,
  },
}

new Vue({
  el: '#app',
  components: {
    A,
  },
  template: `<A></A>`,
  provide: {
    parentData1: {
      name: 'name-2',
      age: 30
    },
    parentData2: {
      name: 'name-2',
      age: 29
    },
  }
})

例子中在new Vue的时候通过provide提供了两个数据来源parentData1parentData2,然后跨了一个A组件在B组件中通过inject注入了这两个数据。

1、initProvide

在执行组件内部的this._init初始化方法时,会执行到initProvide逻辑:

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

如果在当前vm.$options中存在provide,会将其执行结果赋值给vm._provided

2、initInjections

function initInjections (vm: Component) {
  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)
  }
}
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) {
        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
  }
}

如果当前组件中有选项inject,会以while循环的方式不断在source = source.$parent中寻找_provided,然后获取到祖先组件中提供的数据源,这是实现祖先组件向所有子孙后代注入依赖的核心。

三、eventBus

使用场景:兄弟组件传参

const eventBus = new Vue();

const A = {
  template: `<div @click="send">component-a</div>`,
  methods: {
    send() {
      eventBus.$emit('sendData', 'data from A')
    }
  },
}

const B = {
  template: `<div>component-b</div>`,
  created() {
    eventBus.$on('sendData', (args) => {
      console.log(args)
    })
  },
}

new Vue({
  el: '#app',
  components: {
    A,
    B,
  },
  template: `<div><A></A><B></B></div>`,
})

在当前例子中,A组件和B组件称为兄弟组件,A组件通过事件总线eventBus中的$emit分发事件,B组件则通过$on来监听事件。

1、eventsMixin

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  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[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.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on () {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    const cbs = vm._events[event]
    if (!cbs) {
      return vm
    }
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    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}".`
        )
      }
    }
    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
  }
}

Vue构造函数定义完执行的eventsMixin函数中,在Vue.prototype上分别定义了$on$emit$off$once的方法易实现对事件的绑定、分发、取消和只执行一次的方法。eventBus就是利用了当new Vue实例化后实例上的$on$emit$off$once进行数据传递。

四、$attrs$listeners

使用场景:父子组件非props属性和非native方法传递

// main.js文件
import Vue from "vue";

const B = {
  template: `<div @click="emitData">{{ formParentData }}</div>`,
  data() {
    return {
      formParentData: ''
    }
  },
  inheritAttrs: false,

  created() {
    this.formParentData = this.$attrs;
    console.log(this.$attrs, '--------------a-component-$attrs')
    console.log(this.$listeners, '--------------b-component-$listeners')
  },
  methods: {
    emitData() {
      this.$emit('onFun', 'form B component')
    }
  },
}

const A = {
  template: `<B v-bind='$attrs' v-on='$listeners'></B>`,
  components: {
    B,
  },
  props: ['propData'],
  inheritAttrs: false,
  created() {
    console.log(this.$attrs, '--------------b-component-$attrs')
    console.log(this.$listeners, '--------------b-component-$listeners')
  }
}

new Vue({
  el: '#app',
  components: {
    A,
  },
  template: `<A :attrData='parentData' :propData='parentData' @click.native="nativeFun" @onFun="onFun"></A>`,
  data() {
    return {
      parentData: 'msg'
    }
  },
  methods: {
    nativeFun() {
      console.log('方法A');
    },
    onFun(v) {
      console.log('方法B', v);
    },
  }
})

当前例子中,new Vuetemplate模板中有attrDatapropDataclick.nativeonFun在进行传递。实际运行后,在A组件中this.$attrs{attrData: 'msg'}this.$listeners{onFun:f(...)}。在A组件中通过v-bind='$attrs'v-on='$listeners'的方式继续进行属性和方法的传递,在B组件中就可以获取到A组件中传入的$attrs$listeners

当前例子中完成了非props属性和非native方法的传递,并且通过v-bind='$attrs'v-on='$listeners'的方式实现了属性和方法的跨层级传递。

同时通过this.$emit的方法触发了根节点中onFun事件。

关于例子中的inheritAttrs: false,默认情况下父作用域的不被认作propsattribute绑定将会“回退”且作为普通的HTML属性应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置inheritAttrsfalse,这些默认行为将会被去掉。

五、ref

使用场景:父组件获取子组件数据或者执行子组件方法

const A = {
  template: `<div>{{childData.age}}</div>`,
  data() {
    return {
      childData: {
        name: 'qb',
        age: 30
      },
    }
  },
  methods: {
    increaseAge() {
      this.childData.age++;
    }
  }
}

new Vue({
  el: '#app',
  components: {
    A,
  },
  template: `<A ref='childRef' @click.native='changeChildData'></A>`,
  methods: {
    changeChildData() {
      // 执行子组件的方法
      this.$refs.childRef.increaseAge()
      // 获取子组件的数据
      console.log(this.$refs.childRef.childData);
    },
  }
})

在当前例子中,通过ref='childRef'的方式在当前组件中定义一个ref,可以通过this.$refs.childRef的方式获取到子组件A。可以通过this.$refs.childRef.increaseAge()的方式执行子组件中age增加的方法,也可以通过this.$refs.childRef.childData的方式获取到子组件中的数据。

六、$parent$children

使用场景:利用父子关系进行数据的获取或者方法的调用

const A = {
  template: `<div @click="changeParentData">{{childRandom}}</div>`,
  data() {
    return {
      childRandom: Math.random()
    }
  },
  mounted() {
    console.log(this.$parent.parentCount, '--child-created--'); // 获取父组件中的parentCount
  },
  methods: {
    changeParentData() {
      console.log(this.$parent); // 打印当前实例的$parent
      this.$parent.changeParentData(); // 调用当前父级中的方法`changeParentData`
    },
    changeChildData() {
      this.childRandom = Math.random();
    }
  }
}
const B = {
  template: `<div>b-component</div>`,
}

new Vue({
  el: '#app',
  components: {
    A,
    B,
  },
  template: `<div><A></A><B></B><p>{{parentCount}}</p><button  @click="changeChildrenData">修改子组件数据</button></div>`,
  data() {
    return {
      parentCount: 1
    }
  },
  mounted() {
    console.log(this.$children[0].childRandom, '--parent-created--'); // 获取第一个子组件中的childRandom
  },
  methods: {
    changeParentData() {
      this.parentCount++;
    },
    changeChildrenData() {
      console.log(this.$children); // 此时有两个子组件
      this.$children[0].changeChildData(); // 调起第一个子组件中的'changeChildData'方法
    }
  }
})

在当前例子中,父组件可以通过this.$children获取所有的子组件,这里有A组件和B组件,可以通过this.$children[0].childRandom的方式获取子组件A中的数据,也可以通过this.$children[0].changeChildData()的方式调起子组件A中的方法。

子组件可以通过this.$parent的方式获取父组件,可以通过this.$parent.parentCount获取父组件中的数据,也可以通过this.$parent.changeParentData()的方式修改父组件中的数据。

七、v-model

使用场景:父子组件双向绑定

const baseCheckbox = {
  template: `<input type="checkbox" v-bind:checked="checked" v-on:change="$emit('change', $event.target.checked)">`,
  model: {
    prop: "checked",
    event: "change"
  },
  props: {
    checked: Boolean
  }
};

new Vue({
  el: "#app",
  template: `<base-checkbox v-model="lovingVue"></base-checkbox>`,
  components: {
    baseCheckbox
  },
  data() {
    return {
      lovingVue: true
    };
  }
});

当前例子中通过v-model="lovingVue"的方式在组件baseCheckbox上进行绑定,在子组件中通过model选项修改当前默认propvalueeventinput的名称。通过v-bind:checked="checked"实现数据的绑定,通过v-on:change实现数据变化的触发。

具体数据传递方式可参考vue中的v-model

八、vuex

使用场景:多个视图依赖于同一状态,来自不同视图的行为需要变更同一状态。

vuex是状态管理仓库,其管理的状态是响应式的,修改也只能显式提交mutation的方式修改。vuexstategettermutationactionmodule五个核心模块组成。

总结

功能和业务的开发都来源于数据,数据是一个功能和业务的源头