08-谈谈虚拟DOM

315 阅读7分钟

为什么会出现虚拟DOM这种技术,多复杂啊,增加了代码的难度,也不易于代码的阅读,那么接下来我带着大家看看虚拟DOM的必要性~

整体流程梳理

  1. 用户修改或者操作数据
  2. 触发Object.defineProperty中的setter
  3. setter触发dep
  4. dep.notify()通知watcher更新
  5. watcher执行update
  6. update让我们重新render,生成虚拟dom
  7. render完成后,重新update将虚拟dom转为真实dom

真实DOM是如何解析的

浏览器渲染进程中有GUI渲染线程,负责渲染浏览器界面,解析HTML、CSS,构建DOM树和RenderObject树,布局与绘制等。

工作流程分为以下5步:

1. 创建DOM树

解析HTML文件,构建DOM树,同时浏览器主进程负责下载css文件

2. 生成css样式表

css文件下载完成,用css解析器,解析css文件和元素上的inline样式,生成页面的样式表

3. 构建Render树

将DOM树和样式表关联起来,构建一颗Render树

4. 布局

根据Render树结构,为每个Render树上的节点确定尺寸,位置等

5. 绘制页面

根据Render树及节点坐标,将它们绘制出来

注意:
理解了GUI的渲染过程,我们也就理解了为什么平时一直强调css文件的引入要放入头部,是为了尽早的完成页面的渲染,而JS文件放到body底部引入,是因为JS引擎执行脚本的时候,GUI渲染线程是被挂起的,两者是互斥的,会影响css文件的引入,所以为了尽快将页面展示出来,css文件放头部,js文件放底部。

虚拟DOM

  • 概念

虚拟DOM(virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用的各种状态变化会作用于虚拟DOM,最终映射到DOM上。

  • 优点

    • 虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM对比可以得到最小DOM操作量,从而提升性能与用户体验。
    • 页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象速度会更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。
    • 跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台。
    • 兼容性:还可以加入兼容性代码增强操作的兼容性。
  • 必要性

    1. vue1.0中有细粒度的数据变化侦测(每一个key一个watcher),它是不需要虚拟DOM的,但是细粒度造成了大量的开销,这对于大型项目来说是不可接受的。因此,vue2.0选择了中等粒度的解决方案,每一个组件一个watcher实例,这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。
    2. 原生JS操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。如果一次操作需要更新10个dom节点,那么浏览器会执行10次渲染流程,因为浏览器执行某次dom操作的时候,是不知道后面还有dom更新的,这样就造成了很大的浪费,而且频繁操作还会出现页面卡顿,影响用户的体验。
  • 代价

    消耗一些性能、消耗一些cpu,换来的是更好的用户体验。

vue是如何生成虚拟DOM

以下分析依然从vue源码分析,大家可以对照文件位置去看源码,最开始看源码的时候要以囫囵吞枣的方式看,不要扣细节,看关键点即可,后续二刷三刷的时候,再一步步去深入。

1. $mount挂载时执行了mountComponent方法

位置:src/platforms/web/runtime/index.js

功能:$mount的时候只执行了mountComponent方法

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

2. 渲染、更新组件

位置:src/core/instance/lifecycle.js

功能:mountComponent做了什么?创建一个更新函数,创建一个watcher,两者之间要进行挂钩,watcher收到通知后,就会执行updateComponent,去执行render和update方法

mountComponent:创建一个watcher,创建一个更新函数updateComponent,将来watcher得到通知后,会执行更新函数,更新函数中会执行update方法,render函数返回虚拟dom,updat将虚拟dom转换成真实dom

// 创建更新函数
updateComponent = () => {
    // _render() 生成虚拟DOM
    // _update() 转换vdom为dom
    vm._update(vm._render(), hydrating)
}
// 创建watcher
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
}, true /* isRenderWatcher */)

3. render

位置:src/core/instance/render.js

功能:生成虚拟DOM,真正用来创建vnode树的函数是vm.$createElement

Vue.prototype._render = function (): VNode {
    // 最终需要计算出的虚拟dom,在vue中称为vnode
    let vnode
    // 执行render函数,传入参数是$createElement
    // render(h){}
    vnode = render.call(vm._renderProxy, vm.$createElement)
}
// 声明vm.$createElement
function initRender (vm: Component) {
    // 声明了两个方法:_c与$createElement
    // _c:编译器生成的render函数用这个
    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
    // $createElement用户编写的render用这个
    vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

4. createElement方法

位置:src/core/vdom/create-element.js

功能:$createElement()是对createElement函数的封装,createElement就是生成虚拟DOM,createComponent用于创建组件并返回VNode

function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode>{
    // 处理传入的data
    ...
    // vnode生成过程
    // 传入tag可能是原生的html标签,也可能是自定义组件标签
    let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 原生标签
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      // 直接创建vnode实例
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  }  else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  return vnode
}

5. VNode对象

位置:src/core/vdom/vnode.js

功能:render返回的一个VNode实例,它的children还是VNode,最终构成一个树,就是虚拟DOM树,

介绍:

一个VNode的实例对象包含了以下属性:

  • tag:当前节点的标签名
  • data:当前节点的数据对象
  • children:数组类型,包含了当前节点的子节点
  • text:当前节点的文本,一般文本节点或注释节点会有该属性
  • ns:节点的namespace
  • componentOptions:创建组件实例时会用到的选项信息
  • child:当前节点对应的组件实例
  • parent:组件的占位节点
  • raw:raw HTML
  • isStatic:静态节点的标识
  • isRootInsert:是否作为根节点插入,被包裹的节点,该属性的值为false
  • isComment:当前节点是否为克隆节点
  • isCloned:当前节点是否为克隆节点
  • isOnce:当前节点是否有v-once指令

VNode分类

VNode可以理解为vue框架的虚拟dom的基类,通过new实例化的VNode大致可以分为几类

  • EmptyVNode: 没有内容的注释节点

  • TextVNode: 文本节点

  • ElementVNode: 普通元素节点

  • ComponentVNode: 组件节点

  • CloneVNode: 克隆节点,可以是以上任意类型的节点,唯一的区别在于isCloned属性为true

  • ...

/* @flow */

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode (vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    // #7975
    // clone children array to avoid mutating original in case of cloning
    // a child.
    vnode.children && vnode.children.slice(),
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned
}

6. 生成虚拟dom呈现

  • 真实dom
// 真实dom
<div id="virtual-dom">
    <p>Virtual DOM</p>
    <ul id="list">
      <li class="item">Item 1</li>
      <li class="item">Item 2</li>
      <li class="item">Item 3</li>
    </ul>
    <div>Hello World</div>
</div> 
  • 虚拟dom
// 虚拟dom
var container = el('div',{id:'virtual-dom'},[
  el('p',{},['Virtual DOM']),
  el('ul', { id: 'list' }, [
	el('li', { class: 'item' }, ['Item 1']),
	el('li', { class: 'item' }, ['Item 2']),
	el('li', { class: 'item' }, ['Item 3'])
  ]),
  el('div',{},['Hello World'])
]) 
  • 将上面的container数据结构打印出来

至此,Vdom生成结束。

vue是如何将虚拟DOM转为真实DOM的

1. vue源码层层调用,是怎么走到patch打补丁的

(1) _update

位置:core\instance\lifecycle.js

功能:update负责更新dom,转换vnode为dom

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    if (!prevVnode) {
      // 首次渲染-initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 更新-updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
}

(2) patch

位置:platforms/web/runtime/index.js

功能:定义补丁函数

Vue.prototype.__patch__ = inBrowser ? patch : noop

(3) patch之createPatchFunction

位置:src/core/vdom/patch.js

功能:工厂函数createPatchFunction生成了真正的patch

// 传入平台特有的节点操作方法实现跨平台
export const patch: Function = createPatchFunction({ nodeOps, modules })

nodeOps:节点操作(platforms/web/runtime/node-ops.js)
modules:属性操作(platforms/web/runtime/modules/index.js)

(4) createPatchFunction

位置:src/core/vdom/patch.js

功能:返回平台特有patch方法,backend是平台特有节点扩展代码,从700行开始浏览,patch是如何打补丁的

2. 具体分析patch

patch策略

要想实现这么低的时间复杂度,只能平层比较两棵树的节点,放弃了深度遍历,是一种相当高效的算法。

同层级只做三件事:增删改。具体:new VNode不存在就删;old VNode不存在就增;都存在就比较类型,类型不同直接替换、类型相同则执行更新。

    1. new VNode不存在,old VNode存在,那就是销毁老节点,调用invokeDestroyHook(oldVnode)
    1. old VNode不存在,new VNode存在,那就是创建新节点,调用createElm(vnode, insertedVnodeQueue)
    1. new VNode与old VNode都存在
    • 判断是不是同一个节点,是则调用patchVnode来更新
    • 不是的话,直接replace

patch-diff算法

patchVnode

说说patchVnode,走到这一步时,我们都知道,两个VNode类型相同,那么操作dom的差异类型有以下3种情况:

  • 属性更新(PROPS):修改了节点的属性,例如:删除节点上的class等
  • 文本改变(TEXT):改变文本节点的文本内容
  • 顺序互换(REORDER):移动、删除、新增子节点

以下不同情况的处理方式:

  • 两个节点完全相同,则直接return
if (oldVnode === vnode) {
      return
}
  • 如果新老节点都是静态的,那么只需要替换elm以及componentInstance即可
if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
      vnode.componentInstance = oldVnode.componentInstance
      return
}
  • 新老节点均有children,则对子节点进行diff操作,调用updateChildren
if (isDef(oldCh) && isDef(ch)) {
    // 更新孩子
    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} 
  • 只有老节点有children,移除该DOM节点的所有子节点
if (isDef(oldCh)) {
    // 老的有孩子-删除
    removeVnodes(oldCh, 0, oldCh.length - 1)
} 
  • 只有新节点有children,先清空老节点的文本内容,然后为当前DOM节点加入子节点
if (isDef(ch)) {
    // 新的有孩子,老的没有, 创建并追加孩子,
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} 
  • 新老节点都是文本节点或注释节点,只需更新文本内容即可
if (oldVnode.text !== vnode.text) {
    // 两个都有文本,修改文本
    nodeOps.setTextContent(elm, vnode.text)
}
updateChildren

updateChildren主要作用是用一种比较高效的方式比对新旧两个VNode的children,得出最小操作补丁。执行一个双循环,vue中针对web场景特点做了特别的算法优化:

新老VNode头尾都会做一个变量标记,在遍历过程中这个几个变量都会向中间靠拢,下面是遍历规则:

  • 情况1:头头比较或者尾尾比较,满足相同节点,则直接将该VNode节点进行patchVnode,不需要再遍历就完成了一次循环

  • 情况2:头尾比较,如果oldStartVnode与newEndVnode满足sameVnode,说明节点进行了移动,那么进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode后面

  • 情况3:尾头比较,如果oldEndVnode与newStartVnode满足sameVnode,说明节点进行了移动,那么进行patchVnode的同时还需要将真实DOM节点移动到oldStartVnode前面

  • 以上情况都不符合

    • newStartVnode在old VNode节点中找不到一致的key,或者是即便key相同却不是 sameVnode,这个时候会调用createElm创建一个新的DOM节点。

    • 旧VNode遍历结束后,新的节点还没有找到,说明新节点新增了,直接将剩下的VNode对应的DOM插入到真实DOM中,此时调用addVnodes

    • 新VNode遍历结束,老节点还有剩余,需要从文档中删除节点

    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    // 循环条件,开始游标<=结束游标
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 老开始与新开始相同:直接patchVnode,游标++
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 老结束与新结束相同,打补丁两者,游标--
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 老开始与新结束相同,打补丁两者,移动老开始到结尾,游标:老开始++,新结束--
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 老结束与新开始相同,打补丁两者,移动老结束到头部,游标:老结束--,新开始++
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 以上情况都不是,开始循环比较
        // 从新的开头拿一个,与老的做比较
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          // 新增,并追加到头部
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 打补丁,并移动
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      // 老数组结束,批量创建并追加
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      // 新数组先结束,批量删除
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

节点属性是如何更新的

以上讲述了节点的文本或者位置、子节点不同时是如何更新的,但是没有说到当节点上的属性发生变化时,要如何更新节点上的属性,一起看看~

位置:src/core/vdom/patch.js

从73行开始

// 将属性相关dom操作按hooks归类,在patchVnode时一起执行
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
// modules中是所有节点属性相关操作
const { modules, nodeOps } = backend

for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        // 添加到相应数组中:
        // cbs.create = [fn1,fn2,...]
        // cbs.update = [fn1,fn2,...]
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
}
// 以上代码的具体拆解
modules= [
    attrs,
    klass,
    events,
    domProps,
    style,
    transition
  ]
  attrs={
    create: updateAttrs,
    update: updateAttrs
  }
  klass={
    create: updateClass,
    update: updateClass
  }
  events={
    create: updateDOMListeners,
    update: updateDOMListeners
  }

  domProps={
    create: updateDOMProps,
    update: updateDOMProps
  }
  style={
    create: updateStyle,
    update: updateStyle
  }
  transition={
    create: _enter,
    activate: _enter,
    remove (vnode: VNode, rm: Function) {
      /* istanbul ignore else */
      if (vnode.data.show !== true) {
        leave(vnode, rm)
      } else {
        rm()
      }
    }
  }
  cbs:{
    'create':[updateAttrs,updateClass,updateDOMListeners,updateDOMProps,updateStyle,_enter],
    'activate':[_enter], 
    'update':[updateAttrs,updateClass,updateDOMListeners,updateDOMProps,updateStyle], 
    'remove':[_enter], 
    'destroy':[]
  }

跳至570行——patchVnode

function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // 更新属性
    if (isDef(data) && isPatchable(vnode)) {
      // 执行默认的钩子
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 执行用户定义的钩子
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
  }

位置:src/platform/web/modules/attrs.js

功能:节点上的属性具体如何更新的,以attr为例

// updateAttrs
function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  const opts = vnode.componentOptions
  if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) {
    return
  }
  if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
    return
  }
  let key, cur, old
  const elm = vnode.elm
  const oldAttrs = oldVnode.data.attrs || {}
  let attrs: any = vnode.data.attrs || {}
  // clone observed objects, as the user probably wants to mutate it
  if (isDef(attrs.__ob__)) {
    attrs = vnode.data.attrs = extend({}, attrs)
  }

  for (key in attrs) {
    cur = attrs[key]
    old = oldAttrs[key]
    if (old !== cur) {
      setAttr(elm, key, cur)
    }
  }
  // #4391: in IE9, setting type can reset value for input[type=radio]
  // #6666: IE/Edge forces progress value down to 1 before setting a max
  /* istanbul ignore if */
  if ((isIE || isEdge) && attrs.value !== oldAttrs.value) {
    setAttr(elm, 'value', attrs.value)
  }
  for (key in oldAttrs) {
    if (isUndef(attrs[key])) {
      if (isXlink(key)) {
        elm.removeAttributeNS(xlinkNS, getXlinkProp(key))
      } else if (!isEnumeratedAttr(key)) {
        elm.removeAttribute(key)
      }
    }
  }
}
// setAttr
function setAttr (el: Element, key: string, value: any) {
  if (el.tagName.indexOf('-') > -1) {
    baseSetAttr(el, key, value)
  } else if (isBooleanAttr(key)) {
    // set attribute for blank value
    // e.g. <option disabled>Select one</option>
    if (isFalsyAttrValue(value)) {
      el.removeAttribute(key)
    } else {
      // technically allowfullscreen is a boolean attribute for <iframe>,
      // but Flash expects a value of "true" when used on <embed> tag
      value = key === 'allowfullscreen' && el.tagName === 'EMBED'
        ? 'true'
        : key
      el.setAttribute(key, value)
    }
  } else if (isEnumeratedAttr(key)) {
    el.setAttribute(key, convertEnumeratedValue(key, value))
  } else if (isXlink(key)) {
    if (isFalsyAttrValue(value)) {
      el.removeAttributeNS(xlinkNS, getXlinkProp(key))
    } else {
      el.setAttributeNS(xlinkNS, key, value)
    }
  } else {
    baseSetAttr(el, key, value)
  }
}

patch() => patchVnode() => cbs.update[i](oldVnode, vnode)

组件是怎么更新的

组件声明

位置:src/core/global-api/assets.js

功能:使用extend方法,将传入组件配置转换为构造函数

// src/core/index.js
// 全局api注册
initGlobalAPI(Vue)

// src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
    initAssetRegisters(Vue)
}
// src/core/global-api/assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  // ['component','filter','directive']
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          // component处理:指定name,获取组件构造函数
          // Vue.component('comp',{template:''})
          definition.name = definition.name || id
          // 转换组件配置对象为构造函数
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        // 全局注册:options[components]=Ctor
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

全局注册组件后,在实例的components中可找到注册的组件

组件创建及挂载

  • 创建自定义组件VNode

位置:src/core/vdom/create-element.js

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
    let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // resolveAsset(context.$options, 'components', tag):当前组件实例的选项中有没有components的配置,里面有没有tag的配置
      // component
      // 首先查找自定义组件构造函数声明
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
}
  • createComponent

位置:src/core/vdom/create-component.js

// 185行
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
   // install component management hooks onto the placeholder node
  // 安装组件的钩子:
  installComponentHooks(data) 
}

// 228行
function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  // 由于用户也有可能传递自定义钩子函数,所以需要合并一下
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

// 36行
const componentVNodeHooks = {
  // 初始化钩子用来创建组件实例和挂载的
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
  insert (vnode: MountedComponentVNode) {},
  destroy (vnode: MountedComponentVNode) {}
}  

首先创建的是根组件,首次_render()时,会得到整棵树的VNode结构
整体流程:new Vue() => $mount() => vm._render() => createElement() => createComponent()

  • 创建自定义组件实例

位置:src/core/vdom/patch.js

功能:根组件执行更新函数时,会递归创建子元素和子组件

// patch
// 首次执行_update()时,patch()会通过createEle()创建根元素,子元素创建也就从这里开始
return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // create new node
    createElm(
      vnode,
      insertedVnodeQueue,
      // extremely rare edge case: do not insert if old element is in a
      // leaving transition. Only happens when combining transition +
      // keep-alive + HOCs. (#4590)
      oldElm._leaveCb ? null : parentElm,
      nodeOps.nextSibling(oldElm)
    )
}
//createElm 
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
    }
} 
// createComponent
// 自定义组件创建
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    // 获取data,主要是hook
    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)
        // dom插入操作
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }
// initComponent
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)
    }
}
// invokeCreateHooks
function invokeCreateHooks (vnode, insertedVnodeQueue) {
    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)
    }
}

结论:
组件创建顺序自上而下
组件挂载顺序自下而上

事件处理整体流程

  • 编译阶段

处理为data中的on

(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)} })
  • 事件处理分为:普通事件与自定义事件

    <div id="demo">
          <h1>事件处理机制</h1>
          <!--普通事件-->
          <p @click="onClick">this is p</p>
          <!--自定义事件-->
          <comp @myclick="onMyClick"></comp>
    </div>
    
    • 普通事件

    位置:src/platform/web/runtime/modules/events.js

    整体流程:patch() => createElm() => invokeCreateHooks() => updateDOMListeners()

    // patch.js——patch
    function patch (oldVnode, vnode, hydrating, removeOnly) {
        createElm(
                vnode,
                insertedVnodeQueue,
                // extremely rare edge case: do not insert if old element is in a
                // leaving transition. Only happens when combining transition +
                // keep-alive + HOCs. (#4590)
                oldElm._leaveCb ? null : parentElm,
                nodeOps.nextSibling(oldElm)
         )
    }
    // patch.js——createElm
    function createElm (
      vnode,
      insertedVnodeQueue,
      parentElm,
      refElm,
      nested,
      ownerArray,
      index
    ) {
       invokeCreateHooks(vnode, insertedVnodeQueue) 
    }
    // patch.js——invokeCreateHooks
    function invokeCreateHooks (vnode, insertedVnodeQueue) {
      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/platform/web/runtime/modules/events.js——updateDOMListeners
    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
    }
    // src/core/vdom/helpers/update-listeners.js——updateListeners
    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)
          }
        }
      }
    // src/platform/web/runtime/modules/events.js——add
    function add (
        name: string,
        handler: Function,
        capture: boolean,
        passive: boolean
      ) {
        // async edge case #6566: inner click event triggers patch, event handler
        // attached to outer element during patch, and triggered again. This
        // happens because browsers fire microtask ticks between event propagation.
        // the solution is simple: we save the timestamp when a handler is attached,
        // and the handler would only fire if the event passed to it was fired
        // AFTER it was attached.
        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)
            }
          }
        }
        // addEventListener
        target.addEventListener(
          name,
          handler,
          supportsPassive
            ? { capture, passive }
            : capture
        )
      }
    
    • 自定义事件

    位置:src/core/instance/events.js

    整体流程:patch() => createElm() => createComponent() => hook.init() => createComponentInstanceForVnode() => _init() =>initEvents() => updateComponentListeners()

    // src/core/vdom/create-element.js
    export function _createElement (
        context: Component,
        tag?: string | Class<Component> | Function | Object,
        data?: VNodeData,
        children?: any,
        normalizationType?: number
      ): VNode | Array<VNode> {
        if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
        // resolveAsset(context.$options, 'components', tag):当前组件实例的选项中有没有components的配置,里面有没有tag的配置
        // component
        // 首先查找自定义组件构造函数声明
        vnode = createComponent(Ctor, data, context, children, tag)
      }   
    }
    // src/core/vdom/create-component.js——createComponent
    export function createComponent (
        Ctor: Class<Component> | Function | Object | void,
        data: ?VNodeData,
        context: Component,
        children: ?Array<VNode>,
        tag?: string
    ): VNode | Array<VNode> | void {
        // 安装组件的钩子:
        installComponentHooks(data)
    }
    // src/core/vdom/create-component.js——installComponentHooks
    function installComponentHooks (data: VNodeData) {
        const hooks = data.hook || (data.hook = {})
        // 由于用户也有可能传递自定义钩子函数,所以需要合并一下
        for (let i = 0; i < hooksToMerge.length; i++) {
          const key = hooksToMerge[i]
          const existing = hooks[key]
          const toMerge = componentVNodeHooks[key]
          if (existing !== toMerge && !(existing && existing._merged)) {
            hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
          }
        }
    }
    // src/core/vdom/create-component.js——componentVNodeHooks
    const componentVNodeHooks = {
        // 初始化钩子用来创建组件实例和挂载的
        init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
          if (
            vnode.componentInstance &&
            !vnode.componentInstance._isDestroyed &&
            vnode.data.keepAlive
          ) {
            // kept-alive components, treat as a patch
            const mountedNode: any = vnode // work around flow
            componentVNodeHooks.prepatch(mountedNode, mountedNode)
          } else {
            const child = vnode.componentInstance = createComponentInstanceForVnode(
              vnode,
              activeInstance
            )
            child.$mount(hydrating ? vnode.elm : undefined, hydrating)
          }
        },
        prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
        insert (vnode: MountedComponentVNode) {},
        destroy (vnode: MountedComponentVNode) {}
    }
    // src/core/vdom/create-component.js——createComponentInstanceForVnode
    
    
    // src/core/instance/events.js——initEvents
    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)
        }
    }
    // src/core/instance/events.js——updateComponentListeners
    export function updateComponentListeners (
        vm: Component,
        listeners: Object,
        oldListeners: ?Object
     ) {
        target = vm
        updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
        target = undefined
     }
     function add (event, fn) {
      target.$on(event, fn)
     }
    // src/core/vdom/helpers/update-listeners.js——updateListeners
    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)
          }
        }
    }
    

    事件监听和派发者均是组件实例,自定义组件中一定伴随着原生事件的监听与处理

v-model双向绑定实现原理

编译阶段

  • 测试代码:
<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' }
        });
    </script>
</body>
  • v-model进行特殊处理
// 生成的渲染函数 (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)} })

// input——原生标签
_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
} }
})
// comp——自定义组件
// <comp value="foo" @input="">
_c('comp',{
    model:{
        value:(foo),
        callback:function (?v) {foo=?v},
        expression:"foo"
    }
})

整体流程

  • 初始化阶段:对节点赋值及事件监听
  • 对节点赋值:platforms\web\runtime\modules\dom-props.js
function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
    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
      }
    }
}
  • 事件监听:platforms\web\runtime\modules\events.js
function add (
  name: string,
  handler: Function,
  capture: boolean,
  passive: boolean
) {
    target.addEventListener(
    name,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}
  • 额外的model指令:platforms\web\runtime\directives\model.js
const directive = {
  inserted (el, binding, vnode, oldVnode) {
    if (vnode.tag === 'select') {
      // #6903
      if (oldVnode.elm && !oldVnode.elm._vOptions) {
        mergeVNodeHook(vnode, 'postpatch', () => {
          directive.componentUpdated(el, binding, vnode)
        })
      } else {
        setSelected(el, binding, vnode.context)
      }
      el._vOptions = [].map.call(el.options, getValue)
    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
      el._vModifiers = binding.modifiers
      if (!binding.modifiers.lazy) {
        el.addEventListener('compositionstart', onCompositionStart)
        el.addEventListener('compositionend', onCompositionEnd)
        // Safari < 10.2 & UIWebView doesn't fire compositionend when
        // switching focus before confirming composition choice
        // this also fixes the issue where some browsers e.g. iOS Chrome
        // fires "change" instead of "input" on autocomplete.
        el.addEventListener('change', onCompositionEnd)
        /* istanbul ignore if */
        if (isIE9) {
          el.vmodel = true
        }
      }
    }
  },

  componentUpdated (el, binding, vnode) {
    if (vnode.tag === 'select') {
      setSelected(el, binding, vnode.context)
      // in case the options rendered by v-for have changed,
      // it's possible that the value is out-of-sync with the rendered options.
      // detect such cases and filter out values that no longer has a matching
      // option in the DOM.
      const prevOptions = el._vOptions
      const curOptions = el._vOptions = [].map.call(el.options, getValue)
      if (curOptions.some((o, i) => !looseEqual(o, prevOptions[i]))) {
        // trigger change event if
        // no matching option found for at least one value
        const needReset = el.multiple
          ? binding.value.some(v => hasNoMatchingOption(v, curOptions))
          : binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, curOptions)
        if (needReset) {
          trigger(el, 'change')
        }
      }
    }
  }
}
  • 自定义组件会转换为属性和事件:core/vdom/create-component.js
// transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

转换完以后:

  • 自定组件事件监听:core\instance\events.js
function add (event, fn) {
  target.$on(event, fn)
}
  • 自定义组件可以指定v-model石建明和属性名
model:{
    prop:'foo',
    event:"change"
}

不同类型输入项编译结果和后续处理是不同的

_c('input', {
    directives: [{ name: "model", rawName: "v-model", value: (foo),
expression: "foo" }],
    attrs: { "type": "checkbox" },
    domProps: { "checked": Array.isArray(foo) ? _i(foo, null) > -1 :
(foo) }, on: {
        "change": function ($event) {
            var ?a = foo,
                ?el = $event.target,
                ?c = ?el.checked ? (true) : (false);
            if (Array.isArray(?a)) {
                var ?v = null, ?i = _i(?a, ?v);
                if (?el.checked) { ?i < 0 && (foo =
?a.concat([?v])) }
                else {
                    ?i > -1 && (foo = ?a.slice(0,
?i).concat(?a.slice(?i + 1)))
                }
            } else {
                foo = ?c
}
} }
})

总结

整体流程如下,大家可以对着去理解。

参考

详解vue的diff算法

深入剖析:Vue核心之虚拟DOM