Vue双向数据绑定代码解读

118 篇文章 1 订阅
74 篇文章 3 订阅

 Vue核心基础-CSDN博客

数据双向绑定原理_哔哩哔哩_bilibili

原理示意图

 前置知识

reduce()方法

用于链式获取对象的属性值

Object.defineProperty()方法

Object.defineProperty(obj, prop, descriptor)

  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称或 Symbol。
  • descriptor:将被定义或修改的属性描述符。

属性描述符(Descriptor)

属性描述符对象可以包含以下属性之一或多个:

  • value:属性的值(对于 getter 和 setter 属性,该属性会被忽略)。
  • writable:当且仅当该属性的值为 true 时,属性的值才可以被 [[Set]] 操作改变(即可以重新赋值)。默认为 false
  • enumerable:当且仅当该属性的值为 true 时,该属性才会出现在对象的枚举属性中(例如,通过 for...in 循环或 Object.keys() 方法)。默认为 false
  • configurable:当且仅当该属性的值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false
  • get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,会调用此 getter 方法,执行时不传入任何参数,但是会传入 this 对象(即该属性的宿主对象)。
  • set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,会调用此 setter 方法。该方法将接受唯一参数,即被赋予的新值。
const object1 = {};  
  
Object.defineProperty(object1, 'property1', {  
  value: 42,  
  writable: false  
});  
  
console.log(object1.property1); // 输出:42  
  
// 尝试修改属性值  
object1.property1 = 100;  
console.log(object1.property1); // 输出:42,因为 writable 为 false  
  
// 尝试删除属性  
delete object1.property1;  
console.log(object1.property1); // 输出:42,因为 configurable 为默认值 false,所以属性不能被删除
const object2 = {  
  _value: 42  
};  
 ------------------------------------------------------------------------------------------
Object.defineProperty(object2, 'value', {  
  get() {  
    return this._value;  
  },  
  set(newValue) {  
    if (newValue < 0) {  
      throw new Error('值必须大于或等于0');  
    }  
    this._value = newValue;  
  }  
});  
  
console.log(object2.value); // 输出:42  
  
object2.value = 100;  
console.log(object2.value); // 输出:100  
  
// 尝试设置无效值  
object2.value = -1; // 抛出错误:值必须大于或等于0

发布订阅者模式

Javascript常见设计模式-CSDN博客

流程

数据劫持(递归实现深层次)

1. 初始化过程

当 Vue 实例被创建时,它会通过 Vue.options.data 函数(或组件的 data 函数)获取到初始数据对象。然后,Vue 会遍历这个对象的所有属性,并使用 Object.defineProperty() 将它们转换为 getter/setter。

2. 递归转换

对于对象中的每个属性,Vue 会检查其值是否为对象或数组。如果是,Vue 会递归地调用一个内部函数(如 observe),以确保这个对象或数组中的所有属性也被转换为响应式。

  • 对象:对于对象,Vue 会遍历其所有属性,并对每个属性应用 Object.defineProperty()
  • 数组:对于数组,Vue 不能直接通过 Object.defineProperty() 拦截数组索引的访问,因为数组的长度是动态的。Vue 通过修改数组原型上的方法(如 pushpopshiftunshiftsplicesortreverse)来实现对数组操作的拦截。
function defineReactive(obj, key, val) {  
  // 递归地将对象属性转换为 getter/setter  
  observe(val);  
  
  // 使用 Object.defineProperty 拦截属性的访问  
  Object.defineProperty(obj, key, {  
    enumerable: true,  
    configurable: true,  
    get: function reactiveGetter() {  
      // 这里可以添加依赖收集的逻辑(省略)  
      return val;  
    },  
    set: function reactiveSetter(newVal) {  
      if (newVal === val) return;  
      // 这里可以添加派发更新的逻辑(省略)  
      // 如果 newVal 是对象或数组,则进行递归劫持  
      observe(newVal);  
      val = newVal;  
    }  
  });  
}  
  
function observe(value) {  
  if (!isObject(value) || value instanceof VNode) {  
    return;  
  }  
    
  // 数组需要特殊处理,因为不能拦截索引的访问  
  if (Array.isArray(value)) {  
    // 这里可以扩展为修改数组原型方法(省略)  
    // 或者使用 Object.defineProperty 对数组的长度进行劫持(但通常不推荐)  
    // 这里只是简单处理,不展开  
  } else {  
    // 遍历对象的所有属性  
    Object.keys(value).forEach(function (key) {  
      defineReactive(value, key, value[key]);  
    });  
  }  
}  
  
function isObject(value) {  
  // 简单的类型检查  
  return value !== null && typeof value === 'object';  
}  
  

代理数据劫持

Object.keys(this.$data).forEach((key)=>{
    Object.defineProperty(this,key,{
        enumerable:true,
        configurable:true,
        get(){
           return this.$data[key]
        },
        set(vaue){
           this.$data[key]=value
        }
})
})

模板编译

Vue 2 的模板编译流程是一个复杂但有序的过程,它涉及到将 Vue 模板(通常是 HTML 字符串,可能包含 Vue 特有的指令和插值表达式)转换成高效的渲染函数(render function)。这个过程主要在 Vue 的内部实现,特别是通过 vue-template-compiler 包来完成。以下是 Vue 2 模板编译的大致流程:

  1. 解析模板(Parse)
    • 将模板字符串转换为抽象语法树(AST)。AST 是一种树状的数据结构,用于表示源代码的语法结构。
    • 在这一步,Vue 会识别出模板中的所有元素、属性、指令(如 v-bindv-model)、插值表达式(如 {{ message }})等。
  2. 优化 AST(Optimize)
    • 对 AST 进行静态分析,标记出哪些部分是静态的(在多次渲染中不会改变),哪些部分是动态的。
    • Vue 会利用这些静态信息来优化渲染过程,比如通过静态提升(hoisting)来避免不必要的DOM操作。
  3. 生成渲染函数(Generate)
    • 将优化后的 AST 转换成 JavaScript 渲染函数。这个函数是一个纯 JavaScript 函数,它接收组件的上下文(如 props、data、computed、methods 等)作为参数,并返回一个虚拟 DOM(VNode)树。
    • 渲染函数是 Vue 组件渲染过程的核心,它会在组件的每次更新时被调用,并生成新的 VNode 树。
  4. 挂载或更新 DOM
    • Vue 的运行时(runtime)会接收渲染函数生成的 VNode 树,并将其与实际的 DOM 进行比较(使用虚拟 DOM 的 diff 算法)。
    • 根据比较结果,Vue 会最小化地进行 DOM 更新,以达到高效渲染的目的。

需要注意的是,这个过程是在 Vue 组件的编译阶段完成的,而不是在运行时。当你使用 Vue 的单文件组件(.vue 文件)或直接在 JavaScript 中定义模板时,Vue 的构建工具(如 webpack、Vue CLI)会在构建过程中调用 vue-template-compiler 来编译模板。

简化版的编译过程

下面是一个非常简化的模拟过程,说明Vue是如何处理{{ }}插值表达式的:

  1. 解析模板:首先,模板(HTML字符串)会被解析成一个抽象语法树(AST)。在这个过程中,Vue会识别出模板中的所有Vue特有的指令和{{ }}插值表达式。

  2. 转换AST:然后,Vue会遍历这个AST,并将{{ }}插值表达式转换为特定的代码块。对于每个{{ }}插值,Vue会生成一个JavaScript表达式,该表达式在组件的渲染过程中会被计算,并用于替换原始的{{ }}文本。

  3. 生成渲染函数:最后,Vue会将转换后的AST转换成一个JavaScript渲染函数。这个渲染函数会基于组件的状态(如数据、计算属性等)来生成最终的HTML字符串。

示例:模拟处理{{ }}插值

虽然Vue的内部实现要复杂得多,但我们可以模拟一个非常简单的处理过程:

function compileTemplate(template) {  
  // 假设template是一个简单的字符串,我们手动替换{{ }}内的内容  
  // 实际应用中,你会使用正则表达式或更复杂的解析器来解析模板  
  let code = template.replace(/\{\{ (.*?)\}\}/g, (_, expr) => {  
    // 这里expr是`{{ }}`内的表达式  
    // 在Vue中,这个表达式会被转换成类似`_s(this.expr)`的JavaScript代码  
    // 这里我们简单地返回表达式本身,实际应用中你需要根据组件状态计算这个值  
    return `_s(${expr})`; // 假设_s是一个将值转换为字符串的函数  
  });  
  
  // 这里的code只是一个字符串示例,并不是真正的渲染函数  
  // 在Vue中,这个字符串会被转换成JavaScript代码,并生成渲染函数  
  console.log(code);  
  // 注意:这里只是为了演示,并没有真正执行任何渲染逻辑  
}  
  
// 示例模板  
let template = `<div>{{ message }}</div>`;  
  
// 编译模板  
compileTemplate(template);  
// 输出: "<div>_s(message)</div>"  
// 注意:这里的输出只是为了说明如何替换{{ }},并不是Vue实际生成的渲染函数

 正则表达式-CSDN博客

发布(set中)与订阅(get中)

// 定义一个函数来观察一个对象,使其属性变为响应式  
function observe(data) {    
    // 遍历对象的所有键  
    Object.keys(data).forEach(key => {    
        let internalValue = data[key];  // 获取当前属性的值  
        const dep = new Dep();  // 为每个属性创建一个依赖实例  
    
        // 使用Object.defineProperty来定义属性的getter和setter  
        Object.defineProperty(data, key, {    
            enumerable: true,  // 属性可枚举  
            configurable: true,  // 属性可配置  
            get() {    
                dep.depend();  // 访问属性时,收集依赖  
                return internalValue;  // 返回属性的值  
            },    
            set(newVal) {    
                if (newVal === internalValue) return;  // 如果新值等于旧值,则不执行任何操作  
                internalValue = newVal;  // 更新属性值  
                dep.notify();  // 通知所有依赖此属性的观察者,属性已更改  
            }    
        });    
    });    
}    
  
// 定义一个依赖类,用于收集依赖和通知观察者  
class Dep {    
    constructor() {    
        this.subscribers = [];  // 存储所有依赖此属性的观察者  
    }    
    
    // 依赖收集方法,将当前活动的观察者添加到依赖的订阅者列表中  
    depend() {    
        if (Dep.target) {    
            this.subscribers.push(Dep.target);    
        }    
    }    
    
    // 通知所有订阅者(观察者)更新  
    notify() {    
        this.subscribers.forEach(sub => sub.update());    
    }    
    
    // 静态属性,用于存储当前活动的观察者  
    static target = null;    
}    
  
// 定义一个观察者构造函数,用于观察Vue实例上的表达式  
function watcher(vm, exp, cb) {    
    // 设置当前活动的观察者为当前watcher实例  
    Dep.target = this;    
    this.vm = vm;  // 绑定Vue实例  
    this.exp = exp;  // 绑定要观察的表达式  
    this.cb = cb;  // 绑定回调函数  
    // 触发getter,进行依赖收集  
    this.value = vm[exp];   
    // 清除当前活动的观察者,避免污染后续操作  
    Dep.target = null;    
}    
  
// 定义watcher实例的update方法,用于在数据变化时执行回调函数  
watcher.prototype.update = function() {    
    const newValue = this.vm[this.exp];  // 获取最新值  
    if (newValue !== this.value) {  // 如果新值不等于旧值  
        this.cb(newValue);  // 执行回调函数,并传入新值  
        this.value = newValue;  // 更新旧值  
    }    
};  
  
// 注意:上述代码仅为演示Vue响应式系统的一部分,并未完全模拟Vue的全部功能。  
// 在Vue中,watcher的创建和管理、以及Dep的target的设置和清除通常是通过Vue的内部机制来完成的。

输入框中数据改变

(涉及到解析模板指令v-model)

<template>  
  <div>  
    <input :value="message" @input="updateMessage" placeholder="edit me">  
    <p>Message is: {{ message }}</p>  
  </div>  
</template>  
  
<script>  
export default {  
  data() {  
    return {  
      message: ''  
    }  
  },  
  methods: {  
    updateMessage(event) {  
      // 你可以从event.target.value获取到输入框的值  
      this.message = event.target.value;  
    }  
  }  
}  
</script>

不是很了解的:模板的编译解析

在Vue.js 2.x中,模板编译和解析是Vue内部的一个复杂过程,它主要负责将Vue模板(HTML字符串或模板文件)转换成渲染函数(render function)。这个过程并不是直接通过DOM操作完成的,而是利用了JavaScript的字符串处理、正则表达式以及Vue的编译系统内部逻辑。然而,理解这个过程涉及到的一些基本概念和工具是有帮助的。

模板编译与解析的概述

Vue模板编译主要发生在Vue的初始化阶段,它涉及到以下几个步骤:

  1. 解析模板:将模板字符串转换成AST(抽象语法树)。
  2. 优化AST:静态内容提升等优化操作。
  3. 生成代码:将AST转换成渲染函数代码字符串。
  4. 编译成函数:使用new Function()将渲染函数代码字符串编译成可执行函数。

常用到的DOM操作方法(间接相关)

虽然Vue的模板编译过程不直接操作DOM,但Vue的渲染函数和虚拟DOM系统最终会操作DOM。不过,在模板编译阶段,我们讨论的是字符串处理和JavaScript操作,而非直接的DOM操作。然而,了解Vue如何与DOM交互是有帮助的:

  • createElementappendChildremoveChild等(Vue内部通过虚拟DOM模拟这些操作)。

常用到的正则表达式

在Vue模板编译的上下文中,正则表达式通常用于解析模板字符串,提取指令(如v-bindv-model等)、插值表达式({{ }})等。不过,Vue的源代码中这些正则表达式是高度定制的,且随着版本更新而变化。这里提供一个简化的例子,说明如何可能使用正则表达式来识别插值表达式:

// 简化的正则表达式,用于匹配插值表达式 
const interpolationRE = /\{\{([^}]+)\}\}/g; 


let template = 'Hello, {{ name }}!'; 
let matches = template.match(interpolationRE); 
if (matches) { 
console.log(matches[1]); // 输出: name 
}

常用到的JavaScript方法

在Vue模板编译过程中,会大量使用JavaScript的字符串和数组方法,以及对象操作:

  • 字符串方法replace()split()trim()indexOf()substring()等,用于处理和转换模板字符串。
  • 数组方法map()filter()reduce()forEach()等,用于遍历和处理AST节点。
  • 对象方法hasOwnProperty()Object.keys()Object.assign()Object.create()等,用于操作对象属性和原型链。
  • 正则表达式相关方法exec()test()match()等,用于在模板字符串中查找和匹配特定的模式。

总结

Vue的模板编译和解析是一个复杂的过程,它涉及到JavaScript的多个方面,包括字符串处理、正则表达式、数组和对象操作等。然而,这个过程主要发生在Vue内部,开发者通常不需要直接处理。理解Vue模板编译的基本原理和目的,有助于更好地理解和使用Vue框架。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值