有手就行篇—手写简易版vue

508 阅读5分钟

图怪兽_900d0d4844a8e73b386376a6fdaaeb97_45310.png

前言

源码并非遥不可及,vue源码还是较简单易懂。纸上得来终觉浅,实操敲几遍,对每个类加深理解,相信对vue的理解会更上一层楼,一起加油进步鸭!

1、vue中几个核心类

  • 1.Observe数据监听器:对data里的属性添加getter/setter,进行依赖收集以及派发更新
  • 2.Dep消息订阅器:用于收集当前响应式对象的依赖关系,每个响应式对象都有一个dep实例。dep.subs=watcher[],当数据发生变化时,触发dep.notify,该方法会遍历subs数组,调用每一个watcher的update方法。
  • 3.Watcher观察者:作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知Watcher类有多种,比如computed watcher,user watcher(自己在watch里定义的需要监听数据变化的watcher)。
  • 4.Compile指令解析器:它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。

依赖收集

  • initstate,对computed属性初始化时,触发computed watcher依赖收集
  • initstate,对监听属性初始化时,触发user watcher依赖收集
  • render,会触发render依赖收集

派发更新 Object.defineProperty

  • 1.组件中对相应的数据发生了修改,会触发setter
  • 2.dep.notify
  • 3.遍历subs数组,调用每一个watcher的update方法

2、结构

index.html    
index.js 
vue.js
observe.js    
dep.js       
compiler.js   
watch.js 

3、vue.js

/*
 * @Author: Mx
 * @Date: 2022-01-18 13:12:27
 * @Description: Vue构造函数 配置参数等
 */
import Observer from "./observer.js";
import Complier from "./compiler.js";
export default class Vue {
    constructor (options = {}){
        this.$options = options
        this.$data = options.data
        this.$methods = options.methods

        // 初始化el 对传进来的根元素进行判断
        this.initRootElement(options)

        // 利用Object.defineProperty将data里的属性注入到vue实例中
        this._proxyData(this.$data)

        // 实例化observer对象 监听数据变化
        new Observer(this.$data)

        // 实例化Complier对象 解析指令和模板
        new Complier(this)


    }
    /**
     * 获取根元素 并存储到vue实现 检查传入的el是否合规
     */
    initRootElement(options){
        if(typeof options.el === 'string'){
            this.$el = document.querySelector(options.el)
        }else if (options.el instanceof HTMLElement){
            //真实的元素
            this.$el = options.el
        }

        if(!this.$el){
            throw new Error("el不合法")
        }
    }

    _proxyData(data){
         //通过Object.defineProperty将data里的属性绑定到vue实例上
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                //表示可以被枚举,也就是可以被循环
                enumerable:true,
                //表示可以进行相应配置
                configurable:true,
                get(){
                    return data[key]
                },
                set(newValue){
                    if(data[key] === newValue){
                        return
                    }
                    data[key] = newValue
                }
            })
        })
    }
    //_proxyData函数用于将属性绑定在vue实例上,而不是用于进行依赖的收集和派发更新,所以不用递归的对每一个值进行劫持
}

4、observe.js

/*
 * @Author: Mx
 * @Date: 2022-01-31 09:55:30
 * @Description: 数据监听器Observer
 * 依赖收集和派发更新就是在这里实现
 */
import Dep from "./dep.js"
export default class Observer {
    constructor(data){
        this.traverse(data)
    }
    /**递归遍历data里的所有属性 */
    traverse(obj){
        console.log('obj',obj);
        if(!obj || typeof obj !== 'object'){
            return
        }
        //遍历监听数据
        Object.keys(obj).forEach(data => {            
            this.defineReactive(obj,data,obj[data])
        });
    }
    /**给传入数据设置setter getter */
    defineReactive(obj,data,value){
        
        //可能又是一个object
        this.traverse(value)

        //实例化dep
        let dep = new Dep()

        //保存this
        const that = this

        Object.defineProperty(obj,data,{
            enumerable:true,
            configurable:true,
            get(){
                //在这里添加依赖 拿到绑定dep身上的watcher
                Dep.targer && dep.addsubs(Dep.targer)
                //这一步不能够用obj[data],会造成循环的get这个值
                return value
            },
            set(newValue){
                if(value === newValue){
                    return
                }
                value = newValue
                //可能是一个object
                that.traverse(newValue)

                //派发更新
                dep.notify()
            }

        })
    }
}

5、dep.js

/*
 * @Author: Mx
 * @Date: 2022-01-31 09:55:30
 * @Description: Dep
 * 发布订阅模式 存储所有的观察者 每个watcher都有一个update方法 通知subs里每个watcher实例 触发update
 */
export default class Dep {
    constructor(){
        //储存所有的观察者
        this.subs = []
    }
   /** 添加观察者 */
    addsubs(watcher){
        if(watcher && watcher.unpdate){
            this.subs.push(watcher)
        }
    }
   /** 发送通知 */
    notify(){
        //遍历subs数组,调用每一个watcher的updatae方法
        this.subs.forEach(watcher=>{
            watcher.update()
        })
    }
}
//dep类在什么时候实例化?在哪里addSubs?
// => Observer遍历各个属性的时候实例化
// => get 收集依赖 addSubs
//dep类在什么时候调用notify方法?
// => set 派发更新 notify

6、watcher.js

/*
 * @Author: Mx
 * @Date: 2022-01-31 09:55:30
 * @Description: 
 * Watcher 订阅者, 作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数
 */
import Dep from "./dep.js";

export default class Watcher {
    /**
     * @description: 
     * @param {*} vm vue实例
     * @param {*} key data属性名
     * @param {*} callback 回调函数
     */    
    constructor(vm,key,cb){
        this.vm = vm
        this.key = key
        this.callback = cb

        //为什么要往dep.target上添加watcher实例
        Dep.target = this;

        //拿到旧值
        //同时注意一点,在这里会触发变量的get方法
        this.oldValue = vm[key]

        Dep.target = null;
    }
    /**数据发生变化更新视图 */
    update() {
        let newValue = this.vm[this.key]
        if(this.oldValue === newValue){
            return
        }
        this.cb(newValue)
    }

}
//为什么要往dep.target上添加watcher实例?是为了能够将在同一时间只维持一份watcher,因为在computed里,watch里用到时,会添加多个watcher,容易造成数据紊乱,
// 所以在一个时间里只有一个watcher,保证watcher是正常添加的

7、complier.js



/*
 * @Author: Mx
 * @Date: 2022-01-31 09:55:30
 * @Description:  指令解析器,它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
 */
import Watcher from "./watcher.js";

export default class Compiler{
    constructor(vm){        
        this.vm = vm
        this.el = vm.$el
        this.methods = vm.$methods

        this.complie(vm.$el)
    }
    /** 编译模板 对模板进行替换*/
    complie(el){
        //拿到元素的子节点,一个类数组
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach(node =>{
            console.log('node',node);
            //判断节点类型
            if(this.isTextNode(node)){
                //文本节点
                this.complieText(node)
            }else if(this.isElementType(node)){
                //元素节点
                this.compileElement(node);
            }

            if(node.childNodes && node.childNodes.length > 0){
                this.complie(node)
            }
        })
    }

     //文本节点编译
    complieText(node){
        //{{msg}} msg hello mx
        const reg = /\{\{(.+?)\}\}/
        const value = node.textContent //hello mx
        if(reg.test(value)){
            const key = RegExp.$1.trim(); // 拿到msg
            console.log('key',key);
            node.textContent = value.replace(reg,this.vm[key])

             //数据需要动态改变,所以需要依赖收集
            new Watcher(this.vm,key,(newValue)=>{
                node.textContent = newValue 
            })
        }
    }

    //元素节点编译
    compileElement(node){
        console.log('node11',node,node.attributes);
        if(node.attributes.length > 0){
            Array.from(node.attributes).forEach(attr=>{
                //属性名
                console.log('attrObj',attr);
                const attrName = attr.name
                console.log('attrName',attrName);
                //判断是否是v-开头
                if(this.isVStartsWith(attrName)){
                    //特殊判断是:号 例如v-on:click
                    const directiveName = attrName.indexOf(':') > -1 ? attrName.substr(5):attrName.substr(2)
                    //拿到值 v-model='msg'的msg
                    console.log('directiveName',directiveName);
                    
                    let key = attr.value
                    this.update(node,key,directiveName)
                }
                
            })
        }
    }

    // 进行拼接 找到对应函数
    update(node,key,directiveName){
        //v-model v-text v-html v-on:click
        const updaterFn = this[directiveName+'Updater']
        updaterFn && updaterFn.call(this,node,this.vm[key],key,directiveName)
    }

    //v-text
    textUpdater(node,value,key){
        console.log('textUpdater');
        
        node.textContent = value
        new Watcher(this.vm,key,(newValue)=>{
            node.textContent = newValue
        })
    }

    //v-model
    modelUpdater(node,value,key){
        console.log('modelUpdater');
        
        //对应于input
        node.value = value
        new Watcher(this.vm,key,(newValue)=>{
            node.value = newValue
        })

        node.addEventListener('input',()=>{
            //触发setter
            this.vm[key] = node.value
        })
    }

    //v-html
    htmlUpdater(node,value,key){
        console.log('htmlUpdater',value);
        console.log('node',node);
        
        node.innerHTML = value

        new Watcher(this.vm,key,(newValue)=>{
            node.innerHTML = newValue
        })
    }  
    
    //v:on-click
    clickUpdater(node,value,key,directiveName){
        node.addEventListener(directiveName,this.vm.$methods[key])
    }

    //判断文本节点
    isTextNode(node){
        return node.nodeType === 3;
    }

    //判断元素节点
    isElementType(node){
        return node.nodeType === 1;
    }

    //判断v-开头
    isVStartsWith(attr){
        console.log('attr',attr);
        return attr.startsWith('v-');
    }
}

8、完整代码

链接:gitee.com/chunfeng520…

9、❤️感谢阅读

  1. 如果本文对你有帮助,不要吝啬你的赞哟,你的「赞」是我前行的动力。
  2. 欢迎关注公众号  【冥想侃前端】 一起学习进步。