Open CommanderXL opened 4 years ago
Render Function 这块的内容我觉得是 Mpx 设计上的一大亮点内容。Mpx 引入 Render Function 主要解决的问题是性能优化方向相关的,因为小程序的架构设计,逻辑层和渲染层是2个独立的。
这里直接引用 Mpx 有关 Render Function 对于性能优化相关开发工作的描述:
作为一个接管了小程序setData的数据响应开发框架,我们高度重视Mpx的渲染性能,通过小程序官方文档中提到的性能优化建议可以得知,setData对于小程序性能来说是重中之重,setData优化的方向主要有两个: 尽可能减少setData调用的频次 尽可能减少单次setData传输的数据 为了实现以上两个优化方向,我们做了以下几项工作: 将组件的静态模板编译为可执行的render函数,通过render函数收集模板数据依赖,只有当render函数中的依赖数据发生变化时才会触发小程序组件的setData,同时通过一个异步队列确保一个tick中最多只会进行一次setData,这个机制和Vue中的render机制非常类似,大大降低了setData的调用频次; 将模板编译render函数的过程中,我们还记录输出了模板中使用的数据路径,在每次需要setData时会根据这些数据路径与上一次的数据进行diff,仅将发生变化的数据通过数据路径的方式进行setData,这样确保了每次setData传输的数据量最低,同时避免了不必要的setData操作,进一步降低了setData的频次。
作为一个接管了小程序setData的数据响应开发框架,我们高度重视Mpx的渲染性能,通过小程序官方文档中提到的性能优化建议可以得知,setData对于小程序性能来说是重中之重,setData优化的方向主要有两个:
将组件的静态模板编译为可执行的render函数,通过render函数收集模板数据依赖,只有当render函数中的依赖数据发生变化时才会触发小程序组件的setData,同时通过一个异步队列确保一个tick中最多只会进行一次setData,这个机制和Vue中的render机制非常类似,大大降低了setData的调用频次;
将模板编译render函数的过程中,我们还记录输出了模板中使用的数据路径,在每次需要setData时会根据这些数据路径与上一次的数据进行diff,仅将发生变化的数据通过数据路径的方式进行setData,这样确保了每次setData传输的数据量最低,同时避免了不必要的setData操作,进一步降低了setData的频次。
接下来我们看下 Mpx 是如何实现 Render Function 的。这里我们从一个简单的 demo 来说起:
<template> <text>Computed reversed message: "{{ reversedMessage }}"</text> <view>the c string {{ demoObj.a.b.c }}</view> <view wx:class="{{ { active: isActive } }}"></view> </template> <script> import { createComponent } from "@mpxjs/core"; createComponent({ data: { isActive: true, message: 'messages', demoObj: { a: { b: { c: 'c' } } } }, computed() { reversedMessage() { return this.message.split('').reverse().join('') } } }) </script>
.mpx 文件经过 loader 编译转换的过程中。对于 template 模块的处理和 vue 类似,首先将 template 转化为 AST,然后再将 AST 转化为 code 的过程中做相关转化的工作,最终得到我们需要的 template 模板代码。
.mpx
在packages/webpack-plugin/lib/template-compiler.js模板处理 loader 当中:
packages/webpack-plugin/lib/template-compiler.js
let renderResult = bindThis(`global.currentInject = { moduleId: ${JSON.stringify(options.moduleId)}, render: function () { var __seen = []; var renderData = {}; ${compiler.genNode(ast)}return renderData; } };\n`, { needCollect: true, ignoreMap: meta.wxsModuleMap })
在 render 方法内部,创建 renderData 局部变量,调用compiler.genNode(ast)方法完成 Render Function 核心代码的生成工作,最终将这个 renderData 返回。例如在上面给出来的 demo 实例当中,通过compiler.genNode(ast)方法最终生成的代码为:
compiler.genNode(ast)
((mpxShow)||(mpxShow)===undefined?'':'display:none;'); if(( isActive )){ } "Computed reversed message: \""+( reversedMessage )+"\""; "the c string "+( demoObj.a.b.c ); (__injectHelper.transformClass("list", ( {active: isActive} )));
TODO: compiler.genNode 方法的具体的流程实现思路
mpx 文件当中的 template 模块被初步处理成上面的代码后,可以看到这是一段可执行的 js 代码。那么这段 js 代码到底是用作何处呢?可以看到compiler.genNode方法是被包裹至bindThis方法当中的。即这段 js 代码还会被bindThis方法做进一步的处理。打开 bind-this.js 文件可以看到内部的实现其实就是一个 babel 的 transform plugin。在处理上面这段 js 代码的 AST 的过程中,通过这个插件对 js 代码做进一步的处理。最终这段 js 代码处理后的结果是:
compiler.genNode
bindThis
TODO: Babel 插件的具体功效
/* mpx inject */ global.currentInject = { moduleId: "2271575d", render: function () { var __seen = []; var renderData = {}; (renderData["mpxShow"] = [this.mpxShow, "mpxShow"], this.mpxShow) || (renderData["mpxShow"] = [this.mpxShow, "mpxShow"], this.mpxShow) === undefined ? '' : 'display:none;'; "Computed reversed message: \"" + (renderData["reversedMessage"] = [this.reversedMessage, "reversedMessage"], this.reversedMessage) + "\""; "the c string " + (renderData["demoObj.a.b.c"] = [this.demoObj.a.b.c, "demoObj"], this.__get(this.__get(this.__get(this.demoObj, "a"), "b"), "c")); this.__get(__injectHelper, "transformClass")("list", { active: (renderData["isActive"] = [this.isActive, "isActive"], this.isActive) }); return renderData; } };
bindThis 方法对于 js 代码的转化规则就是:
这里的 this 为 mpx 构造的一个代理对象,在你业务代码当中调用 createComponent/createPage 方法传入的配置项,例如 data,都会通过这个代理对象转化为响应式的数据。
需要注意的是不管哪种数据形式的改造,最终需要达到的效果就是确保在 Render Function 执行的过程当中,这些被模板使用到的数据能被正常的访问到,在访问的阶段中,这些被访问到的数据即被加入到 mpx 构建的整个响应式的系统当中。
只要在 template 当中使用到的 data 数据(包括衍生的 computed 数据),最终都会被 renderData 所记录,而记录的数据形式是例如:
renderData['xxx'] = [this.xxx, 'xxx'] // 数组的形式,第一项为这个数据实际的值,第二项为这个数据的 firstKey(主要用以数据 diff 的工作)
以上就是 mpx 生成 Render Function 的整个过程。总结下 Render Function 所做的工作:
Render Function 这块的内容我觉得是 Mpx 设计上的一大亮点内容。Mpx 引入 Render Function 主要解决的问题是性能优化方向相关的,因为小程序的架构设计,逻辑层和渲染层是2个独立的。
这里直接引用 Mpx 有关 Render Function 对于性能优化相关开发工作的描述:
接下来我们看下 Mpx 是如何实现 Render Function 的。这里我们从一个简单的 demo 来说起:
.mpx
文件经过 loader 编译转换的过程中。对于 template 模块的处理和 vue 类似,首先将 template 转化为 AST,然后再将 AST 转化为 code 的过程中做相关转化的工作,最终得到我们需要的 template 模板代码。在
packages/webpack-plugin/lib/template-compiler.js
模板处理 loader 当中:在 render 方法内部,创建 renderData 局部变量,调用
compiler.genNode(ast)
方法完成 Render Function 核心代码的生成工作,最终将这个 renderData 返回。例如在上面给出来的 demo 实例当中,通过compiler.genNode(ast)
方法最终生成的代码为:TODO: compiler.genNode 方法的具体的流程实现思路
mpx 文件当中的 template 模块被初步处理成上面的代码后,可以看到这是一段可执行的 js 代码。那么这段 js 代码到底是用作何处呢?可以看到
compiler.genNode
方法是被包裹至bindThis
方法当中的。即这段 js 代码还会被bindThis
方法做进一步的处理。打开 bind-this.js 文件可以看到内部的实现其实就是一个 babel 的 transform plugin。在处理上面这段 js 代码的 AST 的过程中,通过这个插件对 js 代码做进一步的处理。最终这段 js 代码处理后的结果是:TODO: Babel 插件的具体功效
bindThis 方法对于 js 代码的转化规则就是:
这里的 this 为 mpx 构造的一个代理对象,在你业务代码当中调用 createComponent/createPage 方法传入的配置项,例如 data,都会通过这个代理对象转化为响应式的数据。
需要注意的是不管哪种数据形式的改造,最终需要达到的效果就是确保在 Render Function 执行的过程当中,这些被模板使用到的数据能被正常的访问到,在访问的阶段中,这些被访问到的数据即被加入到 mpx 构建的整个响应式的系统当中。
只要在 template 当中使用到的 data 数据(包括衍生的 computed 数据),最终都会被 renderData 所记录,而记录的数据形式是例如:
以上就是 mpx 生成 Render Function 的整个过程。总结下 Render Function 所做的工作: