ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

侯策《前端开发核心知识进阶》读书笔记——前端框架的共性

2020-05-04 13:56:42  阅读:256  来源: 互联网

标签:node tagName 进阶 读书笔记 Object value let key 侯策


响应式框架基本原理

响应式或数据双向绑定的框架,直观上,数据在变化时,不再需要开发者去手动更新视图,而视图会根据变化的数据“自动”进行更新。

在这个过程中,我们需要:

  • 收集视图依赖了哪些数据
  • 感知被依赖数据的变化
  • 数据变化时,自动“通知”需要更新的视图部分,并进行更新

这个思考过程换成对应的技术概念就是:

  • 依赖收集
  • 数据劫持 / 数据代理
  • 发布订阅模式

数据劫持与代理

Object.defineProperty 

感知数据变化的方法很直接,就是进行数据劫持或数据代理。我们往往通过 Object.defineProperty 实现。这个方法可以定义数据的 getter 和 setter。

对于Object.defineProperty 的参数分析:

descriptor 中结构如下:

var obj = {};
obj.a = "a";
obj['b'] = 'b';
Object.defineProperty(obj, 'c', {
    value: 'c'
});
console.log(Object.getOwnPropertyDescriptor(obj, 'c'));

通过Object.defineProperty方法可以精确的控制对象的属性,可以精确的控制对应的属性隐藏起来,序列化或者Object.keys不会枚举出来。

enumerable

var obj = {};
obj.a = "a";
obj['b'] = 'b';
Object.defineProperty(obj, 'c', {
    value: 'c',
    enumerable: false
});
for(var i in obj) {
    console.log(i);
}
console.log(Object.keys(obj));

我们隐藏了属性c,然后通过for... in ... 进行了遍历尝试,以及使用Object.keys()来进行遍历尝试,结果发现,在遍历结果中,已经没有c属性了。

writable

var obj = {};
Object.defineProperty(obj, 'c', {
    value: 'c',
    writable: false
})
obj.c = "a";
obj["c"] = "b";
Object.defineProperty(obj, 'c', {
    value: 'd'
})
console.log(obj.c);
我们创建了一个不可修改的c属性之后,通过3中不同的赋值方式,其中普通的2种方式修改之后不会出现错误,但是具体值并未改变。
但是使用Object.defineProperty来进行属性赋值时,会直接报错,错误如下如,直接告知无法重新定义,并且报错:

configurable

与writable同理,调用传统方式来进行对象属性的修改以及删除时,不会起到任何作用,但是浏览器也不会报错。
但是使用精细化控制的Object.defineProperty进行对象操作时,浏览器会直接报错。

set、get

为了更精细化的管理,Object.defineProperty 提供了对应对象属性的setter和getter函数。
我们获取对应的对象属性时,可以通过getter来进行数据的更改,而当属性值变更时,对象中的set方法会触发,这个时候就是整个系统重新渲染的时候,这也就是MVVM中双向绑定的精华所在。

var obj = {};
obj.a = "a";
Object.defineProperty(obj, 'a', {
    get: () => {
        return 'a 已经被我修改了';
    }
})
console.log(obj.a);

var obj = {};
obj.a = "a";
var temp = null;
Object.defineProperty(obj, 'a', {
    set: (newValue) => {
        console.log('原有属性值:', newValue);
        temp = newValue + '1';
        console.log('经过set方法处理的属性值:', temp);
    }
})
obj.a = 'b';

当然我们还是需要注意一点事情的,Object.defineProperty 的descriptor中存在两种描述符,数据描述符和存储描述符。在函数中,描述符必须是两种性质之一,不能同时是两者。

默认值

var obj = {};
Object.defineProperty(obj, 'a', {
    value: 'a'
})
console.log('a的descriptor:', Object.getOwnPropertyDescriptor(obj, 'a'))
obj.b = 'b';
console.log('b的descriptor:', Object.getOwnPropertyDescriptor(obj, 'b'))

如果使用Object.defineProperty来创建对象属性的话,默认是无法修改,无法枚举。
但是我们采用传统的方式创建的属性默认值则全部为true。

框架的getter和setter定义拦截

了解了Object.defineProperty,我们可以很轻松地完成对data数据的getter和setter进行定义拦截
let data = {
  stage: 'GitChat',
  course: {
    title: '前端开发进阶',
    author: 'Lucas',
    publishTime: '2018 年 5 月'
  }
}

Object.keys(data).forEach(key => {
  let currentValue = data[key]

  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: false,
    get() {
      console.log(`getting ${key} value now, getting value is:`, currentValue)
      return currentValue
    },
    set(newValue) {
      currentValue = newValue
      console.log(`setting ${key} value now, setting value is`, currentValue)
    }
  })
}) 

data.course

// getting course value now, getting value is: {title: "前端开发进阶", author: "Lucas", publishTime: "2018 年 5 月"}

data.course = '前端开发进阶2'
// setting course value now, setting value is 前端开发进阶2

但是有一个问题是对嵌套的数据类型没有进行拦截,对这种数据类型的拦截相当于深拷贝,这里就不再额外地做一个阐述了。

数组变化

即便是完成了对嵌套的数据类型的处理,数组 push 行为并没有被拦截。这是因为 Array.prototype 上挂载的方法并不能触发 data.course.author 属性值的 setter,由于这并不属于做赋值操作,而是 push API 调用操作。然而对于框架实现来说,这显然是不满足要求的,当数组变化时我们应该也有所感知。 Vue 同样存在这样的问题,它的解决方法是:将数组的常用方法进行重写,进而覆盖掉原生的数组方法,重写之后的数组方法需要能够被拦截。
const arrExtend = Object.create(Array.prototype)
const arrMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

arrMethods.forEach(method => {
  const oldMethod = Array.prototype[method]
  const newMethod = function(...args) {
    oldMethod.apply(this, args)
    console.log(`${method} 方法被执行了`)
  }
  arrExtend[method] = newMethod
})

这种 monkey patch 本质是重写原生方法,这天生不是很安全,也很不优雅,更好的实现方式是ES Next 的新特性——Proxy

Proxy

尝试使用 Proxy 来完成代码重构
let data = {
  stage: 'GitChat',
  course: {
    title: '前端开发进阶',
    author: ['Lucas'],
    publishTime: '2018 年 5 月'
  }
}

const observe = data => {
  if (!data || Object.prototype.toString.call(data) !== '[object Object]') {
      return
  }

  Object.keys(data).forEach(key => {
    let currentValue = data[key]
    // 事实上 proxy 也可以对函数类型进行代理。这里只对承载数据类型的 object 进行处理,读者了解即可。
    if (typeof currentValue === 'object') {
      observe(currentValue)
      data[key] = new Proxy(currentValue, {
        set(target, property, value, receiver) {
          // 因为数组的 push 会引起 length 属性的变化,所以 push 之后会触发两次 set 操作,我们只需要保留一次即可,property 为 length 时,忽略
          if (property !== 'length') {
            console.log(`setting ${key} value now, setting value is`, currentValue)
          }
          return Reflect.set(target, property, value, receiver)
        }
      })
    }
    else {
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: false,
        get() {
          console.log(`getting ${key} value now, getting value is:`, currentValue)
          return currentValue
        },
        set(newValue) {
          currentValue = newValue
          console.log(`setting ${key} value now, setting value is`, currentValue)
        }
      })
    }
  }) 
}

observe(data)

data.course.author.push('messi')
// setting author value now, setting value is ["Lucas"]

简单总结一下,对于数据键值为基本类型的情况,我们使用 Object.defineProperty;对于键值为对象类型的情况,继续递归调用 observe 方法,并通过 Proxy 返回的新对象对 data[key] 重新赋值,这个新值的 getter 和 setter 已经被添加了代理。

两种方式的总结

  • Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写
  • Object.defineProperty 必须遍历对象的每个属性,且对于嵌套结构需要深层遍历
  • Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的
  • Proxy 支持代理数组的变化
  • Proxy 的第二个参数除了 set 和 get 以外,可以有 13 种拦截方法,比起 Object.defineProperty() 更加强大,这里不再一一列举
  • Proxy 性能将会被底层持续优化,而 Object.defineProperty 已经不再是优化重点

模版编译原理

监听完数据的变化后就是进行模版编译了。

<body>
  <div id="app">
    <h1>{{stage}} 平台课程:{{course.title}}</h1>
    <p>{{course.title}} 是 {{course.author}} 发布的课程</p>
    <p>发布时间为 {{course.publishTime}} </p>
  </div>

  <script>
    let vue = new Vue({ 
      ele: '#app', 
      data: {
        stage: 'GitChat',
        course: {
          title: '前端开发进阶',
          author: 'Lucas',
          publishTime: '2018 年 5 月'
        },
      }
    })
  </script>
</body>

模版变量使用了 {{}} 的表达方式输出模版变量。最终输出的 HTML 内容应该被合适的数据进行填充替换,因此还需要一步编译过程,该过程任何框架或类库中都是相通的,比如 React 中的 JSX,也是编译为 React.createElement,并在生成虚拟 DOM 时进行数据填充。

compile(document.querySelector('#app'), data)

function compile(el, data) {
  let fragment = document.createDocumentFragment()

  while (child = el.firstChild) {
    fragment.appendChild(child)
  }

  // 对 el 里面的内容进行替换
  function replace(fragment) {
    Array.from(fragment.childNodes).forEach(node => {
      let textContent = node.textContent
      let reg = /\{\{(.*?)\}\}/g

      if (node.nodeType === 3 && reg.test(textContent)) {
         const nodeTextContent = node.textContent
        const replaceText = () => {
            node.textContent = nodeTextContent.replace(reg, (matched, placeholder) => {
                return placeholder.split('.').reduce((prev, key) => {
                    return prev[key]
                }, data)

            })
        }

        replaceText()
      }

      // 如果还有子节点,继续递归 replace
      if (node.childNodes && node.childNodes.length) {
        replace(node)
      }
    })
  }

  replace(fragment)

  el.appendChild(fragment)
  return el
}

使用 fragment 变量储存生成的真实 HTML 节点内容。通过 replace 方法对 {{变量}} 进行数据替换,同时 {{变量}} 的表达只会出现在 nodeType === 3 的文本类型节点中,因此对于符合 node.nodeType === 3 && reg.test(textContent) 条件的情况,进行数据获取和填充。我们借助字符串 replace 方法第二个参数进行一次性替换,此时对于形如 {{data.course.title}} 的深层数据,通过 reduce 方法,获得正确的值。

因为 DOM 结构可能是多层的,所以对存在子节点的节点,依然使用递归进行 replace 替换。

双向绑定实现方式

我们需要在模版编译中,对于存在 v-model 属性的 node 进行事件监听,在输入框输入时,改变 v-model 属性值对应的数据即可(这里为 inputData),增加 compile 中的 replace 方法逻辑,对于 node.nodeType === 1 的 DOM 类型,伪代码如下:

function replace(el, data) {
  // 省略...
  if (node.nodeType === 1) {

    let attributesArray = node.attributes

    Array.from(attributesArray).forEach(attr => {
      let attributeName = attr.name
      let attributeValue = attr.value

      if (name.includes('v-')) {
        node.value = data[attributeValue]
      }

      node.addEventListener('input', e => {
        let newVal = e.target.value
        data[attributeValue] = newVal
        // ...
        // 更改数据源,触发 setter
        // ...
      })
    })

  }

  if (node.childNodes && node.childNodes.length) {
    replace(node)
  }
}

发布订阅模式

class Notify {
  constructor() {
    this.subscribers = []
  }
  add(handler) {
    this.subscribers.push(handler)
  }
  emit() {
    this.subscribers.forEach(subscriber => subscriber())
  }
}

//使用
let notify = new Notify()

notify.add(() => {
    console.log('emit here')
})

notify.emit()
// emit here

当获取数据时,会触发get函数,并且此时会添加订阅者,如果数据变更时会触发set函数,对所有订阅者进行notify来通知,之后进行页面的渲染。

MVVM的实现方式

虚拟 DOM

简单来说,虚拟 DOM 就是用数据结构表示 DOM 结构,它并没有真实 append 到 DOM 上,因此称之为“虚拟”。

应用虚拟 DOM 的收益也很直观:操作数据结构远比和浏览器交互去操作 DOM 快很多。请读者准确理解这句话:操作数据结构是指改变对象(虚拟 DOM),这个过程比修改真实 DOM 快很多。但虚拟 DOM 也最终是要挂载到浏览器上成为真实 DOM 节点,因此使用虚拟 DOM 并不能使得操作 DOM 的数量减少,但能够精确地获取最小的、最必要的操作 DOM 的集合。

来看一下DOM结构对应的js表示:

<ul id="chapterList">
  <li class="chapter">chapter1</li>
  <li class="chapter">chapter2</li>
  <li class="chapter">chapter3</li>
</ul>
const chapterListVirtualDom = {
  tagName: 'ul',
  attributes: {
    id: 'chapterList'
  },
  children: [
    { tagName: 'li', attributes: { class: 'chapter' }, children: ['chapter1'] },
    { tagName: 'li', attributes: { class: 'chapter' }, children: ['chapter2'] },
    { tagName: 'li', attributes: { class: 'chapter' }, children: ['chapter3'] },
  ]
}

可以将虚拟DOM的生成类单独抽离出来:

class Element {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName
    this.attributes = attributes
    this.children = children
  }
}

function element(tagName, attributes, children) {
  return new Element(tagName, attributes, children)
}

const chapterListVirtualDom = element('ul', { id: 'list' }, [ element('li', { class: 'chapter' }, ['chapter1']), element('li', { class: 'chapter' }, ['chapter2']), element('li', { class: 'chapter' }, ['chapter3']) ])

实现一个 setAttribute 方法,使用 setAttribute 方法来对 DOM 节点进行属性设置。

const setAttribute = (node, key, value) => {
  switch (key) {
    case 'style':
      node.style.cssText = value
      break
    case 'value':
      let tagName = node.tagName || ''
      tagName = tagName.toLowerCase()
      if (
        tagName === 'input' || tagName === 'textarea'
      ) {
        node.value = value
      } else {
        // 如果节点不是 input 或者 textarea, 则使用 setAttribute 去设置属性
        node.setAttribute(key, value)
      }
      break
    default:
      node.setAttribute(key, value)
      break
  }
}

编写render函数

class Element {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName
    this.attributes = attributes
    this.children = children
  }

  render () {
    let element = document.createElement(this.tagName)
    let attributes = this.attributes

     for (let key in attributes) {
        setAttribute(element, key, attributes[key])
     }

    let children = this.children

    children.forEach(child => {
      let childElement = child instanceof Element
        ? child.render() // 若 child 也是虚拟节点,递归进行
        : document.createTextNode(child)  // 若是字符串,直接创建文本节点
      element.appendChild(childElement)
    })

    return element
  }
}

function element (tagName, attributes, children) {
  return new Element(tagName, attributes, children)
}

将真实的dom截点渲染到浏览器上

const renderDom = (element, target) => {
  target.appendChild(element)
}

DOM diff

walkToDiff 前两个参数是两个需要比较的虚拟 DOM 对象;第三个参数记录 nodeIndex,在删除节点时使用,初始为 0;第四个参数是一个闭包变量,记录 diff 结果:

let initialIndex = 0

const walkToDiff = (oldVirtualDom, newVirtualDom, index, patches) => {
  let diffResult = []

  // 如果 newVirtualDom 不存在,说明该节点被移除,我们将 type 为 REMOVE 的对象推进 diffResult 变量,并记录 index
  if (!newVirtualDom) {
    diffResult.push({
      type: 'REMOVE',
      index
    })
  }
  // 如果新旧节点都是文本节点,是字符串
  else if (typeof oldVirtualDom === 'string' && typeof newVirtualDom === 'string') {
    // 比较文本是否相同,如果不同则记录新的结果
    if (oldVirtualDom !== newVirtualDom) {
      diffResult.push({
        type: 'MODIFY_TEXT',
        data: newVirtualDom,
        index
      })
    }
  }
  // 如果新旧节点类型相同
  else if (oldVirtualDom.tagName === newVirtualDom.tagName) {
    // 比较属性是否相同
    let diffAttributeResult = {}

    for (let key in oldVirtualDom) {
      if (oldVirtualDom[key] !== newVirtualDom[key]) {
        diffAttributeResult[key] = newVirtualDom[key]
      }
    }

    for (let key in newVirtualDom) {
      // 旧节点不存在的新属性
      if (!oldVirtualDom.hasOwnProperty(key)) {
          diffAttributeResult[key] = newVirtualDom[key]
      }
    }

    if (Object.keys(diffAttributeResult).length > 0) {
        diffResult.push({ 
          type: 'MODIFY_ATTRIBUTES', 
          diffAttributeResult 
        })
    }

    // 如果有子节点,遍历子节点
    oldVirtualDom.children.forEach((child, index) => {
      walkToDiff(child, newVirtualDom.children[index], ++initialIndex, patches)
    })
  }
  // else 说明节点类型不同,被直接替换了,我们直接将新的结果 push
  else {
    diffResult.push({ 
      type: 'REPLACE', 
      newVirtualDom
    })
  }

  if (!oldVirtualDom) {
    diffResult.push({ 
      type: 'REPLACE', 
      newVirtualDom
    })
  }

  if (diffResult.length) {
    patches[index] = diffResult
  }
}

最小化差异应用

通过patch将差异更新到现有的DOM节点当中去

const patch = (node, patches) => {
  let walker = { index: 0 }
  walk(node, walker, patches)
}

const walk = (node, walker, patches) => {
  let currentPatch = patches[walker.index]

  let childNodes = node.childNodes

  childNodes.forEach(child => {
    walker.index++
    walk(child, walker, patches)
  })

  if (currentPatch) {
    doPatch(node, currentPatch)
  }
}

const doPatch = (node, patches) => {
  patches.forEach(patch => {
    switch (patch.type) {
      case 'MODIFY_ATTRIBUTES':
        const attributes = patch.diffAttributeResult.attributes
        for (let key in attributes) {
            if (node.nodeType !== 1) return
            const value = attributes[key]
            if (value) {
              setAttribute(node, key, value)
            } else {
              node.removeAttribute(key)
            }
        }
        break
      case 'MODIFY_TEXT':
        node.textContent = patch.data
        break
      case 'REPLACE':
        let newNode = (patch.newNode instanceof Element) ? render(patch.newNode) : document.createTextNode(patch.newNode)
        node.parentNode.replaceChild(newNode, node)
        break
      case 'REMOVE':
        node.parentNode.removeChild(node)
        break
      default:
        break
    }
  })
}

总结

 以上代码进行整合后:

const setAttribute = (node, key, value) => {
  switch (key) {
    case 'style':
      node.style.cssText = value
      break
    case 'value':
      let tagName = node.tagName || ''
      tagName = tagName.toLowerCase()
      if (
        tagName === 'input' || tagName === 'textarea'
      ) {
        node.value = value
      } else {
        // 如果节点不是 input 或者 textarea, 则使用 setAttribute 去设置属性
        node.setAttribute(key, value)
      }
      break
    default:
      node.setAttribute(key, value)
      break
  }
}

class Element {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName
    this.attributes = attributes
    this.children = children
  }

  render () {
    let element = document.createElement(this.tagName)
    let attributes = this.attributes

     for (let key in attributes) {
        setAttribute(element, key, attributes[key])
     }

    let children = this.children

    children.forEach(child => {
      let childElement = child instanceof Element
        ? child.render() // 若 child 也是虚拟节点,递归进行
        : document.createTextNode(child)  // 若是字符串,直接创建文本节点
      element.appendChild(childElement)
    })

    return element
  }
}

function element (tagName, attributes, children) {
  return new Element(tagName, attributes, children)
}

const renderDom = (element, target) => {
  target.appendChild(element)
}

const diff = (oldVirtualDom, newVirtualDom) => {
  let patches = {}

  // 递归树 比较后的结果放到 patches
  walkToDiff(oldVirtualDom, newVirtualDom, 0, patches)

  return patches
}

let initialIndex = 0

const walkToDiff = (oldVirtualDom, newVirtualDom, index, patches) => {
  let diffResult = []

  // 如果 newVirtualDom 不存在,说明该节点被移除,我们将 type 为 REMOVE 的对象推进 diffResult 变量,并记录 index
  if (!newVirtualDom) {
    diffResult.push({
      type: 'REMOVE',
      index
    })
  }
  // 如果新旧节点都是文本节点,是字符串
  else if (typeof oldVirtualDom === 'string' && typeof newVirtualDom === 'string') {
    // 比较文本是否相同,如果不同则记录新的结果
    if (oldVirtualDom !== newVirtualDom) {
      diffResult.push({
        type: 'MODIFY_TEXT',
        data: newVirtualDom,
        index
      })
    }
  }
  // 如果新旧节点类型相同
  else if (oldVirtualDom.tagName === newVirtualDom.tagName) {
    // 比较属性是否相同
    let diffAttributeResult = {}

    for (let key in oldVirtualDom) {
      if (oldVirtualDom[key] !== newVirtualDom[key]) {
        diffAttributeResult[key] = newVirtualDom[key]
      }
    }

    for (let key in newVirtualDom) {
      // 旧节点不存在的新属性
      if (!oldVirtualDom.hasOwnProperty(key)) {
          diffAttributeResult[key] = newVirtualDom[key]
      }
    }

    if (Object.keys(diffAttributeResult).length > 0) {
        diffResult.push({ 
          type: 'MODIFY_ATTRIBUTES', 
          diffAttributeResult 
        })
    }

    // 如果有子节点,遍历子节点
    oldVirtualDom.children.forEach((child, index) => {
      walkToDiff(child, newVirtualDom.children[index], ++initialIndex, patches)
    })
  }
  // else 说明节点类型不同,被直接替换了,我们直接将新的结果 push
  else {
    diffResult.push({ 
      type: 'REPLACE', 
      newVirtualDom
    })
  }

  if (!oldVirtualDom) {
    diffResult.push({ 
      type: 'REPLACE', 
      newVirtualDom
    })
  }

  if (diffResult.length) {
    patches[index] = diffResult
  }
}

const chapterListVirtualDom = element('ul', { id: 'list' }, [
  element('li', { class: 'chapter' }, ['chapter1']),
  element('li', { class: 'chapter' }, ['chapter2']),
  element('li', { class: 'chapter' }, ['chapter3'])
])

const chapterListVirtualDom1 = element('ul', { id: 'list2' }, [
  element('li', { class: 'chapter2' }, ['chapter4']),
  element('li', { class: 'chapter2' }, ['chapter5']),
  element('li', { class: 'chapter2' }, ['chapter6'])
])

const patch = (node, patches) => {
  let walker = { index: 0 }
  walk(node, walker, patches)
}

const walk = (node, walker, patches) => {
  let currentPatch = patches[walker.index]

  let childNodes = node.childNodes

  childNodes.forEach(child => {
    walker.index++
    walk(child, walker, patches)
  })

  if (currentPatch) {
    doPatch(node, currentPatch)
  }
}

const doPatch = (node, patches) => {
  patches.forEach(patch => {
    switch (patch.type) {
      case 'MODIFY_ATTRIBUTES':
        const attributes = patch.diffAttributeResult.attributes
        for (let key in attributes) {
            if (node.nodeType !== 1) return
            const value = attributes[key]
            if (value) {
              setAttribute(node, key, value)
            } else {
              node.removeAttribute(key)
            }
        }
        break
      case 'MODIFY_TEXT':
        node.textContent = patch.data
        break
      case 'REPLACE':
        let newNode = (patch.newNode instanceof Element) ? render(patch.newNode) : document.createTextNode(patch.newNode)
        node.parentNode.replaceChild(newNode, node)
        break
      case 'REMOVE':
        node.parentNode.removeChild(node)
        break
      default:
        break
    }
  })
}

//应用
var element = chapterListVirtualDom.render()
renderDom(element, document.body)
const patches = diff(chapterListVirtualDom, chapterListVirtualDom1)

patch(element, patches)

 

 

 



参考文献:https://www.jianshu.com/p/621c52b435e7

标签:node,tagName,进阶,读书笔记,Object,value,let,key,侯策
来源: https://www.cnblogs.com/fmyao/p/12824896.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有