Vue.js 使用了基于 HTML 的模板语法,所以我们一般直接写模板,例如:
template: '<div style="width: 200px; height: 200px; border: 5px solid red">{{msg}}</div>'
当然我们也可以直接写render函数。下面我们就看看怎么用render函数达到相同的效果。
<html>
<head>
<style type="text/css">
</style>
</head>
<body>
<script src="./vue.js"></script>
<div id="app">
<my-render-comp :msg="message"></my-render-comp>
</div>
<script>
Vue.component('myRenderComp', {
props : ["msg"],
//template: '<div style="width: 200px; height: 200px; border: 5px solid red">{{msg}}</div>'
render(createElement){
return createElement(
"div",
{
style : {
width: "200px",
height: "200px",
border: "5px solid red"
}
},
this.msg
)
}
})
const app = new Vue({
data(){
return{
message : "my render component"
}
}
}).$mount("#app");
</script>
</body>
</html>
效果如下:
代码很简单,关键是render函数的参数createElement, 此参数是个函数,是用来创建Vnode的函数。而这个函数有四个参数,我们引用Vue官网上对这4个参数的描述:
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中属性对应的数据对象。可选。
{
// (详情见下一节)
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
下面我们深入源码看看怎么使用render函数的。
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render; //在这里取出的就是组件中的render函数
var _parentVnode = ref._parentVnode;
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode;
// render self
var vnode;
try {
// There's no need to maintain a stack becaues all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm;
vnode = render.call(vm._renderProxy, vm.$createElement); //把vm.$createElement
//传递给render函数的参数,且render函数的上下文是vm,render函数要返回
//vnode
} catch (e) {
handleError(e, vm, "render");
//some code
}
vm.$createElement 传递给了render函数的参数,在render函数中要调用vm.$createElement函数。
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
//在这里判断data如果是数组或简单类型变量,则认为data不存在,把data赋值给第三个参数children
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
console.log("liubbc data is: " + JSON.stringify(data));
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (
context,
tag,
data,
children,
normalizationType
) {
// some code
if (typeof tag === 'string') {
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options,
'components', tag))) {
// component 不是元素节点,而是组件节点my-render-comp,所以走这个else if分支
vnode = createComponent(Ctor, data, context, children, tag);
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
);
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}
走到这里就和用模板一样了。render函数省去了模板编译步骤这一步。模板最终也是编译成render函数。
下面我们看看render函数中,createElement函数中的第二个参数是怎么处理的。
{
style : {
width: "200px",
height: "200px",
border: "5px solid red"
}
},
从Vue官方网站上看到这个参数是object类型。对象属性可以是style,class等。我们从源码看看,对象属性还可以有哪些。
其实在render函数生成Vnode阶段只是把这个参数传递给Vnode对象的data属性,在基于Vnode创建真实Dom阶段会处理这个data属性,之前文章也分析过基于Vnode创建真实Dom过程,我们就直接看createElm函数了。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode);
}
vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
//普通元素节点
{
if (data && data.pre) {
creatingElmInVPre++;
}
if (isUnknownElement$$1(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
);
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode); //创建真实Dom节点
setScope(vnode);
/* istanbul ignore if */
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
//在这里遍历create hooks,之前文章分析过register ref过程
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);//挂载节点到父节点
}
if (data && data.pre) {
creatingElmInVPre--;
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
我们之前文章分析过Vue $refs用法,其实ref也作为data对象中的一个属性,和style,class属性处理过程是一样的。这里我们再分析一遍。
if (isDef(data)) {
//在这里遍历create hooks,之前文章分析过register ref过程
invokeCreateHooks(vnode, insertedVnodeQueue);
}
function updateStyle (oldVnode, vnode) {
//这个函数对style属性进行处理
var data = vnode.data;
var oldData = oldVnode.data;
if (isUndef(data.staticStyle) && isUndef(data.style) &&
isUndef(oldData.staticStyle) && isUndef(oldData.style)
) {
return
}
var cur, name;
var el = vnode.elm;
var oldStaticStyle = oldData.staticStyle;
var oldStyleBinding = oldData.normalizedStyle || oldData.style || {};
// if static style exists, stylebinding already merged into it when doing
normalizeStyleData
var oldStyle = oldStaticStyle || oldStyleBinding;
var style = normalizeStyleBinding(vnode.data.style) || {};
// store normalized style under a different key for next diff
// make sure to clone it if it's reactive, since the user likely wants
// to mutate it.
vnode.data.normalizedStyle = isDef(style.__ob__)
? extend({}, style)
: style;
var newStyle = getStyle(vnode, true);
for (name in oldStyle) {
if (isUndef(newStyle[name])) {
setProp(el, name, '');
}
}
for (name in newStyle) {
cur = newStyle[name];
if (cur !== oldStyle[name]) {
// ie9 setting to null has no effect, must use empty string
setProp(el, name, cur == null ? '' : cur); //在这里对节点加样式
}
}
}
var style = {
create: updateStyle,
update: updateStyle
};
var platformModules = [
//data对象中可以包含以下属性
attrs,
klass,
events,
domProps,
style, //style 在这里处理的
transition
];
/* */
// the directive module should be applied last, after all
// built-in modules have been applied.
var modules = platformModules.concat(baseModules);
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
function createPatchFunction (backend) {
var i, j;
var cbs = {};
var modules = backend.modules;
var nodeOps = backend.nodeOps;
//在这里注册钩子函数
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]]);
}
}
}
写的比较粗糙,但大体流程,关键代码都在这了,如果哪里还有不明白的地方,请留言。