手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法

您所在的位置:网站首页 手写发布订阅者和观察者模式 手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法

手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法

#手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法| 来源: 网络整理| 查看: 265

相关概念数据驱动数据响应式的核心原理Vue2.xVue3发布/订阅模式和观察者模式发布/订阅模式观察者模式总结分析Vue 的基本结构Vue功能结构实现Observer功能结构实现Compiler功能结构实现Dep功能结构实现Wacther功能结构实现总结Demo

相关概念

数据驱动

学习 Vue 的时候经常能看到三个词:数据响应式、双向绑定、数据驱动。

数据响应式 数据模型仅仅是普通的 JavaScript 对象,而当我们改变数据的时候,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率。 双向绑定 数据改变,视图改变;视图改变,数据改变。可以使用 v-model 在表单元素上创建双向数据绑定。 数据驱动 Vue 最独特的特性之一,开发过程仅需要关注数据本身,不需要关心数据是如何渲染到视图的。

数据响应式的核心原理

Vue2.x

官方介绍:当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。用 Object.defineProperty 定义的对象的属性,如果实现它的 getter 函数,在访问该属性时,会调用此函数;如果实现它的 setter 函数,当属性值被修改时,会调用此函数,函数被接收新值作为参数。

defineProperty 多个成员 hello // 模拟 Vue 中的 data 选项 let data = { msg: 'hello', count: 10 } // 模拟 Vue 的实例 let vm = {} proxyData(data) function proxyData(data) { // 遍历 data 对象的所有属性 Object.keys(data).forEach(key => { // 把 data 中的属性,转换成 vm 的 setter/setter Object.defineProperty(vm, key, { enumerable: true, configurable: true, get () { console.log('get: ', key, data[key]) return data[key] }, set (newValue) { console.log('set: ', key, newValue) if (newValue === data[key]) { return } data[key] = newValue // 数据更改,更新 DOM 的值 document.querySelector('#app').textContent = data[key] } }) }) } // 测试 vm.msg = 'Hello World' console.log(vm.msg)

Vue3

官方介绍:当我们从一个组件的 data 函数中返回一个普通的 JavaScript 对象时,Vue 会将该对象包裹在一个带有 get 和 set 处理程序的 Proxy 中。Proxy 是在 ES6 中引入的,它使 Vue 3 避免了 Vue 早期版本中存在的一些响应性问题。Proxy 是一个对象,它包装了另一个对象,并允许你拦截对该对象的任何交互。

Proxy hello // 模拟 Vue 中的 data 选项 let data = { msg: 'hello', count: 0 } // 模拟 Vue 实例 let vm = new Proxy(data, { // 执行代理行为的函数 // 当访问 vm 的成员会执行 get (target, key) { console.log('get, key: ', key, target[key]) return target[key] }, // 当设置 vm 的成员会执行 set (target, key, newValue) { console.log('set, key: ', key, newValue) if (target[key] === newValue) { return } target[key] = newValue document.querySelector('#app').textContent = target[key] } }) // 测试 vm.msg = 'Hello World' console.log(vm.msg)

发布/订阅模式和观察者模式

发布/订阅模式

发布订阅模式有三个角色:发布者、订阅者、信号中心。发布者不会直接把信息发给订阅者,而是传给信号中心,由信号中心按需发给订阅者。

发布订阅模式 // 事件触发器 class EventEmitter { constructor () { // { 'click': [fn1, fn2], 'change': [fn] } this.subs = Object.create(null) } // 注册事件(订阅消息) 的方法 $on (eventType, handler) { this.subs[eventType] = this.subs[eventType] || [] this.subs[eventType].push(handler) } // 触发事件(发布消息) 的方法 $emit (eventType) { if (this.subs[eventType]) { this.subs[eventType].forEach(handler => { handler() }) } } } // 测试 let em = new EventEmitter() em.$on('click', () => { console.log('click1') }) em.$on('click', () => { console.log('click2') }) em.$emit('click') // click1 click2

观察者模式

观察者模式没有中间的事件中心,只有发布者和观察者,并且发布者需要知道观察者的存在。所以发布者要存储观察者,当发布信息时,调用方法通知观察者。

观察者模式 // 发布者-目标 class Dep { constructor () { // 记录所有的订阅者 this.subs = [] } // 添加订阅者 addSub (sub) { if (sub && sub.update) { this.subs.push(sub) } } // 发布通知 notify () { this.subs.forEach(sub => { sub.update() }) } } // 订阅者-观察者 class Watcher { update () { console.log('update') } } // 测试 let dep = new Dep() let watcher = new Watcher() dep.addSub(watcher) dep.notify()

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

分析

Vue 的基本结构

首先 new 一个 Vue 实例,构造函数接收一个对象,这个对象有 el 和 data 属性。

Vue 基础结构 插值表达式 {{ msg }} {{ count }} v-text v-model let vm = new Vue({ el: '#app', data: { msg: 'Hello Vue', count: 20, items: ['a', 'b', 'c'] } })

打印出这个 Vue 实例,我们要实现的是在实例上挂载一些属性:

data中的成员以及它们的 getter 和 setter 函数。$data:这个对象里面存放了原来 data 中的成员以及它们的 getter 和 setter,这里的 setter 是真正监视数据变化的函数。$options:记录传入 Vue 构造函数的参数。$el:传入的参数中的 el 可以是选择器,也可以是 DOM 对象。$el 是一个 DOM 对象,如果传入的是选择器,需要获取对应的 DOM 对象。

一个最小版本的 Vue 的整体结构:

Vue 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter Observer 监听 data 中的所有属性,如有变动可拿到最新值并通知 Dep Compiler 解析每个元素中的指令/插值表达式,并替换成相应的数据 Dep 添加观察者,当数据变化时通知观察者 Watcher 数据变化时更新视图

Vue

功能 负责接收初始化的参数(选项)负责把 data 的属性注入到 Vue 实例中,转换成 getter/setter负责调用 observer 监听 data 中所有属性的变化负责调用 compiler 解析指令/插值表达式

结构 Vue------------+ $options+ $el+ $data-------------- _proxyData()

实现class Vue { constructor (options) { // 1. 通过属性保存选项的数据 this.$options = options || {} this.$data = options.data || {} this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 2. 把data中的成员转换成getter和setter,并注入到vue实例中 this._proxyData(this.$data) // 3. 调用observer对象,监听数据的变化 new Observer(this.$data) // 4. 调用compiler对象,解析指令和差值表达式 new Compiler(this) } _proxyData (data) { // 遍历data中的所有属性 Object.keys(data).forEach(key => { // 把data的属性注入到vue实例中 Object.defineProperty(this, key, { enumerable: true, configurable: true, get () { return data[key] }, set (newValue) { if (newValue === data[key]) { return } data[key] = newValue } }) }) }}

Observer

功能 负责把 data 选项中的属性转换成响应式数据data 中的某个属性也是对象,把该属性也转换成响应式数据数据变化发送通知

结构 Observer---------------+ walk(data)+ defineReactive(data, key, value)

实现class Observer { constructor (data) { this.walk(data) } walk (data) { // 1. 判断data是否是对象 if (!data || typeof data !== 'object') { return } // 2. 遍历data对象的所有属性 Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } defineReactive (obj, key, val) { let that = this // 负责收集依赖,并发送通知 let dep = new Dep() // 如果val是对象,把val内部的属性转换成响应式数据 this.walk(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get () { // 收集依赖 Dep.target && dep.addSub(Dep.target) return val }, set (newValue) { if (newValue === val) { return } val = newValue that.walk(newValue) // 1. 这里直接使用this指向的是data 2.当值从一个基础类型改成对象,对象里的属性也要转换 getter/setter // 发送通知 dep.notify() } }) }}

Compiler

功能 负责编译模板,解析指令/插值表达式负责页面的首次渲染当数据变化后重新渲染视图

结构 Compiler---------------+ el+ vm--------------+ compile(el)+ compileElement(node)+ compileText(node)+ isDirective(attrName)+ isTextNode(node)+ isElementNode(node)

实现class Compiler { constructor (vm) { this.el = vm.$el this.vm = vm this.compile(this.el) } // 编译模板,处理文本节点和元素节点 compile (el) { let childNodes = el.childNodes Array.from(childNodes).forEach(node => { // 处理文本节点 if (this.isTextNode(node)) { this.compileText(node) } else if (this.isElementNode(node)) { // 处理元素节点 this.compileElement(node) } // 判断node节点,是否有子节点,如果有子节点,要递归调用compile if (node.childNodes && node.childNodes.length) { this.compile(node) } }) } // 编译元素节点,处理指令 compileElement (node) { // console.log(node.attributes) // 遍历所有的属性节点 Array.from(node.attributes).forEach(attr => { // 判断是否是指令 let attrName = attr.name if (this.isDirective(attrName)) { // v-text --> text attrName = attrName.substr(2) let key = attr.value this.update(node, key, attrName) } }) } update (node, key, attrName) { let updateFn = this[attrName + 'Updater'] updateFn && updateFn.call(this, node, this.vm[key], key) } // 处理 v-text 指令 textUpdater (node, value, key) { node.textContent = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model modelUpdater (node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // 双向绑定 node.addEventListener('input', () => { this.vm[key] = node.value }) } // 编译文本节点,处理差值表达式 compileText (node) { // console.dir(node) // {{ msg }} let reg = /\{\{(.+?)\}\}/ let value = node.textContent if (reg.test(value)) { let key = RegExp.$1.trim() node.textContent = value.replace(reg, this.vm[key]) // 创建watcher对象,当数据改变更新视图 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } // 判断元素属性是否是指令 isDirective (attrName) { return attrName.startsWith('v-') } // 判断节点是否是文本节点 isTextNode (node) { return node.nodeType === 3 } // 判断节点是否是元素节点 isElementNode (node) { return node.nodeType === 1 }}

Dep

功能 收集依赖,添加观察者(watcher)通知所有观察者

结构 Dep---------------+ subs--------------+ addSub(sub)+ notify()

实现class Dep { constructor () { // 存储所有的观察者 this.subs = [] } // 添加观察者 addSub (sub) { if (sub && sub.update) { this.subs.push(sub) } } // 发送通知 notify () { this.subs.forEach(sub => { sub.update() }) }}

Wacther

功能 当数据变化触发依赖,dep 通知所有的 Watcher 实例更新视图自身实例化的时候往 dep 对象中添加自己

结构 Watcher---------------+ vm+ key+ cb+ oldValue--------------+ update()

实现class Watcher { constructor (vm, key, cb) { this.vm = vm // data中的属性名称 this.key = key // 回调函数负责更新视图 this.cb = cb // 把watcher对象记录到Dep类的静态属性target Dep.target = this // 触发get方法,在get方法中会调用addSub this.oldValue = vm[key] Dep.target = null } // 当数据发生变化的时候更新视图 update () { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) }}

总结

Vue 记录传入的选项,设置 $data/$el把 data 的成员注入到 Vue 实例负责调用 Observer 实现数据响应式处理(数据劫持)负责调用 Compiler 编译指令/插值表达式等 Observer 数据劫持 负责把 data 中的成员转换成 getter/setter负责把多层属性转换成 getter/setter如果给属性赋值为新对象,把新对象的成员设置为 getter/setter 添加 Dep 和 Watcher 的依赖关系数据变化发送通知 Compiler 负责编译模板,解析指令/插值表达式负责页面的首次渲染过程当数据变化后重新渲染 Dep 收集依赖,添加订阅者(watcher) 通知所有订阅者 Watcher 自身实例化的时候往dep对象中添加自己当数据变化dep通知所有的 Watcher 实例更新视图

Demo Mini Vue 差值表达式 {{ msg }} {{ count }} v-text v-model let vm = new Vue({ el: '#app', data: { msg: 'Hello Vue', count: 100, person: { name: 'zs' } } }) console.log(vm.msg) // vm.msg = { test: 'Hello' } vm.test = 'abc'


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3