vue2 源码学习 双向绑定/事件处理

490 阅读4分钟

1.双向绑定

   <!DOCTYPE html>
<html>
<head>
    <title>Vue事件处理</title>
</head>
<body>
    <div id="demo">
        <h1>双向绑定机制</h1>
        <!--表单控件绑定-->
        <input type="text" v-model="foo">
        <!--自定义事件-->
        <comp v-model="foo"></comp>
    </div>
    <script src="../../dist/vue.js"></script>
    <script>
        // 声明自定义组件
        Vue.component('comp', {
            template: `
                <input type="text" :value="$attrs.value"
                    @input="$emit('input', $event.target.value)">
            `
        })
        // 创建实例
        const app = new Vue({
            el: '#demo',
            data: { foo: 'foo' }
        });

        console.log(app.$options.render);
        
    </script>
</body>
</html>

生成的渲染函数

(function anonymous(
) {
    with (this) {
        return _c('div', { attrs: { "id": "demo" } },
            [_c('h1', [_v("双向绑定机制")]),
             _v(" "), 
             _c('input', 
                    { 
                        directives:  [{ name: "model", rawName: "v-model", value: (foo), expression: "foo" }], 
                        attrs: { "type": "text" },
                        domProps: { "value": (foo) }, 
                        on: { "input": function ($event) { 
                                    if ($event.target.composing) return;
                                     foo = $event.target.value 
                                }
                            }
                    }
                )
                , _v(" "), 
                
                _c('comp', {
                     model: {
                          value: (foo), 
                          callback: function ($$v) { foo = $$v }, 
                          expression: "foo" 
                        } 
                    }) 
                ], 1)
    }
})

1.1普通控件绑定

//通过 domProps 相关信息更新控件
domProps: { "value": (foo) }, 
//src/platforms/web/runtime/modules/dom-props.js

function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
    return
  }
  let key, cur
  const elm: any = vnode.elm
  const oldProps = oldVnode.data.domProps || {}
  let props = vnode.data.domProps || {}
  // clone observed objects, as the user probably wants to mutate it
  if (isDef(props.__ob__)) {
    props = vnode.data.domProps = extend({}, props)
  }

  for (key in oldProps) {
    if (!(key in props)) {
      elm[key] = ''
    }
  }

  for (key in props) {
    cur = props[key]
    // ignore children if the node has textContent or innerHTML,
    // as these will throw away existing DOM nodes and cause removal errors
    // on subsequent patches (#3360)
    if (key === 'textContent' || key === 'innerHTML') {
      if (vnode.children) vnode.children.length = 0
      if (cur === oldProps[key]) continue
      // #6601 work around Chrome version <= 55 bug where single textNode
      // replaced by innerHTML/textContent retains its parentNode property
      if (elm.childNodes.length === 1) {
        elm.removeChild(elm.childNodes[0])
      }
    }
    //这里通过参数key为value 设置input.value = ''
    if (key === 'value' && elm.tagName !== 'PROGRESS') {
      // store value as _value as well since
      // non-string values will be stringified
      elm._value = cur
      // avoid resetting cursor position when value is the same
      const strCur = isUndef(cur) ? '' : String(cur)
      if (shouldUpdateValue(elm, strCur)) {
        elm.value = strCur //这里进行赋值
      }
    } else if (key === 'innerHTML' && isSVG(elm.tagName) && isUndef(elm.innerHTML)) {
      // IE doesn't support innerHTML for SVG elements
      svgContainer = svgContainer || document.createElement('div')
      svgContainer.innerHTML = `<svg>${cur}</svg>`
      const svg = svgContainer.firstChild
      while (elm.firstChild) {
        elm.removeChild(elm.firstChild)
      }
      while (svg.firstChild) {
        elm.appendChild(svg.firstChild)
      }
    } else if (
      // skip the update if old and new VDOM state is the same.
      // `value` is handled separately because the DOM value may be temporarily
      // out of sync with VDOM state due to focus, composition and modifiers.
      // This  #4521 by skipping the unnecessary `checked` update.
      cur !== oldProps[key]
    ) {
      // some property updates can throw
      // e.g. `value` on <progress> w/ non-finite value
      try {
        elm[key] = cur
      } catch (e) {}
    }
  }
}
  • directives 解析方式
  • src/platforms/web/compiler/directives/model.js
  • 平台特有的信息写在 web平台上
//  directives:  [{ name: "model", rawName: "v-model", value: (foo), expression: "foo" }], 
export default function model (
  el: ASTElement,
  dir: ASTDirective,
  _warn: Function
): ?boolean {
  warn = _warn
  const value = dir.value
  const modifiers = dir.modifiers
  const tag = el.tag
  const type = el.attrsMap.type

  if (process.env.NODE_ENV !== 'production') {
    // inputs with type="file" are read only and setting the input's
    // value will throw an error.
    if (tag === 'input' && type === 'file') {
      warn(
        `<${el.tag} v-model="${value}" type="file">:\n` +
        `File inputs are read only. Use a v-on:change listener instead.`,
        el.rawAttrsMap['v-model']
      )
    }
  }
    //这里做平台上不同控件的判断
  if (el.component) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  } 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)) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `<${el.tag} v-model="${value}">: ` +
      `v-model is not supported on this element type. ` +
      'If you are working with contenteditable, it\'s recommended to ' +
      'wrap a library dedicated for that purpose inside a custom component.',
      el.rawAttrsMap['v-model']
    )
  }

  // ensure runtime directive metadata
  return true
}

1.2自定义组件绑定

虽然是自定义,但最终还是要走4.1普通控件绑定 进行绑定,多了个transformModel处理方法
//src/core/vdom/create-element.js
_createElement{
    createComponent()
}

//src/core/vdom/create-component.js
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }
  const baseCtor = context.$options._base
  ...

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)//这里做v-model操作,处理data.on 信息给下面使用
  }
// extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  //这里做data.on 的处理
  const listeners = data.on //开始事件监听处理
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn
  
  
  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  //最终在这里传入 listeners 进行渲染成虚拟dom
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }
  return vnode
  ...
}

// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data: any) {
//系统模式使用 value和input事件,如果用户自定义了 优先取用户的属性和事件
  const prop = (options.model && options.model.prop) || 'value'
  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)) {//如果用户已经定义了 @input ="xxx" 相同的事件,则把事件存储在数组里
    if (
      Array.isArray(existing)
        ? existing.indexOf(callback) === -1 //如是数组并且不是系统自带的input事件
        : existing !== callback //如果不是数组,并且与新增方法的不一致
    ) {
      on[event] = [callback].concat(existing)//保存的是数组
    }
  } else {
    on[event] = callback //正常保存系统自带一个input方法
  }
}

2.事件

2.1例子&生成render

<!DOCTYPE html>
<html>
<head>
    <title>Vue事件处理</title>
</head>
<body>
    <div id="demo">
        <h1>事件处理机制</h1>
        <!--普通事件-->
        <p @click="onClick">this is p</p>
        <!--自定义事件-->
        <comp @myclick="onMyClick"></comp>
    </div>
    <script src="../../dist/vue.js"></script>
    <script>
        // 声明自定义组件
        Vue.component('comp', {
            template: `
                <div @click="onClick">this is comp</div>
            `,
            methods: {
                onClick() {
                    this.$emit('myclick')
                }
            }
        })
        // 创建实例
        const app = new Vue({
            el: '#demo',
            methods: {
                onClick() {
                    console.log('普通事件');
                },
                onMyClick() {
                    console.log('自定义事件');
                }
            },
        });
        console.log(app.$options.render);
        
    </script>
</body>
</html>
//render生成的js内容
(function anonymous(
    ) {
    with(this){
        return _c('div',{attrs:{"id":"demo"}},
            [
                    _c('h1',[_v("事件处理机制")]),_v(" "), 
                    _c('p',{on:{"click":onClick}},[_v("this is p")]),  _v(" "),
                    _c('comp',{on:{"myclick":onMyClick}})
            ]
            ,1)
        }
    })

2.2事件类型

所有事件都是基于已经构建生成的dom,所以需要在运行时动态添加

2.2.1 原生事件

注意事件多数是在path动态创建的,依赖于invokeCreateHooks触发绑定事件
/src/platforms/web/runtime/modules/events.js 的updateDOMListeners

2.2.1.1执行流程

Vue -> Vue._init -> Vue.$mount -> mountComponent -> watcher -> get updateComponent -> Vue._update -> patch -> createElm -> createChild -> invokeCreateHooks -> updateDOMListeners -> updateListeners ->

//src/core/vdom/patch.js
  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }
      //处理原生事件
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) { //注意这里的data是包含事件on的内容,不单单是指vue里面的data属性
          //如果有data定义就会有,对应的事件监听操作
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }
    
  function invokeCreateHooks (vnode, insertedVnodeQueue) {
    //执行所有回调 这里包含下面的updateDOMListeners
    for (let i = 0; i < cbs.create.length; ++i) {
      cbs.create[i](emptyNode, vnode)
    }
    i = vnode.data.hook // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) i.create(emptyNode, vnode)
      if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
    }
  }

//src/platforms/web/runtime/modules/events.js
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 || {}
  target = vnode.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
    handler = original._wrapper = function (e) {
      if (
        // no bubbling, should always fire.
        // this is just a safety net in case event.timeStamp is unreliable in
        // certain weird environments...
        e.target === e.currentTarget ||
        // event is fired after handler attachment
        e.timeStamp >= attachedTimestamp ||
        // bail for environments that have buggy event.timeStamp implementations
        // #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState
        // #9681 QtWebEngine event.timeStamp is negative value
        e.timeStamp <= 0 ||
        // #9448 bail if event is fired in another document in a multi-page
        // electron/nw.js app, since event.timeStamp will be using a different
        // starting reference
        e.target.ownerDocument !== document
      ) {
        return original.apply(this, arguments)
      }
    }
  }
  //添加事件监听
  target.addEventListener(
    name,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

export default {
  create: updateDOMListeners,
  update: updateDOMListeners
}

//src/core/vdom/helpers/update-listeners.js
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) {
    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)
    }
  }
}

2.2.2 自定义组件事件

事件的监听和派发均是实例
//src/core/instance/events.js

2.2.2.1执行流程

Vue -> Vue._init -> Vue.$mount -> mountComponent -> watcher -> get updateComponent -> Vue._update -> patch -> createElm -> createComponent -> hook.init ->createComponent... ->init() -> initEvents -> updateComponentListeners() -> updateDOMListeners() -> updateListeners ->

  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 */)
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
      //初始化钩子
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }
  //初始化钩子
  function initComponent (vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
      vnode.data.pendingInsert = null
    }
    vnode.elm = vnode.componentInstance.$el
    if (isPatchable(vnode)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
      setScope(vnode)
    } else {
      // empty component root.
      // skip all element-related modules except for ref (#3455)
      registerRef(vnode)
      // make sure to invoke the insert hook
      insertedVnodeQueue.push(vnode)
    }
  }


2.3全局事件 $on $emit

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    //判断事件是否是 数组,即可以多个事件key绑定一个方法,递归继续调用$on绑定
   // 传入$on([key1,key2],fn1)转化为  {key1:[fn1],key2:[fn1],}
   // 传入$on([key1],fn2)转化为  {key1:[fn1,fn2],key2:[fn1],}
   //上面一个key绑定多个方法 ,如fn1和fn2
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
    //下面是真正的绑定如: _events = {key1:[fn1,fn2],key2:[fn1,fn2],}
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      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)//2.由于只执行一遍,所以立马取消,在全局的_event对象里已经清空所有监听
      fn.apply(vm, arguments)//3.人为触发一次事件
    }
    
    on.fn = fn//该赋值是为了 在$off的时候找不到原来的fn,会通过 on.fn去判断移除的函数
    vm.$on(event, on)//1.这里正常的调用了$on注册,对应当前定义的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
    //下面是正真的单个事件移除,一个key对应一个数组数据
    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) {//这里的 .fn是上面$once留下来判断的依据
        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}".`
        )
      }
    }
    //根据key找到对应的回调方法,然后逐一执行
    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 方法主要是做 调用方法时候安全捕获
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}

//src/core/instance/events.js
export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  //这里把父亲的事件传入
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

let target: any
//这里定义了add 方法给下面updateListeners 传入使用,最终就是谁定义谁派发与接受
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
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}

//src/core/vdom/helpers/update-listeners.js
//这里传入参数 add 就是上面src/core/instance/events.js定义的
export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component
) { 
   ...
      add(event.name, cur, event.capture, event.passive, event.params)
   ...
}