当前,Vue和React已成为两大炙手可热的前端框架,这两个框架都算是业内一些最佳实践的集合体。其中,Vue最大的亮点和特色就是数据响应化,而React的特点则是单向数据流与jsx。
笔者近期正在研究Vue源码,在此过程中尝试实现一个简易版的Vue,而实现Vue的第一步便是解决数据响应化的问题。以下便是对Vue响应化的简易版实现。
数据响应的原理:
1、依赖收集:data通过Observer变成带有getter和setter方法的响应式对象,当外界通过Watcher获取数据时,会将该Watcher加入到Dep的依赖列表中,至此,就算完成了依赖收集
2、通知更新:当外界对已经响应化的对象,即data中的对象进行修改时,会触发setter方法,setter会通过Dep的通知方法,循环调用Dep依赖列表中Watcher的update方法通知外界更新视图或触发用户所给的监听回调
// kinerVue.js 简易版小程序入口
import Watcher from './Watcher.js';
import Observer,{set, del} from './Observer.js'
// 预期用法
// let vue = new KinerVue({
// data(){
// return {
// name: "kiner",
// userInfo: {
// age: 20
// },
// classify:['game','reading','running']
// }
// }
// });
// 数据响应的原理:
// 1、依赖收集:data通过Observer编程带有getter和setter方法的响应式对象,当外界通过Watcher获取数据时,会将该Watcher加入到Dep的依赖列表中,至此,就算完成了依赖收集
// 2、通知更新:当外界对已经响应化的对象,即data中的对象进行修改时,会触发setter方法,setter会通过Dep的通知方法,循环调用Dep依赖列表中Watcher的update方法通知外界更新视图或触发用户所给的监听回调
/**
* 自定义简易版Vue
*/
class KinerVue{
constructor(options){
this.$options = options;
this.$data = options.data.apply(this);
// 将数据交给Observer,让Observer将这个数据变成响应式对象
new Observer(this,this.$data);
this.isVue = true;
// test data start
//测试$watch start
let unWatchUserInfo = this.$watch("userInfo",(newVal,oldVal)=>{
console.log(`$watch监听到[userInfo]发生改变,新值:`,newVal,`;旧值:`,oldVal);
},{deep: false, immediate: true});
this.$watch("userInfo.age",(newVal,oldVal)=>{
console.log(`$watch监听到[userInfo.age]发生改变,新值:${newVal};旧值:${oldVal}`);
});
this.$watch("classify",function classifyWatcher(newVal,oldVal){
console.log(`$watch监听到[classify]发生改变,新值:${newVal},;旧值:${oldVal}`);
});
this.$watch("friends",function classifyWatcher(newVal,oldVal){
console.log(`$watch监听到[friends]发生改变,新值:${newVal},;旧值:${oldVal}`);
});
this.userInfo.age = 11;
// 取消订阅,执行了这行代码之后$watch("userInfo",()=>{})将失效
// unWatchUserInfo();
this.userInfo.age = 20;
//通过$set为数组设置值
this.$set(this.classify,3,'999');
this.$set(this.userInfo,'sex','男');
this.$set(this.userInfo,'sex','女');
//通过$delete删除后属性
this.$delete(this.userInfo,"sex");
console.log('sex:',this.userInfo)
// console.log(this.classify);
//测试$watch end
// new Watcher(this,"name");
// this.name;
// new Watcher(this,"userInfo.age");
// this.userInfo.age;
// new Watcher(this,"classify");
// this.classify;
//
// this.name = 'kanger';
// console.log(this.name);
// this.userInfo.age = 18;
// console.log(this.userInfo.age);
//
this.classify.push(10);
this.classify.splice(5,1,11);
this.classify.unshift(12);
this.classify.shift();
this.classify.sort((a,b)=>a-b);
this.classify.reverse();
this.friends.push('zzz');
this.friends.splice(2,1,'fff');
this.friends.unshift('kkk');
this.friends.sort((a,b)=>a-b);
this.friends.reverse();
this.friends.shift();
// 由于未采用ES6的元编程能力,也就是proxy和reflect,因此无法监控类似arr[0]=xxxx和arr.length=0之类的数值变化,
// 因此,在编码时要尽量避免这些写法,以免产生一些不可意料的问题
//
// this.classify[2] = 'working'; //错误用法
// console.log(this.classify);
// test data end
}
/**
* 监听器,用于监听属性变化,并将新旧值传递回来,方便做一些拦截操作
* @param exp 表达式或函数
* @param cb 回调
* @param options 配置项
* @returns {Function} 取消观察的方法
*/
$watch(exp,cb,options={immediate: true,deep: false}){
let watcher = new Watcher(this,exp,cb,options);
return ()=>{
watcher.unWatch();
};
}
/**
* 设置属性,用来解决无法使用arr[0]=xxx,obj={} obj.name=xxx
* @param target
* @param key
* @param value
*/
$set(target,key,value){
return set(target,key,value);
}
/**
* 删除目标对象上的数据
* @param target
* @param key
* @returns {undefined}
*/
$delete(target,key){
return del(target,key);
}
}
export default KinerVue;
// utils.js 基础工具库,提供一些工具方法
/**
* 判断对象是否支持__proto__属性
* @type {boolean}
*/
export const hasProto = '__proto__' in {};
/**
* 判断传递过来的对象是否是纯对象
* @param obj
* @returns {boolean}
*/
export const isPlainObject = function(obj){
let prototype;
return Object.prototype.toString.call(obj) === '[object Object]'
&& (prototype = Object.getPrototypeOf(obj), prototype === null ||
prototype === Object.getPrototypeOf({}))
};
/**
* 判断是否为非空对象
* @param obj
* @returns {boolean}
*/
export const isObject = obj => (obj !== null && typeof obj === 'object');
/**
* 显示警告消息
* @param message
*/
export const warn = function (message) {
console.warn(message);
};
/**
* 定义不可枚举的属性
* @param obj
* @param key
* @param value
* @param enumerable 能否枚举
*/
export const def = function (obj,key,value,enumerable) {
if(typeof obj === "object"){
Object.defineProperty(obj,key,{
value: value,
configurable: true,
enumerable: !!enumerable,
writable: true
});
}
};
/**
* 删除数组中的元素
* @param arr
* @param item
* @returns {T[]}
*/
export const removeArrItem = function (arr, item) {
const index = arr.indexOf(item);
if(index!==-1){
return arr.splice(index,1);
}
};
/**
* 根据表达式从目标对象中找到对应的值
* e.g.
* 若obj={userInfo:{userName}}
* exp="userInfo.userName"
*
* @param obj
* @param exp
* @returns {*}
*/
export const parseExp = function (exp) {
return obj => {
let reg = /[^\w.$]/;
if(reg.test(exp)){
return;
}else{
let subExp = exp.split('.');
subExp.forEach(item=>{
obj = obj[item];
});
return obj;
}
};
};
/**
* 判断两个变量是否相等(但因为一个特殊情况,当a和b都等于NaN时,因为NaN===NaN输出为false)
* @param a
* @param b
* @returns {boolean}
*/
export const isEqual = (a,b) => a===b||(a!==a&&b!==b);
/**
* 将拦截器方法直接覆盖到目标对象的原型链上__proto__
* @param obj
* @param target
* @returns {*}
*/
export const patchToProto = (obj,target) => obj.__proto__ = target;
/**
* 直接在目标对象上定义不可枚举的属性
* @param obj
* @param arrayMethods
* @param keys
* @returns {*}
*/
export const copyArgument = (obj,arrayMethods,keys) => keys.forEach(key=>def(obj,key,arrayMethods[key]));
/**
* 判断当前浏览器是否支持__proto__若支持,这直接将目标方法覆盖到__proto__上,否则,直接将方法定义在目标对象上
* @param obj
* @param src
* @param keys
* @returns {*}
*/
export const defProtoOrArgument = (obj,src,keys=Object.getOwnPropertyNames(src)) => hasProto ? patchToProto(obj,src) : copyArgument(obj,src,keys);
/**
* 判断目标对象是否含有指定属性
* @param obj
* @param key
* @returns {boolean}
*/
export const hasOwn = (obj,key) => obj.hasOwnProperty(key);
/**
* 判断目标对象是否已经响应化
* @param obj
* @returns {boolean}
*/
export const hasOb = obj => hasOwn(obj,'__ob__');
/**
* 判断传入参数类型是否为函数
* @param fn
* @returns {boolean}
*/
export const isFn = fn => typeof fn === "function";
/**
* 判断所给参数是否是一个数组
* @param arr
* @returns {arg is Array<any>}
*/
export const isA = arr => Array.isArray(arr);
/**
* 判断给定参数是否是合法的数组索引
*/
export const isValidArrayIndex = (val) => {
const n = parseFloat(String(val));
return n >= 0 && Math.floor(n) === n && isFinite(val)
};
export default {
hasProto,
isPlainObject,
isObject,
warn,
def,
removeArrItem,
isEqual,
parseExp,
patchToProto,
copyArgument,
defProtoOrArgument,
hasOwn,
hasOb,
isFn,
isA
}
// Array.js 定义一些针对数组响应化时需要用到的辅助数据以及定义了
import {def} from "./utils.js";
// 数组原型,在对数组方法打补丁的时候,需要用到数组原型方法用于实现原本的数组操作
export const arrayProto = Array.prototype;
/**
* 需要打补丁的数组方法,即会改变数组的方法
* @type {string[]}
*/
export const needPatchArrayMethods = [
"push",
"pop",
"unshift",
"shift",
"sort",
"reverse",
"splice"
];
// 根据数组原型创建一个新的基础数组对象,避免为数组方法打补丁的时候污染原始数组
export const arrayMethods = Object.create(arrayProto);
// 实现数组拦截器,通过这个拦截器实现拦截数组操作方法操作
needPatchArrayMethods.forEach(method=>{
// 从数组原型中将原始方法取出
const originalMethod = arrayProto[method];
def(arrayMethods,method,function mutator(...args){
// const oldVal = [...this];
// 调用数组原始方法实现数组操作
const res = originalMethod.apply(this,args);
// 若当前数组已经是响应化后的数组,则将其Observe实例取出,用户后续通知更新操作
const ob = this.__ob__;
// 若执行的是会新增数组元素的方法,我们需要对新增的元素也进行响应化处理
// 其中push和unshift接收的所有参数都是新增元素,因此直接将参数对象传递给defineReactiveForArray进行响应化处理
// splice第2个之后的参数便为新增或替换的元素,因此将第2个之后的参数提取出来,传递给defineReactiveForArray进行响应化处理
let inserted;
switch (method){
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.splice(2);
break;
}
inserted && ob.defineReactiveForArray(inserted);
//通知依赖更新
ob&&ob.dep.notify();
// console.log(`---->触发了数组的${method}方法:新值:`,this,`;旧值:`,oldVal);
return res;
});
});
// Dep.js 依赖类,用于统一管理观察者,一旦依赖跟新,便可通过此类的notify方法通知其订阅的所有
// 观察者进行更新数据
import {removeArrItem} from "./utils.js";
let uid = 0;
/**
* 用来管理所有的watcher
*/
class Dep {
constructor(){
// 订阅者列表
this.subs = [];
// 为每一个依赖定义一个唯一的id
this.id = uid++;
}
/**
* 触发添加依赖
*/
depend(){
//为实现取消订阅的功能,将订阅的方法放在watcher中,此处通过调用watcher的addDep将当前依赖加入到订阅列表,
Dep.target&&Dep.target.addDep(this);
// 初版实现,未实现取消订阅功能
// Dep.target&&this.addDep(Dep.target);
}
/**
* 添加订阅者
* @param watcher 订阅者
*/
addSub(watcher){
// 为解决当调用数组的splice和sort方法时,会触发多次更新的问题,加入订阅时先看一下该依赖是否已经被添加
if(this.subs.indexOf(watcher)<0){
this.subs.push(watcher);
}
}
/**
* 从从订阅列表中移除订阅者
* @param watcher
*/
removeSub(watcher){
removeArrItem(this.subs,watcher);
}
/**
* 通知订阅者更新
*/
notify(){
this.subs.forEach(watcher=>{
watcher.update()
});
}
}
export default Dep;
/**
* Observer.js 数据响应化对象
* Vue数据响应化的核心,Vue2.0时代通过Object.defineProperty方式进行数据响应化,而Vue3.0时代则采用Proxy和Reflect方式实现
* 无论采用哪种方式,但其实现原理都是一样的,都是通过数据劫持的方式实现响应化
*/
import Dep from "./Dep.js";
import {isPlainObject, warn, isEqual,defProtoOrArgument, hasOb, def, isA, isValidArrayIndex, hasOwn} from "./utils.js";
import {arrayMethods} from "./Array.js";
class Observer {
/**
* 定义统一的操作方法,方便之后收集依赖和响应通知的统一操作
* @param obj 待响应的对象
* @param key 待响应的键值
* @param value 待响应的值
* @param childOb 子响应对象
* @returns {*}
*/
static baseHandler(obj, key, value,childOb) {
//定义一个依赖对象,与data的key存在一一对应的关系
const dep = new Dep();
return {
enumerable: true,
configurable: true,
get() {
// Dep.target && dep.addDep(Dep.target);
// 在访问对象属性时,将当前属性加入到依赖列表中
dep.depend();
// console.log('收集依赖',obj,key,value,childOb);
// 用于收集数组对象的依赖
childOb && childOb.dep.depend();
// console.log(`获取${key}的值:${value}`);
return value;
},
set(val) {
// isEqual:原本的目的是为了判断新值val和旧值value相等的情况下,便直接退出,
// 但因为一个特殊情况,当val和value都等于NaN时,因为NaN===NaN输出为false
// 会让set方法继续往下执行,因此多加了一个(value!==value&&val!==val)进行拦截
//
if (isEqual(val, value)) {
return;
}
// 由于旧值仍处于闭包当中,this.$data未释放的情况下,直接对value赋值可直接操作this.$data下对应键值下的数据,所以进行以下赋值操作
value = val;
// 通知依赖列表循环更新依赖
dep.notify();
// console.log(`设置${key}的值:${val}`);
}
}
};
constructor(vm, target) {
this.$vm = vm;
// 将跟数据target设置为已响应,以免重复创建示例
def(target,'__ob__',this);
//在此定义依赖收集对象,用来收集数组的依赖
this.dep = new Dep();
// 将目标变化变为响应式对象
this.observer(target);
}
/**
* 对传入的数据进行响应化处理
* @param data
*/
observer(data) {
if (Array.isArray(data)) {//传过来的数据是否是数组
defProtoOrArgument(data,arrayMethods);
return this.defineReactiveForArray(data)
} else if (isPlainObject(data)) {//传递过来的
return this.defineReactiveForObject(data);
} else {
warn(`传递的数据必须是对象或数组,当前传递的值【${data}】类型为:${typeof data},因此无需响应化`);
}
}
/**
* 实现对象类型的响应化处理
* @param obj
*/
defineReactiveForObject(obj) {
let keys = Object.keys(obj);
keys.forEach(key => {
this.defineReactive(obj, key, obj[key]);
// 添加数据代理,将$data中的值代理到this,这样就可以直接通过this.xxx访问$data中的属性了
this.proxyData(key);
});
}
/**
* 实现数组类型的响应化处理
* @param data
*/
defineReactiveForArray(data) {
data.forEach(item=>this.createObserver(item));
}
/**
* 将对象变为响应式对象,通过递归调用observer方法可以实现嵌套对象响应化
* @param obj 带响应化对象
* @param key 待响应的键值
* @param value 待响应的值
*/
defineReactive(obj, key, value) {
let childOb = this.createObserver(value);
Object.defineProperty(obj, key, Observer.baseHandler(obj, key, value,childOb));
}
/**
* 判断目标数据是否已经响应化,如果响应化,则直接返回其响应化对象__ob__,佛则示例话一个响应化对象
* @param data
* @returns {*}
*/
createObserver(data){
let ob;
if(hasOb(data)){//该对象已经响应化,直接获取
ob = data.__ob__;
}else{
ob = new Observer(this.$vm,data);
}
return ob;
}
/**
* 代理$data,将$data中的数据代理到vue实例中,便可直接通过this.xxx获取或设置值
* @param key
* @returns {*}
*/
proxyData(key) {
Object.defineProperty(this.$vm, key, {
get() {
return this.$data[key];
},
set(val) {
this.$data[key] = val;
}
})
}
}
/**
* 为目标对象或数组增加设置/新增值
* @param target
* @param key
* @param val
* @returns {*}
*/
export const set = (target,key,val)=>{
//如果target是数组且key是合法的数组索引,则将目标值加入到数组中
if(isA(target) && isValidArrayIndex(key)){
target.length = Math.max(target.length,key);
target.splice(key,1,val);
return val;
}
//如果key是target非原型链上的属性,说明该key已经是响应化对象了,无需重复响应化,直接修改对应的值即可
if(key in target && !(key in Observer.prototype)){
target[key] = val;
return val;
}
//新增属性
const ob = target.__ob__;
//如果当前对象未被响应化,则直接设置目标值
if(!ob){
target[key] = val;
return val;
}
// TODO 不能在跟对象this.$data和Vue示例上添加属性
// 如果target是响应化对象,则通过Observer的defineRelative方法设置属性
ob.defineReactive(target,key,val);
ob.dep.notify();
return val;
};
export const del = (target,key) => {
//如果target是数组且key是合法的数组索引,则删除掉指定索引的数组项
if(isA(target) && isValidArrayIndex(key)){
target.splice(key,1);
return;
}
//若target本身就不具有key属性,则无需删除,直接返回
if(!hasOwn(target,key)) return;
// TODO 不能在跟对象this.$data和Vue示例上删除属性
const ob = target.__ob__;
delete target[key];
// 通知依赖更新
ob && ob.dep.notify();
};
export default Observer;
// Watcher.js
// 它相当于是依赖Dep与具体的更新操作的一个中介,也可以理解为他是一个物流中转站,依赖就像是快递,具体更新操作就是快递的目的地,具体流程是这样的:
// 我们把快递(更新)交给快递代收点(Dep),当快递代收点(Dep)接收到快递之后,会有人来收集快递送到快递中转站(watcher),然后再由快递中转账再统一派发到不同的地址。
import Dep from './Dep.js';
import {parseExp, isObject,isFn} from "./utils.js";
import {arrayMethods} from "./Array.js";
import {traverse} from "./Traverse.js";
class Watcher {
constructor(vm,expOrFn,cb=function(){},options={immediate: true,deep: false}){
// 创建实例时,将当前实例对象指向Dep的静态属性target
this.$vm = vm;
// 需要坚挺的表达式或者是给定的函数(注:如为函数,则可在函数内使用到的响应化对象属性都会被观察,一旦任一属性值发生变化,都会触发cb回调通知)
this.expOrFn = expOrFn;
// 选项
// options.immediate true|false 代表是否在创建watcher实例时变直接运行表达式或函数获取结果
// options.deep true|false 代表是否进行深度观察,如果为true,会对指定表达式或对象下使用的属性的子属性进行递归观察操作
e.g. data下的对象userInfo的结构是:userInfo:{friends:[{name:'kiner'},{name:'kanger'}],bankInfo:{bankCardNum: 'xxxxxxx'}}
那么,如果我们要进行深度观察,则如:this.$watch("userInfo",()=>{},{deep:true})
此后,一旦userInfo下面的任一属性,包括子对象、数组中的值发生改变,上述的$watch都能够观察得到
this.options = options;
// 若给出的是函数,则直接将其赋值给gutter
if(isFn(expOrFn)){
this.gutter = expOrFn;
}else{
// 若给出的是一个如:userInfo.name或age之类的表达式,则通过parseExp这个高阶函数将表达式进行一定的处理并赋值给gutter
// 使我们可以直接通过this.gutter.call(this.$vm,this.$vm);的方式直接获得表达式对应的结果
this.gutter = parseExp(expOrFn);
}
// 观察者通知的回调函数
this.cb = cb;
// 为实现取消订阅功能,需要知道watcher都订阅了哪些依赖,在取消订阅时,秩序把对应的依赖从依赖列表移除即可
// 为方便订阅,将依赖列表从Dep移到watcher
this.deps = [];
// 为了标志依赖的唯一性,定义一个不可重复的Set用于存储依赖的id
this.depIds = new Set();
// 如果指定immediate=true则在实例化时离开触发get获取目标值
if(options.immediate){
this.value = this.get();
}
}
/**
* 尝试通过表达式或者所给方法获取目标值
* @returns {*}
*/
get(){
Dep.target = this;//指定快递代收点所属的中转站,这样才能够将快递精确的从代收点送到中转站
//根据给定的表达式或函数直接或取目标值,与此同时,因为触发了get,会将Dep.target添加到依赖列表当中
let value = this.gutter.call(this.$vm,this.$vm);
this.sourceValue = value;
// 若需要观察对象系所有子对象的变化(注:此步骤必须放在`Dep.target = undefined;`之前,因为递归收集子对象依赖时仍需要使用到Dep.target)
if(this.options.deep){
traverse(value);
}
//尝试解决当value为数组或对象时,newVal和oldVal恒等问题(注:此步骤是因为个人开发原因需要获取对象或数组的新旧值,为方便操作,尝试性实现,Vue官方并无此步骤)
if(value.__proto__===arrayMethods){
value = [...value];
}else if(isObject(value)){
value = {...value}
}
// 加入依赖列表之后释放target
Dep.target = undefined;
return value;
}
// 中转站已经收到快递了,准备派送,通知各位快递小哥过来拿各自负责区域(视图中的表达式或$watch中监听的方法)的快递进行派送
update(){
// 接收到更新通知时,触发get方法获取改表达式最新的值
const value = this.get();
// vue源码中:如果value是数组/对象时,我们通过$watch((newVal,oldVal)=>{})获取到的newVal和oldVal其实是始终相等的,因为他们都是东一个对象的引用
if(this.value!==value||isObject(value)){
const oldVal = this.value;
this.value = value;
// 将新旧值传递给回调函数,即完成$watch('xxxxx',function(newVal,oldVal){})的通知
this.cb.call(this.$vm,value,oldVal);
}
// console.log(`属性${this.expOrFn}发生了变化`);
}
/**
* 添加依赖并经自己订阅到依赖当中
* @param dep
*/
addDep(dep){
const depId = dep.id;
// 判断依赖是否已经在依赖列表中,若不存在,则添加依赖
if(!this.depIds.has(depId)){
this.deps.push(dep);
this.depIds.add(depId);
// 为添加的依赖订阅观察者
dep.addSub(this);
}
}
/**
* 取消观察,移除依赖列表中所有的当前观察者
*/
unWatch(){
let len = this.deps.length;
while (len--){
this.deps[len].removeSub(this);
}
}
}
export default Watcher;
// Traverse.js 通过traverse递归访问指定对象,通过触发getter的方式实现依赖收集
import {isA,isObject,hasOb} from "./utils.js";
// 用于存储依赖id
const depIds = new Set();
// 通过这个方法访问一下给定目标对象的子对象,从而触发依赖通知
export const traverse = (val) => {
_traverse(val,depIds);
depIds.clear();
};
function _traverse(val,depIds){
let len,keys;
// 所传对象如果类型不是非冻结对象或数组,就直接终止
if((!isA(val) && !isObject(val)) || Object.isFrozen(val)){
return;
}
// 判断当前对象是否已经是响应化对象
if(hasOb(val)){
const depId = val.__ob__.dep.id;
if(depIds.has(depId)){//已经访问过了,直接终止
return;
}
//若未访问过,则将依赖id加入到depIds中
depIds.add(depId);
}
if(isA(val)){//如果是数组,则循环访问其子项并递归访问
len = val.length;
while (len--) _traverse(val,depIds);
}else{//循环对象下的所有属性并递归访问
keys = Object.keys(val);
len = keys.length;
while (len--) _traverse(val,depIds);
}
}
以上代码已实现功能:
- 数据响应化-Observe.js(Array.js-数组响应化的一些相关处理)
- 数据观察者-Watcher.js(Traverse.js-通过traverse递归访问指定对象,通过触发getter的方式实现依赖收集)
- 依赖管理者-Dep.js
- 工具方法$watch-观察属性变化的方法、$set-为对象添加属性或者为数组添加子项,并通知依赖更新、$delete-删除对象属性或删除数组子项并通知依赖更新
以后将陆续会尝试实现:
- 虚拟Dom(VNode)
- 编译器
- Vue生命周期钩子、工具方法、全局Api实现
- 指令的解析
- 过滤器的实现
- 行业最佳实践学习(Vue-Router、Vuex、Element-UI、Element-Admin、Vant)
文章将根据本人对Vue及其相关最佳实践的学习进度不断更新,如有不对,欢迎指正,谢谢!
PS:如果对Vue3(即vue-next)感兴趣的同学,可以看一下本人撰写的另一篇文章 Vue3(Vue-next)响应化实现剖析,这里简单的实现了使用es6元编程能力(proxy和reflect)实现的数据响应化原理。