Vue源码后记-其余内置指令(3)
其实吧,写这些后记我才真正了解到vue源码的精髓,之前的跑源码跟闹着玩一样。
go!
之前将AST转换成了render函数,跳出来后,由于仍是字符串,所以调用了makeFunction将其转换成了真正的函数:
function compileToFunctions(template, options, vm) {
// code...
// compile
var compiled = compile(template, options);
// code...
// 转换render
res.render = makeFunction(compiled.render, fnGenErrors);
var l = compiled.staticRenderFns.length;
// 转换staticRenderFns
res.staticRenderFns = new Array(l);
for (var i = 0; i < l; i++) {
res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors);
}
// code...
return (functionCompileCache[key] = res)
}
function makeFunction(code, errors) {
try {
return new Function(code)
} catch (err) {
// error...
}
}
这个没啥讲的
将render转换成VNode其实也没什么讲的,重点看一下之前没见过的函数,
_c('div' /*<div id='app'>*/ , {
attrs: {
"id": "app"
}
}, [(vIfIter) /*v-if条件*/ ?
// 条件为真渲染下面的DOM
_c('div' /*<div v-if="vIfIter" v-bind:style="styleObject">*/ , {
style: (styleObject)
}, [_c('input' /*<input v-show="vShowIter" v-model='vModel' />*/ , {
directives: [{
name: "show",
rawName: "v-show",
value: (vShowIter),
expression: "vShowIter"
}, {
name: "model",
rawName: "v-model",
value: (vModel),
expression: "vModel"
}],
domProps: {
"value": (vModel)
},
on: {
"input": function($event) {
if ($event.target.composing) return;
vModel = $event.target.value
}
}
}),
_v(" ") /*这些是回车换行符*/ ,
_m(0) /*<span v-once>{{msg}}</span>*/ , _v(" "),
_c('div' /*<div v-html="html"></div>*/ , {
domProps: {
"innerHTML": _s(html)
}
})
]) :
// 否则渲染一个空的div...(错了)
_e() /*comment*/ ,
_v(" "),
_c('div' /*<div class='on'>empty Node</div>*/ , {
staticClass: "on"
}, [_v("empty Node")])
])
该render函数包含_c、_v、_m、_e、_s5个函数,其中_c、_v、_s之前都讲过,这里看一下_m、_e是什么。
_m
直接看源码:
Vue.prototype._m = renderStatic;
function renderStatic(index, isInFor) {
var tree = this._staticTrees[index];
// 如果该静态节点已经被渲染且不在v-for中
// 复用该节点
if (tree && !isInFor) {
return Array.isArray(tree) ?
cloneVNodes(tree) :
cloneVNode(tree)
}
// otherwise, render a fresh tree.
tree = this._staticTrees[index] =
this.$options.staticRenderFns[index].call(this._renderProxy);
markStatic(tree, ("__static__" + index), false);
return tree
}
可以看到,对于静态节点,vue做了一层缓存,尽量复用现成的虚拟DOM,但是目前是初次渲染,所以会创建一个新的。
这里有两步。
第一步:this.$options.staticRenderFns[index].call(this._renderProxy)
即取出staticRenderFns对应索引的函数并执行,将其缓存到_staticTrees上。
之前在生成render函数时,将v-once的节点当成静态节点处理,弹入了该数组,函数如下:
(function() {
with(this) {
return _c('span', [_v(_s(msg))])
}
})
这里_s将msg字符串化,_v生成一个文本VNode,_c生成一个带有tag的VNode,children为之前的VNode。
第二步:markStatic(tree, ("__static__" + index), false)
给VNode做标记。
// tree => VNode
// key => __static__0
// isonce => false
function markStatic(tree, key, isOnce) {
if (Array.isArray(tree)) {
for (var i = 0; i < tree.length; i++) {
if (tree[i] && typeof tree[i] !== 'string') {
// key => __static__0_0...
markStaticNode(tree[i], (key + "_" + i), isOnce);
}
}
} else {
markStaticNode(tree, key, isOnce);
}
} function markStaticNode(node, key, isOnce) {
node.isStatic = true;
node.key = key;
node.isOnce = isOnce;
}
比较简单,直接看结果了:
_e
这个其实我在注释里写了,就是一个空的div,瞄一眼源码,发现我错了:
Vue.prototype._e = createEmptyVNode;
var createEmptyVNode = function() {
var node = new VNode();
node.text = '';
node.isComment = true;
return node
};
生成一个空的VNode,将其标记为注释,内容为空。
//剩下的太简单,我不想讲啦!撤了,最近心情不好,兼容360,这客户我真是日了狗了。
还没完,讲讲patch阶段那些directives、on、domProps是如何渲染的吧!
input
<input v-show="vShowIter" v-model='vModel' />



VNode中的data属性细节可以看图,这里看一下domProps、on是如何渲染的。
首先on是事件相关,刚发现chrome调试一个特别好用的东西,可以看函数流向!

图中patch是渲染DOM的入口函数,createElm生成DOM节点,createChildren递归处理子节点,invokeCreateHooks则负责处理节点的属性,updateDOMListeners很显然是处理事件绑定,看一下源码:
function updateDOMListeners(oldVnode, vnode) {
// 新旧VNode至少有一个存在on属性
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
// 保存属性
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
target$1 = vnode.elm;
// 特殊情况处理
normalizeEvents(on);
updateListeners(on, oldOn, add$1, remove$2, vnode.context);
}
除去判断,这里会先对特殊情况下的on做特殊处理,然后再进行事件绑定,可以看下处理的代码:
function normalizeEvents(on) {
var event;
/* istanbul ignore if */
if (isDef(on[RANGE_TOKEN])) {
// IE input[type=range] only supports `change` event
event = isIE ? 'change' : 'input';
on[event] = [].concat(on[RANGE_TOKEN], on[event] || []);
delete on[RANGE_TOKEN];
}
if (isDef(on[CHECKBOX_RADIO_TOKEN])) {
// Chrome fires microtasks in between click/change, leads to #4521
event = isChrome ? 'click' : 'change';
on[event] = [].concat(on[CHECKBOX_RADIO_TOKEN], on[event] || []);
delete on[CHECKBOX_RADIO_TOKEN];
}
}
可以看到,特殊情况有两种:
第一种是IE下的type=range,这个H5属性只支持IE10+,并且在IE中只有change事件。
第二种是Chrome下的radio、checkbox,事件类型会被置换为click。
接下来是事件的绑定函数:
// on/oldOn => 新旧VNode事件
// add/remove$$1 => 事件的绑定与解绑函数
function updateListeners(on, oldOn, add, remove$$1, vm) {
var name, cur, old, event;
for (name in on) {
cur = on[name];
old = oldOn[name];
// str => obj
event = normalizeEvent(name);
if (isUndef(cur)) {
// error
}
// 添加事件
else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur);
}
add(event.name, cur, event.once, event.capture, event.passive);
}
// 事件替换
else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
}
// 旧VNode事件解绑
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name);
remove$$1(event.name, oldOn[name], event.capture);
}
}
}
在遍历所有事件类型字符串的时候,由于可能会有特殊标记,所以会对其进行解析转换为一个对象:
var normalizeEvent = cached(function(name) {
var passive = name.charAt(0) === '&';
name = passive ? name.slice(1) : name;
var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first
name = once$$1 ? name.slice(1) : name;
var capture = name.charAt(0) === '!';
name = capture ? name.slice(1) : name;
return {
name: name,
once: once$$1,
capture: capture,
passive: passive
}
});
意思简单明了,这里就不解释了。
接下来会将事件处理函数作为属性挂载到一个invoker函数上:
// 将函数或函数数组作为属性挂到函数上 可以调用执行
function createFnInvoker(fns) {
function invoker() {
var arguments$1 = arguments; var fns = invoker.fns;
if (Array.isArray(fns)) {
for (var i = 0; i < fns.length; i++) {
fns[i].apply(null, arguments$1);
}
} else {
// return handler return value for single handlers
return fns.apply(null, arguments)
}
}
invoker.fns = fns;
return invoker
}
这样做的原因可能是方便执行事件,有时候一个DOM会有多个相同事件,此时事件会是一个数组,通过这样处理后,无论是单一函数还是函数数组都可以通过直接调用invoker来执行。
下面就是最后一个步骤,事件绑定:
// event => input
// handler => invoker
// 剩余三个为之前normalizeEvent的属性
function add$1(event, handler, once$$1, capture, passive) {
// 一次性执行事件
if (once$$1) {
var oldHandler = handler;
var _target = target$1;
handler = function(ev) {
// 单参数 or 多参数
var res = arguments.length === 1 ?
oldHandler(ev) :
oldHandler.apply(null, arguments);
// 执行完立马解绑事件
if (res !== null) {
remove$2(event, handler, capture, _target);
}
};
}
target$1.addEventListener(
event,
handler,
supportsPassive ? {
capture: capture,
passive: passive
} :
capture
);
}
对于一次性执行事件,这里的处理和jQuery源码里还是蛮像的,不过要简洁多了,这个很简单,没啥讲的。
下面处理domProps,起初我以为这个属性是专门处理组件间传值那个props的,后来发现这属性有点瞎:
function updateDOMProps(oldVnode, vnode) {
if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
return
}
var key, cur;
var elm = vnode.elm;
var oldProps = oldVnode.data.domProps || {};
var props = vnode.data.domProps || {};
// __ob__属性代表动态变化的值
if (isDef(props.__ob__)) {
props = vnode.data.domProps = extend({}, props);
}
// 新VNode缺失属性置为空
for (key in oldProps) {
if (isUndef(props[key])) {
elm[key] = '';
}
}
for (key in props) {
cur = props[key];
// 这两种情况特殊处理
if (key === 'textContent' || key === 'innerHTML') {
}
if (key === 'value') {
// 先保存值 之后所有值会被转为字符串
elm._value = cur;
// avoid resetting cursor position when value is the same
var strCur = isUndef(cur) ? '' : String(cur);
if (shouldUpdateValue(elm, vnode, strCur)) {
elm.value = strCur;
}
} else {
elm[key] = cur;
}
}
}
function shouldUpdateValue(elm, vnode, checkVal) {
return (!elm.composing && (
vnode.tag === 'option' ||
// document.activeElement !== elm && elm.value !== checkVal
isDirty(elm, checkVal) ||
// 处理trim,number
isInputChanged(elm, checkVal)
));
}
其中包括两种情况,textContent、innerHTML、value以及其他,此处props的值为value,会判断当前DOM的值是否一致,然后进行修正。
当props为textContent或innerHTML时,需要将所有子节点清空,然后将对应的属性修改为对应的值。
至此,基本上必要的点已经完事了~啊。。。。88
Vue源码后记-其余内置指令(3)的更多相关文章
- 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源码后记-vFor列表渲染(1)
钩子函数比较简单,没有什么意思,这一节搞点大事情 => 源码中v-for的渲染过程. vue的内置指令包含了v-html.v-if.v-once.v-bind.v-on.v-show等,先从一个 ...
- Vue源码后记-钩子函数
vue源码的马拉松跑完了,可以放松一下写点小东西,其实源码讲20节都讲不完,跳了好多地方. 本人技术有限,无法跟大神一样,模拟vue手把手搭建一个MVVM框架,然后再分析原理,只能以门外汉的姿态简单过 ...
- Vue源码后记-vFor列表渲染(2)
这一节争取搞完! 回头来看看那个render代码,为了便于分析,做了更细致的注释: (function() { // 这里this指向vue对象 下面的所有方法默认调用Vue$3.prototype上 ...
- Vue源码后记-更多options参数(2)
写起来感觉都是老三套,AST => render => VNode => patch 之前是把AST弄完了,对事件和过滤器处理如图: render函数也只看这两部分的转换吧! 首先是 ...
- android studio应用修改到android源码中作为内置应用
1. 方法一:导入,编译(太麻烦,各种不兼容问题) android studio和eclipse的应用结构目录是不同的,但是在android源码中的应用基本上都是使用的eclipse目录结构(在/pa ...
- 10.源码分析---SOFARPC内置链路追踪SOFATRACER是怎么做的?
SOFARPC源码解析系列: 1. 源码分析---SOFARPC可扩展的机制SPI 2. 源码分析---SOFARPC客户端服务引用 3. 源码分析---SOFARPC客户端服务调用 4. 源码分析- ...
随机推荐
- jz2440重新分区
在购买开发板的时候,板子上已经烧写好了bootloader.内核和文件系统.但是在具体使用时,发现板子上划分的内核分区只有2M,但是我编译出来的内核大于2M,于是将内核烧写到nandflash上面时会 ...
- mapreduce自定义排序(map端1.4步)
3 3 3 2 3 1 2 2 2 1 1 1 -----------------期望输出 1 1 2 1 2 2 3 1 3 2 3 3 将以上数据进行排序,排序规则是:按照第一列升序排序,如果第一 ...
- JVM菜鸟进阶高手之路五
转载请注明原创出处,谢谢! 参考gc,发现大概一小时运行一次FGC,特别奇怪,笨神一看这样的问题就知道是system gc导致的,rmi默认一小时主动触发一次,由于没有gc日志,通过jstat命令观察 ...
- [UIKit学习]00.关于前置知识(storyboard,UIViewController,类扩展,项目属性)
storyboard文件的认识 用来描述软件界面 默认情况下,程序一启动就会加载Main.storyboard 加载storyboard时,会首先创建和显示箭头所指的控制器界面 IBAction和IB ...
- nodejs 初次链接 mongodb 的详细细节
时间 2016-06-2613:05:16 在前端的学习也有一段时间了,学习了html,css,javascript,jqery,ajax,php,mysql,学习了这些,了解了一些皮毛,也没有什么 ...
- asp.net core合并压缩资源文件引发的学习之旅
0. 在asp.net core中使用BuildBundlerMinifier合并压缩资源文件 在asp.net mvc中可以使用Bundle来压缩合并css,js 不知道的见:http://www. ...
- 接口测试——HttpClient工具的https请求、代理设置、请求头设置、获取状态码和响应头
目录 https请求 代理设置 请求头设置 获取状态码 接收响应头 https请求 https协议(Secure Hypertext Transfer Protocol) : 安全超文本传输协议, H ...
- SVN初体验
呐,部门领导要求今后项目部分内容要实行版本控制,因此有机会深入接触下SVN这门功课 ---------------------------------------------------------- ...
- ZOJ-2091-Mean of Subsequence (反证法的运用!!)
http://blog.csdn.net/u014355480/article/details/40862041 ZOJ2091 题意:其实就是找后几个数的平均值的最大值!! (贪心策略!要找对) k ...
- springboot mybatis优雅的添加多数据源
springboot的原则是简化配置,本文试图不通过xml配置,使用configuration配置数据源,并进行简单的数据访问. 并且配置了多数据源,在开发过程中这种场景很容易遇到. 1.依赖 spr ...