✋🤚手写 Vue render

906 阅读4分钟

✋🤚 手写 Vue render

Vue 的设计是渐进式,设计界于 Angular 和 React 之间。Vue 灵活具有多种构建方式,模板写法、JSX 写法,render 函数的写法。这些不同的写法本质是适用不同的场景:

  • 模板写法,更加适用于原生 HTML、JS、CSS 写法,更加适合初学 Vue 的小伙伴。
  • JSX 写法,JSX 其实也是借助的 webpack 和 Vue 是虚拟 dom 等解析。JSX 的特点是具有 JavaScript 的开发能力,灵活,适用于对 JavaScript 是更加灵活的小伙伴。
  • render 函数,render 函数本质就是在创建 VNode,也是虚拟节点,虚拟节点用于创建生成虚拟 dom。大家都知道 dom 操作是昂贵的,但是经过虚拟 dom 处理之后,不在直接操作 dom。虚拟 dom 创建好之后 vue 就可以进入 patch 过程。patch 的过程将生成实际 dom。

render 函数优点和适用场景

  • render 函数更加接近于 Vue 的底层,与创建 VNode 直接相连。
  • render 函数不在需要 template 模板,可以直接使用 JavaScript 的能力

一个简单的例子

export default {
  name: "App",
  data() {
    return {
      a: "this is a"
    };
  },
  render(h) {
    h("view", { class: "view", id: "yoi" });
  }
};

render 函数参数:

  • tag/component/VNode
  • VNodeInterface: VNodeData
  • Children

在 Vue2 中 render 函数第一个参数是要渲染的标签或者组件。第二个参数是给 render 函数标签上的属性,与 template 上属性相对应。第三个参数用于渲染子元素或者子组件。

render 参数 data 接口

第二个参数要符合 VNodeData 接口:

export interface VNodeData {
  key?: string | number;
  slot?: string;
  scopedSlots?: { [key: string]: ScopedSlot | undefined };
  ref?: string;
  refInFor?: boolean;
  tag?: string;
  staticClass?: string;
  class?: any;
  staticStyle?: { [key: string]: any };
  style?: string | object[] | object;
  props?: { [key: string]: any };
  attrs?: { [key: string]: any };
  domProps?: { [key: string]: any };
  hook?: { [key: string]: Function };
  on?: { [key: string]: Function | Function[] };
  nativeOn?: { [key: string]: Function | Function[] };
  transition?: object;
  show?: boolean;
  inlineTemplate?: {
    render: Function,
    staticRenderFns: Function[]
  };
  directives?: VNodeDirective[];
  keepAlive?: boolean;
}

render VNodeData 详解

render staticClass 静态样式类

h(
  "div",
  {
    staicClass: "yoi-divider yoi-tag"
  },
  [render]
);

静态的 class 直接渲染到 div 的 class 属性上

render staticStyle

h(
  "div",
  {
    style: {
      borderRadius: "2px",
      margin: "0px 10px",
      background: "blue",
      color: "red"
    }
  },
  [render]
);

与静态的 class 同理,直接将静态的 class 渲染到 div 的 内联样式 style 上。

render style

h(
  "div",
  {
    style: {
      borderRadius: this.a ? "2px" : '',
      margin:  this.b ? "0px 10px" : '',
      background: this. c ? "blue" : '',
      color: this. d ? "red": '';
    }
  },
  [render]
);

动态的 style, 有了变量的控制,在不同的变量下,使用不同的值。

render 动态类

h(
  "div",
  {
    class: this.isDynamic ? "class1" : "class2"
  },
  [render]
);

h(
  "div",
  {
    class: {
      [`yoi-${this.type}`]: !!this.type
    }
  },
  [render]
);

Vue 的 class 支持两种方式,第一中是字符串的方式。第二种是 对象的方式。第二种方式更加的灵活。

render 事件 on

<view data-id='{{id}}'></view>;

h(
  "transition",
  {
    on: {
      tap: this.clickHandler
    },
    nativeOn: {
      click: this.nativeClickHandler
    }
  },
  [render]
);

事件需要使用 on、nativeOn 字段 和 methods 进行配合。

组件 props

h(
  "div",
  {
    props: {
      name: this.transition,
      origin: this.origin,
      mode: this.mode
    }
  },
  [render]
);

props 不适用于 element, 适用于组件,需要传递 props 的情况。主要单向数据流,不能在组件内部更改 props。

render attrs

h(
  "div",
  {
    attrs: {
      "data-dd": "xxxxx",
      "data-id": this.index
    }
  },
  [render]
);

html 属性传递直接使用 attrs 特性,如果对 attrs 和 domProps 分部清楚,何以先弥补这部分知识。

render domProps

h(
  "div",
  {
    innerHTML: "innerHTML"
  },
  [render]
);

dom 属性,dom 属性属于 dom 属性

render 插槽和作用域插槽

h(
  "div",
  {
    scopedSlots: {
      default: props => createElement("span", props.text)
    },
    slot: "name-of-slot"
  },
  [render]
);

ref 和 refInFor

h(
  "transition",
  {
    ref: "myRef",
    refInFor: true
  },
  [render]
);

render key

h(
  "transition",
  {
    key: "myKey"
  },
  [render]
);

key 值,一般用于表示唯一的值。在列表渲染的时候,需要渲染数据,需要绑定一个 key, 这个 key 是 vnode 需要的,不定义在 attrs 或者 domProps 中。

实例

拒绝模板", 我们不使用模板,将 Vue 全部放在 JavaScript 中, 下面是一个 Taro - Vue 的例子:

import "./Article.css";
import Taro from "@tarojs/taro";
import { View } from "@tarojs/components";

export default {
  name: "Article",
  data() {
    return {
      title: "my title"
    };
  },
  props: {
    type: String
  },
  methods: {
    genContent() {
      this.$createElement(View, "this renderContent");
    },
    clickHandler() {
      Taro.showToast({ title: "成功!" });
    }
  },
  render(h) {
    const data = {
      style: {
        borderRadius: "2px",
        margin: "0px 10px",
        background: "blue",
        color: "red"
      },
      attrs: {
        "data-dd": "xxxxx",
        "data-id": "455"
      },
      props: {},
      domProps: {},
      on: {
        tap: this.clickHandler
      },
      ...otherData
    };
    return h(View, data, [this.genContent()]);
  }
};

动态 class 解决方案

  • class 的对象绑定方法
  • class 动态绑定方法与 Vue 组件的 Computed 配合使用
  • classnames 一个简单的 javascript 实用工具,用于有条件地将 classNames 连接在一起
classNames("foo", "bar"); // => 'foo bar'
classNames("foo", { bar: true }); // => 'foo bar'
classNames({ "foo-bar": true }); // => 'foo-bar'
classNames({ "foo-bar": false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

// lots of arguments of various types
classNames("foo", { bar: true, duck: false }, "baz", { quux: true }); // => 'foo bar baz quux'

// other falsy values are just ignored
classNames(null, false, "bar", undefined, 0, 1, { baz: null }, ""); // => 'bar 1'

render 源码解析

Vue initMixin

vue 先执行 initMixin, 初始化 mixin 中,实现 initRender(vm) 方法。对于 render 而言就是定义了实例 createElement 方法

  • initRender 函数中初始化了 createElement
export function initRender(vm: Component) {
  // 模板编译使用 vm._c 方法渲染
  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);
}

所以在实例上是可以直接使用 this.$createElement 方法来直接床架 vnode

render 函数本质是在调用 createElement 方法创建组件或者元素。本质其实是再在创建这些元素或者组件的虚拟 dom, 也就是 VNode。

createElement 方法最终会调用 _createElement 创建 vnode

初始化

Vue 在进行初始化的时候会执行 render 函数的混入:

renderMixin(Vue);

renderMixin 渲染混入的内部在 在 Vue.prototype 上挂载了 _render 方法。 _render 方法中调用 render 函数,这个 render 要么事手写的,要么事编译而成。调用了 call 方法之后,我们会生成一个 vnode

实现:

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    // ...
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      currentRenderingInstance = vm
      // 核心是调用了 call render
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      // ...other
    return vnode
  }

_render 创建好 vnode 之后,就是组件将 vnode 渲染到真实的 dom 的流程。

vnode 渲染到真实的 dom

使用到了 Vue.prototype._update, 看看源码实现:

Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
  const vm: Component = this;
  // ...other

  if (!prevVnode) {
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
  // other
};

vm.__patch__ 的 patch 的方法 的定义:

Vue.prototype.__patch__ = inBrowser ? patch : noop;

// patch 其实就是根据不同的平台,进行创建的
export const patch: Function = createPatchFunction({ nodeOps, modules });

fuction createPatchFunction() {

  // other
  function patch (oldVnode, vnode, hydrating, removeOnly) {
    // other
    return vnode.elm
  }
}

patch 理解是相对于 render 是比较复杂的,因为要干更多的事情。

流程:

  1. new Vue 实例
  2. Vue 运行时 执行初始化操作,包括 mixins State Render 等等
  3. 挂载
  4. 有编译的时候就进行编译
  5. 得到 render 函数
  6. render 生成 vnode
  7. 将 vnode patch成 dom
  8. 渲染成真实的 dom。

所以我们这里 render 关注点是与 vnode 最为接近的,如果我们使用 template 或者 JSX 书写,还要编译一次才能转换成 render, 然后在调用 render 生成 vnode>

参考