Vue源码学习1.4:_render

548 阅读3分钟

上一章中我们知道了 updateComponent 其实是先调用 vm._render 生成 VNode,最终调用 vm._update 更新 DOM

在分析 _render 函数之前,我们先来看看如何在 Vue 中使用 render 函数的。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode

render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  }, this.message)
}

正文开始
vm._render 定义在 src/core/instance/render.js 文件中

Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  if (_parentVnode) {
    // slot相关逻辑
  }

  // 允许 render 函数能访问到占位符vnode 的 data 数据
  vm.$vnode = _parentVnode

  let vnode
  try {
    // 无需维护一个栈,因为所有的 render 函数是彼此分开调用的
    // 嵌套组件的 render 函数在父组件 patched 时被调用
    currentRenderingInstance = vm
    // 核心:调用 render 函数
    vnode = render.call(vm._renderProxy, vm.$createElement)
    
  } catch (e) {
    // 错误处理...
    
  } finally {
    currentRenderingInstance = null
  }
  
  // 允许返回的数组仅包含一个节点
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }
  
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

核心逻辑就是调用了 render 函数,传入 vm.$createElement 参数,然后将 this 绑定为 vm._renderProxy,最终返回一个 vnode

下面来介绍一下 vm._renderProxyvm.$createElement

1. vm._renderProxy

vm._renderProxy 如果在生产环境下,其实就是 vm ,如果在开发环境下,就是 Proxy 对象。其在 _init 方法中定义。

// src/core/instance/init.js

Vue.prototype._init = function (options?: Object) {
  // ...
  if (process.env.NODE_ENV !== 'production') {
    initProxy(vm)
  } else {
    vm._renderProxy = vm
  }
  // ...
}

生产环境下调用了 initProxy 函数. initProxy 专门定义在 src/core/instance/proxy.js

// src/core/instance/proxy.js

initProxy = function initProxy (vm) {
  // 判断浏览器是否支持proxy
  if (hasProxy) {
    // 根据 _withStripped 来决定使用哪个 proxy handler
    const options = vm.$options
    const handlers = options.render && options.render._withStripped
      ? getHandler
      : hasHandler
    vm._renderProxy = new Proxy(vm, handlers)
  } else {
    vm._renderProxy = vm
  }
}
  • 首先判断浏览器是否支持 Proxy,如果支持的话,判断 _withStripped 属性
    • _withStripped 为真:Proxy handler 采用 getHandler 方法
    • _withStripped 为假:Proxy handler 采用 hasHandler 方法。
  • 不支持的话直接将 vm 赋值给 _renderProxy

1.1. getHandler

// src/core/instance/proxy.js

const getHandler = {
  get (target, key) {
    if (typeof key === 'string' && !(key in target)) {
      if (key in target.$data) warnReservedPrefix(target, key)
      else warnNonPresent(target, key)
    }
    return target[key]
  }
}

主要作用当读取代理对象的属性时,如果属性不在真实的 vm 实例中,针对不同的情况抛出警告

  • 情况一:key 不在 vm 中,但是在 vm.$data 中,说明我们自定义的数据以 $_ 开头,这种情况是不会被数据代理,也就是直接通过 this.$xx 访问会得到 undefined(关于数据代理这里有介绍)
  • 情况二:确实不在 vm 中,就抛出一个警告

1.2. hasHandler

// src/core/instance/proxy.js

const hasHandler = {
  has (target, key) {
    const has = key in target
    const isAllowed = allowedGlobals(key) ||
      (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
    if (!has && !isAllowed) {
      if (key in target.$data) warnReservedPrefix(target, key)
      else warnNonPresent(target, key)
    }
    return has || !isAllowed
  }
}
  • 首先使用 in 操作符判断该属性是否在 vm 实例上存在。
  • 判断属性是否可用:当是 特殊属性(Number,Array等) 或者 下划线开头的且不在$data中的字符串 则为可用。
  • 如果属性在 vm 中不存在,且属性不可用,则抛出警告。
  • 返回 has || !isAllowed!isAllowed 表示如果属性不可用的话就当成属性存在。

1.3. 为什么根据 _withStripped 来使用hasHandler或getHandler呢?

这其实是为了处理不同情况下的 render 函数

在这之前,我们先明确两点,当 proxy.foo 访问时会触发 Proxyget,当 with(proxy) { (foo); } 会触发 Proxyhas。详细查看 MDN

情况一
对于非单文件组件,使用 el 或者 templete 来创建组件的方式,vue 会解析 template 生成 render,形如

vm.$options.render = function () {
    with (this) {
        // 这里的 _c 是 vm._c,下文有介绍
        return _c(/*...*/)
    }
}

我们访问 _c 会触发 Proxyhas,也就是上面的 hasHandler

情况二
但是对于 单文件组件(SFC) 而言,vue-loader 工具将 template 编译成严格模式下是不包含 with 的代码,但是会为编译后的 render 设置 render._withStripped = true,问题,编译后的 render 长这样:

var render = function() {
    var _vm = this;
    var _h = _vm.$createElement;
    var _c = _vm._self._c || _h;
    return _c(...)
}

这时我们通过 _vm.xx 的形式访问属性,则会触发 Proxyget,也就是 getHandler

2. vm.$createElement

render 函数中的 createElement 方法就是 vm.$createElement 方法,定义在src/core/instance/render.js

// src/core/instance/render.js

export function initRender (vm: Component) {
  // ...
  
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // ...
}
  • vm._c 是内部函数,它是被模板编译成的 render 函数使用
  • vm.$createElement 是提供给用户编写的 render 函数使用的

他们两个其实都是执行了 createElement 函数,有关此函数将单独放到一个章节来介绍

总结

vm._render 最终是通过执行 createElement 方法并返回的是 vnode