美文网首页
只需六步,写一个属于自己的 vue!

只需六步,写一个属于自己的 vue!

作者: 梁相辉 | 来源:发表于2020-07-21 14:53 被阅读0次

    Vue 是国内目前最火的前端框架,它功能强大而又上手简单,基本成为前端工程师们的标配,但很多同学都只是停留在如何使用上,知其然不知所以然,对它内部的实现原理一知半解,今天就带领大家动手写一个类Vue的迷你库,进一步加深对Vue的理解,在前端进阶的道路上如虎添翼!

    内容摘要

    • MVVM 简介和流程分析
    • 核心入口-MyVue类的实现
    • 观察者 - Watcher 类的实现
    • 发布订阅 - Dep 类的实现
    • 数据劫持 - Observer 类的实现
    • 大功告成,运行 MyVue

    MVVM 简介和流程分析

    作为前端最火的框架之一,Vue是MVVM设计模式实现的典型代表,什么是MVVM呢?MVVM是Model-View-ViewModel的简写,M - 数据模型(Model),V - 视图层(View),VM - 视图模型(ViewModel),它本质上就是MVC 的改进版。

    MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

    mvvm 实现原理的可以用下图简略表示·

    mvvm.png

    mvvm 实现流程分析

    process.png

    本案例我们通过实现 vue 中的插值表达式解析和指令 v-model 功能来探究vue的基本运行原理。

    1. 核心入口-MyVue类的实现

    实现数据代理

    先了解些必备知识,Object.defineProperty(obj, prop, descriptor)该方法可以定义或修改对象的属性描述符

    MyVue 的创建

    class MyVue {
        constructor(option) {
            this.$el = document.querySelector(option.el);
            this.$data = option.data;
    
            if (this.$el) {
                // 1, 代理数据
                this.proxyData();
    
                // 2, 数据劫持
                // new Observer(this.$data);
    
                // 3, 编译数据
                // new Compile(this);
            }
        }
        // 代理数据,用于监听对 data 数据的访问和修改
        proxyData() {
            for (const key in this.$data) {
                Object.defineProperty(this, key, {
                    enumerable: true, // 设为false后,该属性无法被删除。
                    configurable: false, // 设为true后,该属性可以被 for...in或Object.keys 枚举到。
                    get() {
                        return this.$data[key]
                    },
                    set(newVal) {
                        this.$data[key] = newVal
                    }
                })
            }
        }
    }
    

    2. 编译模板 - Compile 类的实现

    class Compile {
      constructor(vm) {
        // 要编译的容器
        this.el = vm.$el
    
        // 挂载实例对象,方便其他实例方法访问
        this.vm = vm
    
        // 通过文档片段来编译模板
        // 1,createDocumentFragment()方法,是用来创建一个虚拟的节点对象
        // 2,DocumentFragment(以下简称DF)节点不属于文档树,它有如下特点:
        //  2-1 当把该节点插入文档树时,插入的不是该节点自身,而是它所有的子孙节点
        //  2-2 当添加多个dom元素时,如果先将这些元素添加到DF中,再统一将DF添加到页面,会减少
        // 页面的重排和重绘,进而提升页面渲染性能。
        //  2-3 使用 appendChild 方法将dom树中的节点添加到 DF 中时,会删除原来的节点
    
        // 1,获取文档片段
        const fragment = this.nodeToFragment(this.el)
    
        // 2,编译模板
        this.compile(fragment) 
    
        // 3,将编译好的子元素重新追加到模板容器中
        this.el.appendChild(fragment)
        // console.log(fragment, fragment.nodeType, this.el.nodeType)
      }
    
      // dom元素转为文档片段
      nodeToFragment(element) {
        // 1,创建文档片段
        const f = document.createDocumentFragment()
    
        // 2, 迁移子元素
        while(element.firstChild) {
          f.appendChild(element.firstChild)
        }
        
        // 3,返回文档片段 
        return f
      }
    
      // 编译方法
      compile(fragment) {
        // 1,获取所有的子节点
        const childNodes = fragment.childNodes;
    
        // 2,遍历子节点数组
        childNodes.forEach(node => {
          // 分别处理元素节点(nodeType: 1)和文档节点(nodeType:3)
          const ntype = node.nodeType
          
          if (ntype === 1) {
            // 如果是元素节点,解析指令
            this.compileElement(node)
          } else if (ntype === 3) {
            // 如果是文档节点,解析双花括号
            this.compileText(node)
          }
    
          // 如果存在子节点则递归调用 compile
          if (node.childNodes && node.childNodes.length > 0) {
            this.compile(node)
          }
        })
      }
    
      // 编译元素节点
      compileElement(node){
        // 获取元素的所有属性
        const attrs = node.attributes;
        
        Array.from(attrs).forEach(atr => {
          const {name, value} = atr
    
          if (name.startsWith('v-')) {
            // 对指令做处理
            // name == v-model
            const [, b] = name.split('-')
            if (b === 'model') {
              node.value = this.vm[value]
            }
          }
        })
      }
      // 编译文档节点
      compileText(node) {
        const con = node.textContent;
        const reg = /\{\{(.+?)\}\}/g;
    
        if (reg.test(con)) {
          const value = con.replace(reg, (...args) => {
            // console.log(args)
            return this.vm[args[1]]
          })
    
          // 更新文档节点内容
          node.textContent = value
        }
      }
    }
    

    3. 观察者 - Watcher 类的实现

    class Watcher {
      // 当观察者对应的数据发生变化时,使其可以更新视图
      constructor(vm, key, cb) {
        this.vm = vm;
        this.key = key;
        this.cb = cb;
    
        // 保存旧值
        this.oldVal = this.getOldVal()
      }
    
      getOldVal() {
        Dep.target = this
        const oldVal = this.vm[this.key]
        Dep.target = null
    
        return oldVal
      }
    
      // 更新视图
      update() {
        this.cb()
      }
    }
    // 接下来处理:1,谁来通知观察者去更新视图;2,在什么时机更新视图
    

    4. 发布订阅 - Dep 类的实现

    // 收集依赖
    class Dep {
      constructor() {
        // 初始化观察者列表
        this.subs = []
      }
    
      // 收集观察者
      addSub(watcher) {
        this.subs.push(watcher)
      }
    
      // 通知观察者去更新视图
      notify() {
        this.subs.forEach(w => w.update())
      }
    }
    

    5. 数据劫持 - Observer 类的实现

    class Observer {
      constructor(data) {
        this.observer(data)
      }
    
      observer(data) {
        // 实例化依赖收集器,专门收集所有的观察者对象
        const dep = new Dep();
          
        for (const key in data) {
          let val = data[key]
          Object.defineProperty(data, key, {
            enumerable: true,
            configurable: false,
            get() {
              // 当观察者实例化的时候会访问对应属性,进而触发get函数,然后添加订阅者
              // 之后,数据的变化就会触发 set 函数,而 set 函数触发就会执行 dep.notify()
              // 从而实现,数据变化驱动视图更新。
              Dep.target && dep.addSub(Dep.target);
              return val
            },
            set(newVal) {
              val = newVal
              // 既然数据更新,视图也应该随之更新
              dep.notify()
            }
          })
        }
      }
    }
    

    6. 大功告成,运行 MyVue

    对之前代码进行调整

    • 首先是在 MyVue 中
    class MyVue {
        constructor(option) {
            this.$el = document.querySelector(option.el);
            this.$data = option.data;
    
            if (this.$el) {
                // 1, 代理数据
                this.proxyData();
    
                // 2, 数据劫持
    -           // new Observer(this.$data);
    +           new Observer(this.$data);
    
                // 3, 编译数据
    -           // new Compile(this);
    +           new Compile(this);
            }
        }
    }
    
    • 然后是在 Compile 中
    class Compile {
      ...
      // 编译元素节点
      compileElement(node){
        // 获取元素的所有属性
        const attrs = node.attributes;
        
        Array.from(attrs).forEach(atr => {
          const {name, value} = atr
    
          if (name.startsWith('v-')) {
            // 对指令做处理
            // name == v-model
            const [, b] = name.split('-')
            if (b === 'model') {
              // 将v-model 的绑定的数据解析到输入框中
              node.value = this.vm[value]
                       
    +         // 输入框内容变化,则修改数据(这一步是从视图到数据的变化,需要手动添加)
    +         node.addEventListener('input', (e) => {
    +          this.vm.$data[value] = e.target.value;
    +         });
    
    +         // 通过添加一个观察者实例对象,当数据发生任何变化则自动更新视图
    +         new Watcher(this.vm, value, () => {
    +           node.value = this.vm.$data[value];
    +         });
            }
          }
        })
      }
      // 编译文档节点
      compileText(node) {
        const con = node.textContent;
        const reg = /\{\{(.+?)\}\}/g;
    
        if (reg.test(con)) {
          const value = con.replace(reg, (...args) => {
            // console.log(args)
    +       // 通过添加一个观察者实例对象,当数据发生任何变化则自动更新视图
    +       new Watcher(this.vm, args[1], () => {
    +         node.textContent = con.replace(reg, (...args) => {
    +           return this.vm[args[1]]
    +         })
    +       })
            return this.vm[args[1]]
          })
    
          // 更新文档节点内容
          node.textContent = value
        }
      }
    }
    

    引入前面创建的五个文件,就可以 new 一个自己的vue实例对象了,赶紧去试试吧!

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>MyVue</title>
    </head>
    <body>
      <div id="app">
        <!-- v-model 指令功能的实现 -->
        <input type="text" v-model="msg">
        <div>
          <!-- 插值表达式 -->
          <p>{{msg}} --- {{info}}</p>
        </div>
      </div>
      <script src="js/Dep.js"></script>
      <script src="js/Observer.js"></script>
      <script src="js/Watcher.js"></script>
      <script src="js/Compile.js"></script>
      <script src="js/MyVue.js"></script>
      <script>
        var mv = new MyVue({
          el: "#app",
          data: {
            msg: "Hello",
            info: "World"
          }
        })
      </script>
    </body>
    </html>
    

    相关文章

      网友评论

          本文标题:只需六步,写一个属于自己的 vue!

          本文链接:https://www.haomeiwen.com/subject/tapikktx.html