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

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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270