写在前面

因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出。
文章的原地址:https://github.com/answershuto/learnVue
在学习过程中,为Vue加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习Vue源码的小伙伴有所帮助。
可能会有理解存在偏差的地方,欢迎提issue指出,共同学习,共同进步。

抽象Dom树

在刀耕火种的年代,我们需要在各个事件方法中直接操作Dom来达到修改视图的目的。但是当应用一大就会变得难以维护。

那我们是不是可以把真实Dom树抽象成一棵以javascript对象构成的抽象树,在修改抽象树数据后将抽象树转化成真实Dom重绘到页面上呢?于是虚拟Dom出现了,它是真实Dom的一层抽象,用属性描述真实Dom的各个特性。当它发生变化的时候,就会去修改视图。

但是这样的javascript操作Dom进行重绘整个视图层是相当消耗性能的,我们是不是可以每次只更新它的修改呢?所以Vue.js将Dom抽象成一个以javascript对象为节点的虚拟Dom树,以VNode节点模拟真实Dom,可以对这颗抽象树进行创建节点、删除节点以及修改节点等操作,在这过程中都不需要操作真实Dom,只需要操作javascript对象,大大提升了性能。修改以后经过diff算法得出一些需要修改的最小单位,再将这些小单位的视图进行更新。这样做减少了很多不需要的Dom操作,大大提高了性能。

Vue就使用了这样的抽象节点VNode,它是对真实Dom的一层抽象,而不依赖某个平台,它可以是浏览器平台,也可以是weex,甚至是node平台也可以对这样一棵抽象Dom树进行创建删除修改等操作,这也为前后端同构提供了可能。

VNode基类

先来看一下Vue.js源码中对VNode类的定义。

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
functionalContext: Component | void; // only for functional component root nodes
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
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? constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空间*/
this.ns = undefined
/*编译作用域*/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*静态节点标志*/
this.isStatic = false
/*是否作为跟节点插入*/
this.isRootInsert = true
/*是否为注释节点*/
this.isComment = false
/*是否为克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
} // DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next https://github.com/answershuto/learnVue*/
get child (): Component | void {
return this.componentInstance
}
}

这是一个最基础的VNode节点,作为其他派生VNode类的基类,里面定义了下面这些数据。

tag: 当前节点的标签名

data: 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息

children: 当前节点的子节点,是一个数组

text: 当前节点的文本

elm: 当前虚拟节点对应的真实dom节点

ns: 当前节点的名字空间

context: 当前节点的编译作用域

functionalContext: 函数化组件作用域

key: 节点的key属性,被当作节点的标志,用以优化

componentOptions: 组件的option选项

componentInstance: 当前节点对应的组件的实例

parent: 当前节点的父节点

raw: 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false

isStatic: 是否为静态节点

isRootInsert: 是否作为跟节点插入

isComment: 是否为注释节点

isCloned: 是否为克隆节点

isOnce: 是否有v-once指令


打个比方,比如说我现在有这么一个VNode树

{
tag: 'div'
data: {
class: 'test'
},
children: [
{
tag: 'span',
data: {
class: 'demo'
}
text: 'hello,VNode'
}
]
}

渲染之后的结果就是这样的

<div class="test">
<span class="demo">hello,VNode</span>
</div>

生成一个新的VNode的方法

下面这些方法都是一些常用的构造VNode的方法。

createEmptyVNode 创建一个空VNode节点

/*创建一个空VNode节点*/
export const createEmptyVNode = () => {
const node = new VNode()
node.text = ''
node.isComment = true
return node
}

createTextVNode 创建一个文本节点

/*创建一个文本节点*/
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}

createComponent 创建一个组件节点

 // plain options object: turn it into a constructor https://github.com/answershuto/learnVue
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
} // if at this stage it's not a constructor or an async component factory,
// reject.
/*Github:https://github.com/answershuto*/
/*如果在该阶段Ctor依然不是一个构造函数或者是一个异步组件工厂则直接返回*/
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
} // async component
/*处理异步组件*/
if (isUndef(Ctor.cid)) {
Ctor = resolveAsyncComponent(Ctor, baseCtor, context)
if (Ctor === undefined) {
// return nothing if this is indeed an async component
// wait for the callback to trigger parent update.
/*如果这是一个异步组件则会不会返回任何东西(undifiened),直接return掉,等待回调函数去触发父组件更新。s*/
return
}
} // resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor) data = data || {} // transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
} // extract 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
data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners
data = {}
} // merge component management hooks onto the placeholder node
mergeHooks(data) // return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }
)
return vnode
}

cloneVNode 克隆一个VNode节点

export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children,
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isCloned = true
return cloned
}

createElement

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode {
/*兼容不传data的情况*/
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
/*如果alwaysNormalize为true,则normalizationType标记为ALWAYS_NORMALIZE*/
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
/*Github:https://github.com/answershuto*/
/*创建虚拟节点*/
return _createElement(context, tag, data, children, normalizationType)
} /*创建虚拟节点*/
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode {
/*
如果data未定义(undefined或者null)或者是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()
}
/*如果tag不存在也是创建一个空节点*/
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// 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
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
/*获取tag的名字空间*/
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
/*tag不是字符串的时候则是组件的构造类*/
vnode = createComponent(tag, data, context, children)
}
if (isDef(vnode)) {
/*如果有名字空间,则递归所有子节点应用该名字空间*/
if (ns) applyNS(vnode, ns)
return vnode
} else {
/*如果vnode没有成功创建则创建空节点*/
return createEmptyVNode()
}
}

createElement用来创建一个虚拟节点。当data上已经绑定ob的时候,代表该对象已经被Oberver过了,所以创建一个空节点。tag不存在的时候同样创建一个空节点。当tag不是一个String类型的时候代表tag是一个组件的构造类,直接用new VNode创建。当tag是String类型的时候,如果是保留标签,则用new VNode创建一个VNode实例,如果在vm的option的components找得到该tag,代表这是一个组件,否则统一用new VNode创建。

关于

作者:染陌

Email:answershuto@gmail.com or answershuto@126.com

Github: https://github.com/answershuto

Blog:http://answershuto.github.io/

知乎专栏:https://zhuanlan.zhihu.com/ranmo

掘金: https://juejin.im/user/58f87ae844d9040069ca7507

osChina:https://my.oschina.net/u/3161824/blog

转载请注明出处,谢谢。

欢迎关注我的公众号

说说VNode节点(Vue.js实现)的更多相关文章

  1. 聊聊Vue.js的template编译

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:https://github.com/a ...

  2. Vue.js 2.0源码解析之前端渲染篇

    一.前言 Vue.js框架是目前比较火的MVVM框架之一,简单易上手的学习曲线,友好的官方文档,配套的构建工具,让Vue.js在2016大放异彩,大有赶超React之势.前不久Vue.js 2.0正式 ...

  3. vue.js插入dom节点的方式

    html代码: <div id="app"></div> js代码: var MyComponent = Vue.extend({ template: '& ...

  4. 从template到DOM(Vue.js源码角度看内部运行机制)

    写在前面 这篇文章算是对最近写的一系列Vue.js源码的文章(https://github.com/answershuto/learnVue)的总结吧,在阅读源码的过程中也确实受益匪浅,希望自己的这些 ...

  5. Vue.js之render函数基础

    刚才翻了一下博客,才发现,距离自己写的第一篇Vue的博客vue.js之绑定class和style(2016-10-30)已经过去一年零两天.这一年里,自己从船厂的普通技术员,成为了一个微型不靠谱创业公 ...

  6. vue.js框架原理浅析

    vue.js是一个非常优秀的前端开发框架,不是我说的,大家都知道. 首先我现在的能力,独立阅读源码还是有很大压力的,所幸vue写的很规范,通过方法名基本可以略知一二,里面的原理不懂的地方多方面查找资料 ...

  7. Vue.js 2.x笔记:指令(4)

    1. 内置指令 指令是Vue.js 中一个重要的特性,主要提供了一种机制将数据的变化映射为DOM 行为. Vue.js 本身提供了大量的内置指令来进行对DOM 的操作,同时可以开发自定义指令. 2. ...

  8. 你应该要知道的vue.js

    前言 小组同事最近都在学习前端,目前我们小组前端技术栈主要是vue.在和同事交流过程成,发现他们对vue都不了解,所以整理了问的比较多的问题. 组件data为什么必须是函数? 因为组件可能被多处使用, ...

  9. Vue.js入门系列教程(二)

    过滤器:filter 全局过滤器 <!DOCTYPE html> <html lang="en"> <head> <meta charse ...

随机推荐

  1. 关于Vuex的初步使用

    store.js文件中定义各个访问状态和方法 import Vue from "vue" import Vuex from "vuex" Vue.use(Vue ...

  2. Django实现组合搜索

    一.实现方法 1.纯模板语言实现 2.自定义simpletag实现(本质是简化了纯模板语言的判断) 二.基本原理 原理都是通过django路由系统,匹配url筛选条件,将筛选条件作为数据库查询结果,返 ...

  3. Python3之数据类型

    1.基本数据类型数字类型整型 int浮点型 float布尔型 bool: True==1.False==0复数类型 complex算术运算符 + - * / // % **赋值运算符 += -= *= ...

  4. P1144 最短路计数

    P1144 最短路计数 题目描述 给出一个N个顶点M条边的无向无权图,顶点编号为1-N.问从顶点1开始,到其他每个点的最短路有几条. 输入输出格式 输入格式: 输入第一行包含2个正整数N,M,为图的顶 ...

  5. linux 中nvme 的中断申请及处理

    /** * struct irq_desc - interrupt descriptor * @irq_data: per irq and chip data passed down to chip ...

  6. Mysql中的force index和ignore index

    前几天统计一个sql,是一个人提交了多少工单,顺便做了相关sql优化.数据大概2000多w. ) c order by c desc; 为了实验最少受其他因素干扰,将生产库的200多w数据导出来,用测 ...

  7. springMVC中@RequestParam和@RequestBody注解的用法

    springMVC中@RequestParam注解用在Controller层获解析.提取参数,当然你也可以用request.getParameter("name")来获取参数,而@ ...

  8. HTTP基本知识

    1.TCP/IP 传输控制协议/因特网互联协议 (1)应用层:决定向用户提供应用服务时通信的活动(FTP.DNS和HTTP都属于该层). (2)传输层:提供处于网络连接中的两台计算机之间的数据传输(T ...

  9. Python实现XML文件解析

    1. XML简介 XML(eXtensible Markup Language)指可扩展标记语言,被设计用来传输和存储数据,已经日趋成为当前许多新生技术的核心,在不同的领域都有着不同的应用.它是web ...

  10. 不免费的PacMan

    课程内容介绍: 本套课程适合以下人士: - 免费资料没教会你游戏开发的: - 学了Unity基础不知道怎么用在游戏项目里的: - 想快速开发一款好玩的游戏的: - 想学游戏不知道如何入门的: - 对游 ...