dave-wind / blog

native javascript blog
0 stars 0 forks source link

v-if; v-for 实现原理 以及优先级 && v-model && $emit $on 原理 #6

Open dave-wind opened 1 year ago

dave-wind commented 1 year ago

v-if; v-for;v-model 实现原理

这些指令 都是通过compiler编译模版 实现的,用到的库就叫:vue-template-compiler

const compiler = require("vue-template-compiler");
let ast1 = compiler.compile(`<div v-if="false"></div>`)

[v-if]:
ast1.render() // with(this){return  false? _c('div'):_e()}

 // 基本上所有模版引擎都是 with+ new Function 模式; with传进去的就是实例vm
// render() 就是执行render函数 本质就是三元运算符,_e是空节点函数, _c 是
 Vue.prototype._c = function () {
        return createElement(this, ...arguments); // 创建div
    }

[v-show]:
let ast2 = compiler.compile(`<div v-for="i in 3"></div>`)
// 生成一个 _l 函数 for循环 3次 返回 _c
ast2.render() // with(this){return _l((3),function(i){return _c('div')})}

[v-if 和 v-for] 优先级:
let ast3 = compiler.compile(`<div v-if="false" v-for="i in 3"></div>`);

// 很明显是 v-for优先级高, 是先_l函数循环, 每次再判断v-if; 这样很影响性能 所以最好不要这样连用
ast3.render()  // with(this){return _l((3),function(i){return (false)?_c('div'):_e()})}

[v-model] input
let ast3 = compiler.compile(`<input v-model="name" />`);
ast3.render();
// 他真的是编译了一个指令,生成了一个domProps属性,value属性, 以及加了一个input事件
with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}

[v-model]component
let ast4 = compiler.compile(`<component v-model="name" />`);
ast4.render();
// 组件绑上model属性 并没有生成指令 而是增加了 value属性 和回调函数
with(this){return _c('component',{model:{value:(name),callback:function ($$v) {name=$$v},expression:"name"}})}
dave-wind commented 1 year ago

v-model 绑定 自定义组件原理

参考: https://v2.template-explorer.vuejs.org/

// 自定义 子组件 绑定 v-model 实际上是语法糖;  解析为 render 函数里如下:
function render() {
  with(this) {
    return _c('Demo', {
      attrs: {
        "type": "input"
      },
      model: {  // model 对象
        value: (msg), // 传递 value 属性
        callback: function ($$v) {  // 回调函数
          msg = $$v // 外部不写回调函数 都会修改 值
        },
        expression: "msg"
      }
    })
  }
}
源码里 在create-component.js 里  transformModel 如下
//  他会转化model
function transformModel(options,data) {
  const prop = (options.model && options.model.prop) || 'value';  // 默认model对象里 value属性
  const event = (options.model && options.model.event) || 'input';  // callback event 默认为 input 事件
}
<template>
  <div>
    <div @click="handleClick">{{ isVal }}</div>
  </div>
</template>
<script>
export default {
  name: "Demo",
  props: {
    str: Number,
  },
  data() {
    return {
      isClick: false,
    };
  },
  model: {
    prop: "number", // 这里改写 value 为 number 
    event: "update-number",
  },
  computed: {
    isVal() {
      return this.number >= 100 ? ">= 100" : "<100";
    },
  },
  mounted() {},
  methods: {
    handleClick() {
      this.isClick = !this.isClick;
      const val = this.isClick ? this.number - 1 : this.number + 1;
      this.$emit("update-number", val);
    },
  },
};
</script>
// 父组件
<template>
  <div id="app">
    <div>函数过滤展示: {{ showStr() }}</div>
    <Demo v-model="str" @updateNumber="changeNumber" />
  </div>
</template>

<script>
import Demo from "./demo";
export default {
  name: "App",
  components: {
    Demo,
  },
  data() {
    return {
      list: [],
      str: 100,
    };
  },
  methods: {
    showStr() {
      return this.str / 2;
    },
    changeNumber(val) { //父组件 不去定义 该函数 都会修改 str
      console.log("父组件1---", val);
      this.str = val;
    },
  },
};
</script>

$emit $on 原理

 实际上 就是 发布订阅模式
 1. <Demo v-model="str" /> 会被转化为 ast --> 通过 createComponent 生成vnode
 2. 在 createComponent方法中 会把 data( ast 生成的数据) 上的 on 赋值给 listeners :
    var listeners = data.on;
3. 生成 虚拟节点 调用 new VNode():
 var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
  );
4.初始化组件的时候 在 initInternalComponent 方法中:
const opts = vm.$options = Object.create(vm.constructor.options); // 继承于父类构造函数 options
opts_parentListener = options.listeners // 相当于 组件初始化传入的所有数据
实际上 就是 挂载 在 实例上的 $options
5. 初始化事件: 在 initEvents 方法中: 
function initEvents(vm) {
 vm._events = Object.create(null);
// 实际上就是为了 可以拿到所有的 listeners 
 const listeners = vm.$options._parentListeners;
 if(listeners) {
  // 判断 存在 就进行注册
   updateComponentListeners(vm,listeners);
}
}
6.所以最终 就是 把自定义组件的事件 绑定到$on 方法上
Vue.prototype.$on = function(event: string | Array<string>, fn ) {
   var vm = this;
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn);
      }
    } else {
   // 一个事件名 对应 多个 fn 要注意
      (vm._events[event] || (vm._events[event] = [])).push(fn);
    }
    return vm
}
Vue.prototype.$emit= function (event){
  var vm = this;
 var cbs = vm._events[event]; // 先取到对应的 fns 数组
 if (cbs) {
     for (var i = 0, l = cbs.length; i < l; i++) {
         cbs[i].apply(vm, arguments). //简单理解 就是 循环执行 fn
     }
 }
}