简单手写vuejs

70 阅读5分钟

前言

本文主要帮助理解 vue前端框架做了什么 ,顺着vue响应式功能结果逆推主要功能简单实现。目标:手写 Vue 2.x 的简单响应式实现

1. 数据响应式核心原理

首先 了解一下核心API:Vue2.x Object.defineProperty() | Vue3.x Proxy()

vue的数据响应式核心实现 在2.x版本使用Object.defineProperty() 和3.x版本更新使用Proxy()

数据响应式:数据模型仅仅是普通的js对象,当我们修改数据时候,视图进行更新,避免繁琐 DOM操作

双向绑定: MVVM模式 数据改变 视图改变; 视图改变 数据也随之改变 v-model在表单元素上创建双向数据绑定

数据驱动是vue的特性之一:仅仅关注数据本身 不需要关心数据如何渲染到视图的 vue背后都自己做了

1.1 数据劫持:Object.defineProperty(obj, prop, descriptor)

引用MDN介绍该API: Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

参数:

  • obj

    要定义属性的对象。

  • prop

    要定义或修改的属性的名称或 Symbol

  • descriptor

    要定义或修改的属性描述符。

返回值:

被传递给函数的对象。

object.defineProperty(),里面的get set方法可以劫持对象数据 并修改对象数据 返回这个对象

    let  data = {
        msg:'hello',
        count:19
    }
​
    //vue实例
     let vm ={}
    //数据劫持  vue2 defineProperty
    //Object.keys() 遍历对象属性 并给对象的所有属性添加get set 方法  也就是:当你在外部代码处使用到这个数据的时候data[key]就会触发这个get方法 ,当你改变这个数据的时候 this.xxx = sss  就会触发 set方法。
    //这就是数据劫持 ,可以看出 这个get和set 是个很好的节点 在get set这里 可以捕捉到最新变化  比如: 数据变化之后 去更新数据修改视图  document.querySelector("#app").textContent= data[keys]
     Object.keys(data).forEach((keys)=>{
         Object.defineProperty(vm,keys,{
         configurable:true,
         enumerable:true,
         get(){
             console.log("getter",data[keys]);
             return keys
         },
         set(newValue){
             data[keys]=newValue;
             console.log("setter",newValue);
             //当数据改变之后 更新数据渲染到视图上 
             document.querySelector("#app").textContent= data[keys]
         }
     })
      vm.msg="hello  World"
      vm.count="20"
      console.log(vm.msg, vm.count);
     })

1.2 对象代理: Proxy()

MDN:Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

const p = new Proxy(target, handler)
​

参数

  • target

    要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

  • handler

    一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

get 两个参数 target 包装目标对象 和key 目标属性

set 三个参数 newValue 目标属性改变的新值

    let vm = new Proxy(data,{
        get(target,key){
            console.log("getter!",target[key]);
            return target[key]
        },
        set(target,key,newValue){
            target[key]=newValue;
            console.log("setter", target[key]);
            document.querySelector("#app").textContent= target[key]
        }
    })

2.发布订阅者模式

某个任务完成,发布者向事件中心发布信号,其他任务可以向事件中心订阅这个信号,从而知道什么时候可以执行任务

Vue中的例子 自定义事件:EventBus onon emit

EventBus :事件触发器(事件中心)

$on :注册事件 (发布者)

$emit: 触发事件 (订阅者)

    // 简写一下 vue中的发布订阅模式
    //事件中心 是一个对象 存储注册的事件
    class EventEmitter {
        constructor(){
            //构造函数 创建一个对象
            //{onchange:[fn1,fn2],click:[fn3] }
            this.subs=Object.create(null)
        }
        // 两个参数  事件名字 和事件函数
        $on(eventType,handler){
            this.subs[eventType] = this.subs[eventType] || []
            // this.subs是对象  对象里的每一项是一个数组
            this.subs[eventType].push(handler)
        }
        //一个参数 事件名字 拿到事件中心已经注册的事件 调用它
        $emit(eventType){
            this.subs[eventType].forEach(handler => {
                handler()
            });
        }
​
​
    }
​
    let em= new EventEmitter()
    //注册事件
    em.$on('onchange',()=> {
        console.log("on1....");
    })
    em.$on('onchange',()=> {
        console.log("on2....");
    })
    //触发事件 
    em.$emit('onchange')

3.观察者模式

观察者没有事件中心 只有 发布者-目标、 订阅者-观察者

其中 发布者有所有的观察者,改变后通知观察者。 观察者就调用update方法 订阅事件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>观察者模式</title>
</head>
<body>
    <div id="app">
        hell0
    </div>
</body>
<script ></script>
<script>
  //发布者
    class Dep {
        //添加所有的观察者
        constructor(){
            this.subs=[]
        }
        // 将观察者添加
        addSub(sub){
            if(sub && sub.update){
                this.subs.push(sub)
            }
        }
        notify(){
            this.subs.forEach(sub=>{
                sub.update()
            })
        }
    }
    //观察者(订阅者)
    class Watcher {
        update(){
            console.log("dddddddd");
        }
    }
    let dep = new Dep()
    let watcher = new Watcher()
    dep.addSub(watcher)
    dep.notify()
    //发布者会将观察者添加到自己数组列表中,然后当数据改变之后 发布通知,(notify) 观察者就会更新自己的update方法 
</script>
</html>

4.简写Vue

简化成 核心代码 vue.js observe.js compiler.js dep.js watch.js 有这五个

文件整体目录:

vue-01.png

4.1 vue.js

Vue:

负责接收初始化的参数(data)

负责把data中的属性注入到vue实例中,转换成getter setter

负责调用observer 监听data中所有属性的变化

负责调用compiler 解析指令\差值表达式

vue.js

class Vue {
    constructor(options){
        //初始化的参数
        this.$options=options;
        this.$data = options.data;
        this.$el = typeof options.el ==='string'?document.querySelector(options.el):options.el;
        //注入到vue 实例 并转换getter setter
        this._proxyData(this.$data)
        // 在 创建observer 再考虑调用observe对象 监听数据变化
        //new Observe(this.$data)
        // 在 Compiler 再考虑 调用compiler对象 解析指令和差值表达式
        //new Compiler (this.$el)
    }
    //这里是把data 注入到vue实例上
    _proxyData(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                configurable:true,
                enumerable:true,
                get(){
                    return key
                },
                set(newValue){
                    if(newValue===data[key]){
                        return
                    }
                    data[key]=newValue
                }
            })
        })
    }
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mini Vue</title>
</head>
<script src="./js/vue.js"></script>
<body>
    <div id="app">
        <h1>差值表达式</h1>
        <h3>{{ msg }}</h3>
        <h3>{{  count  }}</h3>
        <h1>v-text</h1>
        <div v-text="msg"></div>
        <h1>v-model</h1>
        <input type="text" v-model="msg"></input>
        <input type="text" v-model="count"></input>
    </div>
</body>
<script>
    let vm = new Vue({
        el:"#app",
        data:{
            msg:"hello world",
            count :1011,
            person:{
                name:"zzws"
            }
        }
    })
    console.log(vm)
    //打印vm 可以查看已经注入到vue中 也转换了get set
</script>
</html>

vue-02.png

4.2 observer.js

Observer:

负责把data选项中的属性 转换成响应式数据 defineReactuve

data中的属性是对象的话 也把属性转换成响应式数据

数据变化发送通知 (这里是用到dep这个对象 后续细表)

observe:

walk(data)

defineReactuve(data,key,value)

observe.js

class Observer{
    constructor(data){
        this.walk(data)
    }
    walk(data){
        if(typeof data !=='object' || !data){
            return
        }
        //遍历所有属性
        Object.keys(data).forEach(key=>{
        //这里为什么要穿data[key] 已经有了 前两个参数  是因为到Object.defineProperty get方法的时候 我们使用data[key] 时 就相当于又使用了get方法 就会陷入栈溢出 陷入死循环
            this.defineReactive(data,key,data[key])
        })
    }
    defineReactive(data,key,value){
        //this 指向问题需要注意 
        let that = this
        // value 是对象 把内部属性转换成响应式数据
        this.walk(data)
        
        Object.defineProperty(data,key,{
            get(){
            //这里如果使用data[key] 不信你试试
                return value
            },
            set(newValue){
                if(newValue===value){
                    rerurn 
                }
                value=newValue
                // 改变后的新值如果是对象 把内部属性也转换成响应式数据
                that.walk(newValue)
                //改变了就发送通知
            }
        })
    }
}

不要忘记 html中引入 和 vue.js中创建 new Observer对象

4.3 compiler.js

Compiler :

负责编译模版 解析指令 差值表达式

负责页面的首次渲染

当数据变化后 重新渲染视图

el vm compile(el) compileElement(node) compileText(node)

isDirective(attrName) isTextNode(node) isElementNode(node)

compiler.js

class Compiler {
	constructor(vm){
		this.$el = vm.el;
		this.vm = vm 
		//立即调用编译模版
		this.compile(this.$el)
		
	}
	//编译模版 处理指令和差值表达式
	compile(el){
		let childNodes = el.childNodes //伪数组
		Array.from(childNodes).forEach(node=>{
		
			if(this.isTextNode(node)){
				this.compileText(node)
			}else if(this.isElementNode(node)){
				this.compileElement(node)
			}
			
			
			//递归调用子节点  如果子节点下面还有节点
			if(node.childNodes&&childNodes.length){
				this.compile(node)
			}
		})
	}
	//编译元素节点 解析指令
	compileElement(node){
	//通过属性节点 可以找到属性名称和属性值
	 // console.log(node.attributes)
	        //遍历所有属性节点
        Array.from(node.attributes).forEach(attr=>{
            let attrName = attr.name
            // 判断是否是指定  是哪个指令 v-type 还是v-modle 
            if(this.isDirective(attrName)){
                //v-text   变成 text 
                attrName = attrName.substr(2)
                let key = attr.value
                console.log(key);
                this.update(node,key,attrName)
            }
        })
	}
	
	 update(node,key,attrName){
        let updateFn = this[attrName + 'Updater']
        console.log(this.vm);
        updateFn && updateFn(node,this.vm[key])
    }
    //处理v-text 指令
    textUpdater(node,value){
        node.textContent = value
    }
  //处理v-model 指令
    modelUpdater(node,value){
        node.value = value
    }
	
	
	//编译文本节点  处理差值表达式
	compileText(node){
		console.dir(ndoe)
		// console.dir(node)  .匹配任意字符   匹配到差值表达式
        // {{  msg  }}
        let reg = /{{(.+?)}}/     
        let value = node.textContent
        if(reg.test(value)){
            let key = RegExp.$1.trim()//拿到msg
            console.log(key);
            node.textContent = value.replace(reg,this.vm[key])
        }

		
	}
	//判断元素属性 是不是指令
	isDirective(attrName){
		return attrName.startsWith('v-')
	}
	//是否是文本节点
	isTextNode(node){
		return node.nodeType==3
	}     
	//是否是元素节点
	isElementNode(node){
		return node.nodeType==1
	}
}

4.4 dep.js

dep.js

发布者 Dep 当Observer监听数据变化,把data数据变成get set dep的作用就是在 get的时候收集依赖,(每一个响应式属性都会创建一个dep对象,收集依赖于该属性的地方,所有依赖于该属性的地方,都会创建一个watcher对象,所以dep都会收集依赖于该属性的watcher对象 。 在set方法中通知依赖,当属性发生变化时,dep.notify发送通知 调用watcher的update方法。

get中收集观察者 set中通知观察者

class Dep {
    constructor(){
        this.subs = []//保存sub
    }
    addSub(sub){
    //添加sub
        if(sub && sub.update){
            this.subs.push(sub)
        }
    }
    notify(){
        每个订阅者都要更新方法
        this.subs.forEach(sub=>{
            sub.update()
        })
    }
}

再在observer里 添加依赖 和通知

    defineReactive(obj,key,value){
        let that = this
        let dep = new Dep()
        // value 是对象 把内部属性转换成响应式数据
        that.walk(value)
  
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get(){
            //检查Dep类的target存不存在 这个target是保存watcher对象的 在watcher里面保存
            //存在就添加进dep数组里保存依赖 等到数据更新的时候在set里通知到订阅者watcher
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set(newValue){
                if(newValue===value){
                    return
                }
                value=newValue
                that.walk(newValue)
                // 发送通知 数据更新了就调用notify方法通知 会调用watcher的update方法
                dep.notify()
            }
        })
    }

注意 在index.html里五个js文件的引入关系

vue-04.png

4.5 watcher.js

watch.js

当数据变化触发依赖 dep通知所有的watcher 实例更新视图

自身实例化的时候往dep对象中添加自己

watcher.js

class Watcher{
    constructor(vm,key,cb){
        this.vm = vm 
        this.key = key
        this.cb = cb
​
        //把watcher对象记录到dep类的静态属性target方法
        Dep.target = this
        //触发get方法,在get方法中调用addSub
        this.oldValue = vm[key]
        Dep.target = null 
    }
    update(){
    //更新方法 会调用回调函数更新数据
        let newValue = this.vm[this.key]
        if(this.oldValue === newValue){
            return
        }
       
        this.cb(newValue)
    }
}

代码中 所有依赖于数据的地方都要创建一个watcher对象 当数据改变的时候 dep对象 会通知watcher对象 去渲染视图

compiler.js

class Compiler {
    constructor(vm){
        this.el = vm.$el;
        this.vm = vm 
        //立即调用编译模版
        this.compile(this.el)
        
    }
    //编译模版 处理指令和差值表达式
    compile(el){
        let childNodes = el.childNodes //伪数组
        Array.from(childNodes).forEach(node=>{
        
            if(this.isTextNode(node)){
                this.compileText(node)
            }else if(this.isElementNode(node)){
                this.compileElement(node)
            }
            //递归调用子节点  如果子节点下面还有节点
            if(node.childNodes&&childNodes.length){
                this.compile(node)
            }
        })
    }
    //编译元素节点 解析指令
    compileElement(node){
    //通过属性节点 可以找到属性名称和属性值
     console.log(node.attributes)
        //遍历所有属性节点
        Array.from(node.attributes).forEach(attr=>{
            let attrName = attr.name
            // 判断是否是指定  是哪个指令 v-type 还是v-modle 
            if(this.isDirective(attrName)){
                //v-text   变成 text 
                attrName = attrName.substr(2)
                let key = attr.value
                console.log(key);
                this.update(node,key,attrName)
            }
        })
​
    }
    
    update(node,key,attrName){
        let updateFn = this[attrName + 'Updater']
        console.log(this);
        //这里使用call 改变this指向
        updateFn && updateFn.call(this,node,this.vm[key],key)
    }
    //处理v-text 指令
    textUpdater(node,value,key){
        console.log(this);
        node.textContent = value
        //创建watcher对象  当数据改变 渲染视图 这里的this是compiler类 所以要上面updatFn函数改变this指向 回调函数传改变的值进去
        new Watcher(this.vm,key,(newValue)=>{
            node.textContent = newValue
        })
    }
  //处理v-model 指令
    modelUpdater(node,value,key){
        node.value = value
        //创建watcher对象  当数据改变 渲染视图
        new Watcher(this.vm,key,(newValue)=>{
            node.value = newValue
        })
            // 双向绑定 当input框值改变时 更新数据
        node.addEventListener('input', () => {
            this.vm[key] = node.value
        })
    }
    //编译文本节点  处理差值表达式
    compileText(node){
        // console.dir(node)  .匹配任意字符   匹配到差值表达式
        // {{  msg  }}
        let reg = /{{(.+?)}}/     
        let value = node.textContent
        if(reg.test(value)){
            let key = RegExp.$1.trim()//拿到msg
            // console.log(key);
            node.textContent = value.replace(reg,this.vm[key])
​
            //创建watcher对象  当数据改变 渲染视图
            new Watcher(this.vm,key,(newValue)=>{
                node.textContent = newValue
            })
        }
​
    }
    //判断元素属性 是不是指令
    isDirective(attrName){
        return attrName.startsWith('v-')
    }
    //是否是文本节点
    isTextNode(node){
        return node.nodeType === 3
    }     
    //是否是元素节点
    isElementNode(node){
        return node.nodeType === 1
    }
}

vue-03.png

简单的vue的双向数据绑定 完成.