Blog

数据驱动

time: 2019.11.11
author: heyunjiang

目录

1 根组件初始化
  1.1 原型初始化
  1.2 实例初始化
2 组件 $mount 方法执行 mount 主要包含了 render, update , watcher
3 组件 render - 生成 vnode tree
  3.1 vm.$createElement
  3.2 children 规范化
  3.3 生成 vnode 节点
4 组件 update - 渲染 vnode tree

说明

本章总结的数据驱动,也叫做 template -> vtree -> rtree 的一个转换过程

1 根组件初始化

// vue 构造函数关键代码
function Vue (options) {
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

this._init() 函数执行过程中,前面都是初始化我们 vue 组件中定义好的 lifecycle, events, render, injections, state, provide
在生命周期初始化之后,数据 injections 初始化之前,会触发生命周期 beforeCreate,在数据 provide 初始化完成之后,触发生命周期 created

从这里是否可以联想到每个组件的初始化过程?每个组件都有 mixin 初始化,状态 state 初始化,事件 event 初始化,生命周期初始化,渲染 render 初始化

在自己做了这么久的 vue 项目,随着对 vue 的使用越来越熟练,对其原理也想知道。这里总结一下 vue 初始化的过程,归纳一下 vue 对象各属性、特点,追求逐步达到全面掌控 vue 对象,做到对 vue 胸有成竹。

1.1 原型初始化

在构造函数定义时(实例化之前),执行了一系列的 mixin,这个给 Vue 对象本身及原型增加了哪些属性呢?

Vue 对象是基于函数,原型是基于 prototype

initMixin

  1. Vue.prototype._init 方法

stateMixin

  1. Vue.prototype.$data 属性,返回 vm._data
  2. Vue.prototype.$props 属性,返回 vm._props
  3. Vue.prototype.$set 方法
  4. Vue.prototype.$delete 方法
  5. Vue.prototype.$watch 方法,用于将 vue 对象绑定到一个 watcher 实例

eventsMixin

  1. Vue.prototype.$on
  2. Vue.prototype.$once
  3. Vue.prototype.$off
  4. Vue.prototype.$emit

lifecycleMixin

  1. Vue.prototype._update
  2. Vue.prototype.$forceUpdate
  3. Vue.prototype.$destroy

renderMixin

  1. Vue.prototype.$nextTick
  2. Vue.prototype._render

1.2 实例初始化

在 vue._init() 执行 实例化过程中,初始化了一系列 vue 属性

  1. vm._uid:vue 实例对象唯一标识
  2. vm._isVue:避免 vue 对象被响应式系统监听
  3. vm.$options:包含了常用组件配置属性、 vm.options._isComponent:是否是组件对象 vm.options.parent:父实例
  4. vm._self:指向自身

在生成基本属性之后,会执行下列属性、方法初始化方法

// 挂载 vm.$parent、vm.$root、vm.$children、vm.$refs、vm._watcher 属性,标识组件 _inactive、_isMounted 等状态属性
initLifecycle(vm) 
// 初始化 vm._events、vm._hasHookEvent 属性,处理父子组件绑定的事件
initEvents(vm)
// 初始化 vm._vnode、vm.$createElement,通过闭包绑定数据到当前 vm 对象
initRender(vm)
callHook(vm, 'beforeCreate')
// 初始化 inject 数据,并且在当前 vm 对象上 defineReactive 其为响应式数据
initInjections(vm) // resolve injections before data/props
// 初始化 vm._watchers, vm._props, vm._data, vm.computed, vm.methods,处理 watch 方法
initState(vm)
// 初始化当前 provide 对象,主要是解决函数绑定到 vm 问题
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

按顺序阅读下来之后,会发现

  1. 在处理数据之前,会执行 beforeCreate 方法,也就是说,我们在 beforeCreate 生命周期之前,是拿不到 this.data 数据的
  2. inject 数据是可以更改的,并且在当前组件为响应式数据,但是本身不会随着祖先原有数据变更而更改,所以其适用于高阶组件场景
  3. props 也是响应式的,当 prop 改变,也会出发组件更新
  4. computed 数据在本次并没有被赋值,在后续读取的时候才会去赋值

2 组件 $mount 方法执行

组件初始化之后,就会调用 $mount 方法,执行挂载。

源码路径 vue/src/core/instance/lifecycle.jsmountComponent 方法

  1. mountComponent 作为 Vue.prototype.$mount 的内部直接调用方法
  2. mountComponent 作为所有组件渲染的方法

mountComponent 方法中,主要是定义了 updateComponent 方法和实例化了一个 watcher 对象

let updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
}

这里只能看出 watcher 调用了 updateComponent 方法,那么它的作用是什么呢?

  1. 每个组件都会实例化一个 watcher 对象
  2. 调用 updateComponent 来执行 vm._render 生成 vnode
  3. 调用 vm._update 来 patch 渲染真实节点

那么它的 render 生成虚拟节点,update 更新虚拟节点是怎么操作的呢?它是如何 watch 数据变化的呢?

3 组件 render

在组件实例化的过程中,会调用 vm.$mount 方法渲染,在 $mount 方法过程中,具体又是采用 render 和 update 渲染和更新节点的。
在组件初始化的过程中,我们知道 vm._render 是通过 renderMixin 方法生成的。

源码路径 vue/src/core/instance/render.jsrenderMixin 方法,该方法定义了下列实例原型方法

  1. Vue.prototype.$nextTick
  2. Vue.prototype._render

下面是 _render 的实现过程

  1. 在 _render 方法中,会读取之前我们通过 compileToFunctions 方法生成的 vm.$options.render 方法

注意点:Vue.prototype._render 和 vm.$options.render 是2个不同名不同用的方法

_render 方法内部的核心实现,是调用了 vm.$options.render 方法,下面是核心代码

Vue.prototype._render = function () {
    const vm = this
    const { render, _parentVnode } = vm.$options

    vnode = render.call(vm._renderProxy, vm.$createElement)

    vnode.parent = _parentVnode
    return vnode
}
  1. 调用 render 方法生成虚拟节点
  2. 设置父节点
  3. 返回虚拟节点

可以看到 _render 是一个执行的过程,而具体生成虚拟节点,还是要分析 vm.$createElement 方法

前面扯了这么多,都是创建虚拟节点的一个过程准备,关键点在 createElement 方法实现

3.1 vm.$createElement

源码路径 vue/src/core/vdom/create-element.js_createElement 方法

先看输入输出定义
输入:context, tag, data, children, normalizationType
输出:vnode

  1. context 表示 vm 对象, function 实例
  2. normalizationType 表示如何规范化 children 对象,normalizeChildren + simpleNormalizeChildren 2种规范化方式, number
  3. data 表示标签上的属性集合,object
  4. children 表示后代集合,array
  5. tag: 标签名称,string
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
) {
  // 1 规范化
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }

  // 2 生成 vnode
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor, ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // always false, 为什么?
    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))) {
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

关键点:children 规范化 + 生成 vnode 节点

3.2 children 规范化

思考:在我们编写的 template 结构中,必然存在标签各种嵌套,在编译过后,每个节点的 _createElement 方法中,children 参数都表示它后代节点的集合。

问题:各个节点之间的关系怎么保存的呢?是保存为一个对象树吗?

children 的每一项可能类型

  1. 基本类型:string, number, symbol, boolean
  2. vnode 节点
  3. [] - 子节点为 functional component 会被编译成数组

先来看看规范化,源码路径 vue/src/core/vdom/helpers/normalize-children.js

// 普通数组扁平化 - 用于 render 函数编译生成,不处理 for, slot 等复杂结构,扁平化是为了处理 functional 组件
export function simpleNormalizeChildren (children) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children) // apply 要求所有函数参数都放到数组中
    }
  }
  return children
}

// 基础类型 或者 数组 - 用于处理 for, slot 等复杂结构的规范化,因为它内部的 normalizeArrayChildren 内部实现采用来递归
export function normalizeChildren (children) {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

// 伪代码
function normalizeArrayChildren (children, nestedIndex) {
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (Array.isArray(c)) {
      c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
    } else if (isPrimitive(c)) {
      res.push(createTextVNode(c))
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // 合并连续2个文本节点
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // 设置默认 key
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        res.push(c)
      }
    }
  }
}

simpleNormalizeChildren 和 normalizeChildren 2个处理后代类型多样化、层级次数不同

children 规范化的作用

  1. 将基本类型元素采用 createTextVNode 转换成标准 vnode text 节点
  2. 文本节点没有 key
  3. 连续文本节点合并
  4. 连续节点设置 key : c.key = `__vlist${nestedIndex}_${i}__`
  5. children 变成了 vnode array

3.3 生成 vnode 节点

前面知道了 children 规范化的作用,也就是把 children 统一成 vnode array。

  1. vnode 节点是如何具体生成的呢?
  2. 当前节点和父节点、children节点的关系如何关联?

createElement 部分代码

// 关键代码
let vnode, ns
if (typeof tag === 'string') {
  let Ctor, ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
  return vnode
} else if (isDef(vnode)) {
  if (isDef(ns)) applyNS(vnode, ns)
  if (isDef(data)) registerDeepBindings(data)
  return vnode
} else {
  return createEmptyVNode()
}

createComponent 部分源码

const baseCtor = context.$options._base
if (isObject(Ctor)) {
  // Vue.extend 表示合并新的配置,生成新的 Vue 函数对象,此刻未实例化
  Ctor = baseCtor.extend(Ctor)
}
if (isTrue(Ctor.options.functional)) {
  return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// 为普通组件注入 component hooks.init 等方法
installComponentHooks(data)
const vnode = new VNode(
  `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  data, undefined, undefined, undefined, context,
  { Ctor, propsData, listeners, tag, children },
  asyncFactory
)
return vnode

可以看到

  1. 在组件生成 vnode 时,普通组件是直接 new Vnode 对象返回
  2. 函数式组件的处理可以看 函数式组件 这章
  3. 通过 installComponentHooks 为组件 data 添加了 hooks,这个在后续 patch 时用到,用于组件的渲染和 destroy

生成 vnode 的3种方式

  1. new VNode: 如果传入的是普通 string
  2. createComponent: 如果传入的是一个对象,此刻包含了普通组件、函数式组件、内置 keep-alive 组件等
  3. createEmptyVNode: 生成一个空的 vnode

3.4 组件化

使用 createComponent 创建一个 vnode 节点实例,不同于普通字符串的 vnode,它代表的是一个组件
我们编写的组件,同样会被转成 createElement(),不同于普通元素,它拥有 context 环境,我们来看看是如何创建组件的呢?

组件化分析

4 组件 update

在生成 vnode 对象之后,就该 patch 渲染它了。
vm._update 的作用是将虚拟 vnode 渲染成真实 dom,先来看看关键代码

// vm._update 关键代码
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) {
    // 更新
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // 创建
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
}

要点: 生成 vm.$el 属性,关键方法 vm.__patch__()

4.1 patch 入口

// 生成实例 __patch__ 方法
Vue.prototype.__patch__ = inBrowser ? patch : noop

// 定义在 runtime 路径下的 patch 方法,本质还是调用的 core 下面的 createPatchFunction 方法
export const patch: Function = createPatchFunction({ nodeOps, modules })

// 浏览器 patch 关键源码 生成 $el 真实 dom 
// core/vdom/patch
export function createPatchFunction (backend) {
  function removeNode (el) {}
  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {}
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    let isInitialPatch = false
    const insertedVnodeQueue = []

    // 新建时直接创建
    if (isUndef(oldVnode)) {
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 更新操作
      const isRealElement = isDef(oldVnode.nodeType)
      // 同一个节点,新旧树对比
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 非同一节点,直接整体替换
        if (isRealElement) {
          oldVnode = emptyNodeAt(oldVnode)
        }

        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

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

patch 中,有3个关键方法:createElm, createComponent, patchVnode

4.2 createElm 入口

// createElm, 直接将节点插入父节点
function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  vnode.isRootInsert = !nested // for transition enter check
  // 如果是创建一个组件
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  // 创建的是一个 真实 dom
  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    createChildren(vnode, children, insertedVnodeQueue)
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
    }
    insert(parentElm, vnode.elm, refElm)
  } 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)
  }
}

createElm 会创建2种类型节点

  1. component: createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
  2. 普通节点: nodeOps.createElement(tag, vnode)

4.3 createComponent 渲染组件

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

patch.createComponent 作用

  1. 调用 componentVNodeHooks.init 钩子实例化组件对象,内部执行 vm.$mount 生成 dom tree
  2. 通过 initComponent 将 vnode.elm = vnode.componentInstance.$el, 并激活 created 生命周期钩子;将生成的 dom tree 插入 parent dom
  3. 如果是 keepAlive 子组件,则会触发 activate 钩子

而 patchVnode 的主要作用,就是更新节点,对比前后 vnode 进行更新,解析逻辑可以在响应式系统 dom diff 模块看到。
更新流程,主要是数据变动,由属性的 dep 对象通知关联的 watcher 对象进行 update 更新,再次生成 vnode 节点进行 patch 渲染。

总结

1 初次渲染时的一个过程

new Vue() -> vm._init() -> vm.$mount() -> compile() -> vm._render() -> vm._update()

vm._render: _render() -> createElement() -> children 标准化 -> new Vnode()
vm._update: vnode -> vm.__patch__() -> createElm() | patchVnode() -> DOM -> insert

关键函数源码对应路径

  1. _render: core/instance/render.js
  2. $createElement: core/vdom/create-element.js
  3. _update: core/instance/lifecycle.js
  4. patch: core/vdom/patch.js