「試着讀讀 Vue 源代碼」初始化先後作了哪些事情 ❓

說明

  • 首先這篇文章是讀 vue.js 源代碼的梳理性文章,文章分塊梳理,記錄着本身的一些理解及大體過程;更重要的一點是但願在 vue.js 3.0 發佈前深刻的瞭解其原理。css

  • 若是你從未看過或者接觸過 vue.js 源代碼,建議你參考如下列出的 vue.js 解析的相關文章,由於這些文章更細緻的講解了這個工程,本文只是以一些 demo 演示某一功能點或 API 實現,力求簡要梳理過程。html

  • 若是搞清楚了工程目錄及入口,建議直接去看代碼,這樣比較高效 ( 遇到難以理解對應着回來看看別人的講解,加以理解便可 )vue

  • 文章所涉及到的代碼,基本都是縮減版,具體還請參閱 vue.js - 2.5.17node

  • 若有任何疏漏和錯誤之處歡迎指正、交流git

初始化前

調用關係

JavaScript 自己是一種直譯式腳本語言,在找到入口後,主要須要理清其調用關係? 找出 Vue 構造函數的在哪定義了?按照這個邏輯,跟着程序一步一步走便可。github

首先src/platforms/web/entry-runtime-with-compiler.jsweb

這個文件最開始,引入一些方法與配置,並導入了 Vue 進而程序去執行 ./runtime/index 文件算法

import config from 'core/config';
import { warn, cached } from 'core/util/index';
import { mark, measure } from 'core/util/perf';
import Vue from './runtime/index';
import { query } from './util/index';
import { compileToFunctions } from './compiler/index';
import {
  shouldDecodeNewlines,
  shouldDecodeNewlinesForHref
} from './util/compat';

如下代碼省略, 將在分析初始化時展開...

複製代碼

接着 src/platforms/web/runtime/index.jsapi

這個文件也是引入一些方法與配置,並導入了 Vue , 程序繼續走到 core/index緩存

import Vue from 'core/index';
import config from 'core/config';
import { extend, noop } from 'shared/util';
import { mountComponent } from 'core/instance/lifecycle';
import { devtools, inBrowser, isChrome } from 'core/util/index';
import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index';

import { patch } from './patch';
import platformDirectives from './directives/index';
import platformComponents from './components/index';

如下代碼省略, 將在分析初始化時展開...

複製代碼

來到核心代碼 src/core/index.js

該文件仍然也是從外部文件導入了 Vue , 程序來到 ./instance/index

import Vue from './instance/index';
import { initGlobalAPI } from './global-api/index';
import { isServerRendering } from 'core/util/env';
import { FunctionalRenderContext } from 'core/vdom/create-functional-component';

如下代碼省略, 將在分析初始化時展開...

複製代碼

最後 src/core/instance/index.js

import { initMixin } from './init';
...

/** * Vue構造函數 * * @param {*} options 選項參數 */
function Vue(options) {
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
    warn('Vue是一個構造函數,應該用「new」關鍵字調用');
  }
  this._init(options);
}

export default Vue;

如下代碼省略, 將在分析初始化時展開...

複製代碼

綜上:

  • src/core/instance/index.js ( 定義 Vue 構造函數 ) =>
  • src/core/index.js ( 在 Vue 構造函數上添加全局的 API ) =>
  • web/runtime/index.js ( 安裝特定於平臺的 utils & 運行時指令和組件 & 定義公用的掛載方法 & 配置 devtools 全局鉤子 ) =>
  • web/entry-runtime-with-compiler.js ( 重寫 mount 函數,給運行時版的mount 函數增長編譯模板的能力 )

初始化前作的事情

根據上述調用關係一步一步走,首先看到最初定義 Vue 構造函數的文件到底作了哪些事情

初始化前 - 定義 Vue 構造函數並執行五個函數

import { initMixin } from './init';
import { stateMixin } from './state';
import { renderMixin } from './render';
import { eventsMixin } from './events';
import { lifecycleMixin } from './lifecycle';
import { warn } from '../util/index';

function Vue(options) {
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
    warn('Vue是一個構造函數,應該用「new」關鍵字調用');
  }
  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) {
    // 代碼省略,在初始化會細緻分析
  };
}
複製代碼

stateMixin

import {
  set,
  del,
  observe,
  defineReactive,
  toggleObserving
} from '../observer/index';
...

export function stateMixin(Vue: Class<Component>) {
  // 在使用object.defineproperty時,flow在直接聲明定義對象方面存在一些問題,所以咱們必須在這裏以程序的方式構建對象。
  const dataDef = {};
  dataDef.get = function() {
    return this._data;
  };
  const propsDef = {};
  propsDef.get = function() {
    return this._props;
  };

  // 在非生產環境下 設置 $data $props 爲只讀屬性
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function(newData: Object) {
      warn('避免替換實例根$data。 而是使用嵌套數據屬性。', this);
    };
    propsDef.set = function() {
      warn(`$props 是隻讀的。`, this);
    };
  }
  // 在Vue原型上定義兩個屬性,並分別代理了 _data _props 的實例屬性
  Object.defineProperty(Vue.prototype, '$data', dataDef);
  Object.defineProperty(Vue.prototype, '$props', propsDef);

  // 在 vue 原型上添加 實例方法 / 數據相關: $set/$delete/$watch
  Vue.prototype.$set = set; // 向響應式對象中添加一個屬性,並確保這個新屬性一樣是響應式的,且觸發視圖更新
  Vue.prototype.$delete = del; // 刪除對象的屬性。若是對象是響應式的,確保刪除能觸發更新視圖。
  Vue.prototype.$watch = function( // 觀察 Vue 實例變化的一個表達式或計算屬性函數。回調函數獲得的參數爲新值和舊值。 expOrFn: string | Function, cb: any, options?: Object ): Function {
    // 代碼省略,在初始化會細緻分析
  };

  ...
}
複製代碼

eventsMixin

Vue.prototype 添加實例方法 / 事件相關:$on/$once/$off/$emit

export function eventsMixin(Vue: Class<Component>) {
  // 做用:監聽當前實例上的自定義事件。事件能夠由vm.$emit觸發。回調函數會接收全部傳入事件觸發函數的額外參數。
  Vue.prototype.$on = function( event: string | Array<string>, fn: Function ): Component {
    // ...
  };

  // 做用:監聽一個自定義事件,可是隻觸發一次,在第一次觸發以後移除監聽器
  Vue.prototype.$once = function(event: string, fn: Function): Component {
    // ...
  };

  // 做用:移除自定義事件監聽器。
  Vue.prototype.$off = function( event?: string | Array<string>, fn?: Function ): Component {
    // ...
  };

  // 做用:觸發當前實例上的事件。附加參數都會傳給監聽器回調。
  Vue.prototype.$emit = function(event: string): Component {
    // ...
  };
}
複製代碼

lifecycleMixin

Vue.prototype 添加實例方法 / 生命週期相關:_update/$forceUpdate/$destroy

export function lifecycleMixin(Vue: Class<Component>) {
  Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
    // ...
  }

  // 做用:迫使 Vue 實例從新渲染。注意它僅僅影響實例自己和插入插槽內容的子組件,而不是全部子組件。
  Vue.prototype.$forceUpdate = function() {
    // ...
  }

  // 做用:徹底銷燬一個實例。清理它與其它實例的鏈接,解綁它的所有指令及事件監聽器。
  Vue.prototype.$destroy = function() {
    // ...
  }

複製代碼

initRender

Vue.prototype 添加實例方法:$nextTick/_render/_o/_n等。

export function installRenderHelpers(target: any) {
  target._o = markOnce;
  target._n = toNumber;
  target._s = toString;
  target._l = renderList;
  target._t = renderSlot;
  target._q = looseEqual;
  target._i = looseIndexOf;
  target._m = renderStatic;
  target._f = resolveFilter;
  target._k = checkKeyCodes;
  target._b = bindObjectProps;
  target._v = createTextVNode;
  target._e = createEmptyVNode;
  target._u = resolveScopedSlots;
  target._g = bindObjectListeners;
}
複製代碼
import {
  warn,
  nextTick,
  emptyObject,
  handleError,
  defineReactive
} from '../util/index';
import { installRenderHelpers } from './render-helpers/index';

export function renderMixin(Vue: Class<Component>) {
  installRenderHelpers(Vue.prototype); // 安裝運行時方便助手

  Vue.prototype.$nextTick = function(fn: Function) {
    return nextTick(fn, this);
  };

  Vue.prototype._render = function(): VNode {
    // ...
  };
}
複製代碼

斷點調試

綜上所述該文件主要作了兩件事:定義 Vue 構造函數包裝 Vue.prototype

初始化前 - 在 Vue 構造函數上添加全局的 API

import Vue from './instance/index';
import { initGlobalAPI } from './global-api/index';
import { isServerRendering } from 'core/util/env';
import { FunctionalRenderContext } from 'core/vdom/create-functional-component';

initGlobalAPI(Vue); // 在 Vue 構造函數上添加全局的API

// 在 Vue.prototype 上添加 $isServer 只讀屬性,該屬性代理了 isServerRendering 方法
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
});

// 在 Vue.prototype 上添加 $ssrContext 只讀屬性,該屬性代理了 $vnode.ssrContext
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get() {
    return this.$vnode && this.$vnode.ssrContext;
  }
});

// 爲 ssr 運行時助手安裝公開 FunctionalRenderContext
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
});

Vue.version = '__VERSION__'; // 在 Vue 上添加靜態屬性 version

export default Vue;
複製代碼

initGlobalAPI

初始化全局 API

/* @flow */
import config from '../config';
import { initUse } from './use';
import { initMixin } from './mixin';
import { initExtend } from './extend';
import { initAssetRegisters } from './assets';
import { set, del } from '../observer/index';
import { ASSET_TYPES } from 'shared/constants';
import builtInComponents from '../components/index';

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index';

// 全局API以靜態屬性和方法的形式被添加到 Vue 構造函數
export function initGlobalAPI(Vue: GlobalAPI) {
  const configDef = {};
  configDef.get = () => config;
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn('不要替換 Vue.config 對象,請設置單獨的字段代替。');
    };
  }
  Object.defineProperty(Vue, 'config', configDef); // 在 Vue 上添加 config 只讀屬性,該屬性代理了 config

  // 暴露 util 的方法。注意:這些不被認爲是公共API的一部分——除非您意識到了風險,不然請避免依賴它們。
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  };

  // 在 Vue 上添加 set/delete/nextTick/options 屬性
  Vue.set = set;
  Vue.delete = del;
  Vue.nextTick = nextTick;
  Vue.options = Object.create(null);

  // 在 Vue.options 添加 components, directives, filters 屬性
  // ASSET_TYPES = [ 'component', 'directive', 'filter' ]
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null);
  });

  // 這用於標識「基本」構造函數,以便在Weex的多實例場景中擴展全部純對象組件。
  Vue.options._base = Vue;

  // 將 builtInComponents 的屬性混入到 Vue.options.components 中
  extend(Vue.options.components, builtInComponents); // extend() 將屬性混合到目標對象中

  /* 包裝以後 Vue.options 結果以下: Vue.options = { components: { KeepAlive }, directives: Object.create(null), filters: Object.create(null), _base: Vue } */

  // 在 Vue 構造函數上添加 use 靜態方法,全局API Vue.use
  initUse(Vue);
  // 在 Vue 構造函數上添加 mixins 靜態方法,全局API Vue.mixins
  initMixin(Vue);
  // 在 Vue 構造函數上添加 Vue.cid 靜態屬性 extend 靜態方法,全局API Vue.extend
  initExtend(Vue);
  // 在 Vue 構造函數上添加 三個 靜態方法,分別用來全局註冊組件,指令和過濾器
  initAssetRegisters(Vue);
}
複製代碼

接下來就其中細節部分分別展開討論

來自 ../components/indexbuiltInComponents

實際只是導出了包含內置組件(keep-alive)屬性的對象

import KeepAlive from './keep-alive';

export default {
  KeepAlive
};
複製代碼

keep-alive 內容以下:

export default {
  name: 'keep-alive',

  abstract: true, // 是不是抽象組件

  props: {
    // ...
  },

  created() {
    // ...
  },

  destroyed() {
    // ...
  },

  mounted() {
    // ...
  },

  render() {
    // ...
  }
};
複製代碼

initUse

export function initUse(Vue: GlobalAPI) {
  // 做用:安裝 Vue.js 插件。若是插件是一個對象,必須提供 install 方法。
  // 若是插件是一個函數,它會被做爲 install 方法。install 方法調用時,會將 Vue 做爲參數傳入。
  Vue.use = function(plugin: Function | Object) {
    // ...
  };
}
複製代碼

initMixin

export function initMixin(Vue: GlobalAPI) {
  // 做用:全局註冊一個混入,影響註冊以後全部建立的每一個 Vue 實例。
  // 插件做者可使用混入,向組件注入自定義的行爲。不推薦在應用代碼中使用。
  Vue.mixin = function(mixin: Object) {
    // ...
  };
}
複製代碼

initExtend

export function initExtend(Vue: GlobalAPI) {

  // 每一個實例構造函數,包括Vue,都有一個唯一的cid。這使咱們可以爲原型繼承建立包裝的「子構造函數」並緩存它們。
  Vue.cid = 0
  let cid = 1

  // 做用:使用基礎 Vue 構造器,建立一個「子類」。參數是一個包含組件選項的對象。
  Vue.extend = function (extendOptions: Object): Function {
    // ...
  }
複製代碼

initAssetRegisters

export function initAssetRegisters(Vue: GlobalAPI) {

  // 建立 asset 註冊方法
  // ASSET_TYPES = [ 'component', 'directive', 'filter' ]
  ASSET_TYPES.forEach(type => {
    Vue[type] = function( id: string, definition: Function | Object ): Function | Object | void {
    // ...
  }

  // Vue.component( id, [definition] ) 註冊或獲取全局組件。註冊還會自動使用給定的id設置組件的名稱
  // Vue.directive( id, [definition] ) 註冊或獲取全局指令。
  // Vue.filter( id, [definition] ) 註冊或獲取全局過濾器。
}
複製代碼

斷點調試

綜上所述該文件主要作了一件事:包裝 Vue 構造函數

初始化前 - 安裝特定於平臺的 utils & 運行時指令和組件 & 定義公用的掛載方法 & 配置 devtools 全局鉤子

/* @flow */
import Vue from 'core/index';
import config from 'core/config';
import { extend, noop } from 'shared/util';
import { mountComponent } from 'core/instance/lifecycle';
import { devtools, inBrowser, isChrome } from 'core/util/index';
import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index';

import { patch } from './patch';
import platformDirectives from './directives/index';
import platformComponents from './components/index';

/********* 安裝特定於平臺的utils **********/

Vue.config.mustUseProp = mustUseProp; // 檢查屬性是否必須使用屬性綁定,例如,值與平臺相關。
Vue.config.isReservedTag = isReservedTag; // 檢查是不是保留標籤,以便不能將其註冊爲組件。這是平臺相關的,可能會被覆蓋。
Vue.config.isReservedAttr = isReservedAttr; // 檢查是不是保留屬性,使其不能用做組件 prop。這是平臺相關的,可能會被覆蓋。
Vue.config.getTagNamespace = getTagNamespace; // 獲取元素的名稱空間
Vue.config.isUnknownElement = isUnknownElement; // 檢查標記是否爲未知元素。平臺相關的。

/********* 安裝特定於平臺的utils **********/

/********* 安裝平臺運行時指令和組件 **********/

extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);
/* 對 Vue.options.directives/components 合併包裝以後: Vue.options = { components: { KeepAlive, Transition, TransitionGroup }, directives: { model, show }, filters: Object.create(null), _base: Vue } */

/********* 安裝平臺運行時指令和組件 **********/

Vue.prototype.__patch__ = inBrowser ? patch : noop; // 安裝平臺補丁功能

/** * 公用的掛載方法 * * @param {String | Element} el 掛載元素 * @param {Boolean} hydrating 用於 Virtual DOM 的補丁算法 * @returns {Function} 真正的掛載組件的方法 */
Vue.prototype.$mount = function( el?: string | Element, hydrating?: boolean ): Component {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};

/************** 配置 devtools 全局鉤子函數 與 開發提示 **************/
if (inBrowser) {
  setTimeout(() => {
    if (config.devtools) {
      if (devtools) {
        devtools.emit('init', Vue);
      } else if (
        process.env.NODE_ENV !== 'production' &&
        process.env.NODE_ENV !== 'test' &&
        isChrome
      ) {
        console[console.info ? 'info' : 'log'](
          '下載Vue Devtools擴展以得到更好的開發體驗:\n' +
            'https://github.com/vuejs/vue-devtools'
        );
      }
    }
    if (
      process.env.NODE_ENV !== 'production' &&
      process.env.NODE_ENV !== 'test' &&
      config.productionTip !== false &&
      typeof console !== 'undefined'
    ) {
      console[console.info ? 'info' : 'log'](
        `您正在以開發模式運行Vue。\n` +
          `在部署生產時,請確保打開生產模式。\n` +
          `詳情請瀏覽 https://vuejs.org/guide/deployment.html`
      );
    }
  }, 0);
}
/************** 配置 devtools 全局鉤子函數 與 開發提示 **************/

export default Vue;
複製代碼

platformDirectives

import model from './model';
import show from './show';

export default {
  model,
  show
};
複製代碼
  • model實現:

    const directive = {
        inserted (el, binding, vnode, oldVnode) {
          // ...
        }
        componentUpdated (el, binding, vnode) {
          // ...
        }
      };
    
      export default directive;
    複製代碼
  • show實現:

    export default {
      bind(el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
        // ...
      },
    
      update(el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) {
        // ...
      },
    
      unbind(
        el: any,
        binding: VNodeDirective,
        vnode: VNodeWithData,
        oldVnode: VNodeWithData,
        isDestroy: boolean
      ) {
        // ...
      }
    };
    複製代碼

platformComponents

import Transition from './transition';
import TransitionGroup from './transition-group';

export default {
  Transition,
  TransitionGroup
};
複製代碼
  • Transition實現:

    export const transitionProps = {
      name: String,
      appear: Boolean,
      css: Boolean,
      mode: String,
      type: String,
      enterClass: String,
      leaveClass: String,
      enterToClass: String,
      leaveToClass: String,
      enterActiveClass: String,
      leaveActiveClass: String,
      appearClass: String,
      appearActiveClass: String,
      appearToClass: String,
      duration: [Number, String, Object]
    };
    
    export default {
      name: 'transition',
      props: transitionProps,
      abstract: true,
    
      render(h: Function) {
        // ...
      }
    };
    複製代碼
  • TransitionGroup實現:

    const props = extend(
      {
        tag: String,
        moveClass: String
      },
      transitionProps
    );
    
    export default {
      props,
    
      beforeMount() {
        // ...
      },
    
      render(h: Function) {
        // ...
      },
    
      updated() {
        // ...
      },
    
      methods: {
        hasMove(el: any, moveClass: string): boolean {
          // ...
        }
      }
    };
    複製代碼

斷點調試

綜上所述該文件主要對 Vue.config 進行擴展、 對 Vue.options.directives/components 進行合併包裝、添加公用的掛載方法 $mount、配置 devtools 全局鉤子函數。

初始化前 - 重寫 $mount 函數,給運行時版的 $mount 函數增長編譯模板的能力

import config from 'core/config';
import { warn, cached } from 'core/util/index';
import { mark, measure } from 'core/util/perf';
import Vue from './runtime/index';
import { query } from './util/index';
import { compileToFunctions } from './compiler/index';
import {
  shouldDecodeNewlines,
  shouldDecodeNewlinesForHref
} from './util/compat';

const mount = Vue.prototype.$mount; // 緩存運行時版的 $mount 函數

// 重寫 $mount 函數,給運行時版的 $mount 函數增長編譯模板的能力
Vue.prototype.$mount = function( el?: string | Element, hydrating?: boolean ): Component {
  el = el && query(el); // 處理 掛載點

  // 過濾 body html
  if (el === document.body || el === document.documentElement /*html*/) {
    process.env.NODE_ENV !== 'production' &&
      warn(
        `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
      );
    return this;
  }

  /*************** 解析模板/el並轉換爲render函數 ***************/
  const options = this.$options;
  if (!options.render) {
    let template = options.template; // 獲取合適的內容做爲模板(template)
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          // 把該字符串做爲 css 選擇符去選中對應的元素,並把該元素的 innerHTML 做爲模板
          template = idToTemplate(template);
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(`模板元素未找到或爲空: ${options.template}`, this);
          }
        }
      } else if (template.nodeType) {
        // 元素節點
        template = template.innerHTML;
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('無效的模板選項:' + template, this);
        }
        return this;
      }
    } else if (el) {
      template = getOuterHTML(el); // el 選項指定的掛載點將被做爲組件模板
    }

    if (template) {
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile');
      }
      /*************** 將模板(template)字符串編譯爲渲染函數 ***************/
      const { render, staticRenderFns } = compileToFunctions(
        template,
        {
          shouldDecodeNewlines,
          shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        },
        this
      );
      options.render = render;
      options.staticRenderFns = staticRenderFns;
      /*************** 將模板(template)字符串編譯爲渲染函數 ***************/

      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end');
        measure(`vue ${this._name} compile`, 'compile', 'compile end');
      }
    }
  }
  /*************** 解析模板/el並轉換爲render函數 ***************/

  return mount.call(this, el, hydrating);
};

/** * 獲取元素的outerHTML,並在IE中處理SVG元素。 */
function getOuterHTML(el: Element): string {
  // IE9-11 中 SVG 標籤元素是沒有 innerHTML 和 outerHTML 這兩個屬性
  if (el.outerHTML) {
    return el.outerHTML;
  } else {
    const container = document.createElement('div');
    container.appendChild(el.cloneNode(true)); // 返回調用該方法的節點的一個副本(是否深度克隆)
    return container.innerHTML;
  }
}

/** * 根據 ID 獲取或替換 HTML 元素的內容 */
const idToTemplate = cached(id => {
  const el = query(id);
  return el && el.innerHTML;
});

Vue.compile = compileToFunctions;

export default Vue;
複製代碼

總結: 跟着程序執行過程看下來,整個初始化的過程就是對 Vue 構造函數的包裝與豐富。

本部份內容旨在梳理初始化的全過程,對其中全局 API 及方法實現並未細化。


承接上文 - 「試着讀讀Vue源代碼」工程目錄及本地運行(斷點調試)

承接下文 - 「試着讀讀 Vue 源代碼」new Vue()發生了什麼 ❓

相關文章
相關標籤/搜索