[javascript核心-02]彻底弄清Vue3响应式原理及手写实现

1. 响应式原理及实现

本文github地址:JavaScript_Interview_Everything大前端知识体系与面试宝典,从前端到后端,全栈工程师,六边形战士

1.1. 何为响应式?

如果我们有一个对象:

const obj = {
    name: 'flten',
    age: 16,
}

有一些函数使用了这个对象中的数据:

function fn1(){
    console.log(obj.name)
}

function fn2(){
    console.log(obj.name)
}

如何在obj对象的属性name发生改变时,让fn1和fn2函数自动重新触发更新数据呢?

能够在数据更新时通知到所有使用到它的函数,使其重新触发,这就是响应式。

1.2. 如何区分需要响应式和非响应式

但是有些函数是使用了响应式的数据,而有些则没有,那我们如何判断呢?最简单的方式就是把所有使用到了某个数据的所有函数都保存在一个数组中,数据发生改变时直接全部遍历函数重新执行就可以了。

// 01.js
const reactiveArr = [];

const obj = {
    name: 'flten',
    age: 16,
}

function fn1(){
    console.log(obj.name)
}

reactiveArr.push(fn1)

function fn2(){
    console.log(obj.name)
}

reactiveArr.push(fn2)

reactiveArr.forEach(fn => {
    fn()
})

等一下,难道每次我们都要手动push吗?重复的操作当然是可以封装在一个函数里了

//02.js
const reactiveArr = [];

function addReactive(fn){
    reactiveArr.push(fn)
}

const obj = {
    name: 'flten',
    age: 16,
}

function fn1(){
    console.log(obj.name)
}

addReactive(fn1)

function fn2(){
    console.log(obj.name)
}

addReactive(fn2)

reactiveArr.forEach(fn=>{
    fn()
})

看起来还不错,但是这里我们只是收集了使用了name属性的函数,如果我们还要收集age属性的函数呢?这时我们就需要另外一个数组和另外一个将函数添加到数组中的方法

//03.js
const reactiveNameArr = [];
const reactiveAgeArr = [];

function addNameReactive(fn){
    reactiveNameArr.push(fn)
}

function addAgeReactive(fn){
    reactiveAgeArr.push(fn)
}

const obj = {
    name: 'flten',
    age: 16,
}

// name 属性的使用收集
function fn1(){
    console.log(obj.name)
}

addNameReactive(fn1)

function fn2(){
    console.log(obj.name)
}

addNameReactive(fn2)

reactiveNameArr.forEach(fn=>{
    fn()
})

// age属性的使用收集

function fn3(){
    console.log(obj.age)
}

addAgeReactive(fn3)

function fn4(){
    console.log(obj.age)
}

addAgeReactive(fn4)

reactiveAgeArr.forEach(fn=>{
    fn()
})

但是这样太麻烦了,每一个数据都要另外新建一个数组和添加函数吗?什么方式可以封装这些重复操作呢?它可以自动生成一个数组和对应的函数操作呢?当然是类了,我们可以通过定义一个类,为每个响应式数据都实例化一个对象,这样就把这些操作封装起来不需要每次都手动重建了。

1.3. 依赖收集

我们将上面的操作封装为一个类:

//04.js
class Depend{
    constructor(){
        this.reactiveArr = []
    }

    addDepend(fn){
        this.reactiveArr.push(fn)
    }

    notify(){
        this.reactiveArr.forEach(fn=>fn())
    }
}

const obj = {
    name: 'flten',
    age: 16,
}

const dependName = new Depend();
const dependAge = new Depend();

// name 属性的使用收集
function fn1(){
    console.log(obj.name)
}

dependName.addDepend(fn1)

function fn2(){
    console.log(obj.name)
}

dependName.addDepend(fn2)

dependName.notify()

// age属性的使用收集

function fn3(){
    console.log(obj.age)
}

dependAge.addDepend(fn3)

function fn4(){
    console.log(obj.age)
}

dependAge.addDepend(fn4)

dependAge.notify()

/*
flten
flten
16
16
*/

1.4. 封装响应式函数

可是上面reactiveName.addDepend(fn1)这样的操作也进行了很多次,这样的情况下我们将它封装为一个函数,专门将数据变为响应式

//05.js
class Depend{
    constructor(){
        this.reactiveArr = []
    }

    addDepend(fn){
        this.reactiveArr.push(fn)
    }

    notify(){
        this.reactiveArr.forEach(fn=>fn())
    }
}

const obj = {
    name: 'flten',
    age: 16,
}

const dependName = new Depend();
const dependAge = new Depend();

function watch(depend, fn){
    depend.addDepend(fn)
}

// name 属性的使用收集
function fn1(){
    console.log(obj.name)
}

watch(dependName,fn1)

function fn2(){
    console.log(obj.name)
}

watch(dependName,fn2)

dependName.notify()

// age属性的使用收集

function fn3(){
    console.log(obj.age)
}

watch(dependAge,fn3)

function fn4(){
    console.log(obj.age)
}

watch(dependAge,fn4)

dependAge.notify()

/*
flten
flten
16
16
*/

1.5. Proxy数据监听,自动触发

但是,数据更新以后我们每次都要手动调用notify进行触发操作吗?这个动作可以自动执行吗?能够在数据发生变化时,去自动触发notify?因此我们需要在数据变化时能够监听到数据的变化,什么东西可以实现数据监听呢?答案是 Proxy 数据代理。

// 06.js
class Depend{
    constructor(){
        this.reactiveArr = []
    }

    addDepend(fn){
        this.reactiveArr.push(fn)
    }

    notify(){
        this.reactiveArr.forEach(fn=>fn())
    }
}

const obj = {
    name: 'flten',
    age: 16,
}

const dependName = new Depend();
const dependAge = new Depend();

// 将使用到监听数据的函数包裹为响应式
function watch(dependData, fn){
    dependData.addDepend(fn)
}

// 创建代理对象进行数据监听

const proxy = new Proxy(obj, {
    get: function(target, key, receiver){
        return Reflect.get(target, key, receiver)
    },
    set: function(target, key, newValue, receiver){
        Reflect.set(target, key, newValue, receiver)
        if(Object.is(key, 'name')){ 
            console.log(`属性${key}发生了变化,值变为${newValue}`)
            dependName.notify
         }
        if(Object.is(key, 'age')){ 
            console.log(`属性${key}发生了变化,值变为${newValue}`)
            dependAge.notify 
        }
    }
})

// name 属性的使用收集
function fn1(){
    console.log(proxy.name)
}

watch(dependName,fn1)

function fn2(){
    console.log(proxy.name)
}

watch(dependName,fn1)

// age 属性的使用收集

function fn3(){
    console.log(proxy.age)
}

watch(dependAge,fn1)

function fn4(){
    console.log(proxy.age)
}

watch(dependAge,fn1)

// age数据更新
proxy.age = 16
proxy.name = 'fltenwall'
proxy.age = 17
proxy.name = 'yj'

/*
属性age发生了变化,值变为16
属性name发生了变化,值变为fltenwall
属性age发生了变化,值变为17
属性name发生了变化,值变为yj
*/

1.6. 依赖收集的自动管理

我们看上面的代码,我们并不知道当前触发的是哪个数据,以及我们应该触发哪一个数据的notify方法,即我们不知道每一个数据对应的depend是什么,我们需要手动去判断,然后手动维护这种对应关系。

而且实际上我们开发中会用到多个对象,而多个对象又有多个属性,按照上面的 方案的话,我们需要手动为每一个对象的每一个属性都去手动实例化,让每一属性都对应一个depend对象,即数据依赖的数组及操作方法,可以这样实在太不方便了,我们可以用一个数据结构来保存这样所有的对应关系吗?即保存每一个属性和它对应的depend对象的关系?当然我们想到了映射,js中最好的描述映射关系的结果就是Map。不过不同的对象可能存在相同名称的属性,因此我们需要为每一个对象创建一个Map映射来关系其每一个属性和对应的depend对象关系。

这样就存在了多个Map,但我们又如何管理每个对象和每个Map的映射关系呢?我们可以使用Weakmap。因此我们的目标是实现如下结构:

const obj1 =  {name:'flten1',age:16}
const obj2 =  {name:'flten2',age:26}

//假设 obj1NameDepend是obj1的name属性对应的depend对象

// map1保存obj1对象的所有属性和其depend对象的对应关系
const map1 = new Map()
map1.set('name', obj1NameDepend)
map1.set('age', obj1AgeDepend)

// map2保存obj2对象的所有属性和其depend对象的对应关系
const map2 = new Map()
map1.set('name', obj2NameDepend)
map1.set('age', obj2AgeDepend)

// 通过objMap保存每个对象和map的对应关系
const objMap = new Weakmap()
objMap.set(obj1, map1)
objMap.set(obj2, map2)

// 获取到每个对象的某个属性所对应的depend对象
objMap.get(obj1).get(name)
objMap.get(obj2).get(age)

按照上面的分析,我们可以封装一个getDepend函数:

const objMap = new WeakMap()

// obj是对象,key是obj的属性
function getDepend(obj, key){
    // 取出obj对应的map,即取出obj每个属性和其对应的depend的映射表
    let map = objMap.get(obj)
    // 如果还没有对象obj对应的map映射表,则创建映射表
    // 并将其存入objMap
    if(!map){
        map = new Map()
        objMap.set(obj, map)
    }

    // 从映射表中取出obj对象的key属性所对应的depend对象
    let depend = map.get(key)
    // 同样如果还没有key属性对应的depend对象,则创建depend对象
    // 并将key与depend的对应关系存入映射表map
    if(!depend){
        depend = new Depend()
        map.set(key, depend)
    }
    return depend
}

1.7. 依赖收集的时机

但上面的getDepend函数应该在哪来执行呢?即我们在哪里能够知道数据被使用(访问或修改)了呢?因为我们使用了proxy代理,因此使用数据的操作会被proxy拦截监听,因此我们能够知道哪些数据被使用,能够得到正在使用的对象及其属性,因此在这里能够执行getDepend,获取或新建属性对应的depend对象,并将其添加到映射表map里。

但又如何知道是哪个函数正在操作该属性呢?因为我们需要将该函数添加到depend对象的依赖数组中,因此我们必须知道目前是哪个对象正在试图操作该属性。我们可以用一个全局变量来跟踪正在操作该属性的函数,这样我们就可以在proxygetset监听中获取到该对象,并将该对象添加到depend对象的依赖数组中。

后续每一次对某对象的某个属性的操作,都会被监听到,并且能够从map取出对应的depend对象,并且可以遍历执行所有已经添加到依赖数组中的函数,即发布更新数据的通知。

代码如下:

activeReactiveFn全局变量跟踪正在操作该属性的函数,因为我们可以用proxy知道目前正在操作的对象和属性,因此watch函数可以不传入对象作为第一个参数来区分不同对象

let activeReactiveFn = null
// 将使用到监听数据的函数包裹为响应式
function watch(fn){
    activeReactiveFn = fn
    fn()
    activeReactiveFn = null
}

在proxy中收集依赖和触发更新操作

const proxy = new Proxy(obj, {
    get: function(target, key, receiver){
        const depend = getDepend(target, key)
        depend.addDepend()
        return Reflect.get(target, key, receiver)
    },
    set: function(target, key, newValue, receiver){
        Reflect.set(target, key, newValue, receiver)
        const depend = getDepend(target, key)
        depend.notify()
    }
})

Depend类的addDepend方法则需要判断activeReactiveFn是否为null

    addDepend(){
        // 触发set操作时,依赖函数被执行
        // 如果依赖函数有获取值的操作,那么就会同时触发get
        // 而此时activeReactiveFn被重置为了null
        // 因此需要判断activeReactiveFn是否为null来决定是否将其添加到依赖数组
        if(activeReactiveFn){this.reactiveArr.push(activeReactiveFn)}
    }

目前的整体代码为:

//07.js
class Depend{
    constructor(){
        this.reactiveArr = []
    }

    addDepend(){
        if(activeReactiveFn){this.reactiveArr.push(activeReactiveFn)}
    }

    notify(){
        this.reactiveArr.forEach(fn=>fn())
    }
}

const obj = {
    name: 'flten',
    age: 16,
}

let activeReactiveFn = null
// 将使用到监听数据的函数包裹为响应式
function watch(fn){
    activeReactiveFn = fn
    fn()
    activeReactiveFn = null
}

const objMap = new WeakMap()

// obj是对象,key是obj的属性
function getDepend(obj, key){
    // 取出obj对应的map,即取出obj每个属性和其对应的depend的映射表
    let map = objMap.get(obj)
    // 如果还没有对象obj对应的map映射表,则创建映射表
    // 并将其存入objMap
    if(!map){
        map = new Map()
        objMap.set(obj, map)
    }

    // 从映射表中取出obj对象的key属性所对应的depend对象
    let depend = map.get(key)
    // 同样如果还没有key属性对应的depend对象,则创建depend对象
    // 并将key与depend的对应关系存入映射表map
    if(!depend){
        depend = new Depend()
        map.set(key, depend)
    }
    return depend
}

// 创建代理对象进行数据监听

const proxy = new Proxy(obj, {
    get: function(target, key, receiver){
        const depend = getDepend(target, key)
        // 触发set操作时,依赖函数被执行
        // 如果依赖函数有获取值的操作,那么就会同时触发get
        // 而此时activeReactiveFn被重置为了null
        // 因此需要判断activeReactiveFn是否为null来决定是否将其添加到依赖数组
        depend.addDepend()
        return Reflect.get(target, key, receiver)
    },
    set: function(target, key, newValue, receiver){
        Reflect.set(target, key, newValue, receiver)
        const depend = getDepend(target, key)
        depend.notify()
    }
})

// name 属性的使用收集
function fn1(){
    console.log(proxy.age)
}

watch(fn1)

// 匿名函数
watch(function(){console.log(proxy.age)})
watch(function(){console.log(proxy.name)})

// age数据更新
proxy.age = 16
proxy.name = 'fltenwall'
proxy.age = 17
proxy.name = 'yj'

1.8. 解决依赖重复收集的问题

同一个函数可能会被添加到一个属性的依赖数组中多次

watch(function(){
    console.log(proxy.age)
    console.log(proxy.age)
})

例如在上面代码这种情况下,age属性的依赖收集数组会将匿名函数添加进去两次,但实际我们只需要收集一次就可以了,因此我们要去重,而js里的Set()可以帮助我们直接解决这个问题。

直接将依赖收集数组改为Set类型即可。

this.reactiveArr = new Set()

1.9. 将对象自动用Proxy代理

在上面的代码中我们是手动对obj对象进行了proxy的代理监听,但是如果我们有多个对象,就需要对每一个都进行手动的代理监听,这显然是不合适的。

const obj1 = {name:'flten'},
const obj2 = {name:'flten2'},
const obj3 = {name:'flten3'},

const proxy1 = new Proxy(obj1, {...})
const proxy2 = new Proxy(obj2, {...})
const proxy3 = new Proxy(obj3, {...})

这样重复的创建过程我们可以将其封装为一个reactive函数,将每一个对象都用这个函数进行包装,用proxy进行代理。

function reactive(obj){
    return new Proxy(obj, {
            get: function(target, key, receiver){
                const depend = getDepend(target, key)
                depend.depend()
                return Reflect.get(target, key, receiver)
            },
            set: function(target, key, newValue, receiver){
                Reflect.set(target, key, newValue, receiver)
                const depend = getDepend(target, key)
                depend.notify()
            }
        })
}

const obj1 = {name:'flten'}
const obj2 = {name:'flten2'}
const obj3 = {name:'flten3'}

const proxy1 = reactive(obj1)
const proxy2 = reactive(obj2)
const proxy3 = reactive(obj3)

完整代码如下:

//08.js
class Depend{
    constructor(){
        this.reactiveArr = new Set()
    }

    depend() {
        if (activeReactiveFn) {
          this.reactiveArr.add(activeReactiveFn)
        }
      }

    notify(){
        this.reactiveArr.forEach(fn=>fn())
    }
}

let activeReactiveFn = null
// 将使用到监听数据的函数包裹为响应式
function watch(fn){
    activeReactiveFn = fn
    fn()
    activeReactiveFn = null
}

const objMap = new WeakMap()

// obj是对象,key是obj的属性
function getDepend(obj, key){
    // 取出obj对应的map,即取出obj每个属性和其对应的depend的映射表
    let map = objMap.get(obj)
    // 如果还没有对象obj对应的map映射表,则创建映射表
    // 并将其存入objMap
    if(!map){
        map = new Map()
        objMap.set(obj, map)
    }

    // 从映射表中取出obj对象的key属性所对应的depend对象
    let depend = map.get(key)
    // 同样如果还没有key属性对应的depend对象,则创建depend对象
    // 并将key与depend的对应关系存入映射表map
    if(!depend){
        depend = new Depend()
        map.set(key, depend)
    }
    return depend
}

// 创建代理对象进行数据监听
function reactive(obj){
    return new Proxy(obj, {
            get: function(target, key, receiver){
                const depend = getDepend(target, key)
                depend.depend()
                return Reflect.get(target, key, receiver)
            },
            set: function(target, key, newValue, receiver){
                Reflect.set(target, key, newValue, receiver)
                const depend = getDepend(target, key)
                depend.notify()
            }
        })
}

const obj1 = {age:16}
const obj2 = {age:17}
const obj3 = {age:18}

const proxy1 = reactive(obj1)
const proxy2 = reactive(obj2)
const proxy3 = reactive(obj3)

watch(function(){console.log(proxy1.age)})
watch(function(){console.log(proxy2.age)})
watch(function(){console.log(proxy3.age)})

// age数据更新
proxy1.age = 20
proxy1.age = 21
proxy1.age = 22

/*
16
17
18
20
21
22
*/

本文github地址:JavaScript_Interview_Everything大前端知识体系与面试宝典,从前端到后端,全栈工程师,六边形战士

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vivado 下 IP核之双端口 RAM 读写

目录 Vivado 下 IP核之双端口 RAM 读写 1、RAM IP 核简介 2、实验任务 3、程序设计 3.1、RAM IP 核配置 3.2、顶层模块设计 (1)绘制波形图 4、编写代码 4.1、顶层模块 ip_2port_ram 4.2、RAM 写模块设计 4.3、ram_wr 模块代码 4.4、RAM 读模…

Goby 漏洞发布|maxView Storage Manager 系统 dynamiccontent.properties.xhtml 远程代码执行漏洞

漏洞名称:maxView Storage Manager 系统 dynamiccontent.properties.xhtml 远程代码执行漏洞 English Name:maxView Storage Manager dynamiccontent.properties.xhtml RCE CVSS core: 9.8 影响资产数:1465 漏洞描述: maxVie…

架构之冷热分离

本文依据《从程序员到架构师》阅读有感,记录书中案例并且结合作者工作经历进行分析。 当数据量过大,业务查询慢甚至导致数据库服务器CPU飙升,导致数据库宕机,影响用户体验。 场景: 1.客户两年多产生了近2000万的工单…

Django - 页面静态化和crontab定时任务(二)

一. 前言 一个网页会有很多数据是不需要经常变动的,比如说首页,变动频率低而访问量大,我们可以把它静态化,这样就不需要每次有请求都要查询数据库再返回,可以减少服务器压力 我们可以使用Django的模板渲染功能完成页面…

Amazon SageMaker:探索AI绘画云端部署新方案

目录 1 从艺术实验到AI绘画2 什么是Amazon SageMaker?3 云端部署AI绘画应用3.1 模型构建与部署3.2 AI绘画测试(文生图) 4 亚马逊云科技中国峰会 1 从艺术实验到AI绘画 在过去,人们只希望基于已有的给定数据做一些预测和拟合,因此判别式模型得…

怎么登录远程轻量云服务器?

​  轻量云服务器是一种基于云计算技术的服务器,具有价格低廉、配置灵活、易于管理等优点。但是,由于轻量云服务器通常是在云端运行,需要通过远程连接才能进行管理和操作。那么,怎么登录远程轻量云服务器呢? 要远程连接轻量云服…

JMeter测试笔记(五):JDBC请求

引言: 进行性能测试时,我们有时需要模拟数据库的访问和操作,而JMeter中的JDBC请求可以帮助我们实现这个目的。 在本文中,我们将深入了解JDBC请求的使用方式,并学习如何正确配置它们来模拟对数据库的访问和操作。 如…

C#,彩票数学——彩票预测是玄学还是数学?什么是彩票分析?怎么实现彩票号码的预测?

彩票原理系列文章 彩票与数学——彩票预测是玄学还是数学?https://mp.csdn.net/mp_blog/creation/editor/122517043彩票与数学——常用彩票术语的统计学解释https://mp.csdn.net/mp_blog/creation/editor/122474853彩票与数学——彩票缩水的数学概念与原理https://…

Codeforces Round 877 div2 C No Prime Differences

目录 一、题目 二、题目分析 三、 一、题目 传送门 C. No Prime Differences time limit per test 2 seconds memory limit per test 256 megabytes input standard input output standard output You are given integers n and m. Fill an n by m grid with the…

连杆滑块伸缩模组的制作

1. 运动功能说明 连杆滑块伸缩模组的主要运动方式为舵机带动滑块沿着光轴平行方向做伸缩运动。 2. 结构说明 本模组主要是由舵机、滑块、光轴、连杆等组成。 3. 电子硬件 在这个示例中,我们采用了以下硬件,请大家参考: 主控板 Basra主控板&…

java ssm贸易平台-物流管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java ssm贸易平台-物流管理系统是一套完善的web设计系统(系统采用SSM框架进行设计开发,springspringMVCmybatis),对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主…

工业深度学习软件 从标注 训练 到测试 再到现场部署

工业深度学习软件 从标注 训练 到测试 再到现场部署 M7000技术规格表 Producer Specification 影像系统 Imaging Sys 适配相机 supported cameras 支持海康,迈德威视,度申2D相机(可根据需求增加适配其他厂家相机) Support for Hi…

为什么有了IP地址,还需要MAC地址呢?

不知道大家有没有困惑:为什么有了IP地址,还需要MAC地址呢?他们之间到底有什么联系?又有什么区别?是不是有一个是多余的? 流言传到了“IP地址”和“MAC地址”的耳朵里,他俩也非常苦恼&#xff0c…

php通过cURL爬取数据(2):CURLINFO_HTTP_CODE返回0

CURLINFO_HTTP_CODE返回0 一、项目说明二、curl_getinfo返回异常1.小鹅通SDK2.CURLINFO_HTTP_CODE为0的原因有哪些?3.返回CURLINFO_HTTP_CODE0的解决方案4.请求超时和服务器配置,CPU的使用率有关系吗5.结论 三、阿里云短信发送延迟后而集中发送1.发送集中…

【回眸】Python入门(五)基础语法列表和词典:Python如何消灭重复性劳动

前言 本篇博客为填坑篇,这个系列的上一篇竟然是2021年的9月30更新的,离谱,差点就到断更两周年纪念日了,后续逐渐走向填坑的每一天,继续创作,希望这个系列的专栏文章能帮助到更多有需要的人。 列表 什么是列…

操作系统的发展史(DOS/Windows篇)

█ DOS操作系统 20世纪70年代,伴随着计算机技术的成熟,操作系统也进入了一个快速发展阶段。现代操作系统的概念,也在那一时期逐渐形成。 1975年初,MITS电脑公司推出了基于Intel 8080芯片的Altair 8800微型计算机。这是人类历史上…

电子招标采购系统源码之从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。

统一供应商门户 便捷动态、呈现丰富 供应商门户具备内外协同的能力,为外部供应商集中推送展示与其相关的所有采购业务信息(历史合作、考察整改,绩效评价等),支持供应商信息的自助维护,实时风险自动提示。…

CSAPP - AttackLab实验(阶段1-5)

AttackLab实验 实验内容 官网:http://csapp.cs.cmu.edu/3e/labs.html “AttackLab”是一个Linux下的可执行C程序,包含了5个阶段(phase1~phase5)的不同内容。程序运行过程中,要求学生能够根据缓冲区的工作方式和程序…

Sui Builder House日本京都站开启报名

下一站Sui Builder House将于6月29-30日在日本京都举行,为世界各地的开发者提供身临其境地学习和交流的机会。 春日的樱花已经绽放,黄金周也已经过去,现在是时候来京都参加Sui Builder House一起庆祝夏天的来临了。来自日本和周边地区的开发…

我用ChatGPT写2023高考语文作文(一):全国甲卷

2023年 全国甲卷 适用地区:广西、贵州、四川、西藏 人们因技术发展得以更好地掌控时间,但也有人因此成了时间的仆人。 这句话引发了你怎样的联想与思考?请写一篇文章。 要求:选准角度,确定立意,明确文体&am…
最新文章