深入理解Vue响应式原理-手写mini-vue__Vue.js
发布于 3 年前 作者 banyungong 1196 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

文章内容输出来源:拉勾教育-大前端高薪训练营

心得

不知不觉,已经连续学习两个多月了(这真是个奇迹,Amazing!)。到底是什么魔力让我这样平时上课就犯困,学习都是从入门到放弃的人,能一直坚持学习,并且还有了写博客的冲动?,我想在开头,非常有必要说一下这段时间参加“拉勾教育-大前端高薪训练营”学习的一些心得体会:

    首先是课程,说实话从我看到这门课程的大纲之后,我就觉得,靠谱,就是它了。课程总共包含了8个大模块(详见截图),几乎包含了前端工程师必备的所有技能;而且课程结构非常合理,内容循序渐进,由浅而深,非常适合我这样有一定基础和开发经验,但是知识体系不够全面,不够深入的前端工程师。可谓是确认过眼神,这就是对的课程(爱了爱了),于是就果断下手啦。两个多月学习下来,目前已经学到模块3了,真的发现,自己无论从基础知识、技术认知还是工作方法,都有了质的提升。
image.png

好的课程千千万,不坚持到底都不算(这算不算双押?哈哈)。而让我坚持学习的动力,除了课程,还有就是老师和同学们了。让我映像最深,感触最多的当然是zce(汪磊)老师了。汪老师可谓是真的把课讲活了,枯燥的知识点,在他生动的课件演示和细心讲述下,真的是想不会都很难。就拿前端工程化课程来说,平时看起来那么复杂的东西,听完汪老师的课之后,现在我已经能把plop、gulp、webpack这些工具用到实际项目开发中了,甚至还用yeoman手写了自己的脚手架,提升了团队的开发效率。除了讲课有趣,最最最主要的还是责任感,为人师表、传道受业的责任感,每次直播课,汪老师都是讲到深夜12点才结束,不给我们讲明白,不罢休。大家也可以去看看他Github,从中能学到很多东西。当然除了汪老师,还有深夜“提刀”催学习的班主任-小雪老师, 颜值导师-熊熊老师,“美女”导师-小北老师以及天天一起水群、讨论、争辩的同学们,跟优秀的人在一起学习,真的让我体会到了学习的乐趣。

正文

在手写Vue之前,有三个必要的知识点需要先讲一下,那就是数据驱动的概念、发布/订阅模式和观察者模式。

1. 数据驱动

核心概念

  • 数据响应式
    • 以普通的JavaScript对象作为数据模型,修改数据的时候,视图会自动更新,避免了繁琐的dom操作,提高开发效率
  • 双向绑定
    • 数据改变,视图改变,视图改变 ,数据也随之改变
    • 具体表现,可以通过v-model创建双向绑定
  • 数据驱动
    • 开发过程中仅需要关注数据本身,不需要关心如何渲染到视图中

核心原理

  • Vue2.x
    • 基于Object.defineProperty,对数据进行劫持,在程序启动的时候,给data属性设置getter和setter,当数据发生变化的时候,自动进行视图的更新
const data = {
  msg: 'Hello'
}
const vm = {}
Object.defineProperty(vm, 'msg', {
  //监听数据的获取
  get(key) {
    console.log('data get')
    return data.msg
  },
  //监听数据的赋值
  set(val) {
    //判断值是否相同,相同则不作任何操作
    if (data.msg === val) return
    //不相同的话,赋值,然后改变dom
    data.msg = val;
    document.getElementById('app').textContent = val;
  }
})
//触发数据的变化,更新dom
function textInput(e) {
  vm.msg = e.value;
}
  • Vue3.x
    • 使用Proxy代理对象,从而实现对数据的响应式处理(Proxy比defineProperty功能更强大,此外Proxy还是非入侵的数据劫持,感兴趣的可以进一步了解)

const data = {
  msg: "",
};
const dataProxy = new Proxy(data, {
  get(target, key) {
    return target[key];
  },
  set(target, key, value) {
    target[key] = value;
    if (key == "msg") {
      document.getElementById("app").textContent = value;
    } else {
      console.log(key + ":changed");
    }
  },
});
function textInput(target) {
  dataProxy.msg = target.value;
}

2. 发布/订阅、观察者模式

发布/订阅模式

  • 概念
    • 以一个普通的JavaScript对象为“信号中心”, 记录注册的所有事件
    • 通过$on函数注册一个事件,存储到对应的事件数组中
    • 通过$emit函数触发一个事件
  • 实现
class EventEmiter {
  constructor() {
    this.subs = Object.create(null);
  }
  // 注册事件
  $on(eventType, eventHandler) {
    this.subs[eventType] = this.subs[eventType] || [];
    this.subs[eventType].push(eventHandler);
  }
  // 触发事件
  $emit(eventType, datas) {
    this.subs[eventType] && this.subs[eventType].forEach(handler => {
      handler(datas);
    })
  }
}
//使用
const em = new EventEmiter();
//注册事件
em.$on('input', value => {
  document.getElementById('app').textContent = value;
})
//触发事件
function onTextInput(target) {
  em.$emit('input', target.value)
}

观察者模式

  • 概念
    • 观察者(订阅者,可以想象成租/购房客户,将自己的租/购房意愿告诉中介)–watcher
      • update(): 当事件发生时,具体要做的事情
    • 目标(发布者,可以想象成一个房屋中介,房屋降价、有新房源的时候发送通知给客户)–Dep
      • subs数组:存储所有的观察者
      • addSub():添加观察者
      • notify(): 当事件发生,调用所有观察者的update方法
  • 实现
//Dep类,添加观察者,通知观察者
class Dep {
  constructor() {
    this.subs = []
  }
  add(sub) {
    if (sub && sub.update && typeof sub.update == 'function') {
      this.subs.push(sub)
    }
  }
  notify(datas) {
    this.subs.forEach(sub => {
      sub.update(datas)
    })
  }
}
//观察者,接受一个回调参数,时间触发的时候调用回调函数
class Watcher {
  constructor(callback) {
    this.callback = callback;
  }
  update(datas) {
    typeof this.callback == 'function' && this.callback(datas);
  }
}
//实例化发布者
const dep = new Dep();
//实例化观察者并注册回调
const watcher = new Watcher(onInput);
//添加观察者到发布者中
dep.add(watcher);
//事件函数,事件发生是通过发布者通知所有观察者
function onTextInput(target) {
  dep.notify(target.value)
}
//回调函数
function onInput(datas) {
  document.getElementById('app').textContent = datas;
}

两者区别

  • 观察者模式是由具体的目标调度,比如当事件触发,Dep就会调用观察者的方法,所以观察者模式的订阅者与发布者之间存在依赖
  • 发布/订阅模式由统一的调度中心调用,因此发布者和订阅者不需要知道对方的存在

image.png

3. Vue响应式原理模拟

- 分析过程

  • Vue的基本结构
const vueInstance = new Vue({
  el: "#app",
  router,
  render: h => h(App)
}).$mount("#app");
  • 打印Vue实例并观察其属性

image.png

  • 整体结构

image.png

  • Vue
    • 把data中的成员注入到Vue实例,并且把data中的成员转换成getter/setter
  • Observer
    • 能够对数据对象的所有属性进行监听,如果有变动可以拿到最新值并通过Dep发送通知

- 实现Vue

image.png

a. 功能

  • 负责接收初始化参数
  • 把data中的属性注入到Vue实例,转换成getter/setter
  • 调用Observer监听data中所有属性的变化
  • 调用compiler解析执行/插值表达式

b. Vue类

  • 属性
    • $options    -构造函数传入的参数
    • $el            - 挂载的元素(选择器或者dom元素)
    • $data        - 数据
  • 方法
    • _proxyData()    - 将数据转换成getter/setter
  • 实现
/**
 * 属性
 * - $el:挂载的dom对象
 * - $data: 数据
 * - $options: 传入的属性
 * 方法:
 * - _proxyData 将数据转换成getter/setter形式
 */
class Vue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data || Object.create(null);
    this.$el =
      typeof options.el === "string"
        ? document.querySelector(options.el)
        : options.el;
    this._proxyData(this.$data);
    // 监测数据的变化,渲染视图
    new Observer(this.$data);
  }
  //   将数据代理到vue(this)中,并使数据是响应式的。使数据能通过this.xxx访问并复制
  _proxyData(data) {
    Reflect.ownKeys(data).forEach((key) => {
      Reflect.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key];
        },
        set(newValue) {
          if (newValue == data[key]) {
            return;
          }
          data[key] = newValue;
        },
      });
    });
  }
}


c. Observer

  • 功能:
    • 把$data中的属性,转换成响应式数据
    • 如果$data中的某个属性也是对象,把该属性转换成响应式数据
    • 数据变化的时候,发送通知
  • 类图
    • 方法
      • walk(data)    - 遍历data属性,调用defineReactive将数据转换成getter/setter
      • defineReactive(data, key, value)    - 将数据转换成getter/setter
import Dep from './Dep.js'
class Observer {
  constructor(data) {
    this.walk(data);
  }
  walk(data) {
    //   如果data为空或者或者data不是对象
    if (!data || typeof data !== "object") {
      return;
    }
    Reflect.ownKeys(data).forEach((key) => {
      this.defineReactive(data, key, data[key]);
    });
  }
  defineReactive(data, key, value) {
    const that = this;
    // 给每个data添加一个观察者
    const dep = new Dep();
    // 递归检测属性值是否对象,是对象的话,继续将对象转换为响应式的
    this.walk(value);
    Reflect.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 实例化Wathcer的时候,会获取并缓存对应的值,触发get,此时将watcher添加到dep
        // 获取watcher实例,并添加到dep中
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set(newValue) {
        if (newValue == value) {
          return;
        }
        // 此处形成了闭包,延长了value的作用域
        value = newValue;
        // 属性被赋予新值的时候,将检查属性是否对象,是对象则将属性转换为响应式的
        that.walk(newValue);
        // 数据变化,发送通知,触发watcher的pudate方法
        dep.notify();
      },
    });
  }
}
export default Observer

d. Compiler

  • 功能
    • 编译模板,解析指令/插值表达式
    • 负责页面的首次渲染
    • 数据变化后,重新渲染视图
  • 属性
    • el -app元素
    • vm -vue实例
  • 方法
    • compile(el) -编译入口
    • compileElement(node) -编译元素(指令)
    • compileText(node) 编译文本(插值)
    • isDirective(attrName) -(判断是否为指令)
    • isTextNode(node) -(判断是否为文本节点)
    • isElementNode(node) - (判断是否问元素节点)
/**
. 属性
    • el  -app元素
    • vm  -vue实例
• 方法
    • compile(el) -编译入口
    • compileElement(node)  -编译元素(指令)
    • compileText(node) 编译文本(插值)
    • isDirective(attrName) -(判断是否为指令)
    • isTextNode(node)  -(判断是否为文本节点)
    • isElementNode(node) - (判断是否问元素节点)
 */
import Watcher from "./Watcher.js";
class Compiler {
  constructor(vm) {
    this.vm = vm;
    this.el = vm.$el;
    this.compile(this.el);
  }
  compile(el) {
    if (!el) return;
    const nodes = el.childNodes;
    //收集
    Array.from(nodes).forEach((node) => {
      if (this.isTextNode(node)) {
        this.compileText(node);
      } else if (this.isElementNode(node)) {
        this.compileElement(node);
      }
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  }
  update(node, value, attrName, key) {
    const updateFn = this[`${attrName}Updater`];
    updateFn && updateFn.call(this, node, value, key);
  }
  textUpdater(node, value, key) {
    node.textContent = value;
  }
  modelUpdater(node, value, key) {
    node.value = value;
    node.addEventListener("input", (e) => {
      this.vm[key] = node.value;
    });
  }
  compileElement(node) {
    Array.from(node.attributes).forEach((attr) => {
      if (this.isDirective(attr.name)) {
        const attrName = attr.name.substr(2);
        const key = attr.value;
        const value = this.vm[key];
        this.update(node, value, attrName, key);
        // 数据更新之后,通过wather更新视图
        new Watcher(this.vm, key, (newValue) => {
          this.update(node, newValue, attrName, key);
        });
      }
    });
  }
  compileText(node) {
    /**
     * . 表示任意单个字符,不包含换行符
     * + 表示匹配前面多个相同的字符
     * ?表示非贪婪模式,尽可能早的结束查找
     * */
    const reg = /\{\{(.+?)\}\}/;
    var param = node.textContent;
    if (reg.test(param)) {
      //  $1表示匹配的第一个
      const key = RegExp.$1.trim();
      node.textContent = param.replace(reg, this.vm[key]);
      // 编译模板的时候,创建一个watcher实例,并在内部挂载到Dep上
      new Watcher(this.vm, key, (newValue) => {
        // 通过回调函数,更新视图
        node.textContent = newValue;
      });
    }
  }
  isDirective(attrName) {
    return attrName && attrName.startsWith("v-");
  }
  isTextNode(node) {
    return node && node.nodeType === 3;
  }
  isElementNode(node) {
    return node && node.nodeType === 1;
  }
}
export default Compiler;

4. Dep

  • 功能
    • 收集观察者
    • 触发观察者
  • 属性
    • subs:Array
    • target:Watcher
  • 方法
    • addSub(sub)    -添加观察者
    • notify()    -触发观察者的update
/**
 * 观察者类
 */
export default class Dep {
  constructor() {
    this.subs = [];
  }
  // 添加观察者
  addSub(sub) {
    if (sub && sub.update && typeof sub.update === "function") {
      this.subs.push(sub);
    }
  }
  // 发送通知
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}

5. Watcher

  • 功能
    • 生成观察者更新视图
    • 将观察者实例挂载到Dep类中
    • 数据发生变化的时候,调用回调函数更新视图
  • 属性
    • vm    -vue实例
    • key    -观察的键
    • cb    -回调函数
  • 方法
    • update()
/**
 * 属性:
 *  vm -vue实例
 *  key -观察的元素的key
 *  cb  -注册一个回调,变化的时候调用
 */
import Dep from "./Dep.js";
export default class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    // oldValue缓存之前,将watcher实例挂载到Dep
    Dep.target = this;
    // 缓存旧值
    this.oldValue = vm[key];
    // get值之后,清除Dep中的实例
    Dep.target = null;
  }
  update() {
    // 调用update的时候,获取新值
    const newValue = this.vm[this.key];
    // 比较,相同则不更新
    if (this.oldValue === newValue) {
      return;
    }
    this.cb(newValue);
  }
}

总结

那么到这里,我们的简版Vue写完了,其核心就是通过Object.defineProperty(),监听数据的获取和赋值。在获取数据(getter)的时候,给属性设置watcher,并添加到dep中;在数据赋值(setter)的时候,通过dep.notify()发送更新通知,遍历注册的watcher,调用watcher的update方法,实现视图的更新。具体代码可以从github中获取,有任何疑问欢迎在评论区留言。

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 小灰🌾🐛 原文链接:https://juejin.im/post/6854573222533267463

回到顶部