深度学习vue系列 —— render函数

·  阅读 630

渲染三部曲= compile + render生成Vnode + 将Vnode通过 update 挂载到 页面上

render的作用

render函数可以作为一道分割线,render函数的左边可以称之为编译期,将Vue的模板转换为渲染函数。render函数的右边是Vue的运行时,主要是基于渲染函数生成Virtual DOM树,DiffPatch

  • render渲染函数将结合数据生成Virtual DOM的。

  • 有了虚拟的DOM树后,再交给Patch函数,负责把这些虚拟DOM真正施加到真是的DOM上,在这个过程中,Vue有自身的响应式系统来侦测在渲染过程中所依赖到的数据来源。在渲染过程中,侦测到数据来源之后就可以精确感知数据源的变动。

  • 根据需要重新进行渲染。当重新进行渲染之后,会生成一个新的树,将新的树与旧的树进行diff对比,就可以最终落实到真是的DOM上的改动。

vue的渲染机制可以总结如下图:

render前置操作

  • 为什么要用with(this){}包裹?

with通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

with(this)会形成块级作用域this,render函数里面的变量都会指向Vue实例(this)

  • 何时将render函数的字符创转换成函数?

经过Parse生成AST和optimize对AST树的优化,使用AST生成render函数的字符串。在下图框处的createCompilerCreator(在src\compiler\create-compiler.js),电泳createCompileToFunctionFn将字符串转成函数。

createCompileToFunctionFn(在src\compiler\to-function.js)中,会优先读缓存信息,若没有才会执行编译方法,同时将render字符串通过createFunction(在src\compiler\to-function.js中)调用New Function()的方法,创造render函数,并且缓存信息。

function createFunction (code, errors) { 
 try {    
    return new Function(code)  
 } catch (err) {    
    errors.push({ err, code })    
    return noop 
 }
}
复制代码

render详解

render初始化入口:src\core\instance\index.js renderMixin(在src\core\instance\render.js)主要做了三件事情:

  • 执行installRenderHelpers(Vue.prototype),在原型上扩展如下,生成vnode节点的几个函数解析,在执行render函数时,会调用。如下图:

export function renderSlot (  
    name: string,  
    fallback: ?Array<VNode>,  
    props: ?Object,  
    bindObject: ?Object): ?Array<VNode>     { 
       const scopedSlotFn = this.$scopedSlots[name]  
       let nodes
     if (scopedSlotFn) {         
         props = props || {}   
         if (bindObject) {     
            props = extend(extend({}, bindObject), props)    
         }    
         nodes = scopedSlotFn(props) || fallback 
     } else {    
         nodes = this.$slots[name] || fallback  
     }  
     const target = props && props.slot  
     if (target) {    
    	//调用createElemnet          
    	return this.$createElement('template', { slot: target }, nodes) 
     } else {    
         return nodes 
     }
}
//有作用域插槽,会合并props和需要绑定的对象,
//不然直接去$solt数组里取,最后会调用creatElement()
复制代码
  • 在原型上扩展Vue.prototype.$nextTick方法,在watch监听数据变化时,不会立马更新视图,会推到一个队列里,nextTick会触发视图的更新.
  • 在原型上扩展Vue.prototype._render
// 核心代码
vnode = render.call(vm._renderProxy, vm.$createElement)
复制代码

以上是render函数执行的核心代码,render归根结底,是调用createElement创建vnode节点。下面会详细分析createElement到底做了哪些事情,首先我们先通过一个例子,来看下createElement方法的入参,主要为3个入参,可通过一个例子呈现:

  • 第一个参数是HTML标签字符 ==》必选
  • 第二个参数是包含模板相关属性的数据对象 ==》可选
  • 第三个参数是传函盖子元素的一个数组 ==》可选
<div id="app">
	<render-element></render-element>
</div>

Vue.component('render-element',{
	render:function(createElement){
    	var self = this;
        return createElement(
        	// 第一个参数是HTML标签字符 ==》必选
            'div',
            // 第二个参数是包含模板相关属性的数据对象  ==》可选
            {
            	class: {
                	title: true
                },
                style: {
                	border: '1px solid',
                    padding: '10px'
                }
            },
            // 第三个参数是传函盖子元素的一个数组 ==》可选
            [
            	createElement('h1', 'Hello Vue!'),
                createElement('p', '开始学些Vue')
            ]
            
        )
    }
})

let app = new Vue({
	el: 'app'
})
复制代码

createElement的详细解析过程

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

function createElement(context, tag, data, children, normalizationType, alwaysNormalize){
	// 兼容不传data的情况
	if(Array.isArray(data) || isPrimitive(data)){
    	normalizationType = children;
        children = data;
        data = undefined;
    }
    if(alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
    // 调用_createElement创建虚拟节点
    return _createElement(context, tag, data, children, normalizationType)
}

function _creartElement(context, tag, data, children, normalizationType){
	// 如果存在data.__ob__,说明data是被Observer官场的数据,
    // 不能用作虚拟及诶单的data,返回一个空节点.
    if(data && data.__ob__){
    	process.env.NODE_ENV !== 'production' && warn(
        	`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
            'Always create fresh vnode data objects in each render!',
            context
        )
        return createEmptyVnode()
    }
    // 当组件的tage为空,渲染一个空节点
    if(!tag){
    	return createEmptyVnode()
    }
    
    // 作用域插槽
    if(Array.isArray(children) &&typeof children[0] === 'function'){
    	data = data ||{};
        data.scopedSlots = {default: children[0]};
        children.length = 0
    }
    
    // 根据normalizationType的值,选择不同的处理方法
    if(normalizationType === ALWAYS_NORMALIZE){
    	children = normalizationType(children)
    }else if(normalizationType === SIMPLE_NORMALIZE){
    	children = simpleNormalizeChildren(children)
    }
    let vnode, ns
    
    // 如果标签名是字符串类型
    if(typeof tag === 'string'){
    	let Ctor
        // 获取标签命名空间
        ns = config.getTagNamespace(tag)
        
        // 判断是否为保留标签
        if(config.isReservedTag(tag)){
        	// 如果是保留标签,就创建一个这样的vnode
            vnode = new VNode(
            	config.parsePlatformTagName(tag), data, children, 
                undefined, undefined, context
            )
            // vm的components 上查找是否有这个标签定义
        }else if((Ctor = resolveAsset(context.$options, 'components', tage))){
        	// 如果找到了这个标签的定义,就以此创建虚拟组件节点
            vnode = createComponent(Ctor, data, context, children, tag)
        }else {
        	// 兜底方案,正常创建一个vnode
            vnode = new VNode(tag, data, children, 
                undefined, undefined, context
            )
        }
    // 当tag不是字符串的时候,我们认为tag是组建的构造类,直接创建
    }else {
    	vnode = createComponent(tag, data, context, children)
    }
	// 如果有vnode
	if(vnode){
    	// 应用namespace,绑定data,然后返回vnode
    	if(ns) applyNS(vnode, ns)
        if(isDef(data)) registerDeepBindings(data)
        return vnode
    }else{
    	return createEmptyVNode()
    }
}
复制代码

流程图如下:

特别注意当通过tag判断为组件时,会执行createcompontent()

总结

最后总结下render函数的编译的主要几个步骤:

  • 将template字符串解析成ast
  • 优化:将那些不会被改变的节点(statics)打上标记
  • 生成render函数字符串,并用with包裹(最新版本有改为buble)
  • 通过new Function的方式生成render函数并缓存

另外本文的重点是createElement如何生成一个vnode,接下来vnode如何映射到正式的dom上,是通过数据变化,通知vm.watcher,最终调用vm.update,最后调用patch方法映射到真实的dom节点中,这里将要涉及到数据双向绑定。

分类:
阅读
标签: