该版把上一次的数据修改就更新全部页面改为了局部更新,相比于上一版的在数据绑定上不是简单的一个监听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的更多相关文章

  1. 写一个vue组件

    写一个vue组件 我下面写的是以.vue结尾的单文件组件的写法,是基于webpack构建的项目.如果还不知道怎么用webpack构建一个vue的工程的,可以移步到vue-cli. 一个完整的vue组件 ...

  2. 写一个Vue loading 插件

    什么是vue插件? 从功能上说,插件是为Vue添加全局功能的一种机制,比如给Vue添加一个全局组件,全局指令等: 从代码结构上说,插件就是一个必须拥有install方法的对象,这个方法的接收的第一个参 ...

  3. 如何优雅的写一个Vue 的弹框

    写Vue或者是react 都会遇见弹框的问题.也尝试了多种办法来写弹框,一直都不太满意,今天特地看了一下 Element UI 的源码,模仿着写了一个简易版. 大概有一下几个问题: 1.弹框的层级问题 ...

  4. 面试题:你能写一个Vue的双向数据绑定吗?

    在目前的前端面试中,vue的双向数据绑定已经成为了一个非常容易考到的点,即使不能当场写出来,至少也要能说出原理.本篇文章中我将会仿照vue写一个双向数据绑定的实例,名字就叫myVue吧.结合注释,希望 ...

  5. 写一个vue的滚动条插件

    组件源码如下: vue-scroll.vue <template> <div class="vue-scroll" ref="vueScrollW&qu ...

  6. 学习如何写一个vue插件【入门篇】

    #### 疑答 1.市面上已经有那么多插件可用,为什么还要造轮子?学习.借鉴思想.应用到开发 2.能否在项目中使用?与网上插件使用相同   更新维护问题怎么解决? 自身动力,使用者反馈等 #### 准 ...

  7. 写一个简单易用可扩展vue表单验证插件(vue-validate-easy)

    写一个vue表单验证插件(vue-validate-easy) 需求 目标:简单易用可扩展 如何简单 开发者要做的 写了一个表单,指定一个name,指定其验证规则. 调用提交表单方法,可以获取验证成功 ...

  8. 剖析手写Vue,你也可以手写一个MVVM框架

    剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ...

  9. Java基础-继承-编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight。小车类Car是Vehicle的子类,其中包含的属性有载人数 loader。卡车类Truck是Car类的子类,其中包含的属性有载重量payload。每个 类都有构造方法和输出相关数据的方法。最后,写一个测试类来测试这些类的功 能。

    #29.编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight.小车类Car是Vehicle的子类,其中包含的属性有载人数 loader.卡车类T ...

随机推荐

  1. 解决redis集群版本不一致导致RDB同步失败的问题

    某天,运维反馈某两个机房的出口流量和入口流量过大,并且持续了好一段时间. 再仔细排查后发现是 redis 集群的几台服流量问题,于是开始查日志. 在日志中发现出现大量的 Can't handle RD ...

  2. java后端知识点梳理——java基础

    面向对象 java三大特性 封装: 将事务封装成一个类,达到解耦,隐藏细节的效果.通过get/set等方法,封装了内部逻辑,并保留了特定的接口与外界联系. 继承: 从一个已知的类中派生出一个新的类,新 ...

  3. 「题解」PA2019 Terytoria

    本文将同步发布于: 洛谷博客: csdn: 博客园: 因为简书系统升级,所以本文未在简书上发布. 题目 题目链接:洛谷 P5987.LOJ 3320.官网. 题意概述 在二维平面直角坐标系上,有一个长 ...

  4. Django工作流

    1.Django的处理流程(没有涉及到数据库层的操作) 1-1:URL组成 http://127.0.0.1:8000/index/(协议:/ip地址:端口/主机上的目录或文件地址) 1-2:URLc ...

  5. 面试热点|理解TCP/IP传输层拥塞控制算法

    0x00.前言 通过本文你将了解到以下内容: 拥塞控制概念以及其背景 流量控制和拥塞控制的区别与联系 拥塞控制主要过程详解 伙伴们认真学习一下,让offer来得更猛烈些吧! 0x01.TCP/IP协议 ...

  6. 解析 Nebula Graph 子图设计及实践

    本文首发于 Nebula Graph 公众号 NebulaGraphCommunity,Follow 看大厂图数据库技术实践. 前言 在先前的 Query Engine 源码解析中,我们介绍了 2.0 ...

  7. 百炼 POJ2393:Yogurt factory【把存储费用用递推的方式表达】

    2393:Yogurt factory 总时间限制:  1000ms 内存限制:  65536kB 描述 The cows have purchased a yogurt factory that m ...

  8. 浅析C++的函数式编程

    前言 Java8在Java中通过lambda表达式.Stream API引入了函数式编程,那么C++中是否也支持函数式编程呢?答案是肯定的.目前关于C++进行函数式编程的语法探究的相关博客.文章并不多 ...

  9. gitlab 设置分支保护功能及取消分支保护

      使用gitlab管理员账户登录gitlab系统 进入需要分支保护的项目 进行分支保护设置 保护开发分支策略配置 保护RC送测库分支策略配置 调整分支保护策略 效果展示 取消分支保护 效果展示

  10. Custom Controller CollectionQT样式自定义 001 :SliderLineEdit 滑动输入框

    主要是继承QLineEdit类重新实现其鼠标事件,建议禁用输入框默认的菜单项. SliderLineEdit 滑动输入框 参照图形平台 Adobe系列中属性输入框 做的样式,支持点击编辑和长按鼠标拖动 ...