资讯专栏INFORMATION COLUMN

了解MVVM及Vue实现原理,手把手带你撸源码。

cooxer / 448人阅读

摘要:方法实现将所有属性挂载在观察对象,将每一项做一个数据劫持就是将中每一项用定义新属性并返回这个对象。当和发生变化时,自动会触发视图更新,获取得到的也就是最新值。

MVVM及Vue实现原理

Github源码地址:https://github.com/wyj2443573...

mvvm 双向数据绑定
数据影响视图,视图影响数据
angular 脏值检测 vue数据劫持+发布订阅模式
vue 不兼容低版本 用的是Object.defineProperty
下面涉及涵盖的知识点
1. Object.defineProperty
因为vue底层是基于Object.defineProperty实现的,所以对于这方面不懂的自己先学习。

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
语法

Object.defineProperty(obj, prop, descriptor)

基本用法

var o = {};

o.a = 1;
// 等同于 :
Object.defineProperty(o, "a", {
  value : 1,
  writable : true,
  configurable : true,
  enumerable : true
});
// 另一方面,
Object.defineProperty(o, "a", { value : 1 });
// 等同于 :
Object.defineProperty(o, "a", {
  value : 1,
  writable : false,
  configurable : false,
  enumerable : false
});
 let o={}
 Object.defineProperty(o,"a",{
    get(){ //获取o.a的值时,会调用get方法
        return "hello";
    },
    set(value){ //o.a="s" 赋值的时候触发set方法
        console.log(value)
    }
  })
  o.a="s"//"s"
  o.a //"hello"
2.数据劫持Observe

vue基本格式

//html
{{a}}

模仿vue的格式
vue 中有

$options : 存在属性 data、el、components 等等

_data : Vue实例参数中data对象

接下来构建基本页面

index.html




    
    Title
    


{{a}}

mvvm.js

function Vue(options={}){
    this.$options=options;
    //将所有属性挂载在$options;
    //this._data
    let data=this._data=this.$options.data;
    //观察data对象,将每一项做一个数据劫持;就是将data中每一项用Object.defineProperty定义新属性并返回这个对象。
    observe(data);
}
function Observe(data) { //这里写的是主要逻辑
    for(let key in data){ //把data属性通过object.defineProperty的方式 定义属性
        let val=data[key];
        Object.defineProperty(data,key,{
            enumerable:true, //可以枚举
            get(){
                return val; //仅仅是将以前的 a:1的方式 转换换位defineProperty的方式
            },
            set(newValue){ //更改值的时候触发
                if(newValue===val){ //如果设置的值跟之前的值一样则什么也不做
                    return;
                }
                val=newValue; //将新值赋给val,那么get获取val的时候,获取的就是newValue;
            }
        })
    }
}
//观察对象给对象增加ObjectDefinedProperty
function observe(data){
    return new Observe(data);
}

此时在控制台打印 vue,发现已经在vue._data中映射了正确的数据。

接下来操作: vue._data.a=30

如果此时data中a是一个复杂的对象类型,如下

{{a.a}}

则此时打印输出vue._data,只有第一层添加上了defineProperty,第二层的a无法劫持

那么我们要递归 深层添加defineProperty 另外递归的时候注意添加退出条件,当value不是对象的时候退出。

代码添加如下

function Observe(data) { //这里写的是主要逻辑
    for(let key in data){ //把data属性通过object.defineProperty的方式 定义属性
        let val=data[key];
        observe(val); //递归 劫持
        Object.defineProperty(data,key,{
            enumerable:true, //可以枚举
            get(){
                return val; //仅仅是将以前的 a:1的方式 装换位defineProperty的方式
            },
            set(newValue){ //更改值的时候触发
                if(newValue===val){ //如果设置的值跟之前的值一样则什么也不做
                    return;
                }
                val=newValue; //将新值赋给val,那么get获取val的时候,获取的就是newValue;
            }
        })
    }
}
function observe(data){
    if(typeof data!="object"){  //如果非对象,则退出遍历递归
        return;
    }
    return new Observe(data);
}

此时内层的a也同样得到劫持

如果我们给a设置新值的时候vue._data.a={b:3} ,会发现内层b并没有被数据劫持。那么在赋新值的时候,也应该通过defineProperty去定义。

在set中用defineProperty定义新属性

 set(newValue){ //更改值的时候触发
     if(newValue===val){ //如果设置的值跟之前的值一样则什么也不做
         return;
     }
     val=newValue; //将新值赋给val,那么get获取val的时候,获取的就是newValue;
     observe(newValue)
  }

分析到这里,我们已经能够实现了深度的数据观察

3.数据代理

上面的代码如果想要访问a的属性需要通过 vue._data.a 这样的写法获得,这种写法过于繁琐。我们接下来改善一下:用vue.a的方式直接访问到a(用vue 代理 vue._data
-- 1.首先用this代理this._data; 让数据中的每一项都用defineProperty代理。

function Vue(options={}){
    this.$options=options;
    //将所有属性挂载在$options;
    //this._data
    let data=this._data=this.$options.data;
    //观察data对象,将每一项做一个数据劫持;就是将data中每一项用Object.defineProperty定义新属性并返回这个对象。
    observe(data);
    for(let key in data){
        Object.defineProperty(this,key,{  //this 代理this._data;
            enumerable:true,
            get(){
                return this._data[key]; //相当于 this.a={a:1}
            },
            set(newVal){
                
            }
        })
    }
}

到这一步我们已经实现了数据代理的初级版,vue.a 可以直接获取值而非vue._data.a

-- 2.get方法很容易理解,这里较为重点的是在set中设置。首先思考:如果直接设置this.a={name:1} ,this.a 与this._data.a 它们的值同步改变吗?

set(newVal){
    this[key]=newVal; //?可以这样做吗?我们来实践下
}

很显然两者是不能够同步改变的。

方法实现

 function Vue(options={}){
    this.$options=options;
    //将所有属性挂载在$options;
    //this._data
    let data=this._data=this.$options.data;
    //观察data对象,将每一项做一个数据劫持;就是将data中每一项用Object.defineProperty定义新属性并返回这个对象。
    observe(data);
    //this 代理this._data;
    for(let key in data){
        Object.defineProperty(this,key,{
            enumerable:true,
            get(){
                return this._data[key]; //相当于 this.a={a:1}
            },
            set(newVal){ //如果直接更改this[key]="XXX",那么this._data[key]的值是不会被同步改变的。
                // 我们可以通过给this._data[key]=value赋值,从而调取Observe方法中的set,赋予this._data[key]新值。
                // get(){return this._data[key]},获取到的值即是调取Observe方法中get方法return的值
                // 也就是根源上的改变是this._data[key];这样不管是this._data[key]还是this[key]随便哪一个被赋予新值,两者都是同步变化的
                this._data[key]=newVal;
            }
        })
    }
}

下面来分析一下思路

1.我们可以在set中设置this._data[key]=newValue,如果此时vue.a={name:1}它调取是Observe方法中的set,赋予this._data[key]新值。
设置值的时候相当于走的是这一步

set(newValue){ //更改值的时候触发
             if(newValue===val){ //如果设置的值跟之前的值一样则什么也不做
                 return;
             }
             val=newValue; //将新值赋给val,那么get获取val的时候,获取的就是newValue;
             observe(newValue)
         }

2.如果此时我们获取vue.a 的值,即通过get方法获取return this._data[key],得到的就是最新值

这里解释说明一下

学到这里我们应该了解到:

vue特点不能新增不存在的属性 因为不存在的属性没有get 和 set,它就不会监控数据的变化

深度响应: 因为每次赋予一个新对象时会给这个新对象增加数据劫持。

4.编译模板Compile
这一步我们要做的目的是将目标元素内{{xx}} 花括号中的xx替换成对应的值。

第一步、代码实现如下

function Vue(options={}){
    /*代码承接上面*/
    new Compile(options.el,this) //实例化Compile
}
function Compile(el){
    //el表示替换哪个元素范围内的模板
    let replacePart=document.querySelector(el);
    let fragment=document.createDocumentFragment(); 
    while(child = replacePart.firstChild){ //将app中的内容移至内存中
        fragment.appendChild(child);
    }
    replace() //我们在此要做的是通过replace方法,将代码片段中的{{a.a}}的a.a替换为data中对应的值。
    replacePart.appendChild(fragment);
}
function replace(){

}

第二步、replace方法先找到所有要替换的地方,代码如下:

{{A}}

{{a.a}}

{{b}}

{{c}}

{{d}}

 function Compile(el,vm){
    //el代表替换的范围
    let replacePart=document.querySelector(el);
    let fragment=document.createDocumentFragment();
    while(child = replacePart.firstChild){ //将app中的内容移至内存中
        fragment.appendChild(child);
    }
    replace(fragment) //我们在此要做的是通过replace方法,将代码片段中的{{a.a}}的a.a替换为data中对应的值。
    replacePart.appendChild(fragment);
    function replace(fragment){
        Array.from(fragment.childNodes).forEach(function(node){
            let text=node.textContent;
            let reg=/{{(.*)}}/;
            if(node.nodeType===3&& reg.test(text)){ //nodeType:3 文本节点
                console.log(RegExp.$1); // A、 a.a 、b 等等
            }
            if(node.childNodes){
                replace(node)  //如果当前node存在子节点,递归找到所有需要替换的地方
            }
        })
    }
}

这一步我们能够找到所有要替换的目标了

第三步、replace方法中 用对应值替换掉需要替换掉的地方,代码如下:

 function replace(fragment){
        Array.from(fragment.childNodes).forEach(function(node){
            let text=node.textContent;
            let reg=/{{(.*)}}/;
            if(node.nodeType===3&®.test(text)){ //nodeType:3 文本节点
                console.log(RegExp.$1);
                let arr=RegExp.$1.split(".") // [A] [a,a] [b] ...
                let val=vm; //val:{a:{a:1}}
                arr.forEach(function(k){
                    val=val[k]  //举例 第一次循环  val=val.a   val赋值后val:{a:1} ;第二次循环  val=val.a  val赋值后为1
                })
                node.textContent=text.replace(reg,val)
            }
            if(node.childNodes){
                replace(node)  //如果当前node存在子节点,递归替换
            }
        })
    }

替换结果如下

5.发布订阅模式

不明白发布订阅模式的朋友先去学习
参考链接:https://segmentfault.com/a/11...

代码一:

 //发布订阅模式  先订阅 再发布
// 订阅就是往事件池里面扔函数   发布的就是事件池中的函数依次执行

//我们假设subs中每个方法中都有update属性,
function Dep(){
    this.subs=[]
}
Dep.prototype.addSub=function(sub){ //订阅
    this.subs.push(sub)
}
Dep.prototype.notify=function(){
    this.subs.forEach(sub=>{
        sub.update();
    })
}
function Watcher(fn){ //watch是一个类 通过这个类创建的实例都拥有update方法
    this.fn=fn
}
Watcher.prototype.update=function(){
    this.fn();
}
let watcher=new Watcher(function(){console.log(1)}); //监听函数
let dep=new Dep();
dep.addSub(watcher); //将watcher放在数组中
dep.addSub(watcher);
dep.addSub(watcher);
dep.notify() // 数组关系

代码二(然后我们将代码二的这个发布订阅的模板放到我们的mvvm.js最下面)

function Dep(){
    this.subs=[]
}
Dep.prototype.addSub=function(sub){ //订阅
    this.subs.push(sub)
}
Dep.prototype.notify=function(){
    this.subs.forEach(sub=>{
        sub.update();
    })
}
function Watcher(fn){ 
    this.fn=fn
}
Watcher.prototype.update=function(){
    this.fn();
}
6.连接视图与数据
那么我们接下来的目的是:当数据变化的时候,我们需要重新刷新视图,将页面中的{{a.a}}双括号中的a.a也能够被实时的替换掉。这就用到了上面介绍的发布订阅模式。

方法:我们得先将node.textContent=text.replace(reg,val)订阅一下,当数据发生变化的时候,执行node.textContent=text.replace(reg,val)此操作。

我们先写要订阅的事件

这里思考当数据变化的时候会产生新的值,我们需要用newValue替换原有的值。要想取到新的值,我们需要用到当前实例vm与正则的捕获到的RegExp.$1,从而获取类似this.a.a的最新值。

 new Watcher(vm,RegExp.$1,function(newValue){ //订阅的事件  函数里需要接受新的值
     node.textContent=text.replace(reg,newValue);
 });

此时Watcher类也应该改动下,不懂没关系,可以顺着看下面的解析。

function Watcher(vm,exp,fn){
    this.fn=fn;
    this.vm=vm;
    this.exp=exp;
    //我们要将fn添加到订阅中
    Dep.target=this;
    console.log(Dep.target)
    let val=vm;
    let arr=exp.split(".");
    arr.forEach(function(k){
        val=val[k];
    })
    Dep.target=null;
}

Dep.target为

下面我们来分析代码
这个逻辑相对复杂,不明白的话多看几遍

上述代码中,我们可以看到执行了这一步
    let val=vm;
    let arr=exp.split(".");
    arr.forEach(function(k){  
        val=val[k];  //获取this.a.a的时候就会触发get方法
    })

这一步遍历 arr 取val[k]的时候相当于获取this.a,会触发了默认的get方法。也就是触发这里的get方法

在get方法中我们需要订阅事件:

上述代码中,取值的时候会调取get方法,Dep.target值是存在的,此时将Dep.target放到我们的事件池中。

当我们set的时候,触发事件池中的事件

此时update的方法我们得更改下,赋予新值

Watcher.prototype.update=function(){
    let val=this.vm;
    let arr=this.exp.split(".");
    arr.forEach(function(k){ //this.a.a
        val=val[k];
    })
    this.fn(val); //newValue
}

此时我们就能得到我们想要的结果了,当数据发生改变时,视图也会更新。

7.双向数据绑定的实现

思路: 先找到v-model这个属性,取对应的属性值s,然后用vue.s 替换input中value值
在Compile方法中写逻辑

打印结果如下

此时已经能够将对应的s值赋值给输入框

接下来我们需要做的是,每次更改输入框中的值的时候,对应的s也会跟着改变

操作结果如下

8.实现computed

官网上有两种computed的基础用法
用法一

var vm = new Vue({
  el: "#example",
  data: {
    message: "Hello"
  },
  computed: {
    // 一个 computed 属性的 getter 函数
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split("").reverse().join("")
    }
  }
})

用法二

computed: {
  fullName: {
    // getter 函数
    get: function () {
      return this.firstName + " " + this.lastName
    },
    // setter 函数
    set: function (newValue) {
      var names = newValue.split(" ")
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

思路: computed实现相对来说比较简单,只是把数据挂在vm上

1.html页面如下

 

s的值:{{s}}

computed的值:{{hello}}

{{A}}

{{a.a}}

{{b}}

{{c}}

{{d}}

2.将hello属性挂载在当前实例上,先初始化computed

initComputed 函数实现
function initComputed(){
    let vm=this;
    let computed=this.$options.computed; //Object.keys [name:"tom",age:2]=>[name,age]
    Object.keys(computed).forEach(function(key){
        Object.defineProperty(vm,key,{ //computed[key]
            get:typeof computed[key]==="function"? computed[key]:computed[key].get,
            set(){

            }
        })
    })
}
分析以上代码,我们能得知this.hello的变化 只依赖于this.s 和 this.c 。当s和c发生变化时,自动会触发视图更新,获取this.hello得到的也就是最新值。

++++++++++++到此已经完成了分享,如有相关的问题和建议还望不吝提出++++++++++++

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103195.html

相关文章

  • 设计架构

    摘要:先来看一张系统前后端架构模型图。一种接口的约定本文用于定义一种统一的接口设计方案,希望具有参考价值。,和都是常见的软件架构设计模式,它通过分离关注点来改进代码的组织方式。 如何无痛降低 if else 面条代码复杂度 相信不少同学在维护老项目时,都遇到过在深深的 if else 之间纠缠的业务逻辑。面对这样的一团乱麻,简单粗暴地继续增量修改常常只会让复杂度越来越高,可读性越来越差,有没...

    graf 评论0 收藏0
  • JavaScript - 收藏集 - 掘金

    摘要:插件开发前端掘金作者原文地址译者插件是为应用添加全局功能的一种强大而且简单的方式。提供了与使用掌控异步前端掘金教你使用在行代码内优雅的实现文件分片断点续传。 Vue.js 插件开发 - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins译者:jeneser Vue.js插件是为应用添加全局功能的一种强大而且简单的方式。插....

    izhuhaodev 评论0 收藏0
  • javascript知识点

    摘要:模块化是随着前端技术的发展,前端代码爆炸式增长后,工程化所采取的必然措施。目前模块化的思想分为和。特别指出,事件不等同于异步,回调也不等同于异步。将会讨论安全的类型检测惰性载入函数冻结对象定时器等话题。 Vue.js 前后端同构方案之准备篇——代码优化 目前 Vue.js 的火爆不亚于当初的 React,本人对写代码有洁癖,代码也是艺术。此篇是准备篇,工欲善其事,必先利其器。我们先在代...

    Karrdy 评论0 收藏0
  • 把手你撸一个cli工具

    摘要:最近,笔者就在为组里的框架去做一套基本的工具。通过这边文章,笔者希望大家都能简单的去实现一个属于自己的脚手架工具。我们在下新增文件,这个文件导出一个的类。结语到此,一个简单的就制作完成了,大家可以参考等优秀的适当的扩展自己的工具。 你有没有遇到过在没有vue-cli、create-react-app这样子的脚手架的时候一个文件一个文件的去拷贝老项目的配置文件。最近,笔者就在为组里的框架...

    xingqiba 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<