• 一面高频vue面试题


    组件通信

    组件通信的方式如下:

    (1) props  /   $emit

    父组件通过props向子组件传递数据,子组件通过$emit和父组件通信

    1. 父组件向子组件传值
    • props只能是父组件向子组件进行传值,props使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。
    • props 可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。
    • props属性名规则:若在props中使用驼峰形式,模板中需要使用短横线的形式
    // 父组件
    <template>
      <div id="father">
        <son :msg="msgData" :fn="myFunction"></son>
      </div>
    </template>
    
    <script>
    import son from "./son.vue";
    export default {
      name: father,
      data() {
        msgData: "父组件数据";
      },
      methods: {
        myFunction() {
          console.log("vue");
        },
      },
      components: { son },
    };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    // 子组件
    <template>
      <div id="son">
        <p>{{ msg }}</p>
        <button @click="fn">按钮</button>
      </div>
    </template>
    <script>
    export default { name: "son", props: ["msg", "fn"] };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    2. 子组件向父组件传值
    • $emit绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on监听并接收参数。
    // 父组件
    <template>
      <div class="section">
        <com-article
          :articles="articleList"
          @onEmitIndex="onEmitIndex"
        ></com-article>
        <p>{{ currentIndex }}</p>
      </div>
    </template>
    
    <script>
    import comArticle from "./test/article.vue";
    export default {
      name: "comArticle",
      components: { comArticle },
      data() {
        return { currentIndex: -1, articleList: ["红楼梦", "西游记", "三国演义"] };
      },
      methods: {
        onEmitIndex(idx) {
          this.currentIndex = idx;
        },
      },
    };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    //子组件
    <template>
      <div>
        <div
          v-for="(item, index) in articles"
          :key="index"
          @click="emitIndex(index)"
        >
          {{ item }}
        </div>
      </div>
    </template>
    
    <script>
    export default {
      props: ["articles"],
      methods: {
        emitIndex(index) {
          this.$emit("onEmitIndex", index); // 触发父组件的方法,并传递参数index
        },
      },
    };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    (2)eventBus事件总线($emit / $on

    eventBus事件总线适用于父子组件非父子组件等之间的通信,使用步骤如下: (1)创建事件中心管理组件之间的通信

    // event-bus.js
    
    import Vue from 'vue'
    export const EventBus = new Vue()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (2)发送事件 假设有两个兄弟组件firstComsecondCom

    <template>
      <div>
        <first-com></first-com>
        <second-com></second-com>
      </div>
    </template>
    
    <script>
    import firstCom from "./firstCom.vue";
    import secondCom from "./secondCom.vue";
    export default { components: { firstCom, secondCom } };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    firstCom组件中发送事件:

    <template>
      <div>
        <button @click="add">加法</button>
      </div>
    </template>
    
    <script>
    import { EventBus } from "./event-bus.js"; // 引入事件中心
    
    export default {
      data() {
        return { num: 0 };
      },
      methods: {
        add() {
          EventBus.$emit("addition", { num: this.num++ });
        },
      },
    };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    (3)接收事件secondCom组件中发送事件:

    <template>
      <div>求和: {{ count }}</div>
    </template>
    
    <script>
    import { EventBus } from "./event-bus.js";
    export default {
      data() {
        return { count: 0 };
      },
      mounted() {
        EventBus.$on("addition", (param) => {
          this.count = this.count + param.num;
        });
      },
    };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在上述代码中,这就相当于将num值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。

    虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。

    (3)依赖注入(provide / inject)

    这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。

    provide / inject是Vue提供的两个钩子,和datamethods是同级的。并且provide的书写形式和data一样。

    • provide 钩子用来发送数据或方法
    • inject钩子用来接收数据或方法

    在父组件中:

    provide() { 
        return {     
            num: this.num  
        };
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在子组件中:

    inject: ['num']
    
    
    • 1
    • 2

    还可以这样写,这样写就可以访问父组件中的所有属性:

    provide() {
     return {
        app: this
      };
    }
    data() {
     return {
        num: 1
      };
    }
    
    inject: ['app']
    console.log(this.app.num)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    注意: 依赖注入所提供的属性是非响应式的。

    (3)ref / $refs

    这种方式也是实现父子组件之间的通信。

    ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。

    在子组件中:

    export default {
      data () {
        return {
          name: 'JavaScript'
        }
      },
      methods: {
        sayHello () {
          console.log('hello')
        }
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在父组件中:

    <template>
      <child ref="child"></component-a>
    </template>
    <script>
    import child from "./child.vue";
    export default {
      components: { child },
      mounted() {
        console.log(this.$refs.child.name); // JavaScript
        this.$refs.child.sayHello(); // hello
      },
    };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    (4)$parent / $children

    • 使用$parent可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法)
    • 使用$children可以让组件访问子组件的实例,但是,$children并不能保证顺序,并且访问的数据也不是响应式的。

    在子组件中:

    <template>
      <div>
        <span>{{ message }}</span>
        <p>获取父组件的值为: {{ parentVal }}</p>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return { message: "Vue" };
      },
      computed: {
        parentVal() {
          return this.$parent.msg;
        },
      },
    };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在父组件中:

    // 父组件中
    <template>
      <div class="hello_world">
        <div>{{ msg }}</div>
        <child></child>
        <button @click="change">点击改变子组件值</button>
      </div>
    </template>
    
    <script>
    import child from "./child.vue";
    export default {
      components: { child },
      data() {
        return { msg: "Welcome" };
      },
      methods: {
        change() {
          // 获取到子组件
          this.$children[0].message = "JavaScript";
        },
      },
    };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在上面的代码中,子组件获取到了父组件的parentVal值,父组件改变了子组件中message的值。 需要注意:

    • 通过$parent访问到的是上一级父组件的实例,可以使用$root来访问根组件的实例
    • 在组件中使用$children拿到的是所有的子组件的实例,它是一个数组,并且是无序的
    • 在根组件#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组
    • $children 的值是数组,而$parent是个对象

    (5)$attrs / $listeners

    考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?

    如果是用props/$emit来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。

    针对上述情况,Vue引入了$attrs / $listeners,实现组件之间的跨代通信。

    先来看一下inheritAttrs,它的默认值true,继承所有的父组件属性除props之外的所有属性;inheritAttrs:false 只继承class属性 。

    • $attrs:继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上
    • $listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)

    A组件(APP.vue):

    <template>
      <div id="app">
        //此处监听了两个事件,可以在B组件或者C组件中直接触发
        <child1
          :p-child1="child1"
          :p-child2="child2"
          @test1="onTest1"
          @test2="onTest2"
        ></child1>
      </div>
    </template>
    <script>
    import Child1 from "./Child1.vue";
    export default {
      components: { Child1 },
      methods: {
        onTest1() {
          console.log("test1 running");
        },
        onTest2() {
          console.log("test2 running");
        },
      },
    };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    B组件(Child1.vue):

    <template>
      <div class="child-1">
        <p>props: {{ pChild1 }}</p>
        <p>$attrs: {{ $attrs }}</p>
        <child2 v-bind="$attrs" v-on="$listeners"></child2>
      </div>
    </template>
    <script>
    import Child2 from "./Child2.vue";
    export default {
      props: ["pChild1"],
      components: { Child2 },
      inheritAttrs: false,
      mounted() {
        this.$emit("test1"); // 触发APP.vue中的test1方法
      },
    };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    C 组件 (Child2.vue):

    <template>
      <div class="child-2">
        <p>props: {{ pChild2 }}</p>
        <p>$attrs: {{ $attrs }}</p>
      </div>
    </template>
    <script>
    export default {
      props: ["pChild2"],
      inheritAttrs: false,
      mounted() {
        this.$emit("test2"); // 触发APP.vue中的test2方法
      },
    };
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在上述代码中:

    • C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性
    • 在B组件中通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)

    (6)总结

    (1)父子组件间通信

    • 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
    • 通过 ref 属性给子组件设置一个名字。父组件通过 $refs 组件名来获得子组件,子组件通过 $parent 获得父组件,这样也可以实现通信。
    • 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。

    (2)兄弟组件间通信

    • 使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
    • 通过 $parent/$refs 来获取到兄弟组件,也可以进行通信。

    (3)任意组件之间

    • 使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。

    如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。

    extend 有什么作用

    这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount 一起使用。

    // 创建组件构造器
    let Component = Vue.extend({ template: "
    test
    "
    }); // 挂载到 #app 上new Component().$mount('#app') // 除了上面的方式,还可以用来扩展已有的组件 let SuperComponent = Vue.extend(Component); new SuperComponent({ created() { console.log(1); }, }); new SuperComponent().$mount("#app");
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Vue Ref的作用

    • 获取dom元素this.$refs.box
    • 获取子组件中的datathis.$refs.box.msg
    • 调用子组件中的方法this.$refs.box.open()

    Vue组件data为什么必须是个函数?

    • 根实例对象data可以是对象也可以是函数 (根实例是单例),不会产生数据污染情况
    • 组件实例对象data必须为函数 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数,

    简版理解

    // 1.组件的渲染流程 调用Vue.component -> Vue.extend -> 子类 -> new 子类
    // Vue.extend 根据用户定义产生一个新的类
    function Vue() {}
    function Sub() { // 会将data存起来
        this.data = this.constructor.options.data();
    }
    Vue.extend = function(options) {
        Sub.options = options; // 静态属性
        return Sub;
    }
    let Child = Vue.extend({
        data:()=>( { name: 'zf' })
    });
    
    // 两个组件就是两个实例, 希望数据互不感染
    let child1 = new Child();
    let child2 = new Child();
    
    console.log(child1.data.name);
    child1.data.name = 'poetry';
    console.log(child2.data.name);
    
    // 根不需要 任何的合并操作   根才有vm属性 所以他可以是函数和对象  但是组件mixin他们都没有vm 所以我就可以判断 当前data是不是个函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    相关源码

    // 源码位置 src/core/global-api/extend.js
    export function initExtend (Vue: GlobalAPI) {
      Vue.extend = function (extendOptions: Object): Function {
        extendOptions = extendOptions || {}
        const Super = this
        const SuperId = Super.cid
        const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
        if (cachedCtors[SuperId]) {
          return cachedCtors[SuperId]
        }
    
        const name = extendOptions.name || Super.options.name
        if (process.env.NODE_ENV !== 'production' && name) {
          validateComponentName(name)
        }
    
        const Sub = function VueComponent (options) {
          this._init(options)
        }
        // 子类继承大Vue父类的原型
        Sub.prototype = Object.create(Super.prototype)
        Sub.prototype.constructor = Sub
        Sub.cid = cid++
        Sub.options = mergeOptions(
          Super.options,
          extendOptions
        )
        Sub['super'] = Super
    
        // For props and computed properties, we define the proxy getters on
        // the Vue instances at extension time, on the extended prototype. This
        // avoids Object.defineProperty calls for each instance created.
        if (Sub.options.props) {
          initProps(Sub)
        }
        if (Sub.options.computed) {
          initComputed(Sub)
        }
    
        // allow further extension/mixin/plugin usage
        Sub.extend = Super.extend
        Sub.mixin = Super.mixin
        Sub.use = Super.use
    
        // create asset registers, so extended classes
        // can have their private assets too.
        ASSET_TYPES.forEach(function (type) {
          Sub[type] = Super[type]
        })
        // enable recursive self-lookup
        if (name) { 
          Sub.options.components[name] = Sub // 记录自己 在组件中递归自己  -> jsx
        }
    
        // keep a reference to the super options at extension time.
        // later at instantiation we can check if Super's options have
        // been updated.
        Sub.superOptions = Super.options
        Sub.extendOptions = extendOptions
        Sub.sealedOptions = extend({}, Sub.options)
    
        // cache constructor
        cachedCtors[SuperId] = Sub
        return Sub
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    Vue 模板编译原理

    Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步

    第一步是将 模板字符串 转换成 element ASTs(解析器)
    第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
    第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
    
    • 1
    • 2
    • 3

    说说你对 proxy 的理解,Proxy 相比于 defineProperty 的优势

    Object.defineProperty() 的问题主要有三个:

    • 不能监听数组的变化 :无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应
    • 必须遍历对象的每个属性 :只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历。Proxy 可以劫持整个对象,并返回一个新的对象
    • 必须深层遍历嵌套的对象

    Proxy的优势如下:

    • 针对对象: 针对整个对象,而不是对象的某个属性 ,所以也就不需要对 keys 进行遍历
    • 支持数组:Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的
    • Proxy的第二个参数可以有 13 种拦截方:不限于applyownKeysdeletePropertyhas等等是Object.defineProperty不具备的
    • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
    • Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利

    proxy详细使用点击查看(opens new window)

    Object.defineProperty的优势如下:

    兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平

    defineProperty的属性值有哪些

    Object.defineProperty(obj, prop, descriptor)
    
    // obj 要定义属性的对象
    // prop 要定义或修改的属性的名称
    // descriptor 要定义或修改的属性描述符
    
    Object.defineProperty(obj,"name",{
      value:"poetry", // 初始值
      writable:true, // 该属性是否可写入
      enumerable:true, // 该属性是否可被遍历得到(for...in, Object.keys等)
      configurable:true, // 定该属性是否可被删除,且除writable外的其他描述符是否可被修改
      get: function() {},
      set: function(newVal) {}
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    相关代码如下

    import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑
    import { isObject } from "./util"; // 工具方法
    
    export function reactive(target) {
      // 根据不同参数创建不同响应式对象
      return createReactiveObject(target, mutableHandlers);
    }
    function createReactiveObject(target, baseHandler) {
      if (!isObject(target)) {
        return target;
      }
      const observed = new Proxy(target, baseHandler);
      return observed;
    }
    
    const get = createGetter();
    const set = createSetter();
    
    function createGetter() {
      return function get(target, key, receiver) {
        // 对获取的值进行放射
        const res = Reflect.get(target, key, receiver);
        console.log("属性获取", key);
        if (isObject(res)) {
          // 如果获取的值是对象类型,则返回当前对象的代理对象
          return reactive(res);
        }
        return res;
      };
    }
    function createSetter() {
      return function set(target, key, value, receiver) {
        const oldValue = target[key];
        const hadKey = hasOwn(target, key);
        const result = Reflect.set(target, key, value, receiver);
        if (!hadKey) {
          console.log("属性新增", key, value);
        } else if (hasChanged(value, oldValue)) {
          console.log("属性值被修改", key, value);
        }
        return result;
      };
    }
    export const mutableHandlers = {
      get, // 当获取属性时调用此方法
      set, // 当修改属性时调用此方法
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

    判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。

    监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

    我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger

    vue-router 路由钩子函数是什么 执行顺序是什么

    路由钩子的执行流程, 钩子函数种类有:全局守卫、路由守卫、组件守卫

    完整的导航解析流程:

    1. 导航被触发。
    2. 在失活的组件里调用 beforeRouteLeave 守卫。
    3. 调用全局的 beforeEach 守卫。
    4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
    5. 在路由配置里调用 beforeEnter。
    6. 解析异步路由组件。
    7. 在被激活的组件里调用 beforeRouteEnter。
    8. 调用全局的 beforeResolve 守卫 (2.5+)。
    9. 导航被确认。
    10. 调用全局的 afterEach 钩子。
    11. 触发 DOM 更新。
    12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

    参考:前端vue面试题详细解答

    diff算法

    时间复杂度: 个树的完全 diff 算法是一个时间复杂度为 O(n*3) ,vue进行优化转化成 O(n)

    理解:

    • 最小量更新, key 很重要。这个可以是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个DOM节点
      • 扩展 v-for 为什么要有 key ,没有 key 会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改DOM),加 key 只会移动减少操作DOM。
    • 只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。
    • 只进行同层比较,不会进行跨层比较。

    diff算法的优化策略:四种命中查找,四个指针

    1. 旧前与新前(先比开头,后插入和删除节点的这种情况)
    2. 旧后与新后(比结尾,前插入或删除的情况)
    3. 旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)
    4. 旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)

    MVVM、MVC、MVP的区别

    MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。

    在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗长、混乱,这样对项目开发和后期的项目维护是非常不利的。

    (1)MVC

    MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。

    (2)MVVM

    MVVM 分为 Model、View、ViewModel:

    • Model代表数据模型,数据和业务逻辑都在Model层中定义;
    • View代表UI视图,负责数据的展示;
    • ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

    Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。

    这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。

    (3)MVP

    MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。

    谈谈对keep-alive的了解

    keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性 include/exclude ,2个生命周期 activated deactivated

    v-if和v-show的区别

    • 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
    • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
    • 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
    • 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
    • 使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。

    Vue 初始化页面闪动问题如何解决?

    出现该问题是因为在 Vue 代码尚未被解析之前,尚无法控制页面中 DOM 的显示,所以会看见模板字符串等代码。
    解决方案是,在 css 代码中添加 v-cloak 规则,同时在待编译的标签上添加 v-cloak 属性:

    [v-cloak] { display: none; }
    
    <div v-cloak>
      {{ message }}
    div>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    v-model 的原理?

    我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

    • text 和 textarea 元素使用 value 属性和 input 事件;
    • checkbox 和 radio 使用 checked 属性和 change 事件;
    • select 字段将 value 作为 prop 并将 change 作为事件。

    以 input 表单元素为例:

    <input v-model='something'>
    
    相当于
    
    <input v-bind:value="something" v-on:input="something = $event.target.value">
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:

    父组件:
    <ModelChild v-model="message"></ModelChild>
    
    子组件:
    <div>{{value}}</div>
    
    props:{
        value: String
    },
    methods: {
      test1(){
         this.$emit('input', '小红')
      },
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?

    1)Vue为什么要用vm.$set() 解决对象新增属性不能响应的问题

    1. Vue使用了Object.defineProperty实现双向数据绑定
    2. 在初始化实例时对属性执行 getter/setter 转化
    3. 属性必须在data对象上存在才能让Vue将它转换为响应式的(这也就造成了Vue无法检测到对象属性的添加或删除)

    所以Vue提供了Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)

    2)接下来我们看看框架本身是如何实现的呢?

    Vue 源码位置:vue/src/core/instance/index.js

    export function set (target: Array<any> | Object, key: any, val: any): any {
      // target 为数组  
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
        target.length = Math.max(target.length, key)
        // 利用数组的splice变异方法触发响应式  
        target.splice(key, 1, val)
        return val
      }
      // key 已经存在,直接修改属性值  
      if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
      }
      const ob = (target: any).__ob__
      // target 本身就不是响应式数据, 直接赋值
      if (!ob) {
        target[key] = val
        return val
      }
      // 对属性进行响应式处理
      defineReactive(ob.value, key, val)
      ob.dep.notify()
      return val
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    我们阅读以上源码可知,vm.$set 的实现原理是:

    1. 如果目标是数组,直接使用数组的 splice 方法触发相应式;
    2. 如果目标是对象,会先判读属性是否存在、对象是否是响应式,
    3. 最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理

    defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法

    了解nextTick吗?

    异步方法,异步渲染最后一步,与JS事件循环联系紧密。主要使用了宏任务微任务(setTimeoutpromise那些),定义了一个异步方法,多次调用nextTick会将方法存入队列,通过异步方法清空当前队列。

    computed和watch区别

    1. 当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性computed

    Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理

    <template>{{fullName}}</template>
    export default {
        data(){
            return {
                firstName: 'zhang',
                lastName: 'san',
            }
        },
        computed:{
            fullName: function(){
                return this.firstName + ' ' + this.lastName
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. watch用于观察和监听页面上的vue实例,如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择

    Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销

    <template>{{fullName}}</template>
    export default {
        data(){
            return {
                firstName: 'zhang',
                lastName: 'san',
                fullName: 'zhang san'
            }
        },
        watch:{
            firstName(val) {
                this.fullName = val + ' ' + this.lastName
            },
            lastName(val) {
                this.fullName = this.firstName + ' ' + val
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    computed:

    • computed是计算属性,也就是计算值,它更多用于计算值的场景
    • computed具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算
    • computed适用于计算比较消耗性能的计算场景

    watch:

    • 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props $emit或者本组件的值,当数据变化时来执行回调进行后续操作
    • 无缓存性,页面重新渲染时值不变化也会执行

    小结:

    • computedwatch都是基于watcher来实现的
    • computed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行
    • watch是监控值的变化,当值发生变化时调用其对应的回调函数
    • 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed
    • 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化

    回答范例

    思路分析

    • 先看computed, watch两者定义,列举使用上的差异
    • 列举使用场景上的差异,如何选择
    • 使用细节、注意事项
    • vue3变化

    computed特点:具有响应式的返回值

    const count = ref(1)
    const plusOne = computed(() => count.value + 1)
    
    • 1
    • 2

    watch特点:侦测变化,执行回调

    const state = reactive({ count: 0 })
    watch(
      () => state.count,
      (count, prevCount) => {
        /* ... */
      }
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    回答范例

    1. 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computedmethods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑
    2. 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的DOM操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性.
    3. 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch可以传递对象,设置deepimmediate等选项
    4. vue3watch选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API中新出现了watchwatchEffect可以完全替代目前的watch选项,且功能更加强大

    基本使用

    // src/core/observer:45;
    
    // 渲染watcher  /  computed watcher  /  watch
    const vm = new Vue({
        el: '#app',
        data: {
            firstname:'张',
            lastname:'三'
        },
        computed:{ // watcher  =>   firstname lastname
            // computed 只有取值时才执行
    
            // Object.defineProperty .get
            fullName(){ // firstName lastName 会收集fullName计算属性
                return this.firstname + this.lastname
            }
        },
        watch:{
            firstname(newVal,oldVal){
                console.log(newVal)
            }
        }
    });
    
    setTimeout(() => {
        debugger;
        vm.firstname = '赵'
    }, 1000);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    相关源码

    // 初始化state
    function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
    
      // 初始化计算属性
      if (opts.computed) initComputed(vm, opts.computed) 
    
      // 初始化watch
      if (opts.watch && opts.watch !== nativeWatch) { 
        initWatch(vm, opts.watch)
      }
    }
    
    // 计算属性取值函数
    function createComputedGetter (key) {
      return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          if (watcher.dirty) { // 如果值依赖的值发生变化,就会进行重新求值
            watcher.evaluate(); // this.firstname lastname
          }
          if (Dep.target) { // 让计算属性所依赖的属性 收集渲染watcher
            watcher.depend()
          }
          return watcher.value
        }
      }
    }
    
    // watch的实现
    Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
      ): Function {
        const vm: Component = this
        debugger;
        if (isPlainObject(cb)) {
          return createWatcher(vm, expOrFn, cb, options)
        }
        options = options || {}
        options.user = true
        const watcher = new Watcher(vm, expOrFn, cb, options) // 创建watcher,数据更新调用cb
        if (options.immediate) {
          try {
            cb.call(vm, watcher.value)
          } catch (error) {
            handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
          }
        }
        return function unwatchFn () {
          watcher.teardown()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    谈一下对 vuex 的个人理解

    vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)

    主要包括以下几个模块:

    • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
    • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
    • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
    • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
    • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

    vue是如何实现响应式数据的呢?(响应式数据原理)

    Vue2: Object.defineProperty 重新定义 data 中所有的属性, Object.defineProperty 可以使数据的获取与设置增加一个拦截的功能,拦截属性的获取,进行依赖收集。拦截属性的更新操作,进行通知。

    具体的过程:首先Vue使用 initData 初始化用户传入的参数,然后使用 new Observer 对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value) 对对象进行处理,内部使用 defineeReactive 循环对象属性定义响应式变化,核心就是使用 Object.defineProperty 重新定义数据。

    说说Vue的生命周期吧

    什么时候被调用?

    • beforeCreate :实例初始化之后,数据观测之前调用
    • created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、 watch/event 事件回调。无 $el .
    • beforeMount:在挂载之前调用,相关 render 函数首次被调用
    • mounted:了被新创建的vm.$el替换,并挂载到实例上去之后调用改钩子。
    • beforeUpdate:数据更新前调用,发生在虚拟DOM重新渲染和打补丁,在这之后会调用改钩子。
    • updated:由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用改钩子。
    • beforeDestroy:实例销毁前调用,实例仍然可用。
    • destroyed:实例销毁之后调用,调用后,Vue实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除

    每个生命周期内部可以做什么?

    • created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。
    • mounted:实例已经挂载完成,可以进行一些DOM操作。
    • beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。
    • updated:可以执行依赖于DOM的操作,但是要避免更改状态,可能会导致更新无线循环。
    • destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。

    ajax放在哪个生命周期?:一般放在 mounted 中,保证逻辑统一性,因为生命周期是同步执行的, ajax 是异步执行的。单数服务端渲染 ssr 同一放在 created 中,因为服务端渲染不支持 mounted 方法。 什么时候使用beforeDestroy?:当前页面使用 $on ,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove

    Vue 的父子组件生命周期钩子函数执行顺序

    • 渲染顺序 :先父后子,完成顺序:先子后父
    • 更新顺序 :父更新导致子更新,子更新完成后父
    • 销毁顺序 :先父后子,完成顺序:先子后父

    加载渲染过程

    beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted子组件先挂载,然后到父组件

    子组件更新过程

    beforeUpdate->子 beforeUpdate->子 updated->父 updated

    父组件更新过程

    beforeUpdate->父 updated

    销毁过程

    beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

    之所以会这样是因为Vue创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。

    function patch (oldVnode, vnode, hydrating, removeOnly) { 
        if (isUndef(vnode)) { 
          if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return 
        }
        let isInitialPatch = false 
        const insertedVnodeQueue = [] // 定义收集所有组件的insert hook方法的数组 // somthing ... 
        createElm( 
            vnode, 
            insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, 
            nodeOps.nextSibling(oldElm) 
        )// somthing... 
        // 最终会依次调用收集的insert hook 
        invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
        return vnode.elm
    }
    
    function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { 
        // createChildren 会递归创建儿子组件 
        createChildren(vnode, children, insertedVnodeQueue) // something... 
    } 
    // 将组件的vnode插入到数组中 
    function invokeCreateHooks (vnode, insertedVnodeQueue) { 
        for (let i = 0; i < cbs.create.length; ++i) { 
            cbs.create[i](emptyNode, vnode) 
        }
        i = vnode.data.hook // Reuse variable 
        if (isDef(i)) { 
            if (isDef(i.create)) i.create(emptyNode, vnode) 
            if (isDef(i.insert)) insertedVnodeQueue.push(vnode) 
        } 
    } 
    // insert方法中会依次调用mounted方法 
    insert (vnode: MountedComponentVNode) { 
        const { context, componentInstance } = vnode 
        if (!componentInstance._isMounted) { 
            componentInstance._isMounted = true 
            callHook(componentInstance, 'mounted') 
        } 
    }
    function invokeInsertHook (vnode, queue, initial) { 
        // delay insert hooks for component root nodes, invoke them after the // element is really inserted 
        if (isTrue(initial) && isDef(vnode.parent)) { 
            vnode.parent.data.pendingInsert = queue 
        } else { 
            for (let i = 0; i < queue.length; ++i) { 
                queue[i].data.hook.insert(queue[i]); // 调用insert方法 
            } 
        } 
    }
    
    Vue.prototype.$destroy = function () { 
        callHook(vm, 'beforeDestroy') 
        // invoke destroy hooks on current rendered tree 
        vm.__patch__(vm._vnode, null) // 先销毁儿子 
        // fire destroyed hook 
        callHook(vm, 'destroyed') 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
  • 相关阅读:
    VisualSVN 8.1 Release Notes Date: November 3, 2022
    Synchronized代码详解?
    【安卓毕业设计源码】基于Uniapp+SSM实现的新闻APP[包运行成功]
    测试进阶知识之零日攻击的发现和防御
    Nacos
    Django基础三之路由、视图、模板
    Java反射学习笔记--使用示例
    构建零信任数据动态授权能力的关键步骤.
    基于.NetCore开发博客项目 StarBlog - (6) 页面开发之博客文章列表
    Mentor Pads中的关键技巧和工作方法
  • 原文地址:https://blog.csdn.net/bb_xiaxia1998/article/details/127569542