尤大大在B站进行了直播讲解Vue3.0的一些新特性,由于时间关系 我并没有赶上直播 后期观看了一些视频和文章对3.0的特性有了一定的了解,特此写下此Blog进行记录.
ts
实现了类型推断,新版api全部采用普通函数,在编写代码时可以享受完整的类型推断(避免使用装饰器)<script src="vue.global.js"></script>
<div id="container"></div>
<script>
function usePosition(){ // 实时获取鼠标位置
let state = Vue.reactive({x:0,y:0});
function update(e) {
state.x= e.pageX
state.y = e.pageY
}
Vue.onMounted(() => {
window.addEventListener(‘mousemove‘, update)
})
Vue.onUnmounted(() => {
window.removeEventListener(‘mousemove‘, update)
})
return Vue.toRefs(state);
}
const App = {
setup(){ // Composition API 使用的入口
const state = Vue.reactive({name:‘youxuan‘}); // 定义响应数据
const {x,y} = usePosition(); // 使用公共逻辑
Vue.onMounted(()=>{
console.log(‘当组挂载完成‘)
});
Vue.onUpdated(()=>{
console.log(‘数据发生更新‘)
});
Vue.onUnmounted(()=>{
console.log(‘组件将要卸载‘)
})
function changeName(){
state.name = ‘webyouxuan‘;
}
return { // 返回上下文,可以在模板中使用
state,
changeName,
x,
y
}
},
template:`<button @click="changeName">{{state.name}} 鼠标x: {{x}} 鼠标: {{y}}</button>`
}
Vue.createApp().mount(App,container);
</script>
简单可以理解为 将各个功能模块聚合
无需类似Vue2.0 按照特定的格式划分 将模块的各个功能分散于不同的生命周期钩子函数中,项目各个模块之间耦合更低。
首先总结回忆一下2.0中响应式实现的原理机制
function observer(target){
// 如果不是对象数据类型直接返回即可
if(typeof target !== ‘object‘){
return target
}
// 重新定义key
for(let key in target){
defineReactive(target,key,target[key])
}
}
function update(){
console.log(‘update view‘)
}
function defineReactive(obj,key,value){
observer(value); // 有可能对象类型是多层,递归劫持
Object.defineProperty(obj,key,{
get(){
// 在get 方法中收集依赖
return value
},
set(newVal){
if(newVal !== value){
observer(value);
update(); // 在set方法中触发更新
}
}
})
}
let obj = {name:‘youxuan‘}
observer(obj);
obj.name = ‘webyouxuan‘;
首先写一个vue方法,在里面定义所需要的数据,用vue.prototype.obersever注册get和set,遍历所有的obj然后取到每一个obj里面每一个obj[i],然后判断obj[i]的typeof是不是object,如果是那么重新遍历如果不是那么利用obj.defineproperty进行存取数据,get是用了收集依赖,set里面有一个newvalue是=你的value的,然后渲染render(),这样就注册完setget了,然后获取新的值然后重新渲染。js部分完成。在页面引入写好的js然后new一个vue
因为defineProperty
是无法监听数组变化的,所以在vue中实际上是通过hack了Array原型上的push
和pop
等方法来进行对数组的监听的
let oldProtoMehtods = Array.prototype;
let proto = Object.create(oldProtoMehtods);
[‘push‘,‘pop‘,‘shift‘,‘unshift‘].forEach(method=>{
Object.defineProperty(proto,method,{
get(){
update();
oldProtoMehtods[method].call(this,...arguments)
}
})
})
function observer(target){
if(typeof target !== ‘object‘){
return target
}
// 如果不是对象数据类型直接返回即可
if(Array.isArray(target)){
Object.setPrototypeOf(target,proto);
// 给数组中的每一项进行observr
for(let i = 0 ; i < target.length;i++){
observer(target[i])
}
return
};
// 重新定义key
for(let key in target){
defineReactive(target,key,target[key])
}
}
let obj = {hobby:[{name:‘youxuan‘},‘喝‘]}
observer(obj)
obj.hobby[0].name = ‘webyouxuan‘; // 更改数组中的对象也会触发试图更新
console.log(obj)
数组监听实现
先把array.prototype
取出来,然后在拷贝一份用obj.create
(拷贝是为了防止在修改的时候影响到原来的原型链),然后定义一个储存着数组方法的数组arr,对arr进行forEach
循环,每次循环给拷贝的对象设置一个重写也就是做一个装饰着模式,原型链本身有数组方法,所以拷贝出来的对象也有那些方法。重写先去把刚开始的原型链上的原本的方法比如push
方法用apply(this,arguments)然后再去触发视图更新,然后把这个prototype
关联到get
上的prototype
,将prototype
替换,这样push
方法就即会执行原来的push
方法又会执行触发视图更新
首先必须了解ES6中的Proxy,Reflect及Map,Set
Reflect
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
Proxy
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
const p = new Proxy(target, handler)
***target
***要使用 Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
***handler
***一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p
的行为。
let p = Vue.reactive({name:‘youxuan‘});
Vue.effect(()=>{ // effect方法会立即被触发
console.log(p.name);
})
p.name = ‘webyouxuan‘;; // 修改属性后会再次触发effect方法
reactive
方法实现通过proxy 自定义获取、增加、删除等行为
function reactive(target){
// 创建响应式对象
return createReactiveObject(target);
}
function isObject(target){
return typeof target === ‘object‘ && target!== null;
}
function createReactiveObject(target){
// 判断target是不是对象,不是对象不必继续
if(!isObject(target)){
return target;
}
const handlers = {
get(target,key,receiver){ // 取值
console.log(‘获取‘)
let res = Reflect.get(target,key,receiver);
return res;
},
set(target,key,value,receiver){ // 更改 、 新增属性
console.log(‘设置‘)
let result = Reflect.set(target,key,value,receiver);
return result;
},
deleteProperty(target,key){ // 删除属性
console.log(‘删除‘)
const result = Reflect.deleteProperty(target,key);
return result;
}
}
// 开始代理
observed = new Proxy(target,handlers);
return observed;
}
let p = reactive({name:‘youxuan‘});
console.log(p.name); // 获取
p.name = ‘webyouxuan‘; // 设置
delete p.name; // 删除
那么如何实现多层代理呢?
let p = reactive({ name: "youxuan", age: { num: 10 } });
p.age.num = 11
由于我们只代理了第一层对象,所以对age对象进行更改是不会触发set方法的,但是却触发了get方法,这是由于 p.age会造成 get操作
get(target, key, receiver) {
// 取值
console.log("获取");
let res = Reflect.get(target, key, receiver);
return isObject(res) // 懒代理,只有当取值时再次做代理,vue2.0中一上来就会全部递归增加getter,setter
? reactive(res) : res;
}
这里我们将p.age取到的对象再次进行代理,这样在去更改值即可触发set方法
接下来考虑一下数组的问题吧
我们可以发现Proxy默认可以支持数组,包括数组的长度变化以及索引值的变化
let p = reactive([1,2,3,4]);
p.push(5);
但是这样会触发两次set方法,第一次更新的是数组中的第4项,第二次更新的是数组的length
因此我们重新修改一下更新操作
set(target, key, value, receiver) {
// 更改、新增属性
let oldValue = target[key]; // 获取上次的值
let hadKey = hasOwn(target,key); // 看这个属性是否存在
let result = Reflect.set(target, key, value, receiver);
if(!hadKey){ // 新增属性
console.log(‘更新 添加‘)
}else if(oldValue !== value){ // 修改存在的属性
console.log(‘更新 修改‘)
}
// 当调用push 方法第一次修改时数组长度已经发生变化
// 如果这次的值和上次的值一样则不触发更新
return result;
}
解决重复使用reactive情况
// 情况1.多次代理同一个对象
let arr = [1,2,3,4];
let p = reactive(arr);
reactive(arr);
// 情况2.将代理后的结果继续代理
let p = reactive([1,2,3,4]);
reactive(p);
通过hash
表的方式来解决重复代理的情况
const toProxy = new WeakMap(); // 存放被代理过的对象
const toRaw = new WeakMap(); // 存放已经代理过的对象
function reactive(target) {
// 创建响应式对象
return createReactiveObject(target);
}
function isObject(target) {
return typeof target === "object" && target !== null;
}
function hasOwn(target,key){
return target.hasOwnProperty(key);
}
function createReactiveObject(target) {
if (!isObject(target)) {
return target;
}
let observed = toProxy.get(target);
if(observed){ // 判断是否被代理过
return observed;
}
if(toRaw.has(target)){ // 判断是否要重复代理
return target;
}
const handlers = {
get(target, key, receiver) {
// 取值
console.log("获取");
let res = Reflect.get(target, key, receiver);
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receiver) {
let oldValue = target[key];
let hadKey = hasOwn(target,key);
let result = Reflect.set(target, key, value, receiver);
if(!hadKey){
console.log(‘更新 添加‘)
}else if(oldValue !== value){
console.log(‘更新 修改‘)
}
return result;
},
deleteProperty(target, key) {
console.log("删除");
const result = Reflect.deleteProperty(target, key);
return result;
}
};
// 开始代理
observed = new Proxy(target, handlers);
toProxy.set(target,observed);
toRaw.set(observed,target); // 做映射表
return observed;
}
到这里reactive方法基本实现完毕,接下来就是与Vue2中的逻辑一样实现依赖收集和触发更新
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver);
+ track(target,‘get‘,key); // 依赖收集
return isObject(res)
?reactive(res):res;
},
set(target, key, value, receiver) {
let oldValue = target[key];
let hadKey = hasOwn(target,key);
let result = Reflect.set(target, key, value, receiver);
if(!hadKey){
+ trigger(target,‘add‘,key); // 触发添加
}else if(oldValue !== value){
+ trigger(target,‘set‘,key); // 触发修改
}
return result;
}
track的作用是依赖收集,收集的主要是effect,我们先来实现effect原理,之后再完善 track和trigger方法
effect意思是副作用,此方法默认会先执行一次。如果数据变化后会再次触发此回调函数。
let school = {name:‘youxuan‘}
let p = reactive(school);
effect(()=>{
console.log(p.name); // youxuan
})
我们来实现effect
方法,我们需要将effect
方法包装成响应式effect
。
function effect(fn) {
const effect = createReactiveEffect(fn); // 创建响应式的effect
effect(); // 先执行一次
return effect;
}
const activeReactiveEffectStack = []; // 存放响应式effect
function createReactiveEffect(fn) {
const effect = function() {
// 响应式的effect
return run(effect, fn);
};
return effect;
}
function run(effect, fn) {
try {
activeReactiveEffectStack.push(effect);
return fn(); // 先让fn执行,执行时会触发get方法,可以将effect存入对应的key属性
} finally {
activeReactiveEffectStack.pop(effect);
}
}
当调用fn()
时可能会触发get
方法,此时会触发track
const targetMap = new WeakMap();
function track(target,type,key){
// 查看是否有effectconst effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1];
if(effect){
let depsMap = targetMap.get(target);
if(!depsMap){ // 不存在map
targetMap.set(target,depsMap = new Map());
}
let dep = depsMap.get(target);
if(!dep){ // 不存在set
depsMap.set(key,(dep = new Set()));
}
if(!dep.has(effect)){
dep.add(effect); // 将effect添加到依赖中
}
}
}
当更新属性时会触发trigger
执行,找到对应的存储集合拿出effect
依次执行
function trigger(target,type,key){
const depsMap = targetMap.get(target);
if(!depsMap){
return
}
let effects = depsMap.get(key);
if(effects){
effects.forEach(effect=>{
effect();
})
}
}
我们发现如下问题
let school = [1,2,3];
let p = reactive(school);
effect(()=>{
console.log(p.length);
})
p.push(100);
新增了值,effect方法并未重新执行,因为push中修改length已经被我们屏蔽掉了触发trigger方法,所以当新增项时应该手动触发length属性所对应的依赖。
function trigger(target, type, key) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
let effects = depsMap.get(key);
if (effects) {
effects.forEach(effect => {
effect();
});
}
// 处理如果当前类型是增加属性,如果用到数组的length的effect应该也会被执行if (type === "add") {
let effects = depsMap.get("length");
if (effects) {
effects.forEach(effect => {
effect();
});
}
}
}
ref可以将原始数据类型也转换成响应式数据,需要通过.value
属性进行获取值
function convert(val) {
return isObject(val) ? reactive(val) : val;
}
function ref(raw) {
raw = convert(raw);
const v = {
_isRef:true, // 标识是ref类型get value() {
track(v, "get", "");
return raw;
},
set value(newVal) {
raw = newVal;
trigger(v,‘set‘,‘‘);
}
};
return v;
}
问题又来了我们再编写个案例
let r = ref(1);
let c = reactive({
a:r
});
console.log(c.a.value);
这样做的话岂不是每次都要多来一个.value,这样太难用了
在get
方法中判断如果获取的是ref
的值,就将此值的value
直接返回即可
let res = Reflect.get(target, key, receiver);
if(res._isRef){
return res.value
}
computed
实现也是基于 effect
来实现的,特点是computed
中的函数不会立即执行,多次取值是有缓存机制的
先来看用法:
let a = reactive({name:‘youxuan‘});
let c = computed(()=>{
console.log(‘执行次数‘)
return a.name +‘webyouxuan‘;
})
// 不取不执行,取n次只执行一次console.log(c.value);
console.log(c.value);
function computed(getter){
let dirty = true;
const runner = effect(getter,{ // 标识这个effect是懒执行lazy:true, // 懒执行scheduler:()=>{ // 当依赖的属性变化了,调用此方法,而不是重新执行effect
dirty = true;
}
});
let value;
return {
_isRef:true,
get value(){
if(dirty){
value = runner(); // 执行runner会继续收集依赖
dirty = false;
}
return value;
}
}
}
修改effect
方法
function effect(fn,options) {
let effect = createReactiveEffect(fn,options);
if(!options.lazy){ // 如果是lazy 则不立即执行
effect();
}
return effect;
}
function createReactiveEffect(fn,options) {
const effect = function() {
return run(effect, fn);
};
effect.scheduler = options.scheduler;
return effect;
}
在trigger
时判断
deps.forEach(effect => {
if(effect.scheduler){ // 如果有scheduler 说明不需要执行effect
effect.scheduler(); // 将dirty设置为true,下次获取值时重新执行runner方法
}else{
effect(); // 否则就是effect 正常执行即可
}
});
let a = reactive({name:‘youxuan‘});
let c = computed(()=>{
console.log(‘执行次数‘)
return a.name +‘webyouxuan‘;
})
// 不取不执行,取n次只执行一次console.log(c.value);
a.name = ‘zf10‘; // 更改值 不会触发重新计算,但是会将dirty变成trueconsole.log(c.value); // 重新调用计算方法
原文:https://www.cnblogs.com/NaN-prototype/p/12809100.html