关于 Vue.js 双向数据绑定基本实现认知

news2024/10/6 8:34:04

写在前面


  • 很早的一篇博客,整理了部分,蹭假期整理完
  • 博文内容涉及:
  • 双向数据绑定 实现方式简单介绍
  • 基于发布订阅数据劫持双向数据绑定两种不同实现(ES5/ES6) Demo,以及代码简单分析
  • Object.defineProperty && Proxy API 介绍以及特性对比
  • 理解不足小伙伴帮忙指正 😃,生活加油

对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》


双向数据绑定介绍

在前端框架中,特别是响应式框架(如Vue.js, Angular等)中,双向数据绑定(Two-way data binding)是一个核心特性,它允许开发者在UI和数据之间建立直接的、双向的联系(MVVM)。下面是一些实现双向数据绑定的常见做法:

脏值检查(Dirty Checking)

脏值检查是一种简单的双向数据绑定策略。它周期性地检查数据模型(Model)是否发生了变化,如果发生了变化,则更新视图(View)。脏值检查通常涉及一个“检查周期”或“轮询间隔”,在这个间隔内,框架会遍历所有绑定,并检查是否有任何变化。

然而,脏值检查并不高效,因为它可能需要对整个数据模型进行不必要的遍历,即使数据实际上并没有改变。此外,它也不能立即反映变化,因为它依赖于轮询间隔。

数据劫持(Data Interception)

数据劫持(也称为数据代理或对象劫持)是一种更高效的双向数据绑定策略。它依赖于JavaScriptObject.defineProperty()方法(在ES5中引入),该方法允许你定义或修改对象的属性,包括getter和setter方法。

Vue.js 的早期版本中,当一个对象被用作数据模型时,Vue 会遍历它的所有属性,并使用 Object.defineProperty() 将它们转化为getter/setter,以便在数据变化时能够立即感知到。当视图需要读取数据模型时,getter方法会被调用;当视图需要更新数据模型时,setter方法会被调用,并且可以在这里触发视图的更新。

Vue.js 3.0 开始,引入了更高效的响应式系统,称为Proxy-based reactive systemVue.js 3.0 及以后的版本使用ES6的Proxy来实现双向数据绑定。通过使用Proxy,Vue.js可以更灵活地劫持整个对象,并监视对象的新增和删除属性操作,以及数组的索引和长度变化。

发布者-订阅者模式(Publisher-Subscriber Pattern)

发布者-订阅者模式是一种软件设计模式,它允许一个或多个发布者(Publisher)发布事件,而零个或多个订阅者(Subscriber)会监听这些事件,并在事件发生时执行相应的操作。

在双向数据绑定的上下文中,数据模型可以被视为发布者,而视图则是订阅者。当数据模型发生变化时,它会发布一个事件(通常是一个“change”事件),而所有订阅了这个事件的视图都会收到通知,并更新自己以反映新的数据。

这种模式允许数据模型和视图之间实现松散的耦合,因为它们之间不需要直接通信;它们只需要知道如何发布和监听事件即可。此外,这种模式还具有良好的可扩展性,因为你可以轻松地添加新的发布者或订阅者,而无需修改现有的代码。

MVVM

Vue.js 双向绑定的简单实现

Vue.js 使用了数据劫持(通过Object.defineProperty()、ES6的Proxy)和发布者-订阅者模式(通过自定义的Dep类和Watcher类)来实现其双向数据绑定机制。而Angular则使用了脏值检查Zone.js库(它类似于数据劫持,但工作方式略有不同)来实现类似的功能。

在这里插入图片描述

Object.defineProperty 数据劫持 Demo

下面的 Demo 简化了 Vue.js 实现,通过数据劫持、订阅者和发布者的机制,实现了将数据和DOM节点进行绑定,并在数据变化时自动更新相关的DOM节点,从而实现了简单的双向数据绑定功能。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Two-way data-binding</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="text">
        <br />
        {{ text }}
    </div>
    <script>
        /*
         *@Time    :   2024/05/03 10:59:55
         *@Desc    :   数据劫持 或者叫数据监听
         */
        function observe(obj, vm) {
            // 获取所有的数据,构造数据劫持
            Object.keys(obj).forEach((key) => {
                console.log(1, "劫持的数据对象为", key, "值为", obj[key])
                defineReactive(vm, key, obj[key]);
            });
        }

        /*
         *@Time    :   2024/05/03 10:44:59
         *@Desc    :   响应式处理函数  
         */
        function defineReactive(obj, key, val) {
            let dep = new Dep();
            console.log(2, "创建发布者:", dep)
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                // 使用getter/setter 来实现数据劫持  
                get: _ => {
                    // 注册订阅者,如果发布者存在
                    if (Dep.target) {
                        dep.addSub(Dep.target);
                    }
                    return val   // 返回属性值
                },
                set: (newVal) => {
                    if (newVal === val) {
                        return
                    }
                    val = newVal;
                    console.log(5, "更新监听的数据", key, newVal)
                    // 通知相关的订阅者进行更新
                    dep.notify();
                }
            });
        }

        /*
         *@Time    :   2024/05/03 10:48:55
         *@Desc    :   简化的虚拟DOM编译和更新的示例
         */
        function nodeToFragment(node, vm) {
            var flag = document.createDocumentFragment();
            var child;
            console.log(3, "编译虚拟 Dom 节点")
            while (child = node.firstChild) {
                compile(child, vm);
                flag.appendChild(child);
            }
            // 返回虚拟 Dom
            return flag;
        }

        /*
         *@Time    :   2024/05/03 10:52:42
         *@Desc    :   编译DOM节点
         */
        function compile(node, vm) {
            // 节点类型为元素
            if (node.nodeType === 1) {
                // 获取所有的属性
                var attr = node.attributes;
                // 解析属性
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == 'v-model') {
                        var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                        // 添加 input 事件
                        node.addEventListener('input', function (e) {
                            // 给相应的 data 属性赋值,进而触发该属性的set方法
                            vm[name] = e.target.value;
                            debugger
                        });
                        // 将data的值赋给该node,这里触发 getter 方法
                        // 但是不进行注册
                        node.value = vm[name];
                        node.removeAttribute('v-model');
                        // 构造订阅者
                        new Watcher(vm, node, name, 'input');
                    }
                }

            }
            let reg = /\{\{(.*)\}\}/;
            // 节点类型为 text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    // 构造订阅者
                    new Watcher(vm, node, name, 'text');
                }
            }
        }
        /*
         *@Time    :   2024/05/03 11:53:27
         *@Desc    :   订阅者
         */
        function Watcher(vm, node, name, nodeType) {
            //  this为watcher函数
            Dep.target = this;
            //  console.log(this);
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.nodeType = nodeType;
            this.update();
            Dep.target = null;
        }
        Watcher.prototype = {
            update() {
                this.get();
                //  如果是文本,直接更新 `nodeValue`
                if (this.nodeType == 'text') {
                    this.node.nodeValue = this.value;
                }
                // 如果是输入标签,更新 value 的值
                if (this.nodeType == 'input') {
                    this.node.value = this.value;
                }
                console.log(6.2, "通知 Dom", this.nodeType, "数据为", this.value)
            },
            // 获取 data 中的属性值
            get() {
                // 触发相应属性的 get,这里会进行订阅者注册
                this.value = this.vm[this.name];
                console.log(6.1, "获取", this.nodeType, "数据最新的值:", this.value)
            }
        }
        /*
         *@Time    :   2024/05/03 11:48:38
         *@Desc    :   发布者
         */
        function Dep() {
            this.subs = []
        }
        Dep.prototype = {
            addSub(sub) {
                this.subs.push(sub);
                console.log(4, "suds:", this.subs.length, "注册订阅者:", sub)
            },
            notify() {
                console.log(6, "通知订阅者:", this.subs)
                this.subs.forEach((sub) => {
                    sub.update();
                });
            }
        };

        /*
         *@Time    :   2024/05/03 11:07:01
         *@Desc    :   
         */

        function Vue(options) {
            this.data = options.data;
            let data = this.data;
            // 构造数据劫持
            observe(data, this);
            let id = options.el;
            let dom = nodeToFragment(document.getElementById(id), this);
            // 编译完成后,将dom返回到app中进行挂载
            document.getElementById(id).appendChild(dom);
        }

        /*
         *@Time    :   2024/05/03 11:09:06
         *@Desc    :   定义 Vue 对象,传递需要挂载的元素,双向绑定的数据对象
         */
        let vm = new Vue({
            el: 'app',
            data: {
                text: 'hello world'
            }
        });
    </script>
</body>

在这里插入图片描述

简单分析一下干了什么:

observe 函数用于数据劫持,它接收一个对象和Vue实例作为参数。它通过遍历对象的属性,并调用defineReactive 函数来定义属性的getter和setter,从而实现对属性的劫持和监视。

function observe(obj, vm) {
     // 获取所有的数据,构造数据劫持
     Object.keys(obj).forEach((key) => {
         console.log(1,"劫持的数据对象为", key, "值为",obj[key])
         defineReactive(vm, key, obj[key]);
     
     });
}

defineReactive 函数定义了属性的 getter和setter。它创建了一个Dep对象作为发布者,getter 中注册订阅者(Watcher),setter中更新属性的值并通知相关的订阅者进行更新。

function defineReactive(obj, key, val) {
    let dep = new Dep(); 
    console.log(2,"创建发布者:",dep)
    Object.defineProperty(obj, key, {
        enumerable: true,  
        configurable: true,  
        // 使用getter/setter 来实现数据劫持  
        get: _=> {
            // 注册订阅者,如果发布者存在
            if (Dep.target) {
                dep.addSub(Dep.target);
            }
            return val   // 返回属性值
        },
        set: (newVal) => {
            if (newVal === val) {
                return
            } 
            val = newVal;
            console.log(5,"更新监听的数据",key,newVal)
            // 通知相关的订阅者进行更新
            dep.notify(); 
        }
    });
}

nodeToFragment 函数用于将DOM节点转换为虚拟DOM(DocumentFragment)。它遍历DOM节点的子节点,并调用compile函数来解析和编译节点。

function nodeToFragment(node, vm) {
    var flag = document.createDocumentFragment();
    var child;
    console.log(3,"编译虚拟 Dom 节点")
    while (child = node.firstChild) {
        compile(child, vm);
        flag.appendChild(child);
    }
    // 返回虚拟 Dom
    return flag;
}

compile 函数用于编译DOM节点。对于元素节点,它解析其属性,并处理带有v-model属性的输入节点,实现双向数据绑定。对于文本节点,它解析其中的双括号表达式({{...}}),并创建一个订阅者(Watcher)来监听相关的数据变化。

addEventListener 用于挂载 input 监听事件,当数据发生变化时,会触发 VM 中的 set 方法的数据劫持,从而调用 dep.notify() 方法,实现对所有订阅的通知

function compile(node, vm) {
    // 节点类型为元素
    if (node.nodeType === 1) {
        // 获取所有的属性
        var attr = node.attributes;
        // 解析属性
        for (var i = 0; i < attr.length; i++) {
            if (attr[i].nodeName == 'v-model') {
                var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                // 添加 input 事件
                node.addEventListener('input', function (e) {
                    // 给相应的 data 属性赋值,进而触发该属性的set方法
                    vm[name] = e.target.value;
                    debugger
                });
                node.value = vm[name]; // 将data的值赋给该node
                node.removeAttribute('v-model'); 
                 // 构造订阅者
                new Watcher(vm, node, name, 'input');
            }
        }
       
    }
    let reg = /\{\{(.*)\}\}/;
    // 节点类型为 text
    if (node.nodeType === 3) {
        if (reg.test(node.nodeValue)) {
            var name = RegExp.$1; // 获取匹配到的字符串
            name = name.trim();
            // 构造订阅者
            new Watcher(vm, node, name, 'text');
        }
    }
}

Watcher对象表示一个订阅者。在构造函数中,它将自身赋值给Dep.target,然后通过调用update方法来获取数据并更新DOM节点的值。update方法根据节点类型(文本或输入)更新节点的nodeValue或value属性。在第一次获取值的时候会进行订阅者注册

function Watcher(vm, node, name, nodeType) {
    //  this为watcher函数
    Dep.target = this;
    //  console.log(this);
    this.name = name;
    this.node = node;
    this.vm = vm;
    this.nodeType = nodeType;
    this.update();
    Dep.target = null;
}
Watcher.prototype = {
    update () {
        this.get();
        //  如果是文本,直接更新 `nodeValue`
        if (this.nodeType == 'text') {
            this.node.nodeValue = this.value;
        }
        // 如果是输入标签,更新 value 的值
        if (this.nodeType == 'input') {
            this.node.value = this.value;
        }
        console.log(6.2,"通知 Dom",this.nodeType, "数据为",this.value)
    },
    // 获取 data 中的属性值
    get () {
        // 触发相应属性的 get,这里会进行订阅者注册
        this.value = this.vm[this.name]; 
        console.log(6.1,"获取",this.nodeType,"数据最新的值:",this.value)
    }
}

Dep对象表示一个发布者,用于管理订阅者(Watchers)。它有一个subs数组用于存储订阅者,在addSub方法中添加订阅者,而在notify方法中通知所有订阅者进行更新。

function Dep() {
    this.subs = []
}
Dep.prototype = {
    addSub (sub) {
        this.subs.push(sub);
        console.log(4, "suds:", this.subs.length, "注册订阅者:", sub)
    },
    notify () {
        console.log(6, "通知订阅者:", this.subs)
        this.subs.forEach((sub) => {
            sub.update();
        });
    }
};

Vue对象是自定义的框架的入口点。它接收一个选项对象,其中包含要挂载的元素的选择器和双向绑定的数据对象。在构造函数中,它调用observe函数进行数据劫持,然后调用nodeToFragment函数将DOM节点转换为虚拟DOM,并将其挂载到指定的元素上。

function Vue(options) {
     this.data = options.data;
     let data = this.data;
     // 构造数据劫持
     observe(data, this);
  
     let id = options.el;
     let dom = nodeToFragment(document.getElementById(id), this);
     // 编译完成后,将dom返回到app中进行挂载
     document.getElementById(id).appendChild(dom);
 }

 /*
  *@Time    :   2024/05/03 11:09:06
  *@Desc    :   定义 Vue 对象,传递需要挂载的元素,双向绑定的数据对象
  */
 let vm = new Vue({
     el: 'app',
     data: {
         text: 'hello world'
     }
 });

ES6的Proxy 数据劫持 Demo

在 Vue.js 3.0 开始,使用了ES6的Proxy来实现数据劫持。下面的 Demo 演示了如何使用Proxy来进行数据劫持

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>双向数据绑定Demo</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="text">
        <br />
        {{ text }}
    </div>
    <script>
        /*
         *@Time    :   2024/05/03 10:59:55
         *@Desc    :   数据劫持 或者叫数据监听
         */
        function observeProxy(obj) {
            // 构造代理对象
            debugger
            let dep = new Dep();
            const vm = new Proxy(obj, {
                get(target, key) {
                    // 注册订阅者
                    if (Dep.target) {
                        dep.addSub(Dep.target);
                    }
                    return target[key];
                },
                set(target, key, value) {
                    if (value === target[key]) {
                        return true;
                    }
                    target[key] = value;
                    // 通知相关的订阅者进行更新
                    dep.notify();
                    return true;
                }
            });
            console.log(1, "构造代理对象", vm)
            return vm
        }
        /*
         *@Time    :   2024/05/03 10:48:55
         *@Desc    :   简化的虚拟DOM编译和更新的示例
         */
        function nodeToFragment(node, vm) {
            var flag = document.createDocumentFragment();
            var child;
            console.log(3, "编译虚拟 Dom 节点")
            while (child = node.firstChild) {
                compile(child, vm);
                flag.appendChild(child);
            }
            // 返回虚拟 Dom
            return flag;
        }

        /*
         *@Time    :   2024/05/03 10:52:42
         *@Desc    :   编译DOM节点
         */
        function compile(node, vm) {
            // 节点类型为元素
            if (node.nodeType === 1) {
                // 获取所有的属性
                var attr = node.attributes;
                // 解析属性
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == 'v-model') {
                        var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                        // 添加 input 事件
                        node.addEventListener('input', function (e) {
                            // 给相应的 data 属性赋值,进而触发该属性的set方法
                            vm.vm[name] = e.target.value;
                        });
                        debugger
                        // 将data的值赋给该node,这里触发 getter 方法
                        node.value = vm.vm[name];
                        debugger
                        node.removeAttribute('v-model');
                        // 构造订阅者
                        new Watcher(vm, node, name, 'input');
                    }
                }

            }
            let reg = /\{\{(.*)\}\}/;
            // 节点类型为 text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    // 构造订阅者
                    new Watcher(vm, node, name, 'text');
                }
            }
        }
        /*
         *@Time    :   2024/05/03 11:53:27
         *@Desc    :   订阅者
         */
        function Watcher(vm, node, name, nodeType) {
            //  this为watcher函数

            Dep.target = this;
            console.log(this);
            this.name = name;
            this.node = node;
            this.vm = vm.vm;
            this.nodeType = nodeType;
            this.update();
            Dep.target = null;
        }
        Watcher.prototype = {
            update () {
                this.get();
                //  如果是文本,直接更新 `nodeValue`
                if (this.nodeType == 'text') {
                    debugger
                    this.node.nodeValue = this.value;
                }
                // 如果是输入标签,更新 value 的值
                if (this.nodeType == 'input') {
                    debugger
                    this.node.value = this.value;
                }
                console.log(6.2, "通知 Dom", this.nodeType, "数据为", this.value)
            },
            // 获取 data 中的属性值
            get () {
                // 触发相应属性的 get
                this.value = this.vm[this.name];
                console.log(6.1, "获取", this.nodeType, "数据最新的值:", this.value)
            }
        }
        /*
         *@Time    :   2024/05/03 11:48:38
         *@Desc    :   发布者
         */
        function Dep() {
            this.subs = []
        }
        Dep.prototype = {
            addSub (sub) {
                this.subs.push(sub);
                console.log(4, "suds:", this.subs.length, "注册订阅者:", sub)
            },
            notify () {
                console.log(6, "通知订阅者:", this.subs)
                this.subs.forEach((sub) => {
                    sub.update();
                });
            }
        };

        /*
         *@Time    :   2024/05/03 11:07:01
         *@Desc    :   
         */

        function Vue(options) {
            this.data = options.data;
            let data = this.data;
            // 构造数据劫持
            this.vm = observeProxy(data);

            let id = options.el;
            let dom = nodeToFragment(document.getElementById(id), this);
            // 编译完成后,将dom返回到app中进行挂载
            document.getElementById(id).appendChild(dom);
        }
        /*
         *@Time    :   2024/05/03 11:09:06
         *@Desc    :   定义 Vue 对象,传递需要挂载的元素,双向绑定的数据对象
         */
        let vm = new Vue({
            el: 'app',
            data: {
                text: 'hello world'
            }
        });
    </script>
</body>
</html>

和最上面的 Demo 相比较,observeProxy 方法没有直接修改 VM 对象,Proxy 本身并没有提供一种方法来修改对象属性,所以这里返回一个代理对象Proxy 给了 VM 的 vm 属性,把 需要劫持的数据嵌套了一层放到了 VM 对象。

function Vue(options) {
    this.data = options.data;
    let data = this.data;
    // 构造数据劫持
    this.vm = observeProxy(data);
    let id = options.el;
    let dom = nodeToFragment(document.getElementById(id), this);
    // 编译完成后,将dom返回到app中进行挂载
    document.getElementById(id).appendChild(dom);
}
function observeProxy(obj) {
    // 构造代理对象
    debugger
    let dep = new Dep();
    const vm = new Proxy(obj, {
        get(target, key) {
            // 注册订阅者
            if (Dep.target) {
                dep.addSub(Dep.target);
            }
            return target[key];
        },
        set(target, key, value) {
            if (value === target[key]) {
                return true;
            }
            target[key] = value;
            // 通知相关的订阅者进行更新
            dep.notify();
            return true;
        }
    });
    console.log(1, "构造代理对象", vm)
    return vm
}

Object.defineProperty && Proxy API 介绍

Object.defineProperty

Object.definePropertyES5引入的一个特性,它允许我们将自定义的逻辑应用于对象的属性访问和修改。它可以定义一个新属性或修改现有属性,并定义属性的行为,例如读取(get)和写入(set)时的操作。

const obj = {};

Object.defineProperty(obj, 'name', {
  get() {
    console.log('读取name属性');
    return this._name;
  },
  set(value) {
    console.log('设置name属性');
    this._name = value;
  }
});

obj.name = 'John'; // 输出:设置name属性
console.log(obj.name); // 输出:读取name属性和John

Proxy API

ProxyES6引入的另一个特性,它提供了对对象的拦截和自定义行为的能力。Proxy可以拦截对象上的各种操作,包括属性的读取、写入、函数调用等。通过Proxy,我们可以对对象的访问和修改进行自定义处理

const obj = {
  name: 'John'
};

const proxy = new Proxy(obj, {
  get(target, key) {
    console.log(`访问属性:${key}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`设置属性:${key} = ${value}`);
    target[key] = value;
    return true;
  }
});

proxy.name = 'Jane'; // 输出:设置属性:name = Jane
console.log(proxy.name); // 输出:访问属性:name 和 Jane

简单比较

用法行为:

  • Object.defineProperty:需要逐个定义每个属性的行为,即显式地指定对象的某个属性需要进行拦截和处理。这需要修改现有的对象定义,使其符合拦截要求。这种操作是显式的,需要直接操作对象本身,并且需要事先知道要拦截的属性。后期的操作还是使用目标对象

  • Proxy:创建代理对象时需要提供一个处理器对象,该处理器对象定义了拦截器方法,用于拦截和处理各种操作。代理对象会完全地代理目标对象,并将所有操作转发给目标对象,因此无需修改目标对象本身。这种操作是隐式的,代理对象会在后台拦截和处理所有操作,而不需要直接操作目标对象。代理对象可以在外部对目标对象进行拦截和处理,而目标对象本身保持不变。后期的操作对象是代理对象,而不是目标对象.

拦截能力:

  • Object.defineProperty:主要用于拦截对象的属性读取和写入操作,也可以通过get和set定义一些自定义逻辑。它只能拦截属性级别的操作,无法拦截其他操作。
  • Proxy:具有更强大的拦截能力,可以拦截对象上的多种操作,包括属性的读取、写入、删除、函数调用等。可以通过代理对象的不同处理器方法来自定义拦截逻辑。

动态属性和删除属性:

  • Object.defineProperty:在对象创建后无法动态添加或删除拦截的属性。
  • Proxy可以动态添加和删除属性,并在拦截器中处理相应的操作。

兼容性:

  • Object.defineProperty:相对来说,较好地支持各种现代浏览器和旧版本浏览器,包括IE9+。
  • Proxy:较新的特性,不被所有旧版本浏览器支持,特别是在IE浏览器中不被支持。如果需要在不支持Proxy的环境中运行,需要使用其他解决方案或使用polyfill进行兼容处理。

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 😃


https://liruilong.blog.csdn.net/article/details/117675985


© 2018-2024 liruilonger@gmail.com, All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1643412.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Web API之BOM

BOM 一、window的常见事件1.窗口加载事件2. 调节窗口大小事件 二、定时器1.setTimeout( , )2.setInterval()3.发送短信例子4.this指向问题 三、js执行机制四、location对象1.常见属性2.例子、3.常见方法 五、navigator对象六、history对象 简介&#xff1a; BOM是浏览器对象模型…

python数据分析中数据可视化简单入门

1.折线图表 首先引入相关包pyecharts&#xff0c;如果没下载可以先下载 pip install pyecharts from pyecharts.charts import Lineline Line() # 添加x轴 line.add_xaxis([呱了个呱,羊村,牟多,蜂地,喵帕斯]) # 添加y轴 line.add_yaxis("GDP",[50,30,40,34,63,22])…

我发现不少培训班的就业辅导老师,简直是面试官的卧底——再论培训班学员的就业方式(java方向)

本人知乎账号同公众号&#xff1a;老胡聊Java&#xff0c;欢迎留言并咨询 我最近在帮一些朋友做java方面的就业辅导&#xff0c;其中有些朋友是经过培训班加持后入行java的。由于我本人做过一些大厂和外企的java技术面试官&#xff0c;我发现其中一些朋友的简历甚至根本没法通过…

基于Spring Boot的学生在线答疑系统设计与实现

基于Spring Boot的学生在线答疑系统设计与实现 开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/idea 系统部分展示 管理员登录界面 教师登陆界面 问题发布信息界面&am…

word中取消分页符或分段符前后的空格

在Word中&#xff0c;有时候&#xff0c;我们添加分页符后&#xff0c;从分页符后面的文字就全部掉到了下一页&#xff0c;那么如何避免呢&#xff1f; 选择word选项--高级&#xff0c;然后下滑到下面&#xff0c;将“取消分页符或分段符前后的空格”选中&#xff0c;如下图所…

Java:Map和Set

一、搜索树 在真正学习Map和Set之前&#xff0c;我们有必要先来了解一种树型结构&#xff1a;二叉搜索树&#xff01; 1、概念 二叉搜索树又被称为【二叉排序树】&#xff0c;顾名思义&#xff0c;这是一颗排好序的树&#xff01;它或者是一颗空树&#xff0c;或者是具有以下性…

未来人类文明的可持续发展

未来人类文明若要实现永远延续,建立一个演化模型确实是一个有远见的想法。演化模型可以帮助我们预测和规划未来,从而更好地应对可能出现的挑战。以下是对这个想法的展开论述: 建立演化模型:首先,我们需要收集当前节点的人类文明数据,包括科技、经济、政治、文化、环境等方…

C语言【文件操作】(1)

文章目录 1.为什么使用文件2.文件是什么&#xff1f;2.1程序文件2.2数据文件 3.二进制文件和文本文件4.文件的打开和关闭4.1流和标准流流标准流 4.2文件指针4.3文件的打开和关闭 结语 1.为什么使用文件 很简单 长久的存储数据 如果没有文件&#xff0c;我们写程序所产生的数据…

【Docker】如何注册Hub账号并上传镜像到Hub仓库

一、创建Hub账户 浏览器访问&#xff1a;hub.docker.com 点击【Sign up】注册账号 输入【邮箱】【用户名】【密码】 ps&#xff1a;用户名要有字母数字&#xff1b;订阅不用勾选 点击【Sign up】注册即可 点击【Sign in】登录账号 输入【邮箱】【密码】 点击【Continue】登录 二…

OceanBase 轻量级数仓关键技术解读

码到三十五 &#xff1a; 个人主页 为了更好地聚合和治理跨域数据&#xff0c;帮助企业用较低的成本快速聚合分析&#xff0c;快速决策&#xff0c;不断的让企业积累的数据产生价值&#xff0c;从全域海量数据抓取&#xff0c;高性能流批处理&#xff0c;元数据血缘治理等等方面…

spring高级篇(八)

本篇对Spring MVC 的执行流程做一个简单总结 MVC执行流程总结 当浏览器发送一个请求&#xff0c;例如http://localhost:8080/hello&#xff0c;请求到达服务器后&#xff0c;一般会进行如下操作&#xff1a; 1、首先会经过DispatcherServlet&#xff0c;默认映射路径为 /&…

WAF防火墙可以给您解决什么问题?哪些情况下使用WAF最适合?

一、什么是WAF&#xff1f; Web应用防护系统&#xff08;也称为&#xff1a;网站应用级入侵防御系统。英文&#xff1a;Web Application Firewall&#xff0c;简称&#xff1a;WAF&#xff09;。利用国际上公认的一种说法&#xff1a;Web应用防火墙是通过执行一系列针对HTTP/H…

Vulnstack(一)

0x00 Preface 网上有很多关于 Vulnstack&#xff08;一&#xff09; 的优质文章&#xff0c;本篇文章仅用于记录笔者自身的学习过程。因能力有限&#xff0c;过程中多多少少存在不完善的地方或是未解决的问题&#xff0c;日后有机会会补充上。 内网渗透基础总结&#xff1a;手…

《从Paxos到Zookeeper》——第四、七章:基本概念及原理

目录 第四章 Zookeeper与Paxos 4.1 Zk是什么 4.1.1 Zk特性 4.1.2 Zk基本概念 4.1.2.1 集群角色(Follower, Leader, Observer) 4.1.2.2 数据模型 4.1.2.3 ZNode(数据节点) 4.1.2.4 Session(会话) 4.1.2.5 ACL&#xff08;Access Control Lists&#xff09; 4.1.2.6 Watcher(事件…

网安笔记(纯兴趣,随缘更新)

对于千锋教育的网安课程的笔记 (一)虚拟机环境搭建 01虚拟机概述 传统运行模式:一台计算机同时只能运行一个操作系统 虚拟机运行架构: 1.寄生架构 &#xff08;实验环境、测试环境&#xff09; • 虚拟机作为应用软件安装在操作系统上 • 可以在此应用软件上安装多个操作系统…

AI终端设备的自动化分级

摘要&#xff1a; AI智体被定义为感知环境、做出决策和采取行动的人工实体。 受SAE&#xff08;汽车工程师学会&#xff09;自动驾驶6个级别的启发&#xff0c;AI智体也根据效用和强度进行分类&#xff0c;分为以下几个级别&#xff1a; L0——无AI&#xff0c;有工具&#xf…

Mac上的数字足迹助手,myTracks一键管理!

myTracks for Mac是一款在macOS系统上运行的强大且易于使用的GPS跟踪软件应用程序。它专为户外探险家、运动爱好者和旅行者设计&#xff0c;可以帮助用户轻松记录和管理GPS轨迹、航点和地理标记照片。 首先&#xff0c;myTracks具有出色的GPS轨迹记录功能。它能够从各种设备&a…

Linux课程机房虚拟机

Linux课程机房虚拟机 机房虚拟机&#xff08;默认不能联网的&#xff09;&#xff1a; 百度网盘&#xff1a;https://pan.baidu.com/s/1WqSvqB3Y7b_D4690CDBlJA?pwdaugc 123网盘&#xff1a;https://www.123pan.com/s/tQ0UVv-LiolA.html提取码:F4xm ‍ 联网使用说明&…

AI智能体|使用扣子Coze创建AI绘画助手

大家好&#xff0c;我是无界生长。 昨天我们分享了《AI智能体&#xff5c;使用扣子Coze创建AI绘画工作流》&#xff0c;今天分享下如何使用Coze&#xff08;扣子&#xff09;创建AI绘画助手&#xff0c;调用之前创建的绘画工作流。学会了的话&#xff0c;欢迎分享转发&#xff…

Qt模型视图代理之QTableView应用的简单介绍

往期回顾 Qt绘图与图形视图之绘制带三角形箭头的窗口的简单介绍-CSDN博客 Qt绘图与图形视图之Graphics View坐标系的简单介绍-CSDN博客 Qt模型视图代理之MVD(模型-视图-代理)概念的简单介绍-CSDN博客 Qt模型视图代理之QTableView应用的简单介绍 一、最终效果 二、设计思路 这里…