实现一个简单的vue响应式/vue响应式原理/mvvm双向绑定原理

前言

本文主要是实现一个简单版的vue.js.可以实现双向数据响应

开始

compile解析模板代码

解析模板语法,识别v-指令,给每个节点都添加上一个update更新方法,并将更新方法座位回调传递给订阅者watcher

class Compile{
    constructor(vm,root){
        this.$vm = vm;
        this.$root = root;

        //获取到根节点里面所有的节点通过虚拟DOM的形式操作JS对象 将{{}}等指令进行解析

        //1、获取app节点下面所有的子节点
        this.fragment = this.getNodeFragment(this.$root);
        //2、编译
        this.nodeCompile(this.fragment)
        //3、将编译好的内容重新挂载到页面上
        this.$root.appendChild(this.fragment);
    }
    getNodeFragment(root){
        //在编译过程中,避免不了要操作 Dom 元素,所以这里用了一个 createDocumentFragment 方法来创建文档碎片。这在 Vue 中实际使用的是虚拟 dom,而且在更新的时候用 diff 算法来做 最小代价渲染。

//文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)
        var frag = document.createDocumentFragment();
        var child;
        //它会将root里面的每一个节点插入到内存对象中去,只要节点插入到内存对象中,该节点对应的真实dom就会在页面上消失,所以root会慢慢没,不会出现死循环的问题
        while(child = root.firstChild){
            frag.appendChild(child);
        }

        return frag;
    }
    nodeCompile(fragments){
        //获取所有的节点
        var childNodes = fragments.childNodes;
        Array.from(childNodes).forEach((child)=>{

            if(this.isText(child)){
                //编译文本节点
                this.textCompile(child)
            }

            if(this.isElement(child)){
                const attrs = child.attributes;
                //获取到节点身上的属性
                Array.from(attrs).forEach((attr)=>{
                    const attrName = attr.name;
                    const attrValue = attr.value;
                   //根据属性来判断是一个指令还是一个事件
                    
                   //判断是否是一个指令
                   if(this.dir(attrName)){
                        const type = attrName.substr(2);
                        
                        this[type+'update'] && this[type+'update'](child,this.$vm[attrValue]);

                        new Watcher(this.$vm,attrValue,(value)=>{
                            this[type+'update'] && this[type+'update'](child,value);
                        });
                   }
                   //判断是否是一个事件
                   if(this.event(attrName)){
                        const type = attrName.substr(1);
                        this.handleEvent(child,this.$vm,type,attrValue)
                   }
                })
            }

            //递归遍历查找子节点
            if(child.childNodes && child.childNodes.length>0){
                this.nodeCompile(child);
            }

        })
    }
    dir(attr){
        return attr.indexOf("v-") != -1;
    }
    event(attr){
        return attr.indexOf("@") != -1;
    }
    isText(node){
        return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
    }
    isElement(node){
        return node.nodeType == 1;
    }
    textCompile(child){
        //进行编译
      
        this.update(child,this.$vm,RegExp.$1,'text');
    }
    //指定v-if,v-text等会有很多,在这里做匹配具体是那个指令,然后调用该指令的方法去实现指令的操作
    //这一步其实也是将每个指令对应的节点绑定更新函数
    update(el,vm,exp,type){
        //为了其他的一些指令需要的一些公共的逻辑在这个方法里面编写
        //比如这里是text指令,updateFn=textupdate
        var updateFn = this[type+'update'];
        //如果textupdate方法存在,就去执行textupdate方法
        updateFn && updateFn(el,vm[exp]);
        //给每个节点都添加上监听
        //添加监听数据的订阅者(给watcher传递数据和回调),在index.js中一旦数据有变动会触发dep.notify方法,
        //nodity会遍历dep中收集的watcher,调用watcher中的update方法,在update方法中执行,
        //这里(compile)传递给watcher的方法(updateFn)更新视图
        new Watcher(vm,exp,(value)=>{
            updateFn && updateFn(el,value);
        });
    }
    //v-text指令的实现操作
    textupdate(el,value){
        el.textContent = value;
    }
    handleEvent(el,vm,eventName,callback){
        
        el.addEventListener(eventName,vm.$options.methods[callback].bind(vm));
    }
    
}

vue双向数据响应代码

代码中包含数据劫持,dep依赖收集,watcher订阅者实现

class Vue{
    constructor(options){
      this.$options = options;
      this.$el = document.querySelector(options.el);
      //接收data属性
      this.$data = options.data;
      //将data身上所有的属性转换为响应式数据
      this.observer(this.$data);

      new Compile(this,this.$el)
    }
    observer(data){
        if(!data || typeof data != "object")return;

        Object.keys(data).forEach((key)=>{
            //做数据劫持
            this.defaultReative(data,key,data[key]);

            //属性代理  代理到vm身上
            this.proxyData(key);
        })
    }
    proxyData(key){
//将this.$data[key]代理到this即vm上 Object.defineProperty(
this,key,{ get(){ return this.$data[key]; }, set(newValue){ this.$data[key] = newValue; } }) } defaultReative(data,key,value){ //先去做递归 this.observer(value); //给每个响应数据的get方法上都添加一个dep实例 var dep = new Dep(); //数据劫持 Object.defineProperty(data,key,{ get(){ /*在渲染html的时候会调用响应数据,也就执行了get方法;watcher实例是订阅者,渲染时会给当前dom初始化一个watcher实例,也就是给当前的dom添加了订阅者,          * 订阅者内部会有更新dom的方法,数据变化的时候,发布者通知订阅者,订阅者内部去调用更新dom的方法.          * watcher内部有dep.target赋值操作,赋值为watcher本身,           这样才能确保有我们收集的依赖就是调用了当前响应数据的订阅者,订阅者就是watcher,          */ Dep.target && dep.addDep(Dep.target); return value; }, set(newVal){ if(newVal == value)return; //赋值操作 当外部设置data身上的某个属性的时候 将新值赋值给旧值 value = newVal; dep.notify(); } }) } } //依赖收集 class Dep{ constructor(){ this.deps = []; } addDep(dep){ this.deps.push(dep); } notify(){ this.deps.forEach((item)=>{ item.update(); }) } } //监听数据的变化 class Watcher{ constructor(vm,exp,callback){ this.$vm = vm; this.$exp = exp; this.callback = callback; //给dep添加了一个静态属性target,值是watcher自己 Dep.target = this; //触发属性的getter方法 this.$vm[this.$exp]; Dep.target = null; } //在index.js中一旦数据有变动会触发dep.notify方法, //nodity会遍历dep中收集的watcher,调用watcher中的update方法,在update方法中执行, //这里(compile)传递给watcher的方法(updateFn)更新视图 update(){ //视图更新 this.callback.call(this.$vm,this.$vm[this.$exp]) } }

使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <div v-text="name"></div>
        <button @click="handleClick">点击</button>
    </div>
</body>
</html>
<script src="./index.js"></script>
<script src="./compile.js"></script>
<script>
    let vm = new Vue({
        el:"#app",
        data:{
            name:"di",
        },
        methods:{
            handleClick(){
              this.name = 'didi'
            }
        }
    })


    console.log(vm);
</script>

效果

初次渲染

 

 点击按钮

总结 vue响应数据的底层原理

vue响应数据主要是通过Object.definePrototype+发布订阅者模式实现

Observe方法用于遍历响应数据,在每个遍历代码中new Dep订阅起,给每个响应数据添加上get方法,在get方法内部添加Dep.addDep()方法,Dep.addDep()方法内部的值是watcher订阅者,这一步是依赖的收集,给每个响应数据添加上Dep.notify()方法,用于通知订阅者数据发生了改变要进行更新了

Complie方法用于解析模板,将模板中的变量替换为数据,之后渲染视图,给每个指令对应的节点添加添加更新函数,并且初始化订阅者watcher,给watcher内部传入更新函数

watcher是observe和complie之间的桥梁,在watcher方法内部会向订阅器dep中添加一个target属性指向自己,当我们去修改响应数据的时候,会触发响应数据set中的dep.notify,dep.notify就去遍历收集到的依赖,也就是会执行watcher内部的update函数.从而完成组件的更新.这就是vue响应数据的底层实现

posted @ 2019-08-07 21:05  大笛子  阅读(257)  评论(0编辑  收藏  举报