数据驱动
- 数据驱动是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、重点流程
- 子节点的规范化
- 由于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
}
- 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操作。
本文深入探讨Vue的核心思想——数据驱动,详细分析Vue的挂载过程,包括Render和Update两个阶段。Vue的Render方法生成VNode,Update则将VNode渲染为真实DOM,涉及VNode的规范化、组件创建以及DOM操作等关键步骤。

527

被折叠的 条评论
为什么被折叠?



