开始之前,我们先回顾执行过程图:
- Vue 执行工程 简单了解了整个过程;
- 模板编译 详聊了生成 render 函数的过程;
- reactive 详聊了中间环节,数据的依赖收集和派发更新过程;
由于 _render
和 _update
的过程还是挺复杂的,分成两期。这一期我们就先聊聊 _render
过程。
VNode
如果对于虚拟 DOM
还不是,可以跳转到 认识虚拟DOM。这一节详细聊聊 render
生成 VNode
的过程。上一篇讲解 reactive 时,提到了一些小矮人:
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}
通过下面栗子 ? 去了解其中几个:
<div id="app">
<Child a="hello vue" @click="handleClick"></Child>
<ul>
<li v-for="item of list" :key="item.id">
{{ item.name }}
</li>
</ul>
</div>
<script>
let Child = Vue.extend({
name: 'Child',
props: {
a: String
},
template: `<div id="child">
<span @click="$emit('click')">{{ a }}</span>
</div>`
})
new Vue({
el: '#app',
components: {
Child
},
data() {
return {
list: [{
name: 'A',
id: 'A'
}, {
name: 'B',
id: 'B'
}, {
name: 'C',
id: 'C'
}, {
name: 'D',
id: 'D'
}]
};
},
methods: {
handleClick () {
console.log('click event');
}
}
})
</script>
? 生成的 render
函数如下:
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('child', {
attrs: {
"a": "hello vue"
},
on: {
"click": handleClick
}
}), _v(" "), _c('ul', _l((list), function(item) {
return _c('li', {
key: item.id
}, [_v("\n " + _s(item.name) + "\n ")])
}))], 1)
}
首先看 _l
的逻辑,对应的函数 renderList
定义在 src/core/instance/render-helpers/render-list.js
:
export function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array<VNode> {
let ret: ?Array<VNode>, i, l, keys, key
// 数组
if (Array.isArray(val) || typeof val === 'string') {
ret = new Array(val.length)
// 遍历数组,去生成 VNode
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i)
}
// 骚东西,单一个数字也是能够遍历的
} else if (typeof val === 'number') {
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
// 遍历对象
} else if (isObject(val)) {
keys = Object.keys(val)
ret = new Array(keys.length)
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i]
// 第一个参数是值,第二个参数是对象key,第三个参数是数组位置
ret[i] = render(val[key], key, i)
}
}
if (isDef(ret)) {
(ret: any)._isVList = true
}
return ret
}
? 中执行 renderList
时参数中的 render
就是下面方法:
function (item) {
return _c('li', { key: item.id }, [
_v("\n " + _s(item.name) + "\n ")
])
}
从里往外看,首先 _s
比较简单,就是 toString
函数:
export function toString (val: any): string {
return val == null
? ''
: typeof val === 'object'
? JSON.stringify(val, null, 2)
: String(val)
}
然后是 _v
,对应的函数是 createTextVNode
,方法定义在 core/vdom/vnode
:
/**
* @file 虚拟节点定义
*/
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
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
// 对应的真实dom
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
// 标记是原生HTML还是普通文本
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
}
/**
* 获取实例
*/
get child (): Component | void {
return this.componentInstance
}
}
/**
* 创建文本VNode
*/
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
然后我们看到比较复杂的 _c
也就是 createElement
函数,定义在 src/core/instance/render.js
:
// 写 template 调用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
// 手写 render 函数调用
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
createElement
位于 src/core/vdom/create-element.js
:
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 满足data是一个数组或者基础类型时,说明传的是children参数,需要后移一位
// 参数个数的统一性处理
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// 手写 render 这里传进来的是 true,下面会举个?过一遍
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
函数先判断 data
是不是一个数组或者基本类型,是的话就将参数后移一位。自己写 render
函数的时候,如果没有属性相关的配置时,我们可以第二个参数就写 children
,举个 ?:
/**
* @demo 手写 render 函数的 ?
*/
new Vue({
el: '#app',
render (h) {
return h('div', [
h('span', 'hello vue!')
]);
}
})
上面 ? 中 h('span', 'hello vue!')
第二个参数不是数组也不是基础类型,会进入到参数向后移一位的逻辑,即最后会调用 _createElement(context, 'span', undefined, 'hello vue!', 2)
。
处理完参数之后,会调用 _createElement
:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
/**
* 如果传递data参数且data的__ob__已经定义(代表已经被observed,上面绑定了Oberver对象),
* https://cn.vuejs.org/v2/guide/render-function.html#约束
* 那么创建一个空节点
*/
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
// 用在<component is="...">
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
// 如果没有标签名,创建空节点
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
// 默认作用域插槽
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 规范化子组件参数,手写render函数会进入第一个分支
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
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
// 是的话创建相应的节点
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// 从vm实例的option的components中寻找该tag,存在则就是一个组件,创建相应节点,Ctor为组件的构造类
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)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
// 如果有名字空间,则递归所有子节点应用该名字空间
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
// 如果vnode没有成功创建则创建空节点
return createEmptyVNode()
}
}
接着看手写 render
的 ?, 会执行到 normalizeChildren
:
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
export function normalizeChildren (children: any): ?Array<VNode> {
// 如果是基本类型孩子,创建文本VNode,否则调用normalizeArrayChildren处理
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
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]
// 节点c是数组,递归调用
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)
}
// 节点c是基础类型,则通过 createTextVNode 方法转换成 VNode 类型
} 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
}
normalizeArrayChildren
函数主要对3种情况做处理:
- 孩子
c
是数组,递归调用normalizeArrayChildren
; - 孩子
c
是普通类型,创建文本VNode
处理; - 孩子
c
已经是VNode
类型:这里又有两种情况,如果孩子children
是嵌套数组,那么会自动定义key
,否则就创建文本VNode
。
以上三种情况都有一个共同的处理,就是通过 isTextNode
判断如果前后节点都是文本 VNode
,会合并两个节点。并且最终都会返回一个 VNode
数组。
回到第一个 ? 中的 _c
函数来分析组件和保留标签 VNode
生成的情况,先看组件:
_c('child', {
attrs: {
"a": "hello vue"
},
on: {
"click": handleClick
}
})
Child
组件的 VNode
会直接执行 isDef(Ctor = resolveAsset(context.$options, 'components', tag))
这部分代码,栗子 ? 中的 context.$options
打印出来是:
![](/Users/apple/Documents/vue源码系列/[咖聊] patch 与 diff/context.$options.jpg)
这个 options
是在 app
执行 _init
时赋值的,这部分逻辑位于 src/core/instance/init.js
:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
然后再看看 resolveAsset
,这个函数位于src/core/util/options.js
:
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// check local registration variations first
// 先直接使用 id 拿
if (hasOwn(assets, id)) return assets[id]
// id 变成驼峰的形式再拿
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
// 在驼峰的基础上把首字母再变成大写的形式再拿
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
return res
}
resolveAsset
通过各种变换(驼峰写法、首字母大写驼峰写法),然后找到组件的定义。然后通过 createComponent
生成组件 VNode
:
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
}
// 构造子组件构造函数,在 initGlobalAPI 时定义
const baseCtor = context.$options._base // => Vue
// plain options object: turn it into a constructor
// 我们平时写组件都是 export default { ... },这里会包一层 Vue.extend,可见 Vue 对于一些细节的处理是多么的到位,不管是 export default Vue.extend({}) 还是 export default {} 都是可以正常运行的
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
// 如果在该阶段Ctor不是一个构造函数或者是一个异步组件工厂直接返回
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// ...省略异步组件工厂
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
// 根据组件的options定义,提炼 VNodeData 中存在于组件 props 属性
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
// 函数式组件,无状态,无实例
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// 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
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
// 安装组件钩子函数
// 把 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程中执行相关的钩子函数,patch 过程再回到这里来讲
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
// 实例化 vnode
// 区别普通元素的VNode构造,组件的VNode是没有传children的
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// 省略WEEX相关逻辑
return vnode
}
对于栗子 ? 中的 Child
组件,获取并处理构造函数、处理 VNode
属性,安装组件钩子,最后生成组件 VNode
:
了解完组件 VNode
,再看看 HTML
和 SVG
(⚠️ 这里还包含 SVG
哦!)。
保留标签的情况,本文用 li
标签作为 ?。跟 Child
一样的过程就不陈述了,执行到 _createElement
时会执行下面分支:
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
}
? 中 li
生成的 VNode
:
总结
执行 _render
时,会拉起各种小矮人函数去生成 VNode
,我们着重介绍了 _l
和 _c
这俩小家伙。_c
分成写 template
和写 render
函数两种情况,处理完参数之后调用 _createElement
,这个函数主要做了两件事:规范子元素和生成 VNode
。生成 VNode
又有两种情况:内置标签和组件。组件 VNode
通过 createComponent
生成,该函数做了三件事:
Ctor
-> 生成构造器;installComponentHooks
-> 安装组件钩子;new VNode
-> 实例化组件VNode
;
代码中还有很多分支处理,比如异步工厂函数、动态组件的处理等。这些都不是主流程,可以遇到问题再回头看(偷懒 ?)。有了 VNode
,下一节细品 update
过程——VNode
生成 DOM
和面试必考题 Diff
。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!