@ctsj/vue v1.0.0
概述
如今,前端技术日新月异,发展速度之迅猛,各种各样的库层出不穷,起初从DoJo、jQuery、requirejs、jQueryUI、jQueryEasyUI、EXTJS等前端库风靡一时,在html,js和css技术的生态体系下,前端的开发模式从jsp、php和asp的服务器端渲染发展到了web2.0的ajax,前后端分离的开发模式也应运而生,从此前端和后端的职责越来越清晰,前端之前大部分都是操作DOM,jQuery库也成为了当时事实的标准库,围绕jQeury库的生态体系也是越来越强大,当时的js库大部分都是基于jQuery开发的,比较著名的jQueryUI、jQueryEasyUI和BootStrap也是开发项目的首选库,当时前端的构建也是基于一些exe工具,或者说有的工程都没有进行压缩合并的操作,随着nodejs技术的兴起,前端开发者也能去开发服务端,但是效果并不是很好,和javaweb比起来,nodejs还是没有找到自己的位置,各个方面都处于下风,可以说想撼动java的地位是不可能的,所以nodejs把重点放到了中间件和前端工程化上,当时比较成功的前端工程化库就是Grunt和gulp,也成为了当时前端工程化必不可少的2个选择,再谈一谈当时前端的模板技术,当时前端的模板技术都是单方向的模板技术都是基于指定表达式替换,如jsp的<%%>和freemark,struct1和struct2的标签库这个是web1.0中的模板库,web2.0总的模板库有jsrender,和underscores的template等,但是这些模板库都只是单方向的变量到字符串的形式,也就是我们常说的mv(模型到视图),随着Backbone.js的出现mvvm的思想得以在浏览器端实现,但是Backbone.js没有大红大紫,原因是因为React的出现,React提出的概念就是mvvm和虚拟dom的概念,起初出现只有小部分公司在使用,之后大厂纷纷使用,同时React的生态环境也是越来越强大,Redux和React-Router,babel,在配合Webpack的全家桶的出现,使开发React更加便捷和可靠,这个以nodejs和es6作为工程化开发的模式孕育而生,可以说这种模式的出现,完全颠覆了前端的传统开发模式,很多前端都必须从原始的开发模式转到这种nodejs模式,在随着Vue的出现彻底把这种模式推向了一个至高点,也称成为了现在开发前端事实的标准模式,时代在改变,技术在发展,作为一个老的程序员来说,在接受新的事物的同时也要看清事物本质,只有看清本质才能利于不败之地,才能跟得上时代的发展,老话说的好,万变不离其宗,所以就有了想彻底弄清楚mvvm和虚拟dom本质的想法,所以想从react和vue这两个框架中选取一个进行模拟的编写来更加深入的d对mvvm进行了解,左思右想之后最终选择了Vue框架来进行模拟,原因是Vue的模板渲染更接近于Web的原始技术,数据的改变也更能让人理解,除了这点之外模拟编写这个库也能带来其他的收获,比如可以巩固和深入es和dom的相关知识,磨练自身的意志,最后这也是一个成需要的追求。
目录
所用的知识点
- ES6的Proxy
- eval函数
- with语句
- Function构造函数
- DOM相关操作
- Object.definePropertys
- es基础知识
- Snabbdom(虚拟DOM)
模拟的内容
- Vue实例
- {{}}表达式的解析
- v-bind
- v-on
- v-for
- v-html
- v-if
- v-else
- v-else-if
- v-show
- v-model
- 生命周期
- methods
- computed
- watch
- 组件(Component)
- 组件的注册(全局和局部)
- props
- slot(缺省和作用域)
- template标签
- component(动态组件)标签
- ref和$refs
- $root和$parent
- $emit
- mixin
- vue-router的模拟
- router-link
- router-viewer
- 路由解析
- 导航方法
路由守卫
没有模拟的内容
- filter
- 组件上使用v-model
- 方法的异步方法中多次修改data只渲染一次
关键技术点
- 模板解析技术
- 数据观测(data observer)
- 虚拟dom技术
- 生命周期方法、methods和watch中多次修改数据,只执行一次渲染
- 在proxy中如何判断数组的异动,是增、删和改的操作
- v-for怎么解析
- v-for或v-for嵌套中怎么创建新的作用域
- 如果在指定作用域中执行字符串表达式
- v-if、v-else-if和v-else怎么解析
- 怎么能保留访问的路径字符串,可以在watch中使用
- 组件怎么判断是挂载还是更新
- props如何不能修改
- 怎么实现组件注册中支持2种形式的注册
- 怎么实现$emit的功能
- 父亲更新,和组件自身更新,怎么渲染
Vue相关
Vue实例的建立
首先我们需要建立一个文件index.js,这个文件里应该定义一个Vue的类。
/**
* Vue
* @class Vue
* @classdesc Vue实例
*/
class Vue {
constructor(config) {
// 保存Vue实例的配置
this.$config = config;
// 获取Vue配置中的el实际对象,el可以是HtmlElement或String
this.$config.el = getEl(this.$config.el);
// 将data混入到this中
mergeData.call(this);
// 将computed混入到this中
mergeComputed.call(this);
// 将methods混入到this中
mergeMethods.call(this);
// 创建template(模板)的el对象templateEl,一个Vue实例只建立一次
this.templateEl = createElement(this.$config.template);
// 渲染
render.call(this, this.$config.el, true);
}
}
/**
* getEl - 根据Vue实例配置中的el获取实际的el对象
* 因为VNode渲染的原因它会替换掉原始的el节点,所以要自己创建一个渲染子节点用来被替换
* @param elConfig - HtmlElement | String
* @return HTMLElement
*/
function getEl(elConfig) {
let el;
if (elConfig instanceof HTMLElement) {
el = elConfig;
} else if (typeof elConfig === 'string') {
el = document.querySelector(elConfig);
}
return el;
}
上面的代码的注释已经写的非常清晰,首先是保存Vue实例配置到$config中,获取el配置的DOM元素,再将data、computed个methods中的属性混入到Vue实例的this中,然后根据template模板字符串生成templateEl,最后调用render方法对模板进行解析后插入到el中这么一个过程。
模板解析基础
Vue的核心之一就是模板的解析,首先我们要清楚模板解析都需要解析哪些内容,针对Vue的模板来说,需要解析的内容应该有三种,第一种“{{}}”表达式、第二种“指令”、第三种“Vue中自带的标签”,模板解析完的结果应该就是对应的HtmlDOM元素(也可以是虚拟DOM元素),模板解析的关键技术在于怎样把模板字符串转换成一个可以迭代的数据结构,并且可以对这个结构进行迭代操作,在迭代的过程中根据元素类型的不同分别进行解析。
迭代
首先需要做的是把模板字符串转换成一个可以迭代的数据结构。
/**
* createElement - 根据html字符串创建dom
* @param htmlStr - string
* @return {Element}
*/
function createElement(htmlStr) {
const el = document.createElement('div');
el.innerHTML = htmlStr;
return el.firstElementChild;
}
接下来就是迭代这个数据结构分类型解析的过程,首先获取的数据结构是一个DOMTree,所以选择深度优先遍历这种方式可以对Tree进行遍历,js中还有TreeWalker可以对DOM节点进行遍历。这里我使用了自行实现的方式。
/**
* renderLoop - 进行递归的渲染
* @param context - 上下文对象
* @param el - HtmlElement 当前节点的el
* @param parentVNode - VNode 父节点VNode
* @param parentElement - HtmlElement 父元素
* @return {VNode | Array<VNode>}
*/
function renderLoop({ context, el, parentVNode, parentElement }) {
// 文本节点
if (isTextNode(el)) {
// 文本节点的渲染
return renderTextNode.call(this, { context, el });
}
let isComponent = false;
const isVueIns = isVueInstance(this);
// this是否是vue实例
if (isVueIns) {
// 在vue实例下判断是否是组件节点
isComponent = isComponentNodeByVue(el);
}
// 其他的情况
else {
const isComponentIns = isComponentInstance(this);
// this是否是component实例
if (isComponentIns) {
// 在component实例下判断是否是组件节点
isComponent = isComponentNodeByComponent(el, this.$getComponentsConfig());
}
// this既不是vue实例也不是component实例
else {
return null;
}
}
if (!isComponent) {
// 如果是template元素
if (isTemplateNode(el)) {
return renderTemplateNode.call(this, { context, el, parentVNode, parentElement });
}
// 如果是slot元素 vue实例没有slot元素
if (!isVueIns && isSlotNode(el)) {
return renderSlotNode.call(this, { context, el, parentVNode, parentElement });
}
// 如果是component元素
if (isDynamicComponentNode(el)) {
return renderDynamicComponentNode.call(this, { context, el, parentVNode, parentElement });
}
// 如果是router-link元素
if (isRouterLinkNode(el)) {
return renderRouterLinkNode.call(this, { context, el, parentVNode, parentElement });
}
// 如果是router-view元素
if (isRouterViewNode(el)) {
return renderRouterViewNode.call(this, { context, el, parentVNode, parentElement });
}
// 如果是Html元素
if (isElementNode(el)) {
// 是元素不是组件节点
return renderElementNode.call(this, { context, el, parentVNode, parentElement });
}
} else {
return renderComponentNode.call(this, { context, el, parentVNode, parentElement });
}
return null;
}
这段代码首先是判断节点的类型,分为文本节点和元素节点,而元素节点又分为HTML元素节点、组件节点和Vue提供的标签节点。首先判断文本节点使用了isTextNode方法来判断。
/**
* isTextNode - 是否是文本节点
* @param el - Node
* @return {boolean}
*/
function isTextNode(el) {
return el.nodeType === Node.TEXT_NODE;
}
/**
* isElementNode - 是否是元素节点
* @param el - Element
* @return {boolean}
*/
function isElementNode(el) {
return el.nodeType === Node.ELEMENT_NODE;
}
这个函数应该是解析模板的关键函数,根据判断不同节点类型从而进行不同节点的解析,我们还需要知道一点,就是什么时候进行递归操作,应该是元素类型是HTML元素的时候才进行深度优先遍历的递归操作,因为文本节点,组件节点和Vue提供的标签节点按理来说都应该是叶子节点。
// loop children
for (let i = 0, len = el.childNodes.length; i < len; i++) {
// 继续调用renderLoop
const VNodes = renderLoop.call(this, {
context,
el: el.childNodes[i],
parentVNode: VNode,
parentElement: el,
});
if (!VNodes) continue;
// v-for返回的
if (isArray(VNodes)) {
VNodes.filter((n) => n).forEach((n) => {
VNode.children.push(n);
});
} else if (isObject(VNodes)) {
VNode.children.push(VNodes);
}
}
最后这个renderLoop方法有context,parentVNode和parentElement这几个参数,在这里大家先不要关心,之后会讲到这几个参数的含义。
虚拟DOM
刚才说到了模板解析的结果应该是一个HTMLDOM,然后将这个HTMLDOM插入到Vue实例配置对象el所代表的元素中,应该是如下的一个操作
// 1.解析模板字符串->HTMLDOM
const templateEl = renderLoop(el);
// 2.获取Vue实例config中el所代表的HTMLDOM元素,并将其插入到el中
// 先清空el的内容
el.innerHTML = '';
// 在添加
el.appendChild(templateEl);
上面的代码就是一个从模板解析到插入目标元素的过程,我们应该预想到一点,就是模板解析不是只解析一次,而是随着数据的变化多次调用模板解析方法,也就是上面的操作会随着数据的更改而执行很多次,如果每一次都是这样处理的话,效率上肯定非常低,因为先需要清空,在插入的操作,大家也知道操作DOM的开销是巨大的,而使用innerHTML的开销更大,所以就需要使用虚拟DOM技术来实现整个渲染过程。虚拟DOM这块我们有选择自己弄,原因是在网上找了一些虚拟DOM相关的文章,原理很简单,但实现起来很困难,其实原理很简单,就是对2个Tree的数据结构进行比较,比较出增量,那增量是什么呢,增量就是对Tree的增、删和改的操作,增量中可能包含所有的操作,这个比较的过程是很复杂的,React用的diff算法,Vue应该是自己实现的一套比较算法,经我查阅资料Vue是仿照snabbdom这个库实现的虚拟DOM,我模拟的时候也使用了这个库,那模板解析后的结果就不应该是一个HTMLDOM了,应该是一个虚拟DOM的节点,虚拟DOM节点的结构也非常简单。
{
// 元素名称
sel: 'div',
data: {
// 样式表
class: {},
// HTML特性
props: {},
// 除了特性外的属性
attrs: {},
// data-set
dataset: {},
// 内联样式
style: {},
// 事件
on: {},
// 钩子
hook: {},
},
// 孩子节点
children: [],
// 文本节点内容
text: 'Hello, World',
// 对应的DOM元素
elm: Element,
key: undefined,
}
我也大致了解了一下虚拟DOM比较的一些算法,因素非常多,核心的比较应该是逐层比较,每一个节点的比较,也很复杂,比如说属性的值改了没有,有没有新增或删除的属性,孩子是否有修改和增加删除等,比较的时候还和一个属性key有关系,如果大家想了解具体细节,可以参看snabbdom和virtual-dom的具体代码实现。
接下来模板解析的代码应该就变成如下所示:
// 1.解析模板字符串->VNode
const vnode = renderLoop(el);
// 挂载
if (isMount) {
// 如果是挂载,则用vnode初始化el元素,返回值是一个与真是DOM对用的虚拟DOM节点
this.$preVNode = patch(el, vnode);
}
// 更新
else {
if (!this.$preVNode) {
this.$preVNode = vnode;
}
// 如果是更新,$preVNode是修改之前的虚拟DOM节点,vnode是新的虚拟DOM节点,对两个节点进行比较来找 // 出增量从而对el进行更新
this.$preVNode = patch(this.$preVNode, vnode);
}
{{}}表达式和文本节点的解析
先来实现一个最简单的功能,来解析{{}}表达式从而开启我们模板解析的第一步,{{}}种表达式应该都在文本节点中,所以我们就实现一下renderTextNode这个方法,思路就是在文本节的内容字符串中寻找"{{"和"}}"之间的内容,并解析出这个表达式的值即可。
/**
* renderTextNode - 渲染文本节点
* @param context - 上下文对象
* @param el - HtmlElement
* @return {TextVNode}
*/
export function renderTextNode({ context, el }) {
// 表达式
const expression = el.textContent.trim();
let index = 0;
let value = '';
while (index < expression.length) {
// 这里的START_TAG是{{
// 这里的END_TAG是}}
const startIndex = expression.indexOf(START_TAG, index);
if (startIndex !== -1) {
const endIndex = expression.indexOf(END_TAG, startIndex + START_TAG.length);
if (endIndex !== -1) {
const dfs = expression.substring(startIndex + START_TAG.length, endIndex);
value += expression.substring(index, startIndex) + execExpression.call(this, context, dfs);
index = endIndex + END_TAG.length;
} else {
value += expression.substring(index);
break;
}
} else {
value += expression.charAt(index++);
}
}
// 创建文本节点的虚拟DOM
return createTextVNode(value);
}
/**
* createTextVNode - 创建文本节点的虚拟DOM
* @param value
* @return Object
*/
export function createTextVNode(value) {
return {
text: value,
};
}
上方的代码应该不难理解,就是寻找到所有的"{{"和"}}"之间的内容,把这些内容解析之后拼接在一起生成一个替换后的文本字符串,分隔符之间的内容应该是一个表达式,这个表达式的内容里应该包含data中的数据,比如:
new Vue({
template: `<div>{{'Hello Word' + msg}}</div>`,
data() {
return {
msg: 'end',
}
}
});
现在的问题就是怎么去解析'Hello Wrod' + msg这个表达式,首先这是一个字符串表达式,javascriot中能解析字符串表达式的方式我能想到就是eval函数,eval函数的参数就是js字符串,作用域应该和js中正常的作用域一样,那现在如果我用eval执行这个表达式,如下:
/**
* execExpression - 执行字符串表达式
* @param expressionStr - {String} 表达式
* @return {any}
*/
function execExpression(expressionStr) {
return eval(expressionStr);
}
const dfs = "'Hello Word' + msg";
execExpression(dfs);
如果按照上面的代码执行会报错,msg变量未定义,这个msg变量应该是Vue实例中this的属性,因为这msg的作用域是全局作用域,所以在全局作用域中没有找到msg变量,dfs这个字符串应该在哪个作用域中执行呢,应该在Vue实例的作用域中执行才可以,所以这里面就用到了with这个运算符,这个运算符的作用就是使代码能在指定上下文(这里的上下文一般都是对象)中执行,那上方的代码就需要修改一下。
/**
* execExpression - 执行字符串表达式
* @param context - {Object} with指代的上下文对象
* @param expressionStr - {String} 表达式
* @return {any}
*/
function execExpression(context, expressionStr) {
return eval(`with(context){${expressionStr}}`);
}
const dfs = "'Hello Word' + msg";
execExpression(this,dfs);
这样就可以在context中找到msg这个变量了,这个execExpression函数现在只是一个最简单的版本,在之后的解析当中可能会遇到更复杂的情况,之后会对这个函数进行修改。
HtmlElement元素的解析
刚才实现了文本节点中{{}}的解析,接下来我觉得应该解析HTML元素节点更为合适,因为不管是组件节点,还是Vue提供的标签节点都是基于文本节点和HTML元素节点的解析为基础的,那我们接下来就实现一下renderElementNode这个方法。
/**
* renderElementNode - 渲染元素节点
* @param context - Object 上下文对象
* @param el - HtmlElement el元素
* @param parentVNode - VNode 父元素VNode
* @param parentElement - HtmlElement 父元素
* @return {VNode | Array<VNode>}
*/
function renderElementNode({ context, el, parentVNode, parentElement }) {
// 合并多个文本节点为一个文本节点
el.normalize();
// 解析指令属性
/*let { Continue, VNode } = renderVAttr.call(this, {
el,
parentVNode,
parentElement,
context,
renderFun: renderElementNode,
});
if (!Continue) return VNode;*/
// 如果没有VNode,创建一个
if (!VNode) {
VNode = createVNode(el.tagName.toLowerCase());
}
// 解析非指令属性
renderAttr.call(this, { el, VNode });
// 处理一下option这种情况
/*if (el.tagName.toLowerCase() === 'option' && parentVNode && parentElement) {
parseOption.call(this, { context, VNode, parentElement });
}*/
// loop children
for (let i = 0, len = el.childNodes.length; i < len; i++) {
const VNodes = renderLoop.call(this, {
context,
el: el.childNodes[i],
parentVNode: VNode,
parentElement: el,
});
if (!VNodes) continue;
// v-for返回的
if (isArray(VNodes)) {
VNodes.filter((n) => n).forEach((n) => {
VNode.children.push(n);
});
} else if (isObject(VNodes)) {
VNode.children.push(VNodes);
}
}
return VNode;
}
/**
* createVNode - 使用snabbdom创建一个虚拟DOM的元素节点
* @param tagName - String 元素名称
* @return {VNode}
*/
export function createVNode(tagName) {
return h(
tagName,
{
class: {},
props: {},
attrs: {},
dataset: {},
style: {},
on: {},
hook: {},
},
[],
);
}
/**
* getAttrNames 获取非指令的属性名
* @param el - HtmlElement 元素
* @return {NamedNodeMap}
*/
function getAttrNames(el) {
return el
.getAttributeNames()
// 这里的DIRECT_PREFIX表示v-
.filter((attrName) => attrName.indexOf(DIRECT_PREFIX) === -1 && attrName !== GROUP_KEY_NAME);
}
/**
* toCamelCase - 用连接符链接的字符串转换成驼峰写法
* 例:abc-def AbcDef
* @param str - string 用连接符节点的字符串
* @param toUpperCase - boolean 是否转换成大写
* @return {String}
*/
function toCamelCase(str, toUpperCase = false) {
const result = str
// DIRECT_DIVIDING_SYMBOL 是 '-'
.split(DIRECT_DIVIDING_SYMBOL)
.map((item) => item.charAt(0).toUpperCase() + item.substring(1))
.join('');
return !toUpperCase ? `${result.charAt(0).toLowerCase()}${result.substring(1)}` : result;
}
/**
* renderAttr - 渲染非指令属性
* @param el - HtmlElement 元素的el
* @param VNode - VNode
*/
export function renderAttr({ el, VNode }) {
const self = this;
const attrNames = getAttrNames(el);
if (attrNames.length) {
attrNames.forEach((attrName) => {
const val = el.getAttribute(attrName);
// key属性
if (attrName === 'key') {
VNode.key = val;
}
// ref属性
else if (attrName === 'ref') {
// ref属性不放入到VNode中
// 创建当前VNode的hook
Object.assign(VNode.data.hook, {
/**
* insert - 元素已插入DOM
* @param vnode
*/
insert: (vnode) => {
// 保存HtmlElement的el到$refs中
self.$refs[val] = vnode.elm;
},
});
}
// style属性
else if (attrName === 'style') {
// VNode.data.style[attrName] = val;
val.style
.split(STYLE_RULE_SPLIT)
.filter((t) => t)
.forEach((style) => {
const entry = style.split(STYLE_RULE_ENTRY_SPLIT).filter((t) => t);
VNode.data.style[entry[0]] = entry[1];
});
}
// class属性
else if (attrName === 'class') {
// class属性的值需要使用/\s{1,}/这个正则表达式进行分割
const classNames = val.trim().split(CLASSNAME_SPLIT);
classNames.forEach((className) => {
VNode.data.class[className] = true;
});
}
// data-*属性
else if (attrName.startsWith('data-')) {
VNode.data.dataset[toCamelCase(attrName.substring('data-'.length))] = val;
}
// 其他的属性
else {
VNode.data.attrs[attrName] = val;
}
});
}
}
上方代码有两处注释掉了,一处是对指令的解析,一处是对select的option的解析,这两块在下面的章节中会介绍,此处可以忽略,首先说一下el.normalize()这个方法,这个方法会将多个文本节点合并成一个文本节点,这样就会减少后续迭代的次数,那html元素节点我们应该解析哪些内容呢,说白了就是解析元素的属性,此处暂时不去解析指令属性,那除了指令属性之外,剩下的就是非指令属性了。首先创建一个虚拟的DOM元素节点,然后在获取el的非指令属性的集合,根据获取的属性值分别进行处理,这里处理了ref、key、style、class和data-,剩余的就直接赋值到虚拟DOM的attrs中即可,data-开头的属性需要赋值到虚拟DOM的dataset属性中,这里需要说一点,data-set的值在html也就是模板中是以Pascal方式命名的,但是虚拟DOM的dataset的属性需要camel方式,所以这个地方需要把pascal转换成camel的字符串,key需要赋值到key属性中,ref这块在后面再说,剩下就要说一下style和class这两个属性了,class属性的值需要用“/\s{1,}/”这个正则表达式进行分割,分割出来多个样式表的值,复制到class属性中,这里的class属性是一个对象。
VNode.data.class = {
['样式名称']: true,
...// 多个
}
style属性的值首先需要用“/\s;\s/”这个正则表达式进行分割,也是就是;分割后是多条样式规则,在对每一个样式规则使用“/\s:\s/”这个正则表达式进行分割,分割出规则名和规则值,复制到VNode.data.style属性中,这里的style属性也是一个对象。
VNode.data.style = {
['规则名']: '规则值',
...// 多个
}
解析完非指令属性之后就是迭代下钻的过程了。至此元素节点暂且解析完成。
指定的初探
接下来我认为应该考虑的是指令解析了,刚才的代码里面注释掉了指令解析的部份,那我们在这里就先讨论一下指令解析这块的技术实现,首先要明确的就是指令以什么方式存在,指令都包含有些部组成,指令的作用是什么,用过Vue的人都知道指令就是元素的属性,指令只能以元素的属性存在,这里面的元素代指的是HTML元素,组件元素,Vue提供的标签元素,以上这些元素中都可以存在指令,既然说指令是元素的属性,那它和HTML的其他属性又有什么区别呢,区别就在于指令是我们人为定义的html属性,术语上应该叫做attr,Vue的指令都已v-开头,指令包含“指令名称(name)”、“传给指令的参数(arg)”、“一个包含修饰符的对象(modifiers)”、“字符串形式的指令表达式(expression)和指令的绑定值(value)”这几部分组成,指令也是Vue模板中的一个重要概念,此图是截取VUE官网。
指令的解析就是对这些指令属性的解析,在不同的标签上指令有着不同的功能。
v-bind指令解析
现在讨论一下在HTML元素上解析v-bind这个指令。有如下代码所示,也就是上面在解析元素节点的方法中注释掉的renderVAttr方法,这个方法很长,所有的指令解析都在这个方法中,此时我只把解析v-bind指令的代码先给出,其余指令的解析在下面会逐个讲解。
/**
* getVAttrNames 获取所有指令的属性名
* @param el - HtmlElement 元素
* @return {NamedNodeMap}
*/
function getVAttrNames(el) {
// 这里的DIRECT_PREFIX是"v-"
return el.getAttributeNames().filter((attrName) => attrName.startsWith(DIRECT_PREFIX));
}
/**
* hasVAttr - 查看attrName是否是指令属性
* @param attrNames Array<string> 所有属性的集合
* @param attrName string 属性的名称
* @return {boolean}
*/
function hasVAttr(attrNames, attrName) {
return attrNames.some((itemAttrName) => itemAttrName.startsWith(attrName));
}
/**
* hasVBind - 是否存在v-bind属性
* @param attrNames - Array 所有的指令属性
* @return {boolean}
*/
function hasVBind(attrNames) {
return hasVAttr(attrNames, `${DIRECT_PREFIX}bind`);
}
/**
* parseVBind - 解析v-bind
* @param context - Object 上下文对象
* @param el - HtmlElement
* @param vAttrNames - Array 所有指令的集合
* @param VNode - VNode 当前的VNode节点
* @return {Object}
*/
function parseVBind({ context, el, vAttrNames, VNode }) {
const self = this;
const entrys = getVBindEntrys.call(this, { context, el, vAttrNames });
entrys.forEach((entry) => {
// key属性
if (entry.arg === 'key') {
VNode.key = entry.value;
}
// ref属性
if (entry.arg === 'ref') {
// ref属性不放入到VNode中
// 创建当前VNode的hook
Object.assign(VNode.data.hook, {
/**
* insert - 元素已插入DOM
* @param vnode
*/
insert: (vnode) => {
// 保存HtmlElement的el到$refs中
self.$refs[entry.value] = vnode.elm;
},
});
}
// class属性
else if (entry.arg === 'class') {
const value = {};
Object.keys(entry.value).forEach((key) => {
if (isProxyProperty(key)) {
value[key] = entry.value[key];
}
});
Object.assign(VNode.data.class, value);
}
// style属性
else if (entry.arg === 'style') {
Object.assign(VNode.data.style, entry.value);
}
// data-*属性
else if (entry.arg.startsWith('data-')) {
VNode.data.dataset[toCamelCase(entry.arg.substring('data-'.length))] = entry.value;
}
// 其他的属性
else {
VNode.data.attrs[entry.arg] = entry.value;
}
});
return entrys;
}
/**
* getDirectiveEntry - 根据vAttrName获取指令实体
* @param el - HtmlElement
* @param attrName - string 指令名称 如:v-on:click.stop.prev
* @return {Object}
*/
function getDirectiveEntry(el, attrName) {
return {
name: getDirectName(attrName), // 指令名,不包括 v- 前缀。(on)
value: '', // 指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
expression: el.getAttribute(attrName), // 字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
arg: getDirectArg(attrName), // 传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
modifiers: getDirectModifiers(attrName), // 一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
};
}
/**
* getDirectName - 在指令属性中获取指令的名称
* 例如:v-bind:id 获取的是bind
* @param attrName - string 指令属性名称
* @return {string}
*/
function getDirectName(attrName) {
let directSymbolIndex = -1;
for (let i = 0; i < DIRECT_SYMBOLS.length; i++) {
const directSymbol = DIRECT_SYMBOLS[i];
directSymbolIndex = attrName.indexOf(directSymbol, DIRECT_PREFIX.length);
if (directSymbolIndex !== -1) break;
}
return attrName.substring(
DIRECT_PREFIX.length,
directSymbolIndex === -1 ? attrName.length : directSymbolIndex,
);
}
/**
* getDirectArg - 获取指令属性中的arg
* 例如:v-bind:id="" 获取的是id
* @param attrName - string 指令属性
* @return {string}
*/
function getDirectArg(attrName) {
const startIndex = attrName.indexOf(DIRECT_SYMBOLS[0]);
if (startIndex === -1) return '';
const endIndex = attrName.indexOf(DIRECT_SYMBOLS[1], startIndex + 1);
return attrName.substring(startIndex + 1, endIndex === -1 ? attrName.length : endIndex);
}
/**
* getDirectModifiers - 获取指令属性中的modifiers
* 例如 v-on:click.stop 获取的是{stop:true}
* @param attrName - string 指令属性
* @return {Object}
*/
function getDirectModifiers(attrName) {
const index = attrName.indexOf(DIRECT_SYMBOLS[1]);
if (index === -1) return {};
const substr = attrName.substring(index);
const arr = substr.split(DIRECT_SYMBOLS[1]).slice(1);
const modifiers = {};
arr.forEach((modifier) => {
modifiers[modifier] = true;
});
return modifiers;
}
/**
* getVBindEntrys - 获取所有v-bind属性的实体集合
* @param context - Object 上下文对象
* @param el - HtmlElement
* @param vAttrNames - Array 所有指令属性的集合
* @return Array
*/
function getVBindEntrys({ context, el, vAttrNames }) {
// 获取所有v-bind标签
const bindAttrs = vAttrNames.filter((n) => n.indexOf(`${DIRECT_PREFIX}bind`) !== -1);
const cloneEl = document.createElement(el.tagName);
const resultAttrs = [];
bindAttrs.forEach((bindAttr) => {
// 如果bindAttr是v-bind对象绑定
if (bindAttr === `${DIRECT_PREFIX}bind`) {
const attrValue = el.getAttribute(bindAttr);
const value = execExpression.call(this, context, attrValue);
// 如果这个值是Object
if (isObject(value)) {
Object.keys(value).forEach((key) => {
if (isProxyProperty(key)) {
// key是驼峰命名的需要转换成xxx-xxx形式
const bindKey = `${DIRECT_PREFIX}bind:${pascalCaseToKebabCase(key)}`;
cloneEl.setAttribute(bindKey, `${attrValue}.${key}`);
resultAttrs.push(bindKey);
}
});
}
}
// bindAttr是v-bind:xxx
else {
cloneEl.setAttribute(bindAttr, el.getAttribute(bindAttr));
resultAttrs.push(bindAttr);
}
});
return resultAttrs.map((attrName) => {
// 获取一个v-bind的实体
const entry = getDirectiveEntry(cloneEl, attrName);
// arg是class或者是style
if (entry.arg === 'class' || entry.arg === 'style') {
// arg是class
if (entry.arg === 'class') {
if (entry.expression.startsWith('{') && entry.expression.endsWith('}')) {
// { active: isActive, 'text-danger': hasError }
entry.expression = `Object(${entry.expression})`;
}
// <div v-bind:class="classObject"></div>
// [activeClass, errorClass]
entry.value = execExpression.call(this, context, entry.expression);
// 如果是数组绑定
if (isArray(entry.value)) {
const classNames = entry.value;
entry.value = {};
classNames.forEach((className) => {
entry.value[className] = true;
});
}
}
// arg是style
if (entry.arg === 'style') {
if (
entry.expression.indexOf('{') === 0 &&
entry.expression.lastIndexOf('}') === entry.expression.length - 1
) {
entry.expression = `Object(${entry.expression})`;
}
entry.value = execExpression.call(this, context, entry.expression);
}
} else if (entry.arg) {
// 其他的情况
entry.value = execExpression.call(this, context, entry.expression);
}
return entry;
});
}
/**
* renderVAttr - 解析指令属性
* @param el - HtmlElement 元素的el
* @param parentVNode - VNode 父元素VNode
* @param parentElement - HtmlElement 父元素
* @param context - Object 上下文对象
* @param renderFun - Function 渲染函数
* @return {VNode | Array<VNode>}
*/
function renderVAttr({ el, parentVNode, parentElement, context, renderFun }) {
...
// 获取所有指令属性
const vAttrNames = getVAttrNames(el);
// 获取标签名称
const tagName = el.tagName.toLowerCase();
// createVNode
const VNode = createVNode(tagName);
// 解析v-bind
if (hasVBind(vAttrNames)) {
// parse v-bind
parseVBind.call(this, { context, el, vAttrNames, VNode });
}
...
}
在上面的代码中解析v-bind分为3个步骤,第一步是先调用getVattrNames获取el中的所有指令元素的名称,第二步是调用hasVBind方法在vAttrNames中寻找v-bind指令是否存在,第三步是调用parseVBind这个方法解析所有v-bind指令,这里着重说的就是parseVBind这个方法,这个方法首先在vAttrNames中过滤出所有v-bind的指令,然后在对每一个v-bind指令进行解析,解析主要分为两种解析,第一种是v-bind的值是基本类型,第二种v-bind的值是对象类型,如果是对象类型则需要把对象拆解成多个v-bind指令,这就是v-bind绑定一个对象的操作,最后把所有v-bind属性放入一个集合中进行统一处理,处理的过程其实就是把每一个v-bind属性解析到实体对象中,调用getDirectiveEntry方法解析出每一个v-bind属性的name、arg、expression、modifiers和value,value值这块需要针对,key、class、style分别进行处理,其中如果arg的值是class,class支持对象绑定、对象直接量绑定和数组绑定,
if (entry.expression.startsWith('{') && entry.expression.endsWith('}')) {
// { active: isActive, 'text-danger': hasError }
entry.expression = `Object(${entry.expression})`;
}
// <div v-bind:class="classObject"></div>
// [activeClass, errorClass]
entry.value = execExpression.call(this, context, entry.expression);
// 如果是数组绑定
if (isArray(entry.value)) {
const classNames = entry.value;
entry.value = {};
classNames.forEach((className) => {
entry.value[className] = true;
});
}
上方就是解析class的具体过程,style的解析也是如此。
// arg是style
if (entry.arg === 'style') {
if (
entry.expression.indexOf('{') === 0 &&
entry.expression.lastIndexOf('}') === entry.expression.length - 1
) {
entry.expression = `Object(${entry.expression})`;
}
entry.value = execExpression.call(this, context, entry.expression);
}
v-show指令解析
v-show指令是用来控制元素是否显示的,其实就是给元素加上display:none或者''的操作,那下面就实现一下对这个指令的解析。
/**
* renderVAttr - 解析指令属性
* @param el - HtmlElement 元素的el
* @param parentVNode - VNode 父元素VNode
* @param parentElement - HtmlElement 父元素
* @param context - Object 上下文对象
* @param renderFun - Function 渲染函数
* @return {VNode | Array<VNode>}
*/
function renderVAttr({ el, parentVNode, parentElement, context, renderFun }) {
...
// 获取所有指令属性
const vAttrNames = getVAttrNames(el);
// 获取标签名称
const tagName = el.tagName.toLowerCase();
// createVNode
const VNode = createVNode(tagName);
// 解析v-show
if (hasVShow(vAttrNames)) {
// parse v-show
parseVShow.call(this, { context, el, vAttrNames, VNode });
}
...
}
/**
* hasVShow - 是否有v-show属性
* @param attrNames - Array 所有的指令属性集合
* @return {boolean}
*/
function hasVShow(attrNames) {
return hasVAttr(attrNames, `${DIRECT_PREFIX}show`);
}
/**
* parseVShow
* @param context
* @param el
* @param vAttrNames
* @param VNode
* @return {*}
*/
function parseVShow({ context, el, vAttrNames, VNode }) {
const attrName = vAttrNames.find((n) => n.indexOf(`${DIRECT_PREFIX}show`) !== -1);
const value = el.getAttribute(attrName);
const display = execExpression.call(this, context, value);
VNode.data.style.display = display ? '' : 'none';
return display;
}
v-show的解析相对简单,还是分为2个步骤,在vAttrNames中寻找v-show属性,然后解析出v-show的值,最后在对VNode.data.style.display赋值即可。
v-html指令解析
v-html的效果就是把html插入到元素的children中,下面来看一下解析过程。
/**
* renderVAttr - 解析指令属性
* @param el - HtmlElement 元素的el
* @param parentVNode - VNode 父元素VNode
* @param parentElement - HtmlElement 父元素
* @param context - Object 上下文对象
* @param renderFun - Function 渲染函数
* @return {VNode | Array<VNode>}
*/
function renderVAttr({ el, parentVNode, parentElement, context, renderFun }) {
...
// 获取所有指令属性
const vAttrNames = getVAttrNames(el);
// 获取标签名称
const tagName = el.tagName.toLowerCase();
// createVNode
const VNode = createVNode(tagName);
// 解析v-html
// 非表单标签的时候 && 是否是表单控件元素
if (!isFormTag(tagName) && hasVHtml(vAttrNames)) {
// parse v-html
parseVHtml.call(this, { context, el, vAttrNames, VNode });
}
...
}
/**
* parseVHtml
* @param context
* @param el
* @param vAttrNames
* @param VNode
* @return {String}
*/
function parseVHtml({ context, el, vAttrNames, VNode }) {
const attrName = vAttrNames.find((n) => n.indexOf(`${DIRECT_PREFIX}html`) !== -1);
const value = el.getAttribute(attrName);
// 在此处需要进行实体字符的替换
const html = execExpression.call(this, context, value);
const htmlVNode = toVNode(createElement(html));
VNode.children.push(htmlVNode);
return html;
}
/**
* hasVHtml - 是否存在v-html属性
* @param attrNames - Array 所有的指令属性集合
* @return {boolean}
*/
function hasVHtml(attrNames) {
return hasVAttr(attrNames, `${DIRECT_PREFIX}html`);
}
/**
* isFormTag
* @param tagName
* @return {boolean}
*/
function isFormTag(tagName) {
return FORM_CONTROL_BINDING_TAG_NAMES.includes(tagName);
}
解析v-html还是分为2个步骤,和解析上两个标签不一样的地方在于只有非表单控件元素才能含有v-html指令,比如input元素或者textarea和select元素是不能含有v-html指令的,isFormTag函数是用来判断元素是否是表单控件元素,在这里要介绍一个虚拟DOM的api,toVNode这个方法可以把一个HTML元素转换成VNode,那我们就可以通过html字符串创建一个HTMLElement在调用toVNode来实现v-html的操作。
v-if、v-else-if和v-else解析
这三个指令v-if相对于后两个的解析难度要低一些,v-if的值如果为true则不作任何操作,否则节点就不显示,而其他两个指令的解析方式类似。
v-if解析
我们来看一下实现代码
/**
* renderVAttr - 解析指令属性
* @param el - HtmlElement 元素的el
* @param parentVNode - VNode 父元素VNode
* @param parentElement - HtmlElement 父元素
* @param context - Object 上下文对象
* @param renderFun - Function 渲染函数
* @return {VNode | Array<VNode>}
*/
function renderVAttr({ el, parentVNode, parentElement, context, renderFun }) {
...
// 获取所有指令属性
const vAttrNames = getVAttrNames(el);
// 获取标签名称
const tagName = el.tagName.toLowerCase();
// 解析v-if
if (hasVIf(vAttrNames)) {
// parse v-if
const display = parseVIf.call(this, { context, el, vAttrNames });
// 如果不显示则返回null
if (!display) {
return {
Continue: false,
VNode: null,
};
}
}
// createVNode
const VNode = createVNode(tagName);
...
}
/**
* hasVIf - 是否有v-if属性
* @param attrNames - Array 所有的指令属性集合
* @return {boolean}
*/
function hasVIf(attrNames) {
return hasVAttr(attrNames, `${DIRECT_PREFIX}if`);
}
/**
* parseVIf - 解析v-if标签
* @param context - Object 上下文对象
* @param el - HtmlElement 元素
* @param vAttrNames - Array 指令标签的集合
* @return {string}
*/
function parseVIf({ context, el, vAttrNames }) {
return execExpression.call(
this,
context,
el.getAttribute(vAttrNames.find((n) => n.indexOf(`${DIRECT_PREFIX}if`) !== -1)),
);
}
v-if的解析相对是很简单的,就是针对返回值来判断是否生成节点,还是继续向下进行。
v-else-if解析
想要解析v-else-if这个指令,首先需要知道这个指令的一个出现顺序和范围,一共2种情况
// 第一种情况
<div v-if="xxx"></div>
// 可以有多个v-else-if
<div v-else-if=""></div>
// 第二种情况
<div v-if="xxx"></div>
// 含有多个空的文本节点
// 或者含有多个v-else-if节点
// 这两种情况可以交替出现
<div v-else-if></div>
从分析不难得知,v-else-if必须要有一个v-if发起才行,中间可以包含若干个空的文件节点和若干个v-else-if节点,且这两种情况可以交替出现,其实解析就是看一下当前含有v-else-if的节点是否符合这种格式的要求,如果不符合则解析错误,如果符合这个结构,那就直接解析v-else-if的值,结果和v-if是一样的是否继续执行或者不显示节点,下面看一下具体实现。
/**
* renderVAttr - 解析指令属性
* @param el - HtmlElement 元素的el
* @param parentVNode - VNode 父元素VNode
* @param parentElement - HtmlElement 父元素
* @param context - Object 上下文对象
* @param renderFun - Function 渲染函数
* @return {VNode | Array<VNode>}
*/
function renderVAttr({ el, parentVNode, parentElement, context, renderFun }) {
...
// 获取所有指令属性
const vAttrNames = getVAttrNames(el);
// 获取标签名称
const tagName = el.tagName.toLowerCase();
// 解析v-else-if
if (hasVElseIf(vAttrNames)) {
// 合理性判断
// 如果合理则进行计算
const entry = parseVElseIf.call(this, { context, el, parentElement });
if (!entry.valid) {
return {
Continue: false,
VNode: null,
};
}
if (!entry.result) {
return {
Continue: false,
VNode: null,
};
}
}
// createVNode
const VNode = createVNode(tagName);
...
}
/**
* hasVElseIf - 是否有v-else-if属性
* @param attrNames - Array 所有的指令属性集合
* @return {boolean}
*/
function hasVElseIf(attrNames) {
return hasVAttr(attrNames, `${DIRECT_PREFIX}else-if`);
}
/**
* parseVElseIf
* @param context - Object 上下文对象
* @param el - HtmlElement 当前元素
* @param parentElement = HtmlElement 父元素
* @return {
* valid : boolean 链路是否有效
* result: boolean else的值
* }
*/
function parseVElseIf({ context, el, parentElement }) {
// 说明el是v-for的一个克隆元素
if (!parentElement) {
// 寻找el元素的克隆元素
const groupName = el.getAttribute(GROUP_KEY_NAME);
// 获取含有GROUP_KEY_NAME属性且值等于groupName的元素
const srcEl = this.templateEl.querySelector(`[${GROUP_KEY_NAME}=${groupName}]`);
if (srcEl) {
parentElement = srcEl.parentElement;
}
}
if (!parentElement) {
return {
valid: false,
result: false,
};
}
// 获取parentElement中的所有childNodes
let childNodes;
if (parentElement.nodeName.toLowerCase() === 'template') {
childNodes = parentElement.content.childNodes;
} else {
childNodes = parentElement.childNodes;
}
// 获取el在childNodes中的位置
let elIndex = -1;
for (let i = 0, len = childNodes.length; i < len; i++) {
const childNode = childNodes.item(i);
if (childNode === el) {
elIndex = i;
break;
}
}
if (elIndex === -1) {
return {
valid: false,
result: false,
};
}
// 获取elIndex之前第一个v-if的位置
let firstIfIndex = -1;
for (let i = elIndex - 1; i >= 0; i--) {
const childNode = childNodes[i];
// 如果是元素 && 有v-if属性
if (isElementNode(childNode) && hasVAttr(getVAttrNames(childNode), `${DIRECT_PREFIX}if`)) {
firstIfIndex = i;
break;
}
}
if (firstIfIndex === -1) {
return {
valid: false,
result: false,
};
}
// v-if和v-else之前只能包含文本节点或者v-else-if
let valid = true;
for (let i = firstIfIndex + 1; i <= elIndex - 1; i++) {
const childNode = childNodes[i];
// 如果是文本节点
if (isTextNode(childNode)) {
// 如果文本节点不是''
if (childNode.nodeValue.trim() !== '') {
valid = false;
break;
}
} else {
const hasVElseIfAttr = hasVAttr(getVAttrNames(childNode), `${DIRECT_PREFIX}else-if`);
// 如果不是v-else-if
if (!hasVElseIfAttr) {
valid = false;
break;
}
}
}
return {
valid,
result: valid
? execExpression.call(this, context, el.getAttribute(`${DIRECT_PREFIX}else-if`))
: false,
};
}
这里主要对parseVElseIf函数进行详细讲解,第一步先获取el在父元素childrenNodes中的索引值,基于此索引值向前寻找含有v-if指令的节点,如果找到则在2个索引之间查看所有元素是否符合只含有空的文本节点或者是含有v-else-if指令的节点, 如果都符合要求,则对v-else-if的值进行解析。
v-else解析
v-else的解析和v-else-if的解析基本上是相同的,只有一个地方不一致,就是怎么计算v-else的值是true还是false,这就需要在2个索引之间进行迭代的时候,记录每一个v-else-if的值,最后加上v-if的值,只要这些里面有一个值是true,那么v-else的值就是false,否则就是true。
/**
* parseVElse
* @param context - Object 上下文对象
* @param el - HtmlElement 当前元素
* @param parentElement = HtmlElement 父元素
* @return {
* valid : boolean 链路是否有效
* result: boolean else的值
* }
*/
function parseVElse({ context, el, parentElement }) {
// 说明el是v-for的一个克隆元素
if (!parentElement) {
// 寻找el元素的克隆元素
const groupName = el.getAttribute(GROUP_KEY_NAME);
// 获取含有GROUP_KEY_NAME属性且值等于groupName的元素
const srcEl = this.templateEl.querySelector(`[${GROUP_KEY_NAME}=${groupName}]`);
if (srcEl) {
parentElement = srcEl.parentElement;
}
}
if (!parentElement) {
return {
valid: false,
result: false,
};
}
// 获取parentElement中的所有childNodes
let childNodes;
if (parentElement.nodeName.toLowerCase() === 'template') {
childNodes = parentElement.content.childNodes;
} else {
childNodes = parentElement.childNodes;
}
// 获取el在childNodes中的位置
let elIndex = -1;
for (let i = 0, len = childNodes.length; i < len; i++) {
const childNode = childNodes.item(i);
if (childNode === el) {
elIndex = i;
break;
}
}
if (elIndex === -1) {
return {
valid: false,
result: false,
};
}
// 获取elIndex之前第一个v-if的位置
let firstIfIndex = -1;
for (let i = elIndex - 1; i >= 0; i--) {
const childNode = childNodes[i];
// 如果是元素 && 有v-if属性
if (isElementNode(childNode) && hasVAttr(getVAttrNames(childNode), `${DIRECT_PREFIX}if`)) {
firstIfIndex = i;
break;
}
}
if (firstIfIndex === -1) {
return {
valid: false,
result: false,
};
}
// v-if和v-else之前只能包含文本节点或者v-else-if
let valid = true;
const values = [];
for (let i = firstIfIndex + 1; i <= elIndex - 1; i++) {
const childNode = childNodes[i];
// 如果是文本节点
if (isTextNode(childNode)) {
// 如果文本节点不是''
if (childNode.nodeValue.trim() !== '') {
valid = false;
break;
}
} else {
const hasVElseIf = hasVAttr(getVAttrNames(childNode), `${DIRECT_PREFIX}else-if`);
// 如果不是v-else-if
if (!hasVElseIf) {
valid = false;
break;
}
// 如果是v-else-if
else {
values.push(
execExpression.call(this, context, childNode.getAttribute(`${DIRECT_PREFIX}else-if`)),
);
}
}
}
if (valid) {
// 计算v-if的值
const ifNode = childNodes[firstIfIndex];
values.push(execExpression.call(this, context, ifNode.getAttribute(`${DIRECT_PREFIX}if`)));
}
return {
valid,
result: valid ? !values.some((value) => value) : false,
};
}
v-for指令解析
v-for指令是这些指令里面解析相对较难的一个,首先我们要明确一件事,就是Vue这些指令里面解析指令是有顺序的,我在这先给出解析的一个顺序,然后在说明为什么是这个顺序。
v-for -> v-if -> v-else -> v-else-if -> v-show -> v-bind -> v-model -> v-on -> v-html
其中v-else和v-else-if的顺序可以互换,v-show和v-bind可以互换,那为什么是这个顺序呢,首先有一个法则,法则就是能影响元素个数的指令先解析,那么能影响元素个数的指令有v-for和v-if|v-else-if|v-else,那么这两个指令又先解析哪个呢,应该是v-for先解析,因为v-for是确定整体元素数量的一个指令,而v-if等指令只能控制一个节点是否显示,根据Vue文档v-for可以迭代数组、对象和number的常量,v-for的解析还涉及到一个问题就是它会创建新的作用域,而且v-for还可以嵌套的使用,每一个v-for都会创建新的作用域,下面就先给出解析v-for的代码,然后详细对代码进行讲解。
/**
* renderVAttr - 解析指令属性
* @param el - HtmlElement 元素的el
* @param parentVNode - VNode 父元素VNode
* @param parentElement - HtmlElement 父元素
* @param context - Object 上下文对象
* @param renderFun - Function 渲染函数
* @return {VNode | Array<VNode>}
*/
function renderVAttr({ el, parentVNode, parentElement, context, renderFun }) {
...
// 获取所有指令属性
const vAttrNames = getVAttrNames(el);
// 解析el的v-for标签
if (hasVFor(vAttrNames)) {
// parse v-for
return {
Continue: false,
// 如果没有父元素是不能使用v-for的所以返回null
VNode: parentVNode
? parseVFor.call(
this,
// 如果context是this.$dataProxy则需要重新创建新的context(上下文),因为一个v-for就是一个新的上下文环境,因为v-for会有新的变量放入到this中
{
context,
// context === this.$dataProxy ? createContext.call(self, this.$dataProxy) : context,
el,
parentVNode,
vAttrNames,
renderFun,
},
)
: null,
};
}
...
}
const ITERATOR_CHAIN = [
{
condition: (obj) => isObject(obj),
handler: iteratorObj,
},
{
condition: (obj) => isArray(obj),
handler: iteratorArray,
},
{
condition: (val) => isNumber(val),
handler: iteratorNumber,
},
];
/**
* iteratorObj - 迭代对象
* @param val - Object
* @param context - Object
* @param el - HtmlElement
* @param parentVNode - VNode
* @param itItemStr - string
* @param renderFun - Function
* @param VNodes
* @return VNodes
*/
function iteratorObj({ val, context, el, parentVNode, itItemStr, renderFun, VNodes }) {
let index = 0;
for (const p in val) {
if (isProxyProperty(p)) {
// iteratorVFor会创建一个VNode或VNodes
const itemVNodes = iteratorVFor.call(
this,
{
context: createContext(context),
el,
parentVNode,
itItemStr,
itItemObj: val[p],
renderFun,
},
// 如果是迭代对象则是属性名
p,
// 索引值
index,
);
if (isArray(itemVNodes)) {
VNodes = VNodes.concat(itemVNodes);
} else if (isObject(itemVNodes)) {
VNodes.push(itemVNodes);
}
index++;
}
}
return VNodes;
}
/**
* iteratorArray - 迭代数组
* @param val - Array
* @param context - Object
* @param el - HtmlElement
* @param parentVNode - VNode
* @param itItemStr - string
* @param renderFun - Function
* @param VNodes
* @return VNodes
*/
function iteratorArray({ val, context, el, parentVNode, itItemStr, renderFun, VNodes }) {
for (let i = 0; i < val.length; i++) {
const itemVNodes = iteratorVFor.call(
this,
{
context: createContext(context),
el,
parentVNode,
itItemStr,
itItemObj: val[i],
renderFun,
},
null,
// 如果是迭代数组则是索引值
i,
);
if (isArray(itemVNodes)) {
VNodes = VNodes.concat(itemVNodes);
} else if (isObject(itemVNodes)) {
VNodes.push(itemVNodes);
}
}
return VNodes;
}
/**
* iteratorNumber - 迭代范围值
* @param - val - number
* @param context - Object
* @param el - HtmlElement
* @param parentVNode - VNode
* @param itItemStr - string
* @param renderFun - Function
* @param VNodes
* @return VNodes
*/
function iteratorNumber({ val, context, el, parentVNode, itItemStr, renderFun, VNodes }) {
for (let i = 1; i <= val; i++) {
const itemVNodes = iteratorVFor.call(
this,
{
context: createContext(context),
el,
parentVNode,
itItemStr,
itItemObj: i,
renderFun,
},
// 如果是迭代对象则是属性名
null,
// 索引值
i,
);
if (isArray(itemVNodes)) {
VNodes = VNodes.concat(itemVNodes);
} else if (isObject(itemVNodes)) {
VNodes.push(itemVNodes);
}
}
return VNodes;
}
/**
* hasVFor - 是否有v-for属性
* @param attrNames - Array 所有的指令属性集合
* @return {boolean}
*/
function hasVFor(attrNames) {
return hasVAttr(attrNames, `${DIRECT_PREFIX}for`);
}
/**
* createContext - 创建上下文(主要是在v-for的时候需要重新创建一个新的上下文)
* @param srcContext - Object 原始的srcContext对象
* @param argv - Object 上下文的参数
* @return Object 新的上下文
*/
function createContext(srcContext, argv = {}) {
return { ...srcContext, ...(argv || {}) };
}
/**
* parseVFor - 解析v-for
* @param context - Object 上下文对象
* @param el - HtmlElement 当前元素
* @param parentVNode - VNode 父VNode节点
* @param vAttrNames - Array 指令属性集合
* @param renderFun - Function render函数
* @return {Array<VNode>}
*/
function parseVFor({ context, el, parentVNode, vAttrNames, renderFun }) {
// 如果没有group属性则创建一个
// group属性使用来给v-for进行分组的
let groupName = el.getAttribute(GROUP_KEY_NAME);
if (!el.hasAttribute(GROUP_KEY_NAME) || !groupName) {
groupName = uuid();
el.setAttribute(GROUP_KEY_NAME, groupName);
}
const attrName = vAttrNames.find((n) => n.indexOf(`${DIRECT_PREFIX}for`) !== -1);
// v-for=""的值
const value = el.getAttribute(attrName);
// 把值进行分割获取表达式的部分
const grammar = value.split(EMPTY_SPLIT);
if (grammar.length !== 2) return null;
// item 获取 (item,index)
const itItemStr = grammar[0].trim();
// data中的值
const itObjStr = grammar[1].trim();
let VNodes = [];
// 获取迭代的对象,分为对象迭代和数组迭代
const itObj = execExpression.call(this, context, itObjStr);
let index = 0;
while (index < ITERATOR_CHAIN.length) {
if (ITERATOR_CHAIN[index].condition(itObj)) {
VNodes = ITERATOR_CHAIN[index].handler.call(this, {
val: itObj,
context,
el,
parentVNode,
itItemStr,
renderFun,
VNodes,
});
break;
}
index++;
}
// 比较删除componentsMap中没有的组件引用
const componentKeys = this.componentsMap.keys();
// 迭代进行删除
while (componentKeys.length) {
const currentKey = componentKeys.pop();
// componentKeys和VNode之间的插集
const has = VNodes.some((VNode) => VNode.key === currentKey);
if (!has) {
this.componentsMap.delete(currentKey);
}
}
return VNodes;
}
/**
* iteratorVFor - 对v-for进行迭代(一个的生成)
* @param context - Object 上下文对象
* @param el - HtmlElement
* @param parentVNode - VNode 父VNode
* @param itItemStr - Object 迭代项变量
* @param itItemObj - Object | Array 迭代的变量
* @param renderFun - Function 渲染函数
* @param property - string 属性名
* @param index - number v-for的索引
* @return {VNode | Array<VNode>}
*/
function iteratorVFor(
{ context, el, parentVNode, itItemStr, itItemObj, renderFun },
property,
index,
) {
// 如果项的迭代对象是用()进行包裹的
if (itItemStr.startsWith('(') && itItemStr.endsWith(')')) {
// item , index
// 截取出()中的值
itItemStr = itItemStr.substring(1, itItemStr.length - 1).trim();
// 如果内容中包含','
if (itItemStr.indexOf(',') !== -1) {
const itItemArr = itItemStr.split(COMMA_SPLIT).map((t) => t.trim());
// 从context中获取迭代项数据
context[itItemArr[0].trim()] = itItemObj;
// 如果是迭代对象则是属性名,否则是索引
context[itItemArr[1].trim()] = property || index;
// 是索引
if (itItemArr.length >= 3) {
context[itItemArr[2].trim()] = index;
}
} else {
// 从context中获取迭代项数据
context[itItemStr] = itItemObj;
}
} else {
// 从context中获取迭代项数据
context[itItemStr] = itItemObj;
}
// 所有属性的集合
const attrNames = el.getAttributeNames();
// 处理cloneEl的key 需要加入group的值
const groupName = el.getAttribute(GROUP_KEY_NAME);
// 元素有key属性
// 对v-for元素进行克隆,克隆之后cloneEl会有groupName属性
const cloneEl = el.cloneNode(true);
if (attrNames.indexOf(`${DIRECT_PREFIX}bind:key`) !== -1) {
const key = `${DIRECT_PREFIX}bind:key`;
const value = cloneEl.getAttribute(key);
const expressVal = execExpression.call(this, context, value);
cloneEl.setAttribute(key, `'${groupName}' + '(${expressVal})'`);
} else if (attrNames.indexOf('key')) {
const key = 'key';
const value = cloneEl.getAttribute(key);
cloneEl.setAttribute(key, `${groupName}(${value})`);
} else {
cloneEl.setAttribute('key', `${groupName}${index}`);
}
// 删除v-for属性
cloneEl.removeAttribute(`${DIRECT_PREFIX}for`);
return renderFun.call(this, {
context,
el: cloneEl,
parentVNode,
parentElement: el.parentElement,
});
}
3 years ago