Vue的MVVM是如何實現的?本文項目詳解原理

来源:https://www.cnblogs.com/chengxuyuanaa/archive/2020/06/10/13089233.html
-Advertisement-
Play Games

相信只要你去面試vue,都會被問到vue的雙向數據綁定,你要是就說個mvvm就是視圖模型模型視圖,只要數據改變視圖也會同時更新!那你離被pass就不遠了! 視頻已錄製,地址(www.bilibili.com/video/BV1qJ…) 幾種實現雙向綁定的做法 目前幾種主流的mvc(vm)框架都實現了 ...


 

相信只要你去面試vue,都會被問到vue的雙向數據綁定,你要是就說個mvvm就是視圖模型模型視圖,只要數據改變視圖也會同時更新!那你離被pass就不遠了!

視頻已錄製,地址(www.bilibili.com/video/BV1qJ…)

幾種實現雙向綁定的做法

目前幾種主流的mvc(vm)框架都實現了單向數據綁定,而我所理解的雙向數據綁定無非就是在單向綁定的基礎上給可輸入元素(input、textare等)添加了change(input)事件,來動態修改model和 view,並沒有多高深。所以無需太過介懷是實現的單向或雙向綁定。

實現數據綁定的做法有大致如下幾種:

發佈者-訂閱者模式(backbone.js)

臟值檢查(angular.js)

數據劫持(vue.js)

發佈者-訂閱者模式: 一般通過sub, pub的方式實現數據和視圖的綁定監聽,更新數據方式通常做法是 vm.set('property', value),這裡有篇文章講的比較詳細,有興趣可點這裡

這種方式現在畢竟太low了,我們更希望通過 vm.property = value這種方式更新數據,同時自動更新視圖,於是有了下麵兩種方式

臟值檢查: angular.js 是通過臟值檢測的方式比對數據是否有變更,來決定是否更新視圖,最簡單的方式就是通過 setInterval() 定時輪詢檢測數據變動,當然Google不會這麼low,angular只有在指定的事件觸發時進入臟值檢測,大致如下:

  • DOM事件,譬如用戶輸入文本,點擊按鈕等。( ng-click )
  • XHR響應事件 ( $http )
  • 瀏覽器Location變更事件 ( $location )
  • Timer事件( timeout ,interval )
  • 執行 digest() 或apply()
    另外要註意除了本項目,,結合多年開發經驗整理出2020最新企業級實戰視頻教程, 包括 Vue3.0/Js/ES6/TS/React/node等,想學的進扣扣裙 519293536 免費獲取,小白勿進哦!,

數據劫持: vue.js 則是採用數據劫持結合發佈者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的settergetter,在數據變動時發佈消息給訂閱者,觸發相應的監聽回調。

MVVM原理

Vue響應式原理最核心的方法便是通過Object.defineProperty()來實現對屬性的劫持,達到監聽數據變動的目的,無疑這個方法是本文中最重要、最基礎的內容之一

整理了一下,要實現mvvm的雙向綁定,就必須要實現以下幾點:

  • 1、實現一個數據監聽器Observer,能夠對數據對象的所有屬性進行監聽,如有變動可拿到最新值並通知訂閱者
  • 2、實現一個指令解析器Compile,對每個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數
  • 3、實現一個Watcher,作為連接Observer和Compile的橋梁,能夠訂閱並收到每個屬性變動的通知,執行指令綁定的相應回調函數,從而更新視圖
  • 4、mvvm入口函數,整合以上三者

 

 

先看之前vue的功能

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <h2>{{obj.name}}--{{obj.age}}</h2>
        <h2>{{obj.age}}</h2>
        <h3 v-text='obj.name'></h3>
        <h4 v-text='msg'></h4>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <h3>{{msg}}</h3>
        <div v-html='htmlStr'></div>
        <div v-html='obj.fav'></div>
        <input type="text" v-model='msg'>
        <img v-bind:src="imgSrc" v-bind:alt="altTitle">
        <button v-on:click='handlerClick'>按鈕1</button>
        <button v-on:click='handlerClick2'>按鈕2</button>
        <button @click='handlerClick2'>按鈕3</button>
    </div>
    <script src="./vue.js"></script>
    <script>
        let vm = new MVue({
            el: '#app',
            data: {
                obj: {
                    name: '小馬哥',
                    age: 19,
                    fav:'<h4>前端Vue</h4>'
                },
                msg: 'MVVM實現原理',
                htmlStr:"<h3>hello MVVM</h3>",
                imgSrc:'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1568782284688&di=8635d17d550631caabfeb4306b5d76fa&imgtype=0&src=http%3A%2F%2Fh.hiphotos.baidu.com%2Fimage%2Fpic%2Fitem%2Fb3b7d0a20cf431ad7427dfad4136acaf2fdd98a9.jpg',
                altTitle:'眼睛',
                isActive:'true'

            },
            methods: {
                handlerClick() {
                    alert(1);
                    console.log(this);
                    
                },
                handlerClick2(){
                    console.log(this);
                    alert(2)
                }
            }

        })
    </script>
</body>

</html>
複製代碼

實現指令解析器Compile

實現一個指令解析器Compile,對每個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數,添加監聽數據的訂閱者,一旦數據有變動,收到通知,更新視圖,如圖所示:

初始化

新建MVue.js

class MVue {
    constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        //保存 options參數,後面處理數據要用到
        this.$options = options;
        // 如果這個根元素存在則開始編譯模板
        if (this.$el) {
            // 1.實現一個指令解析器compile
            new Compile(this.$el, this)
        }
    }
}
class Compile{
    constructor(el,vm) {
        // 判斷el參數是否是一個元素節點,如果是直接賦值,如果不是 則獲取賦值
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
    }
    isElementNode(node){
        // 判斷是否是元素節點
        return node.nodeType === 1
    }
}
複製代碼

這樣外界可以這樣操作

let vm = new Vue({
    el:'#app'
})
//or
let vm = new Vue({
    el:document.getElementById('app')
})
複製代碼

優化編譯使用文檔碎片

<h2>{{obj.name}}--{{obj.age}}</h2>
<h2>{{obj.age}}</h2>
<h3 v-text='obj.name'></h3>
<h4 v-text='msg'></h4>
<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-html='htmlStr'></div>
<div v-html='obj.fav'></div>
<input type="text" v-model='msg'>
<img v-bind:src="imgSrc" v-bind:alt="altTitle">
<button v-on:click='handlerClick'>按鈕1</button>
<button v-on:click='handlerClick2'>按鈕2</button>
<button @click='handlerClick2'>按鈕3</button>
複製代碼

接下來,找到子元素的值,比如obj.name,obj.age,obj.fav 找到obj 再找到fav,獲取數據中的值替換掉

但是在這裡我們不得不想到一個問題,每次找到一個數據替換,都要重新渲染一遍,可能會造成頁面的迴流和重繪,那麼我們最好的辦法就是把以上的元素放在記憶體中,在記憶體中操作完成之後,再替換掉.

class Compile {
    constructor(el, vm) {
        // 判斷el參數是否是一個元素節點,如果是直接賦值,如果不是 則獲取賦值
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 因為每次匹配到進行替換時,會導致頁面的迴流和重繪,影響頁面的性能
        // 所以需要創建文檔碎片來進行緩存,減少頁面的迴流和重繪
        // 1.獲取文檔碎片對象
        const fragment = this.node2Fragment(this.el);
        // console.log(fragment);
        // 2.編譯模板
        // 3.把子元素的所有內容添加到根元素中
        this.el.appendChild(fragment);

    }
    node2Fragment(el) {
        const fragment = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment
    }
    isElementNode(el) {
        return el.nodeType === 1;
    }
}
複製代碼

這時候會發現頁面跟之前沒有任何變化,但是經過Fragment的處理,優化頁面渲染性能

編譯模板

// 編譯數據的類
class Compile {
    constructor(el, vm) {
        // 判斷el參數是否是一個元素節點,如果是直接賦值,如果不是 則獲取賦值
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 因為每次匹配到進行替換時,會導致頁面的迴流和重繪,影響頁面的性能
        // 所以需要創建文檔碎片來進行緩存,減少頁面的迴流和重繪
        // 1.獲取文檔碎片對象
        const fragment = this.node2Fragment(this.el);
        // console.log(fragment);
        // 2.編譯模板
        this.compile(fragment)

        // 3.把子元素的所有內容添加到根元素中
        this.el.appendChild(fragment);

    }
    compile(fragment) {
        // 1.獲取子節點
        const childNodes = fragment.childNodes;
        // 2.遍歷子節點
        [...childNodes].forEach(child => {

            // 3.對子節點的類型進行不同的處理
            if (this.isElementNode(child)) {
                // 是元素節點
                // 編譯元素節點
                // console.log('我是元素節點',child);
                this.compileElement(child);
            } else {
                // console.log('我是文本節點',child);
                this.compileText(child);
                // 剩下的就是文本節點
                // 編譯文本節點
            }
            // 4.一定要記得,遞歸遍歷子元素
            if (child.childNodes && child.childNodes.length) {
                this.compile(child);
            }
        })
    }
    // 編譯文本的方法
    compileText(node) {
        console.log('編譯文本')

    }
    node2Fragment(el) {
        const fragment = document.createDocumentFragment();
        // console.log(el.firstChild);
        let firstChild;
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment
    }
    isElementNode(el) {
        return el.nodeType === 1;
    }
}
複製代碼

接下來根據不同子元素的類型進行渲染

編譯元素

compileElement(node) {
    // 獲取該節點的所有屬性
    const attributes = node.attributes;
    // 對屬性進行遍歷
    [...attributes].forEach(attr => {
        const { name, value } = attr; //v-text v-model   v-on:click  @click 
        // 看當前name是否是一個指令
        if (this.isDirective(name)) {
            //對v-text進行操作
            const [, directive] = name.split('-'); //text model html
            // v-bind:src
            const [dirName, eventName] = directive.split(':'); //對v-on:click 進行處理
            // 更新數據
            compileUtil[dirName] && compileUtil[dirName](node, value, this.vm, eventName);
            // 移除當前元素中的屬性
            node.removeAttribute('v-' + directive);

        }else if(this.isEventName(name)){
            // 對事件進行處理 在這裡處理的是@click
            let [,eventName] =  name.split('@');
            compileUtil['on'](node, value, this.vm, eventName)
        }

    })

}
// 是否是@click這樣事件名字
isEventName(attrName){
    return attrName.startsWith('@')
}
//判斷是否是一個指令
isDirective(attrName) {
    return attrName.startsWith('v-')
}
複製代碼

編譯文本

// 編譯文本的方法
compileText(node) {
    const content = node.textContent;
    // 匹配{{xxx}}的內容
    if (/\{\{(.+?)\}\}/.test(content)) {
        // 處理文本節點
        compileUtil['text'](node, content, this.vm)
    }

}
複製代碼

大家也會發現,compileUtil這個對象它是什麼鬼?真正的編譯操作我將它放入到這個對象中,根據不同的指令來做不同的處理.比如v-text是處理文本的 v-html是處理html元素 v-model是處理表單數據的.....

這樣我們在當前對象compileUtil中通過updater函數來初始化視圖

處理元素/處理文本/處理事件....

const compileUtil = {
    // 獲取值的方法
    getVal(expr, vm) {
        return expr.split('.').reduce((data, currentVal) => {
            return data[currentVal]
        }, vm.$data)
    },
    getAttrs(expr,vm){

    },
    text(node, expr, vm) { //expr 可能是 {{obj.name}}--{{obj.age}} 
        let val;
        if (expr.indexOf('{{') !== -1) {
            // 
            val = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                return this.getVal(args[1], vm);
            })
        }else{ //也可能是v-text='obj.name' v-text='msg'
            val = this.getVal(expr,vm);
        }
        this.updater.textUpdater(node, val);
    },
    html(node, expr, vm) {
        // html處理 非常簡單 直接取值 然後調用更新函數即可
        let val = this.getVal(expr,vm);
        this.updater.htmlUpdater(node,val);
    },
    model(node, expr, vm) {
        const val = this.getVal(expr,vm);
        this.updater.modelUpdater(node,val);
    },
    // 對事件進行處理
    on(node, expr, vm, eventName) {
        // 獲取事件函數
        let fn = vm.$options.methods && vm.$options.methods[expr];
        // 添加事件 因為我們使用vue時 都不需要關心this的指向問題,這是因為源碼的內部幫咱們處理了this的指向
        node.addEventListener(eventName,fn.bind(vm),false);
    },
    // 綁定屬性 簡單的屬性 已經處理 類名樣式的綁定有點複雜 因為對應的值可能是對象 也可能是數組 大家根據個人能力嘗試寫一下
    bind(node,expr,vm,attrName){
        let attrVal = this.getVal(expr,vm);
        this.updater.attrUpdater(node,attrName,attrVal);
    },
    updater: {
        attrUpdater(node, attrName, attrVal){
            node.setAttribute(attrName,attrVal);
        },
        modelUpdater(node,value){
            node.value = value;
        },
        textUpdater(node, value) {

            node.textContent = value;

        },
        htmlUpdater(node,value){
            node.innerHTML = value;
        }
    }

}
複製代碼

通過以上操作:我們實現了一個編譯器compile,用它來解析指令,通過updater初始化視圖

實現一個數據監聽器Observer

ok, 思路已經整理完畢,也已經比較明確相關邏輯和模塊功能了,let's do it 我們知道可以利用Obeject.defineProperty()來監聽屬性變動 那麼將需要observe的數據對象進行遞歸遍歷,包括子屬性對象的屬性,都加上 settergetter 這樣的話,給這個對象的某個值賦值,就會觸發setter,那麼就能監聽到了數據變化。。相關代碼可以是這樣:

//test.js
let data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // 哈哈哈,監聽到值變化了 kindeng --> dmq

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // 取出所有屬性遍歷
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};

function defineReactive(data, key, val) {
    observe(val); // 監聽子屬性
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚舉
        configurable: false, // 不能再define
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('哈哈哈,監聽到值變化了 ', val, ' --> ', newVal);
            val = newVal;
        }
    });
}
複製代碼

 

 

再看這張圖,我們接下來實現的是一個數據監聽器Observer,能夠對數據對象的所有屬性進行監聽,如有變動可拿到最新值通知依賴收集對象(Dep)並通知訂閱者(Watcher)來更新視圖

// 創建一個數據監聽者  劫持並監聽所有數據的變化
class Observer{
    constructor(data) {
        this.observe(data);
    }
    observe(data){
        // 如果當前data是一個對象才劫持並監聽
        if(data && typeof data === 'object'){
            // 遍歷對象的屬性做監聽
            Object.keys(data).forEach(key=>{
                this.defineReactive(data,key,data[key]);
            })
            
        }
    }
    defineReactive(obj,key,value){
        // 迴圈遞歸 對所有層的數據進行觀察
        this.observe(value);//這樣obj也能被觀察了
        Object.defineProperty(obj,key,{
            get(){
                return value;
            },
            set:(newVal)=>{
                if (newVal !== value){
                    // 如果外界直接修改對象 則對新修改的值重新觀察
                    this.observe(newVal);
                    value = newVal;
                    // 通知變化
                    dep.notify();
                }
            }
        })
    }
}
複製代碼

這樣我們已經可以監聽每個數據的變化了,那麼監聽到變化之後就是怎麼通知訂閱者了,所以接下來我們需要實現一個消息訂閱器,很簡單,維護一個數組,用來收集訂閱者,數據變動觸發notify,再調用訂閱者的update方法,代碼改善之後是這樣:

創建Dep

  • 添加訂閱者
  • 定義通知的方法
class Dep{
    constructor() {
        this.subs = []
    }
    // 添加訂閱者
    addSub(watcher){
        this.subs.push(watcher);
 
    }
    // 通知變化
    notify(){
        // 觀察者中有個update方法 來更新視圖
        this.subs.forEach(w=>w.update());
    }
}
複製代碼

雖然我們已經創建了Observer,Dep(訂閱器),那麼問題來了,誰是訂閱者?怎麼往訂閱器添加訂閱者?

沒錯,上面的思路整理中我們已經明確訂閱者應該是Watcher, 而且const dep = new Dep();是在 defineReactive方法內部定義的,所以想通過dep添加訂閱者,就必須要在閉包內操作,所以我們可以在 getOldVal裡面動手腳:

實現一個Watcher

它作為連接Observer和Compile的橋梁,能夠訂閱並收到每個屬性變動的通知,執行指令綁定的相應回調函數,從而更新視圖

只要所做事情:

1、在自身實例化時往屬性訂閱器(dep)裡面添加自己

2、自身必須有一個update()方法

3、待屬性變動dep.notify()通知時,能調用自身的update()方法,並觸發Compile中綁定的回調,則功成身退。

//Watcher.js
class Watcher{
    constructor(vm,expr,cb) {
        // 觀察新值和舊值的變化,如果有變化 更新視圖
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 先把舊值存起來  
        this.oldVal = this.getOldVal();
    }
    getOldVal(){
        Dep.target = this;
        let oldVal = compileUtil.getVal(this.expr,this.vm);
        Dep.target = null;
        return oldVal;
    }
    update(){
        // 更新操作 數據變化後 Dep會發生通知 告訴觀察者更新視圖
        let newVal = compileUtil.getVal(this.expr, this.vm);
        if(newVal !== this.oldVal){
            this.cb(newVal);
        }
    }
}

//Observer.js
defineReactive(obj,key,value){
    // 迴圈遞歸 對所有層的數據進行觀察
    this.observe(value);//這樣obj也能被觀察了
    const dep = new Dep();
    Object.defineProperty(obj,key,{
        get(){
            //訂閱數據變化,往Dep中添加觀察者
            Dep.target && dep.addSub(Dep.target);
            return value;
        },
        //....省略
    })
}
複製代碼

當我們修改某個數據時,數據已經發生了變化,但是視圖沒有更新

 

 

我們在什麼時候來添加綁定watcher呢,繼續看圖

 

 

也就是說,當我們訂閱數據變化時,來綁定更新函數,從而讓watcher去更新視圖

修改

// 編譯模板工具類
const compileUtil = {
    // 獲取值的方法
    getVal(expr, vm) {
        return expr.split('.').reduce((data, currentVal) => {
            return data[currentVal]
        }, vm.$data)
    },
    //設置值
    setVal(vm,expr,val){
        return expr.split('.').reduce((data, currentVal, index, arr) => {
            return data[currentVal] = val
        }, vm.$data)
    },
    //獲取新值 對{{a}}--{{b}} 這種格式進行處理
    getContentVal(expr, vm) {
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getVal(args[1], vm);
        })
    },
    text(node, expr, vm) { //expr 可能是 {{obj.name}}--{{obj.age}} 
        let val;
        if (expr.indexOf('{{') !== -1) {
            // 
            val = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                //綁定watcher從而更新視圖
                new Watcher(vm,args[1],()=>{           
                    this.updater.textUpdater(node,this.getContentVal(expr, vm));
                })
                return this.getVal(args[1], vm);
            })
        }else{ //也可能是v-text='obj.name' v-text='msg'
            val = this.getVal(expr,vm);
        }
        this.updater.textUpdater(node, val);

    },
    html(node, expr, vm) {
        // html處理 非常簡單 直接取值 然後調用更新函數即可
        let val = this.getVal(expr,vm);
        // 訂閱數據變化時 綁定watcher,從而更新函數
        new Watcher(vm,expr,(newVal)=>{
            this.updater.htmlUpdater(node, newVal);
        })
        this.updater.htmlUpdater(node,val);
    },
    model(node, expr, vm) {
        const val = this.getVal(expr,vm);
        // 訂閱數據變化時 綁定更新函數 更新視圖的變化

        // 數據==>視圖
        new Watcher(vm, expr, (newVal) => {
            this.updater.modelUpdater(node, newVal);
        })
        // 視圖==>數據
        node.addEventListener('input',(e)=>{
            // 設置值
            this.setVal(vm,expr,e.target.value);

        },false);
        this.updater.modelUpdater(node,val);
    },
    // 對事件進行處理
    on(node, expr, vm, eventName) {
        // 獲取事件函數
        let fn = vm.$options.methods && vm.$options.methods[expr];
        // 添加事件 因為我們使用vue時 都不需要關心this的指向問題,這是因為源碼的內部幫咱們處理了this的指向
        node.addEventListener(eventName,fn.bind(vm),false);
    },
    // 綁定屬性 簡單的屬性 已經處理 類名樣式的綁定有點複雜 因為對應的值可能是對象 也可能是數組 大家根據個人能力嘗試寫一下
    bind(node,expr,vm,attrName){
        let attrVal = this.getVal(expr,vm);
        this.updater.attrUpdater(node,attrName,attrVal);
    },
    updater: {
        attrUpdater(node, attrName, attrVal){
            node.setAttribute(attrName,attrVal);
        },
        modelUpdater(node,value){
            node.value = value;
        },
        textUpdater(node, value) {
            node.textContent = value;
        },
        htmlUpdater(node,value){
            node.innerHTML = value;
        }
    }

}
複製代碼

代理proxy

我們在使用vue的時候,通常可以直接vm.msg來獲取數據,這是因為vue源碼內部做了一層代理.也就是說把數據獲取操作vm上的取值操作 都代理到vm.$data上

class Vue {
    constructor(options) {
        this.$data = options.data;
        this.$el = options.el;
        this.$options = options
        // 如果這個根元素存在開始編譯模板
        if (this.$el) {
            // 1.實現一個數據監聽器Observe
            // 能夠對數據對象的所有屬性進行監聽,如有變動可拿到最新值並通知訂閱者
            // Object.definerProperty()來定義
            new Observer(this.$data);

            // 把數據獲取操作 vm上的取值操作 都代理到vm.$data上
            this.proxyData(this.$data);
            
            // 2.實現一個指令解析器Compile
            new Compiler(this.$el, this);

        }
    }
    // 做個代理
    proxyData(data){
       for (const key in data) {
          Object.defineProperty(this,key,{
              get(){
                  return data[key];
              },
              set(newVal){
                  data[key] = newVal;
              }
          })
       }
    }
}
複製代碼

面試題

闡述一下你所理解vue的MVVM響應式原理

vue.js 則是採用數據劫持結合發佈者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的settergetter,在數據變動時發佈消息給訂閱者,觸發相應的監聽回調。

MVVM作為數據綁定的入口,整合Observer、Compile和Watcher三者,通過Observer來監聽自己的model數據變化,通過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁,達到數據變化 -> 視圖更新;視圖交互變化(input) -> 數據model變更的雙向綁定效果

再配合上面的那張圖,想不入職都很難

 

 

最後註意:除了本項目,,結合多年開發經驗整理出2020最新企業級實戰視頻教程, 包括 Vue3.0/Js/ES6/TS/React/node等,想學的可進扣扣裙 519293536 免費獲取,小白勿進哦!,

本文的文字及圖片來源於網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 安裝centos可以在網上找安裝教程 我使用的版本: 虛擬機工具:Workstation 12 Pro 12.1.0 build-3272444 鏡像:CentOS-7-x86_64-Minimal-1611.iso 這裡主要將網路設置方面的問題做個記錄。 vmware軟體 的 NAT設置網路: v ...
  • 1.)說明. 本項目是來自github上的一個項目roottools (https://github.com/Stericson/RootTools ),這裡只是想本地編譯後輸出下jar包供自己進行使用。 2.)操作步驟. 步驟1)按之前你熟悉的方式進行開發待輸出為jar的項目. 步驟2) 一般的g ...
  • 根據本人工作遇到的一些情況,總結如下: 1.)安裝包的最低版本要求高於待安裝的手機的android版本。2.)目標手機上已經存在相同包名的app,且簽名不一樣.3.)android studio下打包輸出app時, 只選擇了v2, 並且遇到的手機是低版本的。會出現安裝失敗.4.)目標手機可用記憶體太低 ...
  • .android文件夾主要是用來存放模擬器的,是占用空間最大的一個,如果你沒有使用它的模擬器,可以直接把這個文件夾刪除。因為我建立了兩個x86的模擬器,吃掉了我20G的空間。果斷搬走。.gradle這個目錄主要是項目編譯時所用的,更改比較簡單,AndroidStudio 有給我們留下介面。.Andr... ...
  • 插件 flutter-carousel-slider A flutter carousel widget, support infinite scroll, and custom child widget. code-builder A fluent API for generating valid ...
  • Js--字元串拼接/連接 博客說明 文章所涉及的資料來自互聯網整理和個人總結,意在於個人學習和經驗彙總,如有什麼地方侵權,請聯繫本人刪除,謝謝! 說明 在開發的過程中很容易遇到字元串連接的問題,下麵列舉三種方法 使用加號運算符 連接字元串最簡便的方法是使用加號運算符。 使用加號運算符連接兩個字元串 ...
  • 本文只是複習HTML筆記 html 骨架: DTD:文檔類型定義,規定了使用哪個版本的html規範 html 標簽:雙標簽,表示整個網頁 head 標簽: 配置HTML頁面 title: 網頁標題 meta :設置字元集 charset body : 書寫標簽組成的網頁 語法: 標簽之間對空格,縮進 ...
  • 前言 就在前段時間,vue官方發佈了3.0.0-beta.1 版本,趁著五一假期有時間,就把之前的一個電商商城的項目,用最新的Composition API拿來改造一下! 👉GitHub地址請訪問🔗:github.com/GitHubGanKa… 項目介紹 vue-jd-h5是一個電商H5頁面前 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...