生命周期管理和元素默认行为

及其内部实现

by Jingwei Liu (@th507), Meituan.com

现状:冗余JS代码多

可以抽象、封装的的代码

异步请求回来直接插入节点

选择器,到处都是选择器!

删除、修改元素的时候需要人工确认调用关系

组件化,足够吗?

组件化是一个非常好的尝试,它制定了一套简单的规则。

把元素对应的 JS, CSS 放到一起,提供基本的初始化功能。

但也有可以改进的地方

元素:启动时机各不同

  • load 之后
  • onhover
  • onscroll
  • ...


现在一般会函数的 init 方法里面处理启动时机

或者在页面上/另一个模块的事件回调里面初始化

怎么统一管理?

提供接口/配置项?

提供一系列 on[] 型的接口/配置项?

  • onscroll
  • onafterload
  • onhover

管理器需要实现一系列接口,每个接口对应一个内部队列

被管理的子模块需要知道管理器的接口列表。

手工初始化比较繁复

比方说

// 注册初始化函数
Y.mt.ComponentHub.attach('login-section', function (nd, params) {
    Y.mt.www.Login.init(params);
});
// AJAX插入后手工初始化,并注册回调函数
Y.mt.ComponentHub.boot(ndDialog, {
    'login-section': handleBooted
});

需要写的代码还是不少

注册回调和初始化应该分离

组件化:更进一步?

减少需要写的代码

减少组件化的对外接口

处理不同的启动时机

怎么改进

看看浏览器怎么处理的

一张图片插入 DOM 的时候到底发生了什么?

图片加载的默认行为

  • 扫描、收集需要下载的资源
  • 按下载优先级排序,等待可用连接
  • 下载
  • 渲染 data
  • 展现

元素也可以有默认行为

元素的默认行为

  • scan: 扫描,收集
  • scan: 下载CSS/JS(可选)
  • render: 展现/渲染到页面中(可选)
  • setup/init: 运行JS模块中的方法,绑定事件(可选)
  • beforeupdate: 准备AJAX更新数据(可选)
  • update: 发送AJAX请求,并处理返回结果(可选)
  • teardown: 元素销毁,事件解绑(可选)


我们可以接管元素整个生命周期。

更明确的职能 = 更短的函数


scan
加载 JS 和 CSS
render
元素展现:lazyRender,模版引擎等
init(setup)
运行对应 JS 的 init 方法:事件绑定
beforeupdate
准备 AJAX 请求的数据
update
处理 AJAX 返回的数据

更明确的职能 = 更短的函数


scan
render
init(setup)
beforeupdate
update

提供默认函数(行为)

不再把所有函数都写到 init 里

需要可覆盖特定默认行为

默认行为/完整生命周期

var defaultScope = {
    render: function(ndElement) {
        var ndContent = ndElement.one("> textarea[rel='lazyrender']");
        if (ndContent) ndElement.setHTML(ndContent.get('value'));
        ndContent = null;
        ndElement.show();
    },
    // initialization: setup and event delegation
    init: function (ndElement, config) {},
    // add post/get data before sending AJAX requests
    beforeupdate: function (url, config) {
        if (!url) return null;
        var _config = config || {};
        if (!Y.Lang.isObject(_config)) _config = { args: config };
        return Y.merge(_config, { act: url});
    },
    // update DOM with AJAX return values
    update: function (ndElement, value, config) {
        if (value && ndElement) ndElement.setHTML(value);
    },
    // when element is about to be destroyed
    teardown: function (ndElement) {
        if (!ndElement) return;
        // remove all event listener
        // on ndElement and its descendents
        ndElement.purge(true);
    }
};

抽象默认行为
允许覆盖

减少代码量,提高开发效率

参数说明


<div class="J-hub"
     data-hubmodule="www-deal-firstscreen" 
     data-huburl="/deal/dynamicomponenent" 
     data-hubconfig="{id: 1223, isMobileOnly: false}">...</div>
            

hubcss
CSS 地址
hubmodule
JS 模块名称
hubnamespace
JS 命名空间
huburl
AJAX 请求地址
hubconfig
JS 需要的额外参数

例子


<div class="J-hub" 
     data-huburl="deal/toprecommend" 
     data-hubconfig="3638724"></div>
            
  1. 没有找到 JS 模块信息
  2. 没有找到命名空间,使用默认元素行为
  3. 执行默认的 render 函数
  4. 执行默认的 init (空函数)
  5. 执行默认的 beforeupdate 得到需要发送AJAX的数据
  6. 发送 AJAX 请求 /deal/toprecommend/3638724
  7. 执行默认的 update,将返回的数据直接填入该节点
不需要一行 JS

例子


<div class="J-hub" 
     data-hubmodule="cate-nav"
     data-hubcss="www/css/cate-nav.css"></div>
            
  1. 找到模块,调用 Y.use("cate-nav")
  2. 加载 CSS
  3. JS 和 CSS 加载完成之后…
  4. 找到命名空间:scope = Y.mt.www.CateNav
  5. 没有找到 scope.render,执行默认的render函数
  6. 执行 scope.init
不需要人工初始化

例子


<div class="J-hub"
     data-hubmodule="www-deal-firstscreen" 
     data-huburl="/deal/dynamicomponenent" 
     data-hubconfig="{id: 1223, isMobileOnly: false}">...</div>
            
  1. 找到模块,调用 Y.use("www-deal-firstscreen"),在回调函数中…
  2. 找到命名空间:scope = Y.mt.www.DealFirstscreen
  3. 没有找到 scope.render,执行默认的render函数
  4. 执行 scope.init
  5. 执行 scope.beforeupdate 得到需要发送AJAX的数据
  6. 检查是否需要更新,如有,发送 AJAX 请求
  7. 执行 scope.update 处理服务器端返回的数据

暂停和继续

每个元素可以自行控制生命周期

暂停和继续

  • 懒加载其实就是在 scan 之前 pause,load 之后 resume
  • lazyDisplay是在 render 之前 pause,load 之后 resume
  • onscroll更新是在 init 的时候 pause,onscroll 之后 resume
  • load之后更新是在 init 的时候 pause,load 之后 resume
  • ...


一个接口就够了

而且任何时候重启都可以,由模块自己控制

例子

<div class="site-sidebar J-hub" 
     data-hubmodule="www-search" 
     data-hubnamespace="search.sidebar" 
     data-hubconfig="<?php echo htmlspecialchars($keyword); ?>" 
     data-huburl="search/sidebar">
</div>
  1. Y.use("www-search"); 找到命名空间 scope = Y.mt.www.search.sidebar
  2. 执行默认的 render 函数
  3. 执行 scope.init,发现 ndContainer.fire("hub:pause");
  4. 暂停元素的剩余行为
  5. 执行 M.loadQueue.push 中的 ndContainer.fire("hub:resume")
  6. 重新开始执行元素的剩余行为
  7. 执行默认的 beforeupdate 得到需要发送AJAX的数据
  8. 发送 AJAX 请求 /search/sidebar/
  9. 执行默认的 update,将返回的数据直接填入该节点

很快就可以…

有了默认行为,用模版引擎就变得很简单了:


  • 在元素中写入模版,数据写到节点 attribute 上
  • 节点默认隐藏,由 render 函数填充节点内容,然后显示
  • 异步请求只返回数据,用 update 函数更新模版


不需要开发者在 JS 中手工调用模版引擎

幕间休息

谈谈内部实现

组件化的缺点

粗放式的加载:每个组件的加载都是独立的。
n 个组件,意味着 n 个 CSS 请求,n 个 JS 请求,可能还有 n 个异步请求

启动方式单一:自定义启动时机比较复杂 自定义启动需要手工启动,或传入配置项+Component 支持

组件化:更进一步?

减少需要写的代码

减少组件化的对外接口

性能优化:无论加载多少组件,性能不会有大的起伏

处理不同的启动时机

各个组件之间通讯比较麻烦

生命周期管理和元素默认行为

组件化 2.0

n 个组件,1 个 CSS 请求,1 个 JS 请求,1 个异步请求

对外没有函数接口,通过事件通信

内部只维护一个队列,但支持各元素按需启动

任何时刻插入 DOM 节点的组件会自动初始化

Before

// 注册初始化函数
Y.mt.ComponentHub.attach('login-section', function (nd, params) {
    Y.mt.www.Login.init(params);
});
// AJAX插入后手工初始化,并注册回调函数
Y.mt.ComponentHub.boot(ndDialog, {
    'login-section': handleBooted
});

After

// 注册初始化函数:不需要
// AJAX插入后手工初始化:不需要
// 注册回调函数
Y.one('.login-section').on("hub:ready", handleBooted);

默认行为和生命周期管理

Introducing Hub

名词解释

Document
页面
Hub
生命周期管理器
Pod
元素和相关事件的封装
Element
元素

回顾:元素的默认行为

scan
加载 JS 和 CSS
render
元素展现:lazyRender,模版引擎等
init(setup)
运行对应 JS 的 init 方法:事件绑定
beforeupdate
准备 AJAX 请求的数据
update
处理 AJAX 返回的数据

默认行为

默认行为 Hub Pod
scan 分发事件,收集、加载所有 JS 和 CSS 上报元素需要的 JS 和 CSS
render 分发 Pod 进行 render 渲染/展现元素
init 分发 Pod 进行 init 运行对应模块或默认的 init
beforeupdate 分发事件,收集异步请求 准备、并上报异步请求数据
update 发送请求,分发结果到对应 Pod 处理元素的更新,上报完成
主要分工 分发事件到每个 Pod
收集返回值
处理对应元素的相关行为
上报相关的数据(如需要)

内部分层

生命周期流水线

生命周期流水线

  • 内部利用事件通讯,利用 Promise 管理异步队列
  • 元素默认行为写到 Pod 事件的 defaultFn 中
  • Pod 接受到 Hub 事件的时候,会检查一下是否暂停
  • 暂停会导致 e.preventDefault(),从而跳过该元素的启动流程
  • 但不影响其他元素的启动
  • 在元素上记录该节点的 state ,下次启动时候从该状态开始
  • 在元素上fire resume,会导致 Pod.fire("resume")
  • 冒泡到 Hub,Hub会在当前lifecycle完成之后重新走一遍流水线

生命周期流水线

一些细节

scan

合并请求

同时下载 CSS 和 JS

提高网络利用效率

不会重复下载

update

合并异步请求

完成后触发 ready 事件

接受回调

事件通讯

上层对下层分发事件

下层完全不知道上层的代码结构
需要通讯的时候在自身上 fire 事件
这些事件会冒泡到上层

参考了 actor 模型

页面和 Hub,Hub 和 Pod,Pod 和元素之间只有发送消息这一种通信方式,消息让各层之间解耦。

外部不能直接调用对象的行为,这样就保证内部数据只有被自己修改。

加上 Promise 可保证生命周期的执行顺序。从而支持任意时刻 pause/resume

pause/resume

直接在元素上 fire 即可,Pod 接管后冒泡到 Hub
Hub 在当前周期结束之后 re-run

下层不需要知道上层的代码结构

参考阅读

Thanks!