重新手写一个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 ...
随机推荐
- 九、Nginx常见问题处理
优化Nginx并发量 优化软件进程数.优化软件最大并发连接数限制.优化内核连接数限制open files(临时和永久同时设置) [root@proxy ~]# ab -n 2000 -c 2000 ...
- 通过Z-Order技术加速Hudi大规模数据集分析方案
1. 背景 多维分析是大数据分析的一个典型场景,这种分析一般带有过滤条件.对于此类查询,尤其是在高基字段的过滤查询,理论上只我们对原始数据做合理的布局,结合相关过滤条件,查询引擎可以过滤掉大量不相关数 ...
- C# 扫描并读取图片中的文字
本文介绍如何通过C# 程序来扫描并读取图片中的文字,这里以创建一个.Net Core程序为例.下面是具体步骤,供参考. 程序测试环境: Visual Studio版本要求不低于2017 图片扫描工具: ...
- 数据权限筛选(RLS)的两种实现介绍
在应用程序中,尤其是在统计的时候, 需要使用数据权限来筛选数据行. 简单的说,张三看张三部门的数据, 李四看李四部门的数据:或者员工只能看自己的数据, 经理可以看部门的数据.这个在微软的文档中叫Row ...
- 精尽Spring Boot源码分析 - 内嵌Tomcat容器的实现
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
- 6、基本数据类型(list)
6.1.列表: 1.li = [1, 12, 9, "age", ["孙子涵", ["19", 10], "张涵予"], ...
- 关于PHP导出数据超时的优化
一般情况下,导出超时可能都是以下三种情况: 一.sql语句复杂,查询时间过长: 二.处理查询后数据逻辑冗余: 三.数据量过大导致响应超时. 接下来分别给出这三种情况的优化建议. 一.sql语句复杂,查 ...
- 企业如何通过CRM系统使销售周期缩短
企业为什么要缩短销售周期?因为这意味着可以节约更多开支,从而达到企业利润最大化.但是有不少企业尤其是B2B行业,销售周期都在三个月以上.通过调查发现,很多企业在客户信息和销售管道上缺乏管理和策略.Zo ...
- Docker安装单点RabbitMQ
环境准备 Centos 7.5 docker环境 安装步骤 拉取镜像 $ docker pull rabbitmq:management 说明: 获取rabbiymq镜像的时候要记得获取managem ...
- CentOS-Docker搭建GitLab
官方教程 下载镜像 $ docker pull gitlab/gitlab-ce:latest 创建相关目录 $ mkdir -p /home/gitlab/config /home/gitlab/l ...