最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 实现简单版Vue

    正文概述 掘金(前端SkyRain)   2021-08-15   401

    这是我参与8月更文挑战的第8天,活动详情查看: 8月更文挑战” juejin.cn/post/698796…

    介绍

    本文目的是实现一个简易版本的Vue用以学习,也是对从网课学习的总结和复习。其中内容仅为简易实现,多有不足之处,请多多交流。

    内容拆分

    • 数据响应式处理

      • 数据拦截
      • 数组和对象的区分处理
      • 数据代理
    • 模板编译

      • 对文本的处理
      • 对元素特性(即指令)的处理
    • 页面渲染

      • 依赖收集
      • 创建watcher实例
      • 触发更新

    项目测试

    本文目的仅为简单版Vue实现,用于学习总结。所以采用html引入js方式进行测试即可。

    <!--主要代码-->
    <body>
      <div id="app">
        <p>{{counter}}</p>
        <p k-text="counter"></p>
        <p k-html="desc"></p>
      </div>
      <script src="./kvue.js"></script>
      <script>
        const app = new KVue({
          el: '#app',
          data: {
            counter: 1,
            desc: '<p>村长<span style="color:red">真棒</span></p>'
          },
          methods: {
            onclick() {
              console.log(this);
            }
          },
        })
        // 暂时可不放开
        /*setInterval(() => {
           app.counter++
        }, 1000);*/
      </script>
    </body>
    

    vue实现

    数据响应式处理

    数据拦截

    有了解过Vue框架原理的同学都知道,vue2.x中数据拦截使用的是Object.defineProperty方法,代码如下:

    function defineReactive(obj,key,val){
      Object.defineProperty(obj,key,{
        get(){
          return val;
        },
        set(newVal){
          if(newVal != val){
            val = newVal;
          }
        }
      }}
    }
    

    属性拦截

    Object.defineProperty方法的三个参数:

    • obj 要拦截的数据对象

    • key 要给数据对象添加的属性

    • {xxx} 属性描述器

      • 属性描述器分为两种:存取描述器和数据描述器,都是对象,此处为存取描述器。
    configurableenumerablevaluewritablegetset
    数据描述符可以可以可以可以不可以不可以存取描述符可以可以不可以不可以可以可以
    • 此处最重要的属性为get和set属性。相当于给obj设置了key属性,当访问obj.key时触发拦截操作,执行的是get方法;obj.key的值发生变化时执行的是set操作。
    • 因为知道了数据被读取和变化时的地方,所以我们可以在这些地方加入一些其他的操作。

    闭包写法

    在此处利用了闭包的写法。通过函数作用域内的某个函数将局部变量保留出去,即为闭包。

    • 局部作用域:val被保存在defineReactive函数中作用域
    • 通过defineProperty的get方法将val暴露出去

    闭包中的局部变量不会被释放,一直保存在内存中。所以如果对值做了修改就会发生变化。

    数组和对象的区分处理

    相对于普通类型数据,进行拦截时不需要过多考虑,直接返回即可。而对于数组和对象的数据,依据不同的处理方法,需要做不同的处理。

    class Observer {
      constructor(value) {
        if (Array.isArray(value)) {
          // xxx
        } else {
          this.walk(value);
        }
      }
      walk(obj) {
        Object.keys(obj).forEach(key => {
          defineReactive(obj, key, obj[key])
        })
      }
    }
    function observe(obj){
      if (typeof obj != 'object') {
          return obj;
      }
      new Observer(obj);
    }
    

    对象嵌套问题

    考虑到数据的值可能是嵌套对象,所以需要在数据拦截时进行递归处理。

    function defineReactive(obj,key,val){
      observe(obj);
      Object.defineProperty(obj,key,{
        get(){
          return val;
        },
        set(newVal){
          if(newVal != val){
            observe(newVal);
            val = newVal;
          }
        }
      }}
    }
    
    • observe(obj):当数据不为对象类型时,observe方法直接将数据返回,继而执行下面的数据拦截操作;当数据是对象类型时,会执行Observer类的操作去区分数组和对象,分别进行处理
    • observe(newVal):此处的设置主要是考虑到给数据直接赋值对象的操作,需要将新对象进行数据响应式之后赋给数据。

    数据代理

    这一步的操作主要是用于可以通过Vue实例直接访问data中的数据,形如:this.xxx。经过了上一步observe的操作之后,访问data中数据需要通过this.$data.xxx形式访问,比较麻烦。

    function proxy(vm) {
      Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
          get() {
            return vm.$data[key]
          },
          set(v) {
            vm.$data[key] = v;
          }
        })
      })
    }
    

    模板编译

    对于vue模板语法和指令等,很多人都是了解的,但是对于背后的逻辑实现,却是一知半解。对于模板的编译需要实现一个编译器来进行这些操作。

    class Compile {
      constructor(el, vm) {
        this.$vm = vm;
        this.$el = document.querySelector(el);
        // 执行编译
        this.compile(this.$el);
      }
      compile(el) {
        el.childNodes.forEach(node => {
          if (node.nodeType === 1) {
            // element元素
            // 遍历元素特性
            this.compileElement(node);
            // 递归
            if (node.childNodes.length > 0) {
              this.compile(node);
            }
          } else if (this.isInter(node)) {
            // text文本
            // 插值表达式
            this.compileText(node)
          }
        })
      }
    

    对文本的处理

    对文本的处理,首先需要辨别插值表达式,可以通过一个正则来区分。

    isInter(node) {
        return node.nodeType === 3 && /{{(.*)}}/.test(node.textContent);
      }
    

    之后对于插值表达式的文本替换,基于上文正则表达式可以获得插值表达式中的变量名为RegExp.$1。

    compileText(node) {
        node.textContent = this.$vm[RegExp.$1];
    }
    

    对元素特性(即指令)的处理

    对元素的处理,主要是对元素特性的处理。对特性attributes进行遍历。

    compileElement(node) {
      const attrs = node.attributes;
      Array.from(attrs).forEach(attr => {
        const attrName = attr.name;
        const attrExp = attr.value;
        if (attrName.startsWith('k-')) {
          // 对指令的处理
          const dir = attrName.substring(2);
          this[dir] && this[dir](node, attrExp);
        }
      })
    }
    // k-text
    text(node, exp) {
      node.textContent = this.$vm[exp]
    }
    // k-html
    html(node, exp) {
      node.innerHTML = this.$vm[exp];
    }
    

    两步结合

    将前两步结合一起。

    class KVue {
      constructor(options) {
        this.$options = options;
        this.$data = options.data;
        // 1.响应式
        observe(this.$data);
        // 1.1  代理:用户可以通过KVue实例直接访问data中数据
        proxy(this)
        // 2.编译:传入宿主元素el和组件实例this
        new Compile(options.el, this)
      }
    }
    

    页面渲染

    在Vue中数据与Dep、Watcher的对应关系是:1个数据 => 1个Dep => n个Watcher

    依赖收集

    根据上文的对应关系,Dep中存储着多个Watcher实例,所以应该是数组形式。同时Dep应该有一个添加方法和触发按更新的方法。

    class Dep {
      constructor() {
        // 存储所有的watcher
        this.deps = [];
      }
      addDep(watcher) {
        this.deps.push(watcher);
      }
      notify() {
        this.deps.forEach(dep => dep.update())
      }
    }
    

    创建watcher实例

    Watcher中需要有一个更新方法,另外需要注意的就是触发依赖筹集的地方,在下一步综合述说。

    class Watcher {
      constructor(vm, key, fn) {
        // fn : 更新函数
        this.vm = vm;
        this.key = key;
        this.fn = fn;
    
        // 触发依赖收集:读取一次key
        Dep.target = this;  // 保存当前实例
        this.vm[this.key];  // 读取一次key,触发getter
        Dep.target = null;
      }
      update() {
        this.fn.call(this.vm, this.vm[this.key])
      }
    }
    

    触发更新

    这一步的实质就是对Dep和Watcher的应用。

    Dep应用

    首先需要确定dep实例应该被创建的位子在哪里,由 1个数据 => 1个Dep 的关系可以确定,dep创建应与数据拦截在一处。

    unction defineReactive(obj, key, val) {
      observe(val);
      //  创建对应的Dep实例
      const dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          console.log('get', key, val);
          //建立映射关系
          // Dep.target就是watcher实例,自行触发getter并将自身填入dep中
          Dep.target && dep.addDep(Dep.target);
          return val;
        },
        set(newVal) {
          console.log('set', key, newVal);
          if (newVal != val) {
            // 新设置的值也可能是对象:解决直接给已有属性赋值对象问题
            observe(newVal)
            val = newVal;
            dep.notify();
          }
        }
      })
    }
    
    • const dep = new Dep()

    创建和数据对应的dep实例

    • dep.notify()

    这里是对数据的属性值进行修改后,需要对应的dep实例通知相关Watcher实例进行更新。

    • Dep.target && dep.addDep(Dep.target)

    这里是需要配合Watcher中的一段代码进行解释。

    // 触发依赖收集:读取一次key
    Dep.target = this;  // 保存当前实例
    this.vm[this.key];  // 读取一次key,触发getter
    Dep.target = null;
    

    Dep.target即为Watcher实例,下一步读取数据的key时会触发数据拦截的get方法,所以在这里通过dep实例的addDep方法将Watcher实例填充进入数据对应的dep数组中。之后再讲Dep.target清空为null。

    Watcher应用

    Watcher和页面上应用的动态数据一一对应,所以创建Watcher实例的地方最好是在编译模板里。结合上文中模板的编译的处理步骤,我们可以建立一个统一的update方法。

    update(node, exp, dir) {
      // 获取实操函数
      const fn = this[dir + 'Updater'];
      // 初始化
      fn && fn(node, this.$vm[exp]);
      // 更新
      new Watcher(this.$vm, exp, function (val) {
        fn && fn(node, val);
      });
    }
    
    // k-text
    text(node, exp) {
      this.update(node, exp, 'text')
    }
    textUpdater(node, val) {
      node.textContent = val;
    }
    // k-html
    html(node, exp) {
      this.update(node, exp, 'html')
    }
    htmlUpdater(node, val) {
      node.innerHTML = val;
    }
    // 将插值表达式编译为文本
    compileText(node) {
      this.update(node, RegExp.$1, 'text')
    } 
    

    至此,可以进行项目的调试,基本可以实现效果。


    下载网 » 实现简单版Vue

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元