手寫簡單Vue

原理

圖片描述

在創建Wvue示例的時候,將掛載在實例中的參數通過數據劫持來做一層代理。使得在訪問或者賦值時候可以進行更多的操作。通過掛載在參數中的el參數來獲取HTML,並且用compile進行分析,將具有特殊含義的,比如{{}}中的內容,@click定義的事件等進行單獨的處理。
{{}}中綁定的數據,每一個都用watch監聽。也就是在每一個new Wvue()實例中掛載的data裏面的變量,在template中每運用一次,就將會被一個watch監聽。而每一個變量的watch都將有一個Dep去統一管理。當變量變化之後,Dep會通知所有的watch去執行之前在watch中綁定的回調函數。從而實現修改data中的變量,渲染真實DOM的功能。

目標

分成三個階段,循序漸進實現{{}}、v-model、v-html、@click功能
圖片描述

圖片描述

第一階段

目錄結構

圖片描述

index.html

<style>
    #app{
        border: 1px solid red;
        margin: 10px;
        padding: 20px;
    }
</style>
<body>
    <div id="app">
        <input type="text" v-modal="name">
        <div class="outer">
            <span>{{name}}</span>
            <p><span v-html="name"></span></p>
        </div>
        <button @click="reset">重置</button>
    </div>
</body>
<script src="./wvue.js"></script>
<script>
    //  階段一
    const data = {
        el: '#app',
        data: {
            name: '米粒'
        },
        methods: {
            reset() {
                this.name = ''
            }
        },
    }
    const app = new Wvue(data)
</script>

Wvue.js

class Wvue {
    constructor(option) {
        this.$option = option
        this.$data = option.data
        this.$methods = option.methods
        // 數據劫持
        // 監聽數據並且做代理 使得訪問this.name即可訪問到this.$data.name
        this.observer(this.$data)
        // 這一步會觸發name與$data.$name的get方法 所以先回打印出get裏面的內容
        console.log(this.name)
        // 一定時間去修改name的內容
        setTimeout(() => {
            console.log('數據發生變化-----------------------------')
            // 在這一步只會觸發name的set
            this.name = '可愛米粒'
        }, 2000)
    }
    observer(obj) {
        if (!obj || typeof obj !== "object") {
            return;
        }
        console.log('observer')
        Object.keys(obj).forEach(key => {
            this.defineProperty(obj, key, obj[key])
            this.proxyObj(key)
        })
    }
    
    defineProperty(obj, key, val) {
        // 如果是綁定的是對象,則用迭代的方式,繼續監聽對象中的數據
        this.observer(val)     
        
        // Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,
        // 或者修改一個對象的現有屬性, 並返回這個對象。
        Object.defineProperty(obj, key, {
            get() {
                console.log('defineProperty獲取')
                return val
            },
            set(newVal) {
                // 採用閉包的形式,只要Wvue沒有銷燬,則val會一直存在
                console.log('defineProperty更新了', newVal)
                val = newVal
            }
        })
    }
    // 做代理 使得訪問更加簡潔
    proxyObj(key) {
        Object.defineProperty(this, key, {
            get() {
                console.log('proxyObj獲取')
                return this.$data[key]
            },
            set(newVal) {
                console.log('proxyObj更新', newVal)
                this.$data[key] = newVal
            }
        })
        
    }
}

實際效果

圖片描述

用Object.defineProperty給data中的變量都設置get,set屬性。對name的賦值,就會觸發$data.$name的set屬性。根據思路,get屬性中就是收集watch放進Dep進行統一管理的地方。
另外,只要Wvue不銷燬,變量的get,set屬性就不會銷燬。

知識點:
Object.defineProperty
閉包與內存

第二階段

實現watch的創建與收集,修改Wvue.js

Wvue.js

class Wvue {
    constructor(option) {
        this.$option = option
        this.$data = option.data
        this.$methods = option.methods
        this.observer(this.$data)
        // ----------------新增Watcher實例,綁定回調方法,當收到通知,打印數據
        new Watcher(this, 'name', () => {
            console.log('watcher生效')
        })
        console.log(this.name)
        setTimeout(() => {
            console.log('數據發送變化-----------------------------')
            this.name = '可愛米粒'
        }, 2000)
    }
    observer(obj) {
        if (!obj || typeof obj !== "object") {
            return;
        }
        console.log('observer')
        Object.keys(obj).forEach(key => {
            this.defineProperty(obj, key, obj[key])
            this.proxyObj(key)
        })
    }
    defineProperty(obj, key, val) {
        this.observer(val)
        //---------------- 新增爲每一個變量都創建管理watcher的Dep實例
        const dep = new Dep()
        Object.defineProperty(obj, key, {
            get() {
                console.log('defineProperty獲取')
                // 每次訪問name 都會創建一個watcher,並加入到Dep中
                Dep.target !== null && dep.addDep(Dep.target)
                return val
            },
            set(newVal) {
                console.log('defineProperty更新了', newVal)
                val = newVal
                dep.notify()
            }
        })
    }

    proxyObj(key) {
        Object.defineProperty(this, key, {
            get() {
                console.log('proxyObj獲取')
                return this.$data[key]
            },
            set(newVal) {
                console.log('proxyObj更新', newVal)
                this.$data[key] = newVal
            }
        })
        
    }
}
// -----------新增Watcher類 用於根據通知觸發綁定的回調函數
class Watcher {
    constructor(vm, key ,cb) {
        this.$vm = vm
        this.$key = key
        this.$cb = cb
        // 用一個全局變量來指代當前watch
        Dep.target = this
        console.log('Watcher-------')
        // 實際是訪問了this.name,觸發了當前變量的get,
        // 當前變量的get會收集當前Dep.target指向的watcher,即當前watcher
        this.$vm[this.$key]
        Dep.target = null

    }
    update() {
        // 執行
        this.$cb.call(this.$vm, this.$vm[this.$key])
    }
}
// -----------新增Dep類 用於收集watcher
class Dep {
    constructor() {
        this.dep = []
    }
    addDep(dep) {
        console.log('addDep')
        this.dep.push(dep)
    }
    notify() {
        // 通知所有的watcher執行更新
        this.dep.forEach(watcher => {
            watcher.update()
        })
    }
}

圖片描述

本階段,在name的get屬性中,將name所有的watcher用Dep實例收集起來。並在set的過程中,觸發Dep中的notify方法,通知所有的watcher更新。所以我們在構造函數中,手動創建了一個watcher。在this.name="可愛米粒"的賦值操作時,就會調用watcher中的callback,打印出數據。
然而我們的watcher不可能是手動創建的,我們平時用Vue的時候,template中{{}}中的內容,就是響應式的,所以當我們改變data中的數據的時候,界面就會重新更改。所以,很明顯,每一個{{}}就需要一個watcher(在Vue1.0中,就是因爲watcher太多了,導致渲染效果差,在vue2.0之後,都改爲一個組件一個watcher)。於是在下一階段,分析html的時候, 就需要加上watcher。

第三階段

新增compile.js

class Compile {
    constructor(el, vm) {
        this.$vm = vm
        // $el掛載的就是需要處理的DOM
        this.$el = document.querySelector(el)
        // 將真實的DOM元素拷貝一份作爲文檔片段,之後進行分析
        const fragment = this.node2Fragment(this.$el)
        // 解析文檔片段
        this.compileNode(fragment)
        // 將文檔片段加入到真實的DOM中去
        this.$el.appendChild(fragment)
    }
    // https://developer.mozilla.org/zh-CN/search?q=querySelector
    // https://developer.mozilla.org/zh-CN/docs/Web/API/Node node對象
    node2Fragment(el) {
        // 創建空白文檔片段
        const fragment = document.createDocumentFragment()
        let child
        //  appendChild會把原來的child給移動到新的文檔中,當el.firstChild爲空時,
        // while也會結束 a = undefined  => 返回 undefined
        while((child = el.firstChild)) {
            fragment.appendChild(child);
        }
        return fragment
    }
    // 通過迭代循環來找出{{}}中的內容,v-xxx與@xxx的內容,並且單獨處理
    compileNode(node) {
        const nodes = node.childNodes
        // 類數組的循環
        Array.from(nodes).forEach(node => {
            if (this.isElement(node)) {
                this.compileElement(node)
            } else if (this.isInterpolation(node)) {
                this.compileText(node)
            }
            node.childNodes.length > 0 && this.compileNode(node)
        });
    }
    // https://developer.mozilla.org/zh-CN/docs/Web/API/Node  Node.nodeType
    isElement(node) {
        return node.nodeType === 1;
    } 
    // 校驗是否是文本節點 並且是大括號中的內容
    isInterpolation(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }
    compileText(node) {
        const reg = /\{\{(.*?)\}\}/g
        const string = node.textContent.match(reg)
        // 取出大括號中的內容,並且處理
        // RegExp.$1是RegExp的一個屬性,指的是與正則表達式匹配的第一個 子匹配(以括號爲標誌)字符串
        // 以此類推,RegExp.$2,RegExp.$3,..RegExp.$99總共可以有99個匹配
        this.text(node, RegExp.$1)
    }
    compileElement(node) {
        const nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach(arr => {
            if (arr.name.indexOf('v-') > -1) {
                this[`${arr.name.substring(2)}`](node, arr.value)
            }
            if (arr.name.indexOf('@') > -1) {
                // console.log(node, arr.value)
                this.eventHandle(node, arr.name.substring(1), arr.value)
            }
        })
    }
    // 因爲是大括號裏面的內容,所以沿用之前的邏輯,都加上watcher
    text(node, key) {
        new Watcher(this.$vm, key, () => {
            node.textContent = this.$vm[key]
        })
        // 第一次初始化界面, 不然如果不進行賦值操作,
        // 就不會觸發watcher裏面的回調函數
        node.textContent = this.$vm[key]
    }
    html(node, key) {
        new Watcher(this.$vm, key, () => {
            node.innerHTML = this.$vm[key]
        })
        node.innerHTML = this.$vm[key]
        
    }
    // 對@xxx事件的處理
    eventHandle(node, eventName, methodName) {
        node.addEventListener(eventName, () => {
            this.$vm.$methods[methodName].call(this.$vm)
        })
    }
    // v-modal的處理 不僅僅當賦值的時候回觸發watcher,並且爲input添加事件
    // input中的值去修改this.$data.$xxx的值,實現雙向綁定
    modal(node, key) {
        console.log(node.value)
        new Watcher(this.$vm, key, () => {
            node.value = this.$vm[key]
        })
        node.value = this.$vm[key]
        node.addEventListener('input', (e) => {
            this.$vm[key] = e.target.value
        })
    }
}

Wvue.js 中Wvue的構造函數

    constructor(option) {
        this.$option = option
        this.$data = option.data
        this.$methods = option.methods
        this.observer(this.$data)
        // -------------- 刪除原來的手動調用watcher
        // ---------------新增對HTML的解析與處理
        // ---------------在這個方法中增加watche 還要將當前this指向傳入
        new Compile(option.el, this)
    }

index.html

<!-- 引入順序問題 -->
<script src="./compile.js"></script>
<script src="./wvue.js"></script>

因爲wvue.js中有對compile的引用,所以引入順序很關鍵。
![圖片描述][10]

重置之前,修改input框,會影響{{}}與v-html中綁定的值
圖片描述

重置之後

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章