Vue 指令(v-*)、渲染(render, jsx)

1,254 阅读4分钟

指令

v-model 双向数据绑定指令

  • v-model 是双向数据绑定指令,用于表单控件。
  • v-model 其实是语法糖就是对不同的表单类型进行了单独的封装。
  • 当然 Vue 的组件也是支持 v-model 双向数据绑定的。
  • v-model 为表单数据录入提供了很大的帮助!

input

1.input 实现的双向数据绑定,其实就是运用了 input 控件的 value 属性,和监听 input 事件。

  • 下面我们来模拟一个 inputv-model:
<template>
    <div>
      <span>v-model 双数据绑定</span>
      <div>
        input 的数据是:{{ a1 }}
        <input type="text" v-model="a1" />
      </div>
      
     <div>
      <span>模拟 input textarea v-model</span>
      <div>input 绑定是数据是:{{ a2 }}</div>
      <div>
        <input
          type="text"
          :value="a2"
          @input="handleSelfInput"
        />
      </div>
    </div>
    </div>
</template>

<script>
export default {
  name: "component-input-v-model-test",
  data() {
    return {
      a1: 0,
      a2: 222,
    };
  },
  methods: {
    handleChange() {
      debugger;
    },
    handleInput(args) {
      debugger;
    },
    handleSelfInput(value) {
      const target = value.target.value;
      this.a2 = target;
    },
  },
};
</script>

a1 是一个普通 v-model 双向绑定 input 数据,a2 是使用 valueinput 事件模拟的 v-model。原理很简单,使用 使用 v-bind 动态绑定数据 a2 然后,当 input 事件触发的时候,从 事件 event 中拿到 value 值,将 a2 用新的 value 值覆盖即可。

textarea

textarea 其实和 text 是一致的:

<template>
  <div>
    <div>
      <div>使用 v-model: {{ textarea }}</div>
      <textarea v-model="textarea"></textarea>
    </div>
    <div>
      <div>模拟 text-area v-model: {{ textarea_self }}</div>
      <textarea :value="textarea_self" @input="onInput"></textarea>
    </div>
  </div>
</template>

<script>
export default {
  name: "v-model-v-text-area",
  data() {
    return {
      textarea: "",
      textarea_self: "",
    };
  },
  methods: {
    onInput(e) {
      this.textarea_self = e.target.value;
    },
  },
};
</script>

radio

单选按钮 type = radioinputtextarea 不同, radio 不在使用 value 属性,而是使用 checked 属性,事件也从 text 的 input 事件,变成了 change 事件

  • 表单控件:text -> radio/checkbox(checkbox 大得多使用数组保存)
  • 绑定控件属性:value -> checked
  • 监听的表单事件:input -> change

首先要搞明白 radio 也是需要一个值的,用于表示 radio 当前的选择,书写 Radio 的时候,不在 radio 元素内部写 radio 的内容,而是配合 label 标签来扩大可点击范围。

<template>
  <div>
    <div>
      <div>默认使用 v-model</div>
      <div>
        绑定的数据: {{ radio }}
        <input type="radio" value="radio1" v-model="radio" />
        <input type="radio" value="radio2" v-model="radio" />
        <input type="radio" value="radio3" v-model="radio" />
      </div>
    </div>
    <div>
      <div>模拟一个radio v-model: {{ checked }}</div>
      <div>
        <input
          type="radio"
          value="r1"
          :checked="checked === 'r1'"
          @change="onChange"
        />
        <input
          type="radio"
          value="r2"
          :checked="checked === 'r2'"
          @change="onChange"
        />
        <input
          type="radio"
          value="r3"
          :checked="checked === 'r3'"
          @change="onChange"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "v-model-radio",
  data() {
    return {
      radio: "",
      checked: '',
    };
  },
  methods: {
    onChange(e) {
      this.checked = e.target.value;
    },
  },
};
</script>

checkbox 的基本模拟 v-mode

<template>
  <div>
    <div>
      <div>使用 checkbox: {{ checkbox }}</div>
      <div>
        <input type="checkbox" value="c1" v-model="checkbox" />
        <input type="checkbox" value="c2" v-model="checkbox" />
        <input type="checkbox" value="c3" v-model="checkbox" />
      </div>
    </div>
    <div>
      <div>模拟 checkbox {{ checkbox_self }}</div>
      <div>
        <input
          type="checkbox"
          value="cc1"
          :checked="checkbox_self.indexOf('cc1') > -1 ? true : false"
          @change="onChange"
        />
        <input
          type="checkbox"
          value="cc2"
          :checked="checkbox_self.indexOf('cc2') > -1 ? true : false"
          @change="onChange"
        />
        <input
          type="checkbox"
          value="cc3"
          :checked="checkbox_self.indexOf('cc3') > -1 ? true : false"
          @change="onChange"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "v-checkbox-v-demo",
  data() {
    return {
      checkbox: [],
      checkbox_self: [],
    };
  },
  methods: {
    onChange(e) {
      const value = e.target.value;

      if (this.checkbox_self.indexOf(value) > -1) {
        this.checkbox_self = this.checkbox_self.filter((item) => {
          item !== value;
        });
      } else {
        this.checkbox_self = [...this.checkbox_self, value];
      }
    },
  },
};
</script>

简单模拟 select 和被选中之间的关系, 这里实现了 select 的单选

<template>
  <div>
    <div>
      <div>在 select 中使用 v-model: {{ selected }}</div>
      <div>
        <select name="my-select" id="" v-model="selected">
          <option value="s1">s111</option>
          <option value="s2">s222</option>
          <option value="s3">s333</option>
        </select>
      </div>
    </div>
    <div>
      <div>模拟一个单选 select 的 v-model: {{ selected_self }}</div>
      <div>
        <select name="ss1" id="" @change="onChange">
          <option value="v1" :selected="selected_self === 'V1'">V1</option>
          <option value="v2" :selected="selected_self === 'V2'">V2</option>
          <option value="v3" :selected="selected_self === 'V3'">V3</option>
        </select>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selected: "s1",
      selected_self: "V2",
    };
  },
  methods: {
    onChange(e) {
      const value = e.target.value;
      this.selected_self = value;
    },
  },
};
</script>

小结

其实就是注意:不同的表单控件有不同的属性和事件监听,v-model 会根据不同的表单进行处理。但是我们也要学会如何自己实现 v-model,因为在 render 函数或者 jsx 中, vue 指令可能就不能得到很好的支持。

渲染

    1. 为什么还有 render 函数?
    1. 为什么还有 jsx?
  1. 我们写Vue的时候,大部分是用 html 模板来写的,但是模板虽然简单粗暴,但是在灵活度方面还是没有 javascript 灵活, 从 Vue 的运行生命周期原理图中,我们就能知道,Vue 是判断是否含有模板,有模板都会编译到 render 函数中,所以学 Vue 重要的是学些这个 render 函数做了哪些事情,原理是什么。
  2. JSX 是 JavaScipt 的扩展,允许在 JavaScript 中编写 html 的能力,也就是让 javascript 天生就具有模板的能力,让 render 函数更加清晰。

render

render 是一个渲染函数,即将创建我们写的组件编译成浏览器识别的html,css,js,

  • render 函数的参数是 createElement(可以简单的写为 h),
  • render 函数的返回值是 createElement 传入参数 options之后的创建的元素
  • createElement 的 options 如下:

位置:src/core/vdom/create-element.js

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
  • createComponent
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}
  • VNode 是一个 class 的类,保存了组件信息,dom节点信息。这里其实就是一个 Vue 组件的创建过程。Vue 组件最终都会被编译成 render 函数,render 函数会实例化 VNode虚拟节点,Vue 本质上就是 VNode 的对比然后生成 dom 的过程。所以说在创建组件的过程中 render 对于开发者是最为重要的。

jsx

github.com/vuejs/jsx 是 babel 的一个插件,

  • 安装
npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props -D
  • babel 配置文件
{
  "presets": ["@vue/babel-preset-jsx"]
}
  • Vue 语法
    • 内容
    • 属性
    • 插槽
    • 指令
    • 函数组件

JSX 中的 slot

vm.$scopedSlots 在使用渲染函数开发一个组件时特别有用。

  • 子组件中使用 this.$scopedSlots.user 来绑定一个作用域插槽
<script>
export default {
  name: 'Start',
  data () {
    return {
      a: '123',
      b: '456'
    }
  },
  mounted () {
    console.log('children', this.$scopedSlots.user)  // 一个函数
  },
  render () {
    return (
      <div class="abc">
        <span>this is goods</span>
        {this.$slots.default}
        {this.$scopedSlots.user({
          a: this.a
        })}
      </div>
    )
  }
}
</script>

<style scoped>
.abc {
  background: rebeccapurple;
  color: #fff;
}
</style>
  • 父组件

父组件中没有使用 render + jsx 的方案,而是直接使用模板,应为 jsx 的babel 转换器是几年前写的,现在没有更新,对与 Vue 的 v-slot 指令并没有支持。

<template>
  <div>
    <hello-world />
    <start v-slot:user="innerSlotData">
      {{innerSlotData}}
      <div>
        <span>this div is test => this.$slot.default</span>
        <span>123</span>
        <span>{{innerSlotData.a}}</span>
      </div>
    </start>
  </div>
</template>

<script>
import HelloWorld from '../components/HelloWorld'
import Start from '../components/Start'
export default {
  name: 'Home',

  components: {
    HelloWorld,
    Start
  },
  mounted () {
    console.log(this.$scopedSlots.user) // undefined
  }
}
</script>

<style lang="scss" scoped>

</style>