理解Vue源码,从0开始撸了一个简版Vue

vue 的双向绑定、虚拟dom、diff算法等等面试常见问题你可能在几年前就学过了,其中有些人可能看过Vue的源码,了解过Vue是如何实现数据监听和数据绑定这些技术的。不过让从零开始实现一个 vue,你可以吗?

模板语法其实早就存在,在Vue发布之前就有了。Vue除了具备基本的模板编译功能外,新增了很多功能属性,比如数据集data、方法集methods、组件集components等等,当然还具备了数据的响应式功能,具备生命周期函数……
我想如果能够从编译功能开始逐步增加这些功能属性,是否可以体会到尤大当初开发Vue的心路历程?或者至少能够更加清楚的理解Vue源码吧。

简易版Vue基本实现思路
简易版Vue基本实现思路

在这个背景下,我按照自己的理解决定从0开始,开发一个简版的Vue:SSvue。

从类的创建开始

创建一个类,参数为对象options,里面是Vue的各种数据集。这里采用es6语法,出于兼容性考虑的话,可以使用babel做处理。

class SSvue {constructor(options) {}
}

具备数据集data、方法集methods和挂载el

class SSvue {constructor(options) {const  { data,method, el } = options// 数据集this.data = data;// 方法集this.methods = methods;// 挂载this.el = el}
}

具备一定编译功能

遵循单一职责原则,编译功能单独拿出来,创建编译类。这里的编译可以处理Mustache语法(双大括号)以及事件指令。

class SSvue {constructor(options) {const  { data,methods, el } = options// 数据集this.data = data;// 方法集this.methods = methods;// 挂载this.el = el// 编译功能new Compile(this)}
}

编译类实现。编译类实现了对元素节点和文本节点处理,能够处理其上的Mustache语法和事件指令。

class Compile {constructor(vm) {this.vm = vmthis.vm.el = document.querySelector(vm.el);this.compile();}compile() {this.replaceData(this.vm.el);const documentFragment = this.nodeToFragment(this.vm.el)this.vm.el.appendChild(documentFragment);}nodeToFragment(el) {let fragment = document.createDocumentFragment();let child;while (child = el.firstChild) {// 将Dom元素移入fragment中fragment.appendChild(child);}return fragment;}replaceData(frag) {Array.from(frag.childNodes).forEach(node => {let txt = node.textContent;let reg = /\{\{(.*?)\}\}/g;if (this.isTextNode(node) && reg.test(txt)) {let replaceTxt = () => {node.textContent = txt.replace(reg, (matched, placeholder) => {return placeholder.split('.').reduce((val, key) => {return val[key];}, this.vm);});};replaceTxt();}if (this.isElementNode(node)) {Array.from(node.attributes).forEach(attr => {if (attr.name.startsWith('@')) {const eventName = attr.name.slice(1);const methodName = attr.value;if (methodName in this.vm.methods) {node.addEventListener(eventName, this.vm.methods[methodName].bind(this.vm));}}});if (node.childNodes && node.childNodes.length) {this.replaceData(node);        }}});}// 元素节点isElementNode(node) {return node.nodeType == 1}// 文本节点isTextNode(node) {return node.nodeType == 3}
}

注意:这个时候使用SSvue,访问data中数据时,需要使用this.data[attr]方式。如果要解决这个问题需要加一层代理,访问代理。在SSvue中添加访问代理方法proxyKeys。

class SSvue {constructor(options) {const  { data,methods, el } = options// 数据集this.data = data;// 数据集代理Object.keys(this.data).forEach(key => {this.proxyKeys(key);});// 挂载this.el = el// 方法集this.methods = methods;// 编译功能new Compile(this)}// 访问代理proxyKeys(key) {Object.defineProperty(this, key, {enumerable: false,configurable: true,get: function proxyGetter() {return this.data[key];},set: function proxySetter(newVal) {this.data[key] = newVal;}});}
}

具备数据的响应式功能

增加Dep类:用于实现订阅发布模式,订阅Watcher对象,发布Watcher对象

增加Observe类:对数据集data数据进行拦截,在拦截过程中,get保存或订阅Watcher对象,set触发或者发布的Watcher对象

增加observe方法:用于对对象数据深层级进行拦截处理

增加Watcher类:用于触发Observe类数据拦截操作,然后以Dep.target为媒介,将当前Watcher对象保存到Dep对象中

总结:通过Observe类实现了对数据集的拦截,创建Watcher时触发get方法,此时Dep类订阅Watcher;设置数据集数据时,触发set方法,此时Dep类发布Watcher,触发update方法,触发回调函数,触发更新

class SSvue {constructor(options) {...this.data = options.data;Object.keys(this.data).forEach(key => {this.proxyKeys(key);});new Observe(this.data)...}
}
class Dep {constructor() {this.subs = [];}addSub(sub) {this.subs.push(sub);}notify() {this.subs.forEach(sub => sub.update());}
}class Watcher {constructor(vm, exp, fn) {this.fn = fn;this.vm = vm;this.exp = exp;Dep.target = this;let arr = exp.split('.');let val = vm;// 手动获取一次data里面的数据 执行Observe添加方法arr.forEach(key => {val = val[key];});Dep.target = null;}update() {let arr = this.exp.split('.');let val = this.vm;arr.forEach(key => {val = val[key];});this.fn(val);}
}class Observe {constructor(data) {let dep = new Dep();for (let key in data) {let val = data[key];observe(val);Object.defineProperty(data, key, {get() {Dep.target && dep.addSub(Dep.target);return val;},set(newVal) {if (val === newVal) {return;}val = newVal;observe(newVal);dep.notify();}});}}
}function observe(data) {if (!data || typeof data !== 'object') return;return new Observe(data);
}

编译类Compile增加Watcher,更新函数作为Watcher对象的回调函数

class Compile {...replaceData(frag) {Array.from(frag.childNodes).forEach(node => {let txt = node.textContent;let reg = /\{\{(.*?)\}\}/g;if (this.isTextNode(node) && reg.test(txt)) {let replaceTxt = () => {node.textContent = txt.replace(reg, (matched, placeholder) => {// 增加Watchernew Watcher(this.vm, placeholder, replaceTxt);return placeholder.split('.').reduce((val, key) => {return val[key];}, this.vm);});};replaceTxt();}if (this.isElementNode(node)) {Array.from(node.attributes).forEach(attr => {if (attr.name.startsWith('@')) {const eventName = attr.name.slice(1);const methodName = attr.value;if (methodName in this.vm.methods) {node.addEventListener(eventName, this.vm.methods[methodName].bind(this.vm));}}});if (node.childNodes && node.childNodes.length) {this.replaceData(node);        }}});}
}

具备计算属性功能

SSvue增加用于处理计算属性功能。

实际就是将计算属性数据集computed打平,将所有计算属性添加到SSvue实例对象上,同时进行拦截。使用计算属性数据时,执行get方法,执行计算属性函数。这里只是实现了基本的计算属性功能。
打平操作也说明了Vue的计算属性computed和数据集data不能有同名属性。

class SSvue {constructor(options) {....const { computed } = optionsthis.computed = computed;Object.keys(this.computed).forEach(key => {Object.defineProperty(this, key, {get: () => {return this.computed[key].call(this);}});});...
}

具备watch功能

SSvue增加用以处理watch数据集的功能。

遍历watch集合,创建Watcher对象,实际就是前面的发布订阅模式。不同的是此时的回调函数是watch里面的方法。这里也只是实现了基本功能。

class SSvue {constructor(options) {...const { watch } = optionsthis.watch = watch;// 处理watchthis.initWatch()}initWatch() {for (let key in this.watch) {new Watcher(this, key, this.watch[key]);}}
}

具备过滤器功能

增加过滤器功能。

过滤器功能就比较简单了,可以说是一种语法糖或者面向切面编程。需要拦截双大括号,判断是否有过滤器标识。然后在编译类更新内容函数replaceTxt里面添加部分代码。

class Compile {...replaceData(frag) {Array.from(frag.childNodes).forEach(node => {let txt = node.textContent;let reg = /\{\{(.*?)\}\}/g;if (this.isTextNode(node) && reg.test(txt)) {let replaceTxt = () => {node.textContent = txt.replace(reg, (matched, placeholder) => {// 判断过滤器是否存在let key = placeholder.split('|')[0].trim();let filter = placeholder.split('|')[1];if (filter) {let filterFunc = this.vm.filters[filter.trim()];if (filterFunc) {new Watcher(this.vm, key, replaceTxt);return filterFunc.call(this.vm, key.split('.').reduce((val, k) => {return val[k];}, this.vm));}} else {// 增加Watchernew Watcher(this.vm, placeholder, replaceTxt);return placeholder.split('.').reduce((val, key) => {return val[key];}, this.vm);}});};replaceTxt();}});}
}

具备组件注册功能

遵循单一职责原则,增加组件类。

组件本质上就是一个特殊的SSvue实例。这里参照Vue,在SSvue中创建静态方法extend,用以生成创建组件的构造函数。

class SSvue {constructor(options) {this._init(options)}..._init(options){if(!options){return}const { data, methods, el, computed, components, watch, filters, template } = optionsthis.data = data;this.methods = methods;this.computed = computed;this.watch = watch;this.filters = filters;this.components =components;this.template = templatethis.el = elObject.keys(this.data).forEach(key => {this.proxyKeys(key);});// 注意如果没有计算属性会报错this.computed && Object.keys(this.computed).forEach(key => {Object.defineProperty(this, key, {get: () => {return this.computed[key].call(this);}});});new Observe(this.data)this.initWatch()// 创建编译模板以及编译赋值new Compile(this);}// 增加extend方法static extend(options) {const Super = this;// 闭包保存optionsconst Sub = (function (){return function VueComponent() {let instance = new Super(options);Object.assign(this, instance);}})()// 创建一个基于SSvue的构造函数Sub.prototype = Object.create(Super.prototype);Sub.prototype.constructor = Sub;// 合并参数Sub.options = Object.assign({}, Super.options, options);return Sub;}
}

编译类Compile增加处理自定义组件功能。

着重说明一下,这里自定义组件使用的模板是template属性。

SSvue本身没有使用template属性,而采用的是查询挂载el下面的dom结构。所以自定义组件的template需要单独处理。增加单独处理方法handleTemplate。然后replaceData方法里增加创建组件功能。

class Compile {constructor(vm) {this.vm = vm// 兼容处理组件this.vm.el = this.handleTemplate(vm)this.compile();}handleTemplate(vm){if(vm.el && typeof vm.el === 'string') {return document.querySelector(vm.el)}// 将字符串转为domconst div = document.createElement('div')div.innerHTML = vm.templatereturn div.firstChild}...replaceData(frag) {Array.from(frag.childNodes).forEach(node => {...if (this.isElementNode(node)) {...let nodeName = node.nodeName.toLowerCase();// 如果存在components 则创建组件if (this.vm.components && this.vm.components[nodeName]) {let ComponentConstructor = this.vm.components[nodeName];let component = new ComponentConstructor();node.parentNode.replaceChild(component.el, node);}if (node.childNodes && node.childNodes.length) {this.replaceData(node);        }}});}// 元素节点isElementNode(node) {return node.nodeType == 1}// 文本节点isTextNode(node) {return node.nodeType == 3}
}

组件注册功能给我的启发比较大。之前一直不理解为什么Vue可以做到局部更新。写了简版的Vue,明白组件实际是特殊的Vue实例,组件本身就有一套更新机制,组件本身就是局部更新。

具备Vuex功能

Vuex本质上Vue的实例属性,而且只能是Vue而不能是组件的,否则就不能全局使用了。将其独立出来,创建Store类。

class Store {constructor(options) {this.state = options.state;this.mutations = options.mutations;this.actions = options.actions;}commit(type, payload) {this.mutations[type](this.state, payload);}dispatch(type, payload) {this.actions[type]({ commit: this.commit.bind(this), state: this.state }, payload);}
}

使用时创建实例,作为SSvue实例的参数。同时需要将store中的state数据加入Observe类中,让其具备响应式特性。

let store = new Store({state: {count: 0},mutations: {increment(state) {state.count++;}},actions: {increment(context) {context.commit('increment');}}
});...
class SSvue {constructor(options) {this._init(options)}_init(options){if(!options){return}const { data, methods, el, computed, components, watch, filters, template, store } = options...this.store = storeObject.keys(this.data).forEach(key => {this.proxyKeys(key);});this.computed && Object.keys(this.computed).forEach(key => {Object.defineProperty(this, key, {get: () => {return this.computed[key].call(this);}});});new Observe(this.data)// 响应式功能this.store && new Observe(this.store.state)this.initWatch()// 创建编译模板以及编译赋值new Compile(this);}
}

具备插件注册功能

SSvue增加静态方法use,用于接收插件。实际就是运行插件里的install方法,将不同种类的插件加到SSvue上,以原型的形式、静态数据形式或其他。

class SSvue {constructor(options) {// ...this.plugins = [];}static use(plugin) {plugin.install(this);}
}
const MyPlugin = {install(ssvue) {// 在这里添加你的插件代码// 你可以添加全局方法或属性ssvue.myGlobalMethod = function () {console.log('This is a global method');}// 添加实例方法ssvue.prototype.$myMethod = function (methodOptions) {// 一些逻辑……}}
}new SSvue({el: '#app',data: {message: 'Hello Vue!'}
});SSvue.use(MyPlugin);

具备生命周期函数

生命周期就简单了,生命周期是切面编程的体现。只需要在对应时机或者位置加上生命周期函数就可以了。

SSvue类增加处理生命周期函数方法_callHook,以及SSvue实例增加对应的生命周期方法beforeCreate和mounted。

class SSvue {constructor(options) {this._init(options)}_callHook(lifecycle) {this.$options[lifecycle] && this.$options[lifecycle].call(this);}_init(options){if(!options){return}...// 创建编译模板以及编译赋值this._callHook('beforeCreate');new Compile(this);this._callHook('mounted')}
}

编译类Compile增加created生命周期

class Compile {constructor(vm) {...this.compile();this.vm._callHook('created');}
}

测试用例和所有功能代码

测试用例

let MyComponent = SSvue.extend({template: '<div>这是一个组件{{message}}</div>',data:{message: 'Hello, Component!'}// ...})let store = new Store({state: {count: 0},mutations: {increment(state) {state.count++;}},actions: {increment(context) {context.commit('increment');}}});const ssvue = new SSvue({el: '#app',store,data: {name: 'canf1oo'},components: {'my-component': MyComponent},computed: {computedName(){return  this.name + '我是计算属性'}},filters: {addSSvue(val){return val + 'SSvue'}},watch: {name(){console.log('测试室测试试试')},computedName(){console.log('测试室测试试试12232323')}},methods: {clickMe() {this.name = 'click me'this.store.commit('increment');this.$plugin()}},beforeCreate() {console.log('beforeCreate')},created() {console.log('created')},mounted() {console.log('mounted')},});const MyPlugin = {install(ssvue) {// 在这里添加你的插件代码// 你可以添加全局方法或属性ssvue.myGlobalMethod = function () {console.log('This is a global method');}// 添加实例方法ssvue.prototype.$plugin = function (methodOptions) {// 一些逻辑……console.log('我是插件')}}}SSvue.use(MyPlugin)

SSvue测试模板

<div id="app"><button @click="clickMe">{{name}}</button><button @click="clickMe">{{name | addSSvue}}</button><button>{{computedName}}</button><span>{{store.state.count}}</span><my-component></my-component></div>

代码地址:github

整体上没有路由功能,所以是一个静态的非单页面的简版Vue。距离真实的Vue还差很远,比如props,比如render函数,比如插槽,比如作用域插槽,比如vdom等等。感兴趣的可以继续添加。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/176646.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux信号量以及基于环形队列的生产者消费者模型

文章目录 信号量信号量的接口初始化销毁等待信号量发布信号量 环形队列结合信号量设计模型 实现基于环形队列的生产者消费者模型Task.hppRingQueue.hppmain.cc效果对于多生产多消费的情况 信号量 信号量的本质是一个计数器 首先一份公共资源在实际情况中可能存在不同的线程可…

STM32——端口复用与重映射概述与配置(HAL库)

文章目录 前言一、什么是端口复用&#xff1f;什么是重映射&#xff1f;有什么区别&#xff1f;二、端口复用配置 前言 本篇文章介绍了在单片机开发过程中使用的端口复用与重映射。做自我学习的简单总结&#xff0c;不做权威使用&#xff0c;参考资料为正点原子STM32F1系列精英…

(免费领源码)python#django#mysql公交线路查询系统85021- 计算机毕业设计项目选题推荐

摘 要 本论文主要论述了如何使用django框架开发一个公交线路查询系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述该系统的当前背景以及系统开发的目的&#xff0c;后续章节将严格按…

LeetCode(15)分发糖果【数组/字符串】【困难】

目录 1.题目2.答案3.提交结果截图 链接&#xff1a; 135. 分发糖果 1.题目 n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求&#xff0c;给这些孩子分发糖果&#xff1a; 每个孩子至少分配到 1 个糖果。相邻两个孩子评分更高的孩子会获…

高级着色语言(HLSL)

High-Level Shading Language&#xff0c;简称为HLSL&#xff0c;可以使用HLSL编写顶点着色器和像素着色器程序&#xff0c;简要地说&#xff0c;顶点着色器和像素着色器就是我们自行编写的一些规模较小的定制程序&#xff0c;这些定制程序可取代固定功能流水线中某一功能模块&…

“技能兴鲁”职业技能大赛-网络安全赛项-学生组初赛 WP

Crypto BabyRSA 共模攻击 题目附件&#xff1a; from gmpy2 import * from Crypto.Util.number import *flag flag{I\m not gonna tell you the FLAG} # 这个肯定不是FLAG了&#xff0c;不要交这个咯p getPrime(2048) q getPrime(2048) m1 bytes_to_long(bytes(flag.e…

数据结构:树的基本概念(二叉树,定义性质,存储结构)

目录 1.树1.基本概念1.空树2.非空树 2.基本术语1.结点之间的关系描述2.结点、树的属性描述3.有序树、无序树4.森林 3.树的常考性质 2.二叉树1.基本概念2.特殊二叉树1.满二叉树2.完全二叉树3.二叉排序树4.平衡二叉树 3.常考性质4.二叉树的存储结构1.顺序存储2.链式存储 1.树 1.…

应用在唱放一体音响麦克风中的投影K歌模组

从十年前智能手机和移动互联网开始兴起&#xff0c;手机K歌&#xff08;全民K歌、唱吧等&#xff09;娱乐潮流成为年轻人的新宠&#xff0c;衍生出全新的手机K歌麦克风。到如今家庭智能电视、智能娱乐影院设备普及&#xff0c;家庭、聚会、户外K歌新娱乐潮流兴起&#xff0c;拓…

智能电网线路阻抗模拟的工作原理

智能电网线路阻抗模拟是一种通过模拟电网线路的阻抗特性来实现电网故障检测和定位的技术。智能电网系统通过安装在电网线路上的传感器&#xff0c;实时采集线路上的电流、电压等参数&#xff0c;并将这些数据传输到监控中心。监控中心接收到传感器采集的数据后&#xff0c;对数…

基于JAVA SpringBoot和HTML美食网站博客程序设计

摘要 美食网站是一个提供各种美食信息和食谱的网站&#xff0c;旨在帮助用户发现、学习和分享美食。旨在探讨美食网站在现代社会中的重要性和影响。随着互联网的普及&#xff0c;越来越多的人开始使用美食网站来获取各种美食信息和食谱。这些网站不仅提供了方便快捷的搜索功能&…

小型洗衣机哪个牌子质量好?性价比高的迷你洗衣机推荐

这两年内衣洗衣机可以称得上较火的小电器&#xff0c;小小的身躯却有大大的能力&#xff0c;一键可以同时启动洗、漂、脱三种全自动为一体化功能&#xff0c;在多功能和性能的提升上&#xff0c;还可以解放我们双手的同时将衣物给清洗干净&#xff0c;让越来越多小伙伴选择一款…