钩子函数比较简单,没有什么意思,这一节搞点大事情 => 源码中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。