热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

手写简单Vue

原理在创建Wvue示例的时候,将挂载在实例中的参数通过数据劫持来做一层代理。使得在访问或者赋值时候可以进行更多的操作。通过挂载在参数中的el参数来获取HTML,并且用compile

原理

《手写简单Vue》

在创建Wvue示例的时候,将挂载在实例中的参数通过数据劫持来做一层代理。使得在访问或者赋值时候可以进行更多的操作。通过挂载在参数中的el参数来获取HTML,并且用compile进行分析,将具有特殊含义的,比如{{}}中的内容,@click定义的事件等进行单独的处理。
{{}}中绑定的数据,每一个都用watch监听。也就是在每一个new Wvue()实例中挂载的data里面的变量,在template中每运用一次,就将会被一个watch监听。而每一个变量的watch都将有一个Dep去统一管理。当变量变化之后,Dep会通知所有的watch去执行之前在watch中绑定的回调函数。从而实现修改data中的变量,渲染真实DOM的功能。

目标

分成三个阶段,循序渐进实现{{}}、v-model、v-html、@click功能
《手写简单Vue》

《手写简单Vue》

第一阶段

目录结构

《手写简单Vue》

index.html






{{name}}







Wvue.js

class Wvue {
constructor(option) {
this.$option = option
this.$data = option.data
this.$methods = option.methods
// 数据劫持
// 监听数据并且做代理 使得访问this.name即可访问到this.$data.name
this.observer(this.$data)
// 这一步会触发name与$data.$name的get方法 所以先回打印出get里面的内容
console.log(this.name)
// 一定时间去修改name的内容
setTimeout(() => {
console.log('数据发生变化-----------------------------')
// 在这一步只会触发name的set
this.name = '可爱米粒'
}, 2000)
}
observer(obj) {
if (!obj || typeof obj !== "object") {
return;
}
console.log('observer')
Object.keys(obj).forEach(key => {
this.defineProperty(obj, key, obj[key])
this.proxyObj(key)
})
} defineProperty(obj, key, val) {
// 如果是绑定的是对象,则用迭代的方式,继续监听对象中的数据
this.observer(val)

// Object.defineProperty() 方法会直接在一个对象上定义一个新属性,
// 或者修改一个对象的现有属性, 并返回这个对象。
Object.defineProperty(obj, key, {
get() {
console.log('defineProperty获取')
return val
},
set(newVal) {
// 采用闭包的形式,只要Wvue没有销毁,则val会一直存在
console.log('defineProperty更新了', newVal)
val = newVal
}
})
}
// 做代理 使得访问更加简洁
proxyObj(key) {
Object.defineProperty(this, key, {
get() {
console.log('proxyObj获取')
return this.$data[key]
},
set(newVal) {
console.log('proxyObj更新', newVal)
this.$data[key] = newVal
}
})

}
}

实际效果

《手写简单Vue》

用Object.defineProperty给data中的变量都设置get,set属性。对name的赋值,就会触发$data.$name的set属性。根据思路,get属性中就是收集watch放进Dep进行统一管理的地方。
另外,只要Wvue不销毁,变量的get,set属性就不会销毁。

知识点:
Object.defineProperty
闭包与内存

第二阶段

实现watch的创建与收集,修改Wvue.js

Wvue.js

class Wvue {
constructor(option) {
this.$option = option
this.$data = option.data
this.$methods = option.methods
this.observer(this.$data)
// ----------------新增Watcher实例,绑定回调方法,当收到通知,打印数据
new Watcher(this, 'name', () => {
console.log('watcher生效')
})
console.log(this.name)
setTimeout(() => {
console.log('数据发送变化-----------------------------')
this.name = '可爱米粒'
}, 2000)
}
observer(obj) {
if (!obj || typeof obj !== "object") {
return;
}
console.log('observer')
Object.keys(obj).forEach(key => {
this.defineProperty(obj, key, obj[key])
this.proxyObj(key)
})
}
defineProperty(obj, key, val) {
this.observer(val)
//---------------- 新增为每一个变量都创建管理watcher的Dep实例
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
console.log('defineProperty获取')
// 每次访问name 都会创建一个watcher,并加入到Dep中
Dep.target !== null && dep.addDep(Dep.target)
return val
},
set(newVal) {
console.log('defineProperty更新了', newVal)
val = newVal
dep.notify()
}
})
}
proxyObj(key) {
Object.defineProperty(this, key, {
get() {
console.log('proxyObj获取')
return this.$data[key]
},
set(newVal) {
console.log('proxyObj更新', newVal)
this.$data[key] = newVal
}
})

}
}
// -----------新增Watcher类 用于根据通知触发绑定的回调函数
class Watcher {
constructor(vm, key ,cb) {
this.$vm = vm
this.$key = key
this.$cb = cb
// 用一个全局变量来指代当前watch
Dep.target = this
console.log('Watcher-------')
// 实际是访问了this.name,触发了当前变量的get,
// 当前变量的get会收集当前Dep.target指向的watcher,即当前watcher
this.$vm[this.$key]
Dep.target = null
}
update() {
// 执行
this.$cb.call(this.$vm, this.$vm[this.$key])
}
}
// -----------新增Dep类 用于收集watcher
class Dep {
constructor() {
this.dep = []
}
addDep(dep) {
console.log('addDep')
this.dep.push(dep)
}
notify() {
// 通知所有的watcher执行更新
this.dep.forEach(watcher => {
watcher.update()
})
}
}

《手写简单Vue》

本阶段,在name的get属性中,将name所有的watcher用Dep实例收集起来。并在set的过程中,触发Dep中的notify方法,通知所有的watcher更新。所以我们在构造函数中,手动创建了一个watcher。在this.name=”可爱米粒”的赋值操作时,就会调用watcher中的callback,打印出数据。
然而我们的watcher不可能是手动创建的,我们平时用Vue的时候,template中{{}}中的内容,就是响应式的,所以当我们改变data中的数据的时候,界面就会重新更改。所以,很明显,每一个{{}}就需要一个watcher(在Vue1.0中,就是因为watcher太多了,导致渲染效果差,在vue2.0之后,都改为一个组件一个watcher)。于是在下一阶段,分析html的时候, 就需要加上watcher。

第三阶段

新增compile.js

class Compile {
constructor(el, vm) {
this.$vm = vm
// $el挂载的就是需要处理的DOM
this.$el = document.querySelector(el)
// 将真实的DOM元素拷贝一份作为文档片段,之后进行分析
const fragment = this.node2Fragment(this.$el)
// 解析文档片段
this.compileNode(fragment)
// 将文档片段加入到真实的DOM中去
this.$el.appendChild(fragment)
}
// https://developer.mozilla.org/zh-CN/search?q=querySelector
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node node对象
node2Fragment(el) {
// 创建空白文档片段
const fragment = document.createDocumentFragment()
let child
// appendChild会把原来的child给移动到新的文档中,当el.firstChild为空时,
// while也会结束 a = undefined => 返回 undefined
while((child = el.firstChild)) {
fragment.appendChild(child);
}
return fragment
}
// 通过迭代循环来找出{{}}中的内容,v-xxx与@xxx的内容,并且单独处理
compileNode(node) {
const nodes = node.childNodes
// 类数组的循环
Array.from(nodes).forEach(node => {
if (this.isElement(node)) {
this.compileElement(node)
} else if (this.isInterpolation(node)) {
this.compileText(node)
}
node.childNodes.length > 0 && this.compileNode(node)
});
}
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node Node.nodeType
isElement(node) {
return node.nodeType === 1;
}
// 校验是否是文本节点 并且是大括号中的内容
isInterpolation(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
compileText(node) {
const reg = /\{\{(.*?)\}\}/g
const string = node.textContent.match(reg)
// 取出大括号中的内容,并且处理
// RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
// 以此类推,RegExp.$2,RegExp.$3,..RegExp.$99总共可以有99个匹配
this.text(node, RegExp.$1)
}
compileElement(node) {
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(arr => {
if (arr.name.indexOf('v-') > -1) {
this[`${arr.name.substring(2)}`](node, arr.value)
}
if (arr.name.indexOf('@') > -1) {
// console.log(node, arr.value)
this.eventHandle(node, arr.name.substring(1), arr.value)
}
})
}
// 因为是大括号里面的内容,所以沿用之前的逻辑,都加上watcher
text(node, key) {
new Watcher(this.$vm, key, () => {
node.textCOntent= this.$vm[key]
})
// 第一次初始化界面, 不然如果不进行赋值操作,
// 就不会触发watcher里面的回调函数
node.textCOntent= this.$vm[key]
}
html(node, key) {
new Watcher(this.$vm, key, () => {
node.innerHTML = this.$vm[key]
})
node.innerHTML = this.$vm[key]

}
// 对@xxx事件的处理
eventHandle(node, eventName, methodName) {
node.addEventListener(eventName, () => {
this.$vm.$methods[methodName].call(this.$vm)
})
}
// v-modal的处理 不仅仅当赋值的时候回触发watcher,并且为input添加事件
// input中的值去修改this.$data.$xxx的值,实现双向绑定
modal(node, key) {
console.log(node.value)
new Watcher(this.$vm, key, () => {
node.value = this.$vm[key]
})
node.value = this.$vm[key]
node.addEventListener('input', (e) => {
this.$vm[key] = e.target.value
})
}
}

Wvue.js 中Wvue的构造函数

constructor(option) {
this.$option = option
this.$data = option.data
this.$methods = option.methods
this.observer(this.$data)
// -------------- 删除原来的手动调用watcher
// ---------------新增对HTML的解析与处理
// ---------------在这个方法中增加watche 还要将当前this指向传入
new Compile(option.el, this)
}

index.html



因为wvue.js中有对compile的引用,所以引入顺序很关键。
《手写简单Vue》][10]

重置之前,修改input框,会影响{{}}与v-html中绑定的值
《手写简单Vue》

重置之后


推荐阅读
  • 本文详细介绍了一种利用 ESP8266 01S 模块构建 Web 服务器的成功实践方案。通过具体的代码示例和详细的步骤说明,帮助读者快速掌握该模块的使用方法。在疫情期间,作者重新审视并研究了这一未被充分利用的模块,最终成功实现了 Web 服务器的功能。本文不仅提供了完整的代码实现,还涵盖了调试过程中遇到的常见问题及其解决方法,为初学者提供了宝贵的参考。 ... [详细]
  • 题目《BZOJ2654: Tree》的时间限制为30秒,内存限制为512MB。该问题通过结合二分查找和Kruskal算法,提供了一种高效的优化解决方案。具体而言,利用二分查找缩小解的范围,再通过Kruskal算法构建最小生成树,从而在复杂度上实现了显著的优化。此方法不仅提高了算法的效率,还确保了在大规模数据集上的稳定性能。 ... [详细]
  • 在PHP中如何正确调用JavaScript变量及定义PHP变量的方法详解 ... [详细]
  • 在处理大规模数据数组时,优化分页组件对于提高页面加载速度和用户体验至关重要。本文探讨了如何通过高效的分页策略,减少数据渲染的负担,提升应用性能。具体方法包括懒加载、虚拟滚动和数据预取等技术,这些技术能够显著降低内存占用和提升响应速度。通过实际案例分析,展示了这些优化措施的有效性和可行性。 ... [详细]
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 在 Vue 应用开发中,页面状态管理和跨页面数据传递是常见需求。本文将详细介绍 Vue Router 提供的两种有效方式,帮助开发者高效地实现页面间的数据交互与状态同步,同时分享一些最佳实践和注意事项。 ... [详细]
  • 本文全面解析了JavaScript中的DOM操作,并提供了详细的实践指南。DOM节点(Node)通常代表一个标签、文本或HTML属性,每个节点都具有一个nodeType属性,用于标识其类型。文章深入探讨了DOM节点的创建、查询、修改和删除等操作,结合实际案例,帮助读者更好地理解和掌握DOM编程技术。 ... [详细]
  • ButterKnife 是一款用于 Android 开发的注解库,主要用于简化视图和事件绑定。本文详细介绍了 ButterKnife 的基础用法,包括如何通过注解实现字段和方法的绑定,以及在实际项目中的应用示例。此外,文章还提到了截至 2016 年 4 月 29 日,ButterKnife 的最新版本为 8.0.1,为开发者提供了最新的功能和性能优化。 ... [详细]
  • 微信小程序实现类似微博的无限回复功能,内置云开发数据库支持
    本文详细介绍了如何利用微信小程序实现类似于微博的无限回复功能,并充分利用了微信云开发的数据库支持。文中不仅提供了关键代码片段,还包含了完整的页面代码,方便开发者按需使用。此外,HTML页面中包含了一些示例图片,开发者可以根据个人喜好进行替换。文章还将展示详细的数据库结构设计,帮助读者更好地理解和实现这一功能。 ... [详细]
  • 本文探讨了资源访问的学习路径与方法,旨在帮助学习者更高效地获取和利用各类资源。通过分析不同资源的特点和应用场景,提出了多种实用的学习策略和技术手段,为学习者提供了系统的指导和建议。 ... [详细]
  • JavaScript XML操作实用工具类:XmlUtilsJS技巧与应用 ... [详细]
  • 在处理木偶评估函数时,我发现可以顺利传递本机对象(如字符串、列表和数字),但每当尝试将JSHandle或ElementHandle作为参数传递时,函数会拒绝接受这些对象。这可能是由于这些句柄对象的特殊性质导致的,建议在使用时进行适当的转换或封装,以确保函数能够正确处理。 ... [详细]
  • 如何在页面底部添加倾斜样式效果? ... [详细]
  • 汽车电子架构与CAN网络基础解析——鉴源实验室专业解读 ... [详细]
author-avatar
手机用户2502910651
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有