很早之前,我曾写过一篇文章,分析并实现过一版简易的 vdom。想看的可以点击 传送门
聊聊为什么又想着写这么一篇文章,实在是项目里,不管自己还是同事,都或多或少会遇到这块的坑。所以这里当给小伙伴们再做一次总结吧,希望大伙看完,能对 vue 中的 vdom 有一个更好的认知。好了,接下来直接开始吧
一、抛出问题在开始之前,我先抛出一个问题,大家可以先思考,然后再接着阅读后面的篇幅。先上下代码
{{ item.label }}
输入筛选后效果图如下
然后我在换一个关键词进行搜索,结果就会出现以下展示的问题
我并没有进行选择,但是 select 选择框中展示的值却发生了变更。老司机可能一开始看代码,就知道问题所在了。其实把 option 里面的 key 绑定换一下就OK,换成如下的
{{ item.label }}
那么问题来了,这样可以避免问题,但是为什么可以避免呢?其实,这块就牵扯到 vdom 里 patch 相关的内容了。接下来我就带着大家重新把 vdom 再捡起来一次
开始之前,看几个下文中经常出现的 API
- isDef()
export function isDef (v: any): boolean %checks {
return v !== undefined && v !== null
}
- isUndef()
export function isUndef (v: any): boolean %checks {
return v === undefined || v === null
}
- isTrue()
export function isTrue (v: any): boolean %checks {
return v === true
}
二、class VNode
开篇前,先讲一下 VNode ,vue 中的 vdom 其实就是一个 vnode 对象。
对 vdom 稍作了解的同学都应该知道,vdom 创建节点的核心首先就是创建一个对真实 dom 抽象的 js 对象树,然后通过一系列操作(后面我再谈具体什么操作)。该章节我们就只谈 vnode 的实现
1、constructor首先,我们可以先看看, VNode 这个类对我们这些使用者暴露了哪些属性出来,挑一些我们常见的看
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array,
text?: string,
elm?: Node,
context?: Component
) {
this.tag = tag // 节点的标签名
this.data = data // 节点的数据信息,如 props,attrs,key,class,directives 等
this.children = children // 节点的子节点
this.text = text // 节点对应的文本
this.elm = elm // 节点对应的真实节点
this.context = context // 节点上下文,为 Vue Component 的定义
this.key = data && data.key // 节点用作 diff 的唯一标识
}
2、for example
现在,我们举个例子,假如我需要解析下面文本
This is a vnode.
使用 js 进行抽象就是这样的
function render () {
return new VNode(
'div',
{
// 静态 class
staticClass: 'vnode',
// 动态 class
class: {
'show-node': isShow
},
// 等同于 directives 里面的 v-show
show: isShow,
[ new VNode(undefined, undefined, undefined, 'This is a vnode.') ]
}
)
}
转换成 vnode 后的表现形式如下
{
tag: 'div',
data: {
show: isShow,
// 静态 class
staticClass: 'vnode',
// 动态 class
class: {
'show-node': isShow
},
},
text: undefined,
children: [
{
tag: undefined,
data: undefined,
text: 'This is a vnode.',
children: undefined
}
]
}
然后我再看一个稍微复杂一点的例子
v-for="n in 5" :key="n">{{ n }}
假如让大家使用 js 对其进行对象抽象,大家会如何进行呢?主要是里面的 v-for 指令,大家可以先自己带着思考试试。
OK,不卖关子,我们现在直接看看下面的 render 函数对其的抽象处理,其实就是循环 render 啦!
function render (val, keyOrIndex, index) {
return new VNode(
'span',
{
directives: [
{
rawName: 'v-for',
name: 'for',
value: val
}
],
key: val,
[ new VNode(undefined, undefined, undefined, val) ]
}
)
}
function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array {
// 仅考虑 number 的情况
let ret: ?Array, i, l, keys, key
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
return ret
}
renderList(5)
转换成 vnode 后的表现形式如下
[
{
tag: 'span',
data: {
key: 1
},
text: undefined,
children: [
{
tag: undefined,
data: undefined,
text: 1,
children: undefined
}
]
}
// 依次循环
]
3、something else
我们看完了 VNode Ctor 的一些属性,也看了一下对于真实 dom vnode 的转换形式,这里我们就稍微补个漏,看看基于 VNode 做的一些封装给我们暴露的一些方法
// 创建一个空节点
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
// 创建一个文本节点
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
// 克隆一个节点,仅列举部分属性
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children,
vnode.text
)
cloned.key = vnode.key
cloned.isCloned = true
return cloned
}
捋清楚 VNode 相关方法,下面的章节,将介绍 vue 是如何将 vnode 渲染成真实 dom
三、render 1、createElement在看 vue 中 createElement 的实现前,我们先看看同文件下私有方法 _createElement 的实现。其中是对 tag 具体的一些逻辑判定
- tagName 绑定在 data 参数里面
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
- tagName 不存在时,返回一个空节点
if (!tag) {
return createEmptyVNode()
}
- tagName 是 string 类型的时候,直接返回对应 tag 的 vnode 对象
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
- tagName 是非 string 类型的时候,则执行 createComponent() 创建一个 Component 对象
vnode = createComponent(tag, data, context, children)
- 判定 vnode 类型,进行对应的返回
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
// namespace 相关处理
if (isDef(ns)) applyNS(vnode, ns)
// 进行 Observer 相关绑定
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
createElement() 则是执行 _createElement() 返回 vnode
return _createElement(context, tag, data, children, normalizationType)
2、render functions
i. renderHelpers
这里我们先整体看下,挂载在 Vue.prototype 上的都有哪些 render 相关的方法
export function installRenderHelpers (target: any) {
target._o = markonce // v-once render 处理
target._n = tonumber // 值转换 Number 处理
target._s = toString // 值转换 String 处理
target._l = renderList // v-for render 处理
target._t = renderSlot // slot 槽点 render 处理
target._q = looseEqual // 判断两个对象是否大体相等
target._i = looseIndexOf // 对等属性索引,不存在则返回 -1
target._m = renderStatic // 静态节点 render 处理
target._f = resolveFilter // filters 指令 render 处理
target._k = checkKeyCodes // checking keyCodes from config
target._b = bindObjectProps // v-bind render 处理,将 v-bind="object" 的属性 merge 到VNode属性中
target._v = createTextVNode // 创建文本节点
target._e = createEmptyVNode // 创建空节点
target._u = resolveScopedSlots // scopeSlots render 处理
target._g = bindObjectListeners // v-on render 处理
}
然后在 renderMixin() 方法中,对 Vue.prototype 进行 init 操作
export function renderMixin (Vue: Class) {
// render helps init 操作
installRenderHelpers(Vue.prototype)
// 定义 vue nextTick 方法
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {
// 此处定义 vm 实例,以及 return vnode。具体代码此处忽略
}
}
ii. AST 抽象语法树
到目前为止,我们看到的 render 相关的操作都是返回一个 vnode 对象,而真实节点的渲染之前,vue 会对 template 模板中的字符串进行解析,将其转换成 AST 抽象语法树,方便后续的操作。关于这块,我们直接来看看 vue 中在 flow 类型里面是如何定义 ASTElement 接口类型的,既然是开篇抛出的问题是由 v-for 导致的,那么这块,我们就仅仅看看 ASTElement 对其的定义,看完之后记得举一反三去源码里面理解其他的定义哦
declare type ASTElement = {
tag: string; // 标签名
attrsMap: { [key: string]: any }; // 标签属性 map
parent: ASTElement | void; // 父标签
children: Array; // 子节点
for?: string; // 被 v-for 的对象
forProcessed?: boolean; // v-for 是否需要被处理
key?: string; // v-for 的 key 值
alias?: string; // v-for 的参数
iterator1?: string; // v-for 第一个参数
iterator2?: string; // v-for 第二个参数
};
iii. generate 字符串转换
- renderList
在看 render function 字符串转换之前,先看下 renderList 的参数,方便后面的阅读
export function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array {
// 此处为 render 相关处理,具体细节这里就不列出来了,上文中有列出 number 情况的处理
}
- genFor
上面看完定义,紧接着我们再来看看,generate 是如何将 AST 转换成 render function 字符串的,这样同理我们就看对 v-for 相关的处理
function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for // v-for 的对象
const alias = el.alias // v-for 的参数
const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' // v-for 第一个参数
const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' // v-for 第二个参数
el.forProcessed = true // 指令需要被处理
// return 出对应 render function 字符串
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
- genElement
这块集成了各个指令对应的转换逻辑
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) { // 静态节点
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) { // v-once 处理
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // v-for 处理
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // v-if 处理
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) { // template 根节点处理
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') { // slot 节点处理
return genSlot(el, state)
} else {
// component or element 相关处理
}
}
- generate
generate 则是将以上所有的方法集成到一个对象中,其中 render 属性对应的则是 genElement 相关的操作,staticRenderFns 对应的则是字符串数组。
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`, // render
staticRenderFns: state.staticRenderFns // render function 字符串数组
}
}
3、render 栗子
看了上面这么多,对 vue 不太了解的一些小伙伴可能会觉得有些晕,这里直接举一个 v-for 渲染的例子给大家来理解。
i. demo
v-for="n in 5" :key="n">{{ n }}
这块首先会被解析成 html 字符串
let html = `
{{ n }}
`
ii. 相关正则
拿到 template 里面的 html 字符串之后,会对其进行解析操作。具体相关的正则表达式在 src/compiler/parser/html-parser.js 里面有提及,以下是相关的一些正则表达式以及 decoding map 的定义。
const attribute = /^s*([^s"'<>/=]+)(?:s*(=)s*(?:"([^"]*)"+|'([^']*)'+|([^s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\w\-\.]*'
const qnameCapture = `((?:${ncname}\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^s*(/?)>/
const endTag = new RegExp(`^<\/${qnameCapture}[^>]*>`)
const doctype = /^]+>/i
const comment = /^
热门相关搜索
路由器设置
木托盘
宝塔面板
儿童python教程
心情低落
朋友圈
vim
双一流学科
专升本
我的学校
日记学校
西点培训学校
汽修学校
情书
化妆学校
塔沟武校
异形模板
西南大学排名
最精辟人生短句
6步教你追回被骗的钱
南昌大学排名
清朝十二帝
北京印刷学院排名
北方工业大学排名
北京航空航天大学排名
首都经济贸易大学排名
中国传媒大学排名
首都师范大学排名
中国地质大学(北京)排名
北京信息科技大学排名
中央民族大学排名
北京舞蹈学院排名
北京电影学院排名
中国戏曲学院排名
河北政法职业学院排名
河北经贸大学排名
天津中德应用技术大学排名
天津医学高等专科学校排名
天津美术学院排名
天津音乐学院排名
天津工业大学排名
北京工业大学耿丹学院排名
北京警察学院排名
天津科技大学排名
北京邮电大学(宏福校区)排名
北京网络职业学院排名
北京大学医学部排名
河北科技大学排名
河北地质大学排名
河北体育学院排名



