【vue源码解析】render到底作了什么?

render的做用

       render函数能够做为一道分割线,render函数的左边能够称之为编译期,将Vue的模板转换为渲染函数。render函数的右边是Vue的运行时,主要是基于渲染函数生成Virtual DOM树,Diff和Patch。vue

  • render渲染函数将结合数据生成Virtual DOM的。
  • 有了虚拟的DOM树后,再交给Patch函数,负责把这些虚拟DOM真正施加到真实的DOM上。在这个过程当中,Vue有自身的响应式系统来侦测在渲染过程当中所依赖到的数据来源。在渲染过程当中,侦测到数据来源以后就能够精确感知数据源的变更。
  • 根据须要从新进行渲染。当从新进行渲染以后,会生成一个新的树,将新的树与旧的树进行diff对比,就能够最终落实到真实DOM上的改动。

一个简单的实例,看render:


vue的渲染机制能够总结以下图:

                         

render前置操做

  • 为什么要用with(this){}包裹?
  • 什么时候将render函数的字符串转成函数?

第1个问题?:node

with一般被当作重复引用同一个对象中的多个属性的快捷方式,能够不须要重复引用对象自己。引自《你不知道的JavaScript》上2.2.2节with

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

第2个问题?:缓存

通过parse生成AST和optimize对AST树的优化,会生成render函数的字符串。在下图框出的createCompilerCreator(在src\compiler\create-compiler.js),调用createCompileToFunctionFn将字符串转成函数。bash



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


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)主要作了三件事情

  1. 执行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()复制代码
  2. 在原型上扩展Vue.prototype.$nextTick方法,在watch监听数据变化时,不会立马更新视图,会推到一个队列里,nextTick会触发视图的更新
  3.  在原型上扩展Vue.prototype._render。

//核心代码
vnode = render.call(vm._renderProxy, vm.$createElement)复制代码

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

  1.  第一个参数是HTML标签字符 “必选”
  2. 第一个参数是HTML标签字符 “必选”
  3. 第三个参数是传涵盖子元素的一个数组 “可选”

<div id="app">
    <render-element></render-element>
</div>

Vue.component('render-element', {
    render: function (createElement) {
        var self = this
        return createElement(
            'div', // 第一个参数是HTML标签字符 “必选”
            {
                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 _createElement (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()
        }

        // 当组件的tag未空,渲染一个空节点
        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 = normalizeChildren(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', tag))) {
                // 若是找到了这个标签的定义,就以此建立虚拟组件节点
                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()oop

总结

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

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

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

相关文章
相关标签/搜索