手写简单Vue

原理

《手写简单Vue》

在创建Wvue示例的时候,将挂载在实例中的参数通过数据劫持来做一层代理。使得在访问或者赋值时候可以进行更多的操作。通过挂载在参数中的el参数来获取HTML,并且用compile进行分析,将具有特殊含义的,比如{{}}中的内容,@click定义的事件等进行单独的处理。
{{}}中绑定的数据,每一个都用watch监听。也就是在每一个new Wvue()实例中挂载的data里面的变量,在template中每运用一次,就将会被一个watch监听。而每一个变量的watch都将有一个Dep去统一管理。当变量变化之后,Dep会通知所有的watch去执行之前在watch中绑定的回调函数。从而实现修改data中的变量,渲染真实DOM的功能。

目标

分成三个阶段,循序渐进实现{{}}、v-model、v-html、@click功能
《手写简单Vue》

《手写简单Vue》

第一阶段

目录结构

《手写简单Vue》

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
            }
        })
        
    }
}

实际效果

《手写简单Vue》

用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()
        })
    }
}

《手写简单Vue》

本阶段,在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的引用,所以引入顺序很关键。
《手写简单Vue》][10]

重置之前,修改input框,会影响{{}}与v-html中绑定的值
《手写简单Vue》

重置之后

    原文作者:老虎不长牙
    原文地址: https://segmentfault.com/a/1190000019884386
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞