摘要:超出此時間則渲染錯誤組件。元素節點總共有種類型,為表示是普通元素,為表示是表達式,為表示是純文本。
實戰 - 插件 form-validate
i18n
實戰 - 組件{{ $t("welcome-message") }}
Vue組件=Vue實例=new Vue(options)
屬性
自定義屬性 props:組件props中聲明的屬性
原生屬性 attrs:沒有聲明的屬性,默認自動掛載到組件根元素上
特殊屬性 class,style:掛載到組件根元素上
事件
普通事件 @click,@input,@change,@xxx 通過this.$emit("xxx")觸發
修飾符事件 @input.trim @click.stop
插槽
v-slot:xxx
v-slot:xxx="props"
相同名稱的插槽替換
動態導入/延遲加載組件基于路由拆分
const routes = [ { path: /foo", component: () => import("./RouteComponent.vue") } ]函數式組件
無狀態 、實例、this上下文、生命周期
臨時變量組件批量渲染標簽組件{{ var1 }} {{ var2 }}
封裝應用實例化函數
function createApp ({ el, model, view, actions }) { const wrappedActions={} Object.keys(actions).forEach(key=>{ const originalAction=actions[key] wrappedActions[key]=()=>{ const nextModel=originalAction(model) vm.model=nextModel } }) const vm=new Vue({ el, data:{model}, render(h){ return view(h,this.model,wrappedActions) } }) } createApp({ el: "#app", model: { count: 0 }, actions: { inc: ({ count }) => ({ count: count + 1 }), dec: ({ count }) => ({ count: count - 1 }) }, view: (h, model, actions) => h("div", { attrs: { id: "app" }}, [ model.count, " ", h("button", { on: { click: actions.inc }}, "+"), h("button", { on: { click: actions.dec }}, "-") ]) })高階組件
異步組件 這是一個具名插槽
const AsyncComp = () => ({ // 需要加載的組件。應當是一個 Promise component: import("./MyComp.vue"), // 加載中應當渲染的組件 loading: LoadingComp, // 出錯時渲染的組件 error: ErrorComp, // 渲染加載中組件前的等待時間。默認:200ms。 delay: 200, // 最長等待時間。超出此時間則渲染錯誤組件。默認:Infinity timeout: 3000 }) Vue.component("async-example", AsyncComp)實戰 - 組件通信 父傳子props
子傳父組件$emit, $on, $off
- {{item}}
在組件中,可以使用 $emit, $on, $off 分別來分發、監聽、取消監聽事件
// NewTodoInput --------------------- // ... methods: { addTodo: function () { eventHub.$emit("add-todo", { text: this.newTodoText }) this.newTodoText = "" } } // DeleteTodoButton --------------------- // ... methods: { deleteTodo: function (id) { eventHub.$emit("delete-todo", id) } } // Todos --------------------- // ... created: function () { eventHub.$on("add-todo", this.addTodo) eventHub.$on("delete-todo", this.deleteTodo) }, // 最好在組件銷毀前 // 清除事件監聽 beforeDestroy: function () { eventHub.$off("add-todo", this.addTodo) eventHub.$off("delete-todo", this.deleteTodo) }, methods: { addTodo: function (newTodo) { this.todos.push(newTodo) }, deleteTodo: function (todoId) { this.todos = this.todos.filter(function (todo) { return todo.id !== todoId }) } }內置$parent、$children、$ref;$attrs和$listeners
$ref ref="xxx"
$parent / $children:訪問父 / 子實例
高階插件/組件庫 provide & inject(observable)
// 父級組件提供 "foo" var Provider = { provide: { foo: "bar" }, // ... } // 子組件注入 "foo" var Child = { inject: ["foo"], created () { console.log(this.foo) // => "bar" } // ... }
// 父級組件提供 "state" var Provider = { provide: { state = Vue.observable({ count: 0 }) }, // ... } // 子組件注入 "foo" var Child = { inject: ["state"], created () { console.log(this.state) // => { count: 0 } } // ... }全局對象 Event Bus
const state={count:0} const Counter = { data(){return state}, render:h=>h("div",state.count) } new Vue({ el: "#app", components:{Counter}, methods:{ inc(){ state.count++ } } })
//中央事件總線 var bus = new Vue(); var app = new Vue({ el: "#app", template: `自實現boradcast和dispatch` }); // 在組件 brother1 的 methods 方法中觸發事件 bus.$emit("say-hello", "world"); // 在組件 brother2 的 created 鉤子函數中監聽事件 bus.$on("say-hello", function(arg) { console.log("hello " + arg); // hello world });
**$dispatch 和 $broadcast 已經被棄用,使用
Vuex代替**
以下自實現參考 iview/emitter.js at 2.0 · iview/iview -github
function broadcast(componentName, eventName, params) { this.$children.forEach(child => { const name = child.$options.name; if (name === componentName) { child.$emit.apply(child, [eventName].concat(params)); } else { // todo 如果 params 是空數組,接收到的會是 undefined broadcast.apply(child, [componentName, eventName].concat([params])); } }); } export default { methods: { dispatch(componentName, eventName, params) { let parent = this.$parent || this.$root; let name = parent.$options.name; while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name; } } if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } }, broadcast(componentName, eventName, params) { broadcast.call(this, componentName, eventName, params); } } };原理 - 響應式
單向數據流,雙向綁定語法糖
demo原理(phoneInfo=val)" :zip-code="zipCode" @update:zipCode="val=>(zipCode=val)" />
數據劫持+觀察訂閱模式:
在讀取屬性的時候依賴收集,在改變屬性值的時候觸發依賴更新
實現一個observer,劫持對象屬性
實現一個全局的訂閱器Dep,可以追加訂閱者,和通知依賴更新
在讀取屬性的時候追加當前依賴到Dep中,在設置屬性的時候循環觸發依賴的更新
new Vue(options)創建實例的時候,initData()進行數據劫持
通過Object.defineProperty(obj,key,desc)對data進行數據劫持,即創建get/set函數
這里需要考慮對對象的以及對數組的數據劫持(支持 push,pop,shift,unshift,splice,sort,reverse,不支持 filter,concat,slice)
對象遞歸調用
數組變異方法的解決辦法:代理原型/實例方法
避免依賴重讀Observer
// 全局的依賴收集器Dep window.Dep = class Dep { constructor() {this.subscribers = new Set()} depend() {activeUpdate&&this.subscribers.add(activeUpdate)} notify() {this.subscribers.forEach(sub =>sub())} } let activeUpdate function autorun(update) { function wrapperUpdate() { activeUpdate = wrapperUpdate update() activeUpdate = null } wrapperUpdate() } function observer(obj) { Object.keys(obj).forEach(key => { var dep = new Dep() let internalValue = obj[key] Object.defineProperty(obj, key, { get() { // console.log(`getting key "${key}": ${internalValue}`) // 將當前正在運行的更新函數追加進訂閱者列表 activeUpdate&&dep.depend() //收集依賴 return internalValue }, set(newVal) { //console.log(`setting key "${key}" to: ${internalValue}`) // 加個if判斷,數據發生變化再觸發更新 if(internalValue !== newVal) { internalValue = newVal dep.notify() // 觸發依賴的更新 } } }) }) } let state = {count:0} observer(state); autorun(() => { console.log("state.count發生變化了", state.count) }) state.count = state.count + 5; // state.count發生變化了 0 // state.count發生變化了 5MVVM實現
實現mvvm-github
Model-View-ViewModel,其核心是提供對View 和 ViewModel 的雙向數據綁定,這使得ViewModel 的狀態改變可以自動傳遞給 View
observerproxy 方法遍歷 data 的 key,把 data 上的屬性代理到 vm 實例上(通過Object.defineProperty 的 getter 和 setter )
observe(data, this) 給 data 對象添加 Observer做監聽。
創建一個 Observer 對象
創建了一個 Dep 對象實例(觀察者模式)
遍歷data,convert(defineReactive) 方法使他們有getter、setter
getter 和 setter 方法調用時會分別調用 dep.depend 方法和 dep.notify
depend:把當前 Dep 的實例添加到當前正在計算的Watcher 的依賴中
notify:遍歷了所有的訂閱 Watcher,調用它們的 update 方法
computedcomputed初始化被遍歷computed,使用Object.defineProperty進行處理,依賴收集到Dep.target
觸發data值時會觸發Watcher監聽函數
computed值緩存 watcher.dirty決定了計算屬性值是否需要重新計算,默認值為true,即第一次時會調用一次。每次執行之后watcher.dirty會設置為false,只有依賴的data值改變時才會觸發
mixin全局注冊的選項,被引用到你的每個組件中
1、Vue.component 注冊的 【全局組件】
2、Vue.filter 注冊的 【全局過濾器】
3、Vue.directive 注冊的 【全局指令】
4、Vue.mixin 注冊的 【全局mixin】
合并權重
1、組件選項
2、組件 - mixin
3、組件 - mixin - mixin
4、.....
x、全局 選項
函數合并疊加(data,provide)
數組疊加(created,watch)
原型疊加(components,filters,directives)
兩個對象合并的時候,不會相互覆蓋,而是 權重小的 被放到 權重大 的 的原型上
覆蓋疊加(props,methods,computed,inject)
兩個對象合并,如果有重復key,權重大的覆蓋權重小的
直接替換(el,template,propData 等)
something | myFilter 被解析成_f("myFilter")( something )
nextTickVue.js 在默認情況下,每次觸發某個數據的 setter 方法后,對應的 Watcher 對象其實會被 push 進一個隊列 queue 中,在下一個 tick 的時候將這個隊列 queue 全部拿出來 run( Watcher 對象的一個方法,用來觸發 patch 操作) 一遍。
原理 - virtaul DOM真實DOM操作昂貴,虛擬DOM就是js對象,操作代價小
模板編譯&渲染平時開發寫vue文件都是用模板template的方法寫html,模板會被編譯成render函數,流程如下:
初始化的時候
- 模板會被編譯成render函數 - render函數返回虛擬DOM - 生成真正的DOM
數據更新的時候
- render函數返回新的virtual Dom - 新的virtual Dom和舊的virtual Dom做diff - 將差異運用到真實DOM
render API
//template, jsx, render本質都是一樣的, 都是一種dom和數據狀態之間關系的表示 render(h) { h(tag, data, children) } // tag可以是原生的html標簽 render(h) { return h("div", { attrs: {}}, []) } // 也可以是一個vue component import vueComponent from "..." render(h) { h(vueComponent, { props: {} }) }
偏邏輯用render,偏視圖用template
compilecompile 編譯可以分成 parse、 optimize 與 generate 三個階段,最終需要得到 render function。
parse:會用正則等方式解析 template 模板中的指令、class、style 等數據,形成 AST。
transclude(el, option) 把 template 編譯成一段 document fragment
compileNode(el, options) 深度遍歷DOM,正則解析指令
vm.bindDir(descriptor, node, host, scope) 根據 descriptor 實例化不同的 Directive 對象
Directive 在初始化時通過 extend(this, def) 擴展 bind 和 update,創建了 Watcher關聯update
解析模板字符串生成 AST `const ast = parse(template.trim(), options)
循環解析 template,利用正則表達式順序解析模板,當解析到開始標簽、閉合標簽、文本的時候都會分別執行對應的回調函數,來達到構造 AST 樹的目的。 AST 元素節點總共有 3 種類型,type 為 1 表示是普通元素,為 2 表示是表達式,為 3 表示是純文本。
optimize:標記 static 靜態節點,而減少了比較的過程 等優化
優化語法樹 optimize(ast, options)
深度遍歷這個 AST 樹,去檢測它的每一顆子樹是不是靜態節點,如果是靜態節點則它們生成 DOM 永遠不需要改變(標記靜態節點 markStatic(root);標記靜態根 markStaticRoots(root, false))
generate:是將 AST 轉化成 render function 字符串
AST轉可執行的代碼 const code = generate(ast, options)
vue模板編譯前后:
`
對比react的jsx編譯前后
`diffHello World` h("div",{ id: "1", "class": "li-1" },"Hello World", h(MyComp, null) )
vnode
{ 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 (oldVnode, vnode)
key和sel相同才去比較,否則新替舊
patchVnode (oldVnode, vnode)節點比較5種情況
if (oldVnode === vnode),他們的引用一致,可以認為沒有變化
if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本節點的比較,需要修改,則會調用Node.textContent = vnode.text
if( oldCh && ch && oldCh !== ch ), 兩個節點都有子節點,而且它們不一樣,這樣我們會調用updateChildren函數比較子節點
else if (ch),只有新的節點有子節點,調用createEle(vnode),vnode.el已經引用了老的dom節點,createEle函數會在老dom節點上添加子節點
else if (oldCh),新節點沒有子節點,老節點有子節點,直接刪除老節點
同層比較作用:將一棵樹轉換成另一棵樹的最小操作次數是O(n^3),同層是O(1)
key的作用:
為了在數據變化時強制更新組件,以避免“原地復用”帶來的副作用。
在交叉對比沒有結果(列表數據的重新排序,插,刪)的時候會采用key來提高這個diff速度(不設key,newCh和oldCh只會進行頭尾兩端的相互比較,設key后,除了頭尾兩端的比較外,還會從用key生成的對象oldKeyToIdx中查找匹配的節點,從而移動dom而不是銷毀再創建)
vue&react vdom區別Vue 很“ 囂張 ”,它宣稱可以更快地計算出Virtual DOM的差異,這是由于它在渲染過程中,由于vue會跟蹤每一個組件的依賴收集,通過setter / getter 以及一些函數的劫持,能夠精確地知道變化,并在編譯過程標記了static靜態節點,在接下來新的Virtual DOM 并且和原來舊的 Virtual DOM進行比較時候,跳過static靜態節點。所以不需要重新渲染整個組件樹。vdom實現React默認是通過比較引用的方式進行,當某個組件的狀態發生變化時,它會以該組件為根,重新渲染整個組件子樹。如果想避免不必要的子組件重新渲染,你需要在所有可能的地方使用PureComponent,或者手動實現shouldComponentUpdate方法。但是Vue中,你可以認定它是默認的優化。
摘自 https://juejin.im/post/5b6178...
類vue vdom
snabbdom-github
snabbdom源碼閱讀分析
Vue 2.0 的 virtual-dom 實現簡析
類react vdom
preact-github
preact工作原理
原理 - Router(路由)vue插件,通過hash /history 2中方式實現可配路由
Hash
push(): 設置新的路由添加歷史記錄并更新視圖,常用情況是直接點擊切換視圖,調用流程:
$router.push() 顯式調用方法
HashHistory.push() 根據hash模式調用,設置hash并添加到瀏覽器歷史記錄(window.location.hash= XXX)
History.transitionTo() 開始更新
History.updateRoute() 更新路由
app._route= route
vm.render() 更新視圖
replace: 替換當前路由并更新視圖,常用情況是地址欄直接輸入新地址,流程與push基本一致
但流程2變為替換當前hash (window.location.replace= XXX)
3.監聽地址欄變化:在setupListeners中監聽hash變化(window.onhashchange)并調用replace
History
push:與hash模式類似,只是將window.hash改為history.pushState
replace:與hash模式類似,只是將window.replace改為history.replaceState
監聽地址變化:在HTML5History的構造函數中監聽popState(window.onpopstate)
實戰
const Foo = { props: ["id"], template: `原理 - props(屬性)foo with id: {{id}}` } const Bar = { template: `bar` } const NotFound = { template: `not found` } const routeTable = { "/foo/:id": Foo, "/bar": Bar, } const compiledRoutes = []; Object.keys(routeTable).forEach(path => { const dynamicSegments = [] const regex = pathToRegexp(path, dynamicSegments) const component = routeTable[path] compiledRoutes.push({ component, regex, dynamicSegments }) }) window.addEventListener("hashchange", () => { app.url = window.location.hash.slice(1); }) const app = new Vue({ el: "#app", data() { return { url: window.location.hash.slice(1) } }, render(h) { const url = "/" + this.url let componentToRender let props = {} compiledRoutes.some(route => { const match = route.regex.exec(url) if (match) { componentToRender = route.component route.dynamicSegments.forEach((segment,index) => { props[segment.name] = match[index+1] }) } }) return h("div", [ h("a", { attrs: { href: "#foo/123" } }, "foo123"), "|", h("a", { attrs: { href: "#foo/234" } }, "foo234"), "|", h("a", { attrs: { href: "#bar" } }, "bar"), h(componentToRender || NotFound, { props }) ]) } })
父組件怎么傳值給子組件的 props
父組件的模板 會被解析成一個 模板渲染函數,執行時會綁定 父組件為作用域
(function() { with(this){ return _c("div",{staticClass:"a"},[ _c("testb",{attrs:{"child-name":parentName}}) ],1) } })
所以渲染函數內部所有的變量,都會從父組件對象 上去獲取
組件怎么讀取 props
子組件拿到父組件賦值過后的 attr,篩選出 props,然后保存到實例的_props 中,并逐一復制到實例上,響應式的。
父組件數據變化,子組件props如何更新
父組件數據變化,觸發set,從而通知依賴收集器的watcher重新渲染
vuex 僅僅是作為 vue 的一個插件而存在,不像 Redux,MobX 等庫可以應用于所有框架,vuex 只能使用在 vue 上,很大的程度是因為其高度依賴于 vue 的 computed 依賴檢測系統以及其插件系統,
vuex 整體思想誕生于 flux,可其的實現方式完完全全的使用了 vue 自身的響應式設計,依賴監聽、依賴收集都屬于 vue 對對象 Property set get 方法的代理劫持。vuex 中的 store 本質就是沒有 template 的隱藏著的 vue 組件;
state
this.$store.state.xxx 取值
提供一個響應式數據
vuex 就是一個倉庫,倉庫里放了很多對象。其中 state 就是數據源存放地,對應于一般 vue 對象里面的 data
state 里面存放的數據是響應式的,vue 組件從 store 讀取數據,若是 store 中的數據發生改變,依賴這相數據的組件也會發生更新
它通過 mapState 把全局的 state 和 getters 映射到當前組件的 computed 計算屬性
getter
this.$store.getters.xxx 取值
借助vue計算屬性computed實現緩存
getter 可以對 state 進行計算操作,它就是 store 的計算屬性
雖然在組件內也可以做計算屬性,但是 getters 可以在多給件之間復用
如果一個狀態只在一個組件內使用,是可以不用 getters
mutaion
this.$store.commit("xxx") 賦值
更改state方法
action 類似于 muation, 不同在于:action 提交的是 mutation,而不是直接變更狀態
action 可以包含任意異步操作
action
this.$store.dispatch("xxx") 賦值
觸發mutation方法
module
Vue.set動態添加state到響應式數據中
簡單版Vuex實現
//min-vuex import Vue from "vue" const Store = function Store (options = {}) { const {state = {}, mutations={}} = options this._vm = new Vue({ data: { $$state: state }, }) this._mutations = mutations } Store.prototype.commit = function(type, payload){ if(this._mutations[type]) { this._mutations[type](this.state, payload) } } Object.defineProperties(Store.prototype, { state: { get: function(){ return this._vm._data.$$state } } }); export default {Store}
使用 Vuex 只需執行 Vue.use(Vuex),并在 Vue 的配置中傳入一個 store 對象的示例,store 是如何實現注入的?
Vue.use(Vuex) 方法執行的是 install 方法,它實現了 Vue 實例對象的 init 方法封裝和注入,使傳入的 store 對象被設置到 Vue 上下文環境的store中。因此在VueComponent任意地方都能夠通過this.store 訪問到該 store。
state 內部支持模塊配置和模塊嵌套,如何實現的?
在 store 構造方法中有 makeLocalContext 方法,所有 module 都會有一個 local context,根據配置時的 path 進行匹配。所以執行如 dispatch("submitOrder", payload)這類 action 時,默認的拿到都是 module 的 local state,如果要訪問最外層或者是其他 module 的 state,只能從 rootState 按照 path 路徑逐步進行訪問。
在執行 dispatch 觸發 action(commit 同理)的時候,只需傳入(type, payload),action 執行函數中第一個參數 store 從哪里獲取的?
store 初始化時,所有配置的 action 和 mutation 以及 getters 均被封裝過。在執行如 dispatch("submitOrder", payload)的時候,actions 中 type 為 submitOrder 的所有處理方法都是被封裝后的,其第一個參數為當前的 store 對象,所以能夠獲取到 { dispatch, commit, state, rootState } 等數據。
Vuex 如何區分 state 是外部直接修改,還是通過 mutation 方法修改的?
Vuex 中修改 state 的唯一渠道就是執行 commit("xx", payload) 方法,其底層通過執行 this._withCommit(fn) 設置_committing 標志變量為 true,然后才能修改 state,修改完畢還需要還原_committing 變量。外部修改雖然能夠直接修改 state,但是并沒有修改_committing 標志位,所以只要 watch 一下 state,state change 時判斷是否_committing 值為 true,即可判斷修改的合法性。
調試時的"時空穿梭"功能是如何實現的?
devtoolPlugin 中提供了此功能。因為 dev 模式下所有的 state change 都會被記錄下來,"時空穿梭" 功能其實就是將當前的 state 替換為記錄中某個時刻的 state 狀態,利用 store.replaceState(targetState) 方法將執行 this._vm.state = state 實現。
原理 - SSRVue.js 是構建客戶端應用程序的框架。默認情況下,可以在瀏覽器中輸出 Vue 組件,進行生成 DOM 和操作 DOM。
然而,也可以將同一個組件渲染為服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最后將靜態標記"混合"為客戶端上完全交互的應用程序。
服務器渲染的 Vue.js 應用程序也可以被認為是"同構"或"通用",因為應用程序的大部分代碼都可以在服務器和客戶端上運行。
服務端渲染的核心就在于:通過vue-server-renderer插件的renderToString()方法,將Vue實例轉換為字符串插入到html文件
其他vue 企業級應用模板-github
VueConf 2018 杭州(第二屆Vue開發者大會)
Vue 3.0 進展 - 尤雨溪
參考資料深入響應式原理 —— Vue.js官網
Advanced Vue.js Features from the Ground Up - 尤雨溪
Vue 源碼解析:深入響應式原理 - 黃軼
Vue 源碼研究會 - 神仙朱
vue組件之間8種組件通信方式總結 - zhoulu_hp
Vue問得最多的面試題 - yangcheng
剖析 Vue.js 內部運行機制 - 染陌
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/109787.html
摘要:純分享直接上干貨操作系統并發支持進程管理內存管理文件系統系統進程間通信網絡通信阻塞隊列數組有界隊列鏈表無界隊列優先級有限無界隊列延時無界隊列同步隊列隊列內存模型線程通信機制內存共享消息傳遞內存模型順序一致性指令重排序原則內存語義線程 純分享 , 直接上干貨! 操作系統并發支持 進程管理內存管...
摘要:異步實戰狀態管理與組件通信組件通信其他狀態管理當需要改變應用的狀態或有需要更新時,你需要觸發一個把和載荷封裝成一個。的行為是同步的。所有的狀態變化必須通過通道。前端路由實現與源碼分析設計思想應用是一個狀態機,視圖與狀態是一一對應的。 React實戰與原理筆記 概念與工具集 jsx語法糖;cli;state管理;jest單元測試; webpack-bundle-analyzer Sto...
摘要:請回復這個帖子并注明組織個人信息來申請加入。版筆記等到中文字幕翻譯完畢后再整理。數量超過個,在所有組織中排名前。網站日超過,排名的峰值為。主頁歸檔社區自媒體平臺微博知乎專欄公眾號博客園簡書合作侵權,請聯系請抄送一份到贊助我們 Special Sponsors showImg(https://segmentfault.com/img/remote/1460000018907426?w=1...
摘要:主頁暫時下線社區暫時下線知識庫自媒體平臺微博知乎簡書博客園合作侵權,請聯系請抄送一份到特色項目中文文檔和教程與機器學習實用指南人工智能機器學習數據科學比賽系列項目實戰教程文檔代碼視頻數據科學比賽收集平臺,,劍指,經典算法實現系列課本課本描述 【主頁】 apachecn.org 【Github】@ApacheCN 暫時下線: 社區 暫時下線: cwiki 知識庫 自媒體平臺 ...
閱讀 658·2021-11-23 09:51
閱讀 3258·2021-10-11 10:58
閱讀 15407·2021-09-29 09:47
閱讀 3528·2021-09-01 11:42
閱讀 1281·2019-08-29 16:43
閱讀 1832·2019-08-29 15:37
閱讀 2089·2019-08-29 12:56
閱讀 1718·2019-08-28 18:21