这一节争取搞完!
回头来看看那个render代码,为了便于分析,做了更细致的注释;
(function() { // 这里this指向vue对象 下面的所有方法默认调用Vue$3.prototype上的方法 with(this){ return _c/*方法调用 => has拦截器过滤*/ ('div',{attrs:{"id":"app"}}, _l/*方法调用 => has拦截器过滤*/( (items/*_data属性访问 => 自定义proxy过滤*/), function(item){ return _c/*方法调用 => has拦截器过滤*/ ('a',{attrs:{"href":"#"}}, [_v/*方法调用 => has拦截器过滤*/ (_s/*方法调用 => has拦截器过滤*/(item))]) })) } })
所有的has拦截器之前分析过了,跳过,但是这里又多了一个特殊的访问,即items,但是Vue$3上并没有这个属性,属性在Vue$3._data上,如图:,那这是如何访问到的呢?
Vue在initState的时候自己又封装了一个proxy,所有对属性的访问会自动跳转到_data上,代码如下:
Vue.prototype._init = function(options) { // code... // 这里处理是ES6的Proxy { initProxy(vm); } // beforeCreate initInjections(vm); // resolve injections before data/props initState(vm); initProvide(vm); // resolve provide after data/props callHook(vm, 'created'); // code... }; function initState(vm) { // if... if (opts.data) { initData(vm); } else { // 没有data参数 observe(vm._data = {}, true /* asRootData */ ); } // if... } function initData(vm) { // code... while (i--) { if (props && hasOwn(props, keys[i])) { // warning } else if (!isReserved(keys[i])) { proxy(vm, "_data", keys[i]); } } // observe data... } // target => vm // sourceKey => _data 这个还有可能是props 不过暂时不管了 // key => data参数中所有的对象、数组 function proxy(target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter() { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter(val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }
可以看到,最后一个函数中,通过defineProperty方法,所有对vm属性的直接访问会被跳转到Vue$3[sourceKey]上,这里指就是_data属性。
而这个属性的读写,同样被特殊处理过,即数据劫持,跑源码的时候也讲过,直接贴核心代码:
function defineReactive$$1(obj, key, val, customSetter) { // var... Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); } if (Array.isArray(value)) { dependArray(value); } } return value }, set: function reactiveSetter(newVal) { // set... } }); }
简单来讲,所有对_data上的属性的读写都会被拦截并调用自定义的get、set方法,这里也不例外,数据会被添加到依赖接受监听,详细过程太细腻就不贴了,有兴趣可以自己去跑跑。
访问items后,数组中的元素会被watch,有变化会通知DOM进行更新,这里接下来会执行_l方法:
Vue.prototype._l = renderList; // val => items // render => function(item){...} function renderList(val, render) { var ret, i, l, keys, key; // 数组 => 遍历进行值渲染 if (Array.isArray(val) || typeof val === 'string') { ret = new Array(val.length); for (i = 0, l = val.length; i < l; i++) { ret[i] = render(val[i], i); } } // 纯数字 => 处理类似于item in 5这种无数据源的模板渲染 else if (typeof val === 'number') { ret = new Array(val); for (i = 0; i < val; i++) { ret[i] = render(i + 1, i); } } // 对象 => 取对应的值进行渲染 else if (isObject(val)) { keys = Object.keys(val); ret = new Array(keys.length); for (i = 0, l = keys.length; i < l; i++) { key = keys[i]; ret[i] = render(val[key], key, i); } } return ret }
代码还是清晰的,三种情况:数组、纯数字、对象。
用过应该都明白是如何处理三种情况的,这里将对应的值取出来调用render方法,这个方法来源于第二个参数:
// item => 1,2,3,4,5 (function(item) { return _c('a', {attrs: {"href": "#"}}, [_v(_s(item))]) })
方法很抽象,慢慢解析。
因为与tag相关,所以再次调用了_c函数,但是执行顺序还是从内到外,因此会对_v、_s做过滤并首先调用_s函数:
Vue.prototype._s = toString; // val => item => 1,2,3,4,5 function toString(val) { return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val) }
这个方法一句话概括就是字符串化传进来的参数。
这里先传了一个数字1,返回字符串1并将其作为参数传入_v函数:
Vue.prototype._v = createTextVNode; // val => 1 function createTextVNode(val) { return new VNode(undefined, undefined, undefined, String(val)) }
这个函数从命名也能看出来,创建一个文本的vnode,值为传进来的参数。
可以看一眼这个虚拟DOM的结构:,因为是文本节点,所以只有text是有值的。
形参都处理完毕,下一步进入_c函数,看下代码:
vm._c = function(a, b, c, d) { return createElement(vm, a, b, c, d, false); }; var SIMPLE_NORMALIZE = 1; var ALWAYS_NORMALIZE = 2; function createElement(context, tag, data, children, normalizationType, alwaysNormalize) { // 参数修正 if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children; children = data; data = undefined; } // 模式设定 if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE; } return _createElement(context, tag, data, children, normalizationType) } // context => vm // tag => 'a' // data => {attr:{'href':'#'}} // children => [vnode...] // normalizationType => undefined // alwaysNormalize => false function _createElement(context, tag, data, children, normalizationType) { if (isDef(data) && isDef((data).__ob__)) { // warning... return createEmptyVNode() } 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); } var vnode, ns; if (typeof tag === 'string') { var Ctor; // 判断标签是否为math、SVG // math是HTML5新出的标签 用来写数学公式 // SVG就不用解释了吧…… ns = config.getTagNamespace(tag); // 判断标签是否为内置标签 if (config.isReservedTag(tag)) { // 生成vnode // config.parsePlatformTagName返回传入的值 是一个傻逼函数 vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ); } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag); } else { // 未知标签 vnode = new VNode( tag, data, children, undefined, undefined, context ); } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children); } if (isDef(vnode)) { // 特殊标签处理 if (ns) { applyNS(vnode, ns); } return vnode } else { return createEmptyVNode() } }
其实吧,这函数看起来那么长,其实也只能根据传进去的参数生成一个vnode,具体过程看注释,看看结果:
可以看出,属性还是那样子,没怎么变,children是之前生成的那个文本虚拟DOM。
在renderList函数中,循环调用render,分别传进去items数组的1、2、3、4、5,所以依次生成了5个vnode,作为数组ret的元素,最后返回一个数组:
接下来进入外部的_c函数,这一次是对div标签进行转化,过程与上面类似,最后生成一个完整的虚拟DOM,如下所示:
这里也就将整个挂载的DOM转化成了虚拟DOM,其实吧,一点也不难,是吧!