众所周知,Vue.js 是 MVVM 框架结构,其特色在于核心重点关注视图层,而虚拟节点(以下简称 VNode)是这一核心的重要支柱(另一根支柱我觉得是实现响应式框架用到的 Watcher)。下面我们稍微深挖一下 VNode 的“背景”。
一、初识
首先看下 VNode 的类定义:
/**
* tag - 标签类型,例如 p、div
* data - 标签上的数据,例如 style、class、data-*
* children - 顾名思义,子节点
* text - 文本内容
* elm - 虚拟节点绑定的真实 DOM 节点
* context - 一般是 Vue 实例
* componentOptions - 父组件传给子组件的属性
*/
var VNode = function VNode(tag, data, children, text, elm, context, componentOptions, asyncFactory) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.ns = undefined;
this.context = context;
this.key = data && data.key;
this.parent = undefined;
this.raw = false;
this.componentOptions = componentOptions;
// ... 还有些属性我就不列出来了,大家可以自行去看源码
};
从定义可以看出,VNode 纯粹是对 View 层的映射,仅有属性,没有方法。
接着看下 VNode 对节点的解释方式:以简单节点 <p>Hello</p>
为例,其会被解释成2个 VNode 的。一个是 tag
类型为 p
,但没有 text
值的节点,下文称为标签节点;另一个是没有 tag
类型,但是有 text
值的节点,下文称为文本节点。
再来了解下 VNode 的解析流程,看下面的代码
<div id="app">
<h1 class="h1" style="color:red;" data-id="1">Hello world</h1>
<div id="wrap">
<p id="text1">日期:2017-11-9</p>
<p id="text2">时间戳:1510196747299</p>
</div>
</div>
大家不妨可以先考虑下,这个页面结构会被拆成多少个节点,再来看下面各个 VNode 的创建顺序:
- 文本节点
Hello world
- 标签节点
h1
- 文本节点
日期:2017-11-9
- 标签节点
p#text1
- 文本节点
时间戳:1510196747299
- 标签节点
p#text2
- 标签节点
div#wrap
- 标签节点
div#app
从上面的列表可以看出 Vue 对节点的解析是自上而下,从内到外解析的。那么当页面结构中含有 component 会如何解析呢?样例我就不写了,直接给个结果:Vue 会先解析主文档,然后解析组件,而父文档中的组件位置就是组件解析完之后的挂载点。
二、Q & A
1. Q:为什么VNode的解析顺序是自上而下的,从内到外的
A:因为当你写的 HTML 经过 parseHTML 的方法后会产生类似如下的函数
/** * _s = toString 方法 * _v = 创建文本节点 * _c = 创建标签节点 */ function f() { with (this) { return _c( 'div', { attrs: { "id": "app" } }, [ _ c( 'h1', { staticClass: "h1", staticStyle: { "color": "red" }, attrs: { "data-id": "1" } }, [_v(_s(message))] ) ] ) } }
正好符合JS的解析顺序。同时该方法会被缓存起来,下次有节点更新直接调用就好,一定程度上提高了页面渲染的性能。
2. Q:节点上的 v-on
、v-bind
等指令是什么时候绑定的
A:在
parseHTML
过程的processAttrs
方法中
三、阅读源码后的一点感想
- 需要比较扎实的正则能力,
parseHTML
中的元素剥离就是靠的正则 - 了解ES2015的新API,例如 proxy,defineProperty 等,这也是 Vue 不支持旧版浏览器的原因
- 闭包知识,常常一个方法被包了3层之多
- 1万多行的代码里面,好多都是常量定义和具方法,如果剥离出来,可能会更方便阅读
网友评论