栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > Web开发 > Vue.js

细谈 vue 核心- vdom 篇

Vue.js 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

细谈 vue 核心- vdom 篇

很早之前,我曾写过一篇文章,分析并实现过一版简易的 vdom。想看的可以点击 传送门

聊聊为什么又想着写这么一篇文章,实在是项目里,不管自己还是同事,都或多或少会遇到这块的坑。所以这里当给小伙伴们再做一次总结吧,希望大伙看完,能对 vue 中的 vdom 有一个更好的认知。好了,接下来直接开始吧

一、抛出问题

在开始之前,我先抛出一个问题,大家可以先思考,然后再接着阅读后面的篇幅。先上下代码




输入筛选后效果图如下

然后我在换一个关键词进行搜索,结果就会出现以下展示的问题

我并没有进行选择,但是 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

现在,我们举个例子,假如我需要解析下面文本


使用 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 = /^



我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号