重学Vue【Vue的 render函数】

简介: 重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。

网络异常,图片无法展示
|

重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。


正文


render

Vue实例挂载的实现 中可以看到 render 函数是Vue实例挂载渲染的重点,那本篇过说一下 vmrender 方法的内部逻辑,看看它是怎么实现的。

它是定义在 src/core/instance/render.js 里面,有一个 Vue.prototype._render 方法:

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    // reset _rendered flag on slots for duplicate slot check
    if (process.env.NODE_ENV !== 'production') {
      for (const key in vm.$slots) {
        // $flow-disable-line
        vm.$slots[key]._rendered = false
      }
    }
    if (_parentVnode) {
      vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
    }
    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        if (vm.$options.renderError) {
          try {
            vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
          } catch (e) {
            handleError(e, vm, `renderError`)
            vnode = vm._vnode
          }
        } else {
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }


首先拿到 $options 里面的 render 函数和一个 _parentVNode 函数,重点看下这个 $optionsrender 。后面有一句 render.call(vm._renderProxy, vm.$createElement),第一个参数 vm._renderProxy 在生产环境中表示vm,在开发环境中表示一个proxy 对象,后面的 vm.$createElement 可以从上面的代码中这个js文件中找到:

// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

它定义在 initRender 方法中, initRenderinitMixin 调用,initMixin 是在Vue初始化的时候被调用了。这里知道就好, 来看下 $createElement 函数做了什么事情。


上面的 vm._c 是被编译生成的 render 函数使用的方法,vm.$createElement 是我们手写 render 函数的时候提供的一个可以生成 VNode 的一个方法。既然它是给手写的 render 使用的,那写个例子看看它是怎样被使用的(写法参考 官网说明):

<!-- index.html -->
<html>
  <body>
    <div id="app"></div>
  </body>
</html>
//main.js
import Vue from "vue";
var app = new Vue({
  el: "#app",
  render(createElement){
    //render会返回一个VNode,这里用渲染函数写法
    return createElement("div", {
      attrs:{
        id: "#app1"
      }
    }, this.name)
  },
  data(){
    return {
      name: "abc"
    }
  }
})

这种写法也可以渲染到页面上,而且没有使用双大括号把变量渲染成字符串并添加到页面上这个过程,因为之前的写法是在  里面先定义了一个比如说 #app 的元素,它在不执行 vue 的时候把这个元素渲染出来,然后在 new Vue 之后,执行 mount 再把定义的插值(这里是name)把它给替换掉,然后显示真实的数据。而通过定义 render 函数,就不用在页面上显示 #app 这个插值,当 render 执行结束就把 name给替换上去。还记得之前说的 render 的参数来源可以是 template 转化成 el 么,这里就不用 template 了,相当于省去了这一步,当然效果是一样的,最终生成的元素的 idapp1


从这里可以看出来,挂载的元素,其实是会替换掉原来的 #app 元素,也就是说生成的东西会把整个 #app 标签给替换掉,所以这也就是为什么不能在  上做事情,不然就替换了整个  了。到此 createElement 的作用完毕,再来看下 vm._renderProxy 是干嘛的。

src/core/instance/init.js 中的 initMixin 方法里有这段代码:

/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}


接着后面调用了 initRender 方法,也就是刚才声明 vm.$createElement 的地方。如果是生产环境,就直接把 vm 赋值给 vm._renderProxy,开发环境就调用 initProxy 函数,它定义在 src/core/instance/proxy.js 内:

initProxy = function initProxy (vm) {
  if (hasProxy) {
    // determine which proxy handler to use
    const options = vm.$options
    const handlers = options.render && options.render._withStripped
    ? getHandler
    : hasHandler
    vm._renderProxy = new Proxy(vm, handlers)
  } else {
    vm._renderProxy = vm
  }


判断了一个 hasProxy,整个参数定义在上面:

const hasProxy =
  typeof Proxy !== 'undefined' && isNative(Proxy)
if (hasProxy) {
  const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact')
  config.keyCodes = new Proxy(config.keyCodes, {
    set (target, key, value) {
      if (isBuiltInModifier(key)) {
        warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
        return false
      } else {
        target[key] = value
        return true
      }
    }
  })
}
const hasHandler = {
  has (target, key) {
    const has = key in target
    const isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_')
    if (!has && !isAllowed) {
      warnNonPresent(target, key)
    }
    return has || !isAllowed
  }
}
const getHandler = {
  get (target, key) {
    if (typeof key === 'string' && !(key in target)) {
      warnNonPresent(target, key)
    }
    return target[key]
  }
}


就是判断了一下浏览器支不支持 Proxy。支持的话就声明一个 proxy 劫持,劫持之后做了的事情都在 hashandler 里面,里面主要就是走 warnNonPresent 方法:

const warnNonPresent = (target, key) => {
   warn(
     `Property or method "${key}" is not defined on the instance but ` +
     'referenced during render. Make sure that this property is reactive, ' +
     'either in the data option, or for class-based components, by ' +
     'initializing the property. ' +
     'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
     target
   )

会报这个错,这个错应该也比较熟悉,报错原因就是说在属性和方法中使用了一个没有定义的值。到此 proxy 劫持参数完毕。


来简单的捋一遍 render 方法里面的 vnode = render.call(vm._renderProxy, vm.$createElement) 都做了什么事情:第一个参数做一个代理劫持,用来报错,第二个参数用来创建一个插值,最终,render 生成了一个 vnode


知道 render 会生成一个vnode之后,继续看 render 方法的后面逻辑。判断 vnode 有没有在 VNode 上,如果没有并且 vnode 是一个数组,就报错:

warn(
  'Multiple root nodes returned from render function. Render function ' +
  'should return a single root node.',
  vm

说明模板有多个根节点,就是说有多个 vnode,这里就报了一个错,说 root 根节点只能有一个 node


所以最终, render 方法通过内部的 createElement 函数,会生成一个 vnode,而 vnode 其实就是一个 Virtual DOM,所以在了解 createElement 做了什么事情之前先来说一下 Virtual DOM 是个什么东西。


Virtual DOM

Virtual DOM 应该都不陌生,出现 Virtual DOM 的原因是因为浏览器加载DOM是比较 “昂贵“ 的,可以看下一个 div 大概有多少东西:

网络异常,图片无法展示
|

可见一个 DOM 元素里面的东西很多很多,这也是浏览器的设计标准,所以我们如果频繁的操作 DOM ,对浏览器的性能损耗是一个很大的问题。而 Virtual DOM 就是通过JS来描述记录一个 dom 节点,所以这个代价要小得多。下面看下在 vue 中, Virtual DOM 是如何定义一个 vnode 的,在 src/core/vdom/vnode.js 中:

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  fnScopeId: ?string; // functional scope id support
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

可以看到,里面定义了标签tag,数据VNodeData(包括了slot,tag,attrs等等,在 ~/vue/flow/vnode.js 中),子节点children,文本text,持有的节点elm等等。


所以总的来看, VNode 其实就是包含了真实节点的一堆东西,以及增加了和 vue 有关的属性,从而扩展 VNode 的灵活性。由于 VNode 只是用来映射到 真实DOM 的渲染,不需要包含操作 DOM 的方法,所以它特别的轻量。


在定义好 VNode 之后,要映射到真实DOM 要经历 VNodecreatediffpatch 等过程,而上面提到的 createElement 就是 create 。在知道了 renderVirtual DOM 之后,下篇说下 createElement 是如何添加元素的。

目录
相关文章
|
5天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
vue学习第四章
|
5天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
vue学习第九章(v-model)
|
5天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
vue学习第十章(组件开发)
|
10天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
10天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
10天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
11天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
11天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
11天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
12天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。