• 实现一个简单的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里面的每一个节点插入到内存对象中去
            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){
            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响应数据的底层实现

  • 相关阅读:
    【Lintcode】91 最小调整代价
    【LintCode】29 交叉字符串
    十二、动态规划
    HttpClient的简单使用
    ajax跨域请求
    session共享
    八大排序算法
    MAC 脚本批量启动应用
    知识点整理-bio、nio的简单demo
    知识点整理-数组如何实现随机访问?
  • 原文地址:https://www.cnblogs.com/liuXiaoDi/p/13061487.html
Copyright © 2020-2023  润新知