从零到一手写迷你版Vue

news2024/11/30 0:29:55

Vue响应式设计思路

Vue响应式主要包含:

  • 数据响应式
  • 监听数据变化,并在视图中更新
  • Vue2使用Object.defineProperty实现数据劫持
  • Vu3使用Proxy实现数据劫持
  • 模板引擎
  • 提供描述视图的模板语法
  • 插值表达式{{}}
  • 指令 v-bind, v-on, v-model, v-for,v-if
  • 渲染
  • 将模板转换为html
  • 解析模板,生成vdom,把vdom渲染为普通dom

数据响应式原理

在这里插入图片描述

数据变化时能自动更新视图,就是数据响应式
Vue2使用Object.defineProperty实现数据变化的检测

原理解析

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

在这里插入图片描述

一些关键类说明

CVue:自定义Vue类 Observer:执⾏数据响应化(分辨数据是对象还是数组) Compile:编译模板,初始化视图,收集依赖(更新函数、 watcher创建) Watcher:执⾏更新函数(更新dom) Dep:管理多个Watcher实例,批量更新

参考 前端手写面试题详细解答

涉及关键方法说明

observe: 遍历vm.data的所有属性,对其所有属性做响应式,会做简易判断,创建Observer实例进行真正响应式处理

html页面

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>cvue</title>
  <script src="./cvue.js"></script>
</head>
<body>
  <div id="app">
    <p>{{ count }}</p>
  </div>

  <script>
    const app = new CVue({      el: '#app',      data: {        count: 0
      }    })    setInterval(() => {      app.count +=1
    }, 1000);  </script>
</body>
</html>

CVue

  • 创建基本CVue构造函数:
  • 执⾏初始化,对data执⾏响应化处理
// 自定义Vue类
class CVue {
  constructor(options) {
    this.$options = options
    this.$data = options.data

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

// 数据响应式, 修改对象的getter,setter
function defineReactive(obj, key, val) {
  // 递归处理,处理val是嵌套对象情况
  observe(val)
  Object.defineProperty(obj, key, {
    get() {
      return val
    },
    set(newVal) {
      if(val !== newVal) {
        console.log(`set ${key}:${newVal}, old is ${val}`)

        val = newVal
        // 继续进行响应式处理,处理newVal是对象情况
        observe(val)
      }
    }
  })
}

// 遍历obj,对其所有属性做响应式
function observe(obj) {
  // 只处理对象类型的
  if(typeof obj !== 'object' || obj == null) {
    return
  }
  // 实例化Observe实例
  new Observe(obj)
}

// 根据传入value的类型做相应的响应式处理
class Observe {
  constructor(obj) {
    if(Array.isArray(obj)) {
      // TODO
    } else {
      // 对象
      this.walk(obj)
    }
  }
  walk(obj) {
    // 遍历obj所有属性,调用defineReactive进行响应化
    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
  }
}

为vm.$data做代理

方便实例上设置和获取数据

例如

原本应该是

vm.$data.count
vm.$data.count = 233

代理之后后,可以使用如下方式

vm.count
vm.count = 233

给vm.$data做代理

class CVue {
  constructor(options) {
    // 省略
    // 响应化处理
    observe(this.$data)

    // 代理data上属性到实例上
    proxy(this)
  }
}

// 把CVue实例上data对象的属性到代理到实例上
function proxy(vm) {
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get() {
        // 实现 vm.count 取值
        return vm.$data[key]
      },
      set(newVal) {
        // 实现 vm.count = 123赋值
        vm.$data[key] = newVal
      }
    })
  })
}

编译

在这里插入图片描述

初始化视图

根据节点类型进行编译
class CVue {
  constructor(options) {
    // 省略。。
    // 2 代理data上属性到实例上
    proxy(this)

    // 3 编译
    new Compile(this, this.$options.el)
  }
}

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
  constructor(vm, el) {
    this.$vm = vm
    this.$el = document.querySelector(el)

    if(this.$el) {
      this.complie(this.$el)
    }
  }
  // 编译
  complie(el) {
    // 取出所有子节点
    const childNodes = el.childNodes
    // 遍历节点,进行初始化视图
    Array.from(childNodes).forEach(node => {
      if(this.isElement(node)) {
        // TODO
        console.log(`编译元素 ${node.nodeName}`)
      } else if(this.isInterpolation(node)) {
        console.log(`编译插值文本 ${node.nodeName}`)
      }
      // 递归编译,处理嵌套情况
      if(node.childNodes) {
        this.complie(node)
      }
    })
  }
  // 是元素节点
  isElement(node) {
    return node.nodeType === 1
  }
  // 是插值表达式
  isInterpolation(node) {
    return node.nodeType === 3
      && /\{\{(.*)\}\}/.test(node.textContent)
  }
}
编译插值表达式
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
  complie(el) {
    Array.from(childNodes).forEach(node => {
      if(this.isElement(node)) {
        console.log(`编译元素 ${node.nodeName}`)
      } else if(this.isInterpolation(node)) {
        // console.log(`编译插值文本 ${node.textContent}`)
        this.complieText(node)
      }
      // 省略
    })
  }
  // 是插值表达式
  isInterpolation(node) {
    return node.nodeType === 3
      && /\{\{(.*)\}\}/.test(node.textContent)
  }
  // 编译插值
  complieText(node) {
    // RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
    // 相等于{{ count }}中的count
    const exp = String(RegExp.$1).trim()
    node.textContent = this.$vm[exp]
  }
}
编译元素节点和指令

需要取出指令和指令绑定值
使用数据更新视图

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
  complie(el) {
    Array.from(childNodes).forEach(node => {
      if(this.isElement(node)) {
        console.log(`编译元素 ${node.nodeName}`)
        this.complieElement(node)
      }
      // 省略
    })
  }
  // 是元素节点
  isElement(node) {
    return node.nodeType === 1
  }
  // 编译元素
  complieElement(node) {
    // 取出元素上属性
    const attrs = node.attributes
    Array.from(attrs).forEach(attr => {
      // c-text="count"中c-text是attr.name,count是attr.value
      const { name: attrName, value: exp } = attr
      if(this.isDirective(attrName)) {
        // 取出指令
        const dir = attrName.substring(2)
        this[dir] && this[dir](node, exp)
      }
    })
  }
  // 是指令
  isDirective(attrName) {
    return attrName.startsWith('c-')
  }
  // 处理c-text文本指令 
  text(node, exp) {
    node.textContent = this.$vm[exp]
  }
  // 处理c-html指令
  html(node, exp) {
    node.innerHTML = this.$vm[exp]
  }
}

以上完成初次渲染,但是数据变化后,不会触发页面更新

依赖收集

视图中会⽤到data中某key,这称为依赖
同⼀个key可能出现多次,每次出现都需要收集(⽤⼀个Watcher来维护维护他们的关系),此过程称为依赖收集。
多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。

  • data中的key和dep是一对一关系
  • 视图中key出现和Watcher关系,key出现一次就对应一个Watcher
  • dep和Watcher是一对多关系

实现思路

  • defineReactive中为每个key定义一个Dep实例
  • 编译阶段,初始化视图时读取key, 会创建Watcher实例
  • 由于读取过程中会触发key的getter方法,便可以把Watcher实例存储到key对应的Dep实例
  • 当key更新时,触发setter方法,取出对应的Dep实例Dep实例调用notiy方法通知所有Watcher更新
定义Watcher类

监听器,数据变化更新对应节点视图

// 创建Watcher监听器,负责更新视图
class Watcher {
  // vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
  constructor(vm, key, updateFn) {
    this.$vm = vm
    this.$key = key
    this.$updateFn = updateFn
  }
  update() {
    // 调用更新函数,获取最新值传递进去
    this.$updateFn.call(this.$vm, this.$vm[this.$key])
  }
}
修改Compile类中的更新函数,创建Watcher实例
class Complie {
  // 省略。。。
  // 编译插值
  complieText(node) {
    // RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
    // 相等于{{ count }}中的count
    const exp = String(RegExp.$1).trim()
    // node.textContent = this.$vm[exp]
    this.update(node, exp, 'text')
  }
  // 处理c-text文本指令 
  text(node, exp) {
    // node.textContent = this.$vm[exp]
    this.update(node, exp, 'text')
  }
  // 处理c-html指令
  html(node, exp) {
    // node.innerHTML = this.$vm[exp]
    this.update(node, exp, 'html')
  }
  // 更新函数
  update(node, exp, dir) {
    const fn = this[`${dir}Updater`]
    fn && fn(node, this.$vm[exp])

    // 创建监听器
    new Watcher(this.$vm, exp, function(newVal) {
      fn && fn(node, newVal)
    })
  }
  // 文本更新器
  textUpdater(node, value) {
    node.textContent = value
  }
  // html更新器
  htmlUpdater(node, value) {
    node.innerHTML = value
  }
}
定义Dep类
  • data的一个属性对应一个Dep实例
  • 管理多个Watcher实例,通知所有Watcher实例更新
// 创建订阅器,每个Dep实例对应data中的一个属性
class Dep {
  constructor() {
    this.deps = []
  }
  // 添加Watcher实例
  addDep(dep) {
    this.deps.push(dep)
  }
  notify() {
    // 通知所有Wather更新视图
    this.deps.forEach(dep => dep.update())
  }
}
创建Watcher时触发getter
class Watcher {
  // vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
  constructor(vm, key, updateFn) {
    // 省略
    // 把Wather实例临时挂载在Dep.target上
    Dep.target = this
    // 获取一次属性,触发getter, 从Dep.target上获取Wather实例存放到Dep实例中
    this.$vm[key]
    // 添加后,重置Dep.target
    Dep.target = null
  }
}
defineReactive中作依赖收集,创建Dep实例
function defineReactive(obj, key, val) {
  // 递归处理,处理val是嵌套对象情况
  observe(val)

  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newVal) {
      if(val !== newVal) {
        val = newVal
        // 继续进行响应式处理,处理newVal是对象情况
        observe(val)
        // 更新视图
        dep.notify()
      }
    }
  })
}

监听事件指令@xxx

  • 在创建vue实例时,需要缓存methods到vue实例上
  • 编译阶段取出methods挂载到Compile实例上
  • 编译元素时
  • 识别出v-on指令时,进行事件的绑定
  • 识别出@属性时,进行事件绑定
  • 事件绑定:通过指令或者属性获取对应的函数,给元素新增事件监听,使用bind修改监听函数的this指向为组件实例
// 自定义Vue类
class CVue {
  constructor(options) {
    this.$methods = options.methods
  }
}

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
  constructor(vm, el) {
    this.$vm = vm
    this.$el = document.querySelector(el)
    this.$methods = vm.$methods
  }

  // 编译元素
  complieElement(node) {
    // 取出元素上属性
    const attrs = node.attributes
    Array.from(attrs).forEach(attr => {
      // c-text="count"中c-text是attr.name,count是attr.value
      const { name: attrName, value: exp } = attr
      if(this.isDirective(attrName)) {
        // 省略。。。
        if(this.isEventListener(attrName)) {
          // v-on:click, subStr(5)即可截取到click
          const eventType = attrName.substring(5)
          this.bindEvent(eventType, node, exp)
        }
      } else if(this.isEventListener(attrName)) {
        // @click, subStr(1)即可截取到click
        const eventType = attrName.substring(1)
        this.bindEvent(eventType, node, exp)
      }
    })
  }
  // 是事件监听
  isEventListener(attrName) {
    return attrName.startsWith('@') || attrName.startsWith('c-on')
  }
  // 绑定事件
  bindEvent(eventType, node, exp) {
    // 取出表达式对应函数
    const method = this.$methods[exp]
    // 增加监听并修改this指向当前组件实例
    node.addEventListener(eventType, method.bind(this.$vm))
  }
}

v-model双向绑定

实现v-model绑定input元素时的双向绑定功能

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
  // 省略...
  // 处理c-model指令
  model(node, exp) {
    // 渲染视图
    this.update(node, exp, 'model')
    // 监听input变化
    node.addEventListener('input', (e) => {
      const { value } = e.target
      // 更新数据,相当于this.username = 'mio'
      this.$vm[exp] = value
    })
  }
  // model更新器
  modelUpdater(node, value) {
    node.value = value
  }
}

数组响应式

  • 获取数组原型
  • 数组原型创建对象作为数组拦截器
  • 重写数组的7个方法
// 数组响应式
// 获取数组原型, 后面修改7个方法
const originProto  = Array.prototype
// 创建对象做备份,修改响应式都是在备份的上进行,不影响原始数组方法
const arrayProto = Object.create(originProto)
// 拦截数组方法,在变更时发出通知
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  // 在备份的原型上做修改
  arrayProto[method] = function() {
    // 调用原始操作
    originProto[method].apply(this, arguments)
    // 发出变更通知
    console.log(`method:${method} value:${Array.from(arguments)}`)
  }
})

class Observe {
  constructor(obj) {
    if(Array.isArray(obj)) {
      // 修改数组原型为自定义的
      obj.__proto__ = arrayProto
      this.observeArray(obj)
    } else {
      // 对象
      this.walk(obj)
    }
  }
  observeArray(items) {
    // 如果数组内部元素时对象,继续做响应化处理
    items.forEach(item => observe(item))
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/3208.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高性能服务器之Reactor设计

今天来针对上一节课讲的多路转接知识再进一步进行设计&#xff0c;Reactor是基于epoll的ET模式设计的&#xff0c;在现在的高校和企业中是广泛应用的&#xff0c;今天我们来实现一个简洁版&#xff0c;完整版博主可没那个实力~ 目录 基本原理 代码实现 epoll_server.cc A…

当面试官让我回答React和Vue框架的区别......

我们为什么需要错误边界 在React组件中可能会由于某些JavaScript错误&#xff0c;导致一些无法追踪的错误&#xff0c;导致应用崩溃。部分 UI 的 JavaScript 错误不应该导致整个应用崩溃。为此&#xff0c;React引入了错误边界(Error Boundary)的概念&#xff1a;可以捕获发生…

MySQL搭建主从复制流程及相关问题

目录一、关于主从复制1.1 关于主从复制1.2 应用场景1.3 优缺点1.4 原理二、配置主从复制2.1 同步各个服务器的时间2.2 修改主库&#xff08;M1&#xff09;配置2.3 主库&#xff08;M1&#xff09;为从库&#xff08;S1\S2&#xff09;增加账号2.3 查看主库&#xff08;M1&…

欢迎女神科学家颜宁回国,并祝她如愿以偿

目录1、女神科学家颜宁是谁2、颜宁在深圳人才论坛最新演讲&#xff0c;以及招聘邮箱3、颜宁微博回应4、结论与展望最近女神科学家颜宁回国了&#xff0c;整个科学界和中国都沸腾了&#xff0c;也上了热搜&#xff0c;成了热门话题&#xff0c;越来越多的海归精英选择回国 1、…

Python 和Java 哪个更适合做自动化测试?

很多小伙伴在功能测试行业工作了2、3年后&#xff0c;发现自己已经把功能测试做的非常好了&#xff0c;已经到职业发展和薪资发展的瓶颈期了&#xff0c;就想着学点东西&#xff0c;提升一下技能。 而对于功能测试升级来说&#xff0c;一般有这么3个主流的发展方向&#xff1a;…

事件/边沿检测--上升沿检测、下降沿检测

检测上升沿&#xff1a;&#xff08;从低到高的跳变 __| ) input sig_a; reg sig_a_d1; wire sig_a_risedge; alaways (posedge clk or negedge rstb) begin if(!rstb) sig_a_d1 < 1b0; else sig_a_d1 < sig_a; end assign sig_a_risedge sig_a & !sig_a_d1; …

【02】概率图模型在真实世界中的应用案例

概率图模型在真实世界中的应用案例 概率图模型有许多不同的实际应用。 为了激起大家对概率图模型的兴趣&#xff0c;也为了让大家能够对概率图模型有感性的认知&#xff0c;本章我会分享概率图模型的诸多实际应用案例。 文章目录图像中的概率模型图像生成图像修复图像降噪语言…

【Python百日进阶-WEB开发】Day171 - Django案例:03配置工程日志

文章目录八、配置工程日志8.1 目的和原因8.2 配置工程日志的步骤8.2.1 配置工程日志8.2.2 准备日志文件目录8.2.3 日志器记录器的使用8.2.4 Git管理工程日志九、配置前端静态文件9.1 准备静态文件9.2 指定静态文件的加载路径十、相关文档八、配置工程日志 8.1 目的和原因 目的…

什么是跨域?以及解决方案

现在的web项目&#xff0c;很多都是前后端分离&#xff0c;特别容易出现跨域问题 那么什么是跨域问题呢?本篇文章带你彻底从本质上弄明白什么是跨域问题以及如何解决 一、跨域有什么现象 首先我们看一下现象&#xff0c;如何出现的跨域问题。例&#xff1a; 前段&#xff1a…

【Linux】基础IO —— 上

&#x1f387;Linux&#xff1a;基础IO详解 博客主页&#xff1a;一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 看似不起波澜的日复一日&#xff0c;一定会在某一天让…

Web渗透测试攻防之浅述信息收集

前言 众所周知渗透测试的本质是信息收集&#xff0c;在渗透测试中信息收集的质量直接关系到渗透测试成果的与否。在对系统进行渗透测试前的信息收集是通过各种方式获取所需要的信息&#xff0c;收集的信息越多对目标进行渗透的优势越有利。通过利用获取到的信息对系统进行渗透…

Java Spring Cloud XVIII 之 Kafka I

Java Spring Cloud XVIII 之 Kafka I Kafka 1.Kafka简介 Kafka是由Apache软件基金会开发的一个开源流处理平台&#xff0c;由Scala和Java编写。该项目的目标是为处理实时数据提供一个统一、高吞吐、低延迟的平台。Kafka最初是由LinkedIn开发&#xff0c;并随后于2011年初开源…

C++ 类和对象 (中)

作者&#xff1a;小萌新 专栏&#xff1a;C初阶 作者简介&#xff1a;大二学生 希望能和大家一起进步 本篇博客目标&#xff1a;梳理自己六个小时学到的知识 并且将类和对象知识分享给大家 专注的去做一件事 如果累了就去休息 C 类和对象 中本章学习目标前言一. 构造函数1.1 概…

破解系统密码与重装windows系统

数据来源 一、利用5次shift漏洞破解win7密码 1.1 漏洞 1. 在未登录时&#xff0c;连续按5次shift键&#xff0c;弹出程序C:\Windows\System32\sethc.exe 2. 部分win7及win10系统在未进入系统时&#xff0c;可以通过系统修复漏洞篡改系统文件名&#xff01; 注意&#xff1a;…

使用Maven部署到远程Linux服务器Tomcat

一、安装JDK 首先给服务器安装jdk&#xff0c;访问官网下载&#xff1a;Java Downloads | Oracle&#xff0c;下载图中的版本。首先我使用的是tomcat10&#xff0c;最低支持jdk1.8。安装了jdk19&#xff0c;是当时的最新版实测tomcat开启失败&#xff0c;新版jdk也不自带jre&a…

Windows上使用QEMU创建aarch64(ARM64)虚拟机

前言 随着国产化的推进&#xff0c;现在采用ARM、MIPS的机器越来越多&#xff0c;作为开发、运维人员要调测软件总不能每种架构的机器都去买一台吧&#xff1f;主要像博主这样的穷B&#xff0c;实在也是承受不起。。 需要的工具 1、QEMU Windows版官网下载地址&#xff1a;…

软件测试最最最重要的事

软件测试用例得出软件测试用例的内容&#xff0c;其次&#xff0c;按照软件测试写作方法&#xff0c;落实到文档中&#xff0c;两者是形式和内容的关系&#xff0c;好的测试用例不仅方便自己和别人查看&#xff0c;而且能帮助设计的时候考虑的更周。 一个好的测试用例必须包含…

Articulate360在线学习课件制作工具

Articulate是一款全新理念的在线和移动学习课件制作工具&#xff0c;可以说是目前国际上用户最广泛的e-learning课件制作工具之一。它包含了全新版的Storyline 360和Rise 360以及大量其他创作应用程序。使用Storyline 360开发可在所有设备上运行的自定义交互式课程&#xff0c;…

Java日志框架的发展历史,你不想了解一下吗

前言 相信大家在项目开发中肯定遇到过log4j&#xff0c;JUL&#xff0c;slf4j&#xff0c;logback&#xff0c;log4j2等日志框架相关名词&#xff0c;这些日志框架之间到底有什么关系&#xff0c;Java日志框架究竟经历了什么样的发展历程&#xff0c;相信有很多人都对此充满了好…

Socket 编程基础

文章目录一、socket 简介二、socket 编程接口介绍1. socket()函数2. bind()函数3. listen()函数4. accept()函数5. connect()函数6. 发送和接收函数read()函数recv()函数write()函数send()函数7. close()关闭套接字三、IP 地址格式转换函数inet_pton()函数inet_ntop()函数本篇会…