【 web高级 01vue 】 vue直播课03 手撸Vue

MVVM模式

在这里插入图片描述

ViewModel是Vue.js的核心,它是一个Vue实例。Vue实例是作用于某一个HTML元素上的,这个元素可以是HTML的body元素,也可以是指定了id的某个元素。

当创建了ViewModel后,双向绑定是如何达成的呢?

  • 首先,我们将上图中的DOM Listeners和Data Bindings看作两个工具,它们是实现双向绑定的关键。
  • 从View侧看,ViewModel中的DOM Listeners工具会帮我们监测页面上DOM元素的变化,如果有变化,则更改Model中的数据;
  • 从Model侧看,当我们更新Model中的数据时,Data Bindings工具会帮我们更新页面中的DOM元素。

MVVM框架三要素:数据响应式、模板引擎及其渲染

数据响应式:监听数据变化并在视图中更新

  • Objext.defineProperty()
  • Proxy

模板引擎:提供描述视图的模板语法

  • 插值:{ {}}
  • 指令:v-bind、v-on、v-model、v-for、v-if

渲染:如何将模板装换为html

  • 模板 => vdom => dom

一、 数据响应式原理

数据变更能够响应在视图,就是数据响应式。vue2利用Object.defineProperty()来实现变更检测。

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

备注:应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。

1.2.1 简单实现数据变更

// 响应式
const obj = {
    
    };



function defineReactiove(obj,key,val){
    
    
    //对传入的obj进行访问的拦截
    Object.defineProperty(obj,key,{
    
    
        get(){
    
    
            console.log('get ' + key);
            return val
        },
        set(newValue){
    
    
            if(newValue !== val){
    
    
                console.log('set ' + key + ":" + newValue);
                val = newValue;
            }
        }
    });
}



defineReactiove(obj,'foo','foo');

obj.foo

obj.foo = 'foooooooooooooo'




在这里插入图片描述

在这里插入图片描述

1.2.2 结合视图

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script>
        const obj = {
     
     };

        function defineReactiove(obj,key,val){
     
     
            //对传入的obj进行访问的拦截
            Object.defineProperty(obj,key,{
     
     
                get(){
     
     
                    console.log('get ' + key);
                    return val
                },
                set(newValue){
     
     
                    if(newValue !== val){
     
     
                        console.log('set ' + key + ":" + newValue);
                        val = newValue;
                        //更新函数,更新界面
                        update();
                    }
                }
            });
        }

        function update(){
     
     
            app.innerText = obj.foo
        }

        defineReactiove(obj,'foo','');

        obj.foo = new Date().toLocaleTimeString();
        
        setInterval(()=>{
     
     
            obj.foo = new Date().toLocaleTimeString();
        },1000);

    </script>
</body>
</html>

1.2.3 遍历需要响应化的对象

// 响应式
// const obj = {};



function defineReactive(obj, key, val) {
    
    

    //对传入的obj进行访问的拦截
    Object.defineProperty(obj, key, {
    
    
        get() {
    
    
            console.log('get ' + key);
            return val
        },
        set(newValue) {
    
    
            if (newValue !== val) {
    
    
                console.log('set ' + key + ":" + newValue);
                val = newValue;
            }
        }
    });
}

对象响应化:遍历每个key,定义getter、setter
function observe(obj) {
    
    

    //不是一个obj 或者 obj 是空,不是对象,直接退出
    if (typeof obj !== 'object' || obj == null) {
    
    
        //希望传入的是obj
        return;
    }

    /**
     * Object.keys() 
     * 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
     * // array like object
     * var obj = { 0: 'a', 1: 'b', 2: 'c' };
     * console.log(Object.keys(obj)); // console: ['0', '1', '2']
     * 
     * 
     */
    
    Object.keys(obj).forEach(key => {
    
    
        defineReactive(obj, key, obj[key])
    })

}

// defineReactive(obj,'foo','foo');

// obj.foo

// obj.foo = 'foooooooooooooo'

const obj = {
    
     foo: 'foo', bar: 'bar', baz: {
    
     a: 1 } };

//遍历做响应化处理
observe(obj);

obj.foo
obj.foo = "fooooooooooooo"
obj.bar
obj.bar = "barrrrrrrrrrrr"

obj.baz.a = 10  //嵌套对象 no  ok

在这里插入图片描述

1.2.4 解决嵌套对象问题

function defineReactive(obj, key, val) {
    
    
    //递归
    observe(val);

    ......
}

在这里插入图片描述

1.2.5 如果添加/删除了新属性无法检测

obj.dong = 'dong' 
obj.dong  //并没有get信息

在这里插入图片描述

function set(obj,key,val){
    
    
    defineReactive(obj,key,val);
}

obj.dong = 'dong' //添加了新属性无法检测
set(obj,'dong','dongggg');
obj.dong  //get

在这里插入图片描述

defineProperty() 对数组无效
分析:改变数组方法只有7个
解决方案:替换数组实例的原型方法,让她们在修改数组的同时还可以通知更新

1.2.6 完整代码

// 响应式
// const obj = {};


//响应式处理
function defineReactive(obj, key, val) {
    
    
    //递归:解决嵌套对象问题
    observe(val);


    //对传入的obj进行访问的拦截
    Object.defineProperty(obj, key, {
    
    
        get() {
    
    
            console.log('get ' + key);
            return val
        },
        set(newValue) {
    
    

            if (newValue !== val) {
    
    
                console.log('set ' + key + ":" + newValue);
                //如果传入的newValue依然是obj,需要做响应化处理
                observe(newValue); //新值是对象的情况
                val = newValue;
            }
        }
    });
}


//遍历需要响应化的对象
function observe(obj) {
    
    

    //不是一个obj 或者 obj 是空,不是对象,直接退出
    if (typeof obj !== 'object' || obj == null) {
    
    
        //希望传入的是obj
        return;
    }

    /**
     * Object.keys() 
     * 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
     * // array like object
     * var obj = { 0: 'a', 1: 'b', 2: 'c' };
     * console.log(Object.keys(obj)); // console: ['0', '1', '2']
     * 
     * 
     */

    Object.keys(obj).forEach(key => {
    
    
        defineReactive(obj, key, obj[key])
    })

}

function set(obj,key,val){
    
    
    defineReactive(obj,key,val);
}




//----------------------------------------------------------------

//响应化处理
// defineReactive(obj,'foo','foo');

// obj.foo

// obj.foo = 'foooooooooooooo'




const obj = {
    
     foo: 'foo', bar: 'bar', baz: {
    
     a: 1 } };

//遍历做响应化处理
observe(obj);

obj.foo
obj.foo = "fooooooooooooo"
obj.bar
obj.bar = "barrrrrrrrrrrr"

// obj.baz.a = 10  //嵌套对象 no  ok ==>  递归
obj.baz = {
    
    a:100}
obj.baz.a = 100000 //赋的值是对象 no  ok


obj.dong = 'dong' //添加了新属性无法检测
set(obj,'dong','dongggg');
obj.dong  //get



二、 Vue中的数据响应化

2.1 目标代码

<!DOCTYPE html>
<html lang="en">

<head>  
    <meta charset="UTF-8">  
    <meta name="viewport" content="width=device-width, initial-scale=1.0">  
    <meta http-equiv="X-UA-Compatible" content="ie=edge">  <title>Document</title>
</head>

<body>  <div id="app">    <p>{
   
   {counter}}</p>  </div> 
    <script src="node_modules/vue/dist/vue.js"></script>
     <script>
        const app = new Vue({
     
     
            el: '#app',
            data: {
     
     
                counter: 1
            },
        }); 
        setInterval(() => {
     
     
            app.counter++
        }, 1000);
    </script>
</body>

</html>

2.2 原理分析

1.new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observer中
2.同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
3.同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
4.由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
5.将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数



                        劫持监听所有属性  
               /---->    Observer   ---------------   通知变化  ----------------------->      Dep  --------------------
              /                                                                               ^                     |
             /                                                                                |                     |
            /                                                                                 |                     |
new MVVM()                                                                                添加订阅者             通知变化       
            \                                                                                 |                     |
             \                                                                                |                     |
              \                                                                               |                     |
               \---->    Compiler    ----------  订阅数据变化,绑定更新函数  ------------->   Watcher   <-------------| 
                        解析指令    \                                                         /
                                    \                                                       /
                                     \                                                     /
                                  初始化视图                                           更新视图    
                                       \                                                 /
                                        \------------>    Updater    <------------------/

在这里插入图片描述

设计类型介绍

  • KVue:框架构造函数
  • Observer:执行数据响应化(分辨数据是对象还是数组)
  • Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
  • Watcher:执行更新函数(更新dom)
  • Dep:管理多个Watcher,批量更新

2.3 KVue

框架构造函数:执行初始化

2.3.1 执行初始化,对data执行响应化处理,kvue.js

/**
 * 响应式处理
 * @param {*} obj 
 * @param {*} key 
 * @param {*} val 
 */
function defineReactive(obj, key, val) {
    
    
  
    observe(val);


    Object.defineProperty(obj, key, {
    
    
        get() {
    
    
            console.log("get",key);
            return val
        },
        set(newValue) {
    
    
            if (newValue !== val) {
    
    
                console.log('set ' + key + ":" + newValue);
                observe(newValue); 
                val = newValue;
            }
        }
    });
}


//------------------------------------------------------------------------------------------------


/**
 * 2.1遍历需要响应化的对象
 * 在响应化的过程中,只是创建了一个Observer实例
 * @param {*} obj 
 */
function observe(obj) {
    
    
    if (typeof obj !== 'object' || obj == null) {
    
    
        return;
    }

    //创建 Observer 实例 -- 判断传入的是对象还是数组
    new Observer(obj);

}


//--------------------------补充start----------------------------------------------------------------------



补充:
Object.defineProperty
https://segmentfault.com/a/1190000007434923


getter/setter
当设置或获取对象的某个属性的值的时候,可以提供getter/setter方法。

getter 是一种获得属性值的方法
setter是一种设置属性值的方法。
在特性中使用get/set属性来定义对应的方法。

var obj = {
    
    };
var initValue = 'hello';
Object.defineProperty(obj,"newKey",{
    
    
    get:function (){
    
    
        //当获取值的时候触发的函数
        return initValue;    
    },
    set:function (value){
    
    
        //当设置值的时候触发的函数,设置的新值通过参数value拿到
        initValue = value;
    }
});
//获取值
console.log( obj.newKey );  //hello

//设置值
obj.newKey = 'change value';

console.log( obj.newKey ); //change value


******************

Object.keys
https://segmentfault.com/a/1190000009986807

获取对象的所有属性
它返回一个数组,就可以结合forEach方法遍历对象

// 1.对象
var a = {
    
    
    a : 123,
    b : 'asd',
    c : function() {
    
    
        console.log( 'haha' );
    }
};
console.log( Object.keys( a ) ); // [ 'a', 'b', 'c' ]



//----------------------------补充end--------------------------------------------------------------------




/**
 * 0.用户的 new  KVueDe 时候
 * 传进来选项,将data拿出来,做响应化处理
 */
//创建 KVue 构造函数
class KVue {
    
    
    constructor(options){
    
    
        //1.保存选项
        this.$options = options;
        this.$data = options.data;

        //2.响应化处理
        observe(this.$data);

       
    }
}



//------------------------------------------------------------------------------------------------


/**2.2 根据对象的类型决定如何做响应化
 *在 Observer 实例,要做的是:
 *判断 类型,如果是对象,就做walk操作
 *对所有的key执行拦截
*/
class  Observer{
    
    
    constructor(value){
    
    
        //2.2.1 保存value
        this.value = value
        //2.2.2 判断其类型
        if(typeof value === 'object'){
    
    
            this.walk(value);//遍历
        }
    }

    //对象数据的响应化
    //劫持监听所有属性
    walk(obj){
    
    
        Object.keys(obj).forEach(key => {
    
    
            defineReactive(obj, key, obj[key])
        })
    }

    //数组数据响应化

}



2.3.2 为$data做代理

/**
 * 3.代理函数
 * 方便用户直接访问 $data 中的数据
 * @param {*} vm  当前的框架实例 
 * @param {*} key 代理的key
 */
function proxy(vm,sourceKey){
    
    

    Object.keys(vm[sourceKey]).forEach(key=>{
    
    
        //将 $data 中的 key 代理到 vm 属性中
        Object.defineProperty(vm,key,{
    
    
            get(){
    
    
                  //当获取值的时候触发的函数
                return vm[sourceKey][key]
            },
            set(newVal){
    
    
                 //当设置值的时候触发的函数,设置的新值通过参数value拿到
                vm[sourceKey][key] = newVal;
            }
        })
    });
}


class KVue {
    
    
    constructor(options){
    
    
        //1.保存选项
        this.$options = options;
        this.$data = options.data;

        //2.响应化处理
        observe(this.$data);

        //3.代理
        //没有代理的话:app.$data.counter++ 也是可以的 
        //实现 vue中访问  app.counter++ 一样
        proxy(this,'$data');//代理的目标


    }
}


以上是:


2.4 编译 - Compile

编译模板中vue模板特殊语法,初始化视图、更新视图



                                                                                      k-text
                                                                                   /----->  处理textContent
                                                                                  /
                                                                                 /
                                                                                /  k-html
                                                                   /---- K开头  --------->  处理innerhtml
                                                                  /             \
                                                                 /               \
                                                                /                 \  k-model
                              /---->   编译节点   ---->   遍历属性                    \------> 监听input
                             /                                  \ 
                            /                                    \
                           /                                      \
获取dom   ---->  遍历子元素                                           \---- at开头   ---->     绑定click
                           \
                            \
                             \         {
    
    {
    
    xx}}
                              \---->  编译文本


在这里插入图片描述

初始化视图

2.4.1 根据节点类型编译,compile.js



//--------------补充start---------------------
补充:

document.querySelector("#app")

指定一个或多个匹配元素的 CSS 选择器。 可以使用它们的 id,, 类型, 属性, 属性值等来选取元素。
返回匹配指定选择器的第一个元素




----------------------------------------------------------------------------------------------

Array.from(childNodes)

from() 方法的作用是:从类数组或迭代对象创建一个新的、浅拷贝的数组示例。



----------------------------------------------------------------------------------------------
https://developer.mozilla.org/zh-CN/docs/Web/API/Node


node.childNodes
返回指定节点的所有子节点,包括节点元素和文本元素

https://www.nhooo.com/jsref/elem_childnodes.html

var len = document.querySelector("div").childNodes.length;


var nodes = document.querySelector("div").childNodes;
nodes[1].style.backgroundColor = "coral";

*******************************************************


node.nodeType  
返回以数字值返回指定节点的节点类型。

https://www.w3school.com.cn/jsref/prop_node_nodetype.asp

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

节点类型 - 返回值
对于每种节点类型,nodeName 和 nodeValue 属性的返回值:


Node.childNodes   返回一个包含了该节点所有子节点的实时的NodeList
Node.nodeName
Node.nodeType 
Node.nodeValue    返回或设置当前节点的值。
Node.textContent  返回或设置一个元素内所有子节点及其后代的文本内容。
Node.attributes   返回指定节点的属性集合

//------------------补充end---------------------














/*
递归遍历dom树
判断节点类型,如果是文本,则判断是否是 插值绑定
如果是元素,则遍历其属性判断是否是指令或时间,然后递归子元素
*/






class Compiler{
    
    
    //el 是宿主元素
    //vm 是KVue实例
    constructor(el,vm){
    
    
        //保存 KVue 实例
        this.$vm = vm;
        this.$el = document.querySelector(el);

        if(this.$el){
    
     //如果this.$el存在,则执行编译
            //执行编译
            this.compile(this.$el);
        }
    }


    compile(el){
    
    
        //遍历el树
        const childNodes = el.childNodes; //子节点集合

        Array.from(childNodes).forEach(node =>{
    
    
            //判断是否是元素
            if(this.isElement(node)){
    
    
                console.log('编译元素' + node.nodeName);
            }else if(this.isInter(node)){
    
    
                console.log('编译插值文本 {
    
    {}}' + node.textContent )
            }

            //递归子节点
            if(node.childNodes && node.childNodes.length > 0 ){
    
    
                this.compile(node);
            }
        });

    }



    isElement(node){
    
    
        return node.nodeType === 1;
    }



    isInter(node){
    
    
        //首先是文本标签,其次内容是{
    
    {xxx}} 正则 //  \{转义字符 由双大括号括起来  .若干字符 *若干个  匹配的内容要拿到 ()分组 
        //   /\{\{(.*))\}\}/.test('{
    
    {xx}}')

        return  node.nodeType  === 3 &&  /\{\{(.*)\}\}/.test(node.textContent)

    }

  
}




2.4.2 编译插值 { {xxx}},compile.js

compile(el){
    
    
     ......

      }else if(this.isInter(node)){
    
    
          // console.log('编译插值文本 {
    
    {}}' + node.textContent )
          this.compileText(node);
      }
     ......


}

compileText(node){
    
     
      // RegExp.$1 正则的构造函数,只要有分组,将来匹配的值 就会放在 RegExp.$1
      // 拿到上面正则中()的内容,也就是 {
    
    {}} 双大括号中间的内容
      console.log(RegExp.$1)
      node.textContent = this.$vm[RegExp.$1]
      
}

2.4.3 编译元素,compile.js


compile(el){
    
    
   		......
   		
        //判断是否是元素
        if(this.isElement(node)){
    
    

            console.log('编译元素' + node.nodeName);
            this.compileElement(node);

        }
        
		......
  
}


compileElement(node){
    
    
       //节点是元素
       //遍历其属性列表
       const nodeAttrs = node.attributes; // 返回指定节点的属性集合
       Array.from(nodeAttrs).forEach(attr =>{
    
    
           //规定:指令以 k-xx = "oo" 定义
           const attrName = attr.name // k-xx  k-text
           const exp = attr.value // oo  counter
           console.log("attr",attr)

           if(this.isDirective(attrName)){
    
    
               const dir = attrName.substring(2) // xx text

               //执行指令  this :Compiler 实例  里面有个方式是 text
               this[dir]  && this[dir](node,exp) 
           }

       });
}

isDirective(attr){
    
    
       return attr.indexOf('k-') === 0
}

// k-text
 /**
  * 更新元素的 textContent
  * <span v-text="msg"></span>
  * <!-- 和下面的一样 -->
  * <span>{
    
    {msg}}</span>
  */
 //exp:counter
 text(node,exp){
    
    
     node.textContent = this.$vm[exp]
 }

2.4.4 k-html,compile.js

//k-html
html(node,exp){
    
    
    node.innerHTML = this.$vm[exp]
}

2.5 依赖收集

视图中会用到data中某key,这成为依赖。同一个key可能出现多次,每次都需要收集出来用一个Watcher来维护它们,此过程称为依赖收集。

多个Watcher需要一个Dep来管理,需要更新时由Dep统一通知。

看下面按钮,理出一个思路

new Vue({
    
    
	template:
		`<div>
			<p>{
     
     {name1}}</p>
			<p>{
     
     {name2}}</p>
			<p>{
     
     {name1}}</p>
		</div>`,
		data:{
    
    
			name1:'name1',
			name2:'name2'
		}
});

2.5.1 实现思路

  • 1.defineReactive时为每一个key创建一个Dep实例
  • 2.初始化视图时读取某个key,例如name1,创建一个watcher1
  • 3.由于触发name1的getter方法,变将watcher1添加到name1对应的Dep中
  • 4.当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新

在这里插入图片描述

在这里插入图片描述

2.5.2 创建watcher,kvue.js

const watchers = [];//临时用于保存watcher测试

//观察者:保存更新函数,值发生变化调用更新函数

class Watcher {
    
    
    constructor(vm, key, updateFn) {
    
    
        this.vm = vm;

        this.key = key;

        this.updateFn = updateFn;

        // 临时放入watchers数组    
        watchers.push(this)

    }

    update() {
    
    

        return this.updateFn.call(this.vm, this.vm[this.key]);//两个参数:指定上下文,newvalue   
    }

}

2.5.3 编写更新函数、创建watcher,compile.js

	//调用update函数执行插值文本复制
    compileText(node){
    
     
        // console.log(RegExp.$1)
        // node.textContent = this.$vm[RegExp.$1]
        //将上面的代码抽取出去

        this.update(node,RegExp.$1,'text'); //参数:node,当前的表达式,指令的名称
    }


	text(node,exp){
    
    
	    // node.textContent = this.$vm[exp]
	    //抽取updater函数
	    
	    this.update(node,exp,'text'); //参数:node,当前的表达式,指令的名称
	
	}


    //k-html
    html(node,exp){
    
    
        // node.innerHTML = this.$vm[exp]
        //抽取updater函数
        
        this.update(node,exp,'html'); //参数:node,当前的表达式,指令的名称
    }



 	/** 
	* 更新函数作用
     * 1.初始化
     * 2.创建watcher实例
     * @param {*} node 当前节点
     * @param {*} exp  当前表达式
     * @param {*} dir  指令名字
     */
    update(node,exp,dir){
    
    
        //1.初始化操作
        //指令对应的更新函数  xxUpdater
        const fn = this[dir + 'Updater'];

        fn && fn(node,this.$vm[exp])

        //2.更新处理,封装一个更新函数,可以更新对应dom元素
        //val哪里来的:  this.updateFn.call(this.vm,this.vm[this.key]);//两个参数:指定上下文,newvalue  
        new Watcher(this.$vm,exp,function(val){
    
    
            fn && fn(node,val);  //更新函数的作用是:fn执行一遍
        });//watcher实例最主要的作用是把更新函数传递进去
    }

    /**
     * 
     * @param {*} node 节点
     * @param {*} value 最新的值
     */
    textUpdater(node,value){
    
    
        node.textContent = value
    }

    htmlUpdater(node,value){
    
    
        node.innerHTML = value
    }

2.5.4 声明Dep,kvue.js

//Dep : 依赖,管理某个 key 相关所有 Watcher 实例

class Dep {
    
    
    constructor() {
    
    
        this.deps = [];
    }

    //增加
    addDep(dep) {
    
    
        this.deps.push(dep)
    }

    //通知
    notify() {
    
    
        this.deps.forEach(dep => dep.update());
    }
}


2.5.5 创建watcher时触发getter,kvue.js


class Watcher {
    
    
    constructor(vm, key, updateFn) {
    
    
        ......

        // Dep.target 静态属性上设置为当前 Watcher实例
        /**
         * Dep.target  将watcher实例放在一个全局可以访问的地方
         * window.target 也可以
         * 不用,是因为  window不通用
         * 如果运行环境没有window就会挂了
         * Dep.target  放在一个类,作为静态属性来用
         */
        Dep.target = this;
        this.vm[this.key] //读取触发了getter
        Dep.target = null //收集完就置空

    }

}

2.5.6 依赖收集,创建Dep实例,kvue.js

function defineReactive(obj, key, val) {
    
    

    observe(val);

    //创建一个Dep和当前key一一对应

    const dep = new Dep();

    Object.defineProperty(obj, key, {
    
    
        get() {
    
    
            //依赖收集在这里   Dep.target 指的是watcher实例
            Dep.target && dep.addDep(Dep.target)

            ......
        },
        set(newValue) {
    
    
            if (newValue !== val) {
    
    
        		......

                //通知更新
                dep.notify();  //dep 和 key 是一对一的关系

            }
        }
    });
}


作业

  • 实现数组响应式
  • 完成后续k-model、@xx

猜你喜欢

转载自blog.csdn.net/weixin_42580704/article/details/114019706
今日推荐