Skip to content

1render

createElement用法

基本参数

createElement构成了 Vue Virtual DOM的模板

js
craeteElement(
  // {String | Object | Function}
  // 一个HTML标签,组件选项,成一个函数
  // 必须 return 上述其中一个
  'div',
  // {Object} 一个对应属性的数据对象,可选,可以在template中使用
  {
    // todo 后面来说
  },
  // {String | Array} 子节点VNodes 可选
  [
    createElement('h1', 'hello world'),
    createElement(MyComponent, {
      props: {
        someProp: 'foo'
      }
    }),
    'bar'
  ]
)

第一个参数必选,可以是一个HTML标签,也可以是一个组件或函数; 第二个是可选参数,数据对象,在template中使用; 第三个是子节点,也是可选参数,用法一致。

对于第二个参数“数据对象”,具体选项如下:

js
{
  // 和v-bind:class 一样的API
  'class': {
    foo: true,
    bar: false
  },
  // 和v-bind:style 一样的API
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 正常的HTML特性
  attrs: {
    id: 'foo'
  },
  // 组件props
  props: {
    myProps: 'bar'
  },
  // DOM属性
  domProps: {
    innerHTML: 'baz'
  },
  // 自定义事劲啊监听器 "on" 不支持如v-on:keyup.enter的修饰器
  // 需要手动匹配 keyCode
  on: {
    click: this.clickHandler
  },
  // 仅对于组件,用于监听原生事件,而不是组件使用 vm.$emit触发的自定义事件
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域slot
  // {name: props => VNode | Array<VNode>}
  scopedSlots: {
    default: props => h('span', props.text)
  },
  // 如果子组件有定义slot的名称
  slot: 'name-of-slot',
  // 其他特殊顶层属性
  key: 'myKey',
  ref: 'myRef'
}

以往在template里,我们都是在组件的标签上使用v-bind:class v-bind:style v-on:click 这样的指令 在render函数都将其写在了数据对象里:

html
<div id="app">
  <ele></ele>
</div>
<script>
  Vue.component('ele', {
    template: `
    <div id="element" :class="{'show': show}" @click="handleClick">
      文本内容
    </div>
    `,
    data() {
      return {
        show: true
      }
    },
    methods: {
      handleClick() {
        console.log('clicked!')
      }
    }
  })
  var app = new Vue({
    el: '#app'
  })
</script>
<!-- 使用Render后改写的代码如下 -->
<div id="app">
  <ele></ele>
</div>
<script>
  Vue.component('ele', {
    render: h => {
      h(
        'div',
        {
          // 动态绑定class
          class: {
            'show': this.show
          },
          // 普通html特性
          attrs: {
            id: 'element'
          },
          // 给div绑定click事件
          on: {
            click: this.handleClick
          }
        },
        '文本内容'
      )
    }
  })
  var app = new Vue({
    el: '#app'
  })
</script>

对于含有组件的slot复用就要稍微复杂一点了,需要将slot的每个子节点都克隆一份。

html
<div id="app">
  <ele>
    <div>
      <Child></Child>
    </div>
  </ele>
</div>
<script>
  // 全局注册组件
  Vue.component('Child', {
    render: h => {
      h('p', 'text')
    }
  })

  Vue.component('ele', {
    render: h => {
      function cloneVNode(vnode) {
        // 递归遍历所有子节点,并克隆
        const cloneChildren = vnode.children && vnode.children.map(vnode => {
          return cloneVNode(vnode)
        });
        const cloned = h(
          vnode.tag,
          vnode.data,
          clonedChildren
        );
        cloned.text = vnode.text;
        cloned.isComment = vnode.isComment;
        cloned.componentOptions = vnode.componentOptions;
        cloned.elm = vnode.elm;
        cloned.context = vnode.context;
        cloned.ns = vnode.ns;
        cloned.isStatic = vnode.isStatic;
        cloned.key = vnode.key;

        return cloned;
      }
      const VNodes = this.$slots.default;
      const clonedVNodes = vNodes.map(vnodes => {
        return cloneVNode(vnode)
      })

      return h('div', [
        vNodes,
        cloneVNodes
      ])
    }
  })

  var app = new Vue({
    el: '#app'
  })
</script>

在render函数里创建一个cloneVNode工厂函数,通过递归将slot所有子节点都克隆了一个份,并对VNode的关键属性也进行复制。

使用JavaScript代替模版功能

在render函数中,不再需要Vue的内置指令,如v-if v-for,当然,也没法使用。可用原生来模拟:

html
<div id="app">
  <ele :show="show"></ele>
  <butotn @click="show = !show">切换</butotn>
</div>
<script>
  Vue.component('ele', {
    render: h => {
      if (this.show) {
        return h('p', 'show的值为true')
      } else {
        return h('p', 'show的值为false')
      }
    },
    props: {
      show: {
        type: Boolean,
        default: false
      }
    }
  })

  var app = new Vue({
    el: '#app'
  })
</script>
<!-- 模拟v-for -->
<script>
  Vue.component('ele', {
    render: h => {
      var nodes = []
      for (var i = 0; i < this.list.length; i++) {
        nodes.push(h('p', this.list[i]))
      }
      return h('div', nodes)
    },
    props: {
      list: {
        type: Array
      }
    }
  })
  new Vue({
    el: '#app',
    data: {
      list: [
        '1',
        '2',
        '3'
      ]
    }
  })
</script>

我们再来看一个完整的render v-if用render来写的示例:

html
<div id="app">
  <ele :list="list"></ele>
  <button @click="handleClick">显示列表</button>
</div>
<script>
  Vue.component('ele', {
    render: h => {
      if (this.list.length) {
        return h('ul', this.list.map(item => {
          return h('li', item)
        }))
      } else {
        return h('p', '列表为空')
      }
    },
    props: {
      list: {
        type: Array,
        default: []
      }
    }
  })

  var app = new Vue({
    el: '#app',
    data: {
      list: []
    },
    methods: {
      handleClick() {
        this.list = [
          '1',
          '2',
          '3'
        ]
      }
    }
  })
</script>

我们再来看一个完整的render v-for用render来写的示例:

html
<div id="app">
  <ele></ele>
</div>
<script>
  Vue.component('ele', {
    render: h => {
      return h('div', [
        h('input', {
          domProps: {
            value: this.value
          },
          on: {
            input: e => {
              this.value = e.target.value
            }
          }
        }),
        h('p', `value: ${this.value}`)
      ])
    },
    data() {
      return {
        value: ''
      }
    }
  })

  var app = new Vue({
    el: '#app'
  })
</script>

对于事件修饰符和按键修饰符,基本也需要自己实现:

.stop event.stopPropagation() .prevent event.preventDefault() .self if (event.target !== event.currentTarget) return .enter .13 if (event.keyCode !== 13) return .ctrl .alt .shift .meta if (!event.ctrlKey) return ...

共 20 个模块,1301 篇 Markdown 文档。