深入理解Vue响应式原理-手写mini-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,当数据发生变化的时候,自动进行视图的更新 javascript 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还是非入侵的数据劫持,感兴趣的可以进一步了解) ```javascript

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函数触发一个事件
  • 实现 javascript 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方法
  • 实现 javascript //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的基本结构 javascript 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
  • 实现 ```javascript /**
    • 属性
      • $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 javascript 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) - (判断是否问元素节点) `<code>javascript /** . 属性 • 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[</code>${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 ```javascript /**
      • 观察者类 */ 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() ```javascript /**
      • 属性:
      • 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中获取,有任何疑问欢迎在评论区留言。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值