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