Skip to content
This repository has been archived by the owner on Dec 26, 2019. It is now read-only.

Latest commit

 

History

History
254 lines (193 loc) · 9.86 KB

【 Vue 源码分析 】渲染 Render (AST -> VNode).md

File metadata and controls

254 lines (193 loc) · 9.86 KB

渲染 Render 功能

在不了解Render实现情况下,很容易会认为它的功能就是直接将AST通过执行渲染方法从而得到真实 DOM(当初我也是这样认为的 😅)。

既然Render不具备上述的功能,那么它的功能又是什么呢?

看到标题,你应该也知道了答案,那就是AST转换为VNode节点。那么转化为真实 DOM 又是发生在哪个步骤呢?不瞒你说,那就是在最后一步Diff过程所得到两个VNode节点差异后,才会将差异渲染到真实环境中形成视图

接下来,我们就来从源码角度探究一下,Render是如何将AST转换为VNode的。😼

从源码角度进行分析

相信看过【从 Template 到 DOM 过程是怎样的】的话,都应该比较清楚。再编译compile后,就会直接调用$mount方法,我们再来简单回顾一下。

Vue.prototype.$mount = function (
 el,
 hydrating
) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating) // 加载元素或组件方法
}

var mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
 el,
 hydrating
) {
  // ...
  var ref = compileToFunctions(template, { // 经过编译Compile后得到最终渲染方法相关对象
    outputSourceRange: "development" !== 'production',
    shouldDecodeNewlines: shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
    delimiters: options.delimiters,
    comments: options.comments
  }, this)
  var render = ref.render;
  var staticRenderFns = ref.staticRenderFns;
  options.render = render; // 将渲染方法挂载到实例选项中
  options.staticRenderFns = staticRenderFns; // 将收集到的静态树挂载到选项中
  // ...
  return mount.call(this, el, hydrating) // 正式开始加载元素
}

从流程上可以看到,通过编译Compile后,就会将相应的渲染方法以及收集到的静态树统一挂载到选项信息中,接着正式开始加载元素。

在加载元素里,最主要就是mountComponent方法,我们再来看看是如何加载的 🤔。

var createEmptyVNode = function (text) { // 创建空VNode节点方法
  if ( text === void 0 ) text = '';

  var node = new VNode();
  node.text = text;
  node.isComment = true;
  return node
}

function mountComponent (
 vm,
 el,
 hydrating
) {
   vm.$el = el
   if (!vm.$options.render) { // 若不存在渲染方法,则作为一个创建空VNode节点来处理
     vm.$options.render = createEmptyVNode;
     // ...
   }
   callHook(vm, 'beforeMount') // 开始执行beforeMount生命周期回调
   var updateComponent // 转化AST为VNode节点外层方法
   // ...
   updateComponent = function () {
     // ...
     var vnode = vm._render() // 最主要的!!!直接转化AST为VNode节点
     // ...
     vm._update(vnode, hydrating) // 将转换好的VNode放到更新函数中使用patch进行比对
     // ...
   }
   new Watcher(vm, updateComponent, noop, { // 创建模板依赖Watcher,其中的更新函数即为updateComponent
     before: function before () {
       if (vm._isMounted && !vm._isDestroyed) {
         callHook(vm, 'beforeUpdate');
       }
     }
   }, true /* isRenderWatcher */)
 }

mountComponent方法会先判断实例选项是否有渲染方法,若无则直接赋值为空VNode节点。接着使用变量updateComponent作为暴露转化ASTVNode外层方法,并在创建模板依赖时将其作为更新回调函数,这样一来,在每次更新时,都会使用updateComponent方法执行转化ASTVNode节点。

在上述代码中,其中_render方法是转化ASTVNode核心方法,而另一个_update方法则是最后用于patch比对用到的(_update方法暂时不讲,会在下一章Diff说法中提及)。现在就来继续看看_render方法是如何将AST转化为VNode节点的。

Vue.prototype._render = function () {
  var vm = this;
  var ref = vm.$options;
  var render = ref.render; // 获取实例选项中保存的渲染方法
  // ...
  var vnode; // VNode节点变量
  // ...
  vnode = render.call(vm._renderProxy, vm.$createElement) // 根据渲染方法执行相应的_c
  // ...
  return vnode
}

可以看到,直接调用了编译Compile出来的渲染方法。

首先,我们来回顾一下渲染方法是咋样的?看看下面这个栗子呀。

<span>Hello Word...</span>
// 渲染方法如下
with(this) {
  _c('span', [_v('Hello Word...')])
}

终究回到了上一章中留下来的问题,究竟_c_v是干嘛用的?🤔

其实它们都是尤大大在内部封装好的各种渲染方法,不妨我们就来看看还有哪些。

function installRenderHelpers (target) { // 针对各种场景封装好的渲染方法
  target._o = markOnce; // 处理v-once指令的渲染方法
  target._n = toNumber; // 将输入值转化为数值,若转化失败则直接使用原始字符串
  target._s = toString; // 将输入值转化为字符串
  target._l = renderList; // 处理v-for指令的渲染方法
  target._t = renderSlot; // 处理v-slot指令的渲染方法
  target._q = looseEqual; // 判断两个值是否相等
  target._i = looseIndexOf; // 判断数组中是否存在与输入值相等的项
  target._m = renderStatic; // 处理静态树的渲染方法
  target._f = resolveFilter; // 处理选项信息中filters项
  target._k = checkKeyCodes; // 判断配置中是否拥有相应的eventKeyCode
  target._b = bindObjectProps; // 将v-bind指令绑定到相应的VNode
  target._v = createTextVNode; // 处理文本节点的渲染方法
  target._e = createEmptyVNode; // 创建空节点的渲染方法
  target._u = resolveScopedSlots; // 处理ScopedSlots
  target._g = bindObjectListeners; // 处理对象监听
  target._d = bindDynamicKeys; // 处理v-bind:key渲染方法
  target._p = prependModifier; // 判断类型是否为唯一字符串
}

vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // 创建元素VNode节点

上述代码,可以很仔细地展示了每一种场景的处理方案。由于时间有限,我就不会将每个场景的处理源码都提出来讲,后面有时间会回来再进行相应的讲解。

目前也大概知道每一个封装的渲染方法的含义,回到上面的主题,调用编译Compile出来的渲染方法,,其实就是调用_c创建元素VNode节点。

接下来我们就来看看_c内部是如何实现的。

function createElement (
 context,
 tag,
 data,
 children,
 normalizationType,
 alwaysNormalize
) {
   if (Array.isArray(data) || isPrimitive(data)) { // 判断数据是否为数组或原始数据类型
     normalizationType = children;
     children = data;
     data = undefined;
   }
   // ...
   return _createElement(context, tag, data, children, normalizationType)
 }

function _createElement (
 context,
 tag,
 data,
 children,
 normalizationType
) {
   if (isDef(data) && isDef((data).__ob__)) { // 判断数据不为空并且无依赖时,则作为一个空VNode节点进行处理
     warn(
        "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
        'Always create fresh vnode data objects in each render!',
        context
      ); // 避免使用非响应式数据创建VNode节点,不然导致每次渲染都包含其中
      return createEmptyVNode() // 返回创建的空VNode节点
   }
   // ...
   var vnode
   if (typeof tag === 'string') {
     // ...
     if (config.isReservedTag(tag)) { // 判断标签名是否为HTML标签
       vnode = new VNode( // 创建元素VNode节点
         config.parsePlatformTagName(tag), data, children,
         undefined, undefined, context
       )
     } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 判断是否为组件
       vnode = createComponent(Ctor, data, context, children, tag) // 创建组件VNode节点
     } else { // 其他一律当未知类型创建VNode节点
       vnode = new VNode(
         tag, data, children,
         undefined, undefined, context
       )
     }
   } else { // 若标签名不是字符串,则作为组件选项/构造函数创建组件
     vnode = createComponent(tag, data, context, children)
   }
   if (Array.isArray(vnode)) { // 判断生成的vnode是否为数组类型
     return vnode
   } else if (isDef(vnode)) { // 判断vnode是否为空
     if (isDef(ns)) { applyNS(vnode, ns); } // 定义当前节点的命名空间
     if (isDef(data)) { registerDeepBindings(data); } // 绑定动态的style、class
     return vnode
   } else { // 若为空,则直接返回一个空VNode节点
     return createEmptyVNode()
   }
 }

可以看到,在执行渲染方法_c时,最终都是返回了创建好的VNode节点。

代码上可能还有些方法没有细讲,如createComponent,其实底层都是经过一系列的处理后得到相应的VNode节点。另外,我个人认为创建好的VNode节点终究就是一个对象形式,不会存在数组形式(若有错误,希望指出哈)。

创建好的VNode节点最终经过绑定当前节点命名空间以及动态的styleclass,最终将节点返回。

最后的最后,渲染Render最终实现的功能就是将转化好的AST渲染方法,直接通过VNode构造函数构建相应的VNode节点。在这个过程中,会建立一个模板依赖 Watcher,并且将转化过程作为一个更新回调方法保存,每当响应式数据更新时,都会触发该过程,先渲染出相应的VNode节点,再进行patch比对,最后将比对的差异直接渲染成相应的真实DOM