Vue核心思想-数据驱动的实现分析

本文深入探讨Vue的核心思想——数据驱动,详细分析Vue的挂载过程,包括Render和Update两个阶段。Vue的Render方法生成VNode,Update则将VNode渲染为真实DOM,涉及VNode的规范化、组件创建以及DOM操作等关键步骤。
数据驱动
  • 数据驱动是Vue的核心思想之一,指视图由数据驱动生成,通过修改数据来实现了对视图的修改,而非直接操作DOM。
  • DOM变成了数据的映射,我们把重点放在了数据的逻辑处理上,提高了开发效率。
  • VirtualDOM就是一个js对象去描述一个DOM节点,在Vue中是用VNode这个class去描述。
Vue的挂载过程

挂载的目标就是把模板渲染成为最终的DOM。
主要是分为 render 和 update 两个过程。
$mount () -> mountComponent () -> vm._render () + vm._uptate ()

1、Render

Vue的_render方法是vue实例的一个私有方法,作用是生成一个VNode。
(src/core/instance/render.js)
在 initRender () 中定义了两种方法,vm._c ⽅法,它是被模板编译成的 render 函数使⽤,⽽vm.$createElement 是⽤户⼿写 render ⽅法使⽤的, 这俩个⽅法⽀持的参数相同, 并且内部都调⽤了 createElement ⽅法

export function initRender (vm: Component) {
  // 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)
  }
}

_createElement ()
1、参数

export function _createElement (
  context: Component, //VNode的上下文环境
  tag?: string | Class<Component> | Function | Object, //标签 字符串类型、Component类型/
  data?: VNodeData, //VNode的数据
  children?: any,//VNode的子节点,为任意类型
  normalizationType?: number//规范化类型
): VNode | Array<VNode> { ....  }

2、重点流程

  1. 子节点的规范化
  • 由于Virtual DOM是树状结构,一个VNode都可能有很多个子节点,这些子节点也应该是VNode类型。 要将类型为any的 children 规范为VNode类型。
  • 根据 normalizationType 的不同分别调用 normalizeChildren 和 simpleNormalizeChildren。
  • 其中simpleNormalizeChildren 是用Array.prototype.concat 将数组拉平为一维数组,深度只有一层。
// 1. When the children contains components - because a functional component
// 返回的是一个数组而不是一个根节点
// normalization is needed - if any child is an Array, we flatten the whole 返回的是一个数组而不是一个根节点
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep  
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}
  • 当子节点包含生成嵌套数组的构造函数时
  • 当编译 template, slot, v-for 这些标签或者是用户自定义的render函数
// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user   
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

如果是子节点一个基础类型,就调用createTextVNode 创建一个文本节点的VNode。如是一个数组类型就调用 normalizeArrayChildren

normalizeArrayChildren()

  • 参数是子节点 和 嵌套的索引
  • 该方法的主要逻辑就是遍历子节点,取到单个节点 c ,接着判断 c 的类型,如果是数组就递归处理这个节点;如果是一个基础类型,则通过createTextVNode方法转换为VNode类型;否则就是VNode类型。
  • 经过对子节点的规范化处理, 原来类型为any的children就成了一个VNode节点集合。
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
      	//递归调用
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // merge adjacent text nodes
        //合并相邻的文本节点为一个文本节点
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        res.push(c)
      }
    }
  }
  return res
}
  1. VNode的创建
  • 判断tag标签类型,如果是string类型,就接着判断是否是html内置的一些节点标签,是的话vnode就赋值为一个普通的VNode;如果是Component类型,则调用createComponent 创建组件类型的VNode节点;其他的是创建一个未知标签的VNode节点。
 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 = 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)
  }
2、uptate
  • 由render生成的VNode节点之后需要update 把 VNode 渲染成真实的 DOM,
  • Vue 的 _update 是实例的⼀个私有⽅法, 它被调⽤的时机有 2 个, ⼀个是⾸次渲染, ⼀个是数据更新
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
	....
	if (!prevVnode) {
	      // initial render 初次渲染
	      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates  更新
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
   ....
}
  • update的核心就是 vm.patch 方法
  • Vue.prototype.__patch__ = inBrowser ? patch : noop 但在非浏览器环境下不需要渲染。在浏览器环境下方法指向了patch方法(src/platforms/web/runtime/patch.js)。
/* @flow */
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
  • 该方法的定义是返回一个createPatchFunction(),传入两个参数 nodeOps 和 modules ,nodeOps是封装了一些操作dom的方法, modules定义了一些模块的钩子函数。
  • patch是有平台相关的,在Web和Weex环境下,把虚拟DOM映射到对应真实DOM的方法是不同的,并且对DOM包括的属性模块创建和更新也不尽相同。因此每个平台都有自己的 nodeOps 和 modules ,代码分别托管在 src/platforms 下。
/**
@param  oldVnode  旧的VNode节点、可以不存在也可以是一个dom对象
@param  vnode  是render后的VNode节点
@param  hydrating  是否是服务器端渲染。
@param  removeOnly
**/

return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(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)) {
        // 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)
        }
      }
    }
  • createElm 的作用是通过虚拟节点创建真实的DOM并插入到它的父节点中。
  • 它的⼀些关键逻辑, createComponent ⽅法⽬的是尝试创建⼦组件, 这个逻辑在之后组件的章节会详细介绍, 在当前这个 case 下它的返回值为 false;接下来判断 vnode 是否包含 tag, 如果包含, 先简单对tag 的合法性在⾮⽣产环境下做校验, 看是否是⼀个合法标签;然后再去调⽤平台 DOM 的操作去创建⼀个占位符元素。
 vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
  • 接下来调用createChildren 创建子元素。createChildren 是遍历虚拟子节点,递归调用createElm。
  • 接着再调⽤ invokeCreateHooks⽅法执⾏所有的 create 的钩⼦并把 vnode push 到insertedVnodeQueue 中。
  • 最后调用 insert方法将 DOM节点插入到父节点中。insert方法主要是调用nodeOps的一些方法
function insert (parent, elm, ref) {
    if (isDef(parent)) {
      if (isDef(ref)) {
        if (nodeOps.parentNode(ref) === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        nodeOps.appendChild(parent, elm)
      }
    }
  }
  • 而这些nodeOps 封装的辅助方法都定义在各自平台(src/platforms/web/runtime/node-ops.js)中。如
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}
export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

其实就是调用原生api进行DOM操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值