vue手写响应式_手写Vue响应式 [Object.defineProperty]

前言

这篇文章早写于 2019年11月份

使用Vue技术栈也有2年了,对里边的各种API,属性,内置组件封装可以说是非常熟练了,一直知道双向数据绑定的原理是通过数据劫持,结合发布-订阅的方式来实现的;可理论始终是理论,忍不住还是动动小手撸了一把 ; 请大家尽管吐槽吧 。

概述

我们先来看一张图,相信大家在不少的博客,贴吧,论坛等都有看到过这张图例 ,那么它到底是什么意思呢 ?下面听我细细道来 。

这张图我先做个简要的描述:

首先创建一个实例对象,分别触发了 compile 解析指令 和 observer 监听器

compile 解析指令则循环递归 解析 类似 v-model 这样的指令,初始化 data 绑定数据,同时为每个data属性创建一个订阅者 watcher , 并添加至组件唯一的 Dep 订阅器

observer 监听器 则利用了 Object.defineProperty() 方法的描述属性里边的 set,get 方法,来监听数据变化,

get 方法是在创建实例对象,生成dom节点的时候都会触发,所以:在compile 解析指令编译的时候,依次给每一个data属性添加了一个订阅者 watcher 到主题对象 Dep (Dep 订阅者集合,我们暂称为 订阅器),源码 dep.depend()

set 方法则是数据发生改变了,通知 Dep 订阅器里的对应订阅者 watcher 触发对应 update 更新视图,源码 dep.update()

简单的说明就是这样了。好吧,废话不多说直接上代码:

代码实践

解析器 Compile

可以扫描和解析每个节点的相关指令(v-model,v-on等指令),如果节点存在v-model,v-on等指令,则解析器Compile初始化这类节点的模板数据,使之可以显示在视图上,同时初始化相应的订阅者(Watcher)

/*

第一步

1,创建文档碎片,劫持所有dom节点,重绘dom节点

2,重绘dom节点,初始化文档碎片绑定数据 实现文档编译 compile

3, 为每一个节点创建一个 watcher

*/

function getDocumentFragment(node, vm){

var flag = document.createDocumentFragment();

var child;

while (child = node.firstChild) {

/*

while (child = node.firstChild)

相当于

child = node.firstChild

while (child)

*/

compile(child, vm);

flag.appendChild(child);

}

node.appendChild(flag);

}

function compile(node, vm){

/*

nodeType 返回数字,表示当前节点类型

1 Element 代表元素 Element, Text,

2 Attr 代表属性 Text, EntityReference

3 Text 代表元素或属性中的文本内容。

. . . 更多请查看文档

*/

if (node.nodeType === 1) {

// 获取当前元素的attr属性

var attr = node.attributes;

for (let i = 0; i < attr.length; i++) {

// nodeName 是attr属性 key 即名称 , 匹配自定义 v-m

if (attr[i].nodeName === 'v-m') {

// 获取当前值 即 v-m = "test" 里边的 test

let name = attr[i].nodeValue;

// 当前节点输入事件

node.addEventListener('keyup', function (e){

vm[name] = e.target.value;

});

// 页面元素写值 vm.data[name] 即 vm.data['test'] 即 MVVM

node.value = vm.data[name];

//最后移除标签中的 v-m 属性

node.removeAttribute('v-m');

// 为每一个节点创建一个 watcher

new Watcher(vm, node, name, "input");

}

}

/*

继续递归调用 文档编译 实现 视图更新 ;

*/

if (child = node.firstChild) {

/*

if (child = node.firstChild)

相当于

child = node.firstChild

id(child)

*/

compile(child, vm);

}

}

if (node.nodeType === 3) {

let reg = /\{\{(.*)\}\}/;

if (reg.test(node.nodeValue)) {

let name = RegExp.$1.trim();

node.nodeValue = vm.data[name];

// 为每一个节点创建一个 watcher

new Watcher(vm, node, name, "text");

}

}

}

复制代码

监听器 Observer

用来劫持并监听所有属性,如果有变动的,就通知订阅者

/*

第二步

1,获取当前实例对象的 data 属性 key

observer(当前实例对象 data ,当前实例对象)

2,使用 Object.defineProperty 方法 实现监听

*/

function observe(data, vm){

Object.keys(data).forEach(function (key){

defineReactive(vm, key, data[key]);

});

}

function defineReactive(vm, key, val){

/*

Object.defineProperty

obj

要在其上定义属性的对象。

prop

要定义或修改的属性的名称。

descriptor

将被定义或修改的属性描述符。 描述符有很多,就包括我们要市用 set , get 方法

*/

var dep = new Dep();

Object.defineProperty(vm, key, {

get: function (){

/*

if (Dep.target) dep.addSub(Dep.target);

看到这段代码不要差异,生成每一个 dom节点,都会走 get 方法

这里为每一个节点添加一个订阅者到主题对象 Dep

*/

if (Dep.target) dep.addSub(Dep.target);

console.log(val)

return val;

},

set: function (newValue){

if (newValue === val) return;

val = newValue;

console.log(val + "=>" + newValue)

// 通知所有订阅者

dep.notify();

}

});

}

复制代码

订阅者 Watcher

每一个Watcher都绑定一个 update,watcher 可以收到属性的变化通知并执行相应的 update ,从而更新视图。

/*

第三步

1,实现一个 watcher 观察者/订阅者添加方法update 渲染视图

2,定义一个消息订阅器

很简单,维护一个数组,用来收集订阅者

消息订阅器原型挂载两个方法 分别是

addSub 添加一个订阅者

notify 数据变动 通知 这个订阅者的 update 方法

*/

function Watcher(vm, node, name, nodeType){

Dep.target = this;

this.vm = vm;

this.node = node;

this.name = name;

this.nodeType = nodeType;

this.update();

console.log(Dep.target)

Dep.target = null;

}

Watcher.prototype = {

update: function (){

/*

this.node 指向当前修改的 dom 元素

this.vm 指向当前 dom 的实例对象

根据 nodeType 类型 赋值渲染页面

*/

if (this.nodeType === 'text') {

this.node.nodeValue = this.vm[this.name]

}

if (this.nodeType === 'input') {

this.node.value = this.vm[this.name]

}

}

}

function Dep(){

this.subs = [];

}

Dep.prototype = {

addSub: function (sub){

this.subs.push(sub);

},

notify: function (){

this.subs.forEach(function (sub){

sub.update();

});

}

}

复制代码

Vue 实例

/*

创建一个构造函数,并生成实例化对象 vm

*/

function Vue(o){

this.id = o.el;

this.data = o.data;

observe(this.data, this);

getDocumentFragment(document.getElementById(this.id), this);

}

var vm = new Vue({

el: 'app',

data: {

msg: 'HiSen',

test: 'Hello,MVVM'

}

});

复制代码

也许看到最后大家也没有看出个所以然,曾几何时的我跟你们一样,看来看去,就是这么几段代码;其实我也是参考,揣摩,调试,最后才成功的 ;

**建议:拿下我的源码,自己跑一跑,看一看,是骡子是马拉出来溜溜。[去溜溜 ~ ](https://github.com/Hi-Sen/Vue_MVVM)**

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值