Vue源码后记-vFor列表渲染(1)
钩子函数比较简单,没有什么意思,这一节搞点大事情 => 源码中v-for的渲染过程。
vue的内置指令包含了v-html、v-if、v-once、v-bind、v-on、v-show等,先从一个入手,其余的也就那么回事。
案例模板依照之前的,但是多加了一个v-for指令,如下所示:
<body>
<div id='app'>
<a href="#" v-for="item in items">{{item}}</a>
</div>
</body>
<script src='./vue.js'></script>
<script>
var app = new Vue({
el: '#app',
data: {
items: [1, 2, 3, 4, 5]
},
});
</script>
为了保持DOM的纯净,没有添加样式和一些额外杂质。
跳过无用的流程,直接进入不同的地方,首先是compile函数,此处将DOM字符串转化为一个对象,直接跳到baseCompile中:
function baseCompile(template,options) {
var ast = parse(template.trim(), options);
optimize(ast, options);
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
一、parse
参数就不解释了,进第一个参数解析内部,parse => parseHTML。
函数会先解析掉<div id='app'>,生成一个对象:
,然后遇到回车符号,省略后继续解析,就到了本文要讲的v-for。
这个函数之前讲过,特别长,不过主要关注一个地方:
function parseHTML(html, options) {
// var...
while (html) {
last = html;
if (!lastTag || !isPlainTextElement(lastTag)) {
var textEnd = html.indexOf('<');
if (textEnd === 0) {
// code...
// Start tag:
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
}
}
// code...
} else {
// code...
}
// code...
}
// Clean up any remaining tags
parseEndTag();
function advance(n) {
index += n;
html = html.substring(n);
}
function parseStartTag() {
// code...
}
function handleStartTag(match) {
// code...
}
function parseEndTag(tagName, start, end) {
// code...
}
}
就是对startTag进行处理的两个函数,第一个parseStartTag函数将字符串切割成如图的对象:
,这里attrs有两个,一个href属性,一个是v-for属性,只是做正则切割,没有区分是HTML属性还是vue属性。
第二个handleStartTag负责将对象进行二次处理,因为可能包含某些特殊的属性,这里只需要关注一个start函数:
function handleStartTag(match) {
var tagName = match.tagName;
var unarySlash = match.unarySlash;
// code...
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end);
}
}
函数接受5个参数,分别为标签名、属性、是否一元、字符串开始索引、字符串结束索引,这个函数也是长得要死,直接看重点:
function start(tag, attrs, unary) {
// code...
if (inVPre) {
processRawAttrs(element);
} else {
processFor(element);
processIf(element);
processOnce(element);
processKey(element);
// determine whether this is a plain element after
// removing structural attributes
element.plain = !element.key && !attrs.length;
processRef(element);
processSlot(element);
processComponent(element);
for (var i$1 = 0; i$1 < transforms.length; i$1++) {
transforms[i$1](element, options);
}
processAttrs(element);
}
function checkRootConstraints(el) {
// code...
}
// code...
}
重点看中间那部分,会对内置指令作处理,跑源码的时候全部跳过了,这里就需要进来看看:
function processFor(el) {
var exp;
// getAndRemoveAttr函数将v-for的值从attrsMap中取出,并将attrsList中对应的删除
// exp => item in items
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
// forAliasRE正则切割in或of
// item in items => ['item in items','item','items']
var inMatch = exp.match(forAliasRE);
if (!inMatch) {
"development" !== 'production' && warn$2(
("Invalid v-for expression: " + exp)
);
return
}
// for的数据源 => items
el.for = inMatch[2].trim();
// 列表数据别名 => item
var alias = inMatch[1].trim();
// 这个iterator暂时不清楚干嘛的 我的v-for表达式改成'item in 5'这里也是null
var iteratorMatch = alias.match(forIteratorRE);
if (iteratorMatch) {
el.alias = iteratorMatch[1].trim();
el.iterator1 = iteratorMatch[2].trim();
if (iteratorMatch[3]) {
el.iterator2 = iteratorMatch[3].trim();
}
} else {
el.alias = alias;
}
}
}
函数执行完后,在el对象上添加了2个属性:for、alias。如图所示:
。
二、optimize
这个没什么好讲,因为DOM节点有v-for属性,所以被认定为非静态节点,staic属性标记为false。
三、generate
这一步将ast打包成一个函数,有一个地方也会专门处理v-for属性:
function generate(ast,options) {
// var code...
var code = ast ? genElement(ast) : '_c("div")';
staticRenderFns = prevStaticRenderFns;
onceCount = prevOnceCount;
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: currentStaticRenderFns
}
}
跳过前面声明变量的代码,这里的genElement会对ast对象做转化处理,如下:
function genElement(el) {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el)
} else if (el.once && !el.onceProcessed) {
return genOnce(el)
} else if (el.for && !el.forProcessed) {
return genFor(el)
} else if (el.if && !el.ifProcessed) {
return genIf(el)
} else if (el.tag === 'template' && !el.slotTarget) {
return genChildren(el) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el)
} else {
// component or element...
}
}
可以看到,针对各个特殊属性,有专门的gen函数处理,这里只看genFor就行了:
function genFor(el) {
// items
var exp = el.for;
// item
var alias = el.alias;
// ''
var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : '';
var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : '';
// key warning...
// 表示for属性处理完了 避免递归
el.forProcessed = true;
return "_l((" + exp + ")," +
"function(" + alias + iterator1 + iterator2 + "){" +
"return " + (genElement(el)) +
'})'
}
v-for的处理比一般的要特殊一些,可以看到,这里再次调用了genElement处理其余属性,由于节点标记了forProcessed,所以不会再次进入这个函数。
第二次调用genElement时,会跳到最后,并生成一个去除v-for属性的gen字符串:
这个字符串是在处理v-for函数中返回的一部分,所有的字符串加起来变成了这样:
前面的属于最外层div,后面_l属于有v-for属性的a标签,而最后的_v是a标签的文本内容。
这样,ast的generate就处理完了。
有了render函数,下面就是vnode的生成和patch过程了。
往下跑,会调用一连串的函数,包含watcher、update等等,截取一下关键的代码片段:
function mountComponent(vm, el, hydrating) {
// 'beforeMount'
var updateComponent;
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
// 开发者模式下的update
} else {
updateComponent = function() {
vm._update(vm._render(), hydrating);
};
}
vm._watcher = new Watcher(vm, updateComponent, noop);
hydrating = false;
// 'mounted'
return vm
}
var Watcher = function Watcher(vm, expOrFn, cb, options) {
// this.a...
// this.b...
// 此处expOrFn为上面的updateComponent
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
// warning...
}
}
this.value = this.lazy ?
undefined :
this.get();
};
Watcher.prototype.get = function get() {
// var...
if (this.user) {
// try:value = this.getter.call(vm, vm);
} else {
// 调用上面的expOrFn
// 即vm._update(vm._render(), hydrating);
value = this.getter.call(vm, vm);
}
// code...
return value
};
Vue.prototype._render = function() {
// var...
try {
// render => return _c('div',{attrs:{"id":"app"}},_l((items),function(item){return _c('a',{attrs:{"href":"#"}},[_v(_s(item))])}))
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
// warning...
}
// warning
return vnode
};
最后面那个vode会返回到第二个函数,作为vm._watcher对象的value属性保存起来。
之前跑源码,跳过了vnode的生成过程,这次硬刚一波!
不要怂,干!
分析一下这个字符串函数,首先忽略那个with(this),没啥解释的,然后是return的主体函数,函数名为_c,这是一个缩写,后面再说,传入了5个参数,分别为:
1、'div' => 根标签的tagName
2、{attrs:{"id":"app"}} => 根标签的相关属性
3、_l((items) => v-for相关函数
4、function(item){return _c('a',{attrs:{"href":"#"}} => v-for相关函数,包含了a标签的tagName与属性
5、[_v(_s(item))] => a标签文本
注意到该函数是用call调用,并且第一个参数传了vm._renderProxy作为执行上下文。而这个vm._renderProxy是什么呢?是一个代理,代码如下:
initProxy = function initProxy(vm) {
if (hasProxy) {
var options = vm.$options;
var handlers = options.render && options.render._withStripped ?
getHandler :
hasHandler;
// 此处handlers => hasHandler
vm._renderProxy = new Proxy(vm, handlers);
} else {
vm._renderProxy = vm;
}
};
var hasHandler = {
has: function has(target, key) {
var has = key in target;
var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';
if (!has && !isAllowed) {
warnNonPresent(target, key);
}
return has || !isAllowed
}
};
var allowedGlobals = makeMap(
'Infinity,undefined,NaN,isFinite,isNaN,' +
'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
'require' // for Webpack/Browserify
);
关于这个Proxy构造函数,是ES6的新特性,专门去阮老师的开源书里摘抄一下介绍:

重点看这句:外界对该对象的访问,都必须通过这层拦截。在这里,vm被设置了一个has拦截器,该拦截器的说明如下:


简单来说,当访问对象属性时,会被has拦截,并调用对应的方法来执行一些过滤。
回到render.cal那里,把函数美化一下如下:
(function() {
with(this){
return _c('div',{attrs:{"id":"app"}},_l((items),function(item){return _c('a',{attrs:{"href":"#"}},[_v(_s(item))])}))
}
})
with中的this指的是vm,即当前vue实例,所以_c调用的实际是vm._c,因为访问了属性,所以会调用拦截器对_c进行过滤,跑一个看看过程:
// target => vm
// key => _c
has: function has(target, key) {
// 判断属性是否在vue实例上
var has = key in target;
// allowedGlobals是所有内置的全局方法
// 缩写方法都是_开头
var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';
if (!has && !isAllowed) {
warnNonPresent(target, key);
}
return has || !isAllowed
}
简单来讲,has拦截器做了两重判断:
一、判断vue实例上是否有此方法
这个_c方法早在beforeCreated的时候就添加上了,如下:
Vue.prototype._init = function(options) {
// code...
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
// code...
};
function initRender(vm) {
// code...
vm._c = function(a, b, c, d) {
return createElement(vm, a, b, c, d, false);
};
// code...
}
至于这个方法是干啥的之后再说,反正vue实例上有这个方法,因此has变量值为true。
二、判断该方法是否是合法的内置全局方法或是否以_开头
vue对象在初始化时,原型上就添加了一系列以_开头的方法,如果一个方法不在原型上又以_ 开头会造成混淆,所以会做此判断,如下:
renderMixin(Vue$3);
function renderMixin(Vue) {
Vue.prototype.$nextTick = function(fn) {
// code...
};
Vue.prototype._render = function() {
// code...
};
// internal render helpers.
// these are exposed on the instance prototype to reduce generated render
// code size.
Vue.prototype._o = markOnce;
Vue.prototype._n = toNumber;
Vue.prototype._s = toString;
Vue.prototype._l = renderList;
Vue.prototype._t = renderSlot;
Vue.prototype._q = looseEqual;
Vue.prototype._i = looseIndexOf;
Vue.prototype._m = renderStatic;
Vue.prototype._f = resolveFilter;
Vue.prototype._k = checkKeyCodes;
Vue.prototype._b = bindObjectProps;
Vue.prototype._v = createTextVNode;
Vue.prototype._e = createEmptyVNode;
Vue.prototype._u = resolveScopedSlots;
}
这些方法全是用来生成虚拟DOM的工具方法。
当检测出问题时,会调用warnNonPresent报错返回false,正常情况会返回true。
这个玩意比我想象中的还要复杂,分开写吧!
Vue源码后记-vFor列表渲染(1)的更多相关文章
- Vue源码后记-vFor列表渲染(2)
这一节争取搞完! 回头来看看那个render代码,为了便于分析,做了更细致的注释: (function() { // 这里this指向vue对象 下面的所有方法默认调用Vue$3.prototype上 ...
- Vue源码后记-vFor列表渲染(3)
这一节肯定能完! 经过DOM字符串的AST转化,再通过render变成vnode,最后就剩下patch到页面上了. render函数跑完应该是在这里: function mountComponent( ...
- Vue源码后记-其余内置指令(3)
其实吧,写这些后记我才真正了解到vue源码的精髓,之前的跑源码跟闹着玩一样. go! 之前将AST转换成了render函数,跳出来后,由于仍是字符串,所以调用了makeFunction将其转换成了真正 ...
- Vue源码后记-钩子函数
vue源码的马拉松跑完了,可以放松一下写点小东西,其实源码讲20节都讲不完,跳了好多地方. 本人技术有限,无法跟大神一样,模拟vue手把手搭建一个MVVM框架,然后再分析原理,只能以门外汉的姿态简单过 ...
- Vue源码后记-其余内置指令(2)
-- 指令这个讲起来还有点复杂,先把html弄上来: <body> <div id='app'> <div v-if="vIfIter" v-bind ...
- Vue源码后记-其余内置指令(1)
把其余的内置指令也搞完吧,来一个全家桶. 案例如下: <body> <div id='app'> <div v-if="vIfIter" v-bind ...
- Vue源码后记-更多options参数(1)
我是这样计划的,写完这个还写一篇数据变动时,VNode是如何更新的,顺便初探一下diff算法. 至于vue-router.vuex等插件源码,容我缓一波好吧,vue看的有点伤. 其实在之前讲其余内置指 ...
- vue项目开发之v-for列表渲染的坑
不知道大家在用vue开发的过程中有没有遇到过在使用v-for的时候会出现大片的黄色警告,比如下图: 其实这是因为没有写key的原因 :key是为vue的响应式渲染提供方法,在列表中单条数据改变的情况下 ...
- vue源码解析阅读列表
https://zhuanlan.zhihu.com/p/24435564 开发vue(或类似的MVVM框架)的过程中,需要面对的主要问题有哪些? 剖析vue实现原理,自己动手实现mvvm 官网介绍
随机推荐
- 在Myeclipse中用Java语言操作mysql数据库
package OperateMysql; import java.sql.*; public class MysqlTest { public static void main(String[] a ...
- JavaScript中的與和或的規則
與(&&)的規則是: 第一項的Boolean值為false,則返回第一項的值 第一項的Boolean值為true,則返回第二項的值 簡記:一假返一,一真返二 與:一假為假,全真為真 或 ...
- 零基础的人该怎么学习JAVA
对于JAVA有所兴趣但又是零基础的人,该如何学习JAVA呢?对于想要学习开发技术的学子来说找到一个合适自己的培训机构是非常难的事情,在选择的过程中总是 因为这样或那样的问题让你犹豫不决,阻碍你前进的 ...
- 极化码之tal-vardy算法(2)
上一节我们了解了tal-vardy算法的大致原理,对所要研究的二元输入无记忆对称信道进行了介绍,并着重介绍了能够避免输出爆炸灾难的合并操作,这一节我们来关注信道弱化与强化操作. [1]<Chan ...
- windows 结束进程的详细过程
windows上如何结束进程的详细过程,下面附详细,图文说明 在cmd下,输入 netstat -ano|findstr 8080 //说明:查看占用8080端口的进程 在cmd下, ...
- [js高手之路] html5 canvas系列教程 - 文本样式(strokeText,fillText,measureText,textAlign,textBaseline)
接着上文线条样式[js高手之路] html5 canvas系列教程 - 线条样式(lineWidth,lineCap,lineJoin,setLineDash)继续. canvas提供两种输出文本的方 ...
- javaWeb正则表达式
对于web来说,字符串的处理特别重要,而正则表达式是对字符串处理的利器,在字符过滤,验证方面都能看到她的身影. 今天需要处理一段json字符串,在用String.replaceAll的过程中,遇到了正 ...
- asp.net core合并压缩资源文件引发的学习之旅
0. 在asp.net core中使用BuildBundlerMinifier合并压缩资源文件 在asp.net mvc中可以使用Bundle来压缩合并css,js 不知道的见:http://www. ...
- 理解ios 11中webview的视口
iOS 11在状态栏区域带来了一些新的,也许是不直观的行为,这对使用Apache Cordova或Ionic等工具的开发人员尤为重要.特别是,这种行为变化会影响任何基于Web的应用程序,这些应用程序在 ...
- AF_INET
AF_INET(又称PF_INET)是 IPv4 网络协议的套接字类型,AF_INET6 则是 IPv6 的:而AF_UNIX 则是Unix系统本地通信. 选择AF_INET 的目的就是使用IPv4 ...