Vue2+Vue3知识补充

defineProperty

一个对象默认的配置规则参数如下,通常都是为true。通过getOwnPropertyDescriptor方法查看

      let obj = {
        x: 10,
      };
      console.log(Object.getOwnPropertyDescriptor(obj, "x"));

在这里插入图片描述
当使用defineProperty定义一个对象中已经存在属性的配置项时。如果没有重新定义配置,那么还是以之前的配置参数为主。

      let obj = {
        x: 10,
      };
      Object.defineProperty(obj, "x", {
        writable: false,
      });
      console.log(Object.getOwnPropertyDescriptor(obj, "x"));

在这里插入图片描述
但是,如果往一个对象身上,使用defineProperty定义一个原本不存在的属性,那么默认的配置信息通常都是false

      Object.defineProperty(obj, "y", {
        value: 300,
      });

在这里插入图片描述
如果是直接往一个对象身上添加一个属性,那么该属性默认的配置通常都是true

      let o = {};
      o.x = 2;
      console.log(Object.getOwnPropertyDescriptor(o, "x"));

在这里插入图片描述

如果存在一个数组,这时候需要写一个公共的方法,那么会写到数组的原型身上。但是for/in循环会将自身的和原型身上可枚举的属性都处理。

      let arr = [1, 2, 3];

      for (let key in arr) {
        console.log(key, arr[key]);
      }
	  Array.prototype.myfun = () => {}; // 这样子新增一个元素,默认配置通常都是为true,如可枚举性

这就会造成了循环的时候将函数打印出来。会造成别人使用forin循环的时候出现不可预测的问题
在这里插入图片描述
正确做法应该是这样子,

      Object.defineProperty(Array.prototype, "myfun", {
        value: () => {},
        enumerable: false,
      });

数据劫持

定义一个obj对象,并对其进行数据劫持操作,在defineProperty中可以使用get,set两个函数,get函数用于读取的时候返回,set函数用于设置新参数。

      let obj = {
        x: 1,
      };

如下这种写法会造成死循环栈溢出。因此需要设置一个代理对象。一旦设置了set和get函数,则不能设置value和writable属性,否则报错

      Object.defineProperty(obj, "x", {
        get() {
          return obj.x;
        },
        set(val) {
          obj.x = val;
        },
      });
      let proxy = { ...obj };
      Object.defineProperty(obj, "x", {
        get() {
          return proxy.x;
        },
        set(val) {
          proxy.x = val;
        },
      });

在这里插入图片描述
凡是被代理的对象属性,都会在原对象身上添加get和set函数
在这里插入图片描述

new Vue针对数据做了哪些事情

Vue构造函数在实例化对象的时候会去执行initData方法进行初始化数据,会针对data中的数据进行响应式处理。凡是经过数据劫持操作的数据。每当set函数被触发,不仅会修改值,还会通知视图更新。实现数据驱动视图。如下是一些细节

      const fn = function () {};
      fn.num = 20;
      const reg = /\d+/;
      reg.xxx = 20;
      let vm = new Vue({
        data: {
          msg: "哈哈哈",
          num: 10,
          arr: [10, 20, [30, 40], { a: "A", b: "B" }],
          obj: {
            x: 10,
            y: {
              z: 20,
            },
            m: [100, 200],
          },
          forz: Object.freeze({ c: 100, d: "200" }),
          fn: fn,
          reg: reg,
        },
      });
      vm.$mount("#app");
      console.log(vm);

首先针对外围的这些属性,vue都会进行数据劫持
在这里插入图片描述
数组内部数据没有进行数据劫持操作,但是如果内部数据本身是一个普通对象,则会进行数据劫持
在这里插入图片描述

展开数据查看,发现对于普通数值类型,如msg等,其值不进行数据劫持操作,但是如普通对象类型,会针对内部数据进行深度递归监听和劫持操作,但是对象内部的数组本身还是会进行处理操作,数组内部数据会针对性的进行数据劫持
在这里插入图片描述
但是像一些函数,正则则不会进行数据监听和劫持
在这里插入图片描述
在这里插入图片描述
被冻结的对象内部数据则无法通过defineProperty无法实现数据劫持操作
在这里插入图片描述
凡是可以被枚举的非Symbol私有属性均可以被数据劫持
在这里插入图片描述
在底层中针对data中的数据,是通过Object.keys方法获取key值,那么非symbol类型,可枚举的私有属性。

定义如下数据添加到data中。验证不可枚举,发现内部y属性不可枚举,所以无法进行数据劫持操作

      let oo = { x: 1 };
      Object.defineProperty(oo, "y", {
        value: 20,
        enumerable: false,
      });

在这里插入图片描述

数组响应式处理

输出发现,数组中,并没有针对每一个索引进行响应式处理。展开其原型发现,vue帮助实现了一个原型,包含各种方法,用来实现数组响应式。而在该原型之上才是数组的原型。
在这里插入图片描述
下面给出部分测试案例

  • 直接根据索引修改数组的某一项无法实现响应式

在这里插入图片描述
在这里插入图片描述

  • 修改冻结的参数
    在这里插入图片描述

数据的更新处理

当实例化一个vm后,再次像对象中添加一个属性,默认是不进行数据劫持的 数据劫持发生在new Vue阶段,后序添加的数据无法实现劫持操作

      let vm = new Vue({
        data: {
          msg: "哈哈哈",
          num: 10,
          arr: [10, 20, [30, 40], { a: "A", b: "B" }],
          obj: {
            x: 10,
            y: {
              z: 20,
            },
            m: [100, 200],
          },
          forz: Object.freeze({ c: 100, d: "200" }),
        },
      });
      vm.obj.name = "12131231232"; // 没有被劫持,无法修改后页面渲染
  • 如果在new Vue的时候,data中obj对象没有name属性,那么可以使用vm.$set()设置新属性,并且进行了数据劫持操作
    在这里插入图片描述
    在这里插入图片描述

  • 如果是原obj中没有的数据,但是后来使用vm.obj.name添加的数据,之后使用$set再次设置,那么是无法对该属性进行数据拦截处理,但是页面会更新。 但是测试发现,在控制台写$set页面不会进行更新渲染
    在这里插入图片描述
    在这里插入图片描述

  • 如果是数组,采用$set设置对应索引的内容,vm.$set(vm.arr,0,100000)

  • vue中提供了$forceupdate强制更新vm.obj.name = "测试一下";vm.$forceUpdate()

数据响应式代码实现

如下是针对数组的主体部分处理

let classtype = {},
  toString = classtype.toString,
  hasOwnProperty = classtype.hasOwnProperty;

function isPlainObject(obj) {
  let proto, ctor;
  if (!obj || toString.call(obj) !== "[object Object]") return false;
  proto = Object.getPrototypeOf(obj);
  //函数自身原型上存放构造器函数
  if (!proto) return true;
  ctor = hasOwnProperty.call(proto, "constructor") && proto.constructor;
  return typeof ctor === "function" && ctor === Object;
}

//处理视图更新
function compiler() {
  console.log("视图更新");
}

//给对象新增不可枚举的属性
function def(obj, key, value, enumerable) {
  Object.defineProperty(obj, key, {
    value,
    writable: true,
    configurable: true,
    enumerable: !!enumerable, //为假
  });
}

let proto = {};
Object.setPrototypeOf(proto, Array.prototype);
["pop", "push", "reverse", "shift", "sort", "splice", "unshift"].forEach(
  (name) => {
    def(proto, name, function mutator(...args) {
      //当前this就是指向调用者arr
      // 通过Array原型的push方法进行新增
      const res = Array.prototype[name].apply(this, args);
      compiler(); //通知视图编译
      return res;
    });
  }
);

//数组处理
function observeArray(arr) {
  //需要实现vue提供的原型,该原型在指向Array原型
  Object.setPrototypeOf(arr, proto);
}

//对象处理
function defineReactive(obj, key, val) {}

function Observe(obj) {
  let isObject = isPlainObject(obj);
  let isArray = Array.isArray(obj);
  if (!isObject && !isArray) return obj;
  if (isArray) {
    //重写原型指向
    observeArray(obj);
    obj.forEach((item) => Observe(item));
    return obj;
  }
  if (isObject) {
  }
}

如下是实现一个基础的数组响应式处理。调用修改后的方法触发视图更新

let arr = [1, 2, 3, [4, 5, 6]];
Observe(arr);
console.log(arr);

在这里插入图片描述
在这里插入图片描述
但是这种情况并没有处理数组中新增数据的劫持,比如数组arr = [1,2]这时候新增一个{name:‘123’}作为第三个参数,那么该对象内部属性是需要做劫持操作。

["pop", "push", "reverse", "shift", "sort", "splice", "unshift"].forEach(
  (name) => {
    def(proto, name, function mutator(...args) {
      //当前this就是指向调用者arr
      // 通过Array原型的push方法进行新增
      const res = Array.prototype[name].apply(this, args);
      //如果是数组中新增某个元素,如果比如将数组的第一个元素从0修改为{name:'123'}那么新对象中的数据需要劫持
      let inserted;
      switch (name) {
        case "push":
        case "unshift":
          //收集新数据
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2); //splice(0,0,2)第三个参数才是数据
          break;
        default:
      }
      if (Array.isArray) Observe(inserted);
      compiler(); //通知视图编译
      return res;
    });
  }
);

处理对象数据的劫持操作

function Observe(obj) {
  let isObject = isPlainObject(obj);
  let isArray = Array.isArray(obj);
  if (!isObject && !isArray) return obj;
  if (isArray) {
    //重写原型指向
    observeArray(obj);
    obj.forEach((item) => Observe(item));
    return obj;
  }
  //对象处理
  let keys = Object.keys(obj);
  let proxy = { ...obj };
  keys.forEach((key) => {
    defineReactive(obj, key, obj[key], proxy);
    //递归处理key所对应的元素值
    Observe(obj[key]);
  });
  return obj;
}
//对象处理
function defineReactive(obj, key, val, proxy) {
  //冻结的对象不需要处理(冻结的对象特性判断)
  let property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) return;
  Object.defineProperty(obj, key, {
    get() {
      return proxy[key];
    },
    set(newVal) {
      if (newVal === proxy[key]) return;
      proxy[key] = Observe(newVal); //如果是修改的新值,如从10设置到{name:'123'}那么新值中的属性也应该是被数据劫持
      compiler();
    },
  });
}

v-on指令原理

对象中函数的两种绑定方法区别,省略写法是没有原型的
在这里插入图片描述
在new Vue阶段,会进行数据的初始化操作,除了针对data中的数据进行initdata操作,还针对methods中的方法进行initMethods操作。在该函数内部,会遍历methods配置项中的数据往vm实例身上绑定,同时还会再bind$1方法中修改this指向。
在这里插入图片描述
在这里插入图片描述
如下这段代码,都是给methods方法中添加方法,那么经过initmethods方法处理都会绑定到vm实例身上,但是在new阶段执行完毕初始化操作,直接往当前vm实例挂载方法,两者有什么区别。

      let vm = new Vue({
        data: {
          msg: "哈哈哈",
        },
        methods: {
          handle(e) {
            console.log("12312", this, e);
          },
        },
      });
      vm.handle2 = function (e) {
        console.log("12312", this, e);
      };
      vm.$mount("#app");

在这里插入图片描述
分别绑定输出查看。在vue配置项methods中写的函数,其this都指向了vm实例,但是初始化完毕后添加的方法,其this没有经过vue处理,还是指向window,那都是vm.handle2,为什么其this没指向vm实例,这涉及到事件绑定底层实现。’

      <button v-on:click="handle">{{msg}}</button>
      <button v-on:click="handle2">{{msg}}</button>

在这里插入图片描述
vue底层对v-on是基于addEventListener处理,会将将v-on:click='fn'进行处理解析大致如下格式。然后会根据该格式对元素进行事件添加。

{
	name:'v-on',
	value:fn,
	args:['click']
}

buttonDOM.addEventListener('click',function(){
	//处理额外操作,如修饰符,别名等
	//最后调用方法
	fn() //未进过vue处理的this指向window
})

v-pre

该指令用来优化渲染。控制vue在编译视图的时候,跳过该元素以及后代的编译,视图中怎么写,渲染结果就是什么,通常针对静态数据进行v-pre处理。在vue3中不需要手动处理,底层默认实现通过静态节点的编译

计算属性

计算属性是会被缓存使用的,其值的改变根据依赖项决定。在new Vue初始化的时候,执行initComputed$1函数进行computed计算属性配置项的处理。在该函数中,会先遍历计算属性的配置项并绑定到userDef属性身上。那么userDef可能是函数或者对象。根据情况获取其getter。
在这里插入图片描述

        computed: {
          x() {
            return 1231;
          },
          y: {
            get() {
              return "xixii1";
            },
            set(v) {
              console.log("我设置了啊", v);
            },
          },
        },

不是服务端渲染的话就会创建Watcher实例。同时将新的值添加到当前vm实例身上。
在这里插入图片描述

然后进入defineComputed函数中处理,优先判断是否服务端渲染。如果getter是函数则并且是浏览器端渲染,则会设置get属性,同时set函数就不应该支持修改,调用createComputedGetter函数,同时将set设置为一个noop,即对应的报错函数用于修改时候触发。如果是对象,则分别设置get和set对应的内容。
执行createComputedGetter函数会返回computedGetter函数,该函数是对计算属性的get进行劫持处理

计算属性内部的值是基于自身创建的新值,不像watch中监视的属性必须存在当vm实例中,如下代码,计算属性最终通过defineProperty将新属性添加到vm实例中。
在这里插入图片描述

watch监视属性

watch监视属性中,监视的属性必须是存在于当前vm实例中的属性。在new Vue的阶段,会去初始化initWatch函数,内部迭代配置项,若当前监视的属性是函数,则handler是函数,否则是对象

        watch: {
          n: {
            handler(n, o) {
              console.log(n, o);
            },
            immediate: true,
          },
          x(n, o) {
            console.log(n, o);
          },
        },

通过源码发现,针对监视的属性,可以将其值写成一个数组格式,如下

        watch: {
          n: [
            {
              handler(n, o) {
                console.log(n, o);
              },
              immediate: true,
            },
          ],
       }

在这里插入图片描述
进入createWatcher函数执行,在该函数中处理相关逻辑,通过代码发现,监视的属性其所对应的值还可以是一个字符串,该字符串就是vm实力身上绑定的方法。
在这里插入图片描述

        watch: {
          n: "upd",
        },
        methods: {
          upd() {
            console.log("upd");
          },
        },

无论是计算属性或者是监视属性,其底层都是经过Watch实例的处理。

函数扁平化

如下这段代码是需要将多个函数的返回值依次作为结果传递给下一个函数,但是下面这种写法的话过于笨重,并且如果有多个函数需要处理,则需要手动将前一个函数的返回值包裹起来传递作为参数。

const a = (n) => n + 10;
const b = (n) => n - 10;
const c = (n) => n * 10;
const d = (n) => n / 10;
console.log(d(c(b(a(10)))));

如下两种写法

function compose(...args) {
  const funcs = args;
  return function handler(param) {
    let res = 0;
    funcs.forEach((func, index) => {
      if (index === 0) {
        res = func(param);
      } else {
        res = func(res);
      }
    });
    return res;
  };
}

console.log(compose(a, b, c, d)(10));
function compose(...args) {
  const funcs = args;
  return function handler(param) {
    if (funcs.length === 0) return param; //reduce遍历空数组会报错
    return funcs.reduce((prev, item, index) => {
      return item(prev);
    }, param);
  };
}

如下是redux中都处理

function compose(...funcs) {
  if (funcs.length === 0) return (par) => par;
  if (funcs.length === 1) return funcs[0];
  //省略默认值的情况,a默认为第一个参数,b默认为第二个参数,这里是针对传入的函数进行逆序处理
  /* 
    第一次 a = a ,b = b , 返回函数地址 0X001 =...args=> a(b(args)), c(d(10))作为args参数,b替换为b,a在这里作为实际是第一个函数,a(b(c(d(10))))
    第二次 a=0x001 ,b=c,返回函数地址0x002 = ...args=> a(b(args)),  d(10)作为args参数。b替换为c,a(c(d(10)))返回上层作用域
    第三次 a=0x002 ,b=d,返回函数地址0x003 = ...args=> a(b(args)),  a(d(10)) //b替换d,返回上层作用域
  */
  return funcs.reduce(
    (a, b) =>
      (...args) =>
        a(b(args))
  );
}

生命周期函数

new Vue的时候会去执行Vue构造函数。在构造函数内部会优先去执行其原型上的_init初始化函数。
在这里插入图片描述
_init函数内部会按照vue的创建流程执行对应了函数,这些步骤大致对应vue的生命周期创建过程。

  1. 首先就是初始化initLifecycle函数,进行生命周期的初始化操作
  2. 其次执行initEvents函数,执行事件的初始化操作
  3. 然后就是执行initRender函数,在该函数中会初始化vm身上一些属性:如$slots,$attrs
  4. 往下就是到了 callHook$1执行生命周期函数beforeCreate。所以到这一步还没有进行到initState函数初始化数据等等,所以在beforeCreate生命周期函数中无法访问当vm实例中数据劫持的数据。
  5. 然后就是进入initInjections函数,类似react的创建上下文
  6. 执行initState函数,在该函数中创建数据的劫持操作,如初始化 initProps$1,initMethods,initData,initComputed$1,initWatch这些内容
  7. initProvide类似react的创建上下文
  8. 然后就是执行callHook$1函数执行created生命周期函数。在此期间可以访问vm实例身上的所有内容,
  9. 然后进入$mount函数,该函数在Vue原型上,主要是针对el或者template进行判断处理。$mount在源码中有两次地方进行了处理,分别赋予不同的函数。第一次进入该函数,会进入mountComponent函数中处理
  10. mountComponent函数中会 去调用beforeMount生命周期方法,做完后去执行mounted生命周期函数处理。但是在该函数内部会完成对beforeUpdate函数的监听处理,每次更新的时候触发都会进行判断是否为非初次挂载情况和销毁情况。
  11. 在一个名为queueWatcher的函数中创建一个监视更新的队列。更新的时候会进入flushSchedulerQueue函数,在该函数内部会去调用callUpdatedHooks更新的方法,并实现updated生命周期函数的触发处理。
  12. 最终queueWatcher函数中更新处理完毕后会,会触发nextTick,将其中的事件依次触发。
  13. 组件销毁可以通过vm.$destroy()或者组件失或销毁。都会去触发原型上的$destroy方法。在该方法内部会触发beforeDestroydestroyed生命周期函数,同时将_isDestroyed属性设置为true,同时将部分数据进行清空处理,但是数据的劫持还存在,所以为了防止数据更新,下次尝试视图更新的时候则会根据判断不会进入视图更新阶段。视图不会销毁,并且会进行事件解绑操作,但是部分初始化的数据依旧保留
  14. callHook$1在触发生命周期的函数内部,都会去调用setCurrentInstance方法将当前生命周期函数的this修改为当前vm实例
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

nextTick

有如下一段代码,了解vue的更新机制,首先需要知道,在vue中更新一个状态值后,可以立即获取到最新的内容。但是通知视图这件事情是放到异步队列中等待。待到该上下文中的代码执行完毕后,进行一次批处理操作。所以在下面的代码中,修改x了状态值后可以立即获取最新的内容,同时将本次状态更新通知视图的操作放到队列中,执行y的修改,把通知视图更新的操作放到队列中,最后进行一次批处理更新,updated生命周期函数执行一次。

      <button @click="handle1">{{x}}</button>
      let vm = new Vue({
        data: {
          x: 1,
          y: 1,
        },
        methods: {
          handle1() {
            this.x++;
            console.log(this.x);
            this.y++;
          },
        },
        updated() {
          console.log("update");
        },
      });

在这里插入图片描述
defineReactive定义响应式数据的时候,在set中函数中会执行如下notify函数,本质就是同步将数据修改,但是会将更新视图的操作放入一个更新队列sub.update();

                  dep.notify({
                      type: "set" /* TriggerOpTypes.SET */,
                      target: obj,
                      key: key,
                      newValue: newVal,
                      oldValue: value
                  });

在这里插入图片描述
即使使用$forceUpdate函数进行强制更新,视图也只会渲染一次,因为该函数底层也是去调用.update()函数将视图更新的操作放入队列中。

          handle1() {
            this.x++;
            console.log(this.x);
            this.$forceUpdate();
            this.y++;
          },

区分react的更新原理,react是当前将更新状态的操作和更新视图的操作都放到了异步队列中,因此在后续上下文中无法获取最新的状态值。

如果需要视图更新两次,就需要放入异步操作中。当vue执行的时候首先遇见x同步更新,然后将当前视图更新操作放入异步队列,然后当前上下文中没有同步代码后执行一次批处理操作,视图更新一次。然后定时器执行,y的值同步修改,然后更新操作放入队列中,上下文中没有内容后执行视图更新。setTimeout是放到宏队列,queueMicrotask是放到微队列

          handle1() {
            this.x++;
            console.log(this.x);

            setTimeout(() => {
              this.y++;
            });
          },
          handle1() {
            this.x++;
            console.log(this.x);

            queueMicrotask(() => {
              this.y++;
            });
          },

在这里插入图片描述
所以这种异步的方式可以使用$nextTick函数完成相应个功能,$nextTick第二个参数用来指定第一个函数的this指向。首先会把传入的函数放入一个callbacks队列中。会由flushCallbacks函数内部处理。将对应的callbacks回调放入如下优先级中处理形成异步任务。Promise > MutationObserver > setImmediate > setTimeout并且$nextTick操作是在updated生命周期执行完毕后执行其回调队列,因此在这里可以获取最新的DOM内容。这种模式就是发布者订阅模式

如下代码中,x的值同步修改后将视图更新的操作放到异步队列中,同时执行$nextTick函数,将内部函数放入callbacks异步队列中。然后视图执行一次更新操作。完成updated生命周期后,执行callbacks中的函数。

          handle1() {
            this.x++;
            this.$nextTick(() => {
              console.log(this);
            });
          },

在这里插入图片描述

Vue3

新特性

性能提升

  • 重写了虚拟DOM的实现,vue2是基于vue-template-compiler实现视图编译。该编译器无论是动态节点或是静态节点都需要重新编译处理。所以通常需要基于v-pre/v-once进行优化
  • vue3中基于@vue/compiler-sfc实现对template视图的编译,该编译器会跳过静态节点,只处理动态节点。因此性能提升1.3-2倍,服务器端性能提升2-3倍。
  • Tree shaking:vue3采用按需打包,基于vite实现,只针对使用到的内容打包,降低打包后的体积。
  • vue3基于proxy实现响应式劫持,性能更高

功能提升

  • 提供了Teleport组件实现传送门效果,可以将编译的内容放到除app容器以外的地方
    在vue中,默认编写的内容都是存放到app容器中,但是有一些需求如全局消息提示等,容器应该放到body中。这个时候可以使用Teleport组件完成。
<template>
  <h1>我是放到app容器中的</h1>
  <Message />
</template>

Message组件
<template>
  <Teleport to="body">
    <div>我是消息组件放到body中</div>
  </Teleport>
</template>

在这里插入图片描述

  • 提供了Suspense异步组件,等待一个组件异步处理完毕后,再渲染组件,可以实现组件一次渲染呈现数据,可以在组件异步加载期间,使用插槽fallback显示默认信息。
    如下代码,在list中进行了异步处理,但是这种List视图中的代码会被渲染两次,第一次进入if判断显示骨架屏,然后经过异步完成后,进入else判断显示数据
<template>
  <h1>我是放到app容器中的</h1>
  <List />
</template>
<template>
  <div v-if="list.length === 0">默认骨架屏</div>
  <ul v-else>
    <li v-for="item in list" :key="item">{{ item }}</li>
  </ul>
</template>
<script setup>
import { onMounted, ref } from "vue";
const list = ref([]);
function delay() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([1, 2, 3]);
    }, 2000);
  });
}

onMounted(async () => {
  list.value = await delay();
});
</script>

但是有的时候可能会出现这样子的代码,这个时候就发现,组件在视图中并没有渲染成功。这个时候就需要异步组件Suspense包裹处理。

  <ul>
    <li v-for="item in list" :key="item">{{ item }}</li>
  </ul>

<script setup>
import { ref } from "vue";
const list = ref([]);
function delay() {。。。}
list.value = await delay();
</script>

实际组件只会渲染一次,等待异步处理完成后,进行视图渲染

  <Suspense>
    <List />
    <template #fallback>
      <div>默认显示内容</div>
    </template>
  </Suspense>
  • 更好的支持Typescript
  • 支持custom render api可以自定义渲染api,用户可以尝试webgl自定义渲染

语法变革

  • vue3支持vue2语法,并且视图支持多个根节点
  • 采用组合式api替换配置式api
  • 采用函数式编程,去除类的概念,所以函数都是解构出来的,没有this指向问题。

Vue3 响应式

vue3中基于Proxy实现响应式处理,使用Proxy可以针对数组或者对象直接进行劫持,并且不需要对内部的数据进行遍历劫持操作,可以直接数据劫持一个对象,针对代理对象的所有操作,如新增,修改,读取,都可以做一个劫持操作,而defienProperty却只有get/set两步劫持操作。

      let obj = {
        name: "12312",
        age: 12,
      };

      let proxy = new Proxy(obj, {
        get(target, val) {
          console.log("get", target, val);
          return target.val;
        },
        set(target, key, val) {
          console.log("set", target, key, val);
          target[key] = val;
        },
        deleteProperty(target, key) {
          console.log("deleteproperty", target, key);
          return Reflect.deleteProperty(target, key);
        },
      });

reactive源码

reactive函数执行的时候传入一个对象,那么进入该函数的时候首先会进行只读判断,接着就是调用createReactiveObject函数,第二参数是处理非只读的情况。mutableHandlers是需要做劫持数据的配置对象。这里可以发现,vue3会比vue2多做几处劫持处理。

let proxy = reactive(data);
const mutableHandlers = {
  get: function get2(target, key) {
    ...
  },
  set: function set2(target, key, value) {
     ...
    };
  deleteProperty: function deleteProperty(target, key) {
    ...
  },
  has: function has$1(target, key) {
    ...
  },
  ownKeys: function ownKeys(target) {
    ...
  },
};

function reactive(target) {
  if (isReadonly(target)) {
    return target;
  }
  return createReactiveObject(target, false, mutableHandlers);
}
function createReactiveObject(target, isReadonly2, baseHandlers) {
  ....
}

createReactiveObject函数内部上来就会针对传递进来的对象进行处理判断是否为普通对象或数组

const isObject = (val) => val !== null && typeof val === "object";

function createReactiveObject(target, baseHandlers) {
  if (!isObject(target)) {
    {
      console.warn(`value cannot be made reactive: ${String(target)}`);
    }
    return target;
  }

  //判断是否是代理过的属性
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  //根据targetTypeMap获取传递参数的类型
  const targetType = getTargetType(target);
  if (targetType === 0 /* INVALID */) {
    return target;
  }
  //如果是数组或者对象类型则进入我们传入的proxy代理类型处理
  const proxy = new Proxy(
    target,
    targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers
  );
  proxyMap.set(target, proxy);
  return proxy;
}

在这里插入图片描述
然后就是get劫持函数,在get劫持中,会先针对数组进行处理,判断是否使用到数组的方法,如push等

const isArray = Array.isArray;
const hasOwn = (val, key) => hasOwnProperty.call(val, key);
const arrayInstrumentations = createArrayInstrumentations();
function createArrayInstrumentations() {
  const instrumentations = {};
  ["includes", "indexOf", "lastIndexOf"].forEach((key) => {
    instrumentations[key] = function (...args) {
      const arr = toRaw(this);
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, "get", i + "");
      }
      const res = arr[key](...args); //执行方法传入参数
      if (res === -1 || res === false) {
        return arr[key](...args.map(toRaw)); //不存在的话进行特殊处理
      } else {
        return res; //返回方法执行的结果
      }
    };
  });
  ["push", "pop", "shift", "unshift", "splice"].forEach((key) => {
    instrumentations[key] = function (...args) {
      pauseTracking();
      const res = toRaw(this)[key].apply(this, args);
      resetTracking(); //触发视图更新
      return res;
    };
  });
  return instrumentations;
}
  get: function get2(target, key) {
    const targetIsArray = isArray(target);
    if (!isReadonly2) {
    //判断读取,修改数据是否是采用数组方法完成的。
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key); 
      }
      if (key === "hasOwnProperty") {
        return hasOwnProperty;
      }
    }
    const res = Reflect.get(target, key);
    if (
      isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)
    ) {
      return res;
    }
    if (!isReadonly2) {
      track(target, "get", key);
    }
    if (shallow) {
      return res;
    }
    if (isRef(res)) {
      return targetIsArray && shared.isIntegerKey(key) ? res : res.value;
    }
    if (isObject(res)) { 
    //如果在get读取监视对象的某一个值,发现该值所对应的value是一个对象,那么会递归进去进行劫持
    //vue2是上来直接将所有的数据直接做响应式处理,而vue3初始情况是只对数据最外层进行响应式处理,数据内部的数据
    //是当被读取到的时候才进行递归响应式处理。所以在reactive函数内部会判断当前数据是否被代理过
      return isReadonly2 ? readonly(res) : reactive(res);
    }
    return res;
  },

在set函数中代码如下,会设置一个值,并进行新旧值判断,然后会判断是添加还是修改一个值

  set: function set2(target, key, value) {
    const result = Reflect.set(target, key, value);
   let oldValue = target[key];
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, "add", key, value);
      } else if (shared.hasChanged(value, oldValue)) {
        trigger(target, "set", key, value, oldValue);
      }
    }
    return result;
  },

v-model

现有一个父组件使用子组件信息如下,使用v-model绑定arr数据传递。在子组件内部接受数据并渲染。但是使用v-model进行数据绑定的时候,可以在子组件内部使用update:变量的方法触发事件更新。从而直接触发父组件数据更新,从而重新传递给子组件。

const arr = ref([1, 2, 3]);
<List v-model:arr="arr" />
<template>
  <div v-for="item in props.arr" :key="item">{{ item }}</div>
  <button @click="add">修改</button>
</template>

<script setup>
import { ref } from "vue";
let props = defineProps(["arr"]);
let emits = defineEmits();
function add() {
  emits("update:arr", [1, 2, 3, 4, 5, 6, 7]);
}

在这里插入图片描述
在这里插入图片描述
但是这种情况如果是reactive定义的响应式数据则不行

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/699632.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

二刷算法训练营Day28 | 回溯算法(4/6)

目录 详细布置&#xff1a; 1. 93. 复原 IP 地址 2. 78. 子集 3. 90. 子集 II 详细布置&#xff1a; 1. 93. 复原 IP 地址 有效 IP 地址 正好由四个整数&#xff08;每个整数位于 0 到 255 之间组成&#xff0c;且不能含有前导 0&#xff09;&#xff0c;整数之间用 . 分隔…

一、SpringBoot框架搭建

一、SpringBoot框架搭建 系列文章目录1.SpringBoot简介2.基础环境1.idea2.jdk3.maven 3.创建、配置、启动SpringBoot项目4.SpringBoot其他配置1.SpringBoot开发拦截器和解决跨域问题2.SpringBoot统一结果封装3.SpringBoot统一异常处理1.Result&#xff08;通用返回结果&#xf…

Thinkpad产品系列进BIOS设置(重装系统)

Thinkpad产品系列进BIOS设置&#xff08;重装系统&#xff09; 对于大多数ThinkPad笔记本产品&#xff08;T、X、W、P、L、E系列部分除外&#xff09;&#xff0c;例如T14、T15、T490、T590、X13、X390等&#xff0c;您需要在启动计算机时&#xff0c;当显示ThinkPad徽标时&…

防止设计图纸泄露:挑选合适的图纸加密解决方案

在技术迅猛发展的今天&#xff0c;企业的技术资产和知识产权成为了竞争的核心。图纸作为创新成果的直接体现&#xff0c;其安全性保护显得尤为重要。本文将探讨如何通过加密软件有效保护企业图纸&#xff0c;防止信息泄露。 一、图纸加密的必要性 图纸加密是确保企业技术资产安…

基于Nuvoton N9H30 咖啡机彩屏解决方案

基于Nuvoton N9H30 咖啡机彩屏解决方案 咖啡分为美式咖啡、滴滤、黑咖啡以及意式咖啡等&#xff0c;各种特色的咖啡都有不同的适应人群。一杯奶盖飘香的咖啡&#xff0c;不仅可以缓解工作上的疲惫&#xff0c;还可以获得苦尽甘来的滋味。在上班族的快节奏环境下&#xff0c;一…

【全网最齐报错的解决方法!】运行mvn命令打包项目jar包报错?“Fatal error compiling: 无效的目标发行版: 19 ”, 让我来看看~

最近写实验&#xff0c;要打包项目&#xff0c;但是不管是在cmd运行“mvn clean package -Dmaven.test.skiptrue”命令&#xff0c;还是在idea上去操作&#xff0c;都出现了这样的一个错误&#xff1a; [EROR] Failed to exeoute goal org.apache.maven.plugins:maven-comnpile…

【论文速读】| 通过大语言模型从协议实现中推断状态机

本次分享论文&#xff1a;Inferring State Machine from the Protocol Implementation via Large Language Model 基本信息 原文作者&#xff1a;Haiyang Wei, Zhengjie Du, Haohui Huang, Yue Liu, Guang Cheng, Linzhang Wang, Bing Mao 作者单位&#xff1a;南京大学&#…

AIHub导航

4、 AIHub https://www.aihub.cn/tools/llm/

CMake的学习之路

目录 一、基础命令 二、编译选项和设置 三、文件和目录操作 四、控制流命令 五、其他命令 六、CMake构建级别 CMake是一个跨平台的自动化建构系统&#xff0c;它使用一种人类可读的配置文件&#xff08;CMakeLists.txt&#xff09;来控制软件编译过程。以下是CMake中的一些…

Python报表需求处理示例

单一文件下&#xff0c;相关主题的共128张字段结构相似的表&#xff0c;对一种需求用Excel手工编辑相当麻烦&#xff0c;下面介绍一种python做自动化报表示例及代码流程。 每张表均有相同的字段结构&#xff0c;因此可对该文件下所有表格同时操作&#xff0c;大大提高了计算效率…

公用nacos,实现只调用本机相应服务,不出现负载均衡到别人机器上

当我们有两个研发同时在调试一个微服务模块时&#xff0c;你和对方本地都会启动服务&#xff0c;这就导致在nacos会同时注册两个实例。默认情况下请求这个服务&#xff0c;具体处理请求的程序会在你和对方之间来回轮询&#xff0c;即一下你的服务一下对方的服务。 其结果就导…

(三十八)Vue之插槽Slots

文章目录 插槽介绍插槽分类默认插槽具名插槽条件插槽动态插槽名 作用域插槽默认作用域插槽具名作用域插槽 上一篇&#xff1a;&#xff08;三十七&#xff09;vue 项目中常用的2个Ajax库 插槽介绍 在之前的文章中&#xff0c;我们已经了解到组件能够接收任意类型的值作为 prop…

bmp转jpg怎么转?给你介绍几种将bmp转成jpg的方法

bmp转jpg怎么转&#xff1f;首先&#xff0c;了解BMP和JPG两种格式的特点对于转换过程非常重要。BMP格式以无损方式存储图像数据&#xff0c;这意味着它可以保留图像的每个像素信息&#xff0c;但文件大小较大。而JPG格式则使用有损压缩算法&#xff0c;可以将文件大小大大减小…

ARM32开发--PWM通道输出

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 文章目录 前言 内容 需求 通用定时器多通道 开发流程 多通道配置 占空比更新 完整代码 高级定时器通道输出 开发流程 通道配置 Break配置 完整代码 总结 前言 加强掌握…

【Python】Flask问答系统Demo项目

学习视频 我是跟着知了传课学的Flask&#xff0c;起初了解Flask还是GPT告诉我的&#xff0c;现在可以说用Flask做后端是真的方便&#xff01; https://www.bilibili.com/video/BV17r4y1y7jJ 项目结构与下载 FlaskOA&#xff08;项目文件夹&#xff09; │ app.py │ conf…

性能测试------LoadRunner 详解

性能测试------LoadRunner的使用 一、什么是LoadRunner LoadRunner是一款由Micro Focus&#xff08;以前是Hewlett-Packard或HP公司&#xff09;开发的性能测试工具。它用于测试和分析系统在负载下的行为和性能。具体来说&#xff0c;LoadRunner可以模拟数千名用户同时访问应…

r语言数据分析案例26-美元兑换欧元汇率分析与研究

一、研究背景&#xff1a; 汇率是国际贸易和金融中最重要的价格之一&#xff0c;它直接影响着各国的经济利益和国际竞争力。美元兑换欧元汇率是全球最重要的汇率之一&#xff0c;它的波动对全球经济和金融市场都有着深远的影响。因此&#xff0c;对美元兑换欧元汇率的分析和研…

树莓派4B_OpenCv学习笔记5:读取窗口鼠标状态坐标_TrackBar滑动条控件的使用

今日继续学习树莓派4B 4G&#xff1a;&#xff08;Raspberry Pi&#xff0c;简称RPi或RasPi&#xff09; 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: Opencv 版本是4.5.1&#xff1a; 今日学习:读取窗口鼠标状态坐标_TrackBar滑动条控件的使…

华为机考入门python3--(35)牛客35-蛇形矩阵

分类&#xff1a;蛇形矩阵 知识点&#xff1a; 取出每行中非零的数字 row [str(num) for num in matrix[i] if num ! 0] 题目来自【牛客】 def generate_snake_matrix(n):# 初始化一个NN的矩阵matrix [[0] * n for _ in range(n)] start 1# i为行&#xff0c;&#xf…

国内著名的四个“大模型”

关于您提到的国内四大模型&#xff0c;这里为您详细介绍&#xff1a; 文心大模型&#xff1a;文心大模型是百度自主研发的产业级知识增强大模型。它以创新性的知识增强技术为核心&#xff0c;从单模态大模型发展到跨模态&#xff0c;从通用基础大模型到跨领域、跨行业&#xff…