ES面试题

292 阅读4分钟

直接上题

1、new 一个箭头函数

  • 会报错,提示: function is not a constructor;
  • babel 编译时,会把 this 转成 (void 0);(面试补充说这句,表明自己了解babel这一块,对安全的undefined也了解)

2、哪些不能用 箭头函数?

  • arguments
  • yield
  • 构造函数的原型方法上
const Person = function (age, name) {
    this.age = age;
    this.name = name;
    
    //arguments是个伪数组
    const obj = { '0': 18, '1': 'luyi', '2': 'teacher', '3': 'esnext' };
    console.log(arguments.callee);//指向函数本身
    obj.length = 4;
    console.log(Array.prototype.slice.call(obj, 2));
}

const p = new Person(18, "luyi", "teacher", "esnext")
console.log(Array.prototype.slice.call(Person, 2));


// arguments / callee / caller
//callee 
//arguments 它包含着所有传入函数中的参数。这也让我们明白了 arguments 的主要用途
//就是保存函数参数。`callee`是`arguments`对象的一个属性,可以获取函数自身。
//该属性是一个指针,指向拥有这个 arguments 对象的函数。
//caller
//`caller`返回函数被谁调用了。

const fibonacci = function (num) {
    if (num <= 2) return 1;
    return arguments.callee.caller(num - 1) + arguments.callee.caller(num - 2)
}

console.log(fibonacci(4))

const fibonacci = (num) => {//改成箭头函数会爆炸
    if (num <= 2) return 1;
    return arguments.callee.caller(num - 1) + arguments.callee.caller(num - 2)
}

3、复杂的模板字符串语法

const consoleList = function (student, teacher) {
    console.log(`hello ${student}, I am ${teacher}, nice 2 meet U`)
    // console.log("hello " + student + ", I am " + teacher + ", nice 2 meet U")
}

const consoleString = function (stringTemplate, ...restVal) {
console.log(
    stringTemplate.reduce(
        (total, item, index) => total + item +(restVal[index] || ''), 
        '')
)
}

const stu = "my students";
const tea = "luyi";

consoleString(['hello', ', I am ', ', nice 2 meet U'], stu, tea);
//结果为:hello my students, I am luyi, nice 2 meet U

// 复杂的模板字符串语法
consoleString`hello ${stu}, I am ${tea}, nice 2 meet U`
const consoleList = function (student, teacher) {
//换行也没问题,打印出来也是换行之后的样子,不会报错
    console.log(`hello ${student}, 
    I am ${teacher}, 
    nice 2 meet U`)
    // console.log("hello " + student + ", I am " + teacher + ", nice 2 meet U")
}

consoleList('my students', 'luyi')

4、数组和对象

数组和对象的细节

// 数组的细节
// 需要使用 Array.from 或者 .fill(0)
const funcGenerator = (num) => Array.from(new Array(num)).map(item => params => console.log(params));//打印12345678910
const funcGenerator = (num) => new Array(num).fill(0).map(item => params => console.log(params));//打印12345678910

// funcGenerator(10).map((func, index) => func(index));

// 对象的细节
console.log(NaN === NaN);//false
console.log(Object.is(NaN, NaN));//true

// 原因:
// ES next 采用了 SameValueZero() 的比较。是一个引擎内置的比较方式。

console.log([NaN].indexOf(NaN)) //-1
console.log([NaN].includes(NaN)) //true ,因为includes也采用了 SameValueZero

// JS runtime : browser / node
// 想跑js文件,不仅仅用浏览器,还可以命令行node xxx.js,直接看打印结果

5、Object.assign

// Object.assign
// 深拷贝还是浅拷贝?

let dist = { foo: "foo" };
let bar = { bar: { bar: "bar" } };
let baz = { baz: "baz" };

const res = Object.assign(dist, bar, baz);

bar.bar.bar = "newBar";
baz.baz = "newBaz"

// 第一层是深拷贝,第二层是浅拷贝;
console.log(res); // { foo: 'foo', bar: { bar: 'newBar' }, baz: 'baz' };
// res -- dist;
console.log(res === dist);//true,真神奇

6、get / set

class Person {
    constructor() {

    }
    _age = "";
    get age() {
        console.log(`actually I am ${this._age} years old~`)
        return "17"
    }

    set age(val) {
        console.log(" It is useless to set my age, I am 17!");
        this._age = val;
    }
}

// const luyi = new Person();
// luyi.age = "35";
// console.log("luyi is", luyi.age);

// java -- mumber private 

//  Proxy 天生的代理模式
// Vue2 Vue3

7、Proxy

Proxy 也就是代理,可以帮助我们完成很多事情,例如对数据的处理,对构造函数的处理,对数据的验证,说白了,就是在我们访问对象前添加了一层拦截,可以过滤很多操作,而这些过滤,由你来定义。

const luyi = {
    age: 35
}

const luyiProxy = new Proxy(luyi, {
    get: function (target, propKey, receiver) {
        console.log("GET:", target, propKey)
        return Reflect.get(target, propKey, receiver);
    },
    set: function (target, propKey, value, receiver) {
        console.log("SET:", target, propKey, value)
        return Reflect.set(target, propKey, value, receiver);
    }
})

console.log(luyiProxy.age = 35)

8、如何实现断言函数(背下来)

// 如何去实现一个断言函数?
const assert = new Proxy({}, {
    set(target, warning, value) {
        if (!value) {
            console.error(warning);//The teacher is Luyi!!!
        }
    }
})

const teacher = "luyi";
// 如果断言的内容是假的,我就打印
assert['The teacher is Luyi!!!'] = (teacher === "yunyin");

9、receiver

const luyi = {
    age: 35
}
const luyiProxy = new Proxy(luyi, {
    get: function (target, propKey, receiver) {
        return receiver; // luyi本身
    },
    set: function (target, propKey, value, receiver) {
        console.log("SET:", target, propKey, value)
        return Reflect.set(target, propKey, value, receiver);
    }
})
// receiver 指向原始的读操作所在的那个对象, 一般情况下,就是 Proxy 的实例。
console.log(luyiProxy.age)//{age: 35}即luyi本身
console.log(luyiProxy.age === luyiProxy) // true

10、 Reflect

方法允许精确添加或修改对象上的属性。

Reflect.defineProperty 静态方法 Reflect .defineProperty()  基本等同于 Object.defineProperty() 方法,唯一不同是返回 Boolean 值。

  • 将 Object 上一些明显属于语言内部的方法,放到 Reflect 对象上,现在 Object 和 Reflect 一同部署;
  • 修改某些 Object 方法的返回结果,让其更合理;
const teacher = {
    age: 18, name: "luyi"
}

Reflect.defineProperty(teacher, 'lessions', {
    writable: false,
    enumerable: false,
    configurable: false,
    value: 'vue'
})

const res = Reflect.defineProperty(teacher, 'lessions', {
    writable: true,
    enumerable: true,
    configurable: true,
    value: ['es6', 'esnext']
})

console.log(res);
// writable: false 时,打印为false,说明设置不成功,因为不可被修改
// writable: true 时,打印为true,说明设置成功,因为可被修改

// Object.defineProperty直接报错: Cannot redefine property: lessions
// Reflect.defineProperty 给 true or false

11、Set、Map、WeakSet和WeakMap的区别

具体看之前的笔记

  • Weak 表示作为唯一的部分,必须是一个对象;
  • Weak 是一个弱引用,不用考虑 GC;
const foos = new WeakSet();

class Foo {
    constructor() {
        foos.add(this);
    }
    method() {
        if (!foos.has(this)) {
            throw new TypeError(" Foo.prototype.method 只能在实例上调用");
        } else {
            console.log("using methods")
        }
    }
}

let f = new Foo();
let b = {};
Foo.prototype.method.call(b)//报错
//这个例子主要为了说明,weakSet弱引用,不用考虑回收问题

12、迭代器,Iterator

- 迭代器是一个接口,为各种不同的数据提供统一的访问机制。任何数据结构只要部署了 Iterator 接口,就可以完成遍历操作:

  • 本质:指针。
  • 该 接口主要供 for...of 消费。
let m = new Map();
m.set('a', 'foo');
m.set('b', 'bar');
m.set('c', 'baz');

let k = m.keys();

console.log(k.next());//{value: "a", done: false}
console.log(k.next());//{value: "b", done: false}
console.log(k.next());//{value: "c", done: false}

let arr = [1, 2, 3, 4, 5];
let k = arr[Symbol.iterator]();//这个就是上面执行的本质,返回的是一个迭代器对象

console.log(k.next());//{value: "1", done: false}
console.log(k.next());//{value: "2", done: false}
console.log(k.next());//{value: "3", done: false}
console.log(k.next());//{value: "4", done: false}
console.log(k.next());//{value: "5", done: false}
console.log(k.next());//{value: undefined, done: true}

// generator生成器函数
原生具备 Iterator 的数据结构有:AASSMNT
  • Array Map Set String TypedArray arguments NodeList
用到generator生成器函数的地方:LRU缓存
红绿灯问题:可用callback,generator,promise,async-await

13、Object.entries


const obj = { a: 11, b: 22, c: 33 }

console.log(Object.entries(obj)); // [ [ 'a', 11 ], [ 'b', 22 ], [ 'c', 33 ] ]
console.log(Object.keys(obj)); // [ 'a', 'b', 'c' ]
console.log(Object.values(obj)); // [ 11, 22, 33 ]

// 非 generator 的方法
function entries(obj) {
    let arr = [];
    for (let key of Object.keys(obj)) {
        arr.push([key, obj[key]])
    }
    return arr;
}

// generator 的方法
function* entires(obj) {
    for (let key of Object.keys(obj)) {
        yield [key, obj[key]];
    }
}

const k = entires(obj);

console.log(k.next());//{value: ["a": 11], done: false}
console.log(k.next());//{value: ["b": 22], done: false}
console.log(k.next());//{value: ["c": 33], done: false}
console.log(k.next());//{value: undefined, done: true}

for (let item of k) {
    console.log(item);
    //依次打印出:
    //["a", 11]
    //["b", 22]
    //["c", 33]
    //这也是yield的返回结果
}

14、 promise.allSettled

Promise.allSettled 与 Promise.all ,都应用于批量处理异步任务的一个场景

Promise.allSettled 接受promise数组,返回promise对象,且其结果状态永远成功

//手写promise.allSettled
<script>
function allSettled(array) {
    return new Promise((resolve, reject) => {
        if (!(array instanceof Array)) return reject(new Error(" not Array!"))
        const res = [];
        let count = 0;
        array.forEach((func, index) => {
            Promise.resolve(func).then(value => {
                res[index] = {
                    status: 'fulfilled', value
                }
            }, (reason) => {
                res[index] = {
                    status: 'rejected', reason
                }
            })
                .finally(() => {
                    ++count === array.length && resolve(res);
                })
        })
    })
}

推荐书籍

  • 《你所不知道的 JS》

答疑

老师能归纳数组的扩展里的api 在使用上的一下特点吗

  • map =>
  • reduce
  • filter

手写实现new问得多吗

new 关键字干了什么

ES6的面试重点和难点就是今天讲的这些吗?

  • Promise

红杉资本怎么样啊

请教老师一下面试问题,建议是准备差不多了开始面 还是 直接面试,边面试边复盘

  • 边面试边复盘

老师仔细说一下 definPropropt 和 Proxy 深层次的区别吗 碰到写框架的 老问这个

  • Vue2: 数组、新增、内存消耗大、Proxy lazy 来代理。

老师老师,校招的话大厂比较看重什么,算法吗?

  • 算法,聪明

老师刚才断言那个 后面 = yunyin那个可以再说一下嘛

promise class这些API,是c++实现,还是用现有旧语法模拟实现提供出来的

快速提点

一堆后端服务器框架

express

Express 是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移
动应用程序提供一组强大的功能;
使用回调函数;
内置了很多中间件。

koa

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用
和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函
数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 
而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序;
使用async / await ;
中间件需要第三方引用。

restana

待补录

nest (next.js, nuxt.js)

待补录

【洋葱模型】与【中间件(middleware / compose)】

中间件

在 Koa 中,中间件就是普通的函数,该函数接收两个参数:context 和 next。其中 context 表示上下文对象,而 next 表示一个调用后返回 Promise 对象的函数对象。

问:中间件一般用来做啥业务?
典型的中间件有:
koa-cors 提前做跨域处理
koa-bodyparser 提前做权限认证数据解析
koa-helmet 提前准备安全策略
spring 层层的control services model这种典型的MVC框架流程,少不了中间件

洋葱模型

洋葱模型,也即是中间件的能力之强大众所周知,现在在 Web 社区发挥极大作用的 Redux、Express、Koa,开发者利用其中的洋葱模型,构建无数强大又有趣的 Web 应用和 Node 应用。更不用提基于这三个衍生出来的 Dva、Egg 等。

 1.   redux 的中间件编排方式由 reduce()来实现;
 2.   koa 的中间件编排方式由 dispatch 递归函数来实现;
 3.   axios //  [ ] req -> unshift(); res -> push()

用express实现洋葱模型

为了说明演示【洋葱模型】
利用node.js,搭一个express后端服务器,
安装:npm install express --save 或者 yarn add express
创建:npm init 或者 yarn init
老师没有用命令行创建,而是手动新建src/index.js
跑起来:nodemon ./src/index.js
备注:
介绍一个辅助工具nodemon:
nodemon用来监视node.js应用程序中的任何更改并自动重启服务,非常适合用在开发环境
中。nodemon将监视启动目录中的文件,如果有任何文件更改,nodemon将自动重新启动
node应用程序。
//src/index.js
const express = require("express");
const app = express();

app.use((req, res, next) => {
    console.log("querying strat level 1");
    next();
    console.log("querying end level 1");
});

app.use((req, res, next) => {
    console.log("querying strat level 2");
    next();
    console.log("querying end level 2");
});

app.use((req, res, next) => {
    console.log("querying strat level 3");
    next();
    console.log("querying end level 3");
});


app.get('/', (req, res) => {
    res.send("hello world!!!")
});

app.listen(3001, () => {
    console.log(" Express server is running in 3001")
})
//不用重新命令行编译,自动更新,并监听服务
//以上整个打印结果为:
//querying strat level 1
//querying strat level 2
//querying strat level 3
//querying end level 3
//querying end level 2
//querying end level 1

//这就是中间件

用koa实现洋葱模型

koa 基本用法

前端启动

yarn install
yarn dev //看package.json的配置  npm则用npm run dev

后端启动

rollup -c -w (等待30s)  
nodemon ./dist/bundle.js
//  -c: 找根目录的 `rollup.config.js` 作为我的构建的配置;
//  -w: watch 监听文件的变化,实时构建;
//  或者,Node.js要求ES6模块化采用.mjs后缀的文件名,这样不用打包,
//  直接跑起来:nodemon ./src/index.js 也能支持了import语法了

前端启动 后端启动 有什么区别???

自己心里清楚

后端 - node 框架

待补录

基本写法

const Koa = require("koa");
const app = new Koa();

const main = ctx => {
    ctx.body = "hello Koa";
}

app.use(main);
app.listen(3002, () => {
    console.log('Koa server is running in 3002')
})

koa-router

// index.js
import Koa from 'koa';
import Router from 'koa-router';
//注意:这里用了ES6的语法,在node里直接跑nodemon ./src/index.js已经不行了,需要
//借助rollup,在控制台输入命令:rollup -c -w,打包之后,应该跑的是打包好的文件,
//即:nodemon ./dist/bundle.js
//  或者,Node.js要求ES6模块化采用.mjs后缀的文件名,这样不用打包也能支持了
//  直接跑起来:nodemon ./src/index.js 也能支持了import语法了
 
// const Koa = require("koa");
// const Router = require('koa-router');

import movie from './movie';
import user from './user';

const app = new Koa();
const router = new Router();

// router.get('/api', (ctx, next) => {
//     ctx.type = "application/json";
//     ctx.body = { data: "hello Api" };
// })

[movie, user].forEach(route => {
    app.use(route.routes());
    app.use(route.allowedMethods());
})

app.listen(3002, () => {
    console.log('Koa server is running in 3002')
})

// movie.js
import Router from 'koa-router';

// koa-cors
// koa-bodyparser
// koa-helmet

const router = new Router();

router.get('/movie', async (ctx, next) => {
    ctx.body = "Movie"
})

export default router;

// user.js类似
import Router from 'koa-router';

const router = new Router();

router.get('/user', async (ctx, next) => {
    ctx.body = "User"
})

export default router;

//rollup.config.js
import babel from 'rollup-plugin-babel'

export default {
    input: './src/index.js',
    output: {
        file: './dist/bundle.js',
        format: "umd",
    },
    treeshake: false,
    plugins: [
        babel({
            runtimeHelpers: true,
            extensions: [".js", ".ts"],
            exclude: "node_modules/**",
            externalHelpers: true
        })
    ]
}
{
  "name": "back_end",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "nodemon ./dist/bundle.js"
    "dev": "rollup -w -c"//改成这样,可直接跑npm run dev、或者直接rollup
    //而不用跑nodemon ./dist/bundle.js
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.17.9",
    "@babel/plugin-proposal-class-properties": "^7.16.7",
    "@babel/plugin-proposal-decorators": "^7.17.9",
    "@babel/preset-env": "^7.16.11",
    "babelrc-rollup": "^3.0.0",
    "core-js": "^3.22.0",
    "koa": "^2.13.4",
    "koa-bodyparser": "^4.3.0",
    "koa-router": "^10.1.1",
    "rollup": "^2.70.2",
    "rollup-plugin-babel": "^4.4.0"
  }
}

写一个装饰器

实现类似于Java里注解的形式,优化上面的的功能逻辑,理清中间件、装饰器、闭包、发布订阅之间的关系。

大概思路:url收集,url参数拼接,url定位,函数执行。

粗暴理解,中间件就是个拦截器,装饰器就是注入

先写后端服务器代码

//别忘了在这文件的plugins里配置好才可识别装饰器
// .babelrc
{
    "presets": [
        [
            "@babel/preset-env",
            {
                "modules": false,
                "loose": true,
                "targets": "node 16",
                "useBuiltIns": "usage",
                "corejs": { "version": "3.22", "proposals": true }
            }
        ]
    ],
    "plugins": [
    //添加两个插件才可以使用装饰器
        ["@babel/plugin-proposal-decorators", { "legacy": true }],
        ["@babel/plugin-proposal-class-properties", { "loose": true }]
    ]
}

//index.js
import Koa from 'koa';
import Router from 'koa-router';
import bodyParser from 'koa-bodyparser';
import { controllersArray } from './utils/decorator';

import R from './controller/index'

const router = new Router();
const app = new Koa();

//使用中间件
// cors 
app.use(async (ctx, next) => {
    ctx.set('Access-Control-Allow-Origin', '*');
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Accept');
    ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    ctx.set('Content-Type', 'application/json;charset=utf-8');
    if (ctx.request.method.toLowerCase() == 'options') {
        ctx.state = 200;
    } else {
        await next();
    }
})

//使用中间件
app.use(bodyParser());

controllersArray.forEach(item => {
    let { url, constructor, method, handler } = item;
    const { prefix } = constructor;
    if (prefix) url = `${prefix}${url}`;
    router[method](url, handler)
})

app.use(router.routes());
app.listen(3003, () => {
    console.log("server running 3003")
})

//  ./controller/index
import Movie from './movieController';
import User from './userController';

export default [
    Movie, User
]
// ./movieController
import { RequestMapping, Controller, RequestMethod } from "../utils/decorator";
import data from '../../mock/data';

@Controller('/movie')
export default class MovieController {

    @RequestMapping(RequestMethod.GET, '/all')
    async getAllMovies(ctx) {
        // 1 -> 0,9 // 2 -> 10, 19
        const [key, page] = ctx.querystring.split('=');
        ctx.body = {
            data: data._embedded.episodes.slice((page - 1) * 10, page * 10 - 1),
            count: data._embedded.episodes.length
        }
    }

    @RequestMapping(RequestMethod.GET)
    async id(ctx) {
        ctx.body = 'getAllMovies'
    }
}

// /movie/all

//./userController
import { RequestMapping, Controller, RequestMethod } from "../utils/decorator";

@Controller('/user')
export default class MovieController {

    @RequestMapping(RequestMethod.GET, '/all')
    async getUser(ctx) {
        ctx.body = 'user'
    }
}

// /user/getAllbooks

//   ../utils/decorator
export const RequestMethod = {
    "GET": 'get',
    "POST": 'post',
    "PUT": 'put',
    "DELETE": 'delete',
    "OPTION": 'option',
    "PATCH": 'patch'
}

export const controllersArray = [];

// 装饰器对类的行为的改变,是代码编译时发生的,而不是运行时;
// 所以,在之前就干了这么一件事
// movieController -->   movieController.prefix = "/movie"
export function Controller(prefix = "") {
    return function (target) {
        target.prefix = prefix;
    }
}

export function RequestMapping(method = "", url = "") {
    return function (target, name, descriptor) {
        let path = '';
        if (!url) {
            path = '/' + name;
        } else {
            path = url;
        }
        // 创建 router 需要的数据
        const item = {
            url: path,
            method: method,
            handler: target[name],
            constructor: target.constructor,
        }
        controllersArray.push(item)
    }
}

//  ../../mock/data
省略

然后写前端代码

跑起来,用 yarn dev

npm 则用 npm run dev

// package.json
{
  "name": "svelte-app",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv public --no-clear"
  },
  "devDependencies": {
    "@fullhuman/postcss-purgecss": "^4.1.3",
    "@rollup/plugin-commonjs": "^17.0.0",
    "@rollup/plugin-node-resolve": "^11.0.0",
    "autoprefixer": "^10.4.2",
    "postcss": "^8.4.5",
    "postcss-load-config": "^3.1.1",
    "rollup": "^2.3.4",
    "rollup-plugin-css-only": "^3.1.0",
    "rollup-plugin-livereload": "^2.0.0",
    "rollup-plugin-svelte": "^7.0.0",
    "rollup-plugin-terser": "^7.0.0",
    "svelte": "^3.0.0",
    "svelte-preprocess": "^4.10.6",
    "tailwindcss": "^3.0.12"
  },
  "dependencies": {
    "rollup-plugin-postcss": "^4.0.2",
    "sirv-cli": "^2.0.0",
    "smelte": "^1.1.8",
    "svelte-spa-router": "^3.2.0",
    "tailwindcss": "^3.0.23"
  }
}


// main.js
import './app.css';
import App from './App.svelte';

const app = new App({
  target: document.body,
  props: {
    name: 'world',
  },
});

export default app;


// App.svelte
<script>
  import "./app.css";
  import "smelte/src/tailwind.css";
  import Router from "svelte-spa-router";
  import { routes } from "./routes.js";
</script>

<main>
  <Router {routes} />
</main>

<style>
</style>


// routes.js
import Home from './views/home/index/main.svelte'
export const routes = {
  '/': Home,
}







知识总结

前端

  • svelte(类似于react / vue)
  • rollup(类似于webpack)
  • smelte(类似于antd / element)
  • tailwind CSS (类似于bootstrap / css / postcss)

后端

  • Sequelize(类似于java-mybatis)

其他

Rollup

rollup -c -w

  • c: 找根目录的 rollup.config.js 作为我的构建的配置;
  • w: watch 监听文件的变化,实时构建;

中间件

  • redux-compose 中间件相当于把函数串联起来
  • 路由守卫也是一个中间件
  • 中间件是面向切面的
function(arr) {
    return arr.reduce((a, b) => (...args) => a(b(...args)) )
}
大白话,就是 一个数组  里面都是函数  一个函数的结果  给下一个函数作为参数  
然后依次调用  最后一个函数输出结果
这就是redux-compose底层实现

想去 虾皮 ---- 去了解以下svelt

亮点,怎么说

我为整个KOA去封装一套东西,封装一套装饰器,让后端同学用node的同学去使用;
我没有用传统的react和vue而是使用了svelte,因为我们对虚拟DOM这个事情不是很关心,而且,我觉得前端目前的发展方向正在往编译器走,我们希望把它的组件更新粒度控制在一个更小的粒度上