没有足够的数据
(゚∀゚ )
暂时没有任何数据
(゚∀゚ )
暂时没有任何数据
leexiaoran 赞了文章 · 2020-10-26
一切要都要从打包构建说起。
当下我们很多项目都是基于 webpack 构建的, 主要用于:
首先,webpack 是一个伟大的工具。
经过不断的完善,webpack 以及周边的各种轮子已经能很好的满足我们的日常开发需求。
我们都知道,webpack 具备将各类资源打包整合在一起,形成 bundle 的能力。
可是,当资源越来越多时,打包的时间也将越来越长。
一个中大型的项目, 启动构建的时间能达到数分钟之久。
拿我的项目为例, 初次构建大概需要三分钟, 而且这个时间会随着系统的迭代越来越长。
相信不少同学也都遇到过类似的问题。 打包时间太久,这是一个让人很难受的事情。
那有没有什么办法来解决呢?
当然是有的。
这就是今天的主角 ESM
, 以及以它为基础的各类构建工具, 比如:
等等。
今天,我们就这个话题展开讨论, 希望能给大家一些其发和帮助。
文章较长,提供一个传送门:
ESM 是理论基础, 我们都需要了解。
「 ESM 」 全称 ECMAScript modules,基本主流的浏览器版本都以已经支持。
当使用ESM 模式时, 浏览器会构建一个依赖关系图。不同依赖项之间的连接来自你使用的导入语句。
通过这些导入语句, 浏览器 或 Node 就能确定加载代码的方式。
通过指定一个入口文件,然后从这个文件开始,通过其中的import语句,查找其他代码。
通过指定的文件路径, 浏览器就找到了目标代码文件。 但是浏览器并不能直接使用这些文件,它需要解析所有这些文件,以将它们转换为称为模块记录的数据结构。
然后,需要将 模块记录
转换为 模块实例
。
模块实例
, 实际上是 「 代码
」(指令列表)与「 状态
」(所有变量的值)的组合。
对于整个系统而言, 我们需要的是每个模块的模块实例。
模块加载的过程将从入口文件变为具有完整的模块实例图。
对于ES模块,这分为 三个步骤
:
在构建阶段时, 发生三件事情:
首先,需要找到入口点文件。
在HTML中,可以通过脚本标记告诉加载程序在哪里找到它。
但是,如何找到下一组模块, 也就是 main.js
直接依赖的模块呢?
这就是导入语句的来源。
导入语句的一部分称为模块说明符, 它告诉加载程序可以在哪里找到每个下一个模块。
在解析文件之前,我们不知道模块需要获取哪些依赖项,并且在提取文件之前,也无法解析文件。
这意味着我们必须逐层遍历树,解析一个文件,然后找出其依赖项,然后查找并加载这些依赖项。
如果主线程要等待这些文件中的每个文件下载,则许多其他任务将堆积在其队列中。
那是因为当浏览器中工作时,下载部分会花费很长时间。
这样阻塞主线程会使使用模块的应用程序使用起来太慢。
这是ES模块规范将算法分为多个阶段的原因之一。
将构造分为自己的阶段,使浏览器可以在开始实例化的同步工作之前获取文件并建立对模块图的理解。
这种方法(算法分为多个阶段)是 ESM
和 CommonJS模块
之间的主要区别之一。
CommonJS可以做不同的事情,因为从文件系统加载文件比通过Internet下载花费的时间少得多。
这意味着Node可以在加载文件时阻止主线程。
并且由于文件已经加载,因此仅实例化和求值(在CommonJS中不是单独的阶段)是有意义的。
这也意味着在返回模块实例之前,需要遍历整棵树,加载,实例化和评估任何依赖项。
在具有CommonJS模块的Node中,可以在模块说明符中使用变量。
require
在寻找下一个模块之前,正在执行该模块中的所有代码。这意味着当进行模块解析时,变量将具有一个值。
但是,使用ES模块时,需要在进行任何评估之前预先建立整个模块图。
这意味着不能在模块说明符中包含变量,因为这些变量还没有值。
但是,有时将变量用于模块路径确实很有用。
例如,你可能要根据代码在做什么,或者在不同环境中运行来记载不同的模块。
为了使ES模块成为可能,有一个建议叫做动态导入。有了它,您可以使用类似的导入语句:
import(`${path}/foo.js`)
。
这种工作方式是将使用加载的任何文件import()
作为单独图的入口点进行处理。
动态导入的模块将启动一个新图,该图将被单独处理。
但是要注意一件事–这两个图中的任何模块都将共享一个模块实例。
这是因为加载程序会缓存模块实例。对于特定全局范围内的每个模块,将只有一个模块实例。
这意味着发动机的工作量更少。
例如,这意味着即使多个模块依赖该模块文件,它也只会被提取一次。(这是缓存模块的一个原因。我们将在评估部分中看到另一个原因。)
加载程序使用称为模块映射的内容来管理此缓存。每个全局变量在单独的模块图中跟踪其模块。
当加载程序获取一个URL时,它将把该URL放入模块映射中,并记下它当前正在获取文件。然后它将发出请求并继续以开始获取下一个文件。
如果另一个模块依赖于同一文件会怎样?加载程序将在模块映射中查找每个URL。如果在其中看到fetching
,它将继续前进到下一个URL。
但是模块图不仅跟踪正在获取的文件。模块映射还充当模块的缓存,如下所示。
现在我们已经获取了该文件,我们需要将其解析为模块记录。这有助于浏览器了解模块的不同部分。
创建模块记录后,它将被放置在模块图中。这意味着无论何时从此处请求,加载程序都可以将其从该映射中拉出。
解析中有一个细节看似微不足道,但实际上有很大的含义。
解析所有模块,就像它们"use strict"
位于顶部一样。还存在其他细微差异。
例如,关键字await
是在模块的顶级代码保留,的值this
就是undefined
。
这种不同的解析方式称为“解析目标”。如果解析相同的文件但使用不同的目标,那么最终将得到不同的结果。
因此,需要在开始解析之前就知道要解析的文件类型是否是模块。
在浏览器中,这非常简单。只需放入type="module"
的script标签。
这告诉浏览器应将此文件解析为模块。并且由于只能导入模块,因此浏览器知道任何导入也是模块。
但是在Node中,您不使用HTML标记,因此无法选择使用type
属性。社区尝试解决此问题的一种方法是使用 .mjs
扩展。使用该扩展名告诉Node,“此文件是一个模块”。您会看到人们在谈论这是解析目标的信号。目前讨论仍在进行中,因此尚不清楚Node社区最终决定使用什么信号。
无论哪种方式,加载程序都将确定是否将文件解析为模块。如果它是一个模块并且有导入,则它将重新开始该过程,直到提取并解析了所有文件。
我们完成了!在加载过程结束时,您已经从只有入口点文件变为拥有大量模块记录。
下一步是实例化此模块并将所有实例链接在一起。
就像我之前提到的,实例将代码与状态结合在一起。
该状态存在于内存中,因此实例化步骤就是将所有事物连接到内存。
首先,JS引擎创建一个模块环境记录。这将管理模块记录的变量。然后,它将在内存中找到所有导出的框。模块环境记录将跟踪与每个导出关联的内存中的哪个框。
内存中的这些框尚无法获取其值。只有在评估之后,它们的实际值才会被填写。该规则有一个警告:在此阶段中初始化所有导出的函数声明。这使评估工作变得更加容易。
为了实例化模块图,引擎将进行深度优先的后顺序遍历。这意味着它将下降到图表的底部-底部的不依赖其他任何内容的依赖项-并设置其导出。
引擎完成了模块下面所有出口的接线-模块所依赖的所有出口。然后,它返回一个级别,以连接来自该模块的导入。
请注意,导出和导入均指向内存中的同一位置。首先连接出口,可以确保所有进口都可以连接到匹配的出口。
这不同于CommonJS模块。在CommonJS中,整个导出对象在导出时被复制。这意味着导出的任何值(例如数字)都是副本。
这意味着,如果导出模块以后更改了该值,则导入模块将看不到该更改。
相反,ES模块使用称为实时绑定的东西。两个模块都指向内存中的相同位置。这意味着,当导出模块更改值时,该更改将显示在导入模块中。
导出值的模块可以随时更改这些值,但是导入模块不能更改其导入的值。话虽如此,如果模块导入了一个对象,则它可以更改该对象上的属性值。
之所以拥有这样的实时绑定,是因为您可以在不运行任何代码的情况下连接所有模块。当您具有循环依赖性时,这将有助于评估,如下所述。
因此,在此步骤结束时,我们已连接了所有实例以及导出/导入变量的存储位置。
现在我们可以开始评估代码,并用它们的值填充这些内存位置。
最后一步是将这些框填充到内存中。JS引擎通过执行顶级代码(函数外部的代码)来实现此目的。
除了仅在内存中填充这些框外,评估代码还可能触发副作用。例如,模块可能会调用服务器。
由于存在潜在的副作用,您只需要评估模块一次。与实例化中发生的链接可以完全相同的结果执行多次相反,评估可以根据您执行多少次而得出不同的结果。
这是拥有模块映射的原因之一。模块映射通过规范的URL缓存模块,因此每个模块只有一个模块记录。这样可以确保每个模块仅执行一次。与实例化一样,这是深度优先的后遍历。
那我们之前谈到的那些周期呢?
在循环依赖关系中,您最终在图中有一个循环。通常,这是一个漫长的循环。但是为了解释这个问题,我将使用一个简短的循环的人为例子。
让我们看一下如何将其与CommonJS模块一起使用。首先,主模块将执行直到require语句。然后它将去加载计数器模块。
然后,计数器模块将尝试message
从导出对象进行访问。但是由于尚未在主模块中对此进行评估,因此它将返回undefined。JS引擎将在内存中为局部变量分配空间,并将其值设置为undefined。
评估一直持续到计数器模块顶级代码的末尾。我们想看看我们是否最终将获得正确的消息值(在评估main.js之后),因此我们设置了超时时间。然后评估在上恢复main.js
。
消息变量将被初始化并添加到内存中。但是由于两者之间没有连接,因此在所需模块中它将保持未定义状态。
如果使用实时绑定处理导出,则计数器模块最终将看到正确的值。到超时运行时,main.js
的评估将完成并填写值。
支持这些循环是ES模块设计背后的重要理由。正是这种设计使它们成为可能。
(以上是关于 ESM 的理论介绍, 原文链接在文末)。
谈及 Bundleless 的优势,首先是启动快。
因为不需要过多的打包,只需要处理修改后的单个文件,所以响应速度是 O(1) 级别,刷新即可即时生效,速度很快。
所以, 在开发模式下,相比于Bundle,Bundleless 有着巨大的优势。
上面的图具体的模块加载机制可以简化为下图:
在项目启动和有文件变化时重新进行打包,这使得项目的启动和二次构建都需要做较多的事情,相应的耗时也会增长。
从上图可以看到,已经不再有一个构建好的 bundle、chunk 之类的文件,而是直接加载本地对应的文件。
从上图可以看到,在 Bundleless 的机制下,项目的启动只需要启动一个服务器承接浏览器的请求即可,同时在文件变更时,也只需要额外处理变更的文件即可,其他文件可直接在缓存中读取。
Bundleless 模式可以充分利用浏览器自主加载的特性,跳过打包的过程,使得我们能在项目启动时获取到极快的启动速度,在本地更新时只需要重新编译单个文件。
Vite 也是基于 ESM 的, 文件处理速度 O(1)级别, 非常快。
作为探索, 我就简单实现了一个乞丐版Vite:
GitHub 地址: Vite-mini,
简要分析一下。
<body>
<div id="app"></div>
<script type="module" data-original="/src/main.js"></script>
</body>
html 文件中直接使用了浏览器原生的 ESM(type="module"
) 能力。
所有的 js 文件经过 vite 处理后,其 import 的模块路径都会被修改,在前面加上 /@modules/
。当浏览器请求 import 模块的时候,vite 会在 node_modules
中找到对应的文件进行返回。
其中最关键的步骤就是模块的记载和解析
, 这里我简单用koa简单实现了一下, 整体结构:
const fs = require('fs');
const path = require('path');
const Koa = require('koa');
const compilerSfc = require('@vue/compiler-sfc');
const compileDom = require('@vue/compiler-dom');
const app = new Koa();
// 处理引入路径
function rewriteImport(content) {
// ...
}
// 处理文件类型等, 比如支持ts, less 等类似webpack的loader的功能
app.use(async (ctx) => {
// ...
}
app.listen(3001, () => {
console.log('3001');
});
我们先看路径相关的处理:
function rewriteImport(content) {
return content.replace(/from ['"]([^'"]+)['"]/g, function (s0, s1) {
// import a from './c.js' 这种格式的不需要改写
// 只改写需要去node_module找的
if (s1[0] !== '.' && s1[0] !== '/') {
return `from '/@modules/${s1}'`;
}
return s0;
});
}
处理文件内容: 源码地址
后续的都是类似的:
这个代码只是解释实现原理, 不同的文件类型处理逻辑其实可以抽离出去, 以中间件的形式去处理。
代码实现的比较简单, 就不额解释了。
和 webpack 的对比:
我使用 Snowpack 做了个 demo , 支持打包, 输出 bundle。
github: Snowpack-React-Demo
能够清晰的看到, 控制台产生了大量的文件请求(也叫瀑布网络请求),
不过因为都是加载的本地文件, 所以速度很快。
配合HMR, 实现编辑完成立刻生效, 几乎不用等待:
但是如果是在生产中,这些请求对于生产中的页面加载时间而言, 就不太好了。
尤其是HTTP1.1,浏览器都会有并行下载的上限,大部分是5个左右,所以如果你有60个依赖性要下载,就需要等好长一点。
虽然说HTTP2多少可以改善这问题,但若是东西太多,依然没办法。
关于这个项目的打包, 直接执行build:
打包完成后的文件目录,和传统的 webpack 基本一致:
在 build 目录下启动一个静态文件服务:
build 模式下,还是借助了 webpack 的打包能力:
做了资源合并:
就这点而言, 我认为未来一段时间内, 生产环境还是不可避免的要走bundle模式。
开门见山吧, 开发体验不是很友好,几点比较突出的问题:
当然还有其他方方面面的问题, 就不一一列举。
我简单改造了一个页面, 就遇到很多奇奇怪怪的问题, 开发起来十分难受, 尽管代码的修改能立刻生效。
bundleless 能在开发模式下带了很大的便利。 但就目前来说,要运用到生产的话, 还是有一段路要走的。
就目当下而言, 如果真的要用的话,可能还是 bundleless(dev) + bundle(production) 的组合。
至于未来能不能全面铺开 bundleless,我认为还是有可能的, 交给时间吧。
本文主要介绍了 esm 的原理, 以及介绍了以此为基础的Vite, Snowpack 等工具, 提供了两个可运行的 demo:
并探索了 bundleless 在生产中的可行性。
Bundleless, 本质上是将原先 Webpack 中模块依赖解析的工作交给浏览器去执行,使得在开发过程中代码的转换变少,极大地提升了开发过程中的构建速度,同时也可以更好地利用浏览器的相关开发工具。
最后,也非常感谢 ESModule、Vite、Snowpack 等标准和工具的出现,为前端开发提效。
才疏学浅, 文中若有错误,还能各位大佬指正, 谢谢。
如果你觉得这篇内容对你挺有启发,可以:
前言一切要都要从打包构建说起。当下我们很多项目都是基于 webpack 构建的, 主要用于:本地开发打包上线首先,webpack 是一个伟大的工具。经过不断的完善,webpack 以及周边的各种轮子已经能很好的满足我们的日常开发需求。我们都知道,webpack 具备将各类资源打包...
赞 47 收藏 21 评论 5
leexiaoran 赞了文章 · 2020-06-05
本文能帮你做什么?
1、了解vue的双向数据绑定原理以及核心代码模块
2、缓解好奇心的同时了解如何实现双向绑定
为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环依赖等,也难免存在一些问题,欢迎大家指正。不过这些并不会影响大家的阅读和理解,相信看完本文后对大家在阅读vue源码的时候会更有帮助<
本文所有相关代码均在github上面可找到 https://github.com/DMQ/mvvm
相信大家对mvvm双向绑定应该都不陌生了,一言不合上代码,下面先看一个本文最终实现的效果吧,和vue一样的语法,如果还不了解双向绑定,猛戳Google
<div id="mvvm-app">
<input type="text" v-model="word">
<p>{{word}}</p>
<button v-on:click="sayHi">change model</button>
</div>
<script data-original="./js/observer.js"></script>
<script data-original="./js/watcher.js"></script>
<script data-original="./js/compile.js"></script>
<script data-original="./js/mvvm.js"></script>
<script>
var vm = new MVVM({
el: '#mvvm-app',
data: {
word: 'Hello World!'
},
methods: {
sayHi: function() {
this.word = 'Hi, everybody!';
}
}
});
</script>
效果:
目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。
实现数据绑定的做法有大致如下几种:
发布者-订阅者模式(backbone.js)脏值检查(angular.js)
数据劫持(vue.js)
发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value)
,这里有篇文章讲的比较详细,有兴趣可点这里
这种方式现在毕竟太low了,我们更希望通过 vm.property = value
这种方式更新数据,同时自动更新视图,于是有了下面两种方式
脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval()
定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:
数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()
来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一,如果不熟悉defineProperty,猛戳这里
整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者
上述流程如图所示:
ok, 思路已经整理完毕,也已经比较明确相关逻辑和模块功能了,let's do it
我们知道可以利用Obeject.defineProperty()
来监听属性变动
那么将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter
和getter
这样的话,给这个对象的某个值赋值,就会触发setter
,那么就能监听到了数据变化。。相关代码可以是这样:
var data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // 哈哈哈,监听到值变化了 kindeng --> dmq
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
// 取出所有属性遍历
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
};
function defineReactive(data, key, val) {
observe(val); // 监听子属性
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
return val;
},
set: function(newVal) {
console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
val = newVal;
}
});
}
这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法,代码改善之后是这样:
// ... 省略
function defineReactive(data, key, val) {
var dep = new Dep();
observe(val); // 监听子属性
Object.defineProperty(data, key, {
// ... 省略
set: function(newVal) {
if (val === newVal) return;
console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
val = newVal;
dep.notify(); // 通知所有订阅者
}
});
}
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
那么问题来了,谁是订阅者?怎么往订阅器添加订阅者?
没错,上面的思路整理中我们已经明确订阅者应该是Watcher, 而且var dep = new Dep();
是在 defineReactive
方法内部定义的,所以想通过dep
添加订阅者,就必须要在闭包内操作,所以我们可以在 getter
里面动手脚:
// Observer.js
// ...省略
Object.defineProperty(data, key, {
get: function() {
// 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
Dep.target && dep.addSub(Dep.target);
return val;
}
// ... 省略
});
// Watcher.js
Watcher.prototype = {
get: function(key) {
Dep.target = this;
this.value = data[key]; // 这里会触发属性的getter,从而添加订阅者
Dep.target = null;
}
}
这里已经实现了一个Observer了,已经具备了监听数据和数据变化通知订阅者的功能,完整代码。那么接下来就是实现Compile了
compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图,如图所示:
因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el
转换成文档碎片fragment
进行解析编译操作,解析完成,再将fragment
添加回原来的真实dom节点中
function Compile(el) {
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
init: function() { this.compileElement(this.$fragment); },
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(), child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
};
compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明:
Compile.prototype = {
// ... 省略
compileElement: function(el) {
var childNodes = el.childNodes, me = this;
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/; // 表达式文本
// 按元素节点方式编译
if (me.isElementNode(node)) {
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
me.compileText(node, RegExp.$1);
}
// 遍历编译子节点
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
},
compile: function(node) {
var nodeAttrs = node.attributes, me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
// 规定:指令以 v-xxx 命名
// 如 <span v-text="content"></span> 中指令为 v-text
var attrName = attr.name; // v-text
if (me.isDirective(attrName)) {
var exp = attr.value; // content
var dir = attrName.substring(2); // text
if (me.isEventDirective(dir)) {
// 事件指令, 如 v-on:click
compileUtil.eventHandler(node, me.$vm, exp, dir);
} else {
// 普通指令
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
}
});
}
};
// 指令处理集合
var compileUtil = {
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
// ...省略
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
// 第一次初始化视图
updaterFn && updaterFn(node, vm[exp]);
// 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
new Watcher(vm, exp, function(value, oldValue) {
// 一旦属性值有变化,会收到通知执行此更新函数,更新视图
updaterFn && updaterFn(node, value, oldValue);
});
}
};
// 更新函数
var updater = {
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
}
// ...省略
};
这里通过递归遍历保证了每个节点及子节点都会解析编译到,包括了{{}}表达式声明的文本节点。指令的声明规定是通过特定前缀的节点属性来标记,如<span v-text="content" other-attr
中v-text
便是指令,而other-attr
不是指令,只是普通的属性。
监听数据、绑定更新函数的处理是在compileUtil.bind()
这个方法中,通过new Watcher()
添加回调来接收数据变化的通知
至此,一个简单的Compile就完成了,完整代码。接下来要看看Watcher这个订阅者的具体实现了
Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
如果有点乱,可以回顾下前面的思路整理
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
// 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
this.value = this.get();
}
Watcher.prototype = {
update: function() {
this.run(); // 属性值变化收到通知
},
run: function() {
var value = this.get(); // 取到最新值
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
}
},
get: function() {
Dep.target = this; // 将当前订阅者指向自己
var value = this.vm[exp]; // 触发getter,添加自己到属性订阅器中
Dep.target = null; // 添加完毕,重置
return value;
}
};
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
get: function() {
// 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
Dep.target && dep.addDep(Dep.target);
return val;
}
// ... 省略
});
Dep.prototype = {
notify: function() {
this.subs.forEach(function(sub) {
sub.update(); // 调用订阅者的update方法,通知变化
});
}
};
实例化Watcher
的时候,调用get()
方法,通过Dep.target = watcherInstance
标记订阅者是当前watcher实例,强行触发属性定义的getter
方法,getter
方法执行的时候,就会在属性的订阅器dep
添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。
ok, Watcher也已经实现了,完整代码。
基本上vue中数据绑定相关比较核心的几个模块也是这几个,猛戳这里 , 在src
目录可找到vue源码。
最后来讲讲MVVM入口文件的相关逻辑和实现吧,相对就比较简单了~
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
一个简单的MVVM构造器是这样子:
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data;
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}
但是这里有个问题,从代码中可看出监听的数据对象是options.data,每次需要更新视图,则必须通过var vm = new MVVM({data:{name: 'kindeng'}}); vm._data.name = 'dmq';
这样的方式来改变数据。
显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:var vm = new MVVM({data: {name: 'kindeng'}}); vm.name = 'dmq';
所以这里需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理为访问vm._data的属性,改造后的代码如下:
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data, me = this;
// 属性代理,实现 vm.xxx -> vm._data.xxx
Object.keys(data).forEach(function(key) {
me._proxy(key);
});
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}
MVVM.prototype = {
_proxy: function(key) {
var me = this;
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
});
}
};
这里主要还是利用了Object.defineProperty()
这个方法来劫持了vm实例对象的属性的读写权,使读写vm实例的属性转成读写了vm._data
的属性值,达到鱼目混珠的效果,哈哈
至此,全部模块和功能已经完成了,如本文开头所承诺的两点。一个简单的MVVM模块已经实现,其思想和原理大部分来自经过简化改造的vue源码,猛戳这里可以看到本文的所有相关代码。
由于本文内容偏实践,所以代码量较多,且不宜列出大篇幅代码,所以建议想深入了解的童鞋可以再次结合本文源代码来进行阅读,这样会更加容易理解和掌握。
本文主要围绕“几种实现双向绑定的做法”、“实现Observer”、“实现Compile”、“实现Watcher”、“实现MVVM”这几个模块来阐述了双向绑定的原理和实现。并根据思路流程渐进梳理讲解了一些细节思路和比较关键的内容点,以及通过展示部分关键代码讲述了怎样一步步实现一个双向绑定MVVM。文中肯定会有一些不够严谨的思考和错误,欢迎大家指正,有兴趣欢迎一起探讨和改进~
最后,感谢您的阅读!
查看原文本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模块2、缓解好奇心的同时了解如何实现双向绑定为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环依赖等,也难免存在一些问题,欢...
赞 1326 收藏 1792 评论 152
leexiaoran 发布了文章 · 2020-05-20
最近两篇面试以及离职相关的文章不容错过哦。
今年面试还是比较顺的,面了五家公司(酷家乐、拼多多、字节、滴滴、蚂蚁),都过了。
在文章里我不仅会列出面试题,还会给到一些答题建议,个人能力有限,也不能保证我回答都正确,如果有错误,希望能纠正我。
说一下浏览器缓存
浏览器缓存分为强缓存和协商缓存,强缓存会直接从浏览器里面拿数据,协商缓存会先访问服务器看缓存是否过期,再决定是否从浏览器里面拿数据。
控制强缓存的字段有:Expires和Cache-Control,Expires 和 Cache-Control。
控制协商缓存的字段是:Last-Modified / If-Modified-Since 和 Etag / If-None-Match,其中 Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。
cookie 与 session 的区别
Session 是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
Cookie 是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现 Session 的一种方式。
浏览器如何做到 session 的功能的。
其实就是考察 http 怎么处理无状态是怎么处理的,具体可见 COOKIE和SESSION有什么区别?里面的答案。
解释一下:csrf 和 xss
XSS:恶意攻击者往 Web 页面里插入恶意 Script 代码,当用户浏览该页之时,嵌入其中 Web 里面的 Script 代码会被执行,从而达到恶意攻击用户的目的。
CSRF:CSRF 攻击是攻击者借助受害者的 Cookie 骗取服务器的信任,可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作。
详见:前端安全面试题
怎么防止 csrf 和 xss
详见:前端安全面试题
跨域的处理方案有哪些
常用的:jsonp、CORS、nginx 代理,完整的大概是九种,可见:九种跨域方式实现原理(完整版)
CORS 是如何做的?
服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。
对于 CORS ,Get 和 POST 有区别吗?
其实想考察的就是什么时候会有预检请求(option 请求)。
了解 HTTPS 的过程吗?
推荐浪浪的 深入理解HTTPS工作原理
webpack 如何做性能优化
webpack 做性能优化主要是考虑打包体积和打包速度。
体积分析用 webpack-bundle-analyzer
插件,速度分析用:speed-measure-webpack-plugin
插件。
打包速度优化瓶子君的:玩转 webpack,使你的打包速度提升 90%。
es module 和 commonjs 的区别
高频题,考察 ES6 模块和 CommonJS 模块 的区别。关键点:1. 前者是值的引用,后者是值的拷贝。 2.前者编译时输出接口,后者运行时加载。
推荐文章:前端模块化:CommonJS,AMD,CMD,ES6
react 里如何做动态加载
React.lazy
,另外通过 webpack 的动态加载:import()
和 ensure.require
动态加载的原理是啥,就是 webpack 编译出来的代码
讲道理 webpack 动态加载就两种方式:import()
和 require.ensure
,不过他们实现原理是相同的。
我觉得这道题的重点在于动态的创建 script 标签,以及通过 jsonp
去请求 chunk,推荐的文章是:webpack是如何实现动态导入的
笔试题:页面结构包括页头(永远在顶部)、主体内容、页脚,页脚永远在页面底部(不是窗口底部),即内容高度不够时,页脚也要保证在页面底部
常规题,考察基本的布局
笔试题:写 new 的执行过程
new 的执行过程大致如下:
function myNew(Con, ...args) {
let obj = Object.create(Con.prototype)
let result = Con.apply(obj, args)
return typeof result === 'object' ? result : obj
}
笔试题:写一个处理加法可能产生精度的函数,比如 0.1 + 0.2 = 0.3
思路:对于浮点数在底层处理是有问题的,所以目的就是想办法将所以的浮点数转化为整数进行处理,同时乘以一个倍数(A),然后加起来后再除以这个倍数(A),这个倍数应该是两个数中最小的那个数的倍数,比如 0.1 + 0.02 ,那么应该同时乘以 100,变为 10 + 2,然后再将值除以 100。
1000000000 + 1000000000 允许返回字符串 处理大数
大数问题就是通过字符串来处理,从后往前加,然后处理进位的问题。
聊项目
项目基本是问:
写一个 es6 的继承过程
这个题我觉得出得很好,很考察基本功。
// 这个是要实现的方法
createClass = fun(sons, super) {
// TODO
return fn;
}
// 这是个 es6 的一个例子,要实现 extends 的功能。
class Man extends Human {
cons (args) {
super(args)
// xxxxx
}
speak() {
console.log('')
}
}
写一个大数相乘的解决方案。传两个字符串进来,返回一个字符串
function multi(str1, str2) {
}
这道题跟一面的时候思路差不多,只是进位的时候不一定是 1。
聊项目写一个防抖函数
字节果然是出了名的考算法题比较多的,基本每面都会算法题和编程题,对编程能力比较看重吧。
讲道理一面还是比较常规的,二三面因为都是团队 leader 和更高级别的,问的技术细节也比较少了,重点考察一些技术方案和项目的问题。
webpack 原理
大致就是:
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
babel 原理
babel的转译过程分为三个阶段:parsing、transforming、generating,以ES6代码转译为ES5代码为例,babel转译的具体过程如下:
虚拟 DOM 的理解
项目里如何做的性能优化
这个跟我的项目相关。
写过webpack loader 或者插件吗讲讲你写的 babel 插件
redux 的原理redux 做状态管理和发布订阅模式有什么区别
redux 其实也是一个发布订阅,但是 redux 可以做到数据的可预测和可回溯。
react-redux 的原理,是怎么跟 react 关联起来的
react-redux 的核心组件只有两个,Provider 和 connect,Provider 存放 Redux 里 store 的数据到 context 里,通过 connect 从 context 拿数据,通过 props 传递给 connect 所包裹的组件。
了解多端的原理吗?
不清楚,没了解过。
http 与 tcp 的关系tcp 可以建立多个连接吗?
我估计是想问 http 的管线化,当时忘了这个叫啥了
介绍一下为什么要有 三次握手,四次挥手写过 babel 插件吗?用来干啥的?怎么写的 babel 插件
写过一些简单的 babel 插件,说了我们公司用来通过代码生成文档的 babel 插件是怎么做的。
知道怎么转化成 AST 的吗?
我估计就是问词法分析和语法分析相关的
研究过 React 的运行时吗?职业规划。
项目介绍说一下你的项目有哪些复杂的点,以及怎么解决的
这个聊了挺久的,还聊了一些数据量比较大的怎么处理。
你们的业务组件库有多少个,是什么样的组件权限组件是怎么设计的
会node 吗?
我说我只会增删改查,会点 express,然后就开始一顿狂轰乱炸的知识。
介绍一下你对中间件的理解怎么保证后端服务稳定性,怎么做容灾
感觉已经超纲了,基本没做过,还好之前跟后端同学聊过他们怎么做容灾的,还记得两点说了下。
怎么让数据库查询更快
数据库是用的什么?
mysql
为什么用 mysql希望滴滴能提供给你什么?
这个题其实还挺常考的,可以好好准备下,背一下答案。
最后面试官问我有什么想问他的么,我说没有,因为我之前问得挺多了。不过他还是给我介绍了他们业务还是很厉害的,集团第三,还拿了 A 级绩效,公司有很多技术上的沉淀,跨端呀,web IDE 呀,等等
这个时候我就感觉自己能过了,感觉是在吸引我去,偷笑。
介绍一下项目的难点以及怎么解决的
一起讨论那些难点
自己有什么技术上的优势最近在研究什么技术?
职业规划
移动端的业务有做过吗?
希望滴滴能提供给你什么?
当业务重的时候怎么安排时间?
滴滴我面的这个岗位是可能回去做一些多端应用,所以会涉及到很多 webpack 和 ast 相关的东西,所以这些问得比较多,感觉这个组还是很不错的,能做到很多技术上的东西。
讲讲来蘑菇街都干了哪些事情聊聊有什么技术上的沉淀
除了 redux mbox dva 还用过其他的 状态管理没
hooks 原理
看过 hooks 相关的草案吗
你对蚂蚁有什么期望,或者说技术的规划,想做的东西
vue3 的 类似 hooks 的原理是怎么样的
聊项目,项目的难点,以及自己做了哪些事情为什么项目里会引入 TS
dva 和 redux 的区别
职业发展,今年的打算
组件升级怎么让使用这个组件的人都知道。
如果让你设计项目自动设计组件升级,并且安全,你会怎么去设计
全程聊项目,因为他不是前端的,所以没问前端任何知识,主要聊业务相关的,看看我对业务的理解,以及一些想法。
一面面试官问了很多我完全不知道的东西,我知道的东西可能答的比较随意吧,记不起来了。反正能深深的感受到面试对知识的广度和深度远超我很多很多,基本不在一个 level 的。
二三面已经是 P9 的大佬来面了,所以也没问太多的技术细节,都是考察一些技术的解决方案和项目的东西。
蚂蚁的面试其实还是比较难的,问题都不是那种有确定的答案,基本都是考察你平时的积累和经验。
上一篇拼多多和酷家乐的面经之后有很多人问我工作几年,怎么学习的之类的,或者你面了这么多都过了是不是有什么技巧呀?
首先我目前工作了快两年了,18 年 7 月份毕业的。
关于如何准备面试的问题,我的策略是把自己会的东西以及常用的东西,尽量让面试官问不倒你,不会的东西可以跟面试官说不会,不怎么了解,那一般面试官也不会问相关的问题了,如果连自己常用的东西都了解得不深,那么就不太有可能把平时不常用的了解得很深了。
比如我可能写 react 比较多,参与过组件库的开发,webpack 写得也比较多。所以我在准备的时候,就尽量准备的这些知识,了解我的人都知道,我对 react 研究得比较多,原理、性能优化都写过文章,所以如果考 react 的东西,我基本不担心。 对于 vue、移动端、小程序、node 啥的,我基本没准备,问倒的时候我就说仅仅了解,没有什么实战经验。
最后我是桃翁,一个爱思考的前端er,期待你的关注。
如果你最近也在找工作,欢迎找我交流,下面是我公众号。
前言 最近两篇面试以及离职相关的文章不容错过哦。 离开蘑菇街后,我最近的一些想法 拼多多和酷家乐面试总结 今年面试还是比较顺的,面了五家公司(酷家乐、拼多多、字节、滴滴、蚂蚁),都过了。 在文章里我不仅会列出面试题,还会给到一些答题建议,个人能力有限,也...
赞 95 收藏 63 评论 4
leexiaoran 发布了文章 · 2020-05-12
离职原因看我这篇文章吧:离开蘑菇街后,我最近的一些想法,然后不得不去找工作恰饭呀。
我目前面了五家公司:滴滴、蚂蚁、拼多多、酷家乐、字节跳动,拼多多和酷家乐基本已拿到 offer,蚂蚁二面完了,滴滴和字节即将三面,我先把我已经面过的面经先总结出来,其他的不管过没过,这周内我都会总结出来,希望能给到正在找工作同学的帮助。
在文章里我不仅会列出面试题,还会给到一些答题建议,个人能力有限,也不能保证我回答都正确,如果有错误,希望能纠正我。
酷家乐是我最早面的一家公司,还没被裁的那周二刚好面完,结果周五就被裁了,可能这就是冥冥之中吧。
面试的部门是工具组,是酷家乐最核心的部门,四面面试官跟我说我面的组是工具团队中的最核心组,会涉及到一些图像相关的技术,比如 Tree.js、WebGL等,所以这个组其实也挺好的,感觉能学到不少技术。
你在项目如何优化的
我在简历里面写到了性能优化相关的,所以这个问题。
你做的项目有什么值得说的
基本上就是考察项目的亮点,可以说一些项目难点是如何解决的,或者介绍一些项目中用到的牛逼的技术。
Ts 有什么优势
讲道理所有现在在网上能查到的优势都是得益于静态语言的优势。
type 和 interface 的区别
这是一个高频题,如果考察 TS,这应该是最容易考察的,网上也都能查到相关的资料,但是很可能忽略一个点:type 只是一个类型别名,并不会产生类型。所以其实 type 和 interface 其实不是同一个概念,其实他们俩不应该用来比较的,只是有时候用起来看着类似。
React 事件机制
我觉得需要答的点:
- React 为什么要用合成事件
- 事件机制:注册和分发的过程。这里面至少要说出事件注册都是在元素的最顶层
document
节点上。参考资料:一文吃透 React 事件机制原理
聊聊 React 的 diff
聊 diff 建议先看看我之前写过的一篇关于虚拟 DOM 的文章:从 React 历史的长河里聊虚拟DOM及其价值,有助于理解 diff 的意义。diff 的细节可以看我之前写的:详解 React 16 的 Diff 策略
React 优化
可以看之前我写的 React 函数式组件性能优化指南,对于类组件也有对应的 API。
怎么理解闭包
基础中的基础,虽然社招考得不多,但是如果连闭包都理解不了,应该会减分不少。闭包由于在规范里没有定义,所以很多人下的定义不一样,理解的角度也不同,但是自己要有一套正确的理解方式,如果按照我的理解 JavaScript 里面所有的函数都是闭包,因为有全局环境,所有的函数都可以访问全局变量。
节流怎么实现的
防抖和节流的代码还是需要会手写的,这也是一个闭包的例子,
原型,class B 继承 class A 翻译成 es5 应该是什么样子
说实话,我觉得这道题其实蛮有水平的,即考察了如何写出一个好的继承方式,也对 new 过程进行了考察,还对考察了对 Class 的理解。注意的点:
class
是有重载功能的,怎么在子类的构造函数里面调用super
react 的基本原理
UI = f(state) ,虚拟 DOM、diff 策略、setState
react 如何做性能优化
这个题也是高频,见一面回答
redux 的重点概念
store、reduce、action、actionCreater、dispatch
聊一聊 React 的生命周期
尽量把 React 15 和 16 的进行对比,然后 16 为什么要准备废除那几个生命周期,以及新增的生命周期可以进行替代。这个图好好的理解一下
聊一聊 hooks 怎么处理生命周期
讲道理函数式组件是没有生命周期的,但是如何去模拟类组件的生命周期的作用,都是在useEffect
里面进行操作的,因为生命周期里面所做的基本都是副作用,放到useEffect
里是最合适的,专门用来处理副作用。
const a = { b : 3}
function foo(obj) {
obj.b = 5
return obj
}
const aa = foo(a)
console.log(a.b)
console.log(aa.b)
function Ofo() {}
function Bick() {
this.name = 'mybick'
}
var myBick = new Ofo()
Ofo.prototype = new Bick()
var youbick = new Bick()
console.log(myBick.name)
console.log(youbick.name)
box-sizing
属性,判断元素的尺寸和颜色。实现一个 fill 函数,不能用循环。
考察递归
用 ES5 实现私有变量
考察闭包的使用
简历里面的性能优化是如何做的
减少请求频率、图片压缩、React.memo
、React.useMemo
class 组件里面如何做性能优化(因为前面我说了用 React.memo 做了性能优化)
shouldComponentUpdate(简称 SCU)。SCU 跟 immutable 强相关,一定要好好理解 react 的 immutable,否则很可能理解不了为什么不能直接去修改 state,然后再去 setState,以及 redux 的 reducer 要返回一个新的对象。
实现一个 Typescript 里的 Pick
type Pick<T, K extends keyof T> = { [P in K]: T[P] }
算法题,怎么判断单链表相交。
很多种方法,我当时说的是最后一个节点如果相同,那么就代表相交。
算法题,怎么找到第一个相交的节点。
同时遍历两个链表到尾部,同时记录两个链表的长度。若两个链表最后的一个节点相同,则两个链表相交。有两个链表的长度后,我们就可以知道哪个链表长,设较长的链表长度为len1,短的链表长度为len2。则先让较长的链表向后移动(len1-len2)个长度。然后开始从当前位置同时遍历两个链表,当遍历到的链表的节点相同时,则这个节点就是第一个相交的节点。这是我刚想到的一种方式,不过当时面试的时候我记得好像更简单,但是想不起来了。
那你聊聊 React 的源码,把你记得起的讲一讲
我看过 React 的一部分源码的,所以关于 React 源码更新部分的东西,应该基本都能说个大概。
酷家乐面试体验还是不错的,我是一面电话面,面完觉得 OK 之后就叫到公司去现场面试,6 点半下班了就骑车去了酷家乐,七点开始面试,一口气面完了三面,饭都没来得及吃,不过面试官很好给我倒了水。
感觉面试官对我的项目似乎不太敢兴趣,很少问项目的东西,可能由于他们是工具组,连我简历里面组件库相关的也没面,考察基础的比较多,基础考察得比较全面。
但是由于考虑到我之前做的项目复杂性不够,以及工作年限的问题,给到的评级不高,导致薪资也不是特别高,但是已经是这个评级的顶峰了,要是没有更好的 offer 酷家乐还是非常值得去的,特别是工具组。
react 16 生命周期有什么改变
componentWillMount
,componentWillReceiveProps
,componentWillUpdate
准备废除,新增了static getDerivedStateFromProps
和getSnapshotBeforeUpdate
我还详细的介绍了为什么要用
getDerivedStateFromProps
来代替即将废除的三个生命周期,主要是16 版本 render 之前的生命周期可能会被多次执行,具体的可看我的这篇文章:Deep In React之浅谈 React Fiber 架构(一)
getDerivedStateFromProps
flex: 0 1 auto;
是什么意思?
flex 这个属性常考题,好好把阮老师的那篇 flex 语法篇看完 flex 的面试题基本没问题。
算法题:求最大公共前缀,如 ['aaafsd', 'aawwewer', 'aaddfff'] => 'aa'
不能调试,全靠编程素养,只能面试官才能运行。
interface 和 type 的区别
又考了,上面有回答
有用状态管理吗?
我常用的是 redux 和 dva,然后再聊了聊区别已经 redux 的理念
有用 ssr 吗?
没用过
node 熟悉吗?
写得少
class 组件与函数式组件的区别
生命周期、设计理念,感觉这道题比较开发,可以看看 dan 的这篇:函数式组件与类组件有何不同?
css 优先级
important > 内联 > ID选择器 > 类选择器 > 标签选择器
避免 css 全局污染。
我常用的 css modules
css modules 的原理
生成唯一的类名
有一个a标签,如何动态的决定他的样式。
我说了先写几个 css,然后外部传一个前缀的方式。面试官问了都要这样吗?我说可以通过 context 的方式,就不需要每个组件都传了。
import 和 require 导入的区别
高频题,考察 ES6 模块和 CommonJS 模块 的区别。关键点:1. 前者是值的引用,后者是值的拷贝。 2.前者编译时输出接口,后者运行时加载。
require 有什么性能问题
好好想想上一个题的区别就能想到了
组件库如何做按需加载
我常用的是babel-plugin-import
webpack 如何实现动态加载
讲道理 webpack 动态加载就两种方式:import()
和require.ensure
,不过他们实现原理是相同的。我觉得这道题的重点在于动态的创建 script 标签,以及通过
jsonp
去请求 chunk,推荐的文章是:webpack是如何实现动态导入的
react 里有动态加载的 api 吗?
React.lazy
webpack 能动态加载 require 引入的模块吗?
应该是不能的,前面说了,webpack 支持动态导入基本上只能用import()
和require.ensure
。
require 引入的模块 webpack 能做 Tree Shaking 吗?
不能,Tree Shaking 需要静态分析,只有 ES6 的模块才支持。
写一个 promise 重试函数,可以设置时间间隔和次数。function foo(fn, interval, times) {}
常规题,网上有参考答案的。
组件平台有哪些功能?
详细的跟我讨论组件平台的设计,因为他们也想做一个组件平台。
实现一个 redux
实现 createStore
的功能,关键点发布订阅的功能,以及取消订阅的功能。
用 ts 实现一个 redux
简单的加上类型,我写的类型没有 redux 源码那么复杂,当时写得比较简单。
一面的时候其实我自己感觉答得不是特别好,连 less 的语法都忘记了,当时面下来感觉要凉了,平时写 样式的时间确实太少了。
很幸运的时候还是给我过了,二面面试官我觉得面得很专业,基本都是从浅入深的考察知识的深度,我感觉答得还可以,因为我是属于那种会的就尽量深一点,暂时不用的就很少花时间,所以我目前知识的广度很有欠缺,对于 node、ssr、移动端、小程序这些方面我的能力都很薄弱,但是面试的时候如果你不熟悉,直接说不熟悉就行,他就不会面了。
我准备面试之前对我自己的要求就是,我会的尽量不会很快就被问倒,所以我重点复习了我擅长的知识。
面下来感觉拼多多也没有想象中那么难,虽然拼多多薪资算行业内高的,不过拼多多在上海,我在杭州,另外就是强制上 6 天班,我比较忌惮这点。考虑到我和女朋友本来就是异地,要是单休,而且节假日也会比正常的少,见面的机会就更少了。
反正选 offer 这种事还是尽量综合考虑吧,团队、薪资、个人生活都应该考虑进去。
我是桃翁,一个爱思考的前端er,期待你的关注。如果你最近也在找工作,欢迎找我交流,下面是我公众号。
我目前面了五家公司:滴滴、蚂蚁、拼多多、酷家乐、字节跳动,拼多多和酷家乐基本已拿到 offer,蚂蚁二面完了,滴滴和字节即将三面,我先把我已经面过的面经先总结出来,其他的不管过没过,这周内我都会总结出来,希望能给到正在找工作同学的帮助。
赞 69 收藏 49 评论 7
leexiaoran 关注了专栏 · 2020-03-25
欢迎github搜索 vianvio/FE-Companions
关注 1067
leexiaoran 赞了文章 · 2020-03-25
我是淘宝技术部的一名普通的前端技术专家,花名磐冲。每年都想给团队内招几个同学,但是努力了几年,一个都没有招进来。是我看简历太少了吗?不是,只算内部简历系统,我看过的简历至少上千。是我要求太严格吗?也许是吧,不过,我电话面试拒绝的同学,只有1位在一段时间后,入职了另一个部门。
好吧,我承认,我自己在招聘上可能是有点没找到方法。但是,看了那么多简历,经历了那么多次面试,我最大的感受却是惋惜。因为有好多同学,在电话那头我听出了努力,听出了能力,听出了激情,但是却没有听到亮点、和让我觉得,能够继续闯过下一关的能力。
我面试过的同学,在结束的时候,我都会指出问题,并给出学习建议。大部分同学不是不够努力,不是不够聪明,而是没有找对方法,没有切中要害。我总结了一下之前所有的面试经历,以及常见的问题,写下这篇文章,希望能够给前端的同学,不论是否来面试阿里的职位,有一个参考。同时,也是写下我自己总结的方法,希望能帮助到其他技术相关的同学。
业务背景
淘宝内部最大创新项目之一,大团队已有百人规模,大部分项目处于保密阶段,前景远大
职位描述
1.负责组件库与业务页面开发。
2.带领团队完成技术产品实现。
3.负责大型多应用架构设计。
4.利用前端技术与服务端协同完成团队业务目标。职位要求
0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。
1.熟练掌握JavaScript。
2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。
3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。
4.熟练掌握react生态常用工具,redux/react-router等。
5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。
6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。
7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。
为什么起这个标题呢?因为有很多人看到职位描述,可能就在和自己做的事情一一比对,把关键字都核对上。而很多前端同学看到职位要求第一条里的图形学,可能就开始打退堂鼓了。或者看到几个关键字自己都认识,就觉得没问题,还挺简单的。
就这样望而却步真的好吗?为什么职位描述看着简单,面试却这么难呢?你真的读懂这份职位描述了吗?
现在,不妨先停一下,就上面的问题,我们来细细品一下。什么叫读懂职位描述呢?从我个人的理解,读懂职位描述,应该是读懂这个职位需要哪些基础能力,以及可能遇到哪些挑战。我们写自己简历的时候,“精通react”和“熟练使用react”,相信大家不会随意去写。同样的,JD里面的:掌握、熟练掌握、了解、熟悉,也不是随意写的,这代表了团队对新同学的能力要求。
回想写自己简历的时候,我们会对这个前缀扪心自问一下。因为会担心一旦写了精通,面试官的问题会更难,甚至觉得只有源码倒背如流的人,才能称得上精通。当然也会有同学非常自信,用react做过几个项目,就写上了精通react。
这两种都可以称为精通,也都不可以。没有客观标准,又怎么去衡量呢?而标准在哪里呢?所以在这里,我从阿里面试官角度,给出我认为的标准,尽可能的做到客观可量化。那么,基于上面这份职位标准,我来翻译一下职位要求:
首先,总览全部的要求,会发现这个职位虽然提到了3d相关的技能,但是大部分却是应用开发相关的能力,所以这个职位并不是想找专业的3d领域同学,而是需要一个工程化能力强,对3d有了解的同学。
初级:
中级:
高级:
初级:
中级:
初级:
中级:
高级:
初级:
中级:
高级:
初级:
中级:
高级:
初级:
中级:
高级:
初级:
高级:
首先,感谢你能看到这里,如果你是仔细看的,那么我更加感动了。而且你已经用实际行动,证明了你的学习能力和耐心。上面那么大篇幅的JD翻译,有一个问题,大家应该都有答案了:为什么职位描述看着简单,面试却这么难呢?然而,有些同学可能会嘲讽起来:写了那么多,我认识的有些阿里P6,P7也不是都会啊,大厂都是螺丝钉,也就面试时候问问,实际工作不还是if else,何况我又遇不到这些场景,我怎么可能知道。
在这里,我想严肃的说明的是:
- 我所认识的淘宝前端,以及我所在团队的P6同学,上面初级都能做到,中级至少覆盖60%,高级覆盖20%;P6+同学,中级覆盖80%以上,高级覆盖50%以上;P7同学高级覆盖80%以上。
- 我们团队的前端,每一个人都负责多个复杂业务项目(客观数据上:至少对接20+服务端接口,5个以上router配置,涉及多个用户角色的综合业务系统),以及一些通用能力,比如组件库等。不存在一个人只接一条业务线,只负责维护某几个组件这种螺丝钉式的工作。我不知道大厂都是螺丝钉的言论为什么会被复用到互联网企业,我个人感受是,如果我在阿里的工作是螺丝钉,那么我以前几份工作可能勉强算是螺纹。另外,如果你想要晋升,那么维护好这几个业务系统只是你的本职工作,晋升时请提供一些更高层面的思考和技术产出。
- if else也分鲜花和牛粪。有的人写的是[].reduce,而有的人写的是var temp = ''; for() { temp += 'xxx' }。另外,如果不知道原理,那么类似webpack这种明星级的技术产品,将永远与你无缘。冷静下来想想,webpack难道也只是if else吗?是,又不全是。
聪明的你应该看出来了,上面JD翻译里的初级、中级和高级,对应的就是我认为的,阿里p6/p6+/p7的能力标准,同时也是一张知识图谱。初级的要求更偏实际应用和基础原理,中级的要求是基于原理的拓展和复杂技术架构的应用,高级的要求是对跨栈、跨端,多领域结合产出综合方案的能力。而且,我们对技术的要求,都是能够与实际业务场景结合,或者能对提升工作效率有帮助的。空谈和尬想,或者只是百度来的文章,没有经过内化,那么面试过程中将被瞬间拆穿。
有时我会在boss直聘上直接打字面试,有时我也会听到面试过程中,电话那头传来键盘敲击的声音,甚至有时候我会主动让面试的同学去百度一下,或者挂电话思考一下,过15分钟再聊。我敢这么面试,因为我知道,我要的答案你查不出来,我看的是你真正理解的东西。能搜索到的,我不在乎,我也希望你去查,来为你更好的表现综合能力。
好了,如果看到这里,并没有把你劝退的话,那么让我们来点希望的曙光。这里用一句阿里土话来给大家一些安慰:不难,要你干嘛?
开篇我提到面试过那么多同学之后,我最大的感受是惋惜,因为有很多同学在我看来就差一点点,他有足够的个人能力,可能只是没有找到感觉。这里我例举两个比较典型的问题。
我相信这是很多同学心中的疑惑,而且事实上,我看到很多简历下面的面试记录都会写:缺乏亮点,暂不考虑。如果仔细看了上文,到这里还有这个疑惑,那么我觉得你需要再静下心来感受一下。
这里我不对亮点做明确的表述,我举一个例子来让大家更有体感一些:
A: 负责公司前端工作,使用webpack打包代码并发布线上。使用webpack配置对整体性能做优化,用happypack加快了打包速度。B: 建设内部云构建体系,产出通用命令行指令工具;将发布、环境切换、快速回滚能力平台化,保证了线上环境稳定性;同时将研发流程量化管控,每周产出研发效能报告。
如果你是面试官,在简历的大海里看一个项目描述,什么最吸引你的眼球呢?是webpack,happypack的关键字吗?还是一句话就让你想到这件事的复杂性,和这个系统带来的巨大价值?
这也是很多同学经常遇到的问题。上面例举了那么多技术点,而我在的环境,前端就我一个,甚至服务端我都要写一点,哪有精力去搞这种大规模团队用到的东西?
首先,时间靠自己合理规划。我和老婆两个人自己带孩子,有两个娃,每天平均9点下班,我每天回家收拾玩具,孩子睡得晚可能需要再陪玩一下,周末我带孩子为主,但是我去年仍然白金了2个ps4的游戏。
在时间问题排除之后,我建议分三个阶段:
我相信每个人都是能快速成长的,只是每个人缺少的东西不同。有的人少了些脚踏实地,有的人少了些登高望远的机会,更多的人或许只是没有找到那条正确的路。
我希望这篇文章能够帮助到正在前端领域努力的人,也希望这一篇文章就能成为指路明灯之一。但同时我也深知,每个人都是不一样的,所以,我这里留下联系方式,需要的同学可以加微信:vianvio
加备注:前端同路人。我可以给你做模拟面试,同时给出我认为的,适合你的发展思路和建议,当然也可以帮你内推。
另外,目前我们成立了一个模拟面试群,有定期活动,可以参考 https://github.com/vianvio/FE...
欢迎有兴趣的同学来参加。
我在阿里巴巴淘宝技术部-ihome业务。目前,ihome正在深耕家居家装行业,纵向深入行业内部,希望能给行业带来一些创新。目前可对外公开的产品和业务形态有:躺平App、位于青岛和宁波的桔至生活门店。我们还有更多有趣、充满挑战和超出你想象的业务。我们期待有志之士的加入!
如果你愿意来和我们一起相信,那请发送简历过来,我们一定会一起看见!
前端简历请发送到:yefei.niuyf@alibaba-inc.com 或 lijie.slj@alibaba-inc.com
主攻3d方向的同学,简历请发送到:jiangcheng.wxd@alibaba-inc.com
java简历请发送到:xiaoxian.zzy@taobao.com 或 wuxin.sn@taobao.com
客户端简历请发送到:fangying.fy@alibaba-inc.com
或许有人会觉得奇怪,联系方式写在最后,还有多少人能看到,这里我引用马爸爸和逍遥子大佬对阿里价值观的解读,来解释一下:我们的价值观是为了帮助我们寻找同路的人。
感谢你陪我一起走到这篇文章的最后,如果你觉得这篇文章已经对你有很大帮助了,那就请我喝杯咖啡吧~
查看原文我是淘宝技术部的一名普通的前端技术专家,花名磐冲。每年都想给团队内招几个同学,但是努力了几年,一个都没有招进来。是我看简历太少了吗?不是,只算内部简历系统,我看过的简历至少上千。是我要求太严格吗?也许是吧,不过,我电话面试拒绝的同学,只有1位在一段...
赞 241 收藏 145 评论 21
leexiaoran 赞了文章 · 2020-03-19
有这样一个热门问题:
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
alert(a.x); // --> undefined
alert(b.x); // --> {n: 2}
其实这个问题很好理解,关键要弄清下面两个知识点:
JS引擎对赋值表达式的处理过程
赋值运算的右结合性
形如
A = B
的表达式称为赋值表达式。其中A和B又分别可以是表达式。B可以是任意表达式,但是A必须是一个左值。
所谓左值,就是可以被赋值的表达式,在ES规范中是用内部类型引用(Reference)描述的。例如:
表达式foo.bar
可以作为一个左值,表示对foo这个对象中bar这个名称的引用;
变量email
可以作为一个左值,表示对当前执行环境中的环境记录项envRec中email这个名称的引用;
同样地,函数名func
可以做左值,然而函数调用表达式func(a, b)
不可以。
那么JS引擎是怎样计算一般的赋值表达式 A = B
的呢?简单地说,按如下步骤:
计算表达式A,得到一个引用refA
;
计算表达式B,得到一个值valueB
;
将valueB
赋给refA
指向的名称绑定;
返回valueB
。
所谓结合性,是指表达式中同一个运算符出现多次时,是左边的优先计算还是右边的优先计算。
赋值表达式是右结合的。这意味着:
A1 = A2 = A3 = A4
等价于
A1 = (A2 = (A3 = A4))
好了,有了上面两部分的知识。下面来看一下JS引擎是怎样运算连等赋值表达式的。
以下面的式子为例:
Exp1 = Exp2 = Exp3 = Exp4
首先根据右结合性,可以转换成
Exp1 = (Exp2 = (Exp3 = Exp4))
然后,我们已经知道对于单个赋值运算,JS引擎总是先计算左边的操作数,再计算右边的操作数。所以接下来的步骤就是:
计算Exp1,得到Ref1;
计算Exp2,得到Ref2;
计算Exp3,得到Ref3;
计算Exp4,得到Value4。
现在变成了这样的:
Ref1 = (Ref2 = (Ref3 = Value4))
接下来的步骤是:
将Value4赋给Exp3;
将Value4赋给Exp2;
将Value4赋给Exp1;
返回表达式最终的结果Value4。
注意:这几个步骤体现了右结合性。
总结一下就是:
先从左到右解析各个引用,然后计算最右侧的表达式的值,最后把值从右到左赋给各个引用。
现在回到文章开头的问题。
首先前两个var语句执行完后,a
和b
都指向同一个对象{n: 1}
(为方便描述,下面称为对象N1)。然后来看
a.x = a = {n: 2};
根据前面的知识,首先依次计算表达式a.x
和a
,得到两个引用。其中a.x
表示对象N1中的x,而a
相当于envRec.a
,即当前环境记录项中的a。所以此时可以写出如下的形式:
[[N1]].x = [[encRec]].a = {n: 2};
其中,[[]]
表示引用指向的对象。
接下来,将{n: 2}
赋值给[[encRec]].a
,即将{n: 2}
绑定到当前上下文中的名称a
。
接下来,将同一个{n: 2}
赋值给[[N1]].x
,即将{n: 2}
绑定到N1中的名称x
。
由于b
仍然指向N1
,所以此时有
b <=> N1 <=> {n: 1, x: {n: 2}}
而a
被重新赋值了,所以
a <=> {n: 2}
并且
a === b.x
如果你明白了上面所有的内容,应该会明白a.x = a = {n:2};
与b.x = a = {n:2};
是完全等价的。因为在解析a.x
或b.x
的那个时间点
。a
和b
这两个名称指向同一个对象,就像C++中同一个对象可以有多个引用一样。而在这个时间点
之后,不论是a.x
还是b.x
,其实早就不存在了,它已经变成了那个内存中的对象.x
了。
最后用一张图表示整个表达式的运算过程:
11.13.1 Simple Assignment ( = )
查看原文有这样一个热门问题: {代码...} 其实这个问题很好理解,关键要弄清下面两个知识点: JS引擎对赋值表达式的处理过程 赋值运算的右结合性 一. 赋值表达式 形如 {代码...} 的表达式称为赋值表达式。其中A和B又分别可以是表达式。B可以是任意表达式,但是A必须是一个左...
赞 71 收藏 137 评论 29
查看全部 个人动态 →
(゚∀゚ )
暂时没有
注册于 2016-05-04
个人主页被 4k 人浏览
推荐关注