diff --git a/.bin/constant.js b/.bin/constant.js new file mode 100644 index 0000000..b48756b --- /dev/null +++ b/.bin/constant.js @@ -0,0 +1,15 @@ +const GitHubApiVersion = "2022-11-28"; + +const GithubOwner = "hankliu62"; + +// 博客Repo +const GithubRepoBlog = "hankliu62.github.com"; + +const AccessToken = (process.env.NEXT_GITHUB_BACKEND_TOKEN || '').split(''); + +module.exports = { + AccessToken, + GitHubApiVersion, + GithubOwner, + GithubRepoBlog +} \ No newline at end of file diff --git a/.bin/libs/issues.js b/.bin/libs/issues.js new file mode 100644 index 0000000..ff20dda --- /dev/null +++ b/.bin/libs/issues.js @@ -0,0 +1,194 @@ +const { Octokit } = require('octokit'); +const { AccessToken, GitHubApiVersion, GithubOwner } = require('../constant'); + +const auth = AccessToken.join(''); + +/** + * 根据分页获取数据 + * + * @param {*} page + * @returns + */ +async function fetchIssues(page, repo, options = {}) { + const octokit = new Octokit({ + auth: auth + }) + + const res = await octokit.request(`GET /repos/${GithubOwner}/${repo}/issues`, { + owner: GithubOwner, + repo, + per_page: 30, + page: page, + headers: { + 'X-GitHub-Api-Version': GitHubApiVersion + }, + direction: 'asc', + ...options, + }); + + + // 请求成功 + if (res.status === 200) { + return res.data; + } else { + throw res; + } +} + +/** + * 获取所有问题 + * @returns + */ +async function fetchAllIssues(repo, options = {}) { + return new Promise((resolve, reject) => { + // 问题列表 + const questions = []; + let page = 1; + async function loopFetchIssue() { + const currentQuestions = await fetchIssues(page, repo, options); + if (currentQuestions.length) { + for (const question of currentQuestions) { + questions.push(question); + } + page ++; + setTimeout(loopFetchIssue, 100); + } else { + resolve(questions); + } + } + + loopFetchIssue(); + }); +} + +/** + * 生成问题 + * + * @param {*} repo + * @param {*} title + * @param {*} answer + * @param {*} options + * @returns + */ +async function createIssue(repo, title, answer, options = {}) { + const octokit = new Octokit({ + auth: auth + }) + + const res = await octokit.request(`POST /repos/${GithubOwner}/${repo}/issues`, { + owner: GithubOwner, + repo: repo, + title: title, + body: answer, + assignees: [ + GithubOwner + ], + headers: { + 'X-GitHub-Api-Version': GitHubApiVersion + }, + ...options, + }); + + return res; +} + +/** + * 生成问题 + * + * @param {*} repo + * @param {*} id + * @param {*} options + * @returns + */ +async function closeIssue(repo, id, options = {}) { + const octokit = new Octokit({ + auth: auth + }) + + const res = await octokit.request(`POST /repos/${GithubOwner}/${repo}/issues/${id}`, { + owner: GithubOwner, + repo: repo, + issue_number: +id, + state: "closed", + state_reason: "completed", + headers: { + 'X-GitHub-Api-Version': GitHubApiVersion + }, + ...options, + }); + + return res; +} + +/** + * 更新问题 + * + * @param {*} repo + * @param {*} id + * @param {*} body + * @param {*} options + * @returns + */ +async function updateIssue(repo, id, body, options = {}) { + const octokit = new Octokit({ + auth: auth + }) + + const res = await octokit.request(`POST /repos/${GithubOwner}/${repo}/issues/${id}`, { + owner: GithubOwner, + repo: repo, + issue_number: +id, + body: body, + headers: { + 'X-GitHub-Api-Version': GitHubApiVersion + }, + ...options, + }); + + return res; +} + +/** + * 获得所有问题的名称Set + * + * @param {*} repo + * @param {*} options + * @returns + */ +async function getIssuesTitleSet(repo, options = {}) { + const set = new Set() + const issues = await fetchAllIssues(repo, options); + for (const issue of issues) { + set.add(issue.title.trim()); + } + + return set; +} + +/** + * 获得所有问题的名称Map + * + * @param {*} repo + * @param {*} options + * @returns + */ +async function getIssuesTitleMap(repo, options = {}) { + const map = new Map() + const issues = await fetchAllIssues(repo, options); + for (const issue of issues) { + map.set(issue.title.trim(), issue); + } + + return map; +} + + +module.exports = { + fetchIssues, + fetchAllIssues, + createIssue, + closeIssue, + updateIssue, + getIssuesTitleSet, + getIssuesTitleMap, +} \ No newline at end of file diff --git a/.bin/libs/labels.js b/.bin/libs/labels.js new file mode 100644 index 0000000..e45e136 --- /dev/null +++ b/.bin/libs/labels.js @@ -0,0 +1,84 @@ +const { Octokit } = require('octokit'); +const { AccessToken, GitHubApiVersion, GithubOwner } = require('../constant'); + +const auth = AccessToken.join(''); + +/** + * 根据获取所有的标签 + * + * @param {*} repo + * @returns + */ +async function fetchLabels(repo) { + const octokit = new Octokit({ + auth: auth + }) + + const res = await octokit.request(`GET /repos/${GithubOwner}/${repo}/labels`, { + owner: GithubOwner, + repo: repo, + per_page: 100, + page: 1, + headers: { + 'X-GitHub-Api-Version': GitHubApiVersion + }, + }); + + + // 请求成功 + if (res.status === 200) { + return res.data; + } else { + throw res; + } +} + +/** + * 生成标签 + * + * @param {*} repo + * @param {*} name + * @param {*} color + * @param {*} description + * @returns + */ +async function createLabel(repo, name, color, description) { + const octokit = new Octokit({ + auth: auth + }) + + const res = await octokit.request(`POST /repos/${GithubOwner}/${repo}/labels`, { + owner: GithubOwner, + repo: repo, + name: name, + description: description, + color: color, + headers: { + 'X-GitHub-Api-Version': GitHubApiVersion + }, + }); + + return res; +} + +/** + * 获得所有标签的名称Set + * + * @param {*} repo + * @returns + */ +async function getLabelsNameSet(repo) { + const set = new Set() + const issues = await fetchLabels(repo); + for (const issue of issues) { + set.add(issue.name.trim()); + } + + return set; +} + +module.exports = { + fetchLabels, + getLabelsNameSet, + createLabel, +} \ No newline at end of file diff --git a/.bin/md2github-issues.js b/.bin/md2github-issues.js new file mode 100644 index 0000000..3ef689d --- /dev/null +++ b/.bin/md2github-issues.js @@ -0,0 +1,206 @@ +/** + * 将本地Markdown文件创建或者更新到github issues中 + */ +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); + +const { createIssue, updateIssue, getIssuesTitleMap } = require('./libs/issues'); +const { getLabelsNameSet } = require('./libs/labels'); +const { GithubRepoBlog } = require('./constant'); + +// 从指定时间开始format: YYYY-MM-DDTHH:MM:SSZ. +const SinceDate = '2016-02-04T12:00:00'; + + +/** + * 获取待提交的文章路径列表 + * 该函数会读取'file_list_to_commit.txt'文件,文件中每一行记录了一个文章的相对路径, + * 并检查这些文件是否存在。最后返回一个包含所有存在文章路径的数组。 + * + * @returns {Promise>} 返回一个Promise,该Promise解析为一个包含所有有效文章路径的数组。 + */ +function getArticles() { + return new Promise(function (resolve) { + + // 计算提交文件列表文件的绝对路径 + const commitFile = path.join(process.cwd(), 'file_list_to_commit.txt'); + // 初始化文章路径列表 + const articlePaths = []; + + // 创建读取流来读取提交文件列表 + const readStream = fs.createReadStream(commitFile); + const rl = readline.createInterface({ + input: readStream, + output: process.stdout, + terminal: false + }); + + // 对读取的每一行进行处理 + rl.on('line', (line) => { + // 尝试匹配行中的文章路径 + const matched = /source\/_posts\/(.+\.md)$/.exec(line) + // 如果匹配成功,则处理该路径 + if (matched && matched.length) { + // 计算文章的绝对路径 + const filePath = path.join(process.cwd(), 'source', '_posts', matched[1]); + // 检查文件是否存在 + const dirStat = fs.statSync(filePath); + if (dirStat.isFile()) { + // 如果文件存在,则加入到文章路径列表中 + articlePaths.push(filePath); + } else { + // 如果文件不存在,则打印错误信息 + console.log(`文件不存在: ${filePath}`); + } + // 此处的代码逻辑似乎存在重复,articlePaths已通过上述条件推入,此处再次推入似乎多余 + articlePaths.push(path.join(process.cwd(), 'source', '_posts', matched[1])) + } + }); + + // 当读取流关闭时,打印文章路径列表并解析Promise + rl.on('close', async () => { + console.log('文件读取完毕,提交的文章列表: ', articlePaths); + resolve(articlePaths); + }); + }) +} + +/** + * 生成文章列表 + * @param {Array} articlePaths - 文章路径数组 + * @param {Set} labelSet - 标签集合,用于筛选文章标签 + * @returns {Array} 按时间排序的文章列表对象数组 + */ +async function generateArticles(articlePaths, labelSet) { + const articles = []; + for (const articlePath of articlePaths) { + // 读取文章内容 + let article = await fs.readFileSync(articlePath, "utf-8"); + let title; + let date; + let tags; + const apiOptions = { labels: ['blog'] }; + + // 解析文章头部信息(标题、标签、日期) + const labelMatched = /^---([\d\D]*?)---/.exec(article.trim()); + if (labelMatched && labelMatched.length) { + const items = labelMatched[1].split(/\n/g).filter(Boolean); + for (const text of items) { + const [item, key, value] = /(\w*?):([\d\D]+)/.exec(text); + if (key === 'title') { + title = value.trim(); + } else if (key === 'tag') { + // 处理标签,匹配标签集合并加入apiOptions的labels中 + tags = value.trim().replace(/\[/g, '').replace(/\]/g, '').split(/[,,]/).filter(Boolean).map((item) => item.trim()); + for (const tag of tags) { + if (labelSet.has(tag)) { + apiOptions.labels.push(tag); + } + } + } else if (key === 'date') { + date = value.trim(); + } + } + + // 移除文章头部信息 + article = article.trim().replace(/^---([\d\D]*?)---/, ''); + } + + // 检查文章日期,排除早于指定日期的文章 + if (date && new Date(date).valueOf() < new Date(SinceDate).valueOf()) { + console.log(`${title} date(${date}) is older than ${SinceDate}`); + continue; + } + + // 添加处理后文章信息到数组 + articles.push({ + title, + content: article, + apiOptions, + ts: date ? new Date(date).valueOf() : 0, + }) + } + + // 按时间戳降序排序文章列表 + return articles.sort((pre, next) => next.ts - pre.ts); +} + +/** + * 异步执行创建或更新GitHub仓库中文章的函数 + * 无参数 + * 无返回值 + */ +async function run() { + // 获取所有文章的路径 + const articlePaths = await getArticles(); + + // 获取GitHub仓库中已有的问题(Issue)标题映射 + const titleMap = await getIssuesTitleMap(GithubRepoBlog); + console.log("fetched issues count", titleMap.size); + + // 获取GitHub仓库中所有标签(Label)的集合 + const labelSet = await getLabelsNameSet(GithubRepoBlog); + + // 根据文章路径和标签集生成文章对象数组,并取第一个文章对象 + const articles = await generateArticles(articlePaths, labelSet); + console.log("articles count", articles.length); + + // 如果存在文章,则开始创建或更新文章 + if (articles.length) { + let index = 0; + /** + * 异步循环创建或更新文章 + * 无参数 + * 无返回值 + */ + async function loopCreateArticle() { + if (index === articles.length) { + return; // 结束循环 + } + + // 获取当前处理的文章对象 + const article = articles[index] + const title = article.title.trim(); // 文章标题 + let content = (article.content || '').trim(); // 文章内容 + + // 检查GitHub仓库中是否存在相同标题的Issue + const findIssue = titleMap.get(title); + if (findIssue) { + // 如果存在,判断是否需要更新内容 + if (findIssue.body && findIssue.body.trim() !== answer) { + // 如果内容不一致,则更新Issue内容 + const res = await updateIssue( + GithubRepoBlog, + findIssue.number, + content, + article.apiOptions, + ); + + console.log('update', index, res.status, title); + } else { + // 如果内容一致,跳过更新,处理下一篇文章 + index ++; + loopCreateArticle(); + return; + } + } else { + // 如果不存在相同标题的Issue,则创建新Issue + const res = await createIssue( + GithubRepoBlog, + title, + content, + article.apiOptions, + ); + console.log("create", index, res.status, title); + } + + index++; // 处理下一篇文章 + setTimeout(loopCreateArticle, 1000); // 延时1秒后继续处理 + } + + loopCreateArticle(); // 开始处理文章 + } +} + +run(); diff --git a/.github/workflows/gh-page.yml b/.github/workflows/gh-page.yml new file mode 100644 index 0000000..345648c --- /dev/null +++ b/.github/workflows/gh-page.yml @@ -0,0 +1,49 @@ +name: github pages +on: + push: + branches: [master] +# 执行的一项或多项任务 +jobs: + build-and-deploy: + # 运行在虚拟机环境ubuntu-latest + # https://docs.github.com/zh/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on + runs-on: ubuntu-latest + steps: + - name: 获取源码 🛎️ + uses: actions/checkout@v3 + with: + lfs: true + - name: Node环境版本 🗜️ + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Yarn缓存 📁 + id: yarn-cache + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + - name: 缓存依赖 📚 + uses: actions/cache@v2 + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: 安装依赖 📦 + run: yarn install --frozen-lockfile + - name: 安装 Hexo 🦊 + run: npm install -g hexo-cli + - name: 打包 🏗️ + env: + NEXT_GITHUB_BACKEND_TOKEN: ${{ secrets.BACKEND_TOKEN }} + run: yarn build && touch ./public/.nojekyll # run: touch ./out/.nojekyll是因为由于 Jekyll 处理,GitHub 默认不提供_next文件夹,.nojekyll文件阻止了这种情况, + - name: 部署 🚀 + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages # 部署后提交到那个分支 + folder: public # 这里填打包好的目录名称 + clean: true + - name: 同步 👣 + env: + NEXT_GITHUB_BACKEND_TOKEN: ${{ secrets.BACKEND_TOKEN }} + run: yarn sync + + diff --git a/.gitignore b/.gitignore index 43bc596..c197cca 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ .deploy_git/ themes/hexo-theme-laughing public/ -db.json \ No newline at end of file +db.json +.env.local + +file_list_to_commit.txt \ No newline at end of file diff --git a/_config.yml b/_config.yml index bfa34a9..3d66d31 100644 --- a/_config.yml +++ b/_config.yml @@ -82,7 +82,7 @@ version: 1 ## Docs: https://hexo.io/docs/deployment.html deploy: type: git - message: "feat(blog): change branch" + message: "feat(blog): modify article time" branch: gh-pages repo: https://github.com/hankliu62/hankliu62.github.com.git diff --git a/file_list_to_commit.txt b/file_list_to_commit.txt new file mode 100644 index 0000000..9a313d4 --- /dev/null +++ b/file_list_to_commit.txt @@ -0,0 +1,36 @@ +.bin/constant.js +.bin/libs/issues.js +.bin/libs/labels.js +.bin/md2github-issues.js +.github/workflows/gh-page.yml +.gitignore +_config.yml +package-lock.json +package.json +source/_posts/cache-loader.md +source/_posts/commomly-css.md +source/_posts/commonly-shell-command.md +source/_posts/git-clone-failure.md +source/_posts/github-pages-env.md +source/_posts/github-pages.md +source/_posts/githut-emoji.md +source/_posts/hexo-github-blog-guide.md +source/_posts/interview-questions.md +source/_posts/javascript-common-function.md +source/_posts/javascript-module.md +source/_posts/mobile-debugging-tools.md +source/_posts/offscreen-canvas.md +source/_posts/security-anti-reptile.md +source/_posts/security-csrf.md +source/_posts/security-sql-injection.md +source/_posts/security-x-content-types-options.md +source/_posts/security-xss.md +source/_posts/service-worker.md +source/_posts/thread-loader.md +source/_posts/ubuntu-netstat.md +source/_posts/vitepress-blog-guide.md +source/_posts/web-mobile-tools-weinre.md +source/_posts/webpack-plugin-mechanism.md +source/_posts/webwork-postmessage.md +themes/hexo-theme-paperbox +yarn.lock diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a823975 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,486 @@ +{ + "name": "hexo-site", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@octokit/app": { + "version": "14.0.2", + "resolved": "https://registry.npmmirror.com/@octokit/app/-/app-14.0.2.tgz", + "integrity": "sha512-NCSCktSx+XmjuSUVn2dLfqQ9WIYePGP95SDJs4I9cn/0ZkeXcPkaoCLl64Us3dRKL2ozC7hArwze5Eu+/qt1tg==", + "requires": { + "@octokit/auth-app": "6.0.4", + "@octokit/auth-unauthenticated": "5.0.1", + "@octokit/core": "5.1.0", + "@octokit/oauth-app": "6.1.0", + "@octokit/plugin-paginate-rest": "9.2.1", + "@octokit/types": "12.6.0", + "@octokit/webhooks": "12.2.0" + } + }, + "@octokit/auth-app": { + "version": "6.0.4", + "resolved": "https://registry.npmmirror.com/@octokit/auth-app/-/auth-app-6.0.4.tgz", + "integrity": "sha512-TPmJYgd05ok3nzHj7Y6we/V7Ez1wU3ztLFW3zo/afgYFtqYZg0W7zb6Kp5ag6E85r8nCE1JfS6YZoZusa14o9g==", + "requires": { + "@octokit/auth-oauth-app": "7.0.1", + "@octokit/auth-oauth-user": "4.0.1", + "@octokit/request": "8.2.0", + "@octokit/request-error": "5.0.1", + "@octokit/types": "12.6.0", + "deprecation": "2.3.1", + "lru-cache": "10.2.0", + "universal-github-app-jwt": "1.1.2", + "universal-user-agent": "6.0.1" + } + }, + "@octokit/auth-oauth-app": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/@octokit/auth-oauth-app/-/auth-oauth-app-7.0.1.tgz", + "integrity": "sha512-RE0KK0DCjCHXHlQBoubwlLijXEKfhMhKm9gO56xYvFmP1QTMb+vvwRPmQLLx0V+5AvV9N9I3lr1WyTzwL3rMDg==", + "requires": { + "@octokit/auth-oauth-device": "6.0.1", + "@octokit/auth-oauth-user": "4.0.1", + "@octokit/request": "8.2.0", + "@octokit/types": "12.6.0", + "@types/btoa-lite": "1.0.2", + "btoa-lite": "1.0.0", + "universal-user-agent": "6.0.1" + } + }, + "@octokit/auth-oauth-device": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/@octokit/auth-oauth-device/-/auth-oauth-device-6.0.1.tgz", + "integrity": "sha512-yxU0rkL65QkjbqQedgVx3gmW7YM5fF+r5uaSj9tM/cQGVqloXcqP2xK90eTyYvl29arFVCW8Vz4H/t47mL0ELw==", + "requires": { + "@octokit/oauth-methods": "4.0.1", + "@octokit/request": "8.2.0", + "@octokit/types": "12.6.0", + "universal-user-agent": "6.0.1" + } + }, + "@octokit/auth-oauth-user": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@octokit/auth-oauth-user/-/auth-oauth-user-4.0.1.tgz", + "integrity": "sha512-N94wWW09d0hleCnrO5wt5MxekatqEJ4zf+1vSe8MKMrhZ7gAXKFOKrDEZW2INltvBWJCyDUELgGRv8gfErH1Iw==", + "requires": { + "@octokit/auth-oauth-device": "6.0.1", + "@octokit/oauth-methods": "4.0.1", + "@octokit/request": "8.2.0", + "@octokit/types": "12.6.0", + "btoa-lite": "1.0.0", + "universal-user-agent": "6.0.1" + } + }, + "@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==" + }, + "@octokit/auth-unauthenticated": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", + "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", + "requires": { + "@octokit/request-error": "5.0.1", + "@octokit/types": "12.6.0" + } + }, + "@octokit/core": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@octokit/core/-/core-5.1.0.tgz", + "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", + "requires": { + "@octokit/auth-token": "4.0.0", + "@octokit/graphql": "7.0.2", + "@octokit/request": "8.2.0", + "@octokit/request-error": "5.0.1", + "@octokit/types": "12.6.0", + "before-after-hook": "2.2.3", + "universal-user-agent": "6.0.1" + } + }, + "@octokit/endpoint": { + "version": "9.0.4", + "resolved": "https://registry.npmmirror.com/@octokit/endpoint/-/endpoint-9.0.4.tgz", + "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", + "requires": { + "@octokit/types": "12.6.0", + "universal-user-agent": "6.0.1" + } + }, + "@octokit/graphql": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/@octokit/graphql/-/graphql-7.0.2.tgz", + "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", + "requires": { + "@octokit/request": "8.2.0", + "@octokit/types": "12.6.0", + "universal-user-agent": "6.0.1" + } + }, + "@octokit/oauth-app": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/@octokit/oauth-app/-/oauth-app-6.1.0.tgz", + "integrity": "sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g==", + "requires": { + "@octokit/auth-oauth-app": "7.0.1", + "@octokit/auth-oauth-user": "4.0.1", + "@octokit/auth-unauthenticated": "5.0.1", + "@octokit/core": "5.1.0", + "@octokit/oauth-authorization-url": "6.0.2", + "@octokit/oauth-methods": "4.0.1", + "@types/aws-lambda": "8.10.136", + "universal-user-agent": "6.0.1" + } + }, + "@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==" + }, + "@octokit/oauth-methods": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@octokit/oauth-methods/-/oauth-methods-4.0.1.tgz", + "integrity": "sha512-1NdTGCoBHyD6J0n2WGXg9+yDLZrRNZ0moTEex/LSPr49m530WNKcCfXDghofYptr3st3eTii+EHoG5k/o+vbtw==", + "requires": { + "@octokit/oauth-authorization-url": "6.0.2", + "@octokit/request": "8.2.0", + "@octokit/request-error": "5.0.1", + "@octokit/types": "12.6.0", + "btoa-lite": "1.0.0" + } + }, + "@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmmirror.com/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" + }, + "@octokit/plugin-paginate-graphql": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-4.0.1.tgz", + "integrity": "sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A==" + }, + "@octokit/plugin-paginate-rest": { + "version": "9.2.1", + "resolved": "https://registry.npmmirror.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz", + "integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==", + "requires": { + "@octokit/types": "12.6.0" + } + }, + "@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmmirror.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "requires": { + "@octokit/types": "12.6.0" + } + }, + "@octokit/plugin-retry": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", + "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "requires": { + "@octokit/request-error": "5.0.1", + "@octokit/types": "12.6.0", + "bottleneck": "2.19.5" + } + }, + "@octokit/plugin-throttling": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", + "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", + "requires": { + "@octokit/types": "12.6.0", + "bottleneck": "2.19.5" + } + }, + "@octokit/request": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/@octokit/request/-/request-8.2.0.tgz", + "integrity": "sha512-exPif6x5uwLqv1N1irkLG1zZNJkOtj8bZxuVHd71U5Ftuxf2wGNvAJyNBcPbPC+EBzwYEbBDdSFb8EPcjpYxPQ==", + "requires": { + "@octokit/endpoint": "9.0.4", + "@octokit/request-error": "5.0.1", + "@octokit/types": "12.6.0", + "universal-user-agent": "6.0.1" + } + }, + "@octokit/request-error": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/@octokit/request-error/-/request-error-5.0.1.tgz", + "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", + "requires": { + "@octokit/types": "12.6.0", + "deprecation": "2.3.1", + "once": "1.4.0" + } + }, + "@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmmirror.com/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "requires": { + "@octokit/openapi-types": "20.0.0" + } + }, + "@octokit/webhooks": { + "version": "12.2.0", + "resolved": "https://registry.npmmirror.com/@octokit/webhooks/-/webhooks-12.2.0.tgz", + "integrity": "sha512-CyuLJ0/P7bKZ+kIYw+fnkeVdhUzNuDKgNSI7pU/m7Nod0T7kP+s4s2f0pNmG9HL8/RZN1S0ZWTDll3VTMrFLAw==", + "requires": { + "@octokit/request-error": "5.0.1", + "@octokit/webhooks-methods": "4.1.0", + "@octokit/webhooks-types": "7.4.0", + "aggregate-error": "3.1.0" + } + }, + "@octokit/webhooks-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/@octokit/webhooks-methods/-/webhooks-methods-4.1.0.tgz", + "integrity": "sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==" + }, + "@octokit/webhooks-types": { + "version": "7.4.0", + "resolved": "https://registry.npmmirror.com/@octokit/webhooks-types/-/webhooks-types-7.4.0.tgz", + "integrity": "sha512-FE2V+QZ2UYlh+9wWd5BPLNXG+J/XUD/PPq0ovS+nCcGX4+3qVbi3jYOmCTW48hg9SBBLtInx9+o7fFt4H5iP0Q==" + }, + "@types/aws-lambda": { + "version": "8.10.136", + "resolved": "https://registry.npmmirror.com/@types/aws-lambda/-/aws-lambda-8.10.136.tgz", + "integrity": "sha512-cmmgqxdVGhxYK9lZMYYXYRJk6twBo53ivtXjIUEFZxfxe4TkZTZBK3RRWrY2HjJcUIix0mdifn15yjOAat5lTA==" + }, + "@types/btoa-lite": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", + "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==" + }, + "@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmmirror.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "requires": { + "@types/node": "20.12.2" + } + }, + "@types/node": { + "version": "20.12.2", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.12.2.tgz", + "integrity": "sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==", + "requires": { + "undici-types": "5.26.5" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "requires": { + "clean-stack": "2.2.0", + "indent-string": "4.0.0" + } + }, + "before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmmirror.com/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, + "btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==" + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "requires": { + "jws": "3.2.2", + "lodash.includes": "4.3.0", + "lodash.isboolean": "3.0.3", + "lodash.isinteger": "4.0.4", + "lodash.isnumber": "3.0.3", + "lodash.isplainobject": "4.0.6", + "lodash.isstring": "4.0.1", + "lodash.once": "4.1.1", + "ms": "2.1.3", + "semver": "7.6.0" + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "5.2.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "1.4.1", + "safe-buffer": "5.2.1" + } + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "octokit": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/octokit/-/octokit-3.1.2.tgz", + "integrity": "sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==", + "requires": { + "@octokit/app": "14.0.2", + "@octokit/core": "5.1.0", + "@octokit/oauth-app": "6.1.0", + "@octokit/plugin-paginate-graphql": "4.0.1", + "@octokit/plugin-paginate-rest": "9.2.1", + "@octokit/plugin-rest-endpoint-methods": "10.4.1", + "@octokit/plugin-retry": "6.0.1", + "@octokit/plugin-throttling": "8.2.0", + "@octokit/request-error": "5.0.1", + "@octokit/types": "12.6.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1.0.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "requires": { + "lru-cache": "6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "4.0.0" + } + } + } + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "universal-github-app-jwt": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/universal-github-app-jwt/-/universal-github-app-jwt-1.1.2.tgz", + "integrity": "sha512-t1iB2FmLFE+yyJY9+3wMx0ejB+MQpEVkH0gQv7dR6FZyltyq+ZZO0uDpbopxhrZ3SLEO4dCEkIujOMldEQ2iOA==", + "requires": { + "@types/jsonwebtoken": "9.0.6", + "jsonwebtoken": "9.0.2" + } + }, + "universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/package.json b/package.json index 963386f..f83acfd 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,30 @@ { - "name": "hexo-site", + "name": "hankliu-blog", "version": "0.0.0", "private": true, "scripts": { "build": "rm -rf public && hexo g", - "dev": "rm -rf public && hexo s", - "deploy": "rm -rf public && hexo g && hexo d" + "dev": "hexo s", + "deploy": "rm -rf public && hexo g && hexo d", + "sync": "node .bin/md2github-issues" }, "hexo": { - "version": "3.9.0" + "version": "7.1.1" }, "dependencies": { - "hexo": "^3.2.0", - "hexo-deployer-git": "^0.3.1", - "hexo-generator-archive": "^0.1.4", - "hexo-generator-category": "^0.1.3", - "hexo-generator-index": "^0.2.0", - "hexo-generator-tag": "^0.2.0", - "hexo-renderer-ejs": "^0.2.0", - "hexo-renderer-marked": "^0.2.10", - "hexo-renderer-stylus": "^0.3.1", - "hexo-server": "^0.2.0" + "hexo": "^7.1.1", + "hexo-deployer-git": "^4.0.0", + "hexo-generator-archive": "^2.0.0", + "hexo-generator-category": "^2.0.0", + "hexo-generator-index": "^3.0.0", + "hexo-generator-tag": "^2.0.0", + "hexo-renderer-ejs": "^2.0.0", + "hexo-renderer-marked": "^6.2.0", + "hexo-renderer-stylus": "^3.0.1", + "hexo-server": "^3.0.0", + "octokit": "^3.1.2" }, "engines": { - "node": "8" + "node": "18" } } \ No newline at end of file diff --git a/source/_posts/cache-loader.md b/source/_posts/cache-loader.md new file mode 100644 index 0000000..6b77d70 --- /dev/null +++ b/source/_posts/cache-loader.md @@ -0,0 +1,155 @@ +--- +title: cache-loader +date: 2023-01-10 10:10:20 +tag: [webpack, loader, translate] +--- + +## cache-loader + +`cache-loader` 允许在磁盘(默认)或数据库中缓存后续加载器的结果。 + +### 开始使用 + +要开始使用,您需要安装 `cache-loader`: + +``` console +npm install --save-dev cache-loader +``` + +将此加载器添加到其他(耗时的)加载器前面,以便在磁盘上缓存结果。 + +webpack.config.js + +``` js +module.exports = { + module: { + rules: [ + { + test: /\.ext$/, + use: ['cache-loader', ...loaders], + include: path.resolve('src'), + }, + ], + }, +}; +``` + +> ⚠️ 请注意,保存和读取缓存文件会有一定的开销,因此只将此加载器用于缓存耗时的加载器。 + + +### 选项 + +| 名称 | 类型 | n 默认值 | 描述 | +| :-------------------: | :----------------------------------------------: | :-----------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`cacheContext`** | `{String}` | `undefined` | 允许您覆盖默认的缓存上下文,以便相对于某个路径生成缓存。默认情况下,它将使用绝对路径。 | +| **`cacheKey`** | `{Function(options, request) -> {String}}` | `undefined` | 允许您覆盖默认的缓存键生成器。 | +| **`cacheDirectory`** | `{String}` | `findCacheDir({ name: 'cache-loader' }) or os.tmpdir()` | 提供一个缓存目录,其中应存储缓存项(用于默认的读写实现)。 | +| **`cacheIdentifier`** | `{String}` | `cache-loader:{version} {process.env.NODE_ENV}` | 提供一个无效标识符,用于生成哈希值。您可以将其用于加载器的额外依赖项(用于默认的读写实现) | +| **`compare`** | `{Function(stats, dep) -> {Boolean}}` | `undefined` | 允许您覆盖缓存的依赖项与正在读取的依赖项之间的默认比较函数。返回 `true` 以使用缓存的资源。 | +| **`precision`** | `{Number}` | `0` | 在将这些参数传递给比较函数之前,使用此毫秒数对`stats`和`dep`的`mtime`进行四舍五入。 | +| **`read`** | `{Function(cacheKey, callback) -> {void}}` | `undefined` | 允许您覆盖从文件中读取默认缓存数据的函数。 | +| **`readOnly`** | `{Boolean}` | `false` | 允许您覆盖默认值,并使缓存只读(在某些环境中很有用,您不希望更新缓存,而只是从中读取)。 | +| **`write`** | `{Function(cacheKey, data, callback) -> {void}}` | `undefined` | 允许您覆盖将默认缓存数据写入文件(例如Redis,memcached)的函数。 | + +### 示例 + +#### 基础 + +**webpack.config.js** + +``` js +module.exports = { + module: { + rules: [ + { + test: /\.js$/, + use: ['cache-loader', 'babel-loader'], + include: path.resolve('src'), + }, + ], + }, +}; +``` + +在基础示例中,`cache-loader` 被用于缓存对 `.js` 文件的处理结果。当文件内容发生变化时,缓存将被无效化,并且会重新处理文件。这种缓存机制可以显著提高构建速度,特别是当处理大量未更改的文件时。 + +#### 数据库集成 + +**webpack.config.js** + +``` js +// 或者使用其他数据库客户端 - memcached, mongodb, ... +const redis = require('redis'); +const crypto = require('crypto'); + +// ... +// 连接到客户端 +// ... + +const BUILD_CACHE_TIMEOUT = 24 * 3600; // 1天 + +function digest(str) { + return crypto + .createHash('md5') + .update(str) + .digest('hex'); +} + +// 生成自定义缓存键 +function cacheKey(options, request) { + return `build:cache:${digest(request)}`; +} + +// 从数据库读取数据并解析 +function read(key, callback) { + client.get(key, (err, result) => { + if (err) { + return callback(err); + } + + if (!result) { + return callback(new Error(`Key ${key} not found`)); + } + + try { + let data = JSON.parse(result); + callback(null, data); + } catch (e) { + callback(e); + } + }); +} + +// 在cacheKey下将数据写入数据库 +function write(key, data, callback) { + client.set(key, JSON.stringify(data), 'EX', BUILD_CACHE_TIMEOUT, callback); +} + +module.exports = { + module: { + rules: [ + { + test: /\.js$/, + use: [ + { + loader: 'cache-loader', + options: { + cacheKey, + read, + write, + }, + }, + 'babel-loader', + ], + include: path.resolve('src'), + }, + ], + }, +}; +``` + +`ache-loader` 被配置为使用自定义的读取和写入函数,这些函数将数据存储在 `Redis` 数据库中。通过 `cacheKey` 函数,可以为每个请求生成一个唯一的缓存键。`read` 函数从数据库中读取缓存数据,并将其解析为 `JavaScript` 对象。`write` 函数将处理后的数据写入数据库,并设置了一个过期时间(在这种情况下为 `1` 天)。 + +通过这种方式,缓存数据可以在构建过程中跨多个运行实例共享,并且可以持久化存储,即使 `webpack` 构建进程重启也不会丢失。这可以进一步提高构建速度,特别是在大型项目中,并且可以在多个构建任务之间共享缓存数据。 + +翻译: [cache-loader](https://www.npmjs.com/package/cache-loader) \ No newline at end of file diff --git a/source/_posts/commomly-css.md b/source/_posts/commomly-css.md new file mode 100644 index 0000000..82738c9 --- /dev/null +++ b/source/_posts/commomly-css.md @@ -0,0 +1,1133 @@ +--- +title: 常用的CSS技巧 +date: 2019-10-12 18:20:12 +tag: [css] +--- + + +01. 网站平滑滚动 + +在``元素中添加scroll-behavior: smooth,以实现整个页面的平滑滚动。 + +``` css +html { + scroll-behavior: smooth; +} +``` + +02. 链接的属性选择器 + +此选择器的目标是具有以“https”开头的 href 属性的链接。 + +``` css +a[href^="https"] { + color: blue; +} +``` + +03. 〜合并兄弟姐妹 + +选择 `

` 后面的所有兄弟元素 `

` 元素。 + +``` css +h2 ~ p { + color: blue; +} +``` + +04. :not() 伪类 + +该选择器将样式应用于不具有“特殊”类的列表项。 + +``` css +li:not(.special) { + font-stlye: italic; +} +``` + +05. 用于响应式排版的视口单位 + +使用视口单位(vw、vh、vmin、vmax)可以使字体大小响应视口大小。 + +``` css +h1 { + font-size: 5vw; +} +``` + +06. `:empty` 表示空元素 + +此选择器定位空的 `

` 元素并隐藏它们。 + +``` css +p:empty { + display: none; +} +``` + +07. 自定义属性(变量) + +可以定义和使用自定义属性,以更轻松地设置主题和维护。 + +``` css +:root { + --main-color: #3498db; +} + +h1 { + color: var(--main-color); +} +``` + +08. 图像控制的Object-fit属性 + +**object-fit** 控制如何调整替换元素(如 ``)的内容大小。 + +``` css +img { + width: 100px; + height: 100px; + object-fit: cover; +} +``` + + + +09. 简化布局的网格 + +CSS 网格提供了一种以更简单的方式创建布局的强大方法。 + +``` css +.container { + display: grid; + grid-template-columns: 1fr 2fr 1fr; +} +``` + +10. `:focus-in` 伪类 + +如果该元素包含任何具有 `:focus` 的子元素,则 `:focus-within` 会选择该元素。 + +``` css +form:focus-within { + box-shadow: 0 0 5px rgba(0, 0, 0, 0, 0.2); +} +``` + +11. 使用 Flexbox 垂直居中 + +使用 Flexbox 轻松将内容在容器内水平和垂直居中。 + +``` css +.container { + display: flex; + align-items: center; + justify-content: center; +} +``` +12. 自定义选择的突出显示颜色 + +自定义在网页上选择文本时的突出显示颜色。 + +``` css +::selection { + background-color: #ffcc00; + color: #333; +} +``` + +13. 占位符文本样式 + +设置输入字段内占位符文本的样式。 + +``` css +::placeholder { + color: #999; + font-style: italic; +} +``` + +14. 渐变边框 + +使用`background-clip`属性创建渐变边框。 + +``` css +.element { + border: 2px solid transparent; + background-clip: padding-box; + background-image: linear-gradient(to right, red, blue); +} +``` + +15. vw 可变字体大小 + +根据视口宽度调整字体大小,从而实现更具响应性的排版。 + +``` css +body { + font-size: calc(16px + 1vw); +} +``` + +16. 彩色元素的圆锥渐变 + +使用圆锥渐变创建色彩缤纷的动态背景。 + +``` css +.element { + background: conic-gradient(#ff5733, #33ff57, #5733ff); +} +``` + +17. 响应式文本的 `Clamp()` 函数 + +使用`clamp()`函数设置字体大小的范围,确保在不同屏幕尺寸上的可读性。 + +``` css +.text { + font-size: clamp(16px, 4vw, 24px); +} +``` + +18. 通过字体显示交换实现高效字体加载 + +使用字体显示:交换; 属性可通过在加载自定义字体时显示后备字体来提高 Web 字体的性能。 + +``` css +@font-face { + font-family: 'YourFont'; + src: url('your-font.woff2') format('woff2'); + font-display: swap; +} +``` + +19. 自定义滚动捕捉点 + +实施自定义滚动捕捉点以获得更流畅的滚动体验,对于图像库或滑块尤其有用。 + +``` css +.scroll-container { + scroll-snap-type: y mandatory; +} + +.scroll-item { + scroll-snap-align: start; +} +``` + +20. 具有字体变化设置的可变字体样式 + +利用可变字体和 `font-variation-settings` 属性对字体粗细、样式和其他变体进行微调控制。 + +``` css +.text { + font-family: 'YourVariableFont', sans-serif; + font-variation-settings: 'wght' 500, 'ital' 1; +} +``` + +21. 自定义下划线 + +使用 `border-bottom` 和 `text-decoration` 的组合自定义链接上下划线的样式。 + +``` css +a { + text-decoration: none; + border-bottom: 1px solid #3498db; +} +``` + +22. 隐藏的辅助文本 + +使用类 `sr-only` 在视觉上隐藏元素,但让屏幕阅读器可以访问它们。 + +``` css +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +``` + +23. 纵横比框 + +使用填充技巧来保持图像或视频等元素的宽高比。 + +``` css +.aspect-ratio-box { + position: relative; + width: 100%; + padding-bottom: 75%; /* Adjust as needed */ +} + +.aspect-ratio-box > iframe { + position: absolute; + width: 100%; + height: 100%; +} +``` + +24. 选择偶数和奇数元素 + +使用 `:nth-child` 伪类设置替代元素的样式。 + +``` css +li:nth-child(even) { + background-color: #f2f2f2; +} + +li:nth-child(odd) { + background-color: #e6e6e6; +} +``` + +25. CSS计数器 + +使用计数器重置和计数器增量属性在列表中创建自动编号。 + +``` css +ol { + counter-reset: item; +} + +li { + counter-increment: item; +} + +li::before { + content: counter(item) ". "; +} +``` + +26. 多个背景图像 + +将多个背景图像应用于具有不同属性的元素。 + +``` css +.bg { + background-image: url('image1.jpg'), url('image2.jpg'); + background-position: top left, bottom right; + background-repeat: no-repeat, repeat-x; +} +``` + +27. 连字符让文本更流畅 + +通过允许使用 `hyphens` 属性自动连字符来提高文本可读性。 + +``` css +p { + hyphens: auto; +} +``` + +28. 动态样式的CSS变量 + +利用 CSS 变量创建动态且可重用的样式。 + +``` css +:root { + --main-color: #3498db; +} + +.element { + color: var(--main-color); +} +``` + +29. 键盘导航的焦点样式 + +改进焦点样式以获得更好的键盘导航和可访问性。 + +``` css +:focus { + outline: 2px solid #27ae60; +} +``` + +30. 平滑渐变过渡 + +对渐变背景应用平滑过渡以获得精美效果。 + +``` css +.gradient-box { + background: linear-gradient(45deg, #3498db, #2ecc71); + transition: background 0.5s ease; +} + +.gradient-box:hover { + background: linear-gradient(45deg, #e74c3c, #f39c12); +} +``` + +31. 文字描边效果 + +为文本添加笔划(轮廓)以获得独特的视觉效果。 + +``` css +h1 { + color: #3498db; + -webkit-text-stroke: 2px #2c3e50; +} +``` + +32. 纯CSS汉堡菜单 + +无需 JavaScript 创建一个简单的汉堡菜单。 + +``` css +.menu-toggle { + display: none; +} + +.menu-toggle:checked + nav { + display: block; +} +/* Add styles for the hamburger icon and menu here */ +``` + +33. CSS `:is()` 选择器 + +使用 `:is()` 伪类简化复杂的选择器。 + +``` css +:is(h1, h2, h3) { + color: blue; +} +``` + +34. CSS变量的计算 + +在动态样式的 CSS 变量中执行计算。 + +``` css +:root { + --base-size: 16px; + --header-size: calc(var(--base-size) * 2); +} + +h1 { + font-size: var(--header-size); +} +``` + +35. 内容的 `attr()` 函数 + +使用 `attr()` 函数检索和显示属性值。 + +``` css +div::before { + content: attr(data-custom-content); +} +``` + +36. CSS 屏蔽 + +对图像应用遮罩以获得创意效果。 + +``` css +.masked-image { + mask: url(mask.svg); + mask-size: cover; +} +``` + +37. 混合模式 + +尝试混合模式以获得有趣的色彩效果。 + +``` css +.blend-mode { + background: url(image.jpg); + mix-blend-mode: screen; +} +``` + +38. 纵横比属性 + +使用宽高比属性简化宽高比框的创建。 + +``` css +.aspect-ratio-box { + aspect-ratio: 16/9; +} +``` + +39. 用于文本换行的 `shape-outside` + +使用 `shape-outside` 属性使文本环绕指定形状,从而实现更动态的布局。 + +``` css +.shape-wrap { + float: left; + width: 150px; + height: 150px; + shape-outside: circle(50%); +} +``` + +40. ch 单位用于一致的尺寸 + +ch 单位表示所选字体中字符“0”的宽度。 它对于创建一致且响应式的布局很有用。 + +``` css +h1 { + font-size: 2ch; +} +``` + +41. `::marker`伪元素 + +使用 `::marker` 伪元素设置列表项标记的样式。 + +``` css +li::marker { + color: blue; +} +``` + +42. 背景的 `element()` 函数 + +使用 `element()` 函数动态引用元素作为背景。 + +``` css +.background { + background: element(#targetElement); +} +``` + +43. 使用 Flexbox 的粘性页脚 + +使用 Flexbox 创建粘性页脚布局。 + +``` css +body { + display: flex; + flex-direction: column; + min-height: 100vh; +} +main { + flex: 1; +} +``` + +44. `scroll-padding`实现平滑滚动 + +通过调整滚动填充来改进滚动行为。 + +``` css +html { + scroll-padding: 20px; +} +``` + +45. 交互式高亮效果 + +使用 CSS 变量创建交互式突出显示效果。 + +``` css +.highlight { + --highlight-color: #e74c3c; + background-image: linear-gradient(transparent 0%, var(--highlight-color) 0%); + background-size: 100% 200%; + transition: background-position 0.3s; +} + +.highlight:hover { + background-position: 0 100%; +} +``` + +46. 自定义单选按钮和复选框 + +设置不带图像的单选按钮和复选框的样式。 + +``` css +input[type="radio"] { + appearance: none; + -webkit-appearance: none; + border-radius: 50%; + width: 16px; + height: 16px; + border: 2px solid #3498db; +} + +input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + width: 16px; + height: 16px; + border: 2px solid #e74c3c; +} +``` + +47. 调整文本区域的属性大小 + +使用 `resize` 属性控制文本区域的大小调整行为。 + +``` css +textarea { + resize: vertical; +} +``` + +48. 文字渐变 + +使用 `background-clip` 和 `text-fill-color` 属性为文本创建渐变效果。 + +``` css +.gradient-text { + background-image: linear-gradient(45deg, #3498db, #2ecc71); + background-clip: text; + color: transparent; +} +``` + +49. 长单词的断字属性 + +使用 `word-break` 属性可以控制不带空格的单词或字符串的长度。 + +``` css +.long-words { + word-break: break-all; +} +``` + +50. 可变字体的 `font-variation-settings` + +使用 `font-variation-settings` 属性微调可变字体样式。 + +``` css +.custom-font { + font-family: 'MyVariableFont'; + font-variation-settings: 'wght' 600, 'ital' 1; +} +``` + +51. 用于创意叠加的混合混合模式 + +使用 `mix-blend-mode` 将混合模式应用于元素,在叠加元素时创建有趣的视觉效果。 + +``` css +.overlay { + mix-blend-mode: overlay; +} +``` + +52. 设计破损图像的样式 + +使用 `:broken` 伪类将样式应用于损坏的图像。 + +``` css +img:broken { + filter: grayscale(100%); +} +``` + +53. CSS 形状 + +使用 `shape-outside` 属性创建有趣的 CSS 形状设计。 + +``` css +.shape { + shape-outside: circle(50%); +} +``` + +54. 子串匹配的属性选择器 + +使用属性选择器和 *= 运算符进行子字符串匹配。 + +``` css +[data-attribute*="value"] { + /* Styles */ +} +``` + +55. 模糊背景的背景滤镜 + +使用背景滤镜对背景应用模糊效果,以获得磨砂玻璃效果。 + +``` css +.element { + backdrop-filter: blur(10px); +} +``` + +56. CSS环境变量 + +使用 `env()` 函数访问 CSS 中的环境变量。 + +``` css +.element { + margin-top: env(safe-area-inset-top); +} +``` + +57. CSS属性计数器 + +使用 `:nth-child` 选择器计算特定属性值的出现次数。 + +``` css +[data-category="example"]:nth-child(3) { + /* Styles for the third occurrence */ +} +``` + +58. 用于文本换行的 CSS 形状 + +将 `shape-outside` 与 `Polygon()` 函数结合使用,可实现围绕不规则形状的精确文本环绕。 + +``` css +.text-wrap { + shape-outside: polygon(0 0, 100% 0, 100% 100%); +} +``` + +59. 自定义光标样式 + +使用光标属性更改光标样式。 + +``` css +.custom-cursor { + cursor: pointer; +} +``` + +60. 用于透明颜色的 HSLA + +使用透明颜色的 HSLA 值,提供对 Alpha 通道的更多控制。 + +``` css +.transparent-bg { + background-color: hsla(120, 100%, 50%, 0.5); +} +``` + +61. 垂直文本的文本方向 + +使用文本方向属性垂直旋转文本。 + +``` css +.vertical-text { + text-orientation: upright; +} +``` + +62. 小型大写字母的字体变体 + +使用 `font-variant` 属性将小型大写字母应用于文本。 + +``` css +.small-caps { + font-variant: small-caps; +} +``` + +63. 背景分割的 `box-decoration-break` + +使用 `box-decoration-break` 控制跨多行的元素的背景。 + +``` css +.split-background { + box-decoration-break: clone; +} +``` + +64. `:focus-visible` 用于特定焦点样式 + +仅当元素处于焦点且焦点不是通过鼠标单击提供时才应用样式。 + +``` css +input:focus-visible { + outline: 2px solid blue; +} +``` + +65. 最佳字体渲染的文本渲染 + +使用文本渲染属性改进文本渲染。 + +``` css +.optimized-text { + text-rendering: optimizeLegibility; +} +``` + +66. 首字母大写字母 + +使用 `::first-letter` 设置块级元素的第一个字母的样式。 + +``` css +p::first-letter { + font-size: 2em; +} +``` + +67. `overscroll-behavior` 滚动超调 + +控制用户滚动超过滚动容器边界时的行为。 + +``` css +.scroll-container { + overscroll-behavior: contain; +} +``` + +68. 垂直布局的写作模式 + +使用 `writing-mode` 属性创建垂直布局。 + +``` css +.vertical-layout { + writing-mode: vertical-rl; +} +``` + +69. `::cue` 用于设置 HTML5 标题样式 + +使用 `::cue` 伪元素设置 HTML5 标题文本的样式。 + +``` css +::cue { + color: blue; +} +``` + +70. 用于截断多行文本的`line-clamp` + +使用 `line-clamp` 属性限制元素内显示的行数。 + +``` css +.truncated-text { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} +``` + +71. `scroll-snap-align` + +`scroll-snap-align` 属性控制滚动容器内滚动捕捉点的对齐方式,确保精确控制滚动行为并增强用户体验。 + +``` css +.container { + scroll-snap-type: x mandatory; +} +.item { + scroll-snap-align: center; +} +``` + +72. `overscroll-behavior` + +`overscroll-behavior` 使您能够定义浏览器应如何处理滚动过度,防止不必要的滚动效果并改善整体滚动体验。 + +``` css +.scrollable { + overscroll-behavior: contain; +} +``` + +73. 字体字距调整 + +字体字距调整允许微调字符间距,通过调整文本元素内字符之间的间距来确保最佳的可读性。 + +``` css +p { + font-kerning: auto; +} +``` + +74. 形状边缘 + +当与 CSS 形状结合使用时,形状边距指定浮动元素形状周围的边距,从而可以精确控制文本换行和布局。 + +``` css +.shape { + shape-margin: 20px; +} +``` + +75. 滚动边距 + +滚动边距设置滚动容器边缘和滚动内容开始之间的边距,通过提供滚动缓冲空间来增强用户体验。 + +``` css +.container { + scroll-margin-top: 100px; +} +``` + +76. 选项卡大小 + +滚动边距设置滚动容器边缘和滚动内容开始之间的边距,通过提供滚动缓冲空间来增强用户体验。 + +``` css +pre { + tab-size: 4; +} +``` + +77. 文本最后对齐 + +`text-align-last` 确定块元素中最后一行文本的对齐方式,从而提供对多行块中文本对齐的精确控制。 + +``` css +p { + text-align-last: justify; +} +``` + +78. 文本对齐 + +此属性控制文本对齐行为,指定是否应使用字间或字符间间距进行文本对齐。 + +``` css +p { + text-align: justify; + text-justify: inter-word; +} +``` + +79. 列填充 + +列填充指示内容如何跨多列布局分布,允许跨列顺序或平衡分布内容。 + +``` css +.container { + column-count: 3; + column-fill: auto; +} +``` + +80. 轮廓偏移 + +轮廓偏移调整轮廓和元素边缘之间的空间,从而可以更好地控制轮廓的外观而不影响布局。 + +``` css +button { + outline: 2px solid blue; + outline-offset: 4px; +} +``` + +81. 字体变体数字 + +此属性允许对数字排版渲染进行细粒度控制,从而启用诸如衬里和旧式数字、分数和序数指示符等功能。 + +``` css +p { + font-variant-numeric: lining-nums; +} +``` + +82. 字体光学尺寸 + +启用或禁用字体光学尺寸调整以调整字符的间距和比例,以改善各种字体大小的视觉和谐。 + +``` css +p { + font-optical-sizing: auto; +} +``` + +83. 文本装饰厚度 + +控制文本装饰的粗细,例如下划线、上划线和穿线,以进行精确定制。 + +``` css +p { + text-decoration-thickness: 2px; +} +``` + +84. 文本下划线偏移 + +调整下划线相对于文本基线的位置,以改进排版细化。 + +``` css +p { + text-underline-offset: 3px; +} +``` + +85. 滚动填充块 + +定义在可滚动块容器周围添加的填充空间,以确保内容在滚动期间保持可见和可访问。 + +``` css +.container { + scroll-padding-block: 20px; +} +``` + +86. 内联滚动填充 + +设置在可滚动内联容器周围添加的填充空间,以增强滚动交互期间的用户体验。 + +``` css +.container { + scroll-padding-inline: 10px; +} +``` + +87. 换行 + +指定单词或字符内的换行方式,以控制换行行为,从而改进文本布局和可读性。 + +``` css +p { + line-break: strict; +} +``` + +88. 盒子装饰打破 + +控制跨分段元素的边框和填充的渲染,以确保跨多行或多列分割的元素的样式一致。 + +``` css +.element { + box-decoration-break: clone; +} +``` + +89. 首字母 + +将块元素的第一个字母或首字母字符设计为装饰性首字下沉或其他视觉上突出的首字母字符。 + +``` css +p::first-letter { + font-size: 2em; + float: left; +} +``` + +90. 图像渲染 + +调整图像的渲染质量和性能,优化各种场景的图像显示。 + +``` css +img { + image-rendering: pixelated; +} +``` + +91. 字体功能设置 + +`font-feature-settings` 允许您启用或禁用字体中的 OpenType 功能,例如,连字、字距调整和样式替代。 + +``` css +p { + font-feature-settings: "liga" on; +} +``` + +92. 文本导向 + +此属性控制文本在其包含框中的方向,从而启用垂直或横向文本布局。 + +``` css +.vertical-text { + text-orientation: sideways; +} +``` + +93. 文本装饰-跳过墨迹 + +`text-decoration-skip-ink` 控制文本装饰是否应跳过上升部分和下降部分,从而改善下划线和穿线的外观。 + +``` css +p { + text-decoration-skip-ink: auto; +} +``` + +94. 文本下划线位置 + +`text-underline-position` 调整下划线相对于文本基线的位置,从而可以精确控制下划线的位置。 + +``` css +p { + text-underline-position: under; +} +``` + +95. 图像导向 + +`image-orientation` 控制图像的方向,允许您根据需要旋转或翻转它。 + +``` css +img { + image-orientation: from-image; +} +``` + +96. `column-span` + +`column-span` 允许一个元素在多列布局中跨越多个列,从而实现更灵活和动态的设计。 + +``` css +.spanning-element { + column-span: all; +} +``` + +97. `contain` + +`contain` 指定元素的包含策略,通过限制布局计算和渲染的范围来实现优化,从而提高性能。 + +``` css +.optimized-element { + contain: layout; +} +``` + +98. 内容可见性 + +内容可见性允许您控制屏幕外或隐藏内容的渲染行为,通过跳过隐藏元素的布局和绘制阶段来提高渲染性能。 + +``` css +.off-screen { + content-visibility: auto; +} +``` + +99. 文字装饰风格 + +`text-decoration-style` 指定用于文本装饰的线条样式,允许您选择不同的线条样式,例如实线、双线、点线或虚线。 + +``` css +p { + text-decoration: underline; + text-decoration-style: wavy; +} +``` + +100. 字间距 + +字间距调整文本元素中字之间的间距,使您可以微调版式布局并提高可读性。 + +``` css +p { + word-spacing: 2px; +} +``` + +101. 超出部分省略号 + +超出部分内容不展示,使用省略号代替 + +``` css +p { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +``` \ No newline at end of file diff --git a/source/_posts/commonly-shell-command.md b/source/_posts/commonly-shell-command.md index bac9a6e..2889bd9 100644 --- a/source/_posts/commonly-shell-command.md +++ b/source/_posts/commonly-shell-command.md @@ -1,7 +1,7 @@ --- title: 常用的shell命令 date: 2017-09-14 13:10:16 -tag: [shell] +tag: [shell, blog] --- ## 一、SSH到服务器上再执行shell命令 @@ -27,7 +27,7 @@ sysctl -a | grep inotify ``` shell # 查看Ubuntu操作系统位数 # 方法一: getconf -getconf LONG_BIT +getconf LONG_BIT # 64 or 32 # 方法二: uname -a @@ -76,22 +76,22 @@ tr(选项)(参数) ``` #### 选项 -* -c或——complerment:取代所有不属于第一字符集的字符; -* -d或——delete:删除所有属于第一字符集的字符; -* -s或--squeeze-repeats:把连续重复的字符以单独一个字符表示; +* -c或——complerment:取代所有不属于第一字符集的字符; +* -d或——delete:删除所有属于第一字符集的字符; +* -s或--squeeze-repeats:把连续重复的字符以单独一个字符表示; * -t或--truncate-set1:先删除第一字符集较第二字符集多出的字符。 #### 参数 -* 字符集1:指定要转换或删除的原字符集。当执行转换操作时,必须使用参数“字符集2”指定转换的目标字符集。但执行删除操作时,不需要参数“字符集2”; +* 字符集1:指定要转换或删除的原字符集。当执行转换操作时,必须使用参数“字符集2”指定转换的目标字符集。但执行删除操作时,不需要参数“字符集2”; *字符集2:指定要转换成的目标字符集。 #### 实例 -将输入字符由大写转换为小写: +将输入字符由大写转换为小写: ``` shell -echo "HELLO WORLD" | tr 'A-Z' 'a-z' -hello world +echo "HELLO WORLD" | tr 'A-Z' 'a-z' +hello world ``` 'A-Z' 和 'a-z'都是集合,集合是可以自己制定的,例如:'ABD-}'、'bB.,'、'a-de-h'、'a-c0-9'都属于集合,集合里可以使用'\n'、'\t',可以可以使用其他ASCII字符。 @@ -143,7 +143,7 @@ fi ## 八、开始行和结束行的内容 ``` shell # 文件最后100行 -tail -n 100 file +tail -n 100 file # 文件开头100行 head -n 100 file @@ -191,10 +191,10 @@ echo "what you input is:" $value ``` shell #!/bin/bash -xxx="Temp" +xxx="Temp" yyy="temp" -x_tmp=$(echo $xxx | tr [A-Z] [a-z]) +x_tmp=$(echo $xxx | tr [A-Z] [a-z]) y_tmp=$(echo $yyy | tr [A-Z] [a-z]) if [ "$x_tmp " = "$y_tmp " ]; then @@ -210,8 +210,8 @@ fi ``` sh command1 && command2 [&& command3 ...] ``` - -&&左边的命令(命令1)返回真(即返回0,成功被执行)后,&&右边的命令(命令2)才能够被执行;换句话说,“如果这个命令执行成功&&那么执行这个命令”。 + +&&左边的命令(命令1)返回真(即返回0,成功被执行)后,&&右边的命令(命令2)才能够被执行;换句话说,“如果这个命令执行成功&&那么执行这个命令”。 1 命令之间使用 && 连接,实现逻辑与的功能。 2 只有在 && 左边的命令返回真(命令返回值 $? == 0),&& 右边的命令才会被执行。 @@ -235,8 +235,8 @@ command1 || command2 [|| command3 ...] 3 只要有一个命令返回真(命令返回值 $? == 0),后面的命令就不会被执行。 ``` shell -str="this is a string" -[[ $str =~ "this" ]] && echo "$str contains this" +str="this is a string" +[[ $str =~ "this" ]] && echo "$str contains this" [[ $str =~ "that" ]] || echo "$str does NOT contain that" ``` diff --git a/source/_posts/git-clone-failure.md b/source/_posts/git-clone-failure.md index cfda801..5e68c17 100644 --- a/source/_posts/git-clone-failure.md +++ b/source/_posts/git-clone-failure.md @@ -1,6 +1,7 @@ +--- title: 使用 git 克隆 github 上的项目失败 date: 2018-11-09 10:00:20 -tag: [git, github] +tag: [git, github, blog] --- ## 现象 diff --git a/source/_posts/github-pages-env.md b/source/_posts/github-pages-env.md new file mode 100644 index 0000000..eaad6aa --- /dev/null +++ b/source/_posts/github-pages-env.md @@ -0,0 +1,68 @@ +--- +title: NextJs 获取 Github Action 部署的环境变量 +date: 2024-01-12 21:51:20 +tag: [git, github, blog] +--- + +## NextJs 获取 Github Action 部署的环境变量 + +### 设置变量 + +项目中需要某些私有密钥,不能直接暴露在仓库中,在编译`Next.js`的时候,需要将该密钥通过环境变量的形式注入到`Next.js`项目中,所以第一步,我们需要将这个密钥储存到当前仓库的`Settings/Secrets`里面,具体操作如下图所示: + +![image](https://github.com/hankliu62/interview/assets/8088864/887a0ad5-6b4a-4977-a2cb-8c7adc5b63b6) + +### 配置变量 + +在`Next.js`中获取`GitHub Actions`环境变量,你可以使用`process.env`对象来访问在`GitHub Actions`中设置的环境变量。以下是一个如何在`Next.js`中获取`GitHub Actions`环境变量的例子: + +首先,在GitHub Actions的工作流文件中设置环境变量,例如在 `.github/workflows/ci.yml`中: + +``` yml +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 14 + - name: Install Dependencies + run: npm install + - name: Build Next.js App + env: + MY_ENV_VAR: ${{ secrets.MY_SECRET_ENV_VAR }} + run: npm run build +``` + +在这个例子中,`MY_ENV_VAR`是一个环境变量,它可以是一个秘密值,通过`GitHub Actions`的秘密(secrets)功能来安全地设置。 + +然后,需要将环境变量添加到 `Next` 项目的配置文件中,在 `next.config.js` 中: + +``` js +module.exports = { + env: { + MY_ENV_VAR: process.env.MY_ENV_VAR, + } +} +``` + +### 获取变量 + +然后,在Next.js的应用代码中,你可以这样获取这个环境变量: + +``` js +// pages/index.js +export default function Home() { + const myEnvVar = process.env.MY_ENV_VAR; + return ( +

+

The environment variable is: {myEnvVar || 'undefined'}

+
+ ); +} +``` + +在这段代码中,`process.env.MY_ENV_VAR` 将会获取在 `GitHub Actions` 中设置的环境变量`MY_ENV_VAR`的值。如果环境变量存在,它将被显示在页面上;如果不存在,则会显示`'undefined'`。 \ No newline at end of file diff --git a/source/_posts/github-pages.md b/source/_posts/github-pages.md index 58a7066..4cef8e4 100644 --- a/source/_posts/github-pages.md +++ b/source/_posts/github-pages.md @@ -1,5 +1,7 @@ --- title: 使用 Github Pages 和 Issues 搭建博客 +date: 2023-12-27 12:20:26 +tag: [github pages, blog] --- ## Github Pages 简介 @@ -56,6 +58,8 @@ title: 使用 Github Pages 和 Issues 搭建博客 这种方案与上一种对比起来其实没多大区别,唯一的区别就是自建服务换成了github的另一个服务,就是说,github帮我们建好了。 github api:https://developer.github.com/v3/ + + #### github pages github Pages可以被认为是用户编写的、托管在github上的静态网页,如下方式可开启: diff --git a/source/_posts/githut-emoji.md b/source/_posts/githut-emoji.md new file mode 100644 index 0000000..c971e91 --- /dev/null +++ b/source/_posts/githut-emoji.md @@ -0,0 +1,38 @@ +--- +title: Emoji 大全 +date: 2024-04-01 23:02:26 +tag: [github] +--- + +## Emoji 大全 + +记录下 Emoji 表情大全,方便以后拷贝使用 + +下面内容可以直接复制来用,emoji 不是图片,所以可以任意字号展示,这里只是一部分,并不是全部: + +> 😀😃😄😁😆😅🤣😂🙂🙃😉😊😇🥰😍🤩😘😚😙😋😛😜🤪😝🤑🤗🤭🤫🤔🤐🤨😐😑😶😏😒🙄😬🤥😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳😎🤓🧐😕😟🙁☹️😮😯😲😳🥺😦😧😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬 + +> 👶🧒👦👧🧑👱👨🧔👨‍🦰👨‍🦱👨‍🦳👨‍🦲👩👩‍🦰🧑👩‍🦱🧑👩‍🦳🧑👩‍🦲🧑👱‍♀️👱‍♂️🧓👴👵🙍🙍‍♂️🙍‍♀️🙎🙎‍♂️🙎‍♀️🙅🙅‍♂️🙅‍♀️🙆🙆‍♂️🙆‍♀️💁💁‍♂️💁‍♀️🙋🙋‍♂️🙋‍♀️🧏🧏‍♂️🧏‍♀️🙇🙇‍♂️🙇‍♀️🤦‍♂️🤦‍♀️🤷‍♀️👨‍⚕️👩‍⚕️👨‍🎓👩‍🎓🧑‍🏫 + +> 👋🤚🖐️✋🖖👌🤏✌️🤞🤟🤘🤙👈👉👆🖕👇☝️👍👎✊👊🤛🤜👏🙌👐🤲🤝🙏✍️💅🤳💪 + +> 👣👀👁️👄💋👂🦻👃👅🧠🦷🦴💪🦾🦿🦵🦶👓🕶️🥽🥼🦺👔👕👖🧣🧤🧥🧦👗👘🥻🩱🩲🩳👙👚👛👜👝🎒👞👟🥾🥿👠👡🩰👢👑👒🎩🎓🧢⛑️💄💅💍💼🌂☂️💈🛀🛌💥💫💦💨 + +> ⬆️➡️⬇️⬅️↩️↪️⤴️⤵️🔃🔄🔙🔚🔛🔜🔝🛐⚛️🕉️✡️️☯️✝️☦️☪️☮️🕎🔯♈♉♊♋♌♍♎♏♐♑♒♓⛎🔀🔁🔂▶️⏩⏭️⏯️◀️⏪⏮️🔼⏫🔽⏬⏸️⏹️⏺️⏏️🎦✖️➕➖➗♾️⁉️❓❔❕❗💱💲⚕️♻️️🔱📛🔰⭕✅☑️✔️❌❎➰➿✳️✴️❇️#️⃣*️⃣0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣🔟🅰️🆎🅱️🆑🉐🈚🈲🉑🈸🈴🈳㊗️㊙️🈺🈵🔴🟠🟡🟢🔵🟣🟤⚫⚪🟥🟧🟨🟩🟦🟪🟫⬛⬜◼️◻️◾◽▪️▫️🔶🔷🔸🔹🔺🔻💠🔘🔳🔲🏁🚩🎌🏴🏳️🏳️‍🌈🏳️‍⚧️🏴‍☠️ + +> 🙈🙉🙊💥💫💦💨🐵🐒🦍🦧🐶🐕🦮🐕‍🦺🐩🐺🦊🦝🐱🐈🐈‍⬛🦁🐯🐅🐆🐴🐎🦄🦓🦌🐮🐂🐃🐄🐷🐖🐗🐽🐏🐑🐐🐪🐫🦙🦒🐘🦏🦛🐭🐁🐀🐹🐰🐇🐿️🦔🦇🐻🐻‍❄️🐨🐼🦥🦦🦨🦘🦡🐾🦃🐔🐓🐣🐤🐥🐦🐧🕊️🦅🦆🦢🦉🦩🦚🦜🐸🐊🐢🦎🐍🐲🐉🦕🦖🐳🐋🐬🐟🐠🐡🦈🐙🐚🐌🦋🐛🐜🐝🐞🦗🕷️🕸️🦂🦟🦠🦀🦞🦐🦑 + +> 💐🌸💮🏵️🌹🥀🌺🌻🌼🌷🌱🌲🌳🌴🌵🌾🌿☘️🍀🍁🍂🍃 + +> 🌍🌎🌏🌐🌑🌒🌓🌔🌕🌖🌗🌘🌙🌚🌛🌜☀️🌝🌞⭐🌟🌠☁️⛅⛈️🌤️🌥️🌦️🌧️🌨️🌩️🌪️🌫️🌬️🌈☂️☔⚡❄️☃️⛄☄️🔥💧🌊 + +> 🍇🍈🍉🍊🍋🍌🍍🥭🍎🍏🍐🍑🍒🍓🥝🍅🥥🥑🍆🥔🥕🌽🌶️🥒🥬🥦🧄🧅🍄🥜🌰🍞🥐🥖🥨🥯🥞🧇🧀🍖🍗🥩🥓🍔🍟🍕🌭🥪🌮🌯🥙🧆🥚🍳🥘🍲🥣🥗🍿🧈🧂🥫🍱🍘🍙🍚🍛🍜🍝🍠🍢🍣🍤🍥🥮🍡🥟🥠🥡🦪🍦🍧🍨🍩🍪🎂🍰🧁🥧🍫🍬🍭🍮🍯🍼🥛☕🍵🍶🍾🍷🍸🍹🍺🍻🥂🥃🥤🧃🧉🧊🥢🍽️🍴🥄 + +> 🧗‍♀️🤺🏇⛷️🏂🏌️🏌️‍♂️🏌️‍♀️🏄🏄‍♂️🏄‍♀️🚣‍♀️🏊‍♀️⛹️⛹️‍♂️⛹️‍♀️🏋️🏋️‍♂️🚴🚵‍♀️🤸🤼‍♀️🤽🤾‍♀️🤹🧘‍♀️🎪🛹🛼🛶🎗️🎟️🎫🎖️🏆🏅🥇🥈🥉⚽⚾🥎🏀🏐🏈🏉🎾🥏🎳🏏🏑🏒🥍🏓🏸🥊🥋🥅⛳⛸️🎣🎽🎿🛷🥌🎯🎱🎮🎰🎲🧩♟️🎭🎨🧵🧶🎼🎤🎧🎷🎸🎹🎺🎻🥁🎬🏹 + +> 😈👿👹👺💀☠👻👽👾💣 + +> 👣🎠🎡🎢🚣🏔️⛰️🌋🗻🏕️🏖️🏜️🏝️🏞️🏟️🏛️🏗️🏘️🏚️🏠🏡🏢🏣🏤🏥🏦🏨🏩🏪🏫🏬🏭🏯🏰💒🗼🗽⛪🕌🛕🕍⛩🕋⛲⛺🌁🌃🏙️🌄🌅🌆🌇🌉🎠🎡🎢🚂🚃🚄🚅🚆🚇🚈🚉🚊🚝🚞🚋🚌🚍🚎🚐🚑🚒🚓🚔🚕🚖🚗🚘🚙🚚🚛🚜🏎️🏍️🛵🛺🚲🛴🚏🛣️🛤️⛽🚨🚥🚦🚧⚓⛵🚤🛳️⛴️🛥️🚢✈️🛩️🛫🛬🪂💺🚁🚟🚠🚡🛰️🚀🛸🪐🌠🌌⛱️🎆🎇🎑💴💵💶💷🗿🛂🛃🛄🛅🧭 + +> 💌💎🔪💈🚪🚽🚿🛁⌛⏳⌚⏰🎈🎉🎊🎎🎏🎐🎀🎁📯📻📱📲☎📞📟📠🔋🔌💻💽💾💿📀🎥📺📷📹📼🔍🔎🔬🔭📡💡🔦🏮📔📕📖📗📘📙📚📓📃📜📄📰📑🔖💰💴💵💶💷💸💳✉📧📨📩📤📥📦📫📪📬📭📮📝📁📂📅📆📇📈📉📊📋📌📍📎📏📐✂🔒🔓🔏🔐🔑🔨🔫🔧🔩🔗💉💊🚬🔮🚩🎌💦💨 +> 💘❤💓💔💕💖💗💙💚💛💜💝💞💟 \ No newline at end of file diff --git a/source/_posts/hexo-github-blog-guide.md b/source/_posts/hexo-github-blog-guide.md index 7d62620..00f32c2 100644 --- a/source/_posts/hexo-github-blog-guide.md +++ b/source/_posts/hexo-github-blog-guide.md @@ -3,6 +3,7 @@ title: MAC搭建个人博客hexo+github详细完整步骤 date: 2017-09-09 12:20:26 tag: [hexo, blog] --- + 自己也算是摸爬滚打搭建成功,然后自己再重新安装部署一遍,把完整步骤分享给大家,同时最后有一些连接,如果我的步骤不行,大家可以参考其他人的. ## 一、安装Homebrew @@ -143,6 +144,8 @@ npm -v ![Node安装成功](https://user-images.githubusercontent.com/8088864/30236355-4d1f8f18-954a-11e7-8da7-54d3f9ae1c91.png) + + ## 四、安装hexo ### 1、利用npm命令即可安装 diff --git a/source/_posts/interview-questions.md b/source/_posts/interview-questions.md new file mode 100644 index 0000000..bb878d6 --- /dev/null +++ b/source/_posts/interview-questions.md @@ -0,0 +1,21278 @@ +--- +title: 前端面试题汇总 +date: 2024-02-08 12:12:26 +tag: [interview, blog] +--- + +## Event Loop + +Event Loop即事件循环,是指浏览器或者Nodejs解决javascript单线程运行时异步逻辑不会阻塞的一种机制。 + +Event Loop是一个执行模型,不同的运行环境有不同的实现,浏览器和nodejs基于不同的技术实现自己的event loop。 + +- 浏览器的Event Loop是在HTML5规范中明确定义。 +- Nodejs的Event Loop是libuv实现的。 +- libuv已经对Event Loop作出了实现,HTML5规范中只是定义的浏览器中Event Loop的模型,具体的实现交给了浏览器厂商。 + +### 宏队列和微队列 + +在javascript中,任务被分为两种,一种为宏任务(macrotask),也称为task,一种为微任务(microtask),也称为jobs。 + +宏任务主要包括: + +- script全部代码 +- setTimeout +- setInterval +- setImmediate (Nodejs独有,浏览器暂时不支持,只有IE10支持) +- requestAnimationFrame (浏览器独有) +- I/O +- UI rendering (浏览器独有) + +微任务主要包括: + +- process.nextTick (Nodejs独有) +- Promise +- Object.observe (废弃) +- MutationObserver + +### 浏览器中的Event Loop + +Javascript 有一个主线程 main thread 和 一个调用栈(执行栈) call-stack,所有任务都会被放到调用栈等待主线程的执行。 + +JS调用栈采用的是后进先出的规则,当函数执行时,会被添加到调用栈的顶部,当执行栈执行完后,就会从栈顶移除,直到栈内被清空。 + +Javascript单线程任务可以分为同步任务和异步任务,同步任务会在调用栈内按照顺序依次被主线程执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空的时候),被读取到调用栈内等待主线程的执行 + +任务队列 Task Queue, 是先进先出的数据结构。 + +![浏览器事件循环的进程模型](https://user-images.githubusercontent.com/8088864/124855609-c2904a00-dfdb-11eb-9138-df80150fa3a3.jpg) + +浏览器Event Loop的具体流程: + +1. 执行全局Javascript的同步代码,可能包含一些同步语句,也可以是异步语句(setTimeout语句不执行回调函数里面的,Promise中.then之前的语句) +2. 全局Javascript执行完毕后,调用栈call-stack会被清空 +3. 从微队列microtask queue中取出位于首部的回调函数,放入到调用栈call-stack中执行,执行完毕后从调用栈中删除,microtask queue的长度减1。 +4. 继续从微队列microtask queue的队首取出任务,放到调用栈中执行,依次循环往复,直至微任务队列microtask queue中的任务都被调用栈执行完毕。**特别注意,如果在执行微任务microtask过程中,又产生了微任务microtask,新产生的微任务也会追加到微任务队列microtask queue的尾部,新生成的微任务也会在当前周期中被执行完毕。** +5. microtask queue中的任务都被执行完毕后,microtask queue为空队列,调用栈也处于空闲阶段 +6. 执行UI rendering +7. 从宏队列macrotask queue的队首取出宏任务,放入调用栈中执行。 +8. 执行完后,调用栈为空闲状态 +9. 重复 3 - 8 的步骤,直至宏任务队列的任务都被执行完毕。 +... + +浏览器Event Loop的3个重点: + +1. 宏队列macrotask queue每次只从中取出一个任务放到调用栈中执行,执行完后去执行微任务队列中的所有任务 +2. 微任务队列中的所有任务都会依次取出来执行,只是微任务队列中的任务清空 +3. UI rendering 的执行节点在微任务队列执行完毕后,宏任务队列中取出任务执行之前执行 + +### NodeJs中的Event Loop + +libuv结构 + +![libuv的事件循环模型](https://user-images.githubusercontent.com/8088864/125010304-d64db600-e098-11eb-824f-de433a12a095.png) + +NodeJs中的宏任务队列和微任务队列 + +NodeJs的Event Loop中,执行宏任务队列的回调有6个阶段 + +![NodeJS中的宏队列执行回调的6个阶段](https://user-images.githubusercontent.com/8088864/125010342-e9608600-e098-11eb-84e0-70a5bd5f5867.png) + +Node的Event Loop可以分为6个阶段,各个阶段执行的任务如下所示: + +- `timers`: 执行setTimeout和setInterval中到期的callback。 +- `I/O callbacks`: 执行几乎所有的回调,除了close callbacks以及timers调度的回调和setImmediate()调度的回调。 +- `idle, prepare`: 仅在内部使用。 +- `poll`: 最重要的阶段,检索新的I/O事件,在适当的情况下回阻塞在该阶段。 +- `check`: 执行setImmediate的callback(setImmediate()会将事件回调插入到事件队列的尾部,主线程和事件队列的任务执行完毕后会立即执行setImmediate中传入的回调函数)。 +- `close callbacks`: 执行`close`事件的callback,例如socket.on('close', fn)或则http.server.on('close', fn)等。 + +NodeJs中的宏任务队列可以分为下列4个: + + 1. Timers Queue + 2. I/O Callbacks Queue + 3. Check Queue + 4. Close Callbacks Queue + +在浏览器中只有一个宏任务队列,所有宏任务都会放入到宏任务队列中等待放入执行栈中被主线程执行,NodeJs中有4个宏任务队列,不同类型的宏任务会被放入到不同的宏任务队列中。 + +NodeJs中的微任务队列可以分为下列2个: + + 1. `Next Tick Queue`: 放置process.nextTick(callback)的回调函数 + 2. `Other Micro Queue`: 其他microtask,例如Promise等 + +在浏览器中只有一个微任务队列,所有微任务都会放入到微任务队列中等待放入执行栈中被主线程执行,NodeJs中有2个微任务队列,不同类型的微任务会被放入到不同的微任务队列中。 + +![NodeJs事件循环](https://user-images.githubusercontent.com/8088864/125030923-71a55200-e0be-11eb-93be-95f1cbc456e3.png) + +NodeJs的Event Loop的具体流程: + +1. 执行全局Javascript的同步代码,可能包含一些同步语句,也可以是异步语句(setTimeout语句不执行回调函数里面的,Promise中.then之前的语句)。 +2. 执行微任务队列中的微任务,先执行Next Tick Queue队列中的所有的所有任务,再执行Other Micro Queue队列中的所有任务。 +3. 开始执行宏任务队列中的任务,共6个阶段,从第1个阶段开始执行每个阶段对应宏任务队列中的所有任务,**注意,这里执行的是该阶段宏任务队列中的所有的任务,浏览器Event Loop每次只会中宏任务队列中取出队首的任务执行,执行完后开始执行微任务队列中的任务,NodeJs的Event Loop会执行完该阶段中宏任务队列中的所有任务后,才开始执行微任务队列中的任务,也就是步骤2**。 +4. Timers Queue -> 步骤2 -> I/O Callbacks Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue -> ...... + +**特别注意:** + +- 上述的第三步,当 NodeJs 版本小于11时,NodeJs的Event Loop会执行完该阶段中宏任务队列中的所有任务 +- 当 NodeJS 版本大于等于11时,**在timer阶段的setTimeout,setInterval...和在check阶段的setImmediate都在node11里面都修改为一旦执行一个阶段里的一个任务就立刻执行微任务队列**。为了和浏览器更加趋同。 + +NodeJs的Event Loop的microtask queue和macrotask queue的执行顺序详情 + +![NodeJS中的微任务队列执行顺序](https://user-images.githubusercontent.com/8088864/125032436-8aaf0280-e0c0-11eb-926a-30be5bf116f9.png) + +![NodeJS中的宏任务队列执行顺序](https://user-images.githubusercontent.com/8088864/125032451-8f73b680-e0c0-11eb-8349-d6c5f20bd11a.png) + +当setTimeout(fn, 0)和setImmediate(fn)放在同一同步代码中执行时,可能会出现下面两种情况: + +1. **第一种情况**: 同步代码执行完后,timer还没到期,setImmediate中注册的回调函数先放入到Check Queue的宏任务队列中,先执行微任务队列,然后开始执行宏任务队列,先从Timers Queue开始,由于在Timer Queue中未发现任何的回调函数,往下阶段走,直到Check Queue中发现setImmediate中注册的回调函数,先执行,然后timer到期,setTimeout注册的回调函数会放入到Timers Queue的宏任务队列中,下一轮后再次执行到Timers Queue阶段时,才会再Timers Queue中发现了setTimeout注册的回调函数,于是执行该timer的回调,所以,**setImmediate(fn)注册的回调函数会早于setTimeout(fn, 0)注册的回调函数执行**。 +2. **第二种情况**: 同步代码执行完之前,timer已经到期,setTimeout注册的回调函数会放入到Timers Queue的宏任务队列中,执行同步代码到setImmediate时,将其回调函数注册到Check Queue中,同步代码执行完后,先执行微任务队列,然后开始执行宏任务队列,先从Timers Queue开始,在Timers Queue发现了timer中注册的回调函数,取出执行,往下阶段走,到Check Queue中发现setImmediate中注册的回调函数,又执行,所以这种情况时,**setTimeout(fn, 0)注册的回调函数会早于setImmediate(fn)注册的回调函数执行**。 + +3. 在同步代码中同时调setTimeout(fn, 0)和setImmediate执行顺序情况是不确定的,但是如果把他们放在一个IO的回调,比如readFile('xx', function () {// ....})回调中,那么IO回调是在I/O Callbacks Queue中,setTimeout到期回调注册到Timers Queue,setImmediate回调注册到Check Queue,I/O Callbacks Queue执行完到Check Queue,Timers Queue得到下个循环周期,所以setImmediate回调这种情况下肯定比setTimeout(fn, 0)回调先执行。 + +``` js +setImmediate(function A() { + console.log(1); + setImmediate(function B(){console.log(2);}); +}); + +setTimeout(function timeout() { + console.log('TIMEOUT FIRED'); +}, 0); + +// 执行结果: 会存在下面两种情况 +// 第一种情况: +// 1 +// TIMEOUT FIRED +// 2 + +// 第二种情况: +// TIMEOUT FIRED +// 1 +// 2 +``` + +注: + +- setImmediate中如果又存在setImmediate语句,内部的setImmediate语句注册的回调函数会在下一个`check`阶段来执行,并不在当前的`check`阶段来执行。 + +poll 阶段详解: + +poll 阶段主要又两个功能: + +1. 当timers到达指定的时间后,执行指定的timer的回调(Executing scripts for timers whose threshold has elapsed, then)。 +2. 处理poll队列的事件(Processing events in the poll queue)。 + +当进入到poll阶段,并且没有timers被调用的时候,会出现下面的情况: + +- 如果poll队列不为空,Event Loop将同步执行poll queue中的任务,直到poll queue队列为空或者执行的callback达到上限。 +- 如果poll队列为空,会发生下面的情况: + - 如果脚本执行过setImmediate代码,Event Loop会结束poll阶段,直接进入check阶段,执行Check Queue中调用setImmediate注册的回调函数。 + - 如果脚本没有执行过setImmediate代码,poll阶段将等待callback被添加到队列中,然后立即执行。 + +当进入到poll阶段,并且调用了timers的话,会发生下面的情况: + +- 一旦poll queue为空,Event Loop会检测Timers Queue中是否存在任务,如果存在任务的话,Event Loop会回到timer阶段并执行Timers Queue中timers注册的回调函数。**执行完后是进入check阶段,还是又重新进入I/O callbacks阶段?** + +setTimeout 对比 setImmediate + +- setTimeout(fn, 0)在timers阶段执行,并且是在poll阶段进行判断是否达到指定的timer时间才会执行 +- setImmediate(fn)在check阶段执行 + +两者的执行顺序要根据当前的执行环境才能确定: + +- 如果两者都在主模块(main module)调用,那么执行先后取决于进程性能,顺序随机 +- 如果两者都不在主模块调用,即在一个I/O Circle中调用,那么setImmediate的回调永远先执行,因为会先到Check阶段 + +setImmediate 对比 process.nextTick + +- setImmediate(fn)的回调任务会插入到宏队列Check Queue中 +- process.nextTick(fn)的回调任务会插入到微队列Next Tick Queue中 +- process.nextTick(fn)调用深度有限制,上限是1000,而setImmediate则没有 + + + +## Fetch API使用的常见问题及其解决办法 + +XMLHttpRequest在发送web请求时需要开发者配置相关请求信息和成功后的回调,尽管开发者只关心请求成功后的业务处理,但是也要配置其他繁琐内容,导致配置和调用比较混乱,也不符合关注分离的原则;fetch的出现正是为了解决XHR存在的这些问题。 + +**fetch是基于Promise设计的**,让开发者只关注请求成功后的业务逻辑处理,其他的不用关心,相当简单,FetchAPI的优点如下: + +- 语法简单,更加语义化 +- 基于标准的Promise实现,支持async/await +- 使用isomorphic-fetch可以方便同构 + +使用fetch来进行项目开发时,也是有一些常见问题的,下面就来说说fetch使用的常见问题。 + +### Fetch 兼容性问题 + +fetch是相对较新的技术,当然就会存在浏览器兼容性的问题,借用上面应用文章的一幅图加以说明fetch在各种浏览器的原生支持情况: + +![Fetch兼容性](https://user-images.githubusercontent.com/8088864/125045722-e03edb80-e0cf-11eb-9457-f56b13350846.png) + +从上图可以看出各个浏览器的低版本都不支持fetch技术。 + +如何在所有浏览器中通用fetch呢,当然就要考虑fetch的polyfill了。 + +fetch是基于Promise来实现的,所以在低版本浏览器中Promise可能也未被原生支持,所以还需要Promise的polyfill;大多数情况下,实现fetch的polyfill需要涉及到的: + +- promise的polyfill,例如es6-promise、babel-polyfill提供的promise实现。 +- fetch的polyfill实现,例如isomorphic-fetch和whatwg-fetch + +IE浏览器中IE8/9还比较特殊:IE8它使用的是ES3,而IE9则对ES5部分支持。这种情况下还需要ES5的polyfill es5-shim支持了。 + +上述有关promise的polyfill实现,需要说明的是: + +babel-runtime是不能作为Promise的polyfill的实现的,否则在IE8/9下使用fetch会报Promise未定义。为什么?我想大家猜到了,因为babel-runtime实现的polyfill是局部实现而不是全局实现,fetch底层实现用到Promise就是从全局中去取的,拿不到这报上述错误。 + +fetch的polyfill实现思路: + +首先判断浏览器是否原生支持fetch,否则结合Promise使用XMLHttpRequest的方式来实现;这正是whatwg-fetch的实现思路,而同构应用中使用的isomorphic-fetch,其客户端fetch的实现是直接require("whatwg-fetch")来实现的。 + +### fetch默认不携带cookie + +fetch发送请求默认是不发送cookie的,不管是同域还是跨域; + +对于那些需要权限验证的请求就可能无法正常获取数据,可以配置其credentials项,其有3个值: + +- omit: 默认值,忽略cookie的发送 +- same-origin: 表示cookie只能同域发送,不能跨域发送 +- include: cookie既可以同域发送,也可以跨域发送 + +credentials所表达的含义,其实与XHR2中的withCredentials属性类似,表示请求是否携带cookie; + +若要fetch请求携带cookie信息,只需设置一下credentials选项即可,例如fetch(url, {credentials: 'include'}); + +fetch默认对服务端通过Set-Cookie头设置的cookie也会忽略,若想选择接受来自服务端的cookie信息,也必须要配置credentials选项; + +### fetch请求对某些错误http状态不会reject + +主要是由fetch返回promise导致的,因为fetch返回的promise在某些错误的http状态下如400、500等不会reject,相反它会被resolve;只有网络错误会导致请求不能完成时,fetch 才会被 reject;所以一般会对fetch请求做一层封装。 + +``` js +function checkStatus(response) { + if (response.status >= 200 && response.status < 300) { + return response; + } + const error = new Error(response.statusText); + error.response = response; + throw error; +} + +function parseJSON(response) { + return response.json(); +} + +export default function request(url, options = {}) { + return fetch(url, { credentials: 'include', ...options }) + .then(checkStatus) + .then(parseJSON) + .then((data) => data) + .catch((err) => err); +} +``` + +### fetch不支持超时timeout处理 + +fetch不像大多数ajax库那样对请求设置超时timeout,它没有有关请求超时的功能,所以在fetch标准添加超时功能之前,都需要polyfill该特性。 + +实际上,我们真正需要的是abort(), timeout可以通过timeout+abort方式来实现,起到真正超时丢弃当前的请求。 + +目前的fetch指导规范中,fetch并不是一个具体实例,而只是一个方法;其返回的promise实例根据Promise指导规范标准是不能abort的,也不能手动改变promise实例的状态,只能由内部来根据请求结果来改变promise的状态。 + +实现fetch的timeout功能,其思想就是新创建一个可以手动控制promise状态的实例,根据不同情况来对新promise实例进行resolve或者reject,从而达到实现timeout的功能; + +根据github上[timeout handling](https://github.com/github/fetch/issues/175)上的讨论,目前可以有两种不同的解决方法: + +方法一: 单纯setTimeout方法 + +``` js +var fetchOrigin = fetch; +window.fetch = function(url, options) { + return new Promise(function(resolve, reject) { + var timerId; + if (options.timeout) { + timerId = setTimeout(function() { + reject(new Error('fetch timeout')); + }, options.timeout); + } + + fetchOrigin(url, option).then(function(response) { + timerId && clearTimeout(timerId); + resolve(response); + }, function(error) { + timerId && clearTimeout(timerId); + reject(error); + }); + }); +} +``` + +使用这种方式还可模拟XHR的abort方法 + +``` js +var fetchOrigin = fetch; +window.fetch = function(url, options) { + return new Promise(function(resolve, reject) { + var abort = function() { + reject(new Error('fetch abort')); + }; + + const p = fetchOrigin(url, option).then(resolve, reject); + p.abort = abort; + + return p; + }); +} +``` + +方法二: 利用Promise.race方法 + +Promise.race方法接受一个promise实例数组参数,表示多个promise实例中任何一个最先改变状态,那么race方法返回的promise实例状态就跟着改变 + +``` js +var fetchOrigin = fetch; +window.fetch = function(url, options) { + var abortFn = null; + var timeoutFn = null; + + var timeoutPromise = new Promise(function(resolve, reject) { + timeoutFn = function () { + reject(new Error('fetch timeout')); + } + }); + + var abortPromise = new Promise(function(resolve, reject) { + abortFn = function () { + reject(new Error('fetch abort')); + } + }); + + const fetchPromise = fetchOrigin(url, option); + + if (option.timeout) { + setTimeout(timeoutFn, option.timeout); + } + + const promise = Promise.race( + timeoutPromise, + abortPromise, + fetchPromise, + ); + + promise.abort = abortFn; + + return promise; +} +``` + +对fetch的timeout的上述实现方式补充几点: + +- timeout不是请求连接超时的含义,它表示发送请求到接收响应的时间,包括请求的连接、服务器处理及服务器响应回来的时间。 +- fetch的timeout即使超时发生了,本次请求也不会被abort丢弃掉,它在后台仍然会发送到服务器端,只是本次请求的响应内容被丢弃而已。 + +### fetch不支持JSONP + +fetch是与服务器端进行异步交互的,而JSONP是外链一个javascript资源,是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题,并不是真正ajax,所以fetch与JSONP没有什么直接关联,当然至少目前是不支持JSONP的。 + +这里我们把JSONP与fetch关联在一起有点差强人意,fetch只是一个ajax库,我们不可能使fetch支持JSONP;只是我们要实现一个JSONP,只不过这个JSONP的实现要与fetch的实现类似,即基于Promise来实现一个JSONP;而其外在表现给人感觉是fetch支持JSONP一样; + +目前比较成熟的开源JSONP实现[fetch-jsonp](https://github.com/camsong/fetch-jsonp)给我们提供了解决方案,想了解可以自行前往。不过再次想唠叨一下其JSONP的实现步骤,因为在本人面试的前端候选人中大部分人对JSONP的实现语焉不详; + +使用它非常简单,首先需要用npm安装fetch-jsonp + +``` shell +npm install fetch-jsonp --save-dev +``` + +fetch-jsonp源码如下所示: + +``` js +const defaultOptions = { + timeout: 5000, + jsonpCallback: 'callback', + jsonpCallbackFunction: null, +}; + +function generateCallbackFunction() { + return `jsonp_${Date.now()}_${Math.ceil(Math.random() * 100000)}`; +} + +function clearFunction(functionName) { + // IE8 throws an exception when you try to delete a property on window + // http://stackoverflow.com/a/1824228/751089 + try { + delete window[functionName]; + } catch (e) { + window[functionName] = undefined; + } +} + +function removeScript(scriptId) { + const script = document.getElementById(scriptId); + if (script) { + document.getElementsByTagName('head')[0].removeChild(script); + } +} + +function fetchJsonp(_url, options = {}) { + // to avoid param reassign + let url = _url; + const timeout = options.timeout || defaultOptions.timeout; + const jsonpCallback = options.jsonpCallback || defaultOptions.jsonpCallback; + + let timeoutId; + + return new Promise((resolve, reject) => { + const callbackFunction = options.jsonpCallbackFunction || generateCallbackFunction(); + const scriptId = `${jsonpCallback}_${callbackFunction}`; + + window[callbackFunction] = (response) => { + resolve({ + ok: true, + // keep consistent with fetch API + json: () => Promise.resolve(response), + }); + + if (timeoutId) clearTimeout(timeoutId); + + removeScript(scriptId); + + clearFunction(callbackFunction); + }; + + // Check if the user set their own params, and if not add a ? to start a list of params + url += (url.indexOf('?') === -1) ? '?' : '&'; + + const jsonpScript = document.createElement('script'); + jsonpScript.setAttribute('src', `${url}${jsonpCallback}=${callbackFunction}`); + if (options.charset) { + jsonpScript.setAttribute('charset', options.charset); + } + jsonpScript.id = scriptId; + document.getElementsByTagName('head')[0].appendChild(jsonpScript); + + timeoutId = setTimeout(() => { + reject(new Error(`JSONP request to ${_url} timed out`)); + + removeScript(scriptId); + + clearFunction(callbackFunction); + + // 当前超时,请求并没有丢弃,请求完成的时候还是会调用该方法,如果直接干掉,会报错,修改函数体,回调过来时删除从全局上删除该函数 + window[callbackFunction] = () => { + clearFunction(callbackFunction); + }; + }, timeout); + + // Caught if got 404/500 + jsonpScript.onerror = () => { + reject(new Error(`JSONP request to ${_url} failed`)); + + clearFunction(callbackFunction); + removeScript(scriptId); + if (timeoutId) clearTimeout(timeoutId); + }; + }); +} + +export default fetchJsonp; +``` + +具体的使用方式: + +``` js +fetchJsonp('/users.jsonp', { + timeout: 3000, + jsonpCallback: 'custom_callback' +}) +.then(function(response) { + return response.json() +}).catch(function(ex) { + console.log('parsing failed', ex) +}); +``` + +### fetch不支持progress事件 + +XHR是原生支持progress事件的,例如下面代码这样: + +``` js +var xhr = new XMLHttpRequest(); +xhr.open('POST', '/uploads'); +xhr.onload = function() {} +xhr.onerror = function() {} +var uploadProgress = function(event) { + if (event.lengthComputable) { + var percent = Math.round((event.loaded / event.total) * 100); + console.log(percent); + } +}; + +// 上传的progress事件 +xhr.upload.onprogress = uploadProgress; +// 下载的progress事件 +xhr.onprogress = uploadProgress; +``` + +但是fetch是不支持有关progress事件的;不过可喜的是,根据fetch的指导规范标准,其内部设计实现了Request和Response类;其中Response封装一些方法和属性,通过Response实例可以访问这些方法和属性,例如response.json()、response.body等等; + +值得关注的地方是,response.body是一个可读字节流对象,其实现了一个getRender()方法,其具体作用是: + +getRender()方法用于读取响应的原始字节流,该字节流是可以循环读取的,直至body内容传输完成; + +因此,利用到这点可以模拟出fetch的progress。 + +代码实现如下: + +``` js +// fetch() returns a promise that resolves once headers have been received +fetch(url).then(response => { + // response.body is a readable stream. + // Calling getReader() gives us exclusive access to the stream's content + var reader = response.body.getReader(); + var bytesReceived = 0; + + // read() returns a promise that resolves when a value has been received + reader.read().then(function processResult(result) { + // Result objects contain two properties: + // done - true if the stream has already given you all its data. + // value - some data. Always undefined when done is true. + if (result.done) { + console.log("Fetch complete"); + return; + } + + // result.value for fetch streams is a Uint8Array + bytesReceived += result.value.length; + console.log('Received', bytesReceived, 'bytes of data so far'); + + // Read some more, and call this function again + return reader.read().then(processResult); + }); +}); +``` + +github上也有使用Promise+XHR结合的方式实现类fetch的progress效果(当然这跟fetch完全不搭边)可以参考[这里](https://github.com/github/fetch/issues/89#issuecomment-256610849),具体代码如下: + +``` js +function fetchProgress(url, opts={}, onProgress) { + return new Promise((resolve, reject)=>{ + var xhr = new XMLHttpRequest(); + xhr.open(opts.method || 'get', url); + + for (var key in opts.headers||{}) { + xhr.setRequestHeader(key, opts.headers[key]); + } + + xhr.onload = function(event) { + resolve(e.target.responseText) + }; + + xhr.onerror = reject; + + if (xhr.upload && onProgress) { + xhr.upload.onprogress = onProgress; // event.loaded / event.total * 100 ; //event.lengthComputable + } + + xhr.send(opts.body); + }); +} + +fetchProgress('/').then(console.log) +``` + +### fetch跨域问题 + +既然是ajax库,就不可避免与跨域扯上关系;XHR2是支持跨域请求的,只不过要满足浏览器端支持CORS,服务器通过Access-Control-Allow-Origin来允许指定的源进行跨域,仅此一种方式。 + +与XHR2一样,fetch也是支持跨域请求的,只不过其跨域请求做法与XHR2一样,需要客户端与服务端支持;另外,fetch还支持一种跨域,不需要服务器支持的形式,具体可以通过其mode的配置项来说明。 + +fetch的mode配置项有3个值,如下: + +- same-origin:该模式是不允许跨域的,它需要遵守同源策略,否则浏览器会返回一个error告知不能跨域;其对应的response type为basic。 +- cors: 该模式支持跨域请求,顾名思义它是以CORS的形式跨域;当然该模式也可以同域请求不需要后端额外的CORS支持;其对应的response type为cors。 +- no-cors: 该模式用于跨域请求但是服务器不带CORS响应头,也就是服务端不支持CORS;这也是fetch的特殊跨域请求方式;其对应的response type为opaque。 + +针对跨域请求,cors模式是常见跨域请求实现,但是fetch自带的no-cors跨域请求模式则较为陌生,该模式有一个比较明显的特点: + +该模式允许浏览器发送本次跨域请求,但是不能访问响应返回的内容,这也是其response type为opaque不透明的原因。 + +这与发送的请求类似,只是该模式不能访问响应的内容信息;但是它可以被其他APIs进行处理,例如ServiceWorker。另外,该模式返回的response可以在Cache API中被存储起来以便后续的对它的使用,这点对script、css和图片的CDN资源是非常合适的,因为这些资源响应头中都没有CORS头。 + +总的来说,fetch的跨域请求是使用CORS方式,需要浏览器和服务端的支持。 + +## 原型链和继承 + +JavaScript是一门面向对象的设计语言,在JS里除了null和undefined,其余一切皆为对象。其中Array/Function/Date/RegExp是Object对象的特殊实例实现,Boolean/Number/String也都有对应的基本包装类型的对象(具有内置的方法)。传统语言是依靠class类来完成面向对象的继承和多态等特性,而JS使用原型链和构造器来实现继承,依靠参数arguments.length来实现多态。并且在ES6里也引入了class关键字来实现类。 + +### 函数与对象的关系 + +有时我们会好奇为什么能给一个函数添加属性,函数难道不应该就是一个执行过程的作用域吗? + +``` js +var name = 'Hank'; +function Person(name) { + this.name = name; + this.sayName = function() { + alert(this.name); + } +} +Person.age = 10; +console.log(Person.age); // 10 +console.log(Person); +/* 输出函数体: +ƒ Person(name) { + this.name = name; +} +*/ +``` + +我们能够给函数赋一个属性值,当我们输出这个函数时这个属性却无影无踪了,这到底是怎么回事,这个属性又保存在哪里了呢? + +其实,在JS里,函数就是一个对象,这些属性自然就跟对象的属性一样被保存起来,函数名称指向这个对象的存储空间。 + +函数调用过程没查到资料,个人理解为:这个对象内部拥有一个内部属性[\[function]]保存有该函数体的字符串形式,当使用()来调用的时候,就会实时对其进行动态解析和执行,如同**eval()**一样。 + +![内存栈和内存堆](https://user-images.githubusercontent.com/8088864/125233637-947b7480-e311-11eb-903e-73397c79b87e.png) + +上图是JS的具体内存分配方式,JS中分为值类型和引用类型,值类型的数据大小固定,我们将其分配在栈里,直接保存其数据。而引用类型是对象,会动态的增删属性,大小不固定,我们把它分配到内存堆里,并用一个指针指向这片地址,也就是Person其实保存的是一个指向这片地址的指针。这里的Person对象是个函数实例,所以拥有特殊的内部属性[\[function]]用于调用。同时它也拥有内部属性arguments/this/name,因为不相关,这里我们没有绘出,而展示了我们为其添加的属性age。 + +### 函数与原型的关系 + +同时在JS里,我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个用于包含该函数所有实例的共享属性和方法的对象。而这个对象同时包含一个指针指向这个这个函数,这个指针就是**constructor**,这个函数也被成为构造函数。这样我们就完成了构造函数和原型对象的双向引用。 + +而上面的代码实质也就是当我们创建了Person构造函数之后,同步开辟了一片空间创建了一个对象作为Person的原型对象,可以通过Person.prototype来访问这个对象,也可以通过Person.prototype.constructor来访问Person该构造函数。通过构造函数我们可以往实例对象里添加属性,如上面的例子里的name属性和sayName()方法。我们也可以通过prototype来添加原型属性,如: + +![函数原型](https://user-images.githubusercontent.com/8088864/125234076-7f531580-e312-11eb-9c55-3d760c70f5e7.png) + +要注意属性和原型属性不是同一个东西,也并不保存在同一个空间里: + +``` js +Person.age; // 10 +Person.prototype.age; // 24 +``` + +### 原型和实例的关系 + +现在有了构造函数和原型对象,那我们接下来new一个实例出来,这样才能真正体现面向对象编程的思想,也就是**继承**: + +``` js +var person1 = new Person('Lee'); +var person2 = new Person('Lucy'); +``` + +我们新建了两个实例person1和person2,这些实例的内部都会包含一个指向其构造函数的原型对象的指针(内部属性),这个指针叫[\[Prototype]],在ES5的标准上没有规定访问这个属性,但是大部分浏览器实现了**__proto__**的属性来访问它,成为了实际的通用属性,于是在ES6的附录里写进了该属性。__proto__前后的双下划线说明其本质上是一个内部属性,而不是对外访问的API,因此官方建议新的代码应当避免使用该属性,转而使用Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。 + +这里的prototype我们称为显示原型,__proto__我们称为隐式原型。 + +``` js +Object.getPrototypeOf({}) === Object.prototype; // true +``` + +同时由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的 [\[Prototype]]在各个浏览器和 JavaScript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.__proto__ = ... 语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[Prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [\[Prototype]]。相反,你应该使用 Object.create()来创建带有你想要的[[Prototype]]的新对象。 + +此时它们的关系是(为了清晰,忽略函数属性的指向,用(function)代指): + +![构造函数实例的原型关系](https://user-images.githubusercontent.com/8088864/125234787-f89f3800-e313-11eb-8f2a-b1e346d904af.png) + +在这里我们可以看到两个实例指向了同一个原型对象,而在new的过程中调用了Person()方法,对每个实例分别初始化了name属性和sayName方法,属性值分别被保存,而方法作为引用对象也指向了不同的内存空间。 + +我们可以用几种方法来验证实例的原型指针到底指向的是不是构造函数的原型对象: + +``` js +person1.__proto__ === Person.prototype // true +Person.prototype.isPrototypeOf(person1); // true +Object.getPrototypeOf(person2) === Person.prototype; // true +person1 instanceof Person; // true +``` + +### 原型链 + +现在我们访问实例person1的属性和方法了: + +``` js +person1.name; // Lee +person1.age; // 24 +person1.toString(); // [object Object] +``` + +想下这个问题,我们的name值来自于person1的属性,那么age值来自于哪?toString( )方法又在哪定义的呢? + +这就是我们要说的原型链,原型链是实现继承的主要方法,其思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。如果我们让一个原型对象等于另一个类型的实例,那么该原型对象就会包含一个指向另一个原型的指针,而如果另一个原型对象又是另一个原型的实例,那么上述关系依然成立,层层递进,就构成了实例与原型的链条,这就是原型链的概念。 + +上面代码的name来自于自身属性,age来自于原型属性,toString( )方法来自于Person原型对象的原型Object。当我们访问一个实例属性的时候,如果没有找到,我们就会继续搜索实例的原型,如果还没有找到,就递归搜索原型链直到原型链末端。我们可以来验证一下原型链的关系: + +``` js +Person.prototype.__proto__ === Object.prototype // true +``` + +同时让我们更加深入的验证一些东西: + +``` js +Person.__proto__ === Function.prototype // true +Function.prototype.__proto__ === Object.prototype // true +``` + +我们会发现Person是Function对象的实例,Function是Object对象的实例,Person原型是Object对象的实例。这证明了我们开篇的观点:JavaScript是一门面向对象的设计语言,在JS里除了null和undefined,其余一切皆为对象。 + +下面祭出我们的原型链图: + +![原型链图](https://user-images.githubusercontent.com/8088864/125235100-7e22e800-e314-11eb-9dd0-bb6d0747ec99.jpg) + +根据我们上面讲述的关于prototype/constructor/__proto__的内容,我相信你可以完全看懂这张图的内容。需要注意两点: + + 1. 构造函数和对象原型一一对应,他们与实例一起作为三要素构成了三面这幅图。最左侧是实例,中间是构造函数,最右侧是对象原型。 + 2. 最最右侧的null告诉我们:Object.prototype.__proto__ = null,也就是Object.prototype是JS中一切对象的根源。其余的对象继承于它,并拥有自己的方法和属性。 + +### 6种继承方法 + +#### 第一种: 原型链继承 + +利用原型链的特点进行继承 + +``` js +function Super(){ + this.name = 'web前端'; + this.type = ['JS','HTML','CSS']; +} +Super.prototype.sayName=function(){ + return this.name; +} +function Sub(){}; +Sub.prototype = new Super(); +Sub.prototype.constructor = Sub; +var sub1 = new Sub(); +sub1.sayName(); +``` + +优点: + +- 可以实现继承。 + +缺点: + +- 子类的原型属性集成了父类实例化对象,所有子类的实例化对象都共享原型对象的属性和方法 + +``` js +var sub1 = new Son(); +var sub2 = new Son(); +sub1.type.push('VUE'); +console.log(sub1.type); // ['JS','HTML','CSS','VUE'] +console.log(sub2.type); // ['JS','HTML','CSS','VUE'] +``` + +- 子类构造函数实例化对象时,无法传递参数给父类 + +#### 第二种: 构造函数继承 + +通过构造函数call方法实现继承。 + +``` js +function Super(){ + this.name = 'web前端'; + this.type = ['JS','HTML','CSS']; + + this.sayName = function() { + return this.name; + } +} +function Sub(){ + Super.call(this); +} +var sub1 = new Sub(); +sub1.type.push('VUE'); +console.log(sub1.type); // ['JS','HTML','CSS','VUE'] +var sub2 = new Sub(); +console.log(sub2.type); // ['JS','HTML','CSS'] +``` + +优点: + +- 实现父类实例化对象的独立性 + +- 还可以给父类实例化对象添加参数 + +缺点: + +- 方法都在构造函数中定义,每次实例化对象都得创建一遍方法,基本无法实现函数复用 + +- call方法仅仅调用了父级构造函数的属性及方法,没有办法访问父级构造函数原型对象的属性和方法 + +#### 第三种: 组合继承 + +利用原型链继承和构造函数继承的各自优势进行组合使用 + +``` js + +function Super(name){ + this.name = name; + this.type = ['JS','HTML','CSS']; +} + +Super.prototype.sayName=function(){ + return this.name; +} + +function Sub(name){ + Super.call(this, name); +} + +Sub.prototype = new Super(); +sub1 = new Sub('张三'); +sub2 = new Sub('李四'); +sub1.type.push('VUE'); +sub2.type.push('PHP'); +console.log(sub1.type); // ['JS','HTML','CSS','VUE'] +console.log(sub2.type); // ['JS','HTML','CSS','PHP'] +sub1.sayName(); // 张三 +sub2.sayName(); // 李四 +``` + +优点: + +- 利用原型链继承,实现原型对象方法的继承,允许访问父级构造函数原型对象属性和方法,实现方法复用 + +- 利用构造函数继承,实现属性的继承,而且可以传递参数 + +缺点: + +- 创建子类实例对象时,无论什么情况下,都会调用两次父级构造函数:一次是在创建子级原型的时候,另一次是在子级构造函数内部(call) + +#### 第四种: 原型式继承 + +创建一个函数,将参数作为一个对象的原型对象。 + +``` js +function create(obj) { + function Sub(){}; + Sub.prototype = obj; + Sub.prototype.constructor = Sub; + return new Sub(); +} + +var parent = { + name: '张三', + type: ['JS','HTML','CSS'], +}; + +var sub1 = create(parent); +var sub2 = create(parent); + +console.log(sub1.name); // 张三 +console.log(sub2.name); // 张三 +``` + +ES5规范化了这个原型继承,新增了Object.create()方法,接收两个参数,第一个为原型对象,第二个为要混合进新对象的属性,格式与Object.defineProperties()相同。 + +``` js +Object.create(null, {name: {value: 'Greg', enumerable: true}}); + +// 相当于 +var parent = { + name: '张三', + type: ['JS','HTML','CSS'], +}; + +var sub1 = Object.create(parent); +var sub2 = Object.create(parent); + +console.log(sub1.name); // 张三 +console.log(sub2.name); // 张三 +``` + +优缺点: + +- 跟原型链类似 + +#### 第五种: 寄生继承 + +在原型式继承的基础上,在函数内部丰富对象 + +``` js +function create(obj) { + function Sub() {}; + Sub.prototype = obj; + Sub.prototype.constructor = Sub; + + return new Sub(); +} + +function Parasitic(obj) { + var clone = create(obj); + clone.sayHi = function() { + console.log('hi'); + }; + return clone; +} + +var parent = { + name: '张三', + type: ['JS','HTML','CSS'], +}; + +var sub1 = Parasitic(parent); +var sub2 = Parasitic(parent); + +console.log(sub1.name); // 张三 +console.log(sub2.name); // 张三 +``` + +如果使用ES5Object.create来代替create函数的话,可以简化成如下所示: + +``` js +function Parasitic(obj) { + var clone = Object.create(obj); + clone.sayHi = function() { + console.log('hi'); + }; + return clone; +} + +var parent = { + name: '张三', + type: ['JS','HTML','CSS']; +}; + +var son1 = Parasitic(parent); +var son2 = Parasitic(parent); + +console.log(son1.name); // 张三 +console.log(son2.name); // 张三 +son1.sayHi(); +son2.sayHi(); +``` + +优缺点: + +- 跟构造函数继承类似,调用一次函数就得创建一遍方法,无法实现函数复用,效率较低 + +#### 第六种: 寄生组合继承 + +利用组合继承和寄生继承各自优势 + +组合继承方法我们已经说了,它的缺点是两次调用父级构造函数,一次是在创建子级原型的时候,另一次是在子级构造函数内部,那么我们只需要优化这个问题就行了,即减少一次调用父级构造函数,正好利用寄生继承的特性,继承父级构造函数的原型来创建子级原型。 + +``` js +function Super(name) { + this.name = name; + this.type = ['JS','HTML','CSS']; +}; + +Super.prototype.sayName = function () { + return this.name; +}; + +function Sub(name, age) { + Super.call(this, name); + this.age = age; +} + +// 我们封装其继承过程 +function inheritPrototype(Sub, Super) { + // 以该对象为原型创建一个新对象 + var prototype = Object.create(Super.prototype); + prototype.constructor = Sub; + Sub.prototype = prototype; +} + +inheritPrototype(Sub, Super); + +// 必须定义在inheritPrototype方法之后 +Sub.prototype.sayAge = function () { + return this.age; +} + +var instance = new Sub('张三', 40); +instance.sayName(); // 张三 +instance.sayAge(); // 40 +``` + +这种方式只调用了一次父类构造函数,只在子类上创建一次对象,同时保持原型链,还可以使用instanceof和isPrototypeOf()来判断原型,是我们最理想的继承方式。 + +#### 第七种: ES6 Class类和extends关键字 + +ES6引进了class关键字,用于创建类,这里的类是作为**ES5构造函数和原型对象的语法糖**存在的,其功能大部分都可以被ES5实现,不过在语言层面上ES6也提供了部分支持。新的写法不过让对象原型看起来更加清晰,更像面向对象的语法而已。 + +``` js +//定义类 +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + + toString() { + return '(' + this.x + ', ' + this.y + ')'; + } +} + +var point = new Point(10, 10); +``` + +我们看到其中的constructor方法就是之前的构造函数,this就是之前的原型对象,toString()就是定义在原型上的方法,只能使用new关键字来新建实例。语法差别在于我们不需要function关键字和逗号分割符。其中,所有的方法都直接定义在原型上,注意所有的方法都不可枚举。类的内部使用严格模式,并且不存在变量提升,其中的this指向类的实例。 + +new是从构造函数生成实例的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。 + +类存在静态方法,使用static关键字表示,其只能类和继承的子类来进行调用,不能被实例调用,也就是不能被实例继承,所以我们称它为静态方法。类不存在内部方法和内部属性。 + +``` js +class Foo { + static classMethod() { + return 'hello'; + } +} + +Foo.classMethod() // 'hello' + +var foo = new Foo(); +foo.classMethod() +// TypeError: foo.classMethod is not a function +``` + +类通过extends关键字来实现继承,在继承的子类的构造函数里我们使用super关键字来表示对父类构造函数的引用;在静态方法里,super指向父类;在其它函数体内,super表示对父类原型属性的引用。其中super必须在子类的构造函数体内调用一次,因为我们需要调用时来绑定子类的元素对象,否则会报错。 + +``` js +class ColorPoint extends Point { + constructor(x, y, color) { + super(x, y); // 调用父类的constructor(x, y) + this.color = color; + } + + toString() { + return this.color + ' ' + super.toString(); // 调用父类的toString() + } +} +``` + +## 前端性能优化 + +性能优化是把双刃剑,有好的一面也有坏的一面。好的一面就是能提升网站性能,坏的一面就是配置麻烦,或者要遵守的规则太多。并且某些性能优化规则并不适用所有场景,需要谨慎使用。 + +下面列出来了前端性能的24条建议: + +### 1. 减少 HTTP 请求 + +一个完整的 HTTP 请求需要经历 DNS 查找,TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程。 + +接下来看一个具体的例子帮助理解 HTTP : + +![http请求瀑布图](https://user-images.githubusercontent.com/8088864/125281253-957bc880-e348-11eb-97bf-464d4531ce8e.png) + +这是一个 HTTP 请求,请求的文件大小为 28.4KB。 + +名词解释: + +- Queueing: 在请求队列中的时间。 +- Stalled: 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。 +- Proxy negotiation: 与代理服务器连接进行协商所花费的时间。 +- DNS Lookup: 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。 +- Initial Connection / Connecting: 建立连接所花费的时间,包括TCP握手,重试和协商SSL。 +- SSL: 完成SSL握手所花费的时间。 +- Request sent: 发出网络请求所花费的时间,通常为一毫秒的时间。 +- Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间。 +- Content Download: 接收响应数据所花费的时间。 + +从这个例子可以看出,真正下载数据的时间占比为 13.05 / 204.16 = 6.39%,文件越小,这个比例越小,文件越大,比例就越高。这就是为什么要建议将多个小文件合并为一个大文件,从而减少 HTTP 请求次数的原因。 + +### 2. 使用 HTTP2 + +HTTP2 相比 HTTP1.1 有如下几个优点: + +#### 解析速度快 + +服务器解析 HTTP1.1 的请求时,必须不断地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦,因为 HTTP2 是基于帧的协议,每个帧都有表示帧长度的字段。 + +#### 多路复用 + +HTTP1.1 如果要同时发起多个请求,就得建立多个 TCP 连接,因为一个 TCP 连接同时只能处理一个 HTTP1.1 的请求。 + +在 HTTP2 上,多个请求可以共用一个 TCP 连接,这称为多路复用。同一个请求和响应用一个流来表示,并有唯一的流 ID 来标识。 多个请求和响应在 TCP 连接中可以乱序发送,到达目的地后再通过流 ID 重新组建。 + +#### 首部压缩 + +HTTP2 提供了首部压缩功能。 + +例如有如下两个请求: + +``` +// 请求1 +:authority: unpkg.zhimg.com +:method: GET +:path: /za-js-sdk@2.16.0/dist/zap.js +:scheme: https +accept: */* +accept-encoding: gzip, deflate, br +accept-language: zh-CN,zh;q=0.9 +cache-control: no-cache +pragma: no-cache +referer: https://www.zhihu.com/ +sec-fetch-dest: script +sec-fetch-mode: no-cors +sec-fetch-site: cross-site +user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36 + +// 请求2 +:authority: zz.bdstatic.com +:method: GET +:path: /linksubmit/push.js +:scheme: https +accept: */* +accept-encoding: gzip, deflate, br +accept-language: zh-CN,zh;q=0.9 +cache-control: no-cache +pragma: no-cache +referer: https://www.zhihu.com/ +sec-fetch-dest: script +sec-fetch-mode: no-cors +sec-fetch-site: cross-site +user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36 +``` + +从上面两个请求可以看出来,有很多数据都是重复的。如果可以把相同的首部存储起来,仅发送它们之间不同的部分,就可以节省不少的流量,加快请求的时间。 + +HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送。 + +下面再来看一个简化的例子,假设客户端按顺序发送如下请求首部: + +``` +Header1:foo +Header2:bar +Header3:bat +``` + +当客户端发送请求时,它会根据首部值创建一张表: + +| 索引 | 首部名称 | 值 | +| ---- | ---- | ---- | +| 62 | Header1 | foo | +| 63 | Header2 | bar | +| 64 | Header3 | bar | + +如果服务器收到了请求,它会照样创建一张表。 当客户端发送下一个请求的时候,如果首部相同,它可以直接发送这样的首部块: + +``` +62 63 64 +``` + +服务器会查找先前建立的表格,并把这些数字还原成索引对应的完整首部。 + +#### 优先级 + +HTTP2 可以对比较紧急的请求设置一个较高的优先级,服务器在收到这样的请求后,可以优先处理。 + +#### 流量控制 + +由于一个 TCP 连接流量带宽(根据客户端到服务器的网络带宽而定)是固定的,当有多个请求并发时,一个请求占的流量多,另一个请求占的流量就会少。流量控制可以对不同的流的流量进行精确控制。 + +#### 服务器推送 + +HTTP2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。 + +例如当浏览器请求一个网站时,除了返回 HTML 页面外,服务器还可以根据 HTML 页面中的资源的 URL,来提前推送资源。 + +现在有很多网站已经开始使用 HTTP2 了,例如知乎: + +![服务器推送](https://user-images.githubusercontent.com/8088864/125283274-d83ea000-e34a-11eb-95d5-7881c4af0403.jpg) + +其中 h2 是指 HTTP2 协议,http/1.1 则是指 HTTP1.1 协议。 + +参考资料: + +- [半小时搞懂 HTTP、HTTPS和HTTP2](https://github.com/woai3c/Front-end-articles/blob/master/http-https-http2.md) + +### 3. 使用服务端渲染 + +客户端渲染: 获取 HTML 文件,根据需要下载 JavaScript 文件,运行文件,生成 DOM,再渲染。 + +服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。 + +- 优点:首屏渲染快,SEO 好。 +- 缺点:配置麻烦,增加了服务器的计算压力。 + +下面我用 Vue SSR 做示例,简单的描述一下 SSR 过程。 + +#### 客户端渲染过程 + +1. 访问客户端渲染的网站。 +2. 服务器返回一个包含了引入资源语句和 \
\
的 HTML 文件。 +3. 客户端通过 HTTP 向服务器请求资源,当必要的资源都加载完毕后,执行 new Vue() 开始实例化并渲染页面。 + +#### 服务端渲染过程 + +1. 访问服务端渲染的网站。 +2. 服务器会查看当前路由组件需要哪些资源文件,然后将这些文件的内容填充到 HTML 文件。如果有 ajax 请求,就会执行它进行数据预取并填充到 HTML 文件里,最后返回这个 HTML 页面。 +3. 当客户端接收到这个 HTML 页面时,可以马上就开始渲染页面。与此同时,页面也会加载资源,当必要的资源都加载完毕后,开始执行 new Vue() 开始实例化并接管页面。 + +从上述两个过程中可以看出,区别就在于第二步。客户端渲染的网站会直接返回 HTML 文件,而服务端渲染的网站则会渲染完页面再返回这个 HTML 文件。 + +这样做的好处是什么?是更快的内容到达时间 (time-to-content)。 + +假设你的网站需要加载完 abcd 四个文件才能渲染完毕。并且每个文件大小为 1 M。 + +这样一算:客户端渲染的网站需要加载 4 个文件和 HTML 文件才能完成首页渲染,总计大小为 4M(忽略 HTML 文件大小)。而服务端渲染的网站只需要加载一个渲染完毕的 HTML 文件就能完成首页渲染,总计大小为已经渲染完毕的 HTML 文件(这种文件不会太大,一般为几百K,我的个人博客网站(SSR)加载的 HTML 文件为 400K)。这就是服务端渲染更快的原因。 + +参考资料: + +- [vue-ssr-demo](https://github.com/woai3c/vue-ssr-demo) +- [Vue.js 服务器端渲染指南](https://ssr.vuejs.org/zh/) + +### 4. 静态资源使用 CDN + +内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。 + +#### CDN 原理 + +当用户访问一个网站时,如果没有 CDN,过程是这样的: + +1. 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。 +2. 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到网站服务器的 IP 地址。 +3. 本地 DNS 将 IP 地址发回给浏览器,浏览器向网站服务器 IP 地址发出请求并得到资源。 + +![没有CDN的资源请求](https://user-images.githubusercontent.com/8088864/125375921-8171ae80-e3bc-11eb-9d66-adb57433b67a.jpg) + +如果用户访问的网站部署了 CDN,过程是这样的: + +1. 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。 +2. 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到全局负载均衡系统(GSLB)的 IP 地址。 +3. 本地 DNS 再向 GSLB 发出请求,GSLB 的主要功能是根据本地 DNS 的 IP 地址判断用户的位置,筛选出距离用户较近的本地负载均衡系统(SLB),并将该 SLB 的 IP 地址作为结果返回给本地 DNS。 +4. 本地 DNS 将 SLB 的 IP 地址发回给浏览器,浏览器向 SLB 发出请求。 +5. SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器发回给浏览器。 +6. 浏览器再根据 SLB 发回的地址重定向到缓存服务器。 +7. 如果缓存服务器有浏览器需要的资源,就将资源发回给浏览器。如果没有,就向源服务器请求资源,再发给浏览器并缓存在本地。 + +![有CDN的资源请求](https://user-images.githubusercontent.com/8088864/125376046-baaa1e80-e3bc-11eb-84ba-c86cd8d63a7f.jpg) + +参考资料: + +- [CDN是什么?使用CDN有什么优势?](https://www.zhihu.com/question/36514327/answer/193768864) +- [CDN原理简析](https://juejin.cn/post/6844903873518239752) + +### 5. 将 CSS 放在文件头部,JavaScript 文件放在底部 + +所有放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染。如果这些 CSS 和 JS 需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件。 + +那为什么 CSS 文件还要放在头部呢? + +因为先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。 + +另外,JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。 + +### 6. 使用字体图标 iconfont 代替图片图标 + +字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。 + +#### 压缩字体文件 + +使用 [fontmin-webpack](https://github.com/patrickhulce/fontmin-webpack) 插件对字体文件进行压缩。 + +![fontmin-webpack](https://user-images.githubusercontent.com/8088864/125377089-efb77080-e3be-11eb-845b-d8992de47838.png) + +参考资料: + +- [fontmin-webpack](https://github.com/patrickhulce/fontmin-webpack) +- [Iconfont-阿里巴巴矢量图标库](https://www.iconfont.cn/) + +### 7. 善用缓存,不重复加载相同的资源 + +为了避免用户每次访问网站都得请求文件,我们可以通过添加 Expires 或 max-age 来控制这一行为。Expires 设置了一个时间,只要在这个时间之前,浏览器都不会请求文件,而是直接使用缓存。而 max-age 是一个相对时间,建议使用 max-age 代替 Expires 。 + +不过这样会产生一个问题,当文件更新了怎么办?怎么通知浏览器重新请求文件? + +可以通过更新页面中引用的资源链接地址,让浏览器主动放弃缓存,加载新资源。 + +具体做法是把资源地址 URL 的修改与文件内容关联起来,也就是说,只有文件内容变化,才会导致相应 URL 的变更,从而实现文件级别的精确缓存控制。什么东西与文件内容相关呢?我们会很自然的联想到利用[数据摘要要算法](https://cloud.tencent.com/developer/article/1584742)对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。 + +参考资料: + +- [webpack + express 实现文件精确缓存](https://github.com/woai3c/node-blog/blob/master/doc/node-blog7.md) +- [webpack-缓存](https://www.webpackjs.com/guides/caching/) +- [张云龙--大公司里怎样开发和部署前端代码?](https://www.zhihu.com/question/20790576/answer/32602154) + +### 8. 压缩文件 + +压缩文件可以减少文件下载时间,让用户体验性更好。 + +得益于 webpack 和 node 的发展,现在压缩文件已经非常方便了。 + +在 webpack 可以使用如下插件进行压缩: + +- JavaScript:UglifyPlugin +- CSS :MiniCssExtractPlugin +- HTML:HtmlWebpackPlugin + +其实,我们还可以做得更好。那就是使用 gzip 压缩。可以通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。当然,服务器也得支持这一功能。 + +gzip 是目前最流行和最有效的压缩方法。举个例子,我用 Vue 开发的项目构建后生成的 app.js 文件大小为 1.4MB,使用 gzip 压缩后只有 573KB,体积减少了将近 60%。 + +附上 webpack 和 node 配置 gzip 的使用方法。 + +下载插件 + +``` shell +npm install compression-webpack-plugin --save-dev +npm install compression +``` + +webpack 配置 + +``` js +const CompressionPlugin = require('compression-webpack-plugin'); + +module.exports = { + plugins: [new CompressionPlugin()], +} +``` + +node 配置 + +``` js +const compression = require('compression') +// 在其他中间件前使用 +app.use(compression()) +``` + +### 9. 图片优化 + +#### (1). 图片延迟加载 + +在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片很多的网站来说,一次性加载全部图片,会对用户体验造成很大的影响,所以需要使用图片延迟加载。 + +首先可以将图片这样设置,在页面不可见时图片不会加载: + +``` html + +``` + +等页面可见时,使用 JS 加载图片: + +``` js +const img = document.querySelector('img') +img.src = img.dataset.src +``` + +这样图片就加载出来了,完整的代码可以看一下参考资料。 + +参考资料: + +- [web 前端图片懒加载实现原理](https://juejin.cn/post/6844903482164510734) + +#### (2). 响应式图片 + +响应式图片的优点是浏览器能够根据屏幕大小自动加载合适的图片。 + +通过 picture 实现 + +``` html + + + + + +``` + +通过 @media 实现 + +``` css +@media (min-width: 769px) { + .bg { + background-image: url(bg1080.jpg); + } +} +@media (max-width: 768px) { + .bg { + background-image: url(bg768.jpg); + } +} +``` + +#### (3). 调整图片大小 + +例如,你有一个 1920 * 1080 大小的图片,用缩略图的方式展示给用户,并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。 + +所以,我们可以用两张图片来实行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种办法,即对大图进行延迟加载,在所有元素都加载完成后手动更改大图的 src 进行下载。 + +#### (4). 降低图片质量 + +例如 JPG 格式的图片,100% 的质量和 90% 质量的通常看不出来区别,尤其是用来当背景图的时候。我经常用 PS 切背景图时, 将图片切成 JPG 格式,并且将它压缩到 60% 的质量,基本上看不出来区别。 + +压缩方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。 + +以下附上 webpack 插件 image-webpack-loader 的用法。 + +``` shell +npm install --save-dev image-webpack-loader +``` + +webpack 配置 + +``` js +{ + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + use:[ + { + loader: 'url-loader', + options: { + limit: 10000, /* 图片大小小于1000字节限制时会自动转成 base64 码引用*/ + name: utils.assetsPath('img/[name].[hash:7].[ext]') + } + }, + /*对图片进行压缩*/ + { + loader: 'image-webpack-loader', + options: { + bypassOnDebug: true, + } + } + ] +} +``` + +参考资料: + +- [img图片在webpack中使用](https://juejin.cn/post/6844903816081457159) + +#### (5). 尽可能利用 CSS3 效果代替图片 + +有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。 + +#### (6). 使用 webp 格式的图片 + +WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。 + +参考资料: + +- [WebP 相对于 PNG、JPG 有什么优势?](https://www.zhihu.com/question/27201061) + +### 10. 通过 webpack 按需加载代码,提取第三库代码,减少 ES6 转为 ES5 的冗余代码 + +懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。 + +#### 根据文件内容生成文件名,结合 import 动态引入组件实现按需加载 + +通过配置 output 的 filename 属性可以实现这个需求。filename 属性的值选项中有一个 [contenthash],它将根据文件内容创建出唯一 hash。当文件内容发生变化时,[contenthash] 也会发生变化。 + +``` js +{ + output: { + filename: '[name].[contenthash].js', + chunkFilename: '[name].[contenthash].js', + path: path.resolve(__dirname, '../dist'), + }, +} +``` + +#### 提取第三方库 + +由于引入的第三方库一般都比较稳定,不会经常改变。所以将它们单独提取出来,作为长期缓存是一个更好的选择。 这里需要使用 webpack4 的 splitChunk 插件 cacheGroups 选项。 + +``` js +optimization: { + runtimeChunk: { + name: 'manifest' // 将 webpack 的 runtime 代码拆分为一个单独的 chunk。 + }, + splitChunks: { + cacheGroups: { + vendor: { + name: 'chunk-vendors', + test: /[\\/]node_modules[\\/]/, + priority: -10, + chunks: 'initial' + }, + common: { + name: 'chunk-common', + minChunks: 2, + priority: -20, + chunks: 'initial', + reuseExistingChunk: true + } + }, + } +}, +``` + +- **test**: 过滤 modules,默认为所有的 modules,可匹配模块路径或 chunk 名字,当匹配到某个 chunk 的名字时,这个 chunk 里面引入的所有 module 都会选中。可以传递的值类型:RegExp、String和Function。 +- **priority**: 权重,数字越大表示优先级越高。一个 module 可能会满足多个 cacheGroups 的正则匹配,到底将哪个缓存组应用于这个 module,取决于优先级。 +- **reuseExistingChunk**: 表示是否使用已有的 chunk,true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的,即几个 chunk 复用被拆分出去的一个 module。 +- **minChunks**(默认是1): 在分割之前,这个代码块最小应该被引用的次数(译注:保证代码块复用性,默认配置的策略是不需要多次引用也可以被分割) +- **chunks**(默认是async): initial、async和all。chunks改为all,表示同时对静态加载(initial)和动态加载(async)起作用。 +- **name**(打包的chunks的名字): 字符串或者函数(函数可以根据条件自定义名字) + +#### 减少 ES6 转为 ES5 的冗余代码 + +Babel 转化后的代码想要实现和原来代码一样的功能需要借助一些帮助函数,比如 + +``` js +class Person {} +``` + +会被转换为: + +``` js +"use strict"; + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +var Person = function Person() { + _classCallCheck(this, Person); +}; +``` + +这里 `_classCallCheck` 就是一个 `helper` 函数,如果在很多文件里都声明了类,那么就会产生很多个这样的 `helper` 函数。 + +这里的 `@babel/runtime` 包就声明了所有需要用到的帮助函数,而 `@babel/plugin-transform-runtime` 的作用就是将所有需要 `helper` 函数的文件,从 `@babel/runtime`包引进来: + +``` js +"use strict"; + +var _classCallCheck2 = require("@babel/runtime/helpers/classCallCheck"); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { default: obj }; +} + +var Person = function Person() { + (0, _classCallCheck3.default)(this, Person); +}; +``` + +这里就没有再编译出 helper 函数 classCallCheck 了,而是直接引用了 @babel/runtime 中的 helpers/classCallCheck。 + +**安装** + +``` shell +npm install --save-dev @babel/plugin-transform-runtime @babel/runtime +``` + +**使用** 在 .babelrc 文件中 + +``` json +{ + "plugins": [ + "@babel/plugin-transform-runtime" + ] +} +``` + +参考资料: + +- [Babel 7.1介绍 transform-runtime polyfill env](https://www.jianshu.com/p/d078b5f3036a) +- [webpack 懒加载](https://webpack.docschina.org/guides/lazy-loading/) +- [Vue 路由懒加载](https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#%E8%B7%AF%E7%94%B1%E6%87%92%E5%8A%A0%E8%BD%BD) +- [webpack 缓存](https://webpack.docschina.org/guides/caching/) +- [一步一步的了解webpack4的splitChunk插件](https://juejin.cn/post/6844903614759043079) + +### 11. 减少重绘重排 + +浏览器渲染过程 + +1. 解析HTML生成DOM树。 +2. 解析CSS生成CSSOM规则树。 +3. 将DOM树与CSSOM规则树合并在一起生成渲染树。 +4. 遍历渲染树开始布局,计算每个节点的位置大小信息。 +5. 将渲染树每个节点绘制到屏幕。 + +![渲染树生成](https://user-images.githubusercontent.com/8088864/125440124-9cc83d52-c342-4959-af1e-dc67cfe7d312.png) + +#### 重排 + +当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。 + +#### 重绘 + +当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致重排,例如改变字体颜色,只会导致重绘。记住,重排会导致重绘,重绘不会导致重排。 + +重排和重绘这两个操作都是非常昂贵的,因为 **JavaScript** 引擎线程与 **GUI** 渲染线程是互斥,它们同时只能一个在工作。 + +什么操作会导致重排? + +- 添加或删除可见的 **DOM** 元素 +- 元素位置改变 +- 元素尺寸改变 +- 内容改变 +- 浏览器窗口尺寸改变 + +如何减少重排重绘? + +- 用 **JavaScript** 修改样式时,最好不要直接写样式,而是替换 **class** 来改变样式。 +- 如果要对 **DOM** 元素执行一系列操作,可以将 **DOM** 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(**display:none**)或文档碎片(**DocumentFragment**),都能很好的实现这个方案。 + +### 12. 使用事件委托 + +事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。 + +``` html + +``` + +``` js +// good +document.querySelector('ul').onclick = (event) => { + const target = event.target; + if (target.nodeName === 'LI') { + console.log(target.innerHTML); + } +} + +// bad +document.querySelectorAll('li').forEach((e) => { + e.onclick = function() { + console.log(this.innerHTML); + } +}) +``` + +### 13. 注意程序的局部性 + +一个编写良好的计算机程序常常具有良好的局部性,它们倾向于引用最近引用过的数据项附近的数据项,或者最近引用过的数据项本身,这种倾向性,被称为局部性原理。有良好局部性的程序比局部性差的程序运行得更快。 + +**局部性通常有两种不同的形式:** + +- 时间局部性: 在一个具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来被多次引用。 +- 空间局部性: 在一个具有良好空间局部性的程序中,如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置。 + +时间局部性示例 + +``` js +function sum(arry) { + let i, sum = 0; + let len = arry.length; + + for (i = 0; i < len; i++) { + sum += arry[i]; + } + + return sum; +} +``` + +在这个例子中,变量sum在每次循环迭代中被引用一次,因此,对于sum来说,具有良好的时间局部性 + +空间局部性示例 + +**具有良好空间局部性的程序** + +``` js +// 二维数组 +function sum1(arry, rows, cols) { + let i, j, sum = 0; + + for (i = 0; i < rows; i++) { + for (j = 0; j < cols; j++) { + sum += arry[i][j]; + } + } + + return sum; +} +``` + +**空间局部性差的程序** + +``` js +// 二维数组 +function sum2(arry, rows, cols) { + let i, j, sum = 0; + + for (j = 0; j < cols; j++) { + for (i = 0; i < rows; i++) { + sum += arry[i][j]; + } + } + + return sum; +} +``` + +看一下上面的两个空间局部性示例,像示例中从每行开始按顺序访问数组每个元素的方式,称为具有步长为1的引用模式。 如果在数组中,每隔k个元素进行访问,就称为步长为k的引用模式。 一般而言,随着步长的增加,空间局部性下降。 + +这两个例子有什么区别?区别在于第一个示例是按行扫描数组,每扫描完一行再去扫下一行;第二个示例是按列来扫描数组,扫完一行中的一个元素,马上就去扫下一行中的同一列元素。 + +数组在内存中是按照行顺序来存放的,结果就是逐行扫描数组的示例得到了步长为 1 引用模式,具有良好的空间局部性;而另一个示例步长为 rows,空间局部性极差。 + +**性能测试** + +运行环境: + +- cpu: i7-10510U +- 浏览器: 83.0.4103.61 + +对一个长度为9000的二维数组(子数组长度也为9000)进行10次空间局部性测试,时间(毫秒)取平均值,结果如下: + +``` js +function sum2(arry, rows, cols) { + let i, j, sum = 0; + + for (j = 0; j < cols; j++) { + for (i = 0; i < rows; i++) { + sum += arry[i][j]; + } + } + + return sum; +} + +// 二维数组 +function sum1(arry, rows, cols) { + let i, j, sum = 0; + + for (i = 0; i < rows; i++) { + for (j = 0; j < cols; j++) { + sum += arry[i][j]; + } + } + + return sum; +} + +var arry = new Array(9000).fill(new Array(9000).fill(1)); + +let ts = 0; +for (let i = 0; i < 10; i++) { + const startTime = new Date().valueOf(); + sum1(arry, 9000, 9000); + ts += (new Date().valueOf() - startTime); +} + +console.log('sum1: ' + (ts / 10)); // 81.5ms + +let ts2 = 0; +for (let i = 0; i < 10; i++) { + const startTime = new Date().valueOf(); + sum2(arry, 9000, 9000); + ts2 += (new Date().valueOf() - startTime); +} + +console.log('sum2: ' + (ts2 / 10)); // 167.3ms +``` + +所用示例为上述两个空间局部性示例 + +| 步长为1(sum1) | 步长为9000(sum2) | +| ---- | ---- | +| 81.5ms | 167.3ms | + +从以上测试结果来看,步长为 1 的数组执行时间比步长为 9000 的数组快了一个数量级。 + +总结: + +- 重复引用相同变量的程序具有良好的时间局部性 +- 对于具有步长为 k 的引用模式的程序,步长越小,空间局部性越好;而在内存中以大步长跳来跳去的程序空间局部性会很差 + +参考资料: + +- [深入理解计算机系统](https://book.douban.com/subject/26912767/) + +### 14. if-else 对比 switch + +当判断条件数量越来越多时,越倾向于使用 switch 而不是 if-else。 + +``` js +if (color == 'blue') { + +} else if (color == 'yellow') { + +} else if (color == 'white') { + +} else if (color == 'black') { + +} else if (color == 'green') { + +} else if (color == 'orange') { + +} else if (color == 'pink') { + +} + +switch (color) { + case 'blue': + + break; + case 'yellow': + + break; + case 'white': + + break; + case 'black': + + break; + case 'green': + + break; + case 'orange': + + break; + case 'pink': + + break; +} +``` + +像以上这种情况,使用 switch 是最好的。假设 color 的值为 pink,则 if-else 语句要进行 7 次判断,switch 只需要进行一次判断。 从可读性来说,switch 语句也更好。 + +从使用时机来说,当条件值大于两个的时候,使用 switch 更好。不过 if-else 也有 switch 无法做到的事情,例如有多个判断条件的情况下,无法使用 switch。 + +### 15. 查找表 + +当条件语句特别多时,使用 switch 和 if-else 不是最佳的选择,这时不妨试一下查找表。查找表可以使用数组和对象来构建。 + +``` js +switch (index) { + case '0': + return result0; + case '1': + return result1; + case '2': + return result2; + case '3': + return result3; + case '4': + return result4; + case '5': + return result5; + case '6': + return result6; + case '7': + return result7; + case '8': + return result8; + case '9': + return result9; + case '10': + return result10; + case '11': + return result11; +} +``` + +可以将这个 switch 语句转换为查找表 + +``` js +const results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11]; + +return results[index]; +``` + +如果条件语句不是数值而是字符串,可以用对象来建立查找表 + +``` js +const map = { + red: result0, + green: result1, +}; + +return map[color]; +``` + +### 16. 避免页面卡顿 + +**60fps 与设备刷新率** + +目前大多数设备的屏幕刷新率为 60 次/秒。因此,如果在页面中有一个动画或渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。 其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工作要做,因此您的所有工作需要在 10 毫秒内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。 此现象通常称为卡顿,会对用户体验产生负面影响。 + +![网页渲染流程](https://user-images.githubusercontent.com/8088864/125445172-29d132ea-e485-49c7-b32d-172956c4349b.jpeg) + +假如你用 JavaScript 修改了 DOM,并触发样式修改,经历重排重绘最后画到屏幕上。如果这其中任意一项的执行时间过长,都会导致渲染这一帧的时间过长,平均帧率就会下降。假设这一帧花了 50 ms,那么此时的帧率为 1s / 50ms = 20fps,页面看起来就像卡顿了一样。 + +对于一些长时间运行的 JavaScript,我们可以使用定时器进行切分,延迟执行。 + +``` js +for (let i = 0, len = arry.length; i < len; i++) { + process(arry[i]); +} +``` + +假设上面的循环结构由于 process() 复杂度过高或数组元素太多,甚至两者都有,可以尝试一下切分。 + +``` js +const todo = arry.concat(); +setTimeout(function(){ + process(todo.shift()); + if (todo.length) { + setTimeout(arguments.callee, 25); + } else { + callback(arry); + } +}, 25); +``` + +如果有兴趣了解更多,可以查看一下高性能JavaScript第 6 章和[高效前端:Web高效编程与优化实践](https://book.douban.com/subject/30170670/)第 3 章。 + +### 17. 使用 requestAnimationFrame 来实现视觉变化 + +从第 16 点我们可以知道,大多数设备屏幕刷新率为 60 次/秒,也就是说每一帧的平均时间为 16.66 毫秒。在使用 JavaScript 实现动画效果的时候,最好的情况就是每次代码都是在帧的开头开始执行。而保证 JavaScript 在帧开始时运行的唯一方式是使用 `requestAnimationFrame`。 + +``` js +/** + * If run as a requestAnimationFrame callback, this + * will be run at the start of the frame. + */ +function updateScreen(time) { + // Make visual updates here. +} + +requestAnimationFrame(updateScreen); +``` + +如果采取 setTimeout 或 setInterval 来实现动画的话,回调函数将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿。 + +![requestAnimationFrame执行点](https://user-images.githubusercontent.com/8088864/125448006-c889aac7-f5d6-4a21-a4fe-b4c6c0cdf197.jpg) + +### 18. 使用 Web Workers + +Web Worker 使用其他工作线程从而独立于主线程之外,它可以执行任务而不干扰用户界面。一个 worker 可以将消息发送到创建它的 JavaScript 代码, 通过将消息发送到该代码指定的事件处理程序(反之亦然)。 + +Web Worker 适用于那些处理纯数据,或者与浏览器 UI 无关的长时间运行脚本。 + +创建一个新的 worker 很简单,指定一个脚本的 URI 来执行 worker 线程(main.js): + +``` js +var myWorker = new Worker('worker.js'); +// 你可以通过postMessage() 方法和onmessage事件向worker发送消息。 +first.onchange = function() { + myWorker.postMessage([first.value,second.value]); + console.log('Message posted to worker'); +} + +second.onchange = function() { + myWorker.postMessage([first.value,second.value]); + console.log('Message posted to worker'); +} +``` + +在 worker 中接收到消息后,我们可以写一个事件处理函数代码作为响应(worker.js): + +``` js +onmessage = function(e) { + console.log('Message received from main script'); + var workerResult = 'Result: ' + (e.data[0] * e.data[1]); + console.log('Posting message back to main script'); + postMessage(workerResult); +} +``` + +onmessage处理函数在接收到消息后马上执行,代码中消息本身作为事件的data属性进行使用。这里我们简单的对这2个数字作乘法处理并再次使用postMessage()方法,将结果回传给主线程。 + +回到主线程,我们再次使用onmessage以响应worker回传的消息: + +``` js +myWorker.onmessage = function(e) { + result.textContent = e.data; + console.log('Message received from worker'); +} +``` + +在这里我们获取消息事件的data,并且将它设置为result的textContent,所以用户可以直接看到运算的结果。 + +不过在worker内,不能直接操作DOM节点,也不能使用window对象的默认方法和属性。然而你可以使用大量window对象之下的东西,包括WebSockets,IndexedDB以及FireFox OS专用的Data Store API等数据存储机制。 + +参考资料: + +- [Web Workers](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers) + +### 19. 使用位操作 + +JavaScript 中的数字都使用 IEEE-754 标准以 64 位格式存储。但是在位操作中,数字被转换为有符号的 32 位格式。即使需要转换,位操作也比其他数学运算和布尔操作快得多。 + +#### 取模 + +由于偶数的最低位为 0,奇数为 1,所以取模运算可以用位操作来代替。 + +``` js +if (value % 2) { + // 奇数 +} else { + // 偶数 +} +// 位操作 +if (value & 1) { + // 奇数 +} else { + // 偶数 +} +``` + +#### 取整 + +``` js +~~10.12 // 10 +~~10 // 10 +~~'1.5' // 1 +~~undefined // 0 +~~null // 0 +``` + +#### 位掩码 + +``` js +const a = 1 +const b = 2 +const c = 4 +const options = a | b | c +``` + +通过定义这些选项,可以用按位与操作来判断 a/b/c 是否在 options 中。 + +``` js +// 选项 b 是否在选项中 +if (b & options) { + // ... +} +``` + +### 20. 不要覆盖原生方法 + +无论你的 JavaScript 代码如何优化,都比不上原生方法。因为原生方法是用低级语言写的(C/C++),并且被编译成机器码,成为浏览器的一部分。当原生方法可用时,尽量使用它们,特别是数学运算和 DOM 操作。 + +### 21. 降低 CSS 选择器的复杂性 + +#### (1). 浏览器读取选择器,遵循的原则是从选择器的右边到左边读取 + +看个示例 + +``` css +#block .text p { + color: red; +} +``` + +1. 查找所有 P 元素。 +2. 查找结果 1 中的元素是否有类名为 text 的父元素 +3. 查找结果 2 中的元素是否有 id 为 block 的父元素 + +#### (2). CSS 选择器优先级 + +``` +内联 > ID选择器 > 类选择器 > 标签选择器 +``` + +根据以上两个信息可以得出结论。 + +1. 选择器越短越好。 +2. 尽量使用高优先级的选择器,例如 ID 和类选择器。 +3. 避免使用通配符 *。 + +最后要说一句,据我查找的资料所得,CSS 选择器没有优化的必要,因为最慢和慢快的选择器性能差别非常小。 + +### 22. 使用 flexbox 而不是较早的布局模型 + +在早期的 CSS 布局方式中我们能对元素实行绝对定位、相对定位或浮动定位。而现在,我们有了新的布局方式 [flexbox](https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox),它比起早期的布局方式来说有个优势,那就是性能比较好。 + +下面的截图显示了在 1300 个框上使用浮动的布局开销: + +![float布局的元素](https://user-images.githubusercontent.com/8088864/125547454-c911b26b-4a1c-44d8-9044-e83a09dc618d.jpg) + +然后我们用 flexbox 来重现这个例子: + +![flexbox布局的元素](https://user-images.githubusercontent.com/8088864/125547509-ecf25fd0-a9ef-438c-827a-987ea0bb9ae5.jpg) + +现在,对于相同数量的元素和相同的视觉外观,布局的时间要少得多(本例中为分别 3.5 毫秒和 14 毫秒)。 + +不过 flexbox 兼容性还是有点问题,不是所有浏览器都支持它,所以要谨慎使用。 + +各浏览器兼容性: + +- Chrome 29+ +- Firefox 28+ +- Internet Explorer 11 +- Opera 17+ +- Safari 6.1+ (prefixed with -webkit-) +- Android 4.4+ +- iOS 7.1+ (prefixed with -webkit-) + +但是在可能的情况下,至少应研究布局模型对网站性能的影响,并且采用最大程度减少网页执行开销的模型。 + +在任何情况下,不管是否选择 Flexbox,都应当在应用的高压力点期间尝试完全避免触发布局! + +### 23. 使用 transform 和 opacity 属性更改来实现动画 + +在 CSS 中,transforms 和 opacity 这两个属性更改不会触发重排与重绘,它们是可以由合成器(composite)单独处理的属性。 + +![使用 transform 和 opacity 属性更改来实现动画](https://user-images.githubusercontent.com/8088864/125547800-ab61c27b-23fb-45bd-9d6a-2585df8d804e.jpeg) + +### 24. 合理使用规则,避免过度优化 + +性能优化主要分为两类: + +1. 加载时优化 +2. 运行时优化 + +上述 23 条建议中,属于加载时优化的是前面 10 条建议,属于运行时优化的是后面 13 条建议。通常来说,没有必要 23 条性能优化规则都用上,根据网站用户群体来做针对性的调整是最好的,节省精力,节省时间。 + +在解决问题之前,得先找出问题,否则无从下手。所以在做性能优化之前,最好先调查一下网站的加载性能和运行性能。 + +#### 检查加载性能 + +一个网站加载性能如何主要看白屏时间和首屏时间。 + +- 白屏时间:指从输入网址,到页面开始显示内容的时间。 +- 首屏时间:指从输入网址,到页面完全渲染的时间。 + +将以下脚本放在 \ 前面就能获取白屏时间。 + +``` html + +``` + +在 `window.onload` 事件里执行 `new Date() - performance.timing.navigationStart` 即可获取首屏时间。 + +#### 检查运行性能 + +配合 chrome 的开发者工具,我们可以查看网站在运行时的性能。 + +打开网站,按 F12 选择 performance,点击左上角的灰色圆点,变成红色就代表开始记录了。这时可以模仿用户使用网站,在使用完毕后,点击 stop,然后你就能看到网站运行期间的性能报告。如果有红色的块,代表有掉帧的情况;如果是绿色,则代表 FPS 很好。performance 的具体使用方法请用搜索引擎搜索一下,毕竟篇幅有限。 + +通过检查加载和运行性能,相信你对网站性能已经有了大概了解。所以这时候要做的事情,就是使用上述 23 条建议尽情地去优化你的网站,加油! + +参考资料: + +- [performance.timing.navigationStart](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming/navigationStart) + +其他参考资料 + +- 高性能网站建设指南 +- Web性能权威指南 +- 高性能JavaScript +- [高效前端:Web高效编程与优化实践](https://book.douban.com/subject/30170670/) + + +## 如何进行网站性能优化 + +[雅虎 Best Practices for Speeding Up Your Web Site](https://developer.yahoo.com/performance/rules.html): + +- content 方面 + + 1. 减少 HTTP 请求:合并文件、CSS 精灵、inline Image + 2. 减少 DNS 查询:DNS 查询完成之前浏览器不能从这个主机下载任何任何文件。方法:DNS 缓存、将资源分布到恰当数量的主机名,平衡并行下载和 DNS 查询 + 3. 避免重定向:多余的中间访问 + 4. 使 Ajax 可缓存 + 5. 非必须组件延迟加载 + 6. 未来所需组件预加载 + 7. 减少 DOM 元素数量 + 8. 将资源放到不同的域下:浏览器同时从一个域下载资源的数目有限,增加域可以提高并行下载量 + 9. 减少 iframe 数量 + 10. 不要 404 + +- Server 方面 + 1. 使用 CDN + 2. 添加 Expires 或者 Cache-Control 响应头 + 3. 对组件使用 Gzip 压缩 + 4. 配置 ETag + 5. Flush Buffer Early + 6. Ajax 使用 GET 进行请求 + 7. 避免空 src 的 img 标签 +- Cookie 方面 + 1. 减小 cookie 大小 + 2. 引入资源的域名不要包含 cookie +- css 方面 + 1. 将样式表放到页面顶部 + 2. 不使用 CSS 表达式 + 3. 使用``不使用@import + 4. 不使用 IE 的 Filter +- Javascript 方面 + 1. 将脚本放到页面底部 + 2. 将 javascript 和 css 从外部引入 + 3. 压缩 javascript 和 css + 4. 删除不需要的脚本 + 5. 减少 DOM 访问 + 6. 合理设计事件监听器 +- 图片方面 + 1. 优化图片:根据实际颜色需要选择色深、压缩 + 2. 优化 css 精灵 + 3. 不要在 HTML 中拉伸图片 + 4. 保证 favicon.ico 小并且可缓存 +- 移动方面 + 1. 保证组件小于 25k + 2. Pack Components into a Multipart Document + +## 强缓存与协商缓存 + +### 浏览器缓存 + +当浏览器去请求某个文件的时候,服务端就在response header里面对该文件做了缓存配置。缓存的时间、缓存类型都由服务端控制 + +#### 缓存优点 + +1. 减少不必要的数据传输,节省带宽 +2. 减少服务器的负担,提升网站性能 +3. 加快了客户端加载网页的速度,用户体验友好 + +#### 缓存缺点 + +资源如果有更改,会导致客户端不及时更新就会造成用户获取信息滞后 + +#### 缓存流程 + +浏览器第一次请求时 + +![浏览器缓存第一次请求](https://user-images.githubusercontent.com/8088864/125554789-a7d7d647-b89f-4c84-a326-5af87e6782f6.png) + +浏览器后续在进行请求时 + +![浏览器缓存再次请求](https://user-images.githubusercontent.com/8088864/125554810-255dcfd2-a1f0-4e09-a329-56bacdee6d22.png) + +从上图可以知道,浏览器缓存包括两种类型,即强缓存(本地缓存)和协商缓存,浏览器在第一次请求发生后,再次请求时 + +- 浏览器在请求某一资源时,会先获取该资源缓存的header信息,判断是否命中强缓存(`cache-control`和`expires`信息),若命中直接从缓存中获取资源信息,包括缓存header信息;本次请求根本就不会与服务器进行通信。 + +请求头信息 + +``` +Accept: xxx +Accept-Encoding: gzip,deflate +Accept-Language: zh-cn +Connection: keep-alive +Host: xxx +Referer: xxx +User-Agent: xxx +``` + +来自缓存的响应头的信息 + +``` +Accept-Ranges: bytes +Cache-Control: max-age= xxxx +Content-Encoding: gzip +Content-length: 3333 +Content-Type: application/javascript +Date: xxx +Expires: xxx +Last-Modified: xxx +Server: 服务器 +``` + +- 如果没有命中强缓存,浏览器会发送请求到服务器,请求会携带第一次请求返回的有关缓存的header字段信息(`Last-Modified`/`If-Modified-Since`和`Etag`/`If-None-Match`),由服务器根据请求中的相关header信息来比对结果是否协商缓存命中;若命中,则服务器返回新的响应header信息更新缓存中的对应header信息,但是并不返回资源内容,它会告知浏览器可以直接从缓存获取;否则返回最新的资源内容。 + +强缓存与协商缓存的区别,可以用下表来进行描述: + +| | 获取资源形式 | 状态码 | 发送请求到服务器 | +| ---- | ---- | ---- | ---- | +| **强缓存** | 从缓存取 | 200(from cache) | 否,直接从缓存取 | +| **协商缓存** | 从缓存取 | 304(not modified) | 是,正如其名,通过服务器来告知缓存是否可用 | + +### 强缓存相关的header字段 + +强缓存上面已经介绍了,直接从缓存中获取资源而不经过服务器;与强缓存相关的header字段有两个: + +1. **expires**: 这是http1.0时的规范;它的值为一个绝对时间的GMT格式的时间字符串,如**Mon, 10 Jun 2015 21:31:12 GMT**,如果发送请求的时间在expires之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源。 +2. **cache-control:max-age=number**: 这是http1.1时出现的header信息,主要是利用该字段的max-age值来进行判断,它是一个相对值;资源第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行;cache-control除了该字段外,还有下面几个比较常用的设置值: + +- **no-cache**: 不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。 +- **no-store**: 直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。 +- **public**: 可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。 +- **private**: 只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。 + +**注意:如果cache-control与expires同时存在的话,cache-control的优先级高于expires。** + +### 协商缓存相关的header字段 + +协商缓存都是由服务器来确定缓存资源是否可用的,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问,这主要涉及到下面两组header字段,这两组搭档都是成对出现的,即第一次请求的响应头带上某个字段(`Last-Modified`或者`Etag`),则后续请求则会带上对应的请求字段(`If-Modified-Since`或者`If-None-Match`),若响应头没有`Last-Modified`或者`Etag`字段,则请求头也不会有对应的字段。 + +#### 1. Last-Modified/If-Modified-Since + +二者的值都是GMT格式的时间字符串,具体过程: + +- 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在response的header加上`Last-Modified`的header,这个header表示这个资源在服务器上的最后修改时间 + +- 浏览器再次跟服务器请求这个资源时,在request的header上加上`If-Modified-Since`的header,这个header的值就是上一次请求时返回的Last-Modified的值 + +- 服务器再次收到资源请求时,根据浏览器传过来`If-Modified-Since`和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回`304 Not Modified`,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回`304 Not Modified`的响应时,response header中不会再添加`Last-Modified`的header,因为既然资源没有变化,那么`Last-Modified`也就不会改变,这是服务器返回304时的response header + +- 浏览器收到304的响应后,就会从缓存中加载资源 + +- 如果协商缓存没有命中,浏览器直接从服务器加载资源时,`Last-Modified`的Header在重新加载的时候会被更新,下次请求时,`If-Modified-Since`会启用上次返回的`Last-Modified`值 + +#### 2. Etag/If-None-Match + +这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变;其判断过程与**Last-Modified/If-Modified-Since**类似,与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。 + +### 既生Last-Modified何生Etag + +你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题: + +- 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET; + +- 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒); + +- 某些服务器不能精确的得到文件的最后修改时间。 + +这时,利用Etag能够更加准确的控制缓存,因为Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符。 + +**注意: Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。** + +### 用户的行为对缓存的影响 + +| 用户操作 | Expires/Cache-Control | Last-Modified/ETag | +| ---- | ---- | ---- | +| 地址栏回车 | 有效 | 有效 | +| 页面链接条状 | 有效 | 有效 | +| 新开窗口 | 有效 | 有效 | +| 前进后退 | 有效 | 有效 | +| F5刷新 | 无效 | 有效 | +| Ctrl + F5强制刷新 | 无效 | 无效 | + +### 强缓存如何重新加载缓存缓存过的资源 + +使用强缓存时,浏览器不会发送请求到服务端,根据设置的缓存时间浏览器一直从缓存中获取资源,在这期间若资源产生了变化,浏览器就在缓存期内就一直得不到最新的资源,那么如何防止这种事情发生呢? + +**通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新资源。** + +``` html + +... +
app
+``` + +这样每次文件改变后就会生成新的query值,这样query值不同,也就是页面引用的资源路径不同了,之前缓存过的资源就被浏览器忽略了,因为资源请求的路径变了。 + +## HTTP 各版本特点与区别 + +HTTP协议到现在为止总共经历了3个版本的演化,第一个HTTP协议诞生于1989年3月。 + +| 版本 | 功能 | 备注 | +| ---- | ---- | ---- | +| HTTP 0.9 | 仅支持 Get
仅能访问 HTML 格式资源 | 简单单一 | +| HTTP 1.0 | 新增POST,DELETE,PUT,HEADER等方式
增加请求头和响应头概念,指定协议版本号,携带其他元信息(状态码、权限、缓存、内容编码)
扩展传输内容格式(图片、音视频、二进制等都可以传输) | 存活时间短 | +| HTTP 1.1 | 长连接:新增 Connection 字段,可以通过keep-alive保持长连接
管道化:一次连接就形成一次管道,管道内进行多次有序响应。允许向服务端发生多次请求,但是响应按序返回
缓存处理:新增 cache-control 和 etag 首部字段
断点续传
状态码增加 | 当前主流版本号
存在Header 重复问题 | +| HTTP 2.0 | 二进制分帧:数据体和头信息可以都是二进制,统称帧
多路复用与数据流:能同时发送和响应多个请求,通过数据流来传输
头部压缩:对 Header 进行压缩,避免重复浪费
服务器推送:服务器可以向客户端主动发送资源 | 2005发布 | + +### 1、HTTP 0.9 + +HTTP 0.9是第一个版本的HTTP协议,已过时。它的组成极其简单,只允许客户端发送GET这一种请求,且不支持请求头。由于没有协议头,造成了HTTP 0.9协议只支持一种内容,即纯文本。不过网页仍然支持用HTML语言格式化,同时无法插入图片。 + +HTTP 0.9具有典型的无状态性,每个事务独立进行处理,事务结束时就释放这个连接。由此可见,HTTP协议的无状态特点在其第一个版本0.9中已经成型。一次HTTP 0.9的传输首先要建立一个由客户端到Web服务器的TCP连接,由客户端发起一个请求,然后由Web服务器返回页面内容,然后连接会关闭。如果请求的页面不存在,也不会返回任何错误码。 + +### 2、HTTP 1.0 + +HTTP协议的第二个版本,第一个在通讯中指定版本号的HTTP协议版本,至今仍被广泛采用。相对于HTTP 0.9 增加了如下主要特性: + +- 请求与响应支持头域 +- 响应对象以一个响应状态行开始 +- 响应对象不只限于超文本 +- 开始支持客户端通过POST方法向Web服务器提交数据,支持GET、HEAD、POST方法 +- (短连接)每一个请求建立一个TCP连接,请求完成后立马断开连接。这将会导致2个问题:连接无法复用,队头阻塞(head of line blocking)。连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类请求影响较大。队头阻塞(head of line blocking) + +### 3、HTTP 1.1 + +HTTP协议的第三个版本是HTTP 1.1,是目前使用最广泛的协议版本 。HTTP 1.1是目前主流的HTTP协议版本,因此这里就多花一些笔墨介绍一下HTTP 1.1的特性。 + +HTTP 1.1引入了许多关键性能优化:keepalive连接,chunked编码传输,字节范围请求,请求流水线等 + +#### Persistent Connection(keepalive连接) + +允许HTTP设备在事务处理结束之后将TCP连接保持在打开的状态,以便未来的HTTP请求重用现在的连接,直到客户端或服务器端决定将其关闭为止。在HTTP1.0中使用长连接需要添加请求头 `Connection: Keep-Alive`,而在HTTP 1.1 所有的连接默认都是长连接,除非特殊声明不支持( HTTP请求报文首部加上`Connection: close` )。服务器端按照FIFO原则来处理不同的Request。 + +![长连接(keepalive连接)](https://user-images.githubusercontent.com/8088864/125572282-1b48362e-ed29-42a1-9882-3710ab106b76.jpg) + +#### chunked编码传输 + +该编码将实体分块传送并逐块标明长度,直到长度为0块表示传输结束,这在实体长度未知时特别有用(比如由数据库动态产生的数据) + +#### 字节范围请求 + +HTTP1.1支持传送内容的一部分。比方说,当客户端已经有内容的一部分,为了节省带宽,可以只向服务器请求一部分。该功能通过在请求消息中引入了range头域来实现,它允许只请求资源的某个部分。在响应消息中Content-Range头域声明了返回的这部分对象的偏移值和长度。如果服务器相应地返回了对象所请求范围的内容,则响应码206(Partial Content) + +#### 断点续传 + +Header 字段 + +服务端 + +Accept-Ranges:表示服务器支持断点续传,并且数据传输以字节为单位 + +Etag:资源的唯一 tag 后端自定义,验证文件是否修改过。修改过就重新重头传输 + +Last-Modified:文件上次修改时间 + +Content-Range:返回数据范围 + +客户端 + +If-Range:服务器给的 Etag 值 + +Range:请求的数据范围 + +If-Modified-Since: 将服务器响应的 Last-Modified 保存, 下次发送可以携带,后台接受判断文件是否修改,没有可以返回 304状态码,叫客户端使用缓存数据,避免重复发出资源。 + +流程 + +![断点续传](https://user-images.githubusercontent.com/8088864/125573335-f1eda73b-ad4f-470a-808f-caa393e38b2e.png) + +**注意:断点续传后台返回状态码为 206。** + +#### Pipelining(请求流水线) + +#### 其他特性 + +另外,HTTP 1.1还新增了如下特性: + +- 请求消息和响应消息都支持Host头域:在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。因此,Host头的引入就很有必要了。 + +- 新增了一批Request method:HTTP1.1增加了OPTIONS, PUT, DELETE, TRACE, CONNECT方法 + +- 缓存处理:HTTP/1.1在1.0的基础上加入了一些cache的新特性,引入了实体标签,一般被称为e-tags,新增更为强大的Cache-Control头。 + +### 4、HTTP 2.0 + +HTTP 2.0是下一代HTTP协议。主要特点有: + +#### 二进制分帧 + +HTTP 2.0最大的特点:不会改动HTTP 的语义,HTTP 方法、状态码、URI 及首部字段,等等这些核心概念上一如往常,却能致力于突破上一代标准的性能限制,改进传输性能,实现低延迟和高吞吐量。而之所以叫2.0,是在于新增的二进制分帧层。在二进制分帧层上, HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。 + +![二进制分帧](https://user-images.githubusercontent.com/8088864/125574741-7645e5f9-3476-44f3-94eb-4a3aaebce2ae.jpg) + +#### 多路复用 + +HTTP 2.0 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。 + +通过单一的 HTTP2.0连接连续发起多重请求-响应消息,即客户端和服务器可以同时发送多个请求和响应,而不用顺序一一对应。 + +每个数据流以HTTP消息的形式发送,HTTP消息被分为独立的帧,然后由一或多个帧组成,这些帧可以乱序发送,接收端根据这些帧的标识符号和首部将信息重新组装起来。 + +默认什么情况下使用同一个连接 + +- 同一个域名下的资源 +- 不同域名但是满足两个条件:1)解析到同一个 IP;2)使用同一个证书 + +#### 头部压缩 + +当一个客户端向相同服务器请求许多资源时,像来自同一个网页的图像,将会有大量的请求看上去几乎同样的,这就需要压缩技术对付这种几乎相同的信息。 + +由于头信息使用文本,没有压缩,请求时候会来回重复传递,造成流量浪费。 + +参考[HTTP2头部压缩技术介绍](https://imququ.com/post/header-compression-in-http2.html) + +头部压缩需要支持 HTTP2的浏览器和服务器之间: + +- 维护一份相同的静态字典(包含常见的头部名称,以及常见的头部名称与值的组合) +- 维护一份相同的动态字典,动态添加内容(即实际的 Header 值) +- 支持基于静态哈夫曼码表的哈夫曼编码(uffman Coding) + +原理图: + +![http头部压缩原理](https://user-images.githubusercontent.com/8088864/125578550-82fd62aa-eb21-4813-87d1-19904e1b42fc.png) + +总结: 通过映射表,传递对应编码和值来达到压缩。 + +#### 随时复位 + +HTTP1.1一个缺点是当HTTP信息有一定长度大小数据传输时,你不能方便地随时停止它,中断TCP连接的代价是昂贵的。使用HTTP2的RST_STREAM将能方便停止一个信息传输,启动新的信息,在不中断连接的情况下提高带宽利用效率。 + +#### 服务器端推流 + +Server Push。客户端请求一个资源X,服务器端判断也许客户端还需要资源Z,在无需事先询问客户端情况下将资源Z推送到客户端,客户端接受到后,可以缓存起来以备后用。 + +#### 优先权和依赖 + +每个流都有自己的优先级别,会表明哪个流是最重要的,客户端会指定哪个流是最重要的,有一些依赖参数,这样一个流可以依赖另外一个流。优先级别可以在运行时动态改变,当用户滚动页面时,可以告诉浏览器哪个图像是最重要的,你也可以在一组流中进行优先筛选,能够突然抓住重点流。 + +## 队头阻塞以及解决办法 + +### 前言 + +通常我们提到队头阻塞,指的可能是TCP协议中的队头阻塞,但是HTTP1.1中也有一个类似TCP队头阻塞的问题,下面各自介绍一下。 + +### TCP队头阻塞 + +队头阻塞(head-of-line blocking)发生在一个TCP分节丢失,导致其后续分节不按序到达接收端的时候。该后续分节将被接收端一直保持直到丢失的第一个分节被发送端重传并到达接收端为止。该后续分节的延迟递送确保接收应用进程能够按照发送端的发送顺序接收数据。这种为了达到完全有序而引入的延迟机制,非常有用,但也有不利之处。 + +假设在单个TCP连接上发送语义独立的消息,比如说服务器可能发送3幅不同的图像供Web浏览器显示。为了营造这几幅图像在用户屏幕上并行显示的效果,服务器先发送第一幅图像的一个断片,再发送第二幅图像的一个断片,然后再发送第三幅图像的一个断片;服务器重复这个过程,直到这3幅图像全部成功地发送到浏览器为止。 + +要是第一幅图像的某个断片内容的TCP分节丢失了,客户端将保持已到达的不按序的所有数据,直到丢失的分节重传成功。这样不仅延缓了第一幅图像数据的递送,也延缓了第二幅和第三幅图像数据的递送。 + +### HTTP队头阻塞 + +上面用浏览器请求图片资源举例子,但实际上HTTP自身也有类似TCP队头阻塞的情况。要介绍HTTP队头阻塞,就需要先讲讲HTTP的管道化(pipelining)。 + +#### HTTP管道化是什么 + +HTTP1.1 允许在持久连接上可选的使用请求管道。这是相对于keep-alive连接的又一性能优化。在响应到达之前,可以将多条请求放入队列,当第一条请求发往服务器的时候,第二第三条请求也可以开始发送了,在高延时网络条件下,这样做可以降低网络的环回时间,提高性能。 + +非管道化与管道化的区别示意图 + +![HTTP非管道化与管道化](https://user-images.githubusercontent.com/8088864/125586316-36604fa7-fcc1-453b-9ae3-4c84b39690bd.png) + +#### HTTP管道化产生的背景 + +在一般情况下,HTTP遵守“请求-响应”的模式,也就是客户端每次发送一个请求到服务端,服务端返回响应。这种模式非常容易理解,但是效率并不是那么高,为了提高速度和效率,人们做了很多尝试: + +- 最简单的情况下,服务端一旦返回响应后就会把对应的连接关闭,客户端的多个请求实际上是串行发送的。 +- 除此之外,客户端可以选择同时创建多个连接,在多个连接上并行的发送不同请求。但是创建更多连接也带来了更多的消耗,当前大部分浏览器都会限制对同一个域名的连接数。 +- 从HTTP1.0开始增加了持久连接的概念(HTTP1.0的Keep-Alive和HTTP1.1的persistent),可以使HTTP能够复用已经创建好的连接。客户端在收到服务端响应后,可以复用上次的连接发送下一个请求,而不用重新建立连接。 +- 现代浏览器大多采用并行连接与持久连接共用的方式提高访问速度,对每个域名建立并行地少量持久连接。 +- 而在持久连接的基础上,HTTP1.1进一步地支持在持久连接上使用管道化(pipelining)特性。管道化允许客户端在已发送的请求收到服务端的响应之前发送下一个请求,借此来减少等待时间提高吞吐;如果多个请求能在同一个TCP分节发送的话,还能提高网络利用率。但是因为HTTP管道化本身可能会导致队头阻塞的问题,以及一些其他的原因,现代浏览器默认都关闭了管道化。 + +#### HTTP管道化的限制 + +1. 管道化要求服务端按照请求发送的顺序返回响应(FIFO),原因很简单,HTTP请求和响应并没有序号标识,无法将乱序的响应与请求关联起来。 +2. 客户端需要保持未收到响应的请求,当连接意外中断时,需要重新发送这部分请求。 +3. 只有幂等的请求才能进行管道化,也就是只有GET和HEAD请求才能管道化,否则可能会出现意料之外的结果 + +#### HTTP管道化引起的请求队头阻塞 + +前面提到HTTP管道化要求服务端必须按照请求发送的顺序返回响应,那如果一个响应返回延迟了,那么其后续的响应都会被延迟,直到队头的响应送达。 + +### 如何解决队头阻塞 + +#### 如何解决HTTP队头阻塞 + +对于HTTP1.1中管道化导致的请求/响应级别的队头阻塞,可以使用HTTP2解决。HTTP2不使用管道化的方式,而是引入了帧、消息和数据流等概念,每个请求/响应被称为消息,每个消息都被拆分成若干个帧进行传输,每个帧都分配一个序号。每个帧在传输是属于一个数据流,而一个连接上可以存在多个流,各个帧在流和连接上独立传输,到达之后在组装成消息,这样就避免了请求/响应阻塞。 + +当然,即使使用HTTP2,如果HTTP2底层使用的是TCP协议,仍可能出现TCP队头阻塞。 + +#### 如何解决TCP队头阻塞 + +TCP中的队头阻塞的产生是由TCP自身的实现机制决定的,无法避免。想要在应用程序当中避免TCP队头阻塞带来的影响,只有舍弃TCP协议。 + +比如google推出的QUIC协议,在某种程度上可以说避免了TCP中的队头阻塞,因为它根本不使用TCP协议,而是在UDP协议的基础上实现了可靠传输。而UDP是面向数据报的协议,数据报之间不会有阻塞约束。 + +此外还有一个SCTP(流控制传输协议),它是和TCP、UDP在同一层次的传输协议。SCTP的多流特性也可以尽可能的避免队头阻塞的情况。 + +### 总结 + +从TCP队头阻塞和HTTP队头阻塞的原因我们可以看到,出现队头阻塞的原因有两个: + + 1. 独立的消息数据都在一个链路上传输,也就是有一个“队列”。比如TCP只有一个流,多个HTTP请求共用一个TCP连接 + 2. 队列上传输的数据有严格的顺序约束。比如TCP要求数据严格按照序号顺序,HTTP管道化要求响应严格按照请求顺序返回 + +所以要避免队头阻塞,就需要从以上两个方面出发,比如quic协议不使用TCP协议而是使用UDP协议,SCTP协议支持一个连接上存在多个数据流等等。 + +## QUIC + +QUIC(Quick UDP Internet Connection)是谷歌制定的一种互联网传输层协议,它基于UDP传输层协议,同时兼具TCP、TLS、HTTP/2等协议的可靠性与安全性,可以有效减少连接与传输延迟,更好地应对当前传输层与应用层的挑战。 + +### QUIC的由来:为什么是UDP而非TCP? + +UDP和TCP都属于传输层协议。TCP是面向连接的,更强调的是传输的可靠性,通过TCP连接传送的数据,无差错,不丢失,不重复,按序到达,但是因为TCP在传递数据之前会有三次握手来建立连接,所以效率低、占用系统的CPU、内存等硬件资源较高;而UDP的无连接的(即发送数据之前不需要建立连接),只需要知道对方地址即可发送数据,具有较好的实时性,工作效率比TCP高,占用系统资源比TCP少,但是在数据传递时,如果网络质量不好,就会很容易丢包。 + +我们知道,大部分Web平台的数据传输都基于TCP协议。实际上,TCP在设计之初,网络环境复杂、丢包率高、网速差,所以TCP可以完美解决可靠性的问题。而如今的网络环境和网速都已经取得了巨大的改善,网络传输可靠性已经不再是棘手的问题。另外,TCP还有一个很大的问题是更新非常困难。这是因为:TCP网络协议栈的实现依赖于系统内核更新,一旦系统内核更新,终端设备、中间设备的系统更新都会非常缓慢,迭代需要花费几年甚至十几年的时间,这显然跟不上当今互联网的发展速度。所以现在解法就是,抛弃TCP而使用UDP,来实现低延迟的传输需求。 + +![QUIC is very similar to TCP TLS HTTP 2 0 implemented on UDP](https://user-images.githubusercontent.com/8088864/125581409-742f54c2-93aa-4d3a-919e-d3710b318361.jpg) + +为了结合两者优点,谷歌公司推出了QUIC,它的升级不依赖于系统内核,只需要Client和Server端更新到指定版本。如此一来,基于UDP的QUIC就能月更甚至周更,很好的解决了TCP部署和更新的困难,更灵活地实现部署和更新。 + +### 为什么要用QUIC? + +#### 1. 建连延迟低 + +网民传统TCP三次握手+TLS1`~`2RTT握手+http数据,基于TCP的HTTPS一次建连至少需要2`~`3个RTT,而QUIC基于UDP,完整握手只需要1RTT乃至0RTT,可以显著降低延迟。 + +![QUIC握手](https://user-images.githubusercontent.com/8088864/125584078-81044014-9ed7-47ba-93a4-24623b716b07.jpg) + +#### 2. 安全又可靠 + +QUIC具备TCP、TLS、HTTPS/2等协议的安全、可靠性的特点,通过提供安全功能(如身份验证和加密)来实现加密传输,这些功能由传输协议本身的更高层协议(如TLS)来实现。 + +#### 3. 改造灵活 + +QUIC在应用程序层面就能实现不同的拥塞控制算法,不需要操作系统和内核支持,这相比于传统的TCP协议改造灵活性更好。 + +#### 4. 改进的拥塞控制 + +QUIC主要实现了TCP的慢启动、拥塞避免、快重传、快恢复。在这些拥塞控制算法的基础上改进,例如单调递增的 Packet Number,解决了重传的二义性,确保RTT准确性,减少重传次数。 + +#### 5. 无队头阻塞的多路复用 + +HTTP2实现了多路复用,可以在一条TCP流上并发多个HTTP请求,但基于TCP的HTTP2在传输层却有个问题,TCP无法识别不同的HTTP2流,实际收数据仍是一个队列,当后发的流先收到时,会因前面的流未到达而被阻塞。QUIC一个connection可以复用传输多个stream,每个stream之间都是独立的,一个stream的丢包不会影响到其他stream的接收和处理。 + +![QUIC特点](https://user-images.githubusercontent.com/8088864/125585210-a874fcb0-87ab-46a5-b254-825c78034943.jpg) + +综上所述,QUIC具有众多优点,它融合了UDP协议的速度、性能与TCP的安全与可靠,大大优化了互联网传输体验。 + +作为提升终端用户访问效率的CDN服务,其节点之间存在大量数据互通,节点之间的网络连接、传输架构等因素都会对CDN服务质量产生影响。而将QUIC应用在CDN系统中,CDN用户开启QUIC功能后,系统将遵循QUIC协议进行用户IP请求处理,既能满足安全传输的需求,也能提升传输效率。 + +### QUIC对客户端的要求 + +- 如果您使用Chrome浏览器,则只支持QUIC协议Q43版本。当前阿里云CDN的QUIC协议是Q39版本,不支持直接对阿里云CDN发起QUIC请求。 +- 如果您使用自研App,则App必须集成支持QUIC协议的网络库,例如:lsquic-client或cronet网络库。 + +### QUIC应用场景 + +1. 图片小文件:明显降低文件下载总耗时,提升效率 +2. 视频点播:提升首屏秒开率,降低卡顿率,提升用户观看体验 +3. 动态请求:适用于动态请求,提升访问速度,如网页登录、交易等交互体验提升 +4. 弱网环境:在丢包和网络延迟严重的情况下仍可提供可用的服务,并优化卡顿率、请求失败率、秒开率、提高连接成功率等传输指标 +5. 大并发连接:连接可靠性强,支持页面资源数较多、并发连接数较多情况下的访问速率提升 +6. 加密连接:具备安全、可靠的传输性能 + + +## HTTP协议 + +一面中,如果有笔试,考HTTP协议的可能性较大。 + +### 1. 前言 + +一面要讲的内容: + +- `HTTP`协议的主要特点 +- `HTTP`报文的组成部分 +- `HTTP`方法 +- `get` 和 `post`的区别 +- `HTTP`状态码 +- 什么是持久连接 +- 什么是管线化 + +二面要讲的内容; + +- 缓存 +- `CSRF`攻击 +- TSL 协商 + +### 2. HTTP协议的主要特点 + +- 简单快速 +- 灵活 +- **无连接** +- **无状态** + +> 通常我们要答出以上四个内容。如果实在记不住,一定要记得后面的两个:**无连接、无状态**。 + + +我们分别来解释一下。 + + +#### 2.1 简单快速 + +> **简单**:每个资源(比如图片、页面)都通过 url 来定位。这都是固定的,在`http`协议中,处理起来也比较简单,想访问什么资源,直接输入url即可。 + + +#### 2.2 灵活 + +> `http`协议的头部有一个`数据类型`,通过`http`协议,就可以完成不同数据类型的传输。 + +#### 2.3 无连接 + +> 连接一次,就会断开,不会继续保持连接。 + +#### 2.4 无状态 + +> 客户端和服务器端是两种身份。第一次请求结束后,就断开了,第二次请求时,**服务器端并没有记住之前的状态**,也就是说,服务器端无法区分客户端是否为同一个人、同一个身份。 + +> 有的时候,我们访问网站时,网站能记住我们的账号,这个是通过其他的手段(比如 `session`)做到的,并不是`http`协议能做到的。 + + +### 3 HTTP报文的组成部分 + +![](http://img.smyhvae.com/20180306_1400.png) + +> 在回答此问题时,我们要按照顺序回答: + +- 先回答的是,`http`报文包括:**请求报文**和**响应报文**。 +- 再回答的是,每个报文包含什么部分。 +- 最后回答,每个部分的内容是什么 + +#### 3.1 请求报文包括: + +![](http://img.smyhvae.com/20180228_1505.jpg) + +- 请求行:包括请求方法、请求的`url`、`http`协议及版本。 +- 请求头:一大堆的键值对。 +- **空行**指的是:当服务器在解析请求头的时候,如果遇到了空行,则表明,后面的内容是请求体 +- 请求体:数据部分。 + +#### 3.2 响应报文包括: + +![](http://img.smyhvae.com/20180228_1510.jpg) + + +- 状态行:`http`协议及版本、状态码及状态描述。 +- 响应头 +- 空行 +- 响应体 + + +### 4 HTTP方法 + +包括: + +- `GET`:获取资源 +- `POST`:传输资源 +- `put`:更新资源 +- `DELETE`:删除资源 +- `HEAD`:获得报文首部 + +> `HTTP`方法有很多,但是上面这五个方法,要求在面试时全部说出来,不要漏掉。 + +- `get` `和 `post` 比较常见。 +- `put` 和 `delete` 在实际应用中用的很少。况且,业务中,一般不删除服务器端的资源。 +- `head` 可能偶尔用的到。 + + +### 5 get 和 post的区别 + +![](http://img.smyhvae.com/20180306_1415.png) + +- 区别有很多,如果记不住,面试时,至少要任意答出其中的三四条。 +- 有一点要强调,**get是相对不隐私的,而post是相对隐私的**。 + +> 我们大概要记住以下几点: + +1. 浏览器在回退时,`get` **不会重新请求**,但是`post`会重新请求。【重要】 +2. `get`请求会被浏览器**主动缓存**,而`post`不会。【重要】 +3. `get`请求的参数,会报**保留**在浏览器的**历史记录**里,而`post`不会。做业务时要注意。为了防止`CSRF`攻击,很多公司把`get`统一改成了`post`。 +4. `get`请求在`url`中`传递的参数有大小限制,基本是`2kb`,不同的浏览器略有不同。而post没有注意。 +5. `get`的参数是直接暴露在`url`上的,相对不安全。而`post`是放在请求体中的。 + + +### 6 http状态码 + +> `http`状态码分类: + +![](http://img.smyhvae.com/20180306_1430.png) + +> 常见的`http`状态码: + +![](http://img.smyhvae.com/20180306_1431.png) + + +**部分解释**: + +- `206`的应用:`range`指的是请求的范围,客户端只请求某个大文件里的一部分内容。比如说,如果播放视频地址或音频地址的前面一部分,可以用到`206`。 +- `301`:重定向(永久)。 +- `302`:重定向(临时)。 +- `304`:我这个服务器告诉客户端,你已经有缓存了,不需要从我这里取了。 + +![](http://img.smyhvae.com/20180306_1440.png) + +- `400`和`401`用的不多,未授权。`403`指的是请求被拒绝。`404`指的是资源不存在。 + +### 7 持久链接/http长连接 + +> 如果你能答出持久链接,这是面试官很想知道的一个点。 + +- **轮询**:`http1.0`中,客户端每隔很短的时间,都会对服务器发出请求,查看是否有新的消息,只要轮询速度足够快,例如`1`秒,就能给人造成交互是实时进行的印象。这种做法是无奈之举,实际上对服务器、客户端双方都造成了大量的性能浪费。 +- **长连接**:`HTTP1.1`中,通过使用`Connection:keep-alive`进行长连接,。客户端只请求一次,但是服务器会将继续保持连接,当再次请求时,避免了重新建立连接。 + +> 注意,`HTTP 1.1`默认进行持久连接。在一次 `TCP` 连接中可以完成多个 `HTTP` 请求,但是对**每个请求仍然要单独发 header**,`Keep-Alive`不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如`Apache`)中设定这个时间。 + + +### 8 长连接中的管线化 + +> 如果能答出**管线化**,则属于加分项。 + +#### 8.1 管线化的原理 + +> 长连接时,**默认**的请求这样的: + +``` + 请求1 --> 响应1 -->请求2 --> 响应2 --> 请求3 --> 响应3 +``` + + +> 管线化就是,我把现在的请求打包,一次性发过去,你也给我一次响应回来。 + + +#### 8.2 管线化的注意事项 + +> 面试时,不会深究管线化。如果真要问你,就回答:“我没怎么研究过,准备回去看看~” + +### 9 TLS 协商 + +Transport Layer Security (TLS) 是一个为计算机网络提供通信安全的加密协议。它广泛应用于大量应用程序,其中之一即浏览网页。网站可以使用 TLS 来保证服务器和网页浏览器之间的所有通信安全。 + +整个 TLS 握手过程包含以下几个步骤: + +- 客户端向服务器发送 『Client hello』 信息,附带着客户端随机值(random_C)和支持的加密算法组合。 +- 服务器返回给客户端 『Server hello』信息,附带着服务器随机值(random_S),以及选择一个客户端发送过来加密算法。 +- 服务器返回给客户端认证证书及或许要求客户端返回一个类似的证书,认证证书里面携带服务端的公钥信息。 +- 服务器返回『Server hello done』信息。 +- 如果服务器要求客户端发送一个证书,客户端进行发送。 +- 客户端创建一个随机的 Pre-Master 密钥然后使用服务器证书中的公钥来进行加密,向服务器发送加密过的 Pre-Master 密钥。 +- 服务器收到 Pre-Master 密钥。服务器和客户端各自生成基于 Pre-Master 密钥的主密钥和会话密钥。两个明文随机数 random_C 和 random_S 与自己计算产生的 pre-master,计算得到协商密钥enc_key=Fuc(random_C, random_S, pre-master) +- 客户端给服务器发送一个 『Change cipher spec』的通知,表明客户端将会开始使用协商密钥和加密算法进行加密通信。 +- 客户端也发送了一个 『Client finished』的消息。 +- 服务器接收到『Change cipher spec』的通知然后使用协商密钥和加密算法进行加密通信。 +- 服务器返回客户端一个 『Server finished』消息。 +- 客户端和服务器现在可以通过建立的安全通道来交换程序数据。所有客户端和服务器之间发送的信息都会使用会话密钥进行加密。 + +每当发生任何验证失败的时候,用户会收到警告。比如服务器使用自签名的证书。 + +## WebRTC的优缺点 + +WebRTC,即网页即时通信(Web Real-Time Communication),是一个支持网页浏览器进行实时语音对话或视频对话的API。 + +目前几乎所有主流浏览器都支持了 WebRTC,越来越多的公司正在使用 WebRTC 并且将其加到自己的应用程序中。在浏览器端,依赖于浏览器获取音视频的能力,以及强大的网页上的渲染能力,就能够为高清的通信体验打下基础。同时,相比移动端来说,屏幕比较大,视窗选择也比较灵活。 + +### 优点 + +1. 方便。对于用户来说,在WebRTC出现之前想要进行实时通信就需要安装插件和客户端,但是对于很多用户来说,插件的下载、软件的安装和更新这些操作是复杂而且容易出现问题的,现在WebRTC技术内置于浏览器中,用户不需要使用任何插件或者软件就能通过浏览器来实现实时通信。对于开发者来说,在Google将WebRTC开源之前,浏览器之间实现通信的技术是掌握在大企业手中,这项技术的开发是一个很困难的任务,现在开发者使用简单的HTML标签和JavaScript API就能够实现Web音/视频通信的功能。 + +2. 免费。虽然WebRTC技术已经较为成熟,其集成了最佳的音/视频引擎,十分先进的codec,但是Google对于这些技术不收取任何费用。 + +3. 强大的打洞能力。WebRTC技术包含了使用STUN、ICE、TURN、RTP-over-TCP的关键NAT和防火墙穿透技术,并支持代理。 + +### 缺点 + +1. 缺乏服务器方案的设计和部署。 + +2. 传输质量难以保证。WebRTC的传输设计基于P2P,难以保障传输质量,优化手段也有限,只能做一些端到端的优化,难以应对复杂的互联网环境。比如对跨地区、跨运营商、低带宽、高丢包等场景下的传输质量基本是靠天吃饭,而这恰恰是国内互联网应用的典型场景。 + +3. WebRTC比较适合一对一的单聊,虽然功能上可以扩展实现群聊,但是没有针对群聊,特别是超大群聊进行任何优化。 + +4. 设备端适配,如回声、录音失败等问题层出不穷。这一点在安卓设备上尤为突出。由于安卓设备厂商众多,每个厂商都会在标准的安卓框架上进行定制化,导致很多可用性问题(访问麦克风失败)和质量问题(如回声、啸叫)。 + +5. 对Native开发支持不够。WebRTC顾名思义,主要面向Web应用,虽然也可以用于Native开发,但是由于涉及到的领域知识(音视频采集、处理、编解码、实时传输等)较多,整个框架设计比较复杂,API粒度也比较细,导致连工程项目的编译都不是一件容易的事。 + +## EventSource和轮询的优缺点 + +### EventSource + +#### 简介 + +EventSource 是服务器推送的一个网络事件接口。一个EventSource实例会对HTTP服务开启一个持久化的连接,以text/event-stream 格式发送事件, 会一直保持开启直到被要求关闭。 + +一旦连接开启,来自服务端传入的消息会以事件的形式分发至你代码中。如果接收消息中有一个事件字段,触发的事件与事件字段的值相同。如果没有事件字段存在,则将触发通用事件。 + +与 WebSockets,不同的是,服务端推送是单向的。数据信息被单向从服务端到客户端分发. 当不需要以消息形式将数据从客户端发送到服务器时,这使它们成为绝佳的选择。例如,对于处理社交媒体状态更新,新闻提要或将数据传递到客户端存储机制(如IndexedDB或Web存储)之类的,EventSource无疑是一个有效方案。 + +- `EventSource`(Server-sent events)简称SSE用于向服务端发送事件,它是基于http协议的单向通讯技术,以`text/event-stream`格式接受事件,如果不关闭会一直处于连接状态,直到调用`EventSource.close()`方法才能关闭连接; + +- `EvenSource`本质上也就是`XHR-streaming`只不过浏览器给它提供了标准的API封装和协议。 + +- 由于`EventSource`是单向通讯,所以只能用来实现像股票报价、新闻推送、实时天气这些只需要服务器发送消息给客户端场景中。 + +- `EventSource`虽然不支持双向通讯,但是在功能设计上他也有一些优点比如可以自动重连接,event IDs,以及发送随机事件的等功能 + +`EventSource`案例浏览器端代码如下所示: + +``` js +// 实例化 EventSource 参数是服务端监听的路由 +var source = new EventSource('http://localhost:3000'); + +source.onopen = function (event) { // 与服务器连接成功回调 + console.log('成功与服务器连接'); +} + +// 监听从服务器发送来的所有没有指定事件类型的消息(没有event字段的消息) +source.onmessage = function (event) { // 监听未命名事件 + console.log('未命名事件', event.data); +} + +source.onerror = function (error) { // 监听错误 + console.log('错误'); +} + +// 监听指定类型的事件(可以监听多个) +source.addEventListener("ping", function (event) { + console.log("ping", event.data) +}) +``` + +服务器端 + +``` js +const http = require('http'); + +http.createServer((req, res) => { + res.writeHead(200, { + 'Content-Type' :'text/event-stream', + 'Access-Control-Allow-Origin':'*' + }); + + let i = 0; + const timer = setInterval(()=>{ + const date = {date:new Date()} + var content ='event: ping\n'+"data:"+JSON.stringify(date)+"" +"\n\n"; + res.write(content); + },1000) + + res.connection.on("close", function(){ + res.end(); + clearInterval(timer); + console.log("Client closed connection. Aborting."); + }); + +}).listen(3000); +console.log('server is run http://localhost:3000'); +``` + +#### EventSource规范字段 + +- **event**: 事件类型,如果指定了该字段,则在客户端接收到该条消息时,会在当前的EventSource对象上触发一个事件,事件类型就是该字段的字段值,你可以使用addEventListener()方法在当前EventSource对象上监听任意类型的命名事件,如果该条消息没有event字段,则会触发onmessage属性上的事件处理函数。 +- **data**: 消息的数据字段,如果该消息包含多个data字段,则客户端会用换行符把他们连接成一个字符串来处理 +- **id**: 事件ID,会成为当前EventSource对象的内部属性“最后一个事件ID”的属性值; +- **retry**: 一个整数值,指定了重新连接的时间(单位为毫秒),如果该字段不是整数,则会被忽略。 + +#### EventSource属性 + +- **EventSource.onerror**: 是一个 EventHandler,当发生错误时被调用,并且在此对象上派发 error 事件。 +- **EventSource.onmessage**: 是一个 EventHandler,当收到一个 message事件,即消息来自源头时被调用。 +- **EventSource.onopen**: 是一个 EventHandler,当收到一个 open 事件,即连接刚打开时被调用。 +- **EventSource.readyState**(只读): 一个 unsigned short 值,代表连接状态。可能值是CONNECTING (0), OPEN (1), 或者 CLOSED (2)。 +- **EventSource.url**(只读): 一个DOMString,代表源头的URL。 + +#### EventSource 通讯过程 + +![EventSource通讯过程](https://user-images.githubusercontent.com/8088864/125590756-ffd10207-83de-4166-a8b5-9fc848c191cc.png) + +#### 缺点 + +1. 因为是服务器->客户端的,所以它不能处理客户端请求流 +2. 因为是明确指定用于传输UTF-8数据的,所以对于传输二进制流是低效率的,即使你转为base64的话,反而增加带宽的负载,得不偿失。 + +### 轮询 + +#### 短轮询(Polling) + +是一种简单粗暴,同样也是一种效率低下的实现“实时”通讯方案,这种方案的原理就是定期向服务器发送请求,主动拉取最新的消息队列。 + +客户端代码: + +``` js +function Polling() { + fetch(url).then(data => { + // somthing + }).catch(err => { + console.error(err); + }); +} + +//每5s执行一次 +setInterval(polling, 5000); +``` + +![短轮询流程](https://user-images.githubusercontent.com/8088864/125591641-814c4239-47e3-41da-ad9e-a0c7e64dfe72.png) + +这种轮询方式比较适合服务器信息定期更新的场景,如天气预报股票行情等,每隔一段时间会进行更新,且轮询间隔的服务器更新频率保持一致是比较理想的方式,但很多多时候会因网络或者服务器出现阻塞早场事件间隔不一致。 + +优点: + +- 可以看到实现非常简单,它的兼容性也比较好的只要支持http协议就可以用这种方式实现 + +缺点: + +- 资源浪费: 比如轮询的间隔小于服务器信息跟新频率,会浪费很多HTTP请求,消耗宝贵的CPU时间和带宽。 + +- 容易导致请求轰炸: 例如当服务器负载比较高时,第一个请求还没有处理完,这时第三、第四个请求接踵而来,无用的额外请求对服务器端进行了轰炸。 + +#### 长轮询(Long Polling) + +这是一种优化的轮询方式,称为长轮询,sockjs就是使用的这种轮询方式,长轮询值的是浏览器发送一个请求到服务器,服务器只有在有可用的新数据时才会响应。 + +客户端代码: + +``` js +function LongPolling() { + fetch(url).then(data => { + LongPolling(); + }).catch(err => { + LongPolling(); + console.log(err); + }); +} +LongPolling(); +``` + +![长轮询流程](https://user-images.githubusercontent.com/8088864/125592542-e5c7fb6b-18b8-434f-a4ee-f986684dcbbf.png) + +客户端向服务器发送一个消息获取请求时,服务器会将当前的消息队列返回给客户端,然后关闭连接。当消息队列为空的时,服务器不会立即关闭连接,而是等待指定的时间间隔,如果在这个时间间隔内没有新的消息,则由客户端主动超时关闭连接。 + +相比Polling,客户端的轮询请求只有在上一个请求连接关闭后才会重新发起。这就解决了Polling的请求轰炸问题。服务器可以控制的请求时序,因为在服务器未响应之前,客户端不会发送额为的请求。 + +优点: + +- 长轮询和短轮询比起来,明显减少了很多不必要的http请求次数,相比之下节约了资源。 + +缺点: + +- 连接挂起也会导致资源的浪费。 + +### EventSource VS 轮询 + +| | 轮询(Polling) | 长轮询(Long-Polling) | EventSource | +| ---- | ---- | ---- | ---- | +| 通信协议 | http | http | http | +| 触发方式 | client(客户端) | client(客户端) | client、server(客户端、服务端) | +| 优点 | 兼容性好容错性强,实现简单 | 比短轮询节约服务器资源 | 实现简便,开发成本低 | +| 缺点 | 安全性差,占较多的内存资源与请求数量,容易对服务器造成压力,请求时间间隔容易导致不一致 | 安全性差,占较多的内存资源与请求数,请求时间间隔容易导致不一致 | 只适用高级浏览器,老版本的浏览器不兼容 | +| 延迟 | 非实时,延迟取决于请求间隔 | 非实时,延迟取决于请求间隔 | 非实时,默认3秒延迟,延迟可自定义 | + +### 总结 + +通过对上面两种对通讯技术比较,可以从不同的角度考虑; + +- 兼容性: 短轮询 > 长轮询 > EventSource +- 性能: EvenSource > 长轮询 > 短轮询 +- 服务端推送: EventSource > 长连接 (短轮询基本不考虑) + +## WebSocket 是什么原理?为什么可以实现持久连接? + +### WebSocket 机制 + +以下简要介绍一下WebSocket的原理及运行机制。 + +WebSocket是HTML5下一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。它与HTTP一样通过已建立的TCP连接来传输数据,但是它和HTTP最大不同是: + +- WebSocket是一种双向通信协议。在建立连接后,WebSocket服务器端和客户端都能主动向对方发送或接收数据,就像Socket一样; +- WebSocket需要像TCP一样,先建立连接,连接成功后才能相互通信。 + +传统HTTP客户端与服务器请求响应模式如下图所示: + +![传统HTTP客户端与服务器请求响应模型](https://user-images.githubusercontent.com/8088864/125600810-db0eaedf-6a66-4d71-b9c6-1a5d891a7b86.jpg) + +WebSocket模式客户端与服务器请求响应模式如下图: + +![WebSocket模式客户端与服务器请求响应模式](https://user-images.githubusercontent.com/8088864/125600954-0e796b1d-dd3a-482c-ab83-0d43f1abf610.jpg) + +上图对比可以看出,相对于传统HTTP每次请求-响应都需要客户端与服务端建立连接的模式,WebSocket是类似Socket的TCP长连接通讯模式。一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。 + +相比HTTP长连接,WebSocket有以下特点: + +- 是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。 +- HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)。此外还有 multiplexing、不同的URL可以复用同一个WebSocket连接等功能。这些都是HTTP长连接不能做到的。 + +### WebSocket协议的原理 + +与http协议一样,WebSocket协议也需要通过已建立的TCP连接来传输数据。具体实现上是通过http协议建立通道,然后在此基础上用真正的WebSocket协议进行通信,所以WebSocket协议和http协议是有一定的交叉关系的。 + +![WebSocket协议原理流程图](https://user-images.githubusercontent.com/8088864/125603352-ba55e8bd-f554-4ef1-8c0c-add611f63023.jpg) + +下面是WebSocket协议请求头: + +![WebSocket协议请求头](https://user-images.githubusercontent.com/8088864/125603469-ef8dfb8e-988a-4bc6-a041-487f697cb72a.jpg) + +其中请求头中重要的字段: + +``` request header +Connection:Upgrade + +Upgrade:websocket + +Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits + +Sec-WebSocket-Key:mg8LvEqrB2vLpyCNnCJV3Q== + +Sec-WebSocket-Version:13 +``` + +1. Connection和Upgrade字段告诉服务器,客户端发起的是WebSocket协议请求 +2. Sec-WebSocket-Extensions表示客户端想要表达的协议级的扩展 +3. Sec-WebSocket-Key是一个Base64编码值,由浏览器随机生成 +4. Sec-WebSocket-Version表明客户端所使用的协议版本 + +而得到的响应头中重要的字段: + +``` response header +Connection:Upgrade + +Upgrade:websocket + +Sec-WebSocket-Accept:AYtwtwampsFjE0lu3kFQrmOCzLQ= +``` + +1. Connection和Upgrade字段与请求头中的作用相同 + +2. Sec-WebSocket-Accept表明服务器接受了客户端的请求 + +``` response header +Status Code:101 Switching Protocols +``` + +并且http请求完成后响应的状态码为101,表示切换了协议,说明WebSocket协议通过http协议来建立运输层的TCP连接,之后便与http协议无关了。 + +### WebSocket协议的优缺点 + +优点: + +- WebSocket协议一旦建议后,互相沟通所消耗的请求头是很小的 +- 服务器可以向客户端推送消息了 + +缺点: + +- 少部分浏览器不支持,浏览器支持的程度与方式有区别 + +WebSocket协议的应用场景 + +- 即时聊天通信 +- 多玩家游戏 +- 在线协同编辑/编辑 +- 实时数据流的拉取与推送 +- 体育/游戏实况 +- 实时地图位置 + +一个使用WebSocket应用于视频的业务思路如下: + +- 使用心跳维护websocket链路,探测客户端端的网红/主播是否在线 +- 设置负载均衡7层的proxy_read_timeout默认为60s +- 设置心跳为50s,即可长期保持Websocket不断开 + +## 网络相关 + +### 1.1 DNS 预解析 + +- DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP + +```html + +``` + +### 1.2 缓存 + +- 缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度 +- 通常浏览器缓存策略分为两种:强缓存和协商缓存 + +**强缓存** + +> 实现强缓存可以通过两种响应头实现:`Expires `和 `Cache-Control` 。强缓存表示在缓存期间不需要请求,`state code `为 `200` + +``` +Expires: Wed, 22 Oct 2018 08:41:00 GMT +``` + +> `Expires` 是 `HTTP / 1.0` 的产物,表示资源会在 `Wed, 22 Oct 2018 08:41:00 GMT` 后过期,需要再次请求。并且 `Expires` 受限于本地时间,如果修改了本地时间,可能会造成缓存失效 + +``` +Cache-control: max-age=30 +``` + +> `Cache-Control` 出现于 `HTTP / 1.1`,优先级高于 `Expires` 。该属性表示资源会在 `30` 秒后过期,需要再次请求 + +**协商缓存** + +- 如果缓存过期了,我们就可以使用协商缓存来解决问题。协商缓存需要请求,如果缓存有效会返回 304 +- 协商缓存需要客户端和服务端共同实现,和强缓存一样,也有两种实现方式 + +Last-Modified 和 If-Modified-Since + +- `Last-Modified` 表示本地文件最后修改日期,`If-Modified-Since` 会将 `Last-Modified `的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来 +- 但是如果在本地打开缓存文件,就会造成 `Last-Modified` 被修改,所以在 `HTTP / 1.1` 出现了 `ETag` + +ETag 和 If-None-Match + +- `ETag` 类似于文件指纹,`If-None-Match` 会将当前 `ETag` 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 `ETag` 优先级比 `Last-Modified` 高 + +**选择合适的缓存策略** + +> 对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略 + +- 对于某些不需要缓存的资源,可以使用 `Cache-control: no-store` ,表示该资源不需要缓存 +- 对于频繁变动的资源,可以使用 `Cache-Control: no-cache` 并配合 `ETag` 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。 +- 对于代码文件来说,通常使用 `Cache-Control: max-age=31536000` 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件 + +### 1.3 使用 HTTP / 2.0 + +- 因为浏览器会有并发请求限制,在 HTTP / 1.1 时代,每个请求都需要建立和断开,消耗了好几个 RTT 时间,并且由于 TCP 慢启动的原因,加载体积大的文件会需要更多的时间 +- 在 HTTP / 2.0 中引入了多路复用,能够让多个请求使用同一个 TCP 链接,极大的加快了网页的加载速度。并且还支持 Header 压缩,进一步的减少了请求的数据大小 + + +### 1.4 预加载 + +- 在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载 +- 预加载其实是声明式的 `fetch` ,强制浏览器请求资源,并且不会阻塞 `onload` 事件,可以使用以下代码开启预加载 + +```html + +``` + +> 预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好 + + + +### 1.5 预渲染 + +> 可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染 + +```html + +``` + +- 预渲染虽然可以提高页面的加载速度,但是要确保该页面百分百会被用户在之后打开,否则就白白浪费资源去渲染 + +## 优化渲染过程 + +### 2.1 懒执行 + +- 懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒 + +### 2.2 懒加载 + +- 懒加载就是将不关键的资源延后加载 + +> 懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载 + +- 懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等 + + +## 三栏弹性布局的5种方法(绝对定位、圣杯、双飞翼、flex、grid) + +### 需求 + +用css实现三栏布局,html结构代码如下,顺序不能变(main优先渲染),可以适当加元素,同时要求left宽度200px,right宽度300px,main宽度自适应。 + +``` html +
+
main 宽度自适应
+
left 宽200px
+
right 宽300px
+
+``` + +![三栏布局](https://user-images.githubusercontent.com/8088864/125612523-d7b144ff-a0a3-4522-ad8b-c2a7179198c2.gif) + +### 5种具体实现和优缺点比较 + +#### 1. 绝对定位布局 + +原始的布局方法 + +- 原理:container为相对定位并设置左右padding为left和right的宽度,left\right绝对定位在左右两侧,main不用设置。 + +- 优点:兼容好、原理简单 + +- 缺点:left和right都为绝对定位,高度不能撑开container + +``` html + + + + 绝对定位布局 + + + +
+
main 宽度自适应
+
left 宽200px
+
right 宽300px
+
+ + +``` + +#### 2. 圣杯布局 + +圣杯布局方法 + +- 原理:container设置左右padding为left和right的宽度,left\right\main 浮动,left\right相对定位并设置left、right、margin-left来偏移位置,main宽100%。 +- 优点:兼容好 +- 缺点:原理复制,left/right/main高度自适应情况下3者不能高度一致。 + +``` html + + + + 圣杯布局 + + + +
+
main 宽度自适应
+
left 宽200px
+
right 宽300px
+
+ + +``` + +#### 3. 双飞翼布局 + +圣杯布局改进方法 + +- 原理:left\right\main 浮动,left\right设置margin-left来偏移位置,main宽100%,main出入content,并设置content的左右边距为left\right宽度 +- 优点:兼容好,原理简单 +- 缺点:left/right/main高度自适应情况下3者不能高度一致。 + +``` html + + + + 双飞翼布局 + + + +
+
+
+ main 宽度自适应 +
+
+
left 宽200px
+
right 宽300px
+
+ + +``` + +#### 4. flex布局 + +css3新布局方式 + +- 原理:container设置`display:flex`,left设置`order:-1`排在最前面,main设置`flex-grow:1`自适应宽度 +- 优点:原理简单,代码简洁,left/right/main高度自适应情况下3者能高度一致 +- 缺点:兼容性不够好,ie10+,chrome20+,正式使用要加各种前缀(-webkit--ms-) + +``` html + + + + flex布局 + + + +
+
main 宽度自适应
+
left 宽200px
+
right 宽300px
+
+ + +``` + +#### 5. grid布局 + +css3新布局方式 + +- 原理:container设置`display:grid` 和 `grid-template-columns:200px auto 300px`,left设置`order: -1`排在最前面 +- 优点:原理简单,代码简洁,left/right/main高度自适应情况下3者能高度一致 +- 缺点:兼容性较差,ie10+,Chrome57+,正式使用要加各种前缀(-webkit--ms-) + +``` html + + + + grid布局 + + + +
+
main 宽度自适应
+
left 宽200px
+
right 宽300px
+
+ + +``` + +## 浅析CSS里的BFC和IFC的用法 + +### BFC简介 + +所谓的 Formatting Context(格式化上下文), 它是 W3C CSS2.1 规范中的一个概念。 + +- 格式化上下文(FC)是页面中的一块渲染区域,并且有一套渲染规则。 +- 格式化上下文(FC)决定了其子元素将如何定位,以及和其他元素的关系和相互作用。 + +Block Formatting Context (BFC,块级格式化上下文),就是一个块级元素的渲染显示规则。通俗一点讲,可以把 BFC 理解为一个封闭的大箱子,容器里面的子元素不会影响到外面的元素,反之也如此。 + +BFC的布局规则如下: + +1. 内部的盒子会在垂直方向,一个个地放置; +2. BFC是页面上的一个隔离的独立容器; +3. 属于同一个BFC的 两个相邻Box的 上下margin会发生重叠; +4. 计算BFC的高度时,浮动元素也参与计算; +5. 每个元素的左边,与包含的盒子的左边相接触,即使存在浮动也是如此;**(存疑)** +6. BFC的区域不会与float重叠。 + +那么如何触发 BFC 呢?只要元素满足下面任一条件即可触发 BFC 特性: + +- body 根元素; +- 浮动元素:float 不为none的属性值; +- 绝对定位元素:position (absolute、fixed); +- display为: inline-block、table-cells、flex; +- overflow 除了visible以外的值 (hidden、auto、scroll)。 + +### BFC的特性及应用 + +#### 同一个 BFC下外边距 会发生折叠 + +``` html + + + + + +
+
+ + +``` + +效果如下所示: + +![同一个 BFC 下两个相邻的普通流中的块元素垂直方向上的 margin会折叠](https://user-images.githubusercontent.com/8088864/125714340-57813f51-5cad-4844-9247-2ba5cc04ac8d.jpg) + +根据BFC规则的第3条: + +盒子垂直方向的距离由margin决定, + +属于 同一个BFC的 + 两个相邻Box的 + 上下margin 会发生重叠。 + +上文的例子 之所以发生外边距折叠,是因为他们 同属于 body这个根元素, 所以我们需要让 它们 不属于同一个BFC,就能避免外边距折叠: + +``` html + + + + + +
+
+
+
+ + +``` + +效果如下所示: + +![利用 BFC 下可以避免两个相邻的块元素垂直方向上的 margin折叠](https://user-images.githubusercontent.com/8088864/125714635-3ff51432-6415-40df-938d-c4c2fe654ca2.jpg) + +#### BFC可以包含浮动的元素(清除浮动) + +正常情况下,浮动的元素会脱离普通文档流,所以下面的代码里: + +``` html + + + + + +
+
+
+ + +``` + +外层的div会无法包含 内部浮动的div。 + +效果如下所示: + +![外层的div会无法包含内部浮动的div](https://user-images.githubusercontent.com/8088864/125714940-1de23469-a365-47f4-82ab-3a89fea5441b.jpg) + +但如果我们 触发外部容器的BFC,根据BFC规范中的第4条:计算BFC的高度时,浮动元素也参与计算,那么外部div容器就可以包裹着浮动元素,所以只要把代码修改如下: + +``` html + + + + + +
+
+
+ + +``` + +效果如下所示: + +![利用BFC外层的div会包含内部浮动的div](https://user-images.githubusercontent.com/8088864/125715066-4a11c8a9-caef-4258-acfb-87e3cc8b8302.jpg) + +#### BFC可以阻止元素被浮动元素覆盖 + +正常情况下,浮动的元素会脱离普通文档流,会覆盖着普通文档流的元素上。所以下面的代码里: + +``` html + + + + + +
+
+ + +``` + +效果如下所示: + +![浮动的元素会脱离普通文档流,会覆盖着普通文档流的元素上](https://user-images.githubusercontent.com/8088864/125716169-ccf5e6b4-f51e-431e-8aff-0b8e753b88a8.png) + + +之所以是这样,是因为上文的 规则5: 每个元素的左边,与包含的盒子的左边相接触,即使存在浮动也是如此; + +所以要想改变效果,使其互补干扰,就得利用规则6 :BFC的区域不会与float重叠,让 \
也能触发BFC的性质。 + +将代码改成下列所示: + +``` html + + + + + +
+
+ + +``` + +效果如下所示: + +![利用BFC可以阻止元素被浮动元素覆盖](https://user-images.githubusercontent.com/8088864/125716325-7b9fe487-9b6e-4d35-b8b7-33839bb9ebce.png) + +通过这种方法,就能 用来实现 两列的自适应布局。 + +### 简要介绍IFC + +1. 框会从包含块的顶部开始,一个接一个地水平摆放。 + +2. 摆放这些框时,它们在水平方向的 内外边距+边框 所占用的空间都会被考虑; + 在垂直方向上,这些框可能会以不同形式来对齐; + 水平的margin、padding、border有效,垂直无效,不能指定宽高。 + +3. 行框的宽度是 由包含块和存在的浮动来决定; + 行框的高度 由行高来决定。 + + +## 题目:谈一谈你对CSS盒模型的认识 + +> 专业的面试,一定会问 `CSS` 盒模型。对于这个题目,我们要回答一下几个方面: + +1. 基本概念:`content`、`padding`、`margin` +2. 标准盒模型、`IE`盒模型的区别。不要漏说了`IE`盒模型,通过这个问题,可以筛选一部分人 +3. `CSS`如何设置这两种模型(即:如何设置某个盒子为其中一个模型)?如果回答了上面的第二条,还会继续追问这一条。 +4. `JS`如何设置、获取盒模型对应的宽和高?这一步,已经有很多人答不上来了。 +5. 实例题:根据盒模型解释**边距重叠**。 + +> 前四个方面是逐渐递增,第五个方面,却鲜有人知。 + +6. `BFC`(边距重叠解决方案)或`IFC`。 + +> 如果能回答第五条,就会引出第六条。`BFC`是面试频率较高的。 + +**总结**:以上几点,从上到下,知识点逐渐递增,知识面从理论、`CSS`、`JS`,又回到`CSS`理论 + +接下来,我们把上面的六条,依次讲解。 + + +**标准盒模型和IE盒子模型** + + +标准盒子模型: + +![](http://img.smyhvae.com/2015-10-03-css-27.jpg) + +`IE`盒子模型: + +![](http://img.smyhvae.com/2015-10-03-css-30.jpg) + +上图显示: + + +> 在 `CSS` 盒子模型 (`Box Model`) 规定了元素处理元素的几种方式: + +- `width`和`height`:**内容**的宽度、高度(不是盒子的宽度、高度)。 +- `padding`:内边距。 +- `border`:边框。 +- `margin`:外边距。 + +> `CSS`盒模型和`IE`盒模型的区别: + + - 在**标准盒子模型**中,**width 和 height 指的是内容区域**的宽度和高度。增加内边距、边框和外边距不会影响内容区域的尺寸,但是会增加元素框的总尺寸。 + + - **IE盒子模型**中,**width 和 height 指的是内容区域+border+padding**的宽度和高度。 + + +**CSS如何设置这两种模型** + +代码如下: + +```javascript +/* 设置当前盒子为 标准盒模型(默认) */ +box-sizing: content-box; + +/* 设置当前盒子为 IE盒模型 */ +box-sizing: border-box; +``` + + +> 备注:盒子默认为标准盒模型。 + + +**JS如何设置、获取盒模型对应的宽和高** + + +> 方式一:通过`DOM`节点的 `style` 样式获取 + + +```js +element.style.width/height; +``` + +> 缺点:通过这种方式,只能获取**行内样式**,不能获取`内嵌`的样式和`外链`的样式。 + +这种方式有局限性,但应该了解。 + + + +> 方式二(通用型) + + +```js +window.getComputedStyle(element).width/height; +``` + + +> 方式二能兼容 `Chrome`、火狐。是通用型方式。 + + +> 方式三(IE独有的) + + +```javascript + element.currentStyle.width/height; +``` + +> 和方式二相同,但这种方式只有IE独有。获取到的即时运行完之后的宽高(三种css样式都可以获取)。 + + +> 方式四 + + +```javascript + element.getBoundingClientRect().width/height; +``` + +> 此 `api` 的作用是:获取一个元素的绝对位置。绝对位置是视窗 `viewport` 左上角的绝对位置。此 `api` 可以拿到四个属性:`left`、`top`、`width`、`height`。 + +**总结:** + +> 上面的四种方式,要求能说出来区别,以及哪个的通用型更强。 + + +**margin塌陷/margin重叠** + + +**标准文档流中,竖直方向的margin不叠加,只取较大的值作为margin**(水平方向的`margin`是可以叠加的,即水平方向没有塌陷现象)。 + +> PS:如果不在标准流,比如盒子都浮动了,那么两个盒子之间是没有`margin`重叠的现象的。 + + +> 我们来看几个例子。 + +**兄弟元素之间** + +如下图所示: + +![](http://img.smyhvae.com/20170805_0904.png) + + +**子元素和父元素之间** + + +```html + + + + + Document + + + +
+
+
+ + + +``` + +> 上面的代码中,儿子的`height`是 `100p`x,`magin-top` 是`10px`。注意,此时父亲的 `height` 是`100`,而不是`110`。因为儿子和父亲在竖直方向上,共一个`margin`。 + +儿子这个盒子: + +![](http://img.smyhvae.com/20180305_2216.png) + +父亲这个盒子: + +![](http://img.smyhvae.com/20180305_2217.png) + + +> 上方代码中,如果我们给父亲设置一个属性:`overflow: hidden`,就可以避免这个问题,此时父亲的高度是110px,这个用到的就是BFC(下一段讲解)。 + + +**善于使用父亲的padding,而不是儿子的margin** + +> 其实,这一小段讲的内容与上一小段相同,都是讲父子之间的margin重叠。 + +我们来看一个奇怪的现象。现在有下面这样一个结构:(`div`中放一个`p`) + +```html +
+

+
+``` + +> 上面的结构中,我们尝试通过给儿子`p`一个`margin-top:50px;`的属性,让其与父亲保持50px的上边距。结果却看到了下面的奇怪的现象: + +![](http://img.smyhvae.com/20170806_1537.png) + + +> 此时我们给父亲`div`加一个`border`属性,就正常了: + +![](http://img.smyhvae.com/20170806_1544.png) + + +> 如果父亲没有`border`,那么儿子的`margin`实际上踹的是“流”,踹的是这“行”。所以,父亲整体也掉下来了。 + +**margin这个属性,本质上描述的是兄弟和兄弟之间的距离; 最好不要用这个marign表达父子之间的距离。** + +> 所以,如果要表达父子之间的距离,我们一定要善于使用父亲的padding,而不是儿子的`margin。 + + +**BFC(边距重叠解决方案)** + +> `BFC(Block Formatting Context)`:块级格式化上下文。你可以把它理解成一个独立的区域。 + +另外还有个概念叫`IFC`。不过,`BFC`问得更多。 + +**BFC 的原理/BFC的布局规则【非常重要】** + +> `BFC` 的原理,其实也就是 `BFC` 的渲染规则(能说出以下四点就够了)。包括: + +1. BFC **内部的**子元素,在垂直方向,**边距会发生重叠**。 +2. BFC在页面中是独立的容器,外面的元素不会影响里面的元素,反之亦然。(稍后看`举例1`) +3. **BFC区域不与旁边的`float box`区域重叠**。(可以用来清除浮动带来的影响)。(稍后看`举例2`) +4. 计算`BFC`的高度时,浮动的子元素也参与计算。(稍后看`举例3`) + +**如何生成BFC** + +> 有以下几种方法: + +- 方法1:`overflow`: 不为`visible`,可以让属性是 `hidden`、`auto`。【最常用】 +- 方法2:浮动中:`float`的属性值不为`none`。意思是,只要设置了浮动,当前元素就创建了`BFC`。 +- 方法3:定位中:只要`posiiton`的值不是 s`tatic`或者是`relative`即可,可以是`absolute`或`fixed`,也就生成了一个`BFC`。 +- 方法4:`display`为`inline-block`, `table-cell`, `table-caption`, `flex`, `inline-flex` + +**BFC 的应用** + + +**举例1:**解决 margin 重叠 + +> 当父元素和子元素发生 `margin` 重叠时,解决办法:**给子元素或父元素创建BFC**。 + +比如说,针对下面这样一个 `div` 结构: + + +```html +
+

+

+
+``` + +> 上面的`div`结构中,如果父元素和子元素发生`margin`重叠,我们可以给子元素创建一个 `BFC`,就解决了: + + +```html +
+

+

+
+``` + +> 因为**第二条:BFC区域是一个独立的区域,不会影响外面的元素**。 + + +**举例2**:BFC区域不与float区域重叠: + +针对下面这样一个div结构; + +```html + + + + + Document + + + + +
+
+ 左侧,生命壹号 +
+
+ 右侧,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae, +
+
+ + + +``` + +效果如下: + +![](http://img.smyhvae.com/20180306_0825.png) + +> 上图中,由于右侧标准流里的元素,比左侧浮动的元素要高,导致右侧有一部分会跑到左边的下面去。 + +**如果要解决这个问题,可以将右侧的元素创建BFC**,因为**第三条:BFC区域不与`float box`区域重叠**。解决办法如下:(将right区域添加overflow属性) + +```html +
+ 右侧,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae,smyhvae, +
+``` + + +![](http://img.smyhvae.com/20180306_0827.png) + +上图表明,解决之后,`father-layout`的背景色显现出来了,说明问题解决了。 + + +**举例3:**清除浮动 + +现在有下面这样的结构: + + +```html + + + + + Document + + + + +
+
+ 生命壹号 +
+ +
+ + +``` + +效果如下: + +![](http://img.smyhvae.com/20180306_0840.png) + +上面的代码中,儿子浮动了,但由于父亲没有设置高度,导致看不到父亲的背景色(此时父亲的高度为0)。正所谓**有高度的盒子,才能关住浮动**。 + +> 如果想要清除浮动带来的影响,方法一是给父亲设置高度,然后采用隔墙法。方法二是 BFC:给父亲增加 `overflow=hidden`属性即可, 增加之后,效果如下: + +![](http://img.smyhvae.com/20180306_0845.png) + +> 为什么父元素成为BFC之后,就有了高度呢?这就回到了**第四条:计算BFC的高度时,浮动元素也参与计算**。意思是,**在计算BFC的高度时,子元素的float box也会参与计算** + + +## DOM事件的总结 + +**知识点主要包括以下几个方面:** + +- 基本概念:`DOM`事件的级别 + +> 面试不会直接问你,DOM有几个级别。但会在题目中体现:“请用`DOM2` ....”。 + + +- `DOM`事件模型、`DOM`事件流 + +> 面试官如果问你“**DOM事件模型**”,你不一定知道怎么回事。其实说的就是**捕获和冒泡**。 + +**DOM事件流**,指的是事件传递的**三个阶段**。 + +- 描述`DOM`事件捕获的具体流程 + +> 讲的是事件的传递顺序。参数为`false`(默认)、参数为`true`,各自代表事件在什么阶段触发。 + +能回答出来的人,寥寥无几。也许有些人可以说出一大半,但是一字不落的人,极少。 + +- `Event`对象的常见应用(`Event`的常用`api`方法) + +> `DOM`事件的知识点,一方面包括事件的流程;另一方面就是:怎么去注册事件,也就是监听用户的交互行为。第三点:在响应时,`Event`对象是非常重要的。 + +**自定义事件(非常重要)** + +> 一般人可以讲出事件和注册事件,但是如果让你讲**自定义事件**,能知道的人,就更少了。 + +**DOM事件的级别** + +> `DOM`事件的级别,准确来说,是**DOM标准**定义的级别。包括: + +**DOM0的写法:** + +```javascript + element.onclick = function () { + + } +``` + + +> 上面的代码是在 `js` 中的写法;如果要在`html`中写,写法是:在`onclick`属性中,加 `js` 语句。 + + +**DOM2的写法:** + + +```javascript + element.addEventListener('click', function () { + + }, false); +``` + +>【重要】上面的第三参数中,**true**表示事件在**捕获阶段**触发,**false**表示事件在**冒泡阶段**触发(默认)。如果不写,则默认为false。 + + +**DOM3的写法:** + + +```javascript + element.addEventListener('keyup', function () { + + }, false); +``` + +> `DOM3`中,增加了很多事件类型,比如鼠标事件、键盘事件等。 + +> PS:为何事件没有`DOM1`的写法呢?因为,`DOM1`标准制定的时候,没有涉及与事件相关的内容。 + +**总结**:关于“DOM事件的级别”,能回答出以上内容即可,不会出题目让你做。 + +**DOM事件模型** + +> `DOM`事件模型讲的就是**捕获和冒泡**,一般人都能回答出来。 + +- 捕获:从上往下。 +- 冒泡:从下(目标元素)往上。 + +**DOM事件流** + +> `DOM`事件流讲的就是:浏览器在于当前页面做交互时,这个事件是怎么传递到页面上的。 + +**完整的事件流,分三个阶段:** + +1. 捕获:从 `window` 对象传到 目标元素。 +2. 目标阶段:事件通过捕获,到达目标元素,这个阶段就是目标阶段。 +3. 冒泡:从**目标元素**传到 `Window` 对象。 + +![](http://img.smyhvae.com/20180306_1058.png) + +![](http://img.smyhvae.com/20180204_1218.jpg) + + +**描述DOM事件捕获的具体流程** + +> 很少有人能说完整。 + +**捕获的流程** + + +![](http://img.smyhvae.com/20180306_1103.png) + +**说明**:捕获阶段,事件依次传递的顺序是:`window` --> `document` --> `html`--> `body` --> 父元素、子元素、目标元素。 + +- PS1:第一个接收到事件的对象是 **window**(有人会说`body`,有人会说`html`,这都是错误的)。 +- PS2:`JS`中涉及到`DOM`对象时,有两个对象最常用:`window`、`doucument`。它们俩也是最先获取到事件的。 + +代码如下: + +```javascript + window.addEventListener("click", function () { + alert("捕获 window"); + }, true); + + document.addEventListener("click", function () { + alert("捕获 document"); + }, true); + + document.documentElement.addEventListener("click", function () { + alert("捕获 html"); + }, true); + + document.body.addEventListener("click", function () { + alert("捕获 body"); + }, true); + + fatherBox.addEventListener("click", function () { + alert("捕获 father"); + }, true); + + childBox.addEventListener("click", function () { + alert("捕获 child"); + }, true); + +``` + + +**补充一个知识点:** + +> 在 `js`中: + +- 如果想获取 `body` 节点,方法是:`document.body`; +- 但是,如果想获取 `html`节点,方法是`document.documentElement`。 + + +**冒泡的流程** + +> 与捕获的流程相反 + + +**Event对象的常见 api 方法** + +> 用户做的是什么操作(比如,是敲键盘了,还是点击鼠标了),这些事件基本都是通过`Event`对象拿到的。这些都比较简单,我们就不讲了。我们来看看下面这几个方法: + +**方法一** + +```javascript + event.preventDefault(); +``` + +- 解释:阻止默认事件。 +- 比如,已知``标签绑定了click事件,此时,如果给``设置了这个方法,就阻止了链接的默认跳转。 + +**方法二:阻止冒泡** + +> 这个在业务中很常见。 + +> 有的时候,业务中不需要事件进行冒泡。比如说,业务这样要求:单击子元素做事件`A`,单击父元素做事件B,如果不阻止冒泡的话,出现的问题是:单击子元素时,子元素和父元素都会做事件`A`。这个时候,就要用到阻止冒泡了。 + + +> `w3c`的方法:(火狐、谷歌、`IE11`) + +```javascript + event.stopPropagation(); +``` + +> `IE10`以下则是: + +```javascript + event.cancelBubble = true; +``` + +> 兼容代码如下: + +```javascript + box3.onclick = function (event) { + + alert("child"); + + //阻止冒泡 + event = event || window.event; + + if (event && event.stopPropagation) { + event.stopPropagation(); + } else { + event.cancelBubble = true; + } + } +``` + +> 上方代码中,我们对`box3`进行了阻止冒泡,产生的效果是:事件不会继续传递到 `father`、`grandfather`、`body`了。 + + +**方法三:设置事件优先级** + + +```javascript + event.stopImmediatePropagation(); +``` + +这个方法比较长,一般人没听说过。解释如下: + +> 比如说,我用`addEventListener`给某按钮同时注册了事件`A`、事件`B`。此时,如果我单击按钮,就会依次执行事件A和事件`B`。现在要求:单击按钮时,只执行事件A,不执行事件`B`。该怎么做呢?这是时候,就可以用到`stopImmediatePropagation`方法了。做法是:在事件A的响应函数中加入这句话。 + +> 大家要记住 `event` 有这个方法。 + +**属性4、属性5(事件委托中用到)** + + +```javascript + + event.currentTarget //当前所绑定的事件对象。在事件委托中,指的是【父元素】。 + + event.target //当前被点击的元素。在事件委托中,指的是【子元素】。 + +``` + +上面这两个属性,在事件委托中经常用到。 + + +> **总结**:上面这几项,非常重要,但是容易弄混淆。 + + +**自定义事件** + +> 自定义事件的代码如下: + + +```javascript + var myEvent = new Event('clickTest'); + element.addEventListener('clickTest', function () { + console.log('smyhvae'); + }); + + //元素注册事件 + element.dispatchEvent(myEvent); //注意,参数是写事件对象 myEvent,不是写 事件名 clickTest + +``` + +> 上面这个事件是定义完了之后,就直接自动触发了。在正常的业务中,这个事件一般是和别的事件结合用的。比如延时器设置按钮的动作: + +```javascript + var myEvent = new Event('clickTest'); + + element.addEventListener('clickTest', function () { + console.log('smyhvae'); + }); + + setTimeout(function () { + element.dispatchEvent(myEvent); //注意,参数是写事件对象 myEvent,不是写 事件名 clickTest + }, 1000); +``` + +## 浅析CSS的性能优化:transform与position区别、硬件加速工作原理及注意事项、强制使用GPU渲染的友好CSS属性 + +在网上看到一个这样的问题: transform与position:absolute 有什么区别?查阅资料后发现这道题目其实不简单,涉及到重排、重绘、硬件加速等网页优化的知识。 + +### 问题背景 + +过去几年,我们常常会听说硬件加速给移动端带来了巨大的体验提升,但是即使对于很多经验丰富的开发者来说,恐怕对其背后的工作原理也是模棱两可,更不要合理地将其运用到网页的动画效果中了。 + +#### 1. position + top/left 的效果 + +下面让我们来看一个动画效果,在该动画中包含了几个堆叠在一起的球并让它们沿相同路径移动。最简单的方式就是实时调整它们的 left 和 top 属性,使用 css 动画实现。 + +``` html + + + + + +
+ + +``` + +在运行的时候,即使是在电脑浏览器上也会隐约觉得动画的运行并不流畅,动画有些停顿的感觉,更不要提在移动端达到 60fps 的流畅效果了。这是因为top和left的改变会触发浏览器的 reflow 和 repaint ,整个动画过程都在不断触发浏览器的重新渲染,这个过程是很影响性能的。 + +#### 2. transform 的效果 + +为了解决这个问题,我们使用 transform 中的 translate() 来替换 top 和 left ,重写一下这个动画效果。 + + +``` html + + + + + +
+ + +``` + +这时候会发现整个动画效果流畅了很多,在动画移动的过程中也没有发生repaint和reflow。 + +那么,为什么 transform 没有触发 repaint 呢?原因就是:transform 动画由GPU控制,支持硬件加速,并不需要软件方面的渲染。 + +### 硬件加速工作原理 + +浏览器接收到页面文档后,会将文档中的标记语言解析为DOM树,DOM树和CSS结合后形成浏览器构建页面的渲染树,渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到GPU形成渲染纹理,而图层在GPU中 transform 是不会触发 repaint 的,这一点非常类似3D绘图功能,最终这些使用transform的图层都会使用独立的合成器进程进行处理。 + +在我们的示例中,CSS transform 创建了一个新的复合图层,可以被GPU直接用来执行 transform 操作。在chrome开发者工具中开启“show layer borders”选项后,每个复合图层就会显示一条黄色的边界。示例中的球就处于一个独立的复合图层,移动时的变化也是独立的。 + +此时,你也许会问:浏览器什么时候会创建一个独立的复合图层呢?事实上一般是在以下几种情况下: + + 1. 3D 或者 CSS transform + 2. video或canvas标签 + 3. CSS filters + 4. 元素覆盖时,比如使用了 z-index 属性 + +等一下,上面的示例使用的是 2D transform 而不是 3D transform 啊?这个说法没错,所以在timeline中我们可以看到:动画开始和结束的时候发生了两次 repaint 操作。 + +![CSS transform网页的重绘时间轴](https://user-images.githubusercontent.com/8088864/125720131-5776ac63-b267-4699-9cbf-06a86c80689b.png) + +3D 和 2D transform 的区别就在于,浏览器在页面渲染前为3D动画创建独立的复合图层,而在运行期间为2D动画创建。 + +动画开始时,生成新的复合图层并加载为GPU的纹理用于初始化 repaint,然后由GPU的复合器操纵整个动画的执行,最后当动画结束时,再次执行 repaint 操作删除复合图层。 + +### 使用 GPU 渲染元素 + +#### 能触发GPU渲染的属性 + +并不是所有的CSS属性都能触发GPU的硬件加速,实际上只有少数属性可以,比如下面的这些: + +1. transform +2. opacity +3. filter + +#### 强制使用GPU渲染 + +为了避免 2D transform 动画在开始和结束时发生的 repaint 操作,我们可以硬编码一些样式来解决这个问题: + +``` css +.exam1 { + transform: translateZ(0); +} + +.exam2 { + transform: rotateZ(360deg); +} +``` + +这段代码的作用就是让浏览器执行 3D transform,浏览器通过该样式创建了一个独立图层,图层中的动画则有GPU进行预处理并且触发了硬件加速。 + +#### 使用硬件加速需要注意的事项 + +使用硬件加速并不是十全十美的事情,比如: + +1. 内存。如果GPU加载了大量的纹理,那么很容易就会发生内存问题,这一点在移动端浏览器上尤为明显,所以,一定要牢记不要让页面的每个元素都使用硬件加速。 +2. 使用GPU渲染会影响字体的抗锯齿效果。这是因为GPU和CPU具有不同的渲染机制,即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。 + +#### will-change + +浏览器还提出了一个 will-change 属性,该属性允许开发者告知浏览器哪一个属性即将发生变化,从而为浏览器对该属性进行优化提供了时间。下面是一个使用 will-change 的示例 + +``` css +.exam3 { + will-change: transform; +} +``` + +缺点在于其兼容性不大好。 + +### 总结 + +1. transform 会使用 GPU 硬件加速,性能更好;position + top/left 会触发大量的重绘和回流,性能影响较大。 +2. 硬件加速的工作原理是创建一个新的复合图层,然后使用合成线程进行渲染。 +3. 3D 动画 与 2D 动画的区别;2D动画会在动画开始和动画结束时触发2次重新渲染。 +4. 使用GPU可以优化动画效果,但是不要滥用,会有内存问题。 +5. 理解强制触发硬件加速的 transform 技巧,使用对GPU友好的CSS属性。 + + +## 五、渲染机制 + +**浏览器的渲染机制一般分为以下几个步骤** + +- 处理 `HTML` 并构建 `DOM` 树。 +- 处理 `CSS` 构建 `CSSOM` 树。 +- 将 `DOM` 与 `CSSOM` 合并成一个渲染树。 +- 根据渲染树来布局,计算每个节点的位置。 +- 调用 `GPU` 绘制,合成图层,显示在屏幕上 + +![](https://user-gold-cdn.xitu.io/2018/4/11/162b2ab2ec70ac5b?w=900&h=352&f=png&s=49983) + +- 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢 +- 当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM + +### 5.1 图层 + +> 一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用 + +**通过以下几个常用属性可以生成新图层** + +- 3D 变换:`translate3d`、`translateZ` +- `will-change` +- `video`、`iframe` 标签 +- 通过动画实现的 `opacity` 动画转换 +- `position: fixed` + +### 5.2 重绘(Repaint)和回流(Reflow) + +- 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘 +- 回流是布局或者几何属性需要改变就称为回流 + +> 回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流 + +**所以以下几个动作可能会导致性能问题**: + +- 改变 window 大小 +- 改变字体 +- 添加或删除样式 +- 文字改变 +- 定位或者浮动 +- 盒模型 + +**很多人不知道的是,重绘和回流其实和 Event loop 有关** + +- 当 Event loop 执行完 `Microtasks` 后,会判断 `document` 是否需要更新。因为浏览器是 `60Hz `的刷新率,每 `16ms `才会更新一次。 +- 然后判断是否有 `resize` 或者 `scroll` ,有的话会去触发事件,所以 `resize` 和 `scroll` 事件也是至少 `16ms` 才会触发一次,并且自带节流功能。 +- 判断是否触发了` media query` +- 更新动画并且发送事件 +- 判断是否有全屏操作事件 +- 执行 `requestAnimationFrame` 回调 +- 执行 `IntersectionObserver` 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 +- 更新界面 +- 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 `requestIdleCallback` 回调 + +**减少重绘和回流** + +- 使用 `translate` 替代 `top` +- 使用 `visibility` 替换` display: none` ,因为前者只会引起重绘,后者会引发回流(改变了布局) +- 不要使用 `table` 布局,可能很小的一个小改动会造成整个 table 的重新布局 +- 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 `requestAnimationFrame` +- `CSS` 选择符从右往左匹配查找,避免 `DOM` 深度过深 +- 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 `video `标签,浏览器会自动将该节点变为图层 + +## 深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调 + +### 前言 + +关于 Event Loop 的文章很多,但是有很多只是在讲「宏任务」、「微任务」,我先提出几个问题: + +1. 每一轮 Event Loop 都会伴随着渲染吗? +2. requestAnimationFrame 在哪个阶段执行,在渲染前还是后?在 microTask 的前还是后? +3. requestIdleCallback 在哪个阶段执行?如何去执行?在渲染前还是后?在 microTask 的前还是后? +4. resize、scroll 这些事件是何时去派发的。 + +这些问题并不是刻意想刁难你,如果你不知道这些,那你可能并不能在遇到一个动画需求的时候合理的选择 requestAnimationFrame,你可能在做一些需求的时候想到了 requestIdleCallback,但是你不知道它运行的时机,只是胆战心惊的去用它,祈祷不要出线上 bug。 + +这也是本文想要从规范解读入手,深挖底层的动机之一。本文会酌情从规范中排除掉一些比较晦涩难懂,或者和主流程不太相关的概念。更详细的版本也可以直接去读这个规范,不过比较费时费力。 + +### 事件循环 + +我们先依据HTML 官方规范从浏览器的事件循环讲起,因为剩下的 API 都在这个循环中进行,它是浏览器调度任务的基础。 + +#### 定义 + +为了协调事件,用户交互,脚本,渲染,网络任务等,浏览器必须使用本节中描述的事件循环。 + +#### 流程 + +1. 从任务队列中取出一个宏任务并执行。 + +2. 检查微任务队列,执行并清空微任务队列,如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行。 + +3. 进入更新渲染阶段,判断是否需要渲染,这里有一个 rendering opportunity 的概念,也就是说不一定每一轮 event loop 都会对应一次浏览器渲染,要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的。(所以多个 task 很可能在一次渲染之间执行) + + - 浏览器会尽可能的保持帧率稳定,例如页面性能无法维持 60fps(每 16.66ms 渲染一次)的话,那么浏览器就会选择 30fps 的更新速率,而不是偶尔丢帧。 + - 如果浏览器上下文不可见,那么页面会降低到 4fps 左右甚至更低。 + - 如果满足以下条件,也会跳过渲染: + + 1. 浏览器判断更新渲染不会带来视觉上的改变。 + 2. map of animation frame callbacks 为空,也就是帧动画回调为空,可以通过 requestAnimationFrame 来请求帧动画。 + +4. 如果上述的判断决定本轮不需要渲染,那么下面的几步也不会继续运行: + This step enables the user agent to prevent the steps below from running for other reasons, for example, to ensure certain tasks are executed immediately after each other, with only microtask checkpoints interleaved (and without, e.g., animation frame callbacks interleaved). Concretely, a user agent might wish to coalesce timer callbacks together, with no intermediate rendering updates. 有时候浏览器希望两次「定时器任务」是合并的,他们之间只会穿插着 microTask的执行,而不会穿插屏幕渲染相关的流程(比如requestAnimationFrame,下面会写一个例子)。 + +5. 对于需要渲染的文档,如果窗口的大小发生了变化,执行监听的 `resize` 方法。 + +6. 对于需要渲染的文档,如果页面发生了滚动,执行 `scroll` 方法。 + +7. 对于需要渲染的文档,执行帧动画回调,也就是 `requestAnimationFrame` 的回调。(后文会详解) + +8. 对于需要渲染的文档,执行 `IntersectionObserver` 的回调。 + +9. 对于需要渲染的文档,**重新渲染**绘制用户界面。 + +10. 判断 `task队列`和`microTask队列`是否都为空,如果是的话,则进行 `Idle` 空闲周期的算法,判断是否要执行 `requestIdleCallback` 的回调函数。(后文会详解) + +对于 `resize` 和 `scroll` 来说,并不是到了这一步才去执行滚动和缩放,那岂不是要延迟很多?浏览器当然会立刻帮你滚动视图,根据CSSOM 规范所讲,浏览器会保存一个 `pending scroll event targets`,等到事件循环中的 `scroll` 这一步,去派发一个事件到对应的目标上,驱动它去执行监听的回调函数而已。`resize` 也是同理。 +可以在这个流程中仔细看一下「宏任务」、「微任务」、「渲染」之间的关系。 +多任务队列 + +#### 多任务队列 + +task 队列并不是我们想象中的那样只有一个,根据规范里的描述: + +An event loop has one or more task queues. For example, a user agent could have one task queue for mouse and key events (to which the user interaction task source is associated), and another to which all other task sources are associated. Then, using the freedom granted in the initial step of the event loop processing model, it could give keyboard and mouse events preference over other tasks three-quarters of the time, keeping the interface responsive but not starving other task queues. Note that in this setup, the processing model still enforces that the user agent would never process events from any one task source out of order. + +事件循环中可能会有一个或多个任务队列,这些队列分别为了处理: + +1. 鼠标和键盘事件 +2. 其他的一些 Task + +览器会在保持任务顺序的前提下,可能分配四分之三的优先权给鼠标和键盘事件,保证用户的输入得到最高优先级的响应,而剩下的优先级交给其他 Task,并且保证不会“饿死”它们。 + +这个规范也导致 Vue 2.0.0-rc.7 这个版本 `nextTick` 采用了从微任务 `MutationObserver` 更换成宏任务 `postMessage` 而导致了一个 [Issue](https://github.com/vuejs/vue/issues/3771#issuecomment-249692588)。 +目前由于一些“未知”的原因,`jsfiddle` 的案例打不开了。简单描述一下就是采用了 `task` 实现的 `nextTick`,在用户持续滚动的情况下 `nextTick` 任务被延后了很久才去执行,导致动画跟不上滚动了。 + +迫于无奈,尤大还是改回了 `microTask` 去实现 `nextTick`,当然目前来说 `promise.then` 微任务已经比较稳定了,并且 Chrome 也已经实现了 `queueMicroTask` 这个官方 API。不久的未来,我们想要调用微任务队列的话,也可以节省掉实例化 `Promise` 在开销了。 + +从这个 Issue 的例子中我们可以看出,稍微去深入了解一下规范还是比较有好处的,以免在遇到这种比较复杂的 Bug 的时候一脸懵逼。 + +#### requestAnimationFrame + +在解读规范的过程中,我们发现 `requestAnimationFrame` 的回调有两个特征: + +1. 在重新渲染前调用。 +2. 很可能在宏任务之后不调用。 + +我们来分析一下,为什么要在重新渲染前去调用?因为 `rAF` 是官方推荐的用来做一些流畅动画所应该使用的 API,做动画不可避免的会去更改 DOM,而如果在渲染之后再去更改 DOM,那就只能等到下一轮渲染机会的时候才能去绘制出来了,这显然是不合理的。 + +`rAF`在浏览器决定渲染之前给你最后一个机会去改变 DOM 属性,然后很快在接下来的绘制中帮你呈现出来,所以这是做流畅动画的不二选择。下面我用一个 setTimeout的例子来对比。 + +##### 闪烁动画 + +假设我们现在想要快速的让屏幕上闪烁 红、蓝两种颜色,保证用户可以观察到,如果我们用 `setTimeout` 来写,并且带着我们长期的误解「宏任务之间一定会伴随着浏览器绘制」,那么你会得到一个预料之外的结果。 + +``` js +setTimeout(() => { + document.body.style.background = "red"; + setTimeout(() => { + document.body.style.background = "blue"; + }); +}); +``` + +![setTimeout闪烁动画](https://user-images.githubusercontent.com/8088864/125749050-c757f81e-6482-4262-a0d4-c455eb78d4f4.gif) + +以看出这个结果是非常不可控的,如果这两个 `Task` 之间正好遇到了浏览器认定的渲染机会,那么它会重绘,否则就不会。由于这俩宏任务的间隔周期太短了,所以很大概率是不会的。 + +如果你把延时调整到 17ms 那么重绘的概率会大很多,毕竟这个是一般情况下 60fps 的一个指标。但是也会出现很多不绘制的情况,所以并不稳定。 +如果你依赖这个 API 来做动画,那么就很可能会造成「掉帧」。 + +接下来我们换成 rAF 试试?我们用一个递归函数来模拟 10 次颜色变化的动画。 + +``` js +let i = 10; +let req = () => { + i--; + requestAnimationFrame(() => { + document.body.style.background = "red"; + requestAnimationFrame(() => { + document.body.style.background = "blue"; + if (i > 0) { + req(); + } + }); + }); +}; + +req(); +``` + +这里由于颜色变化太快,gif 录制软件没办法截出这么高帧率的颜色变换,所以各位可以放到浏览器中自己执行一下试试,我这边直接抛结论,浏览器会非常规律的把这 10 组也就是 20 次颜色变化绘制出来,可以看下 performance 面板记录的表现: + +![requestAnimationFrame闪烁动画](https://user-images.githubusercontent.com/8088864/125750295-ab491df6-c612-4add-b2fe-8819fcf47ef1.png) + +##### 定时器合并 + +在第一节解读规范的时候,第 4 点中提到了,定时器宏任务可能会直接跳过渲染。 + +按照一些常规的理解来说,宏任务之间理应穿插渲染,而定时器任务就是一个典型的宏任务,看一下以下的代码: + +``` js +setTimeout(() => { + console.log("sto1") + requestAnimationFrame(() => console.log("rAF1")) +}) +setTimeout(() => { + console.log("sto2") + requestAnimationFrame(() => console.log("rAF2")) +}) + +queueMicrotask(() => console.log("mic1")) +queueMicrotask(() => console.log("mic2")) +``` + +从直觉上来看,顺序是不是应该是: + +``` text +mic1 +mic2 +sto1 +rAF1 +sto2 +rAF2 +``` + +呢?也就是每一个宏任务之后都紧跟着一次渲染。 + +实际上不会,浏览器会合并这两个定时器任务: + +``` text +mic1 +mic2 +sto1 +sto2 +rAF1 +rAF2 +``` + +#### requestIdleCallback + +#### 草案解读 + +我们都知道 `requestIdleCallback` 是浏览器提供给我们的空闲调度算法,关于它的简介可以看 [MDN 文档](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback),意图是让我们把一些计算量较大但是又没那么紧急的任务放到空闲时间去执行。不要去影响浏览器中优先级较高的任务,比如动画绘制、用户输入等等。 + +React 的时间分片渲染就想要用到这个 API,不过目前浏览器支持的不给力,他们是自己去用 postMessage 实现了一套。 + +**渲染有序进行** + +首先看一张图,很精确的描述了这个 API 的意图: + +![浏览器渲染有序调度](https://user-images.githubusercontent.com/8088864/125756615-7bec3496-94cc-46ba-9298-5df48b99d2d8.png) + +当然,这种有序的 `浏览器 -> 用户 -> 浏览器 -> 用户` 的调度基于一个前提,就是我们要把任务切分成比较小的片,不能说浏览器把空闲时间让给你了,你去执行一个耗时 10s 的任务,那肯定也会把浏览器给阻塞住的。这就要求我们去读取 `rIC` 提供给你的 `deadline` 里的时间,去动态的安排我们切分的小任务。浏览器信任了你,你也不能辜负它呀。 + +**渲染长期空闲** + +![浏览器渲染长期空闲调度](https://user-images.githubusercontent.com/8088864/125756805-79afd49b-e62d-45b9-bb7e-4a4b34eb7bd4.png) + +还有一种情况,也有可能在几帧的时间内浏览器都是空闲的,并没有发生任何影响视图的操作,它也就不需要去绘制页面: +这种情况下为什么还是会有 50ms 的 deadline 呢?是因为浏览器为了提前应对一些可能会突发的用户交互操作,比如用户输入文字。如果给的时间太长了,你的任务把主线程卡住了,那么用户的交互就得不到回应了。50ms 可以确保用户在无感知的延迟下得到回应。 + +MDN 文档中的[幕后任务协作调度 API](https://developer.mozilla.org/zh-CN/docs/Web/API/Background_Tasks_API) 介绍的比较清楚,来根据里面的概念做个小实验: + +屏幕中间有个红色的方块,把 MDN 文档中[requestAnimationFrame](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame)的范例部分的动画代码直接复制过来。 + +草案中还提到: + +1. 当浏览器判断这个页面对用户不可见时,这个回调执行的频率可能被降低到 10 秒执行一次,甚至更低。这点在解读 EventLoop 中也有提及。 + +2. 如果浏览器的工作比较繁忙的时候,不能保证它会提供空闲时间去执行 rIC 的回调,而且可能会长期的推迟下去。所以如果你需要保证你的任务在一定时间内一定要执行掉,那么你可以给 rIC 传入第二个参数 timeout。 +这会强制浏览器不管多忙,都在超过这个时间之后去执行 rIC 的回调函数。所以要谨慎使用,因为它会打断浏览器本身优先级更高的工作。 + +3. 最长期限为 50 毫秒,是根据研究得出的,研究表明,人们通常认为 100 毫秒内对用户输入的响应是瞬时的。 将闲置截止期限设置为 50ms 意味着即使在闲置任务开始后立即发生用户输入,浏览器仍然有剩余的 50ms 可以在其中响应用户输入而不会产生用户可察觉的滞后。 + +4. 每次调用 timeRemaining() 函数判断是否有剩余时间的时候,如果浏览器判断此时有优先级更高的任务,那么会动态的把这个值设置为 0,否则就是用预先设置好的 deadline - now 去计算。 + +5. 这个 timeRemaining() 的计算非常动态,会根据很多因素去决定,所以不要指望这个时间是稳定的。 + +#### 动画例子 + +**滚动** + +如果我鼠标不做任何动作和交互,直接在控制台通过 rIC 去打印这次空闲任务的剩余时间,一般都稳定维持在 49.xx ms,因为此时浏览器没有什么优先级更高的任务要去处理。 + +``` js +requestIdleCallback((deadline) => console.log(deadline.timeRemaining())) +``` + +![requetIdleCallback的timeRemaining时间1](https://user-images.githubusercontent.com/8088864/125778161-1e903c20-19a8-4340-83e9-af15ff16078a.gif) + +而如果我不停的滚动浏览器,不断的触发浏览器的重新绘制的话,这个时间就变的非常不稳定了。 + +![requetIdleCallback的timeRemaining时间2](https://user-images.githubusercontent.com/8088864/125778195-dc9bc852-60e6-475d-a50e-441707e793ff.gif) + +通过这个例子,你可以更加有体感的感受到什么样叫做「繁忙」,什么样叫做「空闲」。 + + +**动画** + +这个动画的例子很简单,就是利用rAF在每帧渲染前的回调中把方块的位置向右移动 10px。 + +``` html + + + + + + Document + + + +
+ + + +``` + +注意在最后我加了一个 requestIdleCallback 的函数,回调里会 alert('rIC'),来看一下演示效果: + +![requetIdleCallback和requestAnmationFrame动画](https://user-images.githubusercontent.com/8088864/125778813-19d6dde2-2b12-4754-bd4c-deba1209d3e6.gif) + +alert 在最开始的时候就执行了,为什么会这样呢一下,想一下「空闲」的概念,我们每一帧仅仅是把 left 的值移动了一下,做了这一个简单的渲染,没有占满空闲时间,所以可能在最开始的时候,浏览器就找到机会去调用 rIC 的回调函数了。 + +我们简单的修改一下 step 函数,在里面加一个很重的任务,1000 次循环打印。 + +``` html + + + + + + Document + + + +
+ + + +``` + +再来看一下它的表现: + +![requetIdleCallback和requestAnmationFrame动画很忙](https://user-images.githubusercontent.com/8088864/125779688-be382539-51e0-4462-afee-ddb5db99b2bb.gif) + +其实和我们预期的一样,由于浏览器的每一帧都"太忙了",导致它真的就无视我们的 rIC 函数了。 + +如果给 rIC 函数加一个 timeout 呢: + + +``` html + + + + + + Document + + + +
+ + + +``` + +效果如下: + +![requetIdleCallback和requestAnmationFrame动画很忙再加上timeout](https://user-images.githubusercontent.com/8088864/125779998-cf9201d8-707b-4d99-aece-9e40c4f7b2a2.gif) + +浏览器会在大概 500ms 的时候,不管有多忙,都去强制执行 `rIC` 函数,这个机制可以防止我们的空闲任务被“饿死”。 + +### 总结 + +通过本文的学习过程,我自己也打破了很多对于 Event Loop 以及 rAF、rIC 函数的固有错误认知,通过本文我们可以整理出以下的几个关键点。 + +1. 事件循环不一定每轮都伴随着重新渲染,但是如果有微任务,一定会伴随着微任务执行。 +2. 决定浏览器视图是否渲染的因素很多,浏览器是非常聪明的。 +3. requestAnimationFrame在重新渲染屏幕之前执行,非常适合用来做动画。 +4. requestIdleCallback在渲染屏幕之后执行,并且是否有空执行要看浏览器的调度,如果你一定要它在某个时间内执行,请使用 timeout参数。 +5. resize和scroll事件其实自带节流,它只在 Event Loop 的渲染阶段去派发事件到 EventTarget 上。 + +## canvas + +Canvas API 提供了一个通过JavaScript 和 HTML的``元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。 + +Canvas API主要聚焦于2D图形。而同样使用``元素的 WebGL API 则用于绘制硬件加速的2D和3D图形。 + +### 标签 + +``` html + +``` + +不给宽高的话默认是300+150 + +### 怎么用 + +``` js +// 拿到canvas +var canvas = document.getElementById("canvas"); +// 创建画图工具 +var context = canvas.getContext("2d"); +``` + +### 相关的api及用法 + +``` html + + + + + + Your browser does not support the HTML5 canvas tag. + + + + + + +``` + +效果如下所示: + +![Canvas LineTo 效果图](https://user-images.githubusercontent.com/8088864/125880740-1d667e65-6511-4fd1-96a2-c697fa62aba3.png) + +#### 画矩形 + +``` js +// 直接传入 x, y, width, height, 就可以绘制一个矩形 +// 画在玻璃纸上 + +context.rect(100, 100, 200, 200); +context.strokeStyle = "red"; +context.stroke(); +context.fillStyle = "yellow"; +context.fill(); +``` + +``` js +// 直接创建一个填充的矩形 +// 创建玻璃纸, 画矩形路径, 填充, 把玻璃纸销毁 +context.fillRect(100, 100, 200, 200); + +// 黄色的边不会显示,是因为上面那一句,画完之后,就把玻璃纸销毁了 +context.strokeStyle = "yellow"; +context.stroke(); +// 如果放在fillRect上面就可以实现 +``` + +#### 圆形绘制 + +``` js +// x轴是0度开始 +// x, y: 圆心位置;radius: 半径的长度; startRadian, endRadian 代表的是起始弧度和结束弧度;dircetion代表的圆形的路径的方向,默认是顺时针(是否逆时针, 默认值是false),如果传true就是逆时针,最后一个参数是可以不传的, 默认就是顺时针 + +// context.arc(x, y, radius, startRadian, endRadian, direction); + +// 从31度的地方,画到81度的地方 +context.arc(300, 200, 100, 31/180*Math.PI, 81/180*Math.PI); + +context.strokeStyle = "yellow"; +context.stroke(); + +context.fillStyle = "red"; +context.fill(); +``` + +#### 画飞镖转盘 + +``` js +for (var i = 0; i < 10; i++) { + context.moveTo(320+i*20,200); + // i % 2代表是奇数还是偶数, 偶数就逆时针, 奇数就顺时针 + context.arc(300, 200, 20 + i * 20, 0, 2*Math.PI, i%2==0); +} +context.fillStyle = "green"; +context.fill(); +context.stroke(); +``` + +效果图如下所示: + +![Canvas arc 画飞镖转盘](https://user-images.githubusercontent.com/8088864/125925307-9fdd88ec-8569-412d-9245-37aceca560ba.png) + +#### 线性渐变 + +``` js +// 1. 需要创建出一个渐变对象 +// var gradient = context.createLinearGradient(100, 100, 300, 100); +// 参数代表哪个点到哪个点,这里写的是左上角到右下角的意思 +var gradient = context.createLinearGradient(100, 100, 300, 380); + +// 2. 添加渐变颜色 +gradient.addColorStop(0, "red"); +gradient.addColorStop(0.5, "hotpink"); +gradient.addColorStop(1, "yellowgreen"); + +// 3. 将渐变对象设为填充色 +context.fillStyle = gradient; + +// 4. 画一个矩形 +context.fillRect(100, 100, 200, 280); +``` + +效果图如下所示: + +![Canvas createLinearGradient 线性渐变](https://user-images.githubusercontent.com/8088864/125925678-c260361c-11cb-44cd-86f9-86156ea7033e.png) + +#### 径向渐变 + +``` js +// 1. 创建渐变对象 +// 内圆 +var c1 = {x: 260, y: 160, r: 0}; +// 外圆 +var c2 = {x: 300, y: 200, r: 120}; + +var gradient = context.createRadialGradient(c1.x, c1.y, c1.r, c2.x, c2.y, c2.r); +gradient.addColorStop(0, "red"); +gradient.addColorStop(0.3, "yellow"); +gradient.addColorStop(0.6, "green"); +gradient.addColorStop(1, "orange"); + +// 2. 把渐变对象设为填充色 +context.fillStyle = gradient; + +// 3. 画圆并填充 +// 内圆的部分是用0的位置填充的; 内圆的边到外圆的边所发生的渐变叫, 径向渐变 +context.arc(c2.x, c2.y, c2.r, 0, 2*Math.PI); +context.fill(); +``` + +效果图如下所示: + +![Canvas createRadialGradient 径向渐变](https://user-images.githubusercontent.com/8088864/125926105-f8d0128c-fc71-4278-9c10-580d9dbf4d3c.png) + +#### 径向渐变画球 + +``` js +//1. 创建一个径向渐变 +var c1 = {x: 240, y: 160, r: 0}; +var c2 = {x: 300, y: 200, r: 120}; + +var gradient = context.createRadialGradient(c1.x, c1.y, c1.r, c2.x, c2.y, c2.r); +gradient.addColorStop(1, "gray"); +gradient.addColorStop(0, "lightgray"); + +//2. 将渐变对象设为填充色 +context.fillStyle = gradient; + +//3. 画圆并填充 +context.arc(c2.x, c2.y, c2.r, 0, 2*Math.PI); +context.fill(); +``` + +效果图如下所示: + +![Canvas createRadialGradient 径向渐变画球](https://user-images.githubusercontent.com/8088864/125926450-19a90cf2-1049-4d2c-a47d-1f22fad0cd58.png) + +#### 径向渐变画彩虹 + +``` js +//实现彩虹,给里面的圆一个半径80是关键 +var c1 = {x: 300, y: 200, r: 80}; +var c2 = {x: 300, y: 200, r: 150}; +var gradient = context.createRadialGradient(c1.x, c1.y, c1.r, c2.x, c2.y, c2.r); +gradient.addColorStop(1, "red"); +gradient.addColorStop(6/7, "orange"); +gradient.addColorStop(5/7, "yellow"); +gradient.addColorStop(4/7, "green"); +gradient.addColorStop(3/7, "cyan"); +gradient.addColorStop(2/7, "skyblue"); +gradient.addColorStop(1/7, "purple"); +gradient.addColorStop(0, "white"); + +//设为填充色 +context.fillStyle = gradient; + +//画圆并填充 +context.arc(c2.x, c2.y, c2.r, 0, 2*Math.PI); +context.fill(); + +//遮挡下半部分 +context.fillStyle = "white"; +context.fillRect(0, 200, 600, 200); +``` + +效果图如下所示: + +![Canvas createRadialGradient 径向渐变画彩虹](https://user-images.githubusercontent.com/8088864/125926965-02866a67-5dd9-4be1-84f3-b0589696e7f7.png) + +#### 阴影效果 + +``` js +//和css3相比, 阴影只能设一个, 不能设内阴影 +//水平偏移, 垂直的偏移, 模糊程度, 阴影的颜色 + +//设置阴影的参数 +context.shadowOffsetX = 10; +context.shadowOffsetY = 10; +context.shadowBlur = 10; +context.shadowColor = "yellowgreen"; + +//画一个矩形 +context.fillStyle = "red"; +context.fillRect(100, 100, 300, 200); +``` + +效果图如下所示: + +![Canvas shadow 阴影效果](https://user-images.githubusercontent.com/8088864/125927364-e91bafb3-7e90-4173-bd39-4ee8ae7da746.png) + +#### 绘制文字api + +``` js +//绘制文字 +//text就是要绘制的文字, x, y就是从什么地方开始绘制 +//context.strokeText("text", x, y) + +context.font = "60px 微软雅黑"; +//context.strokeText("hello, world", 100, 100); +context.fillText("hello, world", 100, 100); +``` + +效果图如下所示: + +![Canvas fillText 绘制文字](https://user-images.githubusercontent.com/8088864/125927573-fdaa09b5-93d7-4990-86ee-2fa9a453eab5.png) + +#### 文字对齐方式 + +``` js +//默认在left +//关键api:context.textAlign = "left"; +context.textAlign = "left"; +context.fillText("left", 300, 120); + +context.textAlign = "center"; +context.fillText("center", 300, 190); + +context.textAlign = "right"; +context.fillText("right", 300, 260); + +// 文字出现在canvas的右上方 +// 1. 先设置right +// 2. 给canvas.width,0即可 +context.font = "60px 微软雅黑"; +context.textAlign = "right"; +context.textBaseline = "top"; +context.fillText("hello, world", canvas.width, 0); +``` + +效果图如下所示: + +![Canvas fillText 水平对齐方式](https://user-images.githubusercontent.com/8088864/125934392-abf31a1e-2b7e-429f-90cc-8cef9fd516d0.png) + + +#### 垂直方向 + +``` js +//默认是top +//关键api:context.textBaseline = "top"; + +context.fillText("default", 50, 200); + +context.textBaseline = "top"; +context.fillText("top", 150, 200); + +context.textBaseline = "middle"; +context.fillText("middle", 251, 200); + +context.textBaseline = "bottom"; +context.fillText("bottom", 400, 200); +``` + +效果图如下所示: + +![Canvas fillText 垂直对齐方式](https://user-images.githubusercontent.com/8088864/125935099-d0a918b3-25e8-4150-bd86-053a5e5cfa98.png) + +#### 图片的绘制 + +3参模式: 将img从x, y的地方开始绘制, 图片有多大,就绘制多大,超出canvas的部分就不显示了 + +``` js +//context.drawImage(img, x, y) + +var image = new Image(); +image.src = "./img/gls.jpg"; + +//必须要等到图片加载出来,才能进行绘制的操作 +image.onload = function () { + context.drawImage(image, 100, 200); +} +``` + +5参模式(缩放模式), 就是将图片显示在画布上的某一块区域(x, y, w, h),如果这个区域的宽高和图片不一至,会被压缩或放大 + +``` js +var image = new Image(); +image.src = "./img/gls.jpg"; + +image.onload = function () { + context.drawImage(image, 100, 100, 100, 100); +} +``` + +图片绘制的9参模式, 就是把原图(img)中的某一块(imagex,imagey,imagew,imageh)截取出来, 显示在画布的某个区域(canvasx, canvasy, canvasw, canvash) + +``` js +//理解关键: +//(imagex,imagey,imagew,imageh) +//(canvasx, canvasy, canvasw, canvash) + +var image = new Image(); +image.src = "./img/gls.jpg"; +image.onload = function () { + /* + 参数的解释: + image: 就是大图片本身 + 中间的四个参数, 代表从图片的150, 0的位置,截取 150 * 200的一块区域 + 后面的四个参数, 将刚才截取的小图, 显示画布上 100, 100, 150, 200的这个区域 + */ + context.drawImage(image, 150, 0, 150, 200, 100, 100, 150, 200); +} +``` + +## WebWorker和postMessage + +### 概述 + +JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。 + +Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。 + +Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。 + +Web Worker 有以下几个使用注意点。 + +#### (1)同源限制 + +分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。 + +#### (2)DOM 限制 + +Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。 + +#### (3)通信联系 + +Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。(postMessage) + +#### (4)脚本限制 + +Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。 + +#### (5)文件限制 + +Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。 + +### 基本用法 + +#### 主线程 + +主线程采用`new`命令,调用`Worker()`构造函数,新建一个 `Worker` 线程。 + +``` js +var worker = new Worker('work.js'); +``` + +`Worker()`构造函数的参数是一个脚本文件,该文件就是 `Worker` 线程所要执行的任务。由于 `Worker` 不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),`Worker` 就会默默地失败。 + +然后,主线程调用`worker.postMessage()`方法,向 `Worker` 发消息。 + +``` js +worker.postMessage('Hello World'); +worker.postMessage({method: 'echo', args: ['Work']}); +// worker.postMessage() 方法的参数,就是主线程传给 Worker 的数据。它可以是各种数据类型,包括二进制数据。 +``` + +接着,主线程通过`worker.onmessage`指定监听函数,接收子线程发回来的消息。 + +``` js +worker.onmessage = function (event) { + console.log('Received message ' + event.data); + doSomething(); +} + +function doSomething() { + // 执行任务 + worker.postMessage('Work done!'); +} +``` + +上面代码中,事件对象的data属性可以获取 `Worker` 发来的数据。 + +`Worker` 完成任务以后,主线程就可以把它关掉。 + +``` js +worker.terminate(); +``` + +#### Worker 线程 + +`Worker` 线程内部需要有一个监听函数,监听`message`事件。 + +``` js +self.addEventListener('message', function (e) { + self.postMessage('You said: ' + e.data); +}, false); +``` + +上面代码中,`self`代表子线程自身,即子线程的全局对象。因此,等同于下面两种写法。 + +```js +// 写法一 +this.addEventListener('message', function (e) { + this.postMessage('You said: ' + e.data); +}, false); + +// 写法二 +addEventListener('message', function (e) { + postMessage('You said: ' + e.data); +}, false); +``` + +除了使用`self.addEventListener()`指定监听函数,也可以使用`self.onmessage`指定。监听函数的参数是一个事件对象,它的data属性包含主线程发来的数据。`self.postMessage()`方法用来向主线程发送消息。 + +根据主线程发来的数据,`Worker` 线程可以调用不同的方法,下面是一个例子。 + +``` js +self.addEventListener('message', function (e) { + var data = e.data; + switch (data.cmd) { + case 'start': + self.postMessage('WORKER STARTED: ' + data.msg); + break; + case 'stop': + self.postMessage('WORKER STOPPED: ' + data.msg); + self.close(); // Terminates the worker. + break; + default: + self.postMessage('Unknown command: ' + data.msg); + }; +}, false); +``` + +上面代码中,`self.close()`用于在 Worker 内部关闭自身。 + +#### Worker 加载脚本 + +Worker 内部如果要加载其他脚本,有一个专门的方法`importScripts()`。 + +``` js +importScripts('script1.js'); +``` + +该方法可以同时加载多个脚本。 + +``` js +importScripts('script1.js', 'script2.js'); +``` + +#### Worker 错误处理 + +主线程可以监听 `Worker` 是否发生错误。如果发生错误,`Worker` 会触发主线程的error事件。 + +``` js +worker.onerror(function (event) { + console.log([ + 'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message + ].join('')); +}); + +// 或者 +worker.addEventListener('error', function (event) { + // ... +}); +``` + +`Worker` 内部也可以监听error事件。 + +#### 关闭 Worker + +使用完毕,为了节省系统资源,必须关闭 Worker。 + +``` js +// 主线程 +worker.terminate(); + +// Worker 线程 +self.close(); +``` + +### 数据通信 + +前面说过,主线程与 Worker 之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。 + +主线程与 Worker 之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer 等类型,也可以在线程之间发送。下面是一个例子。 + +``` js +// 主线程 +var uInt8Array = new Uint8Array(new ArrayBuffer(10)); +for (var i = 0; i < uInt8Array.length; ++i) { + uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...] +} +worker.postMessage(uInt8Array); + +// Worker 线程 +self.onmessage = function (e) { + var uInt8Array = e.data; + postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString()); + postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength); +}; +``` + +但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。 + +如果要直接转移数据的控制权,就要使用下面的写法。 + +``` js +// Transferable Objects 格式 +worker.postMessage(arrayBuffer, [arrayBuffer]); + +// 例子 +var ab = new ArrayBuffer(1); +worker.postMessage(ab, [ab]); +``` + +### 同页面的 Web Worker + +通常情况下,Worker 载入的是一个单独的 JavaScript 脚本文件,但是也可以载入与主线程在同一个网页的代码。 + +``` html + + + + + +``` + +上面是一段嵌入网页的脚本,注意必须指定` + + +``` + +OffscreenCanvas 是可转移的,除了将其指定为传递信息中的字段之一以外,还需要将其作为postMessage(传递信息给Worker的方法)中的第二个参数传递出去,以便可以在Worker线程的context(上下文)中使用它。 + +``` js +// worker.js + +self.onmessage = function (event) { + // 获取传送过来的离屏Canvas(OffscreenCanvas) + var canvas = event.data.canvas; + var context = canvas.getContext('2d'); + + // 画一个曲径球体 + var c1 = {x: 240, y: 160, r: 0}; + var c2 = {x: 300, y: 200, r: 120}; + + var gradient = context.createRadialGradient(c1.x, c1.y, c1.r, c2.x, c2.y, c2.r); + gradient.addColorStop(1, "gray"); + gradient.addColorStop(0, "lightgray"); + + //2. 将渐变对象设为填充色 + context.fillStyle = gradient; + + //3. 画圆并填充 + context.arc(c2.x, c2.y, c2.r, 0, 2*Math.PI); + context.fill(); +} +``` + +效果如下所示: + +![WebWorker中OffscreenCanvas绘制径向渐变画球](https://user-images.githubusercontent.com/8088864/126027866-d78a65fc-8f0f-4a7e-9adf-7eb09a03b956.png) + +任务繁忙的主线程也不会影响在Worker上运行的动画。所以即使主线程非常繁忙,你也可以通过此功能来避免掉帧并保证流畅的动画 + +### WebRTC的YUV媒体流数据的离屏渲染 + +从 WebRTC 中拿到的是 YUV 的原始视频流,将原始的 YUV 视频帧直接转发过来,通过第三方库直接在 Cavans 上渲染。 + +可以使用[yuv-canvas](https://github.com/brion/yuv-canvas)和[yuv-buffer](https://github.com/brion/yuv-buffer)第三方库来渲染YUV的原始视频流。 + +主进程render.js + +``` js +"use strict"; +exports.__esModule = true; +var isEqual = require('lodash.isequal'); +var YUVBuffer = require('yuv-buffer'); +var YUVCanvas = require('yuv-canvas'); +var Renderer = /** @class */ (function () { + function Renderer(workSource) { + var _this = this; + this._sendCanvas = function () { + _this.canvasSent = true; + _this.worker && _this.worker.postMessage({ + type: 'constructor', + data: { + canvas: _this.offCanvas, + id: (_this.element && _this.element.id) || (Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2)) + } + }, [_this.offCanvas]); + }; + /** + * 判断使用渲染的方式 + */ + this._checkRendererWay = function () { + if (_this.workerReady && _this.worker && _this.offCanvas && _this.enableWorker) { + return 'worker'; + } + else { + return 'software'; + } + }; + // workerCanvas渲染 + this._workDrawFrame = function (width, height, yUint8Array, uUint8Array, vUint8Array) { + if (_this.canvasWrapper && _this.canvasWrapper.style.display !== 'none') { + _this.canvasWrapper.style.display = 'none'; + } + if (_this.workerCanvasWrapper && _this.workerCanvasWrapper.style.display === 'none') { + _this.workerCanvasWrapper.style.display = 'flex'; + } + _this.worker && _this.worker.postMessage({ + type: 'drawFrame', + data: { + width: width, + height: height, + yUint8Array: yUint8Array, + uUint8Array: uUint8Array, + vUint8Array: vUint8Array + } + }, [yUint8Array, uUint8Array, vUint8Array]); + }; + // 实际渲染Canvas + this._softwareDrawFrame = function (width, height, yUint8Array, uUint8Array, vUint8Array) { + if (_this.workerCanvasWrapper && _this.workerCanvasWrapper.style.display !== 'none') { + _this.workerCanvasWrapper.style.display = 'none'; + } + if (_this.canvasWrapper && _this.canvasWrapper.style.display === 'none') { + _this.canvasWrapper.style.display = 'flex'; + } + var format = YUVBuffer.format({ + width: width, + height: height, + chromaWidth: width / 2, + chromaHeight: height / 2 + }); + var y = YUVBuffer.lumaPlane(format, yUint8Array); + var u = YUVBuffer.chromaPlane(format, uUint8Array); + var v = YUVBuffer.chromaPlane(format, vUint8Array); + var frame = YUVBuffer.frame(format, y, u, v); + _this.yuv.drawFrame(frame); + }; + this.cacheCanvasOpts = {}; + this.yuv = {}; + this.ready = false; + this.contentMode = 0; + this.container = {}; + this.canvasWrapper; + this.canvas = {}; + this.element = {}; + this.offCanvas = {}; + this.enableWorker = !!workSource; + if (this.enableWorker) { + this.worker = new Worker(workSource); + this.workerReady = false; + this.canvasSent = false; + this.worker.onerror = function (evt) { + console.error('[WorkerRenderer]: the renderer worker catch error: ', evt); + _this.workerReady = false; + _this.enableWorker = false; + }; + this.worker.onmessage = function (evt) { + var data = evt.data; + switch (data.type) { + case 'ready': { + console.log('[WorkerRenderer]: the renderer worker was ready'); + _this.workerReady = true; + if (_this.offCanvas) { + _this._sendCanvas(); + } + break; + } + case 'exited': { + console.log('[WorkerRenderer]: the renderer worker was exited'); + _this.workerReady = false; + _this.enableWorker = false; + break; + } + } + }; + } + } + Renderer.prototype._calcZoom = function (vertical, contentMode, width, height, clientWidth, clientHeight) { + if (vertical === void 0) { vertical = false; } + if (contentMode === void 0) { contentMode = 0; } + var localRatio = clientWidth / clientHeight; + var tempRatio = width / height; + if (isNaN(localRatio) || isNaN(tempRatio)) { + return 1; + } + if (!contentMode) { + if (vertical) { + return localRatio > tempRatio ? + clientHeight / height : clientWidth / width; + } + else { + return localRatio < tempRatio ? + clientHeight / height : clientWidth / width; + } + } + else { + if (vertical) { + return localRatio < tempRatio ? + clientHeight / height : clientWidth / width; + } + else { + return localRatio > tempRatio ? + clientHeight / height : clientWidth / width; + } + } + }; + Renderer.prototype.getBindingElement = function () { + return this.element; + }; + Renderer.prototype.bind = function (element) { + // record element + this.element = element; + // create container + var container = document.createElement('div'); + container.className += ' video-canvas-container'; + Object.assign(container.style, { + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + position: 'relative' + }); + this.container = container; + element && element.appendChild(this.container); + // 创建两个canvas,一个在主线程中渲染,如果web worker中的离屏canvas渲染进程出错了,还可以切换到主进程的canvas进行渲染 + var canvasWrapper = document.createElement('div'); + canvasWrapper.className += ' video-canvas-wrapper canvas-renderer'; + Object.assign(canvasWrapper.style, { + width: '100%', + height: '100%', + justifyContent: 'center', + alignItems: 'center', + position: 'absolute', + left: '0px', + right: '0px', + display: 'none' + }); + this.canvasWrapper = canvasWrapper; + this.container.appendChild(this.canvasWrapper); + var workerCanvasWrapper = document.createElement('div'); + workerCanvasWrapper.className += ' video-canvas-wrapper webworker-renderer'; + Object.assign(workerCanvasWrapper.style, { + width: '100%', + height: '100%', + justifyContent: 'center', + alignItems: 'center', + position: 'absolute', + left: '0px', + right: '0px', + display: 'none' + }); + this.workerCanvasWrapper = workerCanvasWrapper; + this.container.appendChild(this.workerCanvasWrapper); + // create canvas + this.canvas = document.createElement('canvas'); + this.workerCanvas = document.createElement('canvas'); + this.canvasWrapper.appendChild(this.canvas); + this.workerCanvasWrapper.appendChild(this.workerCanvas); + // 创建 OffscreenCanvas 对象 + this.offCanvas = this.workerCanvas.transferControlToOffscreen(); + if (!this.canvasSent && this.offCanvas && this.worker && this.workerReady) { + this._sendCanvas(); + } + this.yuv = YUVCanvas.attach(this.canvas, { webGL: false }); + }; + Renderer.prototype.unbind = function () { + this.canvasWrapper && this.canvasWrapper.removeChild(this.canvas); + this.workerCanvasWrapper && this.workerCanvasWrapper.removeChild(this.workerCanvas); + this.container && this.container.removeChild(this.canvasWrapper); + this.container && this.container.removeChild(this.workerCanvasWrapper); + this.element && this.element.removeChild(this.container); + this.worker && this.worker.terminate(); + this.workerReady = false; + this.canvasSent = false; + this.yuv = null; + this.container = null; + this.workerCanvasWrapper = null; + this.canvasWrapper = null; + this.element = null; + this.canvas = null; + this.workerCanvas = null; + this.offCanvas = null; + this.worker = null; + }; + Renderer.prototype.refreshCanvas = function () { + // Not implemented for software renderer + }; + Renderer.prototype.updateCanvas = function (options) { + if (options === void 0) { options = { + width: 0, + height: 0, + rotation: 0, + mirrorView: false, + contentMode: 0, + clientWidth: 0, + clientHeight: 0 + }; } + // check if display options changed + if (isEqual(this.cacheCanvasOpts, options)) { + return; + } + this.cacheCanvasOpts = Object.assign({}, options); + // check for rotation + if (options.rotation === 0 || options.rotation === 180) { + this.canvas.width = options.width; + this.canvas.height = options.height; + // canvas 调用 transferControlToOffscreen 方法后无法修改canvas的宽度和高度,只允许修改canvas的style属性 + this.workerCanvas.style.width = options.width + "px"; + this.workerCanvas.style.height = options.height + "px"; + } + else if (options.rotation === 90 || options.rotation === 270) { + this.canvas.height = options.width; + this.canvas.width = options.height; + this.workerCanvas.style.height = options.width + "px"; + this.workerCanvas.style.width = options.height + "px"; + } + else { + throw new Error('Invalid value for rotation. Only support 0, 90, 180, 270'); + } + var transformItems = []; + transformItems.push("rotateZ(" + options.rotation + "deg)"); + var scale = this._calcZoom(options.rotation === 90 || options.rotation === 270, options.contentMode, options.width, options.height, options.clientWidth, options.clientHeight); + // transformItems.push(`scale(${scale})`) + this.canvas.style.zoom = scale; + this.workerCanvas.style.zoom = scale; + // check for mirror + if (options.mirrorView) { + // this.canvas.style.transform = 'rotateY(180deg)'; + transformItems.push('rotateY(180deg)'); + } + if (transformItems.length > 0) { + var transform = "" + transformItems.join(' '); + this.canvas.style.transform = transform; + this.workerCanvas.style.transform = transform; + } + }; + Renderer.prototype.drawFrame = function (imageData) { + if (!this.ready) { + this.ready = true; + } + var dv = new DataView(imageData.header); + // let format = dv.getUint8(0); + var mirror = dv.getUint8(1); + var contentWidth = dv.getUint16(2); + var contentHeight = dv.getUint16(4); + var left = dv.getUint16(6); + var top = dv.getUint16(8); + var right = dv.getUint16(10); + var bottom = dv.getUint16(12); + var rotation = dv.getUint16(14); + // let ts = dv.getUint32(16); + var width = contentWidth + left + right; + var height = contentHeight + top + bottom; + this.updateCanvas({ + width: width, height: height, rotation: rotation, + mirrorView: !!mirror, + contentMode: this.contentMode, + clientWidth: this.container && this.container.clientWidth, + clientHeight: this.container && this.container.clientHeight + }); + if (this._checkRendererWay() === 'software') { + // 实际渲染canvas + this._softwareDrawFrame(width, height, imageData.yUint8Array, imageData.uUint8Array, imageData.vUint8Array); + } + else { + this._workDrawFrame(width, height, imageData.yUint8Array, imageData.uUint8Array, imageData.vUint8Array); + } + }; + /** + * 清空整个Canvas面板 + * + * @memberof Renderer + */ + Renderer.prototype.clearFrame = function () { + if (this._checkRendererWay() === 'software') { + this.yuv && this.yuv.clear(); + } + else { + this.worker && this.worker.postMessage({ + type: 'clearFrame' + }); + } + }; + Renderer.prototype.setContentMode = function (mode) { + if (mode === void 0) { mode = 0; } + this.contentMode = mode; + }; + return Renderer; +}()); + +exports["default"] = Renderer; +``` + +渲染Worker的代码如下所示: + +``` js +// render worker + +(function() { + const dateFormat = function(date, formatter = 'YYYY-MM-DD hh:mm:ss SSS') { + if (!date) { + return date; + } + + let time; + + try { + time = new Date(date); + } catch (e) { + return date; + } + + const oDate = { + Y: time.getFullYear(), + M: time.getMonth() + 1, + D: time.getDate(), + h: time.getHours(), + m: time.getMinutes(), + s: time.getSeconds(), + S: time.getMilliseconds() + }; + + return formatter.replace(/(Y|M|D|h|m|s|S)+/g, (res, key) => { + let len = 2; + + switch (res.length) { + case 1: + len = res.slice(1, 0) === 'Y' ? 4 : 2; + break; + case 2: + len = 2; + break; + case 3: + len = 3; + break; + case 4: + len = 4; + break; + default: + len = 2; + } + return (`0${oDate[key]}`).slice(-len); + }); + } + + let yuv; + + try { + importScripts('./yuv-buffer/yuv-buffer.js'); + importScripts('./yuv-canvas/shaders.js'); + importScripts('./yuv-canvas/depower.js'); + importScripts('./yuv-canvas/YCbCr.js'); + importScripts('./yuv-canvas/FrameSink.js'); + importScripts('./yuv-canvas/SoftwareFrameSink.js'); + importScripts('./yuv-canvas/WebGLFrameSink.js'); + importScripts('./yuv-canvas/yuv-canvas.js'); + + self.addEventListener('message', function (e) { + const data = e.data; + switch (data.type) { + case 'constructor': + console.log(`${dateFormat(new Date())} RENDER_WORKER [INFO]: received canvas: `, data.data.canvas, data.data.id); + yuv = YUVCanvas.attach(data.data.canvas, { webGL: false }); + break; + case 'drawFrame': + // 考虑是否使用requestAnimationFrame进行渲染,控制每一帧显示的频率 + const width = data.data.width; + const height = data.data.height; + const yUint8Array = data.data.yUint8Array; + const uUint8Array = data.data.uUint8Array; + const vUint8Array = data.data.vUint8Array; + const format = YUVBuffer.format({ + width: width, + height: height, + chromaWidth: width / 2, + chromaHeight: height / 2 + }); + const y = YUVBuffer.lumaPlane(format, yUint8Array); + const u = YUVBuffer.chromaPlane(format, uUint8Array); + const v = YUVBuffer.chromaPlane(format, vUint8Array); + const frame = YUVBuffer.frame(format, y, u, v); + yuv && yuv.drawFrame(frame); + break; + case 'clearFrame': { + yuv && yuv.clear(frame); + break; + } + default: + console.log(`${dateFormat(new Date())} RENDER_WORKER [INFO]: [RendererWorker]: Unknown message: `, data); + }; + }, false); + + self.postMessage({ + type: 'ready', + }); + } catch (error) { + self.postMessage({ + type: 'exited', + }); + + console.log(`${dateFormat(new Date())} RENDER_WORKER [INFO]: [RendererWorker]: catch error`, error); + } +})(); + +``` + +### 总结 + +如果你对图像绘画使用得非常多,OffscreenCanvas可以有效的提高你APP的性能。它使得Worker可以处理canvas的渲染绘制,让你的APP更好地利用了多核系统。 + +OffscreenCanvas在Chrome 69中已经不需要开启flag(实验性功能)就可以使用了。它也正在被 Firefox 实现。由于其API与普通canvas元素非常相似,所以你可以轻松地对其进行特征检测并循序渐进地使用它,而不会破坏现有的APP或库的运行逻辑。OffscreenCanvas在任何涉及到图形计算以及动画表现且与DOM关系并不密切(即依赖DOM API不多)的情况下,它都具有性能优势。 + +## 常见排序算法的时间复杂度,空间复杂度 + +![排序算法比较](https://user-images.githubusercontent.com/8088864/126057079-6d6fdcfb-cfd1-416c-9f8d-b4ca98e5f50b.png) + +## 前端需要注意哪些 SEO + +1. 合理的 title、description、keywords:搜索对着三项的权重逐个减小,title 值强调重点即可,重要关键词出现不要超过 2 次,而且要靠前,不同页面 title 要有所不同;description 把页面内容高度概括,长度合适,不可过分堆砌关键词,不同页面 description 有所不同;keywords 列举出重要关键词即可 +2. 语义化的 HTML 代码,符合 W3C 规范:语义化代码让搜索引擎容易理解网页 +3. 重要内容 HTML 代码放在最前:搜索引擎抓取 HTML 顺序是从上到下,有的搜索引擎对抓取长度有限制,保证重要内容一定会被抓取 +4. 重要内容不要用 js 输出:爬虫不会执行 js 获取内容 +5. 少用 iframe:搜索引擎不会抓取 iframe 中的内容 +6. 非装饰性图片必须加 alt +7. 提高网站速度:网站速度是搜索引擎排序的一个重要指标 + +## web 开发中会话跟踪的方法有哪些 + +1. cookie +2. session +3. url 重写 +4. 隐藏 input +5. ip 地址 + +## ``的`title`和`alt`有什么区别 + +1. `title`是[global attributes](http://www.w3.org/TR/html-markup/global-attributes.html#common.attrs.core)之一,用于为元素提供附加的 advisory information。通常当鼠标滑动到元素上的时候显示。 +2. `alt`是``的特有属性,是图片内容的等价描述,用于图片无法加载时显示、读屏器阅读图片。可提图片高可访问性,除了纯装饰图片外都必须设置有意义的值,搜索引擎会重点分析。 + +## doctype 是什么,举例常见 doctype 及特点 + +1. ``声明必须处于 HTML 文档的头部,在``标签之前,HTML5 中不区分大小写 +2. ``声明不是一个 HTML 标签,是一个用于告诉浏览器当前 HTML 版本的指令 +3. 现代浏览器的 html 布局引擎通过检查 doctype 决定使用兼容模式还是标准模式对文档进行渲染,一些浏览器有一个接近标准模型。 +4. 在 HTML4.01 中``声明指向一个 DTD,由于 HTML4.01 基于 SGML,所以 DTD 指定了标记规则以保证浏览器正确渲染内容 +5. HTML5 不基于 SGML,所以不用指定 DTD + +常见 dotype: + +1. **HTML4.01 strict**:不允许使用表现性、废弃元素(如 font)以及 frameset。声明:`` +2. **HTML4.01 Transitional**:允许使用表现性、废弃元素(如 font),不允许使用 frameset。声明:`` +3. **HTML4.01 Frameset**:允许表现性元素,废气元素以及 frameset。声明:`` +4. **XHTML1.0 Strict**:不使用允许表现性、废弃元素以及 frameset。文档必须是结构良好的 XML 文档。声明:`` +5. **XHTML1.0 Transitional**:允许使用表现性、废弃元素,不允许 frameset,文档必须是结构良好的 XMl 文档。声明: `` +6. **XHTML 1.0 Frameset**:允许使用表现性、废弃元素以及 frameset,文档必须是结构良好的 XML 文档。声明:`` +7. **HTML 5**: `` + +## HTML 全局属性(global attribute)有哪些 + +参考资料:[MDN: html global attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes)或者[W3C HTML global-attributes](http://www.w3.org/TR/html-markup/global-attributes.html#common.attrs.core) + +- `accesskey`:设置快捷键,提供快速访问元素如
aaa在 windows 下的 firefox 中按`alt + shift + a`可激活元素 +- `class`:为元素设置类标识,多个类名用空格分开,CSS 和 javascript 可通过 class 属性获取元素 +- `contenteditable`: 指定元素内容是否可编辑 +- `contextmenu`: 自定义鼠标右键弹出菜单内容 +- `data-*`: 为元素增加自定义属性 +- `dir`: 设置元素文本方向 +- `draggable`: 设置元素是否可拖拽 +- `dropzone`: 设置元素拖放类型: copy, move, link +- `hidden`: 表示一个元素是否与文档。样式上会导致元素不显示,但是不能用这个属性实现样式效果 +- `id`: 元素 id,文档内唯一 +- `lang`: 元素内容的的语言 +- `spellcheck`: 是否启动拼写和语法检查 +- `style`: 行内 css 样式 +- `tabindex`: 设置元素可以获得焦点,通过 tab 可以导航 +- `title`: 元素相关的建议信息 +- `translate`: 元素和子孙节点内容是否需要本地化 + +## 什么是 web 语义化,有什么好处 + +web 语义化是指通过 HTML 标记表示页面包含的信息,包含了 HTML 标签的语义化和 css 命名的语义化。 +HTML 标签的语义化是指:通过使用包含语义的标签(如 h1-h6)恰当地表示文档结构 +css 命名的语义化是指:为 html 标签添加有意义的 class,id 补充未表达的语义,如[Microformat](http://en.wikipedia.org/wiki/Microformats)通过添加符合规则的 class 描述信息 +为什么需要语义化: + +- 去掉样式后页面呈现清晰的结构 +- 盲人使用读屏器更好地阅读 +- 搜索引擎更好地理解页面,有利于收录 +- 便于团队项目的可持续运作及维护 + +## HTTP method + +1. 一台服务器要与 HTTP1.1 兼容,只要为资源实现**GET**和**HEAD**方法即可 +2. **GET**是最常用的方法,通常用于**请求服务器发送某个资源**。 +3. **HEAD**与 GET 类似,但**服务器在响应中只返回首部,不返回实体的主体部分** +4. **PUT**让服务器**用请求的主体部分来创建一个由所请求的 URL 命名的新文档,或者,如果那个 URL 已经存在的话,就用干这个主体替代它** +5. **POST**起初是用来向服务器输入数据的。实际上,通常会用它来支持 HTML 的表单。表单中填好的数据通常会被送给服务器,然后由服务器将其发送到要去的地方。 +6. **TRACE**会在目的服务器端发起一个环回诊断,最后一站的服务器会弹回一个 TRACE 响应并在响应主体中携带它收到的原始请求报文。TRACE 方法主要用于诊断,用于验证请求是否如愿穿过了请求/响应链。 +7. **OPTIONS**方法请求 web 服务器告知其支持的各种功能。可以查询服务器支持哪些方法或者对某些特殊资源支持哪些方法。 +8. **DELETE**请求服务器删除请求 URL 指定的资源 + +## 从浏览器地址栏输入 url 到显示页面的步骤(以 HTTP 为例) + +1. 在浏览器地址栏输入 URL +2. 浏览器查看**缓存**,如果请求资源在缓存中并且判断缓存是否过期,跳转到转码步骤 + 1. 如果资源未缓存,发起新请求 + 2. 如果已缓存,检验判断缓存是否过期,缓存未过期直接提供给客户端,否则与服务器进行验证。 + 3. 检验缓存是否过期通常有两个 HTTP 头进行控制`Expires`和`Cache-Control`: + - HTTP1.0 提供 Expires,值为一个绝对时间表示缓存过期日期 + - HTTP1.1 增加了 Cache-Control: max-age=,值为以秒为单位的最大过期时间 +3. 浏览器**解析 URL**获取协议,主机,端口,path +4. 浏览器**组装一个 HTTP(GET)请求报文** +5. 浏览器**获取主机 ip 地址**,过程如下: + 1. 浏览器缓存 + 2. 本机缓存 + 3. hosts 文件 + 4. 路由器缓存 + 5. ISP DNS 缓存 + 6. DNS 递归查询(可能存在负载均衡导致每次 IP 不一样) +6. **打开一个 socket 与目标 IP 地址,端口建立 TCP 链接**,三次握手如下: + 1. 客户端发送一个 TCP 的**SYN=1,Seq=X**的包到服务器端口 + 2. 服务器发回**SYN=1, ACK=X+1, Seq=Y**的响应包 + 3. 客户端发送**ACK=Y+1, Seq=Z** +7. TCP 链接建立后**发送 HTTP 请求** +8. 服务器接受请求并解析,将请求转发到服务程序,如虚拟主机使用 HTTP Host 头部判断请求的服务程序 +9. 服务器检查**HTTP 请求头是否包含缓存验证信息**如果验证缓存未过期,返回**304**等对应状态码 +10. 处理程序读取完整请求并准备 HTTP 响应,可能需要查询数据库等操作 +11. 服务器将**响应报文通过 TCP 连接发送回浏览器** +12. 浏览器接收 HTTP 响应,然后根据情况选择**关闭 TCP 连接或者保留重用,关闭 TCP 连接的四次握手如下**: + 1. 主动方发送**Fin=1, Ack=Z, Seq= X**报文 + 2. 被动方发送**ACK=X+1, Seq=Z**报文 + 3. 被动方发送**Fin=1, ACK=X, Seq=Y**报文 + 4. 主动方发送**ACK=Y+1, Seq=X**报文 +13. 浏览器检查响应状态吗:是否为 1XX,3XX, 4XX, 5XX,这些情况处理与 2XX 不同 +14. 如果资源可缓存,**进行缓存协商** +15. 对响应进行**解码**(例如 gzip 压缩) +16. 根据资源类型决定如何处理(假设资源为 HTML 文档) +17. **解析 HTML 文档,构件 DOM 树,下载资源,构造 CSSOM 树,执行 js 脚本**,这些操作没有严格的先后顺序,以下分别解释 +18. **构建 DOM 树**: + 1. **Tokenizing**:根据 HTML 规范将字符流解析为标记 + 2. **Lexing**:词法分析将标记转换为对象并定义属性和规则 + 3. **DOM construction**:根据 HTML 标记关系将对象组成 DOM 树 +19. 解析过程中遇到图片、样式表、js 文件,**启动下载** +20. 构建**CSSOM 树**: + 1. **Tokenizing**:字符流转换为标记流 + 2. **Node**:根据标记创建节点 + 3. **CSSOM**:节点创建 CSSOM 树 +21. **[根据 DOM 树和 CSSOM 树构建渲染树](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction)**: + 1. 从 DOM 树的根节点遍历所有**可见节点**,不可见节点包括:1)`script`,`meta`这样本身不可见的标签。2)被 css 隐藏的节点,如`display: none` + 2. 对每一个可见节点,找到恰当的 CSSOM 规则并应用 + 3. 发布可视节点的内容和计算样式 +22. **js 解析如下**: + 1. 浏览器创建 Document 对象并解析 HTML,将解析到的元素和文本节点添加到文档中,此时**document.readystate 为 loading** + 2. HTML 解析器遇到**没有 async 和 defer 的 script 时**,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用 document.write()把文本插入到输入流中。**同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作 script 和他们之前的文档内容** + 3. 当解析器遇到设置了**async**属性的 script 时,开始下载脚本并继续解析文档。脚本会在它**下载完成后尽快执行**,但是**解析器不会停下来等它下载**。异步脚本**禁止使用 document.write()**,它们可以访问自己 script 和之前的文档元素 + 4. 当文档完成解析,document.readState 变成 interactive + 5. 所有**defer**脚本会**按照在文档出现的顺序执行**,延迟脚本**能访问完整文档树**,禁止使用 document.write() + 6. 浏览器**在 Document 对象上触发 DOMContentLoaded 事件** + 7. 此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些**内容完成载入并且所有异步脚本完成载入和执行**,document.readState 变为 complete,window 触发 load 事件 +23. **显示页面**(HTML 解析过程中会逐步显示页面) + +![HTTP访问过程](https://user-images.githubusercontent.com/8088864/126057166-67172419-c265-4be2-bc9f-5c8e4a3214ee.png) + +## HTTP request 报文结构是怎样的 + +[rfc2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html)中进行了定义: + +1. 首行是**Request-Line**包括:**请求方法**,**请求 URI**,**协议版本**,**CRLF** +2. 首行之后是若干行**请求头**,包括**general-header**,**request-header**或者**entity-header**,每个一行以 CRLF 结束 +3. 请求头和消息实体之间有一个**CRLF 分隔** +4. 根据实际请求需要可能包含一个**消息实体** + 一个请求报文例子如下: + +``` +GET /Protocols/rfc2616/rfc2616-sec5.html HTTP/1.1 +Host: www.w3.org +Connection: keep-alive +Cache-Control: max-age=0 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 +User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36 +Referer: https://www.google.com.hk/ +Accept-Encoding: gzip,deflate,sdch +Accept-Language: zh-CN,zh;q=0.8,en;q=0.6 +Cookie: authorstyle=yes +If-None-Match: "2cc8-3e3073913b100" +If-Modified-Since: Wed, 01 Sep 2004 13:24:52 GMT + +name=qiu&age=25 +``` + +## HTTP response 报文结构是怎样的 + +[rfc2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html)中进行了定义: + +1. 首行是状态行包括:**HTTP 版本,状态码,状态描述**,后面跟一个 CRLF +2. 首行之后是**若干行响应头**,包括:**通用头部,响应头部,实体头部** +3. 响应头部和响应实体之间用**一个 CRLF 空行**分隔 +4. 最后是一个可能的**消息实体** + 响应报文例子如下: + +``` +HTTP/1.1 200 OK +Date: Tue, 08 Jul 2014 05:28:43 GMT +Server: Apache/2 +Last-Modified: Wed, 01 Sep 2004 13:24:52 GMT +ETag: "40d7-3e3073913b100" +Accept-Ranges: bytes +Content-Length: 16599 +Cache-Control: max-age=21600 +Expires: Tue, 08 Jul 2014 11:28:43 GMT +P3P: policyref="http://www.w3.org/2001/05/P3P/p3p.xml" +Content-Type: text/html; charset=iso-8859-1 + +{"name": "qiu", "age": 25} +``` + +## HTTP 状态码及其含义 + +参考[RFC 2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) + +- 1XX:信息状态码 + - **100 Continue**:客户端应当继续发送请求。这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应 + - **101 Switching Protocols**:服务器理解了客户端切换协议的请求,并将通过 Upgrade 消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应后,服务器将会切换到 Upgrade 消息头中定义的那些协议。 +- 2XX:成功状态码 + - **200 OK**:请求成功,请求所希望的响应头或数据体将随此响应返回 + - **201 Created**:请求成功并且服务器创建了新的资源 + - **202 Accepted**:服务器已接受请求,但尚未处理 + - **203 Non-Authoritative Information**:表示文档被正常的返回,但是由于正在使用的是文档副本所以某些响应头信息可能不正确。(HTTP 1.1新) + - **204 No Content**:没有新文档,浏览器应该继续显示原来的文档。 + - **205 Reset Content**:没有新的内容,但浏览器应该重置它所显示的内容。用来强制浏览器清除表单输入内容(HTTP 1.1新) + - **206 Partial Content**:请求成功,返回范围请求的部分资源 +- 3XX:重定向 + - **300 Multiple Choices**:客户请求的文档可以在多个位置找到,这些位置已经在返回的文档内列出。如果服务器要提出优先选择,则应该在Location响应头中指明 + - **301 Moved Permanently**:请求的资源已永久移动到新位置 + - **302 Found**:临时性重定向 + - **303 See Other**:类似于301/302,不同之处在于,如果原来的请求是POST,Location头指定的重定向目标文档应该通过GET提取(HTTP 1.1新) + - **304 Not Modified**:自从上次请求后,请求的资源未修改过 + - **305 Use Proxy**:客户请求的文档应该通过Location头所指明的代理服务器提取(HTTP 1.1新) + - **306 (unused)**:未使用 + - **307 Temporary Redirect**:和302 (Found)相同。许多浏览器会错误地响应302应答进行重定向,即使原来的请求是POST,即使它实际上只能在POST请求的应答是303时才能重定向。由于这个原因,HTTP 1.1新增了307,以便更加清除地区分几个状态代码:当出现303应答时,浏览器可以跟随重定向的GET和POST请求;如果是307应答,则浏览器只能跟随对GET请求的重定向。(HTTP 1.1新) +- 4XX:客户端错误 + - **400 Bad Request**:服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求 + - **401 Unauthorized**:求未授权 + - **402 Payment Required**: + - **403 Forbidden**:禁止访问 + - **404 Not Found**:找不到与 URI 相匹配的资源 + - **405 Method Not Allowed**:请求方法(GET、POST、HEAD、DELETE、PUT、TRACE等)对指定的资源不适用。(HTTP 1.1新) + - **406 Not Acceptable**:指定的资源已经找到,但它的MIME类型和客户在Accpet头中所指定的不兼容(HTTP 1.1新) + - **407 Proxy Authentication Required**:类似于401,表示客户必须先经过代理服务器的授权。(HTTP 1.1新) + - **408 Request Timeout**:在服务器许可的等待时间内,客户一直没有发出任何请求。客户可以在以后重复同一请求。(HTTP 1.1新) + - **409 Conflict**:通常和PUT请求有关。由于请求和资源的当前状态相冲突,因此请求不能成功。(HTTP 1.1新) + - **410 Gone**:所请求的文档已经不再可用,而且服务器不知道应该重定向到哪一个地址。它和404的不同在于,返回407表示文档永久地离开了指定的位置,而 404表示由于未知的原因文档不可用。(HTTP 1.1新) + - **411 Length Required**:服务器不能处理请求,除非客户发送一个Content-Length头。(HTTP 1.1新) + - **412 Precondition Failed**:请求头中指定的一些前提条件失败(HTTP 1.1新) + - **413 Request Entity Too Large**:目标文档的大小超过服务器当前愿意处理的大小。如果服务器认为自己能够稍后再处理该请求,则应该提供一个Retry-After头(HTTP 1.1新) + - **414 Request-URI Too Long**:URI太长(HTTP 1.1新) + - **415 Unsupported Media Type**:请求所带的附件的格式类型服务器不知道如何处理。(HTTP 1.1新) + + - **416 Requested Range Not Satisfiable**:服务器不能满足客户在请求中指定的Range头。(HTTP 1.1新) + - **417 Expectation Failed**:如果服务器得到一个带有100-continue值的Expect请求头信息,这是指客户端正在询问是否可以在后面的请求中发送附件。在这种情况下,服务器也会用该状态(417)告诉浏览器服务器不接收该附件或用100 (SC_CONTINUE)状态告诉客户端可以继续发送附件。(HTTP 1.1新) +- 5XX: 服务器错误 + - **500 Internal Server Error**:服务器端错误 + - **501 Not Implemented**:服务器不支持实现请求所需要的功能。例如,客户发出了一个服务器不支持的PUT请求 + - **502 Bad Gateway**:服务器作为网关或者代理时,为了完成请求访问下一个服务器,但该服务器返回了非法的应答。 + - **503 Service Unavailable**:服务器由于维护或者负载过重未能应答。例如,Servlet可能在数据库连接池已满的情况下返回503。服务器返回503时可以提供一个 Retry-After头。 + - **504 Gateway Timeout**:由作为代理或网关的服务器使用,表示不能及时地从远程服务器获得应答。(HTTP 1.1新) + - **505 HTTP Version Not Supported**:服务器不支持请求中所指明的HTTP版本。(HTTP 1.1新 + +## 什么是渐进增强 + +渐进增强是指在 web 设计时强调可访问性、语义化 HTML 标签、外部样式表和脚本。保证所有人都能访问页面的基本内容和功能同时为高级浏览器和高带宽用户提供更好的用户体验。核心原则如下: + +- 所有浏览器都必须能访问基本内容 +- 所有浏览器都必须能使用基本功能 +- 所有内容都包含在语义化标签中 +- 通过外部 CSS 提供增强的布局 +- 通过非侵入式、外部 javascript 提供增强功能 +- end-user web browser preferences are respected + +## CSS 选择器有哪些 + +1. **`* 通用选择器`**:选择所有元素,**不参与计算优先级**,兼容性 IE6+ +2. **`#X id 选择器`**:选择 id 值为 X 的元素,兼容性:IE6+ +3. **`.X 类选择器`**: 选择 class 包含 X 的元素,兼容性:IE6+ +4. **`X Y 后代选择器`**: 选择满足 X 选择器的后代节点中满足 Y 选择器的元素,兼容性:IE6+ +5. **`X 元素选择器`**: 选择标所有签为 X 的元素,兼容性:IE6+ +6. **`:link,:visited,:focus,:hover,:active 链接状态`**: 选择特定状态的链接元素,顺序 LoVe HAte,兼容性: IE4+ +7. **`X + Y 直接兄弟选择器`**:在**X 之后第一个兄弟节点**中选择满足 Y 选择器的元素,兼容性: IE7+ +8. **`X > Y 子选择器`**: 选择 X 的子元素中满足 Y 选择器的元素,兼容性: IE7+ +9. **`X ~ Y 兄弟`**: 选择**X 之后所有兄弟节点**中满足 Y 选择器的元素,兼容性: IE7+ +10. **`[attr]`**:选择所有设置了 attr 属性的元素,兼容性 IE7+ +11. **`[attr=value]`**:选择属性值刚好为 value 的元素 +12. **`[attr~=value]`**:选择属性值为空白符分隔,其中一个的值刚好是 value 的元素 +13. **`[attr|=value]`**:选择属性值刚好为 value 或者 value-开头的元素 +14. **`[attr^=value]`**:选择属性值以 value 开头的元素 +15. **`[attr$=value]`**:选择属性值以 value 结尾的元素 +16. **`[attr*=value]`**:选择属性值中包含 value 的元素 +17. **`[:checked]`**:选择单选框,复选框,下拉框中选中状态下的元素,兼容性:IE9+ +18. **`X:after, X::after`**:after 伪元素,选择元素虚拟子元素(元素的最后一个子元素),CSS3 中::表示伪元素。兼容性:after 为 IE8+,::after 为 IE9+ +19. **`:hover`**:鼠标移入状态的元素,兼容性 a 标签 IE4+, 所有元素 IE7+ +20. **`:not(selector)`**:选择不符合 selector 的元素。**不参与计算优先级**,兼容性:IE9+ +21. **`::first-letter`**:伪元素,选择块元素第一行的第一个字母,兼容性 IE5.5+ +22. **`::first-line`**:伪元素,选择块元素的第一行,兼容性 IE5.5+ +23. **`:nth-child(an + b)`**:伪类,选择前面有 an + b - 1 个兄弟节点的元素,其中 n + >= 0, 兼容性 IE9+ +24. **`:nth-last-child(an + b)`**:伪类,选择后面有 an + b - 1 个兄弟节点的元素 + 其中 n >= 0,兼容性 IE9+ +25. **`X:nth-of-type(an+b)`**:伪类,X 为选择器,**解析得到元素标签**,选择**前面**有 an + b - 1 个**相同标签**兄弟节点的元素。兼容性 IE9+ +26. **`X:nth-last-of-type(an+b)`**:伪类,X 为选择器,解析得到元素标签,选择**后面**有 an+b-1 个相同**标签**兄弟节点的元素。兼容性 IE9+ +27. **`X:first-child`**:伪类,选择满足 X 选择器的元素,且这个元素是其父节点的第一个子元素。兼容性 IE7+ +28. **`X:last-child`**:伪类,选择满足 X 选择器的元素,且这个元素是其父节点的最后一个子元素。兼容性 IE9+ +29. **`X:only-child`**:伪类,选择满足 X 选择器的元素,且这个元素是其父元素的唯一子元素。兼容性 IE9+ +30. **`X:only-of-type`**:伪类,选择 X 选择的元素,**解析得到元素标签**,如果该元素没有相同类型的兄弟节点时选中它。兼容性 IE9+ +31. **`X:first-of-type`**:伪类,选择 X 选择的元素,**解析得到元素标签**,如果该元素 + 是此此类型元素的第一个兄弟。选中它。兼容性 IE9+ + +## css sprite 是什么,有什么优缺点 + +概念:将多个小图片拼接到一个图片中。通过 background-position 和元素尺寸调节需要显示的背景图案。 + +优点: + +1. 减少 HTTP 请求数,极大地提高页面加载速度 +2. 增加图片信息重复度,提高压缩比,减少图片大小 +3. 更换风格方便,只需在一张或几张图片上修改颜色或样式即可实现 + +缺点: + +1. 图片合并麻烦 +2. 维护麻烦,修改一个图片可能需要重新布局整个图片,样式 + +## `display: none;`与`visibility: hidden;`的区别 + +联系:它们都能让元素不可见 + +区别: + +1. display:none;会让元素完全从渲染树中消失,渲染的时候不占据任何空间;visibility: hidden;不会让元素从渲染树消失,渲染时元素继续占据空间,只是内容不可见。 +2. display: none;是非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示;visibility: hidden;是继承属性,子孙节点由于继承了 hidden 而消失,通过设置 visibility: visible,可以让子孙节点显示。 +3. 修改常规流中元素的 display 通常会造成文档重排。修改 visibility 属性只会造成本元素的重绘。 +4. 读屏器不会读取 display: none;元素内容;会读取 visibility: hidden;元素内容。 + +## css hack 原理及常用 hack + +原理:利用**不同浏览器对 CSS 的支持和解析结果不一样**编写针对特定浏览器样式。常见的 hack 有 1)属性 hack。2)选择器 hack。3)IE 条件注释 + +- IE 条件注释:适用于[IE5, IE9]常见格式如下 + +```js + +``` + +- 选择器 hack:不同浏览器对选择器的支持不一样 + +```css +/***** Selector Hacks ******/ + +/* IE6 and below */ +* html #uno { + color: red; +} + +/* IE7 */ +*:first-child + html #dos { + color: red; +} + +/* IE7, FF, Saf, Opera */ +html > body #tres { + color: red; +} + +/* IE8, FF, Saf, Opera (Everything but IE 6,7) */ +html>/**/body #cuatro { + color: red; +} + +/* Opera 9.27 and below, safari 2 */ +html:first-child #cinco { + color: red; +} + +/* Safari 2-3 */ +html[xmlns*=''] body:last-child #seis { + color: red; +} + +/* safari 3+, chrome 1+, opera9+, ff 3.5+ */ +body:nth-of-type(1) #siete { + color: red; +} + +/* safari 3+, chrome 1+, opera9+, ff 3.5+ */ +body:first-of-type #ocho { + color: red; +} + +/* saf3+, chrome1+ */ +@media screen and (-webkit-min-device-pixel-ratio: 0) { + #diez { + color: red; + } +} + +/* iPhone / mobile webkit */ +@media screen and (max-device-width: 480px) { + #veintiseis { + color: red; + } +} + +/* Safari 2 - 3.1 */ +html[xmlns*='']:root #trece { + color: red; +} + +/* Safari 2 - 3.1, Opera 9.25 */ +*|html[xmlns*=''] #catorce { + color: red; +} + +/* Everything but IE6-8 */ +:root * > #quince { + color: red; +} + +/* IE7 */ +* + html #dieciocho { + color: red; +} + +/* Firefox only. 1+ */ +#veinticuatro, +x:-moz-any-link { + color: red; +} + +/* Firefox 3.0+ */ +#veinticinco, +x:-moz-any-link, +x:default { + color: red; +} +``` + +- 属性 hack:不同浏览器解析 bug 或方法 + +``` +/* IE6 */ +#once { _color: blue } + +/* IE6, IE7 */ +#doce { *color: blue; /* or #color: blue */ } + +/* Everything but IE6 */ +#diecisiete { color/**/: blue } + +/* IE6, IE7, IE8 */ +#diecinueve { color: blue\9; } + +/* IE7, IE8 */ +#veinte { color/*\**/: blue\9; } + +/* IE6, IE7 -- acts as an !important */ +#veintesiete { color: blue !ie; } /* string after ! can be anything */ +``` + +## specified value,computed value,used value 计算方法 + +- specified value: 计算方法如下: + + 1. 如果样式表设置了一个值,使用这个值 + 2. 如果没有设值,且这个属性是继承属性,从父元素继承 + 3. 如果没有设值,并且不是继承属性,则使用 css 规范指定的初始值 + +- computed value: 以 specified value 根据规范定义的行为进行计算,通常将相对值计算为绝对值,例如 em 根据 font-size 进行计算。一些使用百分数并且需要布局来决定最终值的属性,如 width,margin。百分数就直接作为 computed value。line-height 的无单位值也直接作为 computed value。这些值将在计算 used value 时得到绝对值。**computed value 的主要作用是用于继承** + +- used value:属性计算后的最终值,对于大多数属性可以通过 window.getComputedStyle 获得,尺寸值单位为像素。以下属性依赖于布局, + - background-position + - bottom, left, right, top + - height, width + - margin-bottom, margin-left, margin-right, margin-top + - min-height, min-width + - padding-bottom, padding-left, padding-right, padding-top + - text-indent + +## `link`与`@import`的区别 + +1. `link`是 HTML 方式, `@import`是 CSS 方式 +2. `link`最大限度支持并行下载,`@import`过多嵌套导致串行下载,出现[FOUC](http://www.bluerobot.com/web/css/fouc.asp/) +3. `link`可以通过`rel="alternate stylesheet"`指定候选样式 +4. 浏览器对`link`支持早于`@import`,可以使用`@import`对老浏览器隐藏样式 +5. `@import`必须在样式规则之前,可以在 css 文件中引用其他文件 +6. 总体来说:**[link 优于@import](http://www.stevesouders.com/blog/2009/04/09/dont-use-import/)** + +## `display: block;`和`display: inline;`的区别 + +`block`元素特点: + +1. 处于常规流中时,如果`width`没有设置,会自动填充满父容器 +2. 可以应用`margin/padding` +3. 在没有设置高度的情况下会扩展高度以包含常规流中的子元素 +4. 处于常规流中时布局时在前后元素位置之间(独占一个水平空间) +5. 忽略`vertical-align` + +`inline`元素特点 + +1. 水平方向上根据`direction`依次布局 +2. 不会在元素前后进行换行 +3. 受`white-space`控制 +4. `margin/padding`在竖直方向上无效,水平方向上有效 +5. `width/height`属性对非替换行内元素无效,宽度由元素内容决定 +6. 非替换行内元素的行框高由`line-height`确定,替换行内元素的行框高由`height`,`margin`,`padding`,`border`决定 +7. 浮动或绝对定位时会转换为`block` +8. `vertical-align`属性生效 + +## PNG,GIF,JPG 的区别及如何选 + +参考资料: [选择正确的图片格式](http://www.yuiblog.com/blog/2008/11/04/imageopt-2/) +**GIF**: + +1. 8 位像素,256 色 +2. 无损压缩 +3. 支持简单动画 +4. 支持 boolean 透明 +5. 适合简单动画 + +**JPEG**: + +1. 颜色限于 256 +2. 有损压缩 +3. 可控制压缩质量 +4. 不支持透明 +5. 适合照片 + +**PNG**: + +1. 有 PNG8 和 truecolor PNG +2. PNG8 类似 GIF 颜色上限为 256,文件小,支持 alpha 透明度,无动画 +3. 适合图标、背景、按钮 + +## CSS 有哪些继承属性 + +- 关于文字排版的属性如: + - [font](https://developer.mozilla.org/en-US/docs/Web/CSS/font) + - [word-break](https://developer.mozilla.org/en-US/docs/Web/CSS/word-break) + - [letter-spacing](https://developer.mozilla.org/en-US/docs/Web/CSS/letter-spacing) + - [text-align](https://developer.mozilla.org/en-US/docs/Web/CSS/text-align) + - [text-rendering](https://developer.mozilla.org/en-US/docs/Web/CSS/text-rendering) + - [word-spacing](https://developer.mozilla.org/en-US/docs/Web/CSS/word-spacing) + - [white-space](https://developer.mozilla.org/en-US/docs/Web/CSS/white-space) + - [text-indent](https://developer.mozilla.org/en-US/docs/Web/CSS/text-indent) + - [text-transform](https://developer.mozilla.org/en-US/docs/Web/CSS/text-transform) + - [text-shadow](https://developer.mozilla.org/en-US/docs/Web/CSS/text-shadow) +- [line-height](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height) +- [color](https://developer.mozilla.org/en-US/docs/Web/CSS/color) +- [visibility](https://developer.mozilla.org/en-US/docs/Web/CSS/visibility) +- [cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) + +## IE6 浏览器有哪些常见的 bug,缺陷或者与标准不一致的地方,如何解决 + +- IE6 不支持 min-height,解决办法使用 css hack: + +``` +.target { + min-height: 100px; + height: auto !important; + height: 100px; // IE6下内容高度超过会自动扩展高度 +} +``` + +- `ol`内`li`的序号全为 1,不递增。解决方法:为 li 设置样式`display: list-item;` + +- 未定位父元素`overflow: auto;`,包含`position: relative;`子元素,子元素高于父元素时会溢出。解决办法:1)子元素去掉`position: relative;`; 2)不能为子元素去掉定位时,父元素`position: relative;` + +``` + + +
+
+
+``` + +- IE6 只支持`a`标签的`:hover`伪类,解决方法:使用 js 为元素监听 mouseenter,mouseleave 事件,添加类实现效果: + +``` + + +

aaaa bbbbbDDDDDDDDDDDd aaaa lkjlkjdf j

+ + +``` + +- IE5-8 不支持`opacity`,解决办法: + +``` +.opacity { + opacity: 0.4 + filter: alpha(opacity=60); /* for IE5-7 */ + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)"; /* for IE 8*/ +} +``` + +- IE6 在设置`height`小于`font-size`时高度值为`font-size`,解决办法:`font-size: 0;` +- IE6 不支持 PNG 透明背景,解决办法: **IE6 下使用 gif 图片** +- IE6-7 不支持`display: inline-block`解决办法:设置 inline 并触发 hasLayout + +``` + display: inline-block; + *display: inline; + *zoom: 1; +``` + +- IE6 下浮动元素在浮动方向上与父元素边界接触元素的外边距会加倍。解决办法: + 1)使用 padding 控制间距。 + 2)浮动元素`display: inline;`这样解决问题且无任何副作用:css 标准规定浮动元素 display:inline 会自动调整为 block +- 通过为块级元素设置宽度和左右 margin 为 auto 时,IE6 不能实现水平居中,解决方法:为父元素设置`text-align: center;` + +## 容器包含若干浮动元素时如何清理(包含)浮动 + +1. 容器元素闭合标签前添加额外元素并设置`clear: both` +2. 父元素触发块级格式化上下文(见块级可视化上下文部分) +3. 设置容器元素伪元素进行清理[推荐的清理浮动方法](http://nicolasgallagher.com/micro-clearfix-hack/) + +``` +/** +* 在标准浏览器下使用 +* 1 content内容为空格用于修复opera下文档中出现 +* contenteditable属性时在清理浮动元素上下的空白 +* 2 使用display使用table而不是block:可以防止容器和 +* 子元素top-margin折叠,这样能使清理效果与BFC,IE6/7 +* zoom: 1;一致 +**/ + +.clearfix:before, +.clearfix:after { + content: " "; /* 1 */ + display: table; /* 2 */ +} + +.clearfix:after { + clear: both; +} + +/** +* IE 6/7下使用 +* 通过触发hasLayout实现包含浮动 +**/ +.clearfix { + *zoom: 1; +} +``` + +## 什么是 FOUC?如何避免 + +Flash Of Unstyled Content:用户定义样式表加载之前浏览器使用默认样式显示文档,用户样式加载渲染之后再从新显示文档,造成页面闪烁。**解决方法**:把样式表放到文档的`head` + +## 如何创建块级格式化上下文(block formatting context),BFC 有什么用 + +创建规则: + +1. 根元素 +2. 浮动元素(`float`不是`none`) +3. 绝对定位元素(`position`取值为`absolute`或`fixed`) +4. `display`取值为`inline-block`,`table-cell`, `table-caption`,`flex`, `inline-flex`之一的元素 +5. `overflow`不是`visible`的元素 + +作用: + +1. 可以包含浮动元素 +2. 不被浮动元素覆盖 +3. 阻止父子元素的 margin 折叠 + +## display,float,position 的关系 + +1. 如果`display`为 none,那么 position 和 float 都不起作用,这种情况下元素不产生框 +2. 否则,如果 position 值为 absolute 或者 fixed,框就是绝对定位的,float 的计算值为 none,display 根据下面的表格进行调整。 +3. 否则,如果 float 不是 none,框是浮动的,display 根据下表进行调整 +4. 否则,如果元素是根元素,display 根据下表进行调整 +5. 其他情况下 display 的值为指定值 + 总结起来:**绝对定位、浮动、根元素都需要调整`display`** + ![display转换规则](https://user-images.githubusercontent.com/8088864/126057344-e6e66b1a-edc3-4725-bf4a-835f9153a1eb.png) + +## 五外边距折叠(collapsing margins) + +毗邻的两个或多个`margin`会合并成一个 margin,叫做外边距折叠。规则如下: + +1. 两个或多个毗邻的普通流中的块元素垂直方向上的 margin 会折叠 +2. 浮动元素/inline-block 元素/绝对定位元素的 margin 不会和垂直方向上的其他元素的 margin 折叠 +3. 创建了块级格式化上下文的元素,不会和它的子元素发生 margin 折叠 +4. 元素自身的 margin-bottom 和 margin-top 相邻时也会折叠 + +## 如何确定一个元素的包含块(containing block) + +1. 根元素的包含块叫做初始包含块,在连续媒体中他的尺寸与 viewport 相同并且 anchored at the canvas origin;对于 paged media,它的尺寸等于 page area。初始包含块的 direction 属性与根元素相同。 +2. `position`为`relative`或者`static`的元素,它的包含块由最近的块级(`display`为`block`,`list-item`, `table`)祖先元素的**内容框**组成 +3. 如果元素`position`为`fixed`。对于连续媒体,它的包含块为 viewport;对于 paged media,包含块为 page area +4. 如果元素`position`为`absolute`,它的包含块由祖先元素中最近一个`position`为`relative`,`absolute`或者`fixed`的元素产生,规则如下: + + - 如果祖先元素为行内元素,the containing block is the bounding box around the **padding boxes** of the first and the last inline boxes generated for that element. + - 其他情况下包含块由祖先节点的**padding edge**组成 + + 如果找不到定位的祖先元素,包含块为**初始包含块** + +## stacking context,布局规则 + +z 轴上的默认层叠顺序如下(从下到上): + +1. 根元素的边界和背景 +2. 常规流中的元素按照 html 中顺序 +3. 浮动块 +4. positioned 元素按照 html 中出现顺序 + +如何创建 stacking context: + +1. 根元素 +2. z-index 不为 auto 的定位元素 +3. a flex item with a z-index value other than 'auto' +4. opacity 小于 1 的元素 +5. 在移动端 webkit 和 chrome22+,z-index 为 auto,position: fixed 也将创建新的 stacking context + +## 如何水平居中一个元素 + +- 如果需要居中的元素为**常规流中 inline 元素**,为父元素设置`text-align: center;`即可实现 +- 如果需要居中的元素为**常规流中 block 元素**,1)为元素设置宽度,2)设置左右 margin 为 auto。3)IE6 下需在父元素上设置`text-align: center;`,再给子元素恢复需要的值 + +``` html + +
+ aaaaaa aaaaaa a a a a a a a a +
+ + + +``` + +- 如果需要居中的元素为**浮动元素**,1)为元素设置宽度,2)`position: relative;`,3)浮动方向偏移量(left 或者 right)设置为 50%,4)浮动方向上的 margin 设置为元素宽度一半乘以-1 + +``` html + +
+ aaaaaa aaaaaa a a a a a a a a +
+ + + +``` + +- 如果需要居中的元素为**绝对定位元素**,1)为元素设置宽度,2)偏移量设置为 50%,3)偏移方向外边距设置为元素宽度一半乘以-1 + +``` html + +
+ aaaaaa aaaaaa a a a a a a a a +
+ + + +``` + +- 如果需要居中的元素为**绝对定位元素**,1)为元素设置宽度,2)设置左右偏移量都为 0,3)设置左右外边距都为 auto + +``` html + +
+ aaaaaa aaaaaa a a a a a a a a +
+ + + +``` + +## 如何竖直居中一个元素 + +参考资料:[6 Methods For Vertical Centering With CSS](http://www.vanseodesign.com/css/vertical-centering/)。 [盘点 8 种 CSS 实现垂直居中](http://blog.csdn.net/freshlover/article/details/11579669) + +- 需要居中元素为**单行文本**,为包含文本的元素设置大于`font-size`的`line-height`: + +``` html +

center text

+ + +``` + +## DOM 元素 e 的 e.getAttribute(propName)和 e.propName 有什么区别和联系 + +- e.getAttribute(),是标准 DOM 操作文档元素属性的方法,具有通用性可在任意文档上使用,返回元素在源文件中**设置的属性** +- e.propName 通常是在 HTML 文档中访问特定元素的**特性**,浏览器解析元素后生成对应对象(如 a 标签生成 HTMLAnchorElement),这些对象的特性会根据特定规则结合属性设置得到,对于没有对应特性的属性,只能使用 getAttribute 进行访问 +- e.getAttribute()返回值是源文件中设置的值,类型是字符串或者 null(有的实现返回"") +- e.propName 返回值可能是字符串、布尔值、对象、undefined 等 +- 大部分 attribute 与 property 是一一对应关系,修改其中一个会影响另一个,如 id,title 等属性 +- 一些布尔属性``的检测设置需要 hasAttribute 和 removeAttribute 来完成,或者设置对应 property +- 像`link`中 href 属性,转换成 property 的时候需要通过转换得到完整 URL +- 一些 attribute 和 property 不是一一对应如:form 控件中``对应的是 defaultValue,修改或设置 value property 修改的是控件当前值,setAttribute 修改 value 属性不会改变 value property + +## XMLHttpRequest 通用属性和方法 + +1. `readyState`:表示请求状态的整数,取值: + +- UNSENT(0):对象已创建 +- OPENED(1):open()成功调用,在这个状态下,可以为 xhr 设置请求头,或者使用 send()发送请求 +- HEADERS_RECEIVED(2):所有重定向已经自动完成访问,并且最终响应的 HTTP 头已经收到 +- LOADING(3):响应体正在接收 +- DONE(4):数据传输完成或者传输产生错误 + +3. `onreadystatechange`:readyState 改变时调用的函数 +4. `status`:服务器返回的 HTTP 状态码(如,200, 404) +5. `statusText`:服务器返回的 HTTP 状态信息(如,OK,No Content) +6. `responseText`:作为字符串形式的来自服务器的完整响应 +7. `responseXML`: Document 对象,表示服务器的响应解析成的 XML 文档 +8. `abort()`:取消异步 HTTP 请求 +9. `getAllResponseHeaders()`: 返回一个字符串,包含响应中服务器发送的全部 HTTP 报头。每个报头都是一个用冒号分隔开的名/值对,并且使用一个回车/换行来分隔报头行 +10. `getResponseHeader(headerName)`:返回 headName 对应的报头值 +11. `open(method, url, asynchronous [, user, password])`:初始化准备发送到服务器上的请求。method 是 HTTP 方法,不区分大小写;url 是请求发送的相对或绝对 URL;asynchronous 表示请求是否异步;user 和 password 提供身份验证 +12. `setRequestHeader(name, value)`:设置 HTTP 报头 +13. `send(body)`:对服务器请求进行初始化。参数 body 包含请求的主体部分,对于 POST 请求为键值对字符串;对于 GET 请求,为 null + +## offsetWidth/offsetHeight,clientWidth/clientHeight 与 scrollWidth/scrollHeight 的区别 + +- offsetWidth/offsetHeight 返回值包含**content + padding + border**,效果与 e.getBoundingClientRect()相同 +- clientWidth/clientHeight 返回值只包含**content + padding**,如果有滚动条,也**不包含滚动条** +- scrollWidth/scrollHeight 返回值包含**content + padding + 溢出内容的尺寸** + +[Measuring Element Dimension and Location with CSSOM in Windows Internet Explorer 9]() + +![元素尺寸](https://user-images.githubusercontent.com/8088864/126057392-4ee53f39-9730-4aae-aa18-2f25236b6dd2.png) + +## focus/blur 与 focusin/focusout 的区别与联系 + +1. focus/blur 不冒泡,focusin/focusout 冒泡 +2. focus/blur 兼容性好,focusin/focusout 在除 FireFox 外的浏览器下都保持良好兼容性,如需使用事件托管,可考虑在 FireFox 下使用事件捕获 elem.addEventListener('focus', handler, true) +3. 可获得焦点的元素: + 1. window + 2. 链接被点击或键盘操作 + 3. 表单空间被点击或键盘操作 + 4. 设置`tabindex`属性的元素被点击或键盘操作 + +## mouseover/mouseout 与 mouseenter/mouseleave 的区别与联系 + +1. mouseover/mouseout 是标准事件,**所有浏览器都支持**;mouseenter/mouseleave 是 IE5.5 引入的特有事件后来被 DOM3 标准采纳,现代标准浏览器也支持 +2. mouseover/mouseout 是**冒泡**事件;mouseenter/mouseleave**不冒泡**。需要为**多个元素监听鼠标移入/出事件时,推荐 mouseover/mouseout 托管,提高性能** +3. 标准事件模型中 **event.target** 表示正在发生移入/移出的元素,**event.relatedTarget**表示对应移入/移出的目标元素;在老 IE 中 **event.srcElement** 表示正在发生移入/移出的元素,**event.toElement**表示移出的目标元素,**event.fromElement**表示移入时的来源元素 + +例子:鼠标从 div#target 元素移出时进行处理,判断逻辑如下: +``` html +
test
+ + +``` + +## javascript 跨域通信 + +同源:两个文档同源需满足 + +1. 协议相同 +2. 域名相同 +3. 端口相同 + +跨域通信:js 进行 DOM 操作、通信时如果目标与当前窗口不满足同源条件,浏览器为了安全会阻止跨域操作。跨域通信通常有以下方法 + +- 如果是 log 之类的简单**单项通信**,新建``,` + + + + + + + + +``` + +#### 3. 多进程多实例并行压缩 + +并行压缩主流有以下三种方案 + +- 使用 parallel-uglify-plugin 插件 +- uglifyjs-webpack-plugin 开启 parallel 参数 +- terser-webpack-plugin 开启 parallel 参数 (推荐使用这个,支持 ES6 语法压缩) + +安装插件 + +``` shell +npm install --save-dev terser-webpack-plugin +``` + +使用插件 + +修改配置webpack.config.js文件 + +``` js +const path = require("path"); +// 导入速度分析插件 +const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); + +// 导入代码压缩插件 +const TerserPlugin = require("terser-webpack-plugin"); + +// 导入体积分析插件 +const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; + +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +//判断是否为生产环境 +const isProduction = process.env.NODE_ENV === 'production'; + +// 实例化速度分析插件 +const smp = new SpeedMeasurePlugin(); + +//定义 CDN 路径,这里采用 bootstrap 的 cdn +const cdn = { + css: [ + 'https://cdn.bootcss.com/Swiper/4.5.1/css/swiper.min.css' + ], + js: [ + 'https://cdn.bootcss.com/vue/2.6.10/vue.min.js', + 'https://cdn.bootcss.com/vue-router/3.1.3/vue-router.min.js', + 'https://cdn.bootcss.com/vuex/3.1.1/vuex.min.js', + 'https://cdn.bootcss.com/axios/0.19.0/axios.min.js', + 'https://cdn.bootcss.com/echarts/4.3.0/echarts.min.js', + 'https://cdn.bootcss.com/Swiper/4.5.1/js/swiper.min.js', + ] +} + +const webpackConfig = smp.wrap({ + entry: { + // ... + }, + output: { + // ... + }, + resolve: { + // ... + }, + module: { + rules: [ + rules: [ + { + test: /\.js$/, + include: path.resolve('src'), + use: [ + 'thread-loader', + // your expensive loader (e.g babel-loader) + ], + } + ] + ] + }, + //生产环境注入 cdn + externals: isProduction && { + 'vue': 'Vue', + 'vuex': 'Vuex', + 'vue-router': 'VueRouter', + 'axios': 'axios', + 'echarts': 'echarts', + 'swiper': 'Swiper' + } || {}, + optimization: { + minimizer: [ + new TerserPlugin({ + parallel: 4 + }) + ] + }, + plugins: [ + new HtmlWebpackPlugin({ filename: '../index.html' }), // output file relative to output.path + new WebpackCdnPlugin({ + modules: [ + { + name: 'vue', + var: 'Vue', + path: 'vue.min.js' + }, + { + name: 'vuex', + var: 'Vuex', + path: 'vuex.min.js' + } + { + name: 'vue-router', + var: 'VueRouter', + path: 'vue-router.min.js' + }, + { + name: 'axios', + var: 'axios', + path: 'axios.min.js' + } + { + name: 'echarts', + var: 'echarts', + path: 'echarts.min.js' + }, + { + name: 'swiper', + var: 'Swiper', + path: 'swiper.min.js' + }, + ], + prod: isProduction, + prodUrl: '//cdn.bootcdn.net/ajax/libs/:name/:version/:path' // => https://cdn.bootcdn.net/ajax/libs/xxx/xxx/xxx(`:name`,`:version`和`:path`为模板变量) + publicPath: '/node_modules/dist', // override when prod is false + }), + new MyPlugin(), + new MyOtherPlugin(), + ], +}); + +module.exports = webpackConfig; +``` + +#### 4. 使用 polyfill 动态服务 + +Polyfill 可以为旧浏览器提供和标准 API 一样的功能。比如你想要 IE 浏览器实现 Promise 和 fetch 功能,你需要手动引入 es6-promise、whatwg-fetch。而通过 Polyfill.io,你只需要引入一个 JS 文件。 + +Polyfill.io 通过分析请求头信息中的 UserAgent 实现自动加载浏览器所需的 polyfills。 Polyfill.io 有一份默认功能列表,包括了最常见的 polyfills:document.querySelector、Element.classList、ES5 新增的 Array 方法、Date.now、ES6 中的 Object.assign、Promise 等。 + +动态 `polyfill` 指的是根据不同的浏览器,动态载入需要的 `polyfill`。 `Polyfill.io` 通过尝试使用 `polyfill` 重新创建缺少的功能,可以更轻松地支持不同的浏览器,并且可以大幅度的减少构建体积。 + +Polyfill Service 原理 + +识别 User Agent,下发不同的 Polyfill + +![Webpack polyfill 服务](https://user-images.githubusercontent.com/8088864/126054825-2e1a0e44-2eb7-4668-b044-de846427e577.png) + +使用方法: + +在 index.html 中引入如下 script 标签 + +``` html + + + + +Document + + + + + + +``` + +## webpack 做过哪些优化,开发效率方面、打包策略方面等等 + +### 1)优化 Webpack 的构建速度 + +- 使用高版本的 Webpack (使用webpack4) +- 多线程/多实例构建:HappyPack(不维护了)、thread-loader +- 缩小打包作用域: + - exclude/include (确定 loader 规则范围) + - resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找) + - resolve.extensions 尽可能减少后缀尝试的可能性 + - noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句) + - IgnorePlugin (完全排除模块) + - 合理使用alias +- 充分利用缓存提升二次构建速度: + - babel-loader 开启缓存 + - terser-webpack-plugin 开启缓存 + - 使用 cache-loader 或者 hard-source-webpack-plugin + 注意:thread-loader 和 cache-loader 兩個要一起使用的話,請先放 cache-loader 接著是 thread-loader 最後才是 heavy-loader +- DLL: + - 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。 + +### 2)使用webpack4-优化原因 + +1. V8带来的优化(for of替代forEach、Map和Set替代Object、includes替代indexOf) +2. 默认使用更快的md4 hash算法 +3. webpacks AST可以直接从loader传递给AST,减少解析时间 +4. 使用字符串方法替代正则表达式 + +#### noParse + +- 不去解析某个库内部的依赖关系 +- 比如jquery 这个库是独立的, 则不去解析这个库内部依赖的其他的东西 +- 在独立库的时候可以使用 + +``` js +module.exports = { + module: { + noParse: /jquery/, + rules:[] + } +} +``` + +#### IgnorePlugin + +- 忽略掉某些内容 不去解析依赖库内部引用的某些内容 +- 从moment中引用 ./locol 则忽略掉 +- 如果要用local的话 则必须在项目中必须手动引入 import 'moment/locale/zh-cn' + +``` js +module.exports = { + plugins: [ + new Webpack.IgnorePlugin(/./local/, /moment/), + ] +} +``` + +#### dllPlugin + +- 不会多次打包, 优化打包时间 +- 先把依赖的不变的库打包 +- 生成 manifest.json文件 +- 然后在webpack.config中引入 +- webpack.DllPlugin Webpack.DllReferencePlugin + +#### happypack -> thread-loader + +- 大项目的时候开启多线程打包 +- 影响前端发布速度的有两个方面,一个是构建,一个就是压缩,把这两个东西优化起来,可以减少很多发布的时间。 + +#### thread-loader + +thread-loader 会将您的 loader 放置在一个 worker 池里面运行,以达到多线程构建。 +把这个 loader 放置在其他 loader 之前(如下图 example 的位置), 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。 + +``` js +// webpack.config.js +module.exports = { + module: { + rules: [ + { + test: /\.js$/, + include: path.resolve("src"), + use: [ + "thread-loader", + // 你的高开销的loader放置在此 (e.g babel-loader) + ] + } + ] + } +} +``` + +每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。请在高开销的loader中使用,否则效果不佳 + +#### 压缩加速——开启多线程压缩 + +- 不推荐使用 webpack-paralle-uglify-plugin,项目基本处于没人维护的阶段,issue 没人处理,pr没人合并。 + Webpack 4.0以前:uglifyjs-webpack-plugin,parallel参数 + +``` js +module.exports = { + optimization: { + minimizer: [ + new UglifyJsPlugin({ + parallel: true, + }), + ], + }, +}; +``` + +- 推荐使用 terser-webpack-plugin + +``` js +module.exports = { + optimization: { + minimizer: [ + new TerserPlugin({ + parallel: true // 多线程 + }) + ], + }, +}; +``` + +### 2)优化 Webpack 的打包体积 + +- 压缩代码 + - webpack-paralle-uglify-plugin + - uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6) + - terser-webpack-plugin 开启 parallel 参数 + - 多进程并行压缩 + - 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过optimize-css-assets-webpack-plugin插件 开启 cssnano 压缩 CSS。 +- 提取页面公共资源 + - 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中 + - 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件 + - 基础包分离:将一些基础库放到cdn,比如vue,webpack 配置 external是的vue不打入bundle +- Tree shaking + - purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议) + - 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率 + - 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking + - 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码 +- Scope hosting + - 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hosting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突 + - 必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hosting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法 +- 图片压缩 + - 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式) + - 配置 image-webpack-loader +- 动态Polyfill + - 建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。(部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill) + - @babel-preset-env 中通过useBuiltIns: 'usage参数来动态加载polyfill。 + +### 3)speed-measure-webpack-plugin + +简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。 + +### 4)开发阶段常用的插件 + +#### 开启多核压缩 + +插件:**terser-webpack-plugin** + +``` js +const TerserPlugin = require('terser-webpack-plugin') +module.exports = { + optimization: { + minimizer: [ + new TerserPlugin({ + parallel: true, + terserOptions: { + ecma: 6, + }, + }), + ] + } +} +``` + +#### 监控面板 + +插件:**speed-measure-webpack-plugin** + +在打包的时候显示出每一个loader,plugin所用的时间,来精准优化 + +``` js +// webpack.config.js文件 +const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); +const smp = new SpeedMeasurePlugin(); +//............ +// 用smp.warp()包裹一下合并的config +module.exports = smp.wrap(merge(_mergeConfig, webpackConfig)); +``` + +#### 开启一个通知面板 + +插件:**webpack-build-notifier** + +``` js +// webpack.config.js文件 +const WebpackBuildNotifierPlugin = require('webpack-build-notifier'); +const webpackConfig= { + plugins: [ + new WebpackBuildNotifierPlugin({ + title: '我的webpack', + // logo: path.resolve('./img/favicon.png'), + suppressSuccess: true + }) + ] +} +``` + +#### 开启打包进度 + +插件:**progress-bar-webpack-plugin** + +``` js +// webpack.config.js文件 +const ProgressBarPlugin = require('progress-bar-webpack-plugin'); +const webpackConfig= { + plugins: [ + new ProgressBarPlugin(), + ] +} +``` + +#### 开发面板更清晰 + +插件:**webpack-dashboard** + +``` js +// webpack.config.js文件 +const DashboardPlugin = require('webpack-dashboard/plugin'); +const webpackConfig= { + plugins: [ + new DashboardPlugin() + ] +} +``` + +``` json +// package.json文件 +{ + "scripts": { + "dev": "webpack-dashboard webpack --mode development", + }, +} +``` + +#### 开启窗口的标题 + +第三方库: **node-bash-title** + +这个包mac的item用有效果,windows暂时没看到效果 + +``` js +// webpack.config.js文件 +const setTitle = require('node-bash-title'); +setTitle('server'); +``` + +#### friendly-errors-webpack-plugin + +插件:**friendly-errors-webpack-plugin** + +``` js +const webpackConfig= { + plugins: [ + new FriendlyErrorsWebpackPlugin({ + compilationSuccessInfo: { + messages: ['You application is running here http://localhost:3000'], + notes: ['Some additionnal notes to be displayed unpon successful compilation'] + }, + onErrors: function (severity, errors) { + // You can listen to errors transformed and prioritized by the plugin + // severity can be 'error' or 'warning' + }, + // should the console be cleared between each compilation? + // default is true + clearConsole: true, + + // add formatters and transformers (see below) + additionalFormatters: [], + additionalTransformers: [] + }), + ] +} +``` + +## 如何封装 node 中间件 + +在NodeJS中,中间件主要是指封装所有Http请求细节处理的方法。一次Http请求通常包含很多工作,如记录日志、ip过滤、查询字符串、请求体解析、Cookie处理、权限验证、参数验证、异常处理等,但对于Web应用而言,并不希望接触到这么多细节性的处理,因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。 + +中间件的行为比较类似Java中过滤器的工作原理,就是在进入具体的业务处理之前,先让过滤器处理。 + +``` js +const http = require('http') +function compose(middlewareList) { + return function (ctx) { + function dispatch (i) { + const fn = middlewareList[i] + try { + return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1))) + } catch (err) { + Promise.reject(err) + } + } + return dispatch(0) + } +} +class App { + constructor(){ + this.middlewares = [] + } + use(fn){ + this.middlewares.push(fn) + return this + } + handleRequest(ctx, middleware) { + return middleware(ctx) + } + createContext (req, res) { + const ctx = { + req, + res + } + return ctx + } + callback () { + const fn = compose(this.middlewares) + return (req, res) => { + const ctx = this.createContext(req, res) + return this.handleRequest(ctx, fn) + } + } + listen(...args) { + const server = http.createServer(this.callback()) + return server.listen(...args) + } +} +module.exports = App +``` + +## node 中间层怎样做的请求合并转发 + +### 1)什么是中间层 + +- 就是前端---请求---> nodejs ----请求---->后端 ----响应--->nodejs--数据处理---响应---->前端。这么一个流程,这个流程的好处就是当业务逻辑过多,或者业务需求在不断变更的时候,前端不需要过多当去改变业务逻辑,与后端低耦合。前端即显示,渲染。后端获取和存储数据。中间层处理数据结构,返回给前端可用可渲染的数据结构。 +- nodejs是起中间层的作用,即根据客户端不同请求来做相应的处理或渲染页面,处理时可以是把获取的数据做简单的处理交由底层java那边做真正的数据持久化或数据更新,也可以是从底层获取数据做简单的处理返回给客户端。 +- 通常我们把Web领域分为客户端和服务端,也就是前端和后端,这里的后端就包含了网关,静态资源,接口,缓存,数据库等。而中间层呢,就是在后端这里再抽离一层出来,在业务上处理和客户端衔接更紧密的部分,比如页面渲染(SSR),数据聚合,接口转发等等。 +- 以SSR来说,在服务端将页面渲染好,可以加快用户的首屏加载速度,避免请求时白屏,还有利于网站做SEO,他的好处是比较好理解的。 + +### 2)中间层可以做的事情 + +- 代理:在开发环境下,我们可以利用代理来,解决最常见的跨域问题;在线上环境下,我们可以利用代理,转发请求到多个服务端。 +- 缓存:缓存其实是更靠近前端的需求,用户的动作触发数据的更新,node中间层可以直接处理一部分缓存需求。 +- 限流:node中间层,可以针对接口或者路由做响应的限流。 +- 日志:相比其他服务端语言,node中间层的日志记录,能更方便快捷的定位问题(是在浏览器端还是服务端)。 +- 监控:擅长高并发的请求处理,做监控也是合适的选项。 +- 鉴权:有一个中间层去鉴权,也是一种单一职责的实现。 +- 路由:前端更需要掌握页面路由的权限和逻辑。 +- 服务端渲染:node中间层的解决方案更灵活,比如SSR、模板直出、利用一些JS库做预渲染等等。 + +### 3)node转发API(node中间层)的优势 + +- 可以在中间层把java|php的数据,处理成对前端更友好的格式 +- 可以解决前端的跨域问题,因为服务器端的请求是不涉及跨域的,跨域是浏览器的同源策略导致的 +- 可以将多个请求在通过中间层合并,减少前端的请求 + +### 4)如何做请求合并转发 + +- 使用express中间件multifetch可以将请求批量合并 +- 使用express+http-proxy-middleware实现接口代理转发 + +### 5)不使用用第三方模块手动实现一个nodejs代理服务器,实现请求合并转发 + +#### 实现思路 + +- 1. 搭建http服务器,使用Node的http模块的createServer方法 +- 2. 接收客户端发送的请求,就是请求报文,请求报文中包括请求行、请求头、请求体 +- 3. 将请求报文发送到目标服务器,使用http模块的request方法 + +#### 实现步骤 + +- 第一步:http服务器搭建 + +``` js +const http = require("http"); +const server = http.createServer(); +server.on('request',(req,res)=>{ + res.end("hello world") +}) +server.listen(3000,()=>{ + console.log("running"); +}) +``` + +- 第二步:接收客户端发送到代理服务器的请求报文 + +``` js +const http = require("http"); +const server = http.createServer(); +server.on('request', (req, res)=>{ + // 通过req的data事件和end事件接收客户端发送的数据 + // 并用Buffer.concat处理一下 + // + let postbody = []; + req.on("data", chunk => { + postbody.push(chunk); + }) + req.on('end', () => { + let postbodyBuffer = Buffer.concat(postbody); + res.end(postbodyBuffer); + }) +}) +server.listen(3000,()=>{ + console.log("running"); +}) +``` + +这一步主要数据在客户端到服务器端进行传输时在nodejs中需要用到buffer来处理一下。处理过程就是将所有接收的数据片段chunk塞到一个数组中,然后将其合并到一起还原出源数据。合并方法需要用到Buffer.concat,这里不能使用加号,加号会隐式的将buffer转化为字符串,这种转化不安全。 + +- 第三步:使用http模块的request方法,将请求报文发送到目标服务器 + +第二步已经得到了客户端上传的数据,但是缺少请求头,所以在这一步根据客户端发送的请求需要构造请求头,然后发送 + +``` js +const http = require("http"); +const server = http.createServer(); + +server.on("request", (req, res) => { + var { connection, host, ...originHeaders } = req.headers; + var options = { + "method": req.method, + // 随表找了一个网站做测试,被代理网站修改这里 + "hostname": "www.nanjingmb.com", + "port": "80", + "path": req.url, + "headers": { originHeaders } + }; + //接收客户端发送的数据 + var p = new Promise((resolve,reject)=>{ + let postbody = []; + req.on("data", chunk => { + postbody.push(chunk); + }) + req.on('end', () => { + let postbodyBuffer = Buffer.concat(postbody); + resolve(postbodyBuffer); + }); + }); + //将数据转发,并接收目标服务器返回的数据,然后转发给客户端 + p.then((postbodyBuffer)=>{ + let responsebody = []; + var request = http.request(options, (response) => { + response.on('data', (chunk) => { + responsebody.push(chunk); + }); + response.on("end", () => { + responsebodyBuffer = Buffer.concat(responsebody) + res.end(responsebodyBuffer); + }); + }); + // 使用request的write方法传递请求体 + request.write(postbodyBuffer); + // 使用end方法将请求发出去 + request.end(); + }); +}); +server.listen(3000, () => { + console.log("runnng"); +}); +``` + +## 介绍下 promise 的特性、优缺点,内部是如何实现的,动手实现 Promise + +### 1)Promise基本特性 + +1. Promise有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败) +2. Promise对象接受一个回调函数作为参数, 该回调函数接受两个参数,分别是成功时的回调resolve和失败时的回调reject;另外resolve的参数除了正常值以外, 还可能是一个Promise对象的实例;reject的参数通常是一个Error对象的实例。 +3. then方法返回一个新的Promise实例,并接收两个参数onResolved(fulfilled状态的回调);onRejected(rejected状态的回调,该参数可选) +4. catch方法返回一个新的Promise实例 +5. finally方法不管Promise状态如何都会执行,该方法的回调函数不接受任何参数 +6. Promise.all()方法将多个多个Promise实例,包装成一个新的Promise实例,该方法接受一个由Promise对象组成的数组作为参数(Promise.all()方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例),注意参数中只要有一个实例触发catch方法,都会触发Promise.all()方法返回的新的实例的catch方法,如果参数中的某个实例本身调用了catch方法,将不会触发Promise.all()方法返回的新实例的catch方法 +7. Promise.race()方法的参数与Promise.all方法一样,参数中的实例只要有一个率先改变状态就会将该实例的状态传给Promise.race()方法,并将返回值作为Promise.race()方法产生的Promise实例的返回值 +8. Promise.resolve()将现有对象转为Promise对象,如果该方法的参数为一个Promise对象,Promise.resolve()将不做任何处理;如果参数thenable对象(即具有then方法),Promise.resolve()将该对象转为Promise对象并立即执行then方法;如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为fulfilled,其参数将会作为then方法中onResolved回调函数的参数,如果Promise.resolve方法不带参数,会直接返回一个fulfilled状态的 Promise 对象。需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。 +9. Promise.reject()同样返回一个新的Promise对象,状态为rejected,无论传入任何参数都将作为reject()的参数 + +### 2)Promise优点 + +- 统一异步 API +Promise 的一个重要优点是它将逐渐被用作浏览器的异步 API ,统一现在各种各样的 API ,以及不兼容的模式和手法。 +- Promise 与事件对比 +和事件相比较, Promise 更适合处理一次性的结果。在结果计算出来之前或之后注册回调函数都是可以的,都可以拿到正确的值。 Promise 的这个优点很自然。但是,不能使用 Promise 处理多次触发的事件。链式处理是 Promise 的又一优点,但是事件却不能这样链式处理。 +- Promise 与回调对比 +解决了回调地狱的问题,将异步操作以同步操作的流程表达出来。 +- Promise 带来的额外好处是包含了更好的错误处理方式(包含了异常处理),并且写起来很轻松(因为可以重用一些同步的工具,比如 Array.prototype.map() )。 + +### 3)Promise缺点 + +1. 无法取消Promise,一旦新建它就会立即执行,无法中途取消。 +2. 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。 +3. 当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 +4. Promise 真正执行回调的时候,定义 Promise 那部分实际上已经走完了,所以 Promise 的报错堆栈上下文不太友好。 + +### 4)简单代码实现 + +最简单的Promise实现有7个主要属性, state(状态), value(成功返回值), reason(错误信息), resolve方法, reject方法, then方法。 + +``` js +class Promise { + constructor(executor) { + this.state = 'pending'; + this.value = undefined; + this.reason = undefined; + + this.callbacks = []; + + const resolve = (value) => { + if (this.state === 'pending') { + this.state = 'fulfilled'; + this.value = value; + + if (this.callbacks.length) { + this.callbacks.forEach((cb, index) => { + if (index === 0) { + try { + const result = cb.onResolved(this.value); + if (result instanceof Promise) { + result.then((value) => cb.resolve(value), reason => cb.reject(reason)); + } else { + cb.resolve(result); + } + } catch (error) { + cb.reject(error); + } + } else { + cb.onResolved(this.value); + } + }); + } + } + } + + const reject = (reason) => { + if (this.state === 'pending') { + this.state = 'rejected'; + this.reason = reason; + + if (this.callbacks.length) { + this.callbacks.forEach((cb) => { + cb.onRejected(this.reason); + }); + } + } + } + + try { + executor(resolve, reject); + } catch (error) { + reject(error) + } + } +} +``` + +## 实现 Promise.all + +### 1)核心思路 + +1. 接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数 +2. 这个方法返回一个新的 promise 对象, +3. 遍历传入的参数,用Promise.resolve()将参数"包一层",使其变成一个promise对象 +4. 参数所有回调成功才是成功,返回值数组与参数顺序一致 +5. 参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。 + +### 2)实现代码 + +一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了~ + +``` js +function promiseAll(promises) { + return new Promise(function(resolve, reject) { + if (!Array.isArray(promises)) { + throw new TypeError(`argument must be a array`); + } + var resolvedCounter = 0; + var promiseNum = promises.length; + var resolvedResult = []; + for (let i = 0; i < promiseNum; i++) { + Promise.resolve(promises[i]).then(value=>{ + resolvedCounter++; + resolvedResult[i] = value; + if (resolvedCounter === promiseNum) { + return resolve(resolvedResult); + } + }, error=>{ + return reject(error); + }); + } + }); +} + +// test +let p1 = new Promise(function (resolve, reject) { + setTimeout(function () { + resolve(1); + }, 1000); +}) +let p2 = new Promise(function (resolve, reject) { + setTimeout(function () { + resolve(2); + }, 2000); +}) +let p3 = new Promise(function (resolve, reject) { + setTimeout(function () { + resolve(3); + }, 3000); +}) +promiseAll([p3, p1, p2]).then(res => { + console.log(res); // [3, 1, 2] +}); +``` + +## delete 操作的注意点 + +### 知识点 +- 1. delete使用原则:delete 操作符用来删除一个对象的属性。 +- 2. delete在删除一个不可配置的属性时在严格模式和非严格模式下的区别: + - (1)在严格模式中,如果属性是一个不可配置(non-configurable)属性,删除时会抛出异常; + - (2)非严格模式下返回 false。 +- 3. delete能删除隐式声明的全局变量:这个全局变量其实是global对象(window)的属性 +- 4. delete能删除的: + - (1)可配置对象的属性 + - (2)隐式声明的全局变量 + - (3)用户定义的属性 + - (4)在ECMAScript 6中,通过 const 或 let 声明指定的 "temporal dead zone" (TDZ) 对 delete 操作符也会起作用 +- 5. delete不能删除的: + - (1)显式声明的全局变量 + - (2)内置对象的内置属性 + - (3)一个对象从原型继承而来的属性 +- 6. delete删除数组元素: + - (1)当你删除一个数组元素时,数组的 length 属性并不会变小,数组元素变成undefined + - (2)当用 delete 操作符删除一个数组元素时,被删除的元素已经完全不属于该数组。 + - (3)如果你想让一个数组元素的值变为 undefined 而不是删除它,可以使用 undefined 给其赋值而不是使用 delete 操作符。此时数组元素是在数组中的 +- 7. delete 操作符与直接释放内存(只能通过解除引用来间接释放)没有关系。 + +## AMD和CMD规范区别 + +- AMD规范:是 RequireJS在推广过程中对模块定义的规范化产出的 +- CMD规范:是SeaJS 在推广过程中对模块定义的规范化产出的 +- CMD 推崇依赖就近;AMD 推崇依赖前置 +- CMD 是延迟执行;AMD 是提前执行 +- CMD性能好,因为只有用户需要的时候才执行;AMD用户体验好,因为没有延迟,依赖模块提前执行了 + +## SPA单页页面 + +SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。 + +### SPA优点 + +- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染; +- 基于上面一点,SPA 相对对服务器压力小; +- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理; + +### SPA缺点 + +- 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载; +- 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理; +- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。 + +## Vue.js虚拟DOM的优缺点 + +### 1)优点 + +- **保证性能下限**: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限; +- **无需手动操作 DOM**: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率; +- **跨平台**: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。 + +### 2)缺点 + +- **无法进行极致优化**: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。比如VScode采用直接手动操作DOM的方式进行极端的性能优化 + +## Node 中怎么开启一个子线程 + +work_thread +Node 10.5.0 的发布,work_thread 让 Node 有了真正的多线程能力,worker_thread 模块中有 4 个对象和 2 个类 + +- isMainThread: 是否是主线程,源码中是通过 threadId === 0 进行判断的。 +- MessagePort: 用于线程之间的通信,继承自 EventEmitter。 +- MessageChannel: 用于创建异步、双向通信的通道实例。 +- threadId: 线程 ID。 +- Worker: 用于在主线程中创建子线程。第一个参数为 filename,表示子线程执行的入口。 +- parentPort: 在 worker 线程里是表示父进程的 MessagePort 类型的对象,在主线程里为 null +- workerData: 用于在主进程中向子进程传递数据(data 副本) + +在主线程中开启五个子线程,并且主线程向子线程发送简单的消息示例代码如下: + +``` js +const { + isMainThread, + parentPort, + workerData, + threadId, + MessageChannel, + MessagePort, + Worker, +} = require("worker_threads"); + +function mainThread() { + for (let i = 0; i < 5; i++) { + const worker = new Worker(__filename, { workerData: i }); + worker.on("exit", (code) => { + console.log(`main: worker stopped with exit code ${code}`); + }); + worker.on("message", (msg) => { + console.log(`main: receive ${msg}`); + worker.postMessage(msg + 1); + }); + } +} + +function workerThread() { + console.log(`worker: workerDate ${workerData}`); + parentPort.on("message", (msg) => { + console.log(`worker: receive ${msg}`); + }), + parentPort.postMessage(workerData); +} + +if (isMainThread) { + mainThread(); +} else { + workerThread(); +} + +// 上述代码在主线程中开启五个子线程,并且主线程向子线程发送简单的消息 +``` + +## CSS预处理器和Less有什么好处 + +### CSS预处理器 + +为css增加编程特性的拓展语言,可以使用变量,简单逻辑判断,函数等基本编程技巧。 + +css预处理器编译输出还是标准的css样式。 + +less, sass都是动态的样式语言,是css预处理器,css上的一种抽象层。他们是一种特殊的语法语言而编译成css的。 + +less变量符号是@,sass变量符号是$。 + +### 预处理器解决了哪些痛点 + +css语法不够强大。因为无法嵌套导致有很多重复的选择器 没有变量和合理的样式利用机制,导致逻辑上相关的属性值只能以字面量的形式重复输出,难以维护。 + +### 常用规范 + +变量,嵌套语法,混入,@import,运算,函数,继承 + +### 好处 + +比css代码更加整洁,更易维护,代码量更少 修改更快。 +基础颜色使用变量,一处动,处处动。 +常用的代码使用代码块,节省大量代码。 +css嵌套减少大量的重复选择器,避免一些低级错误。 +变量混入大大提升了样式的利用性 额外的工具类似颜色函数(lighten,darken,transparentize),mixins,loops等这些方法使css更像一个真正的编程语言,让开发者能够有能力生成更加复杂的css样式。 + +## Sass + +Sass (英文全称:Syntactically Awesome Stylesheets) 是一个最初由 Hampton Catlin 设计并由 Natalie Weizenbaum 开发的层叠样式表语言。 + +Sass 是一个 CSS 预处理器。 + +Sass 是 CSS 扩展语言,可以帮助我们减少 CSS 重复的代码,节省开发时间。 + +Sass 完全兼容所有版本的 CSS。 + +Sass 扩展了 CSS3,增加了规则、变量、混入、选择器、继承、内置函数等等特性。 + +Sass 生成良好格式化的 CSS 代码,易于组织和维护。 + +Sass 文件后缀为 `.scss`。 + +浏览器并不支持 Sass 代码。因此,你需要使用一个 Sass 预处理器将 Sass 代码转换为 CSS 代码。 + +## 什么是 React? + +React 是一个开源前端 JavaScript 库,用于构建用户界面,尤其是单页应用程序。它用于处理网页和移动应用程序的视图层。React 是由 Facebook 的软件工程师 Jordan Walke 创建的。在 2011 年 React 应用首次被部署到 Facebook 的信息流中,之后于 2012 年被应用到 Instagram 上。 + +## React 的主要特点是什么? + +- 考虑到真实的 DOM 操作成本很高,它使用 VirtualDOM 而不是真实的 DOM。 +- 支持服务端渲染。 +- 遵循单向数据流或数据绑定。 +- 使用可复用/可组合的 UI 组件开发视图。 + +## react 最新版本解决了什么问题 加了哪些东西 + +### 1)React 16.x的三大新特性 Time Slicing, Suspense,hooks + +- Time Slicing(解决CPU速度问题)使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得react能在性能极其差的机器跑时,仍然保持有良好的性能 +- Suspense (解决网络IO问题)和lazy配合,实现异步加载组件。 能暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从react出生到现在都存在的「异步副作用」的问题,而且解决得非常的优雅,使用的是「异步但是同步的写法」,我个人认为,这是最好的解决异步问题的方式 +- 此外,还提供了一个内置函数 componentDidCatch,当有错误发生时, 我们可以友好地展示 fallback 组件;可以捕捉到它的子元素(包括嵌套子元素)抛出的异常;可以复用错误组件。 + +### 2)React16.8 + +- 加入hooks,让React函数式组件更加灵活 +- hooks之前,React存在很多问题 + - 在组件间复用状态逻辑很难 + - 复杂组件变得难以理解,高阶组件和函数组件的嵌套过深。 + - class组件的this指向问题 + - 难以记忆的生命周期 +- hooks很好的解决了上述问题,hooks提供了很多方法 + - useState 返回有状态值,以及更新这个状态值的函数 + - useEffect 接受包含命令式,可能有副作用代码的函数。 + - useContext 接受上下文对象(从React.createContext返回的值)并返回当前上下文值, + - useReducer useState的替代方案。接受类型为(state,action) => newState的reducer,并返回与dispatch方法配对的当前状态。 + - useCallback 返回一个回忆的memoized版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性 + - useMemo 纯的一个记忆函数 + - useRef 返回一个可变的ref对象,其.current属性被初始化为传递的参数,返回的 ref 对象在组件的整个生命周期内保持不变。 + - useImperativeMethods 自定义使用ref时公开给父组件的实例值 + - useMutationEffect 更新兄弟组件之前,它在React执行其DOM改变的同一阶段同步触发 + - useLayoutEffect DOM改变后同步触发。使用它来从DOM读取布局并同步重新渲染 + +### 3)React16.9 + +- 重命名 Unsafe 的生命周期方法。新的 UNSAFE_ 前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出 +- 废弃 javascript: 形式的 URL。以 javascript: 开头的 URL 非常容易遭受攻击,造成安全漏洞。 +- 废弃 “Factory” 组件。 工厂组件会导致 React 变大且变慢。 +- act() 也支持异步函数,并且你可以在调用它时使用 await。 +- 使用 `` 进行性能评估。 在较大的应用中追踪性能回归可能会很方便 + +### 4)React16.13.0 + +- 支持在渲染期间调用setState,但仅适用于同一组件 +- 可检测冲突的样式规则并记录警告 +- 废弃unstable_createPortal,使用createPortal +- 将组件堆栈添加到其开发警告中,使开发人员能够隔离bug并调试其程序,这可以清楚地说明问题所在,并更快地定位和修复错误。 + +## 什么是 JSX? + +JSX 是 ECMAScript 一个类似 XML 的语法扩展。基本上,它只是为 React.createElement() 函数提供语法糖,从而让在我们在 JavaScript 中,使用类 HTML 模板的语法,进行页面描述。 + +## 什么是 Pure Components? + +`React.PureComponent` 与 `React.Component` 完全相同,只是它为你处理了 `shouldComponentUpdate()` 方法。当属性或状态发生变化时,`PureComponent` 将对属性和状态进行浅比较。另一方面,一般的组件不会将当前的属性和状态与新的属性和状态进行比较。因此,在默认情况下,每当调用 `shouldComponentUpdate` 时,默认返回 true,所以组件都将重新渲染。 + +## 状态和属性有什么区别? + +state 和 props 都是普通的 JavaScript 对象。虽然它们都保存着影响渲染输出的信息,但它们在组件方面的功能不同。Props 以类似于函数参数的方式传递给组件,而状态则类似于在函数内声明变量并对它进行管理。 + +States vs Props + +| Conditions | States | Props | +| ---- | ---- | ---- | +| 可从父组件接收初始值 | 是 | 是 | +| 可在父组件中改变其值 | 否 | 是 | +| 在组件内设置默认值 | 是 | 是 | +| 在组件内可改变 | 是 | 否 | +| 可作为子组件的初始值 | 是 | 是 | + +## 我们为什么不能直接更新状态? + +如果你尝试直接改变状态,那么组件将不会重新渲染。 + +``` jsx +//Wrong +this.state.message = 'Hello world' +``` + +正确方法应该是使用 setState() 方法。它调度组件状态对象的更新。当状态更改时,组件通将会重新渲染。 + +``` jsx +//Correct +this.setState({ message: 'Hello World' }) +``` + +**注意:** 你可以在 constructor 中或使用最新的 JavaScript 类属性声明语法直接设置状态对象。 + +另在React文档中,提到永远不要直接更改this.state,而是使用this.setState进行状态更新,这样做的两个主要原因如下: + +- setState分批工作:这意味着不能期望setState立即进行状态更新,这是一个异步操作,因此状态更改可能在以后的时间点发生,这意味着手动更改状态可能会被setState覆盖。 + +- 性能:当使用纯组件或shouldComponentUpdate时,它们将使用===运算符进行浅表比较,但是如果更改状态,则对象引用将仍然相同,因此比较将失败。 + +**注意:** 为了避免避免数组/对象突变,可使用以下方法: + +1. 使用slice +2. 使用Object.assign +3. 在ES6中使用Spread operator +4. 嵌套对象 + +## 为什么 String Refs 被弃用? + +如果你以前使用过 React,你可能会熟悉旧的 API,其中的 `ref` 属性是字符串,如 `ref={'textInput'}`,并且 DOM 节点的访问方式为`this.refs.textInput`。我们建议不要这样做,因为字符串引用有以下问题,并且被认为是遗留问题。字符串 refs 在 React v16 版本中被移除。 + +1. 由于它无法知道this,所以需要React去跟踪当前渲染的组件。这使得React变得比较慢。 + +2. 如果一个库在传递的子组件(子元素)上放置了一个ref,那用户就无法在它上面再放一个ref了。但函数式可以实现这种组合。 + +3. 它们不能与静态分析工具一起使用,如 Flow。Flow 无法猜测出 this.refs 上的字符串引用的作用及其类型。Callback refs 对静态分析更友好。 + +4. 下述例子中,string类型的refs写法会让ref被放置在DataTable组件中,而不是MyComponent中。 + +``` jsx +class MyComponent extends Component { + renderRow = (index) => { + // This won't work. Ref will get attached to DataTable rather than MyComponent: + return ; + + // This would work though! Callback refs are awesome. + return this['input-' + index] = input} />; + } + + render() { + return + } +} +``` + +## 什么是 Virtual DOM? + +Virtual DOM (VDOM) 是 Real DOM 的内存表示形式。UI 的展示形式被保存在内存中并与真实的 DOM 同步。这是在调用的渲染函数和在屏幕上显示元素之间发生的一个步骤。整个过程被称为 reconciliation。 + +Real DOM vs Virtual DOM + +| Real DOM | Virtual DOM | +| ---- | ---- | +| 更新较慢 | 更新较快 | +| 可以直接更新 HTML | 无法直接更新 HTML | +| 如果元素更新,则创建新的 DOM | 如果元素更新,则更新 JSX | +| DOM 操作非常昂贵 | DOM 操作非常简单 | +| 较多的内存浪费 | 没有内存浪费 | + +## Virtual DOM 如何工作? + +Virtual DOM 分为三个简单的步骤。 + +1. 每当任何底层数据发生更改时,整个 UI 都将以 Virtual DOM 的形式重新渲染。 + +2. 然后计算先前 Virtual DOM 对象和新的 Virtual DOM 对象之间的差异。 + +3. 一旦计算完成,真实的 DOM 将只更新实际更改的内容。 + +## 为什么虚拟dom会提高性能 + +> 虚拟dom相当于在js和真实dom中间加了一个缓存,利用dom diff算法避免了没有必要的dom操作,从而提高性能 + +### **具体实现步骤如下** + +- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中 +- 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异 +- 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新 + +## react diff 原理(常考,大厂必考) + +1. 把树形结构按照层级分解,只比较同级元素。 +2. 给列表结构的每个单元添加唯一的 key 属性,方便比较。 +3. React 只会匹配相同 class 的 component(这里面的 class 指的是组件的名字) +4. 合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty. 到每一个事件循环结束, React 检查所有标记 dirty 的 component 重新绘制. +5. 选择性子树渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能。 + + +## React Fiber架构中,迭代器和requestIdleCallback结合的优势 + +### requestIdleCallback API + +requestIdleCallback 是浏览器提供的 Web API,它是 React Fiber 中用到的核心 API。 + +#### API 介绍 + +[requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback) 利用浏览器的空余时间执行任务,如果浏览器没有空余时间,可以随时终止这些任务。 + +这样可以实现如果有更高优先级的任务要执行时,当前执行的任务可以被终止,优先执行高级别的任务。 + +原理是该方法将 在浏览器的空闲时段内调用的函数 排队。 + +这样使得开发者能够在主事件循环上 执行后台和低优先级的任务,而不会影响 像动画和用户交互 这些关键的延迟触发的事件。 + +这里的“延迟”指的是大量计算导致运行时间较长。 + +#### 浏览器空余时间 + +页面是一帧一帧绘制出来的,当每秒绘制的帧数达到 60 时,页面时流畅的,小于这个值时,用户会感觉到卡顿。 + +1秒60帧意思是1秒中60张画面在切换。 + +当帧数低于人眼的捕捉频率(有说24帧或30帧,考虑到视觉残留现象,这个数值可能会更低)时,人脑会识别这是几张图片在切换,也就是静态的。 + +当帧数高于人眼的捕捉频率,人脑会认为画面是连续的,也就是动态的动画。 + +帧数越高画面就看起来更流畅。 + +1秒60帧(大约 1000/60 ≈ 16ms 切换一个画面)差不多是人眼能识别卡顿的分界线。 + +如果每一帧执行的时间小于 16 ms,就说明浏览器有空余时间。 + +一帧时间内浏览器要做的事情包括:脚本执行、样式计算、布局、重绘、合成等。 + +如果某一项内容执行时间过长,浏览器会推迟渲染,造成丢帧卡顿,就没有剩余时间。 + +#### 应用场景 + +比如现在有一项计算任务,这项任务需要花费比较长的时间(例如超过16ms)去执行。 + +在执行任务的过程当中,浏览器的主线程会被一直占用。 + +在主线程被占用的过程中,浏览器是被阻塞的,并不能执行其他的任务。 + +如果此时用户想要操作页面,比如向下滑动页面查看其它内容,浏览器是不能响应用户的操作的,给用户的感觉就是页面卡死了,体验非常差。 + +**如何解决呢?** + +可以将这项任务注册到 `requestIdleCallback` 中,利用浏览器的空余时间执行它。 + +当用户操作页面时,就是**优先级比较高的任务**被执行时,此时计算任务会被终止,优先响应用户的操作,这样用户就不会感觉页面发生卡顿了。 + +当高优先级的任务执行完成后,再继续执行计算任务。 + +`requestIdleCallback` 的作用就是利用浏览器的空余时间执行这些需要大量计算的任务,当空余时间结束,会中断计算任务,执行高优先级的任务,以达到不阻塞主线程任务(例如浏览器 UI 渲染)的目的。 + +#### 使用方式 + +``` js +var handle = window.requestIdleCallback(callback[, options]) +``` + +- callback:一个在空闲时间即将被调用的回调函数 + - 该函数接收一个形参:IdleDeadline,它提供一个方法和一个属性: + - 方法:timeRemaining() + - 用于获取浏览器空闲期的剩余时间,也就是空余时间 + - 返回值是毫秒数 + - 如果闲置期结束,则返回 0 + - 根据时间的多少可以来决定是否要执行任务 + - 属性:didTimeout(Boolean,只读) + - 表示是否是上一次空闲期因为超时而没有执行的回调函数 + - 超时时间由 requestIdleCallback 的参数options.timeout 定义 +- options:可选配置,目前只有一个配置项 + - timeout:超时时间,如果设置了超时时间并超时,回调函数还没有被调用,则会在下一次空闲期强制被调用 + +#### 功能体验 + +页面中有两个按钮和一个 DIV,点击第一个按钮执行一项昂贵的计算,使其长期占用主线程,当计算任务执行的时候去点击第二个按钮更改页面中 DIV 的背景颜色。 + +``` html + + + + + requestIdleCallback + + + +
playground
+ + + + + + +``` + +![requestIdleCallback功能体验1](https://user-images.githubusercontent.com/8088864/125783134-1435e780-a620-4cbf-b8c3-c04fccd4b145.png) + +使用 requestIdleCallback可以完美解决这个卡顿问题: + +``` html + + + + + requestIdleCallback + + + +
playground
+ + + + + + +``` + +![requestIdleCallback功能体验2](https://user-images.githubusercontent.com/8088864/125783529-12c4da73-fe20-4757-b858-169f381efce4.png) + +- 浏览器在空余时间执行 calc 函数 +- 当空余时间小于 1ms 时,跳出while循环 +- calc 根据 number 判断计算任务是否执行完成,如果没有完成,则继续注册新的空闲期的任务 +- 当 btn2 点击事件触发,会等到当前空闲期任务执行完后执行“更改背景颜色”的任务 +- “更改背景颜色”任务执行完成后,继续进入空闲期,执行后面的任务 + +由此可见,所谓执行优先级更高的任务,是手动将计算任务拆分到浏览器的空闲期,以实现每次进入空闲期之前优先执行主线程的任务。 + +### Fiber 出现的目的 + +Fiber 其实是 React 16 新的 DOM 比对算法的名字,旧的 DOM 比对算法的名字是 Stack。 + +#### React 16之前的版本存在的问题 + +React 16之前的版本对比更新 VirtualDOM 的过程是采用**循环加递归**实现的。 + +这种对比方式有一个问题,就是一旦任务开始进行就无法中断(由于递归需要一层一层的进入,一层一层的退出,所以过程不能中断)。 + +如果应用中组件数量庞大,主线程被长期占用,直到整棵 VirtualDOM 树对比更新完成之后主线程才能被释放,主线程才能执行其它任务。 + +这就会导致一些用户交互、动画等任务无法立即得到执行,页面就会产生卡顿,非常影响用户的体验。 + +因为递归利用的 **JavaScript 自身的执行栈**,所以旧版 DOM 比对的算法称为 **Stack(堆栈)**。 + +**核心问题:递归无法中断,执行重任务耗时长,JavaScript 又是单线程的,无法同时执行其它任务,导致在绘制页面的过程当中不能执行其它任务,比如元素动画、用户交互等任务必须延后,给用户的感觉就是页面变得卡顿,用户体验差。** + +### Stack 算法模拟 + +模拟 React 16 之前将虚拟 DOM 转化成真实 DOM 的递归算法: + +``` jsx +// 要渲染的 jsx +const jsx = ( +
+
+
+
+
+
+
+) +``` + +jsx 会被 Babel 转化成 `React.createElement()` 的调用,最终返回一个虚拟 DOM 对象: + +``` js +"use strict"; + +const jsx = /*#__PURE__*/React.createElement("div", { + id: "a1" +}, /*#__PURE__*/React.createElement("div", { + id: "b1" +}, /*#__PURE__*/React.createElement("div", { + id: "c1" +}), /*#__PURE__*/React.createElement("div", { + id: "c2" +})), /*#__PURE__*/React.createElement("div", { + id: "b2" +})); +``` + +去掉一些属性,打印结果: + +``` js +const jsx = { + type: 'div', + props: { + id: 'a1', + children: [ + { + type: 'div', + props: { + id: 'b1', + children: [ + { + type: 'div', + props: { + id: 'c1' + } + }, + { + type: 'div', + props: { + id: 'c2' + } + } + ] + } + }, + { + type: 'div', + props: { + id: 'b2' + } + } + ] + } +} +``` + +递归转化真实 DOM: + +``` js +const jsx = {...} +function render(vdom, container) { + // 创建元素 + const element = document.createElement(vdom.type); + // 为元素添加属性 + Object.keys(vdom.props) + .filter(prop => prop !== 'children') + .forEach(prop => (element[prop] = vdom.props[prop])); + // 递归创建子元素 + if (Array.isArray(vdom.props.children)) { + vdom.props.children.forEach(child => render(child, element)); + } + // 将元素添加到页面中 + container.appendChild(element); +} + +render(jsx, document.getElementById('root')); +``` + +DOM 更新就是在上面递归的过程中加入了 Virtual DOM 对比的过程。 + +可以看到递归是无法中断的。 + +### React 16 解决方案 - Fiber + +1. 利用浏览器空余时间执行任务,拒绝长时间占用主线程 + - 在新版本的 React 版本中,使用了 requestIdleCallback API + - 利用浏览器空余时间执行 VirtualDOM 比对任务,也就表示 VirtualDOM 比对不会长期占用主线程 + - 如果有高优先级的任务要执行,就会暂时终止 VirtualDOM 的比对过程,先去执行高优先级的任务 + - 高优先级任务执行完成,再回来继续执行 VirtualDOM 比对任务 + - 这样页面就不会出现卡顿现象 +2. 放弃递归,只采用循环,因为循环可以被中断 + - 由于递归必须一层一层进入,一层一层退出,所以过程无法中断 + - 所以要实现任务的终止再继续,就必须放弃递归,只采用循环的方式执行比对的过程 + - 因为循环是可以终止的,只需要将循环的条件保存下来,下一次任务就可以从中断的地方执行了 +3. 任务拆分,将任务拆分成一个个的小任务 + - 如果任务要实现终止再继续,任务的单元就必须要小 + - 这样任务即使没有执行完就被终止,重新执行任务的代价就会小很多 + - 所以要进行任务的拆分,将一个大的任务拆分成一个个小的任务 + - VirtualDOM 比对任务如何拆分? + - 以前将整棵 VirtualDOM 树的比对看作一个任务 + - 现在将树中每一个节点的比对看作一个任务 + +新版 React 的解决方案核心就是第 1 点,第 2、3 点都是为了实现第 1 点而存在的, + +Fiber 翻译过来是“纤维”,意思就是执行任务的颗粒度变得细腻,像纤维一样。 + +可以通过这个 [Demo](https://claudiopro.github.io/react-fiber-vs-stack-demo/) 查看 Stack 算法 和 Fiber 算法的效果区别。 + +### 实现思路 + +在 Fiber 方案中,为了实现任务的终止再继续,DOM 对比算法被拆分成了两阶段: + +1. render 阶段(可中断) + - VirtualDOM 的比对,构建 Fiber 对象,构建链表 + +2. commit 阶段(不可中断) + - 根据构建的链表进行 DOM 操作 + +过程就是: + +1. 在使用 React 编写用户界面的时候仍然使用 JSX 语法 +2. Babel 会将 JSX 语法转换成 `React.createElement()` 方法的调用 +3. `React.createElement()` 方法调用后会返回 VirtualDOM 对象 +4. 接下来就可以执行第一个阶段了:**构建 Fiber 对象** + - 采用循环的方式从 VirtualDOM 对象中,找到每一个内部的 VirtualDOM 对象 + - 为每一个 VirtualDOM 对象构建 Fiber 对象 + - Fiber 对象也是 JavaScript 对象,它是从 VirtualDOM 对象衍化来的,它除了 type、props、children以外还存储了更多节点的信息,其中包含的一个核心信息是:当前节点要进行的操作,例如删除、更新、新增 + - 在构建 Fiber 的过程中还要构建链表 +5. 接着进行第二阶段的操作:**执行 DOM 操作** + +总结: + +- DOM 初始渲染:`根据 VirtualDOM` --> `创建 Fiber 对象 及 构建链表` --> `将 Fiber 对象存储的操作应用到真实 DOM 中` +- DOM 更新操作:`newFiber(重新获取所有 Fiber 对象)` --> `newFiber vs oldFiber(获取旧的 Fiber 对象,进行比对) 将差异操作追加到链表` --> `将 Fiber 对象应用到真实 DOM 中` + +### 什么是 Fiber + +Fiber 有两层含义: + +- Fiber 是一个执行单元 +- Fiber 是一种数据结构 + +#### 执行单元 + +在 React 16 之前,将 Virtual DOM 树整体看成一个任务进行递归处理,任务整体庞大执行耗时且不能中断。 + +在 React 16,将整个任务拆分成一个个小的任务进行处理,每个小的任务指的就是一个 Fiber 节点的构建。 + +任务会在浏览器的空闲时间被执行,每个单元执行完成后,React 都会检查是否还有空余时间,如果有继续执行下一个人物单元,直到没有空余时间或者所有任务执行完毕,如果没有空余时间就交还主线程的控制权。 + +![React Fiber 执行单元流程图](https://user-images.githubusercontent.com/8088864/125878368-317a2c5c-8b16-4877-981c-3883075423bc.png) + +#### 数据结构 + +Fiber 是一种数据结构,支撑 React 构建任务的运转。 + +Fiber 其实就是 JavaScript 对象,对象中存储了当前节点的父节点、第一个子节点、下一个兄弟节点,以便在构建链表和执行 DOM 操作的时候知道它们的关系。 + +在 render 阶段的时候,React 会从上(root)向下,再从下向上构建所有节点对应的 Fiber 对象,在从下向上的同时还会构建链表,最后将链头存储到 Root Fiber。 + +- 从上向下 + - 从 Root 节点开始构建,优先构建子节点 + +- 从下向上 + - 如果当前节点没有子节点,就会构建下一个兄弟节点 + - 如果当前节点没有子节点,也没有下一个兄弟节点,就会返回父节点,构建父节点的兄弟节点 + - 如果父节点的下一个兄弟节点有子节点,就继续向下构建 + - 如果父节点没有下一个兄弟节点,就继续向上查找 + +在第二阶段的时候,通过链表结构的属性(child、sibling、parent)准确构建出完整的 DOM 节点树,从而才能将 DOM 对象追加到页面当中。 + +``` js +// Fiber 对象 +{ + type // 节点类型(元素、文本、组件)(具体的类型) + props // 节点属性(props中包含children属性,标识当前节点的子级 VirtualDOM) + stateNode // 节点的真实 DOM 对象 | 类组件实例对象 | 函数组件的定义方法 + tag // 节点标记(对具体类型的分类 host_root[顶级节点root] || host_component[普通DOM节点] || class_component[类组件] || function_component[函数组件]) + effectTag // 当前 Fiber 在 commit 阶段需要被执行的副作用类型/操作(新增、删除、修改) + nextEffect // 单链表用来快速查找下一个 sideEffect + lastEffect // 存储最新副作用,用于构建链表的 nextEffect + firstEffect // 存储第一个要执行的副作用,用于向 root 传递第一个要操作的 DOM + parent // 当前 Fiber 的父级 Fiber(React 中是 `return`) + child // 当前 Fiber 的第一个子级 Fiber + sibling // 当前 Fiber 的下一个兄弟 Fiber + alternate // 当前节点对应的旧 Fiber 的备份,用于新旧 Fiber 比对 +} +``` + +以上面的示例为例: + +``` jsx +
+
+
+
+
+
+
+``` + +![React Fiber 数据结构](https://user-images.githubusercontent.com/8088864/125878754-89f402ab-cb4b-466a-bef3-c6f20b9e10f8.png) + +``` js +// B1 的 Fiber 对象包含这几个属性: +{ + child: C1_Fiber, + sibling: B2_Fiber, + parent: A1_Fiber +} +``` + +## 说一下 react-fiber + +### 1)背景 + +- react在进行组件渲染时,从setState开始到渲染完成整个过程是同步的(“一气呵成”)。如果需要渲染的组件比较庞大,js执行会占据主线程时间较长,会导致页面响应度变差,使得react在动画、手势等应用中效果比较差。 +- 页面卡顿:Stack reconciler的工作流程很像函数的调用过程。父组件里调子组件,可以类比为函数的递归;对于特别庞大的vDOM树来说,reconciliation过程会很长(x00ms),超过16ms,在这期间,主线程是被js占用的,因此任何交互、布局、渲染都会停止,给用户的感觉就是页面被卡住了。 + +### 2)实现原理 + +旧版 React 通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而Fiber实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback这一 API。 + +Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示: + +``` js +const fiber = { + stateNode, // 节点实例 + child, // 子节点 + sibling, // 兄弟节点 + return, // 父节点 +} +``` + +- react内部运转分三层: + - Virtual DOM 层,描述页面长什么样。 + - Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。 + - Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。 + +- 为了实现不卡顿,就需要有一个调度器 (Scheduler) 来进行任务分配。优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。任务的优先级有六种: + - synchronous,与之前的Stack Reconciler操作一样,同步执行 + - task,在next tick之前执行 + - animation,下一帧之前执行 + - high,在不久的将来立即执行 + - low,稍微延迟执行也没关系 + - offscreen,下一次render时或scroll时才执行 + +- Fiber Reconciler(react )执行阶段: + - 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。 + - 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。 + +- Fiber树:React 在 render 第一次渲染时,会通过 React.createElement 创建一颗 Element 树,可以称之为 Virtual DOM Tree,由于要记录上下文信息,加入了 Fiber,每一个 Element 会对应一个 Fiber Node,将 Fiber Node 链接起来的结构成为 Fiber Tree。Fiber Tree 一个重要的特点是链表结构,将递归遍历编程循环遍历,然后配合 requestIdleCallback API, 实现任务拆分、中断与恢复。 + +- 从Stack Reconciler到Fiber Reconciler,源码层面其实就是干了一件递归改循环的事情 + +## 展示组件(Presentational component)和容器组件(Container component)之间有何区别? + +- 展示组件关心组件看起来是什么。展示专门通过 props 接受数据和回调,并且几乎不会有自身的状态,但当展示组件拥有自身的状态时,通常也只关心 UI 状态而不是数据的状态。 + +- 容器组件则更关心组件是如何运作的。容器组件会为展示组件或者其它容器组件提供数据和行为(behavior),它们会调用 Flux actions,并将其作为回调提供给展示组件。容器组件经常是有状态的,因为它们是(其它组件的)数据源。 + +## 类组件(Class component)和 函数式组件(Functional component)之间有何区别? + +1. 函数式组件比类组件操作简单,只是简单的调取和返回 JSX;而类组件可以使用生命周期函数来操作业务 + +2. 函数式组件可以理解为静态组件(组件中的内容调取的时候已经固定了,很难再修改),而类组件,可以基于组件内部的状态来动态更新渲染的内容 + +3. 类组件不仅允许你使用更多额外的功能,如组件自身的状态和生命周期钩子,也能使组件直接访问 store 并维持状态 + +4. 当组件仅是接收 props,并将组件自身渲染到页面时,该组件就是一个 '无状态组件(stateless component)',可以使用一个纯函数来创建这样的组件。这种组件也被称为哑组件(dumb components)或展示组件 + +## 功能组件(Functional Component)与类组件(Class Component)如何选择? + +如果您的组件具有状态( state ) 或 生命周期方法,请使用 Class 组件。否则,使用功能组件 + +解析: + +React中有两种组件:函数组件(Functional Components) 和类组件(Class Components)。据我观察,大部分同学都习惯于用类组件,而很少会主动写函数组件,包括我自己也是这样。但实际上,在使用场景和功能实现上,这两类组件是有很大区别的。 + +来看一个函数组件的例子: + +``` jsx +function Welcome = (props) => { + const sayHi = () => { + alert( `Hi ${props.name}` ); + } + return ( +
+

Hello, {props.name}

+ +
+ ) +} +``` +把上面的函数组件改写成类组件: + +``` jsx +import React from 'react' + +class Welcome extends React.Component { + constructor(props) { + super(props); + this.sayHi = this.sayHi.bind(this); + } + sayHi() { + alert( `Hi ${this.props.name}` ); + } + render() { + return ( +
+

Hello, {this.props.name}

+ +
+ ) + } +} +``` + +下面让我们来分析一下两种实现的区别: + +1. 第一眼直观的区别是,函数组件的代码量比类组件要少一些,所以函数组件比类组件更加简洁。千万不要小看这一点,对于我们追求极致的程序员来说,这依然是不可忽视的。 + +2. 函数组件看似只是一个返回值是DOM结构的函数,其实它的背后是无状态组件(Stateless Components)的思想。函数组件中,你无法使用State,也无法使用组件的生命周期方法,这就决定了函数组件都是展示性组件(Presentational Components),接收Props,渲染DOM,而不关注其他逻辑。 + +3. 函数组件中没有this。所以你再也不需要考虑this带来的烦恼。而在类组件中,你依然要记得绑定this这个琐碎的事情。如示例中的sayHi。 + +4. 函数组件更容易理解。当你看到一个函数组件时,你就知道它的功能只是接收属性,渲染页面,它不执行与UI无关的逻辑处理,它只是一个纯函数。而不用在意它返回的DOM结构有多复杂。 + +5. 性能。目前React还是会把函数组件在内部转换成类组件,所以使用函数组件和使用类组件在性能上并无大的差异。但是,React官方已承诺,未来将会优化函数组件的性能,因为函数组件不需要考虑组件状态和组件生命周期方法中的各种比较校验,所以有很大的性能提升空间。 + +6. 函数组件迫使你思考最佳实践。这是最重要的一点。组件的主要职责是UI渲染,理想情况下,所有的组件都是展示性组件,每个页面都是由这些展示性组件组合而成。如果一个组件是函数组件,那么它当然满足这个要求。所以牢记函数组件的概念,可以让你在写组件时,先思考这个组件应不应该是展示性组件。更多的展示性组件意味着更多的组件有更简洁的结构,更多的组件能被更好的复用。 + +所以,当你下次在动手写组件时,一定不要忽略了函数组件,应该尽可能多地使用函数组件。 + +## createElement 和 cloneElement 有什么区别? + +传入的第一个参数不同 + +React.createElement(): JSX 语法就是用 React.createElement()来构建 React 元素的。它接受三个参数,第一个参数可以是一个标签名。如 div、span,或者 React 组件。第二个参数为传入的属性。第三个以及之后的参数,皆作为组件的子组件。 + +``` js +React.createElement(type, [props], [...children]); +``` + +React.cloneElement()与 React.createElement()相似,不同的是它传入的第一个参数是一个 React 元素,而不是标签名或组件。新添加的属性会并入原有的属性,传入到返回的新元素中,而旧的子元素将被替换。将保留原始元素的键和引用。 + +``` js +React.cloneElement(element, [props], [...children]); +``` + +## 描述事件在 React 中的处理方式 + +为了解决跨浏览器兼容性问题,您的 React 中的事件处理程序将传递 SyntheticEvent 的实例,它是 React 的浏览器本机事件的跨浏览器包装器。 + +这些 SyntheticEvent 与您习惯的原生事件具有相同的接口,除了它们在所有浏览器中都兼容。有趣的是,React 实际上并没有将事件附加到子节点本身。React 将使用单个事件监听器监听顶层的所有事件。这对于性能是有好处的,这也意味着在更新 DOM 时,React 不需要担心跟踪事件监听器。 + +## React 中支持哪些指针事件? + +Pointer Events 提供了处理所有输入事件的统一方法。在过去,我们有一个鼠标和相应的事件监听器来处理它们,但现在我们有许多与鼠标无关的设备,比如带触摸屏的手机或笔。我们需要记住,这些事件只能在支持 Pointer Events 规范的浏览器中工作。 + +目前以下事件类型在 React DOM 中是可用的: + +1. onPointerDown +2. onPointerMove +3. onPointerUp +4. onPointerCancel +5. onGotPointerCapture +6. onLostPointerCaptur +7. onPointerEnter +8. onPointerLeave +9. onPointerOver +10. onPointerOut + +## React 事件绑定原理 + +React并不是将click事件绑在该div的真实DOM上,而是在document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。 +另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault。 + +![react事件绑定原理](https://user-images.githubusercontent.com/8088864/126061467-cd14eaa1-038a-48c2-ae47-221ad03e725c.png) + +### 1)事件注册 + +![事件注册流程](https://user-images.githubusercontent.com/8088864/126061515-beeda52c-8cf9-4af2-9d6f-2da545e09ba7.png) + +- 组件装载 / 更新。 +- 通过lastProps、nextProps判断是否新增、删除事件分别调用事件注册、卸载方法。 +- 调用EventPluginHub的enqueuePutListener进行事件存储 +- 获取document对象。 +- 根据事件名称(如onClick、onCaptureClick)判断是进行冒泡还是捕获。 +- 判断是否存在addEventListener方法,否则使用attachEvent(兼容IE)。 +- 给document注册原生事件回调为dispatchEvent(统一的事件分发机制)。 + +### 2)事件存储 + +![事件存储](https://user-images.githubusercontent.com/8088864/126061553-7d2039f1-9724-4069-b675-559fd95f8bff.png) + +- EventPluginHub负责管理React合成事件的callback,它将callback存储在listenerBank中,另外还存储了负责合成事件的Plugin。 +- EventPluginHub的putListener方法是向存储容器中增加一个listener。 +- 获取绑定事件的元素的唯一标识key。 +- 将callback根据事件类型,元素的唯一标识key存储在listenerBank中。 +- listenerBank的结构是:listenerBank\[registrationName]\[key]。 + +``` js +{ + onClick:{ + nodeid1:()=>{...} + nodeid2:()=>{...} + }, + onChange:{ + nodeid3:()=>{...} + nodeid4:()=>{...} + } +} +``` + +### 3)事件触发执行 + +![事件触发执行](https://user-images.githubusercontent.com/8088864/126061593-4be8d1a6-9001-42c0-af56-928106a89d79.png) + +- 触发document注册原生事件的回调dispatchEvent +- 获取到触发这个事件最深一级的元素 + 这里的事件执行利用了React的批处理机制 + +代码示例 + +``` jsx +
this.parent = ref}> +
this.child = ref}> + test +
+
+``` + +- 首先会获取到this.child +- 遍历这个元素的所有父元素,依次对每一级元素进行处理。 +- 构造合成事件。 +- 将每一级的合成事件存储在eventQueue事件队列中。 +- 遍历eventQueue。 +- 通过isPropagationStopped判断当前事件是否执行了阻止冒泡方法。 +- 如果阻止了冒泡,停止遍历,否则通过executeDispatch执行合成事件。 +- 释放处理完成的事件。 + +### 4)合成事件 + +![事件合成](https://user-images.githubusercontent.com/8088864/126061657-e91d4aa2-d61f-4b3d-82e6-37805b645710.png) + +- 调用EventPluginHub的extractEvents方法。 +- 循环所有类型的EventPlugin(用来处理不同事件的工具方法)。 +- 在每个EventPlugin中根据不同的事件类型,返回不同的事件池。 +- 在事件池中取出合成事件,如果事件池是空的,那么创建一个新的。 +- 根据元素nodeid(唯一标识key)和事件类型从listenerBink中取出回调函数 +- 返回带有合成事件参数的回调函数 + +5)总流程 + +![事件总流程](https://user-images.githubusercontent.com/8088864/126061682-e2ee65f1-651c-4762-bb7c-972602b1d451.png) + + +## 什么是 hooks? + +Hooks 是一个新的草案,它允许你在不编写类的情况下使用状态和其他 React 特性。 + +## Hooks 需要遵循什么规则? + +为了使用 hooks,你需要遵守两个规则: + +1. 仅在顶层的 React 函数调用 hooks。也就是说,你不能在循环、条件或内嵌函数中调用 hooks。这将确保每次组件渲染时都以相同的顺序调用 hooks,并且它会在多个 useState 和 useEffect 调用之间保留 hooks 的状态。 +2. 仅在 React 函数中调用 hooks。例如,你不能在常规的 JavaScript 函数中调用 hooks。 + + +## React memo 函数是什么? + +当类组件的输入属性相同时,可以使用 `pureComponent` 或 `shouldComponentUpdate` 来避免组件的渲染。现在,你可以通过把函数组件包装在 `React.memo` 中来实现相同的功能。 + +``` jsx +const MyComponent = React.memo(function MyComponent(props) { + /* only rerenders if props change */ +}); +``` + +## React lazy 函数是什么? + +使用 React.lazy 函数允许你将动态导入的组件作为常规组件进行渲染。当组件开始渲染时,它会自动加载包含对应组件的包。它必须返回一个 Promise,该 Promise 解析后为一个带有默认导出 React 组件的模块。 + +**注意:** React.lazy 和 Suspense 还不能用于服务端渲染。如果要在服务端渲染的应用程序中进行代码拆分,我们仍然建议使用 React Loadable。 + +## 什么是 Flow? + +Flow 是一个静态类型检查器,旨在查找 JavaScript 中的类型错误。与传统类型系统相比,Flow 类型可以表达更细粒度的区别。例如,与大多数类型系统不同,Flow 能帮助你捕获涉及 `null` 的错误。 + +## Flow 和 PropTypes 有什么区别? + +- Flow 是一个静态类型检查器(静态检查器),它使用该语言的超集,允许你在所有代码中添加类型注释,并在编译时捕获整个类的错误。 + +- PropTypes 是一个基本类型检查器(运行时检查器),已经被添加到 React 中。除了检查传递给给定组件的属性类型外,它不能检查其他任何内容。 + +如果你希望对整个项目进行更灵活的类型检查,那么 Flow/TypeScript 是更合适的选择。 + +## React Native 和 React 有什么区别? + +**React**是一个 JavaScript 库,支持前端 Web 和在服务器上运行,用于构建用户界面和 Web 应用程序。 + +**React Native**是一个移动端框架,可编译为本机应用程序组件,允许您使用 JavaScript 构建本机移动应用程序(iOS,Android和Windows),允许您使用 React 构建组件。 + +## MVW 模式的缺点是什么? + +1. DOM 操作非常昂贵,导致应用程序行为缓慢且效率低下。 +2. 由于循环依赖性,围绕模型和视图创建了复杂的模型。 +3. 协作型应用程序(如Google Docs)会发生大量数据更改。 +4. 如果不添加太多额外代码就无法轻松撤消(及时回退)。 + +## 什么是 Jest? + +Jest是一个由 Facebook 基于 Jasmine 创建的 JavaScript 单元测试框架,提供自动模拟依赖项和jsdom环境。它通常用于测试组件。 + +## Jest 对比 Jasmine 有什么优势? + +与 Jasmine 相比,有几个优点: + +- 自动查找在源代码中要执行的测试。 +- 在运行测试时自动模拟依赖项。 +- 允许您同步测试异步代码。 +- 通过 jsdom 使用假的 DOM 实现运行测试,以便可以在命令行上运行测试。 +- 在并行流程中运行测试,以便更快完成。 + +## 什么是 React Router? + +React Router 是一个基于 React 之上的强大路由库,可以帮助您快速地向应用添加视图和数据流,同时保持 UI 与 URL 同步。 + +## 什么是 React 流行的特定 linters? + +ESLint 是一个流行的 JavaScript linter。有一些插件可以分析特定的代码样式。在 React 中最常见的一个是名为 `eslint-plugin-react` npm 包。默认情况下,它将使用规则检查许多最佳实践,检查内容从迭代器中的键到一组完整的 prop 类型。另一个流行的插件是 `eslint-plugin-jsx-a11y`,它将帮助修复可访问性的常见问题。由于 JSX 提供的语法与常规 HTML 略有不同,因此常规插件无法获取 alt 文本和 tabindex 的问题。 + +## React 和 ReactDOM 之间有什么区别? + +`react` 包中包含 `React.createElement()`, `React.Component`, `React.Children`,以及与元素和组件类相关的其他帮助程序。你可以将这些视为构建组件所需的同构或通用帮助程序。`react-dom` 包中包含了 `ReactDOM.render()`,在 `react-dom/server` 包中有支持服务端渲染的 `ReactDOMServer.renderToString()` 和 `ReactDOMServer.renderToStaticMarkup()` 方法。 + +## 为什么 ReactDOM 从 React 分离出来? + +React 团队致力于将所有的与 DOM 相关的特性抽取到一个名为 ReactDOM 的独立库中。React v0.14 是第一个拆分后的版本。通过查看一些软件包,`react-native`,`react-art`,`react-canvas`,和 `react-three`,很明显,React 的优雅和本质与浏览器或 DOM 无关。为了构建更多 React 能应用的环境,React 团队计划将主要的 React 包拆分成两个:`react` 和 `react-dom`。这为编写可以在 React 和 React Native 的 Web 版本之间共享的组件铺平了道路。 + +## 是否可以在不调用 setState 方法的情况下,强制组件重新渲染? + +默认情况下,当组件的状态或属性改变时,组件将重新渲染。如果你的 `render()` 方法依赖于其他数据,你可以通过调用 `forceUpdate()` 来告诉 React,当前组件需要重新渲染。 + +## React 很多个 setState 为什么是执行完再 render + +react为了提高整体的渲染性能,会将一次渲染周期中的state进行合并,在这个渲染周期中对所有setState的所有调用都会被合并起来之后,再一次性的渲染,这样可以避免频繁的调用setState导致频繁的操作dom,提高渲染性能。 + +具体的实现方面,可以简单的理解为react中存在一个状态变量isBatchingUpdates,当处于渲染周期开始时,这个变量会被设置成true,渲染周期结束时,会被设置成false,react会根据这个状态变量,当出在渲染周期中时,仅仅只是将当前的改变缓存起来,等到渲染周期结束时,再一次性的全部render。 + +## 如何在 React 中启用生产模式? + +你应该使用 Webpack 的 `DefinePlugin` 方法将 `NODE_ENV` 设置为 `production`,通过它你可以去除 `propType` 验证和额外警告等内容。除此之外,如果你压缩代码,如使用 `Uglify` 的死代码消除,以去掉用于开发的代码和注释,它将大大减少包的大小。 + +## React 的优点是什么? + +1. 使用 Virtual DOM 提高应用程序的性能。 +2. JSX 使代码易于读写。 +3. 它支持在客户端和服务端渲染。 +4. 易于与框架(Angular,Backbone)集成,因为它只是一个视图库。 +5. 使用 Jest 等工具轻松编写单元与集成测试。 + +## React 优势 + +1. React 速度很快:它并不直接对 DOM 进行操作,引入了一个叫做虚拟 DOM 的概念,安插在 javascript 逻辑和实际的 DOM 之间,性能好。 + +2. 跨浏览器兼容:虚拟 DOM 帮助我们解决了跨浏览器问题,它为我们提供了标准化的 API,甚至在 IE8 中都是没问题的。 + +3. 一切都是 component:代码更加模块化,重用代码更容易,可维护性高。 + +4. 单向数据流:Flux 是一个用于在 JavaScript 应用中创建单向数据层的架构,它随着 React 视图库的开发而被 Facebook 概念化。 + +5. 同构、纯粹的 javascript:因为搜索引擎的爬虫程序依赖的是服务端响应而不是 JavaScript 的执行,预渲染你的应用有助于搜索引擎优化。 + +6. 兼容性好:比如使用 RequireJS 来加载和打包,而 Browserify 和 Webpack 适用于构建大型应用。它们使得那些艰难的任务不再让人望而生畏。 + +## React 的局限性是什么? + +1. React 只是一个视图库,而不是一个完整的框架。 +2. 对于 Web 开发初学者来说,有一个学习曲线。 +3. 将 React 集成到传统的 MVC 框架中需要一些额外的配置。 +4. 代码复杂性随着内联模板和 JSX 的增加而增加。 +5. 如果有太多的小组件可能增加项目的庞大和复杂。 + +## react性能优化方案 + +- 重写`shouldComponentUpdate`来避免不必要的dom操作 +- 使用 production 版本的react.js +- 使用key来帮助React识别列表中所有子组件的最小变化 + +## React 项目中有哪些细节可以优化?实际开发中都做过哪些性能优化 + +1)对于正常的项目优化,一般都涉及到几个方面,**开发过程中、上线之后的首屏、运行过程的状态** + +- 来聊聊上线之后的首屏及运行状态: + + - 首屏优化一般涉及到几个指标FP、FCP、FMP;要有一个良好的体验是尽可能的把FCP提前,需要做一些工程化的处理,去优化资源的加载 + + - 方式及分包策略,资源的减少是最有效的加快首屏打开的方式; + + - 对于CSR的应用,FCP的过程一般是首先加载js与css资源,js在本地执行完成,然后加载数据回来,做内容初始化渲染,这中间就有几次的网络反复请求的过程;所以CSR可以考虑使用骨架屏及预渲染(部分结构预渲染)、suspence与lazy做懒加载动态组件的方式 + + - 当然还有另外一种方式就是SSR的方式,SSR对于首屏的优化有一定的优势,但是这种瓶颈一般在Node服务端的处理,建议使用stream流的方式来处理,对于体验与node端的内存管理等,都有优势; + + - 不管对于CSR或者SSR,都建议配合使用Service worker,来控制资源的调配及骨架屏秒开的体验 + + - react项目上线之后,首先需要保障的是可用性,所以可以通过React.Profiler分析组件的渲染次数及耗时的一些任务,但是Profile记录的是commit阶段的数据,所以对于react的调和阶段就需要结合performance API一起分析; + + - 由于React是父级props改变之后,所有与props不相关子组件在没有添加条件控制的情况之下,也会触发render渲染,这是没有必要的,可以结合React的PureComponent以及React.memo等做浅比较处理,这中间有涉及到不可变数据的处理,当然也可以结合使用ShouldComponentUpdate做深比较处理; + + - 所有的运行状态优化,都是减少不必要的render,React.useMemo与React.useCallback也是可以做很多优化的地方; + + - 在很多应用中,都会涉及到使用redux以及使用context,这两个都可能造成许多不必要的render,所以在使用的时候,也需要谨慎的处理一些数据; + + - 最后就是保证整个应用的可用性,为组件创建错误边界,可以使用componentDidCatch来处理; + +- 实际项目中开发过程中还有很多其他的优化点: + +1. 保证数据的不可变性 +2. 使用唯一的键值迭代 +3. 使用web worker做密集型的任务处理 +4. 不在render中处理数据 +5. 不必要的标签,使用React.Fragments + +## 什么是无状态组件? + +如果行为独立于其状态,则它可以是无状态组件。你可以使用函数或类来创建无状态组件。但除非你需要在组件中使用生命周期钩子,否则你应该选择函数组件。无状态组件有很多好处: 它们易于编写,理解和测试,速度更快,而且你可以完全避免使用this关键字。 + +## 什么是有状态组件? + +如果组件的行为依赖于组件的state,那么它可以被称为有状态组件。这些有状态组件总是类组件,并且具有在constructor中初始化的状态。 + +## 为什么使用 Fragments 比使用容器 div 更好? + +1. 通过不创建额外的 DOM 节点,Fragments 更快并且使用更少的内存。这在非常大而深的节点树时很有好处。 +2. 一些 CSS 机制如Flexbox和CSS Grid具有特殊的父子关系,如果在中间添加 div 将使得很难保持所需的结构。 +3. 在 DOM 审查器中不会那么的杂乱。 + + +## 什么是高阶组件(HOC)? + +高阶组件(HOC) 就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件,它只是一种模式,这种模式是由react自身的组合性质必然产生的。 + +我们将它们称为纯组件,因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件中的任何行为。 + +``` jsx +const EnhancedComponent = higherOrderComponent(WrappedComponent) +``` + +HOC 有很多用例: + +1. 代码复用,逻辑抽象化 +2. 渲染劫持 +3. 抽象化和操作状态(state) +4. 操作属性(props) + +## 在组件类中方法的推荐顺序是什么? + +从 mounting 到 render stage 阶段推荐的方法顺序: + +1. static 方法 +2. constructor() +3. getChildContext() +4. componentWillMount() +5. componentDidMount() +6. componentWillReceiveProps() +7. shouldComponentUpdate() +8. componentWillUpdate() +9. componentDidUpdate() +10. componentWillUnmount() +11. 点击处理程序或事件处理程序,如 onClickSubmit() 或 onChangeDescription() +12. 用于渲染的getter方法,如 getSelectReason() 或 getFooterContent() +13. 可选的渲染方法,如 renderNavigation() 或 renderProfilePicture() +14. render() + +## 生命周期方法 `getSnapshotBeforeUpdate()` 的目的是什么? + +新的 `getSnapshotBeforeUpdate()` 生命周期方法在 DOM 更新之前被调用。此方法的返回值将作为第三个参数传递给componentDidUpdate()。 + +此生命周期方法与 `componentDidUpdate()` 一起涵盖了 `componentWillUpdate()` 的所有用例。 + +## 生命周期方法 `getDerivedStateFromProps()` 的目的是什么? +新的静态 `getDerivedStateFromProps()` 生命周期方法在实例化组件之后以及重新渲染组件之前调用。它可以返回一个对象用于更新状态,或者返回 null 指示新的属性不需要任何状态更新。 + +此生命周期方法与 `componentDidUpdate()` 一起涵盖了 `componentWillReceiveProps()` 的所有用例。 + +## 在 React v16 中,哪些生命周期方法将被弃用? + +以下生命周期方法将成为不安全的编码实践,并且在异步渲染方面会更有问题。 + +1. componentWillMount() +2. componentWillReceiveProps() +3. componentWillUpdate() + +从 React v16.3 开始,这些方法使用 UNSAFE_ 前缀作为别名,未加前缀的版本将在 React v17 中被移除。 + +## React 生命周期方法有哪些? + +React 16.3+ + +- **getDerivedStateFromProps**: 在调用render()之前调用,并在 每次 渲染时调用。 需要使用派生状态的情况是很罕见得。值得阅读 如果你需要派生状态. +- **componentDidMount**: 首次渲染后调用,所有得 Ajax 请求、DOM 或状态更新、设置事件监听器都应该在此处发生。 +- **shouldComponentUpdate**: 确定组件是否应该更新。 默认情况下,它返回true。 如果你确定在更新状态或属性后不需要渲染组件,则可以返回false值。 它是一个提高性能的好地方,因为它允许你在组件接收新属性时阻止重新渲染。 +- **getSnapshotBeforeUpdate**: 在最新的渲染输出提交给 DOM 前将会立即调用,这对于从 DOM 捕获信息(比如:滚动位置)很有用。 +- **componentDidUpdate**: 它主要用于更新 DOM 以响应 prop 或 state 更改。 如果shouldComponentUpdate()返回false,则不会触发。 +- **componentWillUnmount**: 当一个组件被从 DOM 中移除时,该方法被调用,取消网络请求或者移除与该组件相关的事件监听程序等应该在这里进行。 + +Before 16.3 + +- **componentWillMount**: 在组件render()前执行,用于根组件中的应用程序级别配置。应该避免在该方法中引入任何的副作用或订阅。 +- **componentDidMount**: 首次渲染后调用,所有得 Ajax 请求、DOM 或状态更新、设置事件监听器都应该在此处发生。 +- **componentWillReceiveProps**: 在组件接收到新属性前调用,若你需要更新状态响应属性改变(例如,重置它),你可能需对比this.props和nextProps并在该方法中使用this.setState()处理状态改变。 +- **shouldComponentUpdate**: 确定组件是否应该更新。 默认情况下,它返回true。 如果你确定在更新状态或属性后不需要渲染组件,则可以返回false值。 它是一个提高性能的好地方,因为它允许你在组件接收新属性时阻止重新渲染。 +- **componentWillUpdate**: 当shouldComponentUpdate返回true后重新渲染组件之前执行,注意你不能在这调用this.setState() +- **componentDidUpdate**: 它主要用于更新 DOM 以响应 prop 或 state 更改。 如果shouldComponentUpdate()返回false,则不会触发。 +- **componentWillUnmount**: 当一个组件被从 DOM 中移除时,该方法被调用,取消网络请求或者移除与该组件相关的事件监听程序等应该在这里进行。 + + +## 组件生命周期的不同阶段是什么? + +组件生命周期有三个不同的生命周期阶段: + +1. **Mounting:** 组件已准备好挂载到浏览器的 DOM 中. 此阶段包含来自 constructor(), getDerivedStateFromProps(), render(), 和 componentDidMount() 生命周期方法中的初始化过程。 + +2. **Updating:** 在此阶段,组件以两种方式更新,发送新的属性并使用 setState() 或 forceUpdate() 方法更新状态. 此阶段包含 getDerivedStateFromProps(), shouldComponentUpdate(), render(), getSnapshotBeforeUpdate() 和 componentDidUpdate() 生命周期方法。 + +3. **Unmounting:** 在这个最后阶段,不需要组件,它将从浏览器 DOM 中卸载。这个阶段包含 componentWillUnmount() 生命周期方法。 + +值得一提的是,在将更改应用到 DOM 时,React 内部也有阶段概念。它们按如下方式分隔开: + +1. **Render** 组件将会进行无副作用渲染。这适用于纯组件(Pure Component),在此阶段,React 可以暂停,中止或重新渲染。 + +2. **Pre-commit** 在组件实际将更改应用于 DOM 之前,有一个时刻允许 React 通过getSnapshotBeforeUpdate()捕获一些 DOM 信息(例如滚动位置)。 + +3. **Commit** React 操作 DOM 并分别执行最后的生命周期: componentDidMount() 在 DOM 渲染完成后调用, componentDidUpdate() 在组件更新时调用, componentWillUnmount() 在组件卸载时调用。 React 16.3+ 阶段 (也可以看交互式版本) + +## React 组件通信方式 + +react组件间通信常见的几种情况: + +1. 父组件向子组件通信 +2. 子组件向父组件通信 +3. 跨级组件通信 +4. 非嵌套关系的组件通信 + +### 1)父组件向子组件通信 + +父组件通过 props 向子组件传递需要的信息。 + +### 2)子组件向父组件通信 + +props+回调的方式。 + +### 3)跨级组件通信 + +即父组件向子组件的子组件通信,向更深层子组件通信。 + +- 使用props,利用中间组件层层传递,但是如果父组件结构较深,那么中间每一层组件都要去传递props,增加了复杂度,并且这些props并不是中间组件自己需要的。 +- 使用context,context相当于一个大容器,我们可以把要通信的内容放在这个容器中,这样不管嵌套多深,都可以随意取用,对于跨越多层的全局数据可以使用context实现。 + +### 4)非嵌套关系的组件通信 + +即没有任何包含关系的组件,包括兄弟组件以及不在同一个父级中的非兄弟组件。 + +1. 可以使用自定义事件通信(发布订阅模式) +2. 可以通过redux等进行全局状态管理 +3. 如果是兄弟组件通信,可以找到这两个兄弟节点共同的父节点, 结合父子间通信方式进行通信。 + +## 什么是 Flux? + +Flux 是应用程序设计范例,用于替代更传统的 MVC 模式。它不是一个框架或库,而是一种新的体系结构,它补充了 React 和单向数据流的概念。在使用 React 时,Facebook 会在内部使用此模式。 + +## 简述 flux 思想 + +Flux 的最大特点,就是数据的"单向流动"。 + +1. 用户访问 View +2. View 发出用户的 Action +3. Dispatcher 收到 Action,要求 Store 进行相应的更新 +4. Store 更新后,发出一个"change"事件 +5. View 收到"change"事件后,更新页面 + +## 了解 redux 么,说一下 redux 吧 + +Redux 是基于 Flux设计模式 的 JavaScript 应用程序的可预测状态容器。Redux 可以与 React 一起使用,也可以与任何其他视图库一起使用。它很小(约2kB)并且没有依赖性。 + +### 1、为什么要用redux + +在React中,数据在组件中是单向流动的,数据从一个方向父组件流向子组件(通过props), 所以,两个非父子组件之间通信就相对麻烦,redux的出现就是为了解决state里面的数据问题 + +### 2、Redux设计理念 + +Redux是将整个应用状态存储到一个地方上称为store, 里面保存着一个状态树store tree, 组件可以派发(dispatch)行为(action)给store, 而不是直接通知其他组件,组件内部通过订阅store中的状态state来刷新自己的视图。 + +redux工作流 + +### 3、Redux三大原则 + +1. 唯一数据源 +整个应用的state都被存储到一个状态树里面,并且这个状态树,只存在于唯一的store中 + +2. 保持只读状态 +state是只读的,唯一改变state的方法就是触发action,action是一个用于描述以发生时间的普通对象 + +3. 数据改变只能通过纯函数来执行 +使用纯函数来执行修改,为了描述action如何改变state的,你需要编写reducers + +### 4、Redux概念解析 + +1. Store + +- store就是保存数据的地方,你可以把它看成一个数据,整个应用只能有一个store +- Redux提供createStore这个函数,用来生成Store + +``` js +import { + createStore +} from 'redux' +const store = createStore(fn); +``` + +2. State + +state就是store里面存储的数据,store里面可以拥有多个state,Redux规定一个state对应一个View, 只要state相同,view就是一样的,反过来也是一样的,可以通过store.getState( )获取 + +``` js +import { + createStore +} from 'redux' +const store = createStore(fn); +const state = store.getState(); +``` + +3. Action + +state的改变会导致View的变化,但是在redux中不能直接操作state也就是说不能使用this. setState来操作,用户只能接触到View。在Redux中提供了一个对象来告诉Store需要改变state。Action是一个对象其中type属性是必须的,表示Action的名称,其他的可以根据需求自由设置。 + +``` js +const action = { + type: 'ADD_TODO', + payload: 'redux原理' +} +``` + +在上面代码中,Action的名称是ADD_TODO,携带的数据是字符串‘redux原理’,Action描述当前发生的事情,这是改变state的唯一的方式 + +4. store.dispatch() +store.dispatch() // 是view发出Action的唯一办法 + +``` js +store.dispatch({ + type: 'ADD_TODO', + payload: 'redux原理' +}) +``` + +store.dispatch接收一个Action作为参数,将它发送给store通知store来改变state。 + +5. Reducer + +Store收到Action以后,必须给出一个新的state,这样view才会发生变化。这种state的计算过程就叫做Reducer。 Reducer是一个纯函数,他接收Action和当前state作为参数,返回一个新的state + +注意:Reducer必须是一个纯函数,也就是说函数返回的结果必须由参数state和action决定,而且不产生任何副作用也不能修改state和action对象 + +``` js +const reducer = (state, action) => { + switch (action.type) { + case ADD_TODO: + return newstate; + default + return state + } +} +``` + +### 5、Redux源码 + +``` js +let createStore = (reducer) => { + let state; + //获取状态对象 + //存放所有的监听函数 + let listeners = []; + let getState = () => state; + //提供一个方法供外部调用派发action + let dispath = (action) => { + //调用管理员reducer得到新的state + state = reducer(state, action); + //执行所有的监听函数 + listeners.forEach((l) => l()) + } + //订阅状态变化事件,当状态改变发生之后执行监听函数 + let subscribe = (listener) => { + listeners.push(listener); + } + dispath(); + return { + getState, + dispath, + subscribe + } +} +let combineReducers = (renducers) => { + //传入一个renducers管理组,返回的是一个renducer + return function(state = {}, action = {}) { + let newState = {}; + for (var attr in renducers) { + newState[attr] = renducers[attr](state[attr], action) + + } + return newState; + } +} +export { + createStore, + combineReducers +}; +``` + +## Redux 的核心原则是什么? + +Redux 遵循三个基本原则: + +1. **单一数据来源**: 整个应用程序的状态存储在单个对象树中。单状态树可以更容易地跟踪随时间的变化并调试或检查应用程序。 +2. **状态是只读的**: 改变状态的唯一方法是发出一个动作,一个描述发生的事情的对象。这可以确保视图和网络请求都不会直接写入状态。 +3. **使用纯函数进行更改**: 要指定状态树如何通过操作进行转换,您可以编写reducers。Reducers 只是纯函数,它将先前的状态和操作作为参数,并返回下一个状态。 + +## redux中间件 + +> 中间件提供第三方插件的模式,自定义拦截 action -> reducer 的过程。变为 action -> middlewares -> reducer 。这种机制可以让我们改变数据流,实现如异步 action ,action 过滤,日志输出,异常报告等功能 + +- `redux-logger`:提供日志输出 +- `redux-thunk`:处理异步操作 +- `redux-promise`:处理异步操作,`actionCreator`的返回值是`promise` + +## 我需要将所有状态保存到 Redux 中吗?我应该使用 react 的内部状态吗? + +这取决于开发者的决定。即开发人员的工作是确定应用程序的哪种状态,以及每个状态应该存在的位置,有些用户喜欢将每一个数据保存在 Redux 中,以维护其应用程序的完全可序列化和受控。其他人更喜欢在组件的内部状态内保持非关键或UI状态,例如“此下拉列表当前是否打开”。 + +以下是确定应将哪种数据放入Redux的主要规则: + +1. 应用程序的其他部分是否关心此数据? +2. 您是否需要能够基于此原始数据创建更多派生数据? +3. 是否使用相同的数据来驱动多个组件? +4. 能够将此状态恢复到给定时间点(即时间旅行调试)是否对您有价值? +5. 您是否要缓存数据(即,如果已经存在,则使用处于状态的状态而不是重新请求它)? + +## 与 Flux 相比,Redux 的缺点是什么? + +我们应该说使用 Redux 而不是 Flux 几乎没有任何缺点。这些如下: + +1. **您将需要学会避免突变**: Flux 对变异数据毫不吝啬,但 Redux 不喜欢突变,许多与 Redux 互补的包假设您从不改变状态。您可以使用 dev-only 软件包强制执行此操作,例如redux-immutable-state-invariant,Immutable.js,或指示您的团队编写非变异代码。 +2. **您将不得不仔细选择您的软件包**: 虽然 Flux 明确没有尝试解决诸如撤消/重做,持久性或表单之类的问题,但 Redux 有扩展点,例如中间件和存储增强器,以及它催生了丰富的生态系统。 +3. **还没有很好的 Flow 集成**: Flux 目前可以让你做一些非常令人印象深刻的静态类型检查,Redux 还不支持。 + +## Relay 与 Redux 有何不同? + +Relay 与 Redux 类似,因为它们都使用单个 Store。主要区别在于 relay 仅管理源自服务器的状态,并且通过GraphQL查询(用于读取数据)和突变(用于更改数据)来使用对状态的所有访问。Relay 通过仅提取已更改的数据而为您缓存数据并优化数据提取。 + +## 如何向 Redux 添加多个中间件? + +你可以使用`applyMiddleware()`。 + +例如,你可以添加`redux-thunk`和`logger`作为参数传递给`applyMiddleware()`: + +``` js +import { createStore, applyMiddleware } from 'redux' +const createStoreWithMiddleware = applyMiddleware(ReduxThunk, logger)(createStore) +``` + +## 什么是 Redux Form? + +Redux Form与 React 和 Redux 一起使用,以使 React 中的表单能够使用 Redux 来存储其所有状态。Redux Form 可以与原始 HTML5 输入一起使用,但它也适用于常见的 UI 框架,如 Material UI,React Widgets和React Bootstrap。 + +## 什么是 Redux Thunk? + +Redux Thunk中间件允许您编写返回函数而不是 Action 的创建者。 thunk 可用于延迟 Action 的发送,或仅在满足某个条件时发送。内部函数接收 Store 的方法dispatch()和getState()作为参数。 + + +## 什么是 redux-saga? + +`redux-saga`是一个库,旨在使 React/Redux 项目中的副作用(数据获取等异步操作和访问浏览器缓存等可能产生副作用的动作)更容易,更好。 + +这个包在 NPM 上有发布: + +``` shell +$ npm install --save redux-saga +``` + +## 在 redux-saga 中 `call()` 和 `put()` 之间有什么区别? + +call()和put()都是 Effect 创建函数。 call()函数用于创建 Effect 描述,指示中间件调用 promise。put()函数创建一个 Effect,指示中间件将一个 Action 分派给 Store。 + +让我们举例说明这些 Effect 如何用于获取特定用户数据。 + +``` js +function* fetchUserSaga(action) { + // `call` function accepts rest arguments, which will be passed to `api.fetchUser` function. + // Instructing middleware to call promise, it resolved value will be assigned to `userData` variable + const userData = yield call(api.fetchUser, action.userId) + + // Instructing middleware to dispatch corresponding action. + yield put({ + type: 'FETCH_USER_SUCCESS', + userData + }) +} +``` + +## `redux-saga` 和 `redux-thunk` 之间有什么区别? + +Redux Thunk和Redux Saga都负责处理副作用。在大多数场景中,Thunk 使用Promises来处理它们,而 Saga 使用Generators。Thunk 易于使用,因为许多开发人员都熟悉 Promise,Sagas/Generators 功能更强大,但您需要学习它们。但是这两个中间件可以共存,所以你可以从 Thunks 开始,并在需要时引入 Sagas。 + + +## redux-saga 和 mobx 的比较 + +### 1)状态管理 + +- redux-sage 是 redux 的一个异步处理的中间件。 +- mobx 是数据管理库,和 redux 一样。 + +### 2)设计思想 + +- redux-sage 属于 flux 体系, 函数式编程思想。 +- mobx 不属于 flux 体系,面向对象编程和响应式编程。 + +### 3)主要特点 + +- redux-sage 因为是中间件,更关注异步处理的,通过 Generator 函数来将异步变为同步,使代码可读性高,结构清晰。action 也不是 action creator 而是 pure action, +- 在 Generator 函数中通过 call 或者 put 方法直接声明式调用,并自带一些方法,如 takeEvery,takeLast,race等,控制多个异步操作,让多个异步更简单。 +- mobx 是更简单更方便更灵活的处理数据。 Store 是包含了 state 和 action。state 包装成一个可被观察的对象, action 可以直接修改 state,之后通过 Computed values 将依赖 state 的计算属性更新 ,之后触发 Reactions 响应依赖 state 的变更,输出相应的副作用 ,但不生成新的 state。 + +### 4)数据可变性 + +- redux-sage 强调 state 不可变,不能直接操作 state,通过 action 和 reducer 在原来的 state 的基础上返回一个新的 state 达到改变 state 的目的。 +- mobx 直接在方法中更改 state,同时所有使用的 state 都发生变化,不生成新的 state。 + +### 5)写法难易度 + +- redux-sage 比 redux 在 action 和 reducer 上要简单一些。需要用 dispatch 触发 state 的改变,需要 mapStateToProps 订阅 state。 +- mobx 在非严格模式下不用 action 和 reducer,在严格模式下需要在 action 中修改 state,并且自动触发相关依赖的更新。 + +### 6)使用场景 + +- redux-sage 很好的解决了 redux 关于异步处理时的复杂度和代码冗余的问题,数据流向比较好追踪。但是 redux 的学习成本比 较高,代码比较冗余,不是特别需要状态管理,最好用别的方式代替。 +- mobx 学习成本低,能快速上手,代码比较简洁。但是可能因为代码编写的原因和数据更新时相对黑盒,导致数据流向不利于追踪。 + +## 什么是 Redux DevTools? + +Redux DevTools是 Redux 的实时编辑的时间旅行环境,具有热重新加载,Action 重放和可自定义的 UI。如果您不想安装 Redux DevTools 并将其集成到项目中,请考虑使用 Chrome 和 Firefox 的扩展插件。 + + + +## React 和 Angular 有什么区别? + +| React | Angular | +| ---- | ---- | +| React 是一个库,只有View层 | Angular是一个框架,具有完整的 MVC 功能 | +| React 可以处理服务器端的渲染 | AngularJS 仅在客户端呈现,但 Angular 2 及更高版本可以在服务器端渲染 | +| React 在 JS 中使用看起来像 HTML 的 JSX,这可能令人困惑 | Angular 遵循 HTML 的模板方法,这使得代码更短且易于理解 | +| React Native 是一种 React 类型,它用于构建移动应用程序,它更快,更稳定 | Ionic,Angular 的移动 app 相对原生 app 来说不太稳定和慢 | +| 在 React中,数据只以单一方向传递,因此调试很容易 | 在 Angular 中,数据以两种方式传递,即它在子节点和父节点之间具有双向数据绑定,因此调试通常很困难 | + +## 与 Vue.js 相比,React 有哪些优势? + +与 Vue.js 相比,React 具有以下优势: + +1. 在大型应用程序开发中提供更大的灵活性。 +2. 更容易测试。 +3. 更适合创建移动端应用程序。 +4. 提供更多的信息和解决方案。 + +## 比较一下React与Vue + +相同点 +1. 都有组件化开发和Virtual DOM +2. 都支持props进行父子组件间数据通信 +3. 都支持数据驱动视图, 不直接操作真实DOM, 更新状态数据界面就自动更新 +4. 都支持服务器端渲染 +5. 都有支持native的方案,React的React Native,Vue的Weex + +不同点 +1. 数据绑定: vue实现了数据的双向绑定,react数据流动是单向的 +2. 组件写法不一样, React推荐的做法是 JSX , 也就是把HTML和CSS全都写进JavaScript了,即'all in js'; Vue推荐的做法是webpack+vue-loader的单文件组件格式,即html,css,js写在同一个文件 +3. state对象在react应用中不可变的,需要使用setState方法更新状态;在vue中,state对象不是必须的,数据由data属性在vue对象中管理 +4. virtual DOM不一样,vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。而对于React而言,每当应用的状态被改变时,全部组件都会重新渲染,所以react中会需要shouldComponentUpdate这个生命周期函数方法来进行控制 +5. React严格上只针对MVC的view层,Vue则是MVVM模式 + +## Vue与React Virtual DOM对比 + +### 相同点 + +1. vue和react都采用了虚拟dom算法,以最小化更新真实DOM,从而减小不必要的性能损耗。 + +2. 按颗粒度分为tree diff, component diff, element diff。 tree diff 比较同层级dom节点,进行增、删、移操作。如果遇到component元素, 就会重新tree diff流程。 + +### 不同点 + +#### dom的更新策略不同 + +react 会自顶向下全diff。 + +vue 会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。 + +1. 在react中,当状态发生改变时,组件树就会自顶向下的全diff, 重新render页面, 重新生成新的虚拟dom tree, 新旧dom tree进行比较, 进行patch打补丁方式,局部跟新dom. 所以react为了避免父组件跟新而引起不必要的子组件更新, 可以在shouldComponentUpdate做逻辑判断,减少没必要的render, 以及重新生成虚拟dom,做差量对比过程。 + +2. 在 vue中, 通过Object.defineProperty 把这些 data 属性 全部转为 getter/setter。同时watcher实例对象会在组件渲染时,将属性记录为dep, 当dep 项中的 setter被调用时,通知watch重新计算,使得关联组件更新。 + +Diff 算法借助元素的 Key 判断元素是新增、删除、修改,从而减少不必要的元素重渲染。 + +### 建议 + +1. 基于tree diff + + - 开发组件时,注意保持DOM结构的稳定;即尽可能少地动态操作DOM结构,尤其是移动操作。 + - 当节点数过大或者页面更新次数过多时,页面卡顿的现象会比较明显。这时可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。 + +2. 基于component diff + + - 注意使用 shouldComponentUpdate() 来减少组件不必要的更新。 + - 对于类似的结构应该尽量封装成组件,既减少代码量,又能减少component diff的性能消耗。 + +3. 基于element diff: + + - 对于列表结构,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响渲染性能。 + - 循环渲染的必须加上key值,唯一标识节点。 + + +## 什么是mvvm? + +> MVVM是Model-View-ViewModel的缩写。mvvm是一种设计思想。Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和 Model的对象 + +- 在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。 +- ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理 + +## MVC、MVP 与 MVVM 模式 + +### 一、MVC + +通信方式如下 + +视图(View):用户界面。 传送指令到 Controller + +控制器(Controller):业务逻辑 完成业务逻辑后,要求 Model 改变状态 + +模型(Model):数据保存 将新的数据发送到 View,用户得到反馈 + +### 二、MVP + +通信方式如下 + +各部分之间的通信,都是双向的。 + +View 与 Model 不发生联系,都通过 Presenter 传递。 + +View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter 非常厚,所有逻辑都部署在那里。 + +### 三、MVVM + +MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。通信方式如下 + +唯一的区别是,它采用双向绑定(data-binding):View 的变动,自动反映在 ViewModel,反之亦然。 + + +## MVVM + +### **MVVM 由以下三个内容组成** + +- `View`:界面 +- `Model`:数据模型 +- `ViewModel`:作为桥梁负责沟通 `View` 和 `Model` + +> - 在 JQuery 时期,如果需要刷新 UI 时,需要先取到对应的 DOM 再更新 UI,这样数据和业务的逻辑就和页面有强耦合 +> - 在 MVVM 中,UI 是通过数据驱动的,数据一旦改变就会相应的刷新对应的 UI,UI 如果改变,也会改变对应的数据。这种方式就可以在业务处理中只关心数据的流转,而无需直接和页面打交道。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel + +- 在 MVVM 中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检测,Vue 中的数据劫持 + +### **脏数据检测** + +- 当触发了指定事件后会进入脏数据检测,这时会调用 $digest 循环遍历所有的数据观察者,判断当前值是否和先前的值有区别,如果检测到变化的话,会调用 $watch 函数,然后再次调用 $digest 循环直到发现没有变化。循环至少为二次 ,至多为十次 +- 脏数据检测虽然存在低效的问题,但是不关心数据是通过什么方式改变的。并且脏数据检测可以实现批量检测出更新的值,再去统一更新 UI,大大减少了操作 DOM 的次数 + +### **数据劫持** + +- `Vue` 内部使用了 `Obeject.defineProperty()` 来实现双向绑定,通过这个函数可以监听到 `set` 和 `get `的事件 + +```javascript +var data = { name: 'yck' } +observe(data) +let name = data.name // -> get value +data.name = 'yyy' // -> change value + +function observe(obj) { + // 判断类型 + if (!obj || typeof obj !== 'object') { + return + } + Object.keys(data).forEach(key => { + defineReactive(data, key, data[key]) + }) +} + +function defineReactive(obj, key, val) { + // 递归子属性 + observe(val) + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter() { + console.log('get value') + return val + }, + set: function reactiveSetter(newVal) { + console.log('change value') + val = newVal + } + }) +} +``` + +> 以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的,还需要在适当的时候给属性添加发布订阅 + +```html +
+ {{name}} +
+``` + +> 在解析如上模板代码时,遇到 `{{name}}` 就会给属性 `name` 添加发布订阅 + + +```javascript +// 通过 Dep 解耦 +class Dep { + constructor() { + this.subs = [] + } + addSub(sub) { + // sub 是 Watcher 实例 + this.subs.push(sub) + } + notify() { + this.subs.forEach(sub => { + sub.update() + }) + } +} +// 全局属性,通过该属性配置 Watcher +Dep.target = null + +function update(value) { + document.querySelector('div').innerText = value +} + +class Watcher { + constructor(obj, key, cb) { + // 将 Dep.target 指向自己 + // 然后触发属性的 getter 添加监听 + // 最后将 Dep.target 置空 + Dep.target = this + this.cb = cb + this.obj = obj + this.key = key + this.value = obj[key] + Dep.target = null + } + update() { + // 获得新值 + this.value = this.obj[this.key] + // 调用 update 方法更新 Dom + this.cb(this.value) + } +} +var data = { name: 'yck' } +observe(data) +// 模拟解析到 `{{name}}` 触发的操作 +new Watcher(data, 'name', update) +// update Dom innerText +data.name = 'yyy' +``` + +> 接下来,对 defineReactive 函数进行改造 + +```javascript +function observe(obj) { + // 判断类型 + if (!obj || typeof obj !== 'object') { + return + } + Object.keys(data).forEach(key => { + defineReactive(data, key, data[key]) + }) +} + +function defineReactive(obj, key, val) { + // 递归子属性 + observe(val) + let dp = new Dep() + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter() { + console.log('get value') + // 将 Watcher 添加到订阅 + if (Dep.target) { + dp.addSub(Dep.target) + } + return val + }, + set: function reactiveSetter(newVal) { + console.log('change value') + val = newVal + // 执行 watcher 的 update 方法 + dp.notify() + } + }) +} +``` + +> 以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加 + +### **Proxy 与 Obeject.defineProperty 对比** + +- `Obeject.defineProperty` 虽然已经能够实现双向绑定了,但是他还是有缺陷的。 + - 只能对属性进行数据劫持,所以需要深度遍历整个对象 + - 对于数组不能监听到数据的变化 + +> 虽然 `Vue` 中确实能检测到数组数据的变化,但是其实是使用了 `hack` 的办法,并且也是有缺陷的 + +## vue的优点是什么? + +- 低耦合。视图(View)可以独立于 Model 变化和修改,一个 ViewModel 可以绑定到不同的"View"上,当 View 变化的时候 Model 可以不变,当 Model 变化的时候 View 也可以不变。 + +- 可重用性。你可以把一些视图逻辑放在一个 ViewModel 里面,让很多 view 重用这段视图逻辑。 + +- 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。 + +- 可测试。界面素来是比较难于测试的,而现在测试可以针对 ViewModel 来写。 + +## 对于 Vue 是一套渐进式框架的理解 + +每个框架都不可避免会有自己的一些特点,从而会对使用者有一定的要求,这些要求就是主张,主张有强有弱,它的强势程度会影响在业务开发中的使用方式。 + +1、使用 vue,你可以在原有大系统的上面,把一两个组件改用它实现,当 jQuery 用; + +2、也可以整个用它全家桶开发,当 Angular 用; + +3、还可以用它的视图,搭配你自己设计的整个下层用。你可以在底层数据逻辑的地方用 OO(Object–Oriented )面向对象和设计模式的那套理念。 也可以函数式,都可以。 + +它只是个轻量视图而已,只做了自己该做的事,没有做不该做的事,仅此而已。 + +你不必一开始就用 Vue 所有的全家桶,根据场景,官方提供了方便的框架供你使用。 + +场景联想 + +- 场景 1: 维护一个老项目管理后台,日常就是提交各种表单了,这时候你可以把 vue 当成一个 js 库来使用,就用来收集 form 表单,和表单验证。 + +- 场景 2: 得到 boss 认可, 后面整个页面的 dom 用 Vue 来管理,抽组件,列表用 v-for 来循环,用数据驱动 DOM 的变化 + +- 场景 3: 越来越受大家信赖,领导又找你了,让你去做一个移动端 webapp,直接上了 vue 全家桶! + +场景 1-3 从最初的只因多看你一眼而用了前端 js 库,一直到最后的大型项目解决方案。 + +## Vue2.0 中,“渐进式框架”和“自底向上增量开发的设计”这两个概念是什么? + +在我看来,渐进式代表的含义是:主张最少。 + +每个框架都不可避免会有自己的一些特点,从而会对使用者有一定的要求,这些要求就是主张,主张有强有弱,它的强势程度会影响在业务开发中的使用方式。 + +比如说,Angular,它两个版本都是强主张的,如果你用它,必须接受以下东西: + +- 必须使用它的模块机制 +- 必须使用它的依赖注入 +- 必须使用它的特殊形式定义组件(这一点每个视图框架都有,难以避免) + +所以Angular是带有比较强的排它性的,如果你的应用不是从头开始,而是要不断考虑是否跟其他东西集成,这些主张会带来一些困扰。 + +比如React,它也有一定程度的主张,它的主张主要是函数式编程的理念,比如说,你需要知道什么是副作用,什么是纯函数,如何隔离副作用。它的侵入性看似没有Angular那么强,主要因为它是软性侵入。 + +你当然可以只用React的视图层,但几乎没有人这么用,为什么呢,因为你用了它,就会觉得其他东西都很别扭,于是你要引入Flux,Redux,Mobx之中的一个,于是你除了Redux,还要看saga,于是你要纠结业务开发过程中每个东西有没有副作用,纯不纯,甚至你连这个都可能不能忍: + +``` js +const getData = () => { + // 如果不存在,就在缓存中创建一个并返回 + // 如果存在,就从缓存中拿 +} +``` + +因为你要纠结它有外部依赖,同样是不加参数调用,连续两次的结果是不一样的,于是不纯。 + +为什么我一直不认同在中后台项目中使用React,原因就在这里,我反对的是整个业务应用的函数式倾向,很多人都是看到有很多好用的React组件,就会倾向于把它引入,然后,你知道怎么把自己的业务映射到函数式的那套理念上吗? + +函数式编程,无副作用,写出来的代码没有bug,这是真理没错,但是有两个问题需要考虑: + +1. JS本身,有太多特性与纯函数式的主张不适配,这一点,题叶能说得更多 +2. 业务系统里面的实体关系,如何组织业务逻辑,几十年来积累了无数的基于设计模式的场景经验,有太多的东西可以模仿,但是,没有人给你总结那么多如何把你的厚重业务映射到函数式理念的经验,这个地方很考验综合水平的,真的每个人都有能力去做这种映射吗? + +函数式编程无bug的根本就在于要把业务逻辑完全都依照这套理念搞好,你看看自己公司做中后台的员工,他们熟悉的是什么?是基于传统OO设计模式的这套东西,他们以为拿着你们给的组件库就得到了一切,但是可能还要被灌输函数式编程的一整套东西,而且又没人告诉他们在业务场景下,如何规划业务模型、组织代码,还要求快速开发,怎么能快起来? + +所以我真是心疼这些人,他们要的只是组件库,却不得不把业务逻辑的思考方式也作转换,这个事情没有一两年时间洗脑,根本洗不到能开发业务的程度。 + +没有好组件库的时候,大家痛点在视图层,有了基于React的组件化,把原先没那么痛的业务逻辑部分搞得也痛起来了,原先大家按照设计模式教的东西,照猫画虎还能继续开发了,学了一套新理念之后,都不知道怎么写代码了,怎么写都怀疑自己不对,可怕。 + +我宁可支持Angular也不支持React的原因也就在此,Angular至少在业务逻辑这块没有软主张,能够跟OO设计模式那套东西配合得很好。我面对过很多商务场景,都是前端很厚重的东西,不仅仅是管理控制台这种,这类东西里面,业务逻辑的占比要比视图大挺多的,如何组织这些东西,目前几个主流技术栈都没有解决方案,要靠业务架构师去摆平。 + +如果你的场景不是这么厚重的,只是简单管理控制台,那当我没说好了。 + +框架是不能解决业务问题的,只能作为工具,放在合适的人手里,合适的场景下。 + +现在我要说说为什么我这么支持Vue了,没什么,可能有些方面是不如React,不如Angular,但它是渐进的,没有强主张,你可以在原有大系统的上面,把一两个组件改用它实现,当jQuery用;也可以整个用它全家桶开发,当Angular用;还可以用它的视图,搭配你自己设计的整个下层用。你可以在底层数据逻辑的地方用OO和设计模式的那套理念,也可以函数式,都可以,它只是个轻量视图而已,只做了自己该做的事,没有做不该做的事,仅此而已。 + +渐进式的含义,我的理解是:没有多做职责之外的事。 + +## Vue computed 实现 + +- 建立与其他属性(如:data、 Store)的联系; +- 属性改变后,通知计算属性重新计算 + +> 实现时,主要如下 + +- 初始化 data, 使用 `Object.defineProperty` 把这些属性全部转为 `getter/setter`。 +- 初始化 `computed`, 遍历 `computed` 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。 +- `Object.defineProperty getter` 依赖收集。用于依赖发生变化时,触发属性重新计算。 +- 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集 + +## Vue complier 实现 + +- 模板解析这种事,本质是将数据转化为一段 html ,最开始出现在后端,经过各种处理吐给前端。随着各种 mv* 的兴起,模板解析交由前端处理。 +- 总的来说,Vue complier 是将 template 转化成一个 render 字符串。 + +> 可以简单理解成以下步骤: + +- parse 过程,将 template 利用正则转化成 AST 抽象语法树。 +- optimize 过程,标记静态节点,后 diff 过程跳过静态节点,提升性能。 +- generate 过程,生成 render 字符串 + +## 如何编译 template 模板? + +1. 首先第一步实例化一个vue项目 +2. 模板编译是在vue生命周期的mount阶段进行的 +3. 在mount阶段的时候执行了compile方法将template里面的内容转化成真正的html代码 +4. parse阶段是将html转化成 AST 抽象语法树,用来表示template代码的数据结构。在 Vue 中我把它理解为嵌套的、携带标签名、属性和父子关系的 JS 对象,以树来表现 DOM 结构。 + +``` js +html: "
texttext
" +// html转换成ast +ast: { + // 标签类型 + type: 1, + // 标签名 + tag: "div", + // 标签行内属性列表 + attrsList: [{name: "id", value: "test"}], + // 标签行内属性 + attrsMap: {id: "test"}, + // 标签关系 父亲 + parent: undefined, + // 字标签属性列表 + children: [{ + type: 3, + text: 'texttext' + } + ], + plain: true, + attrs: [{name: "id", value: "'test'"}] +} +``` +5. optimize 会对parse阶段生成的 AST 树进行静态资源优化(静态内容指的是和数据没有关系,不需要每次都刷新的内容) +6. generate 函数会将每一个 AST 节点生成一个render字符串方法,其实就是一个内部调用的方法等待后面的调用。 +``` vue + +// 最后输出 +// {render: "with(this){return _c('div',{attrs:{"id":"test"}},[[_v(_s(val))]),_v(" "),_m(0)])}"} +``` +7. 在complie过程结束之后会生成一个render字符串,接下来就是 new watcher这个时候会对绑定的数据执行监听,render 函数就是数据监听的回调所调用的,其结果便是重新生成 Vnode。当这个 render 函数字符串在第一次 mount、或者绑定的数据更新的时候,都会被调用,生成 Vnode。如果是数据的更新,那么 Vnode 会与数据改变之前的 Vnode 做 diff,对内容做改动之后,就会更新到我们真正的 DOM 上啦 + +## vue 中的性能优化 + +### 1)编码优化 + +- 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher +- v-if和v-for不能连用 +- 如果需要使用v-for给每项元素绑定事件时使用事件代理 +- SPA 页面采用keep-alive缓存组件 +- 在更多的情况下,使用v-if替代v-show +- key保证唯一 +- 使用路由懒加载、异步组件 +- 防抖、节流 +- 第三方模块按需导入 +- 长列表滚动到可视区域动态加载 +- 图片懒加载 + +### 2)用户体验优化 + +- 骨架屏 +- PWA(渐进式WEB应用) +- 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。 + +### 3)SEO优化 + +- 预渲染 +- 服务端渲染SSR + +### 4)打包优化 + +- 压缩代码; +- Tree Shaking/Scope Hoisting; +- 使用cdn加载第三方模块; +- 多线程打包happypack; +- splitChunks抽离公共文件; +- sourceMap优化; + +说明:优化是个大工程,会涉及很多方面 + +## Vue 的实例生命周期 + +1. beforeCreate 初始化实例后 数据观测和事件配置之前调用 + +2. created 实例创建完成后调用 + +3. beforeMount 挂载开始前被用 + +4. mounted el 被新建 vm. $el 替换并挂在到实例上之后调用 + +5. beforeUpdate 数据更新时调用 + +6. updated 数据更改导致的 DOM 重新渲染后调用 + +7. beforeDestory 实例被销毁前调用 + +8. destroyed 实例销毁后调用 + +Vue2 与Vue3的生命周期对比 + +| 变量 | 实例化(次数) | +| ---- | ---- | +| beforeCreate(组件创建之前) | setup(组件创建之前) | +| created(组件创建完成) | setup(组件创建完成) | +| beforeMount(组件挂载之前) | onBeforeMount(组件挂载之前) | +| mounted(组件挂载完成) | onMounted(组件挂载完成) | +| beforeUpdate(数据更新,虚拟DOM打补丁之前) | onBeforeUpdate(数据更新,虚拟DOM打补丁之前) | +| updated(数据更新,虚拟DOM渲染完成) | onUpdated(数据更新,虚拟DOM渲染完成) | +| beforeDestroy(组件销毁之前) | onBeforeUnmount(组件销毁之前) | +| destroyed(组件销毁之后) | onUnmounted(组件销毁之后) | + + +## Vue 的双向数据绑定的原理 + +VUE 实现双向数据绑定的原理就是利用了 Object. defineProperty() 这个方法重新定义了对象获取属性值(get)和设置属性值(set)的操作来实现的。 + +Vue3. 0 将用原生 Proxy 替换 Object. defineProperty + +## 为什么要替换 Object.defineProperty?(Proxy 相比于 defineProperty 的优势) + +1. 在 Vue 中,Object.defineProperty 无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 + +2. Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。 + +而要取代它的Proxy有以下两个优点: + +- 可以劫持整个对象,并返回一个新对象 +- 有13种劫持操作 + +既然Proxy能解决以上两个问题,而且Proxy作为es6的新属性在vue2.x之前就有了,为什么vue2.x不使用Proxy呢?一个很重要的原因就是: + +Proxy是es6提供的新特性,兼容性不好,最主要的是这个属性无法用polyfill来兼容 + +## 什么是 Proxy? + +### 含义: +Proxy 是 ES6 中新增的一个特性,翻译过来意思是"代理",用在这里表示由它来“代理”某些操作。 Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。 + +Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。 + +使用 Proxy 的核心优点是可以交由它来处理一些非核心逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)。 从而可以让对象只需关注于核心逻辑,达到关注点分离,降低对象复杂度等目的。 + +### 基本用法: + +``` js +let p = new Proxy(target, handler); +``` + +参数: + +- target 是用Proxy包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。 +- handler 是一个对象,其声明了代理target 的一些操作,其属性是当执行一个操作时定义代理的行为的函数。 +- p 是代理后的对象。当外界每次对 p 进行操作时,就会执行 handler 对象上的一些方法。Proxy共有13种劫持操作, + +handler代理的一些常用的方法有如下几个: +``` txt +get: 读取 +set: 修改 +has: 判断对象是否有该属性 +construct: 构造函数 +``` + +### 示例: + +下面就用Proxy来定义一个对象的get和set,作为一个基础demo + +``` js +let obj = {}; +let handler = { + get(target, property) { + console.log( `${property} 被读取` ); + return property in target ? target[property] : 3; + }, + set(target, property, value) { + console.log( `${property} 被设置为 ${value}` ); + target[property] = value; + } +} + +let p = new Proxy(obj, handler); +p.name = 'tom' //name 被设置为 tom +p.age; //age 被读取 3 +``` + +p 读取属性的值时,实际上执行的是 handler.get() :在控制台输出信息,并且读取被代理对象 obj 的属性。 + +p 设置属性值时,实际上执行的是 handler.set() :在控制台输出信息,并且设置被代理对象 obj 的属性的值。 + +以上介绍了Proxy基本用法,实际上这个属性还有许多内容,具体可参考[Proxy文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + +## 为什么避免 v-if 和 v-for 用在一起 + +当 Vue 处理指令时,v-for 比 v-if 具有更高的优先级,这意味着 v-if 将分别重复运行于每个 v-for 循环中。通过 v-if 移动到容器元素,不会再重复遍历列表中的每个值。取而代之的是,我们只检查它一次,且不会在 v-if 为否的时候运算 v-for。 + +## 组件的设计原则 + +1. 页面上每个独立的可视/可交互区域视为一个组件(比如页面的头部,尾部,可复用的区块) +2. 每个组件对应一个工程目录,组件所需要的各种资源在这个目录下就近维护(组件的就近维护思想体现了前端的工程化思想,为前端开发提供了很好的分治策略,在vue.js中,通过.vue文件将组件依赖的模板,js,样式写在一个文件中) +(每个开发者清楚开发维护的功能单元,它的代码必然存在在对应的组件目录中,在该目录下,可以找到功能单元所有的内部逻辑) +3. 页面不过是组件的容器,组件可以嵌套自由组合成完整的页面 + +## vue 等单页面应用及其优缺点 + +优点: +1. 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染。 +2. 前后端职责业分离(前端负责view,后端负责model),架构清晰 +3. 减轻服务器的压力 + +缺点: + +1. SEO(搜索引擎优化)难度高 +2. 初次加载页面更耗时 +3. 前进、后退、地址栏等,需要程序进行管理,所以会大大提高页面的复杂性和逻辑的难度 + +## `$route`和`$router`的区别 + +**$route** 是路由信息对象,包括path,params,hash,query,fullPath,matched,name 等路由信息参数。 + +**$router** 是路由实例对象,包括了路由的跳转方法,钩子函数等 + +## 什么是 vue 的计算属性? + +定义: 当其依赖的属性的值发生变化的时,计算属性会重新计算。反之则使用缓存中的属性值。 计算属性和vue中的其它数据一样,都是响应式的,只不过它必须依赖某一个数据实现,并且只有它依赖的数据的值改变了,它才会更新。 + +## watch的作用是什么 + +watch 主要作用是监听某个数据值的变化。和计算属性相比除了没有缓存,作用是一样的。 + +借助 watch 还可以做一些特别的事情,例如监听页面路由,当页面跳转时,我们可以做相应的权限控制,拒绝没有权限的用户访问页面。 + +## 计算属性的缓存和方法调用的区别 + +计算属性是基于数据的依赖缓存,数据发生变化,缓存才会发生变化,如果数据没有发生变化,调用计算属性直接调用的是存储的缓存值; + +而方法每次调用都会重新计算; + +所以可以根据实际需要选择使用,如果需要计算大量数据,性能开销比较大,可以选用计算属性,如果不能使用缓存可以使用方法; + +其实这两个区别还应加一个watch,watch是用来监测数据的变化,和计算属性相比,是watch没有缓存,但是一般想要在数据变化时响应时,或者执行异步操作时,可以选择watch + +## 指令 v-el 的作用是什么? + +通过`v-el`我们可以获取到`DOM`对象,通过`this.$els[elValue]`获得`DOM`对象;通过`v-ref`获取到整个组件(`component`)的对象,通过`this.$refs[refValue]`获得`Component`实例对象。 + + +## vuex 有哪几种属性? + +有五种,分别是 State、 Getter、Mutation 、Action、 Module + +vuex的State特性 + +1. Vuex就是一个仓库,仓库里面放了很多对象。其中state就是数据源存放地,对应于一般Vue对象里面的data +2. state里面存放的数据是响应式的,Vue组件从store中读取数据,若是store中的数据发生改变,依赖这个数据的组件也会发生更新 +3. 它通过mapState把全局的 state 和 getters 映射到当前组件的 computed 计算属性中 + +vuex的Getter特性 +1. getters 可以对State进行计算操作,它就是Store的计算属性 +2. 虽然在组件内也可以做计算属性,但是getters 可以在多组件之间复用 +3. 如果一个状态只在一个组件内使用,是可以不用getters + +vuex的Mutation特性 +1. Action 类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态;Action 可以包含任意异步操作。 + +## 不用 Vuex 会带来什么问题? + +可维护性会下降,想修改数据要维护三个地方; + +可读性会下降,因为一个组件里的数据,根本就看不出来是从哪来的; + +增加耦合,大量的上传派发,会让耦合性大大增加,本来 Vue 用 Component 就是为了减少耦合,现在这么用,和组件化的初衷相背。 + +## vue-router 有哪几种导航钩子( 导航守卫 )? + +答案:三种 + +- 第一种: 全局导航钩子, router.beforeEach(to, from, next),作用:跳转前进行判断拦截; +``` js +router.beforeEach((to, from, next) => { + // TODO +}); +``` + +- 第二种:单独路由独享组件; +``` js +{ + path: '/home', + name: 'home', + component: Home, + beforeEnter(to, from, next) { + // TODO + } +} +``` + +- 第三种:组件内的钩子。 +``` js +beforeRouteEnter(to, from, next) { + // do someting + // 在渲染该组件的对应路由被 confirm 前调用 +}, +beforeRouteUpdate(to, from, next) { + // do someting + // 在当前路由改变,但是依然渲染该组件是调用 +}, +beforeRouteLeave(to, from ,next) { + // do someting + // 导航离开该组件的对应路由时被调用 +} +``` + +## vue-router 实现路由懒加载( 动态加载路由 ) + +vue项目实现按需加载的3种方式:vue异步组件、es提案的import()、webpack的require.ensure() + +### vue异步组件技术 + +- vue-router配置路由,使用vue的异步组件技术,可以实现按需加载。 + +但是,这种情况下一个组件生成一个js文件。 + +举例如下: + +``` vue +{ + path: '/promisedemo', + name: 'PromiseDemo', + component: resolve => require(['../components/PromiseDemo'], resolve) +} +``` + +### es提案的import() + +- 推荐使用这种方式(需要webpack > 2.4) +- webpack官方文档:webpack中使用import() + +vue官方文档:[路由懒加载(使用import())](https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#%E6%8A%8A%E7%BB%84%E4%BB%B6%E6%8C%89%E7%BB%84%E5%88%86%E5%9D%97) + +- vue-router配置路由,代码如下: + +``` vue +// 下面2行代码,没有指定webpackChunkName,每个组件打包成一个js文件。 +const ImportFuncDemo1 = () => import('../components/ImportFuncDemo1') +const ImportFuncDemo2 = () => import('../components/ImportFuncDemo2') +// 下面2行代码,指定了相同的webpackChunkName,会合并打包成一个js文件。 +// const ImportFuncDemo = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo') +// const ImportFuncDemo2 = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo2') +export default new Router({ + routes: [ + { + path: '/importfuncdemo1', + name: 'ImportFuncDemo1', + component: ImportFuncDemo1 + }, + { + path: '/importfuncdemo2', + name: 'ImportFuncDemo2', + component: ImportFuncDemo2 + } + ] +}) +``` + +### webpack提供的require.ensure() + +- vue-router配置路由,使用webpack的require.ensure技术,也可以实现按需加载。 + +这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。 + +举例如下: + +``` vue +{ + path: '/promisedemo', + name: 'PromiseDemo', + component: resolve => require.ensure([], () => resolve(require('../components/PromiseDemo')), 'demo') +}, +{ + path: '/hello', + name: 'Hello', + // component: Hello + component: resolve => require.ensure([], () => resolve(require('../components/Hello')), 'demo') +} +``` + +## 谈一谈 nextTick 的原理 + +- 在下次 DOM 更新循环结束之后执行延迟回调。 + +- nextTick主要使用了宏任务和微任务。 + +- 根据执行环境分别尝试采用 + Promise MutationObserver setImmediate + +如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。 + +## Vue 的父组件和子组件生命周期钩子执行顺序是什么 + +- 加载渲染过程 + - 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted + +- 子组件更新过程 + - 父beforeUpdate->子beforeUpdate->子updated->父updated + +- 父组件更新过程 + - 父beforeUpdate->父updated + +- 销毁过程 + - 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed + + +## 实现通信方式 + +### 方式1: props +1. 通过一般属性实现父向子通信 +2. 通过函数属性实现子向父通信 +3. 缺点: 隔代组件和兄弟组件间通信比较麻烦 + +### 方式2: vue自定义事件 +1. vue内置实现, 可以代替函数类型的props + - 绑定监听: \{}) + - 发布消息: PubSub.publish(‘msg’, data) +2. 优点: 此方式可用于任意关系组件间通信 + +### 方式4: vuex +1. 是什么: vuex是vue官方提供的集中式管理vue多组件共享状态数据的vue插件 +2. 优点: 对组件间关系没有限制, 且相比于pubsub库管理更集中, 更方便 + +### 方式5: slot +1. 是什么: 专门用来实现父向子传递带数据的标签 + - 子组件 + - 父组件 +2. 注意: 通信的标签模板是在父组件中解析好后再传递给子组件的 + +## 说说Vue的MVVM实现原理 + +- 1. Vue作为MVVM模式的实现库的2种技术 + - 模板解析 + - 数据绑定 + +- 2. 模板解析: 实现初始化显示 + - 解析大括号表达式 + - 解析指令 + +- 3. 数据绑定: 实现更新显示 + - 通过数据劫持实现 + +## Vue.use是干什么的?原理是什么? + +vue.use 是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等。 + +- 1. 检查插件是否注册,若已注册,则直接跳出; + +- 2. 处理入参,将第一个参数之后的参数归集,并在首部塞入 this 上下文; + +- 3. 执行注册方法,调用定义好的 install 方法,传入处理的参数,若没有 install 方法并且插件本身为 function 则直接进行注册; + + - 插件不能重复的加载 + + install 方法的第一个参数是vue的构造函数,其他参数是Vue.use中除了第一个参数的其他参数; 代码:args.unshift(this) + + - 调用插件的install 方法 代码:typeof plugin.install === 'function' + + - 插件本身是一个函数,直接让函数执行。 代码:plugin.apply(null, args) + + - 缓存插件。 代码:installedPlugins.push(plugin) + +``` ts +export function toArray (list: any, start?: number): Array { + start = start || 0 + let i = list.length - start + const ret: Array = new Array(i) + while (i--) { + ret[i] = list[i + start] + } + return ret +} + + +export function initUse (Vue: GlobalAPI) { + Vue.use = function (plugin: Function | Object) { + const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) + if (installedPlugins.indexOf(plugin) > -1) { + return this + } + + // additional parameters + const args = toArray(arguments, 1) + args.unshift(this) + if (typeof plugin.install === 'function') { + plugin.install.apply(plugin, args) + } else if (typeof plugin === 'function') { + plugin.apply(null, args) + } + installedPlugins.push(plugin) + return this + } +} +``` + + +## new Vue() 发生了什么? + +1. 结论:new Vue()是创建Vue实例,它内部执行了根实例的初始化过程。 + +2. 具体包括以下操作: + +选项合并 + +`$children`,`$refs`,`$slots`,`$createElement`等实例属性的方法初始化 + +自定义事件处理 + +数据响应式处理 + +生命周期钩子调用 (beforecreate created) + +可能的挂载 + +3. 总结:new Vue()创建了根实例并准备好数据和方法,未来执行挂载时,此过程还会递归的应用于它的子组件上,最终形成一个有紧密关系的组件实例树。 + +## 请说一下响应式数据的理解? + +根据数据类型来做不同处理,数组和对象类型当值变化时如何劫持。 + +1. 对象内部通过defineReactive方法,使用Object. defineProperty() 监听数据属性的 get 来进行数据依赖收集,再通过 set 来完成数据更新的派发; + +2. 数组则通过重写数组方法来实现的。扩展它的 7 个变更⽅法,通过监听这些方法可以做到依赖收集和派发更新;( push/pop/shift/unshift/splice/reverse/sort ) + +这里在回答时可以带出一些相关知识点 (比如多层对象是通过递归来实现劫持,顺带提出vue3中是使用 proxy来实现响应式数据) + +补充回答: + +内部依赖收集是怎么做到的,每个属性都拥有自己的dep属性,存放他所依赖的 watcher,当属性变化后会通知自己对应的 watcher去更新。 + +响应式流程: + +1. defineReactive 把数据定义成响应式的; + +2. 给属性增加一个 dep,用来收集对应的那些watcher; + +3. 等数据变化进行更新 + +dep.depend() // get 取值:进行依赖收集 + +dep.notify() // set 设置时:通知视图更新 + +这里可以引出性能优化相关的内容: + +1. 对象层级过深,性能就会差。 + +2. 不需要响应数据的内容不要放在data中。 + +3. object.freeze() 可以冻结数据。 + +## Vue如何检测数组变化? + +数组考虑性能原因没有用defineProperty对数组的每一项进行拦截,而是选择重写数组方法。当数组调用到这 7 个方法的时候,执行 ob.dep.notify() 进行派发通知 Watcher 更新; + +重写数组方法:push/pop/shift/unshift/splice/reverse/sort + +补充回答: + +在Vue中修改数组的索引和长度是无法监控到的。需要通过以下7种变异方法修改数组才会触发数组对应的wacther进行更新。数组中如果是对象数据类型也会进行递归劫持。 + +说明:那如果想要改索引更新数据怎么办? + +可以通过Vue.set()来进行处理 =》 核心内部用的是 splice 方法。 + +``` js +// 取出原型方法; + +const arrayProto = Array.prototype + +// 拷贝原型方法; + +export const arrayMethods = Object.create(arrayProto) + +// 重写数组方法; + +def(arrayMethods, method, function mutator (... args) { } + +ob.dep.notify() // 调用方法时更新视图; +``` + +## Vue.set 方法是如何实现的? + +为什么$set可以触发更新,我们给对象和数组本身都增加了dep属性,当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组。 + +补充回答: + +官方定义Vue.set(object, key, value) + +如果是数组,调用重写的splice方法 (这样可以更新视图 ) +代码:target.splice(key, 1, val) + +如果不是响应式的也不需要将其定义成响应式属性。 + +如果是对象,将属性定义成响应式的 defineReactive(ob, key, val) + +通知视图更新 ob.dep.notify() + +## Vue3.x响应式数据原理 + +Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。 + +## Vue3.x中Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢? + +判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。 + +## Vue3.x中监测数组的时候可能触发多次get/set,那么如何防止触发多次呢? + +我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。 + +## vue2.x中如何监测数组变化 +- 使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。 +- 这样当调用数组api时,可以通知依赖更新。 +- 如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。 + + +## Vue2.x和Vue3.x渲染器的diff算法分别说一下 + +简单来说,diff算法有以下过程 + +- 同级比较,再比较子节点 +- 先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除) +- 比较都有子节点的情况(核心diff) +- 递归比较子节点 +- 正常Diff两个树的时间复杂度是O(n^3) ,但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。 + +Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。 + +Vue3.x借鉴了 ivi算法和 inferno算法 + +在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。(实际的实现可以结合Vue3.x源码看。) + +## SSR了解吗? + +- SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端。 + +- SSR有着更好的SEO、并且首屏加载速度更快等优点。 + +- 不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。 + +- 还有就是服务器会有更大的负载需求。 + +## 组件中写 name选项有哪些好处及作用? + +- 可以通过名字找到对应的组件( 递归组件 ) + +- 可以通过name属性实现缓存功能 (keep-alive) + +- 可以通过name来识别组件(跨级组件通信时非常重要) + +``` vue +Vue.extend = function () { + if(name) { + Sub.options.componentd[name] = Sub + } +} +``` + +## 传统diff、react优化diff、vue优化diff + +### 传统diff + +计算两颗树形结构差异并进行转换,传统diff算法是这样做的:循环递归每一个节点 + +![传统diff](https://upload-images.jianshu.io/upload_images/8901652-829ed2769504d3b5.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +比如左侧树a节点依次进行如下对比,左侧树节点b、c、d、e亦是与右侧树每个节点对比,算法复杂度能达到O(n^2),n代表节点的个数 + +> a->e、a->d、a->b、a->c、a->a + +查找完差异后还需计算最小转换方式,这其中的原理我没仔细去看,最终达到的算法复杂度是O(n^3) + +### react优化的diff策略 + +传统diff算法复杂度达到O(n^3 )这意味着1000个节点就要进行数10亿次的比较,这是非常消耗性能的。react大胆的将diff的复杂度从O(n^3)降到了O(n),他是如何做到的呢 + +- 由于web UI中跨级移动操作非常少、可以忽略不计,所以react实现的diff是同层级比较 + +![react中的diff](https://upload-images.jianshu.io/upload_images/8901652-abb72fd92fcacdef.png?imageMogr2/auto-orient/strip|imageView2/2/w/401/format/webp) + +- 拥有相同类型的两个组件产生的DOM结构也是相似的,不同类型的两个组件产生的DOM结构则不近相同 + +- 对于同一层级的一组子节点,通过分配唯一的key进行区分 + +#### react虚拟节点 + +dom中没有直接提供api让我们获取一棵树结构,这里我们自己构建一个虚拟的dom结构,遍历这样的数据结构是一件很轻松直观的事情。 + +对于下面的dom,可以用js构造出一个简单的虚拟dom + +``` html +
+

1

+
2
+ 3 +
+``` + +``` js +{ + type: 'div', + props: { + className: 'myDiv', + }, + chidren: [ + {type: 'p',props:{value:'1'}}, + {type: 'div',props:{value:'2'}}, + {type: 'span',props:{value:'3'}} + ] +} +``` + +#### 先序深度优先遍历 + +首先要遍历新旧两棵树,采用深度优先策略,为树的每个节点标示唯一一个id + +![先深度优先遍历](https://upload-images.jianshu.io/upload_images/7243642-45739cc8c4a5b906.png?imageMogr2/auto-orient/strip|imageView2/2/w/564/format/webp) + +在遍历过程中,对比新旧节点,将差异记录下来,记录差异的方式后面会提到 + +``` js +//若新旧树节点只是位置不同,移动 +//计算差异 +//插入新树中存在但旧树中不存在的节点 +//删除新树中没有的节点 + +// diff 函数,对比两棵树 +function diff (oldTree, newTree) { + // 当前节点的标志,以后每遍历到一个节点,加1 + var index = 0 + var patches = {} // 用来记录每个节点差异的对象 + dfsWalk(oldTree, newTree, index, patches) + return patches +} + +// 对两棵树进行深度优先遍历 +function dfsWalk (oldNode, newNode, index, patches) { + // 对比oldNode和newNode的不同,记录下来 + patches[index] = [...] + + diffChildren(oldNode.children, newNode.children, index, patches) +} + +// 遍历子节点 +function diffChildren (oldChildren, newChildren, index, patches) { + var leftNode = null + var currentNodeIndex = index + oldChildren.forEach(function (child, i) { + var newChild = newChildren[i] + currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识 + ? currentNodeIndex + leftNode.count + 1 + : currentNodeIndex + 1 + dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点 + leftNode = child + }) +} +``` + +#### 差异类型 + +上面代码中,将所有的差异保存在了`patches`对象中,会有如下几种差异类型: + +1. 插入:`patches[0]: {type:'INSERT_MARKUP',node: newNode }` +2. 移动:`patches[0]: {type: 'MOVE_EXISTING'}` +3. 删除:`patches[0]: {type: 'REMOVE_NODE'}` +4. 文本内容改变:`patches[0]: {type: 'TEXT_CONTENT',content: 'virtual DOM2'}` +5. 属性改变:`patches[0]: {type: 'SET_MARKUP',props: {className:''}}` + + +#### 列表对比 + +节点两两进行对比时,我们知道新节点较旧节点有什么不同。如果同一层的多个子节点进行对比,他们只是顺序不同,按照上面的算法,会先删除旧节点,再新增一个相同的节点,这可不是我们想看到的结果 + +实际上,react在同级节点对比时,提供了更优的算法: + +![同级比较](https://upload-images.jianshu.io/upload_images/8901652-2f005f9e67972ae7.png?imageMogr2/auto-orient/strip|imageView2/2/w/865/format/webp) + +> 首先对新集合的节点(nextChildren)进行in循环遍历,通过唯一的key(这里是变量name)可以取得新老集合中相同的节点,如果不存在,prevChildren即为undefined。如果存在相同节点,也即prevChild === nextChild,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,见moveChild函数,如下图 + +![moveChild](https://upload-images.jianshu.io/upload_images/8901652-7953364431896c2e.png?imageMogr2/auto-orient/strip|imageView2/2/w/865/format/webp) + +> if (child._mountIndex < lastIndex),则进行节点移动操作,否则不执行该操作。这是一种顺序优化手段,lastIndex一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比lastIndex大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比lastIndex小时,才需要进行移动操作。 + +所以下图中只需要移动A、C + +![移动](https://upload-images.jianshu.io/upload_images/8901652-7130e33555bd50df.png?imageMogr2/auto-orient/strip|imageView2/2/w/552/format/webp) + +### Vue优化的diff策略 + +既然传统diff算法性能开销如此之大,Vue做了什么优化呢? + +- 跟react一样,只进行同层级比较,忽略跨级操作 + +react以及Vue在diff时,都是在对比虚拟dom节点,下文提到的节点都指虚拟节点。Vue是怎样描述一个节点的呢? + +#### Vue虚拟节点 + +``` js +// body下的
对应的 oldVnode 就是 + +{ + el: div //对真实的节点的引用,本例中就是document.querySelector('#id.classA') + tagName: 'DIV', //节点的标签 + sel: 'div#v.classA' //节点的选择器 + data: null, // 一个存储节点属性的对象,对应节点的el[prop]属性,例如onclick , style + children: [], //存储子节点的数组,每个子节点也是vnode结构 + text: null, //如果是文本节点,对应文本节点的textContent,否则为null +} +``` + +#### patch + +diff时调用patch函数,patch接收两个参数vnode,oldVnode,分别代表新旧节点。 + +``` js +function patch (oldVnode, vnode) { + if (sameVnode(oldVnode, vnode)) { + patchVnode(oldVnode, vnode) + } else { + const oEl = oldVnode.el + let parentEle = api.parentNode(oEl) + createEle(vnode) + if (parentEle !== null) { + api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) + api.removeChild(parentEle, oldVnode.el) + oldVnode = null + } + } + return vnode +} +``` + +patch函数内第一个`if`判断`sameVnode(oldVnode, vnode)`就是判断这两个节点是否为同一类型节点,以下是它的实现: + +``` js +function sameVnode(oldVnode, vnode){ + //两节点key值相同,并且sel属性值相同,即认为两节点属同一类型,可进行下一步比较 + return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel +} +``` + +也就是说,即便同一个节点元素比如div,他的`className`不同,Vue就认为是两个不同类型的节点,执行删除旧节点、插入新节点操作。这与react diff实现是不同的,react对于同一个节点元素认为是同一类型节点,只更新其节点上的属性。 + +#### patchVnode + +对于同类型节点调用`patchVnode(oldVnode, vnode)`进一步比较: + +``` js +patchVnode (oldVnode, vnode) { + const el = vnode.el = oldVnode.el //让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化。 + let i, oldCh = oldVnode.children, ch = vnode.children + if (oldVnode === vnode) return //新旧节点引用一致,认为没有变化 + //文本节点的比较 + if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) { + api.setTextContent(el, vnode.text) + }else { + updateEle(el, vnode, oldVnode) + //对于拥有子节点(两者的子节点不同)的两个节点,调用updateChildren + if (oldCh && ch && oldCh !== ch) { + updateChildren(el, oldCh, ch) + }else if (ch){ //只有新节点有子节点,添加新的子节点 + createEle(vnode) //create el's children dom + }else if (oldCh){ //只有旧节点内存在子节点,执行删除子节点操作 + api.removeChildren(el) + } + } +} +``` + +#### updateChildren + +patchVnode中有一个重要的概念updateChildren,这是Vue diff实现的核心: + +``` js +updateChildren (parentElm, oldCh, newCh) { + let oldStartIdx = 0, newStartIdx = 0 + let oldEndIdx = oldCh.length - 1 + let oldStartVnode = oldCh[0] + let oldEndVnode = oldCh[oldEndIdx] + let newEndIdx = newCh.length - 1 + let newStartVnode = newCh[0] + let newEndVnode = newCh[newEndIdx] + let oldKeyToIdx + let idxInOld + let elmToMove + let before + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { + if (oldStartVnode == null) { //对于vnode.key的比较,会把oldVnode = null + oldStartVnode = oldCh[++oldStartIdx] + }else if (oldEndVnode == null) { + oldEndVnode = oldCh[--oldEndIdx] + }else if (newStartVnode == null) { + newStartVnode = newCh[++newStartIdx] + }else if (newEndVnode == null) { + newEndVnode = newCh[--newEndIdx] + }else if (sameVnode(oldStartVnode, newStartVnode)) { + patchVnode(oldStartVnode, newStartVnode) + oldStartVnode = oldCh[++oldStartIdx] + newStartVnode = newCh[++newStartIdx] + }else if (sameVnode(oldEndVnode, newEndVnode)) { + patchVnode(oldEndVnode, newEndVnode) + oldEndVnode = oldCh[--oldEndIdx] + newEndVnode = newCh[--newEndIdx] + }else if (sameVnode(oldStartVnode, newEndVnode)) { + patchVnode(oldStartVnode, newEndVnode) + api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el)) + oldStartVnode = oldCh[++oldStartIdx] + newEndVnode = newCh[--newEndIdx] + }else if (sameVnode(oldEndVnode, newStartVnode)) { + patchVnode(oldEndVnode, newStartVnode) + api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el) + oldEndVnode = oldCh[--oldEndIdx] + newStartVnode = newCh[++newStartIdx] + }else { + // 使用key时的比较 + if (oldKeyToIdx === undefined) { + oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表 + } + idxInOld = oldKeyToIdx[newStartVnode.key] + if (!idxInOld) { + api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) + newStartVnode = newCh[++newStartIdx] + } + else { + elmToMove = oldCh[idxInOld] + if (elmToMove.sel !== newStartVnode.sel) { + api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) + }else { + patchVnode(elmToMove, newStartVnode) + oldCh[idxInOld] = null + api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el) + } + newStartVnode = newCh[++newStartIdx] + } + } + } + if (oldStartIdx > oldEndIdx) { + before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el + addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx) + }else if (newStartIdx > newEndIdx) { + removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) + } +} +``` + +![双端至中间比较](https://upload-images.jianshu.io/upload_images/8901652-ec9b2ecc01ba64b2.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +> 过程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较。 + +这种由两端至中间的对比方法与react的`updateChildren`实现也是不同,后者是从左至右依次进行对比,各有优点。 +比如一个集合,只是把最后一个节点移到了第一个,react实现就出现了短板,react会依次移动前三个节点到对应的位置: + +[节点移动](https://upload-images.jianshu.io/upload_images/8901652-7b346d474b799a59.png?imageMogr2/auto-orient/strip|imageView2/2/w/786/format/webp) + +而Vue会在首尾对比时,只移动最后一个节点到第一位即可 + +## vue的diff算法和react的diff算法的区别 + +vue和react的diff算法,都是忽略跨级比较,只做同级比较。vue diff时调动patch函数,参数是vnode和oldVnode,分别代表新旧节点。 + +1. vue对比节点。当节点元素相同,但是classname不同,认为是不同类型的元素,删除重建,而react认为是同类型节点,只是修改节点属性。 + +2. vue的列表对比,采用的是两端到中间比对的方式,而react采用的是从左到右依次对比的方式。当一个集合只是把最后一个节点移到了第一个,react会把前面的节点依次移动,而vue只会把最后一个节点移到第一个。总体上,vue的方式比较高效。 + +## axios的特点有哪些? + +1. Axios 是一个基于 promise 的 HTTP 库,支持promise所有的API +2. 它可以拦截请求和响应 +3. 它可以转换请求数据和响应数据,并对响应回来的内容自动转换成 JSON类型的数据 +4. 安全性更高,客户端支持防御 XSRF + +## axios有哪些常用方法? + +1. axios.get(url[, config]) //get请求用于列表和信息查询 +2. axios.delete(url[, config]) //删除 +3. axios.post(url[, data[, config]]) //post请求用于信息的添加 +4. axios.put(url[, data[, config]]) //更新操作 + +## 说下你了解的axios相关配置属性? + +`url` 是用于请求的服务器URL + +`method` 是创建请求时使用的方法,默认是get + +`baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对URL。它可以通过设置一个 `baseURL` 便于为axios实例的方法传递相对URL + +`transformRequest` 允许在向服务器发送前,修改请求数据,只能用在'PUT','POST'和'PATCH'这几个请求方法 + +`headers` 是即将被发送的自定义请求头 +``` js +headers:{'X-Requested-With':'XMLHttpRequest'}, +``` + +`params` 是即将与请求一起发送的URL参数,必须是一个无格式对象(plainobject)或URLSearchParams对象 +``` js +params:{ + ID:12345 +}, +``` + +`auth` 表示应该使用HTTP基础验证,并提供凭据 +这将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization` 头 +``` js +auth:{ + username:'janedoe', + password:'s00pers3cret' +}, +``` + +'proxy'定义代理服务器的主机名称和端口 +`auth` 表示HTTP基础验证应当用于连接代理,并提供凭据 +这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。 +``` js +proxy:{ + host:'127.0.0.1', + port:9000, + auth::{ + username:'mikeymike', + password:'rapunz3l' + } +}, +``` + + +## 事件机制 + +### 1.1 事件触发三阶段 + +- document 往事件触发处传播,遇到注册的捕获事件会触发 +- 传播到事件触发处时触发注册的事件 +- 从事件触发处往 document 传播,遇到注册的冒泡事件会触发 + +> 事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个目标节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行 + +``` +// 以下会先打印冒泡然后是捕获 +node.addEventListener('click',(event) =>{ + console.log('冒泡') +},false); +node.addEventListener('click',(event) =>{ + console.log('捕获 ') +},true) +``` + +### 1.2 注册事件 + +- 通常我们使用 `addEventListener` 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 `useCapture` 参数来说,该参数默认值为 `false` 。`useCapture` 决定了注册的事件是捕获事件还是冒泡事件 +- 一般来说,我们只希望事件只触发在目标上,这时候可以使用 `stopPropagation` 来阻止事件的进一步传播。通常我们认为 `stopPropagation` 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。`stopImmediatePropagation` 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件 + +```javascript +node.addEventListener('click',(event) =>{ + event.stopImmediatePropagation() + console.log('冒泡') +},false); +// 点击 node 只会执行上面的函数,该函数不会执行 +node.addEventListener('click',(event) => { + console.log('捕获 ') +},true) +``` + +### 1.3 事件代理 + +> 如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上 + +```html +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • +
  • 4
  • +
  • 5
  • +
+ +``` + +> 事件代理的方式相对于直接给目标注册事件来说,有以下优点 + +- 节省内存 +- 不需要给子节点注销事件 + +## 跨域 + +> 因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败 + +### 2.1 JSONP + +> JSONP 的原理很简单,就是利用 ` + +``` + +- JSONP 使用简单且兼容性不错,但是只限于 get 请求 + +### 2.2 CORS + +- `CORS`需要浏览器和后端同时支持 +- 浏览器会自动进行 `CORS` 通信,实现CORS通信的关键是后端。只要后端实现了 `CORS`,就实现了跨域。 +- 服务端设置 `Access-Control-Allow-Origin` 就可以开启 `CORS`。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源 + + +### 2.3 document.domain + +- 该方式只能用于二级域名相同的情况下,比如 `a.test.com` 和 `b.test.com` 适用于该方式。 +- 只需要给页面添加 `document.domain = 'test.com'` 表示二级域名都相同就可以实现跨域 + +### 2.4 postMessage + +> 这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息 + +```javascript +// 发送消息端 +window.parent.postMessage('message', 'http://test.com'); + +// 接收消息端 +var mc = new MessageChannel(); +mc.addEventListener('message', (event) => { + var origin = event.origin || event.originalEvent.origin; + if (origin === 'http://test.com') { + console.log('验证通过') + } +}); +``` + + +## 前端错误监控 + +### 1 前言 + +> 错误监控包含的内容是: + +- 前端错误的分类 +- 每种错误的捕获方式 +- 上报错误的基本原理 + +> 面试时,可能有两种问法: + +- 如何监测 `js` 错误?(开门见山的方式) +- 如何保证**产品质量**?(其实问的也是错误监控) + + +### 2 前端错误的分类 + +包括两种: + +- 即时运行错误(代码错误) +- 资源加载错误 + + +### 3 每种错误的捕获方式 + + +#### 3.1 即时运行错误的捕获方式 + +**方式1**:`try ... catch`。 + +> 这种方式要部署在代码中。 + +**方式2:**`window.onerror`函数。这个函数是全局的。 + +```js + window.onerror = function(msg, url, row, col, error) { ... } +``` + +> 参数解释: + +- `msg`为异常基本信息 +- `source`为发生异常`Javascript`文件的`url` +- `row`为发生错误的行号 + +> 方式二中的`window.onerror`是属于DOM0的写法,我们也可以用DOM2的写法:`window.addEventListener("error", fn);`也可以。 + +**问题延伸1:** + +`window.onerror`默认无法捕获**跨域**的`js`运行错误。捕获出来的信息如下:(基本属于无效信息) + +> 比如说,我们的代码想引入`B`网站的`b.js`文件,怎么捕获它的异常呢? + +**解决办法**:在方法二的基础之上,做如下操作: + +1. 在`b.js`文件里,加入如下 `response` `header`,表示允许跨域:(或者世界给静态资源`b.js`加这个 response header) + +```js + Access-Control-Allow-Origin: * +``` + +2. 引入第三方的文件`b.js`时,在` + + + +``` + + +> 打开浏览器,效果如下: + +![](http://img.smyhvae.com/20180311_2055.png) + +上图中,红色那一栏表明,我的请求已经发出去了。点进去看看: + +![](http://img.smyhvae.com/20180311_2057.png) + +> 这种方式,不需要借助第三方的库,一行代码即可搞定。 + + +## HTML + +**语义化** + +- HTML标签的语义化是指:通过使用包含语义的标签(如h1-h6)恰当地表示文档结构 +- css命名的语义化是指:为html标签添加有意义的class + +- 为什么需要语义化: + - 去掉样式后页面呈现清晰的结构 + - 盲人使用读屏器更好地阅读 + - 搜索引擎更好地理解页面,有利于收录 + - 便团队项目的可持续运作及维护 + +**简述一下你对HTML语义化的理解?** +- 用正确的标签做正确的事情。 +- html语义化让页面的内容结构化,结构更清晰,便于对浏览器、搜索引擎解析; +- 即使在没有样式CSS情况下也以一种文档格式显示,并且是容易阅读的; +- 搜索引擎的爬虫也依赖于HTML标记来确定上下文和各个关键字的权重,利于SEO; +- 使阅读源代码的人对网站更容易将网站分块,便于阅读维护理解 + +**Doctype作用?标准模式与兼容模式各有什么区别?** + +- ``声明位于位`于HTML`文档中的第一行,处于 `` 标签之前。告知浏览器的解析器用什么文档标准解析这个文档。`DOCTYPE`不存在或格式不正确会导致文档以兼容模式呈现 +- 标准模式的排版 和JS运作模式都是以该浏览器支持的最高标准运行。在兼容模式中,页面以宽松的向后兼容的方式显示,模拟老式浏览器的行为以防止站点无法工作 + +**HTML5 为什么只需要写 ?** + +- HTML5 不基于 SGML,因此不需要对DTD进行引用,但是需要doctype来规范浏览器的行为(让浏览器按照它们应该的方式来运行) +- 而HTML4.01基于SGML,所以需要对DTD进行引用,才能告知浏览器文档所使用的文档类型 + +**行内元素有哪些?块级元素有哪些? 空(void)元素有那些?** + +- 行内元素有:`a b span img input select strong`(强调的语气) +- 块级元素有:`div ul ol li dl dt dd h1 h2 h3 h4…p` +- 常见的空元素:`

` + +**页面导入样式时,使用link和@import有什么区别?** + +- `link`属于`XHTML`标签,除了加载`CSS`外,还能用于定义`RSS`,定义`rel`连接属性等作用;而`@import`是`CSS`提供的,只能用于加载`CSS` +- 页面被加载的时,`link`会同时被加载,而`@import`引用的`CSS`会等到页面被加载完再加载 +- `import`是`CSS2.1` 提出的,只在`IE5`以上才能被识别,而`link`是`XHTML`标签,无兼容问题 + +**介绍一下你对浏览器内核的理解?** + +- 主要分成两部分:渲染引擎(`layout engineer`或`Rendering Engine`)和`JS`引擎 + +- 渲染引擎:负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入CSS等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核 +- JS引擎则:解析和执行javascript来实现网页的动态效果 +- 最开始渲染引擎和JS引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎 + +**常见的浏览器内核有哪些?** + +- `Trident`内核:`IE,MaxThon,TT,The World,360`,搜狗浏览器等。[又称MSHTML] +- `Gecko`内核:`Netscape6`及以上版本,`FF,MozillaSuite/SeaMonkey`等 +- `Presto`内核:`Opera7`及以上。 [`Opera`内核原为:Presto,现为:`Blink`;] +- `Webkit`内核:`Safari,Chrome`等。 [ `Chrome`的`Blink`(`WebKit`的分支)] + +**html5有哪些新特性、移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5?** + +- HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,多任务等功能的增加 + - 绘画 canvas + - 用于媒介回放的 video 和 audio 元素 + - 本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失 + - sessionStorage 的数据在浏览器关闭后自动删除 + - 语意化更好的内容元素,比如 article、footer、header、nav、section + - 表单控件,calendar、date、time、email、url、search + - 新的技术webworker, websocket, Geolocation + +- 移除的元素: + - 纯表现的元素:basefont,big,center,font, s,strike,tt,u + - 对可用性产生负面影响的元素:frame,frameset,noframes + +- 支持HTML5新标签: + - IE8/IE7/IE6支持通过document.createElement方法产生的标签 + - 可以利用这一特性让这些浏览器支持HTML5新标签 + - 浏览器支持新标签后,还需要添加标签默认的样式 + +- 当然也可以直接使用成熟的框架、比如html5shim + +``` + +``` + +- 如何区分HTML5: DOCTYPE声明\新增的结构元素\功能元素 + +**HTML5的离线储存怎么使用,工作原理能不能解释一下?** + +- 在用户没有与因特网连接时,可以正常访问站点或应用,在用户与因特网连接时,更新用户机器上的缓存文件 + +- 原理:HTML5的离线存储是基于一个新建的.appcache文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像cookie一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示 + +- 如何使用: + - 页面头部像下面一样加入一个manifest的属性; + - 在cache.manifest文件的编写离线存储的资源 + - 在离线状态时,操作window.applicationCache进行需求实现 +``` +CACHE MANIFEST + #v0.11 + CACHE: + js/app.js + css/style.css + NETWORK: + resourse/logo.png + FALLBACK: + / /offline.html +``` + +**浏览器是怎么对HTML5的离线储存资源进行管理和加载的呢?** + +- 在线的情况下,浏览器发现html头部有manifest属性,它会请求manifest文件,如果是第一次访问app,那么浏览器就会根据manifest文件的内容下载相应的资源并且进行离线存储。如果已经访问过app并且资源已经离线存储了,那么浏览器就会使用离线的资源加载页面,然后浏览器会对比新的manifest文件与旧的manifest文件,如果文件没有发生改变,就不做任何操作,如果文件改变了,那么就会重新下载文件中的资源并进行离线存储。 + +- 离线的情况下,浏览器就直接使用离线存储的资源。 + +**请描述一下 cookies,sessionStorage 和 localStorage 的区别?** + +- cookie是网站为了标示用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密) +- cookie数据始终在同源的http请求中携带(即使不需要),记会在浏览器和服务器间来回传递 +- `sessionStorage`和`localStorage`不会自动把数据发给服务器,仅在本地保存 +- 存储大小: + - `cookie`数据大小不能超过4k + - `sessionStorage`和`localStorage`虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大 + +- 有期时间: + - `localStorage` 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据 + - `sessionStorage` 数据在当前浏览器窗口关闭后自动删除 + - `cookie` 设置的`cookie`过期时间之前一直有效,即使窗口或浏览器关闭 + +**iframe有那些缺点?** + +- iframe会阻塞主页面的Onload事件 +- 搜索引擎的检索程序无法解读这种页面,不利于SEO +- iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载 +- 使用`iframe`之前需要考虑这两个缺点。如果需要使用`iframe`,最好是通过`javascript`动态给`iframe`添加`src`属性值,这样可以绕开以上两个问题 + +**Label的作用是什么?是怎么用的?** + +- label标签来定义表单控制间的关系,当用户选择该标签时,浏览器会自动将焦点转到和标签相关的表单控件 + +**HTML5的form如何关闭自动完成功能?** + +- 给不想要提示的 form 或某个 input 设置为 autocomplete=off。 + +**如何实现浏览器内多个标签页之间的通信? (阿里)** + +- WebSocket、SharedWorker +- 也可以调用localstorge、cookies等本地存储方式 + +**webSocket如何兼容低浏览器?(阿里)** + +- Adobe Flash Socket 、 +- ActiveX HTMLFile (IE) 、 +- 基于 multipart 编码发送 XHR 、 +- 基于长轮询的 XHR + +**页面可见性(Page Visibility API) 可以有哪些用途?** + +- 通过 visibilityState 的值检测页面当前是否可见,以及打开网页的时间等; +- 在页面被切换到其他后台进程的时候,自动暂停音乐或视频的播放 + +**如何在页面上实现一个圆形的可点击区域?** + +- map+area或者svg +- border-radius +- 纯js实现 需要求一个点在不在圆上简单算法、获取鼠标坐标等等 + +**实现不使用 border 画出1px高的线,在不同浏览器的标准模式与怪异模式下都能保持一致的效果** + +``` +
+``` + +**网页验证码是干嘛的,是为了解决什么安全问题** + +- 区分用户是计算机还是人的公共全自动程序。可以防止恶意破解密码、刷票、论坛灌水 +- 有效防止黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试 + +**title与h1的区别、b与strong的区别、i与em的区别?** + +- `title`属性没有明确意义只表示是个标题,H1则表示层次明确的标题,对页面信息的抓取也有很大的影响 +- `strong`是标明重点内容,有语气加强的含义,使用阅读设备阅读网络时:``会重读,而``是展示强调内容 +- i内容展示为斜体,em表示强调的文本 + +**页面导入样式时,使用 link 和 @import 有什么区别?** + +* link 属于HTML标签,除了加载CSS外,还能用于定 RSS等;@import 只能用于加载CSS +* 页面加载的时,link 会同时被加载,而 @import 引用的 CSS 会等到页面被加载完再加载 +* @import 只在 IE5 以上才能被识别,而 link 是HTML标签,无兼容问题 + +**介绍一下你对浏览器内核的理解?** + +* 浏览器内核主要分为两部分:渲染引擎(layout engineer 或 Rendering Engine) 和 JS引擎 +* 渲染引擎负责取得网页的内容进行布局计和样式渲染,然后会输出至显示器或打印机 +* JS引擎则负责解析和执行JS脚本来实现网页的动态效果和用户交互 +* 最开始渲染引擎和JS引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎 + +**常见的浏览器内核有哪些?** + +* Blink内核:新版 Chrome、新版 Opera +* Webkit内核:Safari、原Chrome +* Gecko内核:FireFox、Netscape6及以上版本 +* Trident内核(又称MSHTML内核):IE、国产浏览器 +* Presto内核:原Opera7及以上 + +**HTML5有哪些新特性?** + +* 新增选择器 document.querySelector、document.querySelectorAll +* 拖拽释放(Drag and drop) API +* 媒体播放的 video 和 audio +* 本地存储 localStorage 和 sessionStorage +* 离线应用 manifest +* 桌面通知 Notifications +* 语意化标签 article、footer、header、nav、section +* 增强表单控件 calendar、date、time、email、url、search +* 地理位置 Geolocation +* 多任务 webworker +* 全双工通信协议 websocket +* 历史管理 history +* 跨域资源共享(CORS) Access-Control-Allow-Origin +* 页面可见性改变事件 visibilitychange +* 跨窗口通信 PostMessage +* Form Data 对象 +* 绘画 canvas + +**HTML5移除了那些元素?** + +* 纯表现的元素:basefont、big、center、font、s、strike、tt、u +* 对可用性产生负面影响的元素:frame、frameset、noframes + +**如何处理HTML5新标签的浏览器兼容问题?** + +* 通过 document.createElement 创建新标签 +* 使用垫片 html5shim.js + +**如何区分 HTML 和 HTML5?** + +- DOCTYPE声明、新增的结构元素、功能元素 + +**HTML5的离线储存工作原理能不能解释一下,怎么使用?** + +* HTML5的离线储存原理: + - 用户在线时,保存更新用户机器上的缓存文件;当用户离线时,可以正常访离线储存问站点或应用内容 + +* HTML5的离线储存使用: + + - 在文档的 html 标签设置 manifest 属性,如 manifest="/offline.appcache" + - 在项目中新建 manifest 文件,manifest 文件的命名建议:xxx.appcache + - 在 web 服务器配置正确的 MIME-type,即 text/cache-manifest + +**浏览器是怎么对HTML5的离线储存资源进行管理和加载的?** + + +* 在线的情况下,浏览器发现 html 标签有 manifest 属性,它会请求 manifest 文件 +* 如果是第一次访问app,那么浏览器就会根据 manifest 文件的内容下载相应的资源并且进行离线存储 +* 如果已经访问过app且资源已经离线存储了,浏览器会对比新的 manifest 文件与旧的 manifest 文件,如果文件没有发生改变,就不做任何操作。如果文件改变了,那么就会重新下载文件中的资源并进行离线存储 +* 离线的情况下,浏览器就直接使用离线存储的资源。 + +**iframe 有那些优点和缺点?** + +* 优点: + - 用来加载速度较慢的内容(如广告) + - 可以使脚本可以并行下载 + - 可以实现跨子域通信 + +* 缺点: + - iframe 会阻塞主页面的 onload 事件 + - 无法被一些搜索引擎索识别 + - 会产生很多页面,不容易管理 + +**label 的作用是什么?怎么使用的?** + +* label标签来定义表单控件的关系: + - 当用户选择label标签时,浏览器会自动将焦点转到和label标签相关的表单控件上 + +* 使用方法1: + - `` + - `` + +* 使用方法2: + - `` + +**如何实现浏览器内多个标签页之间的通信?** + +* iframe + contentWindow +* postMessage +* SharedWorker(Web Worker API) +* storage 事件(localStorge API) +* WebSocket + +**webSocket 如何兼容低浏览器?** + +* Adobe Flash Socket +* ActiveX HTMLFile (IE) +* 基于 multipart 编码发送 XHR +* 基于长轮询的 XHR + +**页面可见性(Page Visibility API) 可以有哪些用途?** + +* 在页面被切换到其他后台进程的时候,自动暂停音乐或视频的播放 +* 当用户浏览其他页面,暂停网站首页幻灯自动播放 +* 完成登陆后,无刷新自动同步其他页面的登录状态 + +**title 与 h1 的区别、b 与 strong 的区别、i 与 em 的区别?** + +* title 表示是整个页面标题,h1 则表示层次明确的标题,对页面信息的抓取有很大的影响 +- `strong`是标明重点内容,有语气加强的含义,使用阅读设备阅读网络时:``会重读,而``是展示强调内容 +- i内容展示为斜体,em表示强调的文本 + +**是展示强调内容** + + * i 内容展示为斜体,em 表示强调的文本 + * 自然样式标签:b, i, u, s, pre + * 语义样式标签:strong, em, ins, del, code + * 应该准确使用语义样式标签, 但不能滥用。如果不能确定时,首选使用自然样式标签 + +## CSS + +**display: none; 与 visibility: hidden; 的区别** + +- 联系:它们都能让元素不可见 +- 区别: + - `display:none`;会让元素完全从渲染树中消失,渲染的时候不占据任何空间;`visibility: hidden`;不会让元素从渲染树消失,渲染师元素继续占据空间,只是内容不可见 + - `display: none`;是非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示;`visibility:hidden`;是继承属性,子孙节点消失由于继承了`hidden`,通过设置`visibility: visible`;可以让子孙节点显式 + - 修改常规流中元素的`display`通常会造成文档重排。修改`visibility`属性只会造成本元素的重绘 + - 读屏器不会读取`display: none;`元素内容;会读取`visibility: hidden`元素内容 + +**css hack原理及常用hack** + +- 原理:利用不同浏览器对CSS的支持和解析结果不一样编写针对特定浏览器样式。 +- 常见的hack有 + - 属性hack + - 选择器hack + - IE条件注释 + +**link 与 @import 的区别** + + - `link` 是`HTML`方式, `@import` 是`CSS`方式 + - `link `最大限度支持并行下载,` @import` 过多嵌套导致串行下载,出现FOUC + - `link` 可以通过 `rel="alternate stylesheet"` 指定候选样式 + - 浏览器对 `link` 支持早于` @import` ,可以使用 `@import` 对老浏览器隐藏样式 + - `@import` 必须在样式规则之前,可以在`css`文件中引用其他文件 + - 总体来说:`link`优于`@import` + +**CSS有哪些继承属性** + +- 关于文字排版的属性如: + - `font` + - `word-break` + - `letter-spacing` + - `text-align` + - `text-rendering` + - `word-spacing` + - `white-space` + - `text-indent` + - `text-transform` + - `text-shadow` + - `line-height` + - `color` + - `visibility` + - `cursor` + +**display,float,position的关系** + +- 如果 `display` 为`none`,那么`position`和`float`都不起作用,这种情况下元素不产生框 +- 否则,如果`position`值为`absolute`或者`fixed`,框就是绝对定位的,`float`的计算值为`none`,`display`根据下面的表格进行调整 +- 否则,如果`float`不是`none`,框是浮动的,`display`根据下表进行调整 +- 否则,如果元素是根元素,`display`根据下表进行调整 +- 其他情况下`display`的值为指定值 总结起来:绝对定位、浮动、根元素都需要调整 `display` + + ![图片转自网络](https://images2018.cnblogs.com/blog/715962/201805/715962-20180513012245079-391725349.png) + +**外边距折叠(collapsing margins)** + +- 毗邻的两个或多个 `margin` 会合并成一个`margin`,叫做外边距折叠。规则如下: + - 两个或多个毗邻的普通流中的块元素垂直方向上的`margin`会折叠 + - 浮动元素或`inline-block`元素或绝对定位元素的`margin`不会和垂直方向上的其他元素的margin折叠 + - 创建了块级格式化上下文的元素,不会和它的子元素发生margin折叠 + - 元素自身的`margin-bottom`和`margin-top`相邻时也会折 + + +**介绍一下标准的CSS的盒子模型?低版本IE的盒子模型有什么不同的?** + +- 有两种, IE 盒子模型、W3C 盒子模型; +- 盒模型: 内容(content)、填充(padding)、边界(margin)、 边框(border); +- 区 别: IE的content部分把 border 和 padding计算了进去; + +**CSS选择符有哪些?哪些属性可以继承?** + +- id选择器( # myid) +- 类选择器(.myclassname) +- 标签选择器(div, h1, p) +- 相邻选择器(h1 + p) +- 子选择器(ul > li) +- 后代选择器(li a) +- 通配符选择器( * ) +- 属性选择器(a[rel = "external"]) +- 伪类选择器(a:hover, li:nth-child) + +- 可继承的样式: `font-size font-family color, UL LI DL DD DT` +- 不可继承的样式:`border padding margin width height ` + +**CSS优先级算法如何计算?** + +- 优先级就近原则,同权重情况下样式定义最近者为准 +- 载入样式以最后载入的定位为准 +- 优先级为: `!important > id > class > tag` important 比 内联优先级高 + +**CSS3新增伪类有那些?** + +``` +p:first-of-type 选择属于其父元素的首个

元素的每个

元素。 +p:last-of-type 选择属于其父元素的最后

元素的每个

元素。 +p:only-of-type 选择属于其父元素唯一的

元素的每个

元素。 +p:only-child 选择属于其父元素的唯一子元素的每个

元素。 +p:nth-child(2) 选择属于其父元素的第二个子元素的每个

元素。 + +:after 在元素之前添加内容,也可以用来做清除浮动。 +:before 在元素之后添加内容 +:enabled +:disabled 控制表单控件的禁用状态。 +:checked 单选框或复选框被选中 +``` + +**如何居中div?如何居中一个浮动元素?如何让绝对定位的div居中?** + +- 给`div`设置一个宽度,然后添加`margin:0 auto`属性 + +``` +div{ + width:200px; + margin:0 auto; + } + ``` +- 居中一个浮动元素 + +``` +//确定容器的宽高 宽500 高 300 的层 +//设置层的外边距 + + .div { + width:500px ; height:300px;//高度可以不设 + margin: -150px 0 0 -250px; + position:relative; //相对定位 + background-color:pink; //方便看效果 + left:50%; + top:50%; + } + ``` + + - 让绝对定位的div居中 + +``` + position: absolute; + width: 1200px; + background: none; + margin: 0 auto; + top: 0; + left: 0; + bottom: 0; + right: 0; + ``` + +**display有哪些值?说明他们的作用** + +- block 象块类型元素一样显示。 +- none 缺省值。象行内元素类型一样显示。 +- inline-block 象行内元素一样显示,但其内容象块类型元素一样显示。 +- list-item 象块类型元素一样显示,并添加样式列表标记。 +- table 此元素会作为块级表格来显示 +- inherit 规定应该从父元素继承 display 属性的值 + +**position的值relative和absolute定位原点是?** + +- absolute + - 生成绝对定位的元素,相对于值不为 static的第一个父元素进行定位。 +- fixed (老IE不支持) + - 生成绝对定位的元素,相对于浏览器窗口进行定位。 +- relative + - 生成相对定位的元素,相对于其正常位置进行定位。 +- static + - 默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right - z-index 声明)。 +- inherit + - 规定从父元素继承 position 属性的值 + +**CSS3有哪些新特性?** + + - 新增各种CSS选择器 (: not(.input):所有 class 不是“input”的节点) + - 圆角 (border-radius:8px) + - 多列布局 (multi-column layout) + - 阴影和反射 (Shadow\Reflect) + - 文字特效 (text-shadow、) + - 文字渲染 (Text-decoration) + - 线性渐变 (gradient) + - 旋转 (transform) + - 增加了旋转,缩放,定位,倾斜,动画,多背景 + - `transform:\scale(0.85,0.90)\ translate(0px,-30px)\ skew(-9deg,0deg)\Animation:` + +**用纯CSS创建一个三角形的原理是什么?** + +``` +// 把上、左、右三条边隐藏掉(颜色设为 transparent) +#demo { + width: 0; + height: 0; + border-width: 20px; + border-style: solid; + border-color: transparent transparent red transparent; +} +``` + +**一个满屏 品 字布局 如何设计?** + +- 简单的方式: + - 上面的div宽100%, + - 下面的两个div分别宽50%, + - 然后用float或者inline使其不换行即可 + +**经常遇到的浏览器的兼容性有哪些?原因,解决方法是什么,常用hack的技巧 ?** + +- png24位的图片在iE6浏览器上出现背景,解决方案是做成PNG8. +- 浏览器默认的margin和padding不同。解决方案是加一个全局的*{margin:0;padding:0;}来统一 +- IE下,可以使用获取常规属性的方法来获取自定义属性,也可以使用getAttribute()获取自定义属性; +- Firefox下,只能使用getAttribute()获取自定义属性。 + - 解决方法:统一通过getAttribute()获取自定义属性 + +- IE下,even对象有x,y属性,但是没有pageX,pageY属性 +- Firefox下,event对象有pageX,pageY属性,但是没有x,y属性 + +**li与li之间有看不见的空白间隔是什么原因引起的?有什么解决办法?** + +- 行框的排列会受到中间空白(回车\空格)等的影响,因为空格也属于字符,这些空白也会被应用样式,占据空间,所以会有间隔,把字符大小设为0,就没有空格了 + +**为什么要初始化CSS样式** + +- 因为浏览器的兼容问题,不同浏览器对有些标签的默认值是不同的,如果没对CSS初始化往往会出现浏览器之间的页面显示差异 + +**对BFC规范(块级格式化上下文:block formatting context)的理解?** + +- 一个页面是由很多个 Box 组成的,元素的类型和 display 属性,决定了这个 Box 的类型 +- 不同类型的 Box,会参与不同的 Formatting Context(决定如何渲染文档的容器),因此Box内的元素会以不同的方式渲染,也就是说BFC内部的元素和外部的元素不会互相影响 + +**css定义的权重** + +``` +// 以下是权重的规则:标签的权重为1,class的权重为10,id的权重为100,以下/// 例子是演示各种定义的权重值: + +/*权重为1*/ +div{ +} +/*权重为10*/ +.class1{ +} +/*权重为100*/ +#id1{ +} +/*权重为100+1=101*/ +#id1 div{ +} +/*权重为10+1=11*/ +.class1 div{ +} +/*权重为10+10+1=21*/ +.class1 .class2 div{ +} + +// 如果权重相同,则最后定义的样式会起作用,但是应该避免这种情况出现 +``` + +**display:inline-block 什么时候会显示间隙?(携程)** + +- 移除空格、使用margin负值、使用font-size:0、letter-spacing、word-spacing + +**谈谈浮动和清除浮动** + +- 浮动的框可以向左或向右移动,直到他的外边缘碰到包含框或另一个浮动框的边框为止。由于浮动框不在文档的普通流中,所以文档的普通流的块框表现得就像浮动框不存在一样。浮动的块框会漂浮在文档普通流的块框上 + + +**介绍一下标准的CSS的盒子模型?低版本IE的盒子模型有什么不同的?** + +* 盒子模型构成:内容(content)、内填充(padding)、 边框(border)、外边距(margin) +* IE8及其以下版本浏览器,未声明 DOCTYPE,内容宽高会包含内填充和边框,称为怪异盒模型(IE盒模型) +* 标准(W3C)盒模型:元素宽度 = width + padding + border + margin +* 怪异(IE)盒模型:元素宽度 = width + margin +* 标准浏览器通过设置 css3 的 box-sizing: border-box 属性,触发“怪异模式”解析计算宽高 + +**box-sizing 常用的属性有哪些?分别有什么作用?** + +* box-sizing: content-box; // 默认的标准(W3C)盒模型元素效果 +* box-sizing: border-box; // 触发怪异(IE)盒模型元素的效果 +* box-sizing: inherit; // 继承父元素 box-sizing 属性的值 + +**CSS选择器有哪些?** + +* id选择器 #id +* 类选择器 .class +* 标签选择器 div, h1, p +* 相邻选择器 h1 + p +* 子选择器 ul > li +* 后代选择器 li a +* 通配符选择器 * +* 属性选择器 a[rel='external'] +* 伪类选择器 a:hover, li:nth-child + +**CSS哪些属性可以继承?哪些属性不可以继承?** + +* 可以继承的样式:font-size、font-family、color、list-style、cursor +* 不可继承的样式:width、height、border、padding、margin、background + +**CSS如何计算选择器优先?** + +* 相同权重,定义最近者为准:行内样式 > 内部样式 > 外部样式 +* 含外部载入样式时,后载入样式覆盖其前面的载入的样式和内部样式 +* 选择器优先级: 行内样式[1000] > id[100] > class[10] > Tag[1] +* 在同一组属性设置中,!important 优先级最高,高于行内样式 + +**CSS3新增伪类有哪些?** + +- :root 选择文档的根元素,等同于 html 元素 + +- :empty 选择没有子元素的元素 +- :target 选取当前活动的目标元素 +- :not(selector) 选择除 selector 元素意外的元素 + +- :enabled 选择可用的表单元素 +- :disabled 选择禁用的表单元素 +- :checked 选择被选中的表单元素 + +- :after 在元素内部最前添加内容 +- :before 在元素内部最后添加内容 + +- :nth-child(n) 匹配父元素下指定子元素,在所有子元素中排序第n +- :nth-last-child(n) 匹配父元素下指定子元素,在所有子元素中排序第n,从后向前数 +- :nth-child(odd) +- :nth-child(even) +- :nth-child(3n+1) +- :first-child +- :last-child +- :only-child + +- :nth-of-type(n) 匹配父元素下指定子元素,在同类子元素中排序第n +- :nth-last-of-type(n) 匹配父元素下指定子元素,在同类子元素中排序第n,从后向前数 +- :nth-of-type(odd) +- :nth-of-type(even) +- :nth-of-type(3n+1) +- :first-of-type +- :last-of-type +- :only-of-type + +- ::selection 选择被用户选取的元素部分 +- :first-line 选择元素中的第一行 +- :first-letter 选择元素中的第一个字符 + +**请列举几种隐藏元素的方法** + +* visibility: hidden; 这个属性只是简单的隐藏某个元素,但是元素占用的空间任然存在 +* opacity: 0; CSS3属性,设置0可以使一个元素完全透明 +* position: absolute; 设置一个很大的 left 负值定位,使元素定位在可见区域之外 +* display: none; 元素会变得不可见,并且不会再占用文档的空间。 +* transform: scale(0); 将一个元素设置为缩放无限小,元素将不可见,元素原来所在的位置将被保留 +* `