资讯专栏INFORMATION COLUMN

整理DOM事件相关知识点

shenhualong / 1843人阅读

摘要:事件相关内容当用户与浏览器发生的一些交互时如果希望去获得用户行为就需要借助事件来完成事件部分内容在中重要性不言而喻罗列需要了解与事件相关的知识如下这也是面试中遇到的问题事件的级别事件模型事件流事件处理程序描述事件捕获冒泡的具体流程对象常见的

DOM事件相关内容

当用户与浏览器发生的一些交互时, 如果希望去获得用户行为, 就需要借助事件来完成. 事件部分内容在 JS中重要性不言而喻.

罗列需要了解与事件相关的知识如下: 这也是面试中遇到的问题.

DOM 事件的级别

DOM 事件模型

DOM 事件流

DOM 事件处理程序

描述DOM事件捕获(冒泡)的具体流程

Event对象常见的应用

自定义事件

事件级别

DOM0

DOM2

DOM3

DOM 事件模型

事件冒泡

事件捕获

什么是事件流

要想明白事件流,必须先懂的这几个知识点

事件冒泡

事件捕获

先来看一个有趣的问题, 这是 4 代浏览器(IE4)开发团队遇到一个的问题:

What part of the Webpage owns a specific event?

页面的哪一部分会拥有某个特定的事件?

要想明白这个问题可以想象成在一个页面上画了一组同心圆, 当手指放在中间时,它不仅在一个圆圈内,而且在所有的圆圈内。

如下图所示:

这就是浏览器事件的工作原理, 当你点击一个按钮时, 不仅单击按钮,还单击包含的容器和整个页面。

事件生命周期, 分为三个阶段: capturing (捕获), target (目标), bubbling (冒泡). 而这三个阶段也是构成事件流基本部分.

这种处理思想, 在现在流行 NodeJS 后端框架 Koa 对中间件的处理模式也是基于这种, 熟悉Koa或许对这洋葱图并不会陌生:

先来熟悉事件的模型:

Javascript Events: Event bubbing (事件冒泡)

事件冒泡: 既事件开始由最具体的元素接收,然后逐级向上传播最后到达 Document 对象 或 window 上.

先来看一个简单的示例, 代码如下:

    
    
        
            ......
        
        
            
Press here.
    var target = document.getElementById("demo");
    window.addEventListener("click", function(){
        console.log("window bubbling");
    });
    document.addEventListener("click", function(){
        console.log("document bubbling");
    });
    document.documentElement.addEventListener("click", function(){
        console.log("html bubbling");
    });
    document.body.addEventListener("click", function(){
        console.log("body bubbling");
    });
    target.addEventListener("click", function(){
        console.log("target bubbling");
    });

控制台打印输出如下

    "target bubbling"
    "body bubbling"
    "html bubbling"
    "document bubbling"
    "window bubbling"

当一个div 被点击, 这点击事件发生的顺序如下:

div

body

html

Document

Window (现在浏览器中, IE9+, Firfox, Chrome 等)

从执行顺序来说, click事件首先在 div 元素上触发, 然后沿着DOM Tree 向上传播, 在路径上的每个节点上触发,直到它到达文档对象(或Window对象)。

Javascript Events: Event Capturing (捕获)

事件捕获 是另外一种事件流模型, 最先由 Netscape Browser 引入.

根据上面的的模型, 刚好与前面冒泡相反. 根据该模型,最不特定(最外层)的节点首先接收事件,而最特定(目标元素)的节点最后接收事件.

它设计的目标就是在事件到达目标之前,事先进行拦截.

参考前面的示例, 修改下监听方式, 代码修改如下:

    var target = document.getElementById("demo");
    window.addEventListener("click", function(){
        console.log("window Capturing");
    }, true);
    document.addEventListener("click", function(){
        console.log("document Capturing");
    }, true);
    document.documentElement.addEventListener("click", function(){
        console.log("html Capturing");
    }, true);
    document.body.addEventListener("click", function(){
        console.log("body Capturing");
    }, true);
    target.addEventListener("click", function(){
        console.log("target Capturing");
    }, true);

打印的结果:

    "window bubbling"
    "document bubbling"
    "html bubbling"
    "body bubbling"
    "target bubbling"

当点击 div 元素, 按照如下顺序来进行广播事件.

Window (现在浏览器中, IE9+, Firfox, Chrome 等)

Document

Document

body

div

注意:

DOM0 级中默认就是使用冒泡的方式, 不支持捕获的. 所以在 DOM2 级中通过 addEventListener 提供的第三个参数来控制使用哪种事件流来处理.
Javascript Events: DOM Event Flow (事件流)

根据上面两种模型, 可以总结完整事件流应该向如下:

DOM Level 2 Events 指定的事件流模型分为三个阶段:

Event Capturing Phase (事件捕获阶段)

At the target (目标阶段)

Event Bubbling Phase (事件冒泡阶段)

从上面流程图中, 首先发生的是 事件捕获阶段 为截获事件提供了机会, 再到目标阶段 然后进入 事件冒泡阶段, 可以在这个阶段对事件进行响应.

引用前面的例子, 点击 DIV 元素时, 事件将按照上图顺序进行触发.

这就是完整的事件流, 内容看起来挺多的, 实际上一句话就概述事件流.

用来描述事件发生顺序(页面接收事件顺序).
事件处理方式

通过前面的知识点, 事件就是表示用户或浏览器自身执行某种动作. 例如: click, dbclick, load, unload, mouseover,mouseout, mouseenter ,mouseleave 等等, 这些都是事件的名字. 而响应并处理某个事件的函数称为 事件处理程序.

常见事件处理方式包括如下几种:

HTML 事件处理程序

DOM0 级处理程序

DOM2 级处理程序

HTML 事件处理程序

直接来看个简单示例:

    

这种模式, 可以理解为 CSS 行内样式

    

直接在 HTML 元素上绑定相应事件名, 并指定对应的事件处理程序. 从前面语法来说, 事件处理程序是一个函数, 上面传递是一个语句, 也就是说默认执行时, JS引擎会进行相应处理. 等价于这种方式:

    

如果把事件处理函数直接以这种内联值的方式提供, 应该注意不能值中指定未经转义的HTML语法字符 例如: 和号(&) 、双引号("") 等等, 否则会解析出错.

如上面示例,如果想使用双引号, 必须这么处理.

    

这种对于简单语句还行, 除了提供元素属性值的方式, 那么有木有其它方式咧? 答案:当然有 , HTML事件处理程序可以调用在页面其它地方定义的脚本.

示例如下:

    
    

通过这种方式指定事件处理程序具有一些特别的地方.

前面讲述事件处理程序引擎在执行时, 默认封装函数. 在封装的函数中包含一个局部变量 event, 也就是事件对象.

    
    

    // => 等价

    

该函内部 this 指针表示是当前的目标对象

    

    // this === [object HTMLInputElement] === input元素
    

这样可以通过 this来获取目标元素相关的内容了. 比如取值

    
    
    

为什么可以简写方式, input 对象是存在于当前函数的作用域链中(scope). 为了调试方式, 把上面方式作如下改变

    

其实理解上面那句话, 我们可以借助开发这工具来协助理解:

从执行栈(Call Stack)可以看出, 证明前面说的默认会创建封装函数 .

从 Scope 作用链中可以看出, 前面说的 input 对象会被添加在当前匿名函数执行的作用域链上.

根据开发工具来看, 我们是可以模拟引擎帮做的事情, 伪代码如下:

    function () {
        with(document) {
            with(input){
                // dosomething
                console.log(value);    
            }
        }
    }

本质通过 with 来扩展作用域.

上面把基本内容说, 那么这种通过HTML事件处理程序有什么问题么 ?

时差问题

事件处理程序必须优先元素加载, 有可能DOM加载出来, 用户就开始操作, 此时事件处理程序可能未加载导致报错

前面提及扩展程序的作用域链可能在不同浏览器中兼容不一样,导致程序出现错误

视图和行为偶合在一起,也就是说事件处理程序修改相应JS部分、HTML部分都需要去修改.

引用前面的示例:

    
    

如果用户想把函数名为: "onClick", 这时是不是需要同时去修改.

DOM0 级事件处理程序

通过 JavaScript 来指定事件的处理程序, 也就是将一个事件处理程序赋值给一个元素属性(事件类型).

语法

    element.onclick = function(){}

先看个简单示例:

    
    
    


    
    

上述就是 DOM0 级事件处理程序应用.

使用 DOM0级 不同于 HTML事件处理程序 优点在于:

把视图和行为进行解耦.

更容易让人理解(作用域的各种问题)

DOM0 级添加事件有哪些特点

由于通过 target.onclick 这种方式是不是很熟悉, object.property , 那么意味这添加事件处理程序(方法) 属于当前 DOM元素的, 相应其内部 this 指向当前元素.

    

    

打印输出结果为:

   "ClickMe" 

从结果来看,是符合前面说的.

大家是否记得前面说事件流 提及事件两个阶段冒泡捕获, 那么通过 DOM0级别 添加的事件会发生那个阶段 ? 冒泡

通过简单示例来看看:

    

    

打印输出结果:

    "input bubbling"
    "body bubbling"

细心读者发现, 在讲解 事件捕获 时提及了, 事件捕获是 DOM2 级才进行引入的. 也验证现在说法.

移除事件处理程序

简单粗暴, 也只能这样.

    target.onclick = null;
同一个事件名添加多个处理程序

DOM0级中, 使用onclick 方式添加事件处理程序是不支持添加多个, 假如有这样场景需要自己去扩充, 现在浏览器中基本使用 DOM级非常少了, 所以不必担心.

简单实现如下:

    var target = document.querySelector("input");

    function handleOne() {
        console.log("handle one");
    }
    function handleTwo() {
        console.log("handle two");
    }

    function addEvent(eventName, target, handle) {
        var map = target.map ? target.map : target.map = {};
        if (typeof target.onclick === "function") {
            target.map[eventName].push(handle);
        } else {
            target.onclick = function () {
                map[eventName].every(function (fn) {
                    return fn();
                });
            };
            if (!map[eventName]) {
                map[eventName] = [];
            }
            map[eventName].push(handle);
        }
    }
    function removeEvent(eventName, target, handle) {
        var map = target.map;
        var list = [];
        if(!map){return;}
        if(list = map[eventName]) {
            var index = list.indexOf(handle);
            list.splice(index, 1);
        }
    }
    addEvent("onclick", target, handleOne);
    addEvent("onclick", target, handleTwo);

    removeEvent("onclick", target, handleOne)

两个小细节:

为什么迭代循环时为什么使用 every, 而不是用 forEach 、map、some; 这里先预留一个终止执行队列里面的条件, every 只有每一个数组元素满足条件时才会往下执行, 否则直接终止循环.

因为添加时直接把事件处理程序添加在一个数组中, 删除时只能通过函数引用; 所以在 addEvent 时不能通过匿名函数的方式. 否则无法被移除.

兼容性

所有浏览器都支持
DOM2 级事件处理程序

DOM2级事件定义了两个方法, 用来指定和删除事件处理程序的操作, 分别是 addEventListenerremoveEventListener.

语法:

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);    

target.removeEventListener(type, listener[, options]);
target.removeEventListener(type, listener[, useCapture]);

type

表示事件的类型的字符串

listener

事件处理程序必须是一个实现 EventListener 接口的对象, 或 一个函数. 两者实质都是用来接收并处理触发的事件.

options 可选

主要跟 listener 相关的

capture Boolean 表示该 listener 事件处理程序是否在捕获阶段被触发.

once Boolean 表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。

passive: Boolean, 设置为true时, 表示 listener 永远不会调用 preventDefault(). 如果 listener 仍然调用了这个函数, 客户端将会忽略它并抛出一个控制台警告.

useCapture 可选

默认值: false 表示在冒泡阶段调用事件处理函数.

小提示:

从参数签名来看 optionsuseCapture 是不能同时配置, 这就是为什么 options 中会提供 capture 的原因. 这样其实带来另外一个好处, 可以更精确控制到每一个事件处理程序的触发时机

先通过一个简单的示例来把上面知识点应用一下,示例如下:

    

    

控制台依次输出:

    // 第一次点击时
    "one handle"
    "two handle"
    "capture handle"
    "once handle"
    "window"

    // 第二次点击时
    "one handle"
    "two handle"
    "capture handle"
    "window"

从输出来看:

默认是在冒泡阶段来触发事件处理程序 (listener); 需要捕获阶段拦截可以通过把 useCapture 改为 false.

使用DOM2级 默认支持针对同一个事件类型添加多个处理程序. DOM0 级 不支持的.

事件触发的顺序按照添加顺序执行.

在讲述语法时, listener 可以是一个实现 EventListener 接口的对象. 上面指定 listener 时我们可以使用如下方式:

    
    

一般推荐使用前者;

事件处理程序执行的作用域
事件处理程序中 this 指向当前对象(input)
    
    

控制台输出:

    "ClickMe"

移除事件处理程序
移除事件处理程序通过 removeListener 方法. 从语法来看, 传入参数与添加时传入的参数一样, 也就是说事件处理程序(函数)就不能为匿名函数, 否则无法移除

错误的使用方式:

    
    

控制台输出:

    "handleListener"

正确使用方式:

    
    

总结:

指定事件类型时,使用 click 而不是 onclick (click类型为例)

允许支持同一个事件名添加多个处理程序, 执行顺序为添加顺序.

事件处理程序依附的对象, 作为处理函数执行时的作用域.

移除事件处理程序时, 不能使用匿名函数.

兼容性:

IE9、Firefox、Safari、Chrome和 Opera 支持DOM2级事件处理程序

从兼容性来看,那么在IE中有没有类似 addEventListener 和 removeEventListener 事件处理程序?

IE 中添加事件处理程序

IE实现了类似 DOM 中类似的两个方式: attachEvent(), detachEvent(). 这两个方法默认接收两个参数.

语法格式(Non-standard):

    attachEvent(type, listener)
    detachEvent(tyoe, listener)

先来看个具体示例(IE中运行):

    
    
    

控制台输出结果:

    // 点击 "移除事件处理程序按钮之前"
    "handleListener"
    "undefined"

    // 点击之后, 再执行没有输出(事件处理程序被移除)

注意:

事件类型名称(type) 和 DOM0级一样需要指定完整属性名 onclick

移除事件处理程序时, 传入参数必须一样

attachEvent 和 detachEvent 不支持捕获, 也就是说在被添加到冒泡阶段

事件处理程序在全局作用域中执行

兼容

attachEvent 和 detachEvent 只能 IE10以及一下版本中使用.
IE事件处理程序与DOM0级的区别

在IE中使用 attachEvent detachEvent, 与 DOM0级不同的地方在于事件处理程序执行时作用域不一样. 前面讲到过DOM0和 DOM2事件处理程序都会在所属元素的作用域内运行; 从上面 this.id 打印结果来看, attachEvent 在全局作用域(window)中运行.

到目前为止,把添加事件处理程序方式都一一列举完. 具体引用那种可以根据实际需求选择.

Event 对象常见应用
是伴随着事件执行过程中的一个产物, 这个称之为 event (事件对象). 该事件对象中包含很多跟事件以及用户相关的内容(例如: 事件类型、事件对象、用户点击位置、移动位置等等)

下面罗列下 event object 包含常用属性和方法:

名称 描述 返回值
type 指定的事件名称(例如 click, mouseout) string
target 表示要触发事件的元素 HTMLElement
currentTarget 事件处理程序调用时的元素 HTMLElement
eventPhase 事件当前处于事件的那个阶段; 1 捕获 2 事件目标 3 冒泡阶段 number
bubbles 表示当前事件对象是否会向 DOM 树上层元素冒泡 boolean
cancelable 事件的 cancelable 属性表明该事件是否可以被取消默认行为, 如果该事件可以用 preventDefault() 可以阻止与事件关联的默认行为,则返回 true,否则为 false boolean
timeStamp 返回事件发生时的时间戳. string
stopPropagation() 阻止捕获和冒泡阶段中当前事件的进一步传播 如果 bubbles为ture可以使用 void
stopImmediatePropagation() 阻止事件冒泡并且阻止相同事件的其他侦听器被调用 DOM3级中添加 void
preventDefault() 阻止事件默认行为,事件依然会传播除非遇到 stopPropogation 或 stopImmediatePropogation 如果 cancelable为turu则可以使用这个方法 void
defaultPrevented 返回一个布尔值,表明当前事件是否调用了 event.preventDefault()方法 DOM3级中添加 boolean
如何来获取事件对象

回顾前面讲解添加事件处理程序(HTML事件处理程序、DOM0 、 DOM2) 分别看怎么来获取

HTML事件处理程序时:

    
    
    
    

可以通过这两种方式来获取事件对象

DOM0 和 DOM2

    
    

可以通过参数的方式来获取, 这是推荐的获取方式. 除了上述几种方式外, 其实可以通过 window.event 来获取表示当前正触发的事件对象, 不过这种方式不是标准(IE), 所以不推荐使用.

event 属性和方法使用 target 和 currentTarget

引用前面的示例, 讲述下 targetcurrentTarget

    

控制台输出:

    true
    true

结合前面对属性描述, event.target 表示当前事件触发的对象. event.currentTarget 表示当前事件处理程序执行时的对象(上下文). 从打印结果来看,当事件处理程序直接绑定目标元素上时,这两者相同的.

什么时候不一样咧?

    
    true
    false

符合预期, 上面代码中把事件处理程序绑定在了 div#container 元素上, 这时 currentTarget 是当前的事件处理程序绑定的对象.

type

type 用于获取当前事件类型之外, 可以作如下用途, 当希望在同一个事件处理程序中处理多个不同事件类型时, 如下:

   
cancelable、preventDefault() 、 defaultPrevented

具体上个介绍可以参照前面的表格里面描述, 为什么把这三个结合在一块, 彼此间是有关联的.
先来看看具体示例:

    

控制台输出:

    true // 表示可以被阻止
    true // 表示调用 preventDefault() 

什么情况下会有 event.cancelable 返回 false ? 现有 focus、blur、
focusin、focusout 等不允许阻止, 或一些自定义的事件

    

控制台输出:

    false // 表示可以被阻止
    false // 表示调用 preventDefault() 
bubbles、stopPropagation()、stopImmediatePropagation()

具体上个介绍可以参照前面的表格里面描述, 为什么把这三个结合在一块, 彼此间是有关联的.
先来看看具体示例:

    

控制台输出:

    "handleOne" 
    true
    "handleTwo"

从输出结果来看, event.stopPropagation() 只会阻止事件冒泡, 并不会阻止同类型多个事件处理程序的执行.

再来看看 stopImmediatePropagation() 修改如下:

    

控制台输出:

    "handleOne" 
    true

event.stopImmediatePropagation() 阻止事件冒泡的同时当前事件类型下其它事件处理程序的执行. 如果 handleOne 中调用 stopImmediatePropagation, 那么 handleTwo就不会再执行.

eventPhase

当前事件处于那个阶段

      // PhaseType
    CAPTURING_PHASE                = 1;  // 捕获
    AT_TARGET                      = 2;  // 事件目标
    BUBBLING_PHASE                 = 3;  // 冒泡阶段 

先来看看简单示例:

    

前面讲解都是DOM标准,也就是所有浏览器厂商都支持的, 那么来看看 IE9 以下版本中会不会有什么不一样.

IE中事件对象

我们也从添加事件处理程序的维度出发, 看看有什么不一样.

HTML事件处理程序
    

正常弹出 click, 这与前面说到的一样.

DOM0

以前面的示例来演示:

    
    

控制条输出:

    undefined
    [object Object]

测试环境: IE8以及一下的版本

在 DOM0 级事件处理程序中, 事件对象添加在 window 上的属性 event

attachEvent

引用上面代码, 使用 attachEvent 来添加事件,并获取 event

    
    var target = document.getElementById("button");
    target.attachEvent("onclick", function(e){
        console.log(e);
    })
事件对象相关的属性
名称 描述 返回值
type 指定的事件名称(如 click, mouseout) string
srcElement 表示要触发事件的元素(与DOM中 target) HTMLElement
returnValue 默认值为 true, 但将其设置为 false, 就可以取消事件的默认行为(与DOM中 preventDefault()) Boolean
cancelBubble 默认值为 false, 但将其设置为 ture. 就取消冒泡行为(与DOM中的 stopPropagation()) Boolean

看看具体示例:

    
    

当点击按钮时, 输出结果:

    "true"

当点击超链接时, 输出结果:

    "container"

从点击超链接来看, e.returnValue 只会阻止默认行为, 事件对象依然进行会冒泡.

事件、目标、事件冒泡、事件默认行为获取方式-汇总
添加事件程序方式 语法 兼容
HTML事件处理程序 < element onclick="alert(event)" > All
DOM0 element.onclick = function(event){} All
DOM2 addEventListener IE9+,Chrome,Firfox,Safari,360
IE attachEvent IE11<
获取事件对象 方式 例子
HTML事件处理程序 event 变量 < element onclick="alert(event)" >
DOM0 事件处理程序参数 、IE9以下通过 windiw.event function(event){}、window.event
DOM2 事件处理程序参数 function(event){}
IE 事件处理程序参数 function(event){}
获取目标对象方式 属性名 兼容
标准 target IE9+,Chrome,Firfox,Safari,360
IE版本 srcElement IE9<
阻止事件冒泡 属性或方法 兼容
标准 stopPropagation()、stopImmediatePropagation() IE9+,Chrome,Firfox,Safari,360
IE版本 cancelBubble IE9<
阻止默认行为 属性或方法 兼容
标准 preventDefault() IE9+,Chrome,Firfox,Safari,360
IE版本 returnValue IE9<
事件的操作类(兼容所有版本)
    var EventUtil = {

        addHandler: function(element, type, handler) {
            if(element.addEventListener) {
                element.addEventListener(type, handler);
            }else if(element.attachEvent){
                element.attachEvent("on" + type, handler);
            }else {
                element["on" + type] =  handler;
            }
        },
        removeHandler: function(element, type, handler){
            if(element.addEventListener) {
                element.removeEventListener(type, handler);
            }else if(element.attachEvent){
                element.detachEvent("on" + type, handler);
            }else {
                element["on" + type] =  null;
            }
        },

        getEvent: function(event){
            return event? event : window.event;
        },
        getTarget: function(event){
            return event.target ? event.target:event.srcElement;
        },

        stopPropagation: function(event){
            if(event.stopPropagation) {
                event.stopPropagation();
            }else {
                event.cancelBubble = true;
            }
        },
        preventDefault: function(event){
            if(event.preventDefault) {
                event.preventDefault();
            }else {
                event.returnValue = false;
            }
        }
    }

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/54128.html

相关文章

  • DOM知识整理

    摘要:继承接口对象不仅实现了接口,也实现了接口,用来标识当前窗口内的文档节点。继承接口描述了所有相同种类的元素所普遍具有的方法和属性。 由于工作中一直在用框架来解决问题,在平时对dom的关注也比较少(特别像angular这种自己封装了一层视图层的框架,并不建议直接操作DOM),所以dom相关的知识也忘的差不多了,这次做公司产品的官网,没有太多的交互和功能,直接用了原生js,正好借此整理一下遗...

    huayeluoliuhen 评论0 收藏0
  • 整理DOM事件相关识点

    摘要:事件相关内容当用户与浏览器发生的一些交互时如果希望去获得用户行为就需要借助事件来完成事件部分内容在中重要性不言而喻罗列需要了解与事件相关的知识如下这也是面试中遇到的问题事件的级别事件模型事件流事件处理程序描述事件捕获冒泡的具体流程对象常见的 DOM事件相关内容 当用户与浏览器发生的一些交互时, 如果希望去获得用户行为, 就需要借助事件来完成. 事件部分内容在 JS中重要性不言而喻. ...

    red_bricks 评论0 收藏0
  • 前端面试之路二(javaScript基础整理)

    摘要:在标签中添加属性,本质上是跟在标签里面写属性时一样的,所以属性值最终都会编译为字符串类型。这个节点包括很多,比如,以及一些方法等方法。一个对象有很多,该集合名字为,里面有其他以及,里面有很多。 一、变量类型和计算 JS中使用typeof能得到哪些类型 变量类型 值类型:变量本身就是含有赋予给它的数值的,它的变量本身及保存的数据都存储在栈的内存块当中 引用类型:引用类型当然是分配到...

    AbnerMing 评论0 收藏0
  • 前端开发识点整理

    摘要:前言本文主要是有关前端方面知识按照目前的认知进行的收集归类概括和整理,涵盖前端理论与前端实践两方面。 前言:本文主要是有关前端方面知识按照 XX 目前的认知进行的收集、归类、概括和整理,涵盖『前端理论』与『前端实践』两方面。本文会告诉你前端需要了解的知识大致有什么,看上去有很多,但具体你要学什么,还是要 follow your heart & follow your BOSS。 初衷...

    Blackjun 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<