Vue2 事件 v-model

149 阅读1分钟

事件处理

  • 普通元素的事件,是在 invokeInsertHookupdateDOMListeners ,通过原生addEventListener 添加的
  • 组件的事件,是在初始组件实例initEventsupdateComponentListeners,通过 $on 添加的
let Child = {
  template: `
      <button @click="clickHandler($event)">
        click me
      </button>
      `,
  methods: {
    clickHandler(e) {
      console.log('Button clicked!', e)
      this.$emit('select')
      this.$emit('my')
    }
  }
}

let vm = new Vue({
  el: '#app',
  template: `
      <div id="test">
        <child @select="selectHandler" @my="selectHandler" @click.native.prevent="clickHandler"></child>
      </div>
      `,
  data(){
    return {
      msg: 'dxx'
    }
  },
  methods: {
    clickHandler() {
      console.log('Child clicked!')
    },
    selectHandler() {
      console.log('Child select!')
    }
  },
  components: {
    Child
  }
})

1、普通元素上绑定原生事件

比如上面的 <button @click="clickHandler($event)">

2、组件上可以自定义事件和绑定原生事件

上例 child组件自定义事件 select,而绑定原生事件需要加上修饰符native

Vue 怎么处理:

vue/src/compiler/to-function.jscreateCompileToFunctionFn 中可以看到编译 template的结果。

render:'with(this){return _c('div',{attrs:{"id":"test"}},[_c('child',{on:{"select":selectHandler,"my":selectHandler},nativeOn:{"click":function($event){$event.preventDefault();return clickHandler.apply(null, arguments)}}})],1)}'

参数传递

// a='child' b={on:{},nativeOn:{}} c d undefined
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
createElement (
  context: Component,
  tag: any, // a
  data: any, // b
  children: any, // c
  normalizationType: any, // d
  alwaysNormalize: boolean // false
)
_createElement(context, tag, data, children, normalizationType)

createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
}

createComponent

const listeners = data.on
data.on = data.nativeOn

const name = Ctor.options.name || tag
const vnode = new VNode(
  `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  data, undefined, undefined, undefined, context,
  { Ctor, propsData, listeners, tag, children },
  asyncFactory
)

childVnode 创建完,再处理 divVnode

如下:

j1.png

childVnode 是组件,patch 时,createComponent -> initHook,子组件初始化,处理自定义事件

function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // 这边是 childVnode._parentListeners 也是 $listeners
  // 是在子组件初始化 initInternalComponent 中 opts._parentListeners = vnodeComponentOptions.listeners
  // 就是原来 data.on 自定义事件,
  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)
}
function createOnceHandler (event, fn) {
  const _target = target
  return function onceHandler () {
    const res = fn.apply(null, arguments)
    if (res !== null) {
      _target.$off(event, onceHandler)
    }
  }
}

function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  // 更改 target
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}

真正添加事件的函数是 updateListeners,这边组件自定义事件通过 $on 添加。

子组件 $mount 时 render 函数

render:'with(this){return _c('button',{on:{"click":function($event){return clickHandler($event)}}},[_v("\\n click me\\n ")])}'

_vcreateTextVNode 创建文本 Vnode,然后创建 buttonVnode,生成如下:

image.png

button 事件的添加是在这里

patch:
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
  // updateDOMListeners 中添加 vue/src/platforms/web/runtime/modules/events.js
  invokeCreateHooks(vnode, insertedVnodeQueue)
}
// children 没有事件,处理完后到 button 中 data: {on:{click:fn}}
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  const on = vnode.data.on || {}
  const oldOn = oldVnode.data.on || {}
  // vnode is empty when removing all listeners,
  // and use old vnode dom element
  target = vnode.elm || oldVnode.elm
  normalizeEvents(on)
  updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
  target = undefined
}
function add (
  name: string,
  handler: Function,
  capture: boolean,
  passive: boolean
) {

  if (useMicrotaskFix) {
    const attachedTimestamp = currentFlushTimestamp
    const original = handler

  target.addEventListener(
    name,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

function remove (
  name: string,
  handler: Function,
  capture: boolean,
  _target?: HTMLElement
) {
  (_target || target).removeEventListener(
    name,
    handler._wrapper || handler,
    capture
  )
}
function createOnceHandler (event, handler, capture) {
  const _target = target // save current target element in closure
  return function onceHandler () {
    const res = handler.apply(null, arguments)
    if (res !== null) {
      remove(event, onceHandler, capture, _target)
    }
  }
}

添加事件调的原生 addEventListener

到这边,组件自定义事件、button 原生事件已添加,还差组件原生事件添加。

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }

    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

当子组件 DOM 生成后,回到 childVnode ,此时 componentInstance 有值,initComponent 中触发 invokeCreateHooksbutton 时一样。将事件添加到 button 中,childVnode.elm 也是 button 元素。

v-model

1、用在表单元素上

let vm = new Vue({
  el: '#app',
  template: `
      <div id="test">
        <input v-model="msg" />
        {{msg}}
      </div>
      `,
  data() {
    return {
      msg: 'dxx'
    }
  },
});

v-model 编译后的样子

表单:

render:'with(this){return _c('div',{attrs:{"id":"test"}},[_c('input',{directives:[{name:"model",rawName:"v-model",value:(msg),expression:"msg"}],domProps:{"value":(msg)},on:{"input":function($event){if($event.target.composing)return;msg=$event.target.value}}}),_v("\\n "+_s(msg)+"\\n ")])}'

image.png

2、组件上:

let Child = {
  template: `
      <div>
        <input :value="value" @input="updateValue" >
      </div>
      `,
  props:['value'],
  methods: {
    updateValue(e) {
      this.$emit('input', e.target.value)
    }
  }
}
let vm = new Vue({
  el: '#app',
  template: `
      <div id="test">
        <child v-model="msg"></child>
        {{msg}}
      </div>
      `,
  data() {
    return {
      msg: 'dxx'
    }
  },
  components: {
    Child
  }
})

render:'with(this){return _c('div',{attrs:{"id":"test"}},[_c('child',{model:{value:(msg),callback:function ($$v) {msg=$$v},expression:"msg"}}),_v("\\n "+_s(msg)+"\\n ")],1)}'

{
  model:{
    value:(msg),
    callback:function ($$v) {msg=$$v},
    expression:"msg"
  }
}

// 创建组件 Vnode createComponent
if (isDef(data.model)) {
  transformModel(Ctor.options, data)
}
处理后
data {
  attrs:{value: 'dxx'},
  model: {value:(msg)callback:function ($$v) {msg=$$v},expression:"msg",},
  on: {input: data.model.callback}
}

propsData:{value: 'dxx'}
listeners = data.on
data.on = data.nativeOn

{
  attrs:{}
  hook:{init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ}
  model:{value: 'dxx', callback: ƒ, expression: 'msg'}
  on:undefined
}

render:'with(this){return _c('div',[_c('input',{domProps:{"value":value},on:{"input":updateValue}})])}'

image.png