记录一些Vue底层原理

本文探讨了Vue.js的核心——数据驱动和组件系统,重点讲解了Vue的数据双向绑定原理,通过数据劫持(Object.defineProperty)和发布者-订阅者模式实现。在实现过程中涉及Observer、Compile、Watcher的创建,以及Dep类的设计,揭示了Vue如何确保数据变化与视图同步。
  • 使用Vue开发项目有一段时间了,一直停留在使用的阶段,未成真正的深入了解,最近看来一些关于Vue原理的博客和视频,在此记录一些心得方便以后回忆。

vue.js的两个核心

  • 数据驱动:ViewModel,保证数据和视图的一致性。
  • 组件系统:应用类UI可以看作全部是由组件树构成的。

这里先介绍一下Vue的数据双向绑定原理:

vue的数据双向绑定是通过数据劫持&发布者-订阅者模式。所谓的数据双向绑定表现出来的就是通过v-model指令实现视图层变化促使逻辑层变化,当逻辑层变化视图层也发生相应的变化

  1. view层–>model层:个人认为通过给相应的元素添加chang(input)事件,当触发chang(input)事件的时候就可以通过发布订阅者模式来改变model层。
  2. model层—>view层:个人认为通过Object.defineProperty()来劫持对象属性的set和get方法,当对象属性变化或获取时就可以通过该方法来进行监控,触发相应的监听回调。这样我们就可以监听MVVM中的逻辑层中的数据,然后通过发布订阅者模式来触发视图层的改变。
数据数据双向绑定原理图

在这里插入图片描述

Object.defineProperty数据劫持:
<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="app">
      <p id="name"></p>
    </div>
    <script>
      var obj = {};
      Object.defineProperty(obj, "name", {
        get: function() {
          return document.querySelector("#name").innerHTML;
        },
        set: function(val) {
          document.querySelector("#name").innerHTML = val;
        }
      });
      obj.name = "Jerry";
    </script>
  </body>
</html>

订阅者和发布者模式

  • 订阅者和发布者模式,通常用于消息队列中.一般有两种形式来实现消息队列,一是使用生产者和消费者来实现,二是使用订阅者-发布者模式来实现,其中订阅者和发布者实现消息队列的方式,就会用订阅者模式.

所谓的订阅者,就像我们在日常生活中,订阅报纸一样。我们订阅报纸的时候,通常都得需要在报社或者一些中介机构进行注册。当有新版的报纸发刊的时候,邮递员就需要向订阅该报纸的人,依次发放报纸。(这上面三句话引用别人的,感觉很形象)

在这里插入图片描述
在这里插入图片描述

Vue原理开始实现:

在这里插入图片描述

这上面是MVVM原理图

通过上述分析得到一下步骤:

  1. 需要一个数据劫持Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
  2. 需要一个模板解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
  3. 需要一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
  4. 再开始前我们看看需要实现的最终目的是什么?这时候需要一个index.html,如下面的代码,在这个html文件中,我们会发现包含大量的Vue指令、插值表达式、事件绑定等。还包含KVue这个对象,该对象就像我们经常写的Vue对象一样。接下来的目的是将html和Vue联系起来实现一个简单的Vue原理。

index.html代码如下:

<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="app">
      <!-- 插值绑定 -->
      <p>{{name}}</p>
      <!-- 指令解析 -->
      <p k-text="name"></p>
      <p>{{age}}</p>
      <p>{{doubleAge}}</p>
      <!-- 双向绑定 -->
      <input type="text" k-model="name" />
      <!-- 事件处理 -->
      <button @click="changeName">呵呵</button>
      <!-- html内容解析 -->
      <div k-html="html"></div>
    </div>
    <script src="./compile.js"></script>
    <script src="./Dep.js"></script>
    <script src="./Watcher.js"></script>
    <script src="./KVue.js"></script>
    <script>
      const kaikeba = new KVue({
        el: "#app",
        data: {
          name: "I am test.",
          age: 12,
          html: "<button>这是一个按钮</button>"
        },
        created() {
          setTimeout(() => {
            this.name = "我是测试";
          }, 1500);
        },
        methods: {
          changeName() {
            this.name = "哈喽,开课吧";
            this.age = 1;
          }
        }
      });
    </script>
  </body>
</html>

第一步实现Observer数据劫持:

  • 这时候需要将data中的数据显示到html对应的属性中,首先我们需要创建一个KVue类,并且遍历KVue实例中data里的属性,接着我们需要实现Compile解析器,解析模板指令,并替换模板数据,初始化视图。**
  • 数据监听器的核心方法就是Object.defineProperty(),通过遍历循环对所有属性值进行监听,并对其进行Object.defineProperty()处理。

Observe代码:

class KVue{
    constructor(options) {
        this.$options = options;
        this.$data = options.data;
        this.observe(this.$data);
		//实例化解析器
        new Compile(options.el,this)
    }
    observe(dataObj){
        if(!dataObj || typeof(dataObj) !=='object'){
            return false;
        }
        Object.keys(dataObj).forEach(key=>{
            this.defineReactive(dataObj,key,dataObj[key])
            //代理data中的属性到vue实例上
            this.proxyData(key);
        })
    }
    // 将data每一个属性添加set 和get 方法
    defineReactive(dataObj,key,value){
        this.observe(value)
        Object.defineProperty(dataObj,key,{
            get(){
                return value
            },
            set(newValue){
               if(newValue === value){
                   return;
               }
               value = newValue
            }
        })
    }
}
  • 在上述代码中只是简单的对KVue实例中data里的属性进行数据劫持和实例化解析器Compile,并没有在劫持的方法中进行相应的回调监听,因为接下来会牵扯到Dep订阅者和wather观察者。

第二步实现模板解析器Compile(上述代码中已经 new Compile(options.el,this))

  • 可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

Compile代码:

//new Compile(el,vm)
class Compile {
    constructor(el, vm) {
        this.$el = document.querySelector(el);
        this.$vm = vm;
        if (this.$el) {
            // // 因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中(这部分就是虚拟Dom)
            this.fragment = this.node2Fragment(this.$el);
            // 开始编译
            this.compile(this.fragment);
            // 将编译完成的html添加到app上
            this.$el.appendChild(this.fragment);
        }
    }
    node2Fragment(el) {
        let frag = document.createDocumentFragment();
        let child;
        while (child = el.firstChild) {
		//如果文档树中已经存在了 el.firstChild,它将从文档树中删除,然后插入它的新位置。也就是说Dom树中的元素在一个个减少,frag中元素在慢慢的添加。
            frag.appendChild(child)
        }
        return frag;
    }
    compile(el) {
        const childNodes = el.childNodes;
		//childNodes不是真正的数组,是一个类数组,需要用Array.from()方法来将他转化为真正的数组,这样他才能使用forEach()方法。
        Array.from(childNodes).forEach(node => {
		    //判断是不是元素节点(nodeType === 1)
            if (this.isElement(node)) {
                //这是元素节点
                console.log(`这是元素节点${node}`)
				//获取每一个node的属性,因为需要识别还有k-、@等Vue特殊的指令
                const attributes = node.attributes;
				//同样将attributes转为真正的数组
                Array.from(attributes).forEach(attr => {
                    const attrName = attr.name; //属性名(k-html,k-text,k-model,@...)
                    const exp = attr.value; // 属性值(data中的各个属性name,age,html)
					//k-指令
                    if (this.isDirective(attrName)) {
                        // 提取k-model,k-text,k-html 后面的名称(model,text,html)
                        var dir = attrName.substring(2)
                        // 调用指令解析方法,这里会以dir来创建一下方法如 html(node=节点,vm=KVue实例,exp=data的属性name,age..)
                        this[dir] && this[dir](node, this.$vm, exp);
                    }
					//@事件
                    if (this.isEvent(attrName)) {
                        console.log(`这是事件${attrName}`)
						//提取事件类别(click,input,change...)
                        var dir = attrName.substring(1)
						//调用事件解析方法
                        this.EventHander(node, this.$vm, exp, dir)
                    }
                })
            }
			//判断是不是{{...}}
            if (this.isInterpolation(node)) {
                //这是插值表达式
                console.log(`这是插值表达式${node.textContent}`)
				//调用插值表达式的解析方法
                this.textCompile(node);
            }
			//递归遍历含有子节点的Node
            if (node.childNodes && node.childNodes.length != 0) {
                this.compile(node)
            }
        })
    }
	//model解析方法
    model(node, vm, exp) {
        this.updated(node, vm, exp, 'model')
		//为需要双向绑定的元素绑定input change事件
        node.addEventListener("input", function (e) {
            vm[exp] = e.target.value
        })
    }
	//1.html解析方法
    html(node, vm, exp) {
        this.updated(node, vm, exp, 'html')
    }
	//2.文本解析方法
    text(node, vm, exp) {
        this.updated(node, vm, exp, 'text')
    }
	//3.事件解析方法
    EventHander(node, vm, exp, dir) {
        let fn = vm.$options.methods && vm.$options.methods[exp];
        if (dir && fn) {
            node.addEventListener(dir, fn.bind(vm));
        }
    }
    // 编译插值表达式
    textCompile(node) {
        let exp = RegExp.$1;
        this.updated(node, this.$vm, exp, 'text')
    }
	//初始化页面,注册wather的集合
    updated(node, vm, exp, dir) {
        let updateFn = this[dir + 'Update'];
        updateFn && updateFn(node, vm[exp])
        new Watcher(vm, exp, function (value) {
            updateFn(node, value)
        })
    }
    textUpdate(node, value) {
        node.textContent = value
    }
    htmlUpdate(node, value) {
        node.innerHTML = value
    }
    modelUpdate(node, value) {
        node.value = value
    }
    // 判断是否为指令
    isDirective(attrName) {
        return attrName.indexOf('k-') === 0;
    }
    // 判断是否为事件
    isEvent(attrName) {
        return attrName.indexOf('@') === 0;
    }
    // 判断是否为元素节点
    isElement(node) {
        return node.nodeType == 1;
    }
    // 判断是否为插值表达式
    isInterpolation(node) {
        return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
    }
}

第三步实现Dep

  • 发布者,每一个vue的data属性都对应一个Dep,Dep是一个数组,可能包含多个或零个,取决于html中是否使用了该属性,如果同一个属性多次出现在html中,那么该属性对应的Dep含有多个watcher。
  • 初始化,定义Dep实例含有一个deps属性,该属性是一个数组。
  • 添加一个addDep方法,用来添加watcher实例到对应的Dep中
  • 添加一个notify方法,用来通知每一个watcher进行页面更新。
// 订阅者 用来管理watcher
class Dep{
   constructor() {
       this.deps=[]
   }
   addDep(dep){
       this.deps.push(dep)
   }
   notify(){
       this.deps.forEach(item=>{
           item.update()
       })
   }
}

第四步完善Observe

  • 将每一个vue中data的属性都分配一个发布者Dep
  • 每当获取该属性值得时候,就可以将该属性对应的订阅者watcher添加到Dep数组中了。
  • 每当属性值发生变化时,该属性对应的Dep就会调用自身的notify() 方法来通知所包含的watcher改变视图。
//new KVue(data:{....})
class KVue{
    constructor(options) {
        this.$options = options;
        this.$data = options.data;
        this.observe(this.$data);
        // new Watcher();
        // this.name;
        this.$data.name = "我是"
        new Compile(options.el,this)
		//绑定created方法到KVue实例上,使用了call(this) 这也就是为什么created中this指向KVue的原因了
        if(options.created){
            options.created.call(this)
        }
    }
    observe(dataObj){
        if(!dataObj || typeof(dataObj) !=='object'){
            return false;
        }
        Object.keys(dataObj).forEach(key=>{
            this.defineReactive(dataObj,key,dataObj[key])
            //代理data中的属性到vue实例上
            this.proxyData(key);
        })
    }
    // 代理data中的属性到vue实例上 ,方便获取和设置KVue实例data中的值
    proxyData(key){
        Object.defineProperty(this,key,{
            get(){
                return this.$data[key]
            },
            set(newValue){
                console.log(newValue)
                this.$data[key] = newValue
            }
        })
    }
    // 将data每一个属性添加set 和get 方法
    defineReactive(dataObj,key,value){
        this.observe(value)
        const dep = new Dep();
        Object.defineProperty(dataObj,key,{
            get(){
			//当获取该属性值得时候,就可以将该属性对应的订阅者添加到Dep数组中了。
                Dep.target && dep.addDep(Dep.target)
                return value
            },
            set(newValue){
               if(newValue === value){
                   return;
               }
               value = newValue
			   //当值发生变化时,dep通知它所包含的watcher进行相应的更新。
               dep.notify();
            }
        })
    }
}

第五步实现Watcher

  • 在Compile代码中我们能够发现,在遍历整个dom节点中只要发现data中含有的属性或者methods中的方法都会new Watcher生产一个watcher;

  • 也就是说页面中同一个data属性可能有多个watcher。

  • watcher用来将node和KVue中的数据联系起来,在Observe代码中,我们已经为每一个data属性添加了一个自定义的get方法,在该方法中我们需要将对应的watcher添加到Dep中,那么如何才能确保每次只有一个watcher实例能,这时候我们需要通过Dep.target来指向watcher实例。

  • 初始化在Compile代码中 new Watcher(Kvue实例,data中对应的属性,初始化页面的方法)

  • Dep.target指向自己,确保单例

  • 访问一下vm[exp],触发get方法,将watcher添加得到Dep中

  • 清除Dep.target;

  • 创建一个更新方法,通过调用实例化传过来的初始化方法来改变vue页面。

class Watcher {
    constructor(vm, exp, cb) {
        this.$vm = vm;
        this.exp = exp;
        this.$cb = cb;
        Dep.target = this;
        vm[exp]
        Dep.target = null;
    }
    update() {
        console.log("我要更新处理",this.$vm[this.exp])
        this.$cb.call(this.$vm,this.$vm[this.exp])
    }
}
### Vue.js 中数据响应式的实现原理 #### 1. 数据响应式的核心概念 Vue 的数据响应式机制主要依赖于 **Observer**、**Watcher** 和 **Dep** 这三个核心模块来完成。 - **Observer**: 负责将数据对象的所有属性转换成 getter/setter 形式,从而能够监听到这些属性的变化[^2]。 - **Watcher**: 当数据发生改变时,Watcher 会接收到通知并执行相应的更新操作[^1]。 - **Dep**: 它是一个发布订阅模式的实现,用于连接 Observer 和 Watcher。每当一个响应式数据被访问时,当前活跃的 Watcher 就会被添加到 Dep 中;而当数据发生变化时,Dep 则会通知所有的 Watcher 执行回调[^3]。 #### 2. 数据劫持过程 (Observer) 在 Vue 初始化阶段,框架会对 `data` 对象中的每一个属性调用 `Object.defineProperty()` 方法将其转化为可观察的形式。这意味着每次读取或修改某个属性时,都会自动触发其对应的 getter 或 setter 函数。 ```javascript function observe(value, vm) { if (!value || typeof value !== 'object') return; Object.keys(value).forEach(key => defineReactive(vm, value, key)); } function defineReactive(target, obj, key) { let val = obj[key]; const dep = new Dep(); // 创建一个新的 Dep 实例 observe(val); // 如果子属性还是对象,则继续递归观测 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { console.log(`get ${key}`); if (Dep.target) { // 如果存在正在收集依赖的 Watcher dep.addSub(Dep.target); } return val; // 返回原始值 }, set(newVal) { if (newVal === val) return; console.log(`set ${key} to`, newVal); val = newVal; // 更新新值 dep.notify(); // 通知所有订阅者(Watchers) } }); } ``` 上述代码展示了如何利用 JavaScript 的原生特性去拦截对对象属性的操作,并在此基础上构建起整个响应式体系的基础结构。 #### 3. Wacher 工作流程 一旦绑定好 DOM 元素或者组件上的模板表达式之后,在编译过程中就会创建对应数量的 Watcher 来追踪特定变量的状态变化情况。如果检测到了任何变动迹象的话,那么关联起来的那个 View 层面的部分也会随之做出相应调整。 具体来说就是说每个指令/插槽背后都隐藏着至少一个这样的实例化后的 watcher 对象,它们持续监控着自己所关心的目标节点及其上下文中涉及到的一切动态因素——无论是来自父级传下来的 props 参数也好,或者是内部维护状态机里的某些字段也罢,只要满足条件即可激活刷新动作链路直至最终呈现出预期效果为止。 #### 4. 发布订阅模式的应用 (Dep) 为了使多个不同的地方都能够感知同一个共享资源发生了什么类型的事件,因此引入了基于 Publish-Subscribe Pattern 设计理念下的 Dependency Manager 类型的角色扮演者即所谓的 “dep”。每当有新的 subscriber 加入进来准备接收消息推送服务之前都需要先注册成为合法成员名单的一员才行;与此同时还要记得及时清理那些已经失效不再使用的旧记录以免造成不必要的内存泄漏风险等问题出现。 总结而言,通过巧妙运用以上提到的技术手段相结合的方式共同协作完成了这样一个高度灵活高效的双向绑定解决方案! --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值