重新手写一个Vue
该版把上一次的数据修改就更新全部页面改为了局部更新,相比于上一版的在数据绑定上不是简单的一个监听set再全部更新,具体见下文。
总体流程
仍然是根据自己理解来实现的绑定,相较于上一版的数据更新就全部刷新,这次改成了部分页面更改,总体流程大致如图:(字本来就丑,那个笔芯写更丑了,希望能看懂吧)
这里就从头介绍下怎样实现整个流程的
createApp
这里是整个Vue的入口,通过传入options参数会将里面的data,methods等挂载到Vue实例上,再通过代理,让对vm的属性访问转换为对vm.$data中属性的访问:
static createApp(options) {
//? 将data代理到vm上
const vm = new Proxy(new Vue(), {
get(target, p) {
if (Reflect.get(target, p)) {
return Reflect.get(target, p);
} else {
return target.$data[p]._isref ? target.$data[p].value : target.$data[p];
}
},
set(target, p, value) {
if (target[p]) {
Reflect.set(target, p, value);
} else if (target.$data[p]?._isref) {
Reflect.set(target.$data[p], "value", value);
} else {
Reflect.set(target.$data, p, value);
}
return true;
},
});
options.onBeforCreate?.call(vm);
vm.$data = options.data.call(vm);
new Observer(vm).observeData(); //! 将data的数据转为响应式
for (const key in options.methouds) {
vm.$methouds[key] = options.methouds[key].bind(vm);
}
options.onCreated?.call(vm);
return vm;
}
将data中的数据转换为响应式
这个步骤通过Observer实例中的observeData来进行,我这里通过Proxy来实现(Vue2.x中使用Object.defineProperty)。
import Dep from "./dep.js";
const dep = new Dep();
export default class Observer {
constructor(vm) {
this.vm = vm;
}
observeData() {
const data = this.vm.$data;
for (const key in data) {
data[key] = this.ref(data[key]);
}
}
// *===============↓ 将数据转换为响应式数据的方法 ↓===============* //
reactive(data) {
//? 如果对象里还有对象,递归实现响应式
for (const key in data) {
if (typeof data[key] === "object") {
data[key] = this.reactive(data[key]);
}
}
return new Proxy(data, {
get(target, p) {
window.target && dep.add(window.target);
window.target = null; //? 将watch实例保存后删除
return Reflect.get(target, p);
},
//todo 修改对象属性后修改Vnode
set(target, p, value) {
target._isref
? Reflect.set(target, "value", value)
: Reflect.set(target, p, value);
dep.notify();
return true;
},
});
}
ref(data) {
//? 基本数据类型会被包装为对象再进行代理
if (typeof data != "object") {
data = {
value: data,
_isref: true,
toSting() {
return this.value;
},
};
}
return this.reactive(data);
}
}
这里在get上设置了dep.add,在第一次渲染页面的时候会读取到对应的$data中的属性,在这个时候将这个属性的位置和一个用来更新视图的回调函数打包进Watcher的实例再放入dep中储存起来,在以后数据更新时会触发set,通知dep调用储存的所有watcher实例上的update方法,update方法会比较储存的旧值来决定是否触发回调函数来更新视图。
Dep:
import { nextTick } from "./util.js";
export default class Dep {
constructor() {
this.watchers = [];
this.lock = true;
}
add(watcher) {
this.watchers.push(watcher);
}
notify() {
//? 放入微任务队列,只要触发一次notify就不再触发,在微任务里更新视图,这样所有数据都更新后再触发更新
if (this.lock) {
this.lock = false;
nextTick(() => {
this.watchers.forEach((watcher) => {
watcher.update(); //? 用watcher实例的update更新视图
});
this.lock = true;
});
}
}
}
Watcher:
import { getByPath } from "./util.js";
export default class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key; //? 代表该数据在$data哪里的字符串
this.cb = cb; //? 更新页面的回调函数
window.target = this;
//! 获得旧数据,同时触发vm[key]的get把上面一行设置watcher实例push进dep 见observer.js
this.oldValue = getByPath(vm, key);
}
//? dep调用notify来调用所有的update更新视图
update() {
let newValue = getByPath(this.vm, this.key);
if (newValue === this.oldValue) return;
this.oldValue = newValue;
this.cb(newValue);
}
}
为了使用方便,这里把Watcher的实例化过程挂载到vm上,实例化Watcher并推入dep的过程全由vm.$watche完成:
class Vue {
constructor() {
this.$watch = function (key, cb) {
new Watcher(this, key, cb);
};
}
}
页面渲染
通过修改原来的第一版渲染函数,这里改为了挨个读取节点来转换,通过读取每个节点的字符串形式来把数据替换或把方法挂载:
export default function render($el, vm) {
const nodes = $el.children;
Array.prototype.forEach.call(nodes, (el) => {
if (el.children.length > 0) {
render(el, vm); //? 递归渲染子节点
} else {
renderTemplate(vm, el);
}
});
}
function renderTemplate(vm, el) {
renderData(vm, el);
renderEvent(vm, el);
renderVModel(vm, el);
}
//? 将{{}}里的数据渲染
function renderData(vm, el) {
const nodeText = el.textContent;
const regexp = /\{\{(\s*)(?<data>.+?)(\s*)\}\}/g;
if (regexp.test(nodeText)) {
return nodeText.replace(regexp, (...arg) => {
const groups = JSON.parse(JSON.stringify(arg.pop()));
//! 将这个数据相对于vm的位置储存进dep,每次dep收到更新时触发回调
vm.$watch(groups.data, (newValue) => {
el.textContent = newValue;
});
el.textContent = getByPath(vm, groups.data);
});
}
}
... ...
再说明一下,现在的渲染操作只在进行mount的时候会执行,当以后$data属性改变时会触发在这里设置的回调函数,通过它来修改页面。
一些其它细节的地方
在页面渲染时读取$data属性只能通过写在模板上的字符串,这里用了reduce方法来获取字符串对应的值:
export function getByPath(obj, path) {
const pathArr = path.split(".");
return pathArr.reduce((result, curr) => {
return result[curr];
}, obj);
}
nextTick函数在这里只是用了开启微任务队列的方式实现:
export function nextTick(cb, ...arg) {
Promise.resolve().then(() => {
cb(...arg);
});
}
测试
最后简单写个计数器来看看实现的所有功能,可以看到和预期的一样
重新手写一个Vue的更多相关文章
- 写一个vue组件
写一个vue组件 我下面写的是以.vue结尾的单文件组件的写法,是基于webpack构建的项目.如果还不知道怎么用webpack构建一个vue的工程的,可以移步到vue-cli. 一个完整的vue组件 ...
- 写一个Vue loading 插件
什么是vue插件? 从功能上说,插件是为Vue添加全局功能的一种机制,比如给Vue添加一个全局组件,全局指令等: 从代码结构上说,插件就是一个必须拥有install方法的对象,这个方法的接收的第一个参 ...
- 如何优雅的写一个Vue 的弹框
写Vue或者是react 都会遇见弹框的问题.也尝试了多种办法来写弹框,一直都不太满意,今天特地看了一下 Element UI 的源码,模仿着写了一个简易版. 大概有一下几个问题: 1.弹框的层级问题 ...
- 面试题:你能写一个Vue的双向数据绑定吗?
在目前的前端面试中,vue的双向数据绑定已经成为了一个非常容易考到的点,即使不能当场写出来,至少也要能说出原理.本篇文章中我将会仿照vue写一个双向数据绑定的实例,名字就叫myVue吧.结合注释,希望 ...
- 写一个vue的滚动条插件
组件源码如下: vue-scroll.vue <template> <div class="vue-scroll" ref="vueScrollW&qu ...
- 学习如何写一个vue插件【入门篇】
#### 疑答 1.市面上已经有那么多插件可用,为什么还要造轮子?学习.借鉴思想.应用到开发 2.能否在项目中使用?与网上插件使用相同 更新维护问题怎么解决? 自身动力,使用者反馈等 #### 准 ...
- 写一个简单易用可扩展vue表单验证插件(vue-validate-easy)
写一个vue表单验证插件(vue-validate-easy) 需求 目标:简单易用可扩展 如何简单 开发者要做的 写了一个表单,指定一个name,指定其验证规则. 调用提交表单方法,可以获取验证成功 ...
- 剖析手写Vue,你也可以手写一个MVVM框架
剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ...
- Java基础-继承-编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight。小车类Car是Vehicle的子类,其中包含的属性有载人数 loader。卡车类Truck是Car类的子类,其中包含的属性有载重量payload。每个 类都有构造方法和输出相关数据的方法。最后,写一个测试类来测试这些类的功 能。
#29.编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight.小车类Car是Vehicle的子类,其中包含的属性有载人数 loader.卡车类T ...
随机推荐
- pytest 给用例打标签
1.给用例添加自定义标签命令:@pytest.mark.tagname #tagname是自定义的标签名 import pytest class TestClass(): @pytest.ma ...
- 【NX二次开发】多种变换
变换的种类: uf5942 矩阵乘积变换 uf5943 平移变换 uf5944 缩放变换 uf5945 旋转变换 uf5946 镜像变换 最后使用 uf5947 实现uf5942-uf5946的变换. ...
- 裸辞闭关2个月,成功进大厂!吃透这份562页《算法知识手册》,化身offer收割机!
前言 记得我上本科的时候,我们老师一直跟我们强调:"算法才是编程的灵魂,一定要把算法学好."因为不管你是Java编程爱好者.还是python的忠实粉丝,亦或觉得PHP才是这个世界最 ...
- Go语言判断一个字节的高位大于四
Go语言判断一个字节的高位大于四 1.步骤: 第一步,将该字节的低位清零(与0xF0进行&运算) 为了后面与0x40比较 0xF0转为二进制是1111 0000,&运算(两个同时为1, ...
- 使用pdb进行Python调试
调试应用有时是一个不受欢迎的工作,当你长期编码之后,只希望写的代码顺利运行.但是,很多情况下,我们需要学习一个新的语言功能或者实验检测新的方法,从而去理解其中运行的机制原理. 即使不考虑这样的场景,调 ...
- 【Azure 应用服务】Azure Function集成虚拟网络,设置被同在虚拟网络中的Storage Account触发,遇见Function无法触发的问题
一切为了安全,所有的云上资源如支持内网资源访问,则都可以加入虚拟网络 问题描述 使用Azure Function处理Storage Account中Blob 新增,更新,删除等情况.Storage A ...
- SpringBoot缓存管理(一) 默认缓存管理
前言 Spring框架支持透明地向应用程序添加缓存对缓存进行管理,其管理缓存的核心是将缓存应用于操作数据的方法(包括增删查改等),从而减少操作数据的执行次数(主要是查询,直接从缓存中读取数据),同时不 ...
- 24、mysql数据库优化
24.1.如何判断网站慢的排查顺序: 客户端->web->nfs->数据库: 24.2.uptime命令详解: [root@backup ~]#uptime 13:03:23 up ...
- nginx用Certbot配置免费SSL证书(ngx_http_ssl_module模块)
一.准备工作 1.先安装nginx https://files.cnblogs.com/files/blogs/676936/nginx-1.18.0.sh #nginx-1.18.0版安装脚本2.在 ...
- k8s部署docker容器
一.环境 需机器已部署好k8s和docker的环境 二.操作步骤 1.将制作好的镜像推送到docker仓库 docker tag nginx:test harbor:test-nginx docker ...