Vue 3.0 初相识

尤大大在B站进行了直播讲解Vue3.0的一些新特性,因为时间关系 我并无遇上直播 后期观看了一些视频和文章对3.0的特性有了必定的了解,特此写下此Blog进行记录.vue

1.剖析Vue Composition API

  • Vue 3 使用ts实现了类型推断,新版api所有采用普通函数,在编写代码时能够享受完整的类型推断(避免使用装饰器)
  • 解决了多组件间逻辑重用问题 (解决:高阶组件、mixin、做用域插槽)
  • Composition 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>

简单能够理解为 将各个功能模块聚合react

无需相似Vue2.0 按照特定的格式划分 将模块的各个功能分散于不一样的生命周期钩子函数中,项目各个模块之间耦合更低。web

2.响应式原理机制变化

首先总结回忆一下2.0中响应式实现的原理机制api

2.1 Object.defineProperty

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数组

2.2数组劫持

由于defineProperty是没法监听数组变化的,因此在vue中其实是经过hack了Array原型上的pushpop等方法来进行对数组的监听的缓存

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])
    }
}

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/88b53bdb-4e47-499f-9a64-12c28944cf85/Untitled.png

let obj = {hobby:[{name:'youxuan'},'喝']}
observer(obj)
obj.hobby[0].name = 'webyouxuan'; // 更改数组中的对象也会触发试图更新
console.log(obj)

数组监听实现app

先把array.prototype取出来,而后在拷贝一份用obj.create(拷贝是为了防止在修改的时候影响到原来的原型链),而后定义一个储存着数组方法的数组arr,对arr进行forEach循环,每次循环给拷贝的对象设置一个重写也就是作一个装饰着模式,原型链自己有数组方法,因此拷贝出来的对象也有那些方法。重写先去把刚开始的原型链上的本来的方法好比push方法用apply(this,arguments)而后再去触发视图更新,而后把这个prototype关联到get上的prototype,将prototype替换,这样push方法就即会执行原来的push方法又会执行触发视图更新函数

3.Vue3.0的监听实现——Proxy

首先必须了解ES6中的Proxy,Reflect及Map,Setthis

Reflectprototype

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方法

3.1 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中的逻辑同样实现依赖收集和触发更新

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f2f74079-c404-498d-9b5b-aa674c954215/Untitled.png

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方法

3.2effect实现

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();
      });
    }
  }
}

3.3 ref实现

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
}

3.4computed实现

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); // 从新调用计算方法
相关文章
相关标签/搜索