手撕Vue源码全过程(下)

手撕Vue源码全过程(上)
手撕Vue源码全过程(中)—数据驱动界面更新

1.界面驱动数据更新

只有v-model才可以驱动数据更新,v-model只能添加给input,所以只要监听input输入事件,在事件中拿到最新的值,再添加给模型就可以实现界面驱动数据更新.

let CompilerUtil = {
    getValue(vm, value) {
        //time.h-->[time,h],利用reduce遍历逐层取
        //reduce接收的第一个参数为上一次调用回调返回的值,或者是提供的初始值
        //  第二个参数为数组中当前被处理的元素
        // vm.$data作为data第一次调用初始值
        return value.split('.').reduce((data, currentKey) => {
            //第一次执行:data=$data,currentKey=time
            // 第二次执行:data=time,currentKey=h
            return data[currentKey.trim()]
        }, vm.$data)
    },
    getContent(vm, value) {
        let reg = /\{\{(.+?)\}\}/gi
        let val = value.replace(reg, (...args) => {
            return this.getValue(vm, args[1])
        })
        return val
    },
    // ==============================更新======================
    setValue(vm, value, newValue) {
        // console.log(vm, value, newValue);
        // 遍历属性,如果是对象嵌套对象,获取最后一位,赋值
        // vm.$data设定为初始值
        value.split('.').reduce((data, currentKey, index, arr) => {
            // console.log(data, currentKey, index, arr);//$data,属性名,索引,数组
            if (index == arr.length - 1) {
                data[currentKey] = newValue
                console.log(data[currentKey]);
            }
            return data[currentKey]
        }, vm.$data)
    },
    model: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.value = newValue
        })
        // console.log(node, value, vm);
        // v-model作用在input上
        // 根据被替换内容,获取对应的数据
        // node.value=vm.$data[value];
        //遇到复杂类型情况下vm.$data[time.h]-->vm.$data[time]-->time[h]
        let val = this.getValue(vm, value)
        node.value = val
        // ===================更新=============================
        // 监听input事件拿到输入框内容
        node.addEventListener('input', (e) => {
            // 获取到最新的值赋值到模型当中,由于这里value的属性有可能是name,也有可能是time.h
            // 所以创建独立的方法来处理
            let newValue = e.target.value
            // vm:vue实例,value:属性,newValue为新值
            this.setValue(vm, value, newValue)
        })
    },
    html: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.innerHTML = newValue
        })
        let val = this.getValue(vm, value)
        node.innerHTML = val
    },
    text: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.innerText = newValue
        })
        let val = this.getValue(vm, value)
        node.innerText = val
    },
    content: function (node, value, vm) {
        // console.log(value);//{{name}}-->取出name-->将$data[name]的数据赋值
        // let val = this.getContent(vm, value)
        let reg = /\{\{(.+?)\}\}/gi
        let val = value.replace(reg, (...args) => {
            new Warcher(vm, args[1], (newValue, oldValue) => {
                node.textContent = this.getContent(vm, value)
            })
            return this.getValue(vm, args[1])
        })
        node.textContent = val
    }
}
// 模拟创建vue实例(类)
class Vue {
    // 构造器,接收一个参数,参数为对象
    constructor(value) {
        // 判断el是否为一个节点
        if (this.isElement(value.el)) {
            this.$el = value.el
        } else {
            // 如果没有则根据传入的找
            this.$el = document.querySelector(value.el)
        }
        this.$data = value.data
        // 根据传入的位置和数据渲染
        // 先判断是否el是否存在,否则不渲染
        if (this.$el) {
            // 1.给外界传入的所有数据都添加get/set方法,这样就可以监听数据变化了
            // 监听数据变化
            new Observer(this.$data)
            // 传入该实例
            new Compier(this)
        }
    }
    // 判断是否为元素节点,元素节点为1,属性节点为2
    isElement(node) {
        // console.log(node.nodeType === 1);//false
        return node.nodeType === 1
    }

}
class Compier {
    constructor(vm) {
        // 保存vue实例
        this.vm = vm
        // 1.将页面元素提取到碎片文档
        let fragment = this.node2fragment(this.vm.$el)
        // 2.利用指定的数据编译内存中的元素
        this.buildTemplate(fragment)
        // 3.将编译好的内存重新渲染到网页上
        this.vm.$el.appendChild(fragment)
    }
    node2fragment(app) {
        // 1.创建空的文档
        let fragment = document.createDocumentFragment()
        //2.遍历循环去到每一个元素
        let node = app.firstChild
        while (node) {
            // 判断是否还有元素
            // 注意点:只要元素添加到文档碎片对象中,name这个元素就会自动从网页上消失
            fragment.appendChild(node)
            node = app.firstChild
        }
        //3.返回储存了所有元素的文档碎片对象
        return fragment
    }
    buildTemplate(fragment) {
        // 从元素获取所有节点,伪数组转为数组
        let nodeList = [...fragment.childNodes]
        // console.log(nodeList);//[text, p, text, input, text]
        // 循环判断当前的节点是一个元素还是一个文本
        nodeList.forEach(node => {
            if (this.vm.isElement(node)) {
                //是一个元素
                this.buildElement(node)
                //处理子元素,递归
                this.buildTemplate(node)
            } else {
                //不是一个元素
                this.buildText(node)
            }
        })
    }
    // 元素处理
    buildElement(node) {
        // attributes 属性返回指定节点的属性集合
        let attrs = [...node.attributes]
        attrs.forEach(attr => {
            // console.log(attr);//获取到属性
            let { name, value } = attr
            // console.log(name,value);//单独拿到类型与类型的取值
            if (name.startsWith('v-')) {
                // v-开头的有很多,比如v-model/v-html/v-if...
                // startsWith() 方法用于检测字符串是否以指定的子字符串开始。
                // console.log('是v-', name,value);//是v- v-model name
                // 切割并解构,不需要v-
                let [, directive] = name.split('-')//v,model
                // console.log(directive);//model
                // 找到对应的工具类执行对应的方法
                // node:修改的节点,value:修改的值,this.vm新值
                CompilerUtil[directive](node, value, this.vm)
            }
        })
    }
    //文本处理
    buildText(node) {
        // textContent 属性设置或者返回指定节点的文本内容。
        let content = node.textContent
        let reg = /\{\{.+?\}\}/gi
        if (reg.test(content)) {
            // console.log('是{{}}',content);//是{{}} {{name}}
            // 找到对应的工具类执行对应的方法
            // node:修改的节点,content:修改的值,this.vm新值
            CompilerUtil['content'](node, content, this.vm)
        }
    }
}
class Observer {
    // 只要将需要监听的那个对象传递给Observer这个类
    // 这个类就可以快速的给传入的对象的所有属性都添加get/set方法
    // data代表接收的对象
    constructor(data) {
        this.observer(data);
    }
    // 给属性添加get/set;
    observer(obj) {
        // 判断是不是对象
        if (obj && typeof obj === "object") {
            //遍历取出传入对象的所有属性,给遍历到的属性都增加get/set方法
            for (let key in obj) {
                // 参数为:对象,属性,值
                this.defineRecative(obj, key, obj[key]);
            }
        }
    }
    // obj:需要操作的对象
    // attr:需要新增get/set方法的属性
    // value:需要新增get/set方法属性的取值
    defineRecative(obj, attr, value) {
        //如果对象属性里面嵌套对象,则开始递归添加get/set
        this.observer(value);
        // 将当前属性的观察者对象都放到当前属性的发布订阅管理起来
        let dep = new Dep()//创建当前属性的发布订阅对象
        Object.defineProperty(obj, attr, {
            // get方法直接返回值
            get() {
                Dep.target && dep.addSub(Dep.target)
                return value;
            },
            // set方法接收新值并返回
            set: (newValue) => {
                if (value !== newValue) {
                    // 如果新值里面也有对象,也需要添加get/set
                    this.observer(newValue);
                    value = newValue;
                    //属性发生变化的时候通知
                    dep.notify()
                    console.log("监听到数据的变化,需要去更新UI");
                }
            },
        });
    }
}
//想要实现数据变化之后更新UI界面,可以使用发布订阅模式来实现
//先定义一个观察者类,再定义一个发布订阅类,然后通过发布订阅的类来管理观察者类
//发布者类
class Dep {
    constructor() {
        //这个数组就是专门用于管理某个属性所有的观察者对象的
        this.subs = []

    }
    //订阅观察的方法
    // 订阅就是把观察者添加到数组中
    addSub(watcher) {
        this.subs.push(watcher)
    }
    //发布订阅的方法
    //执行数组中所有观察者
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}
// 观察者类
class Warcher {
    // 参数分别为:vue实例,观察哪个属性,接收回调函数
    constructor(vm, attr, cb) {
        this.vm = vm
        this.attr = attr
        this.cb = cb
        // 需要将新旧属性的值对比,才知道观察的属性有没有变化
        //在创建观察者对象的时候就去获取当前的旧值
        this.oldValue = this.getOldValue()
    }
    getOldValue() {
        Dep.target = this
        let oldValue = CompilerUtil.getValue(this.vm, this.attr)
        Dep.target = null
        return oldValue
    }
    //定义一个更新的方法,用于判断新值与旧值是否相同
    update() {
        let newValue = CompilerUtil.getValue(this.vm, this.attr)
        if (this.oldValue !== newValue) {
            // 代表发生变化了,就执行函数
            this.cb(newValue, this.newValue)
        }
    }
}

效果
手撕Vue源码全过程(下)_第1张图片

2.实现事件相关指令

在起初元素处理判断是否v-开头的部分添加识别v-on,处理好属性名后,给事件添加方法,并改变this指向.


<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Documenttitle>
    <script src="./testVue.js">script>
head>

<body>
    <div id="APP">
        <p>{{name}}p>
        <input type="text" v-model='name'>
        
        <div v-on:click='handleClick'>按钮div>
    div>
    <script>
        let vue = new Vue({
            // 指出需要控制的区域
            el: '#APP',
            // 传递数据
            data: {
                name: "山竹",
                age: 18
            },
            // ============================= 更新 ==============
            methods: {
                handleClick() {
                    console.log("啊,是谁触碰了我");
                    console.log(this);
                }
            }
        })
        // console.log(vue.$el);
        // console.log(vue.$data);
    script>
body>

html>
let CompilerUtil = {
    getValue(vm, value) {
        //time.h-->[time,h],利用reduce遍历逐层取
        //reduce接收的第一个参数为上一次调用回调返回的值,或者是提供的初始值
        //  第二个参数为数组中当前被处理的元素
        // vm.$data作为data第一次调用初始值
        return value.split('.').reduce((data, currentKey) => {
            //第一次执行:data=$data,currentKey=time
            // 第二次执行:data=time,currentKey=h
            return data[currentKey.trim()]
        }, vm.$data)
    },
    getContent(vm, value) {
        let reg = /\{\{(.+?)\}\}/gi
        let val = value.replace(reg, (...args) => {
            return this.getValue(vm, args[1])
        })
        return val
    },
    setValue(vm, value, newValue) {
        // console.log(vm, value, newValue);
        // 遍历属性,如果是对象嵌套对象,获取最后一位,赋值
        // vm.$data设定为初始值
        value.split('.').reduce((data, currentKey, index, arr) => {
            // console.log(data, currentKey, index, arr);//$data,属性名,索引,数组
            if (index == arr.length - 1) {
                data[currentKey] = newValue
                console.log(data[currentKey]);
            }
            return data[currentKey]
        }, vm.$data)
    },
    model: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.value = newValue
        })
        // console.log(node, value, vm);
        // v-model作用在input上
        // 根据被替换内容,获取对应的数据
        // node.value=vm.$data[value];
        //遇到复杂类型情况下vm.$data[time.h]-->vm.$data[time]-->time[h]
        let val = this.getValue(vm, value)
        node.value = val
        // 监听input事件拿到输入框内容
        node.addEventListener('input', (e) => {
            // 获取到最新的值赋值到模型当中,由于这里value的属性有可能是name,也有可能是time.h
            // 所以创建独立的方法来处理
            let newValue = e.target.value
            // vm:vue实例,value:属性,newValue为新值
            this.setValue(vm, value, newValue)
        })
    },
    html: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.innerHTML = newValue
        })
        let val = this.getValue(vm, value)
        node.innerHTML = val
    },
    text: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.innerText = newValue
        })
        let val = this.getValue(vm, value)
        node.innerText = val
    },
    content: function (node, value, vm) {
        // console.log(value);//{{name}}-->取出name-->将$data[name]的数据赋值
        // let val = this.getContent(vm, value)
        let reg = /\{\{(.+?)\}\}/gi
        let val = value.replace(reg, (...args) => {
            new Warcher(vm, args[1], (newValue, oldValue) => {
                node.textContent = this.getContent(vm, value)
            })
            return this.getValue(vm, args[1])
        })
        node.textContent = val
    },
    // ==========================更新==============
    on(node, value, vm, type) {
        // console.log(node, value, vm, directiveType);//元素,属性,vue实例,事件名
        // 向该元素添加事件
        node.addEventListener(type, (e) => {
            vm.$methods[value].call(vm, e)
        })
    }
}
// 模拟创建vue实例(类)
class Vue {
    // 构造器,接收一个参数,参数为对象
    constructor(value) {
        // 判断el是否为一个节点
        if (this.isElement(value.el)) {
            this.$el = value.el
        } else {
            // 如果没有则根据传入的找
            this.$el = document.querySelector(value.el)
        }
        this.$data = value.data
        // ========================添加methods===================
        this.$methods = value.methods
        // 根据传入的位置和数据渲染
        // 先判断是否el是否存在,否则不渲染
        if (this.$el) {
            // 1.给外界传入的所有数据都添加get/set方法,这样就可以监听数据变化了
            // 监听数据变化
            new Observer(this.$data)
            // 传入该实例
            new Compier(this)
        }
    }
    // 判断是否为元素节点,元素节点为1,属性节点为2
    isElement(node) {
        // console.log(node.nodeType === 1);//false
        return node.nodeType === 1
    }

}
class Compier {
    constructor(vm) {
        // 保存vue实例
        this.vm = vm
        // 1.将页面元素提取到碎片文档
        let fragment = this.node2fragment(this.vm.$el)
        // 2.利用指定的数据编译内存中的元素
        this.buildTemplate(fragment)
        // 3.将编译好的内存重新渲染到网页上
        this.vm.$el.appendChild(fragment)
    }
    node2fragment(app) {
        // 1.创建空的文档
        let fragment = document.createDocumentFragment()
        //2.遍历循环去到每一个元素
        let node = app.firstChild
        while (node) {
            // 判断是否还有元素
            // 注意点:只要元素添加到文档碎片对象中,name这个元素就会自动从网页上消失
            fragment.appendChild(node)
            node = app.firstChild
        }
        //3.返回储存了所有元素的文档碎片对象
        return fragment
    }
    buildTemplate(fragment) {
        // 从元素获取所有节点,伪数组转为数组
        let nodeList = [...fragment.childNodes]
        // console.log(nodeList);//[text, p, text, input, text]
        // 循环判断当前的节点是一个元素还是一个文本
        nodeList.forEach(node => {
            if (this.vm.isElement(node)) {
                //是一个元素
                this.buildElement(node)
                //处理子元素,递归
                this.buildTemplate(node)
            } else {
                //不是一个元素
                this.buildText(node)
            }
        })
    }
    // 元素处理
    buildElement(node) {
        // attributes 属性返回指定节点的属性集合
        let attrs = [...node.attributes]
        attrs.forEach(attr => {
            // console.log(attr);//获取到属性
            let { name, value } = attr
            // console.log(name,value);//单独拿到类型与类型的取值
            if (name.startsWith('v-')) {
                // =========================================更新==================
                // v-on拿到的属性是v-on:click这种形式,所以需要先从冒号切开并解构
                let [directiveName, directiveType] = name.split(':')
                // v-开头的有很多,比如v-model/v-html/v-if...
                // startsWith() 方法用于检测字符串是否以指定的子字符串开始。
                // console.log('是v-', name,value);//是v- v-model name
                // 切割并解构,不需要v-
                let [, directive] = directiveName.split('-')//v,model
                // console.log(directive);//model
                // 找到对应的工具类执行对应的方法
                // node:修改的节点,value:修改的值,this.vm新值
                CompilerUtil[directive](node, value, this.vm, directiveType)
            }
        })
    }
    //文本处理
    buildText(node) {
        // textContent 属性设置或者返回指定节点的文本内容。
        let content = node.textContent
        let reg = /\{\{.+?\}\}/gi
        if (reg.test(content)) {
            // console.log('是{{}}',content);//是{{}} {{name}}
            // 找到对应的工具类执行对应的方法
            // node:修改的节点,content:修改的值,this.vm新值
            CompilerUtil['content'](node, content, this.vm)
        }
    }
}
class Observer {
    // 只要将需要监听的那个对象传递给Observer这个类
    // 这个类就可以快速的给传入的对象的所有属性都添加get/set方法
    // data代表接收的对象
    constructor(data) {
        this.observer(data);
    }
    // 给属性添加get/set;
    observer(obj) {
        // 判断是不是对象
        if (obj && typeof obj === "object") {
            //遍历取出传入对象的所有属性,给遍历到的属性都增加get/set方法
            for (let key in obj) {
                // 参数为:对象,属性,值
                this.defineRecative(obj, key, obj[key]);
            }
        }
    }
    // obj:需要操作的对象
    // attr:需要新增get/set方法的属性
    // value:需要新增get/set方法属性的取值
    defineRecative(obj, attr, value) {
        //如果对象属性里面嵌套对象,则开始递归添加get/set
        this.observer(value);
        // 将当前属性的观察者对象都放到当前属性的发布订阅管理起来
        let dep = new Dep()//创建当前属性的发布订阅对象
        Object.defineProperty(obj, attr, {
            // get方法直接返回值
            get() {
                Dep.target && dep.addSub(Dep.target)
                return value;
            },
            // set方法接收新值并返回
            set: (newValue) => {
                if (value !== newValue) {
                    // 如果新值里面也有对象,也需要添加get/set
                    this.observer(newValue);
                    value = newValue;
                    //属性发生变化的时候通知
                    dep.notify()
                    console.log("监听到数据的变化,需要去更新UI");
                }
            },
        });
    }
}
//想要实现数据变化之后更新UI界面,可以使用发布订阅模式来实现
//先定义一个观察者类,再定义一个发布订阅类,然后通过发布订阅的类来管理观察者类
//发布者类
class Dep {
    constructor() {
        //这个数组就是专门用于管理某个属性所有的观察者对象的
        this.subs = []

    }
    //订阅观察的方法
    // 订阅就是把观察者添加到数组中
    addSub(watcher) {
        this.subs.push(watcher)
    }
    //发布订阅的方法
    //执行数组中所有观察者
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}
// 观察者类
class Warcher {
    // 参数分别为:vue实例,观察哪个属性,接收回调函数
    constructor(vm, attr, cb) {
        this.vm = vm
        this.attr = attr
        this.cb = cb
        // 需要将新旧属性的值对比,才知道观察的属性有没有变化
        //在创建观察者对象的时候就去获取当前的旧值
        this.oldValue = this.getOldValue()
    }
    getOldValue() {
        Dep.target = this
        let oldValue = CompilerUtil.getValue(this.vm, this.attr)
        Dep.target = null
        return oldValue
    }
    //定义一个更新的方法,用于判断新值与旧值是否相同
    update() {
        let newValue = CompilerUtil.getValue(this.vm, this.attr)
        if (this.oldValue !== newValue) {
            // 代表发生变化了,就执行函数
            this.cb(newValue, this.newValue)
        }
    }
}

效果
手撕Vue源码全过程(下)_第2张图片

将数据代理到Vue实例

上述代码中的this虽然已经指向了vue实例,但是数据是在data上,现在需要实现this.的方式获取数据.

手撕Vue源码全过程(下)_第3张图片


<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Documenttitle>
    <script src="./testVue.js">script>
head>

<body>
    <div id="APP">
        <p>{{name}}p>
        <input type="text" v-model='name'>
        <div v-on:click='handleClick'>按钮div>
    div>
    <script>
        let vue = new Vue({
            // 指出需要控制的区域
            el: '#APP',
            // 传递数据
            data: {
                name: "山竹",
                age: 18
            },
            methods: {
                // ===================更新==============
                handleClick() {
                    console.log(this);
                    console.log(this.name);
                    console.log(this.$data.name);
                }
            }
        })
    script>
body>

html>

let CompilerUtil = {
    getValue(vm, value) {
        //time.h-->[time,h],利用reduce遍历逐层取
        //reduce接收的第一个参数为上一次调用回调返回的值,或者是提供的初始值
        //  第二个参数为数组中当前被处理的元素
        // vm.$data作为data第一次调用初始值
        return value.split('.').reduce((data, currentKey) => {
            //第一次执行:data=$data,currentKey=time
            // 第二次执行:data=time,currentKey=h
            return data[currentKey.trim()]
        }, vm.$data)
    },
    getContent(vm, value) {
        let reg = /\{\{(.+?)\}\}/gi
        let val = value.replace(reg, (...args) => {
            return this.getValue(vm, args[1])
        })
        return val
    },
    setValue(vm, value, newValue) {
        // console.log(vm, value, newValue);
        // 遍历属性,如果是对象嵌套对象,获取最后一位,赋值
        // vm.$data设定为初始值
        value.split('.').reduce((data, currentKey, index, arr) => {
            // console.log(data, currentKey, index, arr);//$data,属性名,索引,数组
            if (index == arr.length - 1) {
                data[currentKey] = newValue
                console.log(data[currentKey]);
            }
            return data[currentKey]
        }, vm.$data)
    },
    model: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.value = newValue
        })
        // console.log(node, value, vm);
        // v-model作用在input上
        // 根据被替换内容,获取对应的数据
        // node.value=vm.$data[value];
        //遇到复杂类型情况下vm.$data[time.h]-->vm.$data[time]-->time[h]
        let val = this.getValue(vm, value)
        node.value = val
        // 监听input事件拿到输入框内容
        node.addEventListener('input', (e) => {
            // 获取到最新的值赋值到模型当中,由于这里value的属性有可能是name,也有可能是time.h
            // 所以创建独立的方法来处理
            let newValue = e.target.value
            // vm:vue实例,value:属性,newValue为新值
            this.setValue(vm, value, newValue)
        })
    },
    html: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.innerHTML = newValue
        })
        let val = this.getValue(vm, value)
        node.innerHTML = val
    },
    text: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.innerText = newValue
        })
        let val = this.getValue(vm, value)
        node.innerText = val
    },
    content: function (node, value, vm) {
        // console.log(value);//{{name}}-->取出name-->将$data[name]的数据赋值
        // let val = this.getContent(vm, value)
        let reg = /\{\{(.+?)\}\}/gi
        let val = value.replace(reg, (...args) => {
            new Warcher(vm, args[1], (newValue, oldValue) => {
                node.textContent = this.getContent(vm, value)
            })
            return this.getValue(vm, args[1])
        })
        node.textContent = val
    },
    // ==========================更新==============
    on(node, value, vm, type) {
        // console.log(node, value, vm, directiveType);//元素,属性,vue实例,事件名
        // 向该元素添加事件
        node.addEventListener(type, (e) => {
            vm.$methods[value].call(vm, e)
        })
    }
}
// 模拟创建vue实例(类)
class Vue {
    // 构造器,接收一个参数,参数为对象
    constructor(value) {
        // 判断el是否为一个节点
        if (this.isElement(value.el)) {
            this.$el = value.el
        } else {
            // 如果没有则根据传入的找
            this.$el = document.querySelector(value.el)
        }
        this.$data = value.data
        // ==========================更新=============
        this.proxyData()
        this.$methods = value.methods
        // 根据传入的位置和数据渲染
        // 先判断是否el是否存在,否则不渲染
        if (this.$el) {
            // 1.给外界传入的所有数据都添加get/set方法,这样就可以监听数据变化了
            // 监听数据变化
            new Observer(this.$data)
            // 传入该实例
            new Compier(this)
        }
    }
    // ==============更新================
    proxyData() {
        // 动态新增属性
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get: () => {
                    return this.$data[key]
                }
            })
        }
    }
    // 判断是否为元素节点,元素节点为1,属性节点为2
    isElement(node) {
        // console.log(node.nodeType === 1);//false
        return node.nodeType === 1
    }

}
class Compier {
    constructor(vm) {
        // 保存vue实例
        this.vm = vm
        // 1.将页面元素提取到碎片文档
        let fragment = this.node2fragment(this.vm.$el)
        // 2.利用指定的数据编译内存中的元素
        this.buildTemplate(fragment)
        // 3.将编译好的内存重新渲染到网页上
        this.vm.$el.appendChild(fragment)
    }
    node2fragment(app) {
        // 1.创建空的文档
        let fragment = document.createDocumentFragment()
        //2.遍历循环去到每一个元素
        let node = app.firstChild
        while (node) {
            // 判断是否还有元素
            // 注意点:只要元素添加到文档碎片对象中,name这个元素就会自动从网页上消失
            fragment.appendChild(node)
            node = app.firstChild
        }
        //3.返回储存了所有元素的文档碎片对象
        return fragment
    }
    buildTemplate(fragment) {
        // 从元素获取所有节点,伪数组转为数组
        let nodeList = [...fragment.childNodes]
        // console.log(nodeList);//[text, p, text, input, text]
        // 循环判断当前的节点是一个元素还是一个文本
        nodeList.forEach(node => {
            if (this.vm.isElement(node)) {
                //是一个元素
                this.buildElement(node)
                //处理子元素,递归
                this.buildTemplate(node)
            } else {
                //不是一个元素
                this.buildText(node)
            }
        })
    }
    // 元素处理
    buildElement(node) {
        // attributes 属性返回指定节点的属性集合
        let attrs = [...node.attributes]
        attrs.forEach(attr => {
            // console.log(attr);//获取到属性
            let { name, value } = attr
            // console.log(name,value);//单独拿到类型与类型的取值
            if (name.startsWith('v-')) {
                // v-on拿到的属性是v-on:click这种形式,所以需要先从冒号切开并解构
                let [directiveName, directiveType] = name.split(':')
                // v-开头的有很多,比如v-model/v-html/v-if...
                // startsWith() 方法用于检测字符串是否以指定的子字符串开始。
                // console.log('是v-', name,value);//是v- v-model name
                // 切割并解构,不需要v-
                let [, directive] = directiveName.split('-')//v,model
                // console.log(directive);//model
                // 找到对应的工具类执行对应的方法
                // node:修改的节点,value:修改的值,this.vm新值
                CompilerUtil[directive](node, value, this.vm, directiveType)
            }
        })
    }
    //文本处理
    buildText(node) {
        // textContent 属性设置或者返回指定节点的文本内容。
        let content = node.textContent
        let reg = /\{\{.+?\}\}/gi
        if (reg.test(content)) {
            // console.log('是{{}}',content);//是{{}} {{name}}
            // 找到对应的工具类执行对应的方法
            // node:修改的节点,content:修改的值,this.vm新值
            CompilerUtil['content'](node, content, this.vm)
        }
    }
}
class Observer {
    // 只要将需要监听的那个对象传递给Observer这个类
    // 这个类就可以快速的给传入的对象的所有属性都添加get/set方法
    // data代表接收的对象
    constructor(data) {
        this.observer(data);
    }
    // 给属性添加get/set;
    observer(obj) {
        // 判断是不是对象
        if (obj && typeof obj === "object") {
            //遍历取出传入对象的所有属性,给遍历到的属性都增加get/set方法
            for (let key in obj) {
                // 参数为:对象,属性,值
                this.defineRecative(obj, key, obj[key]);
            }
        }
    }
    // obj:需要操作的对象
    // attr:需要新增get/set方法的属性
    // value:需要新增get/set方法属性的取值
    defineRecative(obj, attr, value) {
        //如果对象属性里面嵌套对象,则开始递归添加get/set
        this.observer(value);
        // 将当前属性的观察者对象都放到当前属性的发布订阅管理起来
        let dep = new Dep()//创建当前属性的发布订阅对象
        Object.defineProperty(obj, attr, {
            // get方法直接返回值
            get() {
                Dep.target && dep.addSub(Dep.target)
                return value;
            },
            // set方法接收新值并返回
            set: (newValue) => {
                if (value !== newValue) {
                    // 如果新值里面也有对象,也需要添加get/set
                    this.observer(newValue);
                    value = newValue;
                    //属性发生变化的时候通知
                    dep.notify()
                    console.log("监听到数据的变化,需要去更新UI");
                }
            },
        });
    }
}
//想要实现数据变化之后更新UI界面,可以使用发布订阅模式来实现
//先定义一个观察者类,再定义一个发布订阅类,然后通过发布订阅的类来管理观察者类
//发布者类
class Dep {
    constructor() {
        //这个数组就是专门用于管理某个属性所有的观察者对象的
        this.subs = []

    }
    //订阅观察的方法
    // 订阅就是把观察者添加到数组中
    addSub(watcher) {
        this.subs.push(watcher)
    }
    //发布订阅的方法
    //执行数组中所有观察者
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}
// 观察者类
class Warcher {
    // 参数分别为:vue实例,观察哪个属性,接收回调函数
    constructor(vm, attr, cb) {
        this.vm = vm
        this.attr = attr
        this.cb = cb
        // 需要将新旧属性的值对比,才知道观察的属性有没有变化
        //在创建观察者对象的时候就去获取当前的旧值
        this.oldValue = this.getOldValue()
    }
    getOldValue() {
        Dep.target = this
        let oldValue = CompilerUtil.getValue(this.vm, this.attr)
        Dep.target = null
        return oldValue
    }
    //定义一个更新的方法,用于判断新值与旧值是否相同
    update() {
        let newValue = CompilerUtil.getValue(this.vm, this.attr)
        if (this.oldValue !== newValue) {
            // 代表发生变化了,就执行函数
            this.cb(newValue, this.newValue)
        }
    }
}

效果
手撕Vue源码全过程(下)_第4张图片

实现计算属性

当我们定义好计算属性后,在渲染时会寻找到{{}}符号,并替换内容,而替换内容会找到getValue方法,方法又从data寻找数据.所以需要在vue实例中接收computed数据,并传递给data


<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Documenttitle>
    <script src="./testVue.js">script>
head>

<body>
    <div id="APP">
        <p>{{name}}p>
        <input type="text" v-model='name'>
        <div v-on:click='handleClick'>按钮div>
        <p>{{getName}}p>
    div>
    <script>
        let vue = new Vue({
            // 指出需要控制的区域
            el: '#APP',
            // 传递数据
            data: {
                name: "山竹",
                age: 18
            },
            methods: {
                handleClick() {
                    console.log(this);
                    console.log(this.name);
                    console.log(this.$data.name);
                }
            },
            // ==================更新==================
            computed:{
                getName(){
                    return this.name+'修改了计算属性'
                }
            }
        })
    script>
body>

html>

let CompilerUtil = {
    getValue(vm, value) {
        //time.h-->[time,h],利用reduce遍历逐层取
        //reduce接收的第一个参数为上一次调用回调返回的值,或者是提供的初始值
        //  第二个参数为数组中当前被处理的元素
        // vm.$data作为data第一次调用初始值
        return value.split('.').reduce((data, currentKey) => {
            //第一次执行:data=$data,currentKey=time
            // 第二次执行:data=time,currentKey=h
            return data[currentKey.trim()]
        }, vm.$data)
    },
    getContent(vm, value) {
        let reg = /\{\{(.+?)\}\}/gi
        let val = value.replace(reg, (...args) => {
            return this.getValue(vm, args[1])
        })
        return val
    },
    setValue(vm, value, newValue) {
        // console.log(vm, value, newValue);
        // 遍历属性,如果是对象嵌套对象,获取最后一位,赋值
        // vm.$data设定为初始值
        value.split('.').reduce((data, currentKey, index, arr) => {
            // console.log(data, currentKey, index, arr);//$data,属性名,索引,数组
            if (index == arr.length - 1) {
                data[currentKey] = newValue
                console.log(data[currentKey]);
            }
            return data[currentKey]
        }, vm.$data)
    },
    model: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.value = newValue
        })
        // console.log(node, value, vm);
        // v-model作用在input上
        // 根据被替换内容,获取对应的数据
        // node.value=vm.$data[value];
        //遇到复杂类型情况下vm.$data[time.h]-->vm.$data[time]-->time[h]
        let val = this.getValue(vm, value)
        node.value = val
        // 监听input事件拿到输入框内容
        node.addEventListener('input', (e) => {
            // 获取到最新的值赋值到模型当中,由于这里value的属性有可能是name,也有可能是time.h
            // 所以创建独立的方法来处理
            let newValue = e.target.value
            // vm:vue实例,value:属性,newValue为新值
            this.setValue(vm, value, newValue)
        })
    },
    html: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.innerHTML = newValue
        })
        let val = this.getValue(vm, value)
        node.innerHTML = val
    },
    text: function (node, value, vm) {
        // 在第一次渲染的时候,就给所有的属性添加观察者
        new Warcher(vm, value, (newValue, oldValue) => {
            node.innerText = newValue
        })
        let val = this.getValue(vm, value)
        node.innerText = val
    },
    content: function (node, value, vm) {
        // console.log(value);//{{name}}-->取出name-->将$data[name]的数据赋值
        // let val = this.getContent(vm, value)
        let reg = /\{\{(.+?)\}\}/gi
        let val = value.replace(reg, (...args) => {
            new Warcher(vm, args[1], (newValue, oldValue) => {
                node.textContent = this.getContent(vm, value)
            })
            return this.getValue(vm, args[1])
        })
        node.textContent = val
    },
    // ==========================更新==============
    on(node, value, vm, type) {
        // console.log(node, value, vm, directiveType);//元素,属性,vue实例,事件名
        // 向该元素添加事件
        node.addEventListener(type, (e) => {
            vm.$methods[value].call(vm, e)
        })
    }
}
// 模拟创建vue实例(类)
class Vue {
    // 构造器,接收一个参数,参数为对象
    constructor(value) {
        // 判断el是否为一个节点
        if (this.isElement(value.el)) {
            this.$el = value.el
        } else {
            // 如果没有则根据传入的找
            this.$el = document.querySelector(value.el)
        }
        this.$data = value.data
        this.proxyData()
        this.$methods = value.methods
        // =====================更新=============
        this.$computed = value.computed
        // 将computed方法添加到data,只有这样将来渲染的时候
        // 才能从$data中获取到computed中定义的数据
        this.computed2data()
        // 根据传入的位置和数据渲染
        // 先判断是否el是否存在,否则不渲染
        if (this.$el) {
            // 1.给外界传入的所有数据都添加get/set方法,这样就可以监听数据变化了
            // 监听数据变化
            new Observer(this.$data)
            // 传入该实例
            new Compier(this)
        }
    }
    // ===============更新===========
    computed2data(){
        // 将计算属性添加到data,并改变this指向
        for(let key in this.$computed){
            Object.defineProperty(this.$data,key,{
                get:()=>{
                    return this.$computed[key].call(this)
                }
            })
        }
    }
    proxyData() {
        // 动态新增属性
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get: () => {
                    return this.$data[key]
                }
            })
        }
    }
    // 判断是否为元素节点,元素节点为1,属性节点为2
    isElement(node) {
        // console.log(node.nodeType === 1);//false
        return node.nodeType === 1
    }

}
class Compier {
    constructor(vm) {
        // 保存vue实例
        this.vm = vm
        // 1.将页面元素提取到碎片文档
        let fragment = this.node2fragment(this.vm.$el)
        // 2.利用指定的数据编译内存中的元素
        this.buildTemplate(fragment)
        // 3.将编译好的内存重新渲染到网页上
        this.vm.$el.appendChild(fragment)
    }
    node2fragment(app) {
        // 1.创建空的文档
        let fragment = document.createDocumentFragment()
        //2.遍历循环去到每一个元素
        let node = app.firstChild
        while (node) {
            // 判断是否还有元素
            // 注意点:只要元素添加到文档碎片对象中,name这个元素就会自动从网页上消失
            fragment.appendChild(node)
            node = app.firstChild
        }
        //3.返回储存了所有元素的文档碎片对象
        return fragment
    }
    buildTemplate(fragment) {
        // 从元素获取所有节点,伪数组转为数组
        let nodeList = [...fragment.childNodes]
        // console.log(nodeList);//[text, p, text, input, text]
        // 循环判断当前的节点是一个元素还是一个文本
        nodeList.forEach(node => {
            if (this.vm.isElement(node)) {
                //是一个元素
                this.buildElement(node)
                //处理子元素,递归
                this.buildTemplate(node)
            } else {
                //不是一个元素
                this.buildText(node)
            }
        })
    }
    // 元素处理
    buildElement(node) {
        // attributes 属性返回指定节点的属性集合
        let attrs = [...node.attributes]
        attrs.forEach(attr => {
            // console.log(attr);//获取到属性
            let { name, value } = attr
            // console.log(name,value);//单独拿到类型与类型的取值
            if (name.startsWith('v-')) {
                // v-on拿到的属性是v-on:click这种形式,所以需要先从冒号切开并解构
                let [directiveName, directiveType] = name.split(':')
                // v-开头的有很多,比如v-model/v-html/v-if...
                // startsWith() 方法用于检测字符串是否以指定的子字符串开始。
                // console.log('是v-', name,value);//是v- v-model name
                // 切割并解构,不需要v-
                let [, directive] = directiveName.split('-')//v,model
                // console.log(directive);//model
                // 找到对应的工具类执行对应的方法
                // node:修改的节点,value:修改的值,this.vm新值
                CompilerUtil[directive](node, value, this.vm, directiveType)
            }
        })
    }
    //文本处理
    buildText(node) {
        // textContent 属性设置或者返回指定节点的文本内容。
        let content = node.textContent
        let reg = /\{\{.+?\}\}/gi
        if (reg.test(content)) {
            // console.log('是{{}}',content);//是{{}} {{name}}
            // 找到对应的工具类执行对应的方法
            // node:修改的节点,content:修改的值,this.vm新值
            CompilerUtil['content'](node, content, this.vm)
        }
    }
}
class Observer {
    // 只要将需要监听的那个对象传递给Observer这个类
    // 这个类就可以快速的给传入的对象的所有属性都添加get/set方法
    // data代表接收的对象
    constructor(data) {
        this.observer(data);
    }
    // 给属性添加get/set;
    observer(obj) {
        // 判断是不是对象
        if (obj && typeof obj === "object") {
            //遍历取出传入对象的所有属性,给遍历到的属性都增加get/set方法
            for (let key in obj) {
                // 参数为:对象,属性,值
                this.defineRecative(obj, key, obj[key]);
            }
        }
    }
    // obj:需要操作的对象
    // attr:需要新增get/set方法的属性
    // value:需要新增get/set方法属性的取值
    defineRecative(obj, attr, value) {
        //如果对象属性里面嵌套对象,则开始递归添加get/set
        this.observer(value);
        // 将当前属性的观察者对象都放到当前属性的发布订阅管理起来
        let dep = new Dep()//创建当前属性的发布订阅对象
        Object.defineProperty(obj, attr, {
            // get方法直接返回值
            get() {
                Dep.target && dep.addSub(Dep.target)
                return value;
            },
            // set方法接收新值并返回
            set: (newValue) => {
                if (value !== newValue) {
                    // 如果新值里面也有对象,也需要添加get/set
                    this.observer(newValue);
                    value = newValue;
                    //属性发生变化的时候通知
                    dep.notify()
                    console.log("监听到数据的变化,需要去更新UI");
                }
            },
        });
    }
}
//想要实现数据变化之后更新UI界面,可以使用发布订阅模式来实现
//先定义一个观察者类,再定义一个发布订阅类,然后通过发布订阅的类来管理观察者类
//发布者类
class Dep {
    constructor() {
        //这个数组就是专门用于管理某个属性所有的观察者对象的
        this.subs = []

    }
    //订阅观察的方法
    // 订阅就是把观察者添加到数组中
    addSub(watcher) {
        this.subs.push(watcher)
    }
    //发布订阅的方法
    //执行数组中所有观察者
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}
// 观察者类
class Warcher {
    // 参数分别为:vue实例,观察哪个属性,接收回调函数
    constructor(vm, attr, cb) {
        this.vm = vm
        this.attr = attr
        this.cb = cb
        // 需要将新旧属性的值对比,才知道观察的属性有没有变化
        //在创建观察者对象的时候就去获取当前的旧值
        this.oldValue = this.getOldValue()
    }
    getOldValue() {
        Dep.target = this
        let oldValue = CompilerUtil.getValue(this.vm, this.attr)
        Dep.target = null
        return oldValue
    }
    //定义一个更新的方法,用于判断新值与旧值是否相同
    update() {
        let newValue = CompilerUtil.getValue(this.vm, this.attr)
        if (this.oldValue !== newValue) {
            // 代表发生变化了,就执行函数
            this.cb(newValue, this.newValue)
        }
    }
}

效果
手撕Vue源码全过程(下)_第5张图片

你可能感兴趣的:(前端,vue,js,vue,javascript)