阿里前端实习面经

3,232 阅读10分钟

大家好,我是HuiAzir,在这里给大家分享一下我在阿里的面试经历

技术面总结

一面刚开始时候还是非常紧张的,说话都有点哆嗦...,还好面试官非常和蔼👍,紧张感很快就消失了。不多说,直接看经历😁!

面试官👨: 先做一下自我介绍吧😄。

👶🏻: 我是哈理工的一名大三学生,在学校做过xxx项目balabala....,目前在京东实习,正在做xxx项目,balabala...,我从大二开始学前端balabala...(在这里主要表述了我目前的状况,项目经历,通过什么样的方式学习前端)😏

面试官👨: 看到你的项目中Vue和React都有用到过,问你个关于Vue的问题,Vue是如何监听数据变化并触发视图更新的?

在边实际上问的就是MVVM的原理,其实只要理解发布订阅模式就可以很轻松的去理解,还有一个核心就是Object.defineProperty()。Vue3.0中已经改用Proxy。

在这里分享一下我学习Vue原理的时候曾经实现一个简易的MVVM框架,一步一步去分析还是很快就会理解的。

class MVVM {
  // 在构造函数中进行一些数据的初始化,同时将computed和methods代理到this上。
  constructor(options) {
    this.$vm = this;
    this.$el = options.el;
    this.$data = options.data;
    let computed = options.computed;
    let methods = options.methods;
    if (this.$el) {
      //  将数据可观察化
      new Observer(this.$data);
      for (let key in computed) {
        Object.defineProperty(this.$data, key, {
          get() {
            return computed[key].call(this);
          }
        });
      }
      for (let key in methods) {
        Object.defineProperty(this, key, {
          get() {
            return methods[key].bind(this);
          }
        });
      }
      // 模板的编译
      new Compiler(this.$vm, this.$el);
    }
  }
}
// 这里就是将data可观察化的类
class Observer {
  constructor(data) {
    this.observer(data);
  }
  observer(data) {
    if (data && Object.prototype.toString.call(data) === '[object Object]') {
      for (let key in data) {
        this.defineReactive(data, key, data[key]);
      }
    }
  }
  defineReactive(obj, key, value) {
    const self = this;
    this.observer(value);
    let dep = new Dep();
    Object.defineProperty(obj, key, {
      //  在data被get时候将当前观察者存入到订阅容器当中
      get() {
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      //  在data被重新设置时,通知订阅者可以更新了!
      set(newValue) {
        if (value !== newValue) {
          self.observer(newValue);
          value = newValue;
          dep.notify();
        }
      }
    });
  }
}
//  订阅容器
class Dep {

  //  临时存放当前观察者的静态属性
  static target = null;
  
  constructor() {
    this.subs = [];
  }
  
  //  添加订阅者
  addSub(watcher) {
    this.subs.push(watcher);
  }
  //  通知更新
  notify() {
    this.subs.forEach(watcher => watcher.update());
  }
}

// 观察者类
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    this.oldValue = this.getVal();
  }
  getVal() {
    Dep.target = this;
    let value = compileUtils.getVmData(this.vm, this.expr);
    Dep.target = null;
    return value;
  }
  //  每个观察者都必须要有一个更新函数来触发视图更新的回调
  update() {
    let newValue = this.getVal();
    this.cb(newValue);
  }
}
class Compiler {
  constructor(vm, el) {
    this.vm = vm;
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    let fragment = this.nodeToFragment(this.el);
    this.compile(fragment);
    this.el.appendChild(fragment);
  }
  isElementNode(node) {
    return node.nodeType === Node.ELEMENT_NODE;
  }
  isDirective(attrName) {
    return attrName.startsWith('v-');
  }
  nodeToFragment(node) {
    let fragment = document.createDocumentFragment();
    let firstChild;
    while ((firstChild = node.firstChild)) {
      fragment.appendChild(firstChild);
    }
    return fragment;
  }
  compile(node) {
    let childNodes = [...node.childNodes];
    childNodes.forEach(childNode => {
      if (this.isElementNode(childNode)) {
        this.compileElementNode(childNode);
        this.compile(childNode);
      } else {
        this.compileTextNode(childNode);
      }
    });
  }
  compileElementNode(node) {
    let attrs = [...node.attributes];
    attrs.forEach(({ name, value: expr }) => {
      if (this.isDirective(name)) {
        let [, directive] = name.split('-');
        let [directiveName, event] = directive.split(':');
        compileUtils[directiveName](this.vm, expr, node, event);
      }
    });
  }
  compileTextNode(node) {
    let reg = new RegExp(/\{\{(.*?)\}\}/g);
    if (reg.test(node.textContent)) {
      compileUtils.text(this.vm, node);
    }
  }
}
//  这里就是一些编译模板时所需要的工具函数
compileUtils = {
  getVmData(vm, expr) {
    return expr.split('.').reduce((data, current) => {
      return data[current];
    }, vm.$data);
  },
  on(vm, expr, node, event) {
    node.addEventListener(event, e => {
      vm[expr](e);
    });
  },
  model(vm, expr, node) {
    let value = this.getVmData(vm, expr);
    node.addEventListener('input', e => {
      expr.split('.').reduce((data, current, index, arr) => {
        if (index === arr.length - 1) {
          return (data[current] = e.target.value);
        }
        return data[current];
      }, vm.$data);
    });
    //  只要用到data的地方就要创建一个观察者
    new Watcher(vm, expr, newValue => {
      this.update.model(node, newValue);
    });
    this.update.model(node, value);
  },
  text(vm, node) {
    let oldContent = node.textContent;
    let reg = new RegExp(/\{\{(.*?)\}\}/g);
    let newContent = oldContent.replace(reg, (...args) => {
        //  只要用到data的地方就要创建一个观察者
      new Watcher(vm, args[1], () => {
        let newContent = oldContent.replace(reg, (...args) => {
          return this.getVmData(vm, args[1]);
        });
        this.update.text(node, newContent);
      });
      return this.getVmData(vm, args[1]);
    });
    this.update.text(node, newContent);
  },
  update: {
    model(node, value) {
      node.value = value;
    },
    text(node, content) {
      node.textContent = content;
    }
  }
};

用法也很简单

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <div id="app">
    <input type="text" v-model="msg">
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>
    <div>{{msg}}</div>
    <div>
      <p>{{a}}</p>
    </div>

  </div>
  <script src="./MVVM.js"></script>
  <script>
    let vm = new MVVM({
      el: '#app',
      data: {
        msg: 'a',
        a: 'hello'
      },
    })
  </script>
</body>

</html>

面试官👨: 简历上说你对ES6有了解,能跟我说说ES6中的异步请求吗?并且解决了什么问题?

👶🏻: ES6的异步请求是使用Promise的链式调用来完成,主要解决了回调地狱的问题。 回调地狱指的是回调函数嵌套层级过深,导致代码不好阅读和维护。

面试官👨: 那你可以说说Promise的原理吗?

在这里我也分享一下我纯手撸的Promise实现。

class Promise {
  PENDING = 'pending';
  RESOLVED = 'resolved';
  REJECTED = 'rejected';
  static resolve = value => {
    return new Promise((resolve, reject) => {
      try {
        if (value instanceof Promise) {
          value.then(resolve, reject);
        } else {
          resolve(value);
        }
      } catch (error) {
        reject(error);
      }
    });
  };
  static reject = reason => {
    return new Promise((resolve, reject) => {
      reject(reason);
    });
  };
  static all = promises => {
    let count = 0;
    let values = [];
    const promisesLength = promises.length;
    return new Promise((resolve, reject) => {
      promises.forEach((p, index) => {
        Promise.resolve(p).then(
          value => {
            count++;
            values[index] = value;
            if (count === promisesLength) {
              resolve(values);
            }
          },
          reason => {
            reject(reason);
          }
        );
      });
    });
  };
  static race = promises => {
    return new Promise((resolve, reject) => {
      promises.forEach(p => {
        Promise.resolve(p).then(resolve, reject);
      });
    });
  };
  constructor(executor) {
    this.status = this.PENDING;
    this.data = undefined;
    this.cbs = [];
    const resolve = value => {
      if (this.status === this.PENDING) {
        this.status = this.RESOLVED;
        this.data = value;
        setTimeout(() => {
          this.cbs.forEach(cbObj => {
            cbObj.onResolved(this.data);
          });
          this.cbs = [];
        });
      }
    };
    const reject = reason => {
      if (this.status === this.PENDING) {
        this.status = this.REJECTED;
        this.data = reason;
        setTimeout(() => {
          this.cbs.forEach(cbObj => {
            cbObj.onRejected(this.data);
          });
          this.cbs = [];
        });
      }
    };
    try {
      executor(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }
  then(onResolved, onRejected) {
    onResolved = typeof onResolved === 'function' ? onResolved : value => value;

    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : reason => {
            throw reason;
          };
    const self = this;
    return new Promise((resolve, reject) => {
      const handle = cb => {
        try {
          const result = cb(self.data);
          if (result instanceof Promise) {
            result.then(resolve, reject);
          } else {
            resolve(result);
          }
        } catch (error) {
          reject(error);
        }
      };
      if (self.status === self.PENDING) {
        self.cbs.push({
          onResolved() {
            handle(onResolved);
          },
          onRejected() {
            handle(onRejected);
          }
        });
      } else if (self.status === self.RESOLVED) {
        setTimeout(() => {
          handle(onResolved);
        });
      } else if (self.status === self.REJECTED) {
        setTimeout(() => {
          handle(onRejected);
        });
      }
    });
  }
  catch(onRejected) {
    return this.then(null, onRejected);
  }
}

面试官👨: 看你的简历,你最近有在自己做组件库对吗?而且是使用React Hooks,那你可以说一说React Hooks产生的原因,以及它解决了什么问题?

👶🏻:

过去,我们构建React组件的方式与组件的生命周期是耦合的。这使得组件中散布着相关的逻辑。比如说在componentDidmount中绑定了一个监听事件,如果要释放,那么就要在componentWillUnmount中释放。在hooks中就可以

useEffect(()=>{
    // 监听事件
    ...
    return ()=>{
        // 解绑事件
        ...
    }
})

面试官👨: 好,那如果我现在想让你做一个弹窗组件,你会怎么做?

👶🏻: React提供了一个API,Portals可以将组件挂载到body下。

面试官👨: 你可以说说为什么要把它挂载到body下吗?

👶🏻: 一是为了是html标签顺序更逻辑化,二是不影响触发弹窗组件的父组件。

面试官👨: 看到你的项目中使用了React Hooks + Mobx,你可以说说Hooks和Mobx是如何做连接的吗?

👶🏻: 现让Mobx导出一个store,将导出的store使用createContext存放并导出,然后使用的时候要现用mobx-react-lite提供的observe将组件包裹,然后使用useContext就可以了。

面试官👨: 那你可以跟我说说Mobx的原理吗?

👶🏻: 这个我没有去了解过,不过最近在分析Vue和React的源码,之后我会再去阅读一下他们周边生态的源码。

面试官👨: 看到你简历上说对Webpack与了解,熟悉到什么程度?webpack主要解决的问题是什么?

👶🏻: 可以实现项目的基础脚手架的配置,并做一些webpack打包过程的性能优化。webpack主要解决的问题就是 分析项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言,并将其转换和打包为合适的格式供浏览器使用。

面试官👨: 好,那如果项目打包上线了,如何让用户知道文件更新了,而不使用缓存?

👶🏻: 这个只需要在每次发布新版本时,给文件名添加hash名就可以了,对应的webpack配置就是将output的filename属性配置成[name].[contexthash:8].js就可以了

面试官👨: 你可以跟我讲述一下HTTP的缓存机制吗?

👶🏻:

强缓存

强缓存可以通过设置两种 HTTP Header 实现: Expires 和 CacheControl 。强缓存表示在缓存期间不需要请求, state code 为 200

Expires: Wed, 22 Oct 2018 08:41:00 GMT

Expires 是 HTTP/1 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

Cache-control: max-age=30

  • Cache-Control 出现于 HTTP/1.1 ,优先级⾼于 Expires 。该属性值表示资源会在 30 秒后过期,需要再次请求。
  • Cache-Control 可以在请求头或者响应头中设置,并且可以组合使⽤多种指令

协商缓存

  • 如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种 HTTP Header 实现: Last-Modified 和 ETag
  • 当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态 码,并且更新浏览器缓存有效期。

Last-Modified 和 If-Modified-Since

Last-Modified 表示本地⽂件最后修改⽇期, If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该⽇期后资源是否有更 新,有更新的话就会将新的资源发送回来,否则返回 304。

但是 Last-Modified 存在⼀些弊端:

  • 如果本地打开缓存⽂件,即使没有对⽂件进⾏修改,但还是会造成 Last-Modified 被修 改,服务端不能命中缓存导致发送相同的资源
  • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成⽂件,那么服务 端会认为资源还是命中了,不会返回正确的资源 因为以上这些弊端,所以在 HTTP / 1.1 出现了 ETag

ETag 和 If-None-Match

ETag 类似于⽂件指纹, If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级⽐ Last Modified ⾼。

面试官👨: 今后打算怎么深入学习前端?

👶: 首先要先学习React和Vue的源码,然后在学习它们周边的生态的源码,然后结合着更多的项目经验去更加深入的学习前端工程化。

面试官👨: 好,今天就到这里,你有什么想问我的吗?

👶: 我想请您推荐一些关于微前端的资料,想了解下微前端的知识。

这里面试官跟我说了很多,微前端也是刚刚兴起的东西,资料不是很多,如果你可以来阿里,可给你提供内部的资料参考,也可以参与其中的项目,阿里在这方面也有很多实践......。我大概总结了一下,微前端可能不会是将来的必然趋势,还是要根据项目来考虑是否要采用微前端的架构来实现。😌

项目经验总结

淘系技术部比较看重项目经验,特别是项目有没有什么特别的亮点,可惜我实在是总结不出我做过的项目除了做了一点性能优化还有哪些亮点,做过的项目都比较普通。

总结

在阿里总共面了8面,3面淘系,5面菜鸟,最后拿到了菜鸟offer。面试过程中面试官都很和蔼,没有给我一点点的心理压力。这些场面试中我觉得我还是缺乏对原理的认知,项目中也缺乏创新力,今后要多多研究原理性的东西,然后提高下自己的创造力。奥利给!!!👊🏻👊🏾👊👊🏿👊🏽👊🏼