Vue 中三要素的是什麼?Vue中如何解析模板?Vue中如何實現響應式的呢?說一下Vue的整體實現流程…… ...
1. v-bind和v-model的區別?
- v-bind用來綁定數據和屬性以及表達式,縮寫為':'
- v-model使用在表單中,實現雙向數據綁定的,在表單元素外使用不起作用
2. Vue 中三要素的是什麼?
2.1 響應式
// 如何讓實現響應式的呢?
let obj = {};
let name = 'zhangsan';
Object.defineProperties(obj, name, {get : function() {
console.log('name' , name)
}, set : function() {
console.log('name' , name)
}})
// 1. 關鍵是理解Object.defineProperty
// 2. 將data的屬性代理到vm上面的
let mv = {};
let data = {
price: 100,
name: 'zhangsan'
};
for (let key in data) {
(function (key) {
Object.defineProperty(mv, key, {
get: function () {
console.log('get val');
return data[key];
},
set: function (val) {
console.log('set val');
data[key] = val;
}
})
})(key);
}
2.2 Vue中如何解析模板?
2.2.1 模板是什麼?
<div id="app">
<div>
<input v-model="title">
<button v-on:click="add">submit</button>
</div>
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
</div>
// 1(*****). 模板實際上就是一個字元串………………(vue中的模板的本質)
// 2. 模板有邏輯,如v-if, v-for
// 3. 與html格式很像,但是有很大的區別
// 4. 最終還是要轉換為html來顯示
// 5(*****). 模板最終必須轉換成JS代碼,因為:
// (1)有邏輯(v-if v-for):必須用JS才能實現(圖靈完備)
// (2) 轉換成HTML來渲染頁面,必須用JS才能實現
// (3) 因此,模板最終要轉換成為一個JS函數(render函數)
2.3 render函數?
2.3.1 with的用法
var obj = {
name: 'zhangsan',
age: 20,
getAddress(){
alert('shanghai');
}
}
// 不使用with
function fn() {
alert(obj.name);
alert(obj.age);
obj.getAddress();
}
// 使用with(代碼不易維護!!!)
function fn1() {
with(obj){
alert(name);
alert(age);
getAddress();
}
}
fn();
fn1();
2.3.2 render函數的實現機制?
<div id='app'>
<p>{{price}}</p>
</div>
// 使用with限制這個作用域裡面的this
with(this) {
return _c( // this._c
'div',
{
attrs: {"id" : "app"} // id=app
},
[
_c('p', [_v(_s(price))]) // this._c('p', [_v(_s(price))])
]
)
}
// 實現一個自己的render函數
var vm = new Vue({
el: '#app',
data: {
price: 100
}
});
function render() {
with (vm) {
return _c(
'div',
{
attrs: {'id': 'app'}
},
[
_c('p', [_v(_s(price))])
]
);
}
}
function render() {
return vm._c(
'div',
{
attrs: {'id': 'app'}
},
[
// vm._v 轉換為一個文本節點
// vm._s 轉換為一個字元串
// vm._c 轉換為一個DOM節點
vm._c('p', [vm._v(vm._s(price))])
]
);
}
2.3.3 render函數與vdom?
<div id="app">
<div>
<input type="text" v-model="title">
<button @click="add">submit</button>
</div>
<div>
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</div>
with (this) {
// this 就是vm
return _c(
'div',
{attrs: {"id": "app"}},
[
_c('div',
[
_c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (title),
expression: "title"
}],
attrs: {"type": "text"},
domProps: {"value": (title)},
on: {
"input": function ($event) {
if ($event.target.composing) return;
title = $event.target.value
}
}
}),
_v(" "),
_c('button',
{
on: {
"click": add
}
},
[_v("submit")]
)
]
),
_v(" "),
_c('div',
[
_c(
'ul',
// 這裡返回的是一個數組(li標簽組成的數組)
_l((list), function (item) {
return _c('li', [_v(_s(item))])
}), 0
)
]
)
]
)
}
// view ---> data ---> 使用input的事件綁定 ---> 更新頁面數據到data
// data ---> view ---> defineProperty ---> 同步數據到頁面
2.3.4 vm._c是什麼,render函數返回了什麼?
- vdom: 使用js模擬DOm結構
- snabbdom: h函數和patch函數
Vue中的v_c:就是相當於snabbdom函數的h函數
patch函數:
vm._update(vnode) {
const prevNode = vm._vnode;
vm._vnode = vnode;
if (!prevNode) {
// 首次渲染的時候
vm.$el = vm.__patch__(vm.$el, vnode);
}
else{
vm.$el = vm.__patch__(prevNode, vnode);
}
}
// 開始更新vue組件(修改data的屬性的時候,Object.defineProperty)
function updateComponent() {
vm._update(vm._render());
}
******(問題總結)
- vue模板:字元串,有邏輯,嵌入JS變數……
- 模板必須轉換為JS代碼(有邏輯的,渲染html,js變數)
- render函數是什麼樣子的
- render函數的執行結果是返回的vnode
- updateComponent
3. Vue的整個實現流程源碼解讀???(總結點)
3.1 解析模板成render函數
<template></template> --->>> render 函數
[!NOTE]
- with函數的使用
- 模板中的所有信息都被render函數包含
- 模板中用到的data中的屬性,都變成了JS變數
- 模板中的v-model v-for v-on都變成了JS邏輯
- render函數返回vnode
3.2 響應式開始監聽數據
- Object.defineProperty
- 將data的屬性代理到vm上
with(vm) {
}
3.3 首次渲染,顯示頁面,且綁定依賴
[!NOTE]
- 初次渲染,執行updateComponent, 執行vm._render()
- 執行render函數,會訪問到vm.list和vm.title屬性
- 會被響應式的get方法監聽到(Object.defineProperty)
Object.defineProperty(mv, key, {
get: function() {
return data[key];
}
})
- 執行updateComponent, 會執行vdom的patch方法
- patch將vnode渲染成DOM,初次渲染完成
3.4 為何要監聽get, 直接監聽set不行嗎?
[!NOTE]
- data中有很多屬性,有些會被用到,有些可能不被用到
- 被用到的會走到get, 不被用到的不會走到get
- 未走到get中的屬性,set的時候我們也無需關心
- 避免不必要的重覆渲染(關鍵點)
vm._update(vnode) {
const prevNode = vm._vnode;
vm._vnode = vnode;
if (!prevNode) {
// 首次渲染的時候
vm.$el = vm.__patch__(vm.$el, vnode);
}
else{
vm.$el = vm.__patch__(prevNode, vnode);
}
}
// 開始更新vue組件(修改data的屬性的時候,Object.defineProperty)
function updateComponent() {
vm._update(vm._render());
}
3.5 data屬性變化,觸發rerender函數
Object.defineProperty(mv, key, {
set: function(newVal) {
data[key] = newVal;
// 開始執行
updateComponnet()
}
})
[!NOTE]
- 修改屬性,被響應式的set監聽到
- set中執行updateComponnet
- updateComponent重新執行vm._render()
- 生成的vnode和preVnode, 通過patch進行對比
- 渲染到html中去
4. 說一下從使用jQuery和使用框架的區別?
- 數據和視圖的分離(代碼解耦)---開房封閉原則
- 數據驅動視圖,只關係數據變化,DOM操作被封裝
5. 說一下對MVVM的理解?
- MVC
- MVVM
- 關於ViewModel
6. Vue中如何實現響應式的呢?
- 響應式
- 模板引擎
- 渲染(首次渲染,後面的渲染)
- Object.defineProperty
- data的屬性代理到vm上面(with)
7. Vue中是如何解析模板的呢?
- 模板的本質就是字元串(有邏輯)
- 模板必須要轉換為JS代碼
- render函數的實現(返回的是一個vnode)
- updateComponnet(patch函數)
8. 說一下Vue的整體實現流程?
- 解析模板成為render函數
- 響應式開始監聽數據
- 首次渲染,顯示頁面,且綁定依賴
- data屬性數據發生變化,重新觸發惹人的人、函數
9. vue的數據劫持以及操作數組的坑?
給data添加新屬性的時候vm.$set(vm.info,'newKey','newValue')
data上面屬性值是數組的時候,需要用數組的方法操作數組,而不能通過index或者length屬性去操作數組,因為監聽不到屬性操作的動作。
10. 談一下對mvvm和mvc的理解?
- mvc其實是model view Model傳統所有邏輯在controller,難以維護。用戶輸入 => 控制器 => 數據改變,如果數據變了需要獲取dom,操作屬性,再渲染到視圖上。
- mvvm其實是model view viewModel數據變化驅動視圖。數據變了,不需要你獲取dom,然後改變dom的內容。這邊數據變了,vm負責監聽,視圖那邊自動發生變化。最明顯的是不需要document.querySelector之類的了。
11. vm的實質?
[!NOTE]
上面說了vm負責讓數據變了,視圖能自動發生變化。這麼神奇魔術背後的原理是Object.defineProperty。其實就是屬性的讀取和設置操作都進行了監聽,當有這樣的操作的時候,進行某種動作。來一個demo玩下。
// 對obj上面的屬性進行讀取和設置監聽
let obj = {
name:'huahua',
age:18
}
function observer(obj){
if(typeof obj === 'object'){
for (const key in obj) {
defineReactive(obj,key,obj[key])
}
}
}
// get的return的值才是最終你讀取到的值。所以設的值是為讀取準備的。
// set傳的參數是設置的值,註意這裡不要有obj.name = newVal 這樣又觸發set監聽,會死迴圈的。
function defineReactive(obj,key,value){
Object.defineProperty(obj,key,{
get:function(){
console.log('你在讀取')
// happy的話這邊可以value++,這樣你發現讀取的值始終比設置的大一個,因為return就是讀取到的值
return value
},
set:function(newVal){
console.log('數據更新了')
value = newVal
}
})
}
observer(obj)
obj.age = 2
console.log(obj.age)
12. defineReactive的實現(響應式手寫實現)?
[!NOTE]
在瀏覽器執行的時候,控制台隨手也可以obj.name="hua1"類似的操作,發現都監聽到了。但是如果更深一步,obj.name={firstname:'hua',lastname:'piaoliang'};obj.name.lastname='o'就不能監聽到屬性修改了。因為並沒有將新的賦值對象監聽其屬性。所以函數需要改進。
需要在defineReactive的第一行加上observer(value)。設置值的時候如果是對象的話,也需要將這個對象數據劫持。同理,set那邊也需要加這行。
12.1 基礎實現
function defineReactive(obj,key,value){
// 註意這裡!!!!!!!
observer(value)
Object.defineProperty(obj,key,{
get:function(){
return value
},
set:function(newVal){
// 註意這裡!!!!!!!
observer(newVal)
console.log('數據更新了')
value = newVal
}
})
}
12.2 數組方法的劫持
如果obj.name=[1,2,3];obj.name.push(4)發現又沒有通知了,這是因為Object.defineProperty不支持監聽數組變化。所以需要重寫數組上面的方法。
// 把數組上大部分方法重寫了,這裡不一一列舉。但是如果你 [1,2].length--,這是捕捉不到的
let arr = ['push','slice','split']
arr.forEach(method=>{
let oldPush = Array.property[method]
Array.property[method] = function(value){
console.log('數據更新')
oldPush.call(this, value)
}
})
12.3 vue 的雙向綁定的原理是什麼(常考)
vue.js 是採用數據劫持結合發佈者-訂閱者模式的方式,通過 Object.defineProperty()來劫持各個屬性的 setter,getter,在數據變動時發佈消息給訂閱者,觸發相應的監聽回調。
第一步:需要 observe 的數據對象進行遞歸遍歷,包括子屬性對象的屬性,都加上 setter 和 getter 這樣的話,給這個對象的某個值賦值,就會觸發 setter,那麼就能監聽到了數據變化
第二步:compile 解析模板指令,將模板中的變數替換成數據,然後初始化渲染頁面視圖,並將每個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變動,收到通知,更新視圖
- 第三步:Watcher 訂閱者是 Observer 和 Compile 之間通信的橋梁,主要做的事情是:
- 在自身實例化時往屬性訂閱器(dep)裡面添加自己
- 自身必須有一個 update()方法
- 待屬性變動 dep.notice()通知時,能調用自身的 update() 方法,並觸發 Compile 中綁定的回調,則功成身退。
第四步:MVVM 作為數據綁定的入口,整合 Observer、Compile 和 Watcher 三者,通過 Observer 來監聽自己的 model 數據變化,通過 Compile 來解析編譯模板指令,最終利用 Watcher 搭起 Observer 和 Compile 之間的通信橋梁,達到數據變化 -> 視圖更新;視圖交互變化(input) -> 數據 model 變更的雙向綁定效果。
13. Vue如何監聽數組數據變化?
13.1 vm.\(set方法 因為是一開始就數據劫持了。所以後來如果新綁定屬性,是沒有數據劫持的。如果需要調用 vm.\)set(vm.info,'newKey','newValue'),vm是vue的實例。
13.2 使用數組的方法
當屬性值是數組,數組變化的時候,跟蹤不到變化。因為數組雖然是對象,但是Object.defineProperty不支持數組,所以vue改寫了數組的所有方法,當調用數組方法的時候,就調動變動事件。但是不能通過屬性或者索引控制數組,比如length,index。
[!NOTE]
總結:data上,綁定所有屬性避免後期加新屬性。如果是數組,只能通過數組方法修改數組。如下例子,控制台vm.arr--發現視圖並不會變化,vm.arr.push(4)就能變化
<div id="app">{{msg}}{{arr}}</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
let vm = new Vue({
el:'#app',
// template加上之後會替換掉#app這個標簽
// template:'<h1>en</h1>',
data:{msg:'msg',arr:[1,2,3]}
})
vm.msg = 'msg'
</script>
14. vue 的優點和缺點是什麼?
14.1 優點
- 低耦合。視圖(View)可以獨立於 Model 變化和修改,一個 ViewModel 可以綁定到不同的"View"上,當 View 變化的時候 Model 可以不變,當 Model 變化的時候 View 也可以不變。
- 可重用性。你可以把一些視圖邏輯放在一個 ViewModel 裡面,讓很多 view 重用這段視圖邏輯。
- 獨立開發。開發人員可以專註於業務邏輯和數據的開發(ViewModel),設計人員可以專註於頁面設計,使用 Expression Blend 可以很容易設計界面並生成 xml 代碼。
- 可測試。界面素來是比較難於測試的,而現在測試可以針對 ViewModel 來寫。
14.2 缺點(面試常考)
- 網站SEO問題
- 瀏覽器相容性問題
- 海量數據節點的渲染問題
15. 請詳細說下你對 vue 生命周期的理解?
總共分為 8 個階段創建前/後,載入前/後,更新前/後,銷毀前/後。
- 創建前/後: 在 beforeCreate 階段,vue 實例的掛載元素 el 還沒有, created階段。
- 載入前/後:在 beforeMount 階段,vue 實例的$el 和 data 都初始化了,但還是掛載之前為虛擬的 dom 節點,data.message 還未替換。在 mounted 階段,vue 實例掛載完成,data.message 成功渲染。
- 更新前/後:當 data 變化時,會觸發beforeUpdate 和 updated方法。
- 銷毀前/後:在執行 destroy 方法後,對 data 的改變不會再觸發周期函數,說明此時 vue 實例已經解除了事件監聽以及和 dom 的綁定,但是 dom 結構依然存在
16. Vue組件之間的傳值?
16.1. 父組件與子組件傳值
//父組件通過標簽上面定義傳值
<template>
<Main :obj="data"></Main>
</template>
<script>
//引入子組件
import Main form "./main"
exprot default{
name:"parent",
data(){
return {
data:"我要向子組件傳遞數據"
}
},
//初始化組件
components:{
Main
}
}
</script>
//子組件通過props方法接受數據
<template>
<div>{{data}}</div>
</template>
<script>
exprot default{
name:"son",
//接受父組件傳值
props:["data"]
}
</script>
16.2 子組件向父組件傳遞數據
//子組件通過$emit方法傳遞參數
<template>
<div v-on:click="events"></div>
</template>
<script>
//引入子組件
import Main form "./main"
exprot default{
methods:{
events:function(){
}
}
}
</script>
//
<template>
<div>{{data}}</div>
</template>
<script>
exprot default{
name:"son",
//接受父組件傳值
props:["data"]
}
</script>
17. Vue路由相關問題
17.1 active-class 是哪個組件的屬性?
vue-router 模塊的 router-link 組件。
17.2 嵌套路由怎麼定義?
在實際項目中我們會碰到多層嵌套的組件組合而成,但是我們如何實現嵌套路由呢?因此我們需要在 VueRouter 的參數中使用 children 配置,這樣就可以很好的實現路由嵌套。
index.html,只有一個路由出口
<div id="app">
<!-- router-view 路由出口, 路由匹配到的組件將渲染在這裡 -->
<router-view></router-view>
</div>
main.js,路由的重定向,就會在頁面一載入的時候,就會將 home 組件顯示出來,因為重定向指向了 home 組件,redirect 的指向與 path 的必須一致。children 裡面是子路由,當然子路由裡面還可以繼續嵌套子路由。
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
//引入兩個組件
import home from "./home.vue"
import game from "./game.vue"
//定義路由
const routes = [
{ path: "/", redirect: "/home" },//重定向,指向了home組件
{
path: "/home", component: home,
children: [
{ path: "/home/game", component: game }
]
}
]
//創建路由實例
const router = new VueRouter({routes})
new Vue({
el: '#app',
data: {
},
methods: {
},
router
})
home.vue,點擊顯示就會將子路由顯示在出來,子路由的出口必須在父路由裡面,否則子路由無法顯示。
17.3 路由之間跳轉?
- 聲明式(標簽跳轉)
<router-link :to="index">
- 編程式( js 跳轉)
router.push('index')
17.4 懶載入(按需載入路由)(常考)
webpack 中提供了 require.ensure()來實現按需載入。以前引入路由是通過 import 這樣的方式引入,改為 const 定義的方式進行引入。
17.4.1 不進行頁面按需載入引入方式
import home from '../../common/home.vue'
17.4.2 進行頁面按需載入的引入方式
const home = r => require.ensure( [], () => r (require('../../common/home.vue')))
17.5 vue-router 有哪幾種導航鉤子?
- 全局導航鉤子
- router.beforeEach(to, from, next),
- router.beforeResolve(to, from, next),
- router.afterEach(to, from ,next)
- 組件內鉤子
- beforeRouteEnter,
- beforeRouteUpdate,
- beforeRouteLeave
- 單獨路由獨享組件
- beforeEnter
18. Vux相關問題
18.1 vuex 是什麼?怎麼使用?哪種功能場景使用它?
vue 框架中狀態管理。在 main.js 引入 store,註入。新建了一個目錄 store,….. export 。場景有:單頁應用中,組件之間的狀態。音樂播放、登錄狀態、加入購物車
// 新建 store.js
import vue from 'vue'
import vuex form 'vuex'
vue.use(vuex)
export default new vuex.store({
//...code
})
//main.js
import store from './store'
...
18.2 vuex 有哪幾種屬性?
有 5 種,分別是 state、getter、mutation、action、module
18.3 vuex 的 store 特性是什麼
- vuex 就是一個倉庫,倉庫里放了很多對象。其中 state 就是數據源存放地,對應於一般 vue 對象裡面的 data
- state 裡面存放的數據是響應式的,vue 組件從 store 讀取數據,若是 store 中的數據發生改變,依賴這相數據的組件也會發生更新
- 它通過 mapState 把全局的 state 和 getters 映射到當前組件的 computed 計算屬性
18.4 vuex 的 getter 特性是什麼
- getter 可以對 state 進行計算操作,它就是 store 的計算屬性
- 雖然在組件內也可以做計算屬性,但是 getters 可以在多給件之間復用
- 如果一個狀態只在一個組件內使用,是可以不用 getters
18.5 vuex 的 mutation 特性是什麼
- action 類似於 muation, 不同在於:action 提交的是 mutation,而不是直接變更狀態
- action 可以包含任意非同步操作
18.6 vue 中 ajax 請求代碼應該寫在組件的 methods 中還是 vuex 的 action 中
如果請求來的數據不是要被其他組件公用,僅僅在請求的組件內使用,就不需要放入 vuex 的 state 里
如果被其他地方復用,請將請求放入 action 里,方便復用,並包裝成 promise 返回
18.7 不用 vuex 會帶來什麼問題
- 可維護性會下降,你要修改數據,你得維護 3 個地方
- 可讀性下降,因為一個組件里的數據,你根本就看不出來是從哪裡來的
- 增加耦合,大量的上傳派發,會讓耦合性大大的增加,本來 Vue 用 Component 就是為了減少耦合,現在這麼用,和組件化的初衷相背
18.8 vuex 原理
vuex 僅僅是作為 vue 的一個插件而存在,不像 Redux,MobX 等庫可以應用於所有框架,vuex 只能使用在 vue 上,很大的程度是因為其高度依賴於 vue 的 computed 依賴檢測系統以及其插件系統,
vuex 整體思想誕生於 flux,可其的實現方式完完全全的使用了 vue 自身的響應式設計,依賴監聽、依賴收集都屬於 vue 對對象 Property set get 方法的代理劫持。最後一句話結束 vuex 工作原理,vuex 中的 store 本質就是沒有 template 的隱藏著的 vue 組件;
18.9 擴展問題
18.9.1 使用 Vuex 只需執行 Vue.use(Vuex),併在 Vue 的配置中傳入一個 store 對象的示例,store 是如何實現註入的?
Vue.use(Vuex) 方法執行的是 install 方法,它實現了 Vue 實例對象的 init 方法封裝和註入,使傳入的 store 對象被設置到 Vue 上下文環境的\(store 中。因此在 Vue Component 任意地方都能夠通過 this.\)store 訪問到該 store。
18.9.2 state 內部支持模塊配置和模塊嵌套,如何實現的?
在 store 構造方法中有 makeLocalContext 方法,所有 module 都會有一個 local context,根據配置時的 path 進行匹配。所以執行如 dispatch('submitOrder', payload)這類 action 時,預設的拿到都是 module 的 local state,如果要訪問最外層或者是其他 module 的 state,只能從 rootState 按照 path 路徑逐步進行訪問。
18.9.3 在執行 dispatch 觸發 action(commit 同理)的時候,只需傳入(type, payload),action 執行函數中第一個參數 store 從哪裡獲取的?
store 初始化時,所有配置的 action 和 mutation 以及 getters 均被封裝過。在執行如 dispatch('submitOrder', payload)的時候,actions 中 type 為 submitOrder 的所有處理方法都是被封裝後的,其第一個參數為當前的 store 對象,所以能夠獲取到 { dispatch, commit, state, rootState } 等數據。
18.9.4 Vuex 如何區分 state 是外部直接修改,還是通過 mutation 方法修改的?
Vuex 中修改 state 的唯一渠道就是執行 commit('xx', payload) 方法,其底層通過執行 this._withCommit(fn) 設置_committing 標誌變數為 true,然後才能修改 state,修改完畢還需要還原_committing 變數。外部修改雖然能夠直接修改 state,但是並沒有修改_committing 標誌位,所以只要 watch 一下 state,state change 時判斷是否_committing 值為 true,即可判斷修改的合法性。
18.9.5 調試時的"時空穿梭"功能是如何實現的?
devtoolPlugin 中提供了此功能。因為 dev 模式下所有的 state change 都會被記錄下來,'時空穿梭' 功能其實就是將當前的 state 替換為記錄中某個時刻的 state 狀態,利用 store.replaceState(targetState) 方法將執行 this._vm.state = state 實現。
19. 指令相關
19.1 自定義指令(v-check, v-focus) 的方法有哪些? 它有哪些鉤子函數? 還有哪些鉤子函數參數
- 全局定義指令:在 vue 對象的 directive 方法裡面有兩個參數, 一個是指令名稱, 另一個是函數。
- 組件內定義指令:directives
- 鉤子函數: bind(綁定事件出發)、inserted(節點插入時候觸發)、update(組件內相關更新)
- 鉤子函數參數: el、binding
19.2 說出至少 4 種 vue 當中的指令和它的用法
v-if(判斷是否隱藏)、v-for(把數據遍歷出來)、v-bind(綁定屬性)、v-model(實現雙向綁定)
20. axios
20.1 axios 是什麼?怎麼使用?描述使用它實現登錄功能的流程
[!NOTE]
思路:使用Vue的router.beforeEach鉤子函數結合axios的攔截器功能來實現。
20.2 axios與ajax, fetch的區別和優缺點?
- 參考文章:https://blog.csdn.net/qq_36407875/article/details/84642060