Vue源码后记-更多options参数(1)
我是这样计划的,写完这个还写一篇数据变动时,VNode是如何更新的,顺便初探一下diff算法。
至于vue-router、vuex等插件源码,容我缓一波好吧,vue看的有点伤。
其实在之前讲其余内置指令有涉及到on事件绑定,这里再详细从头看一下事件绑定的正统流程吧!
上html代码:
<body> <div id='app'> <div @click.self.number='click'> {{computedValue | filter}} </div> </div> </body> <script src='./vue.js'></script> <script> new Vue({ el: '#app', data: { value: 1 }, computed: { computedValue: function() { return this.value * 2 } }, methods: { click: function() { console.log('click!'); } }, filters: { filter: function(value) { if (!value) { return; } return value * 4; } } }) </script>
包含最常见的click事件,计算属性以及过滤器,钩子函数讲过就不写了,所有的函数都是最简单模式。
1、mergeOptions
源码看的有点恶心,流程都能背下来了。
第一步是进行参数合并,传入的对象作为options与默认的options进行合并,并通过strat对象对应的方法处理:
// parent => baseOptions // child => options function mergeOptions(parent, child, vm) { // code... function mergeField(key) { var strat = strats[key] || defaultStrat; options[key] = strat(parent[key], child[key], vm, key); } return options }
这里主要是调用strat对象方法对每一个options的键进行处理,目前传进来有data、computed、methods、filters四个。
data前面讲过,其余三个依次看一下。
computed、methods
两个参数对应同一个方法:
strats.props = strats.methods = strats.computed = function(parentVal, childVal) { if (!childVal) { return Object.create(parentVal || null) } if (!parentVal) { return childVal } var ret = Object.create(null); extend(ret, parentVal); extend(ret, childVal); return ret };
比较简单,将两个合并,直接返回该对象并挂载到vm上:
filters
function mergeAssets(parentVal, childVal) { var res = Object.create(parentVal || null); return childVal ? extend(res, childVal) : res }
非常无趣的函数:
2、initState
vue上面属性添加完后,接下来是数据初始化阶段:
function initState(vm) { vm._watchers = []; var opts = vm.$options; if (opts.props) { initProps(vm, opts.props); } if (opts.methods) { initMethods(vm, opts.methods); } if (opts.data) { initData(vm); } else { observe(vm._data = {}, true /* asRootData */ ); } if (opts.computed) { initComputed(vm, opts.computed); } if (opts.watch) { initWatch(vm, opts.watch); } }
可以看到这里对props、methods、data、computed、watch几个属性都做了初始化。
methods
function initMethods(vm, methods) { var props = vm.$options.props; for (var key in methods) { // 重新绑定上下文 vm[key] = methods[key] == null ? noop : bind(methods[key], vm); { if (methods[key] == null) { // 函数对象值为空时 } // 判断重名 if (props && hasOwn(props, key)) { // warning } } } }
这个Init函数做了2件事,第一是对所有函数进行上下文绑定并挂载到vm上,第二是与props进行排重,不允许出现相同键名。
initComputed
function initComputed(vm, computed) { var watchers = vm._computedWatchers = Object.create(null); for (var key in computed) { var userDef = computed[key]; var getter = typeof userDef === 'function' ? userDef : userDef.get; { if (getter === undefined) { // warn No getter function getter = noop; } } // create internal watcher for the computed property. watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions); if (!(key in vm)) { defineComputed(vm, key, userDef); } else { // key查重 } } } function defineComputed(target, key, userDef) { if (typeof userDef === 'function') { sharedPropertyDefinition.get = createComputedGetter(key); sharedPropertyDefinition.set = noop; } else { // setter、getter } Object.defineProperty(target, key, sharedPropertyDefinition); } function createComputedGetter(key) { return function computedGetter() { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value } } }
处理computed时,内部会new一个watcher来专门监听相关数据变动,然后用defineProperty在vm上声明一个对象。
2、parseHTML
合并完参数并做init初始化后,第二步应该是解析DOM字符串了。
直接看关键点,外层的div跳过,只看里层的,首先是:
<div @click.self.number='click'>
切割函数为:
var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); continue }
第一次暴力切割后,直接以=分割属性:
接下来进入handleStartTag => start => processAttrs
function processAttrs(el) { var list = el.attrsList; var i, l, name, rawName, value, modifiers, isProp; for (i = 0, l = list.length; i < l; i++) { name = rawName = list[i].name; value = list[i].value; // name => @click.self.number if (dirRE.test(name)) { el.hasBindings = true; // 修饰符 // modifiers => {self:true,number:true} modifiers = parseModifiers(name); if (modifiers) { // name => @click name = name.replace(modifierRE, ''); } if (bindRE.test(name)) { // v-bind // code... } else if (onRE.test(name)) { // v-on name = name.replace(onRE, ''); addHandler(el, name, value, modifiers, false, warn$2); } else { // normal directives // code... } } else { // code... } } } function parseModifiers(name) { // modifierRE => /\.[^.]+/g; var match = name.match(modifierRE); if (match) { var ret = {}; match.forEach(function(m) { ret[m.slice(1)] = true; }); return ret } }
这里会首先对事件name进行修饰符判断,截取出self与number两个字段,保存到modifiers对象中。
修饰符处理完后,进入v-on指令分支,首先正则替换掉第一个@字符,进入addHandler函数。
function addHandler(el, name, value, modifiers, important, warn) { // handle capture,passive,once var events; if (modifiers && modifiers.native) { delete modifiers.native; events = el.nativeEvents || (el.nativeEvents = {}); } else { events = el.events || (el.events = {}); } var newHandler = { value: value, modifiers: modifiers }; var handlers = events[name]; /* istanbul ignore if */ if (Array.isArray(handlers)) { important ? handlers.unshift(newHandler) : handlers.push(newHandler); } else if (handlers) { events[name] = important ? [newHandler, handlers] : [handlers, newHandler]; } else { // {value:click,modifiers:{self:true,number:true}} events[name] = newHandler; } }
该函数处理特殊事件类型,包括capture、once、passive,并会给事件字符串添加特殊的前缀。
完事后,该AST会被添加一个events属性,如图:
下面转换节点内部的表达式:
{{computedValue | filter}}
关键函数是parseFilters,用来分割过滤器:
function parseFilters(exp) { // var... for (i = 0; i < exp.length; i++) { prev = c; c = exp.charCodeAt(i); if (inSingle) { // code... } else if ( c === 0x7C && // pipe exp.charCodeAt(i + 1) !== 0x7C && exp.charCodeAt(i - 1) !== 0x7C && !curly && !square && !paren ) { // 第一次匹配到 | 时 if (expression === undefined) { // first filter, end of expression lastFilterIndex = i + 1; expression = exp.slice(0, i).trim(); } // 多个filter串联 else { pushFilter(); } } else { // code... } } if (expression === undefined) { expression = exp.slice(0, i).trim(); } else if (lastFilterIndex !== 0) { pushFilter(); } function pushFilter() { (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim()); lastFilterIndex = i + 1; } if (filters) { for (i = 0; i < filters.length; i++) { expression = wrapFilter(expression, filters[i]); } } return expression }
其中切割大括号的部分跳过,主要看是如何处理分割后的filter:
function wrapFilter(exp, filter) { // filter有可能带有参数 => {{value | filter(args)}} var i = filter.indexOf('('); if (i < 0) { // _f: resolveFilter return ("_f(\"" + filter + "\")(" + exp + ")") } else { var name = filter.slice(0, i); var args = filter.slice(i + 1); return ("_f(\"" + name + "\")(" + exp + "," + args) } }
这里本来只有两种情况,一种只有函数名,一种是带参数的函数。
1、单纯函数
{{computedValue | filter}}
如果只有函数名,此时i为-1,会进入第一个分支,直接返回对应的拼接字符串,如图:
2、带参数的函数
{{computedValue | filter(1)}}
此时会进入分支2,并且通过正则进行切割,name为函数名,args为参数,最后返回拼接字符串:
3、???
后来,我又发现了第三种情况,就是如果函数名被括号给包起来,解析会变得有点奇怪。
{{computedValue | (filter)}}
此时,由于检测到小括号的存在,后面的被认为是形参,一个空白字符串被当成函数名进行拼接,返回如图:
当然,这个会报错,filter被认为是形参,又不存在对应的函数,既然有warning提示,也不算啥问题,尽量不作死就行。
至此,AST的转化完成,下一节讲render函数的生成。