목차
深入剖析php执行原理(2):函数的编译,深入剖析php
函数的编译
1、语法定义
2、开始编译
3、编译参数列表
3.1 fetch_simple_variable
3.2 zend_do_receive_arg
4、编译函数体
5、结束编译
6、绑定
7、总结
백엔드 개발 PHP 튜토리얼 深入剖析php执行原理(2):函数的编译,深入剖析php_PHP教程

深入剖析php执行原理(2):函数的编译,深入剖析php_PHP教程

Jul 12, 2016 am 08:53 AM
jquery

深入剖析php执行原理(2):函数的编译,深入剖析php

本文只探讨纯粹的函数,并不包含方法。对于方法,会放到类、对象中一起研究。

想讲清楚在zend vm中,函数如何被正确的编译成op指令、如何发生参数传递、如何模拟调用栈、如何切换作用域等等,的确是一个很大范畴的话题。但为了弄明白php的原理,必须要攻克它。

对函数的研究,大致可以分成两块。第一块是函数体的编译,主要涉及到如何将函数转化成zend_op指令。第二块是研究函数的调用,涉及到函数调用语句的编译,以及函数如何被执行等topic。这里先来看看函数如何被编译,我们下一篇再讲函数的调用。

函数的编译

对函数进行编译,最终目的是为了生成一份对应的op指令集,除了op指令集,编译函数还会产生其他一些相关的数据,比如说函数名称、参数列表信息、compiled variables,甚至函数所在文件,起始行数等等。这些信息作为编译的产出,都需要保存起来。保存这些产出的数据结构,正是上一节中所描述的zend_op_array。下文会以op_array作为简称。

下面列出了一个简单的例子:

<?<span>php
</span><span>function</span> foo(<span>$arg1</span><span>)
{
    </span><span>print</span>(<span>$arg1</span><span>);
}

</span><span>$bar</span> = 'hello php'<span>;
foo(</span><span>$bar</span>);
로그인 후 복사

这段代码包含了一个最简单的函数示例。

在这样一份php脚本中,最终其实会产生两个op_array。一个是由函数foo编译而来,另一个则是由除去foo之外代码编译生成的。同理可以推出,假如一份php脚本其中包含有2个函数和若干语句,则最终会产生3个op_array。也就是说,每个函数最终都会被编译成一个对应的op_array。

刚才提到,op_array中有一些字段是和函数息息相关的。比如function_name代表着函数的名称,比如num_args代表了函数的参数个数,比如required_num_args代表了必须的参数个数,比如arg_info代表着函数的参数信息...etc。

下面会继续结合这段代码,来研究foo函数详细的编译过程。

1、语法定义

从zend_language_parser.y文件中可以看出,函数的语法分析大致涉及如下几个推导式:

top_statement:<br />        statement                          { zend_verify_namespace(TSRMLS_C); }<br />    |    function_declaration_statement    { zend_verify_namespace(TSRMLS_C); zend_do_early_binding(TSRMLS_C); }<br />    |    class_declaration_statement       { zend_verify_namespace(TSRMLS_C); zend_do_early_binding(TSRMLS_C); }<br />    ...<br /><br />function_declaration_statement:
	unticked_function_declaration_statement	{ DO_TICKS(); }
;

unticked_function_declaration_statement:
	function is_reference T_STRING { zend_do_begin_function_declaration(&$1, &$3, 0, $2.op_type, NULL TSRMLS_CC); }
	'(' parameter_list ')' '{' inner_statement_list '}' { zend_do_end_function_declaration(&$1 TSRMLS_CC); }
;<br /><br />is_reference:<br />        /* empty */    { $$.op_type = ZEND_RETURN_VAL; }<br />    |    '&'            { $$.op_type = ZEND_RETURN_REF; }<br />;<br /><br />parameter_list:<br />        non_empty_parameter_list<br />    |    /* empty */<br />;

non_empty_parameter_list:
		optional_class_type T_VARIABLE				{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$2, 0 TSRMLS_CC); }
	|	optional_class_type '&' T_VARIABLE			{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$3, 1 TSRMLS_CC); }
	|	optional_class_type '&' T_VARIABLE '=' static_scalar	{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$5, &$1, &$3, 1 TSRMLS_CC); }
	|	optional_class_type T_VARIABLE '=' static_scalar	{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$4, &$1, &$2, 0 TSRMLS_CC); }
	|	non_empty_parameter_list ',' optional_class_type T_VARIABLE              { znode tmp;  fetch_simple_variable(&tmp, &$4, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$3, &$4, 0 TSRMLS_CC); }
	|	non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE            { znode tmp;  fetch_simple_variable(&tmp, &$5, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$3, &$5, 1 TSRMLS_CC); }
	|	non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE	 '=' static_scalar { znode tmp;  fetch_simple_variable(&tmp, &$5, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$7, &$3, &$5, 1 TSRMLS_CC); }
	|	non_empty_parameter_list ',' optional_class_type T_VARIABLE '=' static_scalar    { znode tmp;  fetch_simple_variable(&tmp, &$4, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$6, &$3, &$4, 0 TSRMLS_CC); }
;
로그인 후 복사

这里并没有截取完整,主要是缺少函数体内语句的语法分析,但已经足够我们弄清楚编译过程中的一些细节。

函数体内的语句,其对应的语法为inner_statement_list。inner_statement_list和函数体之外一般的语句并无二致,可以简单当成普通的语句来编译。

最重要的是看下unticked_function_declaration_statement,它定义了函数语法的骨架,同时还可以看出,函数编译中会执行zend_do_begin_function_declaration以及zend_do_end_function_declaration。这两步分别对应着下文提到的开始编译和结束编译。我们先来看zend_do_begin_function_declaration。

2、开始编译

当解析器遇到一段函数声明时,会尝试开始编译函数,这是通过执行zend_do_begin_function_declaration来完成的。

有两点:

1,函数是否返回引用,通过is_reference判断。可以看到在对is_reference进行语法分析时,可能会将op_type赋予ZEND_RETURN_VAL或ZEND_RETURN_REF。根据我们文章开始给出的php代码示例,函数foo并不返回引用,因此这里$2.op_type为ZEND_RETURN_VAL。话说由 function & func_name(){ ... } 这种形式来决定是否返回引用,已经很古老了,还是在CI框架中见过,现在很少有类似需求。

2,zend_do_begin_function_declaration接受的第一个参数,是对function字面进行词法分析生成的znode。这个znode被使用得非常巧妙,因为在编译函数时,zend vm必须将CG(active_op_array)切换成函数自己的op_array,以便于存储函数的编译结果,当函数编译完成之后,zend vm又需要将将CG(active_op_array)恢复成函数体外层的op_array。利用该znode保存函数体外的op_array,可以很方便的在函数编译结束时进行CG(active_op_array)恢复,具体后面会讲到。

研究下zend_do_begin_function_declaration的实现,比较长,我们分段来看:

<span>//</span><span> 声明函数会变编译成的op_array</span>
<span>zend_op_array op_array;

</span><span>//</span><span> 函数名、长度、起始行数</span>
<span>char</span> *name = function_name-><span>u.constant.value.str.val;
</span><span>int</span> name_len = function_name-><span>u.constant.value.str.len;
</span><span>int</span> function_begin_line = function_token-><span>u.opline_num;
zend_uint fn_flags;
</span><span>char</span> *<span>lcname;
zend_bool orig_interactive;
ALLOCA_FLAG(use_heap)

</span><span>if</span><span> (is_method) {
    ...
} </span><span>else</span><span> {
    fn_flags </span>= <span>0</span><span>;
}

</span><span>//</span><span> 对函数来说,fn_flags没用,对方法来说,fn_flags指定了方法的修饰符</span>
<span>if</span> ((fn_flags & ZEND_ACC_STATIC) && (fn_flags & ZEND_ACC_ABSTRACT) && !(CG(active_class_entry)->ce_flags &<span> ZEND_ACC_INTERFACE)) {
    zend_error(E_STRICT, </span><span>"</span><span>Static function %s%s%s() should not be abstract</span><span>"</span>, is_method ? CG(active_class_entry)->name : <span>""</span>, is_method ? <span>"</span><span>::</span><span>"</span> : <span>""</span>, Z_STRVAL(function_name-><span>u.constant));
}</span>
로그인 후 복사

这段代码一开始就印证了我们先前的说法,每个函数都有一份自己的op_array。所以会在开头先声明一个op_array变量。

<span>//</span><span> 第一个znode参数的妙处,它记录了当前的CG(active_op_array)</span>
function_token->u.op_array =<span> CG(active_op_array);
lcname </span>=<span> zend_str_tolower_dup(name, name_len);

</span><span>//</span><span> 对op_array进行初始化,强制op_array.fn_flags会被初始化为0</span>
orig_interactive =<span> CG(interactive);
CG(interactive) </span>= <span>0</span><span>;
init_op_array(</span>&<span>op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE TSRMLS_CC);
CG(interactive) </span>=<span> orig_interactive;

</span><span>//</span><span> 对op_array的一些设置</span>
op_array.function_name =<span> name;
op_array.return_reference </span>=<span> return_reference;
op_array.fn_flags </span>|=<span> fn_flags;
op_array.pass_rest_by_reference </span>= <span>0</span><span>;
op_array.scope </span>= is_method ?<span> CG(active_class_entry):NULL;
op_array.prototype </span>=<span> NULL;
op_array.line_start </span>= zend_get_compiled_lineno(TSRMLS_C);
로그인 후 복사

function_token便是对function字面进行词法分析而生成的znode。这段代码一开始,就让它保存当前的CG(active_op_array),即函数体之外的op_array。保存好CG(active_op_array)之后,便会开始对函数自己的op_array进行初始化。

op_array.fn_flags是个多功能字段,还记得上一篇中提到的交互式么,如果php以交互式打开,则op_array.fn_flags会被初始化为ZEND_ACC_INTERACTIVE,否则会被初始化为0。这里在init_op_array之前设置CG(interactive) = 0,便是确保op_array.fn_flags初始化为0。随后会进一步执行op_array.fn_flags |= fn_flags,如果是在方法中,则op_array.fn_flags含义为static、abstract、final等修饰符,对函数来讲,op_array.fn_flags依然是0。

zend_op *opline =<span> get_next_op(CG(active_op_array) TSRMLS_CC);

</span><span>//</span><span> 如果处于命名空间,则函数名还需要加上命名空间</span>
<span>if</span><span> (CG(current_namespace)) {
    </span><span>/*</span><span> Prefix function name with current namespace name </span><span>*/</span><span>
    znode tmp;

    tmp.u.constant </span>= *<span>CG(current_namespace);
    zval_copy_ctor(</span>&<span>tmp.u.constant);
    zend_do_build_namespace_name(</span>&tmp, &<span>tmp, function_name TSRMLS_CC);
    op_array.function_name </span>=<span> Z_STRVAL(tmp.u.constant);
    efree(lcname);
    name_len </span>=<span> Z_STRLEN(tmp.u.constant);
    lcname </span>=<span> zend_str_tolower_dup(Z_STRVAL(tmp.u.constant), name_len);
}

</span><span>//</span><span> 设置opline</span>
opline->opcode =<span> ZEND_DECLARE_FUNCTION;
</span><span>//</span><span> 第一个操作数</span>
opline->op1.op_type =<span> IS_CONST;
build_runtime_defined_function_key(</span>&opline-><span>op1.u.constant, lcname, name_len TSRMLS_CC);
</span><span>//</span><span> 第二个操作数</span>
opline->op2.op_type =<span> IS_CONST;
opline</span>->op2.u.constant.type =<span> IS_STRING;
opline</span>->op2.u.constant.value.str.val =<span> lcname;
opline</span>->op2.u.constant.value.str.len =<span> name_len;
Z_SET_REFCOUNT(opline</span>->op2.u.constant, <span>1</span><span>);
opline</span>->extended_value =<span> ZEND_DECLARE_FUNCTION;

</span><span>//</span><span> 切换CG(active_op_array)成函数自己的op_array</span>
zend_hash_update(CG(function_table), opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, &op_array, <span>sizeof</span>(zend_op_array), (<span>void</span> **) &CG(active_op_array));
로그인 후 복사

上面这段代码很关键。有几点要说明的:

1,如果函数是处于命名空间中,则其名称会被扩展成命名空间\函数名。比如:

<?<span>php
namespace MyProject;

</span><span>function</span> foo(<span>$arg1</span>, <span>$arg2</span> = 100<span>)
{
    </span><span>print</span>(<span>$arg1</span><span>);
}</span>
로그인 후 복사

则会将函数名改为MyProject\foo。扩展工作由zend_do_build_namespace_name来完成。

2,build_runtime_defined_function_key会生成一个“key”。除了用到函数名称之外,还用到了函数所在文件路径、代码在内存中的地址等等。具体的实现可以自行阅读。将函数放进CG(function_table)时,用的键便是这个“key”。

3,代码中的op_line获取时,尚未发生CG(active_op_array)的切换。也就是说,op_line依然是外层op_array的一条指令。该指令具体为ZEND_DECLARE_FUNCTION,有两个操作数,第一个操作数保存了第二点中提到的“key”,第二个操作数则保存了形如"myproject\foo"这样的函数名(小写)。

4,这段代码的最后,将函数自身对应的op_array存放进了CG(function_table),同时,完成了CG(active_op_array)的切换。从这条语句开始,CG(active_op_array)便开始指向函数自己的op_array,而不再是函数体外层的op_array了。

继续来看zend_do_begin_function_declaration的最后一段:

<span>//</span><span> 需要debuginfo,则函数体内的第一条zend_op,为ZEND_EXT_NOP</span>
<span>if</span> (CG(compiler_options) &<span> ZEND_COMPILE_EXTENDED_INFO) {
    zend_op </span>*opline =<span> get_next_op(CG(active_op_array) TSRMLS_CC);

    opline</span>->opcode =<span> ZEND_EXT_NOP;
    opline</span>->lineno =<span> function_begin_line;
    SET_UNUSED(opline</span>-><span>op1);
    SET_UNUSED(opline</span>-><span>op2);
}

</span><span>//</span><span> 控制switch和foreach内声明的函数</span>
<span>{
    </span><span>/*</span><span> Push a seperator to the switch and foreach stacks </span><span>*/</span><span>
    zend_switch_entry switch_entry;

    switch_entry.cond.op_type </span>=<span> IS_UNUSED;
    switch_entry.default_case </span>= <span>0</span><span>;
    switch_entry.control_var </span>= <span>0</span><span>;

    zend_stack_push(</span>&CG(switch_cond_stack), (<span>void</span> *) &switch_entry, <span>sizeof</span><span>(switch_entry));

    {
        </span><span>/*</span><span> Foreach stack separator </span><span>*/</span><span>
        zend_op dummy_opline;

        dummy_opline.result.op_type </span>=<span> IS_UNUSED;
        dummy_opline.op1.op_type </span>=<span> IS_UNUSED;

        zend_stack_push(</span>&, (<span>void</span> *) &dummy_opline, <span>sizeof</span><span>(zend_op));
    }
}

</span><span>//</span><span> 保存函数的注释语句</span>
<span>if</span><span> (CG(doc_comment)) {
    CG(active_op_array)</span>->doc_comment =<span> CG(doc_comment);
    CG(active_op_array)</span>->doc_comment_len =<span> CG(doc_comment_len);
    CG(doc_comment) </span>=<span> NULL;
    CG(doc_comment_len) </span>= <span>0</span><span>;
}

</span><span>//</span><span> 作用和上面switch,foreach是一样的,函数体内的语句并不属于函数体外的label</span>
zend_stack_push(&CG(labels_stack), (<span>void</span> *) &CG(labels), <span>sizeof</span>(HashTable*<span>));
CG(labels) </span>= NULL;
로그인 후 복사

可能初学者会对CG(switch_cond_stack),CG(foreach_copy_stack),CG(labels_stack)等字段有疑惑。其实也很好理解。以CG(labels_stack)为例,由于进入函数体内之后,op_array发生了切换,外层的CG(active_op_array)被保存到function znode的u.op_array中(如果记不清楚了回头看上文:-))。因此函数外层已经被parse出的一些label也需要被保存下来,用的正是CG(labels_stack)来保存。当函数体完成编译之后,zend vm可以从CG(labels_stack)中恢复出原先的label。举例来说,

<?<span>php
label1</span>:
<span>function</span> foo(<span>$arg1</span><span>)
{
    </span><span>print</span>(<span>$arg1</span><span>);
    goto label2;
    
label2</span>:
    <span>exit</span><span>;
}

</span><span>$bar</span> = 'hello php'<span>;
foo(</span><span>$bar</span>);
로그인 후 복사

解释器在进入zend_do_begin_function_declaration时,CG(labels)中保存的是“label1”。当解释器开始编译函数foo,则需要将“label1”保存到CG(labels_stack)中,同时清空CG(labels)。因为在编译foo的过程中,CG(labels)会保存“labe2”。当foo编译完成,会利用CG(labels_stack)来恢复CG(labels),则CG(labels)再次变成“label1”。

至此,整个zend_do_begin_function_declaration过程已经全部分析完成。最重要的是,一旦完成zend_do_begin_function_declaration,CG(active_op_array)就指向了函数自身对应的op_array。同时,也利用生成的“key”在CG(function_table)中替函数占了一个位

3、编译参数列表

函数可以定义为不接受任何参数,对于参数列表为空的情况,其实不做任何处理。我们前文的例子foo函数,接受了一个参数$arg1,我们下面还是分析有参数的情况。

根据语法推导式non_empty_parameter_list的定义,参数列表一共有8种,前4种对应的是一个参数,后4种对应多个参数。我们只关心前4种,后4种编译的过程,仅仅是重复前4种的步骤而已。

optional_class_type T_VARIABLE				{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$2, 0 TSRMLS_CC); }
optional_class_type '&' T_VARIABLE			{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$3, 1 TSRMLS_CC); }
optional_class_type '&' T_VARIABLE '=' static_scalar	{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$5, &$1, &$3, 1 TSRMLS_CC); }
optional_class_type T_VARIABLE '=' static_scalar	{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$4, &$1, &$2, 0 TSRMLS_CC); }
로그인 후 복사

前4种情况,具体又可以分为2类,1类没有默认值,区别只在于参数的传递是否采用引用,而另1类,都有默认值“static_scalar”。

实际上区别并不大,它们的语法分析的处理过程也几乎一致。都是先调用fetch_simple_variable,再执行zend_do_receive_arg。有没有默认值,区别也仅仅在于zend_do_receive_arg的参数,会不会将默认值传递进去。先来看fetch_simple_variable。

3.1 fetch_simple_variable

fetch_simple_variable是用来获取compiled variables索引的。compiled variables被视作php的性能提升手段之一,因为它利用数组存储了变量,而并非内核中普遍使用的HashTable。这里可以看出,函数的任何一个参数,均会被编译为compiled variables,compiled variables被保存在函数体op_array->vars数组中。虽然根据变量名称去HashTable查询,效率并不低。但显然根据索引去op_array->vars数组中获取变量,会更加高效。

<span>void</span> fetch_simple_variable_ex(znode *result, znode *varname, <span>int</span> bp, zend_uchar op TSRMLS_DC) <span>/*</span><span> {{{ </span><span>*/</span><span>
{
    zend_op opline;
    ...

    </span><span>if</span> (varname->op_type ==<span> IS_CONST) {
        </span><span>if</span> (Z_TYPE(varname->u.constant) !=<span> IS_STRING) {
            convert_to_string(</span>&varname-><span>u.constant);
        }
        </span><span>if</span> (!zend_is_auto_global(varname->u.constant.value.str.val, varname->u.constant.value.str.len TSRMLS_CC) &&
            !(varname->u.constant.value.str.len == (<span>sizeof</span>(<span>"</span><span>this</span><span>"</span>)-<span>1</span>) && !memcmp(varname->u.constant.value.str.val, <span>"</span><span>this</span><span>"</span>, <span>sizeof</span>(<span>"</span><span>this</span><span>"</span>))) &&<span>
            (CG(active_op_array)</span>->last == <span>0</span> || CG(active_op_array)->opcodes[CG(active_op_array)->last-<span>1</span>].opcode !=<span> ZEND_BEGIN_SILENCE)) {
            
            </span><span>//</span><span> 节点的类型为IS_CV,表明是compiled variables</span>
            result->op_type =<span> IS_CV;
            
            </span><span>//</span><span> 用u.var来记录compiled variables在CG(active_op_array)->vars中的索引</span>
            result->u.<span>var</span> = lookup_cv(CG(active_op_array), varname->u.constant.value.str.val, varname-><span>u.constant.value.str.len);
            result</span>->u.EA.type = <span>0</span><span>;
            varname</span>->u.constant.value.str.val = CG(active_op_array)->vars[result->u.<span>var</span><span>].name;
            </span><span>return</span><span>;
        }
    }
    ...
}</span>
로그인 후 복사

这里不做详细的分析了。当fetch_simple_variable获取索引之后,znode中就不必再保存变量的名称,取而代之的是变量在vars数组中的索引,即znode->u.var,其类型为int。fetch_simple_variable完成,会进入zend_do_receive_arg。

3.2 zend_do_receive_arg

zend_do_receive_arg目的是生成一条zend op指令,可以称作RECV

一般而言,除非函数不存在参数,否则RECV是函数的第一条指令(这里表述不准,有extend info时也不是第一条)。该指令的opcode可能为ZEND_RECV或者ZEND_RECV_INIT,取决于是否有默认值。如果参数没有默认值,指令等于ZEND_RECV,有默认值,则为ZEND_RECV_INIT。zend_do_receive_arg的第二个参数,就是上面提到的compiled variables节点。

分析下zend_do_receive_arg的源码,也是分几段来看:

zend_op *<span>opline;
zend_arg_info </span>*<span>cur_arg_info;

</span><span>//</span><span> class_type主要用于限制函数参数的类型</span>
<span>if</span> (class_type->op_type == IS_CONST && Z_TYPE(class_type->u.constant) == IS_STRING && Z_STRLEN(class_type->u.constant) == <span>0</span><span>) {
    </span><span>/*</span><span> Usage of namespace as class name not in namespace </span><span>*/</span><span>
    zval_dtor(</span>&class_type-><span>u.constant);
    zend_error(E_COMPILE_ERROR, </span><span>"</span><span>Cannot use 'namespace' as a class name</span><span>"</span><span>);
    </span><span>return</span><span>;
}

</span><span>//</span><span> 对静态方法来说,参数不能为this</span>
<span>if</span> (<span>var</span>->op_type == IS_CV && <span>var</span>->u.<span>var</span> == CG(active_op_array)->this_var && (CG(active_op_array)->fn_flags & ZEND_ACC_STATIC) == <span>0</span><span>) {
    zend_error(E_COMPILE_ERROR, </span><span>"</span><span>Cannot re-assign $this</span><span>"</span><span>);
} </span><span>else</span> <span>if</span> (<span>var</span>->op_type == IS_VAR && CG(active_op_array)->scope && ((CG(active_op_array)->fn_flags & ZEND_ACC_STATIC) == <span>0</span>) && (Z_TYPE(varname->u.constant) == IS_STRING) && (Z_STRLEN(varname->u.constant) == <span>sizeof</span>(<span>"</span><span>this</span><span>"</span>)-<span>1</span>) && (memcmp(Z_STRVAL(varname->u.constant), <span>"</span><span>this</span><span>"</span>, <span>sizeof</span>(<span>"</span><span>this</span><span>"</span>)) == <span>0</span><span>)) {
    zend_error(E_COMPILE_ERROR, </span><span>"</span><span>Cannot re-assign $this</span><span>"</span><span>);
}

</span><span>//</span><span> CG(active_op_array)此时已经是函数体的op_array了,这里拿一条指令</span>
opline =<span> get_next_op(CG(active_op_array) TSRMLS_CC);
CG(active_op_array)</span>->num_args++<span>;
opline</span>->opcode =<span> op;
opline</span>->result = *<span>var</span><span>;

</span><span>//</span><span> op1节点表明是第几个参数</span>
opline->op1 = *<span>offset;

</span><span>//</span><span> op2节点可能为初始值,也可能为UNUSED</span>
<span>if</span> (op ==<span> ZEND_RECV_INIT) {
    opline</span>->op2 = *<span>initialization;
} </span><span>else</span><span> {
    CG(active_op_array)</span>->required_num_args = CG(active_op_array)-><span>num_args;
    SET_UNUSED(opline</span>-><span>op2);
}</span>
로그인 후 복사

上面这段代码,首先通过get_next_op(CG(active_op_array) TSRMLS_CC)一句获取了opline,opline是未被使用的一条zend_op指令。紧接着,会对opline的各个字段进行设置。opline->op1表明这是第几个参数,opline->op2可能为初始值,也可能被设置为UNUSED。

如果一个参数有默认值,那么在调用函数时,其实是可以不用传递该参数的。所以,required_num_args不会将这类非必须的参数算进去的。可以看到,在op == ZEND_RECV_INIT这段逻辑分支中,并没有处理required_num_args。

继续来看:

<span>//</span><span> 这里采用erealloc进行分配,因为期望最终会形成一个参数信息的数组</span>
CG(active_op_array)->arg_info = erealloc(CG(active_op_array)->arg_info, <span>sizeof</span>(zend_arg_info)*(CG(active_op_array)-><span>num_args));

</span><span>//</span><span> 设置当前的zend_arg_info</span>
cur_arg_info = &CG(active_op_array)->arg_info[CG(active_op_array)->num_args-<span>1</span><span>];
cur_arg_info</span>->name = estrndup(varname->u.constant.value.str.val, varname-><span>u.constant.value.str.len);
cur_arg_info</span>->name_len = varname-><span>u.constant.value.str.len;
cur_arg_info</span>->array_type_hint = <span>0</span><span>;
cur_arg_info</span>->allow_null = <span>1</span><span>;
cur_arg_info</span>->pass_by_reference =<span> pass_by_reference;
cur_arg_info</span>->class_name =<span> NULL;
cur_arg_info</span>->class_name_len = <span>0</span><span>;

</span><span>//</span><span> 如果需要对参数做类型限定</span>
<span>if</span> (class_type->op_type !=<span> IS_UNUSED) {
    cur_arg_info</span>->allow_null = <span>0</span><span>;
    
    </span><span>//</span><span> 限定为类</span>
    <span>if</span> (class_type->u.constant.type ==<span> IS_STRING) {
        </span><span>if</span> (ZEND_FETCH_CLASS_DEFAULT == zend_get_class_fetch_type(Z_STRVAL(class_type->u.constant), Z_STRLEN(class_type-><span>u.constant))) {
            zend_resolve_class_name(class_type, </span>&opline->extended_value, <span>1</span><span> TSRMLS_CC);
        }
        cur_arg_info</span>->class_name = class_type-><span>u.constant.value.str.val;
        cur_arg_info</span>->class_name_len = class_type-><span>u.constant.value.str.len;
        
        </span><span>//</span><span> 如果限定为类,则参数的默认值只能为NULL</span>
        <span>if</span> (op ==<span> ZEND_RECV_INIT) {
            </span><span>if</span> (Z_TYPE(initialization->u.constant) == IS_NULL || (Z_TYPE(initialization->u.constant) == IS_CONSTANT && !strcasecmp(Z_STRVAL(initialization->u.constant), <span>"</span><span>NULL</span><span>"</span><span>))) {
                cur_arg_info</span>->allow_null = <span>1</span><span>;
            } </span><span>else</span><span> {
                zend_error(E_COMPILE_ERROR, </span><span>"</span><span>Default value for parameters with a class type hint can only be NULL</span><span>"</span><span>);
            }
        }
    }
    </span><span>//</span><span> 限定为数组</span>
    <span>else</span><span> {
        </span><span>//</span><span> 将array_type_hint设置为1</span>
        cur_arg_info->array_type_hint = <span>1</span><span>;
        cur_arg_info</span>->class_name =<span> NULL;
        cur_arg_info</span>->class_name_len = <span>0</span><span>;
        
        </span><span>//</span><span> 如果限定为数组,则参数的默认值只能为数组或NULL</span>
        <span>if</span> (op ==<span> ZEND_RECV_INIT) {
            </span><span>if</span> (Z_TYPE(initialization->u.constant) == IS_NULL || (Z_TYPE(initialization->u.constant) == IS_CONSTANT && !strcasecmp(Z_STRVAL(initialization->u.constant), <span>"</span><span>NULL</span><span>"</span><span>))) {
                cur_arg_info</span>->allow_null = <span>1</span><span>;
            } </span><span>else</span> <span>if</span> (Z_TYPE(initialization->u.constant) != IS_ARRAY && Z_TYPE(initialization->u.constant) !=<span> IS_CONSTANT_ARRAY) {
                zend_error(E_COMPILE_ERROR, </span><span>"</span><span>Default value for parameters with array type hint can only be an array or NULL</span><span>"</span><span>);
            }
        }
    }
}
opline</span>->result.u.EA.type |= EXT_TYPE_UNUSED;
로그인 후 복사

这部分代码写的很清晰。注意,对于限定为数组的情况,class_type的op_type会被设置为IS_CONST,而u.constant.type会被设置为IS_NULL:

optional_class_type:
		/* empty */			{ $$.op_type = IS_UNUSED; }
	|	fully_qualified_class_name	{ $$ = $1; }
	|	T_ARRAY				{ $$.op_type = IS_CONST; Z_TYPE($$.u.constant)=IS_NULL;}
로그인 후 복사

因此,zend_do_receive_arg中区分限定为类还是数组,是利用class_type->u.constant.type == IS_STRING来判断的。如果类型限定为数组,则cur_arg_info->array_type_hint会被设置为1。

还有另一个地方需要了解,zend_resolve_class_name函数会修正类名。举例来说:

<?<span>php
namespace A;
</span><span>class</span><span> B { }
</span><span>function</span> foo(B <span>$arg1</span>, <span>$arg2</span> = 100<span>)
{
    </span><span>print</span>(<span>$arg1</span><span>);
}</span>
로그인 후 복사

我们期望参数arg1的类型为B,class_type中也保存了B。但是因为位于命名空间A下,所以,zend_resolve_class_name会将class_type中保存的类名B,修正为A\B。

OK,到这里,zend_do_receive_arg已经全部分析完。zend vm在分析函数参数时,每遇见一个参数,便会调用一次zend_do_receive_arg,生成一条RECV指令。因此,函数有几个参数,就会编译出几条RECV指令。

4、编译函数体

当编译完参数列表,zend vm便会进入函数内部了。函数体的编译其实和正常语句的编译一样。zend vm只需要将函数体内部的php语句,按照正常的statment,进行词法分析、语法分析来处理,最终形成一条条zend_op指令。

来看下语法文件:

unticked_function_declaration_statement:
	function is_reference T_STRING { zend_do_begin_function_declaration(&$1, &$3, 0, $2.op_type, NULL TSRMLS_CC); }
	'(' parameter_list ')' '{' inner_statement_list '}' { zend_do_end_function_declaration(&$1 TSRMLS_CC); }<br />;
로그인 후 복사

函数体内部的语句,表示为inner_statement_list。

inner_statement_list:
		inner_statement_list  { zend_do_extended_info(TSRMLS_C); } inner_statement { HANDLE_INTERACTIVE(); }
	|	/* empty */
;
로그인 후 복사

而inner_statment正是由语句、函数声明、类声明组成的。

inner_statement:
		statement
	|	function_declaration_statement
	|	class_declaration_statement
	|	T_HALT_COMPILER '(' ')' ';'   { zend_error(E_COMPILE_ERROR, "__HALT_COMPILER() can only be used from the outermost scope"); }
;
로그인 후 복사

inner_statement并非专门用于函数,其他譬如foreach,while循环等有block语句块中,都会被识别为inner_statement。从这里其实还能看到一些有意思的语法,比如说我们可以在函数里声明一个类。inner_statement就不展开叙述了,否则相当于将整个php的语法捋一遍,情况太多了。

5、结束编译

我们最后来看下结束编译的过程。结束函数编译是通过zend_do_end_function_declaration来完成的。

zend_do_end_function_declaration接收的参数function_token,其实就是前面提到过的function字面对应的znode。根据我们在“开始编译”一节所述,function_token中保留了函数体之外的op_array。

<span>char</span> lcname[<span>16</span><span>];
</span><span>int</span><span> name_len;

zend_do_extended_info(TSRMLS_C);

</span><span>//</span><span> 返回NULL</span>
zend_do_return(NULL, <span>0</span><span> TSRMLS_CC);

</span><span>//</span><span> 通过op指令设置对应的handler函数</span>
<span>pass_two(CG(active_op_array) TSRMLS_CC);

</span><span>//</span><span> 释放当前函数的CG(labels),并从CG(labels_stack)中还原之前的CG(labels)</span>
<span>zend_release_labels(TSRMLS_C);

</span><span>if</span><span> (CG(active_class_entry)) {
    </span><span>//</span><span> 检查魔术方法的参数是否合法</span>
    zend_check_magic_method_implementation(CG(active_class_entry), (zend_function*<span>)CG(active_op_array), E_COMPILE_ERROR TSRMLS_CC);
} </span><span>else</span><span> {
    </span><span>/*</span><span> we don't care if the function name is longer, in fact lowercasing only 
     * the beginning of the name speeds up the check process </span><span>*/</span><span>
    name_len </span>= strlen(CG(active_op_array)-><span>function_name);
    zend_str_tolower_copy(lcname, CG(active_op_array)</span>->function_name, MIN(name_len, <span>sizeof</span>(lcname)-<span>1</span><span>));
    lcname[</span><span>sizeof</span>(lcname)-<span>1</span>] = <span>'</span><span>\0</span><span>'</span>; <span>/*</span><span> zend_str_tolower_copy won't necessarily set the zero byte </span><span>*/</span>
    
    <span>//</span><span> 检查__autoload函数的参数是否合法</span>
    <span>if</span> (name_len == <span>sizeof</span>(ZEND_AUTOLOAD_FUNC_NAME) - <span>1</span> && !memcmp(lcname, ZEND_AUTOLOAD_FUNC_NAME, <span>sizeof</span>(ZEND_AUTOLOAD_FUNC_NAME)) && CG(active_op_array)->num_args != <span>1</span><span>) {
        zend_error(E_COMPILE_ERROR, </span><span>"</span><span>%s() must take exactly 1 argument</span><span>"</span><span>, ZEND_AUTOLOAD_FUNC_NAME);
    }        
}

CG(active_op_array)</span>->line_end =<span> zend_get_compiled_lineno(TSRMLS_C);

</span><span>//</span><span> 很关键!将CG(active_op_array)还原成函数外层的op_array</span>
CG(active_op_array) = function_token-><span>u.op_array;

</span><span>/*</span><span> Pop the switch and foreach seperators </span><span>*/</span><span>
zend_stack_del_top(</span>&<span>CG(switch_cond_stack));
zend_stack_del_top(</span>&CG(foreach_copy_stack));
로그인 후 복사

有3处值得注意:

1,zend_do_end_function_declaration中会对CG(active_op_array)进行还原。用的正是function_token->u.op_array。一旦zend_do_end_function_declaration完成,函数的整个编译过程就已经结束了。zend vm会继续看接下来函数之外的代码,所以需要将CG(active_op_array)切换成原先的。

2,zend_do_return负责在函数最后添加上一条RETURN指令,因为我们传进去的是NULL,所以这条RETURN指令的操作数被强制设置为UNUSED。注意,不管函数本身是否有return语句,最后这条RETURN指令是必然存在的。假如函数有return语句,return语句也会产生一条RETURN指令,所以会导致可能出现多条RETURN指令。举例来说:

<span>function</span> foo(<span>)<br /><span>{</span>
    </span><span>return</span> <span>true</span><span>;
}</span>
로그인 후 복사

编译出来的OP指令最后两条如下:

 RETURN        true
 RETURN        null
로그인 후 복사

我们可以很明显在最后看到两条RETURN。一条是通过return true编译出来的。另一条,就是在zend_do_end_function_declaration阶段,强制插入的RETURN。

3,我们刚才讲解的所有步骤中,都只是设置了每条指令的opcode,而并没有设置这条指令具体的handle函数。pass_two会负责遍历每条zend_op指令,根据opcode,以及操作数op1和op2,去查找并且设置对应的handle函数。这项工作,是通过ZEND_VM_SET_OPCODE_HANDLER(opline)宏来完成的。

<span>#define</span> ZEND_VM_SET_OPCODE_HANDLER(opline) zend_vm_set_opcode_handler(opline)
로그인 후 복사

zend_vm_set_opcode_handler的实现很简单:

<span>void</span> zend_init_opcodes_handlers(<span>void</span><span>)
{
    </span><span>//</span><span> 超大的数组,里面存放了所有的handler</span>
    <span>static</span> <span>const</span> opcode_handler_t labels[] =<span> {
        ZEND_NOP_SPEC_HANDLER,
        ZEND_NOP_SPEC_HANDLER,
        ZEND_NOP_SPEC_HANDLER,
        ZEND_NOP_SPEC_HANDLER,
        ZEND_NOP_SPEC_HANDLER,
        ZEND_NOP_SPEC_HANDLER,
        ...
    };
    zend_opcode_handlers </span>= (opcode_handler_t*<span>)labels;
}

</span><span>static</span> opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op*<span> op)
{
        </span><span>static</span> <span>const</span> <span>int</span> zend_vm_decode[] =<span> {
            _UNUSED_CODE, </span><span>/*</span><span> 0              </span><span>*/</span><span>
            _CONST_CODE,  </span><span>/*</span><span> 1 = IS_CONST   </span><span>*/</span><span>
            _TMP_CODE,    </span><span>/*</span><span> 2 = IS_TMP_VAR </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 3              </span><span>*/</span><span>
            _VAR_CODE,    </span><span>/*</span><span> 4 = IS_VAR     </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 5              </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 6              </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 7              </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 8 = IS_UNUSED  </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 9              </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 10             </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 11             </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 12             </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 13             </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 14             </span><span>*/</span><span>
            _UNUSED_CODE, </span><span>/*</span><span> 15             </span><span>*/</span><span>
            _CV_CODE      </span><span>/*</span><span> 16 = IS_CV     </span><span>*/</span><span>
        };
        
        </span><span>//</span><span> 去handler数组里找到对应的处理函数</span>
        <span>return</span> zend_opcode_handlers[opcode * <span>25</span> + zend_vm_decode[op->op1.op_type] * <span>5</span> + zend_vm_decode[op-><span>op2.op_type]];
}

ZEND_API </span><span>void</span> zend_vm_set_opcode_handler(zend_op*<span> op)
{
    </span><span>//</span><span> 给zend op设置对应的handler函数</span>
    op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op-><span>opcode], op);
}</span>
로그인 후 복사

所有的opcode都定义在zend_vm_opcodes.h里,从php5.3-php5.6,大概从150增长到170个opcode。上面可以看到通过opcode查找handler的准确算法:

zend_opcode_handlers[opcode * <span>25</span> + zend_vm_decode[op->op1.op_type] * <span>5</span> + zend_vm_decode[op->op2.op_type]
로그인 후 복사

不过zend_opcode_handlers数组太大了...找起来很麻烦。

下面回到文章开始的那段php代码,我们将函数foo进行编译,最终得到的指令如下:

可以看出,因为foo指接受一个参数,所以这里只有一条RECV指令。

print语句的参数为!0,!0是一个compiled variables,其实就是参数中的arg1。0代表着索引,回忆一下,函数的op_array有一个数组专门用于保存compiled variables,0表明arg1位于该数组的开端。

print语句有返回值,所以会存在一个临时变量保存其返回值,即~0。由于我们在函数中并未使用~0,所以随即便会有一条FREE指令对其进行释放。

在函数的最后,是一条RETURN指令。

6、绑定

函数编译完成之后,还需要进行的一步是绑定。zend vm通过zend_do_early_binding来实现绑定。这个名字容易让人产生疑惑,其实只有在涉及到类和方法的时候,才会有早期绑定,与之相对的是延迟绑定,或者叫后期绑定。纯粹函数谈不上这种概念,不过zend_do_early_binding是多功能的,并非仅仅为绑定方法而实现。

来看下zend_do_early_binding:

<span>//</span><span> 拿到的是最近一条zend op,对于函数来说,就是ZEND_DECLARE_FUNCTION</span>
zend_op *opline = &CG(active_op_array)->opcodes[CG(active_op_array)->last-<span>1</span><span>];
HashTable </span>*<span>table;

</span><span>while</span> (opline->opcode == ZEND_TICKS && opline > CG(active_op_array)-><span>opcodes) {
    opline</span>--<span>;
}

</span><span>switch</span> (opline-><span>opcode) {
    </span><span>case</span><span> ZEND_DECLARE_FUNCTION:
        </span><span>//</span><span> 真正绑定函数</span>
        <span>if</span> (do_bind_function(opline, CG(function_table), <span>1</span>) ==<span> FAILURE) {
            </span><span>return</span><span>;
        }
        table </span>=<span> CG(function_table);
        </span><span>break</span><span>;
    </span><span>case</span><span> ZEND_DECLARE_CLASS:
        ...
    </span><span>case</span><span> ZEND_DECLARE_INHERITED_CLASS:
        ...
}

</span><span>//</span><span> op1中保存的是函数的key,这里其从将CG(function_table)中删除</span>
zend_hash_del(table, opline->op1.u.constant.value.str.val, opline-><span>op1.u.constant.value.str.len);
zval_dtor(</span>&opline-><span>op1.u.constant);
zval_dtor(</span>&opline-><span>op2.u.constant);

</span><span>//</span><span> opline置为NOP</span>
MAKE_NOP(opline);
로그인 후 복사

这个函数实现也很简单,主要就是调用了do_bind_function。

ZEND_API <span>int</span> do_bind_function(zend_op *opline, HashTable *function_table, zend_bool compile_time) <span>/*</span><span> {{{ </span><span>*/</span><span>
{
    zend_function </span>*<span>function;

    </span><span>//</span><span> 找出函数</span>
    zend_hash_find(function_table, opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, (<span>void</span> *) &<span>function);
    
    </span><span>//</span><span> 以函数名称作为key,重新加入function_table</span>
    <span>if</span> (zend_hash_add(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+<span>1</span>, function, <span>sizeof</span>(zend_function), NULL)==<span>FAILURE) {
        </span><span>int</span> error_level = compile_time ?<span> E_COMPILE_ERROR : E_ERROR;
        zend_function </span>*<span>old_function;

        </span><span>//</span><span> 加入失败,可能发生重复定义了</span>
        <span>if</span> (zend_hash_find(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+<span>1</span>, (<span>void</span> *) &old_function)==<span>SUCCESS
            </span>&& old_function->type ==<span> ZEND_USER_FUNCTION
            </span>&& old_function->op_array.last > <span>0</span><span>) {
            zend_error(error_level, </span><span>"</span><span>Cannot redeclare %s() (previously declared in %s:%d)</span><span>"</span><span>,
                        function</span>->common.function_name, old_function->op_array.filename, old_function->op_array.opcodes[<span>0</span><span>].lineno);
        } </span><span>else</span><span> {
            zend_error(error_level, </span><span>"</span><span>Cannot redeclare %s()</span><span>"</span>, function-><span>common.function_name);
        }
        </span><span>return</span><span> FAILURE;
    } </span><span>else</span><span> {
        (</span>*function->op_array.refcount)++<span>;
        function</span>->op_array.static_variables = NULL; <span>/*</span><span> NULL out the unbound function </span><span>*/</span>
        <span>return</span><span> SUCCESS;
    }
}</span>
로그인 후 복사

在进入do_bind_function之前,其实CG(function_table)中已经有了函数的op_array。不过用的键并非函数名,而是build_runtime_defined_function_key生成的“key”,这点在前面“开始编译”一节中有过介绍。do_bind_function所做的事情,正是利用这个“key”,将函数查找出来,并且以真正的函数名为键,重新插入到CG(function_table)中。

因此当do_bind_function完成时,function_table中有2个键可以查询到该函数。一个是“key”为索引的,另一个是以函数名为索引的。在zend_do_early_binding的最后,会通过zend_hash_del来删除“key”,从而保证function_table中,该函数只能够以函数名为键值查询到。

7、总结

这篇其实主要是为了弄清楚,函数如何被编译成op_array。一些关键的步骤如下图:

 

至于函数的调用,又是另外一个话题了。

 

 

www.bkjia.comtruehttp://www.bkjia.com/PHPjc/1122764.htmlTechArticle深入剖析php执行原理(2):函数的编译,深入剖析php 本文只探讨纯粹的函数,并不包含方法。对于方法,会放到类、对象中一起研究。 想...
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.

핫 AI 도구

Undresser.AI Undress

Undresser.AI Undress

사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover

AI Clothes Remover

사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool

Undress AI Tool

무료로 이미지를 벗다

Clothoff.io

Clothoff.io

AI 옷 제거제

Video Face Swap

Video Face Swap

완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

뜨거운 도구

메모장++7.3.1

메모장++7.3.1

사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전

SublimeText3 중국어 버전

중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기

스튜디오 13.0.1 보내기

강력한 PHP 통합 개발 환경

드림위버 CS6

드림위버 CS6

시각적 웹 개발 도구

SublimeText3 Mac 버전

SublimeText3 Mac 버전

신 수준의 코드 편집 소프트웨어(SublimeText3)

jQuery 참조 방법에 대한 자세한 설명: 빠른 시작 가이드 jQuery 참조 방법에 대한 자세한 설명: 빠른 시작 가이드 Feb 27, 2024 pm 06:45 PM

jQuery 참조 방법에 대한 자세한 설명: 빠른 시작 가이드 jQuery는 웹 사이트 개발에 널리 사용되는 JavaScript 라이브러리로, JavaScript 프로그래밍을 단순화하고 개발자에게 풍부한 기능을 제공합니다. 이 기사에서는 jQuery의 참조 방법을 자세히 소개하고 독자가 빠르게 시작할 수 있도록 구체적인 코드 예제를 제공합니다. jQuery 소개 먼저 HTML 파일에 jQuery 라이브러리를 도입해야 합니다. CDN 링크를 통해 소개하거나 다운로드할 수 있습니다.

jQuery에서 PUT 요청 방법을 사용하는 방법은 무엇입니까? jQuery에서 PUT 요청 방법을 사용하는 방법은 무엇입니까? Feb 28, 2024 pm 03:12 PM

jQuery에서 PUT 요청 방법을 사용하는 방법은 무엇입니까? jQuery에서 PUT 요청을 보내는 방법은 다른 유형의 요청을 보내는 것과 유사하지만 몇 가지 세부 사항과 매개 변수 설정에 주의해야 합니다. PUT 요청은 일반적으로 데이터베이스의 데이터 업데이트 또는 서버의 파일 업데이트와 같은 리소스를 업데이트하는 데 사용됩니다. 다음은 jQuery에서 PUT 요청 메소드를 사용하는 구체적인 코드 예제입니다. 먼저 jQuery 라이브러리 파일을 포함했는지 확인한 다음 $.ajax({u를 통해 PUT 요청을 보낼 수 있습니다.

심층 분석: jQuery의 장점과 단점 심층 분석: jQuery의 장점과 단점 Feb 27, 2024 pm 05:18 PM

jQuery는 프런트엔드 개발에 널리 사용되는 빠르고, 작고, 기능이 풍부한 JavaScript 라이브러리입니다. 2006년 출시 이후 jQuery는 많은 개발자가 선택하는 도구 중 하나가 되었지만 실제 애플리케이션에서는 몇 가지 장점과 단점도 있습니다. 이 기사에서는 jQuery의 장점과 단점을 심층적으로 분석하고 구체적인 코드 예제를 통해 설명합니다. 장점: 1. 간결한 구문 jQuery의 구문 디자인은 간결하고 명확하여 코드의 가독성과 쓰기 효율성을 크게 향상시킬 수 있습니다. 예를 들어,

jQuery를 사용하여 요소의 높이 속성을 제거하는 방법은 무엇입니까? jQuery를 사용하여 요소의 높이 속성을 제거하는 방법은 무엇입니까? Feb 28, 2024 am 08:39 AM

jQuery를 사용하여 요소의 높이 속성을 제거하는 방법은 무엇입니까? 프런트엔드 개발에서는 요소의 높이 속성을 조작해야 하는 경우가 종종 있습니다. 때로는 요소의 높이를 동적으로 변경해야 할 수도 있고 요소의 높이 속성을 제거해야 하는 경우도 있습니다. 이 기사에서는 jQuery를 사용하여 요소의 높이 속성을 제거하는 방법을 소개하고 구체적인 코드 예제를 제공합니다. jQuery를 사용하여 높이 속성을 연산하기 전에 먼저 CSS의 높이 속성을 이해해야 합니다. height 속성은 요소의 높이를 설정하는 데 사용됩니다.

jQuery 팁: 페이지에 있는 모든 태그의 텍스트를 빠르게 수정하세요. jQuery 팁: 페이지에 있는 모든 태그의 텍스트를 빠르게 수정하세요. Feb 28, 2024 pm 09:06 PM

제목: jQuery 팁: 페이지에 있는 모든 태그의 텍스트를 빠르게 수정하세요. 웹 개발에서는 페이지의 요소를 수정하고 조작해야 하는 경우가 많습니다. jQuery를 사용할 때 페이지에 있는 모든 태그의 텍스트 내용을 한 번에 수정해야 하는 경우가 있는데, 이는 시간과 에너지를 절약할 수 있습니다. 다음은 jQuery를 사용하여 페이지의 모든 태그 텍스트를 빠르게 수정하는 방법을 소개하고 구체적인 코드 예제를 제공합니다. 먼저 jQuery 라이브러리 파일을 도입하고 다음 코드가 페이지에 도입되었는지 확인해야 합니다. &lt

jQuery를 사용하여 모든 태그의 텍스트 내용 수정 jQuery를 사용하여 모든 태그의 텍스트 내용 수정 Feb 28, 2024 pm 05:42 PM

제목: jQuery를 사용하여 모든 태그의 텍스트 내용을 수정합니다. jQuery는 DOM 작업을 처리하는 데 널리 사용되는 인기 있는 JavaScript 라이브러리입니다. 웹 개발을 하다 보면 페이지에 있는 링크 태그(태그)의 텍스트 내용을 수정해야 하는 경우가 종종 있습니다. 이 기사에서는 jQuery를 사용하여 이 목표를 달성하는 방법을 설명하고 구체적인 코드 예제를 제공합니다. 먼저 페이지에 jQuery 라이브러리를 도입해야 합니다. HTML 파일에 다음 코드를 추가합니다.

jQuery에서 eq의 역할 및 적용 시나리오 이해 jQuery에서 eq의 역할 및 적용 시나리오 이해 Feb 28, 2024 pm 01:15 PM

jQuery는 웹 페이지에서 DOM 조작 및 이벤트 처리를 처리하는 데 널리 사용되는 인기 있는 JavaScript 라이브러리입니다. jQuery에서 eq() 메서드는 지정된 인덱스 위치에서 요소를 선택하는 데 사용됩니다. 구체적인 사용 및 적용 시나리오는 다음과 같습니다. jQuery에서 eq() 메서드는 지정된 인덱스 위치에 있는 요소를 선택합니다. 인덱스 위치는 0부터 계산되기 시작합니다. 즉, 첫 번째 요소의 인덱스는 0이고 두 번째 요소의 인덱스는 1입니다. eq() 메소드의 구문은 다음과 같습니다: $("s

jQuery 요소에 특정 속성이 있는지 어떻게 알 수 있나요? jQuery 요소에 특정 속성이 있는지 어떻게 알 수 있나요? Feb 29, 2024 am 09:03 AM

jQuery 요소에 특정 속성이 있는지 어떻게 알 수 있나요? jQuery를 사용하여 DOM 요소를 조작할 때 요소에 특정 속성이 있는지 확인해야 하는 상황이 자주 발생합니다. 이 경우 jQuery에서 제공하는 메소드를 사용하여 이 기능을 쉽게 구현할 수 있습니다. 다음은 jQuery 요소에 특정 속성이 있는지 확인하기 위해 일반적으로 사용되는 두 가지 방법을 특정 코드 예제와 함께 소개합니다. 방법 1: attr() 메서드와 typeof 연산자를 // 사용하여 요소에 특정 속성이 있는지 확인

See all articles