当前位置: 首页 > article >正文

Vue2.x源码:new Vue()做了啥?

vue源码版本vue2.5.2

new Vue()做了啥?

new Vue()会执行_init方法,而_init方法在initMixin函数中定义。

src/core/instance/index.js文件中定义了Vue

function Vue (options) {
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

initMixin函数 初始化

定义的Vue.prototype._init

 export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid,自增ID
    vm._uid = uid++

    vm._isVue = true
    // merge options 合并options
    if (options && options._isComponent) {
     
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

      vm._renderProxy = vm
   
    // expose real self
    vm._self = vm
    // 初始化生命周期相关实例属性
    initLifecycle(vm)
    
    //初始化事件
    initEvents(vm)
    
    //定义$createElement() createElement的执行过程
    initRender(vm)

    // 执行beforeCreate生命周期钩子
    callHook(vm, 'beforeCreate')

    initInjections(vm) // resolve injections before data/props
    
    //将data代理至Vue._data data的代理
    initState(vm)

    // 初始化provide
    initProvide(vm) // resolve provide after data/props

    // 执行created生命周期钩子
    callHook(vm, 'created')

      // 当options中存在el属性,则执行挂载
    if (vm.$options.el) {
      //挂载#app $mount执行过程
      vm.$mount(vm.$options.el)
    }
  }
}

initLifecycle函数

initLifecycle 初始化生命周期相关变量即在vue实例上挂载一些属性并设置默认值,如$parent,$root,$children,$ref,vm._watcher,vm.__inactive,vm._directInactive,vm._isMounted,vm._isDestroyed,vm._isBeingDestroyed

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  //当前组件存在父级并且当前组件不是抽象组件
  if (parent && !options.abstract) {
  //通过while循环来向上循环,如果当前组件的父级是抽象组件并且也存在父级,那就继续向上查找当前组件父级的父级
    while (parent.$options.abstract && parent.$parent) {
    //更新parent
      parent = parent.$parent
    }
    //把该实例自身添加进找到的父级的$children属性中
    parent.$children.push(vm)
  }
  //直到找到第一个不是抽象类型的父级时,将其赋值vm.$parent
  vm.$parent = parent
  //设置实例根元素。判断如果当前实例存在父级,那么当前实例的根实例$root属性就是其父级的根实例$root属性,如果不存在,那么根实例$root属性就是它自己
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  
  //实例是否已挂载
  vm._isMounted = false
  
  //实例是否已销毁
  vm._isDestroyed = false
  
  //实例是否正准备销毁
  vm._isBeingDestroyed = false
}

initEvents 初始化事件

initEvents 初始化事件。 初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件。

export function initEvents (vm: Component) {
//在vm上新增_events属性并将其赋值为空对象,用来存储事件。
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  //获取父组件注册的事件赋给listeners,
  const listeners = vm.$options._parentListeners
  //如果listeners不为空,则调用updateComponentListeners函数,将父组件向子组件注册的事件注册到子组件的实例中
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

初始化渲染initRender

initRender函数 初始化渲染.。

export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  //实例上插槽
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  //在实例上定义_c函数和$_createElement函数
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  
  const parentData = parentVnode && parentVnode.data

  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)

}

初始化inject选项

inject 选项中的每一个数据key都是由其上游父级组件提供的,所以我们应该把每一个数据key从当前组件起,不断的向上游父级组件中查找该数据key对应的值,直到找到为止。如果在上游所有父级组件中没找到,那么就看在inject 选项是否为该数据key设置了默认值,如果设置了就使用默认值,如果没有设置,则抛出异常。

export function initInjections (vm: Component) {
//调用resolveInject把inject选项中的数据转化成键值对的形式赋给result
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    //遍历result中的每一对键值
    Object.keys(result).forEach(key => {
        //调用defineReactive函数将其添加当前实例上
        defineReactive(vm, key, result[key])
    })
    toggleObserving(true)
  }
}

注意,在把result中的键值添加到当前实例上之前,会先调用toggleObserving(false),而这个函数内部是把shouldObserve = false,这是为了告诉defineReactive函数仅仅是把键值添加到当前实例上而不需要将其转换成响应式

resolveInject函数内部是如何把inject 选项中数据转换成键值对的。

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    //创建一个空对象result,用来存储inject 选项中的数据key及其对应的值,作为最后的返回结果。
    const result = Object.create(null)
    const keys = hasSymbol
      ? Reflect.ownKeys(inject).filter(key => {
        /* istanbul ignore next */
        return Object.getOwnPropertyDescriptor(inject, key).enumerable
      })
      : Object.keys(inject)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      //provideKey就是上游父级组件提供的源属性
      const provideKey = inject[key].from
      let source = vm
      //while循环,从当前组件起,不断的向上游父级组件的_provided属性中(父级组件使用provide选项注入数据时会将注入的数据存入自己的实例的_provided属性中)查找,直到查找到源属性的对应的值,将其存入result中
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      if (!source) {
      //是否有default属性,如果有的话,则拿到这个默认值,官方文档示例中说了,默认值可以为一个工厂函数,所以当默认值是函数的时候,就去该函数的返回值,否则就取默认值本身。如果没有设置默认值,则抛出异常。
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    return result
  }
}

官方文档中说inject 选项可以是一个字符串数组,也可以是一个对象,在上面的代码中只看见了处理当为对象的情况,那如果是字符串数组呢?怎么没有处理呢?
其实在初始化阶段_init函数在合并属性的时候还调用了一个将inject 选项数据规范化的函数normalizeInject

初始化initState

从Vue 2.0版本起,Vue不再对所有数据都进行侦测,而是将侦测粒度提高到了组件层面,对每个组件进行侦测,所以在每个组件上新增了vm._watchers属性,用来存放这个组件内用到的所有状态的依赖,当其中一个状态发生变化时,就会通知到组件,然后由组件内部使用虚拟DOM进行数据比对,从而降低内存开销,提高性能。

export function initState (vm: Component) {
//实例上新增了一个属性_watchers,用来存储当前实例中所有的watcher实例,无论是使用vm.$watch注册的watcher实例还是使用watch选项注册的watcher实例,都会被保存到该属性中。
  vm._watchers = []
  const opts = vm.$options
  
//先判断实例中是否有props选项,如果有,就调用props选项初始化函数initProps去初始化props选项;
  if (opts.props) initProps(vm, opts.props)
  
//先判断实例中是否有props选项,如果有,就调用props选项初始化函数initProps去初始化props选项;
  if (opts.methods) initMethods(vm, opts.methods)
  
//接着再判断实例中是否有data选项,如果有,就调用data选项初始化函数initData去初始化data选项;如果没有,就把data当作空对象并将其转换成响应式
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  
//接着再判断实例中是否有computed选项,如果有,就调用computed选项初始化函数initComputed去初始化computed选项
  if (opts.computed) initComputed(vm, opts.computed)
  
//最后判断实例中是否有watch选项,如果有,就调用watch选项初始化函数initWatch去初始化watch选项
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

开发中有注意到我们在data中可以使用props,在watch中可以观察data和props,之所以可以这样做,就是因为在初始化的时候遵循了这种顺序,先初始化props,接着初始化data,最后初始化watch。

props处理initProps

在此之前,在合并options时已将props规范化

function initProps (vm: Component, propsOptions: Object) {
//propsData,父组件传入的真实props数据
  const propsData = vm.$options.propsData || {}
  
//props,指向vm._props的指针,所有设置到props变量中的属性都会保存到vm._props中
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  //指向vm.$options._propKeys的指针,缓存props对象中的key,将来更新props时只需遍历vm.$options._propKeys数组即可得到所有props的key
  const keys = vm.$options._propKeys = []
  //当前组件是否为根组件
  const isRoot = !vm.$parent
  // root instance props should be converted
  //如果不是,那么不需要将props数组转换为响应式的,toggleObserving(false)用来控制是否将数据转换成响应式
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    //调用validateProp函数校验父组件传入的props数据类型是否匹配并获取到传入的值value
    const value = validateProp(key, propsOptions, propsData, vm)
    
    //将键和值通过defineReactive函数添加到props(即vm._props)中
    defineReactive(props, key, value)
    
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    //判断这个key在当前实例vm中是否存在,如果不存在,则调用proxy函数在vm上设置一个以key为属性的代码,当使用vm[key]访问数据时,其实访问的是vm._props[key]
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

methods处理initMethods

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
    //如果methods中某个方法只有key而没有方法体时,抛出异常:提示用户方法未定义
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      //如果methods中某个方法名与props中某个属性名重复了,就抛出异常:提示用户方法名重复了
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      //判断如果methods中某个方法名如果在实例vm中已经存在并且方法名是以_或$开头的,就抛出异常:提示用户方法名命名不规范
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    //将method绑定到实例vm上,然后通过this.xxx来访问methods选项中的xxx方法了
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

data处理initData

function initData (vm: Component) {

//获取到用户传入的data选项,赋给变量data,同时将变量data作为指针指向vm._data,然后判断data是不是一个函数,如果是就调用getData函数获取其返回值,将其保存到vm._data中。如果不是,就将其本身保存到vm._data中
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
    
//如果不是对象的话,就抛出警告:提示用户data应该是一个对象。
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
    //判断data对象中是否存在某一项的key与methods中某个属性名重复,如果存在重复,就抛出警告:提示用户属性名重复。
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    //判断是否存在某一项的key与prop中某个属性名重复,如果存在重复,就抛出警告:提示用户属性名重复
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
      //调用proxy函数将data对象中key不以_或$开头的属性代理到实例vm上,这样,我们就可以通过this.xxx来访问data选项中的xxx数据了
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  //调用observe函数将data中的数据转化成响应式
  observe(data, true /* asRootData */)
}

computed处理initComputed

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  //义了一个变量watchers并将其赋值为空对象,同时将其作为指针指向vm._computedWatchers
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
  //遍历computed选项中的每一项属性,首先获取到每一项的属性值,记作userDef
    const userDef = computed[key]
//判断userDef是不是一个函数,如果是函数,则该函数默认为取值器getter,将其赋值给变量getter;如果不是函数,则说明是一个对象,则取对象中的get属性作为取值器赋给变量getter
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    //如果上面两种情况取到的取值器不存在,则抛出警告:提示用户计算属性必须有取值器
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      //判断如果不是在服务端渲染环境下,则创建一个watcher实例,并将当前循环到的的属性名作为键,创建的watcher实例作为值存入watchers对象中
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    //判断当前循环到的的属性名是否存在于当前实例vm上
    if (!(key in vm)) {
    //不存在,调用defineComputed函数为实例vm上设置计算属性
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
    //存在,则在非生产环境下抛出警告
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

defineComputed函数 该函数接受3个参数,分别是:target、key和userDef。其作用是为target上定义一个属性key,并且属性key的getter和setter根据userDef的值来设置

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
//义了变量sharedPropertyDefinition,它是一个默认的属性描述符。
//定义了变量shouldCache,用于标识计算属性是否应该有缓存。该变量的值是当前环境是否为非服务端渲染环境,如果是非服务端渲染环境则该变量为true。也就是说,只有在非服务端渲染环境下计算属性才应该有缓存
  const shouldCache = !isServerRendering()
  //判断如果userDef是一个函数,则该函数默认为取值器getter
  if (typeof userDef === 'function') {
  //判断如果userDef是一个函数,则该函数默认为取值器getter,此处在非服务端渲染环境下并没有直接使用userDef作为getter,而是调用createComputedGetter函数创建了一个getter,这是因为userDef只是一个普通的getter,它并没有缓存功能,所以我们需要额外创建一个具有缓存功能的getter,而在服务端渲染环境下可以直接使用userDef作为getter,因为在服务端渲染环境下计算属性不需要缓存。由于用户没有设置setter函数,所以将sharedPropertyDefinition.set设置为noop
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
  //如果userDef不是一个函数,那么就将它当作对象处理。在设置sharedPropertyDefinition.get的时候先判断userDef.get是否存在,如果不存在,则将其设置为noop,如果存在,则同上面一样,在非服务端渲染环境下并且用户没有明确的将userDef.cache设置为false时调用createComputedGetter函数创建一个getter赋给sharedPropertyDefinition.get。然后设置sharedPropertyDefinition.set为userDef.set函数
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
      //如果用户没有设置setter的话,那么就给setter一个默认函数,这是为了防止用户在没有设置setter的情况下修改计算属性,从而为其抛出警告
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  //调用Object.defineProperty方法将属性key绑定到target上,其中的属性描述符就是上面设置的sharedPropertyDefinition
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

createComputedGetter

该函数是一个高阶函数,其内部返回了一个computedGetter函数,所以其实是将computedGetter函数赋给了sharedPropertyDefinition.get。当获取计算属性的值时会执行属性的getter,而属性的getter就是 sharedPropertyDefinition.get,也就是说最终执行的 computedGetter函数

function createComputedGetter (key) {
  return function computedGetter () {
  //储在当前实例上_computedWatchers属性中key所对应的watcher实例
    const watcher = this._computedWatchers && this._computedWatchers[key]
    //如果watcher存在,则调用watcher实例上的depend方法和evaluate方法,并且将evaluate方法的返回值作为计算属性的计算结果返回
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}


function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

initwatch处理

watch的几种写法

  watch: {
    a: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    },
    // methods选项中的方法名
    b: 'someMethod',
    // 深度侦听,该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
    c: {
      handler: function (val, oldVal) { /* ... */ },
      deep: true
    },
    // 该回调将会在侦听开始之后被立即调用
    d: {
      handler: 'someMethod',
      immediate: true
    },
    // 调用多个回调
    e: [
      'handle1',
      function handle2 (val, oldVal) { /* ... */ },
      {
        handler: function handle3 (val, oldVal) { /* ... */ },
      }
    ],
    // 侦听表达式
    'e.f': function (val, oldVal) { /* ... */ }
  }

initWatch函数

function initWatch (vm: Component, watch: Object) {
//在函数内部会遍历watch选项
  for (const key in watch) {
  //拿到每一项的key和对应的值handler
    const handler = watch[key]
    
//判断handler是否为数组,如果是数组则循环该数组并将数组中的每一项依次调用createWatcher函数来创建watcher
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
      //不是数组,则直接调用createWatcher函数来创建watcher
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

createWatcher 函数

function createWatcher (
  vm: Component,//当前实例
  keyOrFn: string | Function,//被侦听的属性表达式
  handler: any,//watch选项中每一项的值
  options?: Object//用于传递给vm.$watch的选项对象
) {
//判断handle是一个对象时,handler = handler.handler
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
//如果handle是一个字符串,则直接在实例上找
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(keyOrFn, handler, options)
}

分开解析-如果是一个对象

  if (isPlainObject(handler)) {
  //将handler对象整体记作options
    options = handler
    //把handler对象中的handler属性作为真正的回调函数记作handler
    handler = handler.handler
  }

则为以下情况

watch:{
    c: {
      handler: function (val, oldVal) { /* ... */ },
      deep: true
    },
    // 该回调将会在侦听开始之后被立即调用
    d: {
      handler: 'someMethod',
      immediate: true
    },
  }

分开解析-如果handler为一个字符串

if (typeof handler === 'string') {
  handler = vm[handler]
}

则为以下情况

watch:{
 // methods选项中的方法名
    b: 'someMethod',
    }

Vue.prototype.$watch

 //src\core\instance\state.js
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
//判断回调函数是否是一个对象
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  
  if (options.immediate) {
    cb.call(vm, watcher.value)
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}

初始化provide

var Parent = {
  provide: {
    foo: 'bar'
  },
  // ...
}

const Provider = {
  provide () {
    return {
      [s]: 'foo'
    }
  }
}
export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

http://www.kler.cn/a/2862.html

相关文章:

  • 智能电视/盒子的应用管理——通过ADB工具优化体验
  • MySQL45讲 第二十讲 幻读是什么,幻读有什么问题?
  • 系统上线后发现bug,如何回退版本?已经产生的新业务数据怎么办?
  • 【C++】C++11特性(上)
  • springboot项目中,使用ProGuard 对代码进行混淆
  • Linux探秘坊-------1.系统核心的低语:基础指令的奥秘解析(1)
  • 给程序加个进度条吧,1行Python代码,快速添加~
  • c++ 一些常识 2
  • XILINX关于DDR2的IP的读写控制仿真
  • 【Spring Cloud Alibaba】2.服务注册与发现(Nacos安装)
  • 部署私有npm 库
  • 水文-编程命令快查手册
  • 支持RT-Thread最新版本的瑞萨RA2E1开发板终于要大展身手了
  • 学习 Python 之 Pygame 开发魂斗罗(十)
  • 如何系统型地学习深度学习?
  • Python日志logging实战教程
  • 利用Cookie劫持+HTML注入进行钓鱼攻击
  • 服务端测试知识汇总
  • 基于原生Javascript的放大镜插件的设计和实现
  • 贪心算法(一)
  • 蓝桥杯刷题冲刺 | 倒计时18天
  • MD5加密竟然不安全,应届生表示无法理解?
  • Java每日一练(20230324)
  • hive之视图
  • 手写一个Promise
  • maya python 中的maya.cmds 与maya.mel模块的区别笔记