• 快速进阶Vue3.0


    在2019.10.5日发布了Vue3.0预览版源码,但是预计最早需要等到 2020 年第一季度才有可能发布 3.0 正式版。

    可以直接看 github源码。

    新版Vue 3.0计划并已实现的主要架构改进和新功能:

    • 编译器(Compiler)

      • 使用模块化架构
      • 优化 "Block tree"
      • 更激进的 static tree hoisting 功能 (检测静态语法,进行提升)
      • 支持 Source map
      • 内置标识符前缀(又名"stripWith")
      • 内置整齐打印(pretty-printing)功能
      • 移除 Source map 和标识符前缀功能后,使用 Brotli 压缩的浏览器版本精简了大约10KB
    • 运行时(Runtime)

      • 速度显著提升
      • 同时支持 Composition API 和 Options API,以及 typings
      • 基于 Proxy 实现的数据变更检测
      • 支持 Fragments (允许组件有从多个根结点)
      • 支持 Portals (允许在DOM的其它位置进行渲染)
      • 支持 Suspense w/ async setup()
    目前不支持IE11

    1.剖析Vue Composition API

    可以去看官方地址

    • Vue 3 使用ts实现了类型推断,新版api全部采用普通函数,在编写代码时可以享受完整的类型推断(避免使用装饰器)
    • 解决了多组件间逻辑重用问题 (解决:高阶组件、mixin、作用域插槽)
    • Composition API 使用简单

    先尝鲜Vue3.0看看效果

    <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>
    到这里你会发现响应式才是Vue的灵魂

    2.源码目录剖析

    packages目录中包含着Vue3.0所有功能

    ├── packages
    │   ├── compiler-core # 所有平台的编译器
    │   ├── compiler-dom # 针对浏览器而写的编译器
    │   ├── reactivity # 数据响应式系统
    │   ├── runtime-core # 虚拟 DOM 渲染器 ,Vue 组件和 Vue 的各种API
    │   ├── runtime-dom # 针对浏览器的 runtime。其功能包括处理原生 DOM API、DOM 事件和 DOM 属性等。
    │   ├── runtime-test # 专门为测试写的runtime
    │   ├── server-renderer # 用于SSR
    │   ├── shared # 帮助方法
    │   ├── template-explorer
    │   └── vue # 构建vue runtime + compiler

    compiler
    compiler-core主要功能是暴露编译相关的API以及baseCompile方法
    compiler-dom基于compiler-core封装针对浏览器的compiler (对浏览器标签进行处理)

    runtime
    runtime-core 虚拟 DOM 渲染器、Vue 组件和 Vue 的各种API
    runtime-testDOM结构格式化成对象,方便测试
    runtime-dom 基于runtime-core编写的浏览器的runtime (增加了节点的增删改查,样式处理等),返回rendercreateApp方法

    reactivity
    单独的数据响应式系统,核心方法reactiveeffect、 refcomputed

    vue
    整合 compiler + runtime

    到此我们解析了Vue3.0结构目录,整体来看整个项目还是非常清晰的

    再来尝尝鲜:
    我们可以根据官方的测试用例来看下如何使用Vue3.0

    const app = {
        template:`<div>{{count}}</div>`,
        data(){
            return {count:100}
        },
    }
    let proxy = Vue.createApp().mount(app,container);
    setTimeout(()=>{
        proxy.count = 200;
    },2000)
    接下来我们来对比 Vue 2 和 Vue 3 中的响应式原理区别

    3.Vue2.0响应式原理机制 - defineProperty

    这个原理老生常谈了,就是拦截对象,给对象的属性增加set 和 get方法,因为核心是defineProperty所以还需要对数组的方法进行拦截

    3.1 对对象进行拦截

    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';

    3.2 数组方法劫持

    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)
    这里依赖收集的过程就不详细描述了,我们把焦点放在Vue3.0
    • Object.defineProperty缺点

      • 无法监听数组的变化
      • 需要深度遍历,浪费内存

    4.Vue3.0数据响应机制 - Proxy

    在学习Vue3.0之前,你必须要先熟练掌握ES6中的 ProxyReflect 及 ES6中为我们提供的 MapSet两种数据结构

    先应用再说原理:

    let p = Vue.reactive({name:'youxuan'});
    Vue.effect(()=>{ // effect方法会立即被触发
        console.log(p.name);
    })
    p.name = 'webyouxuan';; // 修改属性后会再次触发effect方法
    源码是采用ts编写,为了便于大家理解原理,这里我们采用js来从0编写,之后再看源码就非常的轻松啦!

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

    tupian

    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原理,之后再完善 tracktrigger方法

    4.2 effect实现

    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){
        // 查看是否有effect
        const 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();
          });
        }
      }
    }

    4.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
    }

    4.4 computed实现

    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变成true
    
    console.log(c.value); // 重新调用计算方法
    到此我们将Vue3.0核心的 Composition Api 就讲解完毕了! 不管是面试还是后期的应用也再也不需要担心啦!~
  • 相关阅读:
    约合¥1720 LG法国称G Watch将于6月开卖
    c语言中的位移位操作
    兼容的动态加载JS【原】
    Oracle 10g AND Oracle 11g手工建库案例--Oracle 10g
    面向对象思想----不看懊悔!
    学习设计模式--观察者模式(C++)
    使用POI 导入excel
    机器学习 F1-Score, recall, precision
    阿里无线11.11:手机淘宝移动端接入网关基础架构演进之路
    ICE 迁移64位安装问题
  • 原文地址:https://www.cnblogs.com/alsohui/p/12028340.html
Copyright © 2020-2023  润新知