Vue 经典面试题解析(二)

1. Vue 为什么需要虚拟 DOM

Virtual DOM 是用 js 对象描述真实 DOM,是对真实 DOM 的抽象描述。

Vue 中虚拟 DOM 的核心定义只有几个关键属性、标签、数据、子节点和键值,其他属性都是对 VNode 的扩展。

因为直接操作 DOM 性能比较低,将 DOM 转换为对象操作,再通过 diff 算法对比差异进行更新 DOM,可以减少对真实 DOM 的操作。

其次虚拟 DOM 还有跨平台的特性,它不依赖于真实平台环境,如果你了解过 MPVUE,肯定知道它只是根据 Vue.js 重新做了实现。

Vue.js 的虚拟 DOM 借鉴了开源库 snabbdom 的实现,然后加入了 Vue.js 特色的东西。

src/core/vdom/vnode.js

export default class VNode {
  // ...
  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 // 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
  }
}
javascript

src/vdom/create-element.js

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export 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)
}
javascript

Vue 在 vm._render 过程中会调用 createElement 方法来创建 VNode。

2. Vue 中 diff 算法原理

Vue 的 diff 算法是平级比较,不考虑跨级比较的情况。

其内部使用深度递归 + 双指针的方式进行比较。diff 算法肯定考虑的还是相同节点,否则会执行创建过程。

  1. 根据 key、tag 比较是否是相同节点(sameVnode 方法);
  2. 相同节点比较属性,并复用老节点(标签复用);
  3. 比较子节点,考虑老节点和新节点子节点的情况;
  4. 如果老节点和新节点都存在且字节点不同,进入核心 diff 算法逻辑。
    比较方式分为 头头、尾尾、头尾、尾头,如果上述情况都不满足,则创建映射表进行乱序比对。

Vue 3 采用最长递增子序列来实现 diff 算法优化。
单纯以算法比较,vue 和 vue3 性能提升并不明显,核心还是相同的逻辑。
Vue3 真正的性能提升在于编译的时候会添加大量标记来优化 diff 算法逻辑。

src/core/vdom/patch.js

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  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[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  // ...
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      // 组件卸载逻辑  
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []
     
    if (isUndef(oldVnode)) {
      // 组件挂载过程,不存在 oldVnode。
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // 非真实元素,并且 oldVnode 和 vnode 相同,这里就是主要的分析过程
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 如果是真实元素,说明是初始化过程,首次加载的时候会走这部分逻辑
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // 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)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}
javascript

src/core/vdom/patch.js

patchVnode()。

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // 如果节点一致,直接跳过。
  if (oldVnode === vnode) {
    return
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  const elm = vnode.elm = oldVnode.elm // 复用老节点
 
  // 如果是异步占位符,直接跳过
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }

  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  // 如果是静态节点,跳过。
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) && // 静态节点,key 相同。
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) // 克隆节点或者带有 once的节点
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  let i
  const data = vnode.data
  // 组件更新逻辑
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }
 
  // 获取子节点
  const oldCh = oldVnode.children
  const ch = vnode.children
  // 调用更新方法,更新类名、属性、样式、指令等
  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)
  }
  // 如果不是文本节点
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
       // 双方都有子节点,而且不是一个子节点,进入核心的 diff 算法过程 ☆
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      // 如果只有新节点存在子节点
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      // 清空老节点内容
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      // 添加新节点
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      // 如果只有老节点存在子节点,删除节点
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      // 如果老节点是文本,清空文本内容
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    // 如果老节点和新节点都是文本节点且文本不相同,直接赋值
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}
javascript

src/core/vdom/patch.js

updateChildren()。diff 算法核心逻辑。

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  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

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly

  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh)
  }

  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(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)
      // 通过 key 判断新节点是否映射表中
      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)) {
          // 如果 key 相同,并且节点相同,移动元素
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // 如果 key 相同,节点不同,创建新元素
          // 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)
  }
}
javascript

Vue2 diff 核心算法各分支逻辑可以参考下图。

头头比较

head2head.png

尾尾比较

tail2tail.png

头尾比较

head2tail.png

尾头比较

tail2head.png

乱序比较

out_order.png

3. vue 已经存在数据劫持,为什么还要使用虚拟 DOM 和 diff 算法

  1. Vue 通过数据劫持确实可以在数据发生变化时,通知视图渲染。但是如果给每个属性都添加 watcher,会产生大量的 watcher 从而降低性能。而且粒度过细也会导致更新不精确的问题,所以 Vue 采用了组件级的 watcher 配置 diff 来检测差异。
  2. 使用虚拟 DOM 配合 diff 算法可以对比差异进行更新 DOM,可以减少对真实 DOM 的操作。

4.  Vue 中 key 的作用和原理

Vue 在 patch 过程中通过 key 可以判断两个虚拟节点是否是相同节点(可以复用老节点)。

src/code/vdom/patch.js

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
javascript

没有 key 会导致更新的时候出现问题(不设置 key 时,key 默认是 undefined)。

no_key.png

has_key.png

尽量不要采用索引作为 key。

比如做一个 todoList,使用 unshift 添加任务到数组中,如果默认选中第一项,这是添加 N个 元素默认选中的都是第一项。
如果使用索引作为 key,会导致后续节点的值都需要更新。如果不使用索引,新节点只会执行一次插入操作。

5. 谈谈你对 vue 组件化的理解

  • 组件的特点:高内聚、低耦合、单向数据流;
  • 降低更新范围,只重新渲染变化的组件(一个组件对应一个 watcher);
  • 组件化开发能大幅提高应用开发效率、测试性、复用性等;
  • 常用组件化技术:属性、自定义事件、插槽等。

6. Vue 组件渲染流程

主要分为三部分。创建组件虚拟节点 -> 创建组件的真实节点 -> 插入到页面中。

component_render.png

7. Vue 组件更新流程

属性更新时会触发 patchVnode 方法 -> 组件虚拟节点会调用 prepatch 钩子 -> 更新属性 -> 组件更新。

组件更新有两种途径。内部数据更新时,组件会更新。传入的响应式属性更新时,组件也会更新。

这里主要分析父节点数据改变,触发 patch 过程从而导致子组件更新的流程。

component_update.png

8. Vue 中异步组件原理

创建方式

vue 有 3 种创建异步组件的方式。

普通异步组件

Vue.component('HelloWorld', function (resolve, reject) {
  require(['./components/HelloWorld'], resolve);
})
javascript

Promise 异步组件

Vue.component(
  'HelloWorld',
  // 该 import 函数返回一个 Promise 对象
  () => import('./components/HelloWorld.vue')
)
javascript

高级异步组件

const AsyncComp = () => ({
  // 需要加载的组件
  component: import('./components/HelloWorld.vue'),
  // 加载中应当渲染的组件
  loading: LoadingComp,
  // 出错时渲染的组件
  error: ErrorComp,
  // 渲染加载中组件前的等待时间。默认:200ms
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
});

Vue.component('HelloWorld', AsyncComp);
javascript

执行流程分析

这里以普通异步组件的创建为例。

默认渲染异步占位符节点 -> 组件加载完毕后调用 foreceUpdate 强制更新。

async_component_render.png

9. 函数组件的优势及原理

特性

函数式组件无状态、无生命周期、无 this,所以性能也高。

正常组件是一个类继承了 Vue,函数式组件就是普通的函数,没有 new 的过程,也没有初始化过程。

执行流程分析

Vue.component('func', {
  functional: true,
  render (h) {
    return h('div', 'yueluo');
  }
});
javascript

functional_component.png

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

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // ...

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }
  
  // 未执行到安装组件钩子函数的逻辑
  // install component management hooks onto the placeholder node
  installComponentHooks(data)
  // ....

  return vnode
}
javascript

10. Vue 组件传值的方式及区别

传值方式

  • props$emit 父组件向子组件传递数据是通过 prop 传递的,子组件向父组件传递数据是通过 $emit 触发事件来做到的;
  • $parent$children 获取当前组件的父组件和当前组件的子组件;
  • $attrs$listeners。Vue 2.4 开始提供 $attrs$listeners 来解决这个问题;
  • 父组件中通过 provide 来提供变量,然后在子组件通过 inject 来注入变量;
  • $refs 获取实例;
  • eventBus 平级组件数据传递。这种情况下可以使用中央事件总线的方式;
  • vuex 状态管理;
  • 。。。

原理分析

为了方便理解,先看下图。下图标注了常见的几种传值方式的源码所在位置。

component_translate_data.png

props 实现原理

<child-component a="1" b="2" c="3" @event01 @event02 @click.native></child-component>
javascript

src/core/vdom/create-component

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // ...
    
  // 处理组件属性(attrs,props)
  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)
  
  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  // 处理事件
  const listeners = data.on 
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  // 处理原生事件
  data.on = data.nativeOn

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  // 创建组件虚拟节点,包含组件的属性及事件(componentOptions)
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}
javascript

src/core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
     
    // ...
      
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      // 组件初始化
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    
    // ...
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    // 初始化属性
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
      
    // 组件初始化执行此处的 $mount    
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
javascript
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  // 组件属性
  opts.propsData = vnodeComponentOptions.propsData 
  // 组件监听器
  opts._parentListeners = vnodeComponentOptions.listeners
  // 组件子节点
  opts._renderChildren = vnodeComponentOptions.children
  // 组件标签名
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}
javascript

src/core/instance/state.js

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 初始化属性
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
javascript
function initProps (vm: Component, propsOptions: Object) {
  // 获取用户数据
  const propsData = vm.$options.propsData || {}
  // 创建 vm._props,组件的属性都放到 _props 中
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    // 如果是根元素,属性需要定义成响应式数据
    toggleObserving(false)
  }
  // 遍历用户传入的 props 
  // 将最终的值定义到 props 上
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    // 属性代理
    // _props  this.xxx => this._props.xxx
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}
javascript

$on$event

基本流程和 props 一致。内部使用订阅模式进行实现。

src/core/instance/init.js

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // 组件监听器,用户在组件上定义的事件
  opts._parentListeners = vnodeComponentOptions.listeners
}
javascript

src/core/instance/event.js

export function initEvents (vm: Component) {
  // 创建 _events 属性
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  // 获取当前所有事件
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}
javascript
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)
    }
  }
}

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  // $change="fn" => this.$on('change')
  // 更新组件事件
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}
javascript
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)
    }
  }
}
javascript

$parent$children

src/core/vdom/create-component.js

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)
  }
}
javascript

组件初始化时会调用此方法。

export function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  // 调用初始化过程
  return new vnode.componentOptions.Ctor(options)
}
javascript

src/core/instance/lifecycle.js

构建父子关系。vm.parent === vm.$parent。

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  // 排除抽象组件,查找父亲不是抽象组件,抽象组件不列入父子关系
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 让父组件记住该实例(子组件)
    parent.$children.push(vm)
  }
 
  // 增加 $parent 指向父组件 
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}
javascript

$attrs$listeners

<child-component a="1" b="2" c="3"></child-component> => $vnode.data.attrs = { a: 1, b: 2 }
javascript

src/core/instance/render.js

export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  // 获取占位符节点
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  // 获取占位符节点上的数据
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    // 非开发环境下处理
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    // 开发环境下处理
    // 定义响应式数据 $attrs、$listeners
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}
javascript

provide、inject

src/core/instance/inject.js

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    // 将用户自定义的 provide 挂载到 vm._provided
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}
javascript
export function initInjections (vm: Component) {
  // 顺着父子关系链条向上查找 _provided,直至返回结果  
  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 {
        // 把父节点的 peovide 的数据组,定义在当前组件的实例上
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}


export 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)) {
          // 查找 _provided 值,如果存在放到 result 中
          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
  }
}
javascript

$ref

src/core/vdom/modules/ref.js

DOM 创建更新删除时会调用一些方法,继而执行该钩子。

export default {
  create (_: any, vnode: VNodeWithData) {
    registerRef(vnode)
  },
  update (oldVnode: VNodeWithData, vnode: VNodeWithData) {
    if (oldVnode.data.ref !== vnode.data.ref) {
      registerRef(oldVnode, true)
      registerRef(vnode)
    }
  },
  destroy (vnode: VNodeWithData) {
    registerRef(vnode, true)
  }
}
javascript
export function registerRef (vnode: VNodeWithData, isRemoval: ?boolean) {
  // 获取 ref
  const key = vnode.data.ref
  if (!isDef(key)) return

  const vm = vnode.context
  // ref 等于 当前组件实例(组件) 或者 组件的真实节点(元素上)
  const ref = vnode.componentInstance || vnode.elm
  const refs = vm.$refs
  if (isRemoval) {
    // 删除 ref
    if (Array.isArray(refs[key])) {
      remove(refs[key], ref)
    } else if (refs[key] === ref) {
      refs[key] = undefined
    }
  } else {
    if (vnode.data.refInFor) {
      // <div v-for="item in 4"></div> => refs[xx] = [div, div, div, div]
      if (!Array.isArray(refs[key])) {
        // 如果在 v-for 里面,会把 ref 变成数组
        refs[key] = [ref]
      } else if (refs[key].indexOf(ref) < 0) {
        // $flow-disable-line
        refs[key].push(ref)
      }
    } else {
      refs[key] = ref
    }
  }
}
javascript

11. $attrs 是为了解决什么问题出现的

$attrs 主要的作用就是实现批量传递数据。

适用于不需要显式传递数据的场景。比如多个组件传递数据,中间的组件不需要数据,只起到传递的作用。

v-bind="$attrs"、v-on="$listeners"
javascript

provide/inject 更适合应用在插件中,主要是实现跨级数据传递。